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/pane/osc.rs | |
| parent | c25f116c5365b2a9dcb621432a7f96b3f5160d43 (diff) | |
fix: answer pane default color queries
refs #253
Diffstat (limited to 'src/pane/osc.rs')
| -rw-r--r-- | src/pane/osc.rs | 246 |
1 files changed, 241 insertions, 5 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"); |
