diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index 5f5649da..bae0c54a 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -6,6 +6,7 @@ import ( "github.com/davecgh/go-spew/spew" "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" @@ -84,9 +85,12 @@ func (c *DockerCommand) GetImages() ([]*Image, error) { name = tags[0] } + nameParts := strings.Split(name, ":") + ownImages[i] = &Image{ ID: image.ID, - Name: name, + Name: nameParts[0], + Tag: nameParts[1], Image: image, Client: c.Client, OSCommand: c.OSCommand, @@ -96,3 +100,9 @@ func (c *DockerCommand) GetImages() ([]*Image, error) { 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/image.go b/pkg/commands/image.go index a06fdc3e..6f428a51 100644 --- a/pkg/commands/image.go +++ b/pkg/commands/image.go @@ -1,6 +1,8 @@ package commands import ( + "context" + "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/fatih/color" @@ -11,6 +13,7 @@ import ( // Image : A docker Image type Image struct { Name string + Tag string ID string Image types.ImageSummary Client *client.Client @@ -20,5 +23,14 @@ type Image struct { // GetDisplayStrings returns the display string of Image func (i *Image) GetDisplayStrings(isFocused bool) []string { - return []string{utils.ColoredString(i.Name, color.FgWhite)} + return []string{utils.ColoredString(i.Name, color.FgWhite), utils.ColoredString(i.Tag, color.FgWhite)} +} + +// Remove removes the image +func (i *Image) Remove(options types.ImageRemoveOptions) error { + if _, err := i.Client.ImageRemove(context.Background(), i.ID, options); err != nil { + return err + } + + return nil } diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index f03bece0..db012140 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -293,7 +293,7 @@ func (gui *Gui) handleContainersNextContext(g *gocui.Gui, v *gocui.View) error { return nil } -type removeOption struct { +type removeContainerOption struct { description string command string configOptions types.ContainerRemoveOptions @@ -301,7 +301,7 @@ type removeOption struct { } // GetDisplayStrings is a function. -func (r *removeOption) GetDisplayStrings(isFocused bool) []string { +func (r *removeContainerOption) GetDisplayStrings(isFocused bool) []string { return []string{r.description, color.New(color.FgRed).Sprint(r.command)} } @@ -311,7 +311,7 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error { return nil } - options := []*removeOption{ + options := []*removeContainerOption{ { description: gui.Tr.SLocalize("remove"), command: "docker rm " + container.ID[1:10], @@ -339,7 +339,7 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error { 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 { + return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("Confirm"), gui.Tr.SLocalize("mustForceToRemoveContainer"), func(g *gocui.Gui, v *gocui.View) error { configOptions.Force = true if err := container.Remove(configOptions); err != nil { return err @@ -348,7 +348,7 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error { }, nil) } } else { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(gui.g, cerr.Error()) } } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index eb03bf64..9effc52c 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -291,7 +291,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { "images": defaultHeight, "options": defaultHeight, } - vHeights[currentCyclebleView] = height - defaultHeight*4 - 1 + vHeights[currentCyclebleView] = height - defaultHeight*2 - 1 } optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1) diff --git a/pkg/gui/images_panel.go b/pkg/gui/images_panel.go index 182ea873..58574887 100644 --- a/pkg/gui/images_panel.go +++ b/pkg/gui/images_panel.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" + "github.com/docker/docker/api/types" + "github.com/fatih/color" "github.com/go-errors/errors" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazydocker/pkg/commands" @@ -206,67 +208,66 @@ func (gui *Gui) handleImagesNextContext(g *gocui.Gui, v *gocui.View) error { return nil } -// type removeOption struct { -// description string -// command string -// configOptions types.ImageRemoveOptions -// runCommand bool -// } +type removeImageOption struct { + description string + command string + configOptions types.ImageRemoveOptions + runCommand bool +} -// // GetDisplayStrings is a function. -// func (r *removeOption) GetDisplayStrings(isFocused bool) []string { -// return []string{r.description, color.New(color.FgRed).Sprint(r.command)} -// } +// GetDisplayStrings is a function. +func (r *removeImageOption) GetDisplayStrings(isFocused bool) []string { + return []string{r.description, color.New(color.FgRed).Sprint(r.command)} +} -// func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error { -// Image, err := gui.getSelectedImage(g) -// if err != nil { -// return nil -// } +func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error { + Image, err := gui.getSelectedImage(g) + if err != nil { + return nil + } -// options := []*removeOption{ -// { -// description: gui.Tr.SLocalize("remove"), -// command: "docker rm " + Image.ID[1:10], -// configOptions: types.ImageRemoveOptions{}, -// runCommand: true, -// }, -// { -// description: gui.Tr.SLocalize("removeWithVolumes"), -// command: "docker rm --volumes " + Image.ID[1:10], -// configOptions: types.ImageRemoveOptions{RemoveVolumes: true}, -// runCommand: true, -// }, -// { -// description: gui.Tr.SLocalize("cancel"), -// runCommand: false, -// }, -// } + options := []*removeImageOption{ + { + description: gui.Tr.SLocalize("remove"), + command: "docker image rm " + Image.ID[1:20], + configOptions: types.ImageRemoveOptions{PruneChildren: true}, + runCommand: true, + }, + { + description: gui.Tr.SLocalize("removeWithoutPrune"), + command: "docker image rm --no-prune " + Image.ID[1:20], + configOptions: types.ImageRemoveOptions{PruneChildren: false}, + 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 := Image.Remove(configOptions); cerr != nil { -// var originalErr commands.ComplexError -// if xerrors.As(cerr, &originalErr) { -// if originalErr.Code == commands.MustStopImage { -// return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("Confirm"), gui.Tr.SLocalize("mustForceToRemove"), func(g *gocui.Gui, v *gocui.View) error { -// configOptions.Force = true -// if err := Image.Remove(configOptions); err != nil { -// return err -// } -// return gui.refreshImages() -// }, nil) -// } -// } else { -// return gui.createErrorPanel(gui.g, err.Error()) -// } -// } + handleMenuPress := func(index int) error { + if !options[index].runCommand { + return nil + } + configOptions := options[index].configOptions + if cerr := Image.Remove(configOptions); cerr != nil { + return gui.createErrorPanel(gui.g, cerr.Error()) + } -// return gui.refreshImages() -// } + return gui.refreshImages() + } -// return gui.createMenu("", options, len(options), handleMenuPress) -// } + return gui.createMenu("", options, len(options), handleMenuPress) +} + +func (gui *Gui) handlePruneImages(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("Confirm"), gui.Tr.SLocalize("confirmPruneImages"), func(g *gocui.Gui, v *gocui.View) error { + return gui.WithWaitingStatus(gui.Tr.SLocalize("PruningStatus"), func() error { + err := gui.DockerCommand.PruneImages() + if err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + return gui.refreshImages() + }) + }, nil) +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 95ecaa3e..977515a0 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -192,6 +192,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleImagesNextContext, Description: gui.Tr.SLocalize("nextContext"), }, + { + ViewName: "images", + Key: 'd', + Modifier: gocui.ModNone, + Handler: gui.handleImagesRemoveMenu, + Description: gui.Tr.SLocalize("removeImage"), + }, + { + ViewName: "images", + Key: 'D', + Modifier: gocui.ModNone, + Handler: gui.handlePruneImages, + Description: gui.Tr.SLocalize("pruneImages"), + }, } // TODO: add more views here diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 863b4c5e..fdf4ae55 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -765,7 +765,7 @@ func addEnglish(i18nObject *i18n.Bundle) error { Other: `remove with volumes`, }, &i18n.Message{ - ID: "mustForceToRemove", + ID: "mustForceToRemoveContainer", Other: "You cannot remove a running container unless you force it. Do you want to force it?", }, &i18n.Message{ @@ -820,5 +820,29 @@ func addEnglish(i18nObject *i18n.Bundle) error { ID: "NoImages", Other: "No images", }, + &i18n.Message{ + ID: "removeImage", + Other: "remove image", + }, + &i18n.Message{ + ID: "removeWithoutPrune", + Other: "remove without deleting untagged parents", + }, + &i18n.Message{ + ID: "mustForceToRemoveImage", + Other: "Regular image delete not allowed. Do you want to force remove this image?", + }, + &i18n.Message{ + ID: "pruneImages", + Other: "prune unused images", + }, + &i18n.Message{ + ID: "confirmPruneImages", + Other: "Are you sure you want to prune all unused images?", + }, + &i18n.Message{ + ID: "PruningStatus", + Other: "pruning", + }, ) }