use indexmap::IndexMap;
use ishou_tokens::{Rgb, VellumPalette};
use serde::Serialize;
fn slot_hex(rgb: Rgb) -> String {
format!("{:02x}{:02x}{:02x}", rgb.r, rgb.g, rgb.b)
}
#[derive(Serialize)]
struct Scheme {
system: &'static str,
name: &'static str,
author: &'static str,
variant: &'static str,
slug: &'static str,
palette: IndexMap<String, String>,
}
fn scheme(system: &'static str, palette: IndexMap<String, String>) -> Scheme {
Scheme {
system,
name: "Vellum",
author: "pleme-io (ishou)",
variant: "dark",
slug: "vellum",
palette,
}
}
const HEADER: &str =
"# Generated by ishou-render::vellum — DO NOT EDIT\n\
# Source of truth: pleme-io/ishou/crates/ishou-tokens/src/vellum.rs\n\
# Vellum — the fleet theme (warm aged-paper Nord-matte)\n";
#[must_use]
pub fn render_base16() -> String {
let p = VellumPalette::vellum();
let mut palette = IndexMap::new();
for (slot, rgb) in p.base16() {
palette.insert(slot.to_string(), slot_hex(rgb));
}
let body = serde_yaml::to_string(&scheme("base16", palette))
.expect("Scheme is always serializable");
format!("{HEADER}{body}")
}
#[must_use]
pub fn render_base24() -> String {
let p = VellumPalette::vellum();
let mut palette = IndexMap::new();
for (slot, rgb) in p.base24() {
palette.insert(slot.to_string(), slot_hex(rgb));
}
let body = serde_yaml::to_string(&scheme("base24", palette))
.expect("Scheme is always serializable");
format!("{HEADER}{body}")
}
#[must_use]
pub fn render_svg_palette() -> String {
let p = VellumPalette::vellum();
let entries = p.entries();
let cols = 6usize;
let chip = 96i32;
let pad = 12i32;
let label_h = 22i32;
let rows = entries.len().div_ceil(cols) as i32;
let width = cols as i32 * (chip + pad) + pad;
let height = rows * (chip + label_h + pad) + pad;
let mut chips = String::new();
for (i, (name, rgb)) in entries.iter().enumerate() {
let col = (i % cols) as i32;
let row = (i / cols) as i32;
let x = pad + col * (chip + pad);
let y = pad + row * (chip + label_h + pad);
let hex = rgb.hex();
let lum = 0.2126 * f64::from(rgb.r) + 0.7152 * f64::from(rgb.g) + 0.0722 * f64::from(rgb.b);
let text_fill = if lum > 140.0 { "#16140E" } else { "#F4EFE2" };
chips.push_str(&format!(
" <rect x=\"{x}\" y=\"{y}\" width=\"{chip}\" height=\"{chip}\" rx=\"8\" fill=\"{hex}\"/>\n \
<text x=\"{tx}\" y=\"{ty}\" font-family=\"monospace\" font-size=\"9\" fill=\"{text_fill}\">{hex}</text>\n \
<text x=\"{x}\" y=\"{ly}\" font-family=\"monospace\" font-size=\"10\" fill=\"#E2DBC8\">{name}</text>\n",
tx = x + 6,
ty = y + chip - 8,
ly = y + chip + 15,
));
}
format!(
"<!-- ishou Vellum palette preview (generated) -->\n\
<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\" width=\"{width}\" height=\"{height}\">\n \
<rect width=\"{width}\" height=\"{height}\" fill=\"#16140E\"/>\n{chips}</svg>\n"
)
}
fn tok(p: &VellumPalette, name: &str) -> String {
p.get(name)
.unwrap_or_else(|| panic!("vellum token `{name}` missing"))
.hex()
}
#[must_use]
pub fn render_skim() -> String {
let p = VellumPalette::vellum();
let rows: [(&str, &str, &str); 14] = [
("fg", "snow1", ""),
("bg", "night0", ""),
("hl", "ice_cyan", ":bold:underlined"),
("fg+", "snow3", ":bold"),
("bg+", "night2", ""),
("hl+", "cyan_bright", ":bold:underlined"),
("info", "shadow0", ""),
("prompt", "aurora_green", ""),
("pointer", "green_bright", ""),
("marker", "solar_magenta", ""),
("spinner", "ice_steel", ""),
("header", "ice_steel", ""),
("border", "shadow0", ""),
("query", "snow3", ":bold"),
];
rows.iter()
.map(|(key, token, attr)| format!("{key}:{}{attr}", tok(&p, token)))
.collect::<Vec<_>>()
.join(",")
}
struct HiRow {
group: &'static str,
fg: &'static str,
bg: &'static str,
bold: bool,
italic: bool,
link: &'static str,
}
impl HiRow {
const fn fg(group: &'static str, fg: &'static str) -> Self {
Self { group, fg, bg: "", bold: false, italic: false, link: "" }
}
const fn bg(group: &'static str, bg: &'static str) -> Self {
Self { group, fg: "", bg, bold: false, italic: false, link: "" }
}
const fn fg_bg(group: &'static str, fg: &'static str, bg: &'static str) -> Self {
Self { group, fg, bg, bold: false, italic: false, link: "" }
}
const fn link(group: &'static str, link: &'static str) -> Self {
Self { group, fg: "", bg: "", bold: false, italic: false, link }
}
const fn b(mut self) -> Self { self.bold = true; self }
const fn i(mut self) -> Self { self.italic = true; self }
}
fn lc_hex(p: &VellumPalette, name: &str) -> String {
let h = tok(p, name); format!("#{}", h[1..].to_ascii_lowercase())
}
fn lisp_form(head: &str, kvs: &[(&str, String)]) -> String {
use std::fmt::Write as _;
let mut s = String::new();
write!(s, "({head}").expect("write to String");
for (k, v) in kvs {
write!(s, " {k} {v}").expect("write to String");
}
s.push(')');
s
}
#[must_use]
pub fn render_escriba_lisp() -> String {
use std::fmt::Write as _;
let p = VellumPalette::vellum();
let mut out = String::new();
out.push_str(
"; escriba — Vellum theme (the fleet default)\n\
; Generated by ishou-render::vellum::render_escriba_lisp — DO NOT EDIT\n\
; Source of truth: pleme-io/ishou/crates/ishou-tokens/src/vellum.rs\n\
; Vellum — warm aged-paper Nord-matte; every hex is a BORN ishou token.\n\n",
);
writeln!(out, "{}", lisp_form("deftheme", &[(":preset", "\"vellum\"".to_string())]))
.expect("write");
out.push('\n');
let palette_kvs: Vec<(&str, String)> = vec![
(":name", "\"vellum\"".to_string()),
(":base00", format!("\"{}\"", lc_hex(&p, "night0"))),
(":base01", format!("\"{}\"", lc_hex(&p, "night1"))),
(":base02", format!("\"{}\"", lc_hex(&p, "night2"))),
(":base03", format!("\"{}\"", lc_hex(&p, "shadow1"))),
(":base04", format!("\"{}\"", lc_hex(&p, "snow0"))),
(":base05", format!("\"{}\"", lc_hex(&p, "snow1"))),
(":base06", format!("\"{}\"", lc_hex(&p, "snow2"))),
(":base07", format!("\"{}\"", lc_hex(&p, "snow3"))),
(":base08", format!("\"{}\"", lc_hex(&p, "aurora_red"))),
(":base09", format!("\"{}\"", lc_hex(&p, "ember"))),
(":base0a", format!("\"{}\"", lc_hex(&p, "first_light"))),
(":base0b", format!("\"{}\"", lc_hex(&p, "aurora_green"))),
(":base0c", format!("\"{}\"", lc_hex(&p, "ice_cyan"))),
(":base0d", format!("\"{}\"", lc_hex(&p, "ice_steel"))),
(":base0e", format!("\"{}\"", lc_hex(&p, "solar_magenta"))),
(":base0f", format!("\"{}\"", lc_hex(&p, "dusk_bronze"))),
];
writeln!(out, "{}", lisp_form("defpalette", &palette_kvs)).expect("write");
out.push('\n');
let rows: &[HiRow] = &[
HiRow::fg_bg("Normal", "snow1", "night0"),
HiRow::fg("Comment", "shadow1").i(),
HiRow::fg("String", "aurora_green"),
HiRow::fg("Number", "solar_magenta"),
HiRow::fg("Boolean", "solar_magenta"),
HiRow::fg("Function", "ice_steel").b(),
HiRow::fg("Keyword", "solar_magenta").i(),
HiRow::fg("Statement", "solar_magenta"),
HiRow::fg("Conditional", "solar_magenta"),
HiRow::fg("Repeat", "solar_magenta"),
HiRow::fg("Operator", "solar_magenta"),
HiRow::fg("Type", "first_light"),
HiRow::fg("Structure", "first_light"),
HiRow::fg("Identifier", "snow1"),
HiRow::fg("Constant", "ember"),
HiRow::fg("PreProc", "ember"),
HiRow::fg("Macro", "ember"),
HiRow::fg("Special", "first_light"),
HiRow::bg("CursorLine", "night1"),
HiRow::bg("CursorColumn", "night1"),
HiRow::fg("LineNr", "shadow1"),
HiRow::bg("SignColumn", "night0"),
HiRow::bg("Visual", "night2"),
HiRow::bg("VisualNOS", "night2"),
HiRow::fg_bg("Search", "night0", "first_light"),
HiRow::fg_bg("IncSearch", "night0", "ember").b(),
HiRow::fg("MatchParen", "ember").b(),
HiRow::fg_bg("StatusLine", "snow1", "night1"),
HiRow::fg_bg("StatusLineNC", "shadow1", "night0"),
HiRow::fg_bg("TabLine", "shadow1", "night0"),
HiRow::bg("TabLineFill", "night0"),
HiRow::fg_bg("TabLineSel", "night0", "ice_cyan").b(),
HiRow::fg("VertSplit", "night3"),
HiRow::fg_bg("Pmenu", "snow1", "night1"),
HiRow::fg_bg("PmenuSel", "night0", "ice_cyan").b(),
HiRow::bg("PmenuSbar", "night1"),
HiRow::bg("PmenuThumb", "shadow1"),
HiRow::fg_bg("NormalFloat", "snow1", "night1"),
HiRow::fg_bg("FloatBorder", "ice_steel", "night1"),
HiRow::fg("DiagnosticError", "aurora_red").b(),
HiRow::fg("DiagnosticWarn", "first_light"),
HiRow::fg("DiagnosticInfo", "ice_cyan"),
HiRow::fg("DiagnosticHint", "aurora_green"),
HiRow::fg("GitSignsAdd", "aurora_green"),
HiRow::fg("GitSignsChange", "first_light"),
HiRow::fg("GitSignsDelete", "aurora_red"),
HiRow::bg("DiffAdd", "green_glass"),
HiRow::bg("DiffChange", "amber_glass"),
HiRow::bg("DiffDelete", "red_glass"),
HiRow::bg("DiffText", "steel_glass"),
HiRow::link("@function.call", "Function"),
HiRow::link("@variable", "Identifier"),
HiRow::fg("@parameter", "snow1").i(),
HiRow::fg("@comment.todo", "first_light").b(),
HiRow::fg("@comment.note", "ice_cyan").b(),
HiRow::fg("@comment.warning", "ember").b(),
];
for r in rows {
let mut kvs: Vec<(&str, String)> = vec![(":group", format!("\"{}\"", r.group))];
if r.link.is_empty() {
if !r.fg.is_empty() {
kvs.push((":fg", format!("\"{}\"", lc_hex(&p, r.fg))));
}
if !r.bg.is_empty() {
kvs.push((":bg", format!("\"{}\"", lc_hex(&p, r.bg))));
}
if r.bold {
kvs.push((":bold", "#t".to_string()));
}
if r.italic {
kvs.push((":italic", "#t".to_string()));
}
} else {
kvs.push((":link", format!("\"{}\"", r.link)));
}
writeln!(out, "{}", lisp_form("defhighlight", &kvs)).expect("write");
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base16_is_deterministic() {
assert_eq!(render_base16(), render_base16());
}
#[test]
fn skim_string_is_deterministic() {
assert_eq!(render_skim(), render_skim());
}
#[test]
fn skim_string_matches_the_authored_vellum_string() {
let expected = "\
fg:#E2DBC8,\
bg:#16140E,\
hl:#94BBB8:bold:underlined,\
fg+:#F4EFE2:bold,\
bg+:#2B2820,\
hl+:#A6CBC8:bold:underlined,\
info:#6E6857,\
prompt:#A9BB8C,\
pointer:#ADD7A3,\
marker:#B8A1B9,\
spinner:#99AABE,\
header:#99AABE,\
border:#6E6857,\
query:#F4EFE2:bold";
assert_eq!(render_skim(), expected, "skim --color string drifted");
}
#[test]
fn escriba_lisp_is_deterministic() {
assert_eq!(render_escriba_lisp(), render_escriba_lisp());
}
#[test]
fn escriba_lisp_has_palette_and_key_highlights() {
let out = render_escriba_lisp();
assert!(
out.contains("(defpalette :name \"vellum\" :base00 \"#16140e\""),
"defpalette base00 missing/drifted:\n{out}"
);
for line in [
"(defhighlight :group \"Normal\" :fg \"#e2dbc8\" :bg \"#16140e\")",
"(defhighlight :group \"Comment\" :fg \"#90897b\" :italic #t)",
"(defhighlight :group \"String\" :fg \"#a9bb8c\")",
"(defhighlight :group \"Function\" :fg \"#99aabe\" :bold #t)",
"(defhighlight :group \"DiffAdd\" :bg \"#4d543e\")",
"(defhighlight :group \"@function.call\" :link \"Function\")",
] {
assert!(out.contains(line), "missing line `{line}`\n{out}");
}
for g in [
"Normal", "Comment", "String", "Number", "Boolean", "Function",
"Keyword", "Statement", "Type", "Constant", "Special",
"Visual", "Search", "DiagnosticError", "GitSignsAdd",
] {
assert!(
out.contains(&format!(":group \"{g}\"")),
"missing group {g}\n{out}"
);
}
}
#[test]
fn base16_has_all_16_slots_lowercase_unprefixed() {
let out = render_base16();
for slot in [
"base00", "base01", "base02", "base03", "base04", "base05",
"base06", "base07", "base08", "base09", "base0A", "base0B",
"base0C", "base0D", "base0E", "base0F",
] {
assert!(out.contains(slot), "missing {slot}");
}
assert!(out.contains("16140e"), "base00 night0 missing:\n{out}");
assert!(!out.contains(": #"), "found prefixed hex:\n{out}");
}
#[test]
fn base16_slots_match_the_born_palette() {
let p = VellumPalette::vellum();
let out = render_base16();
for (slot, rgb) in p.base16() {
let want = format!("{slot}: {}", slot_hex(rgb));
assert!(out.contains(&want), "slot {slot} drifted; want `{want}`\n{out}");
}
}
#[test]
fn base24_has_the_real_brights() {
let out = render_base24();
assert!(out.contains("base14: add7a3"), "base14 green_bright:\n{out}");
assert!(out.contains("base12: d49088"), "base12 red_bright:\n{out}");
assert!(out.contains("system: base24"), "wrong system tag:\n{out}");
}
#[test]
fn svg_palette_is_deterministic_and_covers_every_token() {
let a = render_svg_palette();
assert_eq!(a, render_svg_palette());
let p = VellumPalette::vellum();
for (name, _) in p.entries() {
assert!(a.contains(name), "svg missing token {name}");
}
}
}