diff --git a/docs-master/keybindings/Keybindings_en.md b/docs-master/keybindings/Keybindings_en.md index ca1541dd2..7e80ceb7a 100644 --- a/docs-master/keybindings/Keybindings_en.md +++ b/docs-master/keybindings/Keybindings_en.md @@ -221,8 +221,8 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | Key | Action | Info | |-----|--------|-------------| -| `` mouse wheel down (fn+up) `` | Scroll down | | -| `` mouse wheel up (fn+down) `` | Scroll up | | +| `` (fn+up) `` | Scroll down | | +| `` (fn+down) `` | Scroll up | | | `` `` | Switch view | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | | `` / `` | Search the current view by text | | diff --git a/docs-master/keybindings/Keybindings_ja.md b/docs-master/keybindings/Keybindings_ja.md index 69479db13..bbb53a74b 100644 --- a/docs-master/keybindings/Keybindings_ja.md +++ b/docs-master/keybindings/Keybindings_ja.md @@ -304,8 +304,8 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味 | Key | Action | Info | |-----|--------|-------------| -| `` mouse wheel down (fn+up) `` | 下にスクロール | | -| `` mouse wheel up (fn+down) `` | 上にスクロール | | +| `` (fn+up) `` | 下にスクロール | | +| `` (fn+down) `` | 上にスクロール | | | `` `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 | | `` `` | サイドパネルに戻る | | | `` / `` | 現在のビューをテキストで検索 | | diff --git a/docs-master/keybindings/Keybindings_ko.md b/docs-master/keybindings/Keybindings_ko.md index eeb5ed885..0f18930f6 100644 --- a/docs-master/keybindings/Keybindings_ko.md +++ b/docs-master/keybindings/Keybindings_ko.md @@ -160,8 +160,8 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | Key | Action | Info | |-----|--------|-------------| -| `` mouse wheel down (fn+up) `` | 아래로 스크롤 | | -| `` mouse wheel up (fn+down) `` | 위로 스크롤 | | +| `` (fn+up) `` | 아래로 스크롤 | | +| `` (fn+down) `` | 위로 스크롤 | | | `` `` | 패널 전환 | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | | `` / `` | 검색 시작 | | diff --git a/docs-master/keybindings/Keybindings_nl.md b/docs-master/keybindings/Keybindings_nl.md index 21b8c5b4c..b2d0d8c87 100644 --- a/docs-master/keybindings/Keybindings_nl.md +++ b/docs-master/keybindings/Keybindings_nl.md @@ -229,8 +229,8 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | Key | Action | Info | |-----|--------|-------------| -| `` mouse wheel down (fn+up) `` | Scroll omlaag | | -| `` mouse wheel up (fn+down) `` | Scroll omhoog | | +| `` (fn+up) `` | Scroll omlaag | | +| `` (fn+down) `` | Scroll omhoog | | | `` `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | | `` / `` | Start met zoeken | | diff --git a/docs-master/keybindings/Keybindings_pl.md b/docs-master/keybindings/Keybindings_pl.md index 622a134fd..672c0e3df 100644 --- a/docs-master/keybindings/Keybindings_pl.md +++ b/docs-master/keybindings/Keybindings_pl.md @@ -179,8 +179,8 @@ _Legenda: `` oznacza ctrl+b, `` oznacza alt+b, `B` oznacza shift+b_ | Key | Action | Info | |-----|--------|-------------| -| `` mouse wheel down (fn+up) `` | Przewiń w dół | | -| `` mouse wheel up (fn+down) `` | Przewiń w górę | | +| `` (fn+up) `` | Przewiń w dół | | +| `` (fn+down) `` | Przewiń w górę | | | `` `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). | | `` `` | Exit back to side panel | | | `` / `` | Szukaj w bieżącym widoku po tekście | | diff --git a/docs-master/keybindings/Keybindings_pt.md b/docs-master/keybindings/Keybindings_pt.md index 81dc4085e..9bb9ab097 100644 --- a/docs-master/keybindings/Keybindings_pt.md +++ b/docs-master/keybindings/Keybindings_pt.md @@ -233,8 +233,8 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | Key | Action | Info | |-----|--------|-------------| -| `` mouse wheel down (fn+up) `` | Rolar para baixo | | -| `` mouse wheel up (fn+down) `` | Rolar para cima | | +| `` (fn+up) `` | Rolar para baixo | | +| `` (fn+down) `` | Rolar para cima | | | `` `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). | | `` `` | Exit back to side panel | | | `` / `` | Pesquisar na visualização atual por texto | | diff --git a/docs-master/keybindings/Keybindings_ru.md b/docs-master/keybindings/Keybindings_ru.md index b4531eb73..bd738f3ed 100644 --- a/docs-master/keybindings/Keybindings_ru.md +++ b/docs-master/keybindings/Keybindings_ru.md @@ -104,8 +104,8 @@ _Связки клавиш_ | Key | Action | Info | |-----|--------|-------------| -| `` mouse wheel down (fn+up) `` | Прокрутить вниз | | -| `` mouse wheel up (fn+down) `` | Прокрутить вверх | | +| `` (fn+up) `` | Прокрутить вниз | | +| `` (fn+down) `` | Прокрутить вверх | | | `` `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | | `` / `` | Найти | | diff --git a/docs-master/keybindings/Keybindings_zh-CN.md b/docs-master/keybindings/Keybindings_zh-CN.md index 0385e486b..b16dff18c 100644 --- a/docs-master/keybindings/Keybindings_zh-CN.md +++ b/docs-master/keybindings/Keybindings_zh-CN.md @@ -332,8 +332,8 @@ _图例:`` 意味着ctrl+b, `意味着Alt+b, `B` 意味着shift+b_ | Key | Action | Info | |-----|--------|-------------| -| `` mouse wheel down (fn+up) `` | 向下滚动 | | -| `` mouse wheel up (fn+down) `` | 向上滚动 | | +| `` (fn+up) `` | 向下滚动 | | +| `` (fn+down) `` | 向上滚动 | | | `` `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) | | `` `` | 退出回到侧边面板 | | | `` / `` | 开始搜索 | | diff --git a/docs-master/keybindings/Keybindings_zh-TW.md b/docs-master/keybindings/Keybindings_zh-TW.md index c0579e0ce..3d637160b 100644 --- a/docs-master/keybindings/Keybindings_zh-TW.md +++ b/docs-master/keybindings/Keybindings_zh-TW.md @@ -80,8 +80,8 @@ _說明:`` 表示 Ctrl+B、`` 表示 Alt+B,`B`表示 Shift+B | Key | Action | Info | |-----|--------|-------------| -| `` mouse wheel down (fn+up) `` | 向下捲動 | | -| `` mouse wheel up (fn+down) `` | 向上捲動 | | +| `` (fn+up) `` | 向下捲動 | | +| `` (fn+down) `` | 向上捲動 | | | `` `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | | `` / `` | 搜尋 | | diff --git a/pkg/config/keynames.go b/pkg/config/keynames.go index a6fe9e74e..5be386ecc 100644 --- a/pkg/config/keynames.go +++ b/pkg/config/keynames.go @@ -14,63 +14,35 @@ import ( // docs/keybindings/Custom_Keybindings.md as well var labelByKey = map[gocui.KeyName]string{ - 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.KeyShiftArrowUp: "", - gocui.KeyArrowDown: "", - gocui.KeyShiftArrowDown: "", - gocui.KeyArrowLeft: "", - gocui.KeyArrowRight: "", - gocui.KeyTab: "", // - gocui.KeyBacktab: "", - gocui.KeyEnter: "", // - gocui.KeyAltEnter: "", - gocui.KeyEsc: "", // , - gocui.KeyBackspace: "", // - gocui.KeySpace: "", - gocui.KeyCtrlA: "", - gocui.KeyCtrlB: "", - gocui.KeyCtrlC: "", - gocui.KeyCtrlD: "", - gocui.KeyCtrlE: "", - gocui.KeyCtrlF: "", - gocui.KeyCtrlG: "", - gocui.KeyCtrlJ: "", - gocui.KeyCtrlK: "", - gocui.KeyCtrlL: "", - 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.KeyCtrl8: "", - gocui.MouseWheelUp: "mouse wheel up", - gocui.MouseWheelDown: "mouse wheel down", + 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: "up", + gocui.KeyArrowDown: "down", + gocui.KeyArrowLeft: "left", + gocui.KeyArrowRight: "right", + gocui.KeyTab: "tab", + gocui.KeyBacktab: "backtab", + gocui.KeyEnter: "enter", + gocui.KeyEsc: "esc", + gocui.KeyBackspace: "backspace", + gocui.MouseWheelUp: "mouse wheel up", + gocui.MouseWheelDown: "mouse wheel down", } var keyByLabel = lo.Invert(labelByKey) @@ -80,16 +52,44 @@ func LabelForKey(key gocui.Key) string { return "" } + label := "" + if key.Mod()&gocui.ModCtrl != 0 { + label += "c-" + } + if key.Mod()&gocui.ModAlt != 0 { + label += "a-" + } + if key.Mod()&gocui.ModShift != 0 { + label += "s-" + } + if key.Mod()&gocui.ModMeta != 0 { + label += "m-" + } + if key.KeyName() == gocui.KeyName(tcell.KeyRune) { - return key.Str() + if key.Str() == " " { + label += "space" + } else if key.Str() == "-" && key.Mod() != gocui.ModNone { + label += "minus" + } else if key.Str() == "+" && key.Mod() != gocui.ModNone { + label += "plus" + } else { + label += key.Str() + } + } else { + value, ok := labelByKey[key.KeyName()] + if ok { + label += value + } else { + label += "unknown" + } } - value, ok := labelByKey[key.KeyName()] - if ok { - return value + if utf8.RuneCountInString(label) > 1 { + label = "<" + label + ">" } - return "unknown" + return label } func KeyFromLabel(label string) (gocui.Key, bool) { @@ -97,16 +97,99 @@ func KeyFromLabel(label string) (gocui.Key, bool) { return gocui.Key{}, true } - runeCount := utf8.RuneCountInString(label) - if runeCount > 1 { - keyName, ok := keyByLabel[strings.ToLower(label)] - if !ok { - return gocui.Key{}, false - } - return gocui.NewKeyName(keyName), true + if strings.HasPrefix(label, "<") && strings.HasSuffix(label, ">") { + label = label[1 : len(label)-1] } - return gocui.NewKeyRune([]rune(label)[0]), true + mod := gocui.ModNone + for { + // A bare "-" or "+" with any (or no) modifiers is a literal rune + // key; this also covers lenient forms like `` and ``, + // neither of which we emit (we use `` and ``). + if label == "-" || label == "+" { + return gocui.NewKeyStrMod(label, mod), true + } + + sepIdx := strings.IndexAny(label, "-+") + if sepIdx == -1 { + break + } + modStr, remainder := label[:sepIdx], label[sepIdx+1:] + + label = remainder + + switch modStr { + case "s", "shift": + if (mod & gocui.ModShift) != 0 { + return gocui.Key{}, false + } + mod |= gocui.ModShift + case "c", "ctrl": + if (mod & gocui.ModCtrl) != 0 { + return gocui.Key{}, false + } + mod |= gocui.ModCtrl + case "a", "alt": + if (mod & gocui.ModAlt) != 0 { + return gocui.Key{}, false + } + mod |= gocui.ModAlt + case "m", "meta": + if (mod & gocui.ModMeta) != 0 { + return gocui.Key{}, false + } + mod |= gocui.ModMeta + default: + return gocui.Key{}, false + } + } + + if label == "space" { + return gocui.NewKeyStrMod(" ", mod), true + } + + if label == "minus" { + if mod == gocui.ModShift { + return gocui.Key{}, false + } + return gocui.NewKeyStrMod("-", mod), true + } + + if label == "plus" { + if mod == gocui.ModShift { + return gocui.Key{}, false + } + return gocui.NewKeyStrMod("+", mod), true + } + + if keyName, ok := keyByLabel[label]; ok { + return gocui.NewKey(keyName, "", mod), true + } + + runeCount := utf8.RuneCountInString(label) + if runeCount != 1 { + return gocui.Key{}, false + } + + // Shift on a bare rune is invalid: terminals fold shift into the rune + // itself (shift+a arrives as "A"), so the binding could never fire. + // Space is exempt and handled above; combined with other modifiers, + // shift is fine because the terminal can't fold it into the rune then. + if mod == gocui.ModShift { + return gocui.Key{}, false + } + + // An ASCII uppercase letter with any modifier is invalid. Ctrl+letter + // events always arrive with a lowercase rune — control codes have no + // case distinction (the terminal sends the same byte for ctrl+a and + // ctrl+A), and CSI-u protocols report the unshifted codepoint with + // shift as a separate modifier (alt+shift+a → rune='a' mod=Alt|Shift). + // Users should write rather than . + if mod != gocui.ModNone && len(label) == 1 && label[0] >= 'A' && label[0] <= 'Z' { + return gocui.Key{}, false + } + + return gocui.NewKeyStrMod(label, mod), true } func isValidKeybindingKey(key string) bool { diff --git a/pkg/config/keynames_test.go b/pkg/config/keynames_test.go new file mode 100644 index 000000000..0176a1b06 --- /dev/null +++ b/pkg/config/keynames_test.go @@ -0,0 +1,686 @@ +package config + +import ( + "testing" + + "github.com/jesseduffield/lazygit/pkg/gocui" + "github.com/stretchr/testify/assert" +) + +func TestKeyFromLabel(t *testing.T) { + scenarios := []struct { + name string + label string + expectedKey gocui.Key + expectedOk bool + }{ + // Empty / disabled + { + name: "empty string returns unset key", + label: "", + expectedKey: gocui.Key{}, + expectedOk: true, + }, + { + name: " returns unset key", + label: "", + expectedKey: gocui.Key{}, + expectedOk: true, + }, + + // Plain runes (unwrapped) + { + name: "single lowercase letter", + label: "a", + expectedKey: gocui.NewKeyStrMod("a", gocui.ModNone), + expectedOk: true, + }, + { + name: "single uppercase letter", + label: "A", + expectedKey: gocui.NewKeyStrMod("A", gocui.ModNone), + expectedOk: true, + }, + { + name: "single digit", + label: "5", + expectedKey: gocui.NewKeyStrMod("5", gocui.ModNone), + expectedOk: true, + }, + { + name: "punctuation rune", + label: "?", + expectedKey: gocui.NewKeyStrMod("?", gocui.ModNone), + expectedOk: true, + }, + { + name: "multibyte rune", + label: "ñ", + expectedKey: gocui.NewKeyStrMod("ñ", gocui.ModNone), + expectedOk: true, + }, + { + name: "bare dash is treated as a rune", + label: "-", + expectedKey: gocui.NewKeyRune('-'), + expectedOk: true, + }, + + // Special key names (no modifiers, no brackets — though these are + // always wrapped in brackets in real configs, KeyFromLabel accepts + // the unwrapped form too) + { + name: "function key", + label: "f1", + expectedKey: gocui.NewKey(gocui.KeyF1, "", gocui.ModNone), + expectedOk: true, + }, + { + name: "function key wrapped in brackets", + label: "", + expectedKey: gocui.NewKey(gocui.KeyF12, "", gocui.ModNone), + expectedOk: true, + }, + { + name: "arrow key", + label: "", + expectedKey: gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModNone), + expectedOk: true, + }, + { + name: "tab", + label: "", + expectedKey: gocui.NewKey(gocui.KeyTab, "", gocui.ModNone), + expectedOk: true, + }, + { + name: "enter", + label: "", + expectedKey: gocui.NewKey(gocui.KeyEnter, "", gocui.ModNone), + expectedOk: true, + }, + { + name: "esc", + label: "", + expectedKey: gocui.NewKey(gocui.KeyEsc, "", gocui.ModNone), + expectedOk: true, + }, + { + name: "backspace", + label: "", + expectedKey: gocui.NewKey(gocui.KeyBackspace, "", gocui.ModNone), + expectedOk: true, + }, + { + name: "pgup", + label: "", + expectedKey: gocui.NewKey(gocui.KeyPgup, "", gocui.ModNone), + expectedOk: true, + }, + { + name: "pgdown", + label: "", + expectedKey: gocui.NewKey(gocui.KeyPgdn, "", gocui.ModNone), + expectedOk: true, + }, + { + name: "mouse wheel up", + label: "", + expectedKey: gocui.NewKey(gocui.MouseWheelUp, "", gocui.ModNone), + expectedOk: true, + }, + + // Space + { + name: "space keyword maps to space rune", + label: "", + expectedKey: gocui.NewKeyStrMod(" ", gocui.ModNone), + expectedOk: true, + }, + { + name: "space keyword without brackets", + label: "space", + expectedKey: gocui.NewKeyStrMod(" ", gocui.ModNone), + expectedOk: true, + }, + { + name: "ctrl+space", + label: "", + expectedKey: gocui.NewKeyStrMod(" ", gocui.ModCtrl), + expectedOk: true, + }, + + // Minus + { + name: "minus keyword maps to dash rune", + label: "", + expectedKey: gocui.NewKeyStrMod("-", gocui.ModNone), + expectedOk: true, + }, + { + name: "ctrl+minus via keyword", + label: "", + expectedKey: gocui.NewKeyStrMod("-", gocui.ModCtrl), + expectedOk: true, + }, + { + name: "ctrl+minus via lenient dash form", + label: "", + expectedKey: gocui.NewKeyStrMod("-", gocui.ModCtrl), + expectedOk: true, + }, + { + name: "alt+ctrl+minus via lenient dash form", + label: "", + expectedKey: gocui.NewKeyStrMod("-", gocui.ModAlt|gocui.ModCtrl), + expectedOk: true, + }, + + // Plus + { + name: "plus keyword maps to plus rune", + label: "", + expectedKey: gocui.NewKeyStrMod("+", gocui.ModNone), + expectedOk: true, + }, + { + name: "ctrl+plus via keyword", + label: "", + expectedKey: gocui.NewKeyStrMod("+", gocui.ModCtrl), + expectedOk: true, + }, + { + name: "ctrl+plus via long keyword and plus separator", + label: "", + expectedKey: gocui.NewKeyStrMod("+", gocui.ModCtrl), + expectedOk: true, + }, + { + name: "alt+shift+plus via keyword", + label: "", + expectedKey: gocui.NewKeyStrMod("+", gocui.ModAlt|gocui.ModShift), + expectedOk: true, + }, + { + name: "shift alone on plus is rejected", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + + // Modifiers with runes + { + name: "ctrl+letter", + label: "", + expectedKey: gocui.NewKeyStrMod("a", gocui.ModCtrl), + expectedOk: true, + }, + { + name: "alt+letter", + label: "", + expectedKey: gocui.NewKeyStrMod("x", gocui.ModAlt), + expectedOk: true, + }, + { + name: "meta+letter", + label: "", + expectedKey: gocui.NewKeyStrMod("z", gocui.ModMeta), + expectedOk: true, + }, + + // Long modifier names are accepted as synonyms for the short forms. + { + name: "ctrl long form", + label: "", + expectedKey: gocui.NewKeyStrMod("a", gocui.ModCtrl), + expectedOk: true, + }, + { + name: "alt long form", + label: "", + expectedKey: gocui.NewKeyStrMod("x", gocui.ModAlt), + expectedOk: true, + }, + { + name: "meta long form", + label: "", + expectedKey: gocui.NewKeyStrMod("z", gocui.ModMeta), + expectedOk: true, + }, + { + name: "shift long form combined with ctrl", + label: "", + expectedKey: gocui.NewKeyStrMod("a", gocui.ModShift|gocui.ModCtrl), + expectedOk: true, + }, + { + name: "long forms work with special keys", + label: "", + expectedKey: gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModCtrl), + expectedOk: true, + }, + { + name: "short and long forms can be mixed", + label: "", + expectedKey: gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModCtrl|gocui.ModShift), + expectedOk: true, + }, + { + name: "duplicate via mixed short and long form is rejected", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "unknown long modifier is rejected", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + + // Plus is accepted as an alternative modifier separator. + { + name: "plus separator with short form", + label: "", + expectedKey: gocui.NewKeyStrMod("a", gocui.ModCtrl), + expectedOk: true, + }, + { + name: "plus separator with long form", + label: "", + expectedKey: gocui.NewKeyStrMod("a", gocui.ModCtrl|gocui.ModAlt), + expectedOk: true, + }, + { + name: "plus separator with special key", + label: "", + expectedKey: gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModCtrl), + expectedOk: true, + }, + { + name: "mixed plus and dash separators", + label: "", + expectedKey: gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModCtrl|gocui.ModShift), + expectedOk: true, + }, + { + name: "duplicate detection works across separators", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "ctrl+plus rune via plus separator", + label: "", + expectedKey: gocui.NewKeyStrMod("+", gocui.ModCtrl), + expectedOk: true, + }, + { + name: "ctrl+dash rune via plus separator", + label: "", + expectedKey: gocui.NewKeyStrMod("-", gocui.ModCtrl), + expectedOk: true, + }, + { + name: "ctrl+plus rune via dash separator", + label: "", + expectedKey: gocui.NewKeyStrMod("+", gocui.ModCtrl), + expectedOk: true, + }, + { + name: "bare plus rune", + label: "+", + expectedKey: gocui.NewKeyStrMod("+", gocui.ModNone), + expectedOk: true, + }, + { + name: "bare plus wrapped in brackets", + label: "<+>", + expectedKey: gocui.NewKeyStrMod("+", gocui.ModNone), + expectedOk: true, + }, + + // Shift-on-rune is rejected: terminals fold shift into the rune + // itself, so the binding could never fire. Combined with other + // modifiers it's allowed (the terminal can't fold it then). + { + name: "shift alone on a letter is rejected", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "shift alone on uppercase letter is rejected", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "shift alone on minus is rejected", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "shift on space is allowed (rune does not change)", + label: "", + expectedKey: gocui.NewKeyStrMod(" ", gocui.ModShift), + expectedOk: true, + }, + { + name: "shift combined with ctrl on a letter is allowed", + label: "", + expectedKey: gocui.NewKeyStrMod("x", gocui.ModCtrl|gocui.ModShift), + expectedOk: true, + }, + { + name: "shift combined with alt on minus is allowed", + label: "", + expectedKey: gocui.NewKeyStrMod("-", gocui.ModAlt|gocui.ModShift), + expectedOk: true, + }, + + // Uppercase ASCII letter with a modifier is rejected: ctrl+letter + // always arrives with a lowercase rune (control codes have no case + // distinction), and CSI-u reports the unshifted codepoint with + // shift as a separate modifier. + { + name: "ctrl+uppercase letter is rejected", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "alt+uppercase letter is rejected", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "meta+uppercase letter is rejected", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "combined modifier on uppercase letter is rejected", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "bare uppercase letter is allowed", + label: "A", + expectedKey: gocui.NewKeyStrMod("A", gocui.ModNone), + expectedOk: true, + }, + { + name: "modifier on digit is allowed", + label: "", + expectedKey: gocui.NewKeyStrMod("1", gocui.ModCtrl), + expectedOk: true, + }, + { + name: "modifier on non-ASCII uppercase letter is allowed", + label: "", + expectedKey: gocui.NewKeyStrMod("Ñ", gocui.ModAlt), + expectedOk: true, + }, + + // Modifiers with special keys + { + name: "ctrl+enter", + label: "", + expectedKey: gocui.NewKey(gocui.KeyEnter, "", gocui.ModCtrl), + expectedOk: true, + }, + { + name: "alt+up", + label: "", + expectedKey: gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModAlt), + expectedOk: true, + }, + { + name: "shift+f1", + label: "", + expectedKey: gocui.NewKey(gocui.KeyF1, "", gocui.ModShift), + expectedOk: true, + }, + { + name: "meta+enter", + label: "", + expectedKey: gocui.NewKey(gocui.KeyEnter, "", gocui.ModMeta), + expectedOk: true, + }, + + // Combined modifiers + { + name: "ctrl+alt+letter", + label: "", + expectedKey: gocui.NewKeyStrMod("x", gocui.ModCtrl|gocui.ModAlt), + expectedOk: true, + }, + { + name: "all four modifiers on a letter", + label: "", + expectedKey: gocui.NewKeyStrMod("x", gocui.ModShift|gocui.ModCtrl|gocui.ModAlt|gocui.ModMeta), + expectedOk: true, + }, + { + name: "ctrl+shift+arrow key", + label: "", + expectedKey: gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModCtrl|gocui.ModShift), + expectedOk: true, + }, + + // Bracket handling + { + name: "single rune wrapped in brackets is unwrapped", + label: "", + expectedKey: gocui.NewKeyStrMod("a", gocui.ModNone), + expectedOk: true, + }, + { + name: "dash wrapped in brackets", + label: "<->", + expectedKey: gocui.NewKeyRune('-'), + expectedOk: true, + }, + + // Invalid inputs + { + name: "unknown special key name", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "unknown modifier letter", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "uppercase modifier is not accepted", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "duplicate ctrl modifier", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "duplicate shift modifier", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "duplicate alt modifier", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "duplicate meta modifier", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "trailing modifier with no key", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "multi-character non-special label", + label: "ab", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "empty brackets", + label: "<>", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + { + name: "modifier on unknown key name", + label: "", + expectedKey: gocui.Key{}, + expectedOk: false, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + key, ok := KeyFromLabel(s.label) + assert.Equal(t, s.expectedOk, ok) + assert.Equal(t, s.expectedKey, key) + }) + } +} + +func TestLabelForKey(t *testing.T) { + scenarios := []struct { + name string + key gocui.Key + expected string + }{ + // Unset + {"unset key produces empty string", gocui.Key{}, ""}, + + // Plain runes — single-character output, no brackets + {"lowercase letter", gocui.NewKeyStrMod("a", gocui.ModNone), "a"}, + {"uppercase letter", gocui.NewKeyStrMod("A", gocui.ModNone), "A"}, + {"digit", gocui.NewKeyStrMod("5", gocui.ModNone), "5"}, + {"punctuation", gocui.NewKeyStrMod("?", gocui.ModNone), "?"}, + {"slash", gocui.NewKeyStrMod("/", gocui.ModNone), "/"}, + {"multibyte rune", gocui.NewKeyStrMod("ñ", gocui.ModNone), "ñ"}, + + // Space and dash — special-cased rune output + {"plain dash uses literal", gocui.NewKeyStrMod("-", gocui.ModNone), "-"}, + {"plain space uses keyword", gocui.NewKeyStrMod(" ", gocui.ModNone), ""}, + {"ctrl+dash uses minus keyword", gocui.NewKeyStrMod("-", gocui.ModCtrl), ""}, + {"alt+dash uses minus keyword", gocui.NewKeyStrMod("-", gocui.ModAlt), ""}, + {"plain plus uses literal", gocui.NewKeyStrMod("+", gocui.ModNone), "+"}, + {"ctrl+plus uses plus keyword", gocui.NewKeyStrMod("+", gocui.ModCtrl), ""}, + {"alt+plus uses plus keyword", gocui.NewKeyStrMod("+", gocui.ModAlt), ""}, + {"ctrl+space", gocui.NewKeyStrMod(" ", gocui.ModCtrl), ""}, + + // Single modifier on a rune + {"ctrl+letter", gocui.NewKeyStrMod("a", gocui.ModCtrl), ""}, + {"alt+letter", gocui.NewKeyStrMod("x", gocui.ModAlt), ""}, + {"meta+letter", gocui.NewKeyStrMod("z", gocui.ModMeta), ""}, + {"shift+space", gocui.NewKeyStrMod(" ", gocui.ModShift), ""}, + + // Modifier ordering — canonical output is c-, a-, s-, m- + {"ctrl+alt orders c before a", gocui.NewKeyStrMod("x", gocui.ModCtrl|gocui.ModAlt), ""}, + {"shift+ctrl orders c before s", gocui.NewKeyStrMod("x", gocui.ModShift|gocui.ModCtrl), ""}, + {"meta+shift orders s before m", gocui.NewKeyStrMod("x", gocui.ModMeta|gocui.ModShift), ""}, + { + "all four modifiers ordered c-a-s-m", + gocui.NewKeyStrMod("x", gocui.ModCtrl|gocui.ModAlt|gocui.ModShift|gocui.ModMeta), + "", + }, + + // Special keys (always wrapped, even unmodified) + {"f1", gocui.NewKey(gocui.KeyF1, "", gocui.ModNone), ""}, + {"f12", gocui.NewKey(gocui.KeyF12, "", gocui.ModNone), ""}, + {"insert", gocui.NewKey(gocui.KeyInsert, "", gocui.ModNone), ""}, + {"delete", gocui.NewKey(gocui.KeyDelete, "", gocui.ModNone), ""}, + {"home", gocui.NewKey(gocui.KeyHome, "", gocui.ModNone), ""}, + {"end", gocui.NewKey(gocui.KeyEnd, "", gocui.ModNone), ""}, + {"pgup", gocui.NewKey(gocui.KeyPgup, "", gocui.ModNone), ""}, + {"pgdown", gocui.NewKey(gocui.KeyPgdn, "", gocui.ModNone), ""}, + {"arrow up", gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModNone), ""}, + {"arrow down", gocui.NewKey(gocui.KeyArrowDown, "", gocui.ModNone), ""}, + {"arrow left", gocui.NewKey(gocui.KeyArrowLeft, "", gocui.ModNone), ""}, + {"arrow right", gocui.NewKey(gocui.KeyArrowRight, "", gocui.ModNone), ""}, + {"tab", gocui.NewKey(gocui.KeyTab, "", gocui.ModNone), ""}, + {"backtab", gocui.NewKey(gocui.KeyBacktab, "", gocui.ModNone), ""}, + {"enter", gocui.NewKey(gocui.KeyEnter, "", gocui.ModNone), ""}, + {"esc", gocui.NewKey(gocui.KeyEsc, "", gocui.ModNone), ""}, + {"backspace", gocui.NewKey(gocui.KeyBackspace, "", gocui.ModNone), ""}, + {"mouse wheel up", gocui.NewKey(gocui.MouseWheelUp, "", gocui.ModNone), ""}, + {"mouse wheel down", gocui.NewKey(gocui.MouseWheelDown, "", gocui.ModNone), ""}, + + // Modifiers on special keys + {"shift+f1", gocui.NewKey(gocui.KeyF1, "", gocui.ModShift), ""}, + {"alt+up", gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModAlt), ""}, + {"meta+enter", gocui.NewKey(gocui.KeyEnter, "", gocui.ModMeta), ""}, + {"ctrl+shift+up", gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModCtrl|gocui.ModShift), ""}, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + assert.Equal(t, s.expected, LabelForKey(s.key)) + }) + } +} + +// Round-trip: every label produced by LabelForKey should parse back to the +// same key via KeyFromLabel. +func TestKeyFromLabel_RoundTripFromLabelForKey(t *testing.T) { + scenarios := []struct { + name string + key gocui.Key + }{ + {"unset key", gocui.Key{}}, + {"plain letter", gocui.NewKeyStrMod("a", gocui.ModNone)}, + {"plain digit", gocui.NewKeyStrMod("7", gocui.ModNone)}, + {"space", gocui.NewKeyStrMod(" ", gocui.ModNone)}, + {"ctrl+letter", gocui.NewKeyStrMod("a", gocui.ModCtrl)}, + {"alt+letter", gocui.NewKeyStrMod("x", gocui.ModAlt)}, + {"meta+letter", gocui.NewKeyStrMod("z", gocui.ModMeta)}, + {"shift+space", gocui.NewKeyStrMod(" ", gocui.ModShift)}, + {"ctrl+shift+letter", gocui.NewKeyStrMod("x", gocui.ModCtrl|gocui.ModShift)}, + {"ctrl+alt+letter", gocui.NewKeyStrMod("x", gocui.ModCtrl|gocui.ModAlt)}, + {"f1", gocui.NewKey(gocui.KeyF1, "", gocui.ModNone)}, + {"shift+f1", gocui.NewKey(gocui.KeyF1, "", gocui.ModShift)}, + {"alt+up", gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModAlt)}, + {"meta+enter", gocui.NewKey(gocui.KeyEnter, "", gocui.ModMeta)}, + {"esc", gocui.NewKey(gocui.KeyEsc, "", gocui.ModNone)}, + {"mouse wheel up", gocui.NewKey(gocui.MouseWheelUp, "", gocui.ModNone)}, + {"ctrl+space", gocui.NewKeyStrMod(" ", gocui.ModCtrl)}, + {"plain dash", gocui.NewKeyStrMod("-", gocui.ModNone)}, + {"ctrl+dash", gocui.NewKeyStrMod("-", gocui.ModCtrl)}, + {"alt+shift+dash", gocui.NewKeyStrMod("-", gocui.ModAlt|gocui.ModShift)}, + {"plain plus", gocui.NewKeyStrMod("+", gocui.ModNone)}, + {"ctrl+plus", gocui.NewKeyStrMod("+", gocui.ModCtrl)}, + {"alt+shift+plus", gocui.NewKeyStrMod("+", gocui.ModAlt|gocui.ModShift)}, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + label := LabelForKey(s.key) + parsed, ok := KeyFromLabel(label) + assert.True(t, ok, "expected label %q to parse", label) + assert.Equal(t, s.key, parsed) + }) + } +} diff --git a/pkg/gocui/edit.go b/pkg/gocui/edit.go index a11993a48..32593957d 100644 --- a/pkg/gocui/edit.go +++ b/pkg/gocui/edit.go @@ -6,63 +6,66 @@ package gocui // Editor interface must be satisfied by gocui editors. type Editor interface { - Edit(v *View, key Key, mod Modifier) bool + Edit(v *View, key Key) bool } // The EditorFunc type is an adapter to allow the use of ordinary functions as // Editors. If f is a function with the appropriate signature, EditorFunc(f) // is an Editor object that calls f. -type EditorFunc func(v *View, key Key, mod Modifier) bool +type EditorFunc func(v *View, key Key) bool // Edit calls f(v, key, mod) -func (f EditorFunc) Edit(v *View, key Key, mod Modifier) bool { - return f(v, key, mod) +func (f EditorFunc) Edit(v *View, key Key) bool { + return f(v, key) } // DefaultEditor is the default editor. var DefaultEditor Editor = EditorFunc(SimpleEditor) // SimpleEditor is used as the default gocui editor. -func SimpleEditor(v *View, key Key, mod Modifier) bool { +func SimpleEditor(v *View, key Key) bool { switch { - case (key.KeyName() == KeyBackspace || key.KeyName() == KeyBackspace2) && (mod&ModAlt) != 0, - key.KeyName() == KeyCtrlW: + case key.Equals(NewKey(KeyBackspace, "", ModAlt)), + key.Equals(NewKeyStrMod("w", ModCtrl)): v.TextArea.BackSpaceWord() - case key.KeyName() == KeyBackspace || key.KeyName() == KeyBackspace2 || key.KeyName() == KeyCtrlH: + case key.Equals(NewKeyName(KeyBackspace)): v.TextArea.BackSpaceChar() - case key.KeyName() == KeyCtrlD || key.KeyName() == KeyDelete: + case key.Equals(NewKeyStrMod("d", ModCtrl)), + key.Equals(NewKeyName(KeyDelete)): v.TextArea.DeleteChar() - case key.KeyName() == KeyArrowDown: + case key.Equals(NewKeyName(KeyArrowDown)): v.TextArea.MoveCursorDown() - case key.KeyName() == KeyArrowUp: + case key.Equals(NewKeyName(KeyArrowUp)): v.TextArea.MoveCursorUp() - case (key.KeyName() == KeyArrowLeft || key.Equals(NewKeyRune('b'))) && (mod&ModAlt) != 0: + case key.Equals(NewKeyStrMod("b", ModAlt)), + key.Equals(NewKey(KeyArrowLeft, "", ModAlt)): v.TextArea.MoveLeftWord() - case key.KeyName() == KeyArrowLeft || key.KeyName() == KeyCtrlB: + case key.Equals(NewKeyName(KeyArrowLeft)), + key.Equals(NewKeyStrMod("b", ModCtrl)): v.TextArea.MoveCursorLeft() - case (key.KeyName() == KeyArrowRight || key.Equals(NewKeyRune('f'))) && (mod&ModAlt) != 0: + case key.Equals(NewKeyStrMod("f", ModAlt)), + key.Equals(NewKey(KeyArrowRight, "", ModAlt)): v.TextArea.MoveRightWord() - case key.KeyName() == KeyArrowRight || key.KeyName() == KeyCtrlF: + case key.Equals(NewKeyName(KeyArrowRight)), + key.Equals(NewKeyStrMod("b", ModCtrl)): v.TextArea.MoveCursorRight() - case key.KeyName() == KeyEnter: + case key.Equals(NewKeyName(KeyEnter)): v.TextArea.TypeCharacter("\n") - case key.KeyName() == KeySpace: - v.TextArea.TypeCharacter(" ") - case key.KeyName() == KeyInsert: + case key.Equals(NewKeyName(KeyInsert)): v.TextArea.ToggleOverwrite() - case key.KeyName() == KeyCtrlU: + case key.Equals(NewKeyStrMod("u", ModCtrl)): v.TextArea.DeleteToStartOfLine() - case key.KeyName() == KeyCtrlK: + case key.Equals(NewKeyStrMod("k", ModCtrl)): v.TextArea.DeleteToEndOfLine() - case key.KeyName() == KeyCtrlA || key.KeyName() == KeyHome: + case key.Equals(NewKeyStrMod("a", ModCtrl)), + key.Equals(NewKeyName(KeyHome)): v.TextArea.GoToStartOfLine() - case key.KeyName() == KeyCtrlE || key.KeyName() == KeyEnd: + case key.Equals(NewKeyStrMod("e", ModCtrl)), + key.Equals(NewKeyName(KeyEnd)): v.TextArea.GoToEndOfLine() - case key.KeyName() == KeyCtrlW: - v.TextArea.BackSpaceWord() - case key.KeyName() == KeyCtrlY: + case key.Equals(NewKeyStrMod("y", ModCtrl)): v.TextArea.Yank() - case key.Str() != "": + case key.Str() != "" && key.Mod() == 0: v.TextArea.TypeCharacter(key.Str()) default: return false diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index e4320231e..9aaca3e2a 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -1264,17 +1264,16 @@ func (g *Gui) onKey(ev *GocuiEvent) error { switch ev.Type { case eventKey: - // When pasting text in Ghostty, it sends us '\r' instead of '\n' for // newlines. I actually don't quite understand why, because from reading - // Ghostty's source code (e.g. + // When pasting text in Ghostty, it sends us '\r' (which is delivered as + // ctrl-j by tcell) instead of '\n' for newlines. I actually don't quite + // understand why, because from reading Ghostty's source code (e.g. // https://github.com/ghostty-org/ghostty/commit/010338354a0) it does // this conversion only for non-bracketed paste mode, but I'm seeing it // in bracketed paste mode. Whatever I'm missing here, converting '\r' // back to '\n' fixes pasting multi-line text from Ghostty, and doesn't // seem harmful for other terminal emulators. - // - // KeyCtrlJ (int value 10) is '\r'. - if g.IsPasting && ev.Key.KeyName() == KeyCtrlJ { + if g.IsPasting && ev.Key.Equals(NewKeyStrMod("j", ModCtrl)) { ev.Key = NewKeyName(KeyEnter) } @@ -1316,7 +1315,7 @@ func (g *Gui) onKey(ev *GocuiEvent) error { } } - if ev.Key.KeyName() == MouseLeft && (ev.Mod&ModMotion) == 0 && !v.Editable && g.openHyperlink != nil { + if ev.Key.KeyName() == MouseLeft && (ev.Key.Mod()&ModMotion) == 0 && !v.Editable && g.openHyperlink != nil { if newY >= 0 && newY <= len(v.viewLines)-1 && newX >= 0 && newX <= len(v.viewLines[newY].line)-1 { if link := v.viewLines[newY].line[newX].hyperlink; link != "" { return g.openHyperlink(link, v.name) @@ -1424,7 +1423,7 @@ func (g *Gui) execMouseKeybindings(view *View, ev *GocuiEvent, opts ViewMouseBin isMatch := func(binding *ViewMouseBinding) bool { return binding.ViewName == view.Name() && ev.Key.KeyName() == binding.Key && - ev.Mod == binding.Modifier + ev.Key.Mod() == binding.Modifier } // first pass looks for ones that match the focused view @@ -1486,7 +1485,7 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error { } // if we're searching, and we've hit n/N/Esc, we ignore the default keybinding - if v != nil && v.IsSearching() && ev.Mod == ModNone { + if v != nil && v.IsSearching() { if ev.Key.Equals(g.NextSearchMatchKey) { return v.gotoNextMatch() } else if ev.Key.Equals(g.PrevSearchMatchKey) { @@ -1508,7 +1507,7 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error { if kb.handler == nil { continue } - if !kb.matchKeypress(ev.Key, ev.Mod) { + if !kb.matchKeypress(ev.Key) { continue } if g.matchView(v, kb) { @@ -1523,7 +1522,7 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error { if v != nil && g.matchView(v.ParentView, kb) { matchingParentViewKb = kb } - if globalKb == nil && kb.viewName == "" && ((v != nil && !v.Editable) || (kb.key.keyName != KeyCtrlU && kb.key.keyName != KeyCtrlA && kb.key.keyName != KeyCtrlE)) { + if globalKb == nil && kb.viewName == "" { globalKb = kb } } @@ -1535,7 +1534,7 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error { } if g.currentView != nil && g.currentView.Editable && g.currentView.Editor != nil { - matched := g.currentView.Editor.Edit(g.currentView, ev.Key, ev.Mod) + matched := g.currentView.Editor.Edit(g.currentView, ev.Key) if matched { return nil } @@ -1595,7 +1594,7 @@ func (g *Gui) matchView(v *View, kb *keybinding) bool { if v == nil { return false } - if v.Editable && kb.key.Str() != "" { + if v.Editable && kb.key.Str() != "" && kb.key.Mod() == 0 { return false } if kb.viewName != v.name { diff --git a/pkg/gocui/key.go b/pkg/gocui/key.go index 2a2091696..dd0a912a0 100644 --- a/pkg/gocui/key.go +++ b/pkg/gocui/key.go @@ -9,12 +9,15 @@ import "github.com/gdamore/tcell/v3" type Key struct { keyName KeyName str string + + mod Modifier } -func NewKey(keyName KeyName, str string) Key { +func NewKey(keyName KeyName, str string, mod Modifier) Key { return Key{ keyName: keyName, str: str, + mod: mod, } } @@ -22,6 +25,7 @@ func NewKeyName(keyName KeyName) Key { return Key{ keyName: keyName, str: "", + mod: ModNone, } } @@ -29,6 +33,15 @@ func NewKeyRune(ch rune) Key { return Key{ keyName: KeyName(tcell.KeyRune), str: string(ch), + mod: ModNone, + } +} + +func NewKeyStrMod(str string, mod Modifier) Key { + return Key{ + keyName: KeyName(tcell.KeyRune), + str: str, + mod: mod, } } @@ -40,10 +53,14 @@ func (k Key) Str() string { return k.str } +func (k Key) Mod() Modifier { + return k.mod +} + func (k Key) IsSet() bool { return k.keyName != 0 } func (k Key) Equals(otherKey Key) bool { - return k.keyName == otherKey.keyName && k.str == otherKey.str + return k.keyName == otherKey.keyName && k.str == otherKey.str && k.mod == otherKey.mod } diff --git a/pkg/gocui/keybinding.go b/pkg/gocui/keybinding.go index cdd2f0c0a..b80cb5df6 100644 --- a/pkg/gocui/keybinding.go +++ b/pkg/gocui/keybinding.go @@ -35,8 +35,8 @@ func newKeybinding(viewname string, key Key, mod Modifier, handler func(*Gui, *V } // matchKeypress returns if the keybinding matches the keypress. -func (kb *keybinding) matchKeypress(key Key, mod Modifier) bool { - return kb.key.Equals(key) && kb.mod == mod +func (kb *keybinding) matchKeypress(key Key) bool { + return kb.key.Equals(key) } // Special keys. @@ -69,41 +69,12 @@ const ( // Keys combinations. const ( - KeyCtrlTilde = KeyName(tcell.KeyF64) // arbitrary assignment - KeyCtrlA = KeyName(tcell.KeyCtrlA) - KeyCtrlB = KeyName(tcell.KeyCtrlB) - KeyCtrlC = KeyName(tcell.KeyCtrlC) - KeyCtrlD = KeyName(tcell.KeyCtrlD) - KeyCtrlE = KeyName(tcell.KeyCtrlE) - KeyCtrlF = KeyName(tcell.KeyCtrlF) - KeyCtrlG = KeyName(tcell.KeyCtrlG) - KeyBackspace = KeyName(tcell.KeyBackspace) - KeyCtrlH = KeyName(tcell.KeyCtrlH) - KeyTab = KeyName(tcell.KeyTab) - KeyBacktab = KeyName(tcell.KeyBacktab) - KeyCtrlI = KeyName(tcell.KeyCtrlI) - KeyCtrlJ = KeyName(tcell.KeyCtrlJ) - KeyCtrlK = KeyName(tcell.KeyCtrlK) - KeyCtrlL = KeyName(tcell.KeyCtrlL) - KeyEnter = KeyName(tcell.KeyEnter) - KeyCtrlM = KeyName(tcell.KeyCtrlM) - KeyCtrlN = KeyName(tcell.KeyCtrlN) - KeyCtrlO = KeyName(tcell.KeyCtrlO) - KeyCtrlP = KeyName(tcell.KeyCtrlP) - KeyCtrlQ = KeyName(tcell.KeyCtrlQ) - KeyCtrlR = KeyName(tcell.KeyCtrlR) - KeyCtrlS = KeyName(tcell.KeyCtrlS) - KeyCtrlT = KeyName(tcell.KeyCtrlT) - KeyCtrlU = KeyName(tcell.KeyCtrlU) - KeyCtrlV = KeyName(tcell.KeyCtrlV) - KeyCtrlW = KeyName(tcell.KeyCtrlW) - KeyCtrlX = KeyName(tcell.KeyCtrlX) - KeyCtrlY = KeyName(tcell.KeyCtrlY) - KeyCtrlZ = KeyName(tcell.KeyCtrlZ) - KeyEsc = KeyName(tcell.KeyEscape) - KeySpace = KeyName(32) - KeyBackspace2 = KeyName(tcell.KeyBackspace2) - KeyCtrl8 = KeyName(tcell.KeyBackspace2) // same key as in termbox-go + KeyCtrlTilde = KeyName(tcell.KeyF64) // arbitrary assignment + KeyBackspace = KeyName(tcell.KeyBackspace) + KeyTab = KeyName(tcell.KeyTab) + KeyBacktab = KeyName(tcell.KeyBacktab) + KeyEnter = KeyName(tcell.KeyEnter) + KeyEsc = KeyName(tcell.KeyEscape) // The following assignments were used in termbox implementation. // In tcell, these are not keys per se. But in gocui we have them @@ -123,8 +94,9 @@ const ( // Modifiers. const ( ModNone Modifier = Modifier(0) + ModShift = Modifier(tcell.ModShift) + ModCtrl = Modifier(tcell.ModCtrl) ModAlt = Modifier(tcell.ModAlt) - ModMotion = Modifier(2) // just picking an arbitrary number here that doesn't clash with tcell.ModAlt - // ModCtrl doesn't work with keyboard keys. Use CtrlKey in Key and ModNone. This is was for mouse clicks only (tcell.v1) - // ModCtrl = Modifier(tcell.ModCtrl) + ModMeta = Modifier(tcell.ModMeta) + ModMotion = Modifier(16) // just picking an arbitrary number here that doesn't clash with tcell's modifiers ) diff --git a/pkg/gocui/tcell_driver.go b/pkg/gocui/tcell_driver.go index 2e5278a37..b2fd40c19 100644 --- a/pkg/gocui/tcell_driver.go +++ b/pkg/gocui/tcell_driver.go @@ -163,7 +163,6 @@ type gocuiEventType uint8 // The 'Err' field is valid if 'Type' is 'eventError'. type GocuiEvent struct { Type gocuiEventType - Mod Modifier Key Key Width int Height int @@ -294,42 +293,15 @@ func (g *Gui) pollEvent() GocuiEvent { ch := "" if k == tcell.KeyRune { ch = tev.Str() - if ch == " " { - // special handling for spacebar - k = tcell.Key(KeySpace) - ch = "" - } + } else if k >= tcell.KeyCtrlA && k <= tcell.KeyCtrlZ { + ch = string(rune('a' + (k - tcell.KeyCtrlA))) + k = tcell.KeyRune } mod := tev.Modifiers() - // remove control modifier and setup special handling of ctrl+spacebar, etc. - if mod == tcell.ModCtrl && k == 32 { - ch = " " - k = tcell.KeyRune - } else if mod == tcell.ModShift && k == tcell.KeyUp { - mod = 0 - ch = "" - k = tcell.KeyF62 - } else if mod == tcell.ModShift && k == tcell.KeyDown { - mod = 0 - ch = "" - k = tcell.KeyF63 - } else if mod == tcell.ModCtrl || mod == tcell.ModShift { - // remove Ctrl or Shift if specified - // - shift - will be translated to the final code of rune - // - ctrl - is translated in the key - mod = 0 - } else if mod == tcell.ModAlt && k == tcell.KeyEnter { - // for the sake of convenience I'm having a KeyAltEnter key. I will likely - // regret this laziness in the future. We're arbitrarily mapping that to tcell's - // KeyF64. - mod = 0 - k = tcell.KeyF64 - } return GocuiEvent{ Type: eventKey, - Key: NewKey(KeyName(k), ch), - Mod: Modifier(mod), + Key: NewKey(KeyName(k), ch, Modifier(mod)), } case *tcell.EventMouse: x, y := tev.Position() @@ -411,8 +383,7 @@ func (g *Gui) pollEvent() GocuiEvent { Type: eventMouse, MouseX: x, MouseY: y, - Key: NewKeyName(mouseKey), - Mod: mouseMod, + Key: NewKey(mouseKey, "", mouseMod), } case *tcell.EventFocus: return GocuiEvent{ diff --git a/pkg/gui/controllers/commit_description_controller.go b/pkg/gui/controllers/commit_description_controller.go index 09b518659..63f6876e5 100644 --- a/pkg/gui/controllers/commit_description_controller.go +++ b/pkg/gui/controllers/commit_description_controller.go @@ -119,7 +119,7 @@ func (self *CommitDescriptionController) handleTogglePanel() error { // which is common in pasted code snippets. view := self.Context().GetView() for range 4 { - view.Editor.Edit(view, gocui.NewKeyRune(' '), 0) + view.Editor.Edit(view, gocui.NewKeyRune(' ')) } return nil } diff --git a/pkg/gui/controllers/commit_message_controller.go b/pkg/gui/controllers/commit_message_controller.go index 97865f741..e1561690c 100644 --- a/pkg/gui/controllers/commit_message_controller.go +++ b/pkg/gui/controllers/commit_message_controller.go @@ -130,7 +130,7 @@ func (self *CommitMessageController) handleTogglePanel() error { // switch to the description panel. view := self.context().GetView() for range 4 { - view.Editor.Edit(view, gocui.NewKeyRune(' '), 0) + view.Editor.Edit(view, gocui.NewKeyRune(' ')) } return nil } diff --git a/pkg/gui/editors.go b/pkg/gui/editors.go index bf633db9e..7d3a93de3 100644 --- a/pkg/gui/editors.go +++ b/pkg/gui/editors.go @@ -4,33 +4,33 @@ import ( "github.com/jesseduffield/lazygit/pkg/gocui" ) -func (gui *Gui) handleEditorKeypress(v *gocui.View, key gocui.Key, mod gocui.Modifier, allowMultiline bool) bool { - if key.KeyName() == gocui.KeyEnter && allowMultiline { +func (gui *Gui) handleEditorKeypress(v *gocui.View, key gocui.Key, allowMultiline bool) bool { + if key.Equals(gocui.NewKeyName(gocui.KeyEnter)) && allowMultiline { v.TextArea.TypeCharacter("\n") v.RenderTextArea() return true } - return gocui.DefaultEditor.Edit(v, key, mod) + return gocui.DefaultEditor.Edit(v, key) } // we've just copy+pasted the editor from gocui to here so that we can also re- // render the commit message length on each keypress -func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, mod gocui.Modifier) bool { - matched := gui.handleEditorKeypress(v, key, mod, false) +func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key) bool { + matched := gui.handleEditorKeypress(v, key, false) v.RenderTextArea() gui.c.Contexts().CommitMessage.RenderSubtitle() return matched } -func (gui *Gui) commitDescriptionEditor(v *gocui.View, key gocui.Key, mod gocui.Modifier) bool { - matched := gui.handleEditorKeypress(v, key, mod, true) +func (gui *Gui) commitDescriptionEditor(v *gocui.View, key gocui.Key) bool { + matched := gui.handleEditorKeypress(v, key, true) v.RenderTextArea() return matched } -func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key, mod gocui.Modifier) bool { - matched := gui.handleEditorKeypress(v, key, mod, false) +func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key) bool { + matched := gui.handleEditorKeypress(v, key, false) v.RenderTextArea() @@ -46,8 +46,8 @@ func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key, mod gocui.Modifier) b return matched } -func (gui *Gui) searchEditor(v *gocui.View, key gocui.Key, mod gocui.Modifier) bool { - matched := gui.handleEditorKeypress(v, key, mod, false) +func (gui *Gui) searchEditor(v *gocui.View, key gocui.Key) bool { + matched := gui.handleEditorKeypress(v, key, false) v.RenderTextArea() searchString := v.TextArea.GetContent() diff --git a/pkg/gui/gui_driver.go b/pkg/gui/gui_driver.go index f70e8c9b3..08f3ecf62 100644 --- a/pkg/gui/gui_driver.go +++ b/pkg/gui/gui_driver.go @@ -34,7 +34,7 @@ func (self *GuiDriver) PressKey(keyStr string) { } self.gui.g.ReplayedEvents.Keys <- gocui.NewTcellKeyEventWrapper( - tcell.NewEventKey(tcell.Key(key.KeyName()), key.Str(), tcell.ModNone), + tcell.NewEventKey(tcell.Key(key.KeyName()), key.Str(), tcell.ModMask(key.Mod())), 0, ) diff --git a/pkg/integration/clients/tui.go b/pkg/integration/clients/tui.go index 426f633de..90f02cc32 100644 --- a/pkg/integration/clients/tui.go +++ b/pkg/integration/clients/tui.go @@ -72,7 +72,7 @@ func RunTUI(raceDetector bool) { log.Panicln(err) } - if err := g.SetKeybinding("list", gocui.NewKeyName(gocui.KeyCtrlC), gocui.ModNone, quit); err != nil { + if err := g.SetKeybinding("list", gocui.NewKeyStrMod("c", gocui.ModCtrl), gocui.ModNone, quit); err != nil { log.Panicln(err) } @@ -273,9 +273,9 @@ func (self *app) renderTests() { } } -func (self *app) wrapEditor(f func(v *gocui.View, key gocui.Key, mod gocui.Modifier) bool) func(v *gocui.View, key gocui.Key, mod gocui.Modifier) bool { - return func(v *gocui.View, key gocui.Key, mod gocui.Modifier) bool { - matched := f(v, key, mod) +func (self *app) wrapEditor(f func(v *gocui.View, key gocui.Key) bool) func(v *gocui.View, key gocui.Key) bool { + return func(v *gocui.View, key gocui.Key) bool { + matched := f(v, key) if matched { self.filterWithString(v.TextArea.GetContent()) }