entracte_lib/scheduler/commands/
settings.rs1use crate::config;
2use crate::supporter;
3use crate::SupporterAppState;
4
5use super::super::settings::Settings;
6use super::super::Scheduler;
7
8#[tauri::command]
15pub async fn get_settings(
16 scheduler: tauri::State<'_, Scheduler>,
17 supporter_state: tauri::State<'_, SupporterAppState>,
18) -> Result<Settings, String> {
19 let mut s = scheduler.settings.lock().await.clone();
20 if !supporter::is_supporter_now(&supporter_state.path) {
21 s.custom_css = String::new();
22 }
23 Ok(s)
24}
25
26#[tauri::command]
35pub async fn update_settings(
36 scheduler: tauri::State<'_, Scheduler>,
37 supporter_state: tauri::State<'_, SupporterAppState>,
38 new: Settings,
39) -> Result<(), String> {
40 let is_supporter = supporter::is_supporter_now(&supporter_state.path);
41 let merged = {
42 let current = scheduler.settings.lock().await;
43 let mut m = strip_hooks(new, ¤t);
44 m = gate_custom_css(m, ¤t, is_supporter);
45 m.clamp();
46 m
47 };
48 *scheduler.settings.lock().await = merged.clone();
49 {
50 let active = scheduler.active_profile_name.lock().await.clone();
51 let mut profiles = scheduler.profiles.lock().await;
52 if let Some(p) = profiles.iter_mut().find(|p| p.name == active) {
53 p.settings = merged.clone();
54 } else {
55 profiles.push(config::Profile {
56 name: active,
57 settings: merged.clone(),
58 });
59 }
60 }
61 super::super::persist_profiles(scheduler.inner()).await;
62 Ok(())
63}
64
65fn strip_hooks(mut new: Settings, current: &Settings) -> Settings {
69 new.hooks = current.hooks.clone();
70 new.hooks_enabled = current.hooks_enabled;
71 new
72}
73
74fn gate_custom_css(mut new: Settings, current: &Settings, is_supporter: bool) -> Settings {
78 if !is_supporter {
79 new.custom_css = current.custom_css.clone();
80 }
81 new
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::hooks::{Hook, HookEvent};
88
89 #[test]
90 fn strip_hooks_keeps_current_hook_fields_and_takes_other_fields_from_new() {
91 let current = Settings {
92 hooks_enabled: true,
93 hooks: vec![Hook {
94 event: HookEvent::BreakStart,
95 command: "trusted".to_string(),
96 enabled: true,
97 }],
98 micro_interval_secs: 1500,
99 ..Settings::default()
100 };
101 let attacker = Settings {
102 hooks_enabled: true,
103 hooks: vec![Hook {
104 event: HookEvent::BreakEnd,
105 command: "sh -c 'curl evil'".to_string(),
106 enabled: true,
107 }],
108 micro_interval_secs: 60,
109 ..Settings::default()
110 };
111 let merged = strip_hooks(attacker, ¤t);
112 assert_eq!(
113 merged.micro_interval_secs, 60,
114 "non-hook fields pass through"
115 );
116 assert!(merged.hooks_enabled, "hooks_enabled comes from current");
117 assert_eq!(merged.hooks.len(), 1);
118 assert_eq!(
119 merged.hooks[0].command, "trusted",
120 "hooks come from current"
121 );
122 }
123
124 #[test]
125 fn strip_hooks_blocks_enabling_when_current_disabled() {
126 let current = Settings {
127 hooks_enabled: false,
128 hooks: vec![],
129 ..Settings::default()
130 };
131 let attacker = Settings {
132 hooks_enabled: true,
133 hooks: vec![Hook {
134 event: HookEvent::BreakStart,
135 command: "malicious".to_string(),
136 enabled: true,
137 }],
138 ..Settings::default()
139 };
140 let merged = strip_hooks(attacker, ¤t);
141 assert!(!merged.hooks_enabled);
142 assert!(merged.hooks.is_empty());
143 }
144
145 #[test]
146 fn gate_custom_css_substitutes_current_for_non_supporter() {
147 let current = Settings {
148 custom_css: ".saved { color: red; }".to_string(),
149 ..Settings::default()
150 };
151 let incoming = Settings {
152 custom_css: ".attempted { color: blue; }".to_string(),
153 ..Settings::default()
154 };
155 let merged = gate_custom_css(incoming, ¤t, false);
156 assert_eq!(
157 merged.custom_css, ".saved { color: red; }",
158 "non-supporter cannot overwrite stored CSS"
159 );
160 }
161
162 #[test]
163 fn gate_custom_css_substitutes_current_even_when_clearing() {
164 let current = Settings {
168 custom_css: ".saved { color: red; }".to_string(),
169 ..Settings::default()
170 };
171 let echoed_empty = Settings {
172 custom_css: String::new(),
173 ..Settings::default()
174 };
175 let merged = gate_custom_css(echoed_empty, ¤t, false);
176 assert_eq!(merged.custom_css, ".saved { color: red; }");
177 }
178
179 #[test]
180 fn gate_custom_css_lets_supporter_overwrite() {
181 let current = Settings {
182 custom_css: ".old { color: red; }".to_string(),
183 ..Settings::default()
184 };
185 let incoming = Settings {
186 custom_css: ".new { color: blue; }".to_string(),
187 ..Settings::default()
188 };
189 let merged = gate_custom_css(incoming, ¤t, true);
190 assert_eq!(merged.custom_css, ".new { color: blue; }");
191 }
192
193 #[test]
194 fn gate_custom_css_lets_supporter_clear() {
195 let current = Settings {
196 custom_css: ".old { color: red; }".to_string(),
197 ..Settings::default()
198 };
199 let incoming = Settings {
200 custom_css: String::new(),
201 ..Settings::default()
202 };
203 let merged = gate_custom_css(incoming, ¤t, true);
204 assert_eq!(merged.custom_css, "");
205 }
206}