diff options
Diffstat (limited to 'src/raw_input.rs')
| -rw-r--r-- | src/raw_input.rs | 213 |
1 files changed, 211 insertions, 2 deletions
diff --git a/src/raw_input.rs b/src/raw_input.rs index 57ab6274..9978cc0c 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -95,9 +95,13 @@ use std::os::fd::AsRawFd; use tokio::sync::mpsc; use crate::input::{parse_terminal_key_sequence, TerminalKey}; -use crate::terminal_theme::{parse_default_color_response, DefaultColorKind, RgbColor}; +use crate::terminal_theme::{ + parse_default_color_response, DefaultColorKind, HostAppearance, RgbColor, +}; const ESC: u8 = 0x1b; +pub(crate) const GHOSTTY_COLOR_SCHEME_DARK_REPORT: &[u8] = b"\x1b[?997;1n"; +pub(crate) const GHOSTTY_COLOR_SCHEME_LIGHT_REPORT: &[u8] = b"\x1b[?997;2n"; const BRACKETED_PASTE_START: &[u8] = b"\x1b[200~"; const BRACKETED_PASTE_END: &[u8] = b"\x1b[201~"; @@ -112,6 +116,7 @@ pub enum RawInputEvent { kind: DefaultColorKind, color: RgbColor, }, + HostColorSchemeChanged(HostAppearance), Unsupported, } @@ -129,6 +134,14 @@ impl RawInputFramer { self.byte_framer.host_color_query_sent(); } + pub(crate) fn enable_host_color_scheme_change_tracking(&mut self) { + self.byte_framer.enable_host_color_scheme_change_tracking(); + } + + pub(crate) fn has_pending_input(&self) -> bool { + self.byte_framer.has_pending_input() + } + pub(crate) fn flush_timeout(&mut self) -> Vec<RawInputEvent> { Self::events_from_chunks(self.byte_framer.flush_timeout()) } @@ -159,6 +172,7 @@ pub(crate) struct RawInputByteFramer { discarded_tail_bytes: usize, host_color_replies_awaited: u8, held_pending_color_esc: bool, + host_color_scheme_change_tracking: bool, } const HOST_COLOR_QUERY_REPLIES: u8 = 2; @@ -176,6 +190,14 @@ impl RawInputByteFramer { self.held_pending_color_esc = false; } + pub(crate) fn enable_host_color_scheme_change_tracking(&mut self) { + self.host_color_scheme_change_tracking = true; + } + + pub(crate) fn has_pending_input(&self) -> bool { + !self.buffer.is_empty() + } + pub(crate) fn flush_timeout(&mut self) -> Vec<Vec<u8>> { let mut chunks = self.drain_available_chunks(); @@ -217,6 +239,17 @@ impl RawInputByteFramer { return chunks; } + if starts_with_incomplete_host_color_scheme_report(&self.buffer) { + tracing::debug!( + len = self.buffer.len(), + "discarding incomplete host color scheme report after input timeout" + ); + self.discard_until = Some(ControlStringFamily::HostColorSchemeCsi); + self.discarded_tail_bytes = 0; + self.buffer.clear(); + return chunks; + } + if let Some(ControlString::Incomplete { family }) = control_string(&self.buffer) { tracing::debug!( len = self.buffer.len(), @@ -291,6 +324,10 @@ impl RawInputByteFramer { }; if matches!(event, RawInputEvent::HostDefaultColor { .. }) { self.host_color_replies_awaited = self.host_color_replies_awaited.saturating_sub(1); + } else if self.host_color_scheme_change_tracking + && matches!(event, RawInputEvent::HostColorSchemeChanged(_)) + { + self.host_color_query_sent(); } self.held_pending_color_esc = false; chunks.push(self.buffer[..consumed].to_vec()); @@ -327,6 +364,9 @@ fn plausible_control_string_tail(family: ControlStringFamily, buffer: &[u8]) -> ) }), ControlStringFamily::StTerminated => buffer.last() == Some(&ESC), + ControlStringFamily::HostColorSchemeCsi => buffer + .iter() + .all(|byte| byte.is_ascii_digit() || matches!(*byte, b';' | b'?' | b'n')), } } @@ -340,6 +380,12 @@ pub(crate) fn events_require_host_surface_redraw( .any(|event| matches!(event, RawInputEvent::OuterFocusGained)) } +pub(crate) fn events_require_host_terminal_theme_query(events: &[RawInputEvent]) -> bool { + events + .iter() + .any(|event| matches!(event, RawInputEvent::HostColorSchemeChanged(_))) +} + pub fn spawn_input_reader() -> mpsc::Receiver<RawInputEvent> { let (tx, rx) = mpsc::channel(256); @@ -349,6 +395,7 @@ pub fn spawn_input_reader() -> mpsc::Receiver<RawInputEvent> { let mut scratch = [0u8; 1024]; let mut framer = RawInputFramer::default(); framer.host_color_query_sent(); + framer.enable_host_color_scheme_change_tracking(); loop { match reader.read(&mut scratch) { @@ -357,7 +404,13 @@ pub fn spawn_input_reader() -> mpsc::Receiver<RawInputEvent> { send_raw_input_events(framer.push(&scratch[..n]), &tx); if stdin_read_ready(&reader, 10) == Some(false) { - send_raw_input_events(framer.flush_timeout(), &tx); + let had_pending = framer.has_pending_input(); + let events = framer.flush_timeout(); + let held_escape = had_pending && events.is_empty(); + send_raw_input_events(events, &tx); + if held_escape && stdin_read_ready(&reader, 10) == Some(false) { + send_raw_input_events(framer.flush_timeout(), &tx); + } } } Err(_) => break, @@ -497,6 +550,10 @@ fn extract_one_event(buffer: &[u8]) -> Option<(RawInputEvent, usize)> { _ => {} } + if let Some(appearance) = parse_host_color_scheme_report(&buffer[..seq_len]) { + return Some((RawInputEvent::HostColorSchemeChanged(appearance), seq_len)); + } + if let Some(mouse) = parse_sgr_mouse(seq) { return Some((RawInputEvent::Mouse(mouse), seq_len)); } @@ -519,6 +576,7 @@ fn extract_one_event(buffer: &[u8]) -> Option<(RawInputEvent, usize)> { enum ControlStringFamily { Osc, StTerminated, + HostColorSchemeCsi, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -532,6 +590,14 @@ enum ControlString { }, } +fn parse_host_color_scheme_report(buffer: &[u8]) -> Option<HostAppearance> { + match buffer { + GHOSTTY_COLOR_SCHEME_DARK_REPORT => Some(HostAppearance::Dark), + GHOSTTY_COLOR_SCHEME_LIGHT_REPORT => Some(HostAppearance::Light), + _ => None, + } +} + fn starts_with_incomplete_default_color_response(buffer: &[u8]) -> bool { matches!( control_string(buffer), @@ -541,6 +607,13 @@ fn starts_with_incomplete_default_color_response(buffer: &[u8]) -> bool { ) && matches!(buffer.get(..5), Some(b"\x1b]10;" | b"\x1b]11;")) } +fn starts_with_incomplete_host_color_scheme_report(buffer: &[u8]) -> bool { + buffer.starts_with(b"\x1b[?") + && (GHOSTTY_COLOR_SCHEME_DARK_REPORT.starts_with(buffer) + || GHOSTTY_COLOR_SCHEME_LIGHT_REPORT.starts_with(buffer)) + && buffer.len() < GHOSTTY_COLOR_SCHEME_DARK_REPORT.len() +} + fn control_string(buffer: &[u8]) -> Option<ControlString> { let family = match buffer.get(..2)? { b"\x1b]" => ControlStringFamily::Osc, @@ -650,6 +723,10 @@ fn control_string_terminator_for_family( match family { ControlStringFamily::Osc => osc_string_terminator(buffer), ControlStringFamily::StTerminated => st_string_terminator(buffer), + ControlStringFamily::HostColorSchemeCsi => buffer + .iter() + .position(|byte| *byte == b'n') + .map(|idx| idx + 1), } } @@ -937,6 +1014,69 @@ mod tests { } #[test] + fn parses_ghostty_color_scheme_reports() { + for bytes in [ + GHOSTTY_COLOR_SCHEME_DARK_REPORT, + GHOSTTY_COLOR_SCHEME_LIGHT_REPORT, + ] { + let events = parse_raw_input_bytes_sync(bytes); + assert_eq!(events.len(), 1, "bytes: {bytes:?}"); + assert!(matches!( + events[0], + RawInputEvent::HostColorSchemeChanged(HostAppearance::Dark | HostAppearance::Light) + )); + assert!(events_require_host_terminal_theme_query(&events)); + } + } + + #[test] + fn ghostty_color_scheme_report_parser_is_exact() { + for bytes in [ + b"\x1b[?997;0n".as_slice(), + b"\x1b[?997;3n".as_slice(), + b"\x1b[?998;1n".as_slice(), + ] { + let events = parse_raw_input_bytes_sync(bytes); + assert_eq!(events.len(), 1, "bytes: {bytes:?}"); + assert!(matches!(events[0], RawInputEvent::Unsupported)); + assert!(!events_require_host_terminal_theme_query(&events)); + } + } + + #[test] + fn raw_input_framer_reassembles_split_color_scheme_report() { + let mut framer = RawInputFramer::default(); + + assert!(framer.push(b"\x1b[?997;").is_empty()); + let events = framer.push(b"1n"); + + assert_eq!(events.len(), 1); + assert!(matches!( + events[0], + RawInputEvent::HostColorSchemeChanged(HostAppearance::Dark) + )); + } + + #[test] + fn split_color_scheme_timeout_does_not_swallow_legacy_alt_bracket() { + let mut framer = RawInputByteFramer::default(); + + assert!(framer.push(b"\x1b[").is_empty()); + assert_eq!(framer.flush_timeout(), vec![b"\x1b[".to_vec()]); + } + + #[test] + fn raw_input_byte_framer_discards_timed_out_split_color_scheme_report_tail() { + let mut framer = RawInputByteFramer::default(); + + assert!(framer.push(b"\x1b[?997;").is_empty()); + assert!(framer.flush_timeout().is_empty()); + assert!(framer.push(b"1n").is_empty()); + assert_eq!(framer.push(b"a"), vec![b"a".to_vec()]); + assert!(framer.flush_timeout().is_empty()); + } + + #[test] fn parses_xterm_alt_up_arrow() { let (RawInputEvent::Key(key), consumed) = extract_one_event(b"\x1b[1;3A").unwrap() else { panic!("expected key"); @@ -1800,6 +1940,75 @@ mod tests { } #[test] + fn default_byte_framer_does_not_rearm_after_color_scheme_report() { + let mut framer = RawInputByteFramer::default(); + + assert_eq!( + framer.push(GHOSTTY_COLOR_SCHEME_DARK_REPORT), + vec![GHOSTTY_COLOR_SCHEME_DARK_REPORT.to_vec()] + ); + assert!(framer.push(b"\x1b").is_empty()); + assert_eq!(framer.flush_timeout(), vec![b"\x1b".to_vec()]); + } + + #[test] + fn opt_in_does_not_delay_plain_escape_without_color_scheme_report() { + let mut framer = RawInputByteFramer::default(); + framer.enable_host_color_scheme_change_tracking(); + + assert!(framer.push(b"\x1b").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(); + + assert_eq!( + framer.push(GHOSTTY_COLOR_SCHEME_DARK_REPORT), + vec![GHOSTTY_COLOR_SCHEME_DARK_REPORT.to_vec()] + ); + + assert!(framer.push(b"\x1b").is_empty()); + assert!(framer.flush_timeout().is_empty()); + let chunks = framer.push(b"]10;#abcdef\x07"); + assert_eq!(chunks.len(), 1); + let (event, _) = extract_one_event(&chunks[0]).unwrap(); + assert!(matches!( + event, + RawInputEvent::HostDefaultColor { + kind: DefaultColorKind::Foreground, + color: RgbColor { + r: 0xab, + g: 0xcd, + b: 0xef + } + } + )); + + assert!(framer.push(b"\x1b").is_empty()); + assert!(framer.flush_timeout().is_empty()); + let chunks = framer.push(b"]11;#123456\x07"); + assert_eq!(chunks.len(), 1); + let (event, _) = extract_one_event(&chunks[0]).unwrap(); + assert!(matches!( + event, + RawInputEvent::HostDefaultColor { + kind: DefaultColorKind::Background, + color: RgbColor { + r: 0x12, + g: 0x34, + b: 0x56 + } + } + )); + + assert!(framer.push(b"\x1b").is_empty()); + assert_eq!(framer.flush_timeout(), vec![b"\x1b".to_vec()]); + } + + #[test] fn flushes_lone_escape_when_not_awaiting_host_color_reply() { let mut framer = RawInputByteFramer::default(); |
