texforge 0.6.0

Self-contained LaTeX to PDF compiler CLI
//! Global user configuration system.
//!
//! Stores user preferences in `~/.texforge/config.toml` or `$XDG_CONFIG_HOME/texforge/config.toml`.
//! Supports TOML format with user, institution, defaults, and templates sections.

use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;

/// Global configuration structure
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
    #[serde(default)]
    pub user: UserConfig,
    #[serde(default)]
    pub institution: InstitutionConfig,
    #[serde(default)]
    pub defaults: DefaultsConfig,
    #[serde(default)]
    pub templates: TemplatesConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UserConfig {
    pub name: Option<String>,
    pub email: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct InstitutionConfig {
    pub name: Option<String>,
    pub address: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DefaultsConfig {
    pub documentclass: Option<String>,
    pub fontsize: Option<String>,
    pub papersize: Option<String>,
    pub language: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TemplatesConfig {
    pub source: Option<String>,
    pub auto_update: Option<bool>,
    pub watch: Option<bool>,
}

/// Returns the path to the user's texforge config directory.
/// Prefers `$XDG_CONFIG_HOME/texforge`, falls back to `~/.texforge/config.toml`.
#[allow(dead_code)]
fn config_dir() -> Result<PathBuf> {
    if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
        let path = PathBuf::from(xdg_config).join("texforge");
        return Ok(path);
    }

    let home = dirs::home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
    Ok(home.join(".texforge"))
}

/// Returns the path to the config.toml file
pub fn config_file_path() -> Result<PathBuf> {
    if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
        let path = PathBuf::from(xdg_config).join("texforge/config.toml");
        return Ok(path);
    }

    let home = dirs::home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
    Ok(home.join(".texforge/config.toml"))
}

/// Load config from `~/.texforge/config.toml` or `XDG_CONFIG_HOME/texforge/config.toml`
pub fn load() -> Result<Config> {
    let path = config_file_path()?;

    if !path.exists() {
        // Return empty config if file doesn't exist
        return Ok(Config::default());
    }

    let content = fs::read_to_string(&path)
        .with_context(|| format!("Failed to read config file: {}", path.display()))?;

    toml::from_str(&content).context("Failed to parse config TOML")
}

/// Save config to `~/.texforge/config.toml` or `XDG_CONFIG_HOME/texforge/config.toml`
pub fn save(config: &Config) -> Result<()> {
    let path = config_file_path()?;
    let dir = path
        .parent()
        .ok_or_else(|| anyhow!("Invalid config path"))?;

    // Create directory if it doesn't exist
    fs::create_dir_all(dir).context("Failed to create config directory")?;

    let content = toml::to_string_pretty(config).context("Failed to serialize config")?;

    fs::write(&path, content)
        .with_context(|| format!("Failed to write config file: {}", path.display()))?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_config() {
        let toml_str = r#"
[user]
name = "Jane Doe"
email = "jane@example.com"

[defaults]
documentclass = "article"
fontsize = "11pt"
"#;
        let config: Config = toml::from_str(toml_str).unwrap();
        assert_eq!(config.user.name, Some("Jane Doe".to_string()));
        assert_eq!(config.defaults.fontsize, Some("11pt".to_string()));
    }

    #[test]
    fn test_serialize_config() {
        let mut config = Config::default();
        config.user.name = Some("John Doe".to_string());
        config.user.email = Some("john@example.com".to_string());

        let toml_str = toml::to_string_pretty(&config).unwrap();
        assert!(toml_str.contains("John Doe"));
        assert!(toml_str.contains("john@example.com"));
    }
}