use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Theme {
#[default]
Light,
Dark,
System,
#[serde(rename = "high_contrast_light")]
HighContrastLight,
#[serde(rename = "high_contrast_dark")]
HighContrastDark,
}
impl std::fmt::Display for Theme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Theme::Light => write!(f, "Light"),
Theme::Dark => write!(f, "Dark"),
Theme::System => write!(f, "System"),
Theme::HighContrastLight => write!(f, "High Contrast Light"),
Theme::HighContrastDark => write!(f, "High Contrast Dark"),
}
}
}
impl Theme {
pub fn is_high_contrast(self) -> bool {
matches!(self, Theme::HighContrastLight | Theme::HighContrastDark)
}
pub fn choices() -> &'static [Theme] {
&[Theme::Light, Theme::Dark, Theme::HighContrastLight, Theme::HighContrastDark]
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UserPrefs {
#[serde(default)]
pub theme: Theme,
#[serde(default)]
pub language: String,
#[serde(default = "default_ignored_dirs")]
pub global_ignored_dirs: Vec<String>,
}
fn default_ignored_dirs() -> Vec<String> {
vec![
".git".into(),
"target".into(),
"node_modules".into(),
".DS_Store".into(),
]
}
impl UserPrefs {
fn path() -> anyhow::Result<PathBuf> {
let base = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("Cannot determine home directory"))?
.join(".aaai");
std::fs::create_dir_all(&base)?;
Ok(base.join("prefs.yaml"))
}
pub fn load() -> Self {
match Self::path().and_then(|p| {
if !p.exists() { return Ok(Self::default()); }
let text = std::fs::read_to_string(&p)?;
serde_yaml::from_str(&text).map_err(|e| anyhow::anyhow!(e))
}) {
Ok(prefs) => prefs,
Err(e) => {
log::warn!("Could not load prefs: {e}");
Self::default()
}
}
}
pub fn save(&self) {
if let Err(e) = Self::path().and_then(|p| {
let yaml = serde_yaml::to_string(self).map_err(|e| anyhow::anyhow!(e))?;
std::fs::write(&p, yaml)?;
Ok(())
}) {
log::warn!("Could not save prefs: {e}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_yaml() {
let prefs = UserPrefs { theme: Theme::Dark, ..Default::default() };
let yaml = serde_yaml::to_string(&prefs).unwrap();
let restored: UserPrefs = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(restored.theme, Theme::Dark);
}
#[test]
fn default_is_light() {
assert_eq!(UserPrefs::default().theme, Theme::Light);
}
#[test]
fn display_names() {
assert_eq!(Theme::Light.to_string(), "Light");
assert_eq!(Theme::Dark.to_string(), "Dark");
}
#[test]
fn new_fields_round_trip() {
let p = UserPrefs {
language: "ja".into(),
global_ignored_dirs: vec![".git".into(), "target".into()],
..Default::default()
};
let yaml = serde_yaml::to_string(&p).unwrap();
let r: UserPrefs = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(r.language, "ja");
assert_eq!(r.global_ignored_dirs, vec![".git", "target"]);
}
#[test]
fn missing_fields_get_defaults() {
let yaml = "theme: light\n";
let p: UserPrefs = serde_yaml::from_str(yaml).unwrap();
assert!(!p.global_ignored_dirs.is_empty(), "default dirs should be applied");
assert_eq!(p.language, "", "absent language should be empty string");
}
}