diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-25 22:08:11 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-25 22:08:11 +0300 |
| commit | 292a98a91e35662fd65993956278787ee0ca9840 (patch) | |
| tree | ab14f0f075a5c92dab32373678a4e213c95bb2b0 /src | |
| parent | db1ef28d64279b290b882eea9a7a655dd78d08c9 (diff) | |
fix: answer osc 12 cursor color queries
refs #806
Diffstat (limited to 'src')
| -rw-r--r-- | src/ghostty/mod.rs | 31 | ||||
| -rw-r--r-- | src/pane/osc.rs | 6 | ||||
| -rw-r--r-- | src/pane/terminal.rs | 155 |
3 files changed, 183 insertions, 9 deletions
diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs index 2b45e3d8..3800ab49 100644 --- a/src/ghostty/mod.rs +++ b/src/ghostty/mod.rs @@ -162,6 +162,10 @@ pub const MODE_MOUSE_SGR_PIXELS: u16 = 1016; pub const MODE_BRACKETED_PASTE: u16 = 2004; pub const MODE_SYNCHRONIZED_OUTPUT: u16 = 2026; pub const MODE_GRAPHEME_CLUSTER: u16 = 2027; +// These are documented in vendor/libghostty-vt/include/ghostty/vt/terminal.h, +// but the generated bindings do not currently expose named constants for them. +const TERMINAL_DATA_COLOR_FOREGROUND: ffi::GhosttyTerminalData = 18; +const TERMINAL_DATA_COLOR_CURSOR: ffi::GhosttyTerminalData = 20; const KITTY_IMAGE_STORAGE_LIMIT_BYTES: u64 = 64 * 1024 * 1024; const APC_MAX_BYTES: usize = 16 * 1024 * 1024; @@ -1006,6 +1010,14 @@ impl Terminal { self.get_u16(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_ROWS) } + pub fn effective_foreground_color(&self) -> Result<Option<RgbColor>, Error> { + self.get_optional_rgb_color(TERMINAL_DATA_COLOR_FOREGROUND) + } + + pub fn effective_cursor_color(&self) -> Result<Option<RgbColor>, Error> { + self.get_optional_rgb_color(TERMINAL_DATA_COLOR_CURSOR) + } + fn width_px(&self) -> Result<u32, Error> { self.get_u32(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_WIDTH_PX) } @@ -1052,6 +1064,25 @@ impl Terminal { Ok(out) } + fn get_optional_rgb_color( + &self, + data: ffi::GhosttyTerminalData, + ) -> Result<Option<RgbColor>, Error> { + let mut out = ffi::GhosttyColorRgb::default(); + let result = unsafe { + ffi::ghostty_terminal_get( + self.raw, + data, + (&mut out as *mut ffi::GhosttyColorRgb).cast(), + ) + }; + match result { + ffi::GhosttyResult_GHOSTTY_SUCCESS => Ok(Some(out.into())), + ffi::GhosttyResult_GHOSTTY_NO_VALUE => Ok(None), + other => Err(Error(other)), + } + } + pub fn kitty_image_placements(&self) -> Result<Vec<KittyImagePlacement>, Error> { self.kitty_image_placements_with_data_filter(|_| true) } diff --git a/src/pane/osc.rs b/src/pane/osc.rs index 83cb47ae..2fef72a2 100644 --- a/src/pane/osc.rs +++ b/src/pane/osc.rs @@ -11,6 +11,7 @@ use super::terminal::GhosttyPaneCore; pub(super) enum DefaultColorQuery { Foreground, Background, + Cursor, } impl DefaultColorQuery { @@ -18,6 +19,7 @@ impl DefaultColorQuery { match self { Self::Foreground => 10, Self::Background => 11, + Self::Cursor => 12, } } } @@ -247,6 +249,7 @@ 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"12;?" => Some(DefaultColorEvent::Query(DefaultColorQuery::Cursor)), b"110" | b"110;" => Some(DefaultColorEvent::Reset(DefaultColorQuery::Foreground)), b"111" | b"111;" => Some(DefaultColorEvent::Reset(DefaultColorQuery::Background)), _ => parse_palette_color_query(body).or_else(|| parse_default_color_set_event(body)), @@ -1288,7 +1291,7 @@ mod tests { let mut tracker = DefaultColorEventTracker::default(); tracker.observe( - b"\x1b]10;?\x07\x1b]11;?\x1b\\\x1b]4;0;?\x07\x1b]10;rgb:11/22/33\x07\x1b]111\x07", + b"\x1b]10;?\x07\x1b]11;?\x1b\\\x1b]12;?\x07\x1b]4;0;?\x07\x1b]10;rgb:11/22/33\x07\x1b]111\x07", ); assert_eq!( @@ -1296,6 +1299,7 @@ mod tests { vec![ DefaultColorEvent::Query(DefaultColorQuery::Foreground), DefaultColorEvent::Query(DefaultColorQuery::Background), + DefaultColorEvent::Query(DefaultColorQuery::Cursor), DefaultColorEvent::PaletteQuery(0), DefaultColorEvent::Set(DefaultColorQuery::Foreground), DefaultColorEvent::Reset(DefaultColorQuery::Background), diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index e573f958..a479e5ad 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -1974,14 +1974,20 @@ fn respond_to_default_color_event( } } -fn default_color_query_response(query: DefaultColorQuery, core: &GhosttyPaneCore) -> Option<Bytes> { +fn default_color_query_response( + query: DefaultColorQuery, + core: &mut 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 - } + DefaultColorQuery::Foreground if !core.child_default_foreground_changed => core + .host_terminal_theme + .foreground + .map(host_theme_color_to_ghostty), + DefaultColorQuery::Background if !core.child_default_background_changed => core + .host_terminal_theme + .background + .map(host_theme_color_to_ghostty), + DefaultColorQuery::Cursor => cursor_color_query_color(core), _ => None, }?; Some(osc_rgb_response( @@ -1992,6 +1998,24 @@ fn default_color_query_response(query: DefaultColorQuery, core: &GhosttyPaneCore )) } +fn cursor_color_query_color(core: &mut GhosttyPaneCore) -> Option<crate::ghostty::RgbColor> { + let host_foreground = core.host_terminal_theme.foreground; + let child_foreground_changed = core.child_default_foreground_changed; + core.terminal + .effective_cursor_color() + .ok() + .flatten() + .or_else(|| { + if child_foreground_changed { + core.terminal.effective_foreground_color().ok().flatten() + } else { + host_foreground + .map(host_theme_color_to_ghostty) + .or_else(|| core.terminal.effective_foreground_color().ok().flatten()) + } + }) +} + fn palette_color_query_response(index: u8, core: &mut GhosttyPaneCore) -> Option<Bytes> { let GhosttyPaneCore { terminal, @@ -2016,6 +2040,14 @@ fn osc_rgb_response(command: &str, r: u8, g: u8, b: u8) -> Bytes { Bytes::from(format!("\x1b]{command};rgb:{r:04x}/{g:04x}/{b:04x}\x1b\\")) } +fn host_theme_color_to_ghostty(color: crate::terminal_theme::RgbColor) -> crate::ghostty::RgbColor { + crate::ghostty::RgbColor { + r: color.r, + g: color.g, + b: color.b, + } +} + fn apply_cached_host_default_color(core: &mut GhosttyPaneCore, query: DefaultColorQuery) { write_host_terminal_theme_selective( &mut core.terminal, @@ -2033,6 +2065,7 @@ fn mark_child_default_color_changed( match query { DefaultColorQuery::Foreground => core.child_default_foreground_changed = changed, DefaultColorQuery::Background => core.child_default_background_changed = changed, + DefaultColorQuery::Cursor => {} } } @@ -3766,6 +3799,80 @@ mod tests { } #[test] + fn process_pty_bytes_returns_cursor_color_query_response_from_foreground_fallback() { + 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: None, + }); + + let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]12;?\x07", &tx); + + assert_eq!( + result.terminal_responses, + vec![Bytes::from_static(b"\x1b]12;rgb:6565/7b7b/8383\x1b\\")] + ); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn process_pty_bytes_returns_cursor_color_query_response_from_child_foreground() { + 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: None, + }); + + pane.process_pty_bytes(pane_id, 0, b"\x1b]10;rgb:11/22/33\x07", &tx); + let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]12;?\x07", &tx); + + assert_eq!( + result.terminal_responses, + vec![Bytes::from_static(b"\x1b]12;rgb:1111/2222/3333\x1b\\")] + ); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn process_pty_bytes_returns_explicit_cursor_color_query_response() { + 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: None, + }); + + pane.process_pty_bytes(pane_id, 0, b"\x1b]12;rgb:11/22/33\x07", &tx); + let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]12;?\x07", &tx); + + assert_eq!( + result.terminal_responses, + vec![Bytes::from_static(b"\x1b]12;rgb:1111/2222/3333\x1b\\")] + ); + assert!(rx.try_recv().is_err()); + } + + #[test] fn process_pty_bytes_returns_default_color_query_responses_in_order() { let (tx, mut rx) = mpsc::channel(4); let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); @@ -3784,13 +3891,15 @@ mod tests { }), }); - let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]10;?\x07\x1b]11;?\x07", &tx); + let result = + pane.process_pty_bytes(pane_id, 0, b"\x1b]10;?\x07\x1b]11;?\x07\x1b]12;?\x07", &tx); assert_eq!( result.terminal_responses, vec![ Bytes::from_static(b"\x1b]10;rgb:6565/7b7b/8383\x1b\\"), Bytes::from_static(b"\x1b]11;rgb:fdfd/f6f6/e3e3\x1b\\"), + Bytes::from_static(b"\x1b]12;rgb:6565/7b7b/8383\x1b\\"), ] ); assert!(rx.try_recv().is_err()); @@ -3827,6 +3936,36 @@ mod tests { } #[test] + fn process_pty_bytes_returns_split_cursor_color_query_response() { + 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: 0xfd, + g: 0xf6, + b: 0xe3, + }), + background: None, + }); + + let result = pane.process_pty_bytes(pane_id, 0, b"\x1b]12", &tx); + assert!(result.terminal_responses.is_empty()); + assert!(rx.try_recv().is_err()); + let result = pane.process_pty_bytes(pane_id, 0, b";?\x1b", &tx); + assert!(result.terminal_responses.is_empty()); + assert!(rx.try_recv().is_err()); + let result = pane.process_pty_bytes(pane_id, 0, b"\\", &tx); + + assert_eq!( + result.terminal_responses, + vec![Bytes::from_static(b"\x1b]12;rgb:fdfd/f6f6/e3e3\x1b\\")] + ); + assert!(rx.try_recv().is_err()); + } + + #[test] fn process_pty_bytes_tracks_default_color_set_and_reset_before_replying() { let (tx, mut rx) = mpsc::channel(4); let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); |
