Use a slice of keys for each binding

This is a pure refactor in preparation for letting users configure multiple
alternate bindings for a single command. Every Binding still has exactly one
key, so nothing changes visibly: the cheatsheet, the on-screen options bar,
and the keybindings menu all render identically.

When a Binding ends up with multiple keys, the on-screen options bar will
show only the first (to avoid clutter); the cheatsheet will show all of them (in
a later commit). For now both paths take Key[0].

MenuItem.Key is changed in the same way, it also has a slice of keys now.

In this commit we keep the name `Key` in Binding, KeybindingOpts and MenuItem,
instead of renaming them to `Keys` right away, in order to keep the diff a bit
more readable. We'll do the rename separately in the next commit.
This commit is contained in:
Stefan Haller 2026-05-03 22:44:53 +02:00
parent 22a508fdba
commit 3d18ee8f91
20 changed files with 101 additions and 81 deletions

View file

@ -145,7 +145,7 @@ func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*b
return false return false
} }
return (binding.Description != "" || binding.Alternative != "") && binding.Key.IsSet() return (binding.Description != "" || binding.Alternative != "") && len(binding.Key) > 0
}) })
bindingsByHeader := lo.GroupBy(bindingsToDisplay, func(binding *types.Binding) header { bindingsByHeader := lo.GroupBy(bindingsToDisplay, func(binding *types.Binding) header {
@ -156,7 +156,7 @@ func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*b
bindingsByHeader, bindingsByHeader,
func(header header, hBindings []*types.Binding) headerWithBindings { func(header header, hBindings []*types.Binding) headerWithBindings {
uniqBindings := lo.UniqBy(hBindings, func(binding *types.Binding) string { uniqBindings := lo.UniqBy(hBindings, func(binding *types.Binding) string {
return binding.Description + config.LabelForKey(binding.Key) return binding.Description + config.LabelForKey(binding.Key[0])
}) })
return headerWithBindings{ return headerWithBindings{
@ -214,7 +214,7 @@ func formatTitle(title string) string {
} }
func formatBinding(binding *types.Binding) string { func formatBinding(binding *types.Binding) string {
action := config.LabelForKey(binding.Key) action := config.LabelForKey(binding.Key[0])
description := binding.Description description := binding.Description
if binding.Alternative != "" { if binding.Alternative != "" {
action += fmt.Sprintf(" (%s)", binding.Alternative) action += fmt.Sprintf(" (%s)", binding.Alternative)

View file

@ -28,7 +28,7 @@ func TestGetBindingSections(t *testing.T) {
{ {
ViewName: "files", ViewName: "files",
Description: "stage file", Description: "stage file",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
}, },
expected: []*bindingSection{ expected: []*bindingSection{
@ -38,7 +38,7 @@ func TestGetBindingSections(t *testing.T) {
{ {
ViewName: "files", ViewName: "files",
Description: "stage file", Description: "stage file",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
}, },
}, },
@ -50,7 +50,7 @@ func TestGetBindingSections(t *testing.T) {
{ {
ViewName: "", ViewName: "",
Description: "quit", Description: "quit",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
}, },
expected: []*bindingSection{ expected: []*bindingSection{
@ -60,7 +60,7 @@ func TestGetBindingSections(t *testing.T) {
{ {
ViewName: "", ViewName: "",
Description: "quit", Description: "quit",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
}, },
}, },
@ -72,17 +72,17 @@ func TestGetBindingSections(t *testing.T) {
{ {
ViewName: "files", ViewName: "files",
Description: "stage file", Description: "stage file",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
{ {
ViewName: "files", ViewName: "files",
Description: "unstage file", Description: "unstage file",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
{ {
ViewName: "submodules", ViewName: "submodules",
Description: "drop submodule", Description: "drop submodule",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
}, },
expected: []*bindingSection{ expected: []*bindingSection{
@ -92,12 +92,12 @@ func TestGetBindingSections(t *testing.T) {
{ {
ViewName: "files", ViewName: "files",
Description: "stage file", Description: "stage file",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
{ {
ViewName: "files", ViewName: "files",
Description: "unstage file", Description: "unstage file",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
}, },
}, },
@ -107,7 +107,7 @@ func TestGetBindingSections(t *testing.T) {
{ {
ViewName: "submodules", ViewName: "submodules",
Description: "drop submodule", Description: "drop submodule",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
}, },
}, },
@ -119,23 +119,23 @@ func TestGetBindingSections(t *testing.T) {
{ {
ViewName: "files", ViewName: "files",
Description: "stage file", Description: "stage file",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
{ {
ViewName: "files", ViewName: "files",
Description: "unstage file", Description: "unstage file",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
{ {
ViewName: "files", ViewName: "files",
Description: "scroll", Description: "scroll",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
Tag: "navigation", Tag: "navigation",
}, },
{ {
ViewName: "commits", ViewName: "commits",
Description: "revert commit", Description: "revert commit",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
}, },
expected: []*bindingSection{ expected: []*bindingSection{
@ -145,7 +145,7 @@ func TestGetBindingSections(t *testing.T) {
{ {
ViewName: "files", ViewName: "files",
Description: "scroll", Description: "scroll",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
Tag: "navigation", Tag: "navigation",
}, },
}, },
@ -156,7 +156,7 @@ func TestGetBindingSections(t *testing.T) {
{ {
ViewName: "commits", ViewName: "commits",
Description: "revert commit", Description: "revert commit",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
}, },
}, },
@ -166,12 +166,12 @@ func TestGetBindingSections(t *testing.T) {
{ {
ViewName: "files", ViewName: "files",
Description: "stage file", Description: "stage file",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
{ {
ViewName: "files", ViewName: "files",
Description: "unstage file", Description: "unstage file",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
}, },
}, },
@ -183,34 +183,34 @@ func TestGetBindingSections(t *testing.T) {
{ {
ViewName: "files", ViewName: "files",
Description: "stage file", Description: "stage file",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
{ {
ViewName: "files", ViewName: "files",
Description: "unstage file", Description: "unstage file",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
{ {
ViewName: "files", ViewName: "files",
Description: "scroll", Description: "scroll",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
Tag: "navigation", Tag: "navigation",
}, },
{ {
ViewName: "commits", ViewName: "commits",
Description: "revert commit", Description: "revert commit",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
{ {
ViewName: "commits", ViewName: "commits",
Description: "scroll", Description: "scroll",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
Tag: "navigation", Tag: "navigation",
}, },
{ {
ViewName: "commits", ViewName: "commits",
Description: "page up", Description: "page up",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
Tag: "navigation", Tag: "navigation",
}, },
}, },
@ -221,13 +221,13 @@ func TestGetBindingSections(t *testing.T) {
{ {
ViewName: "files", ViewName: "files",
Description: "scroll", Description: "scroll",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
Tag: "navigation", Tag: "navigation",
}, },
{ {
ViewName: "commits", ViewName: "commits",
Description: "page up", Description: "page up",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
Tag: "navigation", Tag: "navigation",
}, },
}, },
@ -238,7 +238,7 @@ func TestGetBindingSections(t *testing.T) {
{ {
ViewName: "commits", ViewName: "commits",
Description: "revert commit", Description: "revert commit",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
}, },
}, },
@ -248,12 +248,12 @@ func TestGetBindingSections(t *testing.T) {
{ {
ViewName: "files", ViewName: "files",
Description: "stage file", Description: "stage file",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
{ {
ViewName: "files", ViewName: "files",
Description: "unstage file", Description: "unstage file",
Key: gocui.NewKeyRune('a'), Key: []gocui.Key{gocui.NewKeyRune('a')},
}, },
}, },
}, },

View file

@ -205,3 +205,11 @@ func GetValidatedKeyBindingKey(label string) gocui.Key {
return key return key
} }
func GetValidatedKeyBindingKeys(label string) []gocui.Key {
k := GetValidatedKeyBindingKey(label)
if !k.IsSet() {
return nil
}
return []gocui.Key{k}
}

View file

@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/i18n"
@ -73,7 +74,11 @@ func NewMenuViewModel(c *ContextCommon) *MenuViewModel {
func() []*types.MenuItem { return self.menuItems }, func() []*types.MenuItem { return self.menuItems },
func(item *types.MenuItem) []string { func(item *types.MenuItem) []string {
if filterKeybindings { if filterKeybindings {
return []string{config.LabelForKey(item.Key)} // Allow searching all configured keybindings of each item, even though only the
// first one is shown in the menu.
return lo.Map(item.Key, func(k gocui.Key, _ int) string {
return config.LabelForKey(k)
})
} }
return item.LabelColumns return item.LabelColumns
@ -138,8 +143,8 @@ func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string {
} }
keyLabel := "" keyLabel := ""
if item.Key.IsSet() { if len(item.Key) > 0 {
keyLabel = style.FgCyan.Sprint(config.LabelForKey(item.Key)) keyLabel = style.FgCyan.Sprint(config.LabelForKey(item.Key[0]))
} }
checkMark := "" checkMark := ""
@ -205,7 +210,7 @@ func (self *MenuViewModel) GetNonModelItems() []*NonModelItem {
func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
basicBindings := self.ListContextTrait.GetKeybindings(opts) basicBindings := self.ListContextTrait.GetKeybindings(opts)
menuItemsWithKeys := lo.Filter(self.menuItems, func(item *types.MenuItem, _ int) bool { menuItemsWithKeys := lo.Filter(self.menuItems, func(item *types.MenuItem, _ int) bool {
return item.Key.IsSet() return len(item.Key) > 0
}) })
menuItemBindings := lo.Map(menuItemsWithKeys, func(item *types.MenuItem, _ int) *types.Binding { menuItemBindings := lo.Map(menuItemsWithKeys, func(item *types.MenuItem, _ int) *types.Binding {

View file

@ -3,9 +3,9 @@ package helpers
import "github.com/jesseduffield/lazygit/pkg/gocui" import "github.com/jesseduffield/lazygit/pkg/gocui"
// menuKey is a shorthand for constructing a key value for a menu item from a single rune literal, // menuKey is a shorthand for constructing a key value for a menu item from a single rune literal,
// avoiding the noise of `gocui.NewKeyRune('a')` at every call site. There is an intentionally // avoiding the noise of `[]gocui.Key{gocui.NewKeyRune('a')}` at every call site. There is an
// identical helper in the controllers package so that callers in either package can use the // intentionally identical helper in the controllers package so that callers in either package can
// unqualified form. // use the unqualified form.
func menuKey(r rune) gocui.Key { func menuKey(r rune) []gocui.Key {
return gocui.NewKeyRune(r) return []gocui.Key{gocui.NewKeyRune(r)}
} }

View file

@ -39,7 +39,7 @@ const (
func (self *MergeAndRebaseHelper) CreateRebaseOptionsMenu() error { func (self *MergeAndRebaseHelper) CreateRebaseOptionsMenu() error {
type optionAndKey struct { type optionAndKey struct {
option string option string
key gocui.Key key []gocui.Key
} }
options := []optionAndKey{ options := []optionAndKey{

View file

@ -216,7 +216,7 @@ func (self *RefsHelper) ResetToRef(ref string, strength string, envVars []string
func (self *RefsHelper) CreateSortOrderMenu(sortOptionsOrder []string, menuPrompt string, onSelected func(sortOrder string) error, currentValue string) error { func (self *RefsHelper) CreateSortOrderMenu(sortOptionsOrder []string, menuPrompt string, onSelected func(sortOrder string) error, currentValue string) error {
type sortMenuOption struct { type sortMenuOption struct {
key gocui.Key key []gocui.Key
label string label string
description string description string
sortOrder string sortOrder string
@ -260,7 +260,7 @@ func (self *RefsHelper) CreateGitResetMenu(name string, ref string) error {
type strengthWithKey struct { type strengthWithKey struct {
strength string strength string
label string label string
key gocui.Key key []gocui.Key
tooltip string tooltip string
} }
strengths := []strengthWithKey{ strengths := []strengthWithKey{
@ -318,7 +318,7 @@ func (self *RefsHelper) CreateCheckoutMenu(commit *models.Commit) error {
if len(branches) > 0 { if len(branches) > 0 {
menuItems = append(menuItems, lo.Map(branches, func(branch *models.Branch, index int) *types.MenuItem { menuItems = append(menuItems, lo.Map(branches, func(branch *models.Branch, index int) *types.MenuItem {
var key gocui.Key var key []gocui.Key
if index < 9 { if index < 9 {
key = menuKey(rune(index + 1 + '0')) // Convert 1-based index to key key = menuKey(rune(index + 1 + '0')) // Convert 1-based index to key
} }

View file

@ -3,9 +3,9 @@ package controllers
import "github.com/jesseduffield/lazygit/pkg/gocui" import "github.com/jesseduffield/lazygit/pkg/gocui"
// menuKey is a shorthand for constructing a key value for a menu item from a single rune literal, // menuKey is a shorthand for constructing a key value for a menu item from a single rune literal,
// avoiding the noise of `gocui.NewKeyRune('a')` at every call site. There is an intentionally // avoiding the noise of `[]gocui.Key{gocui.NewKeyRune('a')}` at every call site. There is an
// identical helper in the helpers package so that callers in either package can use the unqualified // intentionally identical helper in the helpers package so that callers in either package can use
// form. // the unqualified form.
func menuKey(r rune) gocui.Key { func menuKey(r rune) []gocui.Key {
return gocui.NewKeyRune(r) return []gocui.Key{gocui.NewKeyRune(r)}
} }

View file

@ -27,7 +27,7 @@ func NewPromptController(
func (self *PromptController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { func (self *PromptController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{ bindings := []*types.Binding{
{ {
Key: gocui.NewKeyName(gocui.KeyEnter), Key: []gocui.Key{gocui.NewKeyName(gocui.KeyEnter)},
Handler: func() error { return self.context().State.OnConfirm() }, Handler: func() error { return self.context().State.OnConfirm() },
Description: self.c.Tr.Confirm, Description: self.c.Tr.Confirm,
DisplayOnScreen: true, DisplayOnScreen: true,

View file

@ -24,7 +24,7 @@ func NewSearchPromptController(
func (self *SearchPromptController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { func (self *SearchPromptController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
return []*types.Binding{ return []*types.Binding{
{ {
Key: gocui.NewKeyName(gocui.KeyEnter), Key: []gocui.Key{gocui.NewKeyName(gocui.KeyEnter)},
Handler: self.confirm, Handler: self.confirm,
}, },
{ {

View file

@ -15,7 +15,7 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error {
Items: []*types.MenuItem{ Items: []*types.MenuItem{
{ {
Label: gui.c.Tr.ToggleShowCommandLog, Label: gui.c.Tr.ToggleShowCommandLog,
Key: gocui.NewKeyRune('t'), Key: []gocui.Key{gocui.NewKeyRune('t')},
OnPress: func() error { OnPress: func() error {
currentContext := gui.c.Context().CurrentStatic() currentContext := gui.c.Context().CurrentStatic()
if gui.c.State().GetShowExtrasWindow() && currentContext.GetKey() == context.COMMAND_LOG_CONTEXT_KEY { if gui.c.State().GetShowExtrasWindow() && currentContext.GetKey() == context.COMMAND_LOG_CONTEXT_KEY {
@ -30,7 +30,7 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error {
}, },
{ {
Label: gui.c.Tr.FocusCommandLog, Label: gui.c.Tr.FocusCommandLog,
Key: gocui.NewKeyRune('f'), Key: []gocui.Key{gocui.NewKeyRune('f')},
OnPress: gui.handleFocusCommandLog, OnPress: gui.handleFocusCommandLog,
}, },
}, },

View file

@ -69,7 +69,7 @@ func (gui *Gui) keybindingOpts() types.KeybindingsOpts {
} }
return types.KeybindingsOpts{ return types.KeybindingsOpts{
GetKey: config.GetValidatedKeyBindingKey, GetKey: config.GetValidatedKeyBindingKeys,
Config: keybindingConfig, Config: keybindingConfig,
Guards: guards, Guards: guards,
} }
@ -176,7 +176,7 @@ func (gui *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBin
}, },
{ {
ViewName: "information", ViewName: "information",
Key: gocui.NewKeyName(gocui.MouseLeft), Key: []gocui.Key{gocui.NewKeyName(gocui.MouseLeft)},
Handler: gui.handleInfoClick, Handler: gui.handleInfoClick,
}, },
{ {
@ -196,26 +196,26 @@ func (gui *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBin
}, },
{ {
ViewName: "main", ViewName: "main",
Key: gocui.NewKeyName(gocui.MouseWheelDown), Key: []gocui.Key{gocui.NewKeyName(gocui.MouseWheelDown)},
Handler: gui.scrollDownMain, Handler: gui.scrollDownMain,
Description: gui.c.Tr.ScrollDown, Description: gui.c.Tr.ScrollDown,
Alternative: "fn+up", Alternative: "fn+up",
}, },
{ {
ViewName: "main", ViewName: "main",
Key: gocui.NewKeyName(gocui.MouseWheelUp), Key: []gocui.Key{gocui.NewKeyName(gocui.MouseWheelUp)},
Handler: gui.scrollUpMain, Handler: gui.scrollUpMain,
Description: gui.c.Tr.ScrollUp, Description: gui.c.Tr.ScrollUp,
Alternative: "fn+down", Alternative: "fn+down",
}, },
{ {
ViewName: "secondary", ViewName: "secondary",
Key: gocui.NewKeyName(gocui.MouseWheelDown), Key: []gocui.Key{gocui.NewKeyName(gocui.MouseWheelDown)},
Handler: gui.scrollDownSecondary, Handler: gui.scrollDownSecondary,
}, },
{ {
ViewName: "secondary", ViewName: "secondary",
Key: gocui.NewKeyName(gocui.MouseWheelUp), Key: []gocui.Key{gocui.NewKeyName(gocui.MouseWheelUp)},
Handler: gui.scrollUpSecondary, Handler: gui.scrollUpSecondary,
}, },
{ {
@ -240,12 +240,12 @@ func (gui *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBin
}, },
{ {
ViewName: "confirmation", ViewName: "confirmation",
Key: gocui.NewKeyName(gocui.MouseWheelUp), Key: []gocui.Key{gocui.NewKeyName(gocui.MouseWheelUp)},
Handler: gui.scrollUpConfirmationPanel, Handler: gui.scrollUpConfirmationPanel,
}, },
{ {
ViewName: "confirmation", ViewName: "confirmation",
Key: gocui.NewKeyName(gocui.MouseWheelDown), Key: []gocui.Key{gocui.NewKeyName(gocui.MouseWheelDown)},
Handler: gui.scrollDownConfirmationPanel, Handler: gui.scrollDownConfirmationPanel,
}, },
{ {
@ -287,12 +287,12 @@ func (gui *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBin
}, },
{ {
ViewName: "extras", ViewName: "extras",
Key: gocui.NewKeyName(gocui.MouseWheelUp), Key: []gocui.Key{gocui.NewKeyName(gocui.MouseWheelUp)},
Handler: gui.scrollUpExtra, Handler: gui.scrollUpExtra,
}, },
{ {
ViewName: "extras", ViewName: "extras",
Key: gocui.NewKeyName(gocui.MouseWheelDown), Key: []gocui.Key{gocui.NewKeyName(gocui.MouseWheelDown)},
Handler: gui.scrollDownExtra, Handler: gui.scrollDownExtra,
}, },
{ {
@ -352,7 +352,7 @@ func (gui *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBin
{ {
ViewName: "extras", ViewName: "extras",
Tag: "navigation", Tag: "navigation",
Key: gocui.NewKeyName(gocui.MouseLeft), Key: []gocui.Key{gocui.NewKeyName(gocui.MouseLeft)},
Handler: gui.handleFocusCommandLog, Handler: gui.handleFocusCommandLog,
}, },
} }
@ -448,7 +448,9 @@ func (gui *Gui) SetKeybinding(binding *types.Binding) {
return gui.callKeybindingHandler(binding) return gui.callKeybindingHandler(binding)
} }
gui.g.SetKeybinding(binding.ViewName, binding.Key, handler) for _, key := range binding.Key {
gui.g.SetKeybinding(binding.ViewName, key, handler)
}
} }
func (gui *Gui) SetMouseKeybinding(binding *gocui.ViewMouseBinding) error { func (gui *Gui) SetMouseKeybinding(binding *gocui.ViewMouseBinding) error {

View file

@ -46,8 +46,10 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
maxColumnSize = max(maxColumnSize, len(item.LabelColumns)) maxColumnSize = max(maxColumnSize, len(item.LabelColumns))
// Remove all item keybindings that are the same as one of the essential bindings // Remove all item keybindings that are the same as one of the essential bindings
if !opts.KeepConflictingKeybindings && lo.Contains(essentialKeys, item.Key) { if !opts.KeepConflictingKeybindings {
item.Key = gocui.Key{} item.Key = lo.Filter(item.Key, func(k gocui.Key, _ int) bool {
return !lo.Contains(essentialKeys, k)
})
} }
} }

View file

@ -41,12 +41,12 @@ func (self *OptionsMapMgr) renderContextOptionsMap() {
globalBindings := self.c.Contexts().Global.GetKeybindings(self.c.KeybindingsOpts()) globalBindings := self.c.Contexts().Global.GetKeybindings(self.c.KeybindingsOpts())
currentContextKeys := set.NewFromSlice( currentContextKeys := set.NewFromSlice(
lo.Map(currentContextBindings, func(binding *types.Binding, _ int) gocui.Key { lo.FlatMap(currentContextBindings, func(binding *types.Binding, _ int) []gocui.Key {
return binding.Key return binding.Key
})) }))
allBindings := append(currentContextBindings, lo.Filter(globalBindings, func(b *types.Binding, _ int) bool { allBindings := append(currentContextBindings, lo.Filter(globalBindings, func(b *types.Binding, _ int) bool {
return !currentContextKeys.Includes(b.Key) return len(b.Key) == 0 || !currentContextKeys.Includes(b.Key[0])
})...) })...)
bindingsToDisplay := lo.Filter(allBindings, func(binding *types.Binding, _ int) bool { bindingsToDisplay := lo.Filter(allBindings, func(binding *types.Binding, _ int) bool {
@ -60,7 +60,7 @@ func (self *OptionsMapMgr) renderContextOptionsMap() {
} }
return bindingInfo{ return bindingInfo{
key: config.LabelForKey(binding.Key), key: config.LabelForKey(binding.Key[0]),
description: binding.GetShortDescription(), description: binding.GetShortDescription(),
style: displayStyle, style: displayStyle,
} }

View file

@ -2,6 +2,7 @@ package custom_commands
import ( import (
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/i18n"
@ -45,7 +46,7 @@ func (self *Client) GetCustomCommandKeybindings() ([]*types.Binding, error) {
} }
bindings = append(bindings, &types.Binding{ bindings = append(bindings, &types.Binding{
ViewName: "", // custom commands menus are global; we filter the commands inside by context ViewName: "", // custom commands menus are global; we filter the commands inside by context
Key: config.GetValidatedKeyBindingKey(customCommand.Key), Key: []gocui.Key{config.GetValidatedKeyBindingKey(customCommand.Key)},
Handler: handler, Handler: handler,
Description: getCustomCommandsMenuDescription(customCommand, self.c.Tr), Description: getCustomCommandsMenuDescription(customCommand, self.c.Tr),
OpensMenu: true, OpensMenu: true,
@ -72,7 +73,7 @@ func (self *Client) showCustomCommandsMenu(customCommand config.CustomCommand) e
} }
menuItems = append(menuItems, &types.MenuItem{ menuItems = append(menuItems, &types.MenuItem{
Label: subCommand.GetDescription(), Label: subCommand.GetDescription(),
Key: config.GetValidatedKeyBindingKey(subCommand.Key), Key: []gocui.Key{config.GetValidatedKeyBindingKey(subCommand.Key)},
OnPress: handler, OnPress: handler,
OpensMenu: true, OpensMenu: true,
}) })
@ -92,7 +93,7 @@ func (self *Client) showCustomCommandsMenu(customCommand config.CustomCommand) e
menuItems = append(menuItems, &types.MenuItem{ menuItems = append(menuItems, &types.MenuItem{
Label: subCommand.GetDescription(), Label: subCommand.GetDescription(),
Key: config.GetValidatedKeyBindingKey(subCommand.Key), Key: []gocui.Key{config.GetValidatedKeyBindingKey(subCommand.Key)},
OnPress: self.handlerCreator.call(subCommand), OnPress: self.handlerCreator.call(subCommand),
}) })
} }

View file

@ -232,7 +232,7 @@ func (self *HandlerCreator) menuPrompt(prompt *config.CustomCommandPrompt, wrapp
OnPress: func() error { OnPress: func() error {
return wrappedF(option.Value) return wrappedF(option.Value)
}, },
Key: config.GetValidatedKeyBindingKey(option.Key), Key: []gocui.Key{config.GetValidatedKeyBindingKey(option.Key)},
} }
}) })

View file

@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
@ -35,7 +36,7 @@ func (self *KeybindingCreator) call(customCommand config.CustomCommand, handler
return lo.Map(viewNames, func(viewName string, _ int) *types.Binding { return lo.Map(viewNames, func(viewName string, _ int) *types.Binding {
return &types.Binding{ return &types.Binding{
ViewName: viewName, ViewName: viewName,
Key: config.GetValidatedKeyBindingKey(customCommand.Key), Key: []gocui.Key{config.GetValidatedKeyBindingKey(customCommand.Key)},
Handler: handler, Handler: handler,
Description: customCommand.GetDescription(), Description: customCommand.GetDescription(),
} }

View file

@ -259,9 +259,10 @@ type MenuItem struct {
// Only applies when Label is used // Only applies when Label is used
OpensMenu bool OpensMenu bool
// If Key is defined it allows the user to press the key to invoke the menu // If Key is non-empty, the user can press any of these keys to invoke the
// item, as opposed to having to navigate to it // menu item, as opposed to having to navigate to it. Only the first key is
Key gocui.Key // shown in the menu; the alternates are matched silently.
Key []gocui.Key
// A widget to show in front of the menu item. Supported widget types are // A widget to show in front of the menu item. Supported widget types are
// checkboxes and radio buttons, // checkboxes and radio buttons,

View file

@ -239,7 +239,7 @@ type OnFocusLostOpts struct {
type ContextKey string type ContextKey string
type KeybindingsOpts struct { type KeybindingsOpts struct {
GetKey func(key string) gocui.Key GetKey func(key string) []gocui.Key
Config config.KeybindingConfig Config config.KeybindingConfig
Guards KeybindingGuards Guards KeybindingGuards
} }

View file

@ -11,7 +11,7 @@ import (
type Binding struct { type Binding struct {
ViewName string ViewName string
Handler func() error Handler func() error
Key gocui.Key Key []gocui.Key
Description string Description string
// DescriptionFunc is used instead of Description if non-nil, and is useful for dynamic // DescriptionFunc is used instead of Description if non-nil, and is useful for dynamic
// descriptions that change depending on context. Important: this must not be an expensive call. // descriptions that change depending on context. Important: this must not be an expensive call.