diff --git a/docs-master/Custom_Command_Keybindings.md b/docs-master/Custom_Command_Keybindings.md index cdc4046b7fe..18e37463ea5 100644 --- a/docs-master/Custom_Command_Keybindings.md +++ b/docs-master/Custom_Command_Keybindings.md @@ -192,6 +192,7 @@ The permitted option fields are: | name | The first part of the label | no | | description | The second part of the label | no | | value | the value that will be used in the command | yes | +| key | Keybinding to invoke this menu option without needing to navigate to it. Can be a single letter or one of the values from [here](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md) | no | If an option has no name the value will be displayed to the user in place of the name, so you're allowed to only include the value like so: @@ -233,6 +234,34 @@ customCommands: description: 'branch for a release' ``` +Here's an example of supplying keybindings for menu options: + +```yml +customCommands: + - key: 'a' + command: 'echo {{.Form.BranchType | quote}}' + context: 'commits' + prompts: + - type: 'menu' + title: 'What kind of branch is it?' + key: 'BranchType' + options: + - value: 'feature' + name: 'feature branch' + description: 'branch based off develop' + key: 'f' + - value: 'hotfix' + name: 'hotfix branch' + description: 'branch based off main for fast bug fixes' + key: 'h' + - value: 'release' + name: 'release branch' + description: 'branch for a release' + key: 'r' +``` + +In this example, pressing 'f', 'h', or 'r' will directly select the corresponding option without needing to navigate to it first. + ### Menu-from-command | _field_ | _description_ | _required_ | diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 582d98f7861..bcb5c2f6f85 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -733,6 +733,8 @@ type CustomCommandMenuOption struct { Description string `yaml:"description"` // The value that will be used in the command Value string `yaml:"value" jsonschema:"example=feature,minLength=1"` + // Keybinding to invoke this menu option without needing to navigate to it + Key string `yaml:"key"` } type CustomIconsConfig struct { diff --git a/pkg/config/user_config_validation.go b/pkg/config/user_config_validation.go index fc504fe32c4..16be84521b5 100644 --- a/pkg/config/user_config_validation.go +++ b/pkg/config/user_config_validation.go @@ -136,6 +136,12 @@ func validateCustomCommands(customCommands []CustomCommand) error { return err } } else { + for _, prompt := range customCommand.Prompts { + if err := validateCustomCommandPrompt(prompt); err != nil { + return err + } + } + if err := validateEnum("customCommand.output", customCommand.Output, []string{"", "none", "terminal", "log", "logWithPty", "popup"}); err != nil { return err @@ -144,3 +150,14 @@ func validateCustomCommands(customCommands []CustomCommand) error { } return nil } + +func validateCustomCommandPrompt(prompt CustomCommandPrompt) error { + for _, option := range prompt.Options { + if !isValidKeybindingKey(option.Key) { + return fmt.Errorf("Unrecognized key '%s' for custom command prompt option. For permitted values see %s", + option.Key, constants.Links.Docs.CustomKeybindings) + } + } + + return nil +} diff --git a/pkg/config/user_config_validation_test.go b/pkg/config/user_config_validation_test.go index fcbfe1139db..7fbfd00d42d 100644 --- a/pkg/config/user_config_validation_test.go +++ b/pkg/config/user_config_validation_test.go @@ -176,6 +176,31 @@ func TestUserConfigValidate_enums(t *testing.T) { {value: "invalid_value", valid: false}, }, }, + { + name: "Custom command keybinding in prompt menu", + setup: func(config *UserConfig, value string) { + config.CustomCommands = []CustomCommand{ + { + Key: "X", + Description: "My Custom Commands", + Prompts: []CustomCommandPrompt{ + { + Options: []CustomCommandMenuOption{ + {Key: value}, + }, + }, + }, + }, + } + }, + testCases: []testCase{ + {value: "", valid: true}, + {value: "", valid: true}, + {value: "q", valid: true}, + {value: "", valid: true}, + {value: "invalid_value", valid: false}, + }, + }, { name: "Custom command output", setup: func(config *UserConfig, value string) { diff --git a/pkg/gui/services/custom_commands/handler_creator.go b/pkg/gui/services/custom_commands/handler_creator.go index 04899b00499..a10689f2d0d 100644 --- a/pkg/gui/services/custom_commands/handler_creator.go +++ b/pkg/gui/services/custom_commands/handler_creator.go @@ -9,6 +9,7 @@ import ( "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/style" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" @@ -203,6 +204,7 @@ func (self *HandlerCreator) menuPrompt(prompt *config.CustomCommandPrompt, wrapp OnPress: func() error { return wrappedF(option.Value) }, + Key: keybindings.GetKey(option.Key), } }) diff --git a/pkg/gui/services/custom_commands/resolver.go b/pkg/gui/services/custom_commands/resolver.go index bf269735562..d34d39d5fae 100644 --- a/pkg/gui/services/custom_commands/resolver.go +++ b/pkg/gui/services/custom_commands/resolver.go @@ -108,6 +108,7 @@ func (self *Resolver) resolveMenuOption(option *config.CustomCommandMenuOption, Name: name, Description: description, Value: value, + Key: option.Key, }, nil } diff --git a/pkg/integration/tests/custom_commands/menu_prompt_with_keys.go b/pkg/integration/tests/custom_commands/menu_prompt_with_keys.go new file mode 100644 index 00000000000..f7f733d9c6a --- /dev/null +++ b/pkg/integration/tests/custom_commands/menu_prompt_with_keys.go @@ -0,0 +1,65 @@ +package custom_commands + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var MenuPromptWithKeys = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Using a custom command with menu options that have keybindings", + ExtraCmdArgs: []string{}, + Skip: false, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("initial commit") + }, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().CustomCommands = []config.CustomCommand{ + { + Key: "a", + Context: "files", + Command: `echo {{.Form.Choice | quote}} > result.txt`, + Prompts: []config.CustomCommandPrompt{ + { + Key: "Choice", + Type: "menu", + Title: "Choose an option", + Options: []config.CustomCommandMenuOption{ + { + Name: "first", + Description: "First option", + Value: "FIRST", + Key: "1", + }, + { + Name: "second", + Description: "Second option", + Value: "SECOND", + Key: "H", + }, + { + Name: "third", + Description: "Third option", + Value: "THIRD", + Key: "3", + }, + }, + }, + }, + }, + } + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Press("a") + + t.ExpectPopup().Menu(). + Title(Equals("Choose an option")) + + // 'H' is normally a navigation key (ScrollLeft), so this tests that menu item + // keybindings have proper precedence over non-essential navigation keys + t.Views().Menu().Press("H") + + t.FileSystem().FileContent("result.txt", Equals("SECOND\n")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 9c5d57ec97c..f19d5aef333 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -175,6 +175,7 @@ var tests = []*components.IntegrationTest{ custom_commands.GlobalContext, custom_commands.MenuFromCommand, custom_commands.MenuFromCommandsOutput, + custom_commands.MenuPromptWithKeys, custom_commands.MultipleContexts, custom_commands.MultiplePrompts, custom_commands.RunCommand, diff --git a/schema-master/config.json b/schema-master/config.json index a808c3c54b2..c371f14e9f7 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -163,6 +163,10 @@ "examples": [ "feature" ] + }, + "key": { + "type": "string", + "description": "Keybinding to invoke this menu option without needing to navigate to it" } }, "additionalProperties": false,