Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path // empty' | xargs -I{} gofumpt -w {}"
"command": "jq -r '.tool_input.file_path // empty' | { read -r f; case \"$f\" in *.go) gofumpt -w \"$f\" ;; esac; } 2>/dev/null || true"
}
]
}
Expand Down
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# CLAUDE.md

## Build & test
- All Go commands need `GOFLAGS=-mod=vendor` (deps are vendored, including the `jesseduffield/gocui` fork and the Docker SDK).
- Unit tests: `GOFLAGS=-mod=vendor go test ./...`
16 changes: 16 additions & 0 deletions pkg/commands/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,25 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat
log.Warn(err.Error())
}

// When the user passes -p outside of a compose directory, treat it as the
// local project so the project/services panels still appear and filtering
// is applied. Inside a compose dir, LocalProjectName is derived from
// container labels later in RefreshContainersAndServices.
if !dockerCommand.InDockerComposeProject && config.ProjectName != "" {
dockerCommand.LocalProjectName = config.ProjectName
}
Comment on lines +157 to +163

return dockerCommand, nil
}

// IsProjectScoped reports whether lazydocker should be scoped to a single
// compose project — either because we're inside a compose directory or
// 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 {
return c.InDockerComposeProject || c.Config.ProjectName != ""
}
Comment on lines +168 to +174

func (c *DockerCommand) setDockerComposeCommand(config *config.AppConfig) {
if config.UserConfig.CommandTemplates.DockerCompose != "docker compose" {
return
Expand Down
28 changes: 28 additions & 0 deletions pkg/commands/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/docker/docker/client"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -61,3 +62,30 @@ func TestNewDockerClientVersionNegotiation(t *testing.T) {
"client version should not be locked to DOCKER_API_VERSION env var")
})
}

// TestIsProjectScoped covers the predicate that drives whether the
// project/services panels appear and whether the containers panel filters by
// project. The "outside compose dir + -p" case is the regression we fixed
// after PR #776 silently disabled it.
func TestIsProjectScoped(t *testing.T) {
cases := []struct {
name string
inDockerComposeProject bool
projectName string
want bool
}{
{"inside compose dir, no -p", true, "", true},
{"inside compose dir, with -p", true, "myproject", true},
{"outside compose dir, no -p", false, "", false},
{"outside compose dir, with -p", false, "myproject", true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := &DockerCommand{
InDockerComposeProject: tc.inDockerComposeProject,
Config: &config.AppConfig{ProjectName: tc.projectName},
}
assert.Equal(t, tc.want, c.IsProjectScoped())
})
}
}
17 changes: 8 additions & 9 deletions pkg/gui/containers_panel.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,14 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
return false
}

// Only apply project and standalone filtering when we are inside a
// docker-compose project. Outside of a compose project all
// containers are shown in a flat list regardless of which compose
// project they belong to.
if gui.DockerCommand.InDockerComposeProject {
// This check must be inside the InDockerComposeProject guard:
// outside a compose project, services are still derived from
// container labels, so compose-managed containers from other
// projects would be incorrectly hidden.
// When project-scoped, apply project and standalone filtering.
// Otherwise all containers are shown in a flat list regardless
// of which compose project they belong to.
if gui.DockerCommand.IsProjectScoped() {
// This check must be inside the IsProjectScoped guard: when
// not project-scoped, services are still derived from container
// labels, so compose-managed containers from other projects
// would be incorrectly hidden.
//
// Note that this is O(N*M) time complexity where N is the number of services
// and M is the number of containers. We expect N to be small but M may be large,
Expand Down
2 changes: 1 addition & 1 deletion pkg/gui/gui.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ func (gui *Gui) ShouldRefresh(key string) bool {
}

func (gui *Gui) initiallyFocusedViewName() string {
if gui.DockerCommand.InDockerComposeProject {
if gui.DockerCommand.IsProjectScoped() {
return "services"
}
return "containers"
Expand Down
23 changes: 12 additions & 11 deletions pkg/gui/project_panel.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,7 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
return gui.renderContainersAndServices()
},
Hide: func() bool {
// Only show the project panel when we are inside a docker-compose
// project directory. When launched outside of a compose project
// there is no meaningful local project to display, so we hide the
// panel and let the containers panel show all containers in a flat
// list (matching the behaviour from before v0.25).
return !gui.DockerCommand.InDockerComposeProject
return !gui.DockerCommand.IsProjectScoped()
},
}
}
Expand Down Expand Up @@ -99,18 +94,19 @@ func (gui *Gui) refreshProject() error {
}

// getDiscoveredProjects returns all docker compose projects by examining container labels.
// The local project (from docker-compose.yml in the current directory) is included if
// it has running containers or if InDockerComposeProject is true.
// The local project (from docker-compose.yml in the current directory, or from -p) is
// included even when it has no running containers, so the user always sees the project
// they explicitly scoped to.
func (gui *Gui) getDiscoveredProjects() []*commands.Project {
containers := gui.Panels.Containers.List.GetAllItems()
projectNames := gui.DockerCommand.GetProjectNames(containers)

// If we're in a docker compose project but it has no running containers,
// still include it. We don't fall back to the directory name here to avoid
// If we're scoped to a project but it has no running containers, still
// include it. We don't fall back to the directory name here to avoid
// briefly flashing the wrong project name on startup.
localName := gui.DockerCommand.LocalProjectName

if gui.DockerCommand.InDockerComposeProject && localName != "" {
if gui.DockerCommand.IsProjectScoped() && localName != "" {
found := false
for _, name := range projectNames {
if name == localName {
Expand Down Expand Up @@ -195,6 +191,11 @@ func (gui *Gui) renderAllLogs(project *commands.Project) tasks.TaskFunc {
}

func (gui *Gui) renderDockerComposeConfig(project *commands.Project) tasks.TaskFunc {
if !gui.DockerCommand.InDockerComposeProject {
return gui.NewSimpleRenderStringTask(func() string {
return "Compose config is only available when launched from a docker-compose project directory"
})
}
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
return gui.NewSimpleRenderStringTask(func() string {
return "Compose config is not available for non-local projects"
Expand Down
7 changes: 1 addition & 6 deletions pkg/gui/services_panel.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,7 @@ func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] {
return presentation.GetServiceDisplayStrings(&gui.Config.UserConfig.Gui, service)
},
Hide: func() bool {
// Only show the services panel when we are inside a docker-compose
// project directory. When launched outside of a compose project
// there is no local project context, so the panel is hidden and
// all containers are shown in a flat list (matching pre-v0.25
// behaviour).
return !gui.DockerCommand.InDockerComposeProject
return !gui.DockerCommand.IsProjectScoped()
},
}
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/gui/views.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (gui *Gui) createAllViews() error {

gui.Views.Containers.Highlight = true
gui.Views.Containers.SelBgColor = selectedLineBgColor
if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject {
if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.IsProjectScoped() {
gui.Views.Containers.Title = gui.Tr.ContainersTitle
} else {
gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle
Expand Down
Loading