Skip to content

Commit 6b56d7e

Browse files
feat(tui): add clipboard copy keybinding via OSC 52 (#433)
Press 'c' on Recent Observations, Search Results, Observation Detail, and Session Detail screens to copy the selected observation content to the system clipboard using the OSC 52 terminal escape sequence. Visual feedback ("✓ Copied!") is shown for 2 seconds after each copy. No new dependencies added — uses the existing go-osc52/v2 transitive dep. Closes #141
1 parent 43cc242 commit 6b56d7e

6 files changed

Lines changed: 468 additions & 4 deletions

File tree

internal/tui/clipboard.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package tui
2+
3+
import (
4+
"encoding/base64"
5+
"fmt"
6+
"time"
7+
8+
tea "github.com/charmbracelet/bubbletea"
9+
)
10+
11+
// ─── Messages ─────────────────────────────────────────────────────────────────
12+
13+
// clipboardCopiedMsg is sent after building the OSC 52 sequence to copy.
14+
// The sequence field carries the raw escape sequence so Update can emit it.
15+
type clipboardCopiedMsg struct {
16+
sequence string
17+
}
18+
19+
// clipboardClearMsg is sent after the 2-second feedback timer expires.
20+
type clipboardClearMsg struct{}
21+
22+
// ─── OSC 52 helpers ──────────────────────────────────────────────────────────
23+
24+
// osc52Sequence returns the OSC 52 terminal escape sequence that asks the
25+
// terminal to write content to the system clipboard.
26+
//
27+
// Format: ESC ] 52 ; c ; <base64> BEL
28+
func osc52Sequence(content string) string {
29+
b64 := base64.StdEncoding.EncodeToString([]byte(content))
30+
return fmt.Sprintf("\x1b]52;c;%s\x07", b64)
31+
}
32+
33+
// copyToClipboard returns a Cmd that builds the OSC 52 sequence for content
34+
// and wraps it in a clipboardCopiedMsg.
35+
func copyToClipboard(content string) tea.Cmd {
36+
return func() tea.Msg {
37+
return clipboardCopiedMsg{sequence: osc52Sequence(content)}
38+
}
39+
}
40+
41+
// clearFeedbackAfter returns a Cmd that sends clipboardClearMsg after d.
42+
func clearFeedbackAfter(d time.Duration) tea.Cmd {
43+
return tea.Tick(d, func(_ time.Time) tea.Msg {
44+
return clipboardClearMsg{}
45+
})
46+
}

0 commit comments

Comments
 (0)