Skip to content

Commit 0d4c031

Browse files
author
SqlRush
committed
Parse SS3 cursor sequences
1 parent 4fa1f61 commit 0d4c031

7 files changed

Lines changed: 35 additions & 5 deletions

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ M7 补充:terminal CSI parser 现在把 TBC tab-clear (`CSI g`/`CSI 3g`) 解
136136

137137
M7 补充:terminal ESC parser 现在把 HTS horizontal-tab-set (`ESC H`) 解析成 cursor/tab-set action,和 CSI tab-clear 控制序列形成闭环。
138138

139+
M7 补充:terminal sequence dispatcher 现在把 SS3 application cursor (`ESC OA`/`OB`/`OC`/`OD`) 解析成结构化 cursor move action,避免 application cursor mode 序列落入 unknown。
140+
139141
M7 补充:terminal CSI parser 现在把 REP repeat-preceding-character (`CSI b`/`CSI 4b`) 解析成 edit action,visible-text/snapshot pipeline 和 ANSI message wrapping/trim 会按重复次数展开前一个可重复 grapheme。
140142

141143
M7 补充:terminal CSI parser 现在按 ANSI 默认参数解析 scroll-region (`CSI r`/`CSI ;10r`),缺失 top 默认为 1,缺失 bottom 保持为 0 表示 reset/full-height,避免把 reset 误判为单行区域。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ test/parity/ # golden tests against TS/official behavior
248248
- 本轮补充:terminal CSI parser 把 xterm window manipulation/report (`CSI t`) 归入 report action,覆盖常见 `CSI 14t`/`CSI 18t` 窗口/文本区尺寸查询。
249249
- 本轮补充:terminal CSI parser 把 TBC tab-clear (`CSI g`/`CSI 3g`) 归入 cursor action,保留 clear-current/all code。
250250
- 本轮补充:terminal ESC parser 把 HTS horizontal-tab-set (`ESC H`) 归入 cursor/tab-set action,和 CSI tab-clear 控制序列形成闭环。
251+
- 本轮补充:terminal sequence dispatcher 把 SS3 application cursor (`ESC OA`/`OB`/`OC`/`OD`) 归入结构化 cursor move action,避免 application cursor mode 序列落入 unknown。
251252
- 本轮补充:terminal CSI parser 把 REP repeat-preceding-character (`CSI b`) 归入 edit action,visible-text/snapshot pipeline 和 ANSI message wrapping/trim 会按重复次数展开前一个可重复 grapheme。
252253
- 本轮补充:terminal CSI parser 按 ANSI 默认参数解析 scroll-region (`CSI r`/`CSI ;10r`),缺失 top 默认为 1,缺失 bottom 保持 0 表示 reset/full-height,避免 reset scroll region 被误判为单行区域。
253254
- 本轮补充:terminal CSI parser 把 DECSTR soft reset (`CSI !p`) 归入 reset action,并在 terminal parser 中清理 SGR/link 状态。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ M7 progress now includes:
213213
- `internal/tui`: terminal CSI parsing now emits cursor actions for TBC tab-clear sequences such as `CSI g` and `CSI 3g`, preserving the clear-current/all code.
214214
- `internal/tui`: terminal CSI parsing now emits edit actions for REP repeat-preceding-character sequences such as `CSI b` and `CSI 4b`, and the visible-text/snapshot plus ANSI message wrapping/trim paths expand the previous repeatable grapheme by the requested count.
215215
- `internal/tui`: terminal CSI parsing now emits reset actions for DECSTR soft reset `CSI !p`, and the terminal parser clears SGR/link state through the existing reset path.
216+
- `internal/tui`: terminal sequence dispatch now parses SS3 application cursor sequences such as `ESC OA`/`OB`/`OC`/`OD` as structured cursor move actions.
216217
- `internal/tui`: image hint parsing now accepts OSC ST terminators and base64 `name=` filenames while preserving prompt pasted-content metadata.
217218
- `internal/session`: prompt history writing now skips image pasted-content records like official `history.ts`, while the reader still accepts older image metadata entries.
218219
- `internal/session`: paste-cache now has a best-effort cutoff-mtime cleanup helper for old `.txt` paste files, matching official `cleanupOldPastes` behavior.

internal/tui/terminal_parser.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ func (p *TerminalParser) processSequence(sequence string) (TerminalAction, bool)
224224
return TerminalAction{}, false
225225
}
226226
switch action.Type {
227-
case TerminalSequenceCSI:
227+
case TerminalSequenceCSI, TerminalSequenceSS3:
228228
switch action.CSI.Type {
229229
case CSIActionSGR:
230230
p.style = ApplySGR(action.CSI.SGRParams, p.style)

internal/tui/terminal_parser_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,8 @@ func TestTerminalParserDispatchesSequenceActions(t *testing.T) {
194194
if actions[8].Type != TerminalActionCursor || actions[8].Cursor.Type != CSICursorActionMove || actions[8].Cursor.Direction != CSICursorDown {
195195
t.Fatalf("esc cursor action = %#v", actions[8])
196196
}
197-
if actions[9].Type != TerminalActionUnknown || actions[9].Sequence != "\x1bOA" {
198-
t.Fatalf("unknown action = %#v", actions[9])
197+
if actions[9].Type != TerminalActionCursor || actions[9].Cursor.Type != CSICursorActionMove || actions[9].Cursor.Direction != CSICursorUp || actions[9].Cursor.Count != 1 {
198+
t.Fatalf("ss3 cursor action = %#v", actions[9])
199199
}
200200
}
201201

internal/tui/terminal_sequence.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@ func ParseTerminalSequence(sequence string) (TerminalSequenceAction, bool) {
8181
}
8282
return TerminalSequenceAction{Type: TerminalSequenceESC, ESC: action}, true
8383
case TerminalSequenceSS3:
84-
return TerminalSequenceAction{Type: TerminalSequenceUnknown, Sequence: sequence}, true
84+
action, ok := ParseSS3Sequence(sequence)
85+
if !ok {
86+
return TerminalSequenceAction{Type: TerminalSequenceUnknown, Sequence: sequence}, true
87+
}
88+
return TerminalSequenceAction{Type: TerminalSequenceSS3, CSI: action}, true
8589
case TerminalSequenceDCS, TerminalSequenceAPC, TerminalSequencePM, TerminalSequenceSOS:
8690
return TerminalSequenceAction{
8791
Type: IdentifyTerminalSequence(sequence),
@@ -95,6 +99,24 @@ func ParseTerminalSequence(sequence string) (TerminalSequenceAction, bool) {
9599
}
96100
}
97101

102+
func ParseSS3Sequence(sequence string) (CSIAction, bool) {
103+
if !strings.HasPrefix(sequence, ESCPrefix+"O") || len(sequence) != 3 {
104+
return CSIAction{}, false
105+
}
106+
switch sequence[2] {
107+
case 'A':
108+
return csiCursorMove(CSICursorUp, 1), true
109+
case 'B':
110+
return csiCursorMove(CSICursorDown, 1), true
111+
case 'C':
112+
return csiCursorMove(CSICursorForward, 1), true
113+
case 'D':
114+
return csiCursorMove(CSICursorBack, 1), true
115+
default:
116+
return CSIAction{}, false
117+
}
118+
}
119+
98120
func ParseTerminalStringControl(sequence string) TerminalStringControlAction {
99121
control := TerminalStringControlAction{
100122
Type: IdentifyTerminalSequence(sequence),

internal/tui/terminal_sequence_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,13 @@ func TestParseTerminalSequenceDispatchesActions(t *testing.T) {
4545
}
4646

4747
ss3, ok := ParseTerminalSequence("\x1bOA")
48-
if !ok || ss3.Type != TerminalSequenceUnknown || ss3.Sequence != "\x1bOA" {
48+
if !ok || ss3.Type != TerminalSequenceSS3 || ss3.CSI.Type != CSIActionCursor || ss3.CSI.Cursor.Type != CSICursorActionMove || ss3.CSI.Cursor.Direction != CSICursorUp || ss3.CSI.Cursor.Count != 1 {
4949
t.Fatalf("ss3 dispatch = %#v ok=%v", ss3, ok)
5050
}
51+
unknownSS3, ok := ParseTerminalSequence("\x1bOP")
52+
if !ok || unknownSS3.Type != TerminalSequenceUnknown || unknownSS3.Sequence != "\x1bOP" {
53+
t.Fatalf("unknown ss3 dispatch = %#v ok=%v", unknownSS3, ok)
54+
}
5155

5256
dcs, ok := ParseTerminalSequence("\x1bPtmux;" + EnterAlternateScreen + OSCStringTerminator)
5357
if !ok || dcs.Type != TerminalSequenceDCS || dcs.StringControl.Type != TerminalSequenceDCS || !dcs.StringControl.Complete || dcs.StringControl.Terminator != OSCStringTerminator || dcs.StringControl.Payload != "tmux;"+EnterAlternateScreen {

0 commit comments

Comments
 (0)