Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ module github.com/jesseduffield/lazydocker

go 1.22

toolchain go1.23.6

require (
github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c
Expand All @@ -25,7 +24,6 @@ require (
github.com/mgutz/str v1.2.0
github.com/pmezard/go-difflib v1.0.0
github.com/samber/lo v1.31.0
github.com/sasha-s/go-deadlock v0.3.1
github.com/sirupsen/logrus v1.9.3
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.9.0
Expand Down
4 changes: 2 additions & 2 deletions pkg/commands/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sasha-s/go-deadlock"
"sync"
"github.com/sirupsen/logrus"
"golang.org/x/xerrors"
)
Expand All @@ -37,7 +37,7 @@ type Container struct {
DockerCommand LimitedDockerCommand
Tr *i18n.TranslationSet

StatsMutex deadlock.Mutex
StatsMutex sync.Mutex
}

// Remove removes the container
Expand Down
41 changes: 29 additions & 12 deletions pkg/commands/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sasha-s/go-deadlock"
"sync"
"github.com/sirupsen/logrus"
)

Expand All @@ -44,8 +44,8 @@ type DockerCommand struct {
// LocalProjectName is the compose project name for the directory where lazydocker was launched.
LocalProjectName string
ErrorChan chan error
ContainerMutex deadlock.Mutex
ServiceMutex deadlock.Mutex
ContainerMutex sync.Mutex
ServiceMutex sync.Mutex

Closers []io.Closer
}
Expand Down Expand Up @@ -76,9 +76,9 @@ func (c *DockerCommand) NewCommandObject(obj CommandObject) CommandObject {
// When operating on a specific project, include -p flag so that
// docker compose targets the correct project.
if obj.Service != nil && obj.Service.ProjectName != "" {
defaultObj.DockerCompose = fmt.Sprintf("%s -p %s", defaultObj.DockerCompose, obj.Service.ProjectName)
defaultObj.DockerCompose = fmt.Sprintf("%s -p %s", defaultObj.DockerCompose, c.OSCommand.Quote(obj.Service.ProjectName))
} else if obj.Project != nil && obj.Project.Name != "" {
defaultObj.DockerCompose = fmt.Sprintf("%s -p %s", defaultObj.DockerCompose, obj.Project.Name)
defaultObj.DockerCompose = fmt.Sprintf("%s -p %s", defaultObj.DockerCompose, c.OSCommand.Quote(obj.Project.Name))
}

return defaultObj
Expand Down Expand Up @@ -170,6 +170,9 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat
// because the user passed -p. When false, the project/services panels are
// hidden and all containers are shown in a flat list.
func (c *DockerCommand) IsProjectScoped() bool {
if c.Config.UserConfig.Gui.ProjectAndServicePanelsOutsideCompose {
return true
}
return c.InDockerComposeProject || c.Config.ProjectName != ""
}

Expand Down Expand Up @@ -248,20 +251,34 @@ func (c *DockerCommand) RefreshContainersAndServices(currentContainers []*Contai
// labels to handle cases where the project name differs from the directory
// name (e.g. a `name:` directive in the compose file).
if c.LocalProjectName == "" && c.InDockerComposeProject && composeServices != nil {
// First try to match by working directory (Docker Compose V2)
for _, ctr := range containers {
if ctr.ProjectName == "" || ctr.ServiceName == "" {
continue
}
for _, svc := range composeServices {
if ctr.ServiceName == svc.Name {
if wd, ok := ctr.Container.Labels["com.docker.compose.project.working_dir"]; ok {
if wd == c.Config.ProjectDir && ctr.ProjectName != "" {
c.LocalProjectName = ctr.ProjectName
break
}
}
if c.LocalProjectName != "" {
break
}

// Fall back to service name matching (Docker Compose V1)
if c.LocalProjectName == "" {
for _, ctr := range containers {
if ctr.ProjectName == "" || ctr.ServiceName == "" {
continue
}
for _, svc := range composeServices {
if ctr.ServiceName == svc.Name {
c.LocalProjectName = ctr.ProjectName
break
}
}
if c.LocalProjectName != "" {
break
}
}
}

// Fall back to directory name
if c.LocalProjectName == "" && c.Config.ProjectDir != "" {
c.LocalProjectName = filepath.Base(c.Config.ProjectDir)
Expand Down
10 changes: 9 additions & 1 deletion pkg/commands/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,16 @@ func (c *DockerCommand) RefreshImages() ([]*Image, error) {
return ownImages, nil
}

// PruneImages prunes images
// PruneImages prunes dangling images
func (c *DockerCommand) PruneImages() error {
_, err := c.Client.ImagesPrune(context.Background(), filters.Args{})
return err
}

// PruneUnusedImages prunes all unused images (not just dangling)
func (c *DockerCommand) PruneUnusedImages() error {
pruneFilters := filters.NewArgs()
pruneFilters.Add("dangling", "false")
_, err := c.Client.ImagesPrune(context.Background(), pruneFilters)
return err
}
9 changes: 9 additions & 0 deletions pkg/commands/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ func (s *Service) Up() error {
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.UpService)
}

// UpCmd returns the command to up the service
func (s *Service) UpCmd() *exec.Cmd {
command := utils.ApplyTemplate(
s.OSCommand.Config.UserConfig.CommandTemplates.UpService,
s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
)
return s.OSCommand.ExecutableFromString(command)
}

// Restart restarts the service
func (s *Service) Restart() error {
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.RestartService)
Expand Down
4 changes: 4 additions & 0 deletions pkg/config/app_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ type GuiConfig struct {
// of clutter
ShowAllContainers bool `yaml:"showAllContainers,omitempty"`

// ProjectAndServicePanelsOutsideCompose determines if project and service panels
// should be shown even when lazydocker is started outside of a docker-compose directory.
ProjectAndServicePanelsOutsideCompose bool `yaml:"projectAndServicePanelsOutsideCompose,omitempty"`

// ReturnImmediately determines whether you get the 'press enter to return to
// lazydocker' message after a subprocess has completed. You would set this to
// true if you often want to see the output of subprocesses before returning
Expand Down
18 changes: 18 additions & 0 deletions pkg/gui/containers_panel.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,10 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
LabelColumns: []string{gui.Tr.RemoveWithVolumes, "docker rm --volumes " + ctr.ID[1:10]},
OnPress: func() error { return handleMenuPress(container.RemoveOptions{RemoveVolumes: true}) },
},
{
LabelColumns: []string{gui.Tr.ForceRemove, "docker rm --force " + ctr.ID[1:10]},
OnPress: func() error { return handleMenuPress(container.RemoveOptions{Force: true}) },
},
}

return gui.Menu(CreateMenuOptions{
Expand Down Expand Up @@ -401,6 +405,20 @@ func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
}, nil)
}

func (gui *Gui) handleContainerStart(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}

return gui.WithWaitingStatus(gui.Tr.StartingStatus, func() error {
if err := ctr.Start(); err != nil {
return gui.createErrorPanel(err.Error())
}
return gui.refreshContainersAndServices()
})
}

func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions pkg/gui/gui.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"github.com/jesseduffield/lazydocker/pkg/gui/types"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/sasha-s/go-deadlock"
"sync"
"github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -58,8 +58,8 @@ type Panels struct {
}

type Mutexes struct {
SubprocessMutex deadlock.Mutex
ViewStackMutex deadlock.Mutex
SubprocessMutex sync.Mutex
ViewStackMutex sync.Mutex
}

type mainPanelState struct {
Expand Down
16 changes: 16 additions & 0 deletions pkg/gui/images_panel.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,18 @@ func (gui *Gui) handlePruneImages() error {
}, nil)
}

func (gui *Gui) handlePruneAllImages() error {
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneAllImages, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneUnusedImages()
if err != nil {
return gui.createErrorPanel(err.Error())
}
return gui.reloadImages()
})
}, nil)
}

func (gui *Gui) handleImagesCustomCommand(g *gocui.Gui, v *gocui.View) error {
img, err := gui.Panels.Images.GetSelectedItem()
if err != nil {
Expand All @@ -212,6 +224,10 @@ func (gui *Gui) handleImagesBulkCommand(g *gocui.Gui, v *gocui.View) error {
Name: gui.Tr.PruneImages,
InternalFunction: gui.handlePruneImages,
},
{
Name: gui.Tr.PruneAllImages,
InternalFunction: gui.handlePruneAllImages,
},
}

bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Images...)
Expand Down
7 changes: 7 additions & 0 deletions pkg/gui/keybindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleContainerStop,
Description: gui.Tr.Stop,
},
{
ViewName: "containers",
Key: 'S',
Modifier: gocui.ModNone,
Handler: gui.handleContainerStart,
Description: gui.Tr.Start,
},
{
ViewName: "containers",
Key: 'r',
Expand Down
51 changes: 36 additions & 15 deletions pkg/gui/presentation/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,27 @@ func getContainerDisplayStatus(guiConfig *config.GuiConfig, c *commands.Containe

// GetDisplayStatus returns the exit code if the container has exited, and the health status if the container is running (and has a health check)
func getContainerDisplaySubstatus(guiConfig *config.GuiConfig, c *commands.Container) string {
if !c.DetailsLoaded() {
return ""
}

switch c.Container.State {
case "exited":
exitCode := ""
if c.DetailsLoaded() && c.Details.State != nil {
exitCode = strconv.Itoa(c.Details.State.ExitCode)
} else {
// Parse from summary status string (e.g. "Exited (137) 2 minutes ago")
status := c.Container.Status
if strings.HasPrefix(status, "Exited (") {
if endIdx := strings.Index(status, ")"); endIdx > 8 {
exitCode = status[8:endIdx]
}
}
}

if exitCode == "" {
return ""
}

return utils.ColoredString(
fmt.Sprintf("(%s)", strconv.Itoa(c.Details.State.ExitCode)), getContainerColor(c),
fmt.Sprintf("(%s)", exitCode), getContainerColor(c),
)
case "running":
return getHealthStatus(guiConfig, c)
Expand All @@ -108,8 +121,20 @@ func getContainerDisplaySubstatus(guiConfig *config.GuiConfig, c *commands.Conta
}

func getHealthStatus(guiConfig *config.GuiConfig, c *commands.Container) string {
if !c.DetailsLoaded() {
return ""
var status string
if c.DetailsLoaded() && c.Details.State != nil && c.Details.State.Health != nil {
status = c.Details.State.Health.Status
} else {
// Fallback to parsing from summary status string (e.g. "Up 2 hours (healthy)")
if strings.Contains(c.Container.Status, "(healthy)") {
status = "healthy"
} else if strings.Contains(c.Container.Status, "(unhealthy)") {
status = "unhealthy"
} else if strings.Contains(c.Container.Status, "(health: starting)") {
status = "starting"
} else {
return ""
}
}

healthStatusColorMap := map[string]color.Attribute{
Expand All @@ -118,10 +143,6 @@ func getHealthStatus(guiConfig *config.GuiConfig, c *commands.Container) string
"starting": color.FgYellow,
}

if c.Details.State.Health == nil {
return ""
}

shortHealthStatusMap := map[string]string{
"healthy": "H",
"unhealthy": "U",
Expand All @@ -137,16 +158,16 @@ func getHealthStatus(guiConfig *config.GuiConfig, c *commands.Container) string
var healthStatus string
switch guiConfig.ContainerStatusHealthStyle {
case "short":
healthStatus = shortHealthStatusMap[c.Details.State.Health.Status]
healthStatus = shortHealthStatusMap[status]
case "icon":
healthStatus = string(iconHealthStatusMap[c.Details.State.Health.Status])
healthStatus = string(iconHealthStatusMap[status])
case "long":
fallthrough
default:
healthStatus = c.Details.State.Health.Status
healthStatus = status
}

if healthStatusColor, ok := healthStatusColorMap[c.Details.State.Health.Status]; ok {
if healthStatusColor, ok := healthStatusColorMap[status]; ok {
return utils.ColoredString(fmt.Sprintf("(%s)", healthStatus), healthStatusColor)
}
return ""
Expand Down
8 changes: 1 addition & 7 deletions pkg/gui/services_panel.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,7 @@ func (gui *Gui) handleServiceUp(g *gocui.Gui, v *gocui.View) error {
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
}

return gui.WithWaitingStatus(gui.Tr.UppingServiceStatus, func() error {
if err := service.Up(); err != nil {
return gui.createErrorPanel(err.Error())
}

return nil
})
return gui.runSubprocess(service.UpCmd())
}

func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error {
Expand Down
Loading