// 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 keys (arrows, Home/End, F1–F4); // csiNum covers CSI ;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 / 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 }