screen_app/
recorder_settings.rs1use std::path::{Path, PathBuf};
14
15use serde::{Deserialize, Serialize};
16use tauri::Manager;
17
18const SETTINGS_FILE: &str = "recorder-settings.json";
20
21#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
24pub struct RecorderSettings {
25 #[serde(default)]
27 pub output_dir: Option<PathBuf>,
28 #[serde(default)]
30 pub last_format: Option<String>,
31}
32
33#[must_use]
37pub fn load_from(path: &Path) -> RecorderSettings {
38 let Ok(raw) = std::fs::read_to_string(path) else {
39 return RecorderSettings::default();
40 };
41 serde_json::from_str(&raw).unwrap_or_default()
42}
43
44pub fn save_to(path: &Path, settings: &RecorderSettings) -> std::io::Result<()> {
52 if let Some(parent) = path.parent() {
53 std::fs::create_dir_all(parent)?;
54 }
55 let json = serde_json::to_string_pretty(settings)
56 .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
57 std::fs::write(path, json)
58}
59
60fn settings_path(app: &tauri::AppHandle) -> Option<PathBuf> {
64 Some(app.path().app_config_dir().ok()?.join(SETTINGS_FILE))
65}
66
67#[must_use]
70pub fn load(app: &tauri::AppHandle) -> RecorderSettings {
71 settings_path(app)
72 .map(|p| load_from(&p))
73 .unwrap_or_default()
74}
75
76pub fn save(app: &tauri::AppHandle, settings: &RecorderSettings) -> Result<(), String> {
83 let path = settings_path(app).ok_or("app config dir unavailable")?;
84 save_to(&path, settings).map_err(|err| format!("failed to write recorder settings: {err}"))
85}
86
87#[must_use]
92pub fn resolved_output_dir(app: &tauri::AppHandle) -> PathBuf {
93 load(app)
94 .output_dir
95 .unwrap_or_else(crate::recording_paths::default_output_dir)
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 fn temp_path(tag: &str) -> PathBuf {
103 std::env::temp_dir().join(format!("m-save-settings-{tag}.json"))
104 }
105
106 #[test]
107 fn load_from_missing_file_is_default() {
108 let path = temp_path("missing-never-created");
109 let _ = std::fs::remove_file(&path);
110 assert_eq!(load_from(&path), RecorderSettings::default());
111 }
112
113 #[test]
114 fn load_from_malformed_json_is_default() {
115 let path = temp_path("malformed");
116 std::fs::write(&path, "{ this is not json").expect("write");
117 assert_eq!(load_from(&path), RecorderSettings::default());
118 let _ = std::fs::remove_file(&path);
119 }
120
121 #[test]
122 fn save_then_load_round_trips() {
123 let path = temp_path("round-trip");
124 let _ = std::fs::remove_file(&path);
125 let settings = RecorderSettings {
126 output_dir: Some(PathBuf::from("/Users/x/Desktop/My Recordings")),
127 last_format: Some("webm-vp9".to_owned()),
128 };
129 save_to(&path, &settings).expect("save");
130 assert_eq!(load_from(&path), settings);
131 let _ = std::fs::remove_file(&path);
132 }
133
134 #[test]
135 fn save_creates_parent_dir() {
136 let dir = std::env::temp_dir().join("m-save-settings-nested-parent");
137 let _ = std::fs::remove_dir_all(&dir);
138 let path = dir.join("sub").join(SETTINGS_FILE);
139 save_to(&path, &RecorderSettings::default()).expect("save into nested dir");
140 assert!(path.exists());
141 let _ = std::fs::remove_dir_all(&dir);
142 }
143
144 #[test]
145 fn path_with_special_chars_survives_round_trip() {
146 let path = temp_path("special-chars");
150 let _ = std::fs::remove_file(&path);
151 let settings = RecorderSettings {
152 output_dir: Some(PathBuf::from("/tmp/odd, name: with chars")),
153 last_format: None,
154 };
155 save_to(&path, &settings).expect("save");
156 assert_eq!(load_from(&path), settings);
157 let _ = std::fs::remove_file(&path);
158 }
159
160 #[test]
161 fn partial_json_fills_missing_field_with_none() {
162 let path = temp_path("partial");
165 std::fs::write(&path, r#"{"output_dir":"/tmp/only-dir"}"#).expect("write");
166 let loaded = load_from(&path);
167 assert_eq!(loaded.output_dir, Some(PathBuf::from("/tmp/only-dir")));
168 assert_eq!(loaded.last_format, None);
169 let _ = std::fs::remove_file(&path);
170 }
171
172 #[test]
173 fn default_has_no_overrides() {
174 let d = RecorderSettings::default();
175 assert!(d.output_dir.is_none());
176 assert!(d.last_format.is_none());
177 }
178}