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 }