diff --git a/src/config/file.rs b/src/config/file.rs new file mode 100644 index 0000000..53a3ee9 --- /dev/null +++ b/src/config/file.rs @@ -0,0 +1,152 @@ +use anyhow::{Context, Result}; +use home::home_dir; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalysisConfig { + pub default_time_period: String, + pub max_contributors: usize, + pub include_merges: bool, + pub refactoring_detection: bool, +} + +impl Default for AnalysisConfig { + fn default() -> Self { + Self { + default_time_period: "30 days".to_string(), + max_contributors: 50, + include_merges: false, + refactoring_detection: true, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DisplayConfig { + pub theme: String, + pub chart_height: u16, + pub compact_tables: bool, + pub show_sparklines: bool, +} + +impl Default for DisplayConfig { + fn default() -> Self { + Self { + theme: "dark".to_string(), + chart_height: 10, + compact_tables: false, + show_sparklines: true, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExportConfig { + pub default_format: String, + pub include_timestamps: bool, + pub indent_json: bool, +} + +impl Default for ExportConfig { + fn default() -> Self { + Self { + default_format: "json".to_string(), + include_timestamps: true, + indent_json: true, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ColorsConfig { + pub primary: String, + pub secondary: String, + pub accent: String, + pub error: String, + pub background: String, +} + +impl Default for ColorsConfig { + fn default() -> Self { + Self { + primary: "blue".to_string(), + secondary: "green".to_string(), + accent: "yellow".to_string(), + error: "red".to_string(), + background: "black".to_string(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub analysis: AnalysisConfig, + pub display: DisplayConfig, + pub export: ExportConfig, + pub colors: ColorsConfig, +} + +impl Default for Config { + fn default() -> Self { + Self { + analysis: AnalysisConfig::default(), + display: DisplayConfig::default(), + export: ExportConfig::default(), + colors: ColorsConfig::default(), + } + } +} + +impl Config { + pub fn config_path() -> Result { + let home = home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?; + let config_dir = home.join(".config").join("gitpulse"); + Ok(config_dir.join("config.toml")) + } + + pub fn load(custom_path: Option<&PathBuf>) -> Result { + let config_path = if let Some(path) = custom_path { + path.clone() + } else { + Self::config_path()? + }; + + if !config_path.exists() { + return Ok(Self::default()); + } + + let content = fs::read_to_string(&config_path) + .with_context(|| format!("Failed to read config from: {}", config_path.display()))?; + + let config: Config = toml::from_str(&content) + .with_context(|| format!("Failed to parse config: {}", config_path.display()))?; + + Ok(config) + } + + pub fn save(&self) -> Result<()> { + let config_path = Self::config_path()?; + + if let Some(parent) = config_path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Failed to create config directory: {}", parent.display()))?; + } + + let content = toml::to_string_pretty(self) + .context("Failed to serialize config")?; + + fs::write(&config_path, content) + .with_context(|| format!("Failed to write config to: {}", config_path.display()))?; + + Ok(()) + } + + pub fn merge(&mut self, other: Config) { + self.analysis = other.analysis; + self.display = other.display; + self.export = other.export; + self.colors = other.colors; + } +}