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#[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 pub fn load() -> Result<Self> {
79 let mut config = Self::default();
80
81 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 config.apply_env_overrides();
101
102 Ok(config)
103 }
104
105 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 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 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 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 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 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 = "60s"
231retry_attempts = 5
232retry_delay = "2s"
233
234[output]
235default_format = "json"
236colors = false
237timestamps = false
238
239[logging]
240level = "debug"
241structured = true
242"#;
243
244 std::fs::write(&config_file, config_content).unwrap();
245 let config = Config::load_from_file(&config_file).unwrap();
246
247 assert_eq!(
248 config.server.default_address,
249 "192.168.1.100:8080".parse().unwrap()
250 );
251 assert_eq!(config.output.default_format, "json");
252 assert!(!config.output.colors);
253 assert_eq!(config.logging.level, "debug");
254 }
255}