diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-22 19:37:34 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-22 19:37:34 +0300 |
| commit | 1b9a165f9e9fe9e8082d38e15942cfe0962344e2 (patch) | |
| tree | 4467b1a94ddd2fe9b4b2f59a75a4a0ca7fbef26e /src | |
| parent | c25f116c5365b2a9dcb621432a7f96b3f5160d43 (diff) | |
fix: answer pane default color queries
refs #253
Diffstat (limited to 'src')
| -rw-r--r-- | src/pane/osc.rs | 246 | ||||
| -rw-r--r-- | src/pane/terminal.rs | 149 |
2 files changed, 388 insertions, 7 deletions
diff --git a/src/pane/osc.rs b/src/pane/osc.rs index 0f04b045..217e0c86 100644 --- a/src/pane/osc.rs +++ b/src/pane/osc.rs @@ -6,6 +6,28 @@ use crate::layout::PaneId; use super::terminal::GhosttyPaneCore; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum DefaultColorQuery { + Foreground, + Background, +} + +impl DefaultColorQuery { + pub(super) fn osc_number(self) -> u8 { + match self { + Self::Foreground => 10, + Self::Background => 11, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum DefaultColorEvent { + Query(DefaultColorQuery), + Set(DefaultColorQuery), + Reset(DefaultColorQuery), +} + #[derive(Debug, Default)] pub(super) struct DefaultColorOscTracker { state: DefaultColorOscTrackerState, @@ -19,6 +41,14 @@ enum DefaultColorOscTrackerState { Escape, OscBody, OscEscape, + IgnoreString, + IgnoreStringEscape, + OversizedOsc, + OversizedOscEscape, +} + +fn is_ignored_string_intro(byte: u8) -> bool { + matches!(byte, b'P' | b'_' | b'^' | b'X') } impl DefaultColorOscTracker { @@ -36,6 +66,9 @@ impl DefaultColorOscTracker { if byte == b']' { self.body.clear(); self.state = DefaultColorOscTrackerState::OscBody; + } else if is_ignored_string_intro(byte) { + self.body.clear(); + self.state = DefaultColorOscTrackerState::IgnoreString; } else if byte == 0x1b { self.state = DefaultColorOscTrackerState::Escape; } else { @@ -62,11 +95,37 @@ impl DefaultColorOscTracker { self.state = DefaultColorOscTrackerState::OscBody; } } + DefaultColorOscTrackerState::IgnoreString => { + if byte == 0x1b { + self.state = DefaultColorOscTrackerState::IgnoreStringEscape; + } + } + DefaultColorOscTrackerState::IgnoreStringEscape => { + if byte == b'\\' { + self.state = DefaultColorOscTrackerState::Ground; + } else if byte != 0x1b { + self.state = DefaultColorOscTrackerState::IgnoreString; + } + } + DefaultColorOscTrackerState::OversizedOsc => { + if byte == 0x1b { + self.state = DefaultColorOscTrackerState::OversizedOscEscape; + } else if byte == 0x07 { + self.state = DefaultColorOscTrackerState::Ground; + } + } + DefaultColorOscTrackerState::OversizedOscEscape => { + if byte == b'\\' { + self.state = DefaultColorOscTrackerState::Ground; + } else if byte != 0x1b { + self.state = DefaultColorOscTrackerState::OversizedOsc; + } + } } if self.body.len() > 1024 { self.body.clear(); - self.state = DefaultColorOscTrackerState::Ground; + self.state = DefaultColorOscTrackerState::OversizedOsc; } } @@ -75,12 +134,125 @@ impl DefaultColorOscTracker { } fn is_default_color_set_osc(body: &[u8]) -> bool { - let Some(separator) = body.iter().position(|byte| *byte == b';') else { - return false; + matches!( + parse_default_color_event(body), + Some(DefaultColorEvent::Set(_)) + ) +} + +#[derive(Debug, Default)] +pub(super) struct DefaultColorEventTracker { + state: DefaultColorOscTrackerState, + body: Vec<u8>, + pending: Vec<DefaultColorEvent>, +} + +impl DefaultColorEventTracker { + pub(super) fn observe(&mut self, bytes: &[u8]) { + for &byte in bytes { + match self.state { + DefaultColorOscTrackerState::Ground => { + if byte == 0x1b { + self.state = DefaultColorOscTrackerState::Escape; + } + } + DefaultColorOscTrackerState::Escape => { + if byte == b']' { + self.body.clear(); + self.state = DefaultColorOscTrackerState::OscBody; + } else if is_ignored_string_intro(byte) { + self.body.clear(); + self.state = DefaultColorOscTrackerState::IgnoreString; + } else if byte == 0x1b { + self.state = DefaultColorOscTrackerState::Escape; + } else { + self.state = DefaultColorOscTrackerState::Ground; + } + } + DefaultColorOscTrackerState::OscBody => match byte { + 0x07 => { + self.finalize(); + self.state = DefaultColorOscTrackerState::Ground; + } + 0x1b => self.state = DefaultColorOscTrackerState::OscEscape, + _ => self.body.push(byte), + }, + DefaultColorOscTrackerState::OscEscape => { + if byte == b'\\' { + self.finalize(); + self.state = DefaultColorOscTrackerState::Ground; + } else { + self.body.push(0x1b); + self.body.push(byte); + self.state = DefaultColorOscTrackerState::OscBody; + } + } + DefaultColorOscTrackerState::IgnoreString => { + if byte == 0x1b { + self.state = DefaultColorOscTrackerState::IgnoreStringEscape; + } + } + DefaultColorOscTrackerState::IgnoreStringEscape => { + if byte == b'\\' { + self.state = DefaultColorOscTrackerState::Ground; + } else if byte != 0x1b { + self.state = DefaultColorOscTrackerState::IgnoreString; + } + } + DefaultColorOscTrackerState::OversizedOsc => { + if byte == 0x1b { + self.state = DefaultColorOscTrackerState::OversizedOscEscape; + } else if byte == 0x07 { + self.state = DefaultColorOscTrackerState::Ground; + } + } + DefaultColorOscTrackerState::OversizedOscEscape => { + if byte == b'\\' { + self.state = DefaultColorOscTrackerState::Ground; + } else if byte != 0x1b { + self.state = DefaultColorOscTrackerState::OversizedOsc; + } + } + } + + if self.body.len() > 1024 { + self.body.clear(); + self.state = DefaultColorOscTrackerState::OversizedOsc; + } + } + } + + fn finalize(&mut self) { + if let Some(event) = parse_default_color_event(&self.body) { + self.pending.push(event); + } + self.body.clear(); + } + + pub(super) fn drain_pending(&mut self) -> Vec<DefaultColorEvent> { + std::mem::take(&mut self.pending) + } +} + +fn parse_default_color_event(body: &[u8]) -> Option<DefaultColorEvent> { + match body { + b"10;?" => Some(DefaultColorEvent::Query(DefaultColorQuery::Foreground)), + b"11;?" => Some(DefaultColorEvent::Query(DefaultColorQuery::Background)), + b"110" | b"110;" => Some(DefaultColorEvent::Reset(DefaultColorQuery::Foreground)), + b"111" | b"111;" => Some(DefaultColorEvent::Reset(DefaultColorQuery::Background)), + _ => parse_default_color_set_event(body), + } +} + +fn parse_default_color_set_event(body: &[u8]) -> Option<DefaultColorEvent> { + let separator = body.iter().position(|byte| *byte == b';')?; + let query = match &body[..separator] { + b"10" => DefaultColorQuery::Foreground, + b"11" => DefaultColorQuery::Background, + _ => return None, }; - let command = &body[..separator]; let value = &body[separator + 1..]; - matches!(command, b"10" | b"11") && !value.is_empty() && value != b"?" + (!value.is_empty() && value != b"?").then_some(DefaultColorEvent::Set(query)) } /// 256 KiB of base64 ≈ 192 KiB of text — enough for real source-file copies @@ -329,6 +501,8 @@ pub(super) fn restore_host_terminal_theme_if_needed( } core.transient_default_color_owner_pgid = None; + core.child_default_foreground_changed = false; + core.child_default_background_changed = false; write_host_terminal_theme(&mut core.terminal, core.host_terminal_theme); info!( pane = pane_id.raw(), @@ -399,6 +573,68 @@ mod tests { } #[test] + fn default_color_event_tracker_detects_queries_sets_and_resets() { + let mut tracker = DefaultColorEventTracker::default(); + + tracker.observe(b"\x1b]10;?\x07\x1b]11;?\x1b\\\x1b]10;rgb:11/22/33\x07\x1b]111\x07"); + + assert_eq!( + tracker.drain_pending(), + vec![ + DefaultColorEvent::Query(DefaultColorQuery::Foreground), + DefaultColorEvent::Query(DefaultColorQuery::Background), + DefaultColorEvent::Set(DefaultColorQuery::Foreground), + DefaultColorEvent::Reset(DefaultColorQuery::Background), + ] + ); + } + + #[test] + fn default_color_event_tracker_handles_split_queries() { + let mut tracker = DefaultColorEventTracker::default(); + + tracker.observe(b"\x1b]11"); + assert!(tracker.drain_pending().is_empty()); + tracker.observe(b";?\x1b"); + assert!(tracker.drain_pending().is_empty()); + tracker.observe(b"\\"); + + assert_eq!( + tracker.drain_pending(), + vec![DefaultColorEvent::Query(DefaultColorQuery::Background)] + ); + } + + #[test] + fn default_color_event_tracker_ignores_other_osc_and_dcs_payloads() { + let mut tracker = DefaultColorEventTracker::default(); + + tracker.observe(b"\x1b]0;title\x07"); + tracker.observe(b"\x1b]52;c;?\x07"); + tracker.observe(b"\x1bPtmux;\x1b\x1b]11;?\x07\x1b\\"); + tracker.observe(b"\x1bPtmux;payload\x07\x1b]11;?\x07\x1b\\"); + + assert!(tracker.drain_pending().is_empty()); + } + + #[test] + fn default_color_event_tracker_ignores_oversized_osc_until_terminator() { + let mut tracker = DefaultColorEventTracker::default(); + let mut oversized = Vec::from(b"\x1b]11;".as_slice()); + oversized.extend(std::iter::repeat_n(b'a', 1025)); + oversized.extend_from_slice(b"\x1b]11;?\x07"); + + tracker.observe(&oversized); + assert!(tracker.drain_pending().is_empty()); + + tracker.observe(b"\x1b]11;?\x07"); + assert_eq!( + tracker.drain_pending(), + vec![DefaultColorEvent::Query(DefaultColorQuery::Background)] + ); + } + + #[test] fn osc52_forwarder_detects_write_with_bel() { let mut fw = Osc52Forwarder::default(); fw.observe(b"\x1b]52;c;aGVsbG8=\x07"); diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 6e29774a..e1a57e89 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -20,7 +20,8 @@ use super::{ osc::{ contains_scrollback_clear_sequence, current_transient_default_color_owner, maybe_filter_primary_screen_scrollback_clear, restore_host_terminal_theme_if_needed, - write_host_terminal_theme, DefaultColorOscTracker, Osc52Forwarder, + write_host_terminal_theme, DefaultColorEvent, DefaultColorEventTracker, + DefaultColorOscTracker, DefaultColorQuery, Osc52Forwarder, }, }; @@ -93,6 +94,9 @@ pub(crate) struct GhosttyPaneCore { pub host_terminal_theme: crate::terminal_theme::TerminalTheme, pub transient_default_color_owner_pgid: Option<u32>, pub default_color_tracker: DefaultColorOscTracker, + pub default_color_event_tracker: DefaultColorEventTracker, + pub child_default_foreground_changed: bool, + pub child_default_background_changed: bool, pub osc52_forwarder: Osc52Forwarder, } @@ -282,6 +286,9 @@ impl GhosttyPaneTerminal { host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), transient_default_color_owner_pgid: None, default_color_tracker: DefaultColorOscTracker::default(), + default_color_event_tracker: DefaultColorEventTracker::default(), + child_default_foreground_changed: false, + child_default_background_changed: false, osc52_forwarder: Osc52Forwarder::default(), }), key_encoder: Mutex::new(key_encoder), @@ -292,6 +299,8 @@ impl GhosttyPaneTerminal { if let Ok(mut core) = self.core.lock() { core.host_terminal_theme = theme; core.transient_default_color_owner_pgid = None; + core.child_default_foreground_changed = false; + core.child_default_background_changed = false; write_host_terminal_theme(&mut core.terminal, theme); } } @@ -387,6 +396,10 @@ impl GhosttyPaneTerminal { } core.terminal.write(filtered_bytes.as_ref()); + core.default_color_event_tracker + .observe(filtered_bytes.as_ref()); + respond_to_default_color_events(&mut core, response_writer); + let has_kitty_graphics_sequence = crate::kitty_graphics::is_enabled() && contains_kitty_graphics_sequence(filtered_bytes.as_ref()); if has_kitty_graphics_sequence { @@ -399,7 +412,6 @@ impl GhosttyPaneTerminal { .terminal .mode_get(crate::ghostty::MODE_SYNCHRONIZED_OUTPUT) .unwrap_or(false); - let _ = response_writer; ProcessBytesResult { request_render: !synchronized_output && !has_kitty_graphics_sequence, render_delay: (!synchronized_output && has_kitty_graphics_sequence) @@ -1132,6 +1144,53 @@ fn ghostty_cell_style( style.add_modifier(modifiers) } +fn respond_to_default_color_events( + core: &mut GhosttyPaneCore, + response_writer: &mpsc::Sender<Bytes>, +) { + for event in core.default_color_event_tracker.drain_pending() { + match event { + DefaultColorEvent::Query(query) => { + if let Some(response) = default_color_query_response(query, core) { + let _ = response_writer.try_send(response); + } + } + DefaultColorEvent::Set(query) => mark_child_default_color_changed(core, query, true), + DefaultColorEvent::Reset(query) => mark_child_default_color_changed(core, query, false), + } + } +} + +fn default_color_query_response(query: DefaultColorQuery, core: &GhosttyPaneCore) -> Option<Bytes> { + let color = match query { + DefaultColorQuery::Foreground if !core.child_default_foreground_changed => { + core.host_terminal_theme.foreground + } + DefaultColorQuery::Background if !core.child_default_background_changed => { + core.host_terminal_theme.background + } + _ => None, + }?; + let r = u16::from(color.r) * 257; + let g = u16::from(color.g) * 257; + let b = u16::from(color.b) * 257; + Some(Bytes::from(format!( + "\x1b]{};rgb:{r:04x}/{g:04x}/{b:04x}\x1b\\", + query.osc_number() + ))) +} + +fn mark_child_default_color_changed( + core: &mut GhosttyPaneCore, + query: DefaultColorQuery, + changed: bool, +) { + match query { + DefaultColorQuery::Foreground => core.child_default_foreground_changed = changed, + DefaultColorQuery::Background => core.child_default_background_changed = changed, + } +} + fn ghostty_default_fg( color: crate::ghostty::RgbColor, host_theme: crate::terminal_theme::TerminalTheme, @@ -1962,6 +2021,92 @@ mod tests { } #[test] + fn process_pty_bytes_responds_to_default_color_queries() { + let (tx, mut rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); + let pane_id = PaneId::from_raw(1); + pane.apply_host_terminal_theme(crate::terminal_theme::TerminalTheme { + foreground: Some(crate::terminal_theme::RgbColor { + r: 0x65, + g: 0x7b, + b: 0x83, + }), + background: Some(crate::terminal_theme::RgbColor { + r: 0xfd, + g: 0xf6, + b: 0xe3, + }), + }); + + pane.process_pty_bytes(pane_id, 0, b"\x1b]10;?\x07\x1b]11;?\x07", &tx); + + assert_eq!( + rx.try_recv().unwrap().as_ref(), + b"\x1b]10;rgb:6565/7b7b/8383\x1b\\" + ); + assert_eq!( + rx.try_recv().unwrap().as_ref(), + b"\x1b]11;rgb:fdfd/f6f6/e3e3\x1b\\" + ); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn process_pty_bytes_responds_to_split_default_color_query() { + let (tx, mut rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); + let pane_id = PaneId::from_raw(1); + pane.apply_host_terminal_theme(crate::terminal_theme::TerminalTheme { + foreground: None, + background: Some(crate::terminal_theme::RgbColor { + r: 0xfd, + g: 0xf6, + b: 0xe3, + }), + }); + + pane.process_pty_bytes(pane_id, 0, b"\x1b]11", &tx); + assert!(rx.try_recv().is_err()); + pane.process_pty_bytes(pane_id, 0, b";?\x1b", &tx); + assert!(rx.try_recv().is_err()); + pane.process_pty_bytes(pane_id, 0, b"\\", &tx); + + assert_eq!( + rx.try_recv().unwrap().as_ref(), + b"\x1b]11;rgb:fdfd/f6f6/e3e3\x1b\\" + ); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn process_pty_bytes_stops_replying_after_child_default_color_set() { + let (tx, mut rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); + let pane_id = PaneId::from_raw(1); + pane.apply_host_terminal_theme(crate::terminal_theme::TerminalTheme { + foreground: None, + background: Some(crate::terminal_theme::RgbColor { + r: 0xfd, + g: 0xf6, + b: 0xe3, + }), + }); + + pane.process_pty_bytes(pane_id, 0, b"\x1b]11;rgb:11/22/33\x07\x1b]11;?\x07", &tx); + assert!(rx.try_recv().is_err()); + + pane.process_pty_bytes(pane_id, 0, b"\x1b]111\x07\x1b]11;?\x07", &tx); + assert_eq!( + rx.try_recv().unwrap().as_ref(), + b"\x1b]11;rgb:fdfd/f6f6/e3e3\x1b\\" + ); + assert!(rx.try_recv().is_err()); + } + + #[test] fn render_leaves_host_default_background_transparent() { let (tx, _rx) = mpsc::channel(4); let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); |
