Skip to main content

synheart_sensor_agent/
config.rs

1//! Configuration for the Synheart Sensor Agent.
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use std::time::Duration;
6
7/// Main configuration for the sensor agent.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Config {
10    /// Duration of each collection window
11    #[serde(with = "duration_serde")]
12    pub window_duration: Duration,
13
14    /// Which input sources to capture
15    pub sources: SourceConfig,
16
17    /// Path for exporting HSI snapshots
18    pub export_path: PathBuf,
19
20    /// Path for storing state and transparency logs
21    pub data_path: PathBuf,
22
23    /// Whether collection is currently paused
24    pub paused: bool,
25
26    /// Gap threshold for session boundaries (in seconds)
27    pub session_gap_threshold_secs: u64,
28}
29
30impl Default for Config {
31    fn default() -> Self {
32        let data_dir = dirs::data_local_dir()
33            .unwrap_or_else(|| PathBuf::from("."))
34            .join("synheart-sensor-agent");
35
36        Self {
37            window_duration: Duration::from_secs(10),
38            sources: SourceConfig::default(),
39            export_path: data_dir.join("exports"),
40            data_path: data_dir,
41            paused: false,
42            session_gap_threshold_secs: 300, // 5 minutes
43        }
44    }
45}
46
47impl Config {
48    /// Load configuration from the default location.
49    pub fn load() -> Result<Self, ConfigError> {
50        let config_path = Self::config_path();
51
52        if config_path.exists() {
53            let content = std::fs::read_to_string(&config_path)
54                .map_err(|e| ConfigError::IoError(e.to_string()))?;
55            let config: Config = serde_json::from_str(&content)
56                .map_err(|e| ConfigError::ParseError(e.to_string()))?;
57            Ok(config)
58        } else {
59            Ok(Self::default())
60        }
61    }
62
63    /// Save configuration to the default location.
64    pub fn save(&self) -> Result<(), ConfigError> {
65        let config_path = Self::config_path();
66
67        // Ensure parent directory exists
68        if let Some(parent) = config_path.parent() {
69            std::fs::create_dir_all(parent).map_err(|e| ConfigError::IoError(e.to_string()))?;
70        }
71
72        let content = serde_json::to_string_pretty(self)
73            .map_err(|e| ConfigError::SerializeError(e.to_string()))?;
74
75        std::fs::write(&config_path, content).map_err(|e| ConfigError::IoError(e.to_string()))?;
76
77        Ok(())
78    }
79
80    /// Get the path to the configuration file.
81    pub fn config_path() -> PathBuf {
82        dirs::config_dir()
83            .unwrap_or_else(|| PathBuf::from("."))
84            .join("synheart-sensor-agent")
85            .join("config.json")
86    }
87
88    /// Ensure all required directories exist.
89    pub fn ensure_directories(&self) -> Result<(), ConfigError> {
90        std::fs::create_dir_all(&self.export_path)
91            .map_err(|e| ConfigError::IoError(e.to_string()))?;
92        std::fs::create_dir_all(&self.data_path)
93            .map_err(|e| ConfigError::IoError(e.to_string()))?;
94        Ok(())
95    }
96}
97
98/// Configuration for which input sources to capture.
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct SourceConfig {
101    pub keyboard: bool,
102    pub mouse: bool,
103}
104
105impl Default for SourceConfig {
106    fn default() -> Self {
107        Self {
108            keyboard: true,
109            mouse: true,
110        }
111    }
112}
113
114impl SourceConfig {
115    /// Parse source configuration from a comma-separated string.
116    pub fn from_csv(s: &str) -> Self {
117        let sources: Vec<String> = s.split(',').map(|s| s.trim().to_lowercase()).collect();
118
119        Self {
120            keyboard: sources.iter().any(|s| s == "keyboard" || s == "all"),
121            mouse: sources.iter().any(|s| s == "mouse" || s == "all"),
122        }
123    }
124
125    /// Check if at least one source is enabled.
126    pub fn any_enabled(&self) -> bool {
127        self.keyboard || self.mouse
128    }
129}
130
131/// Configuration errors.
132#[derive(Debug)]
133pub enum ConfigError {
134    IoError(String),
135    ParseError(String),
136    SerializeError(String),
137}
138
139impl std::fmt::Display for ConfigError {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        match self {
142            ConfigError::IoError(e) => write!(f, "IO error: {e}"),
143            ConfigError::ParseError(e) => write!(f, "Parse error: {e}"),
144            ConfigError::SerializeError(e) => write!(f, "Serialize error: {e}"),
145        }
146    }
147}
148
149impl std::error::Error for ConfigError {}
150
151/// Serde support for Duration.
152mod duration_serde {
153    use serde::{Deserialize, Deserializer, Serialize, Serializer};
154    use std::time::Duration;
155
156    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
157    where
158        S: Serializer,
159    {
160        duration.as_secs().serialize(serializer)
161    }
162
163    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
164    where
165        D: Deserializer<'de>,
166    {
167        let secs = u64::deserialize(deserializer)?;
168        Ok(Duration::from_secs(secs))
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_source_config_parsing() {
178        let config = SourceConfig::from_csv("keyboard,mouse");
179        assert!(config.keyboard);
180        assert!(config.mouse);
181
182        let config = SourceConfig::from_csv("keyboard");
183        assert!(config.keyboard);
184        assert!(!config.mouse);
185
186        let config = SourceConfig::from_csv("all");
187        assert!(config.keyboard);
188        assert!(config.mouse);
189    }
190
191    #[test]
192    fn test_default_config() {
193        let config = Config::default();
194        assert_eq!(config.window_duration, Duration::from_secs(10));
195        assert!(config.sources.keyboard);
196        assert!(config.sources.mouse);
197        assert!(!config.paused);
198    }
199}