From 9ad50028f82a58c36f304df1aa58c06c962f0f6d Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 15 Feb 2025 13:55:07 +0100 Subject: [PATCH 1/7] Fix wrong comment This was backwards; we renamed Sha to Hash, so Sha is now deprecated, not Hash. --- pkg/gui/services/custom_commands/models.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/gui/services/custom_commands/models.go b/pkg/gui/services/custom_commands/models.go index 261bace45..eee52ff5d 100644 --- a/pkg/gui/services/custom_commands/models.go +++ b/pkg/gui/services/custom_commands/models.go @@ -14,8 +14,8 @@ import ( // compatibility. We already did this for Commit.Sha, which was renamed to Hash. type Commit struct { - Hash string // deprecated: use Sha - Sha string + Hash string + Sha string // deprecated: use Hash Name string Status models.CommitStatus Action todo.TodoCommand From 7238e41251be2309f2243f042d2f4e1b5bb0ca52 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 16 Feb 2025 18:41:43 +0100 Subject: [PATCH 2/7] Fix a small mistake in Custom_Command_Keybindings.md It has been 'e' instead of 'o' for quite a while now. --- docs/Custom_Command_Keybindings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Custom_Command_Keybindings.md b/docs/Custom_Command_Keybindings.md index b940a7bb3..c9959aa1d 100644 --- a/docs/Custom_Command_Keybindings.md +++ b/docs/Custom_Command_Keybindings.md @@ -1,6 +1,6 @@ # Custom Command Keybindings -You can add custom command keybindings in your config.yml (accessible by pressing 'o' on the status panel from within lazygit) like so: +You can add custom command keybindings in your config.yml (accessible by pressing 'e' on the status panel from within lazygit) like so: ```yml customCommands: From 0df5e088284c4f722904c423de631ba5bb2da2ca Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 24 Feb 2025 08:44:30 +0100 Subject: [PATCH 3/7] Don't set showOutput explicitly It is false by default. This way there's one less change to make in the next commit. --- pkg/integration/tests/custom_commands/global_context.go | 7 +++---- pkg/integration/tests/custom_commands/multiple_contexts.go | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pkg/integration/tests/custom_commands/global_context.go b/pkg/integration/tests/custom_commands/global_context.go index 9f975d04c..82ef53010 100644 --- a/pkg/integration/tests/custom_commands/global_context.go +++ b/pkg/integration/tests/custom_commands/global_context.go @@ -15,10 +15,9 @@ var GlobalContext = NewIntegrationTest(NewIntegrationTestArgs{ SetupConfig: func(cfg *config.AppConfig) { cfg.GetUserConfig().CustomCommands = []config.CustomCommand{ { - Key: "X", - Context: "global", - Command: "touch myfile", - ShowOutput: false, + Key: "X", + Context: "global", + Command: "touch myfile", }, } }, diff --git a/pkg/integration/tests/custom_commands/multiple_contexts.go b/pkg/integration/tests/custom_commands/multiple_contexts.go index a5c982ee3..61d46775b 100644 --- a/pkg/integration/tests/custom_commands/multiple_contexts.go +++ b/pkg/integration/tests/custom_commands/multiple_contexts.go @@ -15,10 +15,9 @@ var MultipleContexts = NewIntegrationTest(NewIntegrationTestArgs{ SetupConfig: func(cfg *config.AppConfig) { cfg.GetUserConfig().CustomCommands = []config.CustomCommand{ { - Key: "X", - Context: "commits, reflogCommits", - Command: "touch myfile", - ShowOutput: false, + Key: "X", + Context: "commits, reflogCommits", + Command: "touch myfile", }, } }, From f93948cb23b10ff11e7909a6f2ac250163e50a2c Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 24 Feb 2025 08:46:55 +0100 Subject: [PATCH 4/7] Change customCommand fields to pointers This allows us to tell whether they appear in the user's config file, which we will need later in this branch. --- pkg/config/user_config.go | 12 ++++++++---- pkg/gui/services/custom_commands/handler_creator.go | 8 ++++---- .../tests/custom_commands/check_for_conflicts.go | 2 +- .../tests/custom_commands/show_output_in_panel.go | 5 +++-- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 116f63487..a1bbe09a6 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -619,7 +619,8 @@ type CustomCommand struct { // The command to run (using Go template syntax for placeholder values) Command string `yaml:"command" jsonschema:"example=git fetch {{.Form.Remote}} {{.Form.Branch}} && git checkout FETCH_HEAD"` // If true, run the command in a subprocess (e.g. if the command requires user input) - Subprocess bool `yaml:"subprocess"` + // [dev] Pointer to bool so that we can distinguish unset (nil) from false. + Subprocess *bool `yaml:"subprocess"` // A list of prompts that will request user input before running the final command Prompts []CustomCommandPrompt `yaml:"prompts"` // Text to display while waiting for command to finish @@ -627,13 +628,16 @@ type CustomCommand struct { // Label for the custom command when displayed in the keybindings menu Description string `yaml:"description"` // If true, stream the command's output to the Command Log panel - Stream bool `yaml:"stream"` + // [dev] Pointer to bool so that we can distinguish unset (nil) from false. + Stream *bool `yaml:"stream"` // If true, show the command's output in a popup within Lazygit - ShowOutput bool `yaml:"showOutput"` + // [dev] Pointer to bool so that we can distinguish unset (nil) from false. + ShowOutput *bool `yaml:"showOutput"` // The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title. OutputTitle string `yaml:"outputTitle"` // Actions to take after the command has completed - After CustomCommandAfterHook `yaml:"after"` + // [dev] Pointer so that we can tell whether it appears in the config file + After *CustomCommandAfterHook `yaml:"after"` } type CustomCommandPrompt struct { diff --git a/pkg/gui/services/custom_commands/handler_creator.go b/pkg/gui/services/custom_commands/handler_creator.go index 95de40a2e..9e17ca9fa 100644 --- a/pkg/gui/services/custom_commands/handler_creator.go +++ b/pkg/gui/services/custom_commands/handler_creator.go @@ -261,7 +261,7 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses cmdObj := self.c.OS().Cmd.NewShell(cmdStr) - if customCommand.Subprocess { + if customCommand.Subprocess != nil && *customCommand.Subprocess { return self.c.RunSubprocessAndRefresh(cmdObj) } @@ -273,7 +273,7 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses return self.c.WithWaitingStatus(loadingText, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.CustomCommand) - if customCommand.Stream { + if customCommand.Stream != nil && *customCommand.Stream { cmdObj.StreamOutput() } output, err := cmdObj.RunWithOutput() @@ -283,14 +283,14 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses } if err != nil { - if customCommand.After.CheckForConflicts { + if customCommand.After != nil && customCommand.After.CheckForConflicts { return self.mergeAndRebaseHelper.CheckForConflicts(err) } return err } - if customCommand.ShowOutput { + if customCommand.ShowOutput != nil && *customCommand.ShowOutput { if strings.TrimSpace(output) == "" { output = self.c.Tr.EmptyOutput } diff --git a/pkg/integration/tests/custom_commands/check_for_conflicts.go b/pkg/integration/tests/custom_commands/check_for_conflicts.go index 024f32733..25f1d2097 100644 --- a/pkg/integration/tests/custom_commands/check_for_conflicts.go +++ b/pkg/integration/tests/custom_commands/check_for_conflicts.go @@ -19,7 +19,7 @@ var CheckForConflicts = NewIntegrationTest(NewIntegrationTestArgs{ Key: "m", Context: "localBranches", Command: "git merge {{ .SelectedLocalBranch.Name | quote }}", - After: config.CustomCommandAfterHook{ + After: &config.CustomCommandAfterHook{ CheckForConflicts: true, }, }, diff --git a/pkg/integration/tests/custom_commands/show_output_in_panel.go b/pkg/integration/tests/custom_commands/show_output_in_panel.go index 171a477d8..7f00d8a5b 100644 --- a/pkg/integration/tests/custom_commands/show_output_in_panel.go +++ b/pkg/integration/tests/custom_commands/show_output_in_panel.go @@ -15,18 +15,19 @@ var ShowOutputInPanel = NewIntegrationTest(NewIntegrationTestArgs{ shell.EmptyCommit("my change") }, SetupConfig: func(cfg *config.AppConfig) { + trueVal := true cfg.GetUserConfig().CustomCommands = []config.CustomCommand{ { Key: "X", Context: "commits", Command: "printf '%s' '{{ .SelectedLocalCommit.Name }}'", - ShowOutput: true, + ShowOutput: &trueVal, }, { Key: "Y", Context: "commits", Command: "printf '%s' '{{ .SelectedLocalCommit.Name }}'", - ShowOutput: true, + ShowOutput: &trueVal, OutputTitle: "Subject of commit {{ .SelectedLocalCommit.Hash }}", }, } From e799976b8a5678db6adf1273960eeef44bd701c9 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 15 Feb 2025 16:19:19 +0100 Subject: [PATCH 5/7] Extract a method CustomCommand.GetDescription We'll reuse it in the next commit. --- pkg/config/user_config.go | 8 ++++++++ pkg/gui/services/custom_commands/keybinding_creator.go | 7 +------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index a1bbe09a6..c4db37b54 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -640,6 +640,14 @@ type CustomCommand struct { After *CustomCommandAfterHook `yaml:"after"` } +func (c *CustomCommand) GetDescription() string { + if c.Description != "" { + return c.Description + } + + return c.Command +} + type CustomCommandPrompt struct { // One of: 'input' | 'menu' | 'confirm' | 'menuFromCommand' Type string `yaml:"type"` diff --git a/pkg/gui/services/custom_commands/keybinding_creator.go b/pkg/gui/services/custom_commands/keybinding_creator.go index 4e6f9d8c7..259954c17 100644 --- a/pkg/gui/services/custom_commands/keybinding_creator.go +++ b/pkg/gui/services/custom_commands/keybinding_creator.go @@ -34,18 +34,13 @@ func (self *KeybindingCreator) call(customCommand config.CustomCommand, handler return nil, err } - description := customCommand.Description - if description == "" { - description = customCommand.Command - } - return lo.Map(viewNames, func(viewName string, _ int) *types.Binding { return &types.Binding{ ViewName: viewName, Key: keybindings.GetKey(customCommand.Key), Modifier: gocui.ModNone, Handler: handler, - Description: description, + Description: customCommand.GetDescription(), } }), nil } From 22512d55a87ba8e9024c2a5d573da54a5886a31f Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 20 Feb 2025 10:25:14 +0100 Subject: [PATCH 6/7] Add commandMenu property to custom commands --- docs/Custom_Command_Keybindings.md | 21 +++++ pkg/config/user_config.go | 3 + pkg/gui/services/custom_commands/client.go | 88 +++++++++++++++++-- pkg/i18n/english.go | 4 + .../custom_commands_submenu.go | 75 ++++++++++++++++ pkg/integration/tests/test_list.go | 1 + schema/config.json | 7 ++ 7 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 pkg/integration/tests/custom_commands/custom_commands_submenu.go diff --git a/docs/Custom_Command_Keybindings.md b/docs/Custom_Command_Keybindings.md index c9959aa1d..265e86eb9 100644 --- a/docs/Custom_Command_Keybindings.md +++ b/docs/Custom_Command_Keybindings.md @@ -324,6 +324,27 @@ We don't support accessing all elements of a range selection yet. We might add t If your custom keybinding collides with an inbuilt keybinding that is defined for the same context, only the custom keybinding will be executed. This also applies to the global context. However, one caveat is that if you have a custom keybinding defined on the global context for some key, and there is an in-built keybinding defined for the same key and for a specific context (say the 'files' context), then the in-built keybinding will take precedence. See how to change in-built keybindings [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#keybindings) +## Menus of custom commands + +For custom commands that are not used very frequently it may be preferable to hide them in a menu; you can assign a key to open the menu, and the commands will appear inside. This has the advantage that you don't have to come up with individual unique keybindings for all those commands that you don't use often; the keybindings for the commands in the menu only need to be unique within the menu. Here is an example: + +```yml +customCommands: +- key: X + description: "Copy/paste commits across repos" + commandMenu: + - key: c + command: 'git format-patch --stdout {{.SelectedCommitRange.From}}^..{{.SelectedCommitRange.To}} | pbcopy' + context: commits, subCommits + description: "Copy selected commits to clipboard" + - key: v + command: 'pbpaste | git am' + context: "commits" + description: "Paste selected commits from clipboard" +``` + +If you use the commandMenu property, none of the other properties except key and description can be used. + ## Debugging If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `showOutput: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved. diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index c4db37b54..77c7d4ead 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -614,6 +614,9 @@ type CustomCommandAfterHook struct { type CustomCommand struct { // The key to trigger the command. Use a single letter or one of the values from https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md Key string `yaml:"key"` + // Instead of defining a single custom command, create a menu of custom commands. Useful for grouping related commands together under a single keybinding, and for keeping them out of the global keybindings menu. + // When using this, all other fields except Key and Description are ignored and must be empty. + CommandMenu []CustomCommand `yaml:"commandMenu"` // The context in which to listen for the key. Valid values are: status, files, worktrees, localBranches, remotes, remoteBranches, tags, commits, reflogCommits, subCommits, commitFiles, stash, and global. Multiple contexts separated by comma are allowed; most useful for "commits, subCommits" or "files, commitFiles". Context string `yaml:"context" jsonschema:"example=status,example=files,example=worktrees,example=localBranches,example=remotes,example=remoteBranches,example=tags,example=commits,example=reflogCommits,example=subCommits,example=commitFiles,example=stash,example=global"` // The command to run (using Go template syntax for placeholder values) diff --git a/pkg/gui/services/custom_commands/client.go b/pkg/gui/services/custom_commands/client.go index 6cb1bf19c..12d8c7862 100644 --- a/pkg/gui/services/custom_commands/client.go +++ b/pkg/gui/services/custom_commands/client.go @@ -1,15 +1,19 @@ package custom_commands import ( - "github.com/jesseduffield/lazygit/pkg/common" + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" + "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/samber/lo" ) // Client is the entry point to this package. It returns a list of keybindings based on the config's user-defined custom commands. // See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Command_Keybindings.md for more info. type Client struct { - c *common.Common + c *helpers.HelperCommon handlerCreator *HandlerCreator keybindingCreator *KeybindingCreator } @@ -28,7 +32,7 @@ func NewClient( keybindingCreator := NewKeybindingCreator(c) return &Client{ - c: c.Common, + c: c, keybindingCreator: keybindingCreator, handlerCreator: handlerCreator, } @@ -37,13 +41,81 @@ func NewClient( func (self *Client) GetCustomCommandKeybindings() ([]*types.Binding, error) { bindings := []*types.Binding{} for _, customCommand := range self.c.UserConfig().CustomCommands { - handler := self.handlerCreator.call(customCommand) - compoundBindings, err := self.keybindingCreator.call(customCommand, handler) - if err != nil { - return nil, err + if len(customCommand.CommandMenu) > 0 { + handler := func() error { + return self.showCustomCommandsMenu(customCommand) + } + bindings = append(bindings, &types.Binding{ + ViewName: "", // custom commands menus are global; we filter the commands inside by context + Key: keybindings.GetKey(customCommand.Key), + Modifier: gocui.ModNone, + Handler: handler, + Description: getCustomCommandsMenuDescription(customCommand, self.c.Tr), + OpensMenu: true, + }) + } else { + handler := self.handlerCreator.call(customCommand) + compoundBindings, err := self.keybindingCreator.call(customCommand, handler) + if err != nil { + return nil, err + } + bindings = append(bindings, compoundBindings...) } - bindings = append(bindings, compoundBindings...) } return bindings, nil } + +func (self *Client) showCustomCommandsMenu(customCommand config.CustomCommand) error { + menuItems := make([]*types.MenuItem, 0, len(customCommand.CommandMenu)) + for _, subCommand := range customCommand.CommandMenu { + if len(subCommand.CommandMenu) > 0 { + handler := func() error { + return self.showCustomCommandsMenu(subCommand) + } + menuItems = append(menuItems, &types.MenuItem{ + Label: subCommand.GetDescription(), + Key: keybindings.GetKey(subCommand.Key), + OnPress: handler, + OpensMenu: true, + }) + } else { + if subCommand.Context != "" && subCommand.Context != "global" { + viewNames, err := self.keybindingCreator.getViewNamesAndContexts(subCommand) + if err != nil { + return err + } + + currentView := self.c.GocuiGui().CurrentView() + enabled := currentView != nil && lo.Contains(viewNames, currentView.Name()) + if !enabled { + continue + } + } + + menuItems = append(menuItems, &types.MenuItem{ + Label: subCommand.GetDescription(), + Key: keybindings.GetKey(subCommand.Key), + OnPress: self.handlerCreator.call(subCommand), + }) + } + } + + if len(menuItems) == 0 { + menuItems = append(menuItems, &types.MenuItem{ + Label: self.c.Tr.NoApplicableCommandsInThisContext, + OnPress: func() error { return nil }, + }) + } + + title := getCustomCommandsMenuDescription(customCommand, self.c.Tr) + return self.c.Menu(types.CreateMenuOptions{Title: title, Items: menuItems, HideCancel: true}) +} + +func getCustomCommandsMenuDescription(customCommand config.CustomCommand, tr *i18n.TranslationSet) string { + if customCommand.Description != "" { + return customCommand.Description + } + + return tr.CustomCommands +} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index f405061b4..81620e47b 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -843,6 +843,8 @@ type TranslationSet struct { RangeSelectNotSupportedForSubmodules string OldCherryPickKeyWarning string CommandDoesNotSupportOpeningInEditor string + CustomCommands string + NoApplicableCommandsInThisContext string Actions Actions Bisect Bisect Log Log @@ -1879,6 +1881,8 @@ func EnglishTranslationSet() *TranslationSet { RangeSelectNotSupportedForSubmodules: "Range select not supported for submodules", OldCherryPickKeyWarning: "The 'c' key is no longer the default key for copying commits to cherry pick. Please use `{{.copy}}` instead (and `{{.paste}}` to paste). The reason for this change is that the 'v' key for selecting a range of lines when staging is now also used for selecting a range of lines in any list view, meaning that we needed to find a new key for pasting commits, and if we're going to now use `{{.paste}}` for pasting commits, we may as well use `{{.copy}}` for copying them. If you want to configure the keybindings to get the old behaviour, set the following in your config:\n\nkeybinding:\n universal:\n toggleRangeSelect: \n commits:\n cherryPickCopy: 'c'\n pasteCommits: 'v'", CommandDoesNotSupportOpeningInEditor: "This command doesn't support switching to the editor", + CustomCommands: "Custom commands", + NoApplicableCommandsInThisContext: "(No applicable commands in this context)", Actions: Actions{ // TODO: combine this with the original keybinding descriptions (those are all in lowercase atm) diff --git a/pkg/integration/tests/custom_commands/custom_commands_submenu.go b/pkg/integration/tests/custom_commands/custom_commands_submenu.go new file mode 100644 index 000000000..45e1fb169 --- /dev/null +++ b/pkg/integration/tests/custom_commands/custom_commands_submenu.go @@ -0,0 +1,75 @@ +package custom_commands + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var CustomCommandsSubmenu = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Using custom commands from a custom commands menu", + ExtraCmdArgs: []string{}, + Skip: false, + SetupRepo: func(shell *Shell) {}, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().CustomCommands = []config.CustomCommand{ + { + Key: "x", + Description: "My Custom Commands", + CommandMenu: []config.CustomCommand{ + { + Key: "1", + Context: "global", + Command: "touch myfile-global", + }, + { + Key: "2", + Context: "files", + Command: "touch myfile-files", + }, + { + Key: "3", + Context: "commits", + Command: "touch myfile-commits", + }, + }, + }, + } + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + Focus(). + IsEmpty(). + Press("x"). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("My Custom Commands")). + Lines( + Contains("1 touch myfile-global"), + Contains("2 touch myfile-files"), + ). + Select(Contains("touch myfile-files")).Confirm() + }). + Lines( + Contains("myfile-files"), + ) + + t.Views().Commits(). + Focus(). + Press("x"). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("My Custom Commands")). + Lines( + Contains("1 touch myfile-global"), + Contains("3 touch myfile-commits"), + ) + t.GlobalPress("3") + }) + + t.Views().Files(). + Lines( + Contains("myfile-commits"), + Contains("myfile-files"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 573daade1..69a9ed5ec 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -140,6 +140,7 @@ var tests = []*components.IntegrationTest{ custom_commands.AccessCommitProperties, custom_commands.BasicCommand, custom_commands.CheckForConflicts, + custom_commands.CustomCommandsSubmenu, custom_commands.FormPrompts, custom_commands.GlobalContext, custom_commands.MenuFromCommand, diff --git a/schema/config.json b/schema/config.json index cbb418667..3a896b9b5 100644 --- a/schema/config.json +++ b/schema/config.json @@ -63,6 +63,13 @@ "type": "string", "description": "The key to trigger the command. Use a single letter or one of the values from https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md" }, + "commandMenu": { + "items": { + "$ref": "#/$defs/CustomCommand" + }, + "type": "array", + "description": "Instead of defining a single custom command, create a menu of custom commands. Useful for grouping related commands together under a single keybinding, and for keeping them out of the global keybindings menu.\nWhen using this, all other fields except Key and Description are ignored and must be empty." + }, "context": { "type": "string", "description": "The context in which to listen for the key. Valid values are: status, files, worktrees, localBranches, remotes, remoteBranches, tags, commits, reflogCommits, subCommits, commitFiles, stash, and global. Multiple contexts separated by comma are allowed; most useful for \"commits, subCommits\" or \"files, commitFiles\".", From df17896de51616e5ef93af29d45de2d675da9388 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 23 Feb 2025 18:19:51 +0100 Subject: [PATCH 7/7] Validate properties of customCommand when commandMenu is used --- pkg/config/user_config_validation.go | 17 ++++++++ pkg/config/user_config_validation_test.go | 52 +++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/pkg/config/user_config_validation.go b/pkg/config/user_config_validation.go index 735c5aad2..2be2f04e9 100644 --- a/pkg/config/user_config_validation.go +++ b/pkg/config/user_config_validation.go @@ -96,6 +96,23 @@ func validateCustomCommands(customCommands []CustomCommand) error { if err := validateCustomCommandKey(customCommand.Key); err != nil { return err } + + if len(customCommand.CommandMenu) > 0 && + (len(customCommand.Context) > 0 || + len(customCommand.Command) > 0 || + customCommand.Subprocess != nil || + len(customCommand.Prompts) > 0 || + len(customCommand.LoadingText) > 0 || + customCommand.Stream != nil || + customCommand.ShowOutput != nil || + len(customCommand.OutputTitle) > 0 || + customCommand.After != nil) { + commandRef := "" + if len(customCommand.Key) > 0 { + commandRef = fmt.Sprintf(" with key '%s'", customCommand.Key) + } + return fmt.Errorf("Error with custom command%s: it is not allowed to use both commandMenu and any of the other fields except key and description.", commandRef) + } } return nil } diff --git a/pkg/config/user_config_validation_test.go b/pkg/config/user_config_validation_test.go index 1d8428019..c02353ea1 100644 --- a/pkg/config/user_config_validation_test.go +++ b/pkg/config/user_config_validation_test.go @@ -74,6 +74,58 @@ func TestUserConfigValidate_enums(t *testing.T) { {value: "invalid_value", valid: false}, }, }, + { + name: "Custom command sub menu", + setup: func(config *UserConfig, _ string) { + config.CustomCommands = []CustomCommand{ + { + Key: "X", + Description: "My Custom Commands", + CommandMenu: []CustomCommand{ + {Key: "1", Command: "echo 'hello'", Context: "global"}, + }, + }, + } + }, + testCases: []testCase{ + {value: "", valid: true}, + }, + }, + { + name: "Custom command sub menu", + setup: func(config *UserConfig, _ string) { + config.CustomCommands = []CustomCommand{ + { + Key: "X", + Context: "global", + CommandMenu: []CustomCommand{ + {Key: "1", Command: "echo 'hello'", Context: "global"}, + }, + }, + } + }, + testCases: []testCase{ + {value: "", valid: false}, + }, + }, + { + name: "Custom command sub menu", + setup: func(config *UserConfig, _ string) { + falseVal := false + config.CustomCommands = []CustomCommand{ + { + Key: "X", + Subprocess: &falseVal, + CommandMenu: []CustomCommand{ + {Key: "1", Command: "echo 'hello'", Context: "global"}, + }, + }, + } + }, + testCases: []testCase{ + {value: "", valid: false}, + }, + }, } for _, s := range scenarios {