···11-use std::time::Duration;
21use anyhow::Result;
22+use std::time::Duration;
3344pub trait IdleDetector: Send {
55 /// Returns how long the user has been idle, or None if detection is unavailable.
···2626 }
2727}
28282929-/// Fallback for unsupported platforms.
2929+// Fallback for unsupported platforms.
3030#[cfg(not(any(target_os = "linux", target_os = "windows")))]
3131pub struct NullIdleDetector;
3232#[cfg(not(any(target_os = "linux", target_os = "windows")))]
···11#[derive(Debug, Clone, Copy, PartialEq)]
22pub enum SessionType {
33- /// X11 (Linux) — explicit position+size works; one Slint window per monitor.
33+ // X11 (Linux) — explicit position+size works; one Slint window per monitor.
44 X11,
55- /// Wayland — position is compositor-managed; wlr-layer-shell for secondary monitors.
55+ // Wayland — position is compositor-managed; wlr-layer-shell for secondary monitors.
66 Wayland,
77- /// Windows — explicit position+size works; one Slint window per monitor.
77+ // Windows — explicit position+size works; one Slint window per monitor.
88 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
99 Windows,
1010}
11111212-/// Detect the current display session type.
1212+// Detect the current display session type.
1313pub fn detect() -> SessionType {
1414 detect_from_env(|k| std::env::var(k))
1515}
···2727 #[cfg(not(target_os = "windows"))]
2828 {
2929 // WAYLAND_DISPLAY set → native Wayland session.
3030- if var("WAYLAND_DISPLAY").map(|v| !v.is_empty()).unwrap_or(false) {
3030+ if var("WAYLAND_DISPLAY")
3131+ .map(|v| !v.is_empty())
3232+ .unwrap_or(false)
3333+ {
3134 return SessionType::Wayland;
3235 }
3336 SessionType::X11
···3841mod tests {
3942 use super::*;
40434141- fn mock_env<'a>(wayland: Option<&'a str>, _display: Option<&'a str>) -> impl for<'k> Fn(&'k str) -> Result<String, std::env::VarError> + 'a {
4444+ fn mock_env<'a>(
4545+ wayland: Option<&'a str>,
4646+ _display: Option<&'a str>,
4747+ ) -> impl for<'k> Fn(&'k str) -> Result<String, std::env::VarError> + 'a {
4248 move |key| match key {
4349 "WAYLAND_DISPLAY" => wayland
4450 .map(|v| Ok(v.to_string()))
+14-11
src/settings/mod.rs
···11use std::rc::Rc;
22use std::sync::{Arc, Mutex};
3344-use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel};
54use fontdue::Font;
55+use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel};
6677use crate::autostart;
88use crate::config::{AppConfig, BreakLevelConfig, BreakModeConfig, LongBreakConfig, OverlayTheme};
···58585959 // Do NOT call hide() — window is created lazily right before show(),
6060 // so hiding would fight against the immediate show() that follows.
6161- Ok(Self { window, levels_model })
6161+ Ok(Self {
6262+ window,
6363+ levels_model,
6464+ })
6265 }
63666467 pub fn window(&self) -> &SettingsWindow {
···6871 pub fn levels_model(&self) -> Rc<VecModel<LevelEntry>> {
6972 Rc::clone(&self.levels_model)
7073 }
7171-7274}
73757474-/// Reads the settings window state back into `cfg`. Also usable from callbacks
7575-/// that capture `SettingsWindow` and `VecModel<LevelEntry>` directly.
7676+// Reads the settings window state back into `cfg`. Also usable from callbacks
7777+// that capture `SettingsWindow` and `VecModel<LevelEntry>` directly.
7678pub fn read_settings_into(
7779 window: &SettingsWindow,
7880 levels_model: &VecModel<LevelEntry>,
···150152 // Measure profile name widths (pixels) using bundled Nunito Regular at 12px
151153 fn measure_name_widths(names: &[SharedString]) -> Vec<i32> {
152154 // include_bytes with concat! so path is resolved from manifest dir at compile time
153153- let font_bytes = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/fonts/Nunito-Regular.ttf"));
155155+ let font_bytes = include_bytes!(concat!(
156156+ env!("CARGO_MANIFEST_DIR"),
157157+ "/assets/fonts/Nunito-Regular.ttf"
158158+ ));
154159 let font = Font::from_bytes(font_bytes.as_ref(), fontdue::FontSettings::default()).unwrap();
155160 // measure at an accessible base size (16px)
156161 let font_size = 16.0;
···193198) {
194199 let prof = cfg.profiles.values().find(|p| p.name == profile_name);
195200196196- window.set_enforced_mode(prof.map_or(false, |p| p.mode == BreakModeConfig::Enforced));
197197- window.set_idle_detection_enabled(prof.map_or(true, |p| p.idle_detection_enabled));
198198- window.set_idle_threshold_mins(
199199- prof.map_or(5, |p| (p.idle_threshold_secs / 60).max(1) as i32),
200200- );
201201+ window.set_enforced_mode(prof.is_some_and(|p| p.mode == BreakModeConfig::Enforced));
202202+ window.set_idle_detection_enabled(prof.is_none_or(|p| p.idle_detection_enabled));
203203+ window.set_idle_threshold_mins(prof.map_or(5, |p| (p.idle_threshold_secs / 60).max(1) as i32));
201204202205 let entries: Vec<LevelEntry> = prof
203206 .map(|p| {
+3-4
src/sound.rs
···2233use rodio::{OutputStream, Sink, Source};
4455-/// Play a short two-tone chime at the given volume (0.0–1.0).
66-/// Spawns a background thread so it never blocks the UI.
55+// Play a short two-tone chime at the given volume (0.0–1.0).
66+// Spawns a background thread so it never blocks the UI.
77pub fn play_chime(volume: f32) {
88 std::thread::Builder::new()
99 .name("ioma-chime".into())
···3535}
36363737fn silence(millis: u64) -> impl Source<Item = f32> + Send {
3838- rodio::source::Zero::<f32>::new(1, 44100)
3939- .take_duration(Duration::from_millis(millis))
3838+ rodio::source::Zero::<f32>::new(1, 44100).take_duration(Duration::from_millis(millis))
4039}
+8-6
src/system.rs
···11use std::sync::Arc;
22use std::sync::atomic::{AtomicBool, Ordering};
3344-/// Starts a background thread that polls the OS dark-mode preference every 5s.
55-/// Returns an Arc<AtomicBool> that the main thread can read at any time.
44+// Starts a background thread that polls the OS dark-mode preference every 5s.
55+// Returns an Arc<AtomicBool> that the main thread can read at any time.
66pub fn start_watcher() -> Arc<AtomicBool> {
77 let dark = Arc::new(AtomicBool::new(query()));
88 let bg = Arc::clone(&dark);
99 std::thread::Builder::new()
1010 .name("system-theme".into())
1111- .spawn(move || loop {
1212- std::thread::sleep(std::time::Duration::from_secs(5));
1313- bg.store(query(), Ordering::Relaxed);
1111+ .spawn(move || {
1212+ loop {
1313+ std::thread::sleep(std::time::Duration::from_secs(5));
1414+ bg.store(query(), Ordering::Relaxed);
1515+ }
1416 })
1517 .ok();
1618 dark
···5355 pub fn is_dark() -> bool {
5456 use windows::{
5557 Win32::Foundation::HKEY_CURRENT_USER,
5656- Win32::System::Registry::{RegGetValueW, RRF_RT_REG_DWORD},
5858+ Win32::System::Registry::{RRF_RT_REG_DWORD, RegGetValueW},
5759 };
5860 let mut val: u32 = 1;
5961 let mut sz = std::mem::size_of::<u32>() as u32;
+24-27
src/timer/mod.rs
···13131414const TICK_INTERVAL: Duration = Duration::from_secs(1);
15151616-/// Commands sent to the timer task from the UI/tray.
1616+// Commands sent to the timer task from the UI/tray.
1717#[derive(Debug)]
1818pub enum TimerCommand {
1919- /// Pause the timer for the given duration.
1919+ // Pause the timer for the given duration.
2020 PauseFor(Duration),
2121- /// Resume from manual pause early.
2121+ // Resume from manual pause early.
2222 Resume,
2323- /// Advance to the next scheduled break immediately.
2323+ // Advance to the next scheduled break immediately.
2424 SkipToNextBreak,
2525- /// Called when a break overlay is dismissed (break finished or snoozed).
2525+ // Called when a break overlay is dismissed (break finished or snoozed).
2626 BreakEnded { snoozed: bool },
2727- /// Called when idle is detected.
2727+ // Called when idle is detected.
2828 IdleDetected { duration: Duration },
2929- /// Reload profile (after settings change).
2929+ // Reload profile (after settings change).
3030 SetProfile(Profile),
3131- /// Quit.
3131+ // Quit.
3232 Shutdown,
3333}
34343535-/// Events sent from the timer task to the UI.
3535+// Events sent from the timer task to the UI.
3636#[derive(Debug, Clone)]
3737pub enum TimerEvent {
3838- /// A break is starting; show the overlay.
3838+ // A break is starting; show the overlay.
3939 BreakStarting(ScheduledBreak),
4040- /// Snooze period starting; overlay hides temporarily.
4040+ // Snooze period starting; overlay hides temporarily.
4141 SnoozePeriodStarting { remaining_secs: u64 },
4242- /// Work phase: tick with current countdown to next break.
4242+ // Work phase: tick with current countdown to next break.
4343 WorkTick { secs_until_break: u64 },
4444- /// Timer is paused (manual or idle).
4444+ // Timer is paused (manual or idle).
4545 Paused,
4646- /// Timer resumed.
4646+ // Timer resumed.
4747 Resumed,
4848}
49495050#[derive(Debug, Clone)]
5151enum TimerState {
5252 Working,
5353- /// `fired_index` is the scheduler level that triggered this break, so
5454- /// record_break_completed knows which counter to reset.
5353+ // `fired_index` is the scheduler level that triggered this break, so
5454+ // record_break_completed knows which counter to reset.
5555 Breaking {
5656 snooze_used: bool,
5757 is_long: bool,
···6666 IdlePaused,
6767}
68686969-/// Manual PartialEq — Instant does not implement PartialEq.
6969+// Manual PartialEq — Instant does not implement PartialEq.
7070impl PartialEq for TimerState {
7171 fn eq(&self, other: &Self) -> bool {
7272 match (self, other) {
···227227 let _ = self.event_tx.send(TimerEvent::SnoozePeriodStarting {
228228 remaining_secs: snooze_dur.as_secs(),
229229 });
230230- } else {
231231- let (was_long, fired_index) = match self.state {
232232- TimerState::Breaking {
233233- is_long,
234234- fired_index,
235235- ..
236236- } => (is_long, fired_index),
237237- _ => (false, 0),
238238- };
239239- self.scheduler.record_break_completed(fired_index, was_long);
230230+ } else if let TimerState::Breaking {
231231+ is_long,
232232+ fired_index,
233233+ ..
234234+ } = self.state
235235+ {
236236+ self.scheduler.record_break_completed(fired_index, is_long);
240237 self.state = TimerState::Working;
241238 let _ = self.event_tx.send(TimerEvent::Resumed);
242239 }
+18-14
src/timer/profile.rs
···17171818#[derive(Debug, Clone)]
1919pub struct LongBreakTrigger {
2020- /// Number of top-level work cycles before a long break.
2020+ // Number of top-level work cycles before a long break.
2121 pub after_cycles: u32,
2222- /// If idle gap exceeds this, cycle counter resets.
2222+ // If idle gap exceeds this, cycle counter resets.
2323 pub max_cycle_gap: Duration,
2424 pub break_duration: Duration,
2525 pub label: String,
···3232 pub snooze_duration: Duration,
3333 pub idle_threshold: Duration,
3434 pub idle_detection_enabled: bool,
3535- /// Sorted ascending by work_duration. Scheduler fires the highest-priority
3636- /// (longest interval) level whose work elapsed time is due.
3535+ // Sorted ascending by work_duration. Scheduler fires the highest-priority
3636+ // (longest interval) level whose work elapsed time is due.
3737 pub levels: Vec<BreakLevel>,
3838 pub long_break: Option<LongBreakTrigger>,
3939}
40404141impl Profile {
4242- pub fn from_config(id: &str, cfg: &ProfileConfig) -> Self {
4242+ pub fn from_config(cfg: &ProfileConfig) -> Self {
4343 let mut levels: Vec<BreakLevel> = cfg
4444 .levels
4545 .iter()
···5252 // Ensure ascending order so scheduler can use index priority correctly.
5353 levels.sort_by_key(|l| l.work_duration);
54545555- let long_break = cfg.long_break.as_ref().map(|lb: &LongBreakConfig| LongBreakTrigger {
5656- after_cycles: lb.after_cycles,
5757- max_cycle_gap: Duration::from_secs(lb.max_cycle_gap_secs),
5858- break_duration: Duration::from_secs(lb.break_secs),
5959- label: lb.label.clone(),
6060- });
5555+ let long_break = cfg
5656+ .long_break
5757+ .as_ref()
5858+ .map(|lb: &LongBreakConfig| LongBreakTrigger {
5959+ after_cycles: lb.after_cycles,
6060+ max_cycle_gap: Duration::from_secs(lb.max_cycle_gap_secs),
6161+ break_duration: Duration::from_secs(lb.break_secs),
6262+ label: lb.label.clone(),
6363+ });
61646262- let _ = id;
6365 Self {
6466 name: cfg.name.clone(),
6567 mode: match cfg.mode {
···7476 }
7577 }
76787777- /// The primary (top-level / longest) break level — used for cycle counting.
7979+ // The primary (top-level / longest) break level — used for cycle counting.
7880 pub fn primary_level(&self) -> &BreakLevel {
7979- self.levels.last().expect("profile must have at least one level")
8181+ self.levels
8282+ .last()
8383+ .expect("profile must have at least one level")
8084 }
8185}
+5-5
src/tray.rs
···7575 })
7676 }
77777878- /// Set icon and menu state to reflect paused state. No-op if state unchanged.
7878+ // Set icon and menu state to reflect paused state. No-op if state unchanged.
7979 pub fn set_paused(&self, paused: bool) {
8080 if self.last_paused.get() == Some(paused) {
8181 return;
···8686 } else {
8787 self.icon_active.clone()
8888 }));
8989- let _ = self.item_resume.set_enabled(paused);
8989+ self.item_resume.set_enabled(paused);
9090 }
91919292- /// Update tray tooltip. No-op if text unchanged.
9292+ // Update tray tooltip. No-op if text unchanged.
9393 pub fn set_tooltip(&self, text: &str) {
9494 if *self.last_tooltip.borrow() == text {
9595 return;
···9898 let _ = self._tray.set_tooltip(Some(text));
9999 }
100100101101- /// Process pending tray/menu events. Call from the main event loop.
102102- /// Returns `true` if a quit was requested.
101101+ // Process pending tray/menu events. Call from the main event loop.
102102+ // Returns `true` if a quit was requested.
103103 pub fn process_events(
104104 &self,
105105 cmd_tx: &tokio::sync::mpsc::UnboundedSender<TimerCommand>,