diff --git a/pkg/commands/container.go b/pkg/commands/container.go index 71661292..6a3c506e 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -8,6 +8,7 @@ import ( "time" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" "github.com/fatih/color" "github.com/go-errors/errors" @@ -400,3 +401,9 @@ func (c *Container) ViewLogs() (*exec.Cmd, error) { return cmd, nil } + +// PruneContainers prunes containers +func (c *DockerCommand) PruneContainers() error { + _, err := c.Client.ContainersPrune(context.Background(), filters.Args{}) + return err +} diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index a7929401..b0b4d402 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -13,7 +13,6 @@ import ( "github.com/acarl005/stripansi" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" "github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/i18n" @@ -37,6 +36,7 @@ type DockerCommand struct { // DisplayContainers is the array of containers we will display in the containers panel. If Gui.ShowAllContainers is false, this will only be those containers which aren't based on a service. This reduces clutter and duplication in the UI DisplayContainers []*Container Images []*Image + Volumes []*Volume } // NewDockerCommand it runs git commands @@ -148,8 +148,8 @@ func (c *DockerCommand) createClientStatMonitor(container *Container) { return } -// GetContainersAndServices returns a slice of docker containers -func (c *DockerCommand) GetContainersAndServices() error { +// RefreshContainersAndServices returns a slice of docker containers +func (c *DockerCommand) RefreshContainersAndServices() error { c.ServiceMutex.Lock() defer c.ServiceMutex.Unlock() @@ -344,56 +344,6 @@ func (c *DockerCommand) UpdateContainerDetails() error { return nil } -// GetImages returns a slice of docker images -func (c *DockerCommand) GetImages() ([]*Image, error) { - images, err := c.Client.ImageList(context.Background(), types.ImageListOptions{}) - if err != nil { - return nil, err - } - - ownImages := make([]*Image, len(images)) - - for i, image := range images { - // func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]image.HistoryResponseItem, error) - - name := "none" - tags := image.RepoTags - if len(tags) > 0 { - name = tags[0] - } - - nameParts := strings.Split(name, ":") - tag := "" - if len(nameParts) > 1 { - tag = nameParts[1] - } - - ownImages[i] = &Image{ - ID: image.ID, - Name: nameParts[0], - Tag: tag, - Image: image, - Client: c.Client, - OSCommand: c.OSCommand, - Log: c.Log, - } - } - - return ownImages, nil -} - -// PruneImages prunes images -func (c *DockerCommand) PruneImages() error { - _, err := c.Client.ImagesPrune(context.Background(), filters.Args{}) - return err -} - -// PruneContainers prunes containers -func (c *DockerCommand) PruneContainers() error { - _, err := c.Client.ContainersPrune(context.Background(), filters.Args{}) - return err -} - // ViewAllLogs attaches to a subprocess viewing all the logs from docker-compose func (c *DockerCommand) ViewAllLogs() (*exec.Cmd, error) { cmd := c.OSCommand.ExecutableFromString(c.OSCommand.Config.UserConfig.CommandTemplates.ViewAllLogs) diff --git a/pkg/commands/image.go b/pkg/commands/image.go index cfbdba2d..65f64137 100644 --- a/pkg/commands/image.go +++ b/pkg/commands/image.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" "github.com/fatih/color" "github.com/jesseduffield/lazydocker/pkg/utils" @@ -97,3 +98,50 @@ func (i *Image) RenderHistory() (string, error) { return utils.RenderList(layers, utils.WithHeader([]string{"ID", "TAG", "SIZE", "COMMAND"})) } + + + + +// GetImages returns a slice of docker images +func (c *DockerCommand) GetImages() ([]*Image, error) { + images, err := c.Client.ImageList(context.Background(), types.ImageListOptions{}) + if err != nil { + return nil, err + } + + ownImages := make([]*Image, len(images)) + + for i, image := range images { + // func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]image.HistoryResponseItem, error) + + name := "none" + tags := image.RepoTags + if len(tags) > 0 { + name = tags[0] + } + + nameParts := strings.Split(name, ":") + tag := "" + if len(nameParts) > 1 { + tag = nameParts[1] + } + + ownImages[i] = &Image{ + ID: image.ID, + Name: nameParts[0], + Tag: tag, + Image: image, + Client: c.Client, + OSCommand: c.OSCommand, + Log: c.Log, + } + } + + return ownImages, nil +} + +// PruneImages prunes images +func (c *DockerCommand) PruneImages() error { + _, err := c.Client.ImagesPrune(context.Background(), filters.Args{}) + return err +} diff --git a/pkg/commands/volume.go b/pkg/commands/volume.go new file mode 100644 index 00000000..4f720315 --- /dev/null +++ b/pkg/commands/volume.go @@ -0,0 +1,66 @@ +package commands + +import ( + "context" + "sort" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/client" + "github.com/sirupsen/logrus" +) + +// Volume : A docker Volume +type Volume struct { + Name string + Volume *types.Volume + Client *client.Client + OSCommand *OSCommand + Log *logrus.Entry +} + +// GetDisplayStrings returns the dispaly string of Container +func (v *Volume) GetDisplayStrings(isFocused bool) []string { + return []string{v.Volume.Driver, v.Name} +} + +// RefreshVolumes gets the volumes and stores them +func (c *DockerCommand) RefreshVolumes() error { + result, err := c.Client.VolumeList(context.Background(), filters.Args{}) + if err != nil { + return err + } + + volumes := result.Volumes + + ownVolumes := make([]*Volume, len(volumes)) + + sort.Slice(volumes, func(i, j int) bool { + return volumes[i].Name < volumes[j].Name + }) + + for i, volume := range volumes { + ownVolumes[i] = &Volume{ + Name: volume.Name, + Volume: volume, + Client: c.Client, + OSCommand: c.OSCommand, + Log: c.Log, + } + } + + c.Volumes = ownVolumes + + return nil +} + +// PruneVolumes prunes volumes +func (c *DockerCommand) PruneVolumes() error { + _, err := c.Client.VolumesPrune(context.Background(), filters.Args{}) + return err +} + +// Remove removes the volume +func (v *Volume) Remove(force bool) error { + return v.Client.VolumeRemove(context.Background(), v.Name, force) +} diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 3035a032..370e78f8 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -72,7 +72,7 @@ func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error { return nil } - key := container.ID + "-" + gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex] + key := "containers-" + container.ID + "-" + gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex] if gui.State.Panels.Main.ObjectKey == key { return nil } else { @@ -243,7 +243,7 @@ func (gui *Gui) refreshContainersAndServices() error { selectedService = gui.DockerCommand.Services[sl] } - if err := gui.refreshStateContainersAndServices(); err != nil { + if err := gui.DockerCommand.RefreshContainersAndServices(); err != nil { return err } @@ -314,10 +314,6 @@ func (gui *Gui) refreshContainersAndServices() error { return nil } -func (gui *Gui) refreshStateContainersAndServices() error { - return gui.DockerCommand.GetContainersAndServices() -} - func (gui *Gui) handleContainersNextLine(g *gocui.Gui, v *gocui.View) error { if gui.popupPanelFocused() { return nil diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 9fcc7c84..221f6a83 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -30,6 +30,7 @@ type SentinelErrors struct { ErrSubProcess error ErrNoContainers error ErrNoImages error + ErrNoVolumes error } // GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here @@ -47,6 +48,7 @@ func (gui *Gui) GenerateSentinelErrors() { ErrSubProcess: errors.New(gui.Tr.RunningSubprocess), ErrNoContainers: errors.New(gui.Tr.NoContainers), ErrNoImages: errors.New(gui.Tr.NoImages), + ErrNoVolumes: errors.New(gui.Tr.NoVolumes), } } @@ -94,12 +96,18 @@ type imagePanelState struct { ContextIndex int // for specifying if you are looking at logs/stats/config/etc } +type volumePanelState struct { + SelectedLine int + ContextIndex int +} + type panelStates struct { Services *servicePanelState Containers *containerPanelState Menu *menuPanelState Main *mainPanelState Images *imagePanelState + Volumes *volumePanelState Status *statusState } @@ -122,6 +130,7 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand Services: &servicePanelState{SelectedLine: -1, ContextIndex: 0}, Containers: &containerPanelState{SelectedLine: -1, ContextIndex: 0}, Images: &imagePanelState{SelectedLine: -1, ContextIndex: 0}, + Volumes: &volumePanelState{SelectedLine: -1, ContextIndex: 0}, Menu: &menuPanelState{SelectedLine: 0}, Main: &mainPanelState{ ObjectKey: "", @@ -245,6 +254,7 @@ func (gui *Gui) Run() error { gui.goEvery(time.Millisecond*50, gui.renderAppStatus) gui.goEvery(time.Millisecond*30, gui.reRenderMain) gui.goEvery(time.Millisecond*100, gui.refreshContainersAndServices) + gui.goEvery(time.Millisecond*100, gui.refreshVolumes) gui.goEvery(time.Millisecond*1000, gui.DockerCommand.UpdateContainerDetails) }() diff --git a/pkg/gui/images_panel.go b/pkg/gui/images_panel.go index 21b37489..1dabb5c7 100644 --- a/pkg/gui/images_panel.go +++ b/pkg/gui/images_panel.go @@ -49,11 +49,11 @@ func (gui *Gui) handleImagesFocus(g *gocui.Gui, v *gocui.View) error { gui.State.Panels.Images.SelectedLine = newSelectedLine - if prevSelectedLine == newSelectedLine && gui.currentViewName() == v.Name() { - return gui.handleImagePress(gui.g, v) - } else { + if !(prevSelectedLine == newSelectedLine && gui.currentViewName() == v.Name()) { return gui.handleImageSelect(gui.g, v) } + + return nil } func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error { @@ -69,7 +69,7 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error { return gui.renderString(g, "main", gui.Tr.NoImages) } - key := Image.ID + "-" + gui.getImageContexts()[gui.State.Panels.Images.ContextIndex] + key := "images-" + Image.ID + "-" + gui.getImageContexts()[gui.State.Panels.Images.ContextIndex] if gui.State.Panels.Main.ObjectKey == key { return nil } else { @@ -98,11 +98,13 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) renderImageConfig(mainView *gocui.View, image *commands.Image) error { go gui.T.NewTask(func(stop chan struct{}) { + padding := 10 output := "" - output += utils.WithPadding("ID: ", 10) + image.Image.ID + "\n" - output += utils.WithPadding("Tags: ", 10) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\n" - output += utils.WithPadding("Size: ", 10) + utils.FormatDecimalBytes(int(image.Image.Size)) + "\n" - output += utils.WithPadding("Created: ", 10) + fmt.Sprintf("%v", time.Unix(image.Image.Created, 0).Format(time.RFC1123)) + "\n" + output += utils.WithPadding("Name: ", padding) + image.Name + "\n" + output += utils.WithPadding("ID: ", padding) + image.Image.ID + "\n" + output += utils.WithPadding("Tags: ", padding) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\n" + output += utils.WithPadding("Size: ", padding) + utils.FormatDecimalBytes(int(image.Image.Size)) + "\n" + output += utils.WithPadding("Created: ", padding) + fmt.Sprintf("%v", time.Unix(image.Image.Created, 0).Format(time.RFC1123)) + "\n" history, err := image.RenderHistory() if err != nil { @@ -156,6 +158,7 @@ func (gui *Gui) refreshImages() error { return nil } +// TODO: leave this to DockerCommand func (gui *Gui) refreshStateImages() error { Images, err := gui.DockerCommand.GetImages() if err != nil { @@ -189,10 +192,6 @@ func (gui *Gui) handleImagesPrevLine(g *gocui.Gui, v *gocui.View) error { return gui.handleImageSelect(gui.g, v) } -func (gui *Gui) handleImagePress(g *gocui.Gui, v *gocui.View) error { - return nil -} - func (gui *Gui) handleImagesNextContext(g *gocui.Gui, v *gocui.View) error { contexts := gui.getImageContexts() if gui.State.Panels.Images.ContextIndex >= len(contexts)-1 { diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 7166d6a0..204f7cc2 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -312,10 +312,38 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handlePruneImages, Description: gui.Tr.PruneImages, }, + { + ViewName: "volumes", + Key: '[', + Modifier: gocui.ModNone, + Handler: gui.handleVolumesPrevContext, + Description: gui.Tr.PreviousContext, + }, + { + ViewName: "volumes", + Key: ']', + Modifier: gocui.ModNone, + Handler: gui.handleVolumesNextContext, + Description: gui.Tr.NextContext, + }, + { + ViewName: "volumes", + Key: 'd', + Modifier: gocui.ModNone, + Handler: gui.handleVolumesRemoveMenu, + Description: gui.Tr.RemoveVolume, + }, + { + ViewName: "volumes", + Key: 'D', + Modifier: gocui.ModNone, + Handler: gui.handlePruneVolumes, + Description: gui.Tr.PruneVolumes, + }, } // TODO: add more views here - for _, viewName := range []string{"status", "services", "containers", "images", "menu"} { + for _, viewName := range []string{"status", "services", "containers", "images", "volumes", "menu"} { bindings = append(bindings, []*Binding{ {ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView}, {ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView}, @@ -334,6 +362,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { "services": {prevLine: gui.handleServicesPrevLine, nextLine: gui.handleServicesNextLine, focus: gui.handleServicesFocus}, "containers": {prevLine: gui.handleContainersPrevLine, nextLine: gui.handleContainersNextLine, focus: gui.handleContainersFocus}, "images": {prevLine: gui.handleImagesPrevLine, nextLine: gui.handleImagesNextLine, focus: gui.handleImagesFocus}, + "volumes": {prevLine: gui.handleVolumesPrevLine, nextLine: gui.handleVolumesNextLine, focus: gui.handleVolumesFocus}, } for viewName, functions := range listPanelMap { diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index a03216d7..78875270 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -92,13 +92,14 @@ func (gui *Gui) layout(g *gocui.Gui) error { usableSpace := height - 4 - tallPanels := 3 + tallPanels := 4 vHeights := map[string]int{ - "status": tallPanels, + "status": 3, "services": usableSpace/tallPanels + usableSpace%tallPanels, "containers": usableSpace / tallPanels, "images": usableSpace / tallPanels, + "volumes": usableSpace / tallPanels, "options": 1, } @@ -112,6 +113,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { "services": defaultHeight, "containers": defaultHeight, "images": defaultHeight, + "volumes": defaultHeight, "options": defaultHeight, } vHeights[currentCyclebleView] = height - defaultHeight*tallPanels - 1 @@ -186,6 +188,18 @@ func (gui *Gui) layout(g *gocui.Gui) error { gui.focusPoint(0, gui.State.Panels.Images.SelectedLine, len(gui.DockerCommand.Images), imagesView) } + volumesView, err := g.SetViewBeneath("volumes", "images", vHeights["volumes"]) + if err != nil { + if err.Error() != "unknown view" { + return err + } + volumesView.Highlight = true + volumesView.Title = gui.Tr.VolumesTitle + volumesView.FgColor = gocui.ColorWhite + + gui.focusPoint(0, gui.State.Panels.Images.SelectedLine, len(gui.DockerCommand.Images), volumesView) + } + if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil { if err.Error() != "unknown view" { return err @@ -244,6 +258,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { listViews := map[*gocui.View]listViewState{ containersView: {selectedLine: gui.State.Panels.Containers.SelectedLine, lineCount: len(gui.DockerCommand.Containers)}, imagesView: {selectedLine: gui.State.Panels.Images.SelectedLine, lineCount: len(gui.DockerCommand.Images)}, + volumesView: {selectedLine: gui.State.Panels.Volumes.SelectedLine, lineCount: len(gui.DockerCommand.Volumes)}, } // menu view might not exist so we check to be safe @@ -252,8 +267,10 @@ func (gui *Gui) layout(g *gocui.Gui) error { } for view, state := range listViews { // check if the selected line is now out of view and if so refocus it - if err := gui.focusPoint(0, state.selectedLine, state.lineCount, view); err != nil { - return err + if view == gui.g.CurrentView() { + if err := gui.focusPoint(0, state.selectedLine, state.lineCount, view); err != nil { + return err + } } } diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index bf2c48d7..1cb82c17 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -63,7 +63,7 @@ func (gui *Gui) handleServiceSelect(g *gocui.Gui, v *gocui.View) error { return nil } - key := service.ID + "-" + gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex] + key := "services-" + service.ID + "-" + gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex] if gui.State.Panels.Main.ObjectKey == key { return nil } else { diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 6d934712..49d4ef97 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -10,7 +10,7 @@ import ( "github.com/spkg/bom" ) -var cyclableViews = []string{"status", "services", "containers", "images"} +var cyclableViews = []string{"status", "services", "containers", "images", "volumes"} func (gui *Gui) refreshSidePanels(g *gocui.Gui) error { // not refreshing containers and services here given that we do it every few milliseconds anyway @@ -91,6 +91,8 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error { return gui.handleContainerSelect(g, v) case "images": return gui.handleImageSelect(g, v) + case "volumes": + return gui.handleVolumeSelect(g, v) case "confirmation": return nil case "main": @@ -245,6 +247,11 @@ func (gui *Gui) getImagesView() *gocui.View { return v } +func (gui *Gui) getVolumesView() *gocui.View { + v, _ := gui.g.View("volumes") + return v +} + func (gui *Gui) getMainView() *gocui.View { v, _ := gui.g.View("main") return v diff --git a/pkg/gui/volumes_panel.go b/pkg/gui/volumes_panel.go new file mode 100644 index 00000000..88e94092 --- /dev/null +++ b/pkg/gui/volumes_panel.go @@ -0,0 +1,298 @@ +package gui + +import ( + "fmt" + "strings" + + "github.com/fatih/color" + "github.com/go-errors/errors" + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazydocker/pkg/commands" + "github.com/jesseduffield/lazydocker/pkg/utils" +) + +// list panel functions + +func (gui *Gui) getVolumeContexts() []string { + return []string{"config"} +} + +func (gui *Gui) getVolumeContextTitles() []string { + return []string{gui.Tr.ConfigTitle} +} + +func (gui *Gui) getSelectedVolume() (*commands.Volume, error) { + selectedLine := gui.State.Panels.Volumes.SelectedLine + if selectedLine == -1 { + return nil, gui.Errors.ErrNoVolumes + } + + return gui.DockerCommand.Volumes[selectedLine], nil +} + +func (gui *Gui) handleVolumesFocus(g *gocui.Gui, v *gocui.View) error { + if gui.popupPanelFocused() { + return nil + } + + cx, cy := v.Cursor() + _, oy := v.Origin() + + prevSelectedLine := gui.State.Panels.Volumes.SelectedLine + newSelectedLine := cy - oy + + if newSelectedLine > len(gui.DockerCommand.Volumes)-1 || len(utils.Decolorise(gui.DockerCommand.Volumes[newSelectedLine].Name)) < cx { + return gui.handleVolumeSelect(gui.g, v) + } + + gui.State.Panels.Volumes.SelectedLine = newSelectedLine + + if !(prevSelectedLine == newSelectedLine && gui.currentViewName() == v.Name()) { + return gui.handleVolumeSelect(gui.g, v) + } + + return nil +} + +func (gui *Gui) handleVolumeSelect(g *gocui.Gui, v *gocui.View) error { + if _, err := gui.g.SetCurrentView(v.Name()); err != nil { + return err + } + + volume, err := gui.getSelectedVolume() + if err != nil { + if err != gui.Errors.ErrNoVolumes { + return err + } + return gui.renderString(g, "main", gui.Tr.NoVolumes) + } + + key := "volumes-" + volume.Name + "-" + gui.getVolumeContexts()[gui.State.Panels.Volumes.ContextIndex] + if gui.State.Panels.Main.ObjectKey == key { + return nil + } else { + gui.State.Panels.Main.ObjectKey = key + } + + if err := gui.focusPoint(0, gui.State.Panels.Volumes.SelectedLine, len(gui.DockerCommand.Volumes), v); err != nil { + return err + } + + mainView := gui.getMainView() + mainView.Tabs = gui.getVolumeContextTitles() + mainView.TabIndex = gui.State.Panels.Volumes.ContextIndex + + switch gui.getVolumeContexts()[gui.State.Panels.Volumes.ContextIndex] { + case "config": + if err := gui.renderVolumeConfig(mainView, volume); err != nil { + return err + } + default: + return errors.New("Unknown context for Volumes panel") + } + + return nil +} + +func (gui *Gui) renderVolumeConfig(mainView *gocui.View, volume *commands.Volume) error { + go gui.T.NewTask(func(stop chan struct{}) { + mainView.Autoscroll = false + mainView.Wrap = true + + padding := 15 + output := "" + output += utils.WithPadding("Name: ", padding) + volume.Name + "\n" + output += utils.WithPadding("Driver: ", padding) + volume.Volume.Driver + "\n" + output += utils.WithPadding("Scope: ", padding) + volume.Volume.Scope + "\n" + output += utils.WithPadding("Mountpoint: ", padding) + volume.Volume.Mountpoint + "\n" + output += utils.WithPadding("Labels: ", padding) + if len(volume.Volume.Labels) > 0 { + output += "\n" + for k, v := range volume.Volume.Labels { + output += formatMapItem(padding, k, v) + } + } else { + output += "none\n" + } + + output += "\n" + utils.WithPadding("Options: ", padding) + if len(volume.Volume.Options) > 0 { + output += "\n" + for k, v := range volume.Volume.Options { + output += formatMapItem(padding, k, v) + } + } else { + output += "none\n" + } + + output += "\n" + utils.WithPadding("Status: ", padding) + if volume.Volume.Status != nil { + output += "\n" + for k, v := range volume.Volume.Status { + output += formatMapItem(padding, k, v) + } + } else { + output += "n/a" + } + + if volume.Volume.UsageData != nil { + output += utils.WithPadding("RefCount: ", padding) + string(volume.Volume.UsageData.RefCount) + "\n" + output += utils.WithPadding("Size: ", padding) + utils.FormatBinaryBytes(int(volume.Volume.UsageData.Size)) + "\n" + } + + gui.renderString(gui.g, "main", output) + }) + + return nil +} + +func formatMapItem(padding int, k string, v interface{}) string { + return fmt.Sprintf("%s%s %v\n", strings.Repeat(" ", padding), utils.ColoredString(k+":", color.FgYellow), utils.ColoredString(fmt.Sprintf("%v", v), color.FgMagenta)) +} + +func (gui *Gui) refreshVolumes() error { + volumesView := gui.getVolumesView() + if volumesView == nil { + // if the volumesView hasn't been instantiated yet we just return + return nil + } + if err := gui.DockerCommand.RefreshVolumes(); err != nil { + return err + } + + if len(gui.DockerCommand.Volumes) > 0 && gui.State.Panels.Volumes.SelectedLine == -1 { + gui.State.Panels.Volumes.SelectedLine = 0 + } + if len(gui.DockerCommand.Volumes)-1 < gui.State.Panels.Volumes.SelectedLine { + gui.State.Panels.Volumes.SelectedLine = len(gui.DockerCommand.Volumes) - 1 + } + + gui.g.Update(func(g *gocui.Gui) error { + volumesView.Clear() + isFocused := gui.g.CurrentView().Name() == "volumes" + list, err := utils.RenderList(gui.DockerCommand.Volumes, utils.IsFocused(isFocused)) + if err != nil { + return err + } + fmt.Fprint(volumesView, list) + + if volumesView == g.CurrentView() { + return gui.handleVolumeSelect(g, volumesView) + } + return nil + }) + + return nil +} + +func (gui *Gui) handleVolumesNextLine(g *gocui.Gui, v *gocui.View) error { + if gui.popupPanelFocused() { + return nil + } + + panelState := gui.State.Panels.Volumes + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Volumes), false) + + return gui.handleVolumeSelect(gui.g, v) +} + +func (gui *Gui) handleVolumesPrevLine(g *gocui.Gui, v *gocui.View) error { + if gui.popupPanelFocused() { + return nil + } + + panelState := gui.State.Panels.Volumes + gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Volumes), true) + + return gui.handleVolumeSelect(gui.g, v) +} + +func (gui *Gui) handleVolumesNextContext(g *gocui.Gui, v *gocui.View) error { + contexts := gui.getVolumeContexts() + if gui.State.Panels.Volumes.ContextIndex >= len(contexts)-1 { + gui.State.Panels.Volumes.ContextIndex = 0 + } else { + gui.State.Panels.Volumes.ContextIndex++ + } + + gui.handleVolumeSelect(gui.g, v) + + return nil +} + +func (gui *Gui) handleVolumesPrevContext(g *gocui.Gui, v *gocui.View) error { + contexts := gui.getVolumeContexts() + if gui.State.Panels.Volumes.ContextIndex <= 0 { + gui.State.Panels.Volumes.ContextIndex = len(contexts) - 1 + } else { + gui.State.Panels.Volumes.ContextIndex-- + } + + gui.handleVolumeSelect(gui.g, v) + + return nil +} + +type removeVolumeOption struct { + description string + command string + force bool + runCommand bool +} + +// GetDisplayStrings is a function. +func (r *removeVolumeOption) GetDisplayStrings(isFocused bool) []string { + return []string{r.description, color.New(color.FgRed).Sprint(r.command)} +} + +func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error { + volume, err := gui.getSelectedVolume() + if err != nil { + return nil + } + + options := []*removeVolumeOption{ + { + description: gui.Tr.Remove, + command: "docker volume rm " + volume.Name, + force: false, + runCommand: true, + }, + { + description: gui.Tr.ForceRemove, + command: "docker volume rm --force " + volume.Name, + force: true, + runCommand: true, + }, + { + description: gui.Tr.Cancel, + runCommand: false, + }, + } + + handleMenuPress := func(index int) error { + if !options[index].runCommand { + return nil + } + return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { + if cerr := volume.Remove(options[index].force); cerr != nil { + return gui.createErrorPanel(gui.g, cerr.Error()) + } + return nil + }) + } + + return gui.createMenu("", options, len(options), handleMenuPress) +} + +func (gui *Gui) handlePruneVolumes(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.ConfirmPruneVolumes, func(g *gocui.Gui, v *gocui.View) error { + return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { + err := gui.DockerCommand.PruneVolumes() + if err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + return nil + }) + }, nil) +} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 24fbd612..0a45bafe 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -24,6 +24,7 @@ type TranslationSet struct { Cancel string CustomCommandTitle string Remove string + ForceRemove string RemoveWithVolumes string MustForceToRemoveContainer string Confirm string @@ -43,15 +44,20 @@ type TranslationSet struct { ContainersTitle string StandaloneContainersTitle string ImagesTitle string + VolumesTitle string NoContainers string NoContainer string NoImages string + NoVolumes string RemoveImage string + RemoveVolume string RemoveWithoutPrune string PruneImages string PruneContainers string + PruneVolumes string ConfirmPruneContainers string ConfirmPruneImages string + ConfirmPruneVolumes string PruningStatus string StopService string PressEnterToReturn string @@ -87,18 +93,21 @@ func englishSet() TranslationSet { EditConfig: "edit config container", Cancel: "cancel", Remove: "remove", + ForceRemove: "force remove", RemoveWithVolumes: "remove with volumes", RemoveService: "remove containers", Stop: "stop", Restart: "restart", Rebuild: "rebuild", - PreviousContext: "previous context", - NextContext: "next context", + PreviousContext: "previous tab", + NextContext: "next tab", Attach: "attach", ViewLogs: "view logs", RemoveImage: "remove image", + RemoveVolume: "remove volume", RemoveWithoutPrune: "remove without deleting untagged parents", PruneContainers: "prune exited containers", + PruneVolumes: "prune unused volumes", PruneImages: "prune unused images", ViewRestartOptions: "view restart options", @@ -110,6 +119,7 @@ func englishSet() TranslationSet { ContainersTitle: "Containers", StandaloneContainersTitle: "Standalone Containers", ImagesTitle: "Images", + VolumesTitle: "Volumes", CustomCommandTitle: "Custom Command:", ErrorTitle: "Error", LogsTitle: "Logs", @@ -121,12 +131,14 @@ func englishSet() TranslationSet { NoContainers: "No containers", NoContainer: "No container", NoImages: "No images", + NoVolumes: "No volumes", ConfirmQuit: "Are you sure you want to quit?", MustForceToRemoveContainer: "You cannot remove a running container unless you force it. Do you want to force it?", NotEnoughSpace: "Not enough space to render panels", ConfirmPruneImages: "Are you sure you want to prune all unused images?", ConfirmPruneContainers: "Are you sure you want to prune all stopped containers?", + ConfirmPruneVolumes: "Are you sure you want to prune all unused volumes?", StopService: "Are you sure you want to stop this service's containers? (enter/esc)", StopContainer: "Are you sure you want to stop this container?", PressEnterToReturn: "Press enter to return to lazydocker",