ddctui 1.0.0

Terminal UI for controlling DDC/CI monitor properties
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

pub fn config_dir() -> Result<PathBuf> {
    let dir = dirs::config_dir()
        .context("could not determine user config directory")?
        .join("ddctui");
    fs::create_dir_all(&dir)
        .with_context(|| format!("failed to create config directory {}", dir.display()))?;
    Ok(dir)
}

/// Favorited VCP feature codes, scoped per monitor (keyed by
/// [`Monitor::cache_key`](crate::ddc::Monitor::cache_key)).
#[derive(Debug, Default)]
pub struct Favorites {
    path: PathBuf,
    entries: HashMap<String, Vec<u8>>,
}

impl Favorites {
    pub fn load(dir: &Path) -> Result<Self> {
        let path = dir.join("favorites.json");
        let entries = match fs::read_to_string(&path) {
            Ok(contents) => serde_json::from_str(&contents)
                .with_context(|| format!("failed to parse {}", path.display()))?,
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => HashMap::new(),
            Err(e) => return Err(e).with_context(|| format!("failed to read {}", path.display())),
        };
        Ok(Self { path, entries })
    }

    pub fn is_favorite(&self, monitor_key: &str, feature_code: u8) -> bool {
        self.entries
            .get(monitor_key)
            .is_some_and(|codes| codes.contains(&feature_code))
    }

    pub fn toggle(&mut self, monitor_key: &str, feature_code: u8) {
        let codes = self.entries.entry(monitor_key.to_string()).or_default();
        if let Some(pos) = codes.iter().position(|&c| c == feature_code) {
            codes.remove(pos);
            if codes.is_empty() {
                self.entries.remove(monitor_key);
            }
        } else {
            codes.push(feature_code);
        }
    }

    pub fn save(&self) -> Result<()> {
        let contents = serde_json::to_string_pretty(&self.entries)?;
        fs::write(&self.path, contents)
            .with_context(|| format!("failed to write {}", self.path.display()))
    }
}