Keep a filtering menu navigable whatever the keybindings are

The keys for paging through a menu are ',' and '.' by default, and there
is no non-printable alternative for them, so a menu that filters as you
type would lose paging altogether as soon as the user typed anything. The
same goes for confirming and cancelling if those keys are configured as
printable ones.

So bind the physical keys for all of it, on top of whatever is configured,
and only where they aren't the configured keys anyway.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-08-25 09:34:24 +02:00
parent fe9b990c4c
commit 2048c7f0a6
2 changed files with 62 additions and 3 deletions

View file

@ -138,6 +138,9 @@ func (gui *Gui) resetHelpersAndControllers() {
common := controllers.NewControllerCommon(helperCommon, gui)
listControllerFactory := controllers.NewListControllerFactory(common)
menuListController := listControllerFactory.Create(gui.State.Contexts.Menu)
syncController := controllers.NewSyncController(
common,
)
@ -156,7 +159,7 @@ func (gui *Gui) resetHelpersAndControllers() {
remoteBranchesController := controllers.NewRemoteBranchesController(common)
menuController := controllers.NewMenuController(common)
menuController := controllers.NewMenuController(common, menuListController)
localCommitsController := controllers.NewLocalCommitsController(common, syncController.HandlePull)
tagsController := controllers.NewTagsController(common)
filesController := controllers.NewFilesController(
@ -359,6 +362,7 @@ func (gui *Gui) resetHelpersAndControllers() {
controllers.AttachControllers(gui.State.Contexts.Menu,
menuController,
menuListController,
)
controllers.AttachControllers(gui.State.Contexts.CommitMessage,
@ -412,8 +416,11 @@ func (gui *Gui) resetHelpersAndControllers() {
)
// this must come last so that we've got our click handlers defined against the context
listControllerFactory := controllers.NewListControllerFactory(common)
for _, context := range gui.c.Context().AllList() {
if context == gui.State.Contexts.Menu {
// already attached above, next to the menu controller that delegates to it
continue
}
controllers.AttachControllers(context, listControllerFactory.Create(context))
}
}

View file

@ -1,20 +1,26 @@
package controllers
import (
"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/types"
"github.com/samber/lo"
)
type MenuController struct {
baseController
*ListControllerTrait[*types.MenuItem]
c *ControllerCommon
// for delegating navigation to, see physicalKeyBindings
listController *ListController
}
var _ types.IController = &MenuController{}
func NewMenuController(
c *ControllerCommon,
listController *ListController,
) *MenuController {
return &MenuController{
baseController: baseController{},
@ -24,7 +30,8 @@ func NewMenuController(
c.Contexts().Menu.GetSelected,
c.Contexts().Menu.GetSelectedItems,
),
c: c,
c: c,
listController: listController,
}
}
@ -52,6 +59,51 @@ func (self *MenuController) GetKeybindings(opts types.KeybindingsOpts) []*types.
},
}
if self.context().FilterAsYouType() {
bindings = append(bindings, self.physicalKeyBindings(opts)...)
}
return bindings
}
// In a menu that filters as you type, the keys configured for driving the menu
// may all be printable, and printable keys become filter text once the user
// starts typing. These keys can't, so binding them on top guarantees that the
// menu stays usable no matter how the keybindings are configured.
func (self *MenuController) physicalKeyBindings(opts types.KeybindingsOpts) []*types.Binding {
candidates := []struct {
key gocui.Key
configured config.Keybinding
binding *types.Binding
}{
{
key: gocui.NewKeyName(gocui.KeyEnter),
configured: opts.Config.Universal.ConfirmMenu,
binding: &types.Binding{
Handler: self.withItem(self.press),
GetDisabledReason: self.require(self.singleItemSelected()),
},
},
{gocui.NewKeyName(gocui.KeyEsc), opts.Config.Universal.Return, &types.Binding{Handler: self.close}},
{gocui.NewKeyName(gocui.KeyArrowUp), opts.Config.Universal.PrevItem, &types.Binding{Handler: self.listController.HandlePrevLine}},
{gocui.NewKeyName(gocui.KeyArrowDown), opts.Config.Universal.NextItem, &types.Binding{Handler: self.listController.HandleNextLine}},
{gocui.NewKeyName(gocui.KeyPgup), opts.Config.Universal.PrevPage, &types.Binding{Handler: self.listController.HandlePrevPage}},
{gocui.NewKeyName(gocui.KeyPgdn), opts.Config.Universal.NextPage, &types.Binding{Handler: self.listController.HandleNextPage}},
{gocui.NewKeyName(gocui.KeyHome), opts.Config.Universal.GotoTop, &types.Binding{Handler: self.listController.HandleGotoTop}},
{gocui.NewKeyName(gocui.KeyEnd), opts.Config.Universal.GotoBottom, &types.Binding{Handler: self.listController.HandleGotoBottom}},
}
bindings := []*types.Binding{}
for _, candidate := range candidates {
if lo.Contains(opts.GetKeys(candidate.configured), candidate.key) {
// this key is the configured one, so it drives the menu already
continue
}
candidate.binding.Keys = []gocui.Key{candidate.key}
bindings = append(bindings, candidate.binding)
}
return bindings
}