Skip to main content

ishou_render/
fleet_fonts.rs

1//! Fleet-fonts renderer — typed Nix attrset every pleme-io GUI app's
2//! home-manager module imports to (a) name the canonical font family
3//! and (b) install the underlying nixpkgs package via `home.packages`.
4//!
5//! Why this exists separately from `stylix_fonts`:
6//!
7//! - `stylix_fonts` produces the **stylix-shaped** `{ monospace,
8//!   sansSerif, serif, emoji }` attrset that foreign apps (GTK,
9//!   alacritty, kitty, btop, k9s, …) read via the stylix theme.
10//!   Stylix has exactly four font slots.
11//!
12//! - `fleet_fonts` produces a **GPU-app-shaped** `{ primary, italic,
13//!   bold, symbols, emoji, fallback_chain }` attrset that pleme-io's
14//!   own terminal emulators (mado today; fumi / kagi / hibiki /
15//!   namimado / nami next) consume directly via flake input. It
16//!   carries the italic + symbols faces stylix doesn't model, plus
17//!   the explicit package reference each HM module needs to
18//!   `home.packages = [ … ]` so the underlying font is actually
19//!   installed on the operator's machine.
20//!
21//! The two surfaces are sibling projections of the same typed
22//! Typography token — changing `MonoFonts::pleme()` in ishou-tokens
23//! is the single source of truth for both. Adding a new font slot
24//! is a one-line edit in typography.rs; both renderers regenerate
25//! in lockstep.
26//!
27//! ## Output shape
28//!
29//! ```nix
30//! { pkgs }:
31//! {
32//!   primary = { name = "JetBrainsMono Nerd Font"; package = pkgs.nerd-fonts.jetbrains-mono; };
33//!   italic  = { name = "Iosevka"; style_intent = "calligraphic"; package = pkgs.iosevka; };
34//!   bold    = { name = "JetBrainsMono Nerd Font"; package = pkgs.nerd-fonts.jetbrains-mono; };
35//!   symbols = { name = "Symbols Nerd Font Mono"; package = pkgs.nerd-fonts.symbols-only; };
36//!   emoji   = { name = "Apple Color Emoji"; fallback_name = "Noto Color Emoji"; package = null; };
37//!   fallback_chain = [ "Symbols Nerd Font Mono" "FiraCode Nerd Font" … ];
38//! }
39//! ```
40//!
41//! ## Consumer pattern
42//!
43//! Every fleet GUI app's HM module imports this attrset and uses it to
44//! (a) name the canonical family in its `font.family` default and
45//! (b) install the underlying package(s) declaratively:
46//!
47//! ```nix
48//! let fonts = import inputs.ishou.packages.${system}.fleet-fonts
49//!               { inherit pkgs; };
50//! in {
51//!   options.myapp.font.family = lib.mkOption {
52//!     default = fonts.primary.name;
53//!   };
54//!   config.home.packages = [
55//!     fonts.primary.package
56//!     fonts.italic.package
57//!     fonts.symbols.package
58//!   ];
59//! }
60//! ```
61
62use ishou_tokens::TokenSet;
63use ishou_tokens::typography::{ItalicStyle, MonoFonts};
64
65use crate::nix_ast::{AttrEntry, NixExpr, NixFile, attrset, lambda, list, raw, str_};
66
67/// Render the canonical fleet-fonts Nix attrset.
68#[must_use]
69pub fn render(t: &TokenSet) -> String {
70    let mono = &t.typography.mono_fonts;
71
72    let primary = attrset(vec![
73        AttrEntry::new("name", str_(mono.primary)),
74        AttrEntry::new("package", primary_pkg(mono)),
75    ]);
76
77    let italic = attrset(vec![
78        AttrEntry::new("name", str_(mono.italic)),
79        AttrEntry::new("style_intent", str_(italic_style_name(mono.italic_style))),
80        AttrEntry::new("package", italic_pkg(mono)),
81    ]);
82
83    let bold = attrset(vec![
84        AttrEntry::new("name", str_(mono.bold)),
85        AttrEntry::new("package", primary_pkg(mono)),
86    ]);
87
88    let symbols = attrset(vec![
89        AttrEntry::new("name", str_("Symbols Nerd Font Mono")),
90        AttrEntry::new("package", raw("pkgs.nerd-fonts.symbols-only")),
91    ]);
92
93    let emoji = attrset(vec![
94        AttrEntry::new("name", str_("Apple Color Emoji")),
95        AttrEntry::new("fallback_name", str_("Noto Color Emoji")),
96        AttrEntry::new("package", NixExpr::Null),
97    ]);
98
99    let fallback_chain = list(mono.fallback.iter().map(|name| str_(*name)).collect());
100
101    let body = attrset(vec![
102        AttrEntry::new("primary", primary).with_comment([
103            "Primary monospace family.",
104            "Source of every cell's regular glyph.",
105        ]),
106        AttrEntry::new("italic", italic)
107            .with_comment(["Italic face — same family slanted (ghostty's model)."]),
108        AttrEntry::new("bold", bold).with_comment(["Bold face — shares the primary package."]),
109        AttrEntry::new("symbols", symbols)
110            .with_comment(["Nerd Font icons (powerline / starship / atuin)."]),
111        AttrEntry::new("emoji", emoji).with_comment([
112            "macOS uses Apple Color Emoji at the OS layer;",
113            "Linux substitutes Noto Color Emoji (operator-installed).",
114        ]),
115        AttrEntry::new("fallback_chain", fallback_chain)
116            .with_comment(["Ordered fallback list cosmic-text walks for missing codepoints."]),
117    ]);
118
119    let file = NixFile::new(
120        [
121            "Generated by ishou-render::fleet_fonts — DO NOT EDIT",
122            "Source of truth: pleme-io/ishou/crates/ishou-tokens/src/typography.rs",
123            "Architecture:    pleme-io/theory/THEME-ARCHITECTURE.md",
124            "",
125            "Consumed as `let fonts = import this { inherit pkgs; }; in …`",
126            "by every pleme-io GPU app's HM module (mado, ghostty, fumi, …).",
127        ],
128        lambda(vec!["pkgs"], body),
129    );
130
131    file.render()
132}
133
134fn primary_pkg(mono: &MonoFonts) -> NixExpr {
135    match mono.nerd_font_package_attr {
136        Some(attr) => raw(format!("pkgs.nerd-fonts.{attr}")),
137        None => NixExpr::Null,
138    }
139}
140
141fn italic_pkg(mono: &MonoFonts) -> NixExpr {
142    match mono.italic_package_attr {
143        Some(attr) => raw(format!("pkgs.{attr}")),
144        None => NixExpr::Null,
145    }
146}
147
148fn italic_style_name(s: ItalicStyle) -> &'static str {
149    match s {
150        ItalicStyle::MatchesPrimary => "matches_primary",
151        ItalicStyle::Calligraphic => "calligraphic",
152        ItalicStyle::Cursive => "cursive",
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn output_is_deterministic() {
162        let t = TokenSet::pleme();
163        assert_eq!(render(&t), render(&t));
164    }
165
166    #[test]
167    fn output_contains_all_canonical_slots() {
168        let out = render(&TokenSet::pleme());
169        for slot in [
170            "primary",
171            "italic",
172            "bold",
173            "symbols",
174            "emoji",
175            "fallback_chain",
176        ] {
177            assert!(
178                out.contains(&format!("{slot} = ")),
179                "missing slot {slot}\n{out}"
180            );
181        }
182    }
183
184    #[test]
185    fn primary_pins_jetbrains_mono_nerd_font_with_correct_package() {
186        let out = render(&TokenSet::pleme());
187        assert!(out.contains("name = \"JetBrainsMono Nerd Font\""));
188        assert!(out.contains("package = pkgs.nerd-fonts.jetbrains-mono"));
189    }
190
191    #[test]
192    fn italic_carries_style_intent() {
193        let out = render(&TokenSet::pleme());
194        // Italics now slant the same JetBrainsMono face (ghostty's
195        // model), so the intent is `matches_primary`, not a separate
196        // calligraphic typeface.
197        assert!(out.contains("style_intent = \"matches_primary\""));
198    }
199
200    #[test]
201    fn symbols_slot_points_at_symbols_only_nerd_font() {
202        let out = render(&TokenSet::pleme());
203        assert!(out.contains("package = pkgs.nerd-fonts.symbols-only"));
204    }
205
206    #[test]
207    fn header_documents_provenance() {
208        let out = render(&TokenSet::pleme());
209        assert!(out.contains("ishou-render::fleet_fonts"));
210        assert!(out.contains("typography.rs"));
211    }
212
213    #[test]
214    fn fallback_chain_is_emitted_in_order() {
215        let out = render(&TokenSet::pleme());
216        let ts = TokenSet::pleme();
217        for name in ts.typography.mono_fonts.fallback {
218            assert!(
219                out.contains(&format!("\"{name}\"")),
220                "missing fallback {name}"
221            );
222        }
223    }
224
225    #[test]
226    fn output_starts_with_pkgs_lambda() {
227        let out = render(&TokenSet::pleme());
228        // After the comment header, the first non-comment line should
229        // be the lambda. Lambda printer renders `{ pkgs }:`.
230        let body = out.lines().find(|l| !l.starts_with('#')).unwrap_or("");
231        assert_eq!(body, "{ pkgs }:");
232    }
233}