2.3 KiB · text History aca7335
//! Small persisted preferences — where you last exported to, mainly.
//!
//! Separate from a document: which folder you export into is a property of
//! this machine, not of the icon, so it should not travel when a `.toml` is
//! shared or committed.

use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

const MAX_RECENT: usize = 8;

#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Debug)]
pub struct Config {
    /// Most recent first.
    #[serde(default)]
    pub recent_targets: Vec<String>,
    #[serde(default)]
    pub last_document_dir: String,
}

pub fn config_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("icon-studio").join("config.toml"))
}

impl Config {
    pub fn load() -> Config {
        // A missing or unreadable config is normal on first run and must never
        // be fatal; the defaults are perfectly usable.
        let Some(p) = config_path() else {
            return Config::default();
        };
        std::fs::read_to_string(p)
            .ok()
            .and_then(|s| toml::from_str(&s).ok())
            .unwrap_or_default()
    }

    pub fn save(&self) {
        let Some(p) = config_path() else { return };
        if let Some(dir) = p.parent() {
            if std::fs::create_dir_all(dir).is_err() {
                return;
            }
        }
        if let Ok(text) = toml::to_string(self) {
            let _ = std::fs::write(p, text);
        }
    }

    /// Records a destination, most-recent-first and without duplicates.
    pub fn remember_target(&mut self, path: &Path) {
        let s = path.display().to_string();
        if s.is_empty() {
            return;
        }
        self.recent_targets.retain(|r| r != &s);
        self.recent_targets.insert(0, s);
        self.recent_targets.truncate(MAX_RECENT);
    }

    /// Recent entries that still exist, so a deleted folder stops being
    /// offered rather than failing when chosen.
    pub fn live_targets(&self) -> Vec<&str> {
        self.recent_targets
            .iter()
            .map(String::as_str)
            .filter(|s| Path::new(s).is_dir())
            .collect()
    }
}