pado 0.1.0

A tidal flow through your projects
Documentation
use crate::error::PadoError;
use crate::project::PROJECT_FILES;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GlobalConfig {
    #[serde(default)]
    pub markers: MarkersConfig,

    #[serde(default)]
    pub defaults: DefaultsConfig,

    #[serde(default)]
    pub indexing: IndexingConfig,

    #[serde(default)]
    pub prompt: PromptConfig,

    #[serde(default)]
    pub behavior: BehaviorConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MarkersConfig {
    #[serde(default)]
    pub additional: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DefaultsConfig {
    #[serde(default = "default_sort_order")]
    pub sort_order: String,

    #[serde(default = "default_output_format")]
    pub output_format: String,

    #[serde(default = "default_recent_limit")]
    pub recent_limit: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexingConfig {
    #[serde(default = "default_indexing_method")]
    pub method: String,

    #[serde(default)]
    pub ignore_patterns: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptConfig {
    #[serde(default = "default_prompt_format")]
    pub format: String,

    #[serde(default)]
    pub show_full_path: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BehaviorConfig {
    #[serde(default)]
    pub auto_star_on_add: bool,

    #[serde(default = "default_auto_add")]
    pub auto_add_on_cd: bool,

    #[serde(default = "default_require_confirmation")]
    pub require_confirmation_on_clear: bool,
}

fn default_sort_order() -> String {
    "time".to_string()
}
fn default_output_format() -> String {
    "table".to_string()
}
fn default_recent_limit() -> usize {
    10
}
fn default_indexing_method() -> String {
    "native".to_string()
}
fn default_prompt_format() -> String {
    "{name}:{type}".to_string()
}
fn default_auto_add() -> bool {
    true
}
fn default_require_confirmation() -> bool {
    true
}

impl Default for DefaultsConfig {
    fn default() -> Self {
        Self {
            sort_order: default_sort_order(),
            output_format: default_output_format(),
            recent_limit: default_recent_limit(),
        }
    }
}

impl Default for IndexingConfig {
    fn default() -> Self {
        Self {
            method: default_indexing_method(),
            ignore_patterns: Vec::new(),
        }
    }
}

impl Default for PromptConfig {
    fn default() -> Self {
        Self {
            format: default_prompt_format(),
            show_full_path: false,
        }
    }
}

impl Default for BehaviorConfig {
    fn default() -> Self {
        Self {
            auto_star_on_add: false,
            auto_add_on_cd: default_auto_add(),
            require_confirmation_on_clear: default_require_confirmation(),
        }
    }
}

impl Default for GlobalConfig {
    fn default() -> Self {
        Self {
            markers: MarkersConfig::default(),
            defaults: DefaultsConfig::default(),
            indexing: IndexingConfig::default(),
            prompt: PromptConfig::default(),
            behavior: BehaviorConfig::default(),
        }
    }
}

impl GlobalConfig {
    pub fn load() -> Result<GlobalConfig, PadoError> {
        let config_path = get_global_config_path()?;

        if !config_path.exists() {
            return Ok(GlobalConfig::default());
        }

        let contents = fs::read_to_string(&config_path)?;
        let config: GlobalConfig = toml::from_str(&contents)
            .map_err(|e| PadoError::ConfigParse(format!("global config: {}", e)))?;

        Ok(config)
    }

    pub fn save(&self) -> Result<(), PadoError> {
        let config_path = get_global_config_path()?;

        if let Some(parent) = config_path.parent() {
            fs::create_dir_all(parent)?;
        }

        let contents = toml::to_string_pretty(self)
            .map_err(|e| PadoError::SerializationError(format!("global config: {}", e)))?;

        fs::write(&config_path, contents)?;
        Ok(())
    }

    pub fn get_all_markers(&self) -> Vec<&str> {
        let mut markers: Vec<&str> = PROJECT_FILES.iter().copied().collect();
        markers.extend(self.markers.additional.iter().map(|s| s.as_str()));
        markers
    }

    pub fn format_prompt(&self, name: &str, project_type: &str, path: &str) -> String {
        self.prompt
            .format
            .replace("{name}", name)
            .replace("{type}", project_type)
            .replace("{path}", path)
    }
}

pub fn get_global_config_path() -> Result<PathBuf, PadoError> {
    let config_dir = dirs::config_dir().ok_or_else(|| {
        PadoError::InvalidPath("could not determine config directory".to_string())
    })?;

    let pado_config_dir = config_dir.join("pado");
    Ok(pado_config_dir.join("config.toml"))
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectConfig {
    #[serde(default)]
    pub commands: HashMap<String, String>,
}

impl ProjectConfig {
    pub fn load(root: &Path) -> Result<Option<ProjectConfig>, PadoError> {
        let config_path = root.join(".pd.toml");
        if !config_path.exists() {
            return Ok(None);
        }

        let contents = fs::read_to_string(&config_path)?;
        let config: ProjectConfig = toml::from_str(&contents)
            .map_err(|e| PadoError::ConfigParse(format!(".pd.toml: {}", e)))?;

        Ok(Some(config))
    }

    pub fn get_command(&self, name: &str) -> Option<&str> {
        self.commands.get(name).map(|s| s.as_str())
    }
}