diff --git a/input.go b/input.go index 58c8a7e0..0ebe10c2 100644 --- a/input.go +++ b/input.go @@ -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) @@ -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 @@ -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 } @@ -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 @@ -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) @@ -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:] } diff --git a/input_test.go b/input_test.go index 64efacab..29594629 100644 --- a/input_test.go +++ b/input_test.go @@ -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 { diff --git a/key.go b/key.go index bb4c790e..054d9b4a 100644 --- a/key.go +++ b/key.go @@ -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 } @@ -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