mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-22 15:11:02 +00:00
Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e7aadc207 | ||
|
|
f5ff116af9 | ||
|
|
3974f6fec1 | ||
|
|
697cd441aa | ||
|
|
9134abeefd | ||
|
|
8106125441 | ||
|
|
b17d474148 | ||
|
|
e3c1c8630a | ||
|
|
ebce4fc631 |
10 changed files with 112 additions and 38 deletions
|
|
@ -2,11 +2,11 @@
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"PostToolUse": [
|
"PostToolUse": [
|
||||||
{
|
{
|
||||||
"matcher": "Edit|Write",
|
"matcher": "Edit|Write|MultiEdit",
|
||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
CLAUDE.md
Normal file
5
CLAUDE.md
Normal file
|
|
@ -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 ./...`
|
||||||
|
|
@ -154,9 +154,25 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat
|
||||||
log.Warn(err.Error())
|
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
|
||||||
|
}
|
||||||
|
|
||||||
return dockerCommand, nil
|
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 != ""
|
||||||
|
}
|
||||||
|
|
||||||
func (c *DockerCommand) setDockerComposeCommand(config *config.AppConfig) {
|
func (c *DockerCommand) setDockerComposeCommand(config *config.AppConfig) {
|
||||||
if config.UserConfig.CommandTemplates.DockerCompose != "docker compose" {
|
if config.UserConfig.CommandTemplates.DockerCompose != "docker compose" {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -61,3 +62,30 @@ func TestNewDockerClientVersionNegotiation(t *testing.T) {
|
||||||
"client version should not be locked to DOCKER_API_VERSION env var")
|
"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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -176,23 +176,30 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The project panel is compact (Size: 3) when not focused, but expands
|
// The project panel is compact (Size: 3) when not focused, but expands
|
||||||
// when focused to show the list of projects.
|
// when focused to show the list of projects. This only applies when the
|
||||||
projectBox := &boxlayout.Box{
|
// project panel is actually visible (i.e. we are inside a compose project).
|
||||||
Window: sideWindowNames[0],
|
if len(sideWindowNames) > 0 && sideWindowNames[0] == "project" {
|
||||||
Size: 3,
|
projectBox := &boxlayout.Box{
|
||||||
}
|
|
||||||
if currentWindow == sideWindowNames[0] {
|
|
||||||
projectBox = &boxlayout.Box{
|
|
||||||
Window: sideWindowNames[0],
|
Window: sideWindowNames[0],
|
||||||
Weight: 2,
|
Size: 3,
|
||||||
}
|
}
|
||||||
|
if currentWindow == sideWindowNames[0] {
|
||||||
|
projectBox = &boxlayout.Box{
|
||||||
|
Window: sideWindowNames[0],
|
||||||
|
Weight: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return append([]*boxlayout.Box{
|
||||||
|
projectBox,
|
||||||
|
}, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box {
|
||||||
|
return accordionBox(&boxlayout.Box{Window: window, Weight: 1})
|
||||||
|
})...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return append([]*boxlayout.Box{
|
return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box {
|
||||||
projectBox,
|
|
||||||
}, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box {
|
|
||||||
return accordionBox(&boxlayout.Box{Window: window, Weight: 1})
|
return accordionBox(&boxlayout.Box{Window: window, Weight: 1})
|
||||||
})...)
|
})
|
||||||
} else {
|
} else {
|
||||||
squashedHeight := 1
|
squashedHeight := 1
|
||||||
if height >= 21 {
|
if height >= 21 {
|
||||||
|
|
|
||||||
|
|
@ -83,25 +83,35 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
|
||||||
return sortContainers(a, b, gui.Config.UserConfig.Gui.LegacySortContainers)
|
return sortContainers(a, b, gui.Config.UserConfig.Gui.LegacySortContainers)
|
||||||
},
|
},
|
||||||
Filter: func(container *commands.Container) bool {
|
Filter: func(container *commands.Container) bool {
|
||||||
// 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,
|
|
||||||
// so we will need to keep an eye on this.
|
|
||||||
if !gui.Config.UserConfig.Gui.ShowAllContainers && !isStandaloneContainer(container) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !gui.State.ShowExitedContainers && container.Container.State == "exited" {
|
if !gui.State.ShowExitedContainers && container.Container.State == "exited" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by selected project. Containers with no project (truly
|
// When project-scoped, apply project and standalone filtering.
|
||||||
// standalone, not from any compose project) are always shown.
|
// Otherwise all containers are shown in a flat list regardless
|
||||||
selectedProject := gui.getSelectedProjectName()
|
// of which compose project they belong to.
|
||||||
if selectedProject == "" {
|
if gui.DockerCommand.IsProjectScoped() {
|
||||||
selectedProject = gui.DockerCommand.LocalProjectName
|
// This check must be inside the IsProjectScoped guard: when
|
||||||
}
|
// not project-scoped, services are still derived from container
|
||||||
if selectedProject != "" && container.ProjectName != "" && container.ProjectName != selectedProject {
|
// labels, so compose-managed containers from other projects
|
||||||
return false
|
// 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,
|
||||||
|
// so we will need to keep an eye on this.
|
||||||
|
if !gui.Config.UserConfig.Gui.ShowAllContainers && !isStandaloneContainer(container) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by selected project. Containers with no project (truly
|
||||||
|
// standalone, not from any compose project) are always shown.
|
||||||
|
selectedProject := gui.getSelectedProjectName()
|
||||||
|
if selectedProject == "" {
|
||||||
|
selectedProject = gui.DockerCommand.LocalProjectName
|
||||||
|
}
|
||||||
|
if selectedProject != "" && container.ProjectName != "" && container.ProjectName != selectedProject {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -459,7 +459,7 @@ func (gui *Gui) ShouldRefresh(key string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) initiallyFocusedViewName() string {
|
func (gui *Gui) initiallyFocusedViewName() string {
|
||||||
if gui.DockerCommand.InDockerComposeProject {
|
if gui.DockerCommand.IsProjectScoped() {
|
||||||
return "services"
|
return "services"
|
||||||
}
|
}
|
||||||
return "containers"
|
return "containers"
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,9 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
|
||||||
// containers to show only those belonging to the selected project.
|
// containers to show only those belonging to the selected project.
|
||||||
return gui.renderContainersAndServices()
|
return gui.renderContainersAndServices()
|
||||||
},
|
},
|
||||||
|
Hide: func() bool {
|
||||||
|
return !gui.DockerCommand.IsProjectScoped()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,18 +94,19 @@ func (gui *Gui) refreshProject() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDiscoveredProjects returns all docker compose projects by examining container labels.
|
// getDiscoveredProjects returns all docker compose projects by examining container labels.
|
||||||
// The local project (from docker-compose.yml in the current directory) is included if
|
// The local project (from docker-compose.yml in the current directory, or from -p) is
|
||||||
// it has running containers or if InDockerComposeProject is true.
|
// 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 {
|
func (gui *Gui) getDiscoveredProjects() []*commands.Project {
|
||||||
containers := gui.Panels.Containers.List.GetAllItems()
|
containers := gui.Panels.Containers.List.GetAllItems()
|
||||||
projectNames := gui.DockerCommand.GetProjectNames(containers)
|
projectNames := gui.DockerCommand.GetProjectNames(containers)
|
||||||
|
|
||||||
// If we're in a docker compose project but it has no running containers,
|
// If we're scoped to a project but it has no running containers, still
|
||||||
// still include it. We don't fall back to the directory name here to avoid
|
// include it. We don't fall back to the directory name here to avoid
|
||||||
// briefly flashing the wrong project name on startup.
|
// briefly flashing the wrong project name on startup.
|
||||||
localName := gui.DockerCommand.LocalProjectName
|
localName := gui.DockerCommand.LocalProjectName
|
||||||
|
|
||||||
if gui.DockerCommand.InDockerComposeProject && localName != "" {
|
if gui.DockerCommand.IsProjectScoped() && localName != "" {
|
||||||
found := false
|
found := false
|
||||||
for _, name := range projectNames {
|
for _, name := range projectNames {
|
||||||
if name == localName {
|
if name == localName {
|
||||||
|
|
@ -187,6 +191,11 @@ func (gui *Gui) renderAllLogs(project *commands.Project) tasks.TaskFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) renderDockerComposeConfig(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 {
|
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
|
||||||
return gui.NewSimpleRenderStringTask(func() string {
|
return gui.NewSimpleRenderStringTask(func() string {
|
||||||
return "Compose config is not available for non-local projects"
|
return "Compose config is not available for non-local projects"
|
||||||
|
|
|
||||||
|
|
@ -90,8 +90,7 @@ func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] {
|
||||||
return presentation.GetServiceDisplayStrings(&gui.Config.UserConfig.Gui, service)
|
return presentation.GetServiceDisplayStrings(&gui.Config.UserConfig.Gui, service)
|
||||||
},
|
},
|
||||||
Hide: func() bool {
|
Hide: func() bool {
|
||||||
// Show services panel if there are any compose projects (local or discovered)
|
return !gui.DockerCommand.IsProjectScoped()
|
||||||
return !gui.DockerCommand.InDockerComposeProject && len(gui.Panels.Services.List.GetAllItems()) == 0
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ func (gui *Gui) createAllViews() error {
|
||||||
|
|
||||||
gui.Views.Containers.Highlight = true
|
gui.Views.Containers.Highlight = true
|
||||||
gui.Views.Containers.SelBgColor = selectedLineBgColor
|
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
|
gui.Views.Containers.Title = gui.Tr.ContainersTitle
|
||||||
} else {
|
} else {
|
||||||
gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle
|
gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue