diff --git a/pkg/commands/container.go b/pkg/commands/container.go index 4942fda9..fb402af8 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -17,9 +17,7 @@ type Container struct { // GetDisplayStrings returns the dispaly string of Container func (c *Container) GetDisplayStrings(isFocused bool) []string { - displayName := utils.ColoredString(c.Name, c.GetColor()) - - return []string{c.ServiceName, displayName} + return []string{utils.ColoredString(c.Container.State, c.GetColor()), utils.ColoredString(c.Name, color.FgWhite)} } // GetColor Container color diff --git a/pkg/commands/container_stats.go b/pkg/commands/container_stats.go index 129c7f81..48af245a 100644 --- a/pkg/commands/container_stats.go +++ b/pkg/commands/container_stats.go @@ -152,8 +152,6 @@ func (s *ContainerStats) CalculateContainerMemoryUsage() float64 { // RenderStats returns a string containing the rendered stats of the container func (s *ContainerStats) RenderStats(viewWidth int, cpuUsageHistory []float64, memoryUsageHistory []float64) (string, error) { - percentMemory := s.CalculateContainerMemoryUsage() - memoryUsageHistory = append(memoryUsageHistory, percentMemory) memoryGraph := asciigraph.Plot( memoryUsageHistory, asciigraph.Height(10), @@ -163,22 +161,20 @@ func (s *ContainerStats) RenderStats(viewWidth int, cpuUsageHistory []float64, m asciigraph.Caption( fmt.Sprintf( "%.2f%% Memory (%s/%s)", - percentMemory, + memoryUsageHistory[len(memoryUsageHistory)-1], formatBinaryBytes(s.MemoryStats.Usage), formatBinaryBytes(int(s.MemoryStats.Limit)), ), ), ) - percentageCPU := s.CalculateContainerCPUPercentage() - cpuUsageHistory = append(cpuUsageHistory, percentageCPU) cpuGraph := asciigraph.Plot( cpuUsageHistory, asciigraph.Height(10), asciigraph.Width(viewWidth-10), asciigraph.Min(0), asciigraph.Max(100), - asciigraph.Caption(fmt.Sprintf("%.2f%% CPU", percentageCPU)), + asciigraph.Caption(fmt.Sprintf("%.2f%% CPU", cpuUsageHistory[len(cpuUsageHistory)-1])), ) pidsCount := fmt.Sprintf("PIDs: %d", s.PidsStats.Current) diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index 48d72630..e48e6c1f 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -11,6 +11,7 @@ import ( "github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/i18n" "github.com/sirupsen/logrus" + "golang.org/x/xerrors" ) // DockerCommand is our main git interface @@ -60,3 +61,54 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) { return ownContainers, nil } + +type removeContainerConfig struct { + // RemoveVolumes removes any volumes attached to the container + RemoveVolumes bool + + // Force forces the container to be removed, even if it's running + Force bool +} + +type RemoveContainerOption func(c *removeContainerConfig) + +// RemoveVolumes is an option to remove volumes attached to the container +func RemoveVolumes(c *removeContainerConfig) { + c.RemoveVolumes = true +} + +// Force is an option to remove volumes attached to the container +func Force(c *removeContainerConfig) { + c.Force = true +} + +// MustStopContainer tells us that we must stop the container before removing it +const MustStopContainer = iota + +// RemoveContainer removes a container +func (c *DockerCommand) RemoveContainer(containerID string, options ...RemoveContainerOption) error { + config := &removeContainerConfig{} + for _, option := range options { + option(config) + } + flags := "" + if config.RemoveVolumes { + flags += " --volumes " + } + if config.Force { + flags += " --force " + } + + err := c.OSCommand.RunCommand(fmt.Sprintf("docker rm %s %s", flags, containerID)) + if err != nil { + if strings.Contains(err.Error(), "Stop the container before attempting removal or force remove") { + return ComplexError{ + Code: MustStopContainer, + Message: err.Error(), + frame: xerrors.Caller(1), + } + } + return err + } + return nil +} diff --git a/pkg/commands/errors.go b/pkg/commands/errors.go index 4723eb95..7abc8f42 100644 --- a/pkg/commands/errors.go +++ b/pkg/commands/errors.go @@ -1,6 +1,11 @@ package commands -import "github.com/go-errors/errors" +import ( + "fmt" + + "github.com/go-errors/errors" + "golang.org/x/xerrors" +) // WrapError wraps an error for the sake of showing a stack trace at the top level // the go-errors package, for some reason, does not return nil when you try to wrap @@ -12,3 +17,23 @@ func WrapError(err error) error { return errors.Wrap(err, 0) } + +// ComplexError an error which carries a code so that calling code has an easier job to do +// adapted from https://medium.com/yakka/better-go-error-handling-with-xerrors-1987650e0c79 +type ComplexError struct { + Message string + Code int + frame xerrors.Frame +} + +func (ce ComplexError) FormatError(p xerrors.Printer) error { + p.Printf("%d %s", ce.Code, ce.Message) + ce.frame.Format(p) + return nil +} +func (ce ComplexError) Format(f fmt.State, c rune) { + xerrors.FormatError(ce, f, c) +} +func (ce ComplexError) Error() string { + return fmt.Sprint(ce) +} diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 24d2e150..047379e9 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -8,10 +8,12 @@ import ( "os/exec" "time" + "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" + "golang.org/x/xerrors" ) // list panel functions @@ -130,10 +132,12 @@ func (gui *Gui) renderStats(mainView *gocui.View, container *commands.Container, var stats commands.ContainerStats json.Unmarshal(data, &stats) + cpuUsageHistory = append(cpuUsageHistory, stats.CalculateContainerCPUPercentage()) if len(cpuUsageHistory) >= 20 { cpuUsageHistory = cpuUsageHistory[1:] } + memoryUsageHistory = append(memoryUsageHistory, stats.CalculateContainerMemoryUsage()) if len(memoryUsageHistory) >= 20 { memoryUsageHistory = memoryUsageHistory[1:] } @@ -281,3 +285,66 @@ func (gui *Gui) handleContainersNextContext(g *gocui.Gui, v *gocui.View) error { return nil } + +type removeOption struct { + description string + command string + configOptions []commands.RemoveContainerOption + runCommand bool +} + +// GetDisplayStrings is a function. +func (r *removeOption) GetDisplayStrings(isFocused bool) []string { + return []string{r.description, color.New(color.FgRed).Sprint(r.command)} +} + +func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error { + container, err := gui.getSelectedContainer(g) + if err != nil { + return nil + } + + options := []*removeOption{ + { + description: gui.Tr.SLocalize("remove"), + command: "docker rm " + container.ID[1:10], + runCommand: true, + }, + { + description: gui.Tr.SLocalize("removeWithVolumes"), + command: "docker rm --volumes " + container.ID[1:10], + configOptions: []commands.RemoveContainerOption{commands.RemoveVolumes}, + runCommand: true, + }, + { + description: gui.Tr.SLocalize("cancel"), + runCommand: false, + }, + } + + handleMenuPress := func(index int) error { + if !options[index].runCommand { + return nil + } + configOptions := options[index].configOptions + if cerr := gui.DockerCommand.RemoveContainer(container.ID, configOptions...); cerr != nil { + var originalErr commands.ComplexError + if xerrors.As(cerr, &originalErr) { + if originalErr.Code == commands.MustStopContainer { + return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("Confirm"), gui.Tr.SLocalize("mustForceToRemove"), func(g *gocui.Gui, v *gocui.View) error { + if err := gui.DockerCommand.RemoveContainer(container.ID, append(configOptions, commands.Force)...); err != nil { + return err + } + return gui.refreshContainers() + }, nil) + } + } else { + return gui.createErrorPanel(gui.g, err.Error()) + } + } + + return gui.refreshContainers() + } + + return gui.createMenu("", options, len(options), handleMenuPress) +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index e51b358a..aff9bc8f 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -148,6 +148,11 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Key: ']', Modifier: gocui.ModNone, Handler: gui.handleContainersNextContext, + }, { + ViewName: "containers", + Key: 'd', + Modifier: gocui.ModNone, + Handler: gui.handleContainersRemoveMenu, }, } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 5ceca3ca..d0f36b49 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -760,5 +760,23 @@ func addEnglish(i18nObject *i18n.Bundle) error { ID: "resetTo", Other: `reset to`, }, + + // ALL LAZYDOCKER STUFF BELOW: + &i18n.Message{ + ID: "remove", + Other: `remove`, + }, + &i18n.Message{ + ID: "removeWithVolumes", + Other: `remove with volumes`, + }, + &i18n.Message{ + ID: "mustForceToRemove", + Other: "You cannot remove a running container unless you force it. Do you want to force it?", + }, + &i18n.Message{ + ID: "Confirm", + Other: "Confirm", + }, ) }