1use indexmap::IndexMap;
29use ishou_tokens::{Rgb, VellumPalette};
30use serde::Serialize;
31
32fn slot_hex(rgb: Rgb) -> String {
34 format!("{:02x}{:02x}{:02x}", rgb.r, rgb.g, rgb.b)
35}
36
37#[derive(Serialize)]
41struct Scheme {
42 system: &'static str,
43 name: &'static str,
44 author: &'static str,
45 variant: &'static str,
46 slug: &'static str,
47 palette: IndexMap<String, String>,
48}
49
50fn scheme(system: &'static str, palette: IndexMap<String, String>) -> Scheme {
51 Scheme {
52 system,
53 name: "Vellum",
54 author: "pleme-io (ishou)",
55 variant: "dark",
56 slug: "vellum",
57 palette,
58 }
59}
60
61const HEADER: &str = "# Generated by ishou-render::vellum — DO NOT EDIT\n\
62 # Source of truth: pleme-io/ishou/crates/ishou-tokens/src/vellum.rs\n\
63 # Vellum — the fleet theme (warm aged-paper Nord-matte)\n";
64
65#[must_use]
70pub fn render_base16() -> String {
71 let p = VellumPalette::vellum();
72 let mut palette = IndexMap::new();
73 for (slot, rgb) in p.base16() {
74 palette.insert(slot.to_string(), slot_hex(rgb));
75 }
76 let body =
77 serde_yaml::to_string(&scheme("base16", palette)).expect("Scheme is always serializable");
78 format!("{HEADER}{body}")
79}
80
81#[must_use]
105pub fn render_base16_nix() -> String {
106 use crate::nix_ast::{AttrEntry, NixFile, attrset, str_};
107
108 let p = VellumPalette::vellum();
109 let meta = scheme("base16", IndexMap::new());
110
111 let mut entries: Vec<AttrEntry> = vec![
126 AttrEntry::new("system", str_(meta.system)),
127 AttrEntry::new("name", str_(meta.name)),
128 AttrEntry::new("author", str_(meta.author)),
129 AttrEntry::new("variant", str_(meta.variant)),
130 AttrEntry::new("slug", str_(meta.slug)),
131 ];
132 entries.extend(
133 p.base16()
134 .into_iter()
135 .map(|(slot, rgb)| AttrEntry::new(slot, str_(slot_hex(rgb)))),
139 );
140
141 NixFile::new(
142 [
143 "Generated by ishou-render::vellum — DO NOT EDIT",
144 "Source of truth: pleme-io/ishou/crates/ishou-tokens/src/vellum.rs",
145 "",
146 "Vellum base16 as an ALREADY-PARSED attrset. Consumed as:",
147 " stylix.base16Scheme = import ./rendered/stylix-base16-vellum.nix;",
148 "",
149 "The `{ yaml = <derivation>; }` form this replaces made stylix",
150 "readFile a derivation — an IFD — which forced a linux build during",
151 "evaluation and left every NixOS node unevaluable from a Mac.",
152 ],
153 attrset(entries),
154 )
155 .render()
156}
157
158#[must_use]
161pub fn render_base24() -> String {
162 let p = VellumPalette::vellum();
163 let mut palette = IndexMap::new();
164 for (slot, rgb) in p.base24() {
165 palette.insert(slot.to_string(), slot_hex(rgb));
166 }
167 let body =
168 serde_yaml::to_string(&scheme("base24", palette)).expect("Scheme is always serializable");
169 format!("{HEADER}{body}")
170}
171
172#[must_use]
175pub fn render_svg_palette() -> String {
176 let p = VellumPalette::vellum();
177 let entries = p.entries();
178 let cols = 6usize;
179 let chip = 96i32;
180 let pad = 12i32;
181 let label_h = 22i32;
182 let rows = entries.len().div_ceil(cols) as i32;
183 let width = cols as i32 * (chip + pad) + pad;
184 let height = rows * (chip + label_h + pad) + pad;
185
186 let mut chips = String::new();
187 for (i, (name, rgb)) in entries.iter().enumerate() {
188 let col = (i % cols) as i32;
189 let row = (i / cols) as i32;
190 let x = pad + col * (chip + pad);
191 let y = pad + row * (chip + label_h + pad);
192 let hex = rgb.hex();
193 let lum = 0.2126 * f64::from(rgb.r) + 0.7152 * f64::from(rgb.g) + 0.0722 * f64::from(rgb.b);
196 let text_fill = if lum > 140.0 { "#16140E" } else { "#F4EFE2" };
197 chips.push_str(&format!(
198 " <rect x=\"{x}\" y=\"{y}\" width=\"{chip}\" height=\"{chip}\" rx=\"8\" fill=\"{hex}\"/>\n \
199 <text x=\"{tx}\" y=\"{ty}\" font-family=\"monospace\" font-size=\"9\" fill=\"{text_fill}\">{hex}</text>\n \
200 <text x=\"{x}\" y=\"{ly}\" font-family=\"monospace\" font-size=\"10\" fill=\"#E2DBC8\">{name}</text>\n",
201 tx = x + 6,
202 ty = y + chip - 8,
203 ly = y + chip + 15,
204 ));
205 }
206
207 format!(
208 "<!-- ishou Vellum palette preview (generated) -->\n\
209 <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\" width=\"{width}\" height=\"{height}\">\n \
210 <rect width=\"{width}\" height=\"{height}\" fill=\"#16140E\"/>\n{chips}</svg>\n"
211 )
212}
213
214fn tok(p: &VellumPalette, name: &str) -> String {
220 p.get(name)
221 .unwrap_or_else(|| panic!("vellum token `{name}` missing"))
222 .hex()
223}
224
225#[must_use]
249pub fn render_skim() -> String {
250 let p = VellumPalette::vellum();
251 let rows: [(&str, &str, &str); 14] = [
253 ("fg", "snow1", ""),
254 ("bg", "night0", ""),
255 ("hl", "ice_cyan", ":bold:underlined"),
256 ("fg+", "snow3", ":bold"),
257 ("bg+", "night2", ""),
258 ("hl+", "cyan_bright", ":bold:underlined"),
259 ("info", "shadow0", ""),
260 ("prompt", "aurora_green", ""),
261 ("pointer", "green_bright", ""),
262 ("marker", "solar_magenta", ""),
263 ("spinner", "ice_steel", ""),
264 ("header", "ice_steel", ""),
265 ("border", "shadow0", ""),
266 ("query", "snow3", ":bold"),
267 ];
268 rows.iter()
269 .map(|(key, token, attr)| format!("{key}:{}{attr}", tok(&p, token)))
270 .collect::<Vec<_>>()
271 .join(",")
272}
273
274struct HiRow {
280 group: &'static str,
281 fg: &'static str,
283 bg: &'static str,
285 bold: bool,
286 italic: bool,
287 link: &'static str,
289}
290
291impl HiRow {
292 const fn fg(group: &'static str, fg: &'static str) -> Self {
293 Self {
294 group,
295 fg,
296 bg: "",
297 bold: false,
298 italic: false,
299 link: "",
300 }
301 }
302 const fn bg(group: &'static str, bg: &'static str) -> Self {
303 Self {
304 group,
305 fg: "",
306 bg,
307 bold: false,
308 italic: false,
309 link: "",
310 }
311 }
312 const fn fg_bg(group: &'static str, fg: &'static str, bg: &'static str) -> Self {
313 Self {
314 group,
315 fg,
316 bg,
317 bold: false,
318 italic: false,
319 link: "",
320 }
321 }
322 const fn link(group: &'static str, link: &'static str) -> Self {
323 Self {
324 group,
325 fg: "",
326 bg: "",
327 bold: false,
328 italic: false,
329 link,
330 }
331 }
332 const fn b(mut self) -> Self {
333 self.bold = true;
334 self
335 }
336 const fn i(mut self) -> Self {
337 self.italic = true;
338 self
339 }
340}
341
342fn lc_hex(p: &VellumPalette, name: &str) -> String {
346 let h = tok(p, name); format!("#{}", h[1..].to_ascii_lowercase())
348}
349
350fn lisp_form(head: &str, kvs: &[(&str, String)]) -> String {
354 use std::fmt::Write as _;
355 let mut s = String::new();
356 write!(s, "({head}").expect("write to String");
357 for (k, v) in kvs {
358 write!(s, " {k} {v}").expect("write to String");
359 }
360 s.push(')');
361 s
362}
363
364#[must_use]
373pub fn render_escriba_lisp() -> String {
374 use std::fmt::Write as _;
375 let p = VellumPalette::vellum();
376
377 let mut out = String::new();
378 out.push_str(
379 "; escriba — Vellum theme (the fleet default)\n\
380 ; Generated by ishou-render::vellum::render_escriba_lisp — DO NOT EDIT\n\
381 ; Source of truth: pleme-io/ishou/crates/ishou-tokens/src/vellum.rs\n\
382 ; Vellum — warm aged-paper Nord-matte; every hex is a BORN ishou token.\n\n",
383 );
384
385 writeln!(
387 out,
388 "{}",
389 lisp_form("deftheme", &[(":preset", "\"vellum\"".to_string())])
390 )
391 .expect("write");
392 out.push('\n');
393
394 let palette_kvs: Vec<(&str, String)> = vec![
398 (":name", "\"vellum\"".to_string()),
399 (":base00", format!("\"{}\"", lc_hex(&p, "night0"))),
400 (":base01", format!("\"{}\"", lc_hex(&p, "night1"))),
401 (":base02", format!("\"{}\"", lc_hex(&p, "night2"))),
402 (":base03", format!("\"{}\"", lc_hex(&p, "shadow1"))),
403 (":base04", format!("\"{}\"", lc_hex(&p, "snow0"))),
404 (":base05", format!("\"{}\"", lc_hex(&p, "snow1"))),
405 (":base06", format!("\"{}\"", lc_hex(&p, "snow2"))),
406 (":base07", format!("\"{}\"", lc_hex(&p, "snow3"))),
407 (":base08", format!("\"{}\"", lc_hex(&p, "aurora_red"))),
408 (":base09", format!("\"{}\"", lc_hex(&p, "ember"))),
409 (":base0a", format!("\"{}\"", lc_hex(&p, "first_light"))),
410 (":base0b", format!("\"{}\"", lc_hex(&p, "aurora_green"))),
411 (":base0c", format!("\"{}\"", lc_hex(&p, "ice_cyan"))),
412 (":base0d", format!("\"{}\"", lc_hex(&p, "ice_steel"))),
413 (":base0e", format!("\"{}\"", lc_hex(&p, "solar_magenta"))),
414 (":base0f", format!("\"{}\"", lc_hex(&p, "dusk_bronze"))),
415 ];
416 writeln!(out, "{}", lisp_form("defpalette", &palette_kvs)).expect("write");
417 out.push('\n');
418
419 let rows: &[HiRow] = &[
423 HiRow::fg_bg("Normal", "snow1", "night0"),
425 HiRow::fg("Comment", "shadow1").i(),
426 HiRow::fg("String", "aurora_green"),
427 HiRow::fg("Number", "solar_magenta"),
428 HiRow::fg("Boolean", "solar_magenta"),
429 HiRow::fg("Function", "ice_steel").b(),
430 HiRow::fg("Keyword", "solar_magenta").i(),
431 HiRow::fg("Statement", "solar_magenta"),
432 HiRow::fg("Conditional", "solar_magenta"),
433 HiRow::fg("Repeat", "solar_magenta"),
434 HiRow::fg("Operator", "solar_magenta"),
435 HiRow::fg("Type", "first_light"),
436 HiRow::fg("Structure", "first_light"),
437 HiRow::fg("Identifier", "snow1"),
438 HiRow::fg("Constant", "ember"),
439 HiRow::fg("PreProc", "ember"),
440 HiRow::fg("Macro", "ember"),
441 HiRow::fg("Special", "first_light"),
442 HiRow::bg("CursorLine", "night1"),
444 HiRow::bg("CursorColumn", "night1"),
445 HiRow::fg("LineNr", "shadow1"),
446 HiRow::bg("SignColumn", "night0"),
447 HiRow::bg("Visual", "night2"),
448 HiRow::bg("VisualNOS", "night2"),
449 HiRow::fg_bg("Search", "night0", "first_light"),
450 HiRow::fg_bg("IncSearch", "night0", "ember").b(),
451 HiRow::fg("MatchParen", "ember").b(),
452 HiRow::fg_bg("StatusLine", "snow1", "night1"),
453 HiRow::fg_bg("StatusLineNC", "shadow1", "night0"),
454 HiRow::fg_bg("TabLine", "shadow1", "night0"),
455 HiRow::bg("TabLineFill", "night0"),
456 HiRow::fg_bg("TabLineSel", "night0", "ice_cyan").b(),
457 HiRow::fg("VertSplit", "night3"),
458 HiRow::fg_bg("Pmenu", "snow1", "night1"),
459 HiRow::fg_bg("PmenuSel", "night0", "ice_cyan").b(),
460 HiRow::bg("PmenuSbar", "night1"),
461 HiRow::bg("PmenuThumb", "shadow1"),
462 HiRow::fg_bg("NormalFloat", "snow1", "night1"),
463 HiRow::fg_bg("FloatBorder", "ice_steel", "night1"),
464 HiRow::fg("DiagnosticError", "aurora_red").b(),
466 HiRow::fg("DiagnosticWarn", "first_light"),
467 HiRow::fg("DiagnosticInfo", "ice_cyan"),
468 HiRow::fg("DiagnosticHint", "aurora_green"),
469 HiRow::fg("GitSignsAdd", "aurora_green"),
471 HiRow::fg("GitSignsChange", "first_light"),
472 HiRow::fg("GitSignsDelete", "aurora_red"),
473 HiRow::bg("DiffAdd", "green_glass"),
474 HiRow::bg("DiffChange", "amber_glass"),
475 HiRow::bg("DiffDelete", "red_glass"),
476 HiRow::bg("DiffText", "steel_glass"),
477 HiRow::link("@function.call", "Function"),
479 HiRow::link("@variable", "Identifier"),
480 HiRow::fg("@parameter", "snow1").i(),
481 HiRow::fg("@comment.todo", "first_light").b(),
482 HiRow::fg("@comment.note", "ice_cyan").b(),
483 HiRow::fg("@comment.warning", "ember").b(),
484 ];
485
486 for r in rows {
487 let mut kvs: Vec<(&str, String)> = vec![(":group", format!("\"{}\"", r.group))];
488 if r.link.is_empty() {
489 if !r.fg.is_empty() {
490 kvs.push((":fg", format!("\"{}\"", lc_hex(&p, r.fg))));
491 }
492 if !r.bg.is_empty() {
493 kvs.push((":bg", format!("\"{}\"", lc_hex(&p, r.bg))));
494 }
495 if r.bold {
496 kvs.push((":bold", "#t".to_string()));
497 }
498 if r.italic {
499 kvs.push((":italic", "#t".to_string()));
500 }
501 } else {
502 kvs.push((":link", format!("\"{}\"", r.link)));
503 }
504 writeln!(out, "{}", lisp_form("defhighlight", &kvs)).expect("write");
505 }
506
507 out
508}
509
510#[cfg(test)]
511mod tests {
512
513 #[test]
520 fn the_nix_render_carries_the_same_palette_as_the_yaml() {
521 let yaml = super::render_base16();
522 let nixs = super::render_base16_nix();
523 for (slot, rgb) in VellumPalette::vellum().base16() {
524 let hex = super::slot_hex(rgb);
525 assert!(
526 yaml.contains(&format!("{slot}: {hex}")),
527 "yaml missing {slot}"
528 );
529 assert!(
530 nixs.contains(&format!("{slot} = \"{hex}\";")),
531 "nix missing {slot}"
532 );
533 }
534 }
535
536 #[test]
546 fn the_nix_render_keeps_the_scheme_identity_not_just_the_colours() {
547 let out = super::render_base16_nix();
548 for (k, v) in [
549 ("system", "base16"),
550 ("name", "Vellum"),
551 ("author", "pleme-io (ishou)"),
552 ("variant", "dark"),
553 ("slug", "vellum"),
554 ] {
555 assert!(
556 out.contains(&format!("{k} = \"{v}\";")),
557 "missing {k}; without it base16.nix names the scheme \"untitled\""
558 );
559 }
560 }
561
562 #[test]
568 fn the_nix_render_is_a_bare_attrset_with_no_yaml_key() {
569 let out = super::render_base16_nix();
570 let body: String = out.lines().filter(|l| !l.starts_with('#')).collect();
571 assert!(
572 body.trim_start().starts_with('{'),
573 "must be an attrset: {body}"
574 );
575 assert!(
576 !body.contains("yaml"),
577 "a `yaml` key would put stylix back on the IFD path"
578 );
579 assert!(!body.contains(':'), "must not be a function: {body}");
581 }
582 use super::*;
583
584 #[test]
585 fn base16_is_deterministic() {
586 assert_eq!(render_base16(), render_base16());
587 }
588
589 #[test]
590 fn skim_string_is_deterministic() {
591 assert_eq!(render_skim(), render_skim());
592 }
593
594 #[test]
595 fn skim_string_matches_the_authored_vellum_string() {
596 let expected = "\
600fg:#E2DBC8,\
601bg:#16140E,\
602hl:#94BBB8:bold:underlined,\
603fg+:#F4EFE2:bold,\
604bg+:#2B2820,\
605hl+:#A6CBC8:bold:underlined,\
606info:#6E6857,\
607prompt:#A9BB8C,\
608pointer:#ADD7A3,\
609marker:#B8A1B9,\
610spinner:#99AABE,\
611header:#99AABE,\
612border:#6E6857,\
613query:#F4EFE2:bold";
614 assert_eq!(render_skim(), expected, "skim --color string drifted");
615 }
616
617 #[test]
618 fn escriba_lisp_is_deterministic() {
619 assert_eq!(render_escriba_lisp(), render_escriba_lisp());
620 }
621
622 #[test]
623 fn escriba_lisp_has_palette_and_key_highlights() {
624 let out = render_escriba_lisp();
625 assert!(
627 out.contains("(defpalette :name \"vellum\" :base00 \"#16140e\""),
628 "defpalette base00 missing/drifted:\n{out}"
629 );
630 for line in [
632 "(defhighlight :group \"Normal\" :fg \"#e2dbc8\" :bg \"#16140e\")",
633 "(defhighlight :group \"Comment\" :fg \"#90897b\" :italic #t)",
634 "(defhighlight :group \"String\" :fg \"#a9bb8c\")",
635 "(defhighlight :group \"Function\" :fg \"#99aabe\" :bold #t)",
636 "(defhighlight :group \"DiffAdd\" :bg \"#4d543e\")",
637 "(defhighlight :group \"@function.call\" :link \"Function\")",
638 ] {
639 assert!(out.contains(line), "missing line `{line}`\n{out}");
640 }
641 for g in [
643 "Normal",
644 "Comment",
645 "String",
646 "Number",
647 "Boolean",
648 "Function",
649 "Keyword",
650 "Statement",
651 "Type",
652 "Constant",
653 "Special",
654 "Visual",
655 "Search",
656 "DiagnosticError",
657 "GitSignsAdd",
658 ] {
659 assert!(
660 out.contains(&format!(":group \"{g}\"")),
661 "missing group {g}\n{out}"
662 );
663 }
664 }
665
666 #[test]
667 fn base16_has_all_16_slots_lowercase_unprefixed() {
668 let out = render_base16();
669 for slot in [
670 "base00", "base01", "base02", "base03", "base04", "base05", "base06", "base07",
671 "base08", "base09", "base0A", "base0B", "base0C", "base0D", "base0E", "base0F",
672 ] {
673 assert!(out.contains(slot), "missing {slot}");
674 }
675 assert!(out.contains("16140e"), "base00 night0 missing:\n{out}");
677 assert!(!out.contains(": #"), "found prefixed hex:\n{out}");
679 }
680
681 #[test]
682 fn base16_slots_match_the_born_palette() {
683 let p = VellumPalette::vellum();
684 let out = render_base16();
685 for (slot, rgb) in p.base16() {
686 let want = format!("{slot}: {}", slot_hex(rgb));
687 assert!(
688 out.contains(&want),
689 "slot {slot} drifted; want `{want}`\n{out}"
690 );
691 }
692 }
693
694 #[test]
695 fn base24_has_the_real_brights() {
696 let out = render_base24();
697 assert!(
699 out.contains("base14: add7a3"),
700 "base14 green_bright:\n{out}"
701 );
702 assert!(out.contains("base12: d49088"), "base12 red_bright:\n{out}");
703 assert!(out.contains("system: base24"), "wrong system tag:\n{out}");
704 }
705
706 #[test]
707 fn svg_palette_is_deterministic_and_covers_every_token() {
708 let a = render_svg_palette();
709 assert_eq!(a, render_svg_palette());
710 let p = VellumPalette::vellum();
711 for (name, _) in p.entries() {
712 assert!(a.contains(name), "svg missing token {name}");
713 }
714 }
715}