use serde_json::{json, Value};
use std::path::{Path, PathBuf};
pub const SCHEMES: &[&str] = &[
"cyberpunk",
"midnight",
"matrix",
"ember",
"arctic",
"crimson",
"toxic",
"vapor",
];
pub fn home() -> PathBuf {
let h = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_else(|_| ".".into());
PathBuf::from(h)
}
pub fn expand(p: &str) -> PathBuf {
if p == "~" {
return home();
}
if let Some(rest) = p.strip_prefix("~/") {
return home().join(rest);
}
PathBuf::from(p)
}
fn sanitize(name: &str, fallback: &str) -> String {
let s: String = name
.chars()
.filter(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
.collect();
let s = s.trim_matches('.').to_string();
if s.is_empty() {
fallback.to_string()
} else {
s
}
}
pub fn app_dir(app: &str) -> PathBuf {
let name = sanitize(app, "zwire");
let d = home().join(format!(".{name}"));
let _ = std::fs::create_dir_all(&d);
d
}
fn write_atomic(path: &Path, bytes: &[u8]) -> bool {
let tmp = path.with_extension("tmp");
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if std::fs::write(&tmp, bytes).is_ok() {
return std::fs::rename(&tmp, path).is_ok();
}
false
}
fn kv_path(app: &str, key: &str) -> PathBuf {
app_dir(app)
.join("kv")
.join(format!("{}.json", sanitize(key, "default")))
}
pub fn kv_get(app: &str, key: &str) -> Value {
std::fs::read_to_string(kv_path(app, key))
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or(Value::Null)
}
pub fn kv_set(app: &str, key: &str, value: &Value) -> bool {
serde_json::to_vec(value)
.map(|b| write_atomic(&kv_path(app, key), &b))
.unwrap_or(false)
}
pub fn kv_merge(app: &str, key: &str, partial: &Value) -> Value {
let mut cur = kv_get(app, key);
match (cur.as_object_mut(), partial.as_object()) {
(Some(c), Some(p)) => {
for (k, v) in p {
c.insert(k.clone(), v.clone());
}
}
_ => cur = partial.clone(),
}
kv_set(app, key, &cur);
cur
}
pub fn kv_del(app: &str, key: &str) -> bool {
let p = kv_path(app, key);
!p.exists() || std::fs::remove_file(&p).is_ok()
}
pub fn kv_keys(app: &str) -> Vec<String> {
let dir = app_dir(app).join("kv");
let mut keys = Vec::new();
if let Ok(rd) = std::fs::read_dir(dir) {
for e in rd.flatten() {
let name = e.file_name().to_string_lossy().to_string();
if let Some(k) = name.strip_suffix(".json") {
keys.push(k.to_string());
}
}
}
keys.sort();
keys
}
pub fn current_scheme(d: &Path) -> String {
std::fs::read_to_string(d.join("hud-scheme"))
.map(|s| s.trim().to_string())
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "cyberpunk".into())
}
pub fn write_scheme(d: &Path, s: &str) {
write_atomic(&d.join("hud-scheme"), format!("{s}\n").as_bytes());
}
pub fn current_ui(d: &Path) -> Value {
std::fs::read_to_string(d.join("hud-ui.json"))
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_else(|| json!({}))
}
pub fn write_ui(d: &Path, partial: &Value) -> Value {
let mut cur = current_ui(d);
if let (Some(c), Some(p)) = (cur.as_object_mut(), partial.as_object()) {
for (k, v) in p {
c.insert(k.clone(), v.clone());
}
}
if let Ok(s) = serde_json::to_vec(&cur) {
write_atomic(&d.join("hud-ui.json"), &s);
}
cur
}