Skip to content

Commit e7b69bc

Browse files
authored
Support custom keybindings in custom command menu prompts (#5129)
## Summary resolves: #3626 This adds support for keybindings on menu options. ### Example Usage: ```yml customCommands: - key: 'e' context: 'files' command: 'echo {{.Form.Choice | quote}} > result.txt' prompts: - type: 'menu' title: 'Choose an option' key: 'Choice' options: - value: 'foo' description: 'FOO' key: 'f' - value: 'bar' description: 'BAR' key: 'b' ``` ### Please check if the PR fulfills these requirements * [x] Cheatsheets are up-to-date (run `go generate ./...`) * [x] Code has been formatted (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting)) * [x] Tests have been added/updated (see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) for the integration test guide) * [x] Text is internationalised (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation)) * [x] If a new UserConfig entry was added, make sure it can be hot-reloaded (see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig)) * [x] Docs have been updated if necessary * [x] You've read through your own file changes for silly mistakes etc
2 parents 262e7f4 + 62913ee commit e7b69bc

File tree

9 files changed

+146
-0
lines changed

9 files changed

+146
-0
lines changed

docs-master/Custom_Command_Keybindings.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ The permitted option fields are:
192192
| name | The first part of the label | no |
193193
| description | The second part of the label | no |
194194
| value | the value that will be used in the command | yes |
195+
| 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 |
195196

196197
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:
197198

@@ -233,6 +234,34 @@ customCommands:
233234
description: 'branch for a release'
234235
```
235236

237+
Here's an example of supplying keybindings for menu options:
238+
239+
```yml
240+
customCommands:
241+
- key: 'a'
242+
command: 'echo {{.Form.BranchType | quote}}'
243+
context: 'commits'
244+
prompts:
245+
- type: 'menu'
246+
title: 'What kind of branch is it?'
247+
key: 'BranchType'
248+
options:
249+
- value: 'feature'
250+
name: 'feature branch'
251+
description: 'branch based off develop'
252+
key: 'f'
253+
- value: 'hotfix'
254+
name: 'hotfix branch'
255+
description: 'branch based off main for fast bug fixes'
256+
key: 'h'
257+
- value: 'release'
258+
name: 'release branch'
259+
description: 'branch for a release'
260+
key: 'r'
261+
```
262+
263+
In this example, pressing 'f', 'h', or 'r' will directly select the corresponding option without needing to navigate to it first.
264+
236265
### Menu-from-command
237266

238267
| _field_ | _description_ | _required_ |

pkg/config/user_config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,8 @@ type CustomCommandMenuOption struct {
733733
Description string `yaml:"description"`
734734
// The value that will be used in the command
735735
Value string `yaml:"value" jsonschema:"example=feature,minLength=1"`
736+
// Keybinding to invoke this menu option without needing to navigate to it
737+
Key string `yaml:"key"`
736738
}
737739

738740
type CustomIconsConfig struct {

pkg/config/user_config_validation.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ func validateCustomCommands(customCommands []CustomCommand) error {
136136
return err
137137
}
138138
} else {
139+
for _, prompt := range customCommand.Prompts {
140+
if err := validateCustomCommandPrompt(prompt); err != nil {
141+
return err
142+
}
143+
}
144+
139145
if err := validateEnum("customCommand.output", customCommand.Output,
140146
[]string{"", "none", "terminal", "log", "logWithPty", "popup"}); err != nil {
141147
return err
@@ -144,3 +150,14 @@ func validateCustomCommands(customCommands []CustomCommand) error {
144150
}
145151
return nil
146152
}
153+
154+
func validateCustomCommandPrompt(prompt CustomCommandPrompt) error {
155+
for _, option := range prompt.Options {
156+
if !isValidKeybindingKey(option.Key) {
157+
return fmt.Errorf("Unrecognized key '%s' for custom command prompt option. For permitted values see %s",
158+
option.Key, constants.Links.Docs.CustomKeybindings)
159+
}
160+
}
161+
162+
return nil
163+
}

pkg/config/user_config_validation_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,31 @@ func TestUserConfigValidate_enums(t *testing.T) {
176176
{value: "invalid_value", valid: false},
177177
},
178178
},
179+
{
180+
name: "Custom command keybinding in prompt menu",
181+
setup: func(config *UserConfig, value string) {
182+
config.CustomCommands = []CustomCommand{
183+
{
184+
Key: "X",
185+
Description: "My Custom Commands",
186+
Prompts: []CustomCommandPrompt{
187+
{
188+
Options: []CustomCommandMenuOption{
189+
{Key: value},
190+
},
191+
},
192+
},
193+
},
194+
}
195+
},
196+
testCases: []testCase{
197+
{value: "", valid: true},
198+
{value: "<disabled>", valid: true},
199+
{value: "q", valid: true},
200+
{value: "<c-c>", valid: true},
201+
{value: "invalid_value", valid: false},
202+
},
203+
},
179204
{
180205
name: "Custom command output",
181206
setup: func(config *UserConfig, value string) {

pkg/gui/services/custom_commands/handler_creator.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/jesseduffield/gocui"
1010
"github.com/jesseduffield/lazygit/pkg/config"
1111
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
12+
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
1213
"github.com/jesseduffield/lazygit/pkg/gui/style"
1314
"github.com/jesseduffield/lazygit/pkg/gui/types"
1415
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -203,6 +204,7 @@ func (self *HandlerCreator) menuPrompt(prompt *config.CustomCommandPrompt, wrapp
203204
OnPress: func() error {
204205
return wrappedF(option.Value)
205206
},
207+
Key: keybindings.GetKey(option.Key),
206208
}
207209
})
208210

pkg/gui/services/custom_commands/resolver.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ func (self *Resolver) resolveMenuOption(option *config.CustomCommandMenuOption,
108108
Name: name,
109109
Description: description,
110110
Value: value,
111+
Key: option.Key,
111112
}, nil
112113
}
113114

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package custom_commands
2+
3+
import (
4+
"github.com/jesseduffield/lazygit/pkg/config"
5+
. "github.com/jesseduffield/lazygit/pkg/integration/components"
6+
)
7+
8+
var MenuPromptWithKeys = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Using a custom command with menu options that have keybindings",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupRepo: func(shell *Shell) {
13+
shell.EmptyCommit("initial commit")
14+
},
15+
SetupConfig: func(cfg *config.AppConfig) {
16+
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
17+
{
18+
Key: "a",
19+
Context: "files",
20+
Command: `echo {{.Form.Choice | quote}} > result.txt`,
21+
Prompts: []config.CustomCommandPrompt{
22+
{
23+
Key: "Choice",
24+
Type: "menu",
25+
Title: "Choose an option",
26+
Options: []config.CustomCommandMenuOption{
27+
{
28+
Name: "first",
29+
Description: "First option",
30+
Value: "FIRST",
31+
Key: "1",
32+
},
33+
{
34+
Name: "second",
35+
Description: "Second option",
36+
Value: "SECOND",
37+
Key: "H",
38+
},
39+
{
40+
Name: "third",
41+
Description: "Third option",
42+
Value: "THIRD",
43+
Key: "3",
44+
},
45+
},
46+
},
47+
},
48+
},
49+
}
50+
},
51+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
52+
t.Views().Files().
53+
IsFocused().
54+
Press("a")
55+
56+
t.ExpectPopup().Menu().
57+
Title(Equals("Choose an option"))
58+
59+
// 'H' is normally a navigation key (ScrollLeft), so this tests that menu item
60+
// keybindings have proper precedence over non-essential navigation keys
61+
t.Views().Menu().Press("H")
62+
63+
t.FileSystem().FileContent("result.txt", Equals("SECOND\n"))
64+
},
65+
})

pkg/integration/tests/test_list.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ var tests = []*components.IntegrationTest{
175175
custom_commands.GlobalContext,
176176
custom_commands.MenuFromCommand,
177177
custom_commands.MenuFromCommandsOutput,
178+
custom_commands.MenuPromptWithKeys,
178179
custom_commands.MultipleContexts,
179180
custom_commands.MultiplePrompts,
180181
custom_commands.RunCommand,

schema-master/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@
163163
"examples": [
164164
"feature"
165165
]
166+
},
167+
"key": {
168+
"type": "string",
169+
"description": "Keybinding to invoke this menu option without needing to navigate to it"
166170
}
167171
},
168172
"additionalProperties": false,

0 commit comments

Comments
 (0)