aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-08-06 17:38:20 +0300
committerGitHub <noreply@github.com>2026-08-06 17:38:20 +0300
commit3825c0c30099dbe4cf19627b0525ce09f6580995 (patch)
tree7c2387374cc9150eaad170793bba0e0baa0a4068 /src
parentfae0b2365782c59a068fa32ee441afa64626de52 (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')
-rw-r--r--src/app/runtime.rs2
-rw-r--r--src/app/theme_sync.rs9
-rw-r--r--src/client/input.rs1
-rw-r--r--src/client/mod.rs25
-rw-r--r--src/raw_input.rs170
-rw-r--r--src/terminal_theme.rs2
6 files changed, 203 insertions, 6 deletions
diff --git a/src/app/runtime.rs b/src/app/runtime.rs
index e05b9599..49420381 100644
--- a/src/app/runtime.rs
+++ b/src/app/runtime.rs
@@ -233,6 +233,8 @@ impl App {
changes_view
}
crate::raw_input::RawInputEvent::OuterFocusGained => {
+ #[cfg(not(windows))]
+ self.query_host_terminal_appearance();
self.send_outer_focus_event(crate::ghostty::FocusEvent::Gained);
if self.state.redraw_on_focus_gained {
self.request_repaint();
diff --git a/src/app/theme_sync.rs b/src/app/theme_sync.rs
index 32716ced..6bf06685 100644
--- a/src/app/theme_sync.rs
+++ b/src/app/theme_sync.rs
@@ -1,6 +1,15 @@
use super::App;
impl App {
+ #[cfg(not(windows))]
+ pub(super) fn query_host_terminal_appearance(&self) {
+ use std::io::Write;
+
+ let _ = std::io::stdout()
+ .write_all(crate::terminal_theme::HOST_COLOR_SCHEME_QUERY_SEQUENCE.as_bytes());
+ let _ = std::io::stdout().flush();
+ }
+
pub(super) fn query_host_terminal_theme(&self) {
use std::io::Write;
diff --git a/src/client/input.rs b/src/client/input.rs
index 9e0d300e..4dc26eae 100644
--- a/src/client/input.rs
+++ b/src/client/input.rs
@@ -77,6 +77,7 @@ fn unix_stdin_reader_loop(
if host_color_query_sent {
framer.host_color_query_sent();
framer.enable_host_color_scheme_change_tracking();
+ framer.enable_host_appearance_query_on_focus();
}
if host_cell_size_query_sent {
framer.host_cell_size_query_sent();
diff --git a/src/client/mod.rs b/src/client/mod.rs
index fd92d188..b153dd93 100644
--- a/src/client/mod.rs
+++ b/src/client/mod.rs
@@ -1442,6 +1442,9 @@ async fn run_client_loop(
) {
state.request_repaint();
}
+ if crate::raw_input::events_require_host_terminal_appearance_query(&events) {
+ query_host_terminal_appearance();
+ }
if crate::raw_input::events_require_host_terminal_theme_query(&events) {
query_host_terminal_theme();
}
@@ -2281,6 +2284,17 @@ fn resize_poll_loop(
// Logging
// ---------------------------------------------------------------------------
+#[cfg(any(not(windows), test))]
+fn query_host_terminal_appearance() {
+ let _ = write_host_terminal_appearance_query(io::stdout());
+}
+
+#[cfg(any(not(windows), test))]
+fn write_host_terminal_appearance_query(mut writer: impl io::Write) -> io::Result<()> {
+ writer.write_all(crate::terminal_theme::HOST_COLOR_SCHEME_QUERY_SEQUENCE.as_bytes())?;
+ writer.flush()
+}
+
/// Initialize logging for the client process.
fn query_host_terminal_theme() {
let _ = write_host_terminal_theme_query(io::stdout());
@@ -2673,6 +2687,13 @@ mod tests {
}
#[test]
+ fn write_host_terminal_appearance_query_emits_mode_2031_query() {
+ let mut output = Vec::new();
+ write_host_terminal_appearance_query(&mut output).unwrap();
+ assert_eq!(output, b"\x1b[?996n");
+ }
+
+ #[test]
fn write_host_terminal_theme_query_emits_osc_queries() {
let mut output = Vec::new();
write_host_terminal_theme_query(&mut output).unwrap();
@@ -2680,6 +2701,10 @@ mod tests {
output,
crate::terminal_theme::host_terminal_theme_query_sequence().as_bytes()
);
+ assert!(!output
+ .windows(crate::terminal_theme::HOST_COLOR_SCHEME_QUERY_SEQUENCE.len())
+ .any(|window| window
+ == crate::terminal_theme::HOST_COLOR_SCHEME_QUERY_SEQUENCE.as_bytes()));
}
#[test]
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();
diff --git a/src/terminal_theme.rs b/src/terminal_theme.rs
index ffe78a95..3f04cc4c 100644
--- a/src/terminal_theme.rs
+++ b/src/terminal_theme.rs
@@ -55,6 +55,8 @@ pub enum DefaultColorKind {
}
pub const HOST_COLOR_QUERY_SEQUENCE: &str = "\x1b]10;?\x1b\\\x1b]11;?\x1b\\";
+#[cfg(any(not(windows), test))]
+pub const HOST_COLOR_SCHEME_QUERY_SEQUENCE: &str = "\x1b[?996n";
pub const HOST_COLOR_SCHEME_REPORT_ENABLE_SEQUENCE: &str = "\x1b[?2031h";
pub const HOST_COLOR_SCHEME_REPORT_DISABLE_SEQUENCE: &str = "\x1b[?2031l";