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 {
222 p.get(name)
223 .unwrap_or_else(|| panic!("vellum token `{name}` missing"))
224 .hex()
225}
226
227#[must_use]
285pub fn render_skim() -> String {
286 let p = VellumPalette::vellum();
287 let rows: [(&str, &str, &str); 14] = [
289 ("fg", "snow1", ""),
290 ("bg", "night0", ""),
291 ("hl", "ice_cyan", ":bold:underlined"),
292 ("fg+", "snow3", ":bold"),
293 ("bg+", "night2", ""),
294 ("hl+", "cyan_bright", ":bold:underlined"),
295 ("info", "shadow0", ""),
296 ("prompt", "aurora_green", ""),
297 ("pointer", "green_bright", ""),
298 ("marker", "solar_magenta", ""),
299 ("spinner", "ice_steel", ""),
300 ("header", "ice_steel", ""),
301 ("border", "shadow0", ""),
302 ("query", "snow3", ":bold"),
303 ];
304 rows.iter()
305 .map(|(key, token, attr)| format!("{key}:{}{attr}", tok(&p, token)))
306 .collect::<Vec<_>>()
307 .join(",")
308}
309
310struct HiRow {
316 group: &'static str,
317 fg: &'static str,
319 bg: &'static str,
321 bold: bool,
322 italic: bool,
323 link: &'static str,
325}
326
327impl HiRow {
328 const fn fg(group: &'static str, fg: &'static str) -> Self {
329 Self {
330 group,
331 fg,
332 bg: "",
333 bold: false,
334 italic: false,
335 link: "",
336 }
337 }
338 const fn bg(group: &'static str, bg: &'static str) -> Self {
339 Self {
340 group,
341 fg: "",
342 bg,
343 bold: false,
344 italic: false,
345 link: "",
346 }
347 }
348 const fn fg_bg(group: &'static str, fg: &'static str, bg: &'static str) -> Self {
349 Self {
350 group,
351 fg,
352 bg,
353 bold: false,
354 italic: false,
355 link: "",
356 }
357 }
358 const fn link(group: &'static str, link: &'static str) -> Self {
359 Self {
360 group,
361 fg: "",
362 bg: "",
363 bold: false,
364 italic: false,
365 link,
366 }
367 }
368 const fn b(mut self) -> Self {
369 self.bold = true;
370 self
371 }
372 const fn i(mut self) -> Self {
373 self.italic = true;
374 self
375 }
376}
377
378fn lc_hex(p: &VellumPalette, name: &str) -> String {
382 let h = tok(p, name); format!("#{}", h[1..].to_ascii_lowercase())
384}
385
386fn lisp_form(head: &str, kvs: &[(&str, String)]) -> String {
390 use std::fmt::Write as _;
391 let mut s = String::new();
392 write!(s, "({head}").expect("write to String");
393 for (k, v) in kvs {
394 write!(s, " {k} {v}").expect("write to String");
395 }
396 s.push(')');
397 s
398}
399
400#[must_use]
409pub fn render_escriba_lisp() -> String {
410 use std::fmt::Write as _;
411 let p = VellumPalette::vellum();
412
413 let mut out = String::new();
414 out.push_str(
415 "; escriba — Vellum theme (the fleet default)\n\
416 ; Generated by ishou-render::vellum::render_escriba_lisp — DO NOT EDIT\n\
417 ; Source of truth: pleme-io/ishou/crates/ishou-tokens/src/vellum.rs\n\
418 ; Vellum — warm aged-paper Nord-matte; every hex is a BORN ishou token.\n\n",
419 );
420
421 writeln!(
423 out,
424 "{}",
425 lisp_form("deftheme", &[(":preset", "\"vellum\"".to_string())])
426 )
427 .expect("write");
428 out.push('\n');
429
430 let palette_kvs: Vec<(&str, String)> = vec![
434 (":name", "\"vellum\"".to_string()),
435 (":base00", format!("\"{}\"", lc_hex(&p, "night0"))),
436 (":base01", format!("\"{}\"", lc_hex(&p, "night1"))),
437 (":base02", format!("\"{}\"", lc_hex(&p, "night2"))),
438 (":base03", format!("\"{}\"", lc_hex(&p, "shadow1"))),
439 (":base04", format!("\"{}\"", lc_hex(&p, "snow0"))),
440 (":base05", format!("\"{}\"", lc_hex(&p, "snow1"))),
441 (":base06", format!("\"{}\"", lc_hex(&p, "snow2"))),
442 (":base07", format!("\"{}\"", lc_hex(&p, "snow3"))),
443 (":base08", format!("\"{}\"", lc_hex(&p, "aurora_red"))),
444 (":base09", format!("\"{}\"", lc_hex(&p, "ember"))),
445 (":base0a", format!("\"{}\"", lc_hex(&p, "first_light"))),
446 (":base0b", format!("\"{}\"", lc_hex(&p, "aurora_green"))),
447 (":base0c", format!("\"{}\"", lc_hex(&p, "ice_cyan"))),
448 (":base0d", format!("\"{}\"", lc_hex(&p, "ice_steel"))),
449 (":base0e", format!("\"{}\"", lc_hex(&p, "solar_magenta"))),
450 (":base0f", format!("\"{}\"", lc_hex(&p, "dusk_bronze"))),
451 ];
452 writeln!(out, "{}", lisp_form("defpalette", &palette_kvs)).expect("write");
453 out.push('\n');
454
455 let rows: &[HiRow] = &[
459 HiRow::fg_bg("Normal", "snow1", "night0"),
461 HiRow::fg("Comment", "shadow1").i(),
462 HiRow::fg("String", "aurora_green"),
463 HiRow::fg("Number", "solar_magenta"),
464 HiRow::fg("Boolean", "solar_magenta"),
465 HiRow::fg("Function", "ice_steel").b(),
466 HiRow::fg("Keyword", "solar_magenta").i(),
467 HiRow::fg("Statement", "solar_magenta"),
468 HiRow::fg("Conditional", "solar_magenta"),
469 HiRow::fg("Repeat", "solar_magenta"),
470 HiRow::fg("Operator", "solar_magenta"),
471 HiRow::fg("Type", "first_light"),
472 HiRow::fg("Structure", "first_light"),
473 HiRow::fg("Identifier", "snow1"),
474 HiRow::fg("Constant", "ember"),
475 HiRow::fg("PreProc", "ember"),
476 HiRow::fg("Macro", "ember"),
477 HiRow::fg("Special", "first_light"),
478 HiRow::bg("CursorLine", "night1"),
480 HiRow::bg("CursorColumn", "night1"),
481 HiRow::fg("LineNr", "shadow1"),
482 HiRow::bg("SignColumn", "night0"),
483 HiRow::bg("Visual", "night2"),
484 HiRow::bg("VisualNOS", "night2"),
485 HiRow::fg_bg("Search", "night0", "first_light"),
486 HiRow::fg_bg("IncSearch", "night0", "ember").b(),
487 HiRow::fg("MatchParen", "ember").b(),
488 HiRow::fg_bg("StatusLine", "snow1", "night1"),
489 HiRow::fg_bg("StatusLineNC", "shadow1", "night0"),
490 HiRow::fg_bg("TabLine", "shadow1", "night0"),
491 HiRow::bg("TabLineFill", "night0"),
492 HiRow::fg_bg("TabLineSel", "night0", "ice_cyan").b(),
493 HiRow::fg("VertSplit", "night3"),
494 HiRow::fg_bg("Pmenu", "snow1", "night1"),
495 HiRow::fg_bg("PmenuSel", "night0", "ice_cyan").b(),
496 HiRow::bg("PmenuSbar", "night1"),
497 HiRow::bg("PmenuThumb", "shadow1"),
498 HiRow::fg_bg("NormalFloat", "snow1", "night1"),
499 HiRow::fg_bg("FloatBorder", "ice_steel", "night1"),
500 HiRow::fg("DiagnosticError", "aurora_red").b(),
502 HiRow::fg("DiagnosticWarn", "first_light"),
503 HiRow::fg("DiagnosticInfo", "ice_cyan"),
504 HiRow::fg("DiagnosticHint", "aurora_green"),
505 HiRow::fg("GitSignsAdd", "aurora_green"),
507 HiRow::fg("GitSignsChange", "first_light"),
508 HiRow::fg("GitSignsDelete", "aurora_red"),
509 HiRow::bg("DiffAdd", "green_glass"),
510 HiRow::bg("DiffChange", "amber_glass"),
511 HiRow::bg("DiffDelete", "red_glass"),
512 HiRow::bg("DiffText", "steel_glass"),
513 HiRow::link("@function.call", "Function"),
515 HiRow::link("@variable", "Identifier"),
516 HiRow::fg("@parameter", "snow1").i(),
517 HiRow::fg("@comment.todo", "first_light").b(),
518 HiRow::fg("@comment.note", "ice_cyan").b(),
519 HiRow::fg("@comment.warning", "ember").b(),
520 ];
521
522 for r in rows {
523 let mut kvs: Vec<(&str, String)> = vec![(":group", format!("\"{}\"", r.group))];
524 if r.link.is_empty() {
525 if !r.fg.is_empty() {
526 kvs.push((":fg", format!("\"{}\"", lc_hex(&p, r.fg))));
527 }
528 if !r.bg.is_empty() {
529 kvs.push((":bg", format!("\"{}\"", lc_hex(&p, r.bg))));
530 }
531 if r.bold {
532 kvs.push((":bold", "#t".to_string()));
533 }
534 if r.italic {
535 kvs.push((":italic", "#t".to_string()));
536 }
537 } else {
538 kvs.push((":link", format!("\"{}\"", r.link)));
539 }
540 writeln!(out, "{}", lisp_form("defhighlight", &kvs)).expect("write");
541 }
542
543 out
544}
545
546#[cfg(test)]
547mod tests {
548
549 #[test]
556 fn the_nix_render_carries_the_same_palette_as_the_yaml() {
557 let yaml = super::render_base16();
558 let nixs = super::render_base16_nix();
559 for (slot, rgb) in VellumPalette::vellum().base16() {
560 let hex = super::slot_hex(rgb);
561 assert!(
562 yaml.contains(&format!("{slot}: {hex}")),
563 "yaml missing {slot}"
564 );
565 assert!(
566 nixs.contains(&format!("{slot} = \"{hex}\";")),
567 "nix missing {slot}"
568 );
569 }
570 }
571
572 #[test]
582 fn the_nix_render_keeps_the_scheme_identity_not_just_the_colours() {
583 let out = super::render_base16_nix();
584 for (k, v) in [
585 ("system", "base16"),
586 ("name", "Vellum"),
587 ("author", "pleme-io (ishou)"),
588 ("variant", "dark"),
589 ("slug", "vellum"),
590 ] {
591 assert!(
592 out.contains(&format!("{k} = \"{v}\";")),
593 "missing {k}; without it base16.nix names the scheme \"untitled\""
594 );
595 }
596 }
597
598 #[test]
604 fn the_nix_render_is_a_bare_attrset_with_no_yaml_key() {
605 let out = super::render_base16_nix();
606 let body: String = out.lines().filter(|l| !l.starts_with('#')).collect();
607 assert!(
608 body.trim_start().starts_with('{'),
609 "must be an attrset: {body}"
610 );
611 assert!(
612 !body.contains("yaml"),
613 "a `yaml` key would put stylix back on the IFD path"
614 );
615 assert!(!body.contains(':'), "must not be a function: {body}");
617 }
618 use super::*;
619
620 #[test]
621 fn base16_is_deterministic() {
622 assert_eq!(render_base16(), render_base16());
623 }
624
625 #[test]
626 fn skim_string_is_deterministic() {
627 assert_eq!(render_skim(), render_skim());
628 }
629
630 #[test]
647 fn skim_string_pins_the_born_vellum_wire_bytes() {
648 let expected = "\
652fg:#E2DBC8,\
653bg:#16140E,\
654hl:#94BBB8:bold:underlined,\
655fg+:#F4EFE2:bold,\
656bg+:#2B2820,\
657hl+:#A6CBC8:bold:underlined,\
658info:#6E6857,\
659prompt:#A9BB8C,\
660pointer:#ADD7A3,\
661marker:#B8A1B9,\
662spinner:#99AABE,\
663header:#99AABE,\
664border:#6E6857,\
665query:#F4EFE2:bold";
666 assert_eq!(
667 render_skim(),
668 expected,
669 "Vellum skim --color bytes drifted; the generated \
670 `nix run .#skim-vellum` artifact changed"
671 );
672 }
673
674 #[test]
675 fn escriba_lisp_is_deterministic() {
676 assert_eq!(render_escriba_lisp(), render_escriba_lisp());
677 }
678
679 #[test]
680 fn escriba_lisp_has_palette_and_key_highlights() {
681 let out = render_escriba_lisp();
682 assert!(
684 out.contains("(defpalette :name \"vellum\" :base00 \"#16140e\""),
685 "defpalette base00 missing/drifted:\n{out}"
686 );
687 for line in [
689 "(defhighlight :group \"Normal\" :fg \"#e2dbc8\" :bg \"#16140e\")",
690 "(defhighlight :group \"Comment\" :fg \"#90897b\" :italic #t)",
691 "(defhighlight :group \"String\" :fg \"#a9bb8c\")",
692 "(defhighlight :group \"Function\" :fg \"#99aabe\" :bold #t)",
693 "(defhighlight :group \"DiffAdd\" :bg \"#4d543e\")",
694 "(defhighlight :group \"@function.call\" :link \"Function\")",
695 ] {
696 assert!(out.contains(line), "missing line `{line}`\n{out}");
697 }
698 for g in [
700 "Normal",
701 "Comment",
702 "String",
703 "Number",
704 "Boolean",
705 "Function",
706 "Keyword",
707 "Statement",
708 "Type",
709 "Constant",
710 "Special",
711 "Visual",
712 "Search",
713 "DiagnosticError",
714 "GitSignsAdd",
715 ] {
716 assert!(
717 out.contains(&format!(":group \"{g}\"")),
718 "missing group {g}\n{out}"
719 );
720 }
721 }
722
723 #[test]
724 fn base16_has_all_16_slots_lowercase_unprefixed() {
725 let out = render_base16();
726 for slot in [
727 "base00", "base01", "base02", "base03", "base04", "base05", "base06", "base07",
728 "base08", "base09", "base0A", "base0B", "base0C", "base0D", "base0E", "base0F",
729 ] {
730 assert!(out.contains(slot), "missing {slot}");
731 }
732 assert!(out.contains("16140e"), "base00 night0 missing:\n{out}");
734 assert!(!out.contains(": #"), "found prefixed hex:\n{out}");
736 }
737
738 #[test]
739 fn base16_slots_match_the_born_palette() {
740 let p = VellumPalette::vellum();
741 let out = render_base16();
742 for (slot, rgb) in p.base16() {
743 let want = format!("{slot}: {}", slot_hex(rgb));
744 assert!(
745 out.contains(&want),
746 "slot {slot} drifted; want `{want}`\n{out}"
747 );
748 }
749 }
750
751 #[test]
752 fn base24_has_the_real_brights() {
753 let out = render_base24();
754 assert!(
756 out.contains("base14: add7a3"),
757 "base14 green_bright:\n{out}"
758 );
759 assert!(out.contains("base12: d49088"), "base12 red_bright:\n{out}");
760 assert!(out.contains("system: base24"), "wrong system tag:\n{out}");
761 }
762
763 #[test]
764 fn svg_palette_is_deterministic_and_covers_every_token() {
765 let a = render_svg_palette();
766 assert_eq!(a, render_svg_palette());
767 let p = VellumPalette::vellum();
768 for (name, _) in p.entries() {
769 assert!(a.contains(name), "svg missing token {name}");
770 }
771 }
772}