From 1780185a13b4a34b43b00dc5ba8087ffc9298d78 Mon Sep 17 00:00:00 2001 From: matthias schedel Date: Wed, 29 Jul 2026 13:27:30 +0200 Subject: [PATCH] fix: refresh host colors before focus returns --- src/client/input.rs | 60 ++++++++++++++++++++++++++++++++++++++------- src/client/mod.rs | 11 ++++++--- src/raw_input.rs | 14 ++++++++--- 3 files changed, 69 insertions(+), 16 deletions(-) diff --git a/src/client/input.rs b/src/client/input.rs index 28d38bda17..4e6ef8b692 100644 --- a/src/client/input.rs +++ b/src/client/input.rs @@ -10,7 +10,7 @@ //! - We avoid duplicating parsing logic in the client //! - Host terminal control replies can be buffered or discarded before they leak -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; #[cfg(unix)] @@ -38,12 +38,12 @@ mod windows_vti; pub fn stdin_reader_loop( event_tx: mpsc::Sender, should_quit: &Arc, - host_color_query_sent: bool, + host_color_query_generation: Arc, host_mouse_capture_active: Arc, ) { #[cfg(windows)] { - let _ = (host_color_query_sent, host_mouse_capture_active); + let _ = (host_color_query_generation, host_mouse_capture_active); windows_stdin_reader_loop(event_tx, should_quit); } @@ -51,7 +51,7 @@ pub fn stdin_reader_loop( unix_stdin_reader_loop( event_tx, should_quit, - host_color_query_sent, + host_color_query_generation, host_mouse_capture_active, ); } @@ -60,23 +60,30 @@ pub fn stdin_reader_loop( fn unix_stdin_reader_loop( event_tx: mpsc::Sender, should_quit: &Arc, - host_color_query_sent: bool, + host_color_query_generation: Arc, host_mouse_capture_active: Arc, ) { let stdin = io::stdin(); let mut reader = stdin.lock(); let mut scratch = [0u8; 4096]; let mut framer = crate::raw_input::RawInputByteFramer::for_host_input(); - if host_color_query_sent { - framer.host_color_query_sent(); - framer.enable_host_color_scheme_change_tracking(); - } + let mut observed_host_color_query_generation = 0; + sync_host_color_query_generation( + &mut framer, + &host_color_query_generation, + &mut observed_host_color_query_generation, + ); let mut pending_palette = Vec::new(); while !should_quit.load(Ordering::Acquire) { match reader.read(&mut scratch) { Ok(0) => break, Ok(n) => { + sync_host_color_query_generation( + &mut framer, + &host_color_query_generation, + &mut observed_host_color_query_generation, + ); if !send_unix_input_chunks( framer.push(&scratch[..n]), &event_tx, @@ -123,6 +130,21 @@ fn unix_stdin_reader_loop( } } +#[cfg(unix)] +fn sync_host_color_query_generation( + framer: &mut crate::raw_input::RawInputByteFramer, + generation: &AtomicU64, + observed_generation: &mut u64, +) { + let current_generation = generation.load(Ordering::Acquire); + if current_generation == *observed_generation { + return; + } + *observed_generation = current_generation; + framer.host_color_query_sent(); + framer.enable_host_color_scheme_change_tracking(); +} + #[cfg(unix)] fn send_unix_input_chunks( chunks: Vec>, @@ -498,6 +520,26 @@ mod tests { assert!(timeout_ms <= 20); } + #[test] + fn later_host_color_query_rearms_split_reply_framing() { + let generation = AtomicU64::new(1); + let mut observed_generation = 0; + let mut framer = crate::raw_input::RawInputByteFramer::default(); + sync_host_color_query_generation(&mut framer, &generation, &mut observed_generation); + + for _ in 0..258 { + assert_eq!(framer.push(b"\x1b]11;rgb:1111/2222/3333\x1b\\").len(), 1); + } + assert!(framer.push(b"\x1b").is_empty()); + assert_eq!(framer.flush_timeout(), vec![b"\x1b".to_vec()]); + + generation.store(2, Ordering::Release); + sync_host_color_query_generation(&mut framer, &generation, &mut observed_generation); + assert!(framer.push(b"\x1b").is_empty()); + assert!(framer.flush_timeout().is_empty()); + assert_eq!(framer.push(b"]11;rgb:aaaa/bbbb/cccc\x1b\\").len(), 1); + } + #[cfg(not(target_os = "macos"))] #[test] fn windows_repeated_escape_keeps_second_escape_pending() { diff --git a/src/client/mod.rs b/src/client/mod.rs index 6be41c0658..58ed8fd858 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1309,6 +1309,7 @@ async fn run_client_loop( }; debug!(?negotiated_encoding, "client render encoding active"); let host_mouse_capture_active = Arc::new(AtomicBool::new(state.mouse_capture_active)); + let host_color_query_generation = Arc::new(std::sync::atomic::AtomicU64::new(0)); // Channel for events from the stdin, resize, and server reader threads. let (event_tx, mut event_rx) = tokio::sync::mpsc::channel::(256); @@ -1319,17 +1320,18 @@ async fn run_client_loop( let stdin_quit = should_quit.clone(); let stdin_tx = event_tx.clone(); let stdin_mouse_capture_active = host_mouse_capture_active.clone(); + let stdin_host_color_query_generation = host_color_query_generation.clone(); std::thread::spawn(move || { input::stdin_reader_loop( stdin_tx, &stdin_quit, - will_query_host_terminal_theme, + stdin_host_color_query_generation, stdin_mouse_capture_active, ); }); if will_query_host_terminal_theme { - query_host_terminal_theme(); + query_host_terminal_theme(&host_color_query_generation); } // Spawn the resize poller thread. @@ -1424,7 +1426,7 @@ async fn run_client_loop( state.request_repaint(); } if crate::raw_input::events_require_host_terminal_theme_query(&events) { - query_host_terminal_theme(); + query_host_terminal_theme(&host_color_query_generation); } data }; @@ -2150,7 +2152,8 @@ fn resize_poll_loop( // --------------------------------------------------------------------------- /// Initialize logging for the client process. -fn query_host_terminal_theme() { +fn query_host_terminal_theme(generation: &std::sync::atomic::AtomicU64) { + generation.fetch_add(1, Ordering::Release); let _ = write_host_terminal_theme_query(io::stdout()); } diff --git a/src/raw_input.rs b/src/raw_input.rs index 6b59b3927e..7c47b06b70 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -519,9 +519,12 @@ pub(crate) fn events_require_host_surface_redraw( #[cfg(any(not(windows), test))] pub(crate) fn events_require_host_terminal_theme_query(events: &[RawInputEvent]) -> bool { - events - .iter() - .any(|event| matches!(event, RawInputEvent::HostColorSchemeChanged(_))) + events.iter().any(|event| { + matches!( + event, + RawInputEvent::OuterFocusLost | RawInputEvent::HostColorSchemeChanged(_) + ) + }) } fn input_flush_timeout_ms(framer: &RawInputFramer) -> i32 { @@ -1330,9 +1333,14 @@ mod tests { let events = parse_raw_input_bytes_sync(b"\x1b[I"); assert!(events_require_host_surface_redraw(&events, true)); assert!(!events_require_host_surface_redraw(&events, false)); + assert!(!events_require_host_terminal_theme_query(&events)); + } + #[test] + fn outer_focus_loss_requests_host_theme_query() { let events = parse_raw_input_bytes_sync(b"\x1b[O"); assert!(!events_require_host_surface_redraw(&events, true)); + assert!(events_require_host_terminal_theme_query(&events)); } #[test]