Skip to content

Commit cfca6ce

Browse files
author
SqlRush
committed
Recognize extended ESC charset selectors
1 parent 67e4c32 commit cfca6ce

7 files changed

Lines changed: 47 additions & 12 deletions

docs/cc-100-roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ M7 补充:terminal CSI parser 现在把带参数的 `CSI Pl;Pr s` 解析成 le
316316

317317
M7 补充:terminal CSI parser 现在识别带 intermediate space 的 `CSI Ps SP @` / `CSI Ps SP A` scroll-left/right 序列,避免误解析成 insert-characters 或 cursor-up。
318318

319-
M7 补充:terminal ESC parser 现在把 charset selection (`ESC ( B` / `ESC ) 0`) 解析成结构化 charset action,并在 terminal parser 可见文本管线中消费,避免常见终端 charset 选择序列残留为 unknown。
319+
M7 补充:terminal ESC parser 现在把 charset selection (`ESC ( B` / `ESC ) 0` / `ESC * B` / `ESC / A`) 解析成结构化 charset action,并在 terminal parser 可见文本管线中消费,避免常见终端 charset 选择序列残留为 unknown。
320320

321321
M7 补充:terminal CSI parser 现在把 DEC selective erase `CSI ? Ps J` / `CSI ? Ps K` 标记为 selective display/line erase,和普通 ED/EL 区分开。
322322

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ test/parity/ # golden tests against TS/official behavior
453453
- 本轮补充:terminal CSI parser 把 DEC `?69h/l` left/right margin mode 解析成结构化 `leftRightMarginMode` mode action,补齐 scroll-region 相邻的 margin 状态序列。
454454
- 本轮补充:terminal CSI parser 现在把带参数的 `CSI Pl;Pr s` 解析成 left/right horizontal margin region action,同时保留无参数 `CSI s` save-cursor 语义,和 DEC `?69h/l` margin mode 闭环。
455455
- 本轮补充:terminal CSI parser 现在识别带 intermediate space 的 `CSI Ps SP @` / `CSI Ps SP A` scroll-left/right 序列,避免误解析成 insert-characters 或 cursor-up。
456-
- 本轮补充:terminal ESC parser 现在把 charset selection (`ESC ( B` / `ESC ) 0`) 解析成结构化 charset action,并在 terminal parser 可见文本管线中消费,避免常见终端 charset 选择序列残留为 unknown。
456+
- 本轮补充:terminal ESC parser 现在把 charset selection (`ESC ( B` / `ESC ) 0` / `ESC * B` / `ESC / A`) 解析成结构化 charset action,并在 terminal parser 可见文本管线中消费,避免常见终端 charset 选择序列残留为 unknown。
457457
- 本轮补充:terminal CSI parser 现在把 DEC selective erase `CSI ? Ps J` / `CSI ? Ps K` 标记为 selective display/line erase,和普通 ED/EL 区分开。
458458
- 本轮补充:terminal CSI parser 现在把 ECMA `CSI Ps N` / `CSI Ps O` 解析成 erase-in-field / erase-in-area action,覆盖 to-end/to-start/all 三种 region。
459459
- 本轮补充:terminal CSI parser 把 DEC insert/delete columns (`CSI Ps ' }` / `CSI Ps ' ~`) 归入 edit action,避免列编辑控制序列落入 unknown fallback。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ M7 progress now includes:
507507
- `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.
508508
- `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.
509509
- `internal/tui`: key parsing and configurable keybindings now expose F1-F12 from common 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.
510-
- `internal/tui`: terminal ESC parsing now recognizes charset selection sequences such as `ESC ( B` and `ESC ) 0` as structured charset actions and consumes them in visible-text parsing instead of surfacing them as unknown terminal output.
510+
- `internal/tui`: terminal ESC parsing now recognizes charset selection sequences such as `ESC ( B`, `ESC ) 0`, `ESC * B`, and `ESC / A` as structured charset actions and consumes them in visible-text parsing instead of surfacing them as unknown terminal output.
511511
- `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.
512512
- `internal/tui`: interaction script single-step JSON now unwraps `step`/`scriptStep`/`interactionStep`/`record`/`entry`/`item`/`event` objects in arrays, JSONL records, and wrapper-object step lists.
513513
- `internal/tui`: interaction script single-step JSON now also unwraps JSON:API/resource-style `resource`/`node`/`attributes`/`properties`/`attrs` step items, so API fixture arrays and JSONL records can keep step resource envelopes.

internal/tui/terminal_esc.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ const (
1313
ESCTabSet = "\x1bH"
1414
ESCFinalStart = 0x30
1515
ESCFinalEnd = 0x7e
16-
ESCCharsetSelectLeft = '('
17-
ESCCharsetSelectRight = ')'
16+
ESCCharsetSelectG0 = '('
17+
ESCCharsetSelectG1 = ')'
18+
ESCCharsetSelectG2 = '*'
19+
ESCCharsetSelectG3 = '+'
20+
ESCCharsetSelectG1Alt = '-'
21+
ESCCharsetSelectG2Alt = '.'
22+
ESCCharsetSelectG3Alt = '/'
1823
)
1924

2025
type ESCActionType string
@@ -49,6 +54,9 @@ func ParseESCContent(chars string) (ESCAction, bool) {
4954
if chars == "" {
5055
return ESCAction{}, false
5156
}
57+
if isESCCharsetSelector(chars[0]) && len(chars) >= 2 {
58+
return ESCAction{Type: ESCActionCharset, CharsetSlot: chars[0], CharsetDesignator: chars[1], Sequence: ESCPrefix + chars}, true
59+
}
5260
switch chars[0] {
5361
case 'c':
5462
return ESCAction{Type: ESCActionReset}, true
@@ -64,10 +72,16 @@ func ParseESCContent(chars string) (ESCAction, bool) {
6472
return ESCAction{Type: ESCActionCursor, Cursor: CSICursorAction{Type: CSICursorActionNextLine, Count: 1}}, true
6573
case 'H':
6674
return ESCAction{Type: ESCActionCursor, Cursor: CSICursorAction{Type: CSICursorActionTabSet}}, true
67-
case ESCCharsetSelectLeft, ESCCharsetSelectRight:
68-
if len(chars) >= 2 {
69-
return ESCAction{Type: ESCActionCharset, CharsetSlot: chars[0], CharsetDesignator: chars[1], Sequence: ESCPrefix + chars}, true
70-
}
7175
}
7276
return ESCAction{Type: ESCActionUnknown, Sequence: ESCPrefix + chars}, true
7377
}
78+
79+
func isESCCharsetSelector(b byte) bool {
80+
switch b {
81+
case ESCCharsetSelectG0, ESCCharsetSelectG1, ESCCharsetSelectG2, ESCCharsetSelectG3,
82+
ESCCharsetSelectG1Alt, ESCCharsetSelectG2Alt, ESCCharsetSelectG3Alt:
83+
return true
84+
default:
85+
return false
86+
}
87+
}

internal/tui/terminal_parser_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ func TestTerminalVisibleTextUsesParserAndPreservesRawBell(t *testing.T) {
4747
"\x1b^pm\x1b\\" +
4848
"e" +
4949
"\x1b(B" +
50+
"\x1b*B" +
5051
"\x1bXsos\x07" +
5152
"f" +
5253
"\x1b)0" +
54+
"\x1b/A" +
5355
"\x07"
5456
if got := TerminalVisibleText(input); got != "aredbcdef\x07" {
5557
t.Fatalf("visible = %q", got)

internal/tui/terminal_sequence_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ func TestParseTerminalSequenceDispatchesActions(t *testing.T) {
7979
if !ok || charset.Type != TerminalSequenceESC || charset.ESC.Type != ESCActionCharset || charset.ESC.CharsetSlot != '(' || charset.ESC.CharsetDesignator != 'B' {
8080
t.Fatalf("charset esc dispatch = %#v ok=%v", charset, ok)
8181
}
82+
extendedCharset, ok := ParseTerminalSequence("\x1b*B")
83+
if !ok || extendedCharset.Type != TerminalSequenceESC || extendedCharset.ESC.Type != ESCActionCharset || extendedCharset.ESC.CharsetSlot != '*' || extendedCharset.ESC.CharsetDesignator != 'B' {
84+
t.Fatalf("extended charset esc dispatch = %#v ok=%v", extendedCharset, ok)
85+
}
8286
if text, ok := ParseTerminalSequence("plain"); ok || text.Type != "" {
8387
t.Fatalf("plain dispatch = %#v ok=%v", text, ok)
8488
}

internal/tui/tui_test.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5743,9 +5743,24 @@ func TestParseESCSequenceActions(t *testing.T) {
57435743
if action, ok := ParseESCContent(""); ok || action.Type != "" {
57445744
t.Fatalf("empty esc content parsed = %#v", action)
57455745
}
5746-
charset, ok := ParseESCSequence("\x1b(B")
5747-
if !ok || charset.Type != ESCActionCharset || charset.CharsetSlot != '(' || charset.CharsetDesignator != 'B' {
5748-
t.Fatalf("charset selection = %#v ok=%v", charset, ok)
5746+
charsetCases := []struct {
5747+
seq string
5748+
slot byte
5749+
designator byte
5750+
}{
5751+
{seq: "\x1b(B", slot: '(', designator: 'B'},
5752+
{seq: "\x1b)0", slot: ')', designator: '0'},
5753+
{seq: "\x1b*B", slot: '*', designator: 'B'},
5754+
{seq: "\x1b+0", slot: '+', designator: '0'},
5755+
{seq: "\x1b-A", slot: '-', designator: 'A'},
5756+
{seq: "\x1b.A", slot: '.', designator: 'A'},
5757+
{seq: "\x1b/A", slot: '/', designator: 'A'},
5758+
}
5759+
for _, tc := range charsetCases {
5760+
charset, ok := ParseESCSequence(tc.seq)
5761+
if !ok || charset.Type != ESCActionCharset || charset.CharsetSlot != tc.slot || charset.CharsetDesignator != tc.designator {
5762+
t.Fatalf("charset selection for %q = %#v ok=%v", tc.seq, charset, ok)
5763+
}
57495764
}
57505765

57515766
reset, ok := ParseESCSequence(ESCResetSequence)

0 commit comments

Comments
 (0)