aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-25 22:08:11 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-25 22:08:11 +0300
commit292a98a91e35662fd65993956278787ee0ca9840 (patch)
treeab14f0f075a5c92dab32373678a4e213c95bb2b0 /src
parentdb1ef28d64279b290b882eea9a7a655dd78d08c9 (diff)
fix: answer osc 12 cursor color queries
refs #806
Diffstat (limited to 'src')
-rw-r--r--src/ghostty/mod.rs31
-rw-r--r--src/pane/osc.rs6
-rw-r--r--src/pane/terminal.rs155
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();