189 lines
6.3 KiB
Go
189 lines
6.3 KiB
Go
// Package keymap parses tmux-style key names ("Enter", "C-c", "C-M-Up") into
|
||
// terminal input. A parsed Key is either a key event handed to the vt
|
||
// emulator's mode-aware encoder, or — for modified special keys, which the vt
|
||
// encoder does not support — pre-encoded xterm escape bytes.
|
||
package keymap
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"unicode"
|
||
"unicode/utf8"
|
||
|
||
uv "github.com/charmbracelet/ultraviolet"
|
||
)
|
||
|
||
// Key is a parsed key name.
|
||
type Key struct {
|
||
Name string
|
||
event *uv.KeyPressEvent
|
||
raw []byte
|
||
}
|
||
|
||
// Event returns the key event to pass to the emulator's encoder, if this key
|
||
// is represented as one.
|
||
func (k Key) Event() (uv.KeyPressEvent, bool) {
|
||
if k.event == nil {
|
||
return uv.KeyPressEvent{}, false
|
||
}
|
||
return *k.event, true
|
||
}
|
||
|
||
// Raw returns the pre-encoded escape bytes, if this key is represented as raw
|
||
// bytes instead of an event.
|
||
func (k Key) Raw() ([]byte, bool) {
|
||
if k.raw == nil {
|
||
return nil, false
|
||
}
|
||
return k.raw, true
|
||
}
|
||
|
||
// specialKey is a named non-character key and how it can be encoded when
|
||
// modified. csiChar covers CSI 1;m<ch> keys (arrows, Home/End, F1–F4);
|
||
// csiNum covers CSI <n>;m~ keys (Insert/Delete/PgUp/PgDn, F5–F12).
|
||
type specialKey struct {
|
||
code rune // uv key code
|
||
csiChar byte // 0 if not a CSI-char key
|
||
csiNum int // 0 if not a CSI-number key
|
||
}
|
||
|
||
var specials = map[string]specialKey{
|
||
"enter": {code: uv.KeyEnter},
|
||
"escape": {code: uv.KeyEscape},
|
||
"esc": {code: uv.KeyEscape},
|
||
"tab": {code: uv.KeyTab},
|
||
"backspace": {code: uv.KeyBackspace},
|
||
"bspace": {code: uv.KeyBackspace},
|
||
"up": {code: uv.KeyUp, csiChar: 'A'},
|
||
"down": {code: uv.KeyDown, csiChar: 'B'},
|
||
"right": {code: uv.KeyRight, csiChar: 'C'},
|
||
"left": {code: uv.KeyLeft, csiChar: 'D'},
|
||
"home": {code: uv.KeyHome, csiChar: 'H'},
|
||
"end": {code: uv.KeyEnd, csiChar: 'F'},
|
||
"insert": {code: uv.KeyInsert, csiNum: 2},
|
||
"ic": {code: uv.KeyInsert, csiNum: 2},
|
||
"delete": {code: uv.KeyDelete, csiNum: 3},
|
||
"dc": {code: uv.KeyDelete, csiNum: 3},
|
||
"pgup": {code: uv.KeyPgUp, csiNum: 5},
|
||
"ppage": {code: uv.KeyPgUp, csiNum: 5},
|
||
"pageup": {code: uv.KeyPgUp, csiNum: 5},
|
||
"pgdn": {code: uv.KeyPgDown, csiNum: 6},
|
||
"npage": {code: uv.KeyPgDown, csiNum: 6},
|
||
"pagedown": {code: uv.KeyPgDown, csiNum: 6},
|
||
"f1": {code: uv.KeyF1, csiChar: 'P'},
|
||
"f2": {code: uv.KeyF2, csiChar: 'Q'},
|
||
"f3": {code: uv.KeyF3, csiChar: 'R'},
|
||
"f4": {code: uv.KeyF4, csiChar: 'S'},
|
||
"f5": {code: uv.KeyF5, csiNum: 15},
|
||
"f6": {code: uv.KeyF6, csiNum: 17},
|
||
"f7": {code: uv.KeyF7, csiNum: 18},
|
||
"f8": {code: uv.KeyF8, csiNum: 19},
|
||
"f9": {code: uv.KeyF9, csiNum: 20},
|
||
"f10": {code: uv.KeyF10, csiNum: 21},
|
||
"f11": {code: uv.KeyF11, csiNum: 23},
|
||
"f12": {code: uv.KeyF12, csiNum: 24},
|
||
// Space is handled as the rune ' ' but gets a name for convenience.
|
||
"space": {code: uv.KeySpace},
|
||
// BTab = Shift-Tab; the emulator encodes {Tab, Shift} as CSI Z.
|
||
"btab": {code: uv.KeyTab},
|
||
}
|
||
|
||
// ctrlEncodable are the non-letter runes the vt encoder maps with Ctrl.
|
||
const ctrlEncodable = " [\\]^_"
|
||
|
||
// Parse turns a tmux-style key name into a Key. Modifier prefixes C- (Ctrl),
|
||
// M- (Alt/Meta) and S- (Shift) stack in any order; the base is either a named
|
||
// key or a single character. The final character may itself be '-' (C--).
|
||
func Parse(name string) (Key, error) {
|
||
rest := name
|
||
var mod uv.KeyMod
|
||
for len(rest) > 2 && rest[1] == '-' {
|
||
switch rest[0] {
|
||
case 'C':
|
||
mod |= uv.ModCtrl
|
||
case 'M':
|
||
mod |= uv.ModAlt
|
||
case 'S':
|
||
mod |= uv.ModShift
|
||
default:
|
||
return Key{}, fmt.Errorf("unknown key %q: bad modifier prefix %q", name, rest[:2])
|
||
}
|
||
rest = rest[2:]
|
||
}
|
||
if rest == "" {
|
||
return Key{}, fmt.Errorf("unknown key %q: empty base key", name)
|
||
}
|
||
|
||
if sp, ok := specials[strings.ToLower(rest)]; ok {
|
||
if strings.EqualFold(rest, "btab") {
|
||
mod |= uv.ModShift
|
||
}
|
||
return encodeSpecial(name, sp, mod)
|
||
}
|
||
|
||
r, size := utf8.DecodeRuneInString(rest)
|
||
if r == utf8.RuneError || size != len(rest) {
|
||
return Key{}, fmt.Errorf("unknown key %q", name)
|
||
}
|
||
return encodeRune(name, r, mod)
|
||
}
|
||
|
||
func encodeSpecial(name string, sp specialKey, mod uv.KeyMod) (Key, error) {
|
||
// Shift-Tab is the one modified special the emulator knows (CSI Z).
|
||
if sp.code == uv.KeyTab && mod == uv.ModShift {
|
||
return Key{Name: name, event: &uv.KeyPressEvent{Code: uv.KeyTab, Mod: uv.ModShift}}, nil
|
||
}
|
||
if sp.code == uv.KeySpace {
|
||
return encodeRune(name, ' ', mod)
|
||
}
|
||
if mod == 0 || (mod == uv.ModAlt && sp.csiChar == 0 && sp.csiNum == 0) {
|
||
// Unmodified — or Alt on a key with no CSI form (M-Enter = ESC CR),
|
||
// which the emulator encodes as an ESC prefix.
|
||
return Key{Name: name, event: &uv.KeyPressEvent{Code: sp.code, Mod: mod}}, nil
|
||
}
|
||
// Modified special keys use the xterm encoding CSI 1;m<ch> / CSI n;m~
|
||
// (m = 1 + Shift(1) + Alt(2) + Ctrl(4)). The vt encoder has no support
|
||
// for these, so we pre-encode the bytes ourselves. xterm emits the CSI
|
||
// form for modified arrows regardless of DECCKM, so bypassing the
|
||
// emulator's DECCKM handling here matches real terminals.
|
||
m := 1
|
||
if mod&uv.ModShift != 0 {
|
||
m += 1
|
||
}
|
||
if mod&uv.ModAlt != 0 {
|
||
m += 2
|
||
}
|
||
if mod&uv.ModCtrl != 0 {
|
||
m += 4
|
||
}
|
||
switch {
|
||
case sp.csiChar != 0:
|
||
return Key{Name: name, raw: fmt.Appendf(nil, "\x1b[1;%d%c", m, sp.csiChar)}, nil
|
||
case sp.csiNum != 0:
|
||
return Key{Name: name, raw: fmt.Appendf(nil, "\x1b[%d;%d~", sp.csiNum, m)}, nil
|
||
default:
|
||
return Key{}, fmt.Errorf("cannot encode %q: no escape sequence exists for that modifier combination", name)
|
||
}
|
||
}
|
||
|
||
func encodeRune(name string, r rune, mod uv.KeyMod) (Key, error) {
|
||
if mod&uv.ModShift != 0 {
|
||
// Shift on a character is the shifted character itself; letters are
|
||
// the only case we can shift reliably.
|
||
up := unicode.ToUpper(r)
|
||
if up == r {
|
||
return Key{}, fmt.Errorf("cannot encode %q: S- is only meaningful on letters, write the shifted character directly", name)
|
||
}
|
||
r = up
|
||
mod &^= uv.ModShift
|
||
}
|
||
if mod&uv.ModCtrl != 0 {
|
||
lo := unicode.ToLower(r)
|
||
if !(lo >= 'a' && lo <= 'z') && !strings.ContainsRune(ctrlEncodable, lo) {
|
||
return Key{}, fmt.Errorf("cannot encode %q: C-%c has no control character", name, r)
|
||
}
|
||
// The vt encoder matches Ctrl combos on the lowercase rune.
|
||
r = lo
|
||
}
|
||
return Key{Name: name, event: &uv.KeyPressEvent{Code: r, Mod: mod}}, nil
|
||
}
|