Skip to content

Commit b92a124

Browse files
author
SqlRush
committed
Consume ESC charset shift controls
1 parent dedd4a0 commit b92a124

8 files changed

Lines changed: 85 additions & 23 deletions

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,8 @@ M7 补充:terminal CSI parser 现在识别带 intermediate space 的 `CSI Ps S
318318

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

321+
M7 补充:terminal ESC parser 现在也把 ISO-2022 charset shift 控制 (`ESC N``ESC n``ESC o``ESC |``ESC }``ESC ~`) 解析成结构化 charset-shift action,并在可见文本管线中消费,继续减少真实终端输出里的 unknown 控制序列。
322+
321323
M7 补充:terminal CSI parser 现在把 DEC selective erase `CSI ? Ps J` / `CSI ? Ps K` 标记为 selective display/line erase,和普通 ED/EL 区分开。
322324

323325
M7 补充:terminal CSI parser 现在把 ECMA `CSI Ps N` / `CSI Ps O` 解析成 erase-in-field / erase-in-area action,覆盖 to-end/to-start/all 三种 region。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ test/parity/ # golden tests against TS/official behavior
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。
456456
- 本轮补充:terminal ESC parser 现在把 charset selection (`ESC ( B` / `ESC ) 0` / `ESC * B` / `ESC / A` / `ESC % G` 等) 解析成结构化 charset action,并在 terminal parser 可见文本管线中消费,避免常见终端 charset 选择序列残留为 unknown。
457+
- 本轮补充:terminal ESC parser 现在也把 ISO-2022 charset shift 控制 (`ESC N``ESC n``ESC o``ESC |``ESC }``ESC ~`) 解析成结构化 charset-shift action,并在可见文本管线中消费,继续减少真实终端输出里的 unknown 控制序列。
457458
- 本轮补充:terminal CSI parser 现在把 DEC selective erase `CSI ? Ps J` / `CSI ? Ps K` 标记为 selective display/line erase,和普通 ED/EL 区分开。
458459
- 本轮补充:terminal CSI parser 现在把 ECMA `CSI Ps N` / `CSI Ps O` 解析成 erase-in-field / erase-in-area action,覆盖 to-end/to-start/all 三种 region。
459460
- 本轮补充: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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,7 @@ M7 progress now includes:
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.
510510
- `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.
511+
- `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.
511512
- `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.
512513
- `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.
513514
- `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: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,49 @@ package tui
33
import "strings"
44

55
const (
6-
ESCPrefix = "\x1b"
7-
ESCResetSequence = "\x1bc"
8-
ESCSaveCursor = "\x1b7"
9-
ESCRestoreCursor = "\x1b8"
10-
ESCIndex = "\x1bD"
11-
ESCReverseIndex = "\x1bM"
12-
ESCNextLine = "\x1bE"
13-
ESCTabSet = "\x1bH"
14-
ESCFinalStart = 0x30
15-
ESCFinalEnd = 0x7e
16-
ESCCharsetSelectUTF8 = '%'
17-
ESCCharsetSelectG0 = '('
18-
ESCCharsetSelectG1 = ')'
19-
ESCCharsetSelectG2 = '*'
20-
ESCCharsetSelectG3 = '+'
21-
ESCCharsetSelectG1Alt = '-'
22-
ESCCharsetSelectG2Alt = '.'
23-
ESCCharsetSelectG3Alt = '/'
6+
ESCPrefix = "\x1b"
7+
ESCResetSequence = "\x1bc"
8+
ESCSaveCursor = "\x1b7"
9+
ESCRestoreCursor = "\x1b8"
10+
ESCIndex = "\x1bD"
11+
ESCReverseIndex = "\x1bM"
12+
ESCNextLine = "\x1bE"
13+
ESCTabSet = "\x1bH"
14+
ESCFinalStart = 0x30
15+
ESCFinalEnd = 0x7e
16+
ESCCharsetSingleShift2 = 'N'
17+
ESCCharsetSingleShift3 = 'O'
18+
ESCCharsetLockingShift2 = 'n'
19+
ESCCharsetLockingShift3 = 'o'
20+
ESCCharsetLockingShift3Right = '|'
21+
ESCCharsetLockingShift2Right = '}'
22+
ESCCharsetLockingShift1Right = '~'
23+
ESCCharsetSelectUTF8 = '%'
24+
ESCCharsetSelectG0 = '('
25+
ESCCharsetSelectG1 = ')'
26+
ESCCharsetSelectG2 = '*'
27+
ESCCharsetSelectG3 = '+'
28+
ESCCharsetSelectG1Alt = '-'
29+
ESCCharsetSelectG2Alt = '.'
30+
ESCCharsetSelectG3Alt = '/'
2431
)
2532

2633
type ESCActionType string
2734

2835
const (
29-
ESCActionCursor ESCActionType = "cursor"
30-
ESCActionReset ESCActionType = "reset"
31-
ESCActionCharset ESCActionType = "charset"
32-
ESCActionUnknown ESCActionType = "unknown"
36+
ESCActionCursor ESCActionType = "cursor"
37+
ESCActionReset ESCActionType = "reset"
38+
ESCActionCharset ESCActionType = "charset"
39+
ESCActionCharsetShift ESCActionType = "charsetShift"
40+
ESCActionUnknown ESCActionType = "unknown"
3341
)
3442

3543
type ESCAction struct {
3644
Type ESCActionType
3745
Cursor CSICursorAction
3846
CharsetSlot byte
3947
CharsetDesignator byte
48+
CharsetShift string
4049
Sequence string
4150
}
4251

@@ -58,6 +67,9 @@ func ParseESCContent(chars string) (ESCAction, bool) {
5867
if isESCCharsetSelector(chars[0]) && len(chars) >= 2 {
5968
return ESCAction{Type: ESCActionCharset, CharsetSlot: chars[0], CharsetDesignator: chars[1], Sequence: ESCPrefix + chars}, true
6069
}
70+
if shift, ok := escCharsetShift(chars[0]); ok && len(chars) == 1 {
71+
return ESCAction{Type: ESCActionCharsetShift, CharsetShift: shift, Sequence: ESCPrefix + chars}, true
72+
}
6173
switch chars[0] {
6274
case 'c':
6375
return ESCAction{Type: ESCActionReset}, true
@@ -86,3 +98,24 @@ func isESCCharsetSelector(b byte) bool {
8698
return false
8799
}
88100
}
101+
102+
func escCharsetShift(b byte) (string, bool) {
103+
switch b {
104+
case ESCCharsetSingleShift2:
105+
return "singleShiftG2", true
106+
case ESCCharsetSingleShift3:
107+
return "singleShiftG3", true
108+
case ESCCharsetLockingShift2:
109+
return "lockingShiftG2", true
110+
case ESCCharsetLockingShift3:
111+
return "lockingShiftG3", true
112+
case ESCCharsetLockingShift3Right:
113+
return "lockingShiftG3Right", true
114+
case ESCCharsetLockingShift2Right:
115+
return "lockingShiftG2Right", true
116+
case ESCCharsetLockingShift1Right:
117+
return "lockingShiftG1Right", true
118+
default:
119+
return "", false
120+
}
121+
}

internal/tui/terminal_parser.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ func (p *TerminalParser) processSequence(sequence string) (TerminalAction, bool)
361361
case ESCActionReset:
362362
p.Reset()
363363
return TerminalAction{Type: TerminalActionReset}, true
364-
case ESCActionCharset:
364+
case ESCActionCharset, ESCActionCharsetShift:
365365
return TerminalAction{}, false
366366
default:
367367
return TerminalAction{Type: TerminalActionUnknown, Sequence: action.ESC.Sequence}, true

internal/tui/terminal_parser_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,13 @@ func TestTerminalVisibleTextUsesParserAndPreservesRawBell(t *testing.T) {
4949
"\x1b(B" +
5050
"\x1b*B" +
5151
"\x1b%G" +
52+
"\x1bN" +
53+
"\x1bn" +
5254
"\x1bXsos\x07" +
5355
"f" +
5456
"\x1b)0" +
5557
"\x1b/A" +
58+
"\x1b|" +
5659
"\x07"
5760
if got := TerminalVisibleText(input); got != "aredbcdef\x07" {
5861
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
@@ -87,6 +87,10 @@ func TestParseTerminalSequenceDispatchesActions(t *testing.T) {
8787
if !ok || utf8Charset.Type != TerminalSequenceESC || utf8Charset.ESC.Type != ESCActionCharset || utf8Charset.ESC.CharsetSlot != '%' || utf8Charset.ESC.CharsetDesignator != 'G' {
8888
t.Fatalf("utf8 charset esc dispatch = %#v ok=%v", utf8Charset, ok)
8989
}
90+
charsetShift, ok := ParseTerminalSequence("\x1bn")
91+
if !ok || charsetShift.Type != TerminalSequenceESC || charsetShift.ESC.Type != ESCActionCharsetShift || charsetShift.ESC.CharsetShift != "lockingShiftG2" {
92+
t.Fatalf("charset shift esc dispatch = %#v ok=%v", charsetShift, ok)
93+
}
9094
if text, ok := ParseTerminalSequence("plain"); ok || text.Type != "" {
9195
t.Fatalf("plain dispatch = %#v ok=%v", text, ok)
9296
}

internal/tui/tui_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5765,6 +5765,24 @@ func TestParseESCSequenceActions(t *testing.T) {
57655765
}
57665766
}
57675767

5768+
charsetShiftCases := []struct {
5769+
seq string
5770+
shift string
5771+
}{
5772+
{seq: "\x1bN", shift: "singleShiftG2"},
5773+
{seq: "\x1bn", shift: "lockingShiftG2"},
5774+
{seq: "\x1bo", shift: "lockingShiftG3"},
5775+
{seq: "\x1b|", shift: "lockingShiftG3Right"},
5776+
{seq: "\x1b}", shift: "lockingShiftG2Right"},
5777+
{seq: "\x1b~", shift: "lockingShiftG1Right"},
5778+
}
5779+
for _, tc := range charsetShiftCases {
5780+
shift, ok := ParseESCSequence(tc.seq)
5781+
if !ok || shift.Type != ESCActionCharsetShift || shift.CharsetShift != tc.shift {
5782+
t.Fatalf("charset shift for %q = %#v ok=%v", tc.seq, shift, ok)
5783+
}
5784+
}
5785+
57685786
reset, ok := ParseESCSequence(ESCResetSequence)
57695787
if !ok || reset.Type != ESCActionReset {
57705788
t.Fatalf("reset action = %#v", reset)

0 commit comments

Comments
 (0)