From cce5a96bb0318c34459f0704b2d5372d7ff56912 Mon Sep 17 00:00:00 2001 From: "Brandon W. King" <70168+kingb@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:55:57 -0700 Subject: [PATCH] fix(session): let OSC 52 reads reach the listener, so they get a reply alacritty_terminal has its own OSC 52 gate ahead of the event listener, and its default (Osc52::OnlyCopy) drops read requests inside the engine. Ember built its Term with Config::default() and never set it, so since the gated clipboard read shipped in 0.5.0 the listener's ClipboardLoad arm never ran. A read got no reply at all: osc52_read = true had no effect, and with it off the documented empty reply never went out, leaving a program that waits on its query to hang. The engine now accepts both copy and paste (Osc52::CopyPaste), and the listener stays the single gate on clipboard contents: the real contents only with osc52_read = true, an empty reply otherwise (the default). Nothing new is exposed. The existing test missed this because it handed a synthetic ClipboardLoad straight to the listener, bypassing the engine gate. The new test sends the real escape bytes through the engine; it failed with an empty outbox before this change and passes after. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014xVjnf8UuQPRQtv2QqgpPK --- CHANGELOG.md | 10 ++++++++++ crates/ember-session/src/local_pty.rs | 23 +++++++++++++++++++++++ crates/ember-session/src/projection.rs | 13 +++++++++++-- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e7ed9f..7816528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ follow [Semantic Versioning](https://semver.org). ## [Unreleased] +### Fixed + +- OSC 52 clipboard reads now work as documented. Since 0.5.0, a program + asking for the clipboard's contents got no reply at all: the request was + dropped before Ember's own setting was consulted, so `osc52_read = true` + had no effect, and with it off a program waiting for an answer could hang. + Ember now answers every read: with the clipboard's contents when + `osc52_read = true`, and with an empty reply otherwise (still the default), + so nothing is shared unless you opt in. + ## [0.6.1] - 2026-09-25 ### Fixed diff --git a/crates/ember-session/src/local_pty.rs b/crates/ember-session/src/local_pty.rs index e05533f..4fc162f 100644 --- a/crates/ember-session/src/local_pty.rs +++ b/crates/ember-session/src/local_pty.rs @@ -522,6 +522,29 @@ mod tests { ); } + /// The same promise, through the real path: the read's escape bytes go into + /// the engine, not a synthetic event into the listener. The engine has its + /// own OSC 52 gate ahead of the listener, and its default drops reads, which + /// left the pane silent while the listener-only test above kept passing. + #[test] + fn osc52_read_through_the_engine_replies_empty() { + let (tx, _rx) = mpsc::channel(); + let outbox = Arc::new(Mutex::new(Vec::new())); + let listener = EmberListener { + events: tx, + outbox: Arc::clone(&outbox), + osc52_read: false, + palette: crate::palette::Palette::dark(), + }; + let mut proj = AlacrittyProjection::new(GridDims::new(80, 24), listener); + proj.advance(b"\x1b]52;c;?\x07"); + let got = String::from_utf8(outbox.lock().unwrap().clone()).unwrap(); + assert_eq!( + got, "\x1b]52;c;\x07", + "an OSC 52 read must reach the listener and get the empty reply, not silence" + ); + } + /// Reconstruct row 0's text from the frame lane until `needle` appears or we /// time out. Proves the full path: shell → PTY → engine → projection → lane. #[test] diff --git a/crates/ember-session/src/projection.rs b/crates/ember-session/src/projection.rs index 789dc3b..c0e18e3 100644 --- a/crates/ember-session/src/projection.rs +++ b/crates/ember-session/src/projection.rs @@ -12,7 +12,7 @@ use alacritty_terminal::index::{Column, Direction, Line, Point, Side}; use alacritty_terminal::term::cell::{Cell, Flags}; use alacritty_terminal::term::search::RegexSearch; use alacritty_terminal::term::test::TermSize; -use alacritty_terminal::term::{Config, Term, TermDamage, TermMode}; +use alacritty_terminal::term::{Config, Osc52, Term, TermDamage, TermMode}; use alacritty_terminal::vte::ansi::{CursorShape as AlacCursorShape, Processor}; use ember_core::{ Attrs, CellContent, CellPatch, CursorShape, CursorState, GridDelta, GridDims, MarkStatus, @@ -129,7 +129,16 @@ enum Scanned { impl AlacrittyProjection { pub fn new(dims: GridDims, listener: L) -> Self { let size = TermSize::new(dims.columns as usize, dims.screen_lines as usize); - let term = Term::new(Config::default(), &size, listener); + // The engine has its own OSC 52 gate ahead of the listener, and its + // default (`OnlyCopy`) drops reads there, so a read would get no reply + // at all. Let both through: the listener is the one gate on clipboard + // contents (`osc52_read`, off by default = an empty reply), which is + // what keeps a well-behaved program from hanging on its query. + let config = Config { + osc52: Osc52::CopyPaste, + ..Config::default() + }; + let term = Term::new(config, &size, listener); Self { term, parser: Processor::new(),