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:
@@ -135,51 +135,42 @@ func fuzzyMatch(hay, needle string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *paletteState) handleKey(b byte, peek []byte) (paletteAction, bool) {
|
||||
// kitty functional keycodes for arrows.
|
||||
const (
|
||||
kittyKeyUp = 57352
|
||||
kittyKeyDown = 57353
|
||||
)
|
||||
|
||||
// handleInput consumes one keystroke from chunk[i:] and updates palette
|
||||
// state. advance is how many bytes the keystroke occupies (1 for legacy
|
||||
// keys, longer for CSI sequences). Returning done=true tells the caller
|
||||
// the palette is finished and action describes what to do next.
|
||||
//
|
||||
// Recognised input includes both legacy byte forms and the kitty
|
||||
// keyboard CSI u encoding that codex/ratatui pushes onto the terminal.
|
||||
// Unknown CSI sequences (including release events from kitty flag 2)
|
||||
// are consumed silently so they don't fall through to the ESC branch
|
||||
// and accidentally cancel the palette.
|
||||
func (p *paletteState) handleInput(chunk []byte, i int) (action paletteAction, done bool, advance int) {
|
||||
b := chunk[i]
|
||||
if b == 0x1b {
|
||||
// Pure Esc cancels; Esc [ A/B is up/down arrow.
|
||||
if len(peek) >= 2 && peek[0] == '[' {
|
||||
switch peek[1] {
|
||||
case 'A':
|
||||
p.cursor--
|
||||
if p.cursor < 0 {
|
||||
p.cursor = 0
|
||||
}
|
||||
return paletteAction{}, false
|
||||
case 'B':
|
||||
p.cursor++
|
||||
if p.cursor >= len(p.items) {
|
||||
p.cursor = len(p.items) - 1
|
||||
}
|
||||
return paletteAction{}, false
|
||||
}
|
||||
if n := csiLen(chunk, i); n > 0 {
|
||||
return p.handleCSI(chunk[i+2:i+n-1], chunk[i+n-1], n)
|
||||
}
|
||||
return paletteAction{kind: "cancel"}, true
|
||||
// Bare ESC (no CSI follow-up): cancel.
|
||||
return paletteAction{kind: "cancel"}, true, 1
|
||||
}
|
||||
switch b {
|
||||
case '\r', '\n':
|
||||
if p.cursor >= 0 && p.cursor < len(p.items) {
|
||||
return p.items[p.cursor].action, true
|
||||
}
|
||||
return paletteAction{kind: "cancel"}, true
|
||||
return p.accept(), true, 1
|
||||
case 0x7f, 0x08:
|
||||
if len(p.query) > 0 {
|
||||
p.query = p.query[:len(p.query)-1]
|
||||
p.rebuild()
|
||||
}
|
||||
p.backspace()
|
||||
case 0x15: // Ctrl-U
|
||||
p.query = p.query[:0]
|
||||
p.rebuild()
|
||||
p.clearQuery()
|
||||
case 0x0e: // Ctrl-N
|
||||
p.cursor++
|
||||
if p.cursor >= len(p.items) {
|
||||
p.cursor = len(p.items) - 1
|
||||
}
|
||||
case 0x10: // Ctrl-P inside palette: cursor up.
|
||||
p.cursor--
|
||||
if p.cursor < 0 {
|
||||
p.cursor = 0
|
||||
}
|
||||
p.cursorDown()
|
||||
case 0x10: // Ctrl-P
|
||||
p.cursorUp()
|
||||
case 0x0b: // Ctrl-K inside palette is a no-op (would re-open); ignore.
|
||||
case 0x16: // Ctrl-V literal-paste — ignore in palette.
|
||||
default:
|
||||
@@ -188,7 +179,91 @@ func (p *paletteState) handleKey(b byte, peek []byte) (paletteAction, bool) {
|
||||
p.rebuild()
|
||||
}
|
||||
}
|
||||
return paletteAction{}, false
|
||||
return paletteAction{}, false, 1
|
||||
}
|
||||
|
||||
func (p *paletteState) handleCSI(params []byte, final byte, n int) (paletteAction, bool, int) {
|
||||
switch final {
|
||||
case 'A':
|
||||
p.cursorUp()
|
||||
return paletteAction{}, false, n
|
||||
case 'B':
|
||||
p.cursorDown()
|
||||
return paletteAction{}, false, n
|
||||
case 'u':
|
||||
k, ok := decodeCSIu(string(params))
|
||||
if !ok || k.event != 1 {
|
||||
// Repeat / release events, or malformed: ignore.
|
||||
return paletteAction{}, false, n
|
||||
}
|
||||
switch k.key {
|
||||
case 13: // Enter
|
||||
return p.accept(), true, n
|
||||
case 27: // Escape
|
||||
return paletteAction{kind: "cancel"}, true, n
|
||||
case 127, 8: // Backspace
|
||||
p.backspace()
|
||||
case kittyKeyUp:
|
||||
p.cursorUp()
|
||||
case kittyKeyDown:
|
||||
p.cursorDown()
|
||||
default:
|
||||
// Ctrl-modified character keys.
|
||||
if k.mods == 5 {
|
||||
switch k.key {
|
||||
case 'u':
|
||||
p.clearQuery()
|
||||
case 'n':
|
||||
p.cursorDown()
|
||||
case 'p':
|
||||
p.cursorUp()
|
||||
}
|
||||
return paletteAction{}, false, n
|
||||
}
|
||||
// Unmodified printable ASCII typed via CSI u (flag 8): treat
|
||||
// as a query keystroke.
|
||||
if k.mods == 1 && k.key >= 0x20 && k.key < 0x7f {
|
||||
p.query = append(p.query, rune(k.key))
|
||||
p.rebuild()
|
||||
}
|
||||
}
|
||||
return paletteAction{}, false, n
|
||||
}
|
||||
// Anything else (~, function keys, etc.): consume silently.
|
||||
return paletteAction{}, false, n
|
||||
}
|
||||
|
||||
func (p *paletteState) accept() paletteAction {
|
||||
if p.cursor >= 0 && p.cursor < len(p.items) {
|
||||
return p.items[p.cursor].action
|
||||
}
|
||||
return paletteAction{kind: "cancel"}
|
||||
}
|
||||
|
||||
func (p *paletteState) backspace() {
|
||||
if len(p.query) > 0 {
|
||||
p.query = p.query[:len(p.query)-1]
|
||||
p.rebuild()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *paletteState) clearQuery() {
|
||||
p.query = p.query[:0]
|
||||
p.rebuild()
|
||||
}
|
||||
|
||||
func (p *paletteState) cursorUp() {
|
||||
p.cursor--
|
||||
if p.cursor < 0 {
|
||||
p.cursor = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (p *paletteState) cursorDown() {
|
||||
p.cursor++
|
||||
if p.cursor >= len(p.items) {
|
||||
p.cursor = len(p.items) - 1
|
||||
}
|
||||
}
|
||||
|
||||
// render draws the palette onto out. Geometry: title bar + filter line +
|
||||
|
||||
Reference in New Issue
Block a user