aboutsummaryrefslogtreecommitdiff
path: root/src/terminal_theme.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-05 03:46:16 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 03:46:16 +0300
commit8525503dba9029d0a5f24990e026d8164f0bf39c (patch)
tree43b902dfab49603c667ae016baa97aee27724073 /src/terminal_theme.rs
parenta08dfaef2f13873fc85dc9e1357235b8b0703a46 (diff)
fix: match ghostty pane default colors to host terminal theme
Diffstat (limited to 'src/terminal_theme.rs')
-rw-r--r--src/terminal_theme.rs143
1 files changed, 143 insertions, 0 deletions
diff --git a/src/terminal_theme.rs b/src/terminal_theme.rs
new file mode 100644
index 00000000..e0b4b9d5
--- /dev/null
+++ b/src/terminal_theme.rs
@@ -0,0 +1,143 @@
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub struct RgbColor {
+ pub r: u8,
+ pub g: u8,
+ pub b: u8,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub struct TerminalTheme {
+ pub foreground: Option<RgbColor>,
+ pub background: Option<RgbColor>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum DefaultColorKind {
+ Foreground,
+ Background,
+}
+
+pub const HOST_COLOR_QUERY_SEQUENCE: &str = "\x1b]10;?\x1b\\\x1b]11;?\x1b\\";
+
+impl TerminalTheme {
+ pub fn with_color(mut self, kind: DefaultColorKind, color: RgbColor) -> Self {
+ match kind {
+ DefaultColorKind::Foreground => self.foreground = Some(color),
+ DefaultColorKind::Background => self.background = Some(color),
+ }
+ self
+ }
+
+ pub fn is_empty(self) -> bool {
+ self.foreground.is_none() && self.background.is_none()
+ }
+}
+
+pub fn parse_default_color_response(sequence: &str) -> Option<(DefaultColorKind, RgbColor)> {
+ let body = sequence.strip_prefix("\x1b]")?;
+ let body = body
+ .strip_suffix("\x1b\\")
+ .or_else(|| body.strip_suffix('\u{7}'))?;
+ let (command, value) = body.split_once(';')?;
+ let kind = match command {
+ "10" => DefaultColorKind::Foreground,
+ "11" => DefaultColorKind::Background,
+ _ => return None,
+ };
+ Some((kind, parse_rgb_color(value)?))
+}
+
+#[cfg_attr(not(feature = "ghostty-vt"), allow(dead_code))]
+pub fn osc_set_default_color_sequence(kind: DefaultColorKind, color: RgbColor) -> String {
+ let command = match kind {
+ DefaultColorKind::Foreground => 10,
+ DefaultColorKind::Background => 11,
+ };
+ format!(
+ "\x1b]{command};rgb:{:02x}/{:02x}/{:02x}\x1b\\",
+ color.r, color.g, color.b
+ )
+}
+
+fn parse_rgb_color(value: &str) -> Option<RgbColor> {
+ if let Some(rgb) = value.strip_prefix("rgb:") {
+ let mut parts = rgb.split('/');
+ return Some(RgbColor {
+ r: parse_hex_component(parts.next()?)?,
+ g: parse_hex_component(parts.next()?)?,
+ b: parse_hex_component(parts.next()?)?,
+ })
+ .filter(|_| parts.next().is_none());
+ }
+
+ if let Some(hex) = value.strip_prefix('#') {
+ let digits = hex.len() / 3;
+ if !matches!(digits, 1..=4) || hex.len() != digits * 3 {
+ return None;
+ }
+ return Some(RgbColor {
+ r: parse_hex_component(&hex[..digits])?,
+ g: parse_hex_component(&hex[digits..digits * 2])?,
+ b: parse_hex_component(&hex[digits * 2..])?,
+ });
+ }
+
+ None
+}
+
+fn parse_hex_component(component: &str) -> Option<u8> {
+ if component.is_empty()
+ || component.len() > 4
+ || !component.chars().all(|ch| ch.is_ascii_hexdigit())
+ {
+ return None;
+ }
+ let value = u32::from_str_radix(component, 16).ok()?;
+ let max = (1u32 << (component.len() * 4)) - 1;
+ Some(((value * 255 + (max / 2)) / max) as u8)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parses_st_terminated_rgb_response() {
+ let parsed = parse_default_color_response("\x1b]10;rgb:cccc/dddd/eeee\x1b\\");
+ assert_eq!(
+ parsed,
+ Some((
+ DefaultColorKind::Foreground,
+ RgbColor {
+ r: 0xcc,
+ g: 0xdd,
+ b: 0xee,
+ },
+ ))
+ );
+ }
+
+ #[test]
+ fn parses_bel_terminated_hash_response() {
+ let parsed = parse_default_color_response("\x1b]11;#123456\u{7}");
+ assert_eq!(
+ parsed,
+ Some((
+ DefaultColorKind::Background,
+ RgbColor {
+ r: 0x12,
+ g: 0x34,
+ b: 0x56,
+ },
+ ))
+ );
+ }
+
+ #[test]
+ fn scales_short_hex_components() {
+ assert_eq!(parse_hex_component("f"), Some(255));
+ assert_eq!(parse_hex_component("80"), Some(128));
+ assert_eq!(parse_hex_component("800"), Some(128));
+ assert_eq!(parse_hex_component("8000"), Some(128));
+ }
+}