Skip to main content

gpui_cn/terminal/
theme.rs

1//! Ghostty theme files.
2//!
3//! A Ghostty theme is a config file with only color keys, such as the files
4//! under `<Ghostty resources>/themes` or `~/.config/ghostty/themes`. The
5//! default [`TerminalColors`] are Ghostty's defaults, so a host that loads
6//! no theme looks like a stock Ghostty.
7
8use ghostty_vt::style::{Palette, RgbColor};
9
10use crate::terminal::frame::{Rgb, TerminalColors};
11
12/// A line a theme file could not be read from.
13#[derive(Clone, Debug, PartialEq, Eq)]
14#[non_exhaustive]
15pub struct ThemeError {
16    line: usize,
17    text: String,
18}
19
20impl ThemeError {
21    /// The 1-based line number.
22    pub fn line(&self) -> usize {
23        self.line
24    }
25
26    /// The offending line.
27    pub fn text(&self) -> &str {
28        &self.text
29    }
30}
31
32impl std::fmt::Display for ThemeError {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        write!(f, "line {}: cannot parse {:?}", self.line, self.text)
35    }
36}
37
38impl std::error::Error for ThemeError {}
39
40fn rgb(color: RgbColor) -> Rgb {
41    Rgb(color.r, color.g, color.b)
42}
43
44impl TerminalColors {
45    /// Parse a Ghostty theme, starting from the defaults.
46    ///
47    /// Recognized keys: `palette`, `background`, `foreground`,
48    /// `cursor-color`, `selection-background`, `selection-foreground`.
49    /// Other keys are ignored so a full Ghostty config can be given too.
50    /// Special values such as `cell-foreground` leave the default in place.
51    pub fn from_ghostty_theme(text: &str) -> Result<Self, ThemeError> {
52        let mut colors = Self::default();
53        let mut palette = Palette(colors.palette.map(|c| RgbColor {
54            r: c.0,
55            g: c.1,
56            b: c.2,
57        }));
58        for (index, raw) in text.lines().enumerate() {
59            let line = raw.trim();
60            if line.is_empty() || line.starts_with('#') {
61                continue;
62            }
63            let Some((key, value)) = line.split_once('=') else {
64                continue;
65            };
66            let key = key.trim();
67            let value = value.trim();
68            let error = || ThemeError {
69                line: index + 1,
70                text: raw.to_owned(),
71            };
72            let color = || RgbColor::parse(value).ok();
73            match key {
74                "palette" => {
75                    let (slot, color) = Palette::parse_palette_entry(value).map_err(|_| error())?;
76                    palette.set(slot, color);
77                }
78                "background" => colors.background = rgb(color().ok_or_else(error)?),
79                "foreground" => colors.foreground = rgb(color().ok_or_else(error)?),
80                "cursor-color" => colors.cursor = color().map(rgb),
81                "selection-background" => colors.selection_background = color().map(rgb),
82                "selection-foreground" => colors.selection_foreground = color().map(rgb),
83                _ => {}
84            }
85        }
86        colors.palette = palette.0.map(rgb);
87        Ok(colors)
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn a_ghostty_theme_sets_palette_and_special_colors() {
97        let theme = "\
98# Dracula
99palette = 0=#21222c
100palette = 1=#ff5555
101background = #282a36
102foreground = f8f8f2
103cursor-color = #f8f8f2
104cursor-text = #282a36
105selection-background = cell-foreground
106window-padding-x = 10
107";
108        let colors = TerminalColors::from_ghostty_theme(theme).unwrap();
109        assert_eq!(colors.palette[0], Rgb(0x21, 0x22, 0x2c));
110        assert_eq!(colors.palette[1], Rgb(0xff, 0x55, 0x55));
111        assert_eq!(colors.palette[2], TerminalColors::default().palette[2]);
112        assert_eq!(colors.background, Rgb(0x28, 0x2a, 0x36));
113        assert_eq!(colors.foreground, Rgb(0xf8, 0xf8, 0xf2));
114        assert_eq!(colors.cursor, Some(Rgb(0xf8, 0xf8, 0xf2)));
115        assert_eq!(colors.selection_background, None);
116    }
117
118    #[test]
119    fn a_bad_line_reports_its_number() {
120        let error =
121            TerminalColors::from_ghostty_theme("background = #fff\npalette = x\n").unwrap_err();
122        assert_eq!(error.line(), 2);
123    }
124}