Skip to content

Commit 70fe7bd

Browse files
authored
Allow using 'j', 'k', 'H', or 'L' as keybindings in custom command menus (#5131)
Previously they would be shown as keybindings in the menu, but they didn't work because their builtin functionality (select next/prev line, scroll view left/right) would take precedence. This will allow us to revert #4934; doing that in a separate PR, see #5132.
2 parents b7a24c8 + 344d386 commit 70fe7bd

File tree

6 files changed

+116
-20
lines changed

6 files changed

+116
-20
lines changed

pkg/gui/context/menu_context.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type MenuViewModel struct {
5656
promptLines []string
5757
columnAlignment []utils.Alignment
5858
allowFilteringKeybindings bool
59+
keybindingsTakePrecedence bool
5960
*FilteredListViewModel[*types.MenuItem]
6061
}
6162

@@ -117,6 +118,10 @@ func (self *MenuViewModel) SetAllowFilteringKeybindings(allow bool) {
117118
self.allowFilteringKeybindings = allow
118119
}
119120

121+
func (self *MenuViewModel) SetKeybindingsTakePrecedence(value bool) {
122+
self.keybindingsTakePrecedence = value
123+
}
124+
120125
// TODO: move into presentation package
121126
func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string {
122127
menuItems := self.FilteredListViewModel.GetItems()
@@ -205,10 +210,19 @@ func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Bin
205210
}
206211
})
207212

208-
// appending because that means the menu item bindings have lower precedence.
209-
// So if a basic binding is to escape from the menu, we want that to still be
210-
// what happens when you press escape. This matters when we're showing the menu
211-
// for all keybindings of say the files context.
213+
if self.keybindingsTakePrecedence {
214+
// This is used for all normal menus except the keybindings menu. In this case we want the
215+
// bindings of the menu items to have higher precedence than the builtin bindings; this
216+
// allows assigning a keybinding to a menu item that overrides a non-essential binding such
217+
// as 'j', 'k', 'H', 'L', etc. This is safe to do because the essential bindings such as
218+
// confirm and return have already been removed from the menu items in this case.
219+
return append(menuItemBindings, basicBindings...)
220+
}
221+
222+
// For the keybindings menu we didn't remove the essential bindings from the menu items, because
223+
// it is important to see all bindings (as a cheat sheet for what the keys are when the menu is
224+
// not open). Therefore we want the essential bindings to have higher precedence than the menu
225+
// item bindings.
212226
return append(basicBindings, menuItemBindings...)
213227
}
214228

pkg/gui/controllers/options_menu_action.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ func (self *OptionsMenuAction) Call() error {
4646
appendBindings(navigation, &types.MenuSection{Title: self.c.Tr.KeybindingsMenuSectionNavigation, Column: 1})
4747

4848
return self.c.Menu(types.CreateMenuOptions{
49-
Title: self.c.Tr.Keybindings,
50-
Items: menuItems,
51-
HideCancel: true,
52-
ColumnAlignment: []utils.Alignment{utils.AlignRight, utils.AlignLeft},
53-
AllowFilteringKeybindings: true,
54-
KeepConfirmKeybindings: true,
49+
Title: self.c.Tr.Keybindings,
50+
Items: menuItems,
51+
HideCancel: true,
52+
ColumnAlignment: []utils.Alignment{utils.AlignRight, utils.AlignLeft},
53+
AllowFilteringKeybindings: true,
54+
KeepConflictingKeybindings: true,
5555
})
5656
}
5757

pkg/gui/menu_panel.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
77
"github.com/jesseduffield/lazygit/pkg/gui/types"
88
"github.com/jesseduffield/lazygit/pkg/theme"
9+
"github.com/samber/lo"
910
)
1011

1112
// note: items option is mutated by this function
@@ -21,7 +22,13 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
2122
}
2223

2324
maxColumnSize := 1
24-
confirmKey := keybindings.GetKey(gui.c.UserConfig().Keybinding.Universal.ConfirmMenu)
25+
26+
essentialKeys := []types.Key{
27+
keybindings.GetKey(gui.c.UserConfig().Keybinding.Universal.ConfirmMenu),
28+
keybindings.GetKey(gui.c.UserConfig().Keybinding.Universal.Return),
29+
keybindings.GetKey(gui.c.UserConfig().Keybinding.Universal.PrevItem),
30+
keybindings.GetKey(gui.c.UserConfig().Keybinding.Universal.NextItem),
31+
}
2532

2633
for _, item := range opts.Items {
2734
if item.LabelColumns == nil {
@@ -34,8 +41,8 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
3441

3542
maxColumnSize = max(maxColumnSize, len(item.LabelColumns))
3643

37-
// Remove all item keybindings that are the same as the confirm binding
38-
if item.Key == confirmKey && !opts.KeepConfirmKeybindings {
44+
// Remove all item keybindings that are the same as one of the essential bindings
45+
if !opts.KeepConflictingKeybindings && lo.Contains(essentialKeys, item.Key) {
3946
item.Key = nil
4047
}
4148
}
@@ -51,6 +58,7 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
5158
gui.State.Contexts.Menu.SetMenuItems(opts.Items, opts.ColumnAlignment)
5259
gui.State.Contexts.Menu.SetPrompt(opts.Prompt)
5360
gui.State.Contexts.Menu.SetAllowFilteringKeybindings(opts.AllowFilteringKeybindings)
61+
gui.State.Contexts.Menu.SetKeybindingsTakePrecedence(!opts.KeepConflictingKeybindings)
5462
gui.State.Contexts.Menu.SetSelection(0)
5563

5664
gui.Views.Menu.Title = opts.Title

pkg/gui/types/common.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,13 +150,13 @@ const (
150150
)
151151

152152
type CreateMenuOptions struct {
153-
Title string
154-
Prompt string // a message that will be displayed above the menu options
155-
Items []*MenuItem
156-
HideCancel bool
157-
ColumnAlignment []utils.Alignment
158-
AllowFilteringKeybindings bool
159-
KeepConfirmKeybindings bool // if true, the keybindings that match the confirm binding will not be removed from menu items
153+
Title string
154+
Prompt string // a message that will be displayed above the menu options
155+
Items []*MenuItem
156+
HideCancel bool
157+
ColumnAlignment []utils.Alignment
158+
AllowFilteringKeybindings bool
159+
KeepConflictingKeybindings bool // if true, the keybindings that match essential bindings such as confirm or return will not be removed from menu items
160160
}
161161

162162
type CreatePopupPanelOpts struct {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 CustomCommandsSubmenuWithSpecialKeybindings = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Using custom commands from a custom commands menu with keybindings that conflict with builtin ones",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupRepo: func(shell *Shell) {},
13+
SetupConfig: func(cfg *config.AppConfig) {
14+
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
15+
{
16+
Key: "x",
17+
Description: "My Custom Commands",
18+
CommandMenu: []config.CustomCommand{
19+
{
20+
Key: "j",
21+
Context: "global",
22+
Command: "echo j",
23+
Output: "popup",
24+
},
25+
{
26+
Key: "H",
27+
Context: "global",
28+
Command: "echo H",
29+
Output: "popup",
30+
},
31+
{
32+
Key: "y",
33+
Context: "global",
34+
Command: "echo y",
35+
Output: "popup",
36+
},
37+
{
38+
Key: "<down>",
39+
Context: "global",
40+
Command: "echo down",
41+
Output: "popup",
42+
},
43+
},
44+
},
45+
}
46+
cfg.GetUserConfig().Keybinding.Universal.ConfirmMenu = "y"
47+
},
48+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
49+
t.Views().Files().
50+
Focus().
51+
IsEmpty().
52+
Press("x").
53+
Tap(func() {
54+
t.ExpectPopup().Menu().
55+
Title(Equals("My Custom Commands")).
56+
Lines(
57+
Contains("j echo j"),
58+
Contains("H echo H"),
59+
Contains(" echo y"),
60+
Contains(" echo down"),
61+
)
62+
t.GlobalPress("j")
63+
t.ExpectPopup().Alert().Title(Equals("echo j")).Content(Equals("j")).Confirm()
64+
}).
65+
Press("x").
66+
Tap(func() {
67+
t.ExpectPopup().Menu().
68+
Title(Equals("My Custom Commands"))
69+
t.GlobalPress("H")
70+
t.ExpectPopup().Alert().Title(Equals("echo H")).Content(Equals("H")).Confirm()
71+
})
72+
},
73+
})

pkg/integration/tests/test_list.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ var tests = []*components.IntegrationTest{
170170
custom_commands.BasicCommand,
171171
custom_commands.CheckForConflicts,
172172
custom_commands.CustomCommandsSubmenu,
173+
custom_commands.CustomCommandsSubmenuWithSpecialKeybindings,
173174
custom_commands.FormPrompts,
174175
custom_commands.GlobalContext,
175176
custom_commands.MenuFromCommand,

0 commit comments

Comments
 (0)