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 ctrl+, alt+, shift+, meta+ {"ctrl+alt orders ctrl before alt", gocui.NewKeyStrMod("x", gocui.ModCtrl|gocui.ModAlt), ""}, {"shift+ctrl orders ctrl before shift", gocui.NewKeyStrMod("x", gocui.ModShift|gocui.ModCtrl), ""}, {"meta+shift orders shift before meta", gocui.NewKeyStrMod("x", gocui.ModMeta|gocui.ModShift), ""}, { "all four modifiers ordered ctrl+alt+shift+meta", 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) }) } }