Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/app/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
9 changes: 9 additions & 0 deletions src/app/theme_sync.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
1 change: 1 addition & 0 deletions src/client/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
25 changes: 25 additions & 0 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
if crate::raw_input::events_require_host_terminal_theme_query(&events) {
query_host_terminal_theme();
}
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -2672,6 +2686,13 @@ mod tests {
assert!(!text.contains("d=A"));
}

#[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();
Expand All @@ -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]
Expand Down
170 changes: 164 additions & 6 deletions src/raw_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down Expand Up @@ -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,
}

Expand Down Expand Up @@ -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))]
Expand All @@ -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()
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -579,6 +619,13 @@ pub(crate) fn events_require_host_surface_redraw(
.any(|event| matches!(event, RawInputEvent::OuterFocusGained))
}

#[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
Expand All @@ -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 {
Expand Down Expand Up @@ -1544,6 +1593,20 @@ mod tests {
assert!(!events_require_host_surface_redraw(&events, true));
}

#[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 [
Expand Down Expand Up @@ -2863,6 +2926,101 @@ mod tests {
assert_eq!(framer.flush_timeout(), vec![b"\x1b".to_vec()]);
}

#[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();
Expand Down
2 changes: 2 additions & 0 deletions src/terminal_theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Loading