From 9c226eed37f6a1f8ad12f5e7935efe9383eab450 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 27 Mar 2022 17:15:17 +1100 Subject: [PATCH] allow menu to store keybindings for quick menu navigation --- pkg/cheatsheet/generate.go | 8 +- pkg/gui/context/menu_context.go | 61 ++++++++-- pkg/gui/controllers/menu_controller.go | 12 +- pkg/gui/keybindings.go | 158 +----------------------- pkg/gui/keybindings/keybindings.go | 160 +++++++++++++++++++++++++ pkg/gui/menu_panel.go | 6 + pkg/gui/options_menu_panel.go | 9 +- pkg/gui/types/common.go | 4 + 8 files changed, 235 insertions(+), 183 deletions(-) create mode 100644 pkg/gui/keybindings/keybindings.go diff --git a/pkg/cheatsheet/generate.go b/pkg/cheatsheet/generate.go index 6c641fa1f..5852f127f 100644 --- a/pkg/cheatsheet/generate.go +++ b/pkg/cheatsheet/generate.go @@ -17,7 +17,7 @@ import ( "github.com/jesseduffield/generics/slices" "github.com/jesseduffield/lazygit/pkg/app" "github.com/jesseduffield/lazygit/pkg/config" - "github.com/jesseduffield/lazygit/pkg/gui" + "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/integration" @@ -135,7 +135,7 @@ func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*b bindingsByHeader, func(header header, hBindings []*types.Binding) headerWithBindings { uniqBindings := lo.UniqBy(hBindings, func(binding *types.Binding) string { - return binding.Description + gui.GetKeyDisplay(binding.Key) + return binding.Description + keybindings.GetKeyDisplay(binding.Key) }) return headerWithBindings{ @@ -203,10 +203,10 @@ func formatBinding(binding *types.Binding) string { if binding.Alternative != "" { return fmt.Sprintf( " %s: %s (%s)\n", - gui.GetKeyDisplay(binding.Key), + keybindings.GetKeyDisplay(binding.Key), binding.Description, binding.Alternative, ) } - return fmt.Sprintf(" %s: %s\n", gui.GetKeyDisplay(binding.Key), binding.Description) + return fmt.Sprintf(" %s: %s\n", keybindings.GetKeyDisplay(binding.Key), binding.Description) } diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index 1f5654902..8082dc961 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -3,6 +3,7 @@ package context import ( "github.com/jesseduffield/generics/slices" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -79,15 +80,59 @@ func (self *MenuViewModel) SetMenuItems(items []*types.MenuItem) { // TODO: move into presentation package func (self *MenuViewModel) GetDisplayStrings(_startIdx int, _length int) [][]string { - return slices.Map(self.menuItems, func(item *types.MenuItem) []string { - if item.DisplayStrings != nil { - return item.DisplayStrings - } + showKeys := slices.Some(self.menuItems, func(item *types.MenuItem) bool { + return item.Key != nil + }) - styledStr := item.DisplayString - if item.OpensMenu { - styledStr = presentation.OpensMenuStyle(styledStr) + return slices.Map(self.menuItems, func(item *types.MenuItem) []string { + displayStrings := getItemDisplayStrings(item) + if showKeys { + displayStrings = slices.Prepend(displayStrings, keybindings.GetKeyDisplay(item.Key)) } - return []string{styledStr} + return displayStrings }) } + +func getItemDisplayStrings(item *types.MenuItem) []string { + if item.DisplayStrings != nil { + return item.DisplayStrings + } + + styledStr := item.DisplayString + if item.OpensMenu { + styledStr = presentation.OpensMenuStyle(styledStr) + } + return []string{styledStr} +} + +func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { + basicBindings := self.ListContextTrait.GetKeybindings(opts) + menuItemsWithKeys := slices.Filter(self.menuItems, func(item *types.MenuItem) bool { + return item.Key != nil + }) + + menuItemBindings := slices.Map(menuItemsWithKeys, func(item *types.MenuItem) *types.Binding { + return &types.Binding{ + Key: item.Key, + Handler: func() error { return self.OnMenuPress(item) }, + } + }) + + // appending because that means the menu item bindings have lower precedence. + // So if a basic binding is to escape from the menu, we want that to still be + // what happens when you press escape. This matters when we're showing the menu + // for all keybindings of say the files context. + return append(basicBindings, menuItemBindings...) +} + +func (self *MenuContext) OnMenuPress(selectedItem *types.MenuItem) error { + if err := self.c.PopContext(); err != nil { + return err + } + + if err := selectedItem.OnPress(); err != nil { + return err + } + + return nil +} diff --git a/pkg/gui/controllers/menu_controller.go b/pkg/gui/controllers/menu_controller.go index 910e50668..9501a0bf2 100644 --- a/pkg/gui/controllers/menu_controller.go +++ b/pkg/gui/controllers/menu_controller.go @@ -49,17 +49,7 @@ func (self *MenuController) GetOnClick() func() error { } func (self *MenuController) press() error { - selectedItem := self.context().GetSelected() - - if err := self.c.PopContext(); err != nil { - return err - } - - if err := selectedItem.OnPress(); err != nil { - return err - } - - return nil + return self.context().OnMenuPress(self.context().GetSelected()) } func (self *MenuController) close() error { diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 3623a6741..7a2d5f757 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -1,7 +1,6 @@ package gui import ( - "fmt" "log" "strings" "unicode/utf8" @@ -10,170 +9,19 @@ import ( "github.com/jesseduffield/lazygit/pkg/constants" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" + "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/types" ) -var keyMapReversed = map[gocui.Key]string{ - gocui.KeyF1: "f1", - gocui.KeyF2: "f2", - gocui.KeyF3: "f3", - gocui.KeyF4: "f4", - gocui.KeyF5: "f5", - gocui.KeyF6: "f6", - gocui.KeyF7: "f7", - gocui.KeyF8: "f8", - gocui.KeyF9: "f9", - gocui.KeyF10: "f10", - gocui.KeyF11: "f11", - gocui.KeyF12: "f12", - gocui.KeyInsert: "insert", - gocui.KeyDelete: "delete", - gocui.KeyHome: "home", - gocui.KeyEnd: "end", - gocui.KeyPgup: "pgup", - gocui.KeyPgdn: "pgdown", - gocui.KeyArrowUp: "▲", - gocui.KeyArrowDown: "▼", - gocui.KeyArrowLeft: "◄", - gocui.KeyArrowRight: "►", - gocui.KeyTab: "tab", // ctrl+i - gocui.KeyBacktab: "shift+tab", - gocui.KeyEnter: "enter", // ctrl+m - gocui.KeyAltEnter: "alt+enter", - gocui.KeyEsc: "esc", // ctrl+[, ctrl+3 - gocui.KeyBackspace: "backspace", // ctrl+h - gocui.KeyCtrlSpace: "ctrl+space", // ctrl+~, ctrl+2 - gocui.KeyCtrlSlash: "ctrl+/", // ctrl+_ - gocui.KeySpace: "space", - gocui.KeyCtrlA: "ctrl+a", - gocui.KeyCtrlB: "ctrl+b", - gocui.KeyCtrlC: "ctrl+c", - gocui.KeyCtrlD: "ctrl+d", - gocui.KeyCtrlE: "ctrl+e", - gocui.KeyCtrlF: "ctrl+f", - gocui.KeyCtrlG: "ctrl+g", - gocui.KeyCtrlJ: "ctrl+j", - gocui.KeyCtrlK: "ctrl+k", - gocui.KeyCtrlL: "ctrl+l", - gocui.KeyCtrlN: "ctrl+n", - gocui.KeyCtrlO: "ctrl+o", - gocui.KeyCtrlP: "ctrl+p", - gocui.KeyCtrlQ: "ctrl+q", - gocui.KeyCtrlR: "ctrl+r", - gocui.KeyCtrlS: "ctrl+s", - gocui.KeyCtrlT: "ctrl+t", - gocui.KeyCtrlU: "ctrl+u", - gocui.KeyCtrlV: "ctrl+v", - gocui.KeyCtrlW: "ctrl+w", - gocui.KeyCtrlX: "ctrl+x", - gocui.KeyCtrlY: "ctrl+y", - gocui.KeyCtrlZ: "ctrl+z", - gocui.KeyCtrl4: "ctrl+4", // ctrl+\ - gocui.KeyCtrl5: "ctrl+5", // ctrl+] - gocui.KeyCtrl6: "ctrl+6", - gocui.KeyCtrl8: "ctrl+8", - gocui.MouseWheelUp: "mouse wheel up", - gocui.MouseWheelDown: "mouse wheel down", -} - -var keymap = map[string]types.Key{ - "": gocui.KeyCtrlA, - "": gocui.KeyCtrlB, - "": gocui.KeyCtrlC, - "": gocui.KeyCtrlD, - "": gocui.KeyCtrlE, - "": gocui.KeyCtrlF, - "": gocui.KeyCtrlG, - "": gocui.KeyCtrlH, - "": gocui.KeyCtrlI, - "": gocui.KeyCtrlJ, - "": gocui.KeyCtrlK, - "": gocui.KeyCtrlL, - "": gocui.KeyCtrlM, - "": gocui.KeyCtrlN, - "": gocui.KeyCtrlO, - "": gocui.KeyCtrlP, - "": gocui.KeyCtrlQ, - "": gocui.KeyCtrlR, - "": gocui.KeyCtrlS, - "": gocui.KeyCtrlT, - "": gocui.KeyCtrlU, - "": gocui.KeyCtrlV, - "": gocui.KeyCtrlW, - "": gocui.KeyCtrlX, - "": gocui.KeyCtrlY, - "": gocui.KeyCtrlZ, - "": gocui.KeyCtrlTilde, - "": gocui.KeyCtrl2, - "": gocui.KeyCtrl3, - "": gocui.KeyCtrl4, - "": gocui.KeyCtrl5, - "": gocui.KeyCtrl6, - "": gocui.KeyCtrl7, - "": gocui.KeyCtrl8, - "": gocui.KeyCtrlSpace, - "": gocui.KeyCtrlBackslash, - "": gocui.KeyCtrlLsqBracket, - "": gocui.KeyCtrlRsqBracket, - "": gocui.KeyCtrlSlash, - "": gocui.KeyCtrlUnderscore, - "": gocui.KeyBackspace, - "": gocui.KeyTab, - "": gocui.KeyBacktab, - "": gocui.KeyEnter, - "": gocui.KeyAltEnter, - "": gocui.KeyEsc, - "": gocui.KeySpace, - "": gocui.KeyF1, - "": gocui.KeyF2, - "": gocui.KeyF3, - "": gocui.KeyF4, - "": gocui.KeyF5, - "": gocui.KeyF6, - "": gocui.KeyF7, - "": gocui.KeyF8, - "": gocui.KeyF9, - "": gocui.KeyF10, - "": gocui.KeyF11, - "": gocui.KeyF12, - "": gocui.KeyInsert, - "": gocui.KeyDelete, - "": gocui.KeyHome, - "": gocui.KeyEnd, - "": gocui.KeyPgup, - "": gocui.KeyPgdn, - "": gocui.KeyArrowUp, - "": gocui.KeyArrowDown, - "": gocui.KeyArrowLeft, - "": gocui.KeyArrowRight, -} - func (gui *Gui) getKeyDisplay(name string) string { key := gui.getKey(name) - return GetKeyDisplay(key) -} - -func GetKeyDisplay(key types.Key) string { - keyInt := 0 - - switch key := key.(type) { - case rune: - keyInt = int(key) - case gocui.Key: - value, ok := keyMapReversed[key] - if ok { - return value - } - keyInt = int(key) - } - - return fmt.Sprintf("%c", keyInt) + return keybindings.GetKeyDisplay(key) } func (gui *Gui) getKey(key string) types.Key { runeCount := utf8.RuneCountInString(key) if runeCount > 1 { - binding := keymap[strings.ToLower(key)] + binding := keybindings.Keymap[strings.ToLower(key)] if binding == nil { log.Fatalf("Unrecognized key %s for keybinding. For permitted values see %s", strings.ToLower(key), constants.Links.Docs.CustomKeybindings) } else { diff --git a/pkg/gui/keybindings/keybindings.go b/pkg/gui/keybindings/keybindings.go new file mode 100644 index 000000000..fba2528a4 --- /dev/null +++ b/pkg/gui/keybindings/keybindings.go @@ -0,0 +1,160 @@ +package keybindings + +import ( + "fmt" + + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +var KeyMapReversed = map[gocui.Key]string{ + gocui.KeyF1: "f1", + gocui.KeyF2: "f2", + gocui.KeyF3: "f3", + gocui.KeyF4: "f4", + gocui.KeyF5: "f5", + gocui.KeyF6: "f6", + gocui.KeyF7: "f7", + gocui.KeyF8: "f8", + gocui.KeyF9: "f9", + gocui.KeyF10: "f10", + gocui.KeyF11: "f11", + gocui.KeyF12: "f12", + gocui.KeyInsert: "insert", + gocui.KeyDelete: "delete", + gocui.KeyHome: "home", + gocui.KeyEnd: "end", + gocui.KeyPgup: "pgup", + gocui.KeyPgdn: "pgdown", + gocui.KeyArrowUp: "▲", + gocui.KeyArrowDown: "▼", + gocui.KeyArrowLeft: "◄", + gocui.KeyArrowRight: "►", + gocui.KeyTab: "tab", // ctrl+i + gocui.KeyBacktab: "shift+tab", + gocui.KeyEnter: "enter", // ctrl+m + gocui.KeyAltEnter: "alt+enter", + gocui.KeyEsc: "esc", // ctrl+[, ctrl+3 + gocui.KeyBackspace: "backspace", // ctrl+h + gocui.KeyCtrlSpace: "ctrl+space", // ctrl+~, ctrl+2 + gocui.KeyCtrlSlash: "ctrl+/", // ctrl+_ + gocui.KeySpace: "space", + gocui.KeyCtrlA: "ctrl+a", + gocui.KeyCtrlB: "ctrl+b", + gocui.KeyCtrlC: "ctrl+c", + gocui.KeyCtrlD: "ctrl+d", + gocui.KeyCtrlE: "ctrl+e", + gocui.KeyCtrlF: "ctrl+f", + gocui.KeyCtrlG: "ctrl+g", + gocui.KeyCtrlJ: "ctrl+j", + gocui.KeyCtrlK: "ctrl+k", + gocui.KeyCtrlL: "ctrl+l", + gocui.KeyCtrlN: "ctrl+n", + gocui.KeyCtrlO: "ctrl+o", + gocui.KeyCtrlP: "ctrl+p", + gocui.KeyCtrlQ: "ctrl+q", + gocui.KeyCtrlR: "ctrl+r", + gocui.KeyCtrlS: "ctrl+s", + gocui.KeyCtrlT: "ctrl+t", + gocui.KeyCtrlU: "ctrl+u", + gocui.KeyCtrlV: "ctrl+v", + gocui.KeyCtrlW: "ctrl+w", + gocui.KeyCtrlX: "ctrl+x", + gocui.KeyCtrlY: "ctrl+y", + gocui.KeyCtrlZ: "ctrl+z", + gocui.KeyCtrl4: "ctrl+4", // ctrl+\ + gocui.KeyCtrl5: "ctrl+5", // ctrl+] + gocui.KeyCtrl6: "ctrl+6", + gocui.KeyCtrl8: "ctrl+8", + gocui.MouseWheelUp: "mouse wheel up", + gocui.MouseWheelDown: "mouse wheel down", +} + +var Keymap = map[string]types.Key{ + "": gocui.KeyCtrlA, + "": gocui.KeyCtrlB, + "": gocui.KeyCtrlC, + "": gocui.KeyCtrlD, + "": gocui.KeyCtrlE, + "": gocui.KeyCtrlF, + "": gocui.KeyCtrlG, + "": gocui.KeyCtrlH, + "": gocui.KeyCtrlI, + "": gocui.KeyCtrlJ, + "": gocui.KeyCtrlK, + "": gocui.KeyCtrlL, + "": gocui.KeyCtrlM, + "": gocui.KeyCtrlN, + "": gocui.KeyCtrlO, + "": gocui.KeyCtrlP, + "": gocui.KeyCtrlQ, + "": gocui.KeyCtrlR, + "": gocui.KeyCtrlS, + "": gocui.KeyCtrlT, + "": gocui.KeyCtrlU, + "": gocui.KeyCtrlV, + "": gocui.KeyCtrlW, + "": gocui.KeyCtrlX, + "": gocui.KeyCtrlY, + "": gocui.KeyCtrlZ, + "": gocui.KeyCtrlTilde, + "": gocui.KeyCtrl2, + "": gocui.KeyCtrl3, + "": gocui.KeyCtrl4, + "": gocui.KeyCtrl5, + "": gocui.KeyCtrl6, + "": gocui.KeyCtrl7, + "": gocui.KeyCtrl8, + "": gocui.KeyCtrlSpace, + "": gocui.KeyCtrlBackslash, + "": gocui.KeyCtrlLsqBracket, + "": gocui.KeyCtrlRsqBracket, + "": gocui.KeyCtrlSlash, + "": gocui.KeyCtrlUnderscore, + "": gocui.KeyBackspace, + "": gocui.KeyTab, + "": gocui.KeyBacktab, + "": gocui.KeyEnter, + "": gocui.KeyAltEnter, + "": gocui.KeyEsc, + "": gocui.KeySpace, + "": gocui.KeyF1, + "": gocui.KeyF2, + "": gocui.KeyF3, + "": gocui.KeyF4, + "": gocui.KeyF5, + "": gocui.KeyF6, + "": gocui.KeyF7, + "": gocui.KeyF8, + "": gocui.KeyF9, + "": gocui.KeyF10, + "": gocui.KeyF11, + "": gocui.KeyF12, + "": gocui.KeyInsert, + "": gocui.KeyDelete, + "": gocui.KeyHome, + "": gocui.KeyEnd, + "": gocui.KeyPgup, + "": gocui.KeyPgdn, + "": gocui.KeyArrowUp, + "": gocui.KeyArrowDown, + "": gocui.KeyArrowLeft, + "": gocui.KeyArrowRight, +} + +func GetKeyDisplay(key types.Key) string { + keyInt := 0 + + switch key := key.(type) { + case rune: + keyInt = int(key) + case gocui.Key: + value, ok := KeyMapReversed[key] + if ok { + return value + } + keyInt = int(key) + } + + return fmt.Sprintf("%c", keyInt) +} diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index 7bf68c519..8bce02011 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -46,6 +46,12 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error { gui.State.Contexts.Menu.SetMenuItems(opts.Items) gui.State.Contexts.Menu.SetSelectedLineIdx(0) + + // resetting keybindings so that the menu-specific keybindings are registered + if err := gui.resetKeybindings(); err != nil { + return err + } + _ = gui.c.PostRefreshUpdate(gui.State.Contexts.Menu) // TODO: ensure that if we're opened a menu from within a menu that it renders correctly diff --git a/pkg/gui/options_menu_panel.go b/pkg/gui/options_menu_panel.go index 20df7e091..76452296b 100644 --- a/pkg/gui/options_menu_panel.go +++ b/pkg/gui/options_menu_panel.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" @@ -22,7 +23,7 @@ func (gui *Gui) getBindings(context types.Context) []*types.Binding { bindings = append(customBindings, bindings...) for _, binding := range bindings { - if GetKeyDisplay(binding.Key) != "" && binding.Description != "" { + if keybindings.GetKeyDisplay(binding.Key) != "" && binding.Description != "" { if len(binding.Contexts) == 0 && binding.ViewName == "" { bindingsGlobal = append(bindingsGlobal, binding) } else if binding.Tag == "navigation" { @@ -65,17 +66,15 @@ func (gui *Gui) handleCreateOptionsMenu() error { menuItems := slices.Map(bindings, func(binding *types.Binding) *types.MenuItem { return &types.MenuItem{ - DisplayStrings: []string{GetKeyDisplay(binding.Key), gui.displayDescription(binding)}, + DisplayString: gui.displayDescription(binding), OnPress: func() error { if binding.Key == nil { return nil } - if err := gui.c.PopContext(); err != nil { - return err - } return binding.Handler() }, + Key: binding.Key, } }) diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index cb39c87b5..986dd6664 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -105,6 +105,10 @@ type MenuItem struct { OnPress func() error // only applies when displayString is used OpensMenu bool + + // if Key is defined it allows the user to press the key to invoke the menu + // item, as opposed to having to navigate to it + Key Key } type Model struct {