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(),