diff --git a/frontends/rioterm/src/application.rs b/frontends/rioterm/src/application.rs index 51b420d6..6cfaf25e 100644 --- a/frontends/rioterm/src/application.rs +++ b/frontends/rioterm/src/application.rs @@ -603,20 +603,28 @@ impl ApplicationHandler for Application<'_> { drop(terminal); } } - RioEventType::Rio(RioEvent::ClipboardLoad(clipboard_type, format)) => { + RioEventType::Rio(RioEvent::ClipboardLoad( + route_id, + clipboard_type, + format, + )) => { let Router { routes, clipboard, .. } = &mut self.router; if let Some(route) = routes.get_mut(&window_id) { if route.window.is_focused { let text = format(clipboard.get(clipboard_type).as_str()); - route + // Route the paste back to the panel that asked for it + // (OSC 52 reply), not whichever panel happens to be + // focused now. + if let Some(item) = route .window .screen - .ctx_mut() - .current_mut() - .messenger - .send_bytes(text.into_bytes()); + .context_manager + .get_by_route_id(route_id) + { + item.val.messenger.send_bytes(text.into_bytes()); + } } } } @@ -630,41 +638,55 @@ impl ApplicationHandler for Application<'_> { } } } - RioEventType::Rio(RioEvent::PtyWrite(text)) => { + RioEventType::Rio(RioEvent::PtyWrite(route_id, text)) => { if let Some(route) = self.router.routes.get_mut(&window_id) { - route + // Route reply bytes (CSI / OSC responses) back to the + // PTY of the panel that emitted them, not whichever + // panel happens to be focused. + if let Some(item) = route .window .screen - .ctx_mut() - .current_mut() - .messenger - .send_bytes(text.into_bytes()); + .context_manager + .get_by_route_id(route_id) + { + item.val.messenger.send_bytes(text.into_bytes()); + } } } - RioEventType::Rio(RioEvent::TextAreaSizeRequest(format)) => { + RioEventType::Rio(RioEvent::TextAreaSizeRequest(route_id, format)) => { if let Some(route) = self.router.routes.get_mut(&window_id) { - let dimension = - route.window.screen.context_manager.current().dimension; - let text = - format(crate::renderer::utils::terminal_dimensions(&dimension)); - route + if let Some(item) = route .window .screen - .ctx_mut() - .current_mut() - .messenger - .send_bytes(text.into_bytes()); + .context_manager + .get_by_route_id(route_id) + { + let dimension = item.val.dimension; + let text = format(crate::renderer::utils::terminal_dimensions( + &dimension, + )); + item.val.messenger.send_bytes(text.into_bytes()); + } } } - RioEventType::Rio(RioEvent::ColorRequest(index, format)) => { + RioEventType::Rio(RioEvent::ColorRequest(route_id, index, format)) => { if let Some(route) = self.router.routes.get_mut(&window_id) { - let terminal = route + // Read the originating panel's terminal colors and + // route the reply back to that same panel — color + // theme overrides via OSC 4 / OSC 10-19 are + // per-context, so reading from `current()` would + // mis-report when the user has focused a different + // split mid-flight. + let renderer_color = route.window.screen.renderer.colors[index]; + let Some(item) = route .window .screen .context_manager - .current() - .terminal - .lock(); + .get_by_route_id(route_id) + else { + return; + }; + let terminal = item.val.terminal.lock(); let color: ColorRgb = match terminal.colors()[index] { Some(color) => ColorRgb::from_color_arr(color), // Ignore cursor color requests unless it was changed. @@ -673,20 +695,11 @@ impl ApplicationHandler for Application<'_> { { return } - None => ColorRgb::from_color_arr( - route.window.screen.renderer.colors[index], - ), + None => ColorRgb::from_color_arr(renderer_color), }; - drop(terminal); - route - .window - .screen - .ctx_mut() - .current_mut() - .messenger - .send_bytes(format(color).into_bytes()); + item.val.messenger.send_bytes(format(color).into_bytes()); } } RioEventType::Rio(RioEvent::CreateWindow) => { diff --git a/rio-backend/src/crosswords/mod.rs b/rio-backend/src/crosswords/mod.rs index 849ab055..0642d5a4 100644 --- a/rio-backend/src/crosswords/mod.rs +++ b/rio-backend/src/crosswords/mod.rs @@ -1881,7 +1881,10 @@ impl Handler for Crosswords { }; self.event_proxy.send_event( - RioEvent::PtyWrite(format!("\x1b[{};{}$y", mode.raw(), state as u8,)), + RioEvent::PtyWrite( + self.route_id, + format!("\x1b[{};{}$y", mode.raw(), state as u8,), + ), self.window_id, ); } @@ -2089,7 +2092,10 @@ impl Handler for Crosswords { }; self.event_proxy.send_event( - RioEvent::PtyWrite(format!("\x1b[?{};{}$y", mode.raw(), state as u8,)), + RioEvent::PtyWrite( + self.route_id, + format!("\x1b[?{};{}$y", mode.raw(), state as u8,), + ), self.window_id, ); } @@ -2104,6 +2110,7 @@ impl Handler for Crosswords { let terminator = terminator.to_owned(); self.event_proxy.send_event( RioEvent::ColorRequest( + self.route_id, index, Arc::new(move |color| { format!( @@ -2680,14 +2687,14 @@ impl Handler for Crosswords { trace!("Reporting primary device attributes"); let text = String::from("\x1b[?62;4;6;22c"); self.event_proxy - .send_event(RioEvent::PtyWrite(text), self.window_id); + .send_event(RioEvent::PtyWrite(self.route_id, text), self.window_id); } Some('>') => { trace!("Reporting secondary device attributes"); let version = version_number(env!("CARGO_PKG_VERSION")); let text = format!("\x1b[>0;{version};1c"); self.event_proxy - .send_event(RioEvent::PtyWrite(text), self.window_id); + .send_event(RioEvent::PtyWrite(self.route_id, text), self.window_id); } _ => debug!("Unsupported device attributes intermediate"), } @@ -2699,7 +2706,7 @@ impl Handler for Crosswords { let version = env!("CARGO_PKG_VERSION"); let text = format!("\x1bP>|Rio {version}\x1b\\"); self.event_proxy - .send_event(RioEvent::PtyWrite(text), self.window_id); + .send_event(RioEvent::PtyWrite(self.route_id, text), self.window_id); } #[inline] @@ -2707,7 +2714,7 @@ impl Handler for Crosswords { let current_mode = self.keyboard_mode_stack[self.keyboard_mode_idx]; let text = format!("\x1b[?{current_mode}u"); self.event_proxy - .send_event(RioEvent::PtyWrite(text), self.window_id); + .send_event(RioEvent::PtyWrite(self.route_id, text), self.window_id); } #[inline] @@ -2763,13 +2770,13 @@ impl Handler for Crosswords { 5 => { let text = String::from("\x1b[0n"); self.event_proxy - .send_event(RioEvent::PtyWrite(text), self.window_id); + .send_event(RioEvent::PtyWrite(self.route_id, text), self.window_id); } 6 => { let pos = self.grid.cursor.pos; let text = format!("\x1b[{};{}R", pos.row + 1, pos.col + 1); self.event_proxy - .send_event(RioEvent::PtyWrite(text), self.window_id); + .send_event(RioEvent::PtyWrite(self.route_id, text), self.window_id); } _ => debug!("unknown device status query: {}", arg), }; @@ -3022,6 +3029,7 @@ impl Handler for Crosswords { self.event_proxy.send_event( RioEvent::ClipboardLoad( + self.route_id, clipboard_type, Arc::new(move |text| { let base64 = general_purpose::STANDARD.encode(text); @@ -3169,11 +3177,14 @@ impl Handler for Crosswords { fn text_area_size_pixels(&mut self) { debug!("text_area_size_pixels"); self.event_proxy.send_event( - RioEvent::TextAreaSizeRequest(Arc::new(move |window_size| { - let height = window_size.height; - let width = window_size.width; - format!("\x1b[4;{height};{width}t") - })), + RioEvent::TextAreaSizeRequest( + self.route_id, + Arc::new(move |window_size| { + let height = window_size.height; + let width = window_size.width; + format!("\x1b[4;{height};{width}t") + }), + ), self.window_id, ); } @@ -3187,7 +3198,7 @@ impl Handler for Crosswords { ); debug!("cells_size_pixels {:?}", text); self.event_proxy - .send_event(RioEvent::PtyWrite(text), self.window_id); + .send_event(RioEvent::PtyWrite(self.route_id, text), self.window_id); } #[inline] @@ -3199,7 +3210,7 @@ impl Handler for Crosswords { ); debug!("text_area_size_chars {:?}", text); self.event_proxy - .send_event(RioEvent::PtyWrite(text), self.window_id); + .send_event(RioEvent::PtyWrite(self.route_id, text), self.window_id); } #[inline] @@ -3259,23 +3270,26 @@ impl Handler for Crosswords { match pa { 1 => { self.event_proxy.send_event( - RioEvent::TextAreaSizeRequest(Arc::new(move |window_size| { - let width = window_size.width; - let height = window_size.height; - let graphic_dimensions = [ - std::cmp::min( - width as usize, - MAX_GRAPHIC_DIMENSIONS[0], - ), - std::cmp::min( - height as usize, - MAX_GRAPHIC_DIMENSIONS[1], - ), - ]; - - let (ps, pv) = (0, &graphic_dimensions[..]); - generate_response(pi, ps, pv) - })), + RioEvent::TextAreaSizeRequest( + self.route_id, + Arc::new(move |window_size| { + let width = window_size.width; + let height = window_size.height; + let graphic_dimensions = [ + std::cmp::min( + width as usize, + MAX_GRAPHIC_DIMENSIONS[0], + ), + std::cmp::min( + height as usize, + MAX_GRAPHIC_DIMENSIONS[1], + ), + ]; + + let (ps, pv) = (0, &graphic_dimensions[..]); + generate_response(pi, ps, pv) + }), + ), self.window_id, ); return; @@ -3297,7 +3311,7 @@ impl Handler for Crosswords { }; self.event_proxy.send_event( - RioEvent::PtyWrite(generate_response(pi, ps, pv)), + RioEvent::PtyWrite(self.route_id, generate_response(pi, ps, pv)), self.window_id, ); } @@ -3894,13 +3908,13 @@ impl Handler for Crosswords { fn kitty_graphics_response(&mut self, response: String) { // Send response back to the terminal self.event_proxy - .send_event(RioEvent::PtyWrite(response), self.window_id); + .send_event(RioEvent::PtyWrite(self.route_id, response), self.window_id); } #[inline] fn xtgettcap_response(&mut self, response: String) { self.event_proxy - .send_event(RioEvent::PtyWrite(response), self.window_id); + .send_event(RioEvent::PtyWrite(self.route_id, response), self.window_id); } #[inline] @@ -5531,7 +5545,7 @@ mod tests { // Verify the event is PtyWrite with the correct format match &captured_events[0] { - RioEvent::PtyWrite(text) => { + RioEvent::PtyWrite(_route_id, text) => { // Expected format: DCS > | Rio {version} ST // DCS = \x1bP, ST = \x1b\\ assert!( diff --git a/rio-backend/src/event/mod.rs b/rio-backend/src/event/mod.rs index d87d2f77..6874fea5 100644 --- a/rio-backend/src/event/mod.rs +++ b/rio-backend/src/event/mod.rs @@ -115,27 +115,40 @@ pub enum RioEvent { /// Request to write the contents of the clipboard to the PTY. /// - /// The attached function is a formatter which will correctly transform the clipboard content - /// into the expected escape sequence format. + /// `route_id` identifies the panel that emitted the request so + /// the bytes land on the originating PTY rather than whichever + /// panel happens to be focused. The attached function is a + /// formatter which transforms the clipboard content into the + /// expected escape-sequence form. ClipboardLoad( + usize, ClipboardType, Arc String + Sync + Send + 'static>, ), /// Request to write the RGB value of a color to the PTY. /// - /// The attached function is a formatter which will correctly transform the RGB color into the - /// expected escape sequence format. + /// `route_id` identifies the panel that emitted the request so + /// the reply lands on the originating PTY. The attached function + /// is a formatter which transforms the RGB color into the + /// expected escape-sequence form. ColorRequest( + usize, usize, Arc String + Sync + Send + 'static>, ), - /// Write some text to the PTY. - PtyWrite(String), + /// Write some text to the PTY identified by `route_id`. Routing + /// by panel (rather than the focused context) is required so + /// CSI / OSC reply bytes land on the shell that asked for them + /// even if the user focuses a different split mid-flight. + PtyWrite(usize, String), - /// Request to write the text area size. - TextAreaSizeRequest(Arc String + Sync + Send + 'static>), + /// Request to write the text area size to the PTY of `route_id`. + TextAreaSizeRequest( + usize, + Arc String + Sync + Send + 'static>, + ), /// Cursor blinking state has changed. CursorBlinkingChange, @@ -187,10 +200,18 @@ impl Debug for RioEvent { RioEvent::ClipboardStore(ty, text) => { write!(f, "ClipboardStore({ty:?}, {text})") } - RioEvent::ClipboardLoad(ty, _) => write!(f, "ClipboardLoad({ty:?})"), - RioEvent::TextAreaSizeRequest(_) => write!(f, "TextAreaSizeRequest"), - RioEvent::ColorRequest(index, _) => write!(f, "ColorRequest({index})"), - RioEvent::PtyWrite(text) => write!(f, "PtyWrite({text})"), + RioEvent::ClipboardLoad(route_id, ty, _) => { + write!(f, "ClipboardLoad(route={route_id}, {ty:?})") + } + RioEvent::TextAreaSizeRequest(route_id, _) => { + write!(f, "TextAreaSizeRequest(route={route_id})") + } + RioEvent::ColorRequest(route_id, index, _) => { + write!(f, "ColorRequest(route={route_id}, idx={index})") + } + RioEvent::PtyWrite(route_id, text) => { + write!(f, "PtyWrite(route={route_id}, {text})") + } RioEvent::Title(title) => write!(f, "Title({title})"), RioEvent::TitleWithSubtitle(title, subtitle) => { write!(f, "TitleWithSubtitle({title}, {subtitle})")