mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04: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:
parent
30b619b3db
commit
22169e22ff
|
|
@ -221,8 +221,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||||
|
|
||||||
| Key | Action | Info |
|
| Key | Action | Info |
|
||||||
|-----|--------|-------------|
|
|-----|--------|-------------|
|
||||||
| `` mouse wheel down (fn+up) `` | Scroll down | |
|
| `` <mouse wheel down> (fn+up) `` | Scroll down | |
|
||||||
| `` mouse wheel up (fn+down) `` | Scroll up | |
|
| `` <mouse wheel up> (fn+down) `` | Scroll up | |
|
||||||
| `` <tab> `` | Switch view | Switch to other view (staged/unstaged changes). |
|
| `` <tab> `` | Switch view | Switch to other view (staged/unstaged changes). |
|
||||||
| `` <esc> `` | Exit back to side panel | |
|
| `` <esc> `` | Exit back to side panel | |
|
||||||
| `` / `` | Search the current view by text | |
|
| `` / `` | Search the current view by text | |
|
||||||
|
|
|
||||||
|
|
@ -304,8 +304,8 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
|
||||||
|
|
||||||
| Key | Action | Info |
|
| Key | Action | Info |
|
||||||
|-----|--------|-------------|
|
|-----|--------|-------------|
|
||||||
| `` mouse wheel down (fn+up) `` | 下にスクロール | |
|
| `` <mouse wheel down> (fn+up) `` | 下にスクロール | |
|
||||||
| `` mouse wheel up (fn+down) `` | 上にスクロール | |
|
| `` <mouse wheel up> (fn+down) `` | 上にスクロール | |
|
||||||
| `` <tab> `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 |
|
| `` <tab> `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 |
|
||||||
| `` <esc> `` | サイドパネルに戻る | |
|
| `` <esc> `` | サイドパネルに戻る | |
|
||||||
| `` / `` | 現在のビューをテキストで検索 | |
|
| `` / `` | 現在のビューをテキストで検索 | |
|
||||||
|
|
|
||||||
|
|
@ -160,8 +160,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||||
|
|
||||||
| Key | Action | Info |
|
| Key | Action | Info |
|
||||||
|-----|--------|-------------|
|
|-----|--------|-------------|
|
||||||
| `` mouse wheel down (fn+up) `` | 아래로 스크롤 | |
|
| `` <mouse wheel down> (fn+up) `` | 아래로 스크롤 | |
|
||||||
| `` mouse wheel up (fn+down) `` | 위로 스크롤 | |
|
| `` <mouse wheel up> (fn+down) `` | 위로 스크롤 | |
|
||||||
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
|
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
|
||||||
| `` <esc> `` | Exit back to side panel | |
|
| `` <esc> `` | Exit back to side panel | |
|
||||||
| `` / `` | 검색 시작 | |
|
| `` / `` | 검색 시작 | |
|
||||||
|
|
|
||||||
|
|
@ -229,8 +229,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||||
|
|
||||||
| Key | Action | Info |
|
| Key | Action | Info |
|
||||||
|-----|--------|-------------|
|
|-----|--------|-------------|
|
||||||
| `` mouse wheel down (fn+up) `` | Scroll omlaag | |
|
| `` <mouse wheel down> (fn+up) `` | Scroll omlaag | |
|
||||||
| `` mouse wheel up (fn+down) `` | Scroll omhoog | |
|
| `` <mouse wheel up> (fn+down) `` | Scroll omhoog | |
|
||||||
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
|
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
|
||||||
| `` <esc> `` | Exit back to side panel | |
|
| `` <esc> `` | Exit back to side panel | |
|
||||||
| `` / `` | Start met zoeken | |
|
| `` / `` | Start met zoeken | |
|
||||||
|
|
|
||||||
|
|
@ -179,8 +179,8 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
|
||||||
|
|
||||||
| Key | Action | Info |
|
| Key | Action | Info |
|
||||||
|-----|--------|-------------|
|
|-----|--------|-------------|
|
||||||
| `` mouse wheel down (fn+up) `` | Przewiń w dół | |
|
| `` <mouse wheel down> (fn+up) `` | Przewiń w dół | |
|
||||||
| `` mouse wheel up (fn+down) `` | Przewiń w górę | |
|
| `` <mouse wheel up> (fn+down) `` | Przewiń w górę | |
|
||||||
| `` <tab> `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). |
|
| `` <tab> `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). |
|
||||||
| `` <esc> `` | Exit back to side panel | |
|
| `` <esc> `` | Exit back to side panel | |
|
||||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||||
|
|
|
||||||
|
|
@ -233,8 +233,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||||
|
|
||||||
| Key | Action | Info |
|
| Key | Action | Info |
|
||||||
|-----|--------|-------------|
|
|-----|--------|-------------|
|
||||||
| `` mouse wheel down (fn+up) `` | Rolar para baixo | |
|
| `` <mouse wheel down> (fn+up) `` | Rolar para baixo | |
|
||||||
| `` mouse wheel up (fn+down) `` | Rolar para cima | |
|
| `` <mouse wheel up> (fn+down) `` | Rolar para cima | |
|
||||||
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
|
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
|
||||||
| `` <esc> `` | Exit back to side panel | |
|
| `` <esc> `` | Exit back to side panel | |
|
||||||
| `` / `` | Pesquisar na visualização atual por texto | |
|
| `` / `` | Pesquisar na visualização atual por texto | |
|
||||||
|
|
|
||||||
|
|
@ -104,8 +104,8 @@ _Связки клавиш_
|
||||||
|
|
||||||
| Key | Action | Info |
|
| Key | Action | Info |
|
||||||
|-----|--------|-------------|
|
|-----|--------|-------------|
|
||||||
| `` mouse wheel down (fn+up) `` | Прокрутить вниз | |
|
| `` <mouse wheel down> (fn+up) `` | Прокрутить вниз | |
|
||||||
| `` mouse wheel up (fn+down) `` | Прокрутить вверх | |
|
| `` <mouse wheel up> (fn+down) `` | Прокрутить вверх | |
|
||||||
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
|
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
|
||||||
| `` <esc> `` | Exit back to side panel | |
|
| `` <esc> `` | Exit back to side panel | |
|
||||||
| `` / `` | Найти | |
|
| `` / `` | Найти | |
|
||||||
|
|
|
||||||
|
|
@ -332,8 +332,8 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||||
|
|
||||||
| Key | Action | Info |
|
| Key | Action | Info |
|
||||||
|-----|--------|-------------|
|
|-----|--------|-------------|
|
||||||
| `` mouse wheel down (fn+up) `` | 向下滚动 | |
|
| `` <mouse wheel down> (fn+up) `` | 向下滚动 | |
|
||||||
| `` mouse wheel up (fn+down) `` | 向上滚动 | |
|
| `` <mouse wheel up> (fn+down) `` | 向上滚动 | |
|
||||||
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
|
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
|
||||||
| `` <esc> `` | 退出回到侧边面板 | |
|
| `` <esc> `` | 退出回到侧边面板 | |
|
||||||
| `` / `` | 开始搜索 | |
|
| `` / `` | 开始搜索 | |
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,8 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
|
||||||
|
|
||||||
| Key | Action | Info |
|
| Key | Action | Info |
|
||||||
|-----|--------|-------------|
|
|-----|--------|-------------|
|
||||||
| `` mouse wheel down (fn+up) `` | 向下捲動 | |
|
| `` <mouse wheel down> (fn+up) `` | 向下捲動 | |
|
||||||
| `` mouse wheel up (fn+down) `` | 向上捲動 | |
|
| `` <mouse wheel up> (fn+down) `` | 向上捲動 | |
|
||||||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
|
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
|
||||||
| `` <esc> `` | Exit back to side panel | |
|
| `` <esc> `` | Exit back to side panel | |
|
||||||
| `` / `` | 搜尋 | |
|
| `` / `` | 搜尋 | |
|
||||||
|
|
|
||||||
|
|
@ -14,63 +14,35 @@ import (
|
||||||
// docs/keybindings/Custom_Keybindings.md as well
|
// docs/keybindings/Custom_Keybindings.md as well
|
||||||
|
|
||||||
var labelByKey = map[gocui.KeyName]string{
|
var labelByKey = map[gocui.KeyName]string{
|
||||||
gocui.KeyF1: "<f1>",
|
gocui.KeyF1: "f1",
|
||||||
gocui.KeyF2: "<f2>",
|
gocui.KeyF2: "f2",
|
||||||
gocui.KeyF3: "<f3>",
|
gocui.KeyF3: "f3",
|
||||||
gocui.KeyF4: "<f4>",
|
gocui.KeyF4: "f4",
|
||||||
gocui.KeyF5: "<f5>",
|
gocui.KeyF5: "f5",
|
||||||
gocui.KeyF6: "<f6>",
|
gocui.KeyF6: "f6",
|
||||||
gocui.KeyF7: "<f7>",
|
gocui.KeyF7: "f7",
|
||||||
gocui.KeyF8: "<f8>",
|
gocui.KeyF8: "f8",
|
||||||
gocui.KeyF9: "<f9>",
|
gocui.KeyF9: "f9",
|
||||||
gocui.KeyF10: "<f10>",
|
gocui.KeyF10: "f10",
|
||||||
gocui.KeyF11: "<f11>",
|
gocui.KeyF11: "f11",
|
||||||
gocui.KeyF12: "<f12>",
|
gocui.KeyF12: "f12",
|
||||||
gocui.KeyInsert: "<insert>",
|
gocui.KeyInsert: "insert",
|
||||||
gocui.KeyDelete: "<delete>",
|
gocui.KeyDelete: "delete",
|
||||||
gocui.KeyHome: "<home>",
|
gocui.KeyHome: "home",
|
||||||
gocui.KeyEnd: "<end>",
|
gocui.KeyEnd: "end",
|
||||||
gocui.KeyPgup: "<pgup>",
|
gocui.KeyPgup: "pgup",
|
||||||
gocui.KeyPgdn: "<pgdown>",
|
gocui.KeyPgdn: "pgdown",
|
||||||
gocui.KeyArrowUp: "<up>",
|
gocui.KeyArrowUp: "up",
|
||||||
gocui.KeyShiftArrowUp: "<s-up>",
|
gocui.KeyArrowDown: "down",
|
||||||
gocui.KeyArrowDown: "<down>",
|
gocui.KeyArrowLeft: "left",
|
||||||
gocui.KeyShiftArrowDown: "<s-down>",
|
gocui.KeyArrowRight: "right",
|
||||||
gocui.KeyArrowLeft: "<left>",
|
gocui.KeyTab: "tab",
|
||||||
gocui.KeyArrowRight: "<right>",
|
gocui.KeyBacktab: "backtab",
|
||||||
gocui.KeyTab: "<tab>", // <c-i>
|
gocui.KeyEnter: "enter",
|
||||||
gocui.KeyBacktab: "<backtab>",
|
gocui.KeyEsc: "esc",
|
||||||
gocui.KeyEnter: "<enter>", // <c-m>
|
gocui.KeyBackspace: "backspace",
|
||||||
gocui.KeyAltEnter: "<a-enter>",
|
gocui.MouseWheelUp: "mouse wheel up",
|
||||||
gocui.KeyEsc: "<esc>", // <c-[>, <c-3>
|
gocui.MouseWheelDown: "mouse wheel down",
|
||||||
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",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyByLabel = lo.Invert(labelByKey)
|
var keyByLabel = lo.Invert(labelByKey)
|
||||||
|
|
@ -80,16 +52,44 @@ func LabelForKey(key gocui.Key) string {
|
||||||
return ""
|
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) {
|
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 utf8.RuneCountInString(label) > 1 {
|
||||||
if ok {
|
label = "<" + label + ">"
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "unknown"
|
return label
|
||||||
}
|
}
|
||||||
|
|
||||||
func KeyFromLabel(label string) (gocui.Key, bool) {
|
func KeyFromLabel(label string) (gocui.Key, bool) {
|
||||||
|
|
@ -97,16 +97,99 @@ func KeyFromLabel(label string) (gocui.Key, bool) {
|
||||||
return gocui.Key{}, true
|
return gocui.Key{}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
runeCount := utf8.RuneCountInString(label)
|
if strings.HasPrefix(label, "<") && strings.HasSuffix(label, ">") {
|
||||||
if runeCount > 1 {
|
label = label[1 : len(label)-1]
|
||||||
keyName, ok := keyByLabel[strings.ToLower(label)]
|
|
||||||
if !ok {
|
|
||||||
return gocui.Key{}, false
|
|
||||||
}
|
|
||||||
return gocui.NewKeyName(keyName), true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
func isValidKeybindingKey(key string) bool {
|
||||||
|
|
|
||||||
686
pkg/config/keynames_test.go
Normal file
686
pkg/config/keynames_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,63 +6,66 @@ package gocui
|
||||||
|
|
||||||
// Editor interface must be satisfied by gocui editors.
|
// Editor interface must be satisfied by gocui editors.
|
||||||
type Editor interface {
|
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
|
// 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)
|
// Editors. If f is a function with the appropriate signature, EditorFunc(f)
|
||||||
// is an Editor object that calls 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)
|
// Edit calls f(v, key, mod)
|
||||||
func (f EditorFunc) Edit(v *View, key Key, mod Modifier) bool {
|
func (f EditorFunc) Edit(v *View, key Key) bool {
|
||||||
return f(v, key, mod)
|
return f(v, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultEditor is the default editor.
|
// DefaultEditor is the default editor.
|
||||||
var DefaultEditor Editor = EditorFunc(SimpleEditor)
|
var DefaultEditor Editor = EditorFunc(SimpleEditor)
|
||||||
|
|
||||||
// SimpleEditor is used as the default gocui editor.
|
// 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 {
|
switch {
|
||||||
case (key.KeyName() == KeyBackspace || key.KeyName() == KeyBackspace2) && (mod&ModAlt) != 0,
|
case key.Equals(NewKey(KeyBackspace, "", ModAlt)),
|
||||||
key.KeyName() == KeyCtrlW:
|
key.Equals(NewKeyStrMod("w", ModCtrl)):
|
||||||
v.TextArea.BackSpaceWord()
|
v.TextArea.BackSpaceWord()
|
||||||
case key.KeyName() == KeyBackspace || key.KeyName() == KeyBackspace2 || key.KeyName() == KeyCtrlH:
|
case key.Equals(NewKeyName(KeyBackspace)):
|
||||||
v.TextArea.BackSpaceChar()
|
v.TextArea.BackSpaceChar()
|
||||||
case key.KeyName() == KeyCtrlD || key.KeyName() == KeyDelete:
|
case key.Equals(NewKeyStrMod("d", ModCtrl)),
|
||||||
|
key.Equals(NewKeyName(KeyDelete)):
|
||||||
v.TextArea.DeleteChar()
|
v.TextArea.DeleteChar()
|
||||||
case key.KeyName() == KeyArrowDown:
|
case key.Equals(NewKeyName(KeyArrowDown)):
|
||||||
v.TextArea.MoveCursorDown()
|
v.TextArea.MoveCursorDown()
|
||||||
case key.KeyName() == KeyArrowUp:
|
case key.Equals(NewKeyName(KeyArrowUp)):
|
||||||
v.TextArea.MoveCursorUp()
|
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()
|
v.TextArea.MoveLeftWord()
|
||||||
case key.KeyName() == KeyArrowLeft || key.KeyName() == KeyCtrlB:
|
case key.Equals(NewKeyName(KeyArrowLeft)),
|
||||||
|
key.Equals(NewKeyStrMod("b", ModCtrl)):
|
||||||
v.TextArea.MoveCursorLeft()
|
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()
|
v.TextArea.MoveRightWord()
|
||||||
case key.KeyName() == KeyArrowRight || key.KeyName() == KeyCtrlF:
|
case key.Equals(NewKeyName(KeyArrowRight)),
|
||||||
|
key.Equals(NewKeyStrMod("b", ModCtrl)):
|
||||||
v.TextArea.MoveCursorRight()
|
v.TextArea.MoveCursorRight()
|
||||||
case key.KeyName() == KeyEnter:
|
case key.Equals(NewKeyName(KeyEnter)):
|
||||||
v.TextArea.TypeCharacter("\n")
|
v.TextArea.TypeCharacter("\n")
|
||||||
case key.KeyName() == KeySpace:
|
case key.Equals(NewKeyName(KeyInsert)):
|
||||||
v.TextArea.TypeCharacter(" ")
|
|
||||||
case key.KeyName() == KeyInsert:
|
|
||||||
v.TextArea.ToggleOverwrite()
|
v.TextArea.ToggleOverwrite()
|
||||||
case key.KeyName() == KeyCtrlU:
|
case key.Equals(NewKeyStrMod("u", ModCtrl)):
|
||||||
v.TextArea.DeleteToStartOfLine()
|
v.TextArea.DeleteToStartOfLine()
|
||||||
case key.KeyName() == KeyCtrlK:
|
case key.Equals(NewKeyStrMod("k", ModCtrl)):
|
||||||
v.TextArea.DeleteToEndOfLine()
|
v.TextArea.DeleteToEndOfLine()
|
||||||
case key.KeyName() == KeyCtrlA || key.KeyName() == KeyHome:
|
case key.Equals(NewKeyStrMod("a", ModCtrl)),
|
||||||
|
key.Equals(NewKeyName(KeyHome)):
|
||||||
v.TextArea.GoToStartOfLine()
|
v.TextArea.GoToStartOfLine()
|
||||||
case key.KeyName() == KeyCtrlE || key.KeyName() == KeyEnd:
|
case key.Equals(NewKeyStrMod("e", ModCtrl)),
|
||||||
|
key.Equals(NewKeyName(KeyEnd)):
|
||||||
v.TextArea.GoToEndOfLine()
|
v.TextArea.GoToEndOfLine()
|
||||||
case key.KeyName() == KeyCtrlW:
|
case key.Equals(NewKeyStrMod("y", ModCtrl)):
|
||||||
v.TextArea.BackSpaceWord()
|
|
||||||
case key.KeyName() == KeyCtrlY:
|
|
||||||
v.TextArea.Yank()
|
v.TextArea.Yank()
|
||||||
case key.Str() != "":
|
case key.Str() != "" && key.Mod() == 0:
|
||||||
v.TextArea.TypeCharacter(key.Str())
|
v.TextArea.TypeCharacter(key.Str())
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -1264,17 +1264,16 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
|
||||||
switch ev.Type {
|
switch ev.Type {
|
||||||
case eventKey:
|
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
|
// 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
|
// https://github.com/ghostty-org/ghostty/commit/010338354a0) it does
|
||||||
// this conversion only for non-bracketed paste mode, but I'm seeing it
|
// this conversion only for non-bracketed paste mode, but I'm seeing it
|
||||||
// in bracketed paste mode. Whatever I'm missing here, converting '\r'
|
// in bracketed paste mode. Whatever I'm missing here, converting '\r'
|
||||||
// back to '\n' fixes pasting multi-line text from Ghostty, and doesn't
|
// back to '\n' fixes pasting multi-line text from Ghostty, and doesn't
|
||||||
// seem harmful for other terminal emulators.
|
// seem harmful for other terminal emulators.
|
||||||
//
|
if g.IsPasting && ev.Key.Equals(NewKeyStrMod("j", ModCtrl)) {
|
||||||
// KeyCtrlJ (int value 10) is '\r'.
|
|
||||||
if g.IsPasting && ev.Key.KeyName() == KeyCtrlJ {
|
|
||||||
ev.Key = NewKeyName(KeyEnter)
|
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 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 != "" {
|
if link := v.viewLines[newY].line[newX].hyperlink; link != "" {
|
||||||
return g.openHyperlink(link, v.name)
|
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 {
|
isMatch := func(binding *ViewMouseBinding) bool {
|
||||||
return binding.ViewName == view.Name() &&
|
return binding.ViewName == view.Name() &&
|
||||||
ev.Key.KeyName() == binding.Key &&
|
ev.Key.KeyName() == binding.Key &&
|
||||||
ev.Mod == binding.Modifier
|
ev.Key.Mod() == binding.Modifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// first pass looks for ones that match the focused view
|
// 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 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) {
|
if ev.Key.Equals(g.NextSearchMatchKey) {
|
||||||
return v.gotoNextMatch()
|
return v.gotoNextMatch()
|
||||||
} else if ev.Key.Equals(g.PrevSearchMatchKey) {
|
} else if ev.Key.Equals(g.PrevSearchMatchKey) {
|
||||||
|
|
@ -1508,7 +1507,7 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error {
|
||||||
if kb.handler == nil {
|
if kb.handler == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !kb.matchKeypress(ev.Key, ev.Mod) {
|
if !kb.matchKeypress(ev.Key) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if g.matchView(v, kb) {
|
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) {
|
if v != nil && g.matchView(v.ParentView, kb) {
|
||||||
matchingParentViewKb = 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
|
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 {
|
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 {
|
if matched {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -1595,7 +1594,7 @@ func (g *Gui) matchView(v *View, kb *keybinding) bool {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if v.Editable && kb.key.Str() != "" {
|
if v.Editable && kb.key.Str() != "" && kb.key.Mod() == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if kb.viewName != v.name {
|
if kb.viewName != v.name {
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,15 @@ import "github.com/gdamore/tcell/v3"
|
||||||
type Key struct {
|
type Key struct {
|
||||||
keyName KeyName
|
keyName KeyName
|
||||||
str string
|
str string
|
||||||
|
|
||||||
|
mod Modifier
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKey(keyName KeyName, str string) Key {
|
func NewKey(keyName KeyName, str string, mod Modifier) Key {
|
||||||
return Key{
|
return Key{
|
||||||
keyName: keyName,
|
keyName: keyName,
|
||||||
str: str,
|
str: str,
|
||||||
|
mod: mod,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -22,6 +25,7 @@ func NewKeyName(keyName KeyName) Key {
|
||||||
return Key{
|
return Key{
|
||||||
keyName: keyName,
|
keyName: keyName,
|
||||||
str: "",
|
str: "",
|
||||||
|
mod: ModNone,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,6 +33,15 @@ func NewKeyRune(ch rune) Key {
|
||||||
return Key{
|
return Key{
|
||||||
keyName: KeyName(tcell.KeyRune),
|
keyName: KeyName(tcell.KeyRune),
|
||||||
str: string(ch),
|
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
|
return k.str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (k Key) Mod() Modifier {
|
||||||
|
return k.mod
|
||||||
|
}
|
||||||
|
|
||||||
func (k Key) IsSet() bool {
|
func (k Key) IsSet() bool {
|
||||||
return k.keyName != 0
|
return k.keyName != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k Key) Equals(otherKey Key) bool {
|
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ func newKeybinding(viewname string, key Key, mod Modifier, handler func(*Gui, *V
|
||||||
}
|
}
|
||||||
|
|
||||||
// matchKeypress returns if the keybinding matches the keypress.
|
// matchKeypress returns if the keybinding matches the keypress.
|
||||||
func (kb *keybinding) matchKeypress(key Key, mod Modifier) bool {
|
func (kb *keybinding) matchKeypress(key Key) bool {
|
||||||
return kb.key.Equals(key) && kb.mod == mod
|
return kb.key.Equals(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special keys.
|
// Special keys.
|
||||||
|
|
@ -69,41 +69,12 @@ const (
|
||||||
|
|
||||||
// Keys combinations.
|
// Keys combinations.
|
||||||
const (
|
const (
|
||||||
KeyCtrlTilde = KeyName(tcell.KeyF64) // arbitrary assignment
|
KeyCtrlTilde = KeyName(tcell.KeyF64) // arbitrary assignment
|
||||||
KeyCtrlA = KeyName(tcell.KeyCtrlA)
|
KeyBackspace = KeyName(tcell.KeyBackspace)
|
||||||
KeyCtrlB = KeyName(tcell.KeyCtrlB)
|
KeyTab = KeyName(tcell.KeyTab)
|
||||||
KeyCtrlC = KeyName(tcell.KeyCtrlC)
|
KeyBacktab = KeyName(tcell.KeyBacktab)
|
||||||
KeyCtrlD = KeyName(tcell.KeyCtrlD)
|
KeyEnter = KeyName(tcell.KeyEnter)
|
||||||
KeyCtrlE = KeyName(tcell.KeyCtrlE)
|
KeyEsc = KeyName(tcell.KeyEscape)
|
||||||
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
|
|
||||||
|
|
||||||
// The following assignments were used in termbox implementation.
|
// The following assignments were used in termbox implementation.
|
||||||
// In tcell, these are not keys per se. But in gocui we have them
|
// In tcell, these are not keys per se. But in gocui we have them
|
||||||
|
|
@ -123,8 +94,9 @@ const (
|
||||||
// Modifiers.
|
// Modifiers.
|
||||||
const (
|
const (
|
||||||
ModNone Modifier = Modifier(0)
|
ModNone Modifier = Modifier(0)
|
||||||
|
ModShift = Modifier(tcell.ModShift)
|
||||||
|
ModCtrl = Modifier(tcell.ModCtrl)
|
||||||
ModAlt = Modifier(tcell.ModAlt)
|
ModAlt = Modifier(tcell.ModAlt)
|
||||||
ModMotion = Modifier(2) // just picking an arbitrary number here that doesn't clash with tcell.ModAlt
|
ModMeta = Modifier(tcell.ModMeta)
|
||||||
// ModCtrl doesn't work with keyboard keys. Use CtrlKey in Key and ModNone. This is was for mouse clicks only (tcell.v1)
|
ModMotion = Modifier(16) // just picking an arbitrary number here that doesn't clash with tcell's modifiers
|
||||||
// ModCtrl = Modifier(tcell.ModCtrl)
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,6 @@ type gocuiEventType uint8
|
||||||
// The 'Err' field is valid if 'Type' is 'eventError'.
|
// The 'Err' field is valid if 'Type' is 'eventError'.
|
||||||
type GocuiEvent struct {
|
type GocuiEvent struct {
|
||||||
Type gocuiEventType
|
Type gocuiEventType
|
||||||
Mod Modifier
|
|
||||||
Key Key
|
Key Key
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
|
|
@ -294,42 +293,15 @@ func (g *Gui) pollEvent() GocuiEvent {
|
||||||
ch := ""
|
ch := ""
|
||||||
if k == tcell.KeyRune {
|
if k == tcell.KeyRune {
|
||||||
ch = tev.Str()
|
ch = tev.Str()
|
||||||
if ch == " " {
|
} else if k >= tcell.KeyCtrlA && k <= tcell.KeyCtrlZ {
|
||||||
// special handling for spacebar
|
ch = string(rune('a' + (k - tcell.KeyCtrlA)))
|
||||||
k = tcell.Key(KeySpace)
|
k = tcell.KeyRune
|
||||||
ch = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
mod := tev.Modifiers()
|
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{
|
return GocuiEvent{
|
||||||
Type: eventKey,
|
Type: eventKey,
|
||||||
Key: NewKey(KeyName(k), ch),
|
Key: NewKey(KeyName(k), ch, Modifier(mod)),
|
||||||
Mod: Modifier(mod),
|
|
||||||
}
|
}
|
||||||
case *tcell.EventMouse:
|
case *tcell.EventMouse:
|
||||||
x, y := tev.Position()
|
x, y := tev.Position()
|
||||||
|
|
@ -411,8 +383,7 @@ func (g *Gui) pollEvent() GocuiEvent {
|
||||||
Type: eventMouse,
|
Type: eventMouse,
|
||||||
MouseX: x,
|
MouseX: x,
|
||||||
MouseY: y,
|
MouseY: y,
|
||||||
Key: NewKeyName(mouseKey),
|
Key: NewKey(mouseKey, "", mouseMod),
|
||||||
Mod: mouseMod,
|
|
||||||
}
|
}
|
||||||
case *tcell.EventFocus:
|
case *tcell.EventFocus:
|
||||||
return GocuiEvent{
|
return GocuiEvent{
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ func (self *CommitDescriptionController) handleTogglePanel() error {
|
||||||
// which is common in pasted code snippets.
|
// which is common in pasted code snippets.
|
||||||
view := self.Context().GetView()
|
view := self.Context().GetView()
|
||||||
for range 4 {
|
for range 4 {
|
||||||
view.Editor.Edit(view, gocui.NewKeyRune(' '), 0)
|
view.Editor.Edit(view, gocui.NewKeyRune(' '))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ func (self *CommitMessageController) handleTogglePanel() error {
|
||||||
// switch to the description panel.
|
// switch to the description panel.
|
||||||
view := self.context().GetView()
|
view := self.context().GetView()
|
||||||
for range 4 {
|
for range 4 {
|
||||||
view.Editor.Edit(view, gocui.NewKeyRune(' '), 0)
|
view.Editor.Edit(view, gocui.NewKeyRune(' '))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,33 +4,33 @@ import (
|
||||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (gui *Gui) handleEditorKeypress(v *gocui.View, key gocui.Key, mod gocui.Modifier, allowMultiline bool) bool {
|
func (gui *Gui) handleEditorKeypress(v *gocui.View, key gocui.Key, allowMultiline bool) bool {
|
||||||
if key.KeyName() == gocui.KeyEnter && allowMultiline {
|
if key.Equals(gocui.NewKeyName(gocui.KeyEnter)) && allowMultiline {
|
||||||
v.TextArea.TypeCharacter("\n")
|
v.TextArea.TypeCharacter("\n")
|
||||||
v.RenderTextArea()
|
v.RenderTextArea()
|
||||||
return true
|
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-
|
// 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
|
// render the commit message length on each keypress
|
||||||
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, mod gocui.Modifier) bool {
|
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key) bool {
|
||||||
matched := gui.handleEditorKeypress(v, key, mod, false)
|
matched := gui.handleEditorKeypress(v, key, false)
|
||||||
v.RenderTextArea()
|
v.RenderTextArea()
|
||||||
gui.c.Contexts().CommitMessage.RenderSubtitle()
|
gui.c.Contexts().CommitMessage.RenderSubtitle()
|
||||||
return matched
|
return matched
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) commitDescriptionEditor(v *gocui.View, key gocui.Key, mod gocui.Modifier) bool {
|
func (gui *Gui) commitDescriptionEditor(v *gocui.View, key gocui.Key) bool {
|
||||||
matched := gui.handleEditorKeypress(v, key, mod, true)
|
matched := gui.handleEditorKeypress(v, key, true)
|
||||||
v.RenderTextArea()
|
v.RenderTextArea()
|
||||||
return matched
|
return matched
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key, mod gocui.Modifier) bool {
|
func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key) bool {
|
||||||
matched := gui.handleEditorKeypress(v, key, mod, false)
|
matched := gui.handleEditorKeypress(v, key, false)
|
||||||
|
|
||||||
v.RenderTextArea()
|
v.RenderTextArea()
|
||||||
|
|
||||||
|
|
@ -46,8 +46,8 @@ func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key, mod gocui.Modifier) b
|
||||||
return matched
|
return matched
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) searchEditor(v *gocui.View, key gocui.Key, mod gocui.Modifier) bool {
|
func (gui *Gui) searchEditor(v *gocui.View, key gocui.Key) bool {
|
||||||
matched := gui.handleEditorKeypress(v, key, mod, false)
|
matched := gui.handleEditorKeypress(v, key, false)
|
||||||
v.RenderTextArea()
|
v.RenderTextArea()
|
||||||
|
|
||||||
searchString := v.TextArea.GetContent()
|
searchString := v.TextArea.GetContent()
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ func (self *GuiDriver) PressKey(keyStr string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.gui.g.ReplayedEvents.Keys <- gocui.NewTcellKeyEventWrapper(
|
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,
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ func RunTUI(raceDetector bool) {
|
||||||
log.Panicln(err)
|
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)
|
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 {
|
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, mod gocui.Modifier) bool {
|
return func(v *gocui.View, key gocui.Key) bool {
|
||||||
matched := f(v, key, mod)
|
matched := f(v, key)
|
||||||
if matched {
|
if matched {
|
||||||
self.filterWithString(v.TextArea.GetContent())
|
self.filterWithString(v.TextArea.GetContent())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue