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
}
}
fn state_base() -> PathBuf {
#[cfg(target_os = "macos")]
{
home().join("Library").join("Application Support")
}
#[cfg(windows)]
{
std::env::var("APPDATA")
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| home().join("AppData").join("Roaming"))
}
#[cfg(not(any(target_os = "macos", windows)))]
{
std::env::var("XDG_CONFIG_HOME")
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| home().join(".config"))
}
}
#[cfg(target_os = "macos")]
const ZWIRE_DIRNAME: &str = "com.menketechnologies.zwire";
#[cfg(not(target_os = "macos"))]
const ZWIRE_DIRNAME: &str = "zwire";
pub fn app_dir(app: &str) -> PathBuf {
let name = sanitize(app, "zwire");
let d = if name == "zwire" {
match std::env::var("ZWIRE_STATE") {
Ok(s) if !s.is_empty() => PathBuf::from(s),
_ => state_base().join(ZWIRE_DIRNAME),
}
} else {
state_base().join(&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 theme_dir() -> PathBuf {
let d = match std::env::var("ZWIRE_GLOBAL_DIR") {
Ok(s) if !s.is_empty() => PathBuf::from(s),
_ => home().join(".zwire"),
};
let _ = std::fs::create_dir_all(&d);
d
}
fn global_path(d: &Path) -> PathBuf {
d.join("global.toml")
}
fn load_global(d: &Path) -> toml::Value {
std::fs::read_to_string(global_path(d))
.ok()
.and_then(|s| s.parse::<toml::Value>().ok())
.filter(|v| v.is_table())
.unwrap_or_else(|| toml::Value::Table(Default::default()))
}
fn save_global(d: &Path, v: &toml::Value) {
if let Ok(s) = toml::to_string_pretty(v) {
write_atomic(&global_path(d), s.as_bytes());
}
}
fn set_path(root: &mut toml::Value, path: &[&str], val: toml::Value) {
fn go(tbl: &mut toml::map::Map<String, toml::Value>, path: &[&str], val: toml::Value) {
if path.len() == 1 {
tbl.insert(path[0].to_string(), val);
return;
}
let e = tbl
.entry(path[0].to_string())
.or_insert_with(|| toml::Value::Table(Default::default()));
if !e.is_table() {
*e = toml::Value::Table(Default::default());
}
go(e.as_table_mut().unwrap(), &path[1..], val);
}
if let Some(tbl) = root.as_table_mut() {
go(tbl, path, val);
}
}
fn ui_from(root: &toml::Value) -> Value {
root.get("theme")
.and_then(|t| t.get("ui"))
.and_then(|u| serde_json::to_value(u).ok())
.filter(|v| v.is_object())
.unwrap_or_else(|| json!({}))
}
pub fn current_scheme(d: &Path) -> String {
load_global(d)
.get("theme")
.and_then(|t| t.get("scheme"))
.and_then(|s| s.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.unwrap_or_else(|| "cyberpunk".into())
}
fn with_global_lock<F: FnOnce()>(d: &Path, f: F) {
let _ = std::fs::create_dir_all(d);
match std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(d.join("global.toml.lock"))
{
Ok(lock) => {
let _ = lock.lock(); f();
let _ = lock.unlock();
}
Err(_) => f(), }
}
pub fn write_scheme(d: &Path, s: &str) {
with_global_lock(d, || {
let mut root = load_global(d);
set_path(
&mut root,
&["theme", "scheme"],
toml::Value::String(s.to_string()),
);
save_global(d, &root);
});
write_atomic(&d.join("hud-scheme"), format!("{s}\n").as_bytes());
let light = current_ui(d)
.get("light")
.and_then(|v| v.as_bool())
.unwrap_or(false);
write_hud_light(d, light);
write_atomic(
&app_dir("zwire").join("hud-scheme"),
format!("{s}\n").as_bytes(),
);
}
pub fn current_ui(d: &Path) -> Value {
ui_from(&load_global(d))
}
pub fn write_ui(d: &Path, partial: &Value) -> Value {
let mut ui = json!({});
with_global_lock(d, || {
let mut root = load_global(d);
ui = ui_from(&root);
if let (Some(c), Some(p)) = (ui.as_object_mut(), partial.as_object()) {
for (k, v) in p {
c.insert(k.clone(), v.clone());
}
}
let ui_toml =
toml::Value::try_from(&ui).unwrap_or_else(|_| toml::Value::Table(Default::default()));
set_path(&mut root, &["theme", "ui"], ui_toml);
save_global(d, &root);
});
write_hud_light(
d,
ui.get("light").and_then(|v| v.as_bool()).unwrap_or(false),
);
ui
}
fn write_hud_light(d: &Path, light: bool) {
write_atomic(&d.join("hud-light"), if light { b"1\n" } else { b"0\n" });
}