diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index e9157606..a106408b 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -82,6 +82,7 @@ type UserConfig struct { Reporting string `yaml:"reporting,omitempty"` ConfirmOnQuit bool `yaml:"confirmOnQuit,omitempty"` CommandTemplates CommandTemplatesConfig `yaml:"commandTemplates,omitempty"` + CustomCommands CustomCommands `yaml:"customCommands,omitempty"` OS OSConfig `yaml:"oS,omitempty"` Update UpdateConfig `yaml:"update,omitempty"` Stats StatsConfig `yaml:"stats,omitempty"` @@ -146,6 +147,16 @@ type StatsConfig struct { MaxDuration time.Duration `yaml:"maxDuration,omitempty"` } +type CustomCommands struct { + Containers []CustomCommand `yaml:"containers,omitempty"` + Services []CustomCommand `yaml:"services,omitempty"` +} + +type CustomCommand struct { + Attach bool + Command string +} + // GetDefaultConfig returns the application default configuration // NOTE: do not default a boolean to true, because false is the boolean zero value and this will be ignored when parsing the user's config func GetDefaultConfig() UserConfig { @@ -178,6 +189,15 @@ func GetDefaultConfig() UserConfig { ViewContainerLogs: "docker logs --timestamps --follow --tail=100 {{ .Container.ID }}", ContainerTTYLogs: "docker logs --follow --tail=100 {{ .Container.ID }}", }, + CustomCommands: CustomCommands{ + Containers: []CustomCommand{ + { + Attach: true, + Command: "docker exec -it {{ .Container.ID }} /bin/bash", + }, + }, + Services: []CustomCommand{}, + }, OS: GetPlatformDefaultConfig(), Update: UpdateConfig{ Method: "never", diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 3450c0e9..33d2ef8d 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -497,3 +497,18 @@ func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error { gui.SubProcess = c return gui.Errors.ErrSubProcess } + +func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error { + container, err := gui.getSelectedContainer() + if err != nil { + return nil + } + + commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{ + Container: container, + }) + + customCommands := gui.Config.UserConfig.CustomCommands.Containers + + return gui.createCustomCommandMenu(customCommands, commandObject) +} diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go new file mode 100644 index 00000000..b9c32e59 --- /dev/null +++ b/pkg/gui/custom_commands.go @@ -0,0 +1,62 @@ +package gui + +import ( + "github.com/jesseduffield/lazydocker/pkg/commands" + "github.com/jesseduffield/lazydocker/pkg/config" + "github.com/jesseduffield/lazydocker/pkg/utils" +) + +type customCommandOption struct { + customCommand config.CustomCommand + description string + command string + runCommand bool + attach bool +} + +// GetDisplayStrings is a function. +func (r *customCommandOption) GetDisplayStrings(isFocused bool) []string { + return []string{r.description} +} + +func (gui *Gui) createCustomCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject) error { + options := make([]*customCommandOption, len(customCommands)+1) + for i, command := range customCommands { + resolvedCommand := utils.ApplyTemplate(command.Command, commandObject) + + options[i] = &customCommandOption{ + customCommand: command, + description: resolvedCommand, + command: resolvedCommand, + runCommand: true, + attach: command.Attach, + } + } + options[len(options)-1] = &customCommandOption{ + description: gui.Tr.Cancel, + runCommand: false, + } + + handleMenuPress := func(index int) error { + option := options[index] + if !option.runCommand { + return nil + } + + // if we have a command for attaching, we attach and return the subprocess error + if option.customCommand.Attach { + cmd := gui.OSCommand.ExecutableFromString(option.command) + gui.SubProcess = cmd + return gui.Errors.ErrSubProcess + } + + return gui.WithWaitingStatus(gui.Tr.RunningCustomCommandStatus, func() error { + if err := gui.OSCommand.RunCommand(option.command); err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + return nil + }) + } + + return gui.createMenu("", options, len(options), handleMenuPress) +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 204f7cc2..81e54a80 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -228,6 +228,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleContainerViewLogs, Description: gui.Tr.ViewLogs, }, + { + ViewName: "containers", + Key: 'c', + Modifier: gocui.ModNone, + Handler: gui.handleContainersCustomCommand, + Description: gui.Tr.RunCustomCommand, + }, { ViewName: "services", Key: 'd', @@ -284,6 +291,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleServiceRestartMenu, Description: gui.Tr.ViewRestartOptions, }, + { + ViewName: "services", + Key: 'c', + Modifier: gocui.ModNone, + Handler: gui.handleServicesCustomCommand, + Description: gui.Tr.RunCustomCommand, + }, { ViewName: "images", Key: '[', diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index bca9d818..8f4368a3 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -344,3 +344,22 @@ func (gui *Gui) createCommandMenu(options []*commandOption, status string) error return gui.createMenu("", options, len(options), handleMenuPress) } + +func (gui *Gui) handleServicesCustomCommand(g *gocui.Gui, v *gocui.View) error { + service, err := gui.getSelectedService() + if err != nil { + return nil + } + + commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{ + Service: service, + Container: service.Container, + }) + + customCommands := gui.Config.UserConfig.CustomCommands.Services + if service.Container != nil { + customCommands = append(customCommands, gui.Config.UserConfig.CustomCommands.Containers...) + } + + return gui.createCustomCommandMenu(customCommands, commandObject) +} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 0a45bafe..0a56bcf0 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -32,6 +32,7 @@ type TranslationSet struct { RestartingStatus string StoppingStatus string RemovingStatus string + RunningCustomCommandStatus string RemoveService string Stop string Restart string @@ -62,6 +63,7 @@ type TranslationSet struct { StopService string PressEnterToReturn string ViewRestartOptions string + RunCustomCommand string LogsTitle string ConfigTitle string @@ -72,10 +74,11 @@ type TranslationSet struct { func englishSet() TranslationSet { return TranslationSet{ - PruningStatus: "pruning", - RemovingStatus: "removing", - RestartingStatus: "restarting", - StoppingStatus: "stopping", + PruningStatus: "pruning", + RemovingStatus: "removing", + RestartingStatus: "restarting", + StoppingStatus: "stopping", + RunningCustomCommandStatus: "running custom command", RunningSubprocess: "running subprocess", NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement", @@ -110,6 +113,7 @@ func englishSet() TranslationSet { PruneVolumes: "prune unused volumes", PruneImages: "prune unused images", ViewRestartOptions: "view restart options", + RunCustomCommand: "run predefined custom command", AnonymousReportingTitle: "Help make lazydocker better", AnonymousReportingPrompt: "Would you like to enable anonymous reporting data to help improve lazydocker? (enter/esc)",