From e1a84a8e340f09b9479ca2df333ba135a45d2048 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Fri, 15 May 2026 21:17:55 +0300 Subject: [PATCH] fix: force redraw on terminal focus gain --- src/app/mod.rs | 10 ++++++ src/app/runtime.rs | 4 ++- src/client/mod.rs | 10 ++++++ src/raw_input.rs | 15 +++++++++ src/server/headless.rs | 69 +++++++++++++++++++++++++++++++++++++----- 5 files changed, 99 insertions(+), 9 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 7c3bae76..a332503f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -80,6 +80,7 @@ pub struct App { HashSet<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>, pub render_notify: Arc, pub render_dirty: Arc, + pub(crate) full_redraw_pending: bool, pub(crate) overlay_panes: HashMap, pub(crate) local_terminal_notifications: bool, } @@ -441,11 +442,16 @@ impl App { last_terminal_size: terminal::size().ok(), render_notify, render_dirty, + full_redraw_pending: false, overlay_panes: HashMap::new(), local_terminal_notifications: true, } } + fn request_full_redraw(&mut self) { + self.full_redraw_pending = true; + } + pub async fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { if self.input_rx.is_none() { self.input_rx = Some(crate::raw_input::spawn_input_reader()); @@ -507,6 +513,10 @@ impl App { if needs_render && self.can_render_now(now) { self.render_dirty.swap(false, Ordering::AcqRel); let _sync_output = SyncOutputGuard::begin()?; + if self.full_redraw_pending { + terminal.clear()?; + self.full_redraw_pending = false; + } let kitty_graphics_enabled = self.state.kitty_graphics_enabled; let mut cell_size = crate::kitty_graphics::HostCellSize::default(); terminal.draw(|frame| { diff --git a/src/app/runtime.rs b/src/app/runtime.rs index 6cdb6b43..4d2f6c47 100644 --- a/src/app/runtime.rs +++ b/src/app/runtime.rs @@ -95,8 +95,10 @@ impl App { true } crate::raw_input::RawInputEvent::OuterFocusGained => { + self.request_full_redraw(); self.state.outer_terminal_focus = Some(true); - self.state.mark_active_tab_seen() + self.state.mark_active_tab_seen(); + true } crate::raw_input::RawInputEvent::OuterFocusLost => { self.state.outer_terminal_focus = Some(false); diff --git a/src/client/mod.rs b/src/client/mod.rs index 1758ffdc..5d52f8b3 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -56,6 +56,12 @@ struct ClientState { kitty_graphics_enabled: bool, } +impl ClientState { + fn request_full_redraw(&mut self) { + self.blit_encoder = blit::BlitEncoder::new(); + } +} + // --------------------------------------------------------------------------- // Error types // --------------------------------------------------------------------------- @@ -497,6 +503,10 @@ async fn run_client_loop( match event { ClientLoopEvent::StdinInput(data) => { + let events = crate::raw_input::parse_raw_input_bytes_sync(&data); + if crate::raw_input::events_require_host_surface_redraw(&events) { + state.request_full_redraw(); + } let msg = ClientMessage::Input { data }; if let Err(e) = write_to_server(&mut write_stream, &msg) { return Err(ClientError::ConnectionLost(e)); diff --git a/src/raw_input.rs b/src/raw_input.rs index 3ebd9554..28a2e561 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -128,6 +128,12 @@ pub enum RawInputEvent { Unsupported, } +pub(crate) fn events_require_host_surface_redraw(events: &[RawInputEvent]) -> bool { + events + .iter() + .any(|event| matches!(event, RawInputEvent::OuterFocusGained)) +} + pub fn spawn_input_reader() -> mpsc::Receiver { let (tx, rx) = mpsc::channel(256); @@ -648,6 +654,15 @@ mod tests { assert!(matches!(event, RawInputEvent::OuterFocusLost)); } + #[test] + fn outer_focus_gained_requests_host_surface_redraw() { + let events = parse_raw_input_bytes_sync(b"\x1b[I"); + assert!(events_require_host_surface_redraw(&events)); + + let events = parse_raw_input_bytes_sync(b"\x1b[O"); + assert!(!events_require_host_surface_redraw(&events)); + } + #[test] fn parses_xterm_alt_up_arrow() { let (RawInputEvent::Key(key), consumed) = extract_one_event(b"\x1b[1;3A").unwrap() else { diff --git a/src/server/headless.rs b/src/server/headless.rs index 9bb3181f..29c8de6e 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -244,6 +244,14 @@ impl ClientConnection { writer, } } + + fn request_full_redraw(&mut self) { + self.render_state.reset_baseline(); + } + + fn request_semantic_redraw_after_input(&mut self) { + self.render_state.reset_semantic_input_baseline(); + } } // --------------------------------------------------------------------------- @@ -529,7 +537,7 @@ impl HeadlessServer { // rendering semantics. Force one fresh frame to every remaining client // even if the next rendered buffer compares equal to its cached frame. for client in self.clients.values_mut() { - client.render_state.reset_baseline(); + client.request_full_redraw(); } } @@ -1154,14 +1162,20 @@ impl HeadlessServer { } ServerEvent::ClientInput { client_id, data } => { debug!(client_id, len = data.len(), "client input received"); - if let Some(client) = self.clients.get_mut(&client_id) { - // Ensure semantic clients receive one post-input frame even if the - // semantic buffer compares equal. Terminal-ANSI clients must keep their - // server-side blit baseline; resetting it here forces a full redraw on - // every keypress and makes remote sessions feel extremely slow. - client.render_state.reset_semantic_input_baseline(); - } let events = crate::raw_input::parse_raw_input_bytes_sync(&data); + let host_surface_redraw = + crate::raw_input::events_require_host_surface_redraw(&events); + if let Some(client) = self.clients.get_mut(&client_id) { + if host_surface_redraw { + client.request_full_redraw(); + } else { + // Ensure semantic clients receive one post-input frame even if the + // semantic buffer compares equal. Terminal-ANSI clients must keep their + // server-side blit baseline; resetting it here forces a full redraw on + // every keypress and makes remote sessions feel extremely slow. + client.request_semantic_redraw_after_input(); + } + } self.update_client_outer_focus_from_events(client_id, &events); let interaction = Self::events_include_interaction(&events); let foreground_changed = if interaction { @@ -2619,6 +2633,45 @@ mod tests { assert!(client_rx.recv_timeout(Duration::from_millis(50)).is_err()); } + #[test] + fn outer_focus_gained_forces_terminal_ansi_full_redraw() { + let mut server = test_headless_server(); + let (client_tx, _client_control_rx, client_rx) = test_client_writer(); + + server.clients.insert( + 1, + ClientConnection::new( + (80, 24), + crate::kitty_graphics::HostCellSize::default(), + crate::terminal_theme::TerminalTheme::default(), + None, + 1, + RenderEncoding::TerminalAnsi, + Some(client_tx), + ), + ); + server.foreground_client_id = Some(1); + + server.render_and_stream(); + let _ = client_rx + .recv_timeout(Duration::from_millis(100)) + .expect("initial terminal frame"); + + assert!(server.handle_server_event(ServerEvent::ClientInput { + client_id: 1, + data: b"\x1b[I".to_vec(), + })); + server.render_and_stream(); + + match read_server_message(client_rx.recv_timeout(Duration::from_millis(100)).unwrap()) { + ServerMessage::Terminal(frame) => { + assert_eq!(frame.seq, 2); + assert!(frame.full); + } + other => panic!("expected terminal frame, got {other:?}"), + } + } + #[test] fn full_render_queue_does_not_advance_terminal_ansi_baseline() { let mut server = test_headless_server();