1use crate::{
2 dynamic_gui::DynamicUiHooks,
3 tray::{SystemTray, SystemTrayRef, TrayPlugin, TrayRoot},
4 window_filter::WindowFilter,
5};
6#[cfg(feature = "persist_settings")]
7use serde::{Deserialize, Deserializer, Serialize};
8use std::{
9 any::TypeId,
10 cell::Cell,
11 collections::BTreeMap,
12 fmt,
13 ops::Deref,
14 path::Path,
15 rc::Rc,
16 sync::{Arc, Condvar, Mutex},
17};
18#[cfg(feature = "persist_settings")]
19use std::{
20 cell::OnceCell,
21 io::{ErrorKind::NotFound, Write},
22 sync::{mpsc, MutexGuard},
23 time::Duration,
24};
25
26#[cfg(feature = "persist_settings")]
36fn ok_or_none<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
37where
38 T: Deserialize<'de>,
39 D: Deserializer<'de>,
40{
41 let v: serde_json::Value = Deserialize::deserialize(deserializer)?;
42 Ok(T::deserialize(v).ok())
43}
44
45macro_rules! default_deserialize {
47 (@inner
48 $(#[$ty_attr:meta])*
49 $ty_vis:vis struct $name:ident { $(
50 $(#[$field_attr:meta])*
51 $field_vis:vis $field_name:ident: $field_ty:ty
52 ,)* $(,)? }
53 ) => {
54 #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
55 #[cfg_attr(feature = "persist_settings", derive(Serialize, Deserialize))]
56 struct UiSettingsFallback { $(
57 $(#[$field_attr])*
58 #[cfg_attr(feature = "persist_settings", serde(deserialize_with = "ok_or_none"))] #[cfg_attr(feature = "persist_settings", serde(default))] $field_vis $field_name: Option<$field_ty>,
61 )* }
62 impl UiSettingsFallback {
63 pub fn has_all_fields(&self) -> bool {
65 $(
66 self.$field_name.is_some()
67 )&&*
68 }
69 }
70 impl From<UiSettingsFallback> for $name {
71 fn from(value: UiSettingsFallback) -> Self {
72 let mut this = <Self as Default>::default();
73 $(
74 if let Some($field_name) = value.$field_name {
75 this.$field_name = $field_name;
76 }
77 )*
78 this
79 }
80 }
81 };
82 ($($token:tt)*) => {
83 $($token)*
84 default_deserialize! { @inner $($token)* }
85 };
86}
87
88#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Debug)]
89#[cfg_attr(feature = "persist_settings", derive(Serialize, Deserialize))]
90#[cfg_attr(feature = "persist_settings", serde(rename_all = "lowercase"))]
91#[allow(dead_code)]
92pub enum AutoStart {
93 #[default]
94 Disabled,
95 Enabled,
96 Elevated,
97}
98impl AutoStart {
99 pub const ALL: &'static [Self] = &[
100 Self::Disabled,
101 Self::Elevated,
104 ];
105}
106impl fmt::Display for AutoStart {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 let text = match *self {
110 AutoStart::Disabled => "No",
111 AutoStart::Enabled => "Yes",
112 AutoStart::Elevated => "Yes, with admin rights",
113 };
114 f.write_str(text)
115 }
116}
117
118#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Debug)]
119#[cfg_attr(feature = "persist_settings", derive(Serialize, Deserialize))]
120#[allow(dead_code)]
121pub enum QuickSwitchMenu {
122 Disabled,
123 TopMenu,
124 #[default]
125 SubMenu,
126}
127impl QuickSwitchMenu {
128 pub const ALL: &'static [Self] = &[Self::Disabled, Self::TopMenu, Self::SubMenu];
129}
130impl fmt::Display for QuickSwitchMenu {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 let text = match *self {
134 QuickSwitchMenu::Disabled => "Off",
135 QuickSwitchMenu::TopMenu => "Inside the main context menu",
136 QuickSwitchMenu::SubMenu => "Inside a submenu",
137 };
138 f.write_str(text)
139 }
140}
141
142#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Debug)]
143#[cfg_attr(feature = "persist_settings", derive(Serialize, Deserialize))]
144#[allow(dead_code)]
145pub enum TrayIconType {
146 #[default]
151 WithBackground,
152 WithBackgroundNoHardcoded,
155 NoBackground,
158 NoBackground2,
161 AppIcon,
163}
164impl TrayIconType {
165 pub const ALL: &'static [Self] = &[
166 #[cfg(feature = "tray_icon_hardcoded")]
167 Self::WithBackground,
168 #[cfg(feature = "tray_icon_with_background")]
169 Self::WithBackgroundNoHardcoded,
170 #[cfg(feature = "tray_icon_text_only")]
171 Self::NoBackground,
172 #[cfg(feature = "tray_icon_text_only_alt")]
173 Self::NoBackground2,
174 Self::AppIcon,
175 ];
176}
177impl fmt::Display for TrayIconType {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 let text = match *self {
181 TrayIconType::WithBackground => "Hardcoded number inside icon",
182 TrayIconType::WithBackgroundNoHardcoded => "Generated number inside icon",
183 TrayIconType::NoBackground => "Only black and white number",
184 TrayIconType::NoBackground2 => "Only purple number",
185 TrayIconType::AppIcon => "Only program icon, no number",
186 };
187 f.write_str(text)
188 }
189}
190
191#[derive(Default, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)]
192#[cfg_attr(feature = "persist_settings", derive(Serialize, Deserialize))]
193#[allow(dead_code)]
194pub enum TrayClickAction {
195 #[default]
196 Disabled,
197 StopFlashingWindows,
198 ToggleConfigurationWindow,
199 ApplyFilters,
200 OpenContextMenu,
201}
202impl TrayClickAction {
203 pub const ALL: &'static [Self] = &[
204 Self::Disabled,
205 Self::StopFlashingWindows,
206 Self::ToggleConfigurationWindow,
207 Self::ApplyFilters,
208 Self::OpenContextMenu,
209 ];
210}
211impl fmt::Display for TrayClickAction {
213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 let text = match *self {
215 Self::Disabled => "Disabled",
216 Self::StopFlashingWindows => "Stop Flashing Windows",
217 Self::ToggleConfigurationWindow => "Open/Close Config Window",
218 Self::ApplyFilters => "Apply Filters",
219 Self::OpenContextMenu => "Open Context Menu",
220 };
221 f.write_str(text)
222 }
223}
224
225#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)]
226#[cfg_attr(feature = "persist_settings", derive(Serialize, Deserialize))]
227pub struct ConfigWindowInfo {
228 pub position: Option<(i32, i32)>,
229 pub size: (u32, u32),
230 pub maximized: bool,
231}
232impl Default for ConfigWindowInfo {
233 fn default() -> Self {
234 Self {
235 position: None,
236 size: (800, 600),
237 maximized: false,
238 }
239 }
240}
241
242default_deserialize!(
243 #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
244 #[cfg_attr(feature = "persist_settings", derive(Serialize, Deserialize))]
245 pub struct UiSettings {
246 pub version: u64,
247 pub auto_start: AutoStart,
249 pub smooth_switch_desktops: bool,
253 pub request_admin_at_startup: bool,
257 pub stop_flashing_windows_after_applying_filter: bool,
263 pub tray_icon_type: TrayIconType,
265 pub quick_switch_menu: QuickSwitchMenu,
268 pub quick_switch_menu_shortcuts: Arc<BTreeMap<String, u32>>,
274 pub quick_switch_menu_shortcuts_only_in_root: bool,
278
279 pub quick_switch_hotkey: Arc<str>,
282
283 pub open_menu_at_mouse_pos_hotkey: Arc<str>,
287
288 pub left_click: TrayClickAction,
289 pub middle_click: TrayClickAction,
292
293 pub config_window: ConfigWindowInfo,
295 pub filters: Arc<[WindowFilter]>,
298 }
299);
300impl UiSettings {
301 const CURRENT_VERSION: u64 = 2;
302
303 fn migrate(&mut self) {
307 self.version = Self::CURRENT_VERSION;
310 }
311}
312impl UiSettingsFallback {
313 fn maybe_migrate(&mut self) -> bool {
316 if self.open_menu_at_mouse_pos_hotkey.is_none() && matches!(self.version, Some(v) if v <= 1) {
317 self.open_menu_at_mouse_pos_hotkey = Some(Arc::from(""));
318 }
319 self.has_all_fields()
320 }
321}
322impl Default for UiSettings {
323 fn default() -> Self {
324 Self {
325 version: Self::CURRENT_VERSION,
326 auto_start: AutoStart::default(),
327 smooth_switch_desktops: true,
328 request_admin_at_startup: false,
329 stop_flashing_windows_after_applying_filter: false,
330 tray_icon_type: TrayIconType::default(),
331 quick_switch_menu: QuickSwitchMenu::default(),
332 quick_switch_menu_shortcuts: Arc::new(BTreeMap::from([
333 (",".to_owned(), 0),
335 ])),
336 quick_switch_menu_shortcuts_only_in_root: false,
337 quick_switch_hotkey: Arc::from(""),
338 open_menu_at_mouse_pos_hotkey: Arc::from(""),
339
340 left_click: TrayClickAction::ToggleConfigurationWindow,
341 middle_click: TrayClickAction::ApplyFilters,
342
343 config_window: ConfigWindowInfo::default(),
344 filters: Arc::new([]),
345 }
346 }
347}
348
349#[cfg(feature = "persist_settings")]
350struct UiState {
351 error_notice: nwg::NoticeSender,
352 error_tx: mpsc::Sender<String>,
353 thread_join: std::thread::JoinHandle<()>,
354}
355
356struct UiSettingsPluginState {
357 settings: Arc<UiSettings>,
358 #[cfg(feature = "persist_settings")]
359 settings_in_file: Arc<UiSettings>,
360 save_path: Option<Arc<Path>>,
361 temp_save_path: Option<Arc<Path>>,
362 #[cfg(feature = "persist_settings")]
363 should_close: bool,
364 #[cfg(feature = "persist_settings")]
365 ui_state: Option<UiState>,
366}
367impl Default for UiSettingsPluginState {
368 fn default() -> Self {
369 let settings = Arc::new(UiSettings::default());
370 #[cfg(feature = "persist_settings")]
371 let settings_in_file = Arc::clone(&settings);
372 Self {
373 settings,
374 #[cfg(feature = "persist_settings")]
375 settings_in_file,
376 #[cfg(feature = "persist_settings")]
377 should_close: false,
378 #[cfg(feature = "persist_settings")]
379 ui_state: None,
380 save_path: None,
381 temp_save_path: None,
382 }
383 }
384}
385
386#[derive(Default)]
387struct UiSettingsPluginShared {
388 state: Mutex<UiSettingsPluginState>,
389 notify_change: Condvar,
391 #[cfg(feature = "persist_settings")]
394 notify_close: Condvar,
395}
396#[cfg(feature = "persist_settings")]
397impl UiSettingsPluginShared {
398 fn close_background_thread(this: &Self, mut guard: MutexGuard<UiSettingsPluginState>) {
399 guard.should_close = true;
400 let ui_state = guard.ui_state.take();
401 drop(guard);
402 this.notify_change.notify_all();
403 this.notify_close.notify_all();
404 if let Some(ui_state) = ui_state {
405 ui_state.thread_join.join().unwrap();
406 }
407 }
408 fn start_background_work(
409 self: &Arc<Self>,
410 error_notice: nwg::NoticeSender,
411 error_tx: mpsc::Sender<String>,
412 ) {
413 let mut guard = self.state.lock().unwrap();
414
415 while guard.should_close && guard.ui_state.is_some() {
417 Self::close_background_thread(self, guard);
418 guard = self.state.lock().unwrap();
419 }
420 guard.should_close = false;
421
422 if let Some(ui_state) = &mut guard.ui_state {
424 ui_state.error_notice = error_notice;
425 ui_state.error_tx = error_tx;
426 return;
427 }
428
429 let thread_join = std::thread::Builder::new()
431 .name("UiSettingsSaveThread".to_owned())
432 .spawn({
433 let shared = Arc::clone(self);
434 move || shared.background_work()
435 })
436 .expect("Failed to spawn thread for saving UI settings");
437 guard.ui_state = Some(UiState {
438 error_notice,
439 error_tx,
440 thread_join,
441 });
442 }
443 fn background_work(self: Arc<Self>) {
444 let mut guard = self.state.lock().unwrap();
445 let mut latest_saved;
446 while !guard.should_close {
447 latest_saved = Arc::clone(&guard.settings);
448 let result = self.save_settings_inner(guard);
449 guard = self.state.lock().unwrap();
450 if guard.should_close {
451 return;
452 }
453 match result {
454 Ok(true) => {
455 guard = self
457 .notify_close
458 .wait_timeout(guard, Duration::from_millis(1000))
459 .unwrap()
460 .0;
461 if guard.should_close {
462 return;
463 }
464 }
465 Ok(false) => {
466 }
468 Err(e) => {
469 tracing::error!(?e, "Failed to save UI settings");
470 if let Some(ui_state) = &guard.ui_state {
471 if let Err(e) = ui_state.error_tx.send(e) {
472 tracing::warn!(error = ?e, "Failed to send UiSettings save error to UI thread");
473 }
474 ui_state.error_notice.notice();
475 }
476 }
477 }
478
479 if Arc::ptr_eq(&latest_saved, &guard.settings) {
481 guard = self.notify_change.wait(guard).unwrap();
482 if guard.should_close {
483 return;
484 }
485 }
486
487 guard = self
489 .notify_close
490 .wait_timeout(guard, Duration::from_millis(50))
491 .unwrap()
492 .0;
493 }
494 }
495
496 fn save_settings_inner(
497 &self,
498 mut guard: MutexGuard<UiSettingsPluginState>,
499 ) -> Result<bool, String> {
500 if Arc::ptr_eq(&guard.settings, &guard.settings_in_file) {
501 return Ok(false);
502 }
503 if guard.settings == guard.settings_in_file {
504 guard.settings_in_file = Arc::clone(&guard.settings);
506 return Ok(false);
507 }
508 let new_data = guard.settings.clone();
509
510 let Some(save_path) = guard.save_path.clone() else {
511 tracing::warn!("Can't save settings since there was no save path");
512 return Ok(false);
513 };
514 let Some(temp_path) = guard.temp_save_path.clone() else {
515 tracing::warn!("Can't save settings since there was no temporary save path");
516 return Ok(false);
517 };
518 drop(guard);
520
521 tracing::trace!(?save_path, ?temp_path, ?new_data, "Saving UI settings");
522
523 let binary_data = serde_json::to_vec_pretty(&*new_data)
524 .map_err(|e| format!("Failed to serialize UI settings: {e}"))?;
525
526 match std::fs::remove_file(&temp_path) {
527 Ok(_) => {}
528 Err(e) if e.kind() == NotFound => {}
529 Err(e) => {
530 return Err(format!("Failed to remove temp ui settings: {e}"));
531 }
532 }
533
534 {
535 let mut file = std::fs::OpenOptions::new()
536 .create_new(true)
537 .truncate(true)
538 .write(true)
539 .open(&temp_path)
540 .map_err(|e| format!("Failed to create new UI settings file: {e}"))?;
541
542 file.write_all(&binary_data)
543 .map_err(|e| format!("Failed to write UI settings to file: {e}"))?;
544
545 file.flush()
546 .map_err(|e| format!("Failed to flush UI settings to file: {e}"))?;
547 }
548
549 std::fs::rename(&temp_path, &save_path)
550 .map_err(|e| format!("Failed to rename new UI settings file: {e}"))?;
551
552 let mut guard = self.state.lock().unwrap();
553 guard.settings_in_file = new_data;
554
555 Ok(true)
556 }
557}
558
559#[derive(Default)]
560struct UiSettingsPluginSharedStrong(Arc<UiSettingsPluginShared>);
561#[cfg(feature = "persist_settings")]
562impl Drop for UiSettingsPluginSharedStrong {
563 fn drop(&mut self) {
564 if let Ok(guard) = self.0.state.lock() {
565 UiSettingsPluginShared::close_background_thread(&self.0, guard);
566 }
567 }
568}
569impl Deref for UiSettingsPluginSharedStrong {
570 type Target = Arc<UiSettingsPluginShared>;
571 fn deref(&self) -> &Self::Target {
572 &self.0
573 }
574}
575
576#[derive(nwd::NwgPartial, Default)]
578pub struct UiSettingsPlugin {
579 tray_ui: SystemTrayRef,
580 #[cfg(feature = "persist_settings")]
581 #[nwg_control]
582 #[nwg_events(OnNotice: [Self::on_background_error])]
583 error_notice: nwg::Notice,
584 #[cfg(feature = "persist_settings")]
585 error_rx: OnceCell<mpsc::Receiver<String>>,
586 load_error: Cell<Option<String>>,
587 shared: UiSettingsPluginSharedStrong,
588}
589impl UiSettingsPlugin {
590 pub fn get(&self) -> Arc<UiSettings> {
591 Arc::clone(&self.shared.state.lock().unwrap().settings)
592 }
593 pub fn set(&self, value: UiSettings) {
594 let new;
595 let prev = {
596 let mut state = self.shared.state.lock().unwrap();
597 if *state.settings == value {
598 return;
599 }
600 new = Arc::new(value);
601 let prev = std::mem::replace(&mut state.settings, Arc::clone(&new));
602 self.shared.notify_change.notify_all();
603 prev
604 };
605 if let Some(tray) = self.tray_ui.get() {
606 tray.notify_settings_changed(&prev, &new);
607 }
608 }
609 pub fn update(&self, f: impl FnOnce(&UiSettings) -> UiSettings) {
610 let current = self.get();
611 let new = f(¤t);
612 drop(current);
613 self.set(new);
614 }
615
616 pub fn with_save_path_next_to_exe() -> Self {
617 let mut this = Self::default();
618 this.set_save_path_next_to_exe();
619 this
620 }
621 pub fn set_save_path_next_to_exe(&mut self) {
622 let exe_path = match std::env::current_exe() {
623 Ok(v) => v,
624 Err(e) => {
625 self.load_error.set(Some(format!(
626 "Failed to find UI settings file, can't get executable's path: {e}"
627 )));
628 return;
629 }
630 };
631 {
632 let mut guard = self.shared.state.lock().unwrap();
633 guard.save_path = Some(Arc::from(exe_path.with_extension("settings.json")));
634 guard.temp_save_path = Some(Arc::from(exe_path.with_extension("settings.temp.json")));
635 }
636 self.load_data();
637 }
638 pub fn load_data(&self) {
639 #[cfg(feature = "persist_settings")]
640 {
641 let Some(save_path) = self.shared.state.lock().unwrap().save_path.clone() else {
642 return;
643 };
644 let (settings, load_error) = match std::fs::read_to_string(&save_path) {
645 Ok(data) => {
646 let mut deserializer = serde_json::Deserializer::from_str(&data);
647 let result: Result<UiSettings, _> = {
648 #[cfg(not(feature = "serde_path_to_error"))]
649 {
650 serde::Deserialize::deserialize(&mut deserializer)
651 }
652 #[cfg(feature = "serde_path_to_error")]
653 {
654 serde_path_to_error::deserialize(&mut deserializer)
655 }
656 };
657 match result {
658 Ok(settings) => (Some(settings), None),
659 Err(e) => {
660 let mut ignore_error = false;
661 (
662 serde_json::from_str::<UiSettingsFallback>(&data)
665 .ok()
666 .map(|mut fallback| {
667 ignore_error = fallback.maybe_migrate();
668 UiSettings::from(fallback)
669 }),
670 Some(format!(
672 "Could not parse UI settings file as JSON: {e}: Settings file at \"{}\"",
673 save_path.display()
674 )).filter(|_| !ignore_error),
675 )
676 }
677 }
678 }
679 Err(e) if e.kind() == NotFound => {
680 tracing::trace!(
681 "Using default settings since no UI settings file was found at \"{}\"",
682 save_path.display()
683 );
684 (None, None)
685 }
686 Err(e) => (
687 None,
688 Some(format!(
689 "Failed to read UI settings file: {e}: Settings file at \"{}\"",
690 save_path.display()
691 )),
692 ),
693 };
694 if let Some(error) = load_error {
696 if let Some(tray) = self.tray_ui.get() {
697 Self::notify_load_error(&tray, &error)
698 } else {
699 self.load_error.set(Some(error));
700 }
701 }
702 if let Some(mut settings) = settings {
704 settings.migrate();
705 let new = Arc::new(settings);
706 let prev = {
707 let mut state = self.shared.state.lock().unwrap();
708 state.settings_in_file = Arc::clone(&new);
709 std::mem::replace(&mut state.settings, Arc::clone(&new))
710 };
711 if let Some(tray) = self.tray_ui.get() {
712 tray.notify_settings_changed(&prev, &new);
713 }
714 }
715 }
716 }
717 fn notify_load_error(tray_ui: &SystemTray, error: &str) {
718 tray_ui.show_notification("Virtual Desktop Manager Error", error);
719 }
720 #[cfg(feature = "persist_settings")]
721 fn on_background_error(&self) {
722 let Some(error_rx) = self.error_rx.get() else {
723 return;
724 };
725 let Some(dynamic_ui) = self.tray_ui.get() else {
726 return;
727 };
728 for error in error_rx.try_iter() {
729 dynamic_ui.show_notification("Virtual Desktop Manager Error", &error);
730 }
731 }
732}
733impl DynamicUiHooks<SystemTray> for UiSettingsPlugin {
734 fn before_partial_build(
735 &mut self,
736 tray_ui: &Rc<SystemTray>,
737 _should_build: &mut bool,
738 ) -> Option<(nwg::ControlHandle, TypeId)> {
739 self.tray_ui.set(tray_ui);
740 Some((tray_ui.root().window.handle, TypeId::of::<TrayRoot>()))
741 }
742 fn after_partial_build(&mut self, tray_ui: &Rc<SystemTray>) {
743 if let Some(error) = self.load_error.take() {
744 Self::notify_load_error(tray_ui, &error);
745 }
746
747 #[cfg(feature = "persist_settings")]
748 {
749 let (tx, rx) = mpsc::channel();
750 self.shared
751 .start_background_work(self.error_notice.sender(), tx);
752 if self.error_rx.set(rx).is_err() {
753 tracing::error!("Failed to set new error receiver for UiSettingsPlugin");
754 }
755 }
756 }
757 fn before_rebuild(&mut self, _dynamic_ui: &Rc<SystemTray>) {
758 self.tray_ui = Default::default();
759 #[cfg(feature = "persist_settings")]
760 {
761 self.error_notice = Default::default();
762 self.error_rx = OnceCell::new();
763 }
764 }
765}
766impl TrayPlugin for UiSettingsPlugin {}