Skip to content

Commit 4dc8f76

Browse files
Merge pull request #107 from jesseduffield/bulk-commands
Support bulk commands in each of the panels
2 parents 64863ad + bd3ce66 commit 4dc8f76

File tree

12 files changed

+272
-74
lines changed

12 files changed

+272
-74
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ A simple terminal UI for both docker and docker-compose, written in Go with the
66

77
[![CircleCI](https://circleci.com/gh/jesseduffield/lazydocker.svg?style=svg)](https://circleci.com/gh/jesseduffield/lazydocker) [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazydocker)](https://goreportcard.com/report/github.com/jesseduffield/lazydocker) [![GolangCI](https://golangci.com/badges/github.com/jesseduffield/lazydocker.svg)](https://golangci.com) [![GoDoc](https://godoc.org/github.com/jesseduffield/lazydocker?status.svg)](http://godoc.org/github.com/jesseduffield/lazydocker) [![GitHub tag](https://img.shields.io/github/tag/jesseduffield/lazydocker.svg)]()
88

9-
10-
119
![Gif](/docs/resources/demo3.gif)
1210

1311
[Demo](https://youtu.be/NICqQPxwJWw)
@@ -31,7 +29,7 @@ Memorising docker commands is hard. Memorising aliases is slightly less hard. Ke
3129

3230
## Requirements
3331

34-
- Docker >= **1.8** (API >= **1.20**)
32+
- Docker >= **1.8** (API >= **1.25**)
3533
- Docker-Compose >= **1.23.2** (optional)
3634

3735
## Installation
@@ -197,18 +195,23 @@ If you want to see what I (Jesse) am up to in terms of development, follow me on
197195
## FAQ
198196
199197
### How do I edit my config?
198+
200199
By opening lazydocker, clicking on the 'project' panel in the top left, and pressing 'o' (or 'e' if your editor is vim). See [Config Docs](/docs/Config.md)
201200
202201
### How do I get text to wrap in my main panel?
202+
203203
In the future I want to make this the default, but for now there are some CPU issues that arise with wrapping. If you want to enable wrapping, use `gui.wrapMainPanel: true`
204204
205205
### How do you select text?
206+
206207
Because we support mouse events, you will need to hold option while dragging the mouse to indicate you're trying to select text rather than click on something. Alternatively you can disable mouse events via the `gui.ignoreMouseEvents` config value
207208

208209
### Does this work with Windows?
210+
209211
Currently not unless you use WSL. Instructions for setting up docker for WSL can be found here [here](https://nickjanetakis.com/blog/setting-up-docker-for-windows-and-wsl-to-work-flawlessly)
210212

211213
### Why can't I see my container's logs?
214+
212215
By default we only show logs from the last hour, so that we're not putting too much strain on the machine. This may be why you can't see logs when you first start lazydocker. This can be overwritten in the config's `commandTemplates`
213216
214217
## Alternatives

pkg/commands/container.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package commands
22

33
import (
44
"context"
5-
"github.com/docker/docker/api/types/container"
5+
"fmt"
66
"os/exec"
77
"strconv"
88
"strings"
99
"time"
1010

11+
"github.com/docker/docker/api/types/container"
12+
1113
"github.com/docker/docker/api/types"
1214
"github.com/docker/docker/api/types/filters"
1315
"github.com/docker/docker/client"
@@ -317,6 +319,7 @@ func (c *Container) GetColor() color.Attribute {
317319

318320
// Remove removes the container
319321
func (c *Container) Remove(options types.ContainerRemoveOptions) error {
322+
c.Log.Warn(fmt.Sprintf("removing container %s", c.Name))
320323
if err := c.Client.ContainerRemove(context.Background(), c.ID, options); err != nil {
321324
if strings.Contains(err.Error(), "Stop the container before attempting removal or force remove") {
322325
return ComplexError{
@@ -333,16 +336,20 @@ func (c *Container) Remove(options types.ContainerRemoveOptions) error {
333336

334337
// Stop stops the container
335338
func (c *Container) Stop() error {
339+
c.Log.Warn(fmt.Sprintf("stopping container %s", c.Name))
336340
return c.Client.ContainerStop(context.Background(), c.ID, nil)
337341
}
338342

339343
// Restart restarts the container
340344
func (c *Container) Restart() error {
345+
c.Log.Warn(fmt.Sprintf("restarting container %s", c.Name))
341346
return c.Client.ContainerRestart(context.Background(), c.ID, nil)
342347
}
343348

344349
// Attach attaches the container
345350
func (c *Container) Attach() (*exec.Cmd, error) {
351+
c.Log.Warn(fmt.Sprintf("attaching to container %s", c.Name))
352+
346353
// verify that we can in fact attach to this container
347354
if !c.Details.Config.OpenStdin {
348355
return nil, errors.New(c.Tr.UnattachableContainerError)

pkg/commands/docker.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222
)
2323

2424
const (
25-
APIVersion = "1.20"
25+
APIVersion = "1.25"
2626
)
2727

2828
// DockerCommand is our main docker interface

pkg/config/app_config.go

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ type UserConfig struct {
5050
// those are found in the commands package
5151
CustomCommands CustomCommands `yaml:"customCommands,omitempty"`
5252

53+
// BulkCommands are commands that apply to all items in a panel e.g.
54+
// killing all containers, stopping all services, or pruning all images
55+
BulkCommands CustomCommands `yaml:"bulkCommands,omitempty"`
56+
5357
// OS determines what defaults are set for opening files and links
5458
OS OSConfig `yaml:"oS,omitempty"`
5559

@@ -260,10 +264,10 @@ type CustomCommands struct {
260264
// Services contains the custom commands for services
261265
Services []CustomCommand `yaml:"services,omitempty"`
262266

263-
// Services contains the custom commands for services
267+
// Images contains the custom commands for images
264268
Images []CustomCommand `yaml:"images,omitempty"`
265269

266-
// Services contains the custom commands for services
270+
// Volumes contains the custom commands for volumes
267271
Volumes []CustomCommand `yaml:"volumes,omitempty"`
268272
}
269273

@@ -289,6 +293,9 @@ type CustomCommand struct {
289293
// field has no effect on customcommands under the 'communications' part of
290294
// the customCommand config.
291295
ServiceNames []string `yaml:"serviceNames"`
296+
297+
// InternalFunction is the name of a function inside lazydocker that we want to run, as opposed to a command-line command. This is only used internally and can't be configured by the user
298+
InternalFunction func() error `yaml:"internalFunction"`
292299
}
293300

294301
// GetDefaultConfig returns the application default configuration NOTE (to
@@ -345,6 +352,52 @@ func GetDefaultConfig() UserConfig {
345352
Images: []CustomCommand{},
346353
Volumes: []CustomCommand{},
347354
},
355+
BulkCommands: CustomCommands{
356+
Services: []CustomCommand{
357+
{
358+
Name: "up",
359+
Command: "{{ .DockerCompose }} up -d",
360+
},
361+
{
362+
Name: "up (attached)",
363+
Command: "{{ .DockerCompose }} up",
364+
Attach: true,
365+
},
366+
{
367+
Name: "stop",
368+
Command: "{{ .DockerCompose }} stop",
369+
},
370+
{
371+
Name: "pull",
372+
Command: "{{ .DockerCompose }} pull",
373+
Attach: true,
374+
},
375+
{
376+
Name: "build",
377+
Command: "{{ .DockerCompose }} build --parallel --force-rm",
378+
Attach: true,
379+
},
380+
{
381+
Name: "down",
382+
Command: "{{ .DockerCompose }} down",
383+
},
384+
{
385+
Name: "down with volumes",
386+
Command: "{{ .DockerCompose }} down --volumes",
387+
},
388+
{
389+
Name: "down with images",
390+
Command: "{{ .DockerCompose }} down --rmi all",
391+
},
392+
{
393+
Name: "down with volumes and images",
394+
Command: "{{ .DockerCompose }} down --volumes --rmi all",
395+
},
396+
},
397+
Containers: []CustomCommand{},
398+
Images: []CustomCommand{},
399+
Volumes: []CustomCommand{},
400+
},
348401
OS: GetPlatformDefaultConfig(),
349402
Update: UpdateConfig{
350403
DockerRefreshInterval: time.Millisecond * 100,

pkg/gui/containers_panel.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/go-errors/errors"
1212
"github.com/jesseduffield/gocui"
1313
"github.com/jesseduffield/lazydocker/pkg/commands"
14+
"github.com/jesseduffield/lazydocker/pkg/config"
1415
"github.com/jesseduffield/lazydocker/pkg/utils"
1516
)
1617

@@ -476,8 +477,8 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
476477
return gui.Errors.ErrSubProcess
477478
}
478479

479-
func (gui *Gui) handlePruneContainers(g *gocui.Gui, v *gocui.View) error {
480-
return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.ConfirmPruneContainers, func(g *gocui.Gui, v *gocui.View) error {
480+
func (gui *Gui) handlePruneContainers() error {
481+
return gui.createConfirmationPanel(gui.g, gui.getContainersView(), gui.Tr.Confirm, gui.Tr.ConfirmPruneContainers, func(g *gocui.Gui, v *gocui.View) error {
481482
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
482483
err := gui.DockerCommand.PruneContainers()
483484
if err != nil {
@@ -517,3 +518,51 @@ func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error
517518

518519
return gui.createCustomCommandMenu(customCommands, commandObject)
519520
}
521+
522+
func (gui *Gui) handleStopContainers() error {
523+
return gui.createConfirmationPanel(gui.g, gui.getContainersView(), gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error {
524+
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
525+
526+
for _, container := range gui.DockerCommand.Containers {
527+
_ = container.Stop()
528+
}
529+
530+
return nil
531+
})
532+
}, nil)
533+
}
534+
535+
func (gui *Gui) handleRemoveContainers() error {
536+
return gui.createConfirmationPanel(gui.g, gui.getContainersView(), gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error {
537+
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
538+
539+
for _, container := range gui.DockerCommand.Containers {
540+
_ = container.Remove(types.ContainerRemoveOptions{Force: true})
541+
}
542+
543+
return nil
544+
})
545+
}, nil)
546+
}
547+
548+
func (gui *Gui) handleContainersBulkCommand(g *gocui.Gui, v *gocui.View) error {
549+
baseBulkCommands := []config.CustomCommand{
550+
{
551+
Name: gui.Tr.StopAllContainers,
552+
InternalFunction: gui.handleStopContainers,
553+
},
554+
{
555+
Name: gui.Tr.RemoveAllContainers,
556+
InternalFunction: gui.handleRemoveContainers,
557+
},
558+
{
559+
Name: gui.Tr.PruneContainers,
560+
InternalFunction: gui.handlePruneContainers,
561+
},
562+
}
563+
564+
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Containers...)
565+
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
566+
567+
return gui.createBulkCommandMenu(bulkCommands, commandObject)
568+
}

pkg/gui/custom_commands.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func (r *customCommandOption) GetDisplayStrings(isFocused bool) []string {
2121
return []string{r.name, utils.ColoredString(r.description, color.FgCyan)}
2222
}
2323

24-
func (gui *Gui) createCustomCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject) error {
24+
func (gui *Gui) createCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject, title string, waitingStatus string) error {
2525
options := make([]*customCommandOption, len(customCommands)+1)
2626
for i, command := range customCommands {
2727
resolvedCommand := utils.ApplyTemplate(command.Command, commandObject)
@@ -46,20 +46,32 @@ func (gui *Gui) createCustomCommandMenu(customCommands []config.CustomCommand, c
4646
return nil
4747
}
4848

49+
if option.customCommand.InternalFunction != nil {
50+
return option.customCommand.InternalFunction()
51+
}
52+
4953
// if we have a command for attaching, we attach and return the subprocess error
5054
if option.customCommand.Attach {
5155
cmd := gui.OSCommand.ExecutableFromString(option.command)
5256
gui.SubProcess = cmd
5357
return gui.Errors.ErrSubProcess
5458
}
5559

56-
return gui.WithWaitingStatus(gui.Tr.RunningCustomCommandStatus, func() error {
60+
return gui.WithWaitingStatus(waitingStatus, func() error {
5761
if err := gui.OSCommand.RunCommand(option.command); err != nil {
5862
return gui.createErrorPanel(gui.g, err.Error())
5963
}
6064
return nil
6165
})
6266
}
6367

64-
return gui.createMenu("", options, len(options), handleMenuPress)
68+
return gui.createMenu(title, options, len(options), handleMenuPress)
69+
}
70+
71+
func (gui *Gui) createCustomCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject) error {
72+
return gui.createCommandMenu(customCommands, commandObject, gui.Tr.CustomCommandTitle, gui.Tr.RunningCustomCommandStatus)
73+
}
74+
75+
func (gui *Gui) createBulkCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject) error {
76+
return gui.createCommandMenu(customCommands, commandObject, gui.Tr.BulkCommandTitle, gui.Tr.RunningBulkCommandStatus)
6577
}

pkg/gui/gui.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ func (gui *Gui) renderGlobalOptions() error {
225225
"PgUp/PgDn": gui.Tr.Scroll,
226226
"← → ↑ ↓": gui.Tr.Navigate,
227227
"esc/q": gui.Tr.Close,
228+
"b": gui.Tr.ViewBulkCommands,
228229
"x": gui.Tr.Menu,
229230
})
230231
}
@@ -283,6 +284,9 @@ func (gui *Gui) Run() error {
283284

284285
go func() {
285286
for err := range gui.ErrorChan {
287+
if err == nil {
288+
continue
289+
}
286290
if strings.Contains(err.Error(), "No such container") {
287291
// this happens all the time when e.g. restarting containers so we won't worry about it
288292
gui.Log.Warn(err)

pkg/gui/images_panel.go

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/go-errors/errors"
1111
"github.com/jesseduffield/gocui"
1212
"github.com/jesseduffield/lazydocker/pkg/commands"
13+
"github.com/jesseduffield/lazydocker/pkg/config"
1314
"github.com/jesseduffield/lazydocker/pkg/utils"
1415
)
1516

@@ -23,7 +24,7 @@ func (gui *Gui) getImageContextTitles() []string {
2324
return []string{gui.Tr.ConfigTitle}
2425
}
2526

26-
func (gui *Gui) getSelectedImage(g *gocui.Gui) (*commands.Image, error) {
27+
func (gui *Gui) getSelectedImage() (*commands.Image, error) {
2728
selectedLine := gui.State.Panels.Images.SelectedLine
2829
if selectedLine == -1 {
2930
return &commands.Image{}, gui.Errors.ErrNoImages
@@ -41,7 +42,7 @@ func (gui *Gui) handleImagesClick(g *gocui.Gui, v *gocui.View) error {
4142
}
4243

4344
func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
44-
Image, err := gui.getSelectedImage(g)
45+
Image, err := gui.getSelectedImage()
4546
if err != nil {
4647
if err != gui.Errors.ErrNoImages {
4748
return err
@@ -207,7 +208,7 @@ func (r *removeImageOption) GetDisplayStrings(isFocused bool) []string {
207208
}
208209

209210
func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
210-
Image, err := gui.getSelectedImage(g)
211+
Image, err := gui.getSelectedImage()
211212
if err != nil {
212213
return nil
213214
}
@@ -248,8 +249,8 @@ func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
248249
return gui.createMenu("", options, len(options), handleMenuPress)
249250
}
250251

251-
func (gui *Gui) handlePruneImages(g *gocui.Gui, v *gocui.View) error {
252-
return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.ConfirmPruneImages, func(g *gocui.Gui, v *gocui.View) error {
252+
func (gui *Gui) handlePruneImages() error {
253+
return gui.createConfirmationPanel(gui.g, gui.getImagesView(), gui.Tr.Confirm, gui.Tr.ConfirmPruneImages, func(g *gocui.Gui, v *gocui.View) error {
253254
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
254255
err := gui.DockerCommand.PruneImages()
255256
if err != nil {
@@ -261,17 +262,30 @@ func (gui *Gui) handlePruneImages(g *gocui.Gui, v *gocui.View) error {
261262
}
262263

263264
func (gui *Gui) handleImagesCustomCommand(g *gocui.Gui, v *gocui.View) error {
264-
image, err := gui.getSelectedImage(g)
265+
image, err := gui.getSelectedImage()
265266
if err != nil {
266267
return nil
267268
}
268269

269-
commandObject := gui.DockerCommand.NewCommandObject(
270-
commands.CommandObject{
271-
Image: image,
272-
})
270+
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
271+
Image: image,
272+
})
273273

274274
customCommands := gui.Config.UserConfig.CustomCommands.Images
275275

276276
return gui.createCustomCommandMenu(customCommands, commandObject)
277277
}
278+
279+
func (gui *Gui) handleImagesBulkCommand(g *gocui.Gui, v *gocui.View) error {
280+
baseBulkCommands := []config.CustomCommand{
281+
{
282+
Name: gui.Tr.PruneImages,
283+
InternalFunction: gui.handlePruneImages,
284+
},
285+
}
286+
287+
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Images...)
288+
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
289+
290+
return gui.createBulkCommandMenu(bulkCommands, commandObject)
291+
}

0 commit comments

Comments
 (0)