ishou_render/stylix.rs
1//! Stylix base16 renderer — emits the YAML format
2//! `nix-community/stylix` consumes for `stylix.base16Scheme`.
3//!
4//! Closes the foreign-app theme loop: every GTK / GNOME / alacritty /
5//! kitty / btop / k9s consumer stylix supports inherits the ishou
6//! palette. After the fleet's `darwin-developer` profile swaps
7//! `pkgs.base16-schemes/nord.yaml` for this renderer's output, there
8//! is exactly **one** Nord in the pleme-io source tree.
9//!
10//! Output format (per base16 spec — <https://github.com/chriskempson/base16>):
11//!
12//! ```yaml
13//! scheme: "Nord (pleme-io / ishou)"
14//! author: "Arctic Ice Studio; mapped by pleme-io ishou"
15//! base00: "2e3440"
16//! base01: "3b4252"
17//! …
18//! base0F: "5e81ac"
19//! ```
20//!
21//! Hex values are **lowercase, unprefixed** — stylix doesn't strip a
22//! leading `#`, so emitting one breaks the GTK theme generation.
23//!
24//! ## base16 → ishou Nord mapping
25//!
26//! The standard base16 contract is *role-named slots*; we map them
27//! onto Nord's palette in the conventional Nord base16 way (matches
28//! what `nix-community/base16-schemes/nord.yaml` ships, which is the
29//! file this renderer displaces).
30//!
31//! | slot | ishou colour | hex | role |
32//! |--------|--------------------|---------|------|
33//! | base00 | `polar_night_0` | 2e3440 | default background |
34//! | base01 | `polar_night_1` | 3b4252 | status bars, line numbers |
35//! | base02 | `polar_night_2` | 434c5e | selection background |
36//! | base03 | `polar_night_3` | 4c566a | comments, line highlight |
37//! | base04 | `snow_storm_0` | d8dee9 | dark foreground |
38//! | base05 | `snow_storm_1` | e5e9f0 | default foreground |
39//! | base06 | `snow_storm_2` | eceff4 | light foreground |
40//! | base07 | `frost_0` | 8fbcbb | light background |
41//! | base08 | `aurora_red` | bf616a | variables, markup deleted |
42//! | base09 | `aurora_orange` | d08770 | integers, markup link url |
43//! | base0A | `aurora_yellow` | ebcb8b | classes, markup bold |
44//! | base0B | `aurora_green` | a3be8c | strings, markup inserted |
45//! | base0C | `frost_1` | 88c0d0 | regexes, escape chars |
46//! | base0D | `frost_2` | 81a1c1 | functions, headings |
47//! | base0E | `aurora_purple` | b48ead | keywords, markup italic |
48//! | base0F | `frost_3` | 5e81ac | deprecated, embedded |
49
50use ishou_tokens::{Rgb, TokenSet};
51
52/// Render the canonical pleme-io base16 YAML stylix consumes.
53///
54/// Pure function — same `TokenSet` always produces byte-identical
55/// output. Cross-pinned against the upstream `nix-community/base16-
56/// schemes/nord.yaml` by the test suite so the swap is byte-stable
57/// for the default theme.
58#[must_use]
59pub fn render(t: &TokenSet) -> String {
60 let c = &t.color;
61 let mut out = String::new();
62 out.push_str("# Generated by ishou-render::stylix — DO NOT EDIT\n");
63 out.push_str("# Source of truth: pleme-io/ishou/crates/ishou-tokens/src/color.rs\n");
64 out.push_str("# Architecture: pleme-io/theory/THEME-ARCHITECTURE.md\n");
65 out.push_str("scheme: \"Nord (pleme-io / ishou)\"\n");
66 out.push_str("author: \"Arctic Ice Studio; mapped by pleme-io ishou\"\n");
67 push_base(&mut out, "00", c.polar_night_0);
68 push_base(&mut out, "01", c.polar_night_1);
69 push_base(&mut out, "02", c.polar_night_2);
70 push_base(&mut out, "03", c.polar_night_3);
71 push_base(&mut out, "04", c.snow_storm_0);
72 push_base(&mut out, "05", c.snow_storm_1);
73 push_base(&mut out, "06", c.snow_storm_2);
74 push_base(&mut out, "07", c.frost_0);
75 push_base(&mut out, "08", c.aurora_red);
76 push_base(&mut out, "09", c.aurora_orange);
77 push_base(&mut out, "0A", c.aurora_yellow);
78 push_base(&mut out, "0B", c.aurora_green);
79 push_base(&mut out, "0C", c.frost_1);
80 push_base(&mut out, "0D", c.frost_2);
81 push_base(&mut out, "0E", c.aurora_purple);
82 push_base(&mut out, "0F", c.frost_3);
83 out
84}
85
86fn push_base(out: &mut String, slot: &str, rgb: Rgb) {
87 // base16 yaml format: hex *without* `#` prefix, lowercase.
88 out.push_str(&format!(
89 "base{}: \"{:02x}{:02x}{:02x}\"\n",
90 slot, rgb.r, rgb.g, rgb.b
91 ));
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use ishou_tokens::TokenSet;
98
99 #[test]
100 fn output_is_deterministic() {
101 let t = TokenSet::pleme();
102 assert_eq!(render(&t), render(&t));
103 }
104
105 #[test]
106 fn output_contains_all_16_base_slots() {
107 let out = render(&TokenSet::pleme());
108 for slot in [
109 "base00", "base01", "base02", "base03", "base04", "base05", "base06", "base07",
110 "base08", "base09", "base0A", "base0B", "base0C", "base0D", "base0E", "base0F",
111 ] {
112 assert!(out.contains(&format!("{slot}: \"")), "missing {slot}");
113 }
114 }
115
116 #[test]
117 fn nord_polar_night_0_lands_at_base00() {
118 let out = render(&TokenSet::pleme());
119 assert!(
120 out.contains("base00: \"2e3440\""),
121 "base00 must be polar_night_0 (#2e3440); got:\n{out}"
122 );
123 }
124
125 #[test]
126 fn nord_snow_storm_1_lands_at_base05_for_foreground_role() {
127 let out = render(&TokenSet::pleme());
128 // base05 = default foreground per the base16 spec; Nord maps
129 // it to snow_storm_1 (#e5e9f0) — the standard mapping that
130 // matches base16-schemes/nord.yaml byte-for-byte.
131 assert!(out.contains("base05: \"e5e9f0\""), "got:\n{out}");
132 }
133
134 #[test]
135 fn nord_aurora_red_lands_at_base08() {
136 let out = render(&TokenSet::pleme());
137 assert!(out.contains("base08: \"bf616a\""), "got:\n{out}");
138 }
139
140 #[test]
141 fn hex_is_lowercase_unprefixed_to_match_stylix_contract() {
142 let out = render(&TokenSet::pleme());
143 // stylix passes the values through to GTK which expects
144 // exactly six-char lowercase hex (no `#`). A regression here
145 // would break theming across the foreign-app world silently.
146 for line in out.lines() {
147 if let Some(rest) = line.strip_prefix("base") {
148 if let Some(value_part) = rest.split(": ").nth(1) {
149 let v = value_part.trim_matches('"');
150 assert_eq!(v.len(), 6, "wrong length: {line}");
151 assert!(
152 v.chars()
153 .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
154 "expected lowercase hex without prefix in: {line}"
155 );
156 }
157 }
158 }
159 }
160
161 #[test]
162 fn full_byte_for_byte_canonical_pleme_output() {
163 // Snapshot of the entire canonical render. Any unintentional
164 // change to the Nord palette or to the renderer's formatting
165 // fails this test loudly. Update only when the upstream
166 // change is deliberate.
167 let out = render(&TokenSet::pleme());
168 let expected = "\
169# Generated by ishou-render::stylix — DO NOT EDIT
170# Source of truth: pleme-io/ishou/crates/ishou-tokens/src/color.rs
171# Architecture: pleme-io/theory/THEME-ARCHITECTURE.md
172scheme: \"Nord (pleme-io / ishou)\"
173author: \"Arctic Ice Studio; mapped by pleme-io ishou\"
174base00: \"2e3440\"
175base01: \"3b4252\"
176base02: \"434c5e\"
177base03: \"4c566a\"
178base04: \"d8dee9\"
179base05: \"e5e9f0\"
180base06: \"eceff4\"
181base07: \"8fbcbb\"
182base08: \"bf616a\"
183base09: \"d08770\"
184base0A: \"ebcb8b\"
185base0B: \"a3be8c\"
186base0C: \"88c0d0\"
187base0D: \"81a1c1\"
188base0E: \"b48ead\"
189base0F: \"5e81ac\"
190";
191 assert_eq!(out, expected);
192 }
193}