//! Runtime colours: the foreground/background, the 256-entry palette, and the //! selection/search/cursor accents. Seeded from config and mutated at runtime //! by OSC colour escapes (4/104, 10/11, 17/19, and their resets). use crate::config::Colors as ColorConfig; /// An 8-bit-per-channel RGB colour. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Rgb(pub u8, pub u8, pub u8); /// Where a `Color::Default` resolves to. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Plane { Fg, Bg, } /// The active colour scheme. #[derive(Clone, Debug)] pub struct Theme { pub fg: Rgb, pub bg: Rgb, /// Cursor body colour; `None` follows the cell under the cursor. pub cursor: Option, pub selection_fg: Option, pub selection_bg: Rgb, pub match_bg: Rgb, pub current_match_bg: Rgb, pub search_bar_bg: Rgb, pub palette: [Rgb; 256], pub bold_as_bright: bool, /// Background opacity, 0 (transparent) - 255 (opaque). pub alpha: u8, /// Configured defaults, restored by OSC reset escapes (110/111/104). default_fg: Rgb, default_bg: Rgb, default_palette: [Rgb; 256], } impl Default for Theme { fn default() -> Self { let palette = default_palette(); Self { fg: DEFAULT_FG, bg: DEFAULT_BG, cursor: None, selection_fg: None, selection_bg: SELECTION_BG, match_bg: MATCH_BG, current_match_bg: CURRENT_MATCH_BG, search_bar_bg: SEARCH_BAR_BG, palette, bold_as_bright: true, alpha: 255, default_fg: DEFAULT_FG, default_bg: DEFAULT_BG, default_palette: palette, } } } impl Theme { /// Build a theme from the `[colors]` config table, falling back to the /// built-in defaults for any unset entry. pub fn from_config(c: &ColorConfig) -> Self { let mut theme = Self::default(); let set = |dst: &mut Rgb, spec: &Option| { if let Some(rgb) = spec.as_deref().and_then(parse_color) { *dst = rgb; } }; set(&mut theme.fg, &c.foreground); set(&mut theme.bg, &c.background); set(&mut theme.selection_bg, &c.selection_background); set(&mut theme.match_bg, &c.match_background); set(&mut theme.current_match_bg, &c.match_current_background); theme.cursor = c.cursor.as_deref().and_then(parse_color); theme.selection_fg = c.selection_foreground.as_deref().and_then(parse_color); if let Some(regular) = &c.regular { apply_palette(&mut theme.palette, 0, regular); } if let Some(bright) = &c.bright { apply_palette(&mut theme.palette, 8, bright); } if let Some(a) = c.alpha { theme.alpha = (a.clamp(0.0, 1.0) * 255.0).round() as u8; } if let Some(b) = c.bold_as_bright { theme.bold_as_bright = b; } // Remember the resolved scheme so OSC resets return here, not to the // compiled-in defaults. theme.default_fg = theme.fg; theme.default_bg = theme.bg; theme.default_palette = theme.palette; theme } /// Resolve a cell colour to RGB on the given plane, applying bold-as-bright /// to the low 8 palette indices. pub fn resolve(&self, color: crate::grid::Color, plane: Plane, bold: bool) -> Rgb { use crate::grid::Color; match color { Color::Default => match plane { Plane::Fg => self.fg, Plane::Bg => self.bg, }, Color::Indexed(i) => { let idx = if bold && self.bold_as_bright && i < 8 { i + 8 } else { i }; self.palette[idx as usize] } Color::Rgb(r, g, b) => Rgb(r, g, b), } } pub fn set_palette(&mut self, index: u8, rgb: Rgb) { self.palette[index as usize] = rgb; } /// Reset palette entry `index` to its configured value. pub fn reset_palette_index(&mut self, index: u8) { self.palette[index as usize] = self.default_palette[index as usize]; } /// Reset the whole palette to its configured values. pub fn reset_palette(&mut self) { self.palette = self.default_palette; } pub fn reset_fg(&mut self) { self.fg = self.default_fg; } pub fn reset_bg(&mut self) { self.bg = self.default_bg; } /// A copy with foreground and background swapped, for the visual bell flash. pub fn inverted(&self) -> Self { let mut t = self.clone(); std::mem::swap(&mut t.fg, &mut t.bg); t } } /// Foreground/background used for `Color::Default`. const DEFAULT_FG: Rgb = Rgb(0xc5, 0xc8, 0xc6); const DEFAULT_BG: Rgb = Rgb(0x18, 0x18, 0x18); const SELECTION_BG: Rgb = Rgb(0x44, 0x47, 0x5a); const MATCH_BG: Rgb = Rgb(0x5a, 0x51, 0x2a); const CURRENT_MATCH_BG: Rgb = Rgb(0xb8, 0x8a, 0x2a); const SEARCH_BAR_BG: Rgb = Rgb(0x30, 0x30, 0x40); /// Overwrite palette entries `[base, base+specs.len())` from hex specs. fn apply_palette(palette: &mut [Rgb; 256], base: usize, specs: &[String]) { for (i, spec) in specs.iter().take(8).enumerate() { if let Some(rgb) = parse_color(spec) { palette[base + i] = rgb; } } } /// The xterm 256-colour palette: 16 base, a 6×6×6 cube, then 24 greys. fn default_palette() -> [Rgb; 256] { const BASE: [Rgb; 16] = [ Rgb(0x00, 0x00, 0x00), Rgb(0xcd, 0x00, 0x00), Rgb(0x00, 0xcd, 0x00), Rgb(0xcd, 0xcd, 0x00), Rgb(0x00, 0x00, 0xee), Rgb(0xcd, 0x00, 0xcd), Rgb(0x00, 0xcd, 0xcd), Rgb(0xe5, 0xe5, 0xe5), Rgb(0x7f, 0x7f, 0x7f), Rgb(0xff, 0x00, 0x00), Rgb(0x00, 0xff, 0x00), Rgb(0xff, 0xff, 0x00), Rgb(0x5c, 0x5c, 0xff), Rgb(0xff, 0x00, 0xff), Rgb(0x00, 0xff, 0xff), Rgb(0xff, 0xff, 0xff), ]; let cube = |step: u8| -> u8 { if step == 0 { 0 } else { 55 + 40 * step } }; std::array::from_fn(|i| match i { 0..=15 => BASE[i], 16..=231 => { let i = i as u8 - 16; Rgb(cube(i / 36), cube((i / 6) % 6), cube(i % 6)) } _ => { let v = 8 + 10 * (i as u8 - 232); Rgb(v, v, v) } }) } /// Parse an X11 colour spec: `rgb:rr/gg/bb` (1-4 hex digits per channel) or /// `#rrggbb`. pub fn parse_color(spec: &str) -> Option { if let Some(rest) = spec.strip_prefix("rgb:") { let mut it = rest.split('/'); let chan = |s: &str| -> Option { let v = u32::from_str_radix(s, 16).ok()?; let max = (1u32 << (4 * s.len() as u32)) - 1; Some((v * 255 / max) as u8) }; return Some(Rgb(chan(it.next()?)?, chan(it.next()?)?, chan(it.next()?)?)); } let hex = spec.strip_prefix('#')?; if hex.len() == 6 { let byte = |i: usize| u8::from_str_radix(&hex[i..i + 2], 16).ok(); return Some(Rgb(byte(0)?, byte(2)?, byte(4)?)); } None } #[cfg(test)] mod tests { use super::*; #[test] fn parses_color_forms() { assert_eq!(parse_color("#ff0080"), Some(Rgb(255, 0, 128))); assert_eq!(parse_color("rgb:ff/00/80"), Some(Rgb(255, 0, 128))); assert_eq!(parse_color("rgb:ffff/0000/8080"), Some(Rgb(255, 0, 128))); assert_eq!(parse_color("nope"), None); } #[test] fn palette_cube_and_grey() { let p = default_palette(); assert_eq!(p[16], Rgb(0, 0, 0)); assert_eq!(p[231], Rgb(255, 255, 255)); assert_eq!(p[232], Rgb(8, 8, 8)); } #[test] fn config_overrides_and_resets() { let cfg = ColorConfig { background: Some("#000000".to_string()), regular: Some(vec!["#111111".to_string()]), ..ColorConfig::default() }; let mut theme = Theme::from_config(&cfg); assert_eq!(theme.bg, Rgb(0, 0, 0)); assert_eq!(theme.palette[0], Rgb(0x11, 0x11, 0x11)); // OSC reset returns to the configured value, not the compiled default. theme.set_palette(0, Rgb(9, 9, 9)); theme.reset_palette_index(0); assert_eq!(theme.palette[0], Rgb(0x11, 0x11, 0x11)); } }