ferrite_config/
config.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::{fs, path::PathBuf};
use tracing::{debug, info};

use crate::{
    error::{ConfigError, Result},
    input::ControlsConfig,
    ui::{IndicatorConfig, SelectionConfig},
    window::WindowConfig,
    zoom::ZoomConfig,
    CONFIG_VERSION,
};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FerriteConfig {
    version:       String,
    pub window:    WindowConfig,
    pub zoom:      ZoomConfig,
    pub controls:  ControlsConfig,
    pub indicator: IndicatorConfig,
    pub selection: SelectionConfig,
}

impl Default for FerriteConfig {
    fn default() -> Self {
        info!("Creating default configuration");
        Self {
            version:   CONFIG_VERSION.to_string(),
            window:    WindowConfig::default(),
            zoom:      ZoomConfig::default(),
            controls:  ControlsConfig::default(),
            indicator: IndicatorConfig::default(),
            selection: SelectionConfig::default(),
        }
    }
}

use std::env;

impl FerriteConfig {
    /// Determines the configuration file path by checking:
    /// 1. FERRITE_CONF environment variable
    /// 2. Default XDG config path
    pub fn resolve_config_path() -> Result<PathBuf> {
        // First check environment variable
        if let Ok(env_path) = env::var("FERRITE_CONF") {
            let path = PathBuf::from(env_path);

            // Validate the path from environment variable
            if let Some(parent) = path.parent() {
                if !parent.exists() {
                    return Err(ConfigError::InvalidPath(format!(
                        "Directory {} from FERRITE_CONF does not exist",
                        parent.display()
                    )));
                }
            }

            return Ok(path);
        }

        // Fall back to default XDG config path
        Self::get_default_path()
    }

    /// Loads configuration using environment-aware path resolution
    pub fn load() -> Result<Self> {
        let config_path = Self::resolve_config_path()?;

        if !config_path.exists() {
            info!(
                "No configuration file found at {:?}, using defaults",
                config_path
            );
            return Ok(Self::default());
        }

        info!("Loading configuration from {:?}", config_path);
        Self::load_from_path(&config_path)
    }

    pub fn load_from_path(path: &PathBuf) -> Result<Self> {
        if !path.exists() {
            debug!("No config file found at {:?}, using defaults", path);
            return Ok(Self::default());
        }

        info!("Loading configuration from {:?}", path);
        let content = fs::read_to_string(path)?;
        let config: Self = toml::from_str(&content)?;

        if config.version != CONFIG_VERSION {
            return Err(ConfigError::VersionError {
                found:     config.version.clone(),
                supported: CONFIG_VERSION.to_string(),
            });
        }

        config.validate()?;
        Ok(config)
    }

    pub fn save_to_path(&self, path: &PathBuf) -> Result<()> {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }

        self.validate()?;
        let content = toml::to_string_pretty(self)?;
        fs::write(path, content)?;

        info!("Saved configuration to {:?}", path);
        Ok(())
    }

    // Default paths handling
    pub fn get_default_path() -> Result<PathBuf> {
        ProjectDirs::from("com", "ferrite", "ferrite")
            .map(|proj_dirs| proj_dirs.config_dir().join("config.toml"))
            .ok_or_else(|| ConfigError::DirectoryError(PathBuf::from(".")))
    }

    // Configuration validation
    pub fn validate(&self) -> Result<()> {
        self.window.validate()?;
        self.zoom.validate()?;
        self.controls.validate()?;
        self.indicator.validate()?;
        self.selection.validate()?;
        Ok(())
    }

    // Utility method for creating new configurations
    pub fn with_modifications<F>(&self, modify_fn: F) -> Result<Self>
    where
        F: FnOnce(&mut Self),
    {
        let mut new_config = self.clone();
        modify_fn(&mut new_config);
        new_config.validate()?;
        Ok(new_config)
    }
}