use std::fmt::Write as _;
use zenith_core::color::{parse_rgb, rgb_to_hex};
use zenith_core::theme::{PaletteSpec, Rgb, Scheme, synth_palette};
#[derive(Debug, Clone, Copy)]
pub struct Shape {
pub radius_box: f64,
pub radius_field: f64,
pub radius_selector: f64,
pub border: f64,
pub depth: bool,
pub noise: bool,
}
impl Default for Shape {
fn default() -> Self {
Self {
radius_box: 16.0,
radius_field: 8.0,
radius_selector: 8.0,
border: 1.0,
depth: false,
noise: false,
}
}
}
#[derive(Debug, Clone)]
pub struct ThemeInput<'a> {
pub name: &'a str,
pub scheme: Scheme,
pub primary: &'a str,
pub secondary: Option<&'a str>,
pub accent: Option<&'a str>,
pub neutral: Option<&'a str>,
pub info: Option<&'a str>,
pub success: Option<&'a str>,
pub warning: Option<&'a str>,
pub error: Option<&'a str>,
pub shape: Shape,
}
#[derive(Debug)]
pub struct ThemeErr {
pub message: String,
pub exit_code: u8,
}
fn hex(label: &str, s: &str) -> Result<Rgb, ThemeErr> {
parse_rgb(s).ok_or_else(|| ThemeErr {
message: format!("invalid {label} colour '{s}': expected '#rrggbb' (lowercase hex)"),
exit_code: 2,
})
}
fn opt(label: &str, s: Option<&str>) -> Result<Option<Rgb>, ThemeErr> {
match s {
Some(v) => Ok(Some(hex(label, v)?)),
None => Ok(None),
}
}
fn px(v: f64) -> String {
if v.fract() == 0.0 {
format!("{}", v as i64)
} else {
format!("{v}")
}
}
pub fn new(input: &ThemeInput) -> Result<String, ThemeErr> {
let spec = PaletteSpec {
scheme: input.scheme,
primary: hex("primary", input.primary)?,
secondary: opt("secondary", input.secondary)?,
accent: opt("accent", input.accent)?,
neutral: opt("neutral", input.neutral)?,
info: opt("info", input.info)?,
success: opt("success", input.success)?,
warning: opt("warning", input.warning)?,
error: opt("error", input.error)?,
};
let palette = synth_palette(&spec);
let raw = emit(input, &palette);
crate::commands::fmt::run(&raw)
.map(|r| String::from_utf8_lossy(&r.formatted).into_owned())
.map_err(|e| ThemeErr {
message: format!(
"internal: synthesized theme failed to format: {}",
e.message
),
exit_code: 2,
})
}
fn emit(input: &ThemeInput, palette: &[(&'static str, Rgb)]) -> String {
let name = input.name;
let scheme = match input.scheme {
Scheme::Light => "light",
Scheme::Dark => "dark",
};
let mut s = String::new();
let _ = writeln!(
s,
"// @zenith/theme.{name} — generated by `zenith theme new`"
);
let _ = writeln!(
s,
"// scheme: {scheme} | depth: {} | noise: {} (1 = apply grain-overlay recipe)",
input.shape.depth as u8, input.shape.noise as u8
);
let _ = writeln!(s, "zenith version=1 {{");
let _ = writeln!(
s,
" project id=\"@zenith/theme.{name}\" name=\"Theme: {name}\""
);
let _ = writeln!(s, " tokens format=\"zenith-token-v1\" {{");
for (id, rgb) in palette {
let _ = writeln!(
s,
" token id=\"color.{id}\" type=\"color\" value=\"{}\"",
rgb_to_hex(*rgb)
);
}
let _ = writeln!(
s,
" token id=\"radius.box\" type=\"dimension\" value=(px){}",
px(input.shape.radius_box)
);
let _ = writeln!(
s,
" token id=\"radius.field\" type=\"dimension\" value=(px){}",
px(input.shape.radius_field)
);
let _ = writeln!(
s,
" token id=\"radius.selector\" type=\"dimension\" value=(px){}",
px(input.shape.radius_selector)
);
let _ = writeln!(
s,
" token id=\"border.width\" type=\"dimension\" value=(px){}",
px(input.shape.border)
);
let _ = writeln!(
s,
" token id=\"space.unit\" type=\"dimension\" value=(px)4"
);
let _ = writeln!(
s,
" token id=\"font.heading\" type=\"fontFamily\" value=\"Noto Sans\""
);
let _ = writeln!(
s,
" token id=\"font.body\" type=\"fontFamily\" value=\"Noto Sans\""
);
let _ = writeln!(
s,
" token id=\"size.h1\" type=\"dimension\" value=(px)64"
);
let _ = writeln!(
s,
" token id=\"size.h2\" type=\"dimension\" value=(px)40"
);
let _ = writeln!(
s,
" token id=\"size.body\" type=\"dimension\" value=(px)28"
);
let _ = writeln!(
s,
" token id=\"size.caption\" type=\"dimension\" value=(px)18"
);
if input.shape.depth {
let _ = writeln!(
s,
" token id=\"color.shadow\" type=\"color\" value=\"#0000002a\""
);
let _ = writeln!(
s,
" token id=\"shadow.depth\" type=\"shadow\" {{ layer dx=(px)0 dy=(px)8 blur=(px)24 color=(token)\"color.shadow\" }}"
);
}
let _ = writeln!(s, " }}");
let _ = writeln!(s, " styles {{}}");
let _ = writeln!(
s,
" document id=\"theme.{name}.preview\" title=\"Theme {name}\" {{"
);
let _ = writeln!(
s,
" page id=\"pg\" w=(px)760 h=(px)220 background=(token)\"color.base.100\" {{"
);
let _ = writeln!(
s,
" text id=\"t.title\" x=(px)40 y=(px)28 w=(px)680 h=(px)52 fill=(token)\"color.base.content\" font-family=(token)\"font.heading\" font-size=(token)\"size.h2\" {{ span \"{name}\" }}"
);
let sh = if input.shape.depth {
" shadow=(token)\"shadow.depth\""
} else {
""
};
for (x, role) in [
(40, "primary"),
(220, "secondary"),
(400, "accent"),
(580, "neutral"),
] {
let _ = writeln!(
s,
" rect id=\"sw.{role}\" x=(px){x} y=(px)104 w=(px)160 h=(px)84 fill=(token)\"color.{role}\" radius=(token)\"radius.box\"{sh}"
);
}
let _ = writeln!(s, " }}");
let _ = writeln!(s, " }}");
let _ = writeln!(s, "}}");
s
}