Skip to content

Commit

Permalink
fix(input): xterm modifiers and improve sequence parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed Feb 16, 2024
1 parent b7709b6 commit 42a0b15
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 120 deletions.
2 changes: 1 addition & 1 deletion exp/term/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/charmbracelet/x/exp/term

go 1.17
go 1.18

require (
github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651
Expand Down
25 changes: 0 additions & 25 deletions exp/term/go.sum
Original file line number Diff line number Diff line change
@@ -1,32 +1,7 @@
github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 h1:3RXpZWGWTOeVXCTv0Dnzxdv/MhNUkBfEcbaTY0zrTQI=
github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
172 changes: 79 additions & 93 deletions exp/term/input/ansi/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,8 @@ func (d *driver) peekInput() (int, []input.Event, error) {
return len(p), []input.Event{k}, nil
}

peekedBytes := 0
i := 0 // index of the current byte

addEvent := func(n int, e input.Event) {
peekedBytes += n
i += n
ev = append(ev, e)
}

for i < len(p) {
var alt bool
b := p[i]
Expand All @@ -185,7 +178,8 @@ func (d *driver) peekInput() (int, []input.Event, error) {
case ansi.ESC:
if bufferedBytes == 1 {
// Special case for Esc
addEvent(1, d.table[esc])
i++
ev = append(ev, d.table[esc])
continue
}

Expand All @@ -194,64 +188,32 @@ func (d *driver) peekInput() (int, []input.Event, error) {
break
}

i++ // we know there's at least one more byte
peekedBytes++
switch p[i] {
switch p[i+1] {
case 'O': // Esc-prefixed SS3
nb, e, err := d.parseSs3(i, p, alt)
if err != nil {
return peekedBytes, ev, err
}

addEvent(nb, e)
d.handleSeq(d.parseSs3, i, p, alt, &i, &ev)
continue
case 'P': // Esc-prefixed DCS
case '[': // Esc-prefixed CSI
nb, e, err := d.parseCsi(i, p, alt)
if err != nil {
return peekedBytes, ev, err
}

addEvent(nb, e)
d.handleSeq(d.parseCsi, i, p, alt, &i, &ev)
continue
case ']': // Esc-prefixed OSC
nb, e, err := d.parseOsc(i, p, alt)
if err != nil {
return peekedBytes, ev, err
}

addEvent(nb, e)
d.handleSeq(d.parseOsc, i, p, alt, &i, &ev)
continue
}

alt = true
b = p[i]
b = p[i+1]

goto begin
case ansi.SS3:
nb, e, err := d.parseSs3(i, p, alt)
if err != nil {
return peekedBytes, ev, err
}

addEvent(nb, e)
d.handleSeq(d.parseSs3, i, p, alt, &i, &ev)
continue
case ansi.DCS:
case ansi.CSI:
nb, e, err := d.parseCsi(i, p, alt)
if err != nil {
return peekedBytes, ev, err
}

addEvent(nb, e)
d.handleSeq(d.parseCsi, i, p, alt, &i, &ev)
continue
case ansi.OSC:
nb, e, err := d.parseOsc(i, p, alt)
if err != nil {
return peekedBytes, ev, err
}

addEvent(nb, e)
d.handleSeq(d.parseOsc, i, p, alt, &i, &ev)
continue
}

Expand All @@ -262,12 +224,13 @@ func (d *driver) peekInput() (int, []input.Event, error) {
if alt {
k.Mod |= input.Alt
}
addEvent(nb, k)
i += nb
ev = append(ev, k)
continue
} else if utf8.RuneStart(b) { // Printable ASCII/UTF-8
nb := utf8ByteLen(b)
if nb == -1 || nb > bufferedBytes {
return peekedBytes, ev, fmt.Errorf("invalid UTF-8 sequence: %x", p)
return i, ev, fmt.Errorf("invalid UTF-8 sequence: %x", p)
}

r := rune(b)
Expand All @@ -280,48 +243,55 @@ func (d *driver) peekInput() (int, []input.Event, error) {
k.Mod |= input.Alt
}

addEvent(nb, k)
i += nb
ev = append(ev, k)
continue
}
}

return peekedBytes, ev, nil
return i, ev, nil
}

func (d *driver) parseCsi(i int, p []byte, alt bool) (n int, e input.Event, err error) {
if p[i] == '[' {
n++
func (d *driver) parseCsi(i int, p []byte, alt bool) (int, input.Event) {
var seq string
if p[i] == ansi.CSI || p[i] == ansi.ESC {
seq += string(p[i])
i++
}
if i < len(p) && p[i-1] == ansi.ESC && p[i] == '[' {
seq += string(p[i])
i++
}

i++
seq := "\x1b["

// Scan parameter bytes in the range 0x30-0x3F
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 ; i < len(p) && p[i] >= 0x20 && p[i] <= 0x2F; i++ {
n++
seq += string(p[i])
}
// Scan final byte in the range 0x40-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:])
if key, ok := d.table[seq]; ok {
if alt {
key.Mod |= input.Alt
}
return len(seq), key
}
return len(seq), input.UnknownEvent{string(seq)}
}
n++
seq += string(p[i])

seq += string(p[i])
csi := ansi.CsiSequence(seq)
initial := csi.Initial()
cmd := csi.Command()
switch {
case seq == "\x1b[M" && i+3 < len(p):
// Handle X10 mouse
return n + 3, parseX10MouseEvent(append([]byte(seq), p[i+1:i+3]...)), nil
return len(seq) + 3, parseX10MouseEvent(append([]byte(seq), p[i+1:i+3]...))
case initial == '<' && (cmd == 'm' || cmd == 'M'):
return n, parseSGRMouseEvent([]byte(seq)), nil
return len(seq), parseSGRMouseEvent([]byte(seq))
case initial == 0 && cmd == 'u':
// Kitty keyboard protocol
params := ansi.Params(csi.Params())
Expand Down Expand Up @@ -354,78 +324,94 @@ func (d *driver) parseCsi(i int, p []byte, alt bool) (n int, e input.Event, err
if len(params) > 2 {
key.Rune = rune(params[2][0])
}
return n, key, nil
return len(seq), key
}

k, ok := d.table[seq]
if ok {
if alt {
k.Mod |= input.Alt
}
return n, k, nil
return len(seq), k
}

return n, csiSequence(seq), nil
return len(seq), csiSequence(seq)
}

// parseSs3 parses a SS3 sequence.
// See https://vt100.net/docs/vt220-rm/chapter4.html#S4.4.4.2
func (d *driver) parseSs3(i int, p []byte, alt bool) (n int, e input.Event, err error) {
if p[i] == 'O' {
n++
func (d *driver) parseSs3(i int, p []byte, alt bool) (int, input.Event) {
var seq string
if p[i] == ansi.SS3 || p[i] == ansi.ESC {
seq += string(p[i])
i++
}
if i < len(p) && p[i-1] == ansi.ESC && p[i] == 'O' {
seq += string(p[i])
i++
}

i++
seq := "\x1bO"

// Scan a GL character
// A GL character is a single byte in the range 0x21-0x7E
// See https://vt100.net/docs/vt220-rm/chapter2.html#S2.3.2
if i >= len(p) || p[i] < 0x21 || p[i] > 0x7E {
return n, nil, fmt.Errorf("%w: invalid SS3 sequence: %q", input.ErrUnknownEvent, p[i])
if key, ok := d.table[seq]; ok {
if alt {
key.Mod |= input.Alt
}
return len(seq), key
}
return len(seq), input.UnknownEvent{string(seq)}
}
n++
seq += string(p[i])

k, ok := d.table[seq]
if ok {
if alt {
k.Mod |= input.Alt
}
return n, k, nil
return len(seq), k
}

return n, ss3Sequence(seq), nil
return len(seq), ss3Sequence(seq)
}

func (d *driver) parseOsc(i int, p []byte, _ bool) (n int, e input.Event, err error) {
if p[i] == ']' {
n++
}
func (d *driver) handleSeq(
seqFn func(int, []byte, bool) (int, input.Event),
i int, p []byte, alt bool,
np *int, ne *[]input.Event,
) {
n, e := seqFn(i, p, alt)
*np += n
*ne = append(*ne, e)
}

i++
seq := "\x1b]"
func (d *driver) parseOsc(i int, p []byte, _ bool) (int, input.Event) {
var seq string
if p[i] == ansi.OSC || p[i] == ansi.ESC {
seq += string(p[i])
i++
}
if i < len(p) && p[i-1] == ansi.ESC && p[i] == ']' {
seq += string(p[i])
i++
}

// Scan a OSC sequence
// An OSC sequence is terminated by a BEL, ESC, or ST character
for ; i < len(p) && p[i] != ansi.BEL && p[i] != ansi.ESC && p[i] != ansi.ST; i++ {
n++
seq += string(p[i])
}

if i >= len(p) {
return n, nil, fmt.Errorf("%w: invalid OSC sequence: %q", input.ErrUnknownEvent, seq[2:])
return len(seq), input.UnknownEvent{string(seq)}
}
n++
seq += string(p[i])

// Check 7-bit ST (string terminator) character
if len(p) > i+1 && p[i] == ansi.ESC && p[i+1] == '\\' {
seq += string(p[i+1])
n++
i++
seq += string(p[i])
}

return n, oscSequence(seq), nil
return len(seq), oscSequence(seq)
}

func utf8ByteLen(b byte) int {
Expand Down
6 changes: 5 additions & 1 deletion exp/term/input/ansi/table.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package ansi

import (
"log"
"strconv"

"github.com/charmbracelet/x/exp/term/ansi"
"github.com/charmbracelet/x/exp/term/input"
)
Expand Down Expand Up @@ -231,14 +234,15 @@ func (d *driver) registerKeys(flags int) {
input.Meta | input.Shift | input.Alt | input.Ctrl, // 15
} {
// XTerm modifier offset +1
xtermMod := string('0' + byte(m+1))
xtermMod := strconv.Itoa(int(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
log.Printf("seq: %q, key: %v\r\n", seq, key)
d.table[seq] = key
}
// CSI <number> ; <modifier> ~
Expand Down
Loading

0 comments on commit 42a0b15

Please sign in to comment.