Skip to content

Commit

Permalink
feat(input): support xterm keys and modifiers, urxvt, and fkeys option
Browse files Browse the repository at this point in the history
Also fix invalid CSI sequences parsing out of bound panic
  • Loading branch information
aymanbagabas committed Feb 15, 2024
1 parent d5246c5 commit a704d29
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 10 deletions.
14 changes: 9 additions & 5 deletions exp/term/input/ansi/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ const (
Ffindhome // treat find symbol as home
Fselectend // treat select symbol as end

Fxterm // register xterm keys
Fterminfo // use terminfo
FFKeys // preserve function keys

Stdflags = Ftabsym | Fentersym | Fescsym | Fspacesym | Fdelbackspace | Ffindhome | Fselectend | Fterminfo
Stdflags = Ftabsym | Fentersym | Fescsym | Fspacesym | Fdelbackspace | Ffindhome | Fselectend | Fterminfo | Fxterm
)

// driver represents a terminal ANSI input driver.
Expand All @@ -41,7 +43,9 @@ type driver struct {
var _ input.Driver = &driver{}

// NewDriver returns a new ANSI input driver.
// This driver uses ANSI control codes compatible with VT100/VT200 terminals.
// This driver uses ANSI control codes compatible with VT100/VT200 terminals,
// and XTerm. It supports reading Terminfo databases to overwrite the default
// key sequences.
func NewDriver(r io.Reader, term string, flags int) input.Driver {
if r == nil {
r = os.Stdin
Expand Down Expand Up @@ -238,17 +242,17 @@ func (d *driver) parseCsi(i int, p []byte, alt bool) (n int, e input.Event, err
seq := "\x1b["

// Scan parameter bytes in the range 0x30-0x3F
for ; p[i] >= 0x30 && p[i] <= 0x3F; i++ {
for ; i < len(p) && p[i] >= 0x30 && p[i] <= 0x3F; i++ {
n++
seq += string(p[i])
}
// Scan intermediate bytes in the range 0x20-0x2F
for ; p[i] >= 0x20 && p[i] <= 0x2F; i++ {
for ; i < len(p) && p[i] >= 0x20 && p[i] <= 0x2F; i++ {
n++
seq += string(p[i])
}
// Scan final byte in the range 0x40-0x7E
if p[i] < 0x40 || p[i] > 0x7E {
if i >= len(p) || p[i] < 0x40 || p[i] > 0x7E {
return n, nil, fmt.Errorf("%w: invalid CSI sequence: %q", input.ErrUnknownEvent, seq[2:])
}
n++
Expand Down
156 changes: 155 additions & 1 deletion exp/term/input/ansi/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,14 @@ func (d *driver) registerKeys(flags int) {
sel.Sym = input.KeyEnd
}

// The following is a table of key sequences and their corresponding key
// events based on the VT100/VT200 terminal specs.
//
// See: https://vt100.net/docs/vt100-ug/chapter3.html#S3.2
// See: https://vt100.net/docs/vt220-rm/chapter3.html
// See: https://vt100.net/docs/vt510-rm/chapter8.html
//
// XXX: These keys may be overwritten by other options like XTerm or
// Terminfo.
d.table = map[string]input.KeyEvent{
// C0 control characters
string(ansi.NUL): nul,
Expand Down Expand Up @@ -184,6 +189,155 @@ func (d *driver) registerKeys(flags int) {
"\x1b[34~": {Sym: input.KeyF20},
}

// CSI function keys
csiFuncKeys := map[string]input.KeyEvent{
"A": {Sym: input.KeyUp}, "B": {Sym: input.KeyDown},
"C": {Sym: input.KeyRight}, "D": {Sym: input.KeyLeft},
"E": {Sym: input.KeyBegin}, "F": {Sym: input.KeyEnd},
"H": {Sym: input.KeyHome}, "P": {Sym: input.KeyF1},
"Q": {Sym: input.KeyF2}, "R": {Sym: input.KeyF3},
"S": {Sym: input.KeyF4},
}

// CSI ~ sequence keys
csiTildeKeys := map[string]input.KeyEvent{
"1": find, "2": {Sym: input.KeyInsert},
"3": {Sym: input.KeyDelete}, "4": sel,
"5": {Sym: input.KeyPgUp}, "6": {Sym: input.KeyPgDown},
"7": {Sym: input.KeyHome}, "8": {Sym: input.KeyEnd},
// There are no 9 and 10 keys
"11": {Sym: input.KeyF1}, "12": {Sym: input.KeyF2},
"13": {Sym: input.KeyF3}, "14": {Sym: input.KeyF4},
"15": {Sym: input.KeyF5}, "17": {Sym: input.KeyF6},
"18": {Sym: input.KeyF7}, "19": {Sym: input.KeyF8},
"20": {Sym: input.KeyF9}, "21": {Sym: input.KeyF10},
"23": {Sym: input.KeyF11}, "24": {Sym: input.KeyF12},
"25": {Sym: input.KeyF13}, "26": {Sym: input.KeyF14},
"28": {Sym: input.KeyF15}, "29": {Sym: input.KeyF16},
"31": {Sym: input.KeyF17}, "32": {Sym: input.KeyF18},
"33": {Sym: input.KeyF19}, "34": {Sym: input.KeyF20},
}

if flags&Fxterm != 0 {
// XTerm modifiers
// These are offset by 1 to be compatible with our Mod type.
// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-PC-Style-Function-Keys
for _, m := range []input.Mod{
input.Shift, // 1
input.Alt, // 2
input.Shift | input.Alt, // 3
input.Ctrl, // 4
input.Shift | input.Ctrl, // 5
input.Alt | input.Ctrl, // 6
input.Shift | input.Alt | input.Ctrl, // 7
input.Meta, // 8
input.Meta | input.Shift, // 9
input.Meta | input.Alt, // 10
input.Meta | input.Shift | input.Alt, // 11
input.Meta | input.Ctrl, // 12
input.Meta | input.Shift | input.Ctrl, // 13
input.Meta | input.Alt | input.Ctrl, // 14
input.Meta | input.Shift | input.Alt | input.Ctrl, // 15
} {
// XTerm modifier offset +1
xtermMod := string('0' + byte(m+1))

// CSI 1 ; <modifier> <func>
for k, v := range csiFuncKeys {
// Functions always have a leading 1 param
seq := "\x1b[1;" + xtermMod + k
key := v
key.Mod = m
d.table[seq] = key
}
// CSI <number> ; <modifier> ~
for k, v := range csiTildeKeys {
seq := "\x1b[" + k + ";" + xtermMod + "~"
key := v
key.Mod = m
d.table[seq] = key
}
}
}

// URxvt keys
// See https://manpages.ubuntu.com/manpages/trusty/man7/urxvt.7.html#key%20codes
d.table["\x1b[a"] = input.KeyEvent{Sym: input.KeyUp, Mod: input.Shift}
d.table["\x1b[b"] = input.KeyEvent{Sym: input.KeyDown, Mod: input.Shift}
d.table["\x1b[c"] = input.KeyEvent{Sym: input.KeyRight, Mod: input.Shift}
d.table["\x1b[d"] = input.KeyEvent{Sym: input.KeyLeft, Mod: input.Shift}
d.table["\x1bOa"] = input.KeyEvent{Sym: input.KeyUp, Mod: input.Ctrl}
d.table["\x1bOb"] = input.KeyEvent{Sym: input.KeyDown, Mod: input.Ctrl}
d.table["\x1bOc"] = input.KeyEvent{Sym: input.KeyRight, Mod: input.Ctrl}
d.table["\x1bOd"] = input.KeyEvent{Sym: input.KeyLeft, Mod: input.Ctrl}
// TODO: invistigate if shift-ctrl arrow keys collide with DECCKM keys i.e.
// "\x1bOA", "\x1bOB", "\x1bOC", "\x1bOD"

// URxvt modifier CSI ~ keys
for k, v := range csiTildeKeys {
key := v
// Normal (no modifier) already defined part of VT100/VT200
// Shift modifier
key.Mod = input.Shift
d.table["\x1b["+k+"$"] = key
// Ctrl modifier
key.Mod = input.Ctrl
d.table["\x1b["+k+"^"] = key
// Shift-Ctrl modifier
key.Mod = input.Shift | input.Ctrl
d.table["\x1b["+k+"@"] = key
}

// URxvt F keys
// Note: Shift + F1-F10 generates F11-F20.
// This means Shift + F1 and Shift + F2 will generate F11 and F12, the same
// applies to Ctrl + Shift F1 & F2.
//
// P.S. Don't like this? Blame URxvt, configure your terminal to use
// different escapes like XTerm, or switch to a better terminal ¯\_(ツ)_/¯
//
// See https://manpages.ubuntu.com/manpages/trusty/man7/urxvt.7.html#key%20codes
d.table["\x1b[23$"] = input.KeyEvent{Sym: input.KeyF11, Mod: input.Shift}
d.table["\x1b[24$"] = input.KeyEvent{Sym: input.KeyF12, Mod: input.Shift}
d.table["\x1b[25$"] = input.KeyEvent{Sym: input.KeyF13, Mod: input.Shift}
d.table["\x1b[26$"] = input.KeyEvent{Sym: input.KeyF14, Mod: input.Shift}
d.table["\x1b[28$"] = input.KeyEvent{Sym: input.KeyF15, Mod: input.Shift}
d.table["\x1b[29$"] = input.KeyEvent{Sym: input.KeyF16, Mod: input.Shift}
d.table["\x1b[31$"] = input.KeyEvent{Sym: input.KeyF17, Mod: input.Shift}
d.table["\x1b[32$"] = input.KeyEvent{Sym: input.KeyF18, Mod: input.Shift}
d.table["\x1b[33$"] = input.KeyEvent{Sym: input.KeyF19, Mod: input.Shift}
d.table["\x1b[34$"] = input.KeyEvent{Sym: input.KeyF20, Mod: input.Shift}
d.table["\x1b[11^"] = input.KeyEvent{Sym: input.KeyF1, Mod: input.Ctrl}
d.table["\x1b[12^"] = input.KeyEvent{Sym: input.KeyF2, Mod: input.Ctrl}
d.table["\x1b[13^"] = input.KeyEvent{Sym: input.KeyF3, Mod: input.Ctrl}
d.table["\x1b[14^"] = input.KeyEvent{Sym: input.KeyF4, Mod: input.Ctrl}
d.table["\x1b[15^"] = input.KeyEvent{Sym: input.KeyF5, Mod: input.Ctrl}
d.table["\x1b[17^"] = input.KeyEvent{Sym: input.KeyF6, Mod: input.Ctrl}
d.table["\x1b[18^"] = input.KeyEvent{Sym: input.KeyF7, Mod: input.Ctrl}
d.table["\x1b[19^"] = input.KeyEvent{Sym: input.KeyF8, Mod: input.Ctrl}
d.table["\x1b[20^"] = input.KeyEvent{Sym: input.KeyF9, Mod: input.Ctrl}
d.table["\x1b[21^"] = input.KeyEvent{Sym: input.KeyF10, Mod: input.Ctrl}
d.table["\x1b[23^"] = input.KeyEvent{Sym: input.KeyF11, Mod: input.Ctrl}
d.table["\x1b[24^"] = input.KeyEvent{Sym: input.KeyF12, Mod: input.Ctrl}
d.table["\x1b[25^"] = input.KeyEvent{Sym: input.KeyF13, Mod: input.Ctrl}
d.table["\x1b[26^"] = input.KeyEvent{Sym: input.KeyF14, Mod: input.Ctrl}
d.table["\x1b[28^"] = input.KeyEvent{Sym: input.KeyF15, Mod: input.Ctrl}
d.table["\x1b[29^"] = input.KeyEvent{Sym: input.KeyF16, Mod: input.Ctrl}
d.table["\x1b[31^"] = input.KeyEvent{Sym: input.KeyF17, Mod: input.Ctrl}
d.table["\x1b[32^"] = input.KeyEvent{Sym: input.KeyF18, Mod: input.Ctrl}
d.table["\x1b[33^"] = input.KeyEvent{Sym: input.KeyF19, Mod: input.Ctrl}
d.table["\x1b[34^"] = input.KeyEvent{Sym: input.KeyF20, Mod: input.Ctrl}
d.table["\x1b[23@"] = input.KeyEvent{Sym: input.KeyF11, Mod: input.Shift | input.Ctrl}
d.table["\x1b[24@"] = input.KeyEvent{Sym: input.KeyF12, Mod: input.Shift | input.Ctrl}
d.table["\x1b[25@"] = input.KeyEvent{Sym: input.KeyF13, Mod: input.Shift | input.Ctrl}
d.table["\x1b[26@"] = input.KeyEvent{Sym: input.KeyF14, Mod: input.Shift | input.Ctrl}
d.table["\x1b[28@"] = input.KeyEvent{Sym: input.KeyF15, Mod: input.Shift | input.Ctrl}
d.table["\x1b[29@"] = input.KeyEvent{Sym: input.KeyF16, Mod: input.Shift | input.Ctrl}
d.table["\x1b[31@"] = input.KeyEvent{Sym: input.KeyF17, Mod: input.Shift | input.Ctrl}
d.table["\x1b[32@"] = input.KeyEvent{Sym: input.KeyF18, Mod: input.Shift | input.Ctrl}
d.table["\x1b[33@"] = input.KeyEvent{Sym: input.KeyF19, Mod: input.Shift | input.Ctrl}
d.table["\x1b[34@"] = input.KeyEvent{Sym: input.KeyF20, Mod: input.Shift | input.Ctrl}

// Register Alt + <key> combinations
for k, v := range d.table {
v.Mod |= input.Alt
Expand Down
70 changes: 66 additions & 4 deletions exp/term/input/ansi/terminfo.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ansi

import (
"log"
"strings"

"github.com/charmbracelet/x/exp/term/input"
Expand All @@ -13,26 +14,30 @@ func (d *driver) registerTerminfoKeys() {
return
}

tiTable := defaultTerminfoKeys()
log.Printf("Found terminfo database for %q: %#v\r\n", d.term, ti.Names)

tiTable := defaultTerminfoKeys(d.flags)

// Default keys
for name, seq := range ti.StringCapsShort() {
if !strings.HasPrefix(name, "k") {
if !strings.HasPrefix(name, "k") || len(seq) == 0 {
continue
}

if k, ok := tiTable[name]; ok {
log.Printf("registering terminfo key %q: %q as %q\r\n", seq, name, k)
d.table[string(seq)] = k
}
}

// Extended keys
for name, seq := range ti.ExtStringCapsShort() {
if !strings.HasPrefix(name, "k") {
if !strings.HasPrefix(name, "k") || len(seq) == 0 {
continue
}

if k, ok := tiTable[name]; ok {
log.Printf("registering terminfo key %q: %q as %q\r\n", seq, name, k)
d.table[string(seq)] = k
}
}
Expand All @@ -52,7 +57,7 @@ func (d *driver) registerTerminfoKeys() {
//
// See https://man7.org/linux/man-pages/man5/terminfo.5.html
// See https://github.com/mirror/ncurses/blob/master/include/Caps-ncurses
func defaultTerminfoKeys() map[string]input.KeyEvent {
func defaultTerminfoKeys(flags int) map[string]input.KeyEvent {
keys := map[string]input.KeyEvent{
"kcuu1": {Sym: input.KeyUp},
"kUP": {Sym: input.KeyUp, Mod: input.Shift},
Expand Down Expand Up @@ -214,5 +219,62 @@ func defaultTerminfoKeys() map[string]input.KeyEvent {
"kf62": {Sym: input.KeyF2, Mod: input.Shift | input.Alt},
"kf63": {Sym: input.KeyF3, Mod: input.Shift | input.Alt},
}

// Preserve F keys from F13 to F63 instead of using them for F-keys
// modifiers.
if flags&FFKeys != 0 {
keys["kf13"] = input.KeyEvent{Sym: input.KeyF13}
keys["kf14"] = input.KeyEvent{Sym: input.KeyF14}
keys["kf15"] = input.KeyEvent{Sym: input.KeyF15}
keys["kf16"] = input.KeyEvent{Sym: input.KeyF16}
keys["kf17"] = input.KeyEvent{Sym: input.KeyF17}
keys["kf18"] = input.KeyEvent{Sym: input.KeyF18}
keys["kf19"] = input.KeyEvent{Sym: input.KeyF19}
keys["kf20"] = input.KeyEvent{Sym: input.KeyF20}
keys["kf21"] = input.KeyEvent{Sym: input.KeyF21}
keys["kf22"] = input.KeyEvent{Sym: input.KeyF22}
keys["kf23"] = input.KeyEvent{Sym: input.KeyF23}
keys["kf24"] = input.KeyEvent{Sym: input.KeyF24}
keys["kf25"] = input.KeyEvent{Sym: input.KeyF25}
keys["kf26"] = input.KeyEvent{Sym: input.KeyF26}
keys["kf27"] = input.KeyEvent{Sym: input.KeyF27}
keys["kf28"] = input.KeyEvent{Sym: input.KeyF28}
keys["kf29"] = input.KeyEvent{Sym: input.KeyF29}
keys["kf30"] = input.KeyEvent{Sym: input.KeyF30}
keys["kf31"] = input.KeyEvent{Sym: input.KeyF31}
keys["kf32"] = input.KeyEvent{Sym: input.KeyF32}
keys["kf33"] = input.KeyEvent{Sym: input.KeyF33}
keys["kf34"] = input.KeyEvent{Sym: input.KeyF34}
keys["kf35"] = input.KeyEvent{Sym: input.KeyF35}
keys["kf36"] = input.KeyEvent{Sym: input.KeyF36}
keys["kf37"] = input.KeyEvent{Sym: input.KeyF37}
keys["kf38"] = input.KeyEvent{Sym: input.KeyF38}
keys["kf39"] = input.KeyEvent{Sym: input.KeyF39}
keys["kf40"] = input.KeyEvent{Sym: input.KeyF40}
keys["kf41"] = input.KeyEvent{Sym: input.KeyF41}
keys["kf42"] = input.KeyEvent{Sym: input.KeyF42}
keys["kf43"] = input.KeyEvent{Sym: input.KeyF43}
keys["kf44"] = input.KeyEvent{Sym: input.KeyF44}
keys["kf45"] = input.KeyEvent{Sym: input.KeyF45}
keys["kf46"] = input.KeyEvent{Sym: input.KeyF46}
keys["kf47"] = input.KeyEvent{Sym: input.KeyF47}
keys["kf48"] = input.KeyEvent{Sym: input.KeyF48}
keys["kf49"] = input.KeyEvent{Sym: input.KeyF49}
keys["kf50"] = input.KeyEvent{Sym: input.KeyF50}
keys["kf51"] = input.KeyEvent{Sym: input.KeyF51}
keys["kf52"] = input.KeyEvent{Sym: input.KeyF52}
keys["kf53"] = input.KeyEvent{Sym: input.KeyF53}
keys["kf54"] = input.KeyEvent{Sym: input.KeyF54}
keys["kf55"] = input.KeyEvent{Sym: input.KeyF55}
keys["kf56"] = input.KeyEvent{Sym: input.KeyF56}
keys["kf57"] = input.KeyEvent{Sym: input.KeyF57}
keys["kf58"] = input.KeyEvent{Sym: input.KeyF58}
keys["kf59"] = input.KeyEvent{Sym: input.KeyF59}
keys["kf60"] = input.KeyEvent{Sym: input.KeyF60}
keys["kf61"] = input.KeyEvent{Sym: input.KeyF61}
keys["kf62"] = input.KeyEvent{Sym: input.KeyF62}
keys["kf63"] = input.KeyEvent{Sym: input.KeyF63}
}

return keys
}
6 changes: 6 additions & 0 deletions exp/term/input/mod.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const (
Shift Mod = 1 << iota
Alt
Ctrl
Meta
)

// IsShift reports whether the Shift modifier is set.
Expand All @@ -24,3 +25,8 @@ func (m Mod) IsAlt() bool {
func (m Mod) IsCtrl() bool {
return m&Ctrl != 0
}

// IsMeta reports whether the Meta modifier is set.
func (m Mod) IsMeta() bool {
return m&Meta != 0
}

0 comments on commit a704d29

Please sign in to comment.