Files
patterm/internal/app/keymatch_test.go
Harry Bayliss cb3e51d568 Handle kitty keyboard protocol input for Ctrl-K and palette
Codex (and other ratatui-based children) pushes kitty keyboard flags
onto the host terminal, so Ctrl-K arrives as `\x1b[107;5u` instead of
0x0B and the palette open never fired. With "report event types" also
on, the release event `\x1b[107;5:3u` followed the press and tripped
the palette's "unknown ESC sequence → cancel" branch, making the
palette flash and close.

Add a small CSI scanner / kitty CSI u decoder and use them in two
places: matchCtrlK now accepts the legacy byte, the kitty CSI u form,
and xterm modifyOtherKeys; the palette's input handler consumes whole
CSI sequences, ignores non-press events, and decodes Enter/Esc/
Backspace/arrows/Ctrl-U-N-P in their kitty forms. Ctrl-K Ctrl-K
forwards the raw matched bytes so nested TUIs that asked for kitty
input still receive kitty input.
2026-05-14 14:46:21 +01:00

57 lines
1.9 KiB
Go

package app
import "testing"
func TestMatchCtrlK(t *testing.T) {
cases := []struct {
name string
chunk string
offset int
wantMatch bool
wantAdvance int
}{
{"legacy lone byte", "\x0b", 0, true, 1},
{"legacy followed by text", "\x0bx", 0, true, 1},
{"kitty plain Ctrl-K", "\x1b[107;5u", 0, true, 8},
{"kitty with press event", "\x1b[107;5:1u", 0, true, 10},
{"kitty with key release", "\x1b[107;5:3u", 0, false, 0},
{"kitty with extra shift", "\x1b[107;6u", 0, false, 0},
{"kitty no modifier", "\x1b[107u", 0, false, 0},
{"kitty wrong key", "\x1b[108;5u", 0, false, 0},
{"kitty with associated text trailing group", "\x1b[107;5;107u", 0, true, 12},
{"modifyOtherKeys Ctrl-K", "\x1b[27;5;107~", 0, true, 11},
{"modifyOtherKeys wrong mods", "\x1b[27;6;107~", 0, false, 0},
{"unrelated CSI", "\x1b[A", 0, false, 0},
{"plain ascii", "k", 0, false, 0},
{"empty", "", 0, false, 0},
{"incomplete CSI", "\x1b[107;5", 0, false, 0},
{"offset past legacy", "x\x0b", 1, true, 1},
{"offset past kitty prefix", "x\x1b[107;5u", 1, true, 8},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, adv := matchCtrlK([]byte(tc.chunk), tc.offset)
if got != tc.wantMatch || adv != tc.wantAdvance {
t.Fatalf("matchCtrlK(%q, %d) = (%v, %d); want (%v, %d)",
tc.chunk, tc.offset, got, adv, tc.wantMatch, tc.wantAdvance)
}
})
}
}
func TestMatchCtrlKConsecutive(t *testing.T) {
// Two kitty Ctrl-K sequences back to back, the chord case.
chunk := []byte("\x1b[107;5u\x1b[107;5u")
hit, adv := matchCtrlK(chunk, 0)
if !hit || adv != 8 {
t.Fatalf("first: hit=%v adv=%d", hit, adv)
}
hit2, adv2 := matchCtrlK(chunk, adv)
if !hit2 || adv2 != 8 {
t.Fatalf("second: hit=%v adv=%d", hit2, adv2)
}
if adv+adv2 != len(chunk) {
t.Fatalf("expected to cover the whole chunk, got %d/%d", adv+adv2, len(chunk))
}
}