Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions input.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,18 @@ const defaultControlStringLimit = 64 * 1024
func newInputParser(eq chan<- Event) *inputParser {
return &inputParser{
evch: eq,
buf: make([]rune, 0, 128),
buf: make([]bufRune, 0, 128),
controlStringMax: defaultControlStringLimit,
}
}

type inputParser struct {
buf []rune // bytes to process (ingest data)
buf []bufRune // bytes to process (ingest data)
utfBuf []byte // accrued UTF8 bytes
strBuf []byte // accrued string data (for ST, OSC, etc.)
csiParams []byte // accrued parameter bytes for CSI (and SS3)
csiInterm []byte // accrued intermediate bytes for CSI
curEsc []byte // source bytes consumed for the sequence currently being parsed
escChar byte // last byte for escape
escaped bool // true if next key should be modified by ESC
btnsDown ButtonMask // mouse buttons down (excludes wheel buttons)
Expand All @@ -95,6 +96,16 @@ type inputParser struct {
discardString bool // drop the rest of an over-limit OSC/XDA sequence
}

// bufRune pairs a decoded rune with the raw source bytes that produced
// it, so the state machine can accumulate a byte-faithful copy of the
// sequence in ip.curEsc for EventKey.EscSeq(). The inline byte array
// holds up to utf8.UTFMax bytes; rawN is how many are valid.
type bufRune struct {
r rune
raw [utf8.UTFMax]byte
rawN int
}

func keyFromInt(n int) (Key, bool) {
if n < 0 || n > 32767 {
return 0, false
Expand Down Expand Up @@ -172,6 +183,13 @@ func (ip *inputParser) post(ev Event) {
}
}

if ke, ok := ev.(*EventKey); ok && len(ip.curEsc) > 0 {
// curEsc is raw source bytes, so this conversion preserves
// the wire form (e.g. a raw C1 0x9b byte stays one byte,
// rather than being re-encoded as its 2-byte UTF-8 form).
ke.esc = string(ip.curEsc)
}

ip.evch <- ev
}

Expand Down Expand Up @@ -518,10 +536,15 @@ var linuxFKeys = map[rune]Key{
}

func (ip *inputParser) scan() {
for _, r := range ip.buf {
for _, br := range ip.buf {
r := br.r
ip.buf = ip.buf[1:]
ip.escChar = 0
ip.keyTime = time.Now()
if ip.state == istInit {
ip.curEsc = ip.curEsc[:0]
}
ip.curEsc = append(ip.curEsc, br.raw[:br.rawN]...)
if r >= 0xA0 {
// 8-bit extended Unicode we just treat as such - this will swallow anything else queued up
ip.state = istInit
Expand Down Expand Up @@ -1438,7 +1461,9 @@ func (ip *inputParser) ScanUTF8(b []byte) {
for len(ip.utfBuf) > 0 {
// fast path, basic ascii, also includes ISO2022 8-bit controls
if ip.utfBuf[0] < 0xA0 {
ip.buf = append(ip.buf, rune(ip.utfBuf[0]))
br := bufRune{r: rune(ip.utfBuf[0]), rawN: 1}
br.raw[0] = ip.utfBuf[0]
ip.buf = append(ip.buf, br)
ip.utfBuf = ip.utfBuf[1:]
} else {
r, utfLen := utf8.DecodeRune(ip.utfBuf)
Expand All @@ -1447,7 +1472,9 @@ func (ip *inputParser) ScanUTF8(b []byte) {
// hopefully it will recover.
utfLen = 1
} else {
ip.buf = append(ip.buf, r)
br := bufRune{r: r, rawN: utfLen}
copy(br.raw[:], ip.utfBuf[:utfLen])
ip.buf = append(ip.buf, br)
}
ip.utfBuf = ip.utfBuf[utfLen:]
}
Expand Down
87 changes: 87 additions & 0 deletions input_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1362,6 +1362,93 @@ func TestEscDuringSs3ResetsParser(t *testing.T) {
}
}

// TestInputEscSeqRoundTrip verifies that EventKey.EscSeq() returns the
// raw source bytes that produced the key event, preserving byte-level
// fidelity so callers can forward the original wire form to a child
// pty. In particular a raw single-byte C1 CSI (\x9b) must come back
// as one byte, not the 2-byte UTF-8 form (\xc2\x9b), and a UTF-8
// encoded C1 must come back as the original two bytes.
func TestInputEscSeqRoundTrip(t *testing.T) {
tests := []struct {
name string
input []byte
key Key
mod ModMask
str string
escSeq string
}{
{"CSI-Up", []byte{'\x1b', '[', 'A'}, KeyUp, ModNone, "", "\x1b[A"},
{"CSI-Ctrl-Up", []byte{'\x1b', '[', '1', ';', '5', 'A'}, KeyUp, ModCtrl, "", "\x1b[1;5A"},
{"SS3-F1", []byte{'\x1b', 'O', 'P'}, KeyF1, ModNone, "", "\x1bOP"},
{"Kitty-Ctrl-I", []byte{'\x1b', '[', '1', '0', '5', ';', '5', 'u'}, 'I', ModCtrl, "", "\x1b[105;5u"},
{"C1-CSI-Up-Raw", []byte{'\x9b', 'A'}, KeyUp, ModNone, "", "\x9bA"},
{"C1-CSI-Up-UTF8", []byte{'\xc2', '\x9b', 'A'}, KeyUp, ModNone, "", "\xc2\x9bA"},
{"Printable-A", []byte{'A'}, KeyRune, ModNone, "A", "A"},
{"UTF8-Euro", []byte("€"), KeyRune, ModNone, "€", "€"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
evch := make(chan Event, 10)
ip := newInputParser(evch)

ip.ScanUTF8(tt.input)

got := firstKey(evch)
if got == nil {
t.Fatal("expected a key event, got none")
}
if got.Key() != tt.key {
t.Errorf("key: expected %v, got %v", tt.key, got.Key())
}
if got.Modifiers() != tt.mod {
t.Errorf("modifiers: expected %v, got %v", tt.mod, got.Modifiers())
}
if got.Str() != tt.str {
t.Errorf("str: expected %q, got %q", tt.str, got.Str())
}
if got.EscSeq() != tt.escSeq {
t.Errorf("EscSeq: expected % x, got % x", tt.escSeq, got.EscSeq())
}
})
}
}

// TestInputEscSeqResetsBetweenSequences verifies that curEsc is reset
// when the parser returns to its initial state, so consecutive key
// events don't leak bytes from earlier sequences into later EscSeq()
// values.
func TestInputEscSeqResetsBetweenSequences(t *testing.T) {
evch := make(chan Event, 10)
ip := newInputParser(evch)

// First sequence: a CSI Up. Then a plain 'A'. EscSeq should be
// "\x1b[A" for the first event and "A" for the second, not
// "\x1b[A" + "A" for the second.
ip.ScanUTF8([]byte{'\x1b', '[', 'A', 'A'})

var events []*EventKey
for range 2 {
select {
case ev := <-evch:
if kev, ok := ev.(*EventKey); ok {
events = append(events, kev)
}
case <-time.After(100 * time.Millisecond):
t.Fatal("timeout waiting for event")
}
}
if len(events) != 2 {
t.Fatalf("expected 2 key events, got %d", len(events))
}
if events[0].EscSeq() != "\x1b[A" {
t.Errorf("first event EscSeq: expected %q, got %q", "\x1b[A", events[0].EscSeq())
}
if events[1].EscSeq() != "A" {
t.Errorf("second event EscSeq: expected %q, got %q", "A", events[1].EscSeq())
}
}

// firstMouse drains the next EventMouse off the channel.
func firstMouse(ch <-chan Event) *EventMouse {
for {
Expand Down
12 changes: 12 additions & 0 deletions key.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type EventKey struct {
key Key
physical Key
str string // string for key, usually just one character, but may be composed sequence
esc string // raw escape-sequence bytes that produced this event, if any
pressed bool
repeat int
}
Expand All @@ -60,6 +61,17 @@ func (ev *EventKey) Str() string {
return ev.str
}

// EscSeq returns the raw escape-sequence bytes that produced this key
// event, if any. The input parser populates this for events derived
// from a CSI / SS3 / OSC sequence so that callers (notably terminal
// passthrough panes) can forward the original bytes to a child
// process rather than reconstructing them from key + modifier state.
// For ordinary printable runes the result may be the rune itself
// encoded as UTF-8; for synthetic events it will be empty.
func (ev *EventKey) EscSeq() string {
return ev.esc
}

// Key returns a virtual key code. We use this to identify specific key
// codes, such as KeyEnter, etc. Most control and function keys are reported
// with unique Key values. Normal alphanumeric and punctuation keys will
Expand Down