use std::collections::HashMap;
use ratatui::style::Color;
use serde_json::Value;
use super::{SourcePalette, Variant, XTERM16};
use crate::tui::render::user_themes::parse_hex;
pub fn parse(src: &str, variant: Variant) -> Result<SourcePalette, String> {
let doc: Value = serde_json::from_str(src).map_err(|e| format!("opencode parse error: {e}"))?;
let defs = raw_defs(doc.get("defs"))?;
let theme = doc.get("theme").ok_or("missing \"theme\" object")?;
let tok = |name: &str, base: Option<Color>| -> Result<Option<Color>, String> {
match theme.get(name) {
Some(v) => resolve_value(v, &defs, variant, base, name),
None => Ok(None),
}
};
let bg =
tok("background", None)?.ok_or("token \"background\" missing or \"none\" (required)")?;
let base = Some(bg);
let fg = tok("text", base)?.ok_or("token \"text\" missing or \"none\" (required)")?;
let xterm = |i: usize| Color::Rgb(XTERM16[i].0, XTERM16[i].1, XTERM16[i].2);
let accent = tok("accent", base)?;
let purple = tok("secondary", base)?;
let info = tok("info", base)?;
let link = tok("markdownLink", base)?;
let muted = tok("textMuted", base)?;
let panel = tok("backgroundPanel", base)?;
let element = tok("backgroundElement", base)?;
let selection = tok("selectionBackground", base)?;
let border = tok("border", base)?;
let border_subtle = tok("borderSubtle", base)?;
let link_text = tok("markdownLinkText", base)?;
let red = tok("error", base)?.unwrap_or_else(|| xterm(1));
let green = tok("success", base)?.unwrap_or_else(|| xterm(2));
let yellow = tok("warning", base)?.unwrap_or_else(|| xterm(3));
let blue = tok("primary", base)?.unwrap_or_else(|| xterm(4));
let magenta = purple.unwrap_or_else(|| xterm(5));
let cyan = link_text.or(info).unwrap_or_else(|| xterm(6));
let black = border_subtle.or(border).unwrap_or_else(|| xterm(0));
let white = muted.unwrap_or(fg);
Ok(SourcePalette {
bg,
fg,
black,
red,
green,
yellow,
blue,
magenta,
cyan,
white,
bright_black: black,
bright_red: red,
bright_green: green,
bright_yellow: yellow,
bright_blue: blue,
bright_magenta: magenta,
bright_cyan: cyan,
bright_white: fg,
accent,
purple,
info,
link,
muted,
panel,
element,
selection,
border,
border_subtle,
})
}
fn raw_defs(defs: Option<&Value>) -> Result<HashMap<String, Value>, String> {
let mut out = HashMap::new();
let Some(defs) = defs else { return Ok(out) };
let obj = defs.as_object().ok_or("\"defs\" is not an object")?;
for (name, v) in obj {
out.insert(name.clone(), v.clone());
}
Ok(out)
}
fn resolve_value(
v: &Value,
defs: &HashMap<String, Value>,
variant: Variant,
base: Option<Color>,
path: &str,
) -> Result<Option<Color>, String> {
if let Some(obj) = v.as_object() {
let key = match variant {
Variant::Dark => "dark",
Variant::Light => "light",
};
let inner = obj
.get(key)
.ok_or_else(|| format!("{path}: variant object without \"{key}\" key"))?;
return resolve_value(inner, defs, variant, base, path);
}
resolve_with_defs(v, defs, base, path)
}
fn scalar_to_color(v: &Value, base: Option<Color>, path: &str) -> Result<Option<Color>, String> {
if let Some(i) = v.as_i64() {
let idx = usize::try_from(i)
.ok()
.filter(|&i| i < XTERM16.len())
.ok_or_else(|| {
format!("{path}: unsupported ANSI index {i} (only 0-15 resolve to RGB)")
})?;
let (r, g, b) = XTERM16[idx];
return Ok(Some(Color::Rgb(r, g, b)));
}
let Some(s) = v.as_str() else {
return Err(format!("{path}: value is neither a string nor an integer"));
};
if s.eq_ignore_ascii_case("none") {
return Ok(None);
}
if s.starts_with('#') {
let normalized = normalize_hex(s, base).map_err(|e| format!("{path}: {e}"))?;
return parse_hex(&normalized)
.map(Some)
.ok_or_else(|| format!("{path}: invalid hex {s:?}"));
}
Err(format!(
"{path}: unresolved value {s:?} (defs references need the defs table)"
))
}
fn normalize_hex(s: &str, base: Option<Color>) -> Result<String, String> {
let h = s
.strip_prefix('#')
.ok_or_else(|| format!("invalid hex {s:?} (missing '#')"))?;
if !h.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(format!("invalid hex {s:?} (non-hex digits)"));
}
match h.len() {
6 => Ok(s.to_string()),
3 => Ok(format!(
"#{}{}{}",
h[0..1].repeat(2),
h[1..2].repeat(2),
h[2..3].repeat(2)
)),
8 => {
let v = u64::from_str_radix(h, 16).map_err(|e| format!("invalid hex {s:?}: {e}"))?;
let (r, g, b, a) = (
(v >> 24) as u8,
((v >> 16) & 0xff) as u8,
((v >> 8) & 0xff) as u8,
(v & 0xff) as u8,
);
let (br, bg_, bb) = match base {
Some(Color::Rgb(r, g, b)) => (r, g, b),
_ => (0, 0, 0),
};
let a32 = u32::from(a);
let over = |c: u8, bs: u8| -> u8 {
((u32::from(c) * a32 + u32::from(bs) * (255 - a32) + 127) / 255) as u8
};
Ok(format!(
"#{:02x}{:02x}{:02x}",
over(r, br),
over(g, bg_),
over(b, bb)
))
}
_ => Err(format!(
"invalid hex {s:?} (expected #RGB, #RRGGBB or #RRGGBBAA)"
)),
}
}
fn resolve_with_defs(
v: &Value,
defs: &HashMap<String, Value>,
base: Option<Color>,
path: &str,
) -> Result<Option<Color>, String> {
if let Some(s) = v.as_str()
&& !s.starts_with('#')
&& !s.eq_ignore_ascii_case("none")
&& let Some(d) = defs.get(s)
{
return scalar_to_color(d, base, &format!("{path} (defs.{s})"));
}
scalar_to_color(v, base, path)
}