1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2026-06-09 22:05:16 +02:00

Move modifiers into Key

This changes not only how we store modifiers (inside of Key instead of passing
it separately), but also how we parse keybinding strings: it supports all
combinations of modifiers now (if the terminal supports it, that is).
This commit is contained in:
Stefan Haller
2026-04-02 18:04:07 +02:00
parent 30b619b3db
commit 22169e22ff
21 changed files with 952 additions and 221 deletions
+2 -2
View File
@@ -221,8 +221,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-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 | |
| `` <mouse wheel down> (fn+up) `` | Scroll down | |
| `` <mouse wheel up> (fn+down) `` | Scroll up | |
| `` <tab> `` | Switch view | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | Search the current view by text | |
+2 -2
View File
@@ -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) `` | 上にスクロール | |
| `` <mouse wheel down> (fn+up) `` | 下にスクロール | |
| `` <mouse wheel up> (fn+down) `` | 上にスクロール | |
| `` <tab> `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 |
| `` <esc> `` | サイドパネルに戻る | |
| `` / `` | 現在のビューをテキストで検索 | |
+2 -2
View File
@@ -160,8 +160,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` mouse wheel down (fn+up) `` | 아래로 스크롤 | |
| `` mouse wheel up (fn+down) `` | 위로 스크롤 | |
| `` <mouse wheel down> (fn+up) `` | 아래로 스크롤 | |
| `` <mouse wheel up> (fn+down) `` | 위로 스크롤 | |
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | 검색 시작 | |
+2 -2
View File
@@ -229,8 +229,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-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 | |
| `` <mouse wheel down> (fn+up) `` | Scroll omlaag | |
| `` <mouse wheel up> (fn+down) `` | Scroll omhoog | |
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | Start met zoeken | |
+2 -2
View File
@@ -179,8 +179,8 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-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ę | |
| `` <mouse wheel down> (fn+up) `` | Przewiń w dół | |
| `` <mouse wheel up> (fn+down) `` | Przewiń w górę | |
| `` <tab> `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
+2 -2
View File
@@ -233,8 +233,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-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 | |
| `` <mouse wheel down> (fn+up) `` | Rolar para baixo | |
| `` <mouse wheel up> (fn+down) `` | Rolar para cima | |
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | Pesquisar na visualização atual por texto | |
+2 -2
View File
@@ -104,8 +104,8 @@ _Связки клавиш_
| Key | Action | Info |
|-----|--------|-------------|
| `` mouse wheel down (fn+up) `` | Прокрутить вниз | |
| `` mouse wheel up (fn+down) `` | Прокрутить вверх | |
| `` <mouse wheel down> (fn+up) `` | Прокрутить вниз | |
| `` <mouse wheel up> (fn+down) `` | Прокрутить вверх | |
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | Найти | |
+2 -2
View File
@@ -332,8 +332,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) `` | 向上滚动 | |
| `` <mouse wheel down> (fn+up) `` | 向下滚动 | |
| `` <mouse wheel up> (fn+down) `` | 向上滚动 | |
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
| `` <esc> `` | 退出回到侧边面板 | |
| `` / `` | 开始搜索 | |
+2 -2
View File
@@ -80,8 +80,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) `` | 向上捲動 | |
| `` <mouse wheel down> (fn+up) `` | 向下捲動 | |
| `` <mouse wheel up> (fn+down) `` | 向上捲動 | |
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | 搜尋 | |
+153 -70
View File
@@ -14,63 +14,35 @@ import (
// docs/keybindings/Custom_Keybindings.md as well
var labelByKey = map[gocui.KeyName]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: "<up>",
gocui.KeyShiftArrowUp: "<s-up>",
gocui.KeyArrowDown: "<down>",
gocui.KeyShiftArrowDown: "<s-down>",
gocui.KeyArrowLeft: "<left>",
gocui.KeyArrowRight: "<right>",
gocui.KeyTab: "<tab>", // <c-i>
gocui.KeyBacktab: "<backtab>",
gocui.KeyEnter: "<enter>", // <c-m>
gocui.KeyAltEnter: "<a-enter>",
gocui.KeyEsc: "<esc>", // <c-[>, <c-3>
gocui.KeyBackspace: "<backspace>", // <c-h>
gocui.KeySpace: "<space>",
gocui.KeyCtrlA: "<c-a>",
gocui.KeyCtrlB: "<c-b>",
gocui.KeyCtrlC: "<c-c>",
gocui.KeyCtrlD: "<c-d>",
gocui.KeyCtrlE: "<c-e>",
gocui.KeyCtrlF: "<c-f>",
gocui.KeyCtrlG: "<c-g>",
gocui.KeyCtrlJ: "<c-j>",
gocui.KeyCtrlK: "<c-k>",
gocui.KeyCtrlL: "<c-l>",
gocui.KeyCtrlN: "<c-n>",
gocui.KeyCtrlO: "<c-o>",
gocui.KeyCtrlP: "<c-p>",
gocui.KeyCtrlQ: "<c-q>",
gocui.KeyCtrlR: "<c-r>",
gocui.KeyCtrlS: "<c-s>",
gocui.KeyCtrlT: "<c-t>",
gocui.KeyCtrlU: "<c-u>",
gocui.KeyCtrlV: "<c-v>",
gocui.KeyCtrlW: "<c-w>",
gocui.KeyCtrlX: "<c-x>",
gocui.KeyCtrlY: "<c-y>",
gocui.KeyCtrlZ: "<c-z>",
gocui.KeyCtrl8: "<c-8>",
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 `<c-->` and `<c++>`,
// neither of which we emit (we use `<c-minus>` and `<c-+>`).
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 <c-s-a> rather than <c-A>.
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 {
+686
View File
@@ -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: "<disabled> returns unset key",
label: "<disabled>",
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: "<f12>",
expectedKey: gocui.NewKey(gocui.KeyF12, "", gocui.ModNone),
expectedOk: true,
},
{
name: "arrow key",
label: "<up>",
expectedKey: gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModNone),
expectedOk: true,
},
{
name: "tab",
label: "<tab>",
expectedKey: gocui.NewKey(gocui.KeyTab, "", gocui.ModNone),
expectedOk: true,
},
{
name: "enter",
label: "<enter>",
expectedKey: gocui.NewKey(gocui.KeyEnter, "", gocui.ModNone),
expectedOk: true,
},
{
name: "esc",
label: "<esc>",
expectedKey: gocui.NewKey(gocui.KeyEsc, "", gocui.ModNone),
expectedOk: true,
},
{
name: "backspace",
label: "<backspace>",
expectedKey: gocui.NewKey(gocui.KeyBackspace, "", gocui.ModNone),
expectedOk: true,
},
{
name: "pgup",
label: "<pgup>",
expectedKey: gocui.NewKey(gocui.KeyPgup, "", gocui.ModNone),
expectedOk: true,
},
{
name: "pgdown",
label: "<pgdown>",
expectedKey: gocui.NewKey(gocui.KeyPgdn, "", gocui.ModNone),
expectedOk: true,
},
{
name: "mouse wheel up",
label: "<mouse wheel up>",
expectedKey: gocui.NewKey(gocui.MouseWheelUp, "", gocui.ModNone),
expectedOk: true,
},
// Space
{
name: "space keyword maps to space rune",
label: "<space>",
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: "<c-space>",
expectedKey: gocui.NewKeyStrMod(" ", gocui.ModCtrl),
expectedOk: true,
},
// Minus
{
name: "minus keyword maps to dash rune",
label: "<minus>",
expectedKey: gocui.NewKeyStrMod("-", gocui.ModNone),
expectedOk: true,
},
{
name: "ctrl+minus via keyword",
label: "<c-minus>",
expectedKey: gocui.NewKeyStrMod("-", gocui.ModCtrl),
expectedOk: true,
},
{
name: "ctrl+minus via lenient dash form",
label: "<c-->",
expectedKey: gocui.NewKeyStrMod("-", gocui.ModCtrl),
expectedOk: true,
},
{
name: "alt+ctrl+minus via lenient dash form",
label: "<a-c-->",
expectedKey: gocui.NewKeyStrMod("-", gocui.ModAlt|gocui.ModCtrl),
expectedOk: true,
},
// Plus
{
name: "plus keyword maps to plus rune",
label: "<plus>",
expectedKey: gocui.NewKeyStrMod("+", gocui.ModNone),
expectedOk: true,
},
{
name: "ctrl+plus via keyword",
label: "<c-plus>",
expectedKey: gocui.NewKeyStrMod("+", gocui.ModCtrl),
expectedOk: true,
},
{
name: "ctrl+plus via long keyword and plus separator",
label: "<ctrl+plus>",
expectedKey: gocui.NewKeyStrMod("+", gocui.ModCtrl),
expectedOk: true,
},
{
name: "alt+shift+plus via keyword",
label: "<a-s-plus>",
expectedKey: gocui.NewKeyStrMod("+", gocui.ModAlt|gocui.ModShift),
expectedOk: true,
},
{
name: "shift alone on plus is rejected",
label: "<s-plus>",
expectedKey: gocui.Key{},
expectedOk: false,
},
// Modifiers with runes
{
name: "ctrl+letter",
label: "<c-a>",
expectedKey: gocui.NewKeyStrMod("a", gocui.ModCtrl),
expectedOk: true,
},
{
name: "alt+letter",
label: "<a-x>",
expectedKey: gocui.NewKeyStrMod("x", gocui.ModAlt),
expectedOk: true,
},
{
name: "meta+letter",
label: "<m-z>",
expectedKey: gocui.NewKeyStrMod("z", gocui.ModMeta),
expectedOk: true,
},
// Long modifier names are accepted as synonyms for the short forms.
{
name: "ctrl long form",
label: "<ctrl-a>",
expectedKey: gocui.NewKeyStrMod("a", gocui.ModCtrl),
expectedOk: true,
},
{
name: "alt long form",
label: "<alt-x>",
expectedKey: gocui.NewKeyStrMod("x", gocui.ModAlt),
expectedOk: true,
},
{
name: "meta long form",
label: "<meta-z>",
expectedKey: gocui.NewKeyStrMod("z", gocui.ModMeta),
expectedOk: true,
},
{
name: "shift long form combined with ctrl",
label: "<shift-ctrl-a>",
expectedKey: gocui.NewKeyStrMod("a", gocui.ModShift|gocui.ModCtrl),
expectedOk: true,
},
{
name: "long forms work with special keys",
label: "<ctrl-up>",
expectedKey: gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModCtrl),
expectedOk: true,
},
{
name: "short and long forms can be mixed",
label: "<ctrl-s-up>",
expectedKey: gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModCtrl|gocui.ModShift),
expectedOk: true,
},
{
name: "duplicate via mixed short and long form is rejected",
label: "<shift-s-a>",
expectedKey: gocui.Key{},
expectedOk: false,
},
{
name: "unknown long modifier is rejected",
label: "<control-a>",
expectedKey: gocui.Key{},
expectedOk: false,
},
// Plus is accepted as an alternative modifier separator.
{
name: "plus separator with short form",
label: "<c+a>",
expectedKey: gocui.NewKeyStrMod("a", gocui.ModCtrl),
expectedOk: true,
},
{
name: "plus separator with long form",
label: "<ctrl+alt+a>",
expectedKey: gocui.NewKeyStrMod("a", gocui.ModCtrl|gocui.ModAlt),
expectedOk: true,
},
{
name: "plus separator with special key",
label: "<ctrl+up>",
expectedKey: gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModCtrl),
expectedOk: true,
},
{
name: "mixed plus and dash separators",
label: "<ctrl+shift-up>",
expectedKey: gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModCtrl|gocui.ModShift),
expectedOk: true,
},
{
name: "duplicate detection works across separators",
label: "<c+c-a>",
expectedKey: gocui.Key{},
expectedOk: false,
},
{
name: "ctrl+plus rune via plus separator",
label: "<c++>",
expectedKey: gocui.NewKeyStrMod("+", gocui.ModCtrl),
expectedOk: true,
},
{
name: "ctrl+dash rune via plus separator",
label: "<c+->",
expectedKey: gocui.NewKeyStrMod("-", gocui.ModCtrl),
expectedOk: true,
},
{
name: "ctrl+plus rune via dash separator",
label: "<c-+>",
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: "<s-q>",
expectedKey: gocui.Key{},
expectedOk: false,
},
{
name: "shift alone on uppercase letter is rejected",
label: "<s-A>",
expectedKey: gocui.Key{},
expectedOk: false,
},
{
name: "shift alone on minus is rejected",
label: "<s-minus>",
expectedKey: gocui.Key{},
expectedOk: false,
},
{
name: "shift on space is allowed (rune does not change)",
label: "<s-space>",
expectedKey: gocui.NewKeyStrMod(" ", gocui.ModShift),
expectedOk: true,
},
{
name: "shift combined with ctrl on a letter is allowed",
label: "<c-s-x>",
expectedKey: gocui.NewKeyStrMod("x", gocui.ModCtrl|gocui.ModShift),
expectedOk: true,
},
{
name: "shift combined with alt on minus is allowed",
label: "<a-s-minus>",
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: "<c-A>",
expectedKey: gocui.Key{},
expectedOk: false,
},
{
name: "alt+uppercase letter is rejected",
label: "<a-A>",
expectedKey: gocui.Key{},
expectedOk: false,
},
{
name: "meta+uppercase letter is rejected",
label: "<m-A>",
expectedKey: gocui.Key{},
expectedOk: false,
},
{
name: "combined modifier on uppercase letter is rejected",
label: "<c-a-A>",
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: "<c-1>",
expectedKey: gocui.NewKeyStrMod("1", gocui.ModCtrl),
expectedOk: true,
},
{
name: "modifier on non-ASCII uppercase letter is allowed",
label: "<a-Ñ>",
expectedKey: gocui.NewKeyStrMod("Ñ", gocui.ModAlt),
expectedOk: true,
},
// Modifiers with special keys
{
name: "ctrl+enter",
label: "<c-enter>",
expectedKey: gocui.NewKey(gocui.KeyEnter, "", gocui.ModCtrl),
expectedOk: true,
},
{
name: "alt+up",
label: "<a-up>",
expectedKey: gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModAlt),
expectedOk: true,
},
{
name: "shift+f1",
label: "<s-f1>",
expectedKey: gocui.NewKey(gocui.KeyF1, "", gocui.ModShift),
expectedOk: true,
},
{
name: "meta+enter",
label: "<m-enter>",
expectedKey: gocui.NewKey(gocui.KeyEnter, "", gocui.ModMeta),
expectedOk: true,
},
// Combined modifiers
{
name: "ctrl+alt+letter",
label: "<c-a-x>",
expectedKey: gocui.NewKeyStrMod("x", gocui.ModCtrl|gocui.ModAlt),
expectedOk: true,
},
{
name: "all four modifiers on a letter",
label: "<s-c-a-m-x>",
expectedKey: gocui.NewKeyStrMod("x", gocui.ModShift|gocui.ModCtrl|gocui.ModAlt|gocui.ModMeta),
expectedOk: true,
},
{
name: "ctrl+shift+arrow key",
label: "<c-s-up>",
expectedKey: gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModCtrl|gocui.ModShift),
expectedOk: true,
},
// Bracket handling
{
name: "single rune wrapped in brackets is unwrapped",
label: "<a>",
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: "<nope>",
expectedKey: gocui.Key{},
expectedOk: false,
},
{
name: "unknown modifier letter",
label: "<x-a>",
expectedKey: gocui.Key{},
expectedOk: false,
},
{
name: "uppercase modifier is not accepted",
label: "<C-a>",
expectedKey: gocui.Key{},
expectedOk: false,
},
{
name: "duplicate ctrl modifier",
label: "<c-c-a>",
expectedKey: gocui.Key{},
expectedOk: false,
},
{
name: "duplicate shift modifier",
label: "<s-s-a>",
expectedKey: gocui.Key{},
expectedOk: false,
},
{
name: "duplicate alt modifier",
label: "<a-a-x>",
expectedKey: gocui.Key{},
expectedOk: false,
},
{
name: "duplicate meta modifier",
label: "<m-m-x>",
expectedKey: gocui.Key{},
expectedOk: false,
},
{
name: "trailing modifier with no key",
label: "<c->",
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: "<c-nope>",
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), "<space>"},
{"ctrl+dash uses minus keyword", gocui.NewKeyStrMod("-", gocui.ModCtrl), "<c-minus>"},
{"alt+dash uses minus keyword", gocui.NewKeyStrMod("-", gocui.ModAlt), "<a-minus>"},
{"plain plus uses literal", gocui.NewKeyStrMod("+", gocui.ModNone), "+"},
{"ctrl+plus uses plus keyword", gocui.NewKeyStrMod("+", gocui.ModCtrl), "<c-plus>"},
{"alt+plus uses plus keyword", gocui.NewKeyStrMod("+", gocui.ModAlt), "<a-plus>"},
{"ctrl+space", gocui.NewKeyStrMod(" ", gocui.ModCtrl), "<c-space>"},
// Single modifier on a rune
{"ctrl+letter", gocui.NewKeyStrMod("a", gocui.ModCtrl), "<c-a>"},
{"alt+letter", gocui.NewKeyStrMod("x", gocui.ModAlt), "<a-x>"},
{"meta+letter", gocui.NewKeyStrMod("z", gocui.ModMeta), "<m-z>"},
{"shift+space", gocui.NewKeyStrMod(" ", gocui.ModShift), "<s-space>"},
// Modifier ordering — canonical output is c-, a-, s-, m-
{"ctrl+alt orders c before a", gocui.NewKeyStrMod("x", gocui.ModCtrl|gocui.ModAlt), "<c-a-x>"},
{"shift+ctrl orders c before s", gocui.NewKeyStrMod("x", gocui.ModShift|gocui.ModCtrl), "<c-s-x>"},
{"meta+shift orders s before m", gocui.NewKeyStrMod("x", gocui.ModMeta|gocui.ModShift), "<s-m-x>"},
{
"all four modifiers ordered c-a-s-m",
gocui.NewKeyStrMod("x", gocui.ModCtrl|gocui.ModAlt|gocui.ModShift|gocui.ModMeta),
"<c-a-s-m-x>",
},
// Special keys (always wrapped, even unmodified)
{"f1", gocui.NewKey(gocui.KeyF1, "", gocui.ModNone), "<f1>"},
{"f12", gocui.NewKey(gocui.KeyF12, "", gocui.ModNone), "<f12>"},
{"insert", gocui.NewKey(gocui.KeyInsert, "", gocui.ModNone), "<insert>"},
{"delete", gocui.NewKey(gocui.KeyDelete, "", gocui.ModNone), "<delete>"},
{"home", gocui.NewKey(gocui.KeyHome, "", gocui.ModNone), "<home>"},
{"end", gocui.NewKey(gocui.KeyEnd, "", gocui.ModNone), "<end>"},
{"pgup", gocui.NewKey(gocui.KeyPgup, "", gocui.ModNone), "<pgup>"},
{"pgdown", gocui.NewKey(gocui.KeyPgdn, "", gocui.ModNone), "<pgdown>"},
{"arrow up", gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModNone), "<up>"},
{"arrow down", gocui.NewKey(gocui.KeyArrowDown, "", gocui.ModNone), "<down>"},
{"arrow left", gocui.NewKey(gocui.KeyArrowLeft, "", gocui.ModNone), "<left>"},
{"arrow right", gocui.NewKey(gocui.KeyArrowRight, "", gocui.ModNone), "<right>"},
{"tab", gocui.NewKey(gocui.KeyTab, "", gocui.ModNone), "<tab>"},
{"backtab", gocui.NewKey(gocui.KeyBacktab, "", gocui.ModNone), "<backtab>"},
{"enter", gocui.NewKey(gocui.KeyEnter, "", gocui.ModNone), "<enter>"},
{"esc", gocui.NewKey(gocui.KeyEsc, "", gocui.ModNone), "<esc>"},
{"backspace", gocui.NewKey(gocui.KeyBackspace, "", gocui.ModNone), "<backspace>"},
{"mouse wheel up", gocui.NewKey(gocui.MouseWheelUp, "", gocui.ModNone), "<mouse wheel up>"},
{"mouse wheel down", gocui.NewKey(gocui.MouseWheelDown, "", gocui.ModNone), "<mouse wheel down>"},
// Modifiers on special keys
{"shift+f1", gocui.NewKey(gocui.KeyF1, "", gocui.ModShift), "<s-f1>"},
{"alt+up", gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModAlt), "<a-up>"},
{"meta+enter", gocui.NewKey(gocui.KeyEnter, "", gocui.ModMeta), "<m-enter>"},
{"ctrl+shift+up", gocui.NewKey(gocui.KeyArrowUp, "", gocui.ModCtrl|gocui.ModShift), "<c-s-up>"},
}
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)
})
}
}
+30 -27
View File
@@ -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
+11 -12
View File
@@ -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 {
+19 -2
View File
@@ -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
}
+12 -40
View File
@@ -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
)
+5 -34
View File
@@ -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{
@@ -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
}
@@ -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
}
+11 -11
View File
@@ -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()
+1 -1
View File
@@ -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,
)
+4 -4
View File
@@ -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())
}