diff --git a/pkg/app/app.go b/pkg/app/app.go index 09a49789..4c0e3e80 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -2,6 +2,7 @@ package app import ( "io" + "path" "strings" "github.com/jesseduffield/lazydocker/pkg/commands" @@ -49,6 +50,15 @@ func NewApp(config *config.AppConfig) (*App, error) { } app.closers = append(app.closers, app.DockerCommand) app.Gui, err = gui.NewGui(app.Log, app.DockerCommand, app.OSCommand, app.Tr, config, app.ErrorChan) + + currentDir := path.Base(app.Config.ProjectDir) + for _, project := range app.DockerCommand.GetAllComposeProjects() { + if project.WorkingDir == currentDir { + app.DockerCommand.SelectedComposeProject = &project + break + } + } + if err != nil { return app, err } diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index 70f4e88d..2c802bee 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -33,7 +33,7 @@ type DockerCommand struct { Tr *i18n.TranslationSet Config *config.AppConfig Client *client.Client - InDockerComposeProject bool + SelectedComposeProject *ComposeProject ErrorChan chan error ContainerMutex deadlock.Mutex ServiceMutex deadlock.Mutex @@ -57,6 +57,11 @@ type CommandObject struct { Volume *Volume } +type ComposeProject struct { + Name string + WorkingDir string +} + // NewCommandObject takes a command object and returns a default command object with the passed command object merged in func (c *DockerCommand) NewCommandObject(obj CommandObject) CommandObject { defaultObj := CommandObject{DockerCompose: c.Config.UserConfig.CommandTemplates.DockerCompose} @@ -77,14 +82,13 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat } dockerCommand := &DockerCommand{ - Log: log, - OSCommand: osCommand, - Tr: tr, - Config: config, - Client: cli, - ErrorChan: errorChan, - InDockerComposeProject: true, - Closers: []io.Closer{tunnelCloser}, + Log: log, + OSCommand: osCommand, + Tr: tr, + Config: config, + Client: cli, + ErrorChan: errorChan, + Closers: []io.Closer{tunnelCloser}, } command := utils.ApplyTemplate( @@ -94,16 +98,13 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat log.Warn(command) - err = osCommand.RunCommand( - utils.ApplyTemplate( - config.UserConfig.CommandTemplates.CheckDockerComposeConfig, - dockerCommand.NewCommandObject(CommandObject{}), - ), - ) - if err != nil { - dockerCommand.InDockerComposeProject = false - log.Warn(err.Error()) - } + //dockerCommand.GetAllComposeProjects() + //err = osCommand.RunCommand( + // utils.ApplyTemplate( + // config.UserConfig.CommandTemplates.CheckDockerComposeConfig, + // dockerCommand.NewCommandObject(CommandObject{}), + // ), + //) return dockerCommand, nil } @@ -112,6 +113,25 @@ func (c *DockerCommand) Close() error { return utils.CloseMany(c.Closers) } +func (c *DockerCommand) GetAllComposeProjects() map[string]ComposeProject { + containers, _ := c.Client.ContainerList(context.Background(), dockerTypes.ContainerListOptions{ + All: true, + }) + + projects := make(map[string]ComposeProject) + for _, c := range containers { + projectName, ok := c.Labels["com.docker.compose.project"] + if ok { + workingDir, ok := c.Labels["com.docker.compose.project.working_dir"] + //configFiles, ok := c.Labels["com.docker.compose.project.config_files"] + if ok { + projects[projectName] = ComposeProject{Name: projectName, WorkingDir: workingDir} + } + } + } + return projects +} + func (c *DockerCommand) CreateClientStatMonitor(container *Container) { container.MonitoringStats = true stream, err := c.Client.ContainerStats(context.Background(), container.ID, true) @@ -239,12 +259,12 @@ func (c *DockerCommand) GetContainers(existingContainers []*Container) ([]*Conta // GetServices gets services func (c *DockerCommand) GetServices() ([]*Service, error) { - if !c.InDockerComposeProject { + if c.SelectedComposeProject == nil { return nil, nil } composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose - output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --services", composeCommand)) + output, err := c.OSCommand.RunCommandWithOutputFrom(fmt.Sprintf("%s config --services", composeCommand), c.SelectedComposeProject.WorkingDir) if err != nil { return nil, err } @@ -302,11 +322,12 @@ func (c *DockerCommand) ViewAllLogs() (*exec.Cmd, error) { // DockerComposeConfig returns the result of 'docker-compose config' func (c *DockerCommand) DockerComposeConfig() string { - output, err := c.OSCommand.RunCommandWithOutput( + output, err := c.OSCommand.RunCommandWithOutputFrom( utils.ApplyTemplate( c.OSCommand.Config.UserConfig.CommandTemplates.DockerComposeConfig, c.NewCommandObject(CommandObject{}), ), + c.SelectedComposeProject.WorkingDir, ) if err != nil { output = err.Error() diff --git a/pkg/commands/os.go b/pkg/commands/os.go index ae229291..5a8e6406 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -55,15 +55,21 @@ func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) { c.command = cmd } -// RunCommandWithOutput wrapper around commands returning their output and error -func (c *OSCommand) RunCommandWithOutput(command string) (string, error) { +// RunCommandWithOutputFrom wrapper around commands returning their output and error +func (c *OSCommand) RunCommandWithOutputFrom(command string, workingDir string) (string, error) { cmd := c.ExecutableFromString(command) before := time.Now() + cmd.Dir = workingDir output, err := sanitisedCommandOutput(cmd.Output()) c.Log.Warn(fmt.Sprintf("'%s': %s", command, time.Since(before))) return output, err } +// RunCommandWithOutput wrapper around commands returning their output and error +func (c *OSCommand) RunCommandWithOutput(command string) (string, error) { + return c.RunCommandWithOutputFrom(command, "") +} + // RunCommandWithOutput wrapper around commands returning their output and error func (c *OSCommand) RunCommandWithOutputContext(ctx context.Context, command string) (string, error) { cmd := c.ExecutableFromStringContext(ctx, command) diff --git a/pkg/commands/project.go b/pkg/commands/project.go index 529ccfa7..8dcf317f 100644 --- a/pkg/commands/project.go +++ b/pkg/commands/project.go @@ -1,5 +1,5 @@ package commands type Project struct { - Name string + Compose ComposeProject } diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 862fc4be..bda3b73f 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -20,17 +20,20 @@ import ( "github.com/samber/lo" ) +var isStandaloneContainer = func(container *commands.Container) bool { + if container.OneOff || container.ServiceName == "" { + return true + } + + project, _ := container.Container.Labels["com.docker.compose.project"] + return project == "" + //return !lo.SomeBy(gui.Panels.Services.List.GetAllItems(), func(service *commands.Service) bool { + // return service.Name == container.ServiceName + //}) +} + func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container] { // Standalone containers are containers which are either one-off containers, or whose service is not part of this docker-compose context. - isStandaloneContainer := func(container *commands.Container) bool { - if container.OneOff || container.ServiceName == "" { - return true - } - - return !lo.SomeBy(gui.Panels.Services.List.GetAllItems(), func(service *commands.Service) bool { - return service.Name == container.ServiceName - }) - } return &panels.SideListPanel[*commands.Container]{ ContextState: &panels.ContextState[*commands.Container]{ @@ -254,6 +257,12 @@ func (gui *Gui) refreshContainersAndServices() error { originalSelectedLineIdx := gui.Panels.Services.SelectedIdx selectedService, isServiceSelected := gui.Panels.Services.List.TryGet(originalSelectedLineIdx) + services, err := gui.DockerCommand.GetServices() + if err != nil { + return err + } + gui.Panels.Services.List.SetItems(services) + containers, services, err := gui.DockerCommand.RefreshContainersAndServices( gui.Panels.Services.List.GetAllItems(), gui.Panels.Containers.List.GetAllItems(), @@ -282,7 +291,7 @@ func (gui *Gui) refreshContainersAndServices() error { } func (gui *Gui) renderContainersAndServices() error { - if gui.DockerCommand.InDockerComposeProject { + if gui.DockerCommand.SelectedComposeProject != nil { if err := gui.Panels.Services.RerenderList(); err != nil { return err } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 1ffc8892..6a24254e 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -440,7 +440,7 @@ func (gui *Gui) ShouldRefresh(key string) bool { } func (gui *Gui) initiallyFocusedViewName() string { - if gui.DockerCommand.InDockerComposeProject { + if gui.DockerCommand.SelectedComposeProject != nil { return "services" } return "containers" diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 4c086469..307ca16c 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -129,21 +129,27 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleCustomCommand, }, { - ViewName: "project", + ViewName: "projects", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleEditConfig, Description: gui.Tr.EditConfig, }, { - ViewName: "project", + ViewName: "projects", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleOpenConfig, Description: gui.Tr.OpenConfig, }, { - ViewName: "project", + ViewName: "projects", + Key: gocui.KeySpace, + Modifier: gocui.ModNone, + Handler: gui.handleComposeProjectSelect, + }, + { + ViewName: "projects", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleViewAllLogs, diff --git a/pkg/gui/presentation/projects.go b/pkg/gui/presentation/projects.go index 44d396c9..5d7228a1 100644 --- a/pkg/gui/presentation/projects.go +++ b/pkg/gui/presentation/projects.go @@ -3,5 +3,6 @@ package presentation import "github.com/jesseduffield/lazydocker/pkg/commands" func GetProjectDisplayStrings(project *commands.Project) []string { - return []string{project.Name} + // TODO show status up down stop + return []string{project.Compose.Name} } diff --git a/pkg/gui/project_panel.go b/pkg/gui/project_panel.go index bfc829f1..13263b80 100644 --- a/pkg/gui/project_panel.go +++ b/pkg/gui/project_panel.go @@ -3,7 +3,7 @@ package gui import ( "bytes" "context" - "path" + "github.com/samber/lo" "strings" "github.com/fatih/color" @@ -23,7 +23,7 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] { return &panels.SideListPanel[*commands.Project]{ ContextState: &panels.ContextState[*commands.Project]{ GetMainTabs: func() []panels.MainTab[*commands.Project] { - if gui.DockerCommand.InDockerComposeProject { + if gui.DockerCommand.SelectedComposeProject != nil { return []panels.MainTab[*commands.Project]{ { Key: "logs", @@ -52,43 +52,39 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] { } }, GetItemContextCacheKey: func(project *commands.Project) string { - return "projects-" + project.Name + return "projects-" + project.Compose.Name // TODO status }, }, ListPanel: panels.ListPanel[*commands.Project]{ List: panels.NewFilteredList[*commands.Project](), - View: gui.Views.Project, + View: gui.Views.Projects, }, NoItemsMessage: "", Gui: gui.intoInterface(), Sort: func(a *commands.Project, b *commands.Project) bool { - return false + return a.Compose.Name < b.Compose.Name }, GetTableCells: presentation.GetProjectDisplayStrings, // It doesn't make sense to filter a list of only one item. - DisableFilter: true, + //DisableFilter: true, } } func (gui *Gui) refreshProject() error { - gui.Panels.Projects.SetItems([]*commands.Project{{Name: gui.getProjectName()}}) + c := gui.getProjects() + list := make([]*commands.Project, len(c)) + for i, s := range lo.Values(c) { + list[i] = &commands.Project{Compose: s} + } + + gui.Panels.Projects.SetItems(list) return gui.Panels.Projects.RerenderList() } -func (gui *Gui) getProjectName() string { - projectName := path.Base(gui.Config.ProjectDir) - if gui.DockerCommand.InDockerComposeProject { - for _, service := range gui.Panels.Services.List.GetAllItems() { - container := service.Container - if container != nil && container.DetailsLoaded() { - return container.Details.Config.Labels["com.docker.compose.project"] - } - } - } - - return projectName +func (gui *Gui) getProjects() map[string]commands.ComposeProject { + return gui.DockerCommand.GetAllComposeProjects() } func (gui *Gui) renderCredits(_project *commands.Project) tasks.TaskFunc { @@ -125,6 +121,7 @@ func (gui *Gui) renderAllLogs(_project *commands.Project) tasks.TaskFunc { gui.DockerCommand.NewCommandObject(commands.CommandObject{}), ), ) + cmd.Dir = _project.Compose.WorkingDir cmd.Stdout = gui.Views.Main cmd.Stderr = gui.Views.Main @@ -156,6 +153,20 @@ func (gui *Gui) handleEditConfig(g *gocui.Gui, v *gocui.View) error { return gui.editFile(gui.Config.ConfigFilename()) } +func (gui *Gui) handleComposeProjectSelect(g *gocui.Gui, v *gocui.View) error { + project, err := gui.Panels.Projects.GetSelectedItem() + if err != nil { + return nil + } + + //if gui.DockerCommand.SelectedComposeProject != &project.Compose { + gui.DockerCommand.SelectedComposeProject = &project.Compose + // gui.renderServiceLogs() + return gui.refreshContainersAndServices() + //} + //return nil +} + func lazydockerTitle() string { return ` _ _ _ diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index 2c1caaec..b8fe6a45 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -75,9 +75,9 @@ func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] { return a.Name < b.Name }, GetTableCells: presentation.GetServiceDisplayStrings, - Hide: func() bool { - return !gui.DockerCommand.InDockerComposeProject - }, + //Hide: func() bool { + // return gui.DockerCommand.SelectedComposeProject == nil + //}, } } diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 05b9db5f..04955308 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -27,7 +27,7 @@ func hideUnderScores() bool { type Views struct { // side panels - Project *gocui.View + Projects *gocui.View Services *gocui.View Containers *gocui.View Images *gocui.View @@ -65,7 +65,7 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping { return []viewNameMapping{ // first layer. Ordering within this layer does not matter because there are // no overlapping views - {viewPtr: &gui.Views.Project, name: "project", autoPosition: true}, + {viewPtr: &gui.Views.Projects, name: "projects", autoPosition: true}, {viewPtr: &gui.Views.Services, name: "services", autoPosition: true}, {viewPtr: &gui.Views.Containers, name: "containers", autoPosition: true}, {viewPtr: &gui.Views.Images, name: "images", autoPosition: true}, @@ -105,7 +105,9 @@ func (gui *Gui) createAllViews() error { // when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default. gui.Views.Main.IgnoreCarriageReturns = true - gui.Views.Project.Title = gui.Tr.ProjectTitle + gui.Views.Projects.Highlight = true + gui.Views.Projects.Title = gui.Tr.ProjectTitle + gui.Views.Projects.SelBgColor = selectedLineBgColor gui.Views.Services.Highlight = true gui.Views.Services.Title = gui.Tr.ServicesTitle @@ -113,7 +115,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.SelectedComposeProject == nil { gui.Views.Containers.Title = gui.Tr.ContainersTitle } else { gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index 1532eeea..2508f2f0 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -54,7 +54,7 @@ func dutchSet() TranslationSet { GlobalTitle: "Globaal", MainTitle: "Hoofd", - ProjectTitle: "Project", + ProjectTitle: "Projects", ServicesTitle: "Diensten", ContainersTitle: "Containers", StandaloneContainersTitle: "Alleenstaande Containers", diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 4d9aa1d2..46f0c543 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -204,7 +204,7 @@ func englishSet() TranslationSet { GlobalTitle: "Global", MainTitle: "Main", - ProjectTitle: "Project", + ProjectTitle: "Projects", ServicesTitle: "Services", ContainersTitle: "Containers", StandaloneContainersTitle: "Standalone Containers",