From 01f9b3973f0a7e0fd4479dd4db28ab3d577a1c69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20P=C3=89AU?= Date: Mon, 12 May 2025 10:36:19 +0200 Subject: [PATCH] wip on projects --- pkg/app/app.go | 17 +++++++- pkg/commands/docker.go | 86 +++++++++++++++++++++++++------------ pkg/gui/containers_panel.go | 1 + pkg/gui/gui.go | 9 +++- pkg/gui/keybindings.go | 7 +++ pkg/gui/project_panel.go | 75 ++++++++++++++++++++++++-------- pkg/i18n/english.go | 2 + 7 files changed, 149 insertions(+), 48 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 09a49789..827a453c 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -41,17 +41,30 @@ func NewApp(config *config.AppConfig) (*App, error) { } app.OSCommand = commands.NewOSCommand(app.Log, config) - // here is the place to make use of the docker-compose.yml file in the current directory - app.DockerCommand, err = commands.NewDockerCommand(app.Log, app.OSCommand, app.Tr, app.Config, app.ErrorChan) if err != nil { return app, err } + app.closers = append(app.closers, app.DockerCommand) app.Gui, err = gui.NewGui(app.Log, app.DockerCommand, app.OSCommand, app.Tr, config, app.ErrorChan) if err != nil { return app, err } + + // here is the place to make use of the docker-compose.yml file in the current directory + err = app.OSCommand.RunCommand( + utils.ApplyTemplate( + config.UserConfig.CommandTemplates.CheckDockerComposeConfig, + app.DockerCommand.NewCommandObject(commands.CommandObject{}), + ), + ) + if err != nil { + app.DockerCommand.InDockerComposeProject = false + app.Log.Warn(err.Error()) + } else { + app.Gui.State.Project = &commands.Project{Name: app.Gui.GetProjectName()} + } return app, nil } diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index a6e702d7..ee083383 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -34,15 +34,16 @@ const ( // DockerCommand is our main docker interface type DockerCommand struct { - Log *logrus.Entry - OSCommand *OSCommand - Tr *i18n.TranslationSet - Config *config.AppConfig - Client *client.Client - InDockerComposeProject bool - ErrorChan chan error - ContainerMutex deadlock.Mutex - ServiceMutex deadlock.Mutex + Log *logrus.Entry + OSCommand *OSCommand + Tr *i18n.TranslationSet + Config *config.AppConfig + Client *client.Client + InDockerComposeProject bool + CurrentDockerComposeProject string + ErrorChan chan error + ContainerMutex deadlock.Mutex + ServiceMutex deadlock.Mutex Closers []io.Closer } @@ -112,17 +113,6 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat dockerCommand.setDockerComposeCommand(config) - err = osCommand.RunCommand( - utils.ApplyTemplate( - config.UserConfig.CommandTemplates.CheckDockerComposeConfig, - dockerCommand.NewCommandObject(CommandObject{}), - ), - ) - if err != nil { - dockerCommand.InDockerComposeProject = false - log.Warn(err.Error()) - } - return dockerCommand, nil } @@ -176,7 +166,7 @@ func (c *DockerCommand) CreateClientStatMonitor(container *Container) { container.MonitoringStats = false } -func (c *DockerCommand) RefreshContainersAndServices(currentServices []*Service, currentContainers []*Container) ([]*Container, []*Service, error) { +func (c *DockerCommand) RefreshContainersAndServices(currentServices []*Service, currentContainers []*Container, currentProject *Project) ([]*Container, []*Service, error) { c.ServiceMutex.Lock() defer c.ServiceMutex.Unlock() @@ -186,17 +176,15 @@ func (c *DockerCommand) RefreshContainersAndServices(currentServices []*Service, } var services []*Service - // we only need to get these services once because they won't change in the runtime of the program - if currentServices != nil { - services = currentServices - } else { - services, err = c.GetServices() + if currentProject != nil { + services, err = c.GetServicesFromContainers(containers, currentProject) if err != nil { return nil, nil, err } - } - c.assignContainersToServices(containers, services) + c.assignContainersToServices(containers, services) + //c.assignContainersToProjects(containers) + } return containers, services, nil } @@ -214,6 +202,11 @@ L: } } +// GetDockerProjects +func (c *DockerCommand) GetDockerProjects() ([]*Project, error) { + return []*Project{}, nil +} + // GetContainers gets the docker containers func (c *DockerCommand) GetContainers(existingContainers []*Container) ([]*Container, error) { c.ContainerMutex.Lock() @@ -269,6 +262,42 @@ func (c *DockerCommand) GetContainers(existingContainers []*Container) ([]*Conta return ownContainers, nil } +// GetServicesFromContainers gets services +func (c *DockerCommand) GetServicesFromContainers(containers []*Container, currentProject *Project) ([]*Service, error) { + var services []*Service + + if currentProject.Name == "" { + return services, nil + } + + for _, cont := range containers { + if cont.ProjectName != currentProject.Name { + continue + } + + service := &Service{ + Name: cont.Name, + ID: cont.ID, + OSCommand: c.OSCommand, + Log: c.Log, + DockerCommand: c, + } + services = append(services, service) + } + + //TODO: Should be run once every time the project changes + if len(services) == 0 { + //If no services are found in running containers fetch services from dockerCompose + var err error + services, err = c.GetServices() + if err != nil { + return nil, err + } + } + + return services, nil +} + // GetServices gets services func (c *DockerCommand) GetServices() ([]*Service, error) { if !c.InDockerComposeProject { @@ -276,6 +305,7 @@ func (c *DockerCommand) GetServices() ([]*Service, error) { } composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose + // TODO: Handle remote docker compose files output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --services", composeCommand)) if err != nil { return nil, err diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 92aa95b3..11e2f6c2 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -258,6 +258,7 @@ func (gui *Gui) refreshContainersAndServices() error { containers, services, err := gui.DockerCommand.RefreshContainersAndServices( gui.Panels.Services.List.GetAllItems(), gui.Panels.Containers.List.GetAllItems(), + gui.State.Project, ) if err != nil { return err diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index fa6199f3..bd3f2cff 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -87,8 +87,15 @@ type guiState struct { // Maintains the state of manual filtering i.e. typing in a substring // to filter on in the current panel. Filter filterState + + // Project used for navigation in projects + Project *commands.Project } +//type projectState struct { +// name string +//} + type filterState struct { // If true then we're either currently inside the filter view // or we've committed the filter and we're back in the list view @@ -297,7 +304,7 @@ func (gui *Gui) updateContainerDetails() error { func (gui *Gui) refresh() { go func() { - if err := gui.refreshProject(); err != nil { + if err := gui.refreshProjects(); err != nil { gui.Log.Error(err) } }() diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index df706024..c1977784 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -135,6 +135,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleEditConfig, Description: gui.Tr.EditConfig, }, + { + ViewName: "project", + Key: gocui.KeyCtrlP, + Modifier: gocui.ModNone, + Handler: gui.handleCreateProjectMenu, + Description: gui.Tr.SwitchProject, + }, { ViewName: "project", Key: 'o', diff --git a/pkg/gui/project_panel.go b/pkg/gui/project_panel.go index 32c5ad99..864a019d 100644 --- a/pkg/gui/project_panel.go +++ b/pkg/gui/project_panel.go @@ -3,6 +3,9 @@ package gui import ( "bytes" "context" + "github.com/docker/docker/api/types/container" + "github.com/jesseduffield/lazydocker/pkg/gui/types" + "log" "path" "strings" @@ -60,35 +63,51 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] { List: panels.NewFilteredList[*commands.Project](), View: gui.Views.Project, }, - NoItemsMessage: "", + NoItemsMessage: "No docker compose projects found.", Gui: gui.intoInterface(), Sort: func(a *commands.Project, b *commands.Project) bool { - return false + return (gui.State.Project != nil && gui.State.Project.Name == a.Name) || a.Name < b.Name }, GetTableCells: presentation.GetProjectDisplayStrings, - // It doesn't make sense to filter a list of only one item. - DisableFilter: true, } } -func (gui *Gui) refreshProject() error { - gui.Panels.Projects.SetItems([]*commands.Project{{Name: gui.getProjectName()}}) - return gui.Panels.Projects.RerenderList() -} +func (gui *Gui) refreshProjects() error { + containers, err := gui.DockerCommand.Client.ContainerList(context.Background(), container.ListOptions{All: true}) + if err != nil { + return err + } -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"] - } + projectsMap := make(map[string]*commands.Project) + + for _, container := range containers { + if projectName, exists := container.Labels["com.docker.compose.project"]; exists && projectName != "" { + projectsMap[projectName] = &commands.Project{Name: projectName} } } - return projectName + projectsList := make([]*commands.Project, 0, len(projectsMap)) + for _, project := range projectsMap { + projectsList = append(projectsList, project) + } + + // Add current's folder project if exists + if gui.DockerCommand.InDockerComposeProject { + projectsList = append(projectsList, &commands.Project{Name: gui.GetProjectName()}) + } + gui.Panels.Projects.SetItems(projectsList) + + return gui.Panels.Projects.RerenderList() +} + +func (gui *Gui) GetProjectName() string { + if gui.State.Project != nil { + return gui.State.Project.Name + } + + // Default to the command line argument + return path.Base(gui.Config.ProjectDir) } func (gui *Gui) renderCredits(_project *commands.Project) tasks.TaskFunc { @@ -180,3 +199,25 @@ func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error { return gui.runSubprocess(c) } + +func (gui *Gui) handleCreateProjectMenu(g *gocui.Gui, v *gocui.View) error { + if gui.isPopupPanel(v.Name()) { + return nil + } + + testMenuItem := &types.MenuItem{ + LabelColumns: []string{"t", "test"}, + OnPress: func() error { + log.Println("tested") + return nil + }, + } + + menuItems := []*types.MenuItem{testMenuItem} + + return gui.Menu(CreateMenuOptions{ + Title: gui.Tr.MenuTitle, + Items: menuItems, + HideCancel: true, + }) +} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index d1ed0320..c1537672 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -109,6 +109,7 @@ type TranslationSet struct { FilterList string OpenInBrowser string SortContainersByState string + SwitchProject string LogsTitle string ConfigTitle string @@ -216,6 +217,7 @@ func englishSet() TranslationSet { FilterList: "filter list", OpenInBrowser: "open in browser (first port is http)", SortContainersByState: "sort containers by state", + SwitchProject: "switch project", GlobalTitle: "Global", MainTitle: "Main",