Skip to content

Commit 84e7ae8

Browse files
author
SqlRush
committed
Support OSC clipboard selections
1 parent 018a794 commit 84e7ae8

5 files changed

Lines changed: 51 additions & 4 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
814814

815815
本轮补充:terminal OSC helper 增加 iTerm2、Kitty、Ghostty notification 序列和 raw BEL helper,调用方可按环境选择是否 wrap multiplexer。
816816

817-
本轮补充:terminal OSC helper 增加 OSC 52 clipboard 序列生成,固定 clipboard selection `c` 并按 UTF-8 base64 编码 payload;native clipboard/tmux buffer runtime 仍未接入。
817+
本轮补充:terminal OSC helper 增加 OSC 52 clipboard 序列生成,支持默认 `c` selection、显式 clipboard selection 和 clear 序列,并按 UTF-8 base64 编码 payload;native clipboard/tmux buffer runtime 仍未接入。
818818

819819
本轮补充:terminal OSC helper 增加显式 ST (`ESC \\`) terminator 入口,可按官方 Kitty 避免 BEL 的路径生成 OSC 序列,同时默认 `OSCSequence` 仍保持 BEL terminator。
820820

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ test/parity/ # golden tests against TS/official behavior
458458
- 本轮补充:terminal OSC helper 增加 OSC 8 hyperlink 开始/结束序列,按官方 rolling hash 为 URL 自动生成 `id=`,并允许显式 params 覆盖。
459459
- 本轮补充:terminal OSC helper 增加 OSC 9;4 progress 序列,覆盖 clear/set/error/indeterminate,running/error 百分比按官方规则 clamp 到 0..100。
460460
- 本轮补充:terminal OSC helper 增加 iTerm2、Kitty、Ghostty notification 序列和 raw BEL helper,调用方可按环境选择是否 wrap multiplexer。
461-
- 本轮补充:terminal OSC helper 增加 OSC 52 clipboard 序列生成,固定 clipboard selection `c` 并按 UTF-8 base64 编码 payload;native clipboard/tmux buffer runtime 仍未接入。
461+
- 本轮补充:terminal OSC helper 增加 OSC 52 clipboard 序列生成,支持默认 `c` selection、显式 clipboard selection 和 clear 序列,并按 UTF-8 base64 编码 payload;native clipboard/tmux buffer runtime 仍未接入。
462462
- 本轮补充:terminal OSC helper 增加显式 ST (`ESC \\`) terminator 入口,可按官方 Kitty 避免 BEL 的路径生成 OSC 序列,同时默认 `OSCSequence` 仍保持 BEL terminator。
463463
- 本轮补充:terminal OSC helper 增加 OSC color parser,支持 `#RRGGBB` 和 XParseColor 风格 `rgb:R/G/B`,按官方规则把 1-4 位 hex component 缩放到 8-bit RGB。
464464
- 本轮补充:terminal OSC parser 把 OSC 10-19 dynamic color 设置/查询解析为结构化 color action,复用既有 OSC color parser,并支持同一序列内按官方递增规则连续设置多个 target;visible text/snapshot 继续剥离这些控制串。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ M7 progress now includes:
261261
- `internal/tui`: terminal OSC helpers now generate OSC 8 hyperlink start/end sequences with official URL-derived id parameters and explicit param overrides.
262262
- `internal/tui`: terminal OSC helpers now generate OSC 9;4 progress clear/set/error/indeterminate sequences with official 0..100 percentage clamping.
263263
- `internal/tui`: terminal OSC helpers now generate iTerm2, Kitty, and Ghostty notification sequences plus raw BEL notifications for caller-managed emission.
264-
- `internal/tui`: terminal OSC helpers now generate OSC 52 clipboard sequences using clipboard selection `c` and UTF-8 base64 payloads; native clipboard and tmux buffer runtime remain future work.
264+
- `internal/tui`: terminal OSC helpers now generate OSC 52 clipboard sequences using UTF-8 base64 payloads, support explicit clipboard selections and clear sequences, and still leave native clipboard and tmux buffer runtime for future work.
265265
- `internal/tui`: terminal OSC helpers now support explicit ST (`ESC \\`) terminators for Kitty-style no-BEL OSC output while preserving BEL as the default.
266266
- `internal/tui`: terminal OSC helpers now parse `#RRGGBB` and XParseColor-style `rgb:R/G/B` colors, scaling 1-4 digit hex components to 8-bit RGB like the official parser.
267267
- `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.

internal/tui/terminal_osc.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,28 @@ func parseColonFields(value string) map[string]string {
853853
}
854854

855855
func TerminalClipboardSequence(text string) string {
856-
return OSCSequence(OSCClipboard, "c", base64.StdEncoding.EncodeToString([]byte(text)))
856+
return TerminalClipboardSelectionSequence("c", text)
857+
}
858+
859+
func TerminalClipboardSelectionSequence(selection string, text string) string {
860+
return OSCSequence(OSCClipboard, normalizeClipboardSelection(selection), base64.StdEncoding.EncodeToString([]byte(text)))
861+
}
862+
863+
func ClearTerminalClipboardSequence() string {
864+
return ClearTerminalClipboardSelectionSequence("c")
865+
}
866+
867+
func ClearTerminalClipboardSelectionSequence(selection string) string {
868+
return OSCSequence(OSCClipboard, normalizeClipboardSelection(selection), "")
869+
}
870+
871+
func normalizeClipboardSelection(selection string) string {
872+
selection = strings.TrimSpace(selection)
873+
selection = strings.ReplaceAll(selection, ";", "")
874+
if selection == "" {
875+
return "c"
876+
}
877+
return selection
857878
}
858879

859880
func TerminalProgressSequence(state TerminalProgressState, percentage int) string {

internal/tui/tui_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4865,6 +4865,32 @@ func TestTerminalClipboardSequence(t *testing.T) {
48654865
if seq != want {
48664866
t.Fatalf("clipboard = %q, want %q", seq, want)
48674867
}
4868+
4869+
primary := TerminalClipboardSelectionSequence("p", "primary")
4870+
wantPrimary := OSCPrefix + OSCClipboard + ";p;cHJpbWFyeQ==" + OSCTerminator
4871+
if primary != wantPrimary {
4872+
t.Fatalf("primary clipboard = %q, want %q", primary, wantPrimary)
4873+
}
4874+
parsed, ok := ParseOSCSequence(primary)
4875+
if !ok || parsed.Type != OSCActionClipboard || !parsed.Clipboard.Valid || parsed.Clipboard.Selection != "p" || parsed.Clipboard.Text != "primary" {
4876+
t.Fatalf("parsed primary clipboard = %#v ok=%v", parsed, ok)
4877+
}
4878+
4879+
clear := ClearTerminalClipboardSelectionSequence("p")
4880+
wantClear := OSCPrefix + OSCClipboard + ";p;" + OSCTerminator
4881+
if clear != wantClear {
4882+
t.Fatalf("clear clipboard = %q, want %q", clear, wantClear)
4883+
}
4884+
parsedClear, ok := ParseOSCSequence(clear)
4885+
if !ok || parsedClear.Type != OSCActionClipboard || !parsedClear.Clipboard.Valid || !parsedClear.Clipboard.Clear || parsedClear.Clipboard.Selection != "p" {
4886+
t.Fatalf("parsed clear clipboard = %#v ok=%v", parsedClear, ok)
4887+
}
4888+
4889+
defaultClear := ClearTerminalClipboardSelectionSequence("")
4890+
wantDefaultClear := OSCPrefix + OSCClipboard + ";c;" + OSCTerminator
4891+
if defaultClear != wantDefaultClear {
4892+
t.Fatalf("default clear clipboard = %q, want %q", defaultClear, wantDefaultClear)
4893+
}
48684894
}
48694895

48704896
func TestTerminalProgressSequence(t *testing.T) {

0 commit comments

Comments
 (0)