synheart_sensor_agent/
config.rs1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use std::time::Duration;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Config {
10 #[serde(with = "duration_serde")]
12 pub window_duration: Duration,
13
14 pub sources: SourceConfig,
16
17 pub export_path: PathBuf,
19
20 pub data_path: PathBuf,
22
23 pub paused: bool,
25
26 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, }
44 }
45}
46
47impl Config {
48 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 pub fn save(&self) -> Result<(), ConfigError> {
65 let config_path = Self::config_path();
66
67 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 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 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#[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 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 pub fn any_enabled(&self) -> bool {
127 self.keyboard || self.mouse
128 }
129}
130
131#[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
151mod 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}