synheart_sensor_agent/
config.rs1use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9use std::time::Duration;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Config {
14 #[serde(with = "duration_serde")]
16 pub window_duration: Duration,
17
18 pub sources: SourceConfig,
20
21 pub export_path: PathBuf,
23
24 pub data_path: PathBuf,
26
27 pub paused: bool,
29
30 pub session_gap_threshold_secs: u64,
32}
33
34impl Default for Config {
35 fn default() -> Self {
36 let data_dir = dirs::data_local_dir()
37 .unwrap_or_else(|| PathBuf::from("."))
38 .join("synheart-sensor-agent");
39
40 Self {
41 window_duration: Duration::from_secs(10),
42 sources: SourceConfig::default(),
43 export_path: data_dir.join("exports"),
44 data_path: data_dir,
45 paused: false,
46 session_gap_threshold_secs: 300, }
48 }
49}
50
51impl Config {
52 pub fn load() -> Result<Self, ConfigError> {
54 let config_path = Self::config_path();
55
56 if config_path.exists() {
57 let content = std::fs::read_to_string(&config_path)
58 .map_err(|e| ConfigError::IoError(e.to_string()))?;
59 let config: Config = serde_json::from_str(&content)
60 .map_err(|e| ConfigError::ParseError(e.to_string()))?;
61 Ok(config)
62 } else {
63 Ok(Self::default())
64 }
65 }
66
67 pub fn save(&self) -> Result<(), ConfigError> {
69 let config_path = Self::config_path();
70
71 if let Some(parent) = config_path.parent() {
73 std::fs::create_dir_all(parent).map_err(|e| ConfigError::IoError(e.to_string()))?;
74 }
75
76 let content = serde_json::to_string_pretty(self)
77 .map_err(|e| ConfigError::SerializeError(e.to_string()))?;
78
79 std::fs::write(&config_path, content).map_err(|e| ConfigError::IoError(e.to_string()))?;
80
81 Ok(())
82 }
83
84 pub fn config_path() -> PathBuf {
86 dirs::config_dir()
87 .unwrap_or_else(|| PathBuf::from("."))
88 .join("synheart-sensor-agent")
89 .join("config.json")
90 }
91
92 pub fn ensure_directories(&self) -> Result<(), ConfigError> {
94 std::fs::create_dir_all(&self.export_path)
95 .map_err(|e| ConfigError::IoError(e.to_string()))?;
96 std::fs::create_dir_all(&self.data_path)
97 .map_err(|e| ConfigError::IoError(e.to_string()))?;
98 Ok(())
99 }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct SourceConfig {
105 pub keyboard: bool,
107 pub mouse: bool,
109}
110
111impl Default for SourceConfig {
112 fn default() -> Self {
113 Self {
114 keyboard: true,
115 mouse: true,
116 }
117 }
118}
119
120impl SourceConfig {
121 pub fn from_csv(s: &str) -> Self {
123 let sources: Vec<String> = s.split(',').map(|s| s.trim().to_lowercase()).collect();
124
125 Self {
126 keyboard: sources.iter().any(|s| s == "keyboard" || s == "all"),
127 mouse: sources.iter().any(|s| s == "mouse" || s == "all"),
128 }
129 }
130
131 pub fn any_enabled(&self) -> bool {
133 self.keyboard || self.mouse
134 }
135}
136
137#[derive(Debug)]
139pub enum ConfigError {
140 IoError(String),
142 ParseError(String),
144 SerializeError(String),
146}
147
148impl std::fmt::Display for ConfigError {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 match self {
151 ConfigError::IoError(e) => write!(f, "IO error: {e}"),
152 ConfigError::ParseError(e) => write!(f, "Parse error: {e}"),
153 ConfigError::SerializeError(e) => write!(f, "Serialize error: {e}"),
154 }
155 }
156}
157
158impl std::error::Error for ConfigError {}
159
160mod duration_serde {
162 use serde::{Deserialize, Deserializer, Serialize, Serializer};
163 use std::time::Duration;
164
165 pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
166 where
167 S: Serializer,
168 {
169 duration.as_secs().serialize(serializer)
170 }
171
172 pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
173 where
174 D: Deserializer<'de>,
175 {
176 let secs = u64::deserialize(deserializer)?;
177 Ok(Duration::from_secs(secs))
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_source_config_parsing() {
187 let config = SourceConfig::from_csv("keyboard,mouse");
188 assert!(config.keyboard);
189 assert!(config.mouse);
190
191 let config = SourceConfig::from_csv("keyboard");
192 assert!(config.keyboard);
193 assert!(!config.mouse);
194
195 let config = SourceConfig::from_csv("all");
196 assert!(config.keyboard);
197 assert!(config.mouse);
198 }
199
200 #[test]
201 fn test_default_config() {
202 let config = Config::default();
203 assert_eq!(config.window_duration, Duration::from_secs(10));
204 assert!(config.sources.keyboard);
205 assert!(config.sources.mouse);
206 assert!(!config.paused);
207 }
208}