1use cleansys_core::{CleanerCategory, Status};
4
5use crate::theme::ThemeColors;
6
7pub struct CleanSysGui {
9 pub categories: Vec<CleanerCategory>,
11 pub logs: Vec<String>,
13 pub total_bytes_cleaned: u64,
15 pub is_running: bool,
17 pub is_root: bool,
19 pub needs_password: bool,
21 pub password_input: String,
23 pub password_error: Option<String>,
25 pub pending_root_ops: Vec<(usize, usize)>,
27 pub active_tab: usize,
29 pub theme_index: usize,
31 pub confirm_run_pending: bool,
33 pub previewing: bool,
35 pub preview_open: bool,
37 pub preview_results: Vec<(String, cleansys_core::CleaningResult)>,
39 pub operations_total: usize,
41 pub operations_completed: usize,
43 pub needs_admin_notice: bool,
47}
48
49impl Default for CleanSysGui {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55impl CleanSysGui {
56 pub fn new() -> Self {
58 let settings = if cfg!(test) {
64 cleansys_core::Settings::default()
65 } else {
66 cleansys_core::load_settings().unwrap_or_default()
67 };
68 let mut categories = cleansys_core::load_categories();
69 for category in &mut categories {
70 for item in &mut category.items {
71 item.selected = settings.is_selected(&category.name, &item.name);
72 }
73 }
74 Self {
75 categories,
76 logs: Vec::new(),
77 total_bytes_cleaned: 0,
78 is_running: false,
79 is_root: cleansys_core::check_root(),
80 needs_password: false,
81 password_input: String::new(),
82 password_error: None,
83 pending_root_ops: Vec::new(),
84 active_tab: 0,
85 theme_index: settings.theme_index(),
86 confirm_run_pending: false,
87 previewing: false,
88 preview_open: false,
89 preview_results: Vec::new(),
90 operations_total: 0,
91 operations_completed: 0,
92 needs_admin_notice: false,
93 }
94 }
95
96 pub fn selected_count(&self) -> usize {
98 self.categories
99 .iter()
100 .flat_map(|c| &c.items)
101 .filter(|i| i.selected)
102 .count()
103 }
104
105 pub fn selected_count_in(&self, cat_idx: usize) -> usize {
107 self.categories
108 .get(cat_idx)
109 .map(|c| c.items.iter().filter(|i| i.selected).count())
110 .unwrap_or(0)
111 }
112
113 pub fn selection_needs_root(&self) -> bool {
115 !self.is_root
116 && self
117 .categories
118 .iter()
119 .flat_map(|c| &c.items)
120 .any(|i| i.selected && i.requires_root)
121 }
122
123 pub fn selected_indices(&self) -> Vec<(usize, usize)> {
125 self.categories
126 .iter()
127 .enumerate()
128 .flat_map(|(ci, c)| {
129 c.items
130 .iter()
131 .enumerate()
132 .filter(|(_, i)| i.selected)
133 .map(move |(ii, _)| (ci, ii))
134 })
135 .collect()
136 }
137
138 pub fn progress_fraction(&self) -> f32 {
141 if self.operations_total == 0 {
142 0.0
143 } else {
144 (self.operations_completed as f32 / self.operations_total as f32).clamp(0.0, 1.0)
145 }
146 }
147
148 pub fn supports_sudo_prompt(&self) -> bool {
151 cleansys_core::utils::supports_sudo_prompt()
152 }
153
154 pub fn push_log(&mut self, line: impl Into<String>) {
156 self.logs.push(line.into());
157 if self.logs.len() > 500 {
158 self.logs.remove(0);
159 }
160 }
161
162 pub fn mark_selected_pending(&mut self) {
164 for category in &mut self.categories {
165 for item in &mut category.items {
166 if item.selected {
167 item.status = Some(Status::Pending);
168 }
169 }
170 }
171 }
172
173 pub fn colors(&self) -> ThemeColors {
177 ThemeColors::from_core(&cleansys_core::theme_by_index(self.theme_index))
178 }
179
180 pub fn iced_theme(&self) -> iced::Theme {
183 crate::theme::iced_theme_for(self.theme_index)
184 }
185
186 pub fn current_theme_name(&self) -> &'static str {
188 cleansys_core::THEME_NAMES
189 .get(self.theme_index)
190 .copied()
191 .unwrap_or("Default")
192 }
193
194 pub fn current_settings(&self) -> cleansys_core::Settings {
197 let selected_cleaners = self
198 .categories
199 .iter()
200 .flat_map(|c| {
201 let cat_name = c.name.clone();
202 c.items
203 .iter()
204 .filter(|i| i.selected)
205 .map(move |i| cleansys_core::Settings::selection_key(&cat_name, &i.name))
206 })
207 .collect();
208
209 cleansys_core::Settings {
210 theme_name: Some(self.current_theme_name().to_string()),
211 selected_cleaners,
212 }
213 }
214
215 pub fn save_theme(&self) {
221 if cfg!(test) {
222 return;
223 }
224 if let Err(e) = cleansys_core::save_settings(&self.current_settings()) {
225 log::warn!("failed to save theme preference: {e}");
226 }
227 }
228
229 pub fn save_selections(&self) {
232 if cfg!(test) {
233 return;
234 }
235 if let Err(e) = cleansys_core::save_settings(&self.current_settings()) {
236 log::warn!("failed to save cleaner selections: {e}");
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn new_loads_categories_and_defaults() {
247 let state = CleanSysGui::new();
248 assert_eq!(state.categories.len(), 2);
249 assert_eq!(state.active_tab, 0);
250 assert_eq!(state.selected_count(), 0);
251 assert!(state.logs.is_empty());
252 assert_eq!(state.total_bytes_cleaned, 0);
253 assert!(!state.is_running);
254 assert!(!state.needs_password);
255 }
256
257 #[test]
258 fn selected_count_tracks_selections() {
259 let mut state = CleanSysGui::new();
260 assert_eq!(state.selected_count(), 0);
261 state.categories[0].items[0].selected = true;
262 assert_eq!(state.selected_count(), 1);
263 assert_eq!(state.selected_count_in(0), 1);
264 assert_eq!(state.selected_count_in(1), 0);
265 state.categories[1].items[0].selected = true;
266 assert_eq!(state.selected_count(), 2);
267 }
268
269 #[test]
270 fn selected_count_in_out_of_range_is_zero() {
271 let state = CleanSysGui::new();
272 assert_eq!(state.selected_count_in(99), 0);
273 }
274
275 fn first_root_required_item(state: &CleanSysGui) -> (usize, usize) {
279 for (ci, category) in state.categories.iter().enumerate() {
280 for (ii, item) in category.items.iter().enumerate() {
281 if item.requires_root {
282 return (ci, ii);
283 }
284 }
285 }
286 panic!("expected at least one root-requiring cleaner on this platform");
287 }
288
289 #[test]
290 fn selection_needs_root_when_not_root_and_system_item_selected() {
291 let mut state = CleanSysGui::new();
292 state.is_root = false;
293 assert!(!state.selection_needs_root());
294
295 let (ci, ii) = first_root_required_item(&state);
296 state.categories[ci].items[ii].selected = true;
297 assert!(state.selection_needs_root());
298 }
299
300 #[test]
301 fn selection_needs_root_false_when_already_root() {
302 let mut state = CleanSysGui::new();
303 state.is_root = true;
304 let (ci, ii) = first_root_required_item(&state);
305 state.categories[ci].items[ii].selected = true;
306 assert!(!state.selection_needs_root());
307 }
308
309 #[test]
310 fn selection_needs_root_false_for_user_only_selection() {
311 let mut state = CleanSysGui::new();
312 state.is_root = false;
313 state.categories[0].items[0].selected = true;
314 assert!(!state.selection_needs_root());
315 }
316
317 #[test]
318 fn push_log_caps_at_500_entries() {
319 let mut state = CleanSysGui::new();
320 for i in 0..600 {
321 state.push_log(format!("line {i}"));
322 }
323 assert_eq!(state.logs.len(), 500);
324 assert_eq!(state.logs.last().unwrap(), "line 599");
327 }
328
329 #[test]
330 fn mark_selected_pending_only_affects_selected_items() {
331 let mut state = CleanSysGui::new();
332 state.categories[0].items[0].selected = true;
333 state.mark_selected_pending();
334
335 assert!(matches!(
336 state.categories[0].items[0].status,
337 Some(Status::Pending)
338 ));
339 assert!(state.categories[0].items[1].status.is_none());
340 }
341
342 #[test]
343 fn theme_index_defaults_in_range() {
344 let state = CleanSysGui::new();
345 assert!(state.theme_index < cleansys_core::THEME_COUNT);
346 }
347
348 #[test]
349 fn current_theme_name_matches_index() {
350 let mut state = CleanSysGui::new();
351 state.theme_index = cleansys_core::theme_index_by_name("Dracula");
352 assert_eq!(state.current_theme_name(), "Dracula");
353 }
354
355 #[test]
356 fn current_theme_name_falls_back_for_out_of_range_index() {
357 let mut state = CleanSysGui::new();
358 state.theme_index = 9999;
359 assert_eq!(state.current_theme_name(), "Default");
360 }
361
362 #[test]
363 fn colors_does_not_panic_for_any_theme() {
364 let mut state = CleanSysGui::new();
365 for i in 0..cleansys_core::THEME_COUNT {
366 state.theme_index = i;
367 let _ = state.colors();
368 let _ = state.iced_theme();
369 }
370 }
371
372 #[test]
373 fn selected_indices_returns_selected_pairs() {
374 let mut state = CleanSysGui::new();
375 state.categories[0].items[0].selected = true;
376 state.categories[0].items[2].selected = true;
377 let indices = state.selected_indices();
378 assert_eq!(indices, vec![(0, 0), (0, 2)]);
379 }
380
381 #[test]
382 fn progress_fraction_is_zero_with_no_operations() {
383 let state = CleanSysGui::new();
384 assert_eq!(state.progress_fraction(), 0.0);
385 }
386
387 #[test]
388 fn progress_fraction_computes_ratio() {
389 let mut state = CleanSysGui::new();
390 state.operations_total = 4;
391 state.operations_completed = 1;
392 assert_eq!(state.progress_fraction(), 0.25);
393 state.operations_completed = 4;
394 assert_eq!(state.progress_fraction(), 1.0);
395 }
396
397 #[test]
398 fn current_settings_includes_selected_cleaners() {
399 let mut state = CleanSysGui::new();
400 state.categories[0].items[0].selected = true;
401 let name = state.categories[0].items[0].name.clone();
402 let cat_name = state.categories[0].name.clone();
403 let settings = state.current_settings();
404 assert!(settings.is_selected(&cat_name, &name));
405 }
406
407 #[test]
408 fn new_restores_selection_from_settings() {
409 let mut state = CleanSysGui::new();
413 state.categories[0].items[1].selected = true;
414 let settings = state.current_settings();
415 let cat_name = state.categories[0].name.clone();
416 let item_name = state.categories[0].items[1].name.clone();
417 assert!(settings.is_selected(&cat_name, &item_name));
418 assert!(!settings.is_selected(&cat_name, "Definitely Not A Real Cleaner"));
419 }
420}