Select the types of activity you want to include in your feed.
fix: remove crash and improve overlay
- eliminate RefCell double-borrow crash - show() before set_fullscreen() - reset snooze_used per break - center progres bar in overlay - lazy overlay creation
···99mod timer;
1010mod tray;
11111212-use std::cell::RefCell;
1312use std::rc::Rc;
1413use std::time::{Duration, Instant};
1514···3029fn main() -> anyhow::Result<()> {
3130 env_logger::init();
32313333- // GTK must be initialized on the main thread before any GTK/tray operations.
3432 #[cfg(target_os = "linux")]
3533 init_gtk();
3634···4341 .enable_all()
4442 .build()?;
45434646- // Enter the tokio runtime context so tokio::spawn works inside run().
4744 let _guard = rt.enter();
4845 run(cfg)
4946}
···6764 let sound_volume = cfg.appearance.sound_volume;
6865 let active_profile_name = active_profile(&cfg).name.clone();
69667070- let overlay = Rc::new(RefCell::new(OverlayManager::new(is_dark)?));
7167 let settings_mgr = Rc::new(SettingsManager::new(&cfg)?);
72687373- // Overlay callbacks (attached to primary/first monitor window).
7474- {
7575- let cmd_tx = state.cmd_tx.clone();
7676- overlay.borrow().primary_window().on_snooze_clicked(move || {
7777- let _ = cmd_tx.send(TimerCommand::BreakEnded { snoozed: true });
7878- });
7979- }
8080- {
8181- let cmd_tx = state.cmd_tx.clone();
8282- overlay.borrow().primary_window().on_unlock_clicked(move || {
8383- // Phase 3: enforced mode will prompt for password first.
8484- let _ = cmd_tx.send(TimerCommand::BreakEnded { snoozed: false });
8585- });
8686- }
8787-8869 // Settings callbacks.
8970 {
9071 let win = settings_mgr.window().clone_strong();
···9677 let settings_ref = Rc::clone(&settings_mgr);
9778 settings_mgr.window().on_save_clicked(move || {
9879 let mut cfg = cfg_arc.lock().unwrap().clone();
9999- // Pull widget state back into config before saving.
10080 settings_ref.read_into(&mut cfg);
10181 *cfg_arc.lock().unwrap() = cfg.clone();
102102-103103- // Apply autostart immediately.
10482 if let Err(e) = autostart::set_enabled(cfg.app.autostart) {
10583 log::warn!("Autostart toggle failed: {e}");
10684 }
107107-10885 win.hide().unwrap_or_default();
10986 config::save(&cfg).unwrap_or_else(|e| log::warn!("save failed: {e}"));
11087 });
···11996 });
12097 settings_mgr.window().on_profile_changed(|_name| {});
12198122122- // Break state tracked on the main thread.
123123- let break_start: Rc<RefCell<Option<(Instant, Duration)>>> = Rc::new(RefCell::new(None));
124124-125125- // Shared event_rx wrapped for polling from Slint timer.
126126- let event_rx = Rc::new(RefCell::new(event_rx));
127127-128128- // Main event polling timer: drains timer events + tray events every 100 ms.
9999+ // Poll timer — owns overlay, break state, and event_rx directly.
100100+ // No Rc<RefCell> needed: this closure is the sole accessor of these values.
129101 let poll_timer = slint::Timer::default();
130102 {
131131- let overlay = overlay.clone();
132103 let tray = tray.clone();
133104 let settings_handle = settings_mgr.window().clone_strong();
134105 let cmd_tx = state.cmd_tx.clone();
135135- let event_rx = event_rx.clone();
136136- let break_start = break_start.clone();
106106+107107+ // These are owned by the closure — no RefCell, no double-borrow risk.
108108+ let mut overlay: Option<OverlayManager> = None;
109109+ let mut break_active: Option<(Instant, Duration)> = None;
110110+ let mut snooze_used = false;
137111138112 poll_timer.start(
139113 slint::TimerMode::Repeated,
140114 Duration::from_millis(100),
141115 move || {
142142- // Pump GTK events so the tray icon/menu stays responsive on Linux.
116116+ // Pump GTK events so the tray menu stays responsive on Linux.
143117 #[cfg(target_os = "linux")]
144118 while gtk::events_pending() {
145119 gtk::main_iteration_do(false);
146120 }
147121148148- // --- Tray events ---
122122+ // --- Tray ---
149123 let mut open_settings = false;
150124 if tray.process_events(&cmd_tx, &mut open_settings) {
151125 slint::quit_event_loop().unwrap_or_default();
···156130 }
157131158132 // --- Timer events ---
159159- let rx = event_rx.borrow();
160160- while let Ok(event) = rx.try_recv() {
133133+ while let Ok(event) = event_rx.try_recv() {
161134 match event {
162135 TimerEvent::BreakStarting(sched) => {
163136 let dur = sched.break_duration;
164164- *break_start.borrow_mut() = Some((Instant::now(), dur));
137137+ break_active = Some((Instant::now(), dur));
138138+ snooze_used = false;
165139166140 if sound_enabled {
167141 sound::play_chime(sound_volume);
168142 }
169169-143143+ tray.set_paused(true);
170144 tray.set_tooltip(&format!("ioma — {}", sched.label));
171145172172- overlay.borrow_mut().show_break(&sched, is_enforced, false);
146146+ // Create overlay windows lazily so they don't appear
147147+ // in the taskbar until a break actually starts.
148148+ if overlay.is_none() {
149149+ match OverlayManager::new(is_dark) {
150150+ Ok(mgr) => {
151151+ let tx = cmd_tx.clone();
152152+ mgr.primary_window().on_snooze_clicked(move || {
153153+ let _ = tx.send(TimerCommand::BreakEnded { snoozed: true });
154154+ });
155155+ let tx = cmd_tx.clone();
156156+ mgr.primary_window().on_unlock_clicked(move || {
157157+ let _ = tx.send(TimerCommand::BreakEnded { snoozed: false });
158158+ });
159159+ overlay = Some(mgr);
160160+ }
161161+ Err(e) => log::warn!("Failed to create overlay: {e}"),
162162+ }
163163+ }
164164+165165+ if let Some(ref mut mgr) = overlay {
166166+ mgr.show_break(&sched, is_enforced, snooze_used);
167167+ }
173168 }
174169 TimerEvent::Resumed => {
175175- *break_start.borrow_mut() = None;
170170+ break_active = None;
176171 tray.set_paused(false);
177172 tray.set_tooltip("ioma");
178178- overlay.borrow().hide_all();
173173+ if let Some(ref mgr) = overlay {
174174+ mgr.hide_all();
175175+ }
179176 }
180177 TimerEvent::SnoozePeriodStarting { remaining_secs } => {
181181- *break_start.borrow_mut() = None;
178178+ snooze_used = true;
179179+ break_active = None;
182180 tray.set_tooltip(&format!(
183181 "ioma — snoozed, break in {}",
184182 fmt_countdown(remaining_secs)
185183 ));
186186- overlay.borrow().hide_all();
184184+ if let Some(ref mgr) = overlay {
185185+ mgr.hide_all();
186186+ }
187187 }
188188 TimerEvent::WorkTick { secs_until_break } => {
189189 tray.set_paused(false);
···200200 }
201201 }
202202203203- // --- Update overlay countdown if break is active ---
204204- if let Some((start, dur)) = *break_start.borrow() {
205205- let elapsed = start.elapsed();
206206- if elapsed < dur {
207207- overlay.borrow().update_countdown();
208208- } else {
209209- *break_start.borrow_mut() = None;
203203+ // --- Overlay countdown tick ---
204204+ // Split the check and the mutation to avoid any borrow conflicts.
205205+ let expired = match break_active {
206206+ Some((start, dur)) if start.elapsed() < dur => {
207207+ if let Some(ref mgr) = overlay {
208208+ mgr.update_countdown();
209209+ }
210210+ false
210211 }
212212+ Some(_) => true, // break duration elapsed
213213+ None => false,
214214+ };
215215+ if expired {
216216+ break_active = None;
211217 }
212218 },
213219 );
+14-6
src/overlay/mod.rs
···1111 windows: Vec<OverlayWindow>,
1212 break_duration: Duration,
1313 break_started: Instant,
1414- is_dark: bool,
1514}
16151716impl OverlayManager {
1817 pub fn new(is_dark: bool) -> anyhow::Result<Self> {
1918 let monitors = get_monitors();
2019 let windows = if monitors.is_empty() {
2121- // Fallback: single fullscreen window.
2220 vec![make_window(is_dark, None)?]
2321 } else {
2422 monitors
···2725 .collect::<anyhow::Result<Vec<_>>>()?
2826 };
29272828+ // Ensure all overlay windows start hidden — Slint shows windows
2929+ // automatically when the event loop runs unless we hide them first.
3030+ for w in &windows {
3131+ w.hide().unwrap_or_default();
3232+ }
3333+3034 Ok(Self {
3135 windows,
3236 break_duration: Duration::from_secs(300),
3337 break_started: Instant::now(),
3434- is_dark,
3538 })
3639 }
3740···4447 w.set_snooze_visible(!is_enforced && !snooze_used);
4548 w.set_countdown_text(fmt_dur(sched.break_duration).as_str().into());
4649 w.set_progress(0.0);
5050+ // show() must come before set_fullscreen(true): the compositor needs
5151+ // the window mapped before it can honour a fullscreen request.
4752 w.show().unwrap_or_default();
5353+ w.window().set_fullscreen(true);
4854 }
4955 }
50565157 pub fn hide_all(&self) {
5258 for w in &self.windows {
5959+ w.window().set_fullscreen(false);
5360 w.hide().unwrap_or_default();
5461 }
5562 }
···6875 }
6976 }
70777171- /// Returns the first window (primary monitor) for attaching callbacks.
7878+ /// The first window — used to attach snooze/unlock callbacks.
7279 pub fn primary_window(&self) -> &OverlayWindow {
7380 &self.windows[0]
7481 }
···99106 w.set_is_dark(is_dark);
100107101108 if let Some(m) = monitor {
102102- // Use physical pixel coordinates from display-info.
109109+ // Pre-position the window on this monitor. On X11 this takes effect;
110110+ // on Wayland set_position is ignored and fullscreen covers the monitor
111111+ // the window was most recently visible on (handled in show_break).
103112 w.window().set_position(PhysicalPosition::new(m.x, m.y));
104113 w.window().set_size(PhysicalSize::new(m.width, m.height));
105114 }
106106- // no-frame is set in overlay.slint; we size manually per monitor.
107115108116 Ok(w)
109117}