Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs-master/Custom_Command_Keybindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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_ |
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
17 changes: 17 additions & 0 deletions pkg/config/user_config_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
25 changes: 25 additions & 0 deletions pkg/config/user_config_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<disabled>", valid: true},
{value: "q", valid: true},
{value: "<c-c>", valid: true},
{value: "invalid_value", valid: false},
},
},
{
name: "Custom command output",
setup: func(config *UserConfig, value string) {
Expand Down
2 changes: 2 additions & 0 deletions pkg/gui/services/custom_commands/handler_creator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -203,6 +204,7 @@ func (self *HandlerCreator) menuPrompt(prompt *config.CustomCommandPrompt, wrapp
OnPress: func() error {
return wrappedF(option.Value)
},
Key: keybindings.GetKey(option.Key),
}
})

Expand Down
1 change: 1 addition & 0 deletions pkg/gui/services/custom_commands/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ func (self *Resolver) resolveMenuOption(option *config.CustomCommandMenuOption,
Name: name,
Description: description,
Value: value,
Key: option.Key,
}, nil
}

Expand Down
65 changes: 65 additions & 0 deletions pkg/integration/tests/custom_commands/menu_prompt_with_keys.go
Original file line number Diff line number Diff line change
@@ -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"))
},
})
1 change: 1 addition & 0 deletions pkg/integration/tests/test_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions schema-master/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@
"examples": [
"feature"
]
},
"key": {
"type": "string",
"description": "Keybinding to invoke this menu option without needing to navigate to it"
}
},
"additionalProperties": false,
Expand Down