Skip to content

Commit c1d4054

Browse files
author
SqlRush
committed
Parse explicit CSI navigation keys
1 parent 99aa2e3 commit c1d4054

5 files changed

Lines changed: 57 additions & 1 deletion

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ M7 补充:keybinding resolver/config 和脚本 named-key 输入已覆盖 `ctrl
212212

213213
M7 补充:terminal input parser 现在把 modified SS3 function-key 序列(如 `ESC O 1;2P``ESC O 1;5Q``ESC O 1;16S`)归一为现有 F1-F4 key surface,补齐 xterm/kitty 兼容模式下的 F-key 输入形态。
214214

215+
M7 补充:terminal input parser 现在把显式默认参数的 CSI navigation key 序列(如 `ESC [ 1 A``ESC [ 1 D``ESC [ 1 H``ESC [ 1 F`)归一为现有 arrow/Home/End key surface,同时继续让 `ESC [ 2 A` 这类 cursor-count 控制保持 unknown。
216+
215217
M7 补充:task runtime 现在会在状态行、任务面板排序/渲染、批量取消和 scripted task expectation 前把 `active`/`in_progress``success`/`done``error``canceled` 等 task state 别名归一为 canonical 状态。
216218

217219
M7 补充:permission runtime 现在会把 `Reject`/`deny`/`decline`/`disallow`/`no` 等 permission action 归一为 denied 结果,把 `Cancel`/`abort` 归一为 cancelled 结果,并让 scripted dialog-result status 断言接受 `rejected`/`approved` 等状态别名。

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ test/parity/ # golden tests against TS/official behavior
366366
- 本轮补充:scripted interaction assertion/check 数组元素的载荷现在也接受 `resource`/`node`/`attributes`/`properties`/`result`/`response`/`output`,让 JSON:API/resource-style 断言体可直接映射到 prompt/event/dialog/snapshot/screen/task/vim/viewport expectation。
367367
- 本轮补充:keybinding config、keymap 解析和 interaction script named-key 输入接受 `ctrl-h`/`ctrl-i`/`ctrl-j`/`ctrl-m`、`ctrl-[`、`ctrl-?`、对应 `control-*` 以及 compact/camel 形式;terminal parser 支持 CSI-u/kitty keyboard protocol 的 ctrl/alt/shift-enter/shift-tab 序列;image hint parser 支持 OSC ST terminator 和 base64 `name=` filename;keybinding JSON loader 支持 wrapper object-map、`shortcuts`、object action 字段、string-array key sequence/chord 和 `null`/`false` unbind;mouse parser 支持 legacy X10/normal tracking 序列;interaction script 支持结构化 mouse/mouse_event 步骤,button 接受 `buttonMask`/`button_mask`/`btn`/`code`/`mask`,坐标接受 `mouseX`/`mouse_x`/`clientX`/`screenX`/`pageX`/`offsetX`/`viewportX` 和对应 Y/row/line 别名,release 接受 `mouseUp`/`isRelease`/`mouseRelease`/`releaseEvent` 等字段别名;interaction script 还支持字符串 `keys` 和 `input`/`input_text`/`keys_text`/`raw_key`/`paste_text` 字段别名,并允许 status/snapshot/viewport/pasted-content contains 断言使用单字符串或字符串数组,`keybindings`、`expectEvents`、`expectDialogResults`、`expectPrompt.pastedContents` 和 `expectTasks.contains` 使用单对象或对象数组。
368368
- 本轮补充:terminal input parser 现在把 modified SS3 function-key 序列(如 `ESC O 1;2P``ESC O 1;5Q``ESC O 1;16S`)归一为现有 F1-F4 key surface,补齐 xterm/kitty 兼容模式下的 F-key 输入形态。
369+
- 本轮补充:terminal input parser 现在把显式默认参数的 CSI navigation key 序列(如 `ESC [ 1 A``ESC [ 1 D``ESC [ 1 H``ESC [ 1 F`)归一为现有 arrow/Home/End key surface,同时继续让 `ESC [ 2 A` 这类 cursor-count 控制保持 unknown。
369370
- 本轮补充:keybinding config 的 page/navigation/editing key name 现在接受 `pgup`/`pg-up`/`prior`/`pageUpKey``pgdn`/`pg-down`/`next`/`pageDownKey``homeKey`/`endKey``deleteForward`/`forwardDelete``deleteBackward`,覆盖常见终端、DOM 和配置别名。
370371
- 本轮补充:keybinding config 和脚本 named-key 输入现在接受 DOM/fixture 风格的 `enterKey`/`returnKey`/`numpadEnter``escapeKey`/`escKey``tabKey``shiftEnterKey`/`shiftNumpadEnter``shiftTabKey`/`backtabKey`,减少配置导入时对基础控制键名称的手工归一。
371372
- 本轮补充:keybinding config 和脚本 named-key 输入接受 DOM-style arrow key aliases,包括 `arrowLeft`/`arrowRight`/`arrowUp`/`arrowDown` 以及 ctrl/alt/meta/option arrow variants。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ M7 progress now includes:
510510
- `internal/tui`: Vim macro recording now treats uppercase macro registers such as `qA` as append-to-lowercase register operations, matching the existing named-register append behavior while keeping `@a`/`@@` playback on the canonical lowercase macro.
511511
- `internal/tui`: key parsing and configurable keybindings now expose `ctrl+space` from raw NUL and Kitty/CSI-u `0u`, `32;5u`, `64;5u`, and `50;5u` forms, with `ctrl-space`/`ctrl-@`/`ctrl-2` and compact `cSpace`/`c@`/`c2` aliases available for user keymaps and scripts.
512512
- `internal/tui`: key parsing and configurable keybindings now expose Ctrl-backslash, Ctrl-], Ctrl-^/Ctrl-6, and Ctrl-_/Ctrl-/ from raw terminal control bytes and Kitty/CSI-u forms, with symbol, named, and compact aliases available for user keymaps and scripts.
513-
- `internal/tui`: key parsing and configurable keybindings now expose F1-F12 from common SS3, modified SS3, legacy Linux-console, tilde CSI, and modified CSI function-key sequences, with `f1`, `function-key-1`, and `fn1` style aliases available for user keymaps and scripts.
513+
- `internal/tui`: key parsing and configurable keybindings now expose F1-F12 from common SS3, modified SS3, legacy Linux-console, tilde CSI, and modified CSI function-key sequences, with `f1`, `function-key-1`, and `fn1` style aliases available for user keymaps and scripts; navigation input also accepts explicit-default `CSI 1 A/B/C/D/H/F` forms while leaving counted cursor controls such as `CSI 2A` as unknown.
514514
- `internal/tui`: terminal ESC parsing now recognizes charset selection sequences such as `ESC ( B`, `ESC ) 0`, `ESC * B`, `ESC / A`, and `ESC % G` as structured charset actions and consumes them in visible-text parsing instead of surfacing them as unknown terminal output.
515515
- `internal/tui`: terminal ESC parsing now also recognizes ISO-2022 charset shift controls such as `ESC N`, `ESC n`, `ESC o`, `ESC |`, `ESC }`, and `ESC ~` as structured charset-shift actions and consumes them in visible-text parsing instead of surfacing them as unknown terminal output.
516516
- `internal/contracts`/`internal/session`: image content blocks now normalize source aliases such as `mediaType`/`mimeType`/`contentType` and `base64`/`payload`, including top-level image block fields, into canonical `ImageSource` values during transcript resume.

internal/tui/input.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ func ParseKey(seq string) Key {
9797
if key, ok := parseCSIuKey(seq); ok {
9898
return key
9999
}
100+
if key, ok := parseNumberedNavigationKey(seq); ok {
101+
return key
102+
}
100103
if key, ok := parseModifiedNavigationKey(seq); ok {
101104
return key
102105
}
@@ -219,6 +222,36 @@ func ParseKey(seq string) Key {
219222
}
220223
}
221224

225+
func parseNumberedNavigationKey(seq string) (Key, bool) {
226+
if !strings.HasPrefix(seq, "\x1b[") || len(seq) < 4 {
227+
return Key{}, false
228+
}
229+
body := strings.TrimPrefix(seq, "\x1b[")
230+
if strings.ContainsAny(body, ";:~") || len(body) < 2 {
231+
return Key{}, false
232+
}
233+
code, ok := firstCSIParamNumber(body[:len(body)-1])
234+
if !ok || code != 1 {
235+
return Key{}, false
236+
}
237+
switch body[len(body)-1] {
238+
case 'A':
239+
return Key{Type: KeyUp}, true
240+
case 'B':
241+
return Key{Type: KeyDown}, true
242+
case 'C':
243+
return Key{Type: KeyRight}, true
244+
case 'D':
245+
return Key{Type: KeyLeft}, true
246+
case 'H':
247+
return Key{Type: KeyHome}, true
248+
case 'F':
249+
return Key{Type: KeyEnd}, true
250+
default:
251+
return Key{}, false
252+
}
253+
}
254+
222255
func parseModifiedNavigationKey(seq string) (Key, bool) {
223256
switch {
224257
case isModifiedNavigationCSI(seq, "\x1b[1;", "A"):

internal/tui/tui_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,26 @@ func TestParseAlternateTerminalNavigationSequences(t *testing.T) {
968968
}
969969

970970
func TestParseModifiedNavigationKeySequences(t *testing.T) {
971+
numberedCases := []struct {
972+
seq string
973+
want KeyType
974+
}{
975+
{seq: "\x1b[1A", want: KeyUp},
976+
{seq: "\x1b[1B", want: KeyDown},
977+
{seq: "\x1b[1C", want: KeyRight},
978+
{seq: "\x1b[1D", want: KeyLeft},
979+
{seq: "\x1b[1H", want: KeyHome},
980+
{seq: "\x1b[1F", want: KeyEnd},
981+
}
982+
for _, tc := range numberedCases {
983+
if key := ParseKey(tc.seq); key.Type != tc.want {
984+
t.Fatalf("ParseKey(%q) = %#v, want %q", tc.seq, key, tc.want)
985+
}
986+
}
987+
if key := ParseKey("\x1b[2A"); key.Type != KeyUnknown {
988+
t.Fatalf("CSI 2A should not parse as navigation key: %#v", key)
989+
}
990+
971991
for _, modifier := range []string{"2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16"} {
972992
cases := []struct {
973993
seq string

0 commit comments

Comments
 (0)