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(
100        mono.fallback
101            .iter()
102            .map(|name| str_(*name))
103            .collect(),
104    );
105
106    let body = attrset(vec![
107        AttrEntry::new("primary", primary)
108            .with_comment(["Primary monospace family.", "Source of every cell's regular glyph."]),
109        AttrEntry::new("italic", italic)
110            .with_comment(["Italic face — same family slanted (ghostty's model)."]),
111        AttrEntry::new("bold", bold)
112            .with_comment(["Bold face — shares the primary package."]),
113        AttrEntry::new("symbols", symbols)
114            .with_comment(["Nerd Font icons (powerline / starship / atuin)."]),
115        AttrEntry::new("emoji", emoji)
116            .with_comment([
117                "macOS uses Apple Color Emoji at the OS layer;",
118                "Linux substitutes Noto Color Emoji (operator-installed).",
119            ]),
120        AttrEntry::new("fallback_chain", fallback_chain)
121            .with_comment(["Ordered fallback list cosmic-text walks for missing codepoints."]),
122    ]);
123
124    let file = NixFile::new(
125        [
126            "Generated by ishou-render::fleet_fonts — DO NOT EDIT",
127            "Source of truth: pleme-io/ishou/crates/ishou-tokens/src/typography.rs",
128            "Architecture:    pleme-io/theory/THEME-ARCHITECTURE.md",
129            "",
130            "Consumed as `let fonts = import this { inherit pkgs; }; in …`",
131            "by every pleme-io GPU app's HM module (mado, ghostty, fumi, …).",
132        ],
133        lambda(vec!["pkgs"], body),
134    );
135
136    file.render()
137}
138
139fn primary_pkg(mono: &MonoFonts) -> NixExpr {
140    match mono.nerd_font_package_attr {
141        Some(attr) => raw(format!("pkgs.nerd-fonts.{attr}")),
142        None => NixExpr::Null,
143    }
144}
145
146fn italic_pkg(mono: &MonoFonts) -> NixExpr {
147    match mono.italic_package_attr {
148        Some(attr) => raw(format!("pkgs.{attr}")),
149        None => NixExpr::Null,
150    }
151}
152
153fn italic_style_name(s: ItalicStyle) -> &'static str {
154    match s {
155        ItalicStyle::MatchesPrimary => "matches_primary",
156        ItalicStyle::Calligraphic => "calligraphic",
157        ItalicStyle::Cursive => "cursive",
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn output_is_deterministic() {
167        let t = TokenSet::pleme();
168        assert_eq!(render(&t), render(&t));
169    }
170
171    #[test]
172    fn output_contains_all_canonical_slots() {
173        let out = render(&TokenSet::pleme());
174        for slot in ["primary", "italic", "bold", "symbols", "emoji", "fallback_chain"] {
175            assert!(out.contains(&format!("{slot} = ")), "missing slot {slot}\n{out}");
176        }
177    }
178
179    #[test]
180    fn primary_pins_jetbrains_mono_nerd_font_with_correct_package() {
181        let out = render(&TokenSet::pleme());
182        assert!(out.contains("name = \"JetBrainsMono Nerd Font\""));
183        assert!(out.contains("package = pkgs.nerd-fonts.jetbrains-mono"));
184    }
185
186    #[test]
187    fn italic_carries_style_intent() {
188        let out = render(&TokenSet::pleme());
189        // Italics now slant the same JetBrainsMono face (ghostty's
190        // model), so the intent is `matches_primary`, not a separate
191        // calligraphic typeface.
192        assert!(out.contains("style_intent = \"matches_primary\""));
193    }
194
195    #[test]
196    fn symbols_slot_points_at_symbols_only_nerd_font() {
197        let out = render(&TokenSet::pleme());
198        assert!(out.contains("package = pkgs.nerd-fonts.symbols-only"));
199    }
200
201    #[test]
202    fn header_documents_provenance() {
203        let out = render(&TokenSet::pleme());
204        assert!(out.contains("ishou-render::fleet_fonts"));
205        assert!(out.contains("typography.rs"));
206    }
207
208    #[test]
209    fn fallback_chain_is_emitted_in_order() {
210        let out = render(&TokenSet::pleme());
211        let ts = TokenSet::pleme();
212        for name in ts.typography.mono_fonts.fallback {
213            assert!(out.contains(&format!("\"{name}\"")), "missing fallback {name}");
214        }
215    }
216
217    #[test]
218    fn output_starts_with_pkgs_lambda() {
219        let out = render(&TokenSet::pleme());
220        // After the comment header, the first non-comment line should
221        // be the lambda. Lambda printer renders `{ pkgs }:`.
222        let body = out
223            .lines()
224            .find(|l| !l.starts_with('#'))
225            .unwrap_or("");
226        assert_eq!(body, "{ pkgs }:");
227    }
228}