aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorOthavio Quiliao <othavioaqa@gmail.com>2026-04-10 07:48:13 -0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-10 14:32:07 +0300
commit6c62dba44e6679e37e1626f07c830c81469a0f57 (patch)
tree48a05d4c0f88a82c5383f4ddfbb4b8b467ba5366 /src/app
parent13fe950fe436982b5ea855e9c32ad471c618732b (diff)
fix: forward OSC 52 clipboard writes from pane children
herdr embeds `libghostty-vt` as a headless terminal emulator per pane, and its readonly stream handler silently drops `.clipboard_contents` actions. As a result, any child process that writes OSC 52 to copy to the system clipboard (for example Amp, which enables mouse reporting and handles selection internally) has its sequence swallowed before it can reach the host terminal — the user sees the child's "copied" toast but the system clipboard is never updated. This change adds an `Osc52Forwarder` that observes raw PTY bytes in `process_pty_bytes`, reconstructs completed `ESC ] 52 ; (c|) ; <base64> (BEL|ST)` sequences (write/clear only — queries are dropped since herdr has no path to reply to the child), and returns them through the existing `ProcessBytesResult`. The reader task forwards the bytes to the main loop via a new `AppEvent::ClipboardWrite`, which is written to stdout from `handle_internal_event`. Routing through the main loop keeps stdout writes serialized with `terminal.draw` and the other existing host-terminal emitters, so the new forwarder does not race with rendering. Existing herdr-native selection (mouse drag in panes without mouse reporting) is unchanged and still writes through `selection::write_osc52`.
Diffstat (limited to 'src/app')
-rw-r--r--src/app/actions.rs3
-rw-r--r--src/app/mod.rs7
2 files changed, 10 insertions, 0 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index da5c8698..fcdeff92 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -518,6 +518,9 @@ impl AppState {
.update_pane_state(pane_id, |pane| pane.release_agent(&source, agent))
.into_iter()
.collect(),
+ // Intercepted in App::handle_internal_event before reaching this
+ // dispatch; never touches AppState.
+ AppEvent::ClipboardWrite { .. } => Vec::new(),
}
}
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 3e79ce05..266baa59 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -784,6 +784,13 @@ impl App {
}
fn handle_internal_event(&mut self, ev: AppEvent) {
+ if let AppEvent::ClipboardWrite { bytes } = ev {
+ use std::io::Write;
+ let _ = std::io::stdout().write_all(&bytes);
+ let _ = std::io::stdout().flush();
+ return;
+ }
+
if let AppEvent::PaneDied { pane_id } = &ev {
if let Some((ws_idx, _)) = self.find_pane(*pane_id) {
if let Some(public_pane_id) = self.public_pane_id(ws_idx, *pane_id) {