git.lucas.co / cce-terminal
terminal emulator
git clone https://git.lucas.co/cce-terminal.git

src/colors.rs (7.5K)

  1 //! Terminal color resolution: the configurable palette (16 ANSI + named
  2 //! specials, from the `terminal.colors` KDL section) plus the computed
  3 //! 6×6×6 cube and grayscale ramp, and the mapping from a cell's
  4 //! `vte::ansi::Color` through runtime overrides (OSC 4/10/11) to concrete RGB.
  5 
  6 use alacritty_terminal::term::color::Colors;
  7 use alacritty_terminal::vte::ansi::{Color, NamedColor, Rgb};
  8 
  9 pub const FOREGROUND: Rgb = Rgb { r: 0xd8, g: 0xd8, b: 0xde };
 10 pub const BACKGROUND: Rgb = Rgb { r: 0x22, g: 0x26, b: 0x2e };
 11 const SELECTION: Rgb = Rgb { r: 0xc8, g: 0xd0, b: 0xe0 };
 12 
 13 /// Base16 default-dark ANSI colors (alacritty's classic defaults) — muted
 14 /// enough to sit on the DE plate.
 15 const ANSI: [Rgb; 16] = [
 16     Rgb { r: 0x18, g: 0x18, b: 0x18 }, // black
 17     Rgb { r: 0xac, g: 0x42, b: 0x42 }, // red
 18     Rgb { r: 0x90, g: 0xa9, b: 0x59 }, // green
 19     Rgb { r: 0xf4, g: 0xbf, b: 0x75 }, // yellow
 20     Rgb { r: 0x6a, g: 0x9f, b: 0xb5 }, // blue
 21     Rgb { r: 0xaa, g: 0x75, b: 0x9f }, // magenta
 22     Rgb { r: 0x75, g: 0xb5, b: 0xaa }, // cyan
 23     Rgb { r: 0xd8, g: 0xd8, b: 0xd8 }, // white
 24     Rgb { r: 0x6b, g: 0x6b, b: 0x6b }, // bright black
 25     Rgb { r: 0xc5, g: 0x55, b: 0x55 }, // bright red
 26     Rgb { r: 0xaa, g: 0xc4, b: 0x74 }, // bright green
 27     Rgb { r: 0xfe, g: 0xca, b: 0x88 }, // bright yellow
 28     Rgb { r: 0x82, g: 0xb8, b: 0xc8 }, // bright blue
 29     Rgb { r: 0xc2, g: 0x8c, b: 0xb8 }, // bright magenta
 30     Rgb { r: 0x93, g: 0xd3, b: 0xc3 }, // bright cyan
 31     Rgb { r: 0xf8, g: 0xf8, b: 0xf8 }, // bright white
 32 ];
 33 
 34 fn scale(rgb: Rgb, f: f32) -> Rgb {
 35     Rgb {
 36         r: (rgb.r as f32 * f) as u8,
 37         g: (rgb.g as f32 * f) as u8,
 38         b: (rgb.b as f32 * f) as u8,
 39     }
 40 }
 41 
 42 /// The user-configurable colors: the 16 ANSI slots plus the named specials
 43 /// and the selection highlight. Loaded from the `terminal.colors` KDL
 44 /// section; every field falls back to the compiled default.
 45 #[derive(Clone, Copy, PartialEq)]
 46 pub struct Palette {
 47     pub foreground: Rgb,
 48     pub background: Rgb,
 49     pub cursor: Rgb,
 50     pub selection: Rgb,
 51     pub ansi: [Rgb; 16],
 52 }
 53 
 54 impl Default for Palette {
 55     fn default() -> Self {
 56         Palette {
 57             foreground: FOREGROUND,
 58             background: BACKGROUND,
 59             cursor: FOREGROUND,
 60             selection: SELECTION,
 61             ansi: ANSI,
 62         }
 63     }
 64 }
 65 
 66 const ANSI_NAMES: [&str; 16] = [
 67     "black",
 68     "red",
 69     "green",
 70     "yellow",
 71     "blue",
 72     "magenta",
 73     "cyan",
 74     "white",
 75     "bright_black",
 76     "bright_red",
 77     "bright_green",
 78     "bright_yellow",
 79     "bright_blue",
 80     "bright_magenta",
 81     "bright_cyan",
 82     "bright_white",
 83 ];
 84 
 85 fn config_rgb(key: &str) -> Option<Rgb> {
 86     let hex = cce_ui::config::get_string(&format!("/terminal/colors/{key}"))?;
 87     let [r, g, b, _] = cce_ui::color::parse_hex_bytes(&hex)?;
 88     Some(Rgb { r, g, b })
 89 }
 90 
 91 impl Palette {
 92     /// The compiled defaults with any `terminal.colors` config entries
 93     /// (shared config.kdl merged with the per-app override) applied on top.
 94     pub fn from_config() -> Self {
 95         let mut palette = Palette::default();
 96         if let Some(rgb) = config_rgb("foreground") {
 97             palette.foreground = rgb;
 98             // Cursor tracks the foreground unless set explicitly.
 99             palette.cursor = rgb;
100         }
101         if let Some(rgb) = config_rgb("background") {
102             palette.background = rgb;
103         }
104         if let Some(rgb) = config_rgb("cursor") {
105             palette.cursor = rgb;
106         }
107         if let Some(rgb) = config_rgb("selection") {
108             palette.selection = rgb;
109         }
110         for (slot, name) in palette.ansi.iter_mut().zip(ANSI_NAMES) {
111             if let Some(rgb) = config_rgb(name) {
112                 *slot = rgb;
113             }
114         }
115         palette
116     }
117 }
118 
119 /// Default color for a palette index (0..269), matching alacritty's layout:
120 /// 0–15 ANSI, 16–231 the xterm 6×6×6 cube, 232–255 the grayscale ramp, then
121 /// the `NamedColor` specials (256 = Foreground …).
122 pub fn default_color(index: usize, palette: &Palette) -> Rgb {
123     match index {
124         0..=15 => palette.ansi[index],
125         16..=231 => {
126             let i = index - 16;
127             let comp = |v: usize| if v == 0 { 0 } else { (55 + v * 40) as u8 };
128             Rgb { r: comp(i / 36), g: comp((i / 6) % 6), b: comp(i % 6) }
129         }
130         232..=255 => {
131             let v = (8 + (index - 232) * 10) as u8;
132             Rgb { r: v, g: v, b: v }
133         }
134         i if i == NamedColor::Foreground as usize => palette.foreground,
135         i if i == NamedColor::Background as usize => palette.background,
136         i if i == NamedColor::Cursor as usize => palette.cursor,
137         i if i == NamedColor::BrightForeground as usize => palette.foreground,
138         i if i == NamedColor::DimForeground as usize => scale(palette.foreground, 0.66),
139         // DimBlack..=DimWhite
140         i if (NamedColor::DimBlack as usize..=NamedColor::DimWhite as usize).contains(&i) => {
141             scale(palette.ansi[i - NamedColor::DimBlack as usize], 0.66)
142         }
143         _ => palette.foreground,
144     }
145 }
146 
147 /// Palette index → RGB through the terminal's runtime overrides.
148 pub fn indexed(index: usize, overrides: &Colors, palette: &Palette) -> Rgb {
149     overrides[index].unwrap_or_else(|| default_color(index, palette))
150 }
151 
152 /// A cell color → RGB. `dim` applies the SGR 2 treatment: named colors route
153 /// to their Dim* palette slots, direct/indexed colors scale by 0.66.
154 pub fn resolve(color: Color, overrides: &Colors, dim: bool, palette: &Palette) -> Rgb {
155     match color {
156         Color::Spec(rgb) => {
157             if dim {
158                 scale(rgb, 0.66)
159             } else {
160                 rgb
161             }
162         }
163         Color::Named(name) => {
164             let name = if dim { name.to_dim() } else { name };
165             indexed(name as usize, overrides, palette)
166         }
167         Color::Indexed(i) => {
168             let rgb = indexed(i as usize, overrides, palette);
169             if dim {
170                 scale(rgb, 0.66)
171             } else {
172                 rgb
173             }
174         }
175     }
176 }
177 
178 #[cfg(test)]
179 mod tests {
180     use super::*;
181 
182     #[test]
183     fn cube_and_gray_ramp() {
184         let p = Palette::default();
185         // 16 = cube (0,0,0); 231 = cube (255,255,255); 244 = mid gray.
186         assert_eq!(default_color(16, &p), Rgb { r: 0, g: 0, b: 0 });
187         assert_eq!(default_color(231, &p), Rgb { r: 255, g: 255, b: 255 });
188         assert_eq!(default_color(196, &p), Rgb { r: 255, g: 0, b: 0 }); // cube (5,0,0)
189         assert_eq!(default_color(244, &p), Rgb { r: 128, g: 128, b: 128 });
190     }
191 
192     #[test]
193     fn named_specials() {
194         let p = Palette::default();
195         assert_eq!(default_color(NamedColor::Foreground as usize, &p), FOREGROUND);
196         assert_eq!(default_color(NamedColor::Background as usize, &p), BACKGROUND);
197         assert_eq!(default_color(NamedColor::DimRed as usize, &p), scale(ANSI[1], 0.66));
198     }
199 
200     #[test]
201     fn resolve_respects_overrides_and_palette() {
202         let mut overrides = Colors::default();
203         let custom = Rgb { r: 1, g: 2, b: 3 };
204         overrides[NamedColor::Red as usize] = Some(custom);
205         let mut p = Palette::default();
206         p.ansi[2] = Rgb { r: 9, g: 9, b: 9 }; // configured green
207         // OSC override wins over the configured palette; palette wins over
208         // the compiled default.
209         assert_eq!(resolve(Color::Named(NamedColor::Red), &overrides, false, &p), custom);
210         assert_eq!(
211             resolve(Color::Named(NamedColor::Green), &overrides, false, &p),
212             Rgb { r: 9, g: 9, b: 9 }
213         );
214         assert_eq!(resolve(Color::Indexed(1), &overrides, false, &p), custom);
215     }
216 }