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}