use crate::theme::ThemeName;
use std::path::PathBuf;
#[derive(Debug, Clone, Default)]
pub struct Prefs {
pub theme: ThemeName,
pub custom: Option<[u8; 6]>,
}
fn prefs_path() -> Option<PathBuf> {
let base = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?;
Some(base.join("zdbview").join("prefs"))
}
pub fn load() -> Prefs {
match prefs_path() {
Some(p) => load_from(&p),
None => Prefs::default(),
}
}
pub(crate) fn load_from(path: &std::path::Path) -> Prefs {
let mut prefs = Prefs::default();
if let Ok(contents) = std::fs::read_to_string(path) {
for line in contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((k, v)) = line.split_once('=') {
let (k, v) = (k.trim(), v.trim());
match k {
"theme" => {
if let Some(t) = ThemeName::from_token(v) {
prefs.theme = t;
}
}
"custom" => {
let nums: Vec<u8> =
v.split(',').filter_map(|n| n.trim().parse().ok()).collect();
if nums.len() == 6 {
prefs.custom =
Some([nums[0], nums[1], nums[2], nums[3], nums[4], nums[5]]);
}
}
_ => {}
}
}
}
}
prefs
}
pub fn save(prefs: &Prefs) {
if let Some(path) = prefs_path() {
save_to(&path, prefs);
}
}
pub(crate) fn save_to(path: &std::path::Path, prefs: &Prefs) {
if let Some(dir) = path.parent() {
let _ = std::fs::create_dir_all(dir);
}
let mut body = format!("theme = {}\n", prefs.theme.token());
if let Some(c) = prefs.custom {
body.push_str(&format!(
"custom = {},{},{},{},{},{}\n",
c[0], c[1], c[2], c[3], c[4], c[5]
));
}
let tmp = path.with_extension("tmp");
if std::fs::write(&tmp, body).is_ok() {
let _ = std::fs::rename(&tmp, path);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scratch(name: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("zdbview_prefs_{}_{}", std::process::id(), name));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
p.join("prefs")
}
#[test]
fn scheme_and_custom_palette_round_trip() {
let path = scratch("roundtrip");
save_to(
&path,
&Prefs {
theme: ThemeName::BladeRunner,
custom: Some([1, 2, 3, 4, 5, 6]),
},
);
let back = load_from(&path);
assert_eq!(back.theme, ThemeName::BladeRunner);
assert_eq!(back.custom, Some([1, 2, 3, 4, 5, 6]));
save_to(
&path,
&Prefs {
theme: ThemeName::RedSector,
custom: None,
},
);
let back = load_from(&path);
assert_eq!(back.theme, ThemeName::RedSector);
assert_eq!(back.custom, None);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[test]
fn save_leaves_no_partial_file_behind() {
let path = scratch("atomic");
save_to(
&path,
&Prefs {
theme: ThemeName::Zaibatsu,
custom: None,
},
);
assert!(path.is_file());
assert!(
!path.with_extension("tmp").exists(),
"the temp file must be renamed away, not left as a sibling"
);
let text = std::fs::read_to_string(&path).unwrap();
assert_eq!(text, "theme = zaibatsu\n", "content must be complete");
for name in [ThemeName::DeepNet, ThemeName::AcidRain] {
save_to(
&path,
&Prefs {
theme: name,
custom: None,
},
);
assert_eq!(load_from(&path).theme, name);
assert!(!path.with_extension("tmp").exists());
}
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[test]
fn a_truncated_file_reads_as_defaults() {
let path = scratch("truncated");
std::fs::write(&path, b"").unwrap();
assert_eq!(load_from(&path).theme, ThemeName::default());
std::fs::write(&path, b"theme = red_se").unwrap();
assert_eq!(
load_from(&path).theme,
ThemeName::default(),
"a half-written token is not a scheme"
);
std::fs::write(&path, b"# c\n\nnope = 1\ntheme = deep_net\n").unwrap();
assert_eq!(load_from(&path).theme, ThemeName::DeepNet);
std::fs::remove_file(&path).unwrap();
assert_eq!(load_from(&path).theme, ThemeName::default());
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
}