Skip to content

Commit 562b7a5

Browse files
author
SqlRush
committed
Parse modified SS3 navigation keys
1 parent 7259aa6 commit 562b7a5

5 files changed

Lines changed: 80 additions & 0 deletions

File tree

docs/cc-100-roadmap.md

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

219219
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 输入形态。
220220

221+
M7 补充:terminal input parser 现在也接受 modified SS3 application-cursor navigation 序列(如 `ESC O 1;2A``ESC O 1;5D``ESC O 1;16C`),和 CSI modified navigation 一样把 shift 降级为方向键、alt/meta 映射到 word-motion key、ctrl 组合映射到 ctrl word-motion key。
222+
221223
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。
222224

223225
M7 补充:task runtime 现在会在状态行、任务面板排序/渲染、批量取消和 scripted task expectation 前把 `active`/`in_progress``success`/`done``error``canceled` 等 task state 别名归一为 canonical 状态。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ test/parity/ # golden tests against TS/official behavior
367367
- 本轮补充:scripted interaction assertion/check 数组元素的载荷现在也接受 `resource`/`node`/`attributes`/`properties`/`result`/`response`/`output`,让 JSON:API/resource-style 断言体可直接映射到 prompt/event/dialog/snapshot/screen/task/vim/viewport expectation。
368368
- 本轮补充: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` 使用单对象或对象数组。
369369
- 本轮补充: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 输入形态。
370+
- 本轮补充:terminal input parser 现在也接受 modified SS3 application-cursor navigation 序列(如 `ESC O 1;2A``ESC O 1;5D``ESC O 1;16C`),和 CSI modified navigation 一样把 shift 降级为方向键、alt/meta 映射到 word-motion key、ctrl 组合映射到 ctrl word-motion key。
370371
- 本轮补充: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。
371372
- 本轮补充:keybinding config 的 page/navigation/editing key name 现在接受 `pgup`/`pg-up`/`prior`/`pageUpKey``pgdn`/`pg-down`/`next`/`pageDownKey``homeKey`/`endKey``deleteForward`/`forwardDelete``deleteBackward`,覆盖常见终端、DOM 和配置别名。
372373
- 本轮补充:keybinding config 和脚本 named-key 输入现在接受 DOM/fixture 风格的 `enterKey`/`returnKey`/`numpadEnter``escapeKey`/`escKey``tabKey``shiftEnterKey`/`shiftNumpadEnter``shiftTabKey`/`backtabKey`,减少配置导入时对基础控制键名称的手工归一。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,7 @@ M7 progress now includes:
514514
- `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.
515515
- `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.
516516
- `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.
517+
- `internal/tui`: terminal key parsing now accepts modified SS3 application-cursor navigation sequences such as `ESC O 1;2A`, `ESC O 1;5D`, and `ESC O 1;16C`, mapping shift to the plain navigation surface and alt/meta/ctrl combinations to the existing word-motion key surfaces.
517518
- `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.
518519
- `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.
519520
- `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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ func ParseKey(seq string) Key {
103103
if key, ok := parseModifiedNavigationKey(seq); ok {
104104
return key
105105
}
106+
if key, ok := parseModifiedSS3NavigationKey(seq); ok {
107+
return key
108+
}
106109
if key, ok := parseFunctionKey(seq); ok {
107110
return key
108111
}
@@ -279,6 +282,27 @@ func parseModifiedNavigationKey(seq string) (Key, bool) {
279282
}
280283
}
281284

285+
func parseModifiedSS3NavigationKey(seq string) (Key, bool) {
286+
switch {
287+
case isModifiedNavigationSS3(seq, "A"):
288+
return Key{Type: KeyUp}, true
289+
case isModifiedNavigationSS3(seq, "B"):
290+
return Key{Type: KeyDown}, true
291+
case isModifiedNavigationSS3(seq, "C"):
292+
modifier, _ := modifiedSS3NavigationModifier(seq, "C")
293+
return Key{Type: modifiedHorizontalArrowKey(modifier, KeyRight, KeyAltRight, KeyCtrlRight)}, true
294+
case isModifiedNavigationSS3(seq, "D"):
295+
modifier, _ := modifiedSS3NavigationModifier(seq, "D")
296+
return Key{Type: modifiedHorizontalArrowKey(modifier, KeyLeft, KeyAltLeft, KeyCtrlLeft)}, true
297+
case isModifiedNavigationSS3(seq, "H"):
298+
return Key{Type: KeyHome}, true
299+
case isModifiedNavigationSS3(seq, "F"):
300+
return Key{Type: KeyEnd}, true
301+
default:
302+
return Key{}, false
303+
}
304+
}
305+
282306
func parseFunctionKey(seq string) (Key, bool) {
283307
switch seq {
284308
case "\x1bOP", "\x1b[[A":
@@ -396,6 +420,11 @@ func isModifiedNavigationCSI(seq, prefix, suffix string) bool {
396420
return ok
397421
}
398422

423+
func isModifiedNavigationSS3(seq, suffix string) bool {
424+
_, ok := modifiedSS3NavigationModifier(seq, suffix)
425+
return ok
426+
}
427+
399428
func modifiedNavigationModifier(seq, prefix, suffix string) (int, bool) {
400429
if !strings.HasPrefix(seq, prefix) || !strings.HasSuffix(seq, suffix) {
401430
return 0, false
@@ -408,6 +437,26 @@ func modifiedNavigationModifier(seq, prefix, suffix string) (int, bool) {
408437
return modifier, modifier >= 2 && modifier <= 16
409438
}
410439

440+
func modifiedSS3NavigationModifier(seq, suffix string) (int, bool) {
441+
if !strings.HasPrefix(seq, "\x1bO") || !strings.HasSuffix(seq, suffix) {
442+
return 0, false
443+
}
444+
params := strings.TrimSuffix(strings.TrimPrefix(seq, "\x1bO"), suffix)
445+
base, modifierText, ok := strings.Cut(params, ";")
446+
if !ok || modifierText == "" {
447+
return 0, false
448+
}
449+
code, ok := firstCSIParamNumber(base)
450+
if !ok || code != 1 || strings.ContainsAny(modifierText, ";:") {
451+
return 0, false
452+
}
453+
modifier, err := strconv.Atoi(modifierText)
454+
if err != nil {
455+
return 0, false
456+
}
457+
return modifier, modifier >= 2 && modifier <= 16
458+
}
459+
411460
func modifiedHorizontalArrowKey(modifier int, plain KeyType, alt KeyType, ctrl KeyType) KeyType {
412461
if modifier >= 5 && modifier <= 8 || modifier >= 13 && modifier <= 16 {
413462
return ctrl

internal/tui/tui_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,33 @@ func TestParseModifiedNavigationKeySequences(t *testing.T) {
10291029
t.Fatalf("ParseKey(%q) = %#v, want %q", tc.seq, key, tc.want)
10301030
}
10311031
}
1032+
1033+
ss3Cases := []struct {
1034+
seq string
1035+
want KeyType
1036+
}{
1037+
{seq: "\x1bO1;2A", want: KeyUp},
1038+
{seq: "\x1bO1;2B", want: KeyDown},
1039+
{seq: "\x1bO1;2C", want: KeyRight},
1040+
{seq: "\x1bO1;2D", want: KeyLeft},
1041+
{seq: "\x1bO1;3C", want: KeyAltRight},
1042+
{seq: "\x1bO1;4D", want: KeyAltLeft},
1043+
{seq: "\x1bO1;5C", want: KeyCtrlRight},
1044+
{seq: "\x1bO1;8D", want: KeyCtrlLeft},
1045+
{seq: "\x1bO1;10C", want: KeyAltRight},
1046+
{seq: "\x1bO1;13D", want: KeyCtrlLeft},
1047+
{seq: "\x1bO1;16C", want: KeyCtrlRight},
1048+
{seq: "\x1bO1;2H", want: KeyHome},
1049+
{seq: "\x1bO1;5F", want: KeyEnd},
1050+
}
1051+
for _, tc := range ss3Cases {
1052+
if key := ParseKey(tc.seq); key.Type != tc.want {
1053+
t.Fatalf("ParseKey(%q) = %#v, want %q", tc.seq, key, tc.want)
1054+
}
1055+
}
1056+
if key := ParseKey("\x1bO2;3A"); key.Type != KeyUnknown {
1057+
t.Fatalf("SS3 2;3A should not parse as navigation key: %#v", key)
1058+
}
10321059
}
10331060

10341061
func TestParseFunctionKeySequences(t *testing.T) {

0 commit comments

Comments
 (0)