feat(tui): add clipboard copy via OSC 52 on observation screens#142
feat(tui): add clipboard copy via OSC 52 on observation screens#142Snakeblack wants to merge 1 commit intoGentleman-Programming:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds OSC 52 clipboard-copy support to the Engram Bubbletea TUI observation-related screens, with short-lived visual feedback after a copy attempt.
Changes:
- Introduces a clipboard copy command (
copyToClipboard) and feedback state/messages on the TUI model. - Handles clipboard-result + auto-clear feedback messages in
Update(), and addsckeybindings on 4 observation screens. - Updates help text and renders copy feedback in the main
View()output.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| internal/tui/model.go | Adds clipboard feedback fields/messages and an OSC 52 copy command. |
| internal/tui/update.go | Wires c key handling plus feedback and auto-clear tick handling into Update(). |
| internal/tui/view.go | Updates help text to include c copy and renders copy feedback. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func copyToClipboard(text string) tea.Cmd { | ||
| return func() tea.Msg { | ||
| _, err := osc52.New(text).WriteTo(os.Stderr) | ||
| return clipboardCopiedMsg{err: err} | ||
| } |
There was a problem hiding this comment.
copyToClipboard() writes OSC 52 sequences directly to os.Stderr from within a Bubbletea command. This bypasses Bubbletea’s configured output writer (the program uses stdout by default) and introduces hard-to-test, side-effecting I/O from a goroutine, which can also interleave with the renderer’s output. Consider emitting the OSC 52 sequence through the same output stream as the TUI (or making the writer injectable) so clipboard writes are routed consistently and can be tested deterministically.
| m.PrevScreen = ScreenSearchResults | ||
| return m, loadTimeline(m.store, obsID) | ||
| } | ||
| case "c": | ||
| if len(m.SearchResults) > 0 && m.Cursor < len(m.SearchResults) { | ||
| r := m.SearchResults[m.Cursor] | ||
| return m, copyToClipboard(r.Content) | ||
| } |
There was a problem hiding this comment.
New clipboard behavior isn’t covered by the existing TUI key-path tests. internal/tui/update_test.go has explicit assertions for other keys on these screens (e.g. recent enter/t/esc), but there are no tests ensuring that pressing c returns a clipboard command and that clipboardCopiedMsg updates CopyFeedback. Adding tests here would prevent regressions across the four screens that now support copy.
| fbStyle := lipgloss.NewStyle().Bold(true).Foreground(colorGreen) | ||
| if strings.HasPrefix(m.CopyFeedback, "✗") { | ||
| fbStyle = lipgloss.NewStyle().Bold(true).Foreground(colorRed) | ||
| } | ||
| content += "\n" + fbStyle.Render(m.CopyFeedback) | ||
| } | ||
|
|
||
| return appStyle.Render(content) | ||
| } | ||
|
|
There was a problem hiding this comment.
The copy feedback color is derived by parsing the rendered text (strings.HasPrefix(m.CopyFeedback, "✗")). This makes styling brittle (changing the message text breaks the color logic) and mixes presentation with state. Consider storing a small structured state (e.g., a bool/enum for success vs failure) alongside the message and selecting the style from that instead.
| fbStyle := lipgloss.NewStyle().Bold(true).Foreground(colorGreen) | |
| if strings.HasPrefix(m.CopyFeedback, "✗") { | |
| fbStyle = lipgloss.NewStyle().Bold(true).Foreground(colorRed) | |
| } | |
| content += "\n" + fbStyle.Render(m.CopyFeedback) | |
| } | |
| return appStyle.Render(content) | |
| } | |
| fbStyle := m.copyFeedbackStyle() | |
| content += "\n" + fbStyle.Render(m.CopyFeedback) | |
| } | |
| return appStyle.Render(content) | |
| } | |
| // copyFeedbackStyle determines the style for the current copy feedback message. | |
| // NOTE: This currently infers error vs. success from the message content, but | |
| // the logic is centralized here so it can later be switched to a dedicated | |
| // structured state (e.g., a bool/enum on the model) without touching View. | |
| func (m Model) copyFeedbackStyle() lipgloss.Style { | |
| // Default to success style. | |
| style := lipgloss.NewStyle().Bold(true).Foreground(colorGreen) | |
| // If the feedback indicates a failure, use the error style. | |
| if strings.HasPrefix(m.CopyFeedback, "✗") { | |
| style = lipgloss.NewStyle().Bold(true).Foreground(colorRed) | |
| } | |
| return style | |
| } |
Linked Issue
Closes #141
PR Type
Summary
ckeybinding to copy observation content to clipboard via OSC 52 escape sequencesChanges
internal/tui/model.goCopyFeedbackfield,clipboardCopiedMsg/copyFeedbackClearMsgmessages, andcopyToClipboard()command usinggo-osc52/v2internal/tui/update.goUpdate(); add"c"key handler inhandleRecentKeys,handleObservationDetailKeys,handleSearchResultsKeys,handleSessionDetailKeysinternal/tui/view.goc copy; render copy feedback overlay inView()using existing green/red color tokensDesign Decisions
atotto/clipboard: Works over SSH, no system dependencies (xclip/xsel/pbcopy), andgo-osc52/v2is already a transitive dependency from the Charm ecosystem — zero new deps addedtea.Tick: Standard Bubbletea pattern. 2-second auto-clear keeps the help area cleanTest Plan
go test ./internal/tui/...— 37 tests, all passinggo build ./...Ctrl+Vpastes the copied content correctlyc-less screensContributor Checklist
type/descriptionnaming