Skip to main content

theater_cli/
config.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::net::SocketAddr;
4use std::path::PathBuf;
5use std::time::Duration;
6use tracing::debug;
7
8/// Theater CLI configuration
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Config {
11    pub server: ServerConfig,
12    pub output: OutputConfig,
13    pub logging: LoggingConfig,
14    pub templates: TemplatesConfig,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ServerConfig {
19    pub default_address: SocketAddr,
20    pub timeout: Duration,
21    pub retry_attempts: u32,
22    pub retry_delay: Duration,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct OutputConfig {
27    pub default_format: String,
28    pub colors: bool,
29    pub timestamps: bool,
30    pub max_width: Option<usize>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct LoggingConfig {
35    pub level: String,
36    pub file: Option<PathBuf>,
37    pub structured: bool,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct TemplatesConfig {
42    pub directories: Vec<PathBuf>,
43    pub auto_update: bool,
44    pub cache_dir: Option<PathBuf>,
45}
46
47impl Default for Config {
48    fn default() -> Self {
49        Self {
50            server: ServerConfig {
51                default_address: "127.0.0.1:9000".parse().unwrap(),
52                timeout: Duration::from_secs(30),
53                retry_attempts: 3,
54                retry_delay: Duration::from_millis(1000),
55            },
56            output: OutputConfig {
57                default_format: "compact".to_string(),
58                colors: true,
59                timestamps: true,
60                max_width: None,
61            },
62            logging: LoggingConfig {
63                level: "warn".to_string(),
64                file: None,
65                structured: false,
66            },
67            templates: TemplatesConfig {
68                directories: vec![],
69                auto_update: true,
70                cache_dir: None,
71            },
72        }
73    }
74}
75
76impl Config {
77    /// Load configuration from the standard locations
78    pub fn load() -> Result<Self> {
79        let mut config = Self::default();
80
81        // Try to load from user config directory
82        if let Ok(config_dir) = std::env::var("XDG_CONFIG_HOME")
83            .map(PathBuf::from)
84            .or_else(|_| {
85                dirs::home_dir()
86                    .map(|home| home.join(".config"))
87                    .ok_or_else(|| anyhow::anyhow!("Could not find home directory"))
88            })
89        {
90            let config_file = config_dir.join("theater").join("config.toml");
91            if config_file.exists() {
92                debug!("Loading config from {}", config_file.display());
93                config = Self::load_from_file(&config_file).with_context(|| {
94                    format!("Failed to load config from {}", config_file.display())
95                })?;
96            }
97        }
98
99        // Override with environment variables
100        config.apply_env_overrides();
101
102        Ok(config)
103    }
104
105    /// Load configuration from a specific file
106    pub fn load_from_file(path: &PathBuf) -> Result<Self> {
107        let content = std::fs::read_to_string(path)
108            .with_context(|| format!("Failed to read config file: {}", path.display()))?;
109
110        let config: Self = toml::from_str(&content)
111            .with_context(|| format!("Failed to parse config file: {}", path.display()))?;
112
113        Ok(config)
114    }
115
116    /// Apply environment variable overrides
117    fn apply_env_overrides(&mut self) {
118        if let Ok(addr) = std::env::var("THEATER_SERVER_ADDRESS") {
119            if let Ok(parsed_addr) = addr.parse() {
120                self.server.default_address = parsed_addr;
121            }
122        }
123
124        if let Ok(level) = std::env::var("THEATER_LOG_LEVEL") {
125            self.logging.level = level;
126        }
127
128        if let Ok(colors) = std::env::var("THEATER_COLORS") {
129            self.output.colors = colors.parse().unwrap_or(true);
130        }
131    }
132
133    /// Get the config directory for this user
134    pub fn config_dir() -> Result<PathBuf> {
135        std::env::var("XDG_CONFIG_HOME")
136            .map(PathBuf::from)
137            .or_else(|_| {
138                dirs::home_dir()
139                    .map(|home| home.join(".config"))
140                    .ok_or_else(|| anyhow::anyhow!("Could not find home directory"))
141            })
142            .map(|dir| dir.join("theater"))
143    }
144
145    /// Get the cache directory for this user
146    pub fn cache_dir() -> Result<PathBuf> {
147        std::env::var("XDG_CACHE_HOME")
148            .map(PathBuf::from)
149            .or_else(|_| {
150                dirs::home_dir()
151                    .map(|home| home.join(".cache"))
152                    .ok_or_else(|| anyhow::anyhow!("Could not find home directory"))
153            })
154            .map(|dir| dir.join("theater"))
155    }
156
157    /// Get the state directory for this user
158    pub fn state_dir() -> Result<PathBuf> {
159        std::env::var("XDG_STATE_HOME")
160            .map(PathBuf::from)
161            .or_else(|_| {
162                dirs::home_dir()
163                    .map(|home| home.join(".local").join("state"))
164                    .ok_or_else(|| anyhow::anyhow!("Could not find home directory"))
165            })
166            .map(|dir| dir.join("theater"))
167    }
168
169    /// Save configuration to the default location
170    pub fn save(&self) -> Result<()> {
171        let config_dir = Self::config_dir()?;
172        std::fs::create_dir_all(&config_dir).with_context(|| {
173            format!(
174                "Failed to create config directory: {}",
175                config_dir.display()
176            )
177        })?;
178
179        let config_file = config_dir.join("config.toml");
180        let content = toml::to_string_pretty(self).context("Failed to serialize configuration")?;
181
182        std::fs::write(&config_file, content)
183            .with_context(|| format!("Failed to write config file: {}", config_file.display()))?;
184
185        debug!("Saved config to {}", config_file.display());
186        Ok(())
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use tempfile::TempDir;
194
195    #[test]
196    fn test_default_config() {
197        let config = Config::default();
198        assert_eq!(
199            config.server.default_address,
200            "127.0.0.1:9000".parse().unwrap()
201        );
202        assert_eq!(config.output.default_format, "compact");
203        assert!(config.output.colors);
204    }
205
206    #[test]
207    fn test_config_serialization() {
208        let config = Config::default();
209        let serialized = toml::to_string_pretty(&config).unwrap();
210        let deserialized: Config = toml::from_str(&serialized).unwrap();
211
212        assert_eq!(
213            config.server.default_address,
214            deserialized.server.default_address
215        );
216        assert_eq!(
217            config.output.default_format,
218            deserialized.output.default_format
219        );
220    }
221
222    #[test]
223    fn test_config_load_from_file() {
224        let temp_dir = TempDir::new().unwrap();
225        let config_file = temp_dir.path().join("config.toml");
226
227        let config_content = r#"
228[server]
229default_address = "192.168.1.100:8080"
230timeout = { secs = 60, nanos = 0 }
231retry_attempts = 5
232retry_delay = { secs = 2, nanos = 0 }
233
234[output]
235default_format = "json"
236colors = false
237timestamps = false
238
239[logging]
240level = "debug"
241structured = true
242
243[templates]
244directories = []
245auto_update = true
246"#;
247
248        std::fs::write(&config_file, config_content).unwrap();
249        let config = Config::load_from_file(&config_file).unwrap();
250
251        assert_eq!(
252            config.server.default_address,
253            "192.168.1.100:8080".parse().unwrap()
254        );
255        assert_eq!(config.output.default_format, "json");
256        assert!(!config.output.colors);
257        assert_eq!(config.logging.level, "debug");
258    }
259}