opensession_runtime_config/
lib.rs1use serde::{Deserialize, Serialize};
9
10pub const CONFIG_FILE_NAME: &str = "opensession.toml";
12
13#[derive(Debug, Clone, Serialize, Deserialize, Default)]
15pub struct DaemonConfig {
16 #[serde(default)]
17 pub daemon: DaemonSettings,
18 #[serde(default)]
19 pub server: ServerSettings,
20 #[serde(default)]
21 pub identity: IdentitySettings,
22 #[serde(default)]
23 pub privacy: PrivacySettings,
24 #[serde(default)]
25 pub watchers: WatcherSettings,
26 #[serde(default)]
27 pub git_storage: GitStorageSettings,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct DaemonSettings {
32 #[serde(default = "default_false")]
33 pub auto_publish: bool,
34 #[serde(default = "default_debounce")]
35 pub debounce_secs: u64,
36 #[serde(default = "default_publish_on")]
37 pub publish_on: PublishMode,
38 #[serde(default = "default_max_retries")]
39 pub max_retries: u32,
40 #[serde(default = "default_health_check_interval")]
41 pub health_check_interval_secs: u64,
42 #[serde(default = "default_realtime_debounce_ms")]
43 pub realtime_debounce_ms: u64,
44 #[serde(default = "default_detail_realtime_preview_enabled")]
46 pub detail_realtime_preview_enabled: bool,
47 #[serde(default = "default_detail_auto_expand_selected_event")]
49 pub detail_auto_expand_selected_event: bool,
50}
51
52impl Default for DaemonSettings {
53 fn default() -> Self {
54 Self {
55 auto_publish: false,
56 debounce_secs: 5,
57 publish_on: PublishMode::Manual,
58 max_retries: 3,
59 health_check_interval_secs: 300,
60 realtime_debounce_ms: 500,
61 detail_realtime_preview_enabled: false,
62 detail_auto_expand_selected_event: true,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
68#[serde(rename_all = "snake_case")]
69pub enum PublishMode {
70 SessionEnd,
71 Realtime,
72 Manual,
73}
74
75impl PublishMode {
76 pub fn cycle(&self) -> Self {
77 match self {
78 Self::SessionEnd => Self::Realtime,
79 Self::Realtime => Self::Manual,
80 Self::Manual => Self::SessionEnd,
81 }
82 }
83
84 pub fn display(&self) -> &'static str {
85 match self {
86 Self::SessionEnd => "Session End",
87 Self::Realtime => "Realtime",
88 Self::Manual => "Manual",
89 }
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
94#[serde(rename_all = "snake_case")]
95pub enum CalendarDisplayMode {
96 Smart,
97 Relative,
98 Absolute,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ServerSettings {
103 #[serde(default = "default_server_url")]
104 pub url: String,
105 #[serde(default)]
106 pub api_key: String,
107}
108
109impl Default for ServerSettings {
110 fn default() -> Self {
111 Self {
112 url: default_server_url(),
113 api_key: String::new(),
114 }
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct IdentitySettings {
120 #[serde(default = "default_nickname")]
121 pub nickname: String,
122}
123
124impl Default for IdentitySettings {
125 fn default() -> Self {
126 Self {
127 nickname: default_nickname(),
128 }
129 }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct PrivacySettings {
134 #[serde(default = "default_true")]
135 pub strip_paths: bool,
136 #[serde(default = "default_true")]
137 pub strip_env_vars: bool,
138 #[serde(default = "default_exclude_patterns")]
139 pub exclude_patterns: Vec<String>,
140 #[serde(default)]
141 pub exclude_tools: Vec<String>,
142}
143
144impl Default for PrivacySettings {
145 fn default() -> Self {
146 Self {
147 strip_paths: true,
148 strip_env_vars: true,
149 exclude_patterns: default_exclude_patterns(),
150 exclude_tools: Vec::new(),
151 }
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct WatcherSettings {
157 #[serde(default = "default_watch_paths")]
158 pub custom_paths: Vec<String>,
159}
160
161impl Default for WatcherSettings {
162 fn default() -> Self {
163 Self {
164 custom_paths: default_watch_paths(),
165 }
166 }
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct GitStorageSettings {
171 #[serde(default)]
172 pub method: GitStorageMethod,
173 #[serde(default)]
174 pub token: String,
175 #[serde(default)]
176 pub retention: GitRetentionSettings,
177}
178
179impl Default for GitStorageSettings {
180 fn default() -> Self {
181 Self {
182 method: GitStorageMethod::Native,
183 token: String::new(),
184 retention: GitRetentionSettings::default(),
185 }
186 }
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct GitRetentionSettings {
191 #[serde(default = "default_false")]
192 pub enabled: bool,
193 #[serde(default = "default_git_retention_keep_days")]
194 pub keep_days: u32,
195 #[serde(default = "default_git_retention_interval_secs")]
196 pub interval_secs: u64,
197}
198
199impl Default for GitRetentionSettings {
200 fn default() -> Self {
201 Self {
202 enabled: false,
203 keep_days: default_git_retention_keep_days(),
204 interval_secs: default_git_retention_interval_secs(),
205 }
206 }
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
210#[serde(rename_all = "snake_case")]
211pub enum GitStorageMethod {
212 #[default]
214 Native,
215 Sqlite,
217}
218
219fn default_true() -> bool {
222 true
223}
224fn default_false() -> bool {
225 false
226}
227fn default_debounce() -> u64 {
228 5
229}
230fn default_max_retries() -> u32 {
231 3
232}
233fn default_health_check_interval() -> u64 {
234 300
235}
236fn default_realtime_debounce_ms() -> u64 {
237 500
238}
239fn default_detail_realtime_preview_enabled() -> bool {
240 false
241}
242fn default_detail_auto_expand_selected_event() -> bool {
243 true
244}
245fn default_publish_on() -> PublishMode {
246 PublishMode::Manual
247}
248fn default_git_retention_keep_days() -> u32 {
249 30
250}
251fn default_git_retention_interval_secs() -> u64 {
252 86_400
253}
254fn default_server_url() -> String {
255 "https://opensession.io".to_string()
256}
257fn default_nickname() -> String {
258 "user".to_string()
259}
260fn default_exclude_patterns() -> Vec<String> {
261 vec![
262 "*.env".to_string(),
263 "*secret*".to_string(),
264 "*credential*".to_string(),
265 ]
266}
267
268pub const DEFAULT_WATCH_PATHS: &[&str] = &[
269 "~/.claude/projects",
270 "~/.codex/sessions",
271 "~/.local/share/opencode/storage/session",
272 "~/.cline/data/tasks",
273 "~/.local/share/amp/threads",
274 "~/.gemini/tmp",
275 "~/Library/Application Support/Cursor/User",
276 "~/.config/Cursor/User",
277];
278
279pub fn default_watch_paths() -> Vec<String> {
280 DEFAULT_WATCH_PATHS
281 .iter()
282 .map(|path| (*path).to_string())
283 .collect()
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn git_storage_method_requires_canonical_values() {
292 let parsed: Result<DaemonConfig, _> = toml::from_str(
293 r#"
294[git_storage]
295method = "platform_api"
296"#,
297 );
298 assert!(parsed.is_err(), "legacy aliases must be rejected");
299 }
300
301 #[test]
302 fn unknown_watcher_flags_are_ignored() {
303 let cfg: DaemonConfig = toml::from_str(
304 r#"
305[watchers]
306claude_code = false
307opencode = false
308cursor = false
309custom_paths = ["~/.codex/sessions"]
310"#,
311 )
312 .expect("parse watcher config");
313
314 assert_eq!(
315 cfg.watchers.custom_paths,
316 vec!["~/.codex/sessions".to_string()]
317 );
318 }
319
320 #[test]
321 fn watcher_settings_serialize_only_current_fields() {
322 let cfg = DaemonConfig::default();
323 let encoded = toml::to_string(&cfg).expect("serialize config");
324
325 assert!(encoded.contains("custom_paths"));
326 assert!(!encoded.contains("\nclaude_code ="));
327 assert!(!encoded.contains("\nopencode ="));
328 assert!(!encoded.contains("\ncursor ="));
329 }
330
331 #[test]
332 fn git_retention_defaults_are_stable() {
333 let cfg = DaemonConfig::default();
334 assert!(!cfg.git_storage.retention.enabled);
335 assert_eq!(cfg.git_storage.retention.keep_days, 30);
336 assert_eq!(cfg.git_storage.retention.interval_secs, 86_400);
337 }
338
339 #[test]
340 fn git_retention_fields_deserialize_from_toml() {
341 let cfg: DaemonConfig = toml::from_str(
342 r#"
343[git_storage]
344method = "native"
345
346[git_storage.retention]
347enabled = true
348keep_days = 14
349interval_secs = 43200
350"#,
351 )
352 .expect("parse retention config");
353
354 assert_eq!(cfg.git_storage.method, GitStorageMethod::Native);
355 assert!(cfg.git_storage.retention.enabled);
356 assert_eq!(cfg.git_storage.retention.keep_days, 14);
357 assert_eq!(cfg.git_storage.retention.interval_secs, 43_200);
358 }
359}