Skip to content

Commit 6e80f01

Browse files
author
SqlRush
committed
Parse OSC special color actions
1 parent 10ad835 commit 6e80f01

7 files changed

Lines changed: 152 additions & 0 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,8 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
816816

817817
本轮补充:terminal OSC parser 把 OSC 4 palette color 设置/查询和 OSC 104 palette reset 解析为结构化 palette action,支持同一序列内多组 index/color、index/? 和按 index reset。
818818

819+
本轮补充:terminal OSC parser 把 OSC 5 special color 设置/查询和 OSC 105 reset 解析为结构化 specialColor action,限制 special index 为 0-4,并保持 visible text/snapshot 清洁。
820+
819821
本轮补充:terminal OSC helper 增加 OSC 21337 tab-status payload parser,支持 `\;`/`\\` 转义、bare key 或空值清理、unknown key ignore,并复用 OSC color parser 解析 indicator/status-color。
820822

821823
本轮补充:terminal OSC helper 增加 OSC 8 hyperlink payload parser,按官方规则解析 params、保留 URL 内部分号,并把空 URL 识别为 hyperlink end。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ test/parity/ # golden tests against TS/official behavior
459459
- 本轮补充:terminal OSC parser 把 OSC 10-19 dynamic color 设置/查询解析为结构化 color action,复用既有 OSC color parser,并支持同一序列内按官方递增规则连续设置多个 target;visible text/snapshot 继续剥离这些控制串。
460460
- 本轮补充:terminal OSC parser 把 OSC 110-119 dynamic color reset 序列解析为结构化 color reset action,覆盖前景/背景/光标、pointer、Tektronix 和 highlight 动态色 reset。
461461
- 本轮补充:terminal OSC parser 把 OSC 4 palette color 设置/查询和 OSC 104 palette reset 解析为结构化 palette action,支持同一序列内多组 index/color、index/? 和按 index reset。
462+
- 本轮补充:terminal OSC parser 把 OSC 5 special color 设置/查询和 OSC 105 reset 解析为结构化 specialColor action,限制 special index 为 0-4,并保持 visible text/snapshot 清洁。
462463
- 本轮补充:terminal OSC helper 增加 OSC 21337 tab-status payload parser,支持 `\;`/`\\` 转义、bare key 或空值清理、unknown key ignore,并复用 OSC color parser 解析 indicator/status-color。
463464
- 本轮补充:terminal OSC helper 增加 OSC 8 hyperlink payload parser,按官方规则解析 params、保留 URL 内部分号,并把空 URL 识别为 hyperlink end。
464465
- 本轮补充:terminal OSC helper 增加轻量 `ParseOSCContent`,覆盖官方 title(0/1/2)、OSC 8 hyperlink、OSC 21337 tab status 和 unknown action 分支。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ M7 progress now includes:
263263
- `internal/tui`: terminal OSC parsing now exposes OSC 10-19 dynamic color set/query sequences as structured color actions, including repeated payload parameters that advance to subsequent dynamic color targets while keeping visible text clean.
264264
- `internal/tui`: terminal OSC parsing now exposes OSC 110-119 dynamic color reset sequences as structured color reset actions for foreground/background/cursor, pointer, Tektronix, and highlight color targets.
265265
- `internal/tui`: terminal OSC parsing now exposes OSC 4 palette color set/query sequences and OSC 104 palette reset sequences as structured palette actions, including repeated index/color pairs and indexed resets.
266+
- `internal/tui`: terminal OSC parsing now exposes OSC 5 special color set/query sequences and OSC 105 special color reset sequences as structured specialColor actions, validating special indexes 0-4.
266267
- `internal/tui`: terminal OSC helpers now parse OSC 21337 tab-status payloads with escaped separators, clear/null semantics, unknown-key ignore behavior, and parsed indicator/status colors.
267268
- `internal/tui`: terminal OSC helpers now parse OSC 8 hyperlink payloads, including params, semicolon-containing URLs, and empty-URL link-end sequences.
268269
- `internal/tui`: terminal OSC helpers now expose a lightweight `ParseOSCContent` covering title, hyperlink, tab-status, and unknown action branches.

internal/tui/terminal_osc.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const (
1717
OSCStringTerminator = "\x1b\\"
1818
OSCSetTitleAndIcon = "0"
1919
OSCPaletteColor = "4"
20+
OSCSpecialColor = "5"
2021
OSCCurrentDirectory = "7"
2122
OSCHyperlink = "8"
2223
OSCITerm2 = "9"
@@ -41,6 +42,7 @@ const (
4142
OSCResetTektronixCursor = "118"
4243
OSCResetHighlightForeground = "119"
4344
OSCResetPalette = "104"
45+
OSCResetSpecialColor = "105"
4446
OSCClipboard = "52"
4547
OSCKitty = "99"
4648
OSCShellIntegration = "133"
@@ -100,6 +102,7 @@ const (
100102
OSCActionClipboard OSCActionType = "clipboard"
101103
OSCActionColor OSCActionType = "color"
102104
OSCActionPalette OSCActionType = "palette"
105+
OSCActionSpecialColor OSCActionType = "specialColor"
103106
OSCActionProgress OSCActionType = "progress"
104107
OSCActionNotification OSCActionType = "notification"
105108
OSCActionShell OSCActionType = "shellIntegration"
@@ -171,6 +174,22 @@ type TerminalOSCPaletteAction struct {
171174
Valid bool
172175
}
173176

177+
type TerminalOSCSpecialColorEntry struct {
178+
Index int
179+
Color *RGBColor
180+
Query bool
181+
Reset bool
182+
Raw string
183+
Valid bool
184+
}
185+
186+
type TerminalOSCSpecialColorAction struct {
187+
Entries []TerminalOSCSpecialColorEntry
188+
ResetAll bool
189+
Raw string
190+
Valid bool
191+
}
192+
174193
type TerminalNotificationAction struct {
175194
Provider string
176195
ID string
@@ -198,6 +217,7 @@ type OSCAction struct {
198217
Clipboard TerminalClipboardAction
199218
Color TerminalOSCColorAction
200219
Palette TerminalOSCPaletteAction
220+
SpecialColor TerminalOSCSpecialColorAction
201221
Progress TerminalProgressAction
202222
Notification TerminalNotificationAction
203223
Shell TerminalShellIntegrationAction
@@ -274,6 +294,8 @@ func ParseOSCContent(content string) OSCAction {
274294
}
275295
case OSCPaletteColor:
276296
return OSCAction{Type: OSCActionPalette, Palette: ParseOSCPalettePayload(data)}
297+
case OSCSpecialColor:
298+
return OSCAction{Type: OSCActionSpecialColor, SpecialColor: ParseOSCSpecialColorPayload(data)}
277299
case OSCCurrentDirectory:
278300
return OSCAction{Type: OSCActionDirectory, Directory: ParseDirectoryPayload(data)}
279301
case OSCHyperlink:
@@ -283,6 +305,8 @@ func ParseOSCContent(content string) OSCAction {
283305
return OSCAction{Type: OSCActionColor, Color: ParseOSCColorPayload(strconv.Itoa(commandNumber), data)}
284306
case OSCResetPalette:
285307
return OSCAction{Type: OSCActionPalette, Palette: ParseOSCPaletteResetPayload(data)}
308+
case OSCResetSpecialColor:
309+
return OSCAction{Type: OSCActionSpecialColor, SpecialColor: ParseOSCSpecialColorResetPayload(data)}
286310
case OSCClipboard:
287311
return OSCAction{Type: OSCActionClipboard, Clipboard: ParseClipboardPayload(data)}
288312
case OSCITerm2:
@@ -602,6 +626,81 @@ func parseOSCPaletteIndex(value string) (int, bool) {
602626
return index, true
603627
}
604628

629+
func ParseOSCSpecialColorPayload(payload string) TerminalOSCSpecialColorAction {
630+
action := TerminalOSCSpecialColorAction{
631+
Raw: payload,
632+
}
633+
parts := strings.Split(payload, ";")
634+
if len(parts) == 0 || len(parts)%2 != 0 {
635+
return action
636+
}
637+
entries := make([]TerminalOSCSpecialColorEntry, 0, len(parts)/2)
638+
for i := 0; i < len(parts); i += 2 {
639+
index, ok := parseOSCSpecialColorIndex(parts[i])
640+
if !ok {
641+
return action
642+
}
643+
spec := strings.TrimSpace(parts[i+1])
644+
entry := TerminalOSCSpecialColorEntry{
645+
Index: index,
646+
Raw: parts[i+1],
647+
}
648+
switch {
649+
case spec == "?":
650+
entry.Query = true
651+
entry.Valid = true
652+
default:
653+
color, ok := ParseOSCColor(spec)
654+
if !ok {
655+
return action
656+
}
657+
entry.Color = color
658+
entry.Valid = true
659+
}
660+
entries = append(entries, entry)
661+
}
662+
action.Entries = entries
663+
action.Valid = len(entries) > 0
664+
return action
665+
}
666+
667+
func ParseOSCSpecialColorResetPayload(payload string) TerminalOSCSpecialColorAction {
668+
action := TerminalOSCSpecialColorAction{
669+
Raw: payload,
670+
}
671+
payload = strings.TrimSpace(payload)
672+
if payload == "" {
673+
action.ResetAll = true
674+
action.Valid = true
675+
return action
676+
}
677+
parts := strings.Split(payload, ";")
678+
entries := make([]TerminalOSCSpecialColorEntry, 0, len(parts))
679+
for _, part := range parts {
680+
index, ok := parseOSCSpecialColorIndex(part)
681+
if !ok {
682+
return action
683+
}
684+
entries = append(entries, TerminalOSCSpecialColorEntry{
685+
Index: index,
686+
Reset: true,
687+
Raw: part,
688+
Valid: true,
689+
})
690+
}
691+
action.Entries = entries
692+
action.Valid = len(entries) > 0
693+
return action
694+
}
695+
696+
func parseOSCSpecialColorIndex(value string) (int, bool) {
697+
index, ok := parseOSCPaletteIndex(value)
698+
if !ok || index > 4 {
699+
return 0, false
700+
}
701+
return index, true
702+
}
703+
605704
func ParseITerm2ProgressPayload(payload string) (TerminalProgressAction, bool) {
606705
parts := strings.Split(payload, ";")
607706
if len(parts) < 2 || parts[0] != ITerm2Progress {

internal/tui/terminal_parser.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const (
2222
TerminalActionClipboard TerminalActionType = "clipboard"
2323
TerminalActionColor TerminalActionType = "color"
2424
TerminalActionPalette TerminalActionType = "palette"
25+
TerminalActionSpecialColor TerminalActionType = "specialColor"
2526
TerminalActionProgress TerminalActionType = "progress"
2627
TerminalActionNotification TerminalActionType = "notification"
2728
TerminalActionShell TerminalActionType = "shellIntegration"
@@ -335,6 +336,8 @@ func (p *TerminalParser) processSequence(sequence string) (TerminalAction, bool)
335336
return TerminalAction{Type: TerminalActionColor, OSC: action.OSC}, true
336337
case OSCActionPalette:
337338
return TerminalAction{Type: TerminalActionPalette, OSC: action.OSC}, true
339+
case OSCActionSpecialColor:
340+
return TerminalAction{Type: TerminalActionSpecialColor, OSC: action.OSC}, true
338341
case OSCActionProgress:
339342
return TerminalAction{Type: TerminalActionProgress, OSC: action.OSC}, true
340343
case OSCActionNotification:

internal/tui/terminal_parser_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,30 @@ func TestTerminalParserDispatchesMultiDynamicOSCColorActions(t *testing.T) {
417417
}
418418
}
419419

420+
func TestTerminalParserDispatchesSpecialOSCColorActions(t *testing.T) {
421+
parser := NewTerminalParser()
422+
input := "a" + OSCSequence(OSCSpecialColor, "0", "#112233", "4", "?") + "b" + OSCSequence(OSCResetSpecialColor, "4") + "c"
423+
actions := parser.Feed(input)
424+
if len(actions) != 5 {
425+
t.Fatalf("actions = %#v", actions)
426+
}
427+
if actions[1].Type != TerminalActionSpecialColor || !actions[1].OSC.SpecialColor.Valid || len(actions[1].OSC.SpecialColor.Entries) != 2 {
428+
t.Fatalf("special color action = %#v", actions[1])
429+
}
430+
if actions[1].OSC.SpecialColor.Entries[0].Index != 0 || actions[1].OSC.SpecialColor.Entries[0].Color == nil || *actions[1].OSC.SpecialColor.Entries[0].Color != (RGBColor{R: 17, G: 34, B: 51}) {
431+
t.Fatalf("special color set = %#v", actions[1].OSC.SpecialColor.Entries[0])
432+
}
433+
if actions[1].OSC.SpecialColor.Entries[1].Index != 4 || !actions[1].OSC.SpecialColor.Entries[1].Query {
434+
t.Fatalf("special color query = %#v", actions[1].OSC.SpecialColor.Entries[1])
435+
}
436+
if actions[3].Type != TerminalActionSpecialColor || !actions[3].OSC.SpecialColor.Valid || len(actions[3].OSC.SpecialColor.Entries) != 1 || !actions[3].OSC.SpecialColor.Entries[0].Reset || actions[3].OSC.SpecialColor.Entries[0].Index != 4 {
437+
t.Fatalf("special color reset = %#v", actions[3])
438+
}
439+
if got := TerminalVisibleText(input); got != "abc" {
440+
t.Fatalf("visible = %q", got)
441+
}
442+
}
443+
420444
func TestTerminalParserResetClearsStyle(t *testing.T) {
421445
parser := NewTerminalParser()
422446
actions := parser.Feed(CSISequence(1, "m") + "bold" + ESCResetSequence + "plain")

internal/tui/tui_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4636,6 +4636,28 @@ func TestParseOSCContent(t *testing.T) {
46364636
if paletteReset.Type != OSCActionPalette || !paletteReset.Palette.Valid || len(paletteReset.Palette.Entries) != 2 || !paletteReset.Palette.Entries[0].Reset || paletteReset.Palette.Entries[1].Index != 2 {
46374637
t.Fatalf("palette reset = %#v", paletteReset)
46384638
}
4639+
specialColor := ParseOSCContent("5;0;#112233;4;?")
4640+
if specialColor.Type != OSCActionSpecialColor || !specialColor.SpecialColor.Valid || len(specialColor.SpecialColor.Entries) != 2 {
4641+
t.Fatalf("special color = %#v", specialColor)
4642+
}
4643+
if specialColor.SpecialColor.Entries[0].Index != 0 || specialColor.SpecialColor.Entries[0].Color == nil || *specialColor.SpecialColor.Entries[0].Color != (RGBColor{R: 17, G: 34, B: 51}) {
4644+
t.Fatalf("special color set = %#v", specialColor.SpecialColor.Entries[0])
4645+
}
4646+
if specialColor.SpecialColor.Entries[1].Index != 4 || !specialColor.SpecialColor.Entries[1].Query || specialColor.SpecialColor.Entries[1].Color != nil {
4647+
t.Fatalf("special color query = %#v", specialColor.SpecialColor.Entries[1])
4648+
}
4649+
specialReset := ParseOSCContent("105;1;4")
4650+
if specialReset.Type != OSCActionSpecialColor || !specialReset.SpecialColor.Valid || len(specialReset.SpecialColor.Entries) != 2 || !specialReset.SpecialColor.Entries[0].Reset || specialReset.SpecialColor.Entries[1].Index != 4 {
4651+
t.Fatalf("special color reset = %#v", specialReset)
4652+
}
4653+
specialResetAll := ParseOSCContent("105")
4654+
if specialResetAll.Type != OSCActionSpecialColor || !specialResetAll.SpecialColor.Valid || !specialResetAll.SpecialColor.ResetAll || len(specialResetAll.SpecialColor.Entries) != 0 {
4655+
t.Fatalf("special color reset all = %#v", specialResetAll)
4656+
}
4657+
invalidSpecialColor := ParseOSCContent("5;5;#112233")
4658+
if invalidSpecialColor.Type != OSCActionSpecialColor || invalidSpecialColor.SpecialColor.Valid {
4659+
t.Fatalf("invalid special color = %#v", invalidSpecialColor)
4660+
}
46394661

46404662
progress := ParseOSCContent("9;4;1;101")
46414663
if progress.Type != OSCActionProgress || progress.Progress.State != TerminalProgressRunning || progress.Progress.Percent != 100 {

0 commit comments

Comments
 (0)