a lightweight, interval-based utility to combat digital strain through "Ma" (intentional pauses) for the eyes and body.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: lazy-create settings on first open

+114 -92
+59 -43
src/main.rs
··· 22 22 use app::{active_profile, spawn_idle_poller, AppState}; 23 23 use config::AppConfig; 24 24 use overlay::OverlayManager; 25 - use settings::SettingsManager; 25 + use settings::{read_settings_into, SettingsManager}; 26 26 use timer::{BreakMode, TimerCommand, TimerEvent}; 27 27 use tray::AppTray; 28 28 ··· 64 64 let sound_volume = cfg.appearance.sound_volume; 65 65 let active_profile_name = active_profile(&cfg).name.clone(); 66 66 67 - let settings_mgr = Rc::new(SettingsManager::new(&cfg)?); 68 - 69 - // Settings callbacks. 70 - { 71 - let win = settings_mgr.window().clone_strong(); 72 - settings_mgr.window().on_cancel_clicked(move || win.hide().unwrap_or_default()); 73 - } 74 - { 75 - let win = settings_mgr.window().clone_strong(); 76 - let cfg_arc = state.cfg.clone(); 77 - let settings_ref = Rc::clone(&settings_mgr); 78 - let cmd_tx_save = state.cmd_tx.clone(); 79 - settings_mgr.window().on_save_clicked(move || { 80 - let mut cfg = cfg_arc.lock().unwrap().clone(); 81 - settings_ref.read_into(&mut cfg); 82 - *cfg_arc.lock().unwrap() = cfg.clone(); 83 - if let Err(e) = autostart::set_enabled(cfg.app.autostart) { 84 - log::warn!("Autostart toggle failed: {e}"); 85 - } 86 - win.hide().unwrap_or_default(); 87 - config::save(&cfg).unwrap_or_else(|e| log::warn!("save failed: {e}")); 88 - // Live apply: switch the running timer to the new profile immediately. 89 - let profile = active_profile(&cfg); 90 - let _ = cmd_tx_save.send(TimerCommand::SetProfile(profile)); 91 - }); 92 - } 93 - settings_mgr.window().on_open_config_dir(|| { 94 - if let Some(dir) = config::config_path().parent() { 95 - let _ = open::that(dir); 96 - } 97 - }); 98 - settings_mgr.window().on_set_password_clicked(|| { 99 - log::info!("Password setup — Phase 3"); 100 - }); 101 - 102 - // Poll timer — owns overlay, break state, and event_rx directly. 103 - // No Rc<RefCell> needed: this closure is the sole accessor of these values. 67 + // Poll timer — owns all mutable state; no Rc<RefCell> on the hot path. 104 68 let poll_timer = slint::Timer::default(); 105 69 { 106 70 let tray = tray.clone(); 107 - let settings_handle = settings_mgr.window().clone_strong(); 108 71 let cmd_tx = state.cmd_tx.clone(); 72 + let cfg_arc = state.cfg.clone(); 73 + let cmd_tx_settings = state.cmd_tx.clone(); 109 74 110 - // These are owned by the closure — no RefCell, no double-borrow risk. 75 + // Settings window is created lazily on first open to avoid appearing 76 + // in the taskbar/dock at startup before the user opens it. 77 + let mut settings_mgr: Option<SettingsManager> = None; 111 78 let mut overlay: Option<OverlayManager> = None; 112 79 let mut break_active: Option<(Instant, Duration)> = None; 113 80 let mut snooze_used = false; ··· 128 95 slint::quit_event_loop().unwrap_or_default(); 129 96 return; 130 97 } 98 + 131 99 if open_settings { 132 - settings_handle.show().unwrap_or_default(); 100 + if settings_mgr.is_none() { 101 + // First open: create window, wire callbacks, then show. 102 + let cfg_snap = cfg_arc.lock().unwrap().clone(); 103 + match SettingsManager::new(&cfg_snap) { 104 + Ok(mgr) => { 105 + // Cancel 106 + { 107 + let win = mgr.window().clone_strong(); 108 + mgr.window().on_cancel_clicked(move || { 109 + win.hide().unwrap_or_default(); 110 + }); 111 + } 112 + // Save 113 + { 114 + let win = mgr.window().clone_strong(); 115 + let lm = mgr.levels_model(); 116 + let cfg_arc2 = cfg_arc.clone(); 117 + let cmd_tx_save = cmd_tx_settings.clone(); 118 + mgr.window().on_save_clicked(move || { 119 + let mut cfg = cfg_arc2.lock().unwrap().clone(); 120 + read_settings_into(&win, &lm, &mut cfg); 121 + *cfg_arc2.lock().unwrap() = cfg.clone(); 122 + if let Err(e) = autostart::set_enabled(cfg.app.autostart) { 123 + log::warn!("Autostart toggle failed: {e}"); 124 + } 125 + win.hide().unwrap_or_default(); 126 + config::save(&cfg) 127 + .unwrap_or_else(|e| log::warn!("save failed: {e}")); 128 + let profile = active_profile(&cfg); 129 + let _ = cmd_tx_save.send(TimerCommand::SetProfile(profile)); 130 + }); 131 + } 132 + // Open config dir 133 + mgr.window().on_open_config_dir(|| { 134 + if let Some(dir) = config::config_path().parent() { 135 + let _ = open::that(dir); 136 + } 137 + }); 138 + // Password (Phase 3) 139 + mgr.window().on_set_password_clicked(|| { 140 + log::info!("Password setup — Phase 3"); 141 + }); 142 + 143 + mgr.window().show().unwrap_or_default(); 144 + settings_mgr = Some(mgr); 145 + } 146 + Err(e) => log::warn!("Failed to create settings window: {e}"), 147 + } 148 + } else if let Some(ref mgr) = settings_mgr { 149 + mgr.window().show().unwrap_or_default(); 150 + } 133 151 } 134 152 135 153 // --- Timer events --- ··· 138 156 TimerEvent::BreakStarting(sched) => { 139 157 let dur = sched.break_duration; 140 158 break_active = Some((Instant::now(), dur)); 141 - // snooze_used carries over — only reset when break fully ends (Resumed) 142 159 143 160 if sound_enabled { 144 161 sound::play_chime(sound_volume); ··· 146 163 tray.set_paused(true); 147 164 tray.set_tooltip(&format!("ioma — {}", sched.label)); 148 165 149 - // Create overlay lazily so it doesn't appear in taskbar at startup. 150 166 if overlay.is_none() { 151 167 match OverlayManager::new(is_dark) { 152 168 Ok(mgr) => {
+55 -49
src/settings/mod.rs
··· 19 19 20 20 populate(&window, &levels_model, cfg); 21 21 22 - // Level editor callbacks 23 22 { 24 23 let model = Rc::clone(&levels_model); 25 24 window.on_level_changed(move |idx, entry| { ··· 46 45 }); 47 46 }); 48 47 } 49 - 50 - // Profile switch: re-populate level editor from saved config 51 48 { 52 49 let cfg_clone = cfg.clone(); 53 50 let model = Rc::clone(&levels_model); ··· 57 54 }); 58 55 } 59 56 60 - window.hide().unwrap_or_default(); 57 + // Do NOT call hide() — window is created lazily right before show(), 58 + // so hiding would fight against the immediate show() that follows. 61 59 Ok(Self { window, levels_model }) 62 60 } 63 61 ··· 65 63 &self.window 66 64 } 67 65 66 + pub fn levels_model(&self) -> Rc<VecModel<LevelEntry>> { 67 + Rc::clone(&self.levels_model) 68 + } 69 + 68 70 pub fn read_into(&self, cfg: &mut AppConfig) { 69 - // Resolve active profile key from display name 70 - let active_name = self.window.get_active_profile().to_string(); 71 - let active_key = cfg 72 - .profiles 73 - .iter() 74 - .find(|(_, p)| p.name == active_name) 75 - .map(|(k, _)| k.clone()); 71 + read_settings_into(&self.window, &self.levels_model, cfg); 72 + } 73 + } 76 74 77 - if let Some(key) = active_key { 78 - cfg.app.active_profile = key.clone(); 75 + /// Reads the settings window state back into `cfg`. Also usable from callbacks 76 + /// that capture `SettingsWindow` and `VecModel<LevelEntry>` directly. 77 + pub fn read_settings_into( 78 + window: &SettingsWindow, 79 + levels_model: &VecModel<LevelEntry>, 80 + cfg: &mut AppConfig, 81 + ) { 82 + let active_name = window.get_active_profile().to_string(); 83 + let active_key = cfg 84 + .profiles 85 + .iter() 86 + .find(|(_, p)| p.name == active_name) 87 + .map(|(k, _)| k.clone()); 79 88 80 - if let Some(prof) = cfg.profiles.get_mut(&key) { 81 - // Levels from model 82 - prof.levels = (0..self.levels_model.row_count()) 83 - .filter_map(|i| self.levels_model.row_data(i)) 84 - .map(|e| BreakLevelConfig { 85 - work_secs: e.work_mins as u64 * 60, 86 - break_secs: e.break_mins as u64 * 60 + e.break_extra_secs as u64, 87 - label: e.label.to_string(), 88 - }) 89 - .collect(); 89 + if let Some(key) = active_key { 90 + cfg.app.active_profile = key.clone(); 90 91 91 - // Long break 92 - prof.long_break = if self.window.get_long_break_enabled() { 93 - Some(LongBreakConfig { 94 - after_cycles: self.window.get_long_break_after_cycles() as u32, 95 - max_cycle_gap_secs: self.window.get_long_break_gap_mins() as u64 * 60, 96 - break_secs: self.window.get_long_break_duration_mins() as u64 * 60, 97 - label: self.window.get_long_break_label().to_string(), 98 - }) 99 - } else { 100 - None 101 - }; 92 + if let Some(prof) = cfg.profiles.get_mut(&key) { 93 + prof.levels = (0..levels_model.row_count()) 94 + .filter_map(|i| levels_model.row_data(i)) 95 + .map(|e| BreakLevelConfig { 96 + work_secs: e.work_mins as u64 * 60, 97 + break_secs: e.break_mins as u64 * 60 + e.break_extra_secs as u64, 98 + label: e.label.to_string(), 99 + }) 100 + .collect(); 102 101 103 - prof.idle_detection_enabled = self.window.get_idle_detection_enabled(); 104 - prof.idle_threshold_secs = 105 - self.window.get_idle_threshold_mins() as u64 * 60; 106 - prof.mode = if self.window.get_enforced_mode() { 107 - BreakModeConfig::Enforced 108 - } else { 109 - BreakModeConfig::Reminder 110 - }; 111 - } 112 - } 102 + prof.long_break = if window.get_long_break_enabled() { 103 + Some(LongBreakConfig { 104 + after_cycles: window.get_long_break_after_cycles() as u32, 105 + max_cycle_gap_secs: window.get_long_break_gap_mins() as u64 * 60, 106 + break_secs: window.get_long_break_duration_mins() as u64 * 60, 107 + label: window.get_long_break_label().to_string(), 108 + }) 109 + } else { 110 + None 111 + }; 113 112 114 - cfg.appearance.sound_enabled = self.window.get_sound_enabled(); 115 - cfg.appearance.sound_volume = self.window.get_sound_volume(); 116 - cfg.app.autostart = self.window.get_autostart(); 113 + prof.idle_detection_enabled = window.get_idle_detection_enabled(); 114 + prof.idle_threshold_secs = window.get_idle_threshold_mins() as u64 * 60; 115 + prof.mode = if window.get_enforced_mode() { 116 + BreakModeConfig::Enforced 117 + } else { 118 + BreakModeConfig::Reminder 119 + }; 120 + } 117 121 } 122 + 123 + cfg.appearance.sound_enabled = window.get_sound_enabled(); 124 + cfg.appearance.sound_volume = window.get_sound_volume(); 125 + cfg.app.autostart = window.get_autostart(); 118 126 } 119 127 120 128 fn populate(window: &SettingsWindow, levels_model: &VecModel<LevelEntry>, cfg: &AppConfig) { ··· 155 163 prof.map_or(5, |p| (p.idle_threshold_secs / 60).max(1) as i32), 156 164 ); 157 165 158 - // Replace level rows 159 166 let entries: Vec<LevelEntry> = prof 160 167 .map(|p| { 161 168 p.levels ··· 177 184 levels_model.push(entry); 178 185 } 179 186 180 - // Long break 181 187 let lb = prof.and_then(|p| p.long_break.as_ref()); 182 188 window.set_long_break_enabled(lb.is_some()); 183 189 window.set_long_break_after_cycles(lb.map_or(4, |l| l.after_cycles as i32));