Skip to content

Commit d2dc198

Browse files
author
SqlRush
committed
Parse ESC screen attribute controls
1 parent 68e95ea commit d2dc198

7 files changed

Lines changed: 75 additions & 0 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,8 @@ M7 补充:terminal ESC parser 现在把 charset selection (`ESC ( B` / `ESC )
338338

339339
M7 补充:terminal ESC parser 现在也把 ISO-2022 charset shift 控制 (`ESC N``ESC n``ESC o``ESC |``ESC }``ESC ~`) 解析成结构化 charset-shift action,并在可见文本管线中消费,继续减少真实终端输出里的 unknown 控制序列。
340340

341+
M7 补充:terminal ESC parser 现在把 DEC line/screen attribute 序列 (`ESC # 3/4/5/6/8`) 解析成结构化 screen action,并在 terminal parser 中透传 alignment-test/line-size 控制,避免 ANSI snapshot 管线落入 unknown。
342+
341343
M7 补充:terminal CSI parser 现在把 DEC selective erase `CSI ? Ps J` / `CSI ? Ps K` 标记为 selective display/line erase,和普通 ED/EL 区分开。
342344

343345
M7 补充:terminal CSI parser 现在把 ECMA `CSI Ps N` / `CSI Ps O` 解析成 erase-in-field / erase-in-area action,覆盖 to-end/to-start/all 三种 region。
@@ -894,6 +896,8 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
894896

895897
本轮补充:terminal CSI parser 把 DECSTR soft reset (`CSI !p`) 归入 reset action,并在 terminal parser 中清理 SGR/link 状态。
896898

899+
本轮补充:terminal ESC parser 把 DEC line/screen attribute (`ESC # 3/4/5/6/8`) 归入 screen action,terminal parser 会结构化透传 double-height top/bottom、single/double-width 和 alignment-test 控制,继续减少真实 ANSI 输出里的 unknown fallback。
900+
897901
本轮补充:renderer/snapshot 增加 opt-in DEC 2026 synchronized output 包裹入口,可用官方 BSU/ESU (`CSI ?2026h`/`CSI ?2026l`) 生成整帧 ANSI fixture,同时默认渲染保持不变。
898902

899903
本轮补充:terminal OSC helper 增加 OSC 0 title/icon 序列生成,输入会先 strip ANSI;`StripANSI` 现在会完整跳过 OSC/DCS/APC/PM/SOS payload,避免 title/snapshot 可见文本被终端控制串污染。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ test/parity/ # golden tests against TS/official behavior
463463
- 本轮补充:terminal CSI parser 现在识别带 intermediate space 的 `CSI Ps SP @` / `CSI Ps SP A` scroll-left/right 序列,避免误解析成 insert-characters 或 cursor-up。
464464
- 本轮补充:terminal ESC parser 现在把 charset selection (`ESC ( B` / `ESC ) 0` / `ESC * B` / `ESC / A` / `ESC % G` 等) 解析成结构化 charset action,并在 terminal parser 可见文本管线中消费,避免常见终端 charset 选择序列残留为 unknown。
465465
- 本轮补充:terminal ESC parser 现在也把 ISO-2022 charset shift 控制 (`ESC N``ESC n``ESC o``ESC |``ESC }``ESC ~`) 解析成结构化 charset-shift action,并在可见文本管线中消费,继续减少真实终端输出里的 unknown 控制序列。
466+
- 本轮补充:terminal ESC parser 现在把 DEC line/screen attribute (`ESC # 3/4/5/6/8`) 解析成结构化 screen action,terminal parser 会透传 double-height top/bottom、single/double-width 和 alignment-test 控制。
466467
- 本轮补充:terminal CSI parser 现在把 DEC selective erase `CSI ? Ps J` / `CSI ? Ps K` 标记为 selective display/line erase,和普通 ED/EL 区分开。
467468
- 本轮补充:terminal CSI parser 现在把 ECMA `CSI Ps N` / `CSI Ps O` 解析成 erase-in-field / erase-in-area action,覆盖 to-end/to-start/all 三种 region。
468469
- 本轮补充: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
@@ -378,6 +378,7 @@ M7 progress now includes:
378378
- `internal/tui`: terminal CSI parser now recognizes DEC left/right margin mode (`?69h/l`) as a structured `leftRightMarginMode` action.
379379
- `internal/tui`: terminal CSI parsing now distinguishes parameterized `CSI Pl;Pr s` left/right horizontal margin regions from bare `CSI s` save-cursor semantics.
380380
- `internal/tui`: terminal CSI parsing now recognizes intermediate-space `CSI Ps SP @` / `CSI Ps SP A` scroll-left/right sequences instead of misclassifying them as insert-characters or cursor-up.
381+
- `internal/tui`: terminal ESC parsing now exposes DEC line/screen attribute controls (`ESC # 3/4/5/6/8`) as structured screen actions instead of falling back to unknown.
381382
- `internal/tui`: terminal CSI parsing now marks DEC selective erase `CSI ? Ps J` / `CSI ? Ps K` separately from ordinary ED/EL erase actions.
382383
- `internal/tui`: terminal CSI parsing now exposes ECMA `CSI Ps N` / `CSI Ps O` erase-in-field and erase-in-area actions with the same to-end/to-start/all regions used by other erase operations.
383384
- `internal/tui`: terminal CSI parsing now treats explicit zero count/position params as ANSI defaults for cursor movement/position/column, insert/repeat/erase chars, and scroll actions while preserving raw zero selector semantics for mode/report/erase selector sequences.

internal/tui/terminal_esc.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const (
1111
ESCReverseIndex = "\x1bM"
1212
ESCNextLine = "\x1bE"
1313
ESCTabSet = "\x1bH"
14+
ESCScreenAttributes = '#'
1415
ESCFinalStart = 0x30
1516
ESCFinalEnd = 0x7e
1617
ESCCharsetSingleShift2 = 'N'
@@ -38,13 +39,29 @@ const (
3839
ESCActionReset ESCActionType = "reset"
3940
ESCActionCharset ESCActionType = "charset"
4041
ESCActionCharsetShift ESCActionType = "charsetShift"
42+
ESCActionScreen ESCActionType = "screen"
4143
ESCActionUnknown ESCActionType = "unknown"
4244
)
4345

46+
type ESCScreenActionType string
47+
48+
const (
49+
ESCScreenActionDoubleHeightTop ESCScreenActionType = "doubleHeightTop"
50+
ESCScreenActionDoubleHeightBottom ESCScreenActionType = "doubleHeightBottom"
51+
ESCScreenActionSingleWidth ESCScreenActionType = "singleWidth"
52+
ESCScreenActionDoubleWidth ESCScreenActionType = "doubleWidth"
53+
ESCScreenActionAlignmentTest ESCScreenActionType = "alignmentTest"
54+
)
55+
56+
type ESCScreenAction struct {
57+
Type ESCScreenActionType
58+
}
59+
4460
type ESCAction struct {
4561
Type ESCActionType
4662
Cursor CSICursorAction
4763
Mode CSIModeAction
64+
Screen ESCScreenAction
4865
CharsetSlot byte
4966
CharsetDesignator byte
5067
CharsetShift string
@@ -72,6 +89,11 @@ func ParseESCContent(chars string) (ESCAction, bool) {
7289
if shift, ok := escCharsetShift(chars[0]); ok && len(chars) == 1 {
7390
return ESCAction{Type: ESCActionCharsetShift, CharsetShift: shift, Sequence: ESCPrefix + chars}, true
7491
}
92+
if chars[0] == ESCScreenAttributes && len(chars) == 2 {
93+
if screenType, ok := escScreenActionType(chars[1]); ok {
94+
return ESCAction{Type: ESCActionScreen, Screen: ESCScreenAction{Type: screenType}, Sequence: ESCPrefix + chars}, true
95+
}
96+
}
7597
switch chars[0] {
7698
case 'c':
7799
return ESCAction{Type: ESCActionReset}, true
@@ -105,6 +127,23 @@ func isESCCharsetSelector(b byte) bool {
105127
}
106128
}
107129

130+
func escScreenActionType(b byte) (ESCScreenActionType, bool) {
131+
switch b {
132+
case '3':
133+
return ESCScreenActionDoubleHeightTop, true
134+
case '4':
135+
return ESCScreenActionDoubleHeightBottom, true
136+
case '5':
137+
return ESCScreenActionSingleWidth, true
138+
case '6':
139+
return ESCScreenActionDoubleWidth, true
140+
case '8':
141+
return ESCScreenActionAlignmentTest, true
142+
default:
143+
return "", false
144+
}
145+
}
146+
108147
func escCharsetShift(b byte) (string, bool) {
109148
switch b {
110149
case ESCCharsetSingleShift2:

internal/tui/terminal_parser.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
TerminalActionShell TerminalActionType = "shellIntegration"
3030
TerminalActionBell TerminalActionType = "bell"
3131
TerminalActionReset TerminalActionType = "reset"
32+
TerminalActionScreen TerminalActionType = "screen"
3233
TerminalActionStringControl TerminalActionType = "stringControl"
3334
TerminalActionUnknown TerminalActionType = "unknown"
3435
)
@@ -49,6 +50,7 @@ type TerminalAction struct {
4950
Scroll CSIScrollAction
5051
Mode CSIModeAction
5152
Modes []CSIModeAction
53+
Screen ESCScreenAction
5254
OSC OSCAction
5355
String TerminalStringControlAction
5456
Sequence string
@@ -363,6 +365,8 @@ func (p *TerminalParser) processSequence(sequence string) (TerminalAction, bool)
363365
case ESCActionReset:
364366
p.Reset()
365367
return TerminalAction{Type: TerminalActionReset}, true
368+
case ESCActionScreen:
369+
return TerminalAction{Type: TerminalActionScreen, Screen: action.ESC.Screen}, true
366370
case ESCActionCharset, ESCActionCharsetShift:
367371
return TerminalAction{}, false
368372
default:

internal/tui/terminal_sequence_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ func TestParseTerminalSequenceDispatchesActions(t *testing.T) {
9999
if !ok || keypadNumeric.Type != TerminalSequenceESC || keypadNumeric.ESC.Type != ESCActionMode || keypadNumeric.ESC.Mode.Type != CSIModeActionApplicationKeypad || keypadNumeric.ESC.Mode.Enabled {
100100
t.Fatalf("numeric keypad esc dispatch = %#v ok=%v", keypadNumeric, ok)
101101
}
102+
screen, ok := ParseTerminalSequence("\x1b#8")
103+
if !ok || screen.Type != TerminalSequenceESC || screen.ESC.Type != ESCActionScreen || screen.ESC.Screen.Type != ESCScreenActionAlignmentTest {
104+
t.Fatalf("screen esc dispatch = %#v ok=%v", screen, ok)
105+
}
102106
if text, ok := ParseTerminalSequence("plain"); ok || text.Type != "" {
103107
t.Fatalf("plain dispatch = %#v ok=%v", text, ok)
104108
}

internal/tui/tui_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5807,6 +5807,23 @@ func TestParseESCSequenceActions(t *testing.T) {
58075807
}
58085808
}
58095809

5810+
screenCases := []struct {
5811+
seq string
5812+
want ESCScreenActionType
5813+
}{
5814+
{seq: "\x1b#3", want: ESCScreenActionDoubleHeightTop},
5815+
{seq: "\x1b#4", want: ESCScreenActionDoubleHeightBottom},
5816+
{seq: "\x1b#5", want: ESCScreenActionSingleWidth},
5817+
{seq: "\x1b#6", want: ESCScreenActionDoubleWidth},
5818+
{seq: "\x1b#8", want: ESCScreenActionAlignmentTest},
5819+
}
5820+
for _, tc := range screenCases {
5821+
screen, ok := ParseESCSequence(tc.seq)
5822+
if !ok || screen.Type != ESCActionScreen || screen.Screen.Type != tc.want {
5823+
t.Fatalf("screen action for %q = %#v ok=%v", tc.seq, screen, ok)
5824+
}
5825+
}
5826+
58105827
reset, ok := ParseESCSequence(ESCResetSequence)
58115828
if !ok || reset.Type != ESCActionReset {
58125829
t.Fatalf("reset action = %#v", reset)
@@ -5850,6 +5867,11 @@ func TestParseESCSequenceActions(t *testing.T) {
58505867
t.Fatalf("terminal parser keypad esc actions = %#v", actions)
58515868
}
58525869

5870+
actions = parser.Feed("\x1b#8")
5871+
if len(actions) != 1 || actions[0].Type != TerminalActionScreen || actions[0].Screen.Type != ESCScreenActionAlignmentTest {
5872+
t.Fatalf("terminal parser screen actions = %#v", actions)
5873+
}
5874+
58535875
unknown, ok := ParseESCContent("]not-osc")
58545876
if !ok || unknown.Type != ESCActionUnknown || unknown.Sequence != "\x1b]not-osc" {
58555877
t.Fatalf("unknown esc action = %#v", unknown)

0 commit comments

Comments
 (0)