diff options
| author | akbash <akbash@herdr.dev> | 2026-08-06 17:38:20 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-06 17:38:20 +0300 |
| commit | 3825c0c30099dbe4cf19627b0525ce09f6580995 (patch) | |
| tree | 7c2387374cc9150eaad170793bba0e0baa0a4068 /src/raw_input.rs | |
| parent | fae0b2365782c59a068fa32ee441afa64626de52 (diff) | |
fix(theme): refresh host appearance on focus (#2417)
* fix(theme): refresh host appearance on focus
refs #2416
* fix(input): preserve split appearance replies
refs #2416
* fix(input): retain fragmented appearance reports
refs #2416
---------
Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/raw_input.rs')
| -rw-r--r-- | src/raw_input.rs | 170 |
1 files changed, 164 insertions, 6 deletions
diff --git a/src/raw_input.rs b/src/raw_input.rs index 12bcba4d..510768fc 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -172,6 +172,11 @@ impl RawInputFramer { self.byte_framer.enable_host_color_scheme_change_tracking(); } + #[cfg(any(not(windows), test))] + pub(crate) fn enable_host_appearance_query_on_focus(&mut self) { + self.byte_framer.enable_host_appearance_query_on_focus(); + } + pub(crate) fn has_pending_input(&self) -> bool { self.byte_framer.has_pending_input() } @@ -216,8 +221,10 @@ pub(crate) struct RawInputByteFramer { lone_escape_recently_flushed: bool, host_color_replies_awaited: u16, host_cell_size_replies_awaited: u16, + host_appearance_reply_awaited: bool, held_pending_host_reply_esc: bool, host_color_scheme_change_tracking: bool, + host_appearance_query_on_focus: bool, split_coalesced_escape: bool, } @@ -252,6 +259,11 @@ impl RawInputByteFramer { self.held_pending_host_reply_esc = false; } + fn host_appearance_query_sent(&mut self) { + self.host_appearance_reply_awaited = true; + self.held_pending_host_reply_esc = false; + } + /// Same hold window as `host_color_query_sent`, for the XTWINOPS cell size /// reply. Only the Unix client sends this query. #[cfg(any(unix, test))] @@ -261,13 +273,22 @@ impl RawInputByteFramer { } fn awaiting_host_reply(&self) -> bool { - self.host_color_replies_awaited > 0 || self.host_cell_size_replies_awaited > 0 + self.host_color_replies_awaited > 0 + || self.host_cell_size_replies_awaited > 0 + || self.host_appearance_reply_awaited } pub(crate) fn enable_host_color_scheme_change_tracking(&mut self) { self.host_color_scheme_change_tracking = true; } + /// Arm the bounded host-reply window when focus gain will emit an appearance query. + /// If the write or reply fails, a lone Escape is delayed for only one extra flush. + #[cfg(any(not(windows), test))] + pub(crate) fn enable_host_appearance_query_on_focus(&mut self) { + self.host_appearance_query_on_focus = true; + } + pub(crate) fn has_pending_input(&self) -> bool { !self.buffer.is_empty() } @@ -365,13 +386,16 @@ impl RawInputByteFramer { return chunks; } - if self.host_cell_size_replies_awaited > 0 && self.buffer.as_slice() == b"\x1b[" { + if (self.host_cell_size_replies_awaited > 0 || self.host_appearance_reply_awaited) + && self.buffer.as_slice() == b"\x1b[" + { if !self.held_pending_host_reply_esc { self.held_pending_host_reply_esc = true; - tracing::trace!("holding incomplete cell size reply one flush"); + tracing::trace!("holding incomplete host CSI reply one flush"); return chunks; } self.host_cell_size_replies_awaited = 0; + self.host_appearance_reply_awaited = false; self.held_pending_host_reply_esc = false; } @@ -391,10 +415,20 @@ impl RawInputByteFramer { } if starts_with_incomplete_host_color_scheme_report(&self.buffer) { + if self.host_appearance_reply_awaited && !self.held_pending_host_reply_esc { + self.held_pending_host_reply_esc = true; + tracing::trace!( + len = self.buffer.len(), + "holding incomplete host color scheme report one flush" + ); + return chunks; + } tracing::debug!( len = self.buffer.len(), "discarding incomplete host color scheme report after input timeout" ); + self.host_appearance_reply_awaited = false; + self.held_pending_host_reply_esc = false; self.discard_until = Some(ControlStringFamily::HostReplyCsi); self.discarded_tail_bytes = 0; self.buffer.clear(); @@ -423,6 +457,7 @@ impl RawInputByteFramer { // No continuation arrived; give up the window so Escape is not delayed again. self.host_color_replies_awaited = 0; self.host_cell_size_replies_awaited = 0; + self.host_appearance_reply_awaited = false; self.held_pending_host_reply_esc = false; tracing::warn!( bytes = ?self.buffer, @@ -522,10 +557,15 @@ impl RawInputByteFramer { } else if matches!(event, RawInputEvent::HostCellSizeReport { .. }) { self.host_cell_size_replies_awaited = self.host_cell_size_replies_awaited.saturating_sub(1); - } else if self.host_color_scheme_change_tracking - && matches!(event, RawInputEvent::HostColorSchemeChanged(_)) + } else if self.host_appearance_query_on_focus + && matches!(event, RawInputEvent::OuterFocusGained) { - self.host_color_query_sent(); + self.host_appearance_query_sent(); + } else if matches!(event, RawInputEvent::HostColorSchemeChanged(_)) { + self.host_appearance_reply_awaited = false; + if self.host_color_scheme_change_tracking { + self.host_color_query_sent(); + } } self.held_pending_host_reply_esc = false; chunks.push(self.buffer[..consumed].to_vec()); @@ -580,6 +620,13 @@ pub(crate) fn events_require_host_surface_redraw( } #[cfg(any(not(windows), test))] +pub(crate) fn events_require_host_terminal_appearance_query(events: &[RawInputEvent]) -> bool { + events + .iter() + .any(|event| matches!(event, RawInputEvent::OuterFocusGained)) +} + +#[cfg(any(not(windows), test))] pub(crate) fn events_require_host_terminal_theme_query(events: &[RawInputEvent]) -> bool { events .iter() @@ -604,6 +651,8 @@ pub fn spawn_input_reader() -> mpsc::Receiver<RawInputEvent> { let mut framer = RawInputFramer::for_host_input(); framer.host_color_query_sent(); framer.enable_host_color_scheme_change_tracking(); + #[cfg(not(windows))] + framer.enable_host_appearance_query_on_focus(); let mut pending_palette = Vec::new(); loop { @@ -1545,6 +1594,20 @@ mod tests { } #[test] + fn outer_focus_gained_requests_host_appearance_query() { + let gained = parse_raw_input_bytes_sync(b"\x1b[I"); + let lost = parse_raw_input_bytes_sync(b"\x1b[O"); + let scheme_report = parse_raw_input_bytes_sync(b"\x1b[?997;1n"); + + assert!(events_require_host_terminal_appearance_query(&gained)); + assert!(!events_require_host_terminal_appearance_query(&lost)); + assert!(!events_require_host_terminal_appearance_query( + &scheme_report + )); + assert!(events_require_host_terminal_theme_query(&scheme_report)); + } + + #[test] fn parses_ghostty_color_scheme_reports() { for bytes in [ GHOSTTY_COLOR_SCHEME_DARK_REPORT, @@ -2864,6 +2927,101 @@ mod tests { } #[test] + fn opted_in_byte_framer_rearms_after_outer_focus_gained() { + let mut framer = RawInputByteFramer::default(); + framer.enable_host_color_scheme_change_tracking(); + framer.enable_host_appearance_query_on_focus(); + + assert_eq!(framer.push(b"\x1b[I"), vec![b"\x1b[I".to_vec()]); + assert!(framer.push(b"\x1b").is_empty()); + assert!(framer.flush_timeout().is_empty()); + assert_eq!( + framer.push(b"[?997;2n"), + vec![GHOSTTY_COLOR_SCHEME_LIGHT_REPORT.to_vec()] + ); + } + + #[test] + fn opted_in_byte_framer_reassembles_appearance_reply_split_after_csi() { + let mut framer = RawInputByteFramer::default(); + framer.enable_host_color_scheme_change_tracking(); + framer.enable_host_appearance_query_on_focus(); + + assert_eq!(framer.push(b"\x1b[I"), vec![b"\x1b[I".to_vec()]); + assert!(framer.push(b"\x1b[").is_empty()); + assert!(framer.flush_timeout().is_empty()); + assert_eq!( + framer.push(b"?997;2n"), + vec![GHOSTTY_COLOR_SCHEME_LIGHT_REPORT.to_vec()] + ); + } + + #[test] + fn opted_in_byte_framer_reassembles_delayed_appearance_reply() { + let mut framer = RawInputByteFramer::default(); + framer.enable_host_color_scheme_change_tracking(); + framer.enable_host_appearance_query_on_focus(); + + assert_eq!(framer.push(b"\x1b[I"), vec![b"\x1b[I".to_vec()]); + assert!(framer.push(b"\x1b[?997;").is_empty()); + assert!(framer.flush_timeout().is_empty()); + assert_eq!( + framer.push(b"2n"), + vec![GHOSTTY_COLOR_SCHEME_LIGHT_REPORT.to_vec()] + ); + } + + #[test] + fn timed_out_appearance_reply_preserves_pending_color_reply_window() { + let mut framer = RawInputByteFramer::default(); + framer.host_color_query_sent(); + framer.enable_host_appearance_query_on_focus(); + + assert_eq!(framer.push(b"\x1b[I"), vec![b"\x1b[I".to_vec()]); + assert!(framer.push(b"\x1b[?997;").is_empty()); + assert!(framer.flush_timeout().is_empty()); + assert!(framer.flush_timeout().is_empty()); + assert!(framer.push(b"2n").is_empty()); + + assert!(framer.push(b"\x1b").is_empty()); + assert!(framer.flush_timeout().is_empty()); + assert_eq!( + framer.push(b"]10;rgb:aaaa/bbbb/cccc\x1b\\"), + vec![b"\x1b]10;rgb:aaaa/bbbb/cccc\x1b\\".to_vec()] + ); + } + + #[test] + fn disabled_focus_query_does_not_rearm_byte_framer() { + let mut framer = RawInputByteFramer::default(); + framer.enable_host_color_scheme_change_tracking(); + + assert_eq!(framer.push(b"\x1b[I"), vec![b"\x1b[I".to_vec()]); + assert!(framer.push(b"\x1b").is_empty()); + assert_eq!(framer.flush_timeout(), vec![b"\x1b".to_vec()]); + } + + #[test] + fn focus_query_policy_does_not_delay_plain_escape_without_focus() { + let mut framer = RawInputByteFramer::default(); + framer.enable_host_appearance_query_on_focus(); + + assert!(framer.push(b"\x1b").is_empty()); + assert_eq!(framer.flush_timeout(), vec![b"\x1b".to_vec()]); + } + + #[test] + fn focus_query_without_reply_holds_escape_for_only_one_flush() { + let mut framer = RawInputByteFramer::default(); + framer.enable_host_appearance_query_on_focus(); + + assert_eq!(framer.push(b"\x1b[I"), vec![b"\x1b[I".to_vec()]); + assert!(framer.push(b"\x1b").is_empty()); + assert!(framer.flush_timeout().is_empty()); + assert_eq!(framer.flush_timeout(), vec![b"\x1b".to_vec()]); + } + + #[test] fn opted_in_byte_framer_rearms_after_color_scheme_report() { let mut framer = RawInputByteFramer::default(); framer.enable_host_color_scheme_change_tracking(); |
