- 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>
103 lines
3.4 KiB
Rust
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()
|
|
}
|