feat: copy the output panel to the system clipboard (#11)

New app-level `copy` / `copy all` / `copy last` command (ADR-0041).
Delivery is OSC 52 *and* a best-effort native write (arboard), always
both — OSC 52 acceptance is undetectable, so a true fallback can't be
built. Payload is the panel's plain text exactly as rendered (tags,
✓/✗, box-drawing), drift-locked to render_output_line. arboard added
--no-default-features (X11-only; OSC 52 covers Wayland).

Amends ADR-0003's command registry; requirements V6.
This commit is contained in:
claude@clouddev1
2026-06-02 14:23:21 +00:00
parent 1ea376be26
commit d0c8f9d5d2
25 changed files with 1203 additions and 13 deletions
+20
View File
@@ -376,6 +376,12 @@ async fn run_loop(
// no wake-ups. See `IndicatorDebounce` for the decision
// logic; `app.input_indicator` mirrors it for the renderer.
let mut debounce = IndicatorDebounce::default();
// Long-lived native clipboard for the `copy` command (ADR-0041).
// Created lazily on first copy (so an OSC-52-only session never
// opens an X11 connection) and kept alive for the session — the
// X11 backend serves the selection from a thread owned by this
// handle, so it must outlive each write.
let mut native_clipboard = crate::clipboard::SystemClipboard::new();
loop {
let event = if debounce.is_armed() {
match tokio::time::timeout(INDICATOR_DEBOUNCE, event_rx.recv()).await {
@@ -557,6 +563,20 @@ async fn run_loop(
tracing::warn!(error = %e, "could not persist input mode");
}
}
Action::CopyToClipboard(text) => {
// OSC 52 to the terminal + a best-effort native
// write (ADR-0041). Both always fire; an OSC 52
// write error is logged but never fatal.
let mut out = std::io::stdout();
if let Err(e) = crate::clipboard::deliver(
&mut out,
&mut native_clipboard,
&text,
crate::clipboard::in_tmux(),
) {
tracing::warn!(error = %e, "could not emit clipboard OSC 52 escape");
}
}
}
}
// A keystroke hides the indicator and re-arms the