Skip to content

Commit 8581a16

Browse files
committed
feat: support custom keybinds in custom command prompt menus
1 parent ffcd09d commit 8581a16

File tree

8 files changed

+109
-5
lines changed

8 files changed

+109
-5
lines changed

docs/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/gui/context/menu_context.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,8 @@ func (self *MenuViewModel) GetNonModelItems() []*NonModelItem {
193193
}
194194

195195
func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
196-
allBindings := self.ListContextTrait.GetKeybindings(opts)
196+
basicBindings := self.ListContextTrait.GetKeybindings(opts)
197197

198-
// Define essential keys that should always work (select, confirm, escape, and prev/next item navigation)
199198
essentialKeys := []types.Key{
200199
opts.GetKey(opts.Config.Universal.Select),
201200
opts.GetKey(opts.Config.Universal.ConfirmMenu),
@@ -206,15 +205,13 @@ func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Bin
206205
opts.GetKey(opts.Config.Universal.NextItemAlt),
207206
}
208207

209-
// Group bindings by whether they're essential or not
210-
bindingsByPriority := lo.GroupBy(allBindings, func(binding *types.Binding) bool {
208+
bindingsByPriority := lo.GroupBy(basicBindings, func(binding *types.Binding) bool {
211209
return lo.Contains(essentialKeys, binding.Key)
212210
})
213211

214212
essentialBindings := bindingsByPriority[true]
215213
navigationBindings := bindingsByPriority[false]
216214

217-
// Create bindings for menu items
218215
menuItemBindings := lo.FilterMap(self.menuItems, func(item *types.MenuItem, _ int) (*types.Binding, bool) {
219216
if item.Key == nil {
220217
return nil, false

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: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
// Verify the menu appears
57+
t.ExpectPopup().Menu().
58+
Title(Equals("Choose an option"))
59+
60+
// Press 'H' to directly select the second option via its keybinding
61+
// 'H' is normally a navigation key (ScrollLeft), so this tests that menu item
62+
// keybindings have proper precedence over non-essential navigation keys
63+
t.Views().Menu().Press("H")
64+
65+
// After pressing 'H', the menu should close and the command should execute
66+
t.FileSystem().FileContent("result.txt", Equals("SECOND\n"))
67+
},
68+
})

pkg/integration/tests/test_list.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ var tests = []*components.IntegrationTest{
174174
custom_commands.GlobalContext,
175175
custom_commands.MenuFromCommand,
176176
custom_commands.MenuFromCommandsOutput,
177+
custom_commands.MenuPromptWithKeys,
177178
custom_commands.MultipleContexts,
178179
custom_commands.MultiplePrompts,
179180
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. Can be a single letter or one of the values from https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md"
166170
}
167171
},
168172
"additionalProperties": false,

0 commit comments

Comments
 (0)