search_in_terminal/core/
config.rs

1use anyhow::Result;
2use once_cell::sync::Lazy;
3use serde::Deserialize;
4use std::{
5    fs,
6    path::{Path, PathBuf},
7};
8
9use crate::error::types::ConfigError;
10
11/// Global configuration instance, lazily initialized when first accessed
12pub static CONFIG: Lazy<Config> =
13    Lazy::new(|| Config::new().expect("Failed to load configuration"));
14
15/// Cache configuration settings
16#[derive(Debug, Deserialize)]
17pub struct CacheConfig {
18    /// Maximum number of items that can be stored in the cache
19    #[serde(default = "default_max_capacity")]
20    pub max_capacity: u64,
21
22    /// Time-to-live for cached items in seconds
23    #[serde(default = "default_time_to_live")]
24    pub time_to_live: u64,
25}
26
27impl CacheConfig {
28    fn validate(&self) -> Result<()> {
29        if self.time_to_live == 0 {
30            return Err(anyhow::anyhow!(ConfigError::ValidationError(
31                "Cache time-to-live must be greater than 0".to_string(),
32            )));
33        }
34        Ok(())
35    }
36}
37
38/// Search engine configuration settings
39#[derive(Debug, Deserialize)]
40pub struct EngineConfig {
41    /// Preferred search engine (google, bing, duckduckgo)
42    #[serde(default = "default_favor")]
43    pub favor: String,
44}
45
46impl EngineConfig {
47    fn validate(&self) -> Result<()> {
48        match self.favor.as_str() {
49            "google" | "bing" | "duckduckgo" => Ok(()),
50            _ => Err(anyhow::anyhow!(ConfigError::ValidationError(
51                "Invalid search engine specified".to_string(),
52            ))),
53        }
54    }
55}
56
57/// Search behavior configuration settings
58#[derive(Debug, Deserialize)]
59pub struct SearchConfig {
60    /// List of user agents to rotate through for requests
61    #[serde(default = "default_user_agents")]
62    pub user_agents: Vec<String>,
63
64    /// Maximum number of retry attempts for failed requests
65    #[serde(default = "default_max_retries")]
66    pub max_retries: u32,
67
68    /// Base delay between requests in milliseconds
69    #[serde(default = "default_base_delay")]
70    pub base_delay: u64,
71
72    /// Maximum random jitter added to delay in milliseconds
73    #[serde(default = "default_max_jitter")]
74    pub max_jitter: u64,
75
76    /// Request timeout in seconds
77    #[serde(default = "default_request_timeout")]
78    pub request_timeout: u64,
79
80    /// Response timeout in seconds
81    #[serde(default = "default_response_timeout")]
82    pub response_timeout: u64,
83}
84
85impl SearchConfig {
86    fn validate(&self) -> Result<()> {
87        if self.max_retries > 10 {
88            return Err(anyhow::anyhow!(ConfigError::ValidationError(
89                "Max retries should not exceed 10".to_string(),
90            )));
91        }
92        if self.request_timeout == 0 || self.response_timeout == 0 {
93            return Err(anyhow::anyhow!(ConfigError::ValidationError(
94                "Timeouts must be greater than 0".to_string(),
95            )));
96        }
97        Ok(())
98    }
99}
100
101/// Main configuration structure containing all settings
102#[derive(Debug, Deserialize, Default)]
103pub struct Config {
104    /// Search-related settings
105    #[serde(default)]
106    pub search: SearchConfig,
107
108    /// Cache-related settings
109    #[serde(default)]
110    pub cache: CacheConfig,
111
112    /// Search engine preferences
113    #[serde(default)]
114    pub engine: EngineConfig,
115}
116
117impl Config {
118    /// Creates a new Config instance by reading from the configuration file
119    ///
120    /// The configuration file is located at:
121    /// - Windows: %APPDATA%\st\config.toml or %USERPROFILE%\AppData\Roaming\st\config.toml
122    /// - Other: ~/.config/st/config.toml
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if:
127    /// - Home directory cannot be found
128    /// - Config directory cannot be created
129    /// - Config file cannot be parsed
130    /// - Configuration values are invalid
131    pub fn new() -> Result<Self> {
132        let config_path = Self::config_path()?;
133        Self::ensure_config_dir(&config_path)?;
134
135        let config = if let Ok(content) = fs::read_to_string(&config_path) {
136            toml::from_str(&content).unwrap_or(Config::default())
137        } else {
138            Config::default()
139        };
140
141        config.validate()?;
142        Ok(config)
143    }
144
145    /// Returns the path to the configuration file
146    fn config_path() -> Result<PathBuf> {
147        let config_dir = if cfg!(target_os = "windows") {
148            dirs::config_dir().unwrap_or_else(|| {
149                dirs::home_dir()
150                    .expect("Home directory not found")
151                    .join("AppData")
152                    .join("Roaming")
153            })
154        } else {
155            dirs::home_dir()
156                .ok_or(ConfigError::NoHomeDir)?
157                .join(".config")
158        };
159
160        Ok(config_dir.join("st").join("config.toml"))
161    }
162
163    /// Ensures the configuration directory exists
164    fn ensure_config_dir(config_path: &Path) -> Result<()> {
165        if let Some(parent) = config_path.parent() {
166            if !parent.exists() {
167                fs::create_dir_all(parent)?;
168            }
169        }
170        Ok(())
171    }
172
173    /// Validates all configuration values
174    fn validate(&self) -> Result<()> {
175        self.search.validate()?;
176        self.cache.validate()?;
177        self.engine.validate()?;
178        Ok(())
179    }
180}
181
182/// Default list of user agents for request rotation
183fn default_user_agents() -> Vec<String> {
184    vec![
185        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36".to_string(),
186        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36".to_string(),
187        "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0".to_string(),
188        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15".to_string(),
189        "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36".to_string(),
190    ]
191}
192
193// Default values for configuration settings
194fn default_max_retries() -> u32 {
195    3
196}
197fn default_base_delay() -> u64 {
198    1000
199}
200fn default_max_jitter() -> u64 {
201    1000
202}
203fn default_request_timeout() -> u64 {
204    10
205}
206fn default_response_timeout() -> u64 {
207    10
208}
209fn default_max_capacity() -> u64 {
210    100
211}
212fn default_time_to_live() -> u64 {
213    600
214}
215fn default_favor() -> String {
216    "google".to_string()
217}
218
219/// Default implementation for SearchConfig
220impl Default for SearchConfig {
221    fn default() -> Self {
222        Self {
223            user_agents: default_user_agents(),
224            max_retries: default_max_retries(),
225            base_delay: default_base_delay(),
226            max_jitter: default_max_jitter(),
227            request_timeout: default_request_timeout(),
228            response_timeout: default_response_timeout(),
229        }
230    }
231}
232
233/// Default implementation for CacheConfig
234impl Default for CacheConfig {
235    fn default() -> Self {
236        Self {
237            max_capacity: default_max_capacity(),
238            time_to_live: default_time_to_live(),
239        }
240    }
241}
242
243/// Default implementation for EngineConfig
244impl Default for EngineConfig {
245    fn default() -> Self {
246        Self {
247            favor: default_favor(),
248        }
249    }
250}