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.
This commit is contained in:
@@ -216,9 +216,6 @@ type uiState struct {
|
||||
// A fresh renderer is allocated per focused child so partial-escape
|
||||
// state cannot bleed between panes.
|
||||
renderer *viewportRenderer
|
||||
// passthrough: when true, the next keystroke is forwarded to the
|
||||
// focused PTY untouched (SPEC §4 Ctrl-K Ctrl-K).
|
||||
passthroughArmed bool
|
||||
|
||||
// attention is the latest request_human_attention surfaced via MCP;
|
||||
// rendered in the status line until cleared.
|
||||
@@ -614,31 +611,13 @@ func (st *uiState) processStdin(chunk []byte) {
|
||||
for i < len(chunk) {
|
||||
b := chunk[i]
|
||||
|
||||
// Passthrough armed: forward this byte literally regardless of
|
||||
// what it is, then disarm.
|
||||
if st.passthroughArmed {
|
||||
forward = append(forward, b)
|
||||
st.passthroughArmed = false
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
// Palette mode swallows all bytes.
|
||||
if st.palette != nil {
|
||||
var peek []byte
|
||||
if i+1 < len(chunk) {
|
||||
peek = chunk[i+1:]
|
||||
}
|
||||
action, done := st.palette.handleKey(b, peek)
|
||||
if b == 0x1b && len(peek) >= 2 && peek[0] == '[' {
|
||||
if peek[1] == 'A' || peek[1] == 'B' {
|
||||
i += 3
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
i++
|
||||
action, done, adv := st.palette.handleInput(chunk, i)
|
||||
if adv <= 0 {
|
||||
adv = 1
|
||||
}
|
||||
i += adv
|
||||
if done {
|
||||
a := action
|
||||
pendingAction = &a
|
||||
@@ -650,42 +629,23 @@ func (st *uiState) processStdin(chunk []byte) {
|
||||
|
||||
// Ctrl-K is the reserved app-level binding. Two cases:
|
||||
// - Ctrl-K then anything except Ctrl-K → open palette.
|
||||
// - Ctrl-K Ctrl-K → arm passthrough; the next byte goes raw.
|
||||
if b == keyCtrlK {
|
||||
// Peek at the next byte if we have it.
|
||||
next := byte(0)
|
||||
haveNext := i+1 < len(chunk)
|
||||
if haveNext {
|
||||
next = chunk[i+1]
|
||||
}
|
||||
if haveNext && next == keyCtrlK {
|
||||
// Chord: forward both Ctrl-K bytes literally. (Some
|
||||
// nested TUIs expect Ctrl-K itself.)
|
||||
// - Ctrl-K Ctrl-K → forward both keystrokes to the child raw.
|
||||
//
|
||||
// Ctrl-K is recognised in legacy (0x0B), kitty CSI u, and xterm
|
||||
// modifyOtherKeys encodings — see matchCtrlK. The chord forwards
|
||||
// the bytes the terminal actually emitted, so a child that asked
|
||||
// for kitty input gets kitty input.
|
||||
if hit, adv := matchCtrlK(chunk, i); hit {
|
||||
if hit2, adv2 := matchCtrlK(chunk, i+adv); hit2 {
|
||||
flushForward()
|
||||
forward = append(forward, keyCtrlK, keyCtrlK)
|
||||
forward = append(forward, chunk[i:i+adv+adv2]...)
|
||||
flushForward()
|
||||
i += 2
|
||||
i += adv + adv2
|
||||
continue
|
||||
}
|
||||
if !haveNext {
|
||||
// Could be the first byte of a chord — arm and wait.
|
||||
st.passthroughArmed = true
|
||||
// But we also want palette-open on a lone Ctrl-K. Resolve
|
||||
// by treating "Ctrl-K at end of read" as palette open;
|
||||
// any subsequent Ctrl-K in the next read still has the
|
||||
// chord semantics because passthroughArmed got set first.
|
||||
// To match the spec's reading, simpler model: lone Ctrl-K
|
||||
// in this read opens the palette.
|
||||
st.passthroughArmed = false
|
||||
flushForward()
|
||||
st.openPaletteLocked()
|
||||
i++
|
||||
continue
|
||||
}
|
||||
// Ctrl-K followed by something that's not Ctrl-K → palette open.
|
||||
flushForward()
|
||||
st.openPaletteLocked()
|
||||
i++
|
||||
i += adv
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user