Files
codecraft/cli/src/theme.rs
T
Richard ZinkandClaude Opus 5.5 6dffcb501a Add terminal client, memory, model fallback, version and install improvements
- codecraft: full-screen terminal client (ratatui) – local tools and files,
  models via the CodeCraft server gateway or own providers (standalone with a
  guided setup), slash commands, @files, permission dialogs, resume, update
- Static Linux CLI builds (x86_64/aarch64, cargo-zigbuild) served by the
  server: /cli/install.sh and /cli/download/<target>
- Project memory (remember tool, .codecraft/memory.md) and personal memory
- Model fallback when a host is unreachable or a budget is used up
- Version/commit in UI and health, daily release check, "What's new" dialog
- PWA: install button, maskable icon, manifest id and screenshots
- Case-insensitive detection of connection errors for retries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 00:13:06 +01:00

103 lines
3.4 KiB
Rust

//! Colours from the web themes (same palettes as the web UI, 24-bit).
use ratatui::style::Color;
use serde_json::Value;
use std::sync::OnceLock;
const PALETTES: &str = include_str!("palettes.json");
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct Theme {
pub bg0: Color,
pub bg1: Color,
pub bg2: Color,
pub bg3: Color,
pub fg0: Color,
pub fg1: Color,
pub fg2: Color,
pub fg3: Color,
pub muted: Color,
pub accent: Color,
pub accent2: Color,
pub on_accent: Color,
pub red: Color,
pub green: Color,
pub blue: Color,
pub aqua: Color,
pub purple: Color,
pub keyword: Color,
pub string: Color,
pub comment: Color,
}
fn hex(v: &Value, key: &str, fallback: Color) -> Color {
v[key]
.as_str()
.and_then(|s| {
let s = s.trim_start_matches('#');
Some(Color::Rgb(u8::from_str_radix(s.get(0..2)?, 16).ok()?, u8::from_str_radix(s.get(2..4)?, 16).ok()?, u8::from_str_radix(s.get(4..6)?, 16).ok()?))
})
.unwrap_or(fallback)
}
pub fn names() -> Vec<String> {
let all: Value = serde_json::from_str(PALETTES).unwrap_or(Value::Null);
let mut out: Vec<String> = all.as_object().map(|o| o.keys().filter_map(|k| k.rsplit_once('-').map(|(b, _)| b.to_string())).collect()).unwrap_or_default();
out.dedup();
out
}
/// Is the terminal background dark? (COLORFGBG, else assume dark)
fn terminal_dark() -> bool {
std::env::var("COLORFGBG").ok().and_then(|v| v.rsplit(';').next().and_then(|b| b.parse::<u8>().ok())).map(|bg| bg < 7 || bg == 8).unwrap_or(true)
}
impl Theme {
/// `base` e.g. "gruvbox", `mode` "light" | "dark" | "auto".
pub fn load(base: &str, mode: &str) -> Self {
let dark = match mode {
"light" => false,
"dark" => true,
_ => terminal_dark(),
};
let all: Value = serde_json::from_str(PALETTES).unwrap_or(Value::Null);
let key = format!("{base}-{}", if dark { "dark" } else { "light" });
let p = all.get(&key).or_else(|| all.get(if dark { "gruvbox-dark" } else { "gruvbox-light" })).cloned().unwrap_or(Value::Null);
let d = |k: &str, f: Color| hex(&p, k, f);
Self {
bg0: d("bg0", Color::Reset),
bg1: d("bg1", Color::Reset),
bg2: d("bg2", Color::DarkGray),
bg3: d("bg3", Color::DarkGray),
fg0: d("fg0", Color::White),
fg1: d("fg1", Color::Reset),
fg2: d("fg2", Color::Gray),
fg3: d("fg3", Color::Gray),
muted: d("muted", Color::DarkGray),
accent: d("accent", Color::Yellow),
accent2: d("accent2", Color::LightYellow),
on_accent: d("on-accent", Color::Black),
red: d("red", Color::Red),
green: d("green", Color::Green),
blue: d("blue", Color::Blue),
aqua: d("aqua", Color::Cyan),
purple: d("purple", Color::Magenta),
keyword: d("syntax-keyword", Color::Red),
string: d("syntax-string", Color::Green),
comment: d("syntax-comment", Color::DarkGray),
}
}
}
static CURRENT: OnceLock<std::sync::RwLock<Theme>> = OnceLock::new();
pub fn set(t: Theme) {
let lock = CURRENT.get_or_init(|| std::sync::RwLock::new(t));
*lock.write().unwrap() = t;
}
pub fn get() -> Theme {
*CURRENT.get_or_init(|| std::sync::RwLock::new(Theme::load("gruvbox", "auto"))).read().unwrap()
}