Skip to main content

caixa_theme/
blackmatter.rs

1//! Blackmatter overlays — the chosen Nord→Semantic mapping.
2//!
3//! The default overlay is `blackmatter_dark`, which matches blackmatter-nvim
4//! and blackmatter-shell. Light and high-contrast overlays are provided so a
5//! caller can pick at runtime.
6
7use crate::palette::{Nord, Rgb};
8use crate::style::Semantic;
9
10#[derive(Debug, Clone, Copy)]
11pub struct Theme {
12    pub name: &'static str,
13    resolver: fn(Semantic) -> Rgb,
14}
15
16impl Theme {
17    #[must_use]
18    pub fn blackmatter_dark() -> Self {
19        Self {
20            name: "blackmatter-dark",
21            resolver: blackmatter_dark_color,
22        }
23    }
24
25    #[must_use]
26    pub fn blackmatter_light() -> Self {
27        Self {
28            name: "blackmatter-light",
29            resolver: blackmatter_light_color,
30        }
31    }
32
33    #[must_use]
34    pub fn color(&self, s: Semantic) -> Rgb {
35        (self.resolver)(s)
36    }
37
38    #[must_use]
39    pub fn ansi(&self, s: Semantic) -> String {
40        self.color(s).fg_ansi()
41    }
42
43    #[must_use]
44    pub fn paint(&self, s: Semantic, text: &str) -> String {
45        format!("{}{}{}", self.ansi(s), text, crate::palette::ANSI_RESET)
46    }
47}
48
49impl Default for Theme {
50    fn default() -> Self {
51        Self::blackmatter_dark()
52    }
53}
54
55fn blackmatter_dark_color(s: Semantic) -> Rgb {
56    match s {
57        Semantic::Keyword => Nord::NORD9,
58        Semantic::Symbol => Nord::NORD4,
59        Semantic::KeywordArg => Nord::NORD8,
60        Semantic::String => Nord::NORD14,
61        Semantic::Number => Nord::NORD15,
62        Semantic::Literal => Nord::NORD13,
63        Semantic::Comment => Nord::NORD3,
64        Semantic::Accent => Nord::NORD8,
65        Semantic::Muted => Nord::NORD3,
66        Semantic::Error => Nord::NORD11,
67        Semantic::Warning => Nord::NORD12,
68        Semantic::Info => Nord::NORD8,
69        Semantic::Hint => Nord::NORD13,
70        Semantic::Added => Nord::NORD14,
71        Semantic::Removed => Nord::NORD11,
72        Semantic::Unchanged => Nord::NORD4,
73    }
74}
75
76fn blackmatter_light_color(s: Semantic) -> Rgb {
77    // Invert background-assuming choices for readability on light terminals.
78    match s {
79        Semantic::Keyword => Nord::NORD10,
80        Semantic::Symbol => Nord::NORD0,
81        Semantic::KeywordArg => Nord::NORD10,
82        Semantic::String => Nord::NORD14,
83        Semantic::Number => Nord::NORD15,
84        Semantic::Literal => Nord::NORD12,
85        Semantic::Comment => Nord::NORD2,
86        Semantic::Accent => Nord::NORD10,
87        Semantic::Muted => Nord::NORD2,
88        Semantic::Error => Nord::NORD11,
89        Semantic::Warning => Nord::NORD12,
90        Semantic::Info => Nord::NORD10,
91        Semantic::Hint => Nord::NORD13,
92        Semantic::Added => Nord::NORD14,
93        Semantic::Removed => Nord::NORD11,
94        Semantic::Unchanged => Nord::NORD0,
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn default_is_dark() {
104        let t = Theme::default();
105        assert_eq!(t.name, "blackmatter-dark");
106    }
107
108    #[test]
109    fn error_maps_to_red() {
110        let t = Theme::blackmatter_dark();
111        assert_eq!(t.color(Semantic::Error), Nord::NORD11);
112    }
113
114    #[test]
115    fn paint_wraps_with_reset() {
116        let t = Theme::blackmatter_dark();
117        let out = t.paint(Semantic::Error, "boom");
118        assert!(out.starts_with("\x1b["));
119        assert!(out.ends_with(crate::palette::ANSI_RESET));
120        assert!(out.contains("boom"));
121    }
122
123    #[test]
124    fn every_semantic_maps_to_a_nord_palette_color_across_both_themes() {
125        // Fail-before-pass-after pin on [`Semantic::ALL`] as the
126        // canonical iteration axis every theme overlay must resolve
127        // in full: for each of the 15 variants and each of the two
128        // shipped [`Theme`] overlays (`blackmatter_dark`,
129        // `blackmatter_light`), the resolver must return one of the
130        // 16 Nord palette colors. Pre-lift the two resolver functions
131        // were paired 15-arm exhaustive matches with no cross-consumer
132        // link back to the closed [`Semantic`] partition, so a future
133        // wildcard arm on either resolver (an `_ => Rgb::from_hex(0)`
134        // for a hypothetical "unstyled" fallback that would render
135        // invisibly on a dark terminal, an `_ => Nord::NORD0` for a
136        // hypothetical "default background" fallback the light
137        // overlay's `Symbol` arm already hand-authored) would silently
138        // slip past both the compile-time exhaustiveness check (each
139        // wildcard *is* exhaustive) and any per-arm smoke test.
140        // Iterating [`Semantic::ALL`] and asserting each resolver's
141        // output falls inside the closed 16-color Nord palette makes
142        // such a slip a caixa-theme build-time failure. Compounding
143        // peer of the peer [`caixa_core::CaixaKind`] /
144        // [`caixa_lint::Severity`] `ALL`-based iteration disciplines.
145        use crate::palette::Nord;
146        let nord_palette: [Rgb; 16] = [
147            Nord::NORD0,
148            Nord::NORD1,
149            Nord::NORD2,
150            Nord::NORD3,
151            Nord::NORD4,
152            Nord::NORD5,
153            Nord::NORD6,
154            Nord::NORD7,
155            Nord::NORD8,
156            Nord::NORD9,
157            Nord::NORD10,
158            Nord::NORD11,
159            Nord::NORD12,
160            Nord::NORD13,
161            Nord::NORD14,
162            Nord::NORD15,
163        ];
164        for theme in [Theme::blackmatter_dark(), Theme::blackmatter_light()] {
165            for &sem in Semantic::ALL {
166                let rgb = theme.color(sem);
167                assert!(
168                    nord_palette.contains(&rgb),
169                    "Theme::{} maps Semantic::{sem:?} to {rgb:?} — a \
170                     color outside the closed Nord palette. Every \
171                     theme-overlay resolver must reach for one of the \
172                     16 Nord entries; a wildcard arm returning a \
173                     color outside the palette (an invisible fallback, \
174                     a paint-bleed on a hypothetical extension arm) \
175                     is a defect.",
176                    theme.name,
177                );
178            }
179        }
180    }
181
182    #[test]
183    fn diagnostic_severity_arms_paint_red_and_orange_across_both_themes() {
184        // Fail-before-pass-after pin on the cross-theme-invariant
185        // paint of the two structurally-most-load-bearing diagnostic
186        // arms — [`Semantic::Error`] (red, `Nord::NORD11`) and
187        // [`Semantic::Warning`] (orange, `Nord::NORD12`). These two
188        // are the Aurora hues every terminal reader keys off
189        // ("something is wrong here"); a future theme overlay that
190        // rerouted either through a Frost blue (silently reading as
191        // an informational tag) would remove the paint's semantic
192        // signal without any compile-time failure. Iterating both
193        // shipped overlays via [`Semantic::ALL`]'s canonical arm-list
194        // and asserting the two paint invariants on the exact Nord
195        // slot pins the semantic-color-contract that
196        // [`crate::palette::Nord::NORD11`]'s `// red — errors` /
197        // [`crate::palette::Nord::NORD12`]'s `// orange — warnings`
198        // documentation comments already assert at the palette
199        // definition site, but that no cross-overlay test currently
200        // guards.
201        use crate::palette::Nord;
202        for theme in [Theme::blackmatter_dark(), Theme::blackmatter_light()] {
203            assert_eq!(
204                theme.color(Semantic::Error),
205                Nord::NORD11,
206                "Theme::{} must paint Semantic::Error as NORD11 (red)",
207                theme.name,
208            );
209            assert_eq!(
210                theme.color(Semantic::Warning),
211                Nord::NORD12,
212                "Theme::{} must paint Semantic::Warning as NORD12 (orange)",
213                theme.name,
214            );
215        }
216    }
217}