From c49f340d2d53782bc56700f174cffa49e20419b5 Mon Sep 17 00:00:00 2001 From: Saravanabalagi Ramachandran Date: Mon, 24 Mar 2025 01:22:05 +0000 Subject: [PATCH] Add compose profile support --- pkg/commands/docker.go | 52 +++- pkg/commands/project.go | 61 ++++- pkg/config/app_config.go | 65 +++-- pkg/gui/keybindings.go | 53 +++- pkg/gui/presentation/projects.go | 20 +- pkg/gui/project_panel.go | 408 ++++++++++++++++++++++++++++--- pkg/gui/services_panel.go | 67 ----- pkg/gui/views.go | 2 + pkg/i18n/english.go | 16 ++ 9 files changed, 619 insertions(+), 125 deletions(-) diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index 28c12c23..148d359d 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -40,6 +40,7 @@ type DockerCommand struct { Config *config.AppConfig Client *client.Client InDockerComposeProject bool + HasProfiles bool ErrorChan chan error ContainerMutex deadlock.Mutex ServiceMutex deadlock.Mutex @@ -57,6 +58,7 @@ type LimitedDockerCommand interface { // CommandObject is what we pass to our template resolvers when we are running a custom command. We do not guarantee that all fields will be populated: just the ones that make sense for the current context type CommandObject struct { DockerCompose string + Profile string Service *Service Container *Container Image *Image @@ -109,7 +111,8 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat Config: config, Client: cli, ErrorChan: errorChan, - InDockerComposeProject: true, + InDockerComposeProject: false, + HasProfiles: false, Closers: []io.Closer{tunnelCloser}, } @@ -121,8 +124,20 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat dockerCommand.NewCommandObject(CommandObject{}), ), ) - if err != nil { - dockerCommand.InDockerComposeProject = false + if err == nil { + dockerCommand.InDockerComposeProject = true + err = osCommand.RunCommand( + utils.ApplyTemplate( + config.UserConfig.CommandTemplates.CheckDockerComposeProfiles, + dockerCommand.NewCommandObject(CommandObject{}), + ), + ) + if err == nil { + dockerCommand.HasProfiles = true + } else { + log.Warn("No profiles found in docker-compose project") + } + } else { log.Warn(err.Error()) } @@ -217,6 +232,22 @@ L: } } +// GetProfiles gets the profiles defined in the compose file +func (c *DockerCommand) GetProfiles() ([]string, error) { + if !c.InDockerComposeProject { + return nil, nil + } + + composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose + output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --profiles", composeCommand)) + if err != nil { + return nil, err + } + + profiles := utils.SplitLines(output) + return profiles, nil +} + // GetContainers gets the docker containers func (c *DockerCommand) GetContainers(existingContainers []*Container) ([]*Container, error) { c.ContainerMutex.Lock() @@ -279,7 +310,7 @@ func (c *DockerCommand) GetServices() ([]*Service, error) { } composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose - output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --services", composeCommand)) + output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s --profile=* config --services", composeCommand)) if err != nil { return nil, err } @@ -360,6 +391,19 @@ func (c *DockerCommand) DockerComposeConfig() string { return output } +func (c *DockerCommand) DockerComposeProfileConfig(profile string) string { + output, err := c.OSCommand.RunCommandWithOutput( + utils.ApplyTemplate( + c.OSCommand.Config.UserConfig.CommandTemplates.DockerComposeProfileConfig, + c.NewCommandObject(CommandObject{Profile: profile}), + ), + ) + if err != nil { + output = err.Error() + } + return output +} + // determineDockerHost tries to the determine the docker host that we should connect to // in the following order of decreasing precedence: // - value of "DOCKER_HOST" environment variable diff --git a/pkg/commands/project.go b/pkg/commands/project.go index 529ccfa7..1592d404 100644 --- a/pkg/commands/project.go +++ b/pkg/commands/project.go @@ -1,5 +1,64 @@ package commands +import ( + "github.com/jesseduffield/lazydocker/pkg/config" + "github.com/jesseduffield/lazydocker/pkg/utils" +) + type Project struct { - Name string + Name string + IsProfile bool + OSCommand *OSCommand + Config *config.AppConfig + DockerCommand LimitedDockerCommand +} + +// Up ups the project +func (p *Project) Up() error { + commandTemplates := p.Config.UserConfig.CommandTemplates + templateCmdStr := commandTemplates.Up + if p.IsProfile { + templateCmdStr = commandTemplates.UpProfile + } + return p.runCommand(templateCmdStr) +} + +// Down downs the project +func (p *Project) Down() error { + defer func() { + if r := recover(); r != nil { + p.OSCommand.Log.Error(r) + } + }() + commandTemplates := p.Config.UserConfig.CommandTemplates + templateCmdStr := commandTemplates.Down + if p.IsProfile { + templateCmdStr = commandTemplates.DownProfile + } + return p.runCommand(templateCmdStr) +} + +// Restart restarts the project +func (p *Project) Restart() error { + commandTemplates := p.Config.UserConfig.CommandTemplates + templateCmdStr := commandTemplates.Restart + if p.IsProfile { + templateCmdStr = commandTemplates.RestartProfile + } + return p.runCommand(templateCmdStr) +} + +// Run custom command on the project +func (p *Project) runCommand(templateCmdStr string) error { + cmdObj := CommandObject{} + if p.IsProfile { + cmdObj.Profile = p.Name + } + + command := utils.ApplyTemplate( + templateCmdStr, + p.DockerCommand.NewCommandObject(cmdObj), + ) + // log command + return p.OSCommand.RunCommand(command) } diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 9c0ad474..f5f74f45 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -166,6 +166,9 @@ type CommandTemplatesConfig struct { // downs and removes volumes DownWithVolumes string `yaml:"downWithVolumes,omitempty"` + // Restarts everything + Restart string `yaml:"restart,omitempty"` + // DockerCompose is for your docker-compose command. You may want to combine a // few different docker-compose.yml files together, in which case you can set // this to "docker compose -f foo/docker-compose.yml -f @@ -202,7 +205,8 @@ type CommandTemplatesConfig struct { // AllLogs is for showing what you get from doing `docker compose logs`. It // combines all the logs together - AllLogs string `yaml:"allLogs,omitempty"` + AllLogs string `yaml:"allLogs,omitempty"` + AllLogsProfile string `yaml:"allLogsProfile,omitempty"` // ViewAllLogs is the command we use when you want to see all logs in a subprocess with no filtering ViewAllLogs string `yaml:"viewAlLogs,omitempty"` @@ -210,7 +214,8 @@ type CommandTemplatesConfig struct { // DockerComposeConfig is the command for viewing the config of your docker // compose. It basically prints out the yaml from your docker-compose.yml // file(s) - DockerComposeConfig string `yaml:"dockerComposeConfig,omitempty"` + DockerComposeConfig string `yaml:"dockerComposeConfig,omitempty"` + DockerComposeProfileConfig string `yaml:"dockerComposeProfileConfig,omitempty"` // CheckDockerComposeConfig is what we use to check whether we are in a // docker-compose context. If the command returns an error then we clearly @@ -218,8 +223,22 @@ type CommandTemplatesConfig struct { // and only show containers CheckDockerComposeConfig string `yaml:"checkDockerComposeConfig,omitempty"` + // CheckDockerComposeProfiles is what we use to check whether we are in a + // docker-compose context with profiles. + CheckDockerComposeProfiles string `yaml:"checkDockerComposeProfiles,omitempty"` + // ServiceTop is the command for viewing the processes under a given service ServiceTop string `yaml:"serviceTop,omitempty"` + + // UpProfile is the command for starting a project + UpProfile string `yaml:"upProfile,omitempty"` + + // DownProfile is the command for stopping a project with a given profile + DownProfile string `yaml:"downProfile,omitempty"` + DownProfileWithVolumes string `yaml:"downProfileWithVolumes,omitempty"` + + // RestartProfile is the command for restarting a project with a given profile + RestartProfile string `yaml:"restartProfile,omitempty"` } // OSConfig contains config on the level of the os @@ -385,23 +404,31 @@ func GetDefaultConfig() UserConfig { Tail: "", }, CommandTemplates: CommandTemplatesConfig{ - DockerCompose: "docker compose", - RestartService: "{{ .DockerCompose }} restart {{ .Service.Name }}", - StartService: "{{ .DockerCompose }} start {{ .Service.Name }}", - Up: "{{ .DockerCompose }} up -d", - Down: "{{ .DockerCompose }} down", - DownWithVolumes: "{{ .DockerCompose }} down --volumes", - UpService: "{{ .DockerCompose }} up -d {{ .Service.Name }}", - RebuildService: "{{ .DockerCompose }} up -d --build {{ .Service.Name }}", - RecreateService: "{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}", - StopService: "{{ .DockerCompose }} stop {{ .Service.Name }}", - ServiceLogs: "{{ .DockerCompose }} logs --since=60m --follow {{ .Service.Name }}", - ViewServiceLogs: "{{ .DockerCompose }} logs --follow {{ .Service.Name }}", - AllLogs: "{{ .DockerCompose }} logs --tail=300 --follow", - ViewAllLogs: "{{ .DockerCompose }} logs", - DockerComposeConfig: "{{ .DockerCompose }} config", - CheckDockerComposeConfig: "{{ .DockerCompose }} config --quiet", - ServiceTop: "{{ .DockerCompose }} top {{ .Service.Name }}", + DockerCompose: "docker compose", + RestartService: "{{ .DockerCompose }} restart {{ .Service.Name }}", + StartService: "{{ .DockerCompose }} start {{ .Service.Name }}", + Up: "{{ .DockerCompose }} up -d", + Down: "{{ .DockerCompose }} down", + DownWithVolumes: "{{ .DockerCompose }} down --volumes", + UpService: "{{ .DockerCompose }} up -d {{ .Service.Name }}", + UpProfile: "{{ .DockerCompose }} --profile {{ .Profile }} up -d", + DownProfile: "{{ .DockerCompose }} --profile {{ .Profile }} down", + DownProfileWithVolumes: "{{ .DockerCompose }} --profile {{ .Profile }} down --volumes", + Restart: "{{ .DockerCompose }} restart", + RestartProfile: "{{ .DockerCompose }} --profile {{ .Profile }} restart", + RebuildService: "{{ .DockerCompose }} up -d --build {{ .Service.Name }}", + RecreateService: "{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}", + StopService: "{{ .DockerCompose }} stop {{ .Service.Name }}", + ServiceLogs: "{{ .DockerCompose }} logs --since=60m --follow {{ .Service.Name }}", + ViewServiceLogs: "{{ .DockerCompose }} logs --follow {{ .Service.Name }}", + AllLogs: "{{ .DockerCompose }} logs --tail=300 --follow", + AllLogsProfile: "{{ .DockerCompose }} --profile {{ .Profile }} logs --tail=300 --follow", + ViewAllLogs: "{{ .DockerCompose }} logs", + DockerComposeConfig: "{{ .DockerCompose }} config", + DockerComposeProfileConfig: "{{ .DockerCompose }} --profile {{ .Profile }} config", + CheckDockerComposeConfig: "{{ .DockerCompose }} config --quiet", + CheckDockerComposeProfiles: "{{ .DockerCompose }} config --profiles --quiet | grep -q '[^[:space:]]'; echo $?", + ServiceTop: "{{ .DockerCompose }} top {{ .Service.Name }}", }, CustomCommands: CustomCommands{ Containers: []CustomCommand{}, diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index df706024..3d2ff6ec 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -149,6 +149,55 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleViewAllLogs, Description: gui.Tr.ViewLogs, }, + { + ViewName: "project", + Key: 'U', + Modifier: gocui.ModNone, + Handler: gui.handleProjectUpMenu, + Description: gui.Tr.UpProject, + }, + { + ViewName: "project", + Key: 'D', + Modifier: gocui.ModNone, + Handler: gui.handleProjectDownMenu, + Description: gui.Tr.DownProject, + }, + { + ViewName: "project", + Key: 'R', + Modifier: gocui.ModNone, + Handler: gui.handleProjectRestartMenu, + Description: gui.Tr.RestartProject, + }, + { + ViewName: "project", + Key: 'u', + Modifier: gocui.ModNone, + Handler: gui.handleProjectUp, + Description: gui.Tr.UpProfile, + }, + { + ViewName: "project", + Key: 'd', + Modifier: gocui.ModNone, + Handler: gui.handleProjectDown, + Description: gui.Tr.DownProfile, + }, + { + ViewName: "project", + Key: 'r', + Modifier: gocui.ModNone, + Handler: gui.handleProjectRestart, + Description: gui.Tr.RestartProfile, + }, + // { + // ViewName: "project", + // Key: 'b', + // Modifier: gocui.ModNone, + // Handler: gui.handleServicesBulkCommand, + // Description: gui.Tr.ViewBulkCommands, + // }, { ViewName: "menu", Key: gocui.KeyEsc, @@ -322,14 +371,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { ViewName: "services", Key: 'U', Modifier: gocui.ModNone, - Handler: gui.handleProjectUp, + Handler: gui.handleProjectUpMenu, Description: gui.Tr.UpProject, }, { ViewName: "services", Key: 'D', Modifier: gocui.ModNone, - Handler: gui.handleProjectDown, + Handler: gui.handleProjectDownMenu, Description: gui.Tr.DownProject, }, { diff --git a/pkg/gui/presentation/projects.go b/pkg/gui/presentation/projects.go index 44d396c9..78d10cba 100644 --- a/pkg/gui/presentation/projects.go +++ b/pkg/gui/presentation/projects.go @@ -1,7 +1,23 @@ package presentation -import "github.com/jesseduffield/lazydocker/pkg/commands" +import ( + "github.com/fatih/color" + "github.com/jesseduffield/lazydocker/pkg/commands" + "github.com/jesseduffield/lazydocker/pkg/utils" +) func GetProjectDisplayStrings(project *commands.Project) []string { - return []string{project.Name} + if project.IsProfile { + // show "profile" word in aqua + return []string{ + utils.ColoredString("profile", color.FgCyan), + "", + project.Name, + } + } + return []string{ + "project", + "", + project.Name, + } } diff --git a/pkg/gui/project_panel.go b/pkg/gui/project_panel.go index 32c5ad99..db8c2065 100644 --- a/pkg/gui/project_panel.go +++ b/pkg/gui/project_panel.go @@ -3,6 +3,7 @@ package gui import ( "bytes" "context" + "os/exec" "path" "strings" @@ -11,9 +12,11 @@ import ( "github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/gui/panels" "github.com/jesseduffield/lazydocker/pkg/gui/presentation" + "github.com/jesseduffield/lazydocker/pkg/gui/types" "github.com/jesseduffield/lazydocker/pkg/tasks" "github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/yaml" + "github.com/samber/lo" ) // Although at the moment we'll only have one project, in future we could have @@ -23,33 +26,31 @@ 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 { - return []panels.MainTab[*commands.Project]{ - { - Key: "logs", - Title: gui.Tr.LogsTitle, - Render: gui.renderAllLogs, - }, - { - Key: "config", - Title: gui.Tr.DockerComposeConfigTitle, - Render: gui.renderDockerComposeConfig, - }, - { - Key: "credits", - Title: gui.Tr.CreditsTitle, - Render: gui.renderCredits, - }, - } - } - - return []panels.MainTab[*commands.Project]{ + tabsList := []panels.MainTab[*commands.Project]{ { Key: "credits", Title: gui.Tr.CreditsTitle, Render: gui.renderCredits, }, } + + if gui.DockerCommand.InDockerComposeProject { + tabsListCompose := []panels.MainTab[*commands.Project]{ + { + Key: "logs", + Title: gui.Tr.LogsTitle, + Render: gui.renderLogs, + }, + { + Key: "config", + Title: gui.Tr.DockerComposeConfigTitle, + Render: gui.renderDockerComposeConfig, + }, + } + tabsList = append(tabsListCompose, tabsList...) + } + + return tabsList }, GetItemContextCacheKey: func(project *commands.Project) string { return "projects-" + project.Name @@ -67,13 +68,36 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] { return false }, 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()}}) + projectName := gui.getProjectName() + profiles, err := gui.DockerCommand.GetProfiles() + if err != nil { + return err + } + + gui.DockerCommand.HasProfiles = len(profiles) > 0 + items := []*commands.Project{{ + Name: projectName, + IsProfile: false, + Config: gui.Config, + OSCommand: gui.OSCommand, + DockerCommand: gui.DockerCommand, + }} + for _, profile := range profiles { + items = append(items, &commands.Project{ + Name: profile, + IsProfile: true, + Config: gui.Config, + OSCommand: gui.OSCommand, + DockerCommand: gui.DockerCommand, + }) + } + + gui.Panels.Projects.SetItems(items) return gui.Panels.Projects.RerenderList() } @@ -112,19 +136,29 @@ func (gui *Gui) creditsStr() string { }, "\n\n") } -func (gui *Gui) renderAllLogs(_project *commands.Project) tasks.TaskFunc { +func (gui *Gui) renderLogs(_project *commands.Project) tasks.TaskFunc { return gui.NewTask(TaskOpts{ Autoscroll: true, Wrap: gui.Config.UserConfig.Gui.WrapMainPanel, Func: func(ctx context.Context) { gui.clearMainView() - cmd := gui.OSCommand.RunCustomCommand( - utils.ApplyTemplate( - gui.Config.UserConfig.CommandTemplates.AllLogs, - gui.DockerCommand.NewCommandObject(commands.CommandObject{}), - ), - ) + var cmd *exec.Cmd + if _project.IsProfile { + cmd = gui.OSCommand.RunCustomCommand( + utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.AllLogsProfile, + gui.DockerCommand.NewCommandObject(commands.CommandObject{Profile: _project.Name}), + ), + ) + } else { + cmd = gui.OSCommand.RunCustomCommand( + utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.AllLogs, + gui.DockerCommand.NewCommandObject(commands.CommandObject{}), + ), + ) + } cmd.Stdout = gui.Views.Main cmd.Stderr = gui.Views.Main @@ -145,6 +179,11 @@ func (gui *Gui) renderAllLogs(_project *commands.Project) tasks.TaskFunc { } func (gui *Gui) renderDockerComposeConfig(_project *commands.Project) tasks.TaskFunc { + if _project.IsProfile { + return gui.NewSimpleRenderStringTask(func() string { + return utils.ColoredYamlString(gui.DockerCommand.DockerComposeProfileConfig(_project.Name)) + }) + } return gui.NewSimpleRenderStringTask(func() string { return utils.ColoredYamlString(gui.DockerCommand.DockerComposeConfig()) }) @@ -180,3 +219,312 @@ func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error { return gui.runSubprocess(c) } + +func (gui *Gui) handleProjectUpMenu(g *gocui.Gui, v *gocui.View) error { + upCommand := utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.Up, + gui.DockerCommand.NewCommandObject(commands.CommandObject{}), + ) + options := []*commandOption{ + { + description: gui.Tr.UpProject, + command: upCommand, + onPress: func() error { + return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error { + if err := gui.OSCommand.RunCommand(upCommand); err != nil { + return gui.createErrorPanel(err.Error()) + } + return nil + }) + }, + }, + } + + if gui.DockerCommand.HasProfiles { + upAllProfilesCommand := utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.UpProfile, + gui.DockerCommand.NewCommandObject(commands.CommandObject{Profile: "*"}), + ) + options = append(options, &commandOption{ + description: gui.Tr.UpAllProfiles, + command: upAllProfilesCommand, + onPress: func() error { + return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error { + if err := gui.OSCommand.RunCommand(upAllProfilesCommand); err != nil { + return gui.createErrorPanel(err.Error()) + } + return nil + }) + }, + }) + + profile, err := gui.Panels.Projects.GetSelectedItem() + if err == nil && profile.IsProfile { + upProfileCommand := utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.UpProfile, + gui.DockerCommand.NewCommandObject(commands.CommandObject{Profile: profile.Name}), + ) + options = append(options, &commandOption{ + description: gui.Tr.UpProfile, + command: upProfileCommand, + onPress: func() error { + return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error { + if err := gui.OSCommand.RunCommand(upProfileCommand); err != nil { + return gui.createErrorPanel(err.Error()) + } + return nil + }) + }, + }) + } + } + + menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem { + return &types.MenuItem{ + LabelColumns: option.getDisplayStrings(), + OnPress: option.onPress, + } + }) + + return gui.Menu(CreateMenuOptions{ + Title: "", + Items: menuItems, + }) +} + +func (gui *Gui) handleProjectDownMenu(g *gocui.Gui, v *gocui.View) error { + downCommand := utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.Down, + gui.DockerCommand.NewCommandObject(commands.CommandObject{}), + ) + + downWithVolumesCommand := utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.DownWithVolumes, + gui.DockerCommand.NewCommandObject(commands.CommandObject{}), + ) + + options := []*commandOption{ + { + description: gui.Tr.Down, + command: downCommand, + onPress: func() error { + return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error { + if err := gui.OSCommand.RunCommand(downCommand); err != nil { + return gui.createErrorPanel(err.Error()) + } + return nil + }) + }, + }, + { + description: gui.Tr.DownWithVolumes, + command: downWithVolumesCommand, + onPress: func() error { + return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error { + if err := gui.OSCommand.RunCommand(downWithVolumesCommand); err != nil { + return gui.createErrorPanel(err.Error()) + } + return nil + }) + }, + }, + } + + if gui.DockerCommand.HasProfiles { + downProfileCommand := utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.DownProfile, + gui.DockerCommand.NewCommandObject(commands.CommandObject{Profile: "*"}), + ) + downProfileWithVolumesCommand := utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.DownProfileWithVolumes, + gui.DockerCommand.NewCommandObject(commands.CommandObject{Profile: "*"}), + ) + options = append(options, &commandOption{ + description: gui.Tr.DownAllProfiles, + command: downProfileCommand, + onPress: func() error { + return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error { + if err := gui.OSCommand.RunCommand(downProfileCommand); err != nil { + return gui.createErrorPanel(err.Error()) + } + return nil + }) + }, + }, &commandOption{ + description: gui.Tr.DownAllProfilesWithVolumes, + command: downProfileWithVolumesCommand, + onPress: func() error { + return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error { + if err := gui.OSCommand.RunCommand(downProfileWithVolumesCommand); err != nil { + return gui.createErrorPanel(err.Error()) + } + return nil + }) + }, + }) + + profile, err := gui.Panels.Projects.GetSelectedItem() + if err == nil && profile.IsProfile { + downProfileCommand := utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.DownProfile, + gui.DockerCommand.NewCommandObject(commands.CommandObject{Profile: profile.Name}), + ) + downProfileWithVolumesCommand := utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.DownProfileWithVolumes, + gui.DockerCommand.NewCommandObject(commands.CommandObject{Profile: profile.Name}), + ) + options = append(options, &commandOption{ + description: gui.Tr.DownProfile, + command: downProfileCommand, + onPress: func() error { + return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error { + if err := gui.OSCommand.RunCommand(downProfileCommand); err != nil { + return gui.createErrorPanel(err.Error()) + } + return nil + }) + }, + }, &commandOption{ + description: gui.Tr.DownProfileWithVolumes, + command: downProfileWithVolumesCommand, + onPress: func() error { + return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error { + if err := gui.OSCommand.RunCommand(downProfileWithVolumesCommand); err != nil { + return gui.createErrorPanel(err.Error()) + } + return nil + }) + }, + }) + } + } + + menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem { + return &types.MenuItem{ + LabelColumns: option.getDisplayStrings(), + OnPress: option.onPress, + } + }) + + return gui.Menu(CreateMenuOptions{ + Title: "", + Items: menuItems, + }) +} + +func (gui *Gui) handleProjectRestartMenu(g *gocui.Gui, v *gocui.View) error { + restartCommand := utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.Restart, + gui.DockerCommand.NewCommandObject(commands.CommandObject{}), + ) + options := []*commandOption{ + { + description: gui.Tr.Restart, + command: restartCommand, + onPress: func() error { + return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { + if err := gui.OSCommand.RunCommand(restartCommand); err != nil { + return gui.createErrorPanel(err.Error()) + } + return nil + }) + }, + }, + } + + if gui.DockerCommand.HasProfiles { + restartProfileCommand := utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.RestartProfile, + gui.DockerCommand.NewCommandObject(commands.CommandObject{Profile: "*"}), + ) + options = append(options, &commandOption{ + description: gui.Tr.Restart, + command: restartProfileCommand, + onPress: func() error { + return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { + if err := gui.OSCommand.RunCommand(restartProfileCommand); err != nil { + return gui.createErrorPanel(err.Error()) + } + return nil + }) + }, + }) + + profile, err := gui.Panels.Projects.GetSelectedItem() + if err == nil && profile.IsProfile { + restartProfileCommand := utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.RestartProfile, + gui.DockerCommand.NewCommandObject(commands.CommandObject{Profile: profile.Name}), + ) + options = append(options, &commandOption{ + description: gui.Tr.Restart, + command: restartProfileCommand, + onPress: func() error { + return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { + if err := gui.OSCommand.RunCommand(restartProfileCommand); err != nil { + return gui.createErrorPanel(err.Error()) + } + return nil + }) + }, + }) + } + } + + menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem { + return &types.MenuItem{ + LabelColumns: option.getDisplayStrings(), + OnPress: option.onPress, + } + }) + + return gui.Menu(CreateMenuOptions{ + Title: "", + Items: menuItems, + }) +} + +func (gui *Gui) handleProjectUp(g *gocui.Gui, v *gocui.View) error { + project, err := gui.Panels.Projects.GetSelectedItem() + if err != nil { + return nil + } + + return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error { + if err := project.Up(); err != nil { + return gui.createErrorPanel(err.Error()) + } + + return nil + }) +} + +func (gui *Gui) handleProjectDown(g *gocui.Gui, v *gocui.View) error { + project, err := gui.Panels.Projects.GetSelectedItem() + if err != nil { + return nil + } + + return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error { + if err := project.Down(); err != nil { + return gui.createErrorPanel(err.Error()) + } + + return nil + }) +} + +func (gui *Gui) handleProjectRestart(g *gocui.Gui, v *gocui.View) error { + project, err := gui.Panels.Projects.GetSelectedItem() + if err != nil { + return nil + } + + return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { + if err := project.Restart(); err != nil { + return gui.createErrorPanel(err.Error()) + } + + return nil + }) +} diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index c9dd4d1d..93e9da0f 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -288,73 +288,6 @@ func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error return gui.runSubprocess(c) } -func (gui *Gui) handleProjectUp(g *gocui.Gui, v *gocui.View) error { - return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmUpProject, func(g *gocui.Gui, v *gocui.View) error { - cmdStr := utils.ApplyTemplate( - gui.Config.UserConfig.CommandTemplates.Up, - gui.DockerCommand.NewCommandObject(commands.CommandObject{}), - ) - - return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error { - if err := gui.OSCommand.RunCommand(cmdStr); err != nil { - return gui.createErrorPanel(err.Error()) - } - return nil - }) - }, nil) -} - -func (gui *Gui) handleProjectDown(g *gocui.Gui, v *gocui.View) error { - downCommand := utils.ApplyTemplate( - gui.Config.UserConfig.CommandTemplates.Down, - gui.DockerCommand.NewCommandObject(commands.CommandObject{}), - ) - - downWithVolumesCommand := utils.ApplyTemplate( - gui.Config.UserConfig.CommandTemplates.DownWithVolumes, - gui.DockerCommand.NewCommandObject(commands.CommandObject{}), - ) - - options := []*commandOption{ - { - description: gui.Tr.Down, - command: downCommand, - onPress: func() error { - return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error { - if err := gui.OSCommand.RunCommand(downCommand); err != nil { - return gui.createErrorPanel(err.Error()) - } - return nil - }) - }, - }, - { - description: gui.Tr.DownWithVolumes, - command: downWithVolumesCommand, - onPress: func() error { - return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error { - if err := gui.OSCommand.RunCommand(downWithVolumesCommand); err != nil { - return gui.createErrorPanel(err.Error()) - } - return nil - }) - }, - }, - } - - menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem { - return &types.MenuItem{ - LabelColumns: option.getDisplayStrings(), - OnPress: option.onPress, - } - }) - - return gui.Menu(CreateMenuOptions{ - Title: "", - Items: menuItems, - }) -} - func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { service, err := gui.Panels.Services.GetSelectedItem() if err != nil { diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 3de6d61a..e26b0025 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -118,6 +118,8 @@ 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.Highlight = true + gui.Views.Project.SelBgColor = selectedLineBgColor gui.Views.Project.Title = gui.Tr.ProjectTitle gui.Views.Project.TitlePrefix = "[1]" diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index d1ed0320..17c4ec65 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -67,6 +67,14 @@ type TranslationSet struct { ViewLogs string UpProject string DownProject string + RestartProject string + UpProfile string + DownProfile string + DownProfileWithVolumes string + RestartProfile string + UpAllProfiles string + DownAllProfiles string + DownAllProfilesWithVolumes string ServicesTitle string ContainersTitle string StandaloneContainersTitle string @@ -197,6 +205,14 @@ func englishSet() TranslationSet { ViewLogs: "view logs", UpProject: "up project", DownProject: "down project", + RestartProject: "restart project", + UpProfile: "up profile", + DownProfile: "down profile", + DownProfileWithVolumes: "down profile with volumes", + RestartProfile: "restart profile", + UpAllProfiles: "up all profiles", + DownAllProfiles: "down all profiles", + DownAllProfilesWithVolumes: "down all profiles with volumes", RemoveImage: "remove image", RemoveVolume: "remove volume", RemoveNetwork: "remove network",