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.

chore: remove stale code and unused imports, fix strict-lint issues

+356 -237
+5 -6
src/app.rs
··· 6 6 use crate::idle; 7 7 use crate::timer::{Profile, TimerCommand, TimerEvent, TimerTask}; 8 8 9 - /// Shared app state accessed from multiple threads. 9 + // Shared app state accessed from multiple threads. 10 10 pub struct AppState { 11 11 pub cfg: Arc<Mutex<AppConfig>>, 12 12 pub cmd_tx: mpsc::UnboundedSender<TimerCommand>, ··· 33 33 } 34 34 35 35 pub fn active_profile(cfg: &AppConfig) -> Profile { 36 - let id = &cfg.app.active_profile; 37 36 let prof_cfg = cfg 38 37 .profiles 39 - .get(id) 38 + .get(&cfg.app.active_profile) 40 39 .or_else(|| cfg.profiles.values().next()) 41 40 .expect("config must have at least one profile"); 42 - Profile::from_config(id, prof_cfg) 41 + Profile::from_config(prof_cfg) 43 42 } 44 43 45 - /// Spawns the idle detection polling loop on a plain OS thread (not tokio) 46 - /// so blocking DBus/Win32 calls work without nesting runtimes. 44 + // Spawns the idle detection polling loop on a plain OS thread (not tokio) 45 + // so blocking DBus/Win32 calls work without nesting runtimes. 47 46 pub fn spawn_idle_poller( 48 47 cmd_tx: mpsc::UnboundedSender<TimerCommand>, 49 48 threshold: Duration,
+5 -1
src/autostart.rs
··· 3 3 4 4 fn launcher() -> Result<AutoLaunch> { 5 5 let exe = std::env::current_exe()?; 6 - Ok(AutoLaunch::new("ioma", exe.to_str().unwrap_or("ioma"), &[] as &[&str])) 6 + Ok(AutoLaunch::new( 7 + "ioma", 8 + exe.to_str().unwrap_or("ioma"), 9 + &[] as &[&str], 10 + )) 7 11 } 8 12 9 13 pub fn set_enabled(enabled: bool) -> Result<()> {
+3 -4
src/config/mod.rs
··· 28 28 } 29 29 let text = std::fs::read_to_string(&path) 30 30 .with_context(|| format!("reading config from {}", path.display()))?; 31 - let cfg: AppConfig = toml::from_str(&text) 32 - .with_context(|| format!("parsing config from {}", path.display()))?; 31 + let cfg: AppConfig = 32 + toml::from_str(&text).with_context(|| format!("parsing config from {}", path.display()))?; 33 33 Ok(cfg) 34 34 } 35 35 ··· 40 40 .with_context(|| format!("creating config dir {}", parent.display()))?; 41 41 } 42 42 let text = toml::to_string_pretty(cfg).context("serializing config")?; 43 - std::fs::write(&path, text) 44 - .with_context(|| format!("writing config to {}", path.display()))?; 43 + std::fs::write(&path, text).with_context(|| format!("writing config to {}", path.display()))?; 45 44 Ok(()) 46 45 }
+9 -17
src/config/types.rs
··· 27 27 pub text_size_mode: u32, 28 28 } 29 29 30 - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 30 + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] 31 31 #[serde(rename_all = "lowercase")] 32 32 pub enum OverlayTheme { 33 33 Dark, 34 34 Light, 35 + #[default] 35 36 System, 36 37 } 37 38 38 - #[derive(Debug, Clone, Serialize, Deserialize)] 39 + #[derive(Debug, Clone, Serialize, Deserialize, Default)] 39 40 #[serde(default)] 40 41 pub struct EnforcedConfig { 41 - /// Argon2 hash of the emergency unlock password. Empty = not set. 42 + // Argon2 hash of the emergency unlock password. Empty = not set. 42 43 pub password_hash: String, 43 44 } 44 45 45 - /// Serializable form of a break profile (stored in config file). 46 + // Serializable form of a break profile (stored in config file). 46 47 #[derive(Debug, Clone, Serialize, Deserialize)] 47 48 pub struct ProfileConfig { 48 49 pub name: String, ··· 83 84 let mut profiles = HashMap::new(); 84 85 profiles.insert("pomodoro".to_string(), ProfileConfig::pomodoro()); 85 86 profiles.insert("52_17".to_string(), ProfileConfig::fifty_two_seventeen()); 86 - profiles.insert("20_20_20".to_string(), ProfileConfig::twenty_twenty_twenty()); 87 + profiles.insert( 88 + "20_20_20".to_string(), 89 + ProfileConfig::twenty_twenty_twenty(), 90 + ); 87 91 profiles.insert("custom".to_string(), ProfileConfig::custom_hierarchical()); 88 92 89 93 Self { ··· 113 117 font_size: 48, 114 118 text_size_mode: 0, 115 119 } 116 - } 117 - } 118 - 119 - impl Default for OverlayTheme { 120 - fn default() -> Self { 121 - OverlayTheme::System 122 - } 123 - } 124 - 125 - impl Default for EnforcedConfig { 126 - fn default() -> Self { 127 - Self { password_hash: String::new() } 128 120 } 129 121 } 130 122
+3 -1
src/idle/linux.rs
··· 10 10 11 11 impl LinuxIdleDetector { 12 12 pub fn new() -> Self { 13 - Self { conn: Self::connect() } 13 + Self { 14 + conn: Self::connect(), 15 + } 14 16 } 15 17 16 18 fn connect() -> Option<Connection> {
+2 -2
src/idle/mod.rs
··· 1 - use std::time::Duration; 2 1 use anyhow::Result; 2 + use std::time::Duration; 3 3 4 4 pub trait IdleDetector: Send { 5 5 /// Returns how long the user has been idle, or None if detection is unavailable. ··· 26 26 } 27 27 } 28 28 29 - /// Fallback for unsupported platforms. 29 + // Fallback for unsupported platforms. 30 30 #[cfg(not(any(target_os = "linux", target_os = "windows")))] 31 31 pub struct NullIdleDetector; 32 32 #[cfg(not(any(target_os = "linux", target_os = "windows")))]
+1 -1
src/idle/windows.rs
··· 1 1 use anyhow::Result; 2 2 use std::time::Duration; 3 - use windows::Win32::UI::Input::KeyboardAndMouse::{GetLastInputInfo, LASTINPUTINFO}; 4 3 use windows::Win32::Foundation::GetLastError; 4 + use windows::Win32::UI::Input::KeyboardAndMouse::{GetLastInputInfo, LASTINPUTINFO}; 5 5 6 6 use super::IdleDetector; 7 7
+30 -13
src/main.rs
··· 21 21 gtk::init().expect("GTK init failed"); 22 22 } 23 23 24 - use app::{active_profile, spawn_idle_poller, AppState}; 24 + use app::{AppState, active_profile, spawn_idle_poller}; 25 25 use config::AppConfig; 26 26 use generated::PasswordSetupWindow; 27 27 use overlay::OverlayManager; 28 - use settings::{read_settings_into, SettingsManager}; 28 + use settings::{SettingsManager, read_settings_into}; 29 29 use timer::{BreakMode, TimerCommand, TimerEvent}; 30 30 use tray::AppTray; 31 31 ··· 197 197 let pw_submit = pw.clone_strong(); 198 198 let cfg_arc_submit = cfg_arc_pw.clone(); 199 199 pw.on_submit_clicked(move |password| { 200 - match overlay::password::hash_password(password.as_str()) { 200 + match overlay::password::hash_password( 201 + password.as_str(), 202 + ) { 201 203 Ok(hash) => { 202 - cfg_arc_submit.lock().unwrap().enforced.password_hash = hash.clone(); 203 - let snap = cfg_arc_submit.lock().unwrap().clone(); 204 - config::save(&snap).unwrap_or_else(|e| log::warn!("save failed: {e}")); 204 + cfg_arc_submit 205 + .lock() 206 + .unwrap() 207 + .enforced 208 + .password_hash = hash.clone(); 209 + let snap = cfg_arc_submit 210 + .lock() 211 + .unwrap() 212 + .clone(); 213 + config::save(&snap).unwrap_or_else( 214 + |e| log::warn!("save failed: {e}"), 215 + ); 216 + } 217 + Err(e) => { 218 + log::warn!("hash_password failed: {e}") 205 219 } 206 - Err(e) => log::warn!("hash_password failed: {e}"), 207 220 } 208 221 pw_submit.hide().unwrap_or_default(); 209 222 }); 210 223 pw.show().unwrap_or_default(); 211 224 } 212 - Err(e) => log::warn!("Failed to create password window: {e}"), 225 + Err(e) => { 226 + log::warn!("Failed to create password window: {e}") 227 + } 213 228 } 214 229 }); 215 230 } ··· 219 234 // show(). On Linux/X11 the WM may not honour with_inner_size 220 235 // during initial window creation; an explicit set_size after 221 236 // mapping ensures the layout is computed at the right height. 222 - mgr.window().window().set_size( 223 - slint::LogicalSize::new(760.0, 680.0), 224 - ); 237 + mgr.window() 238 + .window() 239 + .set_size(slint::LogicalSize::new(760.0, 680.0)); 225 240 settings_mgr = Some(mgr); 226 241 } 227 242 Err(e) => log::warn!("Failed to create settings window: {e}"), ··· 262 277 } 263 278 264 279 if let Some(ref mgr) = overlay { 265 - let is_enforced = active_profile(&cfg_arc.lock().unwrap()).mode == BreakMode::Enforced; 280 + let is_enforced = active_profile(&cfg_arc.lock().unwrap()).mode 281 + == BreakMode::Enforced; 266 282 mgr.show_break(&sched, is_enforced, snooze_used); 267 283 } 268 284 } ··· 348 364 if current != known_monitors { 349 365 log::info!( 350 366 "overlay: monitor config changed ({} → {} monitor(s)), queuing rebuild", 351 - known_monitors.len(), current.len() 367 + known_monitors.len(), 368 + current.len() 352 369 ); 353 370 known_monitors = current; 354 371 overlay_needs_rebuild = true;
+58 -48
src/overlay/layer_shell.rs
··· 6 6 use std::time::Duration; 7 7 8 8 use wayland_client::{ 9 - delegate_noop, 10 - protocol::{ 11 - wl_buffer, wl_compositor, wl_output, wl_registry, wl_shm, wl_shm_pool, wl_surface, 12 - }, 13 - Connection, Dispatch, EventQueue, QueueHandle, 9 + Connection, Dispatch, EventQueue, QueueHandle, delegate_noop, 10 + protocol::{wl_buffer, wl_compositor, wl_output, wl_registry, wl_shm, wl_shm_pool, wl_surface}, 14 11 }; 15 12 use wayland_protocols_wlr::layer_shell::v1::client::{ 16 13 zwlr_layer_shell_v1::{self, ZwlrLayerShellV1}, ··· 19 16 20 17 // ── Public API ─────────────────────────────────────────────────────────────── 21 18 22 - /// Covers all non-primary Wayland outputs with an opaque barrier surface using 23 - /// wlr-layer-shell. Returns `None` if the compositor doesn't support the protocol. 19 + // Covers all non-primary Wayland outputs with an opaque barrier surface using 20 + // wlr-layer-shell. Returns `None` if the compositor doesn't support the protocol. 24 21 pub struct LayerShellBarrier { 25 22 cmd_tx: mpsc::SyncSender<Cmd>, 26 23 _thread: JoinHandle<()>, 27 24 } 28 25 29 26 impl LayerShellBarrier { 30 - /// Try to create a barrier. `primary_x` / `primary_y` are the compositor 31 - /// coordinates of the primary output so we can skip it (the Slint overlay 32 - /// covers it instead). 27 + // Try to create a barrier. `primary_x` / `primary_y` are the compositor 28 + // coordinates of the primary output so we can skip it (the Slint overlay 29 + // covers it instead). 33 30 pub fn try_new(primary_x: i32, primary_y: i32) -> Option<Self> { 34 31 match Self::init(primary_x, primary_y) { 35 32 Ok(Some(b)) => Some(b), 36 33 Ok(None) => { 37 - log::info!("wlr-layer-shell not available; secondary monitors uncovered on Wayland"); 34 + log::info!( 35 + "wlr-layer-shell not available; secondary monitors uncovered on Wayland" 36 + ); 38 37 None 39 38 } 40 39 Err(e) => { ··· 163 162 match create_shm_buffer(&shm, entry.width, entry.height, is_dark, qh) { 164 163 Ok(buf) => { 165 164 entry.surface.attach(Some(&buf), 0, 0); 166 - entry.surface.damage_buffer( 167 - 0, 168 - 0, 169 - entry.width as i32, 170 - entry.height as i32, 171 - ); 165 + entry 166 + .surface 167 + .damage_buffer(0, 0, entry.width as i32, entry.height as i32); 172 168 entry.surface.commit(); 173 169 entry.buffer = Some(buf); 174 170 entry.needs_render = false; ··· 198 194 _: &Connection, 199 195 qh: &QueueHandle<Self>, 200 196 ) { 201 - if let wl_registry::Event::Global { name, interface, .. } = event { 197 + if let wl_registry::Event::Global { 198 + name, interface, .. 199 + } = event 200 + { 202 201 match &interface[..] { 203 202 "wl_compositor" => { 204 203 state.compositor = 205 204 Some(registry.bind::<wl_compositor::WlCompositor, _, _>(name, 4, qh, ())); 206 205 } 207 206 "wl_shm" => { 208 - state.shm = 209 - Some(registry.bind::<wl_shm::WlShm, _, _>(name, 1, qh, ())); 207 + state.shm = Some(registry.bind::<wl_shm::WlShm, _, _>(name, 1, qh, ())); 210 208 } 211 209 "zwlr_layer_shell_v1" => { 212 210 state.layer_shell = ··· 214 212 } 215 213 "wl_output" => { 216 214 let idx = state.outputs.len(); 217 - let output = 218 - registry.bind::<wl_output::WlOutput, _, _>(name, 2, qh, idx); 219 - state.outputs.push(OutputInfo { wl_output: output, x: 0, y: 0 }); 215 + let output = registry.bind::<wl_output::WlOutput, _, _>(name, 2, qh, idx); 216 + state.outputs.push(OutputInfo { 217 + wl_output: output, 218 + x: 0, 219 + y: 0, 220 + }); 220 221 } 221 222 _ => {} 222 223 } ··· 233 234 _: &Connection, 234 235 _: &QueueHandle<Self>, 235 236 ) { 236 - if let wl_output::Event::Geometry { x, y, .. } = event { 237 - if let Some(output) = state.outputs.get_mut(*data) { 238 - output.x = x; 239 - output.y = y; 240 - } 237 + if let wl_output::Event::Geometry { x, y, .. } = event 238 + && let Some(output) = state.outputs.get_mut(*data) 239 + { 240 + output.x = x; 241 + output.y = y; 241 242 } 242 243 } 243 244 } ··· 265 266 _: &QueueHandle<Self>, 266 267 ) { 267 268 match event { 268 - zwlr_layer_surface_v1::Event::Configure { serial, width, height } => { 269 + zwlr_layer_surface_v1::Event::Configure { 270 + serial, 271 + width, 272 + height, 273 + } => { 269 274 layer_surface.ack_configure(serial); 270 - if let Some(entry) = 271 - state.surfaces.iter_mut().find(|e| e.output_idx == *data) 272 - { 275 + if let Some(entry) = state.surfaces.iter_mut().find(|e| e.output_idx == *data) { 273 276 entry.width = width; 274 277 entry.height = height; 275 278 entry.needs_render = true; ··· 354 357 .spawn(move || run_thread(state, event_queue, cmd_rx)) 355 358 .map_err(|e| anyhow::anyhow!("Failed to spawn layer-shell thread: {e}"))?; 356 359 357 - Ok(Some(Self { cmd_tx, _thread: thread })) 360 + Ok(Some(Self { 361 + cmd_tx, 362 + _thread: thread, 363 + })) 358 364 } 359 365 } 360 366 ··· 368 374 qh: &QueueHandle<BarrierState>, 369 375 ) -> anyhow::Result<wl_buffer::WlBuffer> { 370 376 // ARGB8888: dark = almost-black navy, light = light grey 371 - let color: u32 = if is_dark { 0xFF_1A_1A_2E } else { 0xFF_E8_E8_E8 }; 377 + let color: u32 = if is_dark { 378 + 0xFF_1A_1A_2E 379 + } else { 380 + 0xFF_E8_E8_E8 381 + }; 372 382 let color_bytes = color.to_ne_bytes(); 373 383 let stride = width * 4; 374 384 let size = (stride * height) as usize; ··· 399 409 400 410 fn make_tmpfile() -> anyhow::Result<File> { 401 411 use std::os::unix::fs::OpenOptionsExt; 402 - let path = std::env::temp_dir() 403 - .join(format!("ioma-shm-{}.raw", std::process::id())); 412 + let path = std::env::temp_dir().join(format!("ioma-shm-{}.raw", std::process::id())); 404 413 let file = std::fs::OpenOptions::new() 405 414 .read(true) 406 415 .write(true) 407 416 .create(true) 417 + .truncate(true) 408 418 .mode(0o600) 409 419 .open(&path)?; 410 420 let _ = std::fs::remove_file(&path); ··· 474 484 475 485 // ── Manual test matrix (run with `cargo test -- --ignored`) ───────────── 476 486 477 - /// Dual-monitor X11: trigger a break and confirm both monitors are covered 478 - /// by full-screen Slint overlay windows. 487 + // Dual-monitor X11: trigger a break and confirm both monitors are covered 488 + // by full-screen Slint overlay windows. 479 489 #[test] 480 490 #[ignore] 481 491 fn manual_x11_dual_monitor_both_covered() {} 482 492 483 - /// Dual-monitor Wayland (wlroots compositor): trigger a break and confirm 484 - /// primary monitor shows the Slint overlay and secondary shows the dark 485 - /// layer-shell barrier. Verify the secondary barrier cannot be dismissed. 493 + // Dual-monitor Wayland (wlroots compositor): trigger a break and confirm 494 + // primary monitor shows the Slint overlay and secondary shows the dark 495 + // layer-shell barrier. Verify the secondary barrier cannot be dismissed. 486 496 #[test] 487 497 #[ignore] 488 498 fn manual_wayland_dual_monitor_barrier_on_secondary() {} 489 499 490 - /// Wayland GNOME session: trigger a break; log should contain 491 - /// "wlr-layer-shell not available". Primary monitor should still be covered. 500 + // Wayland GNOME session: trigger a break; log should contain 501 + // "wlr-layer-shell not available". Primary monitor should still be covered. 492 502 #[test] 493 503 #[ignore] 494 504 fn manual_wayland_gnome_fallback_primary_only() {} 495 505 496 - /// Single-monitor setup (any session): overlay appears correctly and 497 - /// countdown timer runs to zero before auto-dismissing. 506 + // Single-monitor setup (any session): overlay appears correctly and 507 + // countdown timer runs to zero before auto-dismissing. 498 508 #[test] 499 509 #[ignore] 500 510 fn manual_single_monitor_overlay_and_countdown() {} 501 511 502 - /// Enforced mode + password: overlay appears on all monitors, Alt+F4 is 503 - /// blocked on every window, password field auto-focuses when unlock is 504 - /// clicked, Enter key submits the password. 512 + // Enforced mode + password: overlay appears on all monitors, Alt+F4 is 513 + // blocked on every window, password field auto-focuses when unlock is 514 + // clicked, Enter key submits the password. 505 515 #[test] 506 516 #[ignore] 507 517 fn manual_enforced_mode_password_all_monitors() {}
+10 -4
src/overlay/mod.rs
··· 8 8 use std::time::Duration; 9 9 10 10 use crate::config::AppConfig; 11 - use crate::timer::{ScheduledBreak, TimerCommand}; 12 11 use crate::overlay::session::SessionType; 12 + use crate::timer::{ScheduledBreak, TimerCommand}; 13 13 14 14 #[cfg(target_os = "linux")] 15 15 use layer_shell::LayerShellBarrier; ··· 45 45 "overlay: session={:?}, {} monitor(s): {}", 46 46 session, 47 47 all_monitors.len(), 48 - all_monitors.iter().map(|m| format!("{}x{}@({},{})", m.width, m.height, m.x, m.y)) 49 - .collect::<Vec<_>>().join(", ") 48 + all_monitors 49 + .iter() 50 + .map(|m| format!("{}x{}@({},{})", m.width, m.height, m.x, m.y)) 51 + .collect::<Vec<_>>() 52 + .join(", ") 50 53 ); 51 54 52 55 #[cfg(target_os = "linux")] ··· 59 62 }; 60 63 61 64 let backend = Box::new(multi_slint::MultiSlintBackend::new( 62 - is_dark, &all_monitors, cmd_tx, cfg_arc, 65 + is_dark, 66 + &all_monitors, 67 + cmd_tx, 68 + cfg_arc, 63 69 )?); 64 70 65 71 Ok(Self {
+44 -18
src/overlay/monitors.rs
··· 1 - /// Physical position and size of one monitor, in logical pixels. 1 + // Physical position and size of one monitor, in logical pixels. 2 2 #[derive(Debug, Clone, PartialEq)] 3 3 pub struct MonitorInfo { 4 4 pub x: i32, ··· 10 10 pub name: String, 11 11 } 12 12 13 - /// Returns all connected monitors. Falls back to a single zeroed entry so 14 - /// callers always get at least one monitor even if enumeration fails. 13 + // Returns all connected monitors. Falls back to a single zeroed entry so 14 + // callers always get at least one monitor even if enumeration fails. 15 15 pub fn enumerate() -> Vec<MonitorInfo> { 16 16 match enumerate_impl() { 17 17 Ok(v) if !v.is_empty() => v, ··· 30 30 // On Wayland, use native wl_output enumeration — display_info only does one 31 31 // roundtrip and hardcodes is_primary=false for all outputs. 32 32 #[cfg(target_os = "linux")] 33 - if std::env::var("WAYLAND_DISPLAY").map(|v| !v.is_empty()).unwrap_or(false) { 33 + if std::env::var("WAYLAND_DISPLAY") 34 + .map(|v| !v.is_empty()) 35 + .unwrap_or(false) 36 + { 34 37 return enumerate_wayland(); 35 38 } 36 39 ··· 50 53 .collect()) 51 54 } 52 55 53 - /// Enumerate Wayland outputs using wl_output events directly. 54 - /// Two roundtrips ensure all Geometry and Mode events are received. 56 + // Enumerate Wayland outputs using wl_output events directly. 57 + // Two roundtrips ensure all Geometry and Mode events are received. 55 58 #[cfg(target_os = "linux")] 56 59 fn enumerate_wayland() -> anyhow::Result<Vec<MonitorInfo>> { 57 60 use wayland_client::{ 61 + Connection, Dispatch, QueueHandle, 58 62 protocol::{wl_output, wl_registry}, 59 - Connection, Dispatch, QueueHandle, 60 63 }; 61 64 62 65 struct RawOutput { ··· 79 82 _: &Connection, 80 83 qh: &QueueHandle<Self>, 81 84 ) { 82 - if let wl_registry::Event::Global { name, interface, .. } = event { 83 - if interface == "wl_output" { 84 - let idx = state.outputs.len(); 85 - registry.bind::<wl_output::WlOutput, _, _>(name, 2, qh, idx); 86 - state.outputs.push(RawOutput { x: 0, y: 0, width: 0, height: 0 }); 87 - } 85 + if let wl_registry::Event::Global { 86 + name, interface, .. 87 + } = event 88 + && interface == "wl_output" 89 + { 90 + let idx = state.outputs.len(); 91 + registry.bind::<wl_output::WlOutput, _, _>(name, 2, qh, idx); 92 + state.outputs.push(RawOutput { 93 + x: 0, 94 + y: 0, 95 + width: 0, 96 + height: 0, 97 + }); 88 98 } 89 99 } 90 100 } ··· 98 108 _: &Connection, 99 109 _: &QueueHandle<Self>, 100 110 ) { 101 - let Some(out) = state.outputs.get_mut(*data) else { return }; 111 + let Some(out) = state.outputs.get_mut(*data) else { 112 + return; 113 + }; 102 114 match event { 103 115 wl_output::Event::Geometry { x, y, .. } => { 104 116 out.x = x; 105 117 out.y = y; 106 118 } 107 - wl_output::Event::Mode { flags, width, height, .. } => { 119 + wl_output::Event::Mode { 120 + flags, 121 + width, 122 + height, 123 + .. 124 + } => { 108 125 // Accept if Current flag set, or if we haven't seen a mode yet. 109 126 let is_current = flags 110 127 .into_result() ··· 125 142 let qh = eq.handle(); 126 143 conn.display().get_registry(&qh, ()); 127 144 128 - let mut state = EnumState { outputs: Vec::new() }; 145 + let mut state = EnumState { 146 + outputs: Vec::new(), 147 + }; 129 148 // First roundtrip: bind globals and wl_output objects. 130 149 eq.roundtrip(&mut state)?; 131 150 // Second roundtrip: receive Geometry and Mode events for all outputs. 132 151 eq.roundtrip(&mut state)?; 133 152 134 - log::info!("Wayland wl_output enumeration found {} output(s)", state.outputs.len()); 153 + log::info!( 154 + "Wayland wl_output enumeration found {} output(s)", 155 + state.outputs.len() 156 + ); 135 157 136 158 let mut monitors: Vec<MonitorInfo> = state 137 159 .outputs ··· 162 184 for m in &monitors { 163 185 log::info!( 164 186 " wl_output: {}x{} at ({},{}), primary={}", 165 - m.width, m.height, m.x, m.y, m.is_primary 187 + m.width, 188 + m.height, 189 + m.x, 190 + m.y, 191 + m.is_primary 166 192 ); 167 193 } 168 194
+101 -48
src/overlay/multi_slint.rs
··· 8 8 use crate::app::active_profile; 9 9 use crate::config::AppConfig; 10 10 use crate::generated::OverlayWindow; 11 - use crate::timer::{BreakMode, ScheduledBreak, TimerCommand}; 11 + use crate::overlay::OverlayBackend; 12 12 use crate::overlay::fmt_dur; 13 13 use crate::overlay::monitors::MonitorInfo; 14 - use crate::overlay::OverlayBackend; 14 + use crate::timer::{BreakMode, ScheduledBreak, TimerCommand}; 15 15 16 16 struct WindowEntry { 17 17 window: OverlayWindow, ··· 45 45 for (sort_rank, monitor) in &sorted_monitors { 46 46 log::debug!( 47 47 "overlay window[{}]: {}x{} at ({},{})", 48 - sort_rank, monitor.width, monitor.height, monitor.x, monitor.y 48 + sort_rank, 49 + monitor.width, 50 + monitor.height, 51 + monitor.x, 52 + monitor.y 49 53 ); 50 - let pos = slint::LogicalPosition { x: monitor.x as f32, y: monitor.y as f32 }; 54 + let pos = slint::LogicalPosition { 55 + x: monitor.x as f32, 56 + y: monitor.y as f32, 57 + }; 51 58 #[cfg(not(target_os = "linux"))] 52 59 let size = slint::LogicalSize { 53 60 width: monitor.width as f32, ··· 70 77 } 71 78 } 72 79 73 - /// Given the logical positions of winit monitors (sorted by physical x), find 74 - /// the index of the one that best matches `target`. 75 - /// 76 - /// First tries an exact position match within `tolerance` logical pixels. 77 - /// If nothing matches, falls back to `fallback_index` in the sorted list 78 - /// (guaranteed to pick *some* monitor rather than leaving the window on 79 - /// whatever output the compositor chooses by default). 80 - /// 81 - /// Returns `None` only when `monitors` is empty. 80 + // Given the logical positions of winit monitors (sorted by physical x), find 81 + // the index of the one that best matches `target`. 82 + // 83 + // First tries an exact position match within `tolerance` logical pixels. 84 + // If nothing matches, falls back to `fallback_index` in the sorted list 85 + // (guaranteed to pick *some* monitor rather than leaving the window on 86 + // whatever output the compositor chooses by default). 87 + // 88 + // Returns `None` only when `monitors` is empty. 82 89 pub(crate) fn find_monitor_index( 83 90 monitors: &[(f32, f32)], 84 91 target: (f32, f32), ··· 103 110 for e in &self.entries { 104 111 e.window.set_break_label(sched.label.as_str().into()); 105 112 e.window.set_snooze_visible(!is_enforced && !snooze_used); 106 - e.window.set_countdown_text(fmt_dur(sched.break_duration).as_str().into()); 113 + e.window 114 + .set_countdown_text(fmt_dur(sched.break_duration).as_str().into()); 107 115 e.window.set_progress(0.0); 108 116 109 117 #[cfg(target_os = "linux")] ··· 119 127 let win_weak = e.window.as_weak(); 120 128 e.window.show().unwrap_or_default(); 121 129 slint::spawn_local(async move { 122 - let Some(overlay) = win_weak.upgrade() else { return }; 130 + let Some(overlay) = win_weak.upgrade() else { 131 + return; 132 + }; 123 133 let winit_win = match overlay.window().winit_window().await { 124 134 Ok(w) => w, 125 135 Err(_) => { 126 136 log::warn!( 127 137 "overlay: winit_window() unavailable for ({},{}) — falling back", 128 - target_pos.x, target_pos.y 138 + target_pos.x, 139 + target_pos.y 129 140 ); 130 141 overlay.window().set_fullscreen(true); 131 142 return; ··· 135 146 let mut all_monitors: Vec<_> = winit_win.available_monitors().collect(); 136 147 all_monitors.sort_by_key(|m| m.position().x); 137 148 138 - let candidates: Vec<(f32, f32)> = all_monitors.iter().map(|m| { 139 - let sf = m.scale_factor(); 140 - let phys = m.position(); 141 - let lx = (phys.x as f64 / sf) as f32; 142 - let ly = (phys.y as f64 / sf) as f32; 143 - log::debug!( 144 - "overlay: monitor {:?} phys=({},{}) scale={:.2} logical=({},{})", 145 - m.name().as_deref().unwrap_or("?"), phys.x, phys.y, sf, lx, ly 146 - ); 147 - (lx, ly) 148 - }).collect(); 149 + let candidates: Vec<(f32, f32)> = all_monitors 150 + .iter() 151 + .map(|m| { 152 + let sf = m.scale_factor(); 153 + let phys = m.position(); 154 + let lx = (phys.x as f64 / sf) as f32; 155 + let ly = (phys.y as f64 / sf) as f32; 156 + log::debug!( 157 + "overlay: monitor {:?} phys=({},{}) scale={:.2} logical=({},{})", 158 + m.name().as_deref().unwrap_or("?"), 159 + phys.x, 160 + phys.y, 161 + sf, 162 + lx, 163 + ly 164 + ); 165 + (lx, ly) 166 + }) 167 + .collect(); 149 168 150 169 let chosen_idx = find_monitor_index( 151 170 &candidates, ··· 158 177 match &handle { 159 178 Some(h) => log::info!( 160 179 "overlay: fullscreen on {:?} for ({},{})", 161 - h.name().as_deref().unwrap_or("?"), target_pos.x, target_pos.y 180 + h.name().as_deref().unwrap_or("?"), 181 + target_pos.x, 182 + target_pos.y 162 183 ), 163 184 None => log::warn!( 164 185 "overlay: no monitors available for ({},{}), compositor will choose", 165 - target_pos.x, target_pos.y 186 + target_pos.x, 187 + target_pos.y 166 188 ), 167 189 } 168 190 169 191 winit_win.set_fullscreen(Some(winit::window::Fullscreen::Borderless(handle))); 170 - }).ok(); 192 + }) 193 + .ok(); 171 194 } 172 195 173 196 #[cfg(not(target_os = "linux"))] 174 197 { 175 198 // Windows: explicit position+size is reliable. 176 - e.window.window().set_position(slint::WindowPosition::Logical(e.pos)); 177 - e.window.window().set_size(slint::WindowSize::Logical(e.size)); 199 + e.window 200 + .window() 201 + .set_position(slint::WindowPosition::Logical(e.pos)); 202 + e.window 203 + .window() 204 + .set_size(slint::WindowSize::Logical(e.size)); 178 205 e.window.show().unwrap_or_default(); 179 206 } 180 207 } ··· 193 220 let remaining = total.saturating_sub(elapsed); 194 221 let progress = elapsed.as_secs_f32() / total.as_secs_f32().max(1.0); 195 222 for e in &self.entries { 196 - e.window.set_countdown_text(fmt_dur(remaining).as_str().into()); 223 + e.window 224 + .set_countdown_text(fmt_dur(remaining).as_str().into()); 197 225 e.window.set_progress(progress); 198 226 } 199 227 } ··· 212 240 } 213 241 } 214 242 215 - /// Wire all interaction callbacks onto a single window. Called once per window 216 - /// at construction so every monitor's overlay is independently interactive. 243 + // Wire all interaction callbacks onto a single window. Called once per window 244 + // at construction so every monitor's overlay is independently interactive. 217 245 fn wire_callbacks( 218 246 w: &OverlayWindow, 219 247 cmd_tx: tokio::sync::mpsc::UnboundedSender<TimerCommand>, ··· 234 262 let cfg = cfg_arc.clone(); 235 263 let win = w.clone_strong(); 236 264 w.on_unlock_clicked(move || { 237 - let enforced = 238 - active_profile(&cfg.lock().unwrap()).mode == BreakMode::Enforced; 265 + let enforced = active_profile(&cfg.lock().unwrap()).mode == BreakMode::Enforced; 239 266 if enforced { 240 267 win.set_unlock_input_visible(true); 241 268 win.set_unlock_error("".into()); ··· 266 293 { 267 294 let cfg = cfg_arc.clone(); 268 295 w.window().on_close_requested(move || { 269 - let enforced = 270 - active_profile(&cfg.lock().unwrap()).mode == BreakMode::Enforced; 296 + let enforced = active_profile(&cfg.lock().unwrap()).mode == BreakMode::Enforced; 271 297 if enforced { 272 298 slint::CloseRequestResponse::KeepWindowShown 273 299 } else { ··· 292 318 #[test] 293 319 fn exact_match_second_monitor() { 294 320 let monitors = vec![(0.0f32, 0.0f32), (1920.0, 0.0)]; 295 - assert_eq!(find_monitor_index(&monitors, (1920.0, 0.0), 32.0, 1), Some(1)); 321 + assert_eq!( 322 + find_monitor_index(&monitors, (1920.0, 0.0), 32.0, 1), 323 + Some(1) 324 + ); 296 325 } 297 326 298 327 #[test] 299 328 fn match_within_tolerance() { 300 329 let monitors = vec![(0.0f32, 0.0f32), (1920.0, 0.0)]; 301 330 // Winit rounding may yield 1919.5 instead of 1920.0 302 - assert_eq!(find_monitor_index(&monitors, (1920.0, 0.0), 32.0, 1), Some(1)); 303 - assert_eq!(find_monitor_index(&monitors, (1910.0, 0.0), 32.0, 1), Some(1)); 331 + assert_eq!( 332 + find_monitor_index(&monitors, (1920.0, 0.0), 32.0, 1), 333 + Some(1) 334 + ); 335 + assert_eq!( 336 + find_monitor_index(&monitors, (1910.0, 0.0), 32.0, 1), 337 + Some(1) 338 + ); 304 339 } 305 340 306 341 #[test] 307 342 fn outside_tolerance_uses_fallback() { 308 343 let monitors = vec![(0.0f32, 0.0f32), (1920.0, 0.0)]; 309 344 // Target at 960: not within 32px of either monitor, fallback_index=1 310 - assert_eq!(find_monitor_index(&monitors, (960.0, 0.0), 32.0, 1), Some(1)); 345 + assert_eq!( 346 + find_monitor_index(&monitors, (960.0, 0.0), 32.0, 1), 347 + Some(1) 348 + ); 311 349 } 312 350 313 351 #[test] 314 352 fn fallback_index_clamped_to_last() { 315 353 let monitors = vec![(0.0f32, 0.0f32), (1920.0, 0.0)]; 316 354 // fallback_index=5 is out of bounds, should clamp to last (1) 317 - assert_eq!(find_monitor_index(&monitors, (9999.0, 0.0), 32.0, 5), Some(1)); 355 + assert_eq!( 356 + find_monitor_index(&monitors, (9999.0, 0.0), 32.0, 5), 357 + Some(1) 358 + ); 318 359 } 319 360 320 361 #[test] ··· 327 368 let monitors = vec![(0.0f32, 0.0f32)]; 328 369 assert_eq!(find_monitor_index(&monitors, (0.0, 0.0), 32.0, 0), Some(0)); 329 370 // Even with a wrong target — only one option 330 - assert_eq!(find_monitor_index(&monitors, (9999.0, 0.0), 32.0, 0), Some(0)); 371 + assert_eq!( 372 + find_monitor_index(&monitors, (9999.0, 0.0), 32.0, 0), 373 + Some(0) 374 + ); 331 375 } 332 376 333 377 #[test] 334 378 fn triple_monitor_matches_correct_index() { 335 379 let monitors = vec![(-1920.0f32, 0.0), (0.0, 0.0), (1920.0, 0.0)]; 336 - assert_eq!(find_monitor_index(&monitors, (-1920.0, 0.0), 32.0, 0), Some(0)); 380 + assert_eq!( 381 + find_monitor_index(&monitors, (-1920.0, 0.0), 32.0, 0), 382 + Some(0) 383 + ); 337 384 assert_eq!(find_monitor_index(&monitors, (0.0, 0.0), 32.0, 1), Some(1)); 338 - assert_eq!(find_monitor_index(&monitors, (1920.0, 0.0), 32.0, 2), Some(2)); 385 + assert_eq!( 386 + find_monitor_index(&monitors, (1920.0, 0.0), 32.0, 2), 387 + Some(2) 388 + ); 339 389 } 340 390 341 391 #[test] 342 392 fn y_coordinate_used_for_vertical_stack() { 343 393 let monitors = vec![(0.0f32, 0.0), (0.0, 1080.0)]; 344 394 assert_eq!(find_monitor_index(&monitors, (0.0, 0.0), 32.0, 0), Some(0)); 345 - assert_eq!(find_monitor_index(&monitors, (0.0, 1080.0), 32.0, 1), Some(1)); 395 + assert_eq!( 396 + find_monitor_index(&monitors, (0.0, 1080.0), 32.0, 1), 397 + Some(1) 398 + ); 346 399 } 347 400 348 401 // ── manual integration stubs (run with `cargo test -- --ignored`) ─────────
+1 -1
src/overlay/password.rs
··· 1 1 use anyhow::Result; 2 + use argon2::password_hash::{SaltString, rand_core::OsRng}; 2 3 use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; 3 - use argon2::password_hash::{rand_core::OsRng, SaltString}; 4 4 5 5 #[allow(dead_code)] 6 6 pub fn hash_password(password: &str) -> Result<String> {
+12 -6
src/overlay/session.rs
··· 1 1 #[derive(Debug, Clone, Copy, PartialEq)] 2 2 pub enum SessionType { 3 - /// X11 (Linux) — explicit position+size works; one Slint window per monitor. 3 + // X11 (Linux) — explicit position+size works; one Slint window per monitor. 4 4 X11, 5 - /// Wayland — position is compositor-managed; wlr-layer-shell for secondary monitors. 5 + // Wayland — position is compositor-managed; wlr-layer-shell for secondary monitors. 6 6 Wayland, 7 - /// Windows — explicit position+size works; one Slint window per monitor. 7 + // Windows — explicit position+size works; one Slint window per monitor. 8 8 #[cfg_attr(not(target_os = "windows"), allow(dead_code))] 9 9 Windows, 10 10 } 11 11 12 - /// Detect the current display session type. 12 + // Detect the current display session type. 13 13 pub fn detect() -> SessionType { 14 14 detect_from_env(|k| std::env::var(k)) 15 15 } ··· 27 27 #[cfg(not(target_os = "windows"))] 28 28 { 29 29 // WAYLAND_DISPLAY set → native Wayland session. 30 - if var("WAYLAND_DISPLAY").map(|v| !v.is_empty()).unwrap_or(false) { 30 + if var("WAYLAND_DISPLAY") 31 + .map(|v| !v.is_empty()) 32 + .unwrap_or(false) 33 + { 31 34 return SessionType::Wayland; 32 35 } 33 36 SessionType::X11 ··· 38 41 mod tests { 39 42 use super::*; 40 43 41 - fn mock_env<'a>(wayland: Option<&'a str>, _display: Option<&'a str>) -> impl for<'k> Fn(&'k str) -> Result<String, std::env::VarError> + 'a { 44 + fn mock_env<'a>( 45 + wayland: Option<&'a str>, 46 + _display: Option<&'a str>, 47 + ) -> impl for<'k> Fn(&'k str) -> Result<String, std::env::VarError> + 'a { 42 48 move |key| match key { 43 49 "WAYLAND_DISPLAY" => wayland 44 50 .map(|v| Ok(v.to_string()))
+14 -11
src/settings/mod.rs
··· 1 1 use std::rc::Rc; 2 2 use std::sync::{Arc, Mutex}; 3 3 4 - use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel}; 5 4 use fontdue::Font; 5 + use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel}; 6 6 7 7 use crate::autostart; 8 8 use crate::config::{AppConfig, BreakLevelConfig, BreakModeConfig, LongBreakConfig, OverlayTheme}; ··· 58 58 59 59 // Do NOT call hide() — window is created lazily right before show(), 60 60 // so hiding would fight against the immediate show() that follows. 61 - Ok(Self { window, levels_model }) 61 + Ok(Self { 62 + window, 63 + levels_model, 64 + }) 62 65 } 63 66 64 67 pub fn window(&self) -> &SettingsWindow { ··· 68 71 pub fn levels_model(&self) -> Rc<VecModel<LevelEntry>> { 69 72 Rc::clone(&self.levels_model) 70 73 } 71 - 72 74 } 73 75 74 - /// Reads the settings window state back into `cfg`. Also usable from callbacks 75 - /// that capture `SettingsWindow` and `VecModel<LevelEntry>` directly. 76 + // Reads the settings window state back into `cfg`. Also usable from callbacks 77 + // that capture `SettingsWindow` and `VecModel<LevelEntry>` directly. 76 78 pub fn read_settings_into( 77 79 window: &SettingsWindow, 78 80 levels_model: &VecModel<LevelEntry>, ··· 150 152 // Measure profile name widths (pixels) using bundled Nunito Regular at 12px 151 153 fn measure_name_widths(names: &[SharedString]) -> Vec<i32> { 152 154 // include_bytes with concat! so path is resolved from manifest dir at compile time 153 - let font_bytes = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/fonts/Nunito-Regular.ttf")); 155 + let font_bytes = include_bytes!(concat!( 156 + env!("CARGO_MANIFEST_DIR"), 157 + "/assets/fonts/Nunito-Regular.ttf" 158 + )); 154 159 let font = Font::from_bytes(font_bytes.as_ref(), fontdue::FontSettings::default()).unwrap(); 155 160 // measure at an accessible base size (16px) 156 161 let font_size = 16.0; ··· 193 198 ) { 194 199 let prof = cfg.profiles.values().find(|p| p.name == profile_name); 195 200 196 - window.set_enforced_mode(prof.map_or(false, |p| p.mode == BreakModeConfig::Enforced)); 197 - window.set_idle_detection_enabled(prof.map_or(true, |p| p.idle_detection_enabled)); 198 - window.set_idle_threshold_mins( 199 - prof.map_or(5, |p| (p.idle_threshold_secs / 60).max(1) as i32), 200 - ); 201 + window.set_enforced_mode(prof.is_some_and(|p| p.mode == BreakModeConfig::Enforced)); 202 + window.set_idle_detection_enabled(prof.is_none_or(|p| p.idle_detection_enabled)); 203 + window.set_idle_threshold_mins(prof.map_or(5, |p| (p.idle_threshold_secs / 60).max(1) as i32)); 201 204 202 205 let entries: Vec<LevelEntry> = prof 203 206 .map(|p| {
+3 -4
src/sound.rs
··· 2 2 3 3 use rodio::{OutputStream, Sink, Source}; 4 4 5 - /// Play a short two-tone chime at the given volume (0.0–1.0). 6 - /// Spawns a background thread so it never blocks the UI. 5 + // Play a short two-tone chime at the given volume (0.0–1.0). 6 + // Spawns a background thread so it never blocks the UI. 7 7 pub fn play_chime(volume: f32) { 8 8 std::thread::Builder::new() 9 9 .name("ioma-chime".into()) ··· 35 35 } 36 36 37 37 fn silence(millis: u64) -> impl Source<Item = f32> + Send { 38 - rodio::source::Zero::<f32>::new(1, 44100) 39 - .take_duration(Duration::from_millis(millis)) 38 + rodio::source::Zero::<f32>::new(1, 44100).take_duration(Duration::from_millis(millis)) 40 39 }
+8 -6
src/system.rs
··· 1 1 use std::sync::Arc; 2 2 use std::sync::atomic::{AtomicBool, Ordering}; 3 3 4 - /// Starts a background thread that polls the OS dark-mode preference every 5s. 5 - /// Returns an Arc<AtomicBool> that the main thread can read at any time. 4 + // Starts a background thread that polls the OS dark-mode preference every 5s. 5 + // Returns an Arc<AtomicBool> that the main thread can read at any time. 6 6 pub fn start_watcher() -> Arc<AtomicBool> { 7 7 let dark = Arc::new(AtomicBool::new(query())); 8 8 let bg = Arc::clone(&dark); 9 9 std::thread::Builder::new() 10 10 .name("system-theme".into()) 11 - .spawn(move || loop { 12 - std::thread::sleep(std::time::Duration::from_secs(5)); 13 - bg.store(query(), Ordering::Relaxed); 11 + .spawn(move || { 12 + loop { 13 + std::thread::sleep(std::time::Duration::from_secs(5)); 14 + bg.store(query(), Ordering::Relaxed); 15 + } 14 16 }) 15 17 .ok(); 16 18 dark ··· 53 55 pub fn is_dark() -> bool { 54 56 use windows::{ 55 57 Win32::Foundation::HKEY_CURRENT_USER, 56 - Win32::System::Registry::{RegGetValueW, RRF_RT_REG_DWORD}, 58 + Win32::System::Registry::{RRF_RT_REG_DWORD, RegGetValueW}, 57 59 }; 58 60 let mut val: u32 = 1; 59 61 let mut sz = std::mem::size_of::<u32>() as u32;
+24 -27
src/timer/mod.rs
··· 13 13 14 14 const TICK_INTERVAL: Duration = Duration::from_secs(1); 15 15 16 - /// Commands sent to the timer task from the UI/tray. 16 + // Commands sent to the timer task from the UI/tray. 17 17 #[derive(Debug)] 18 18 pub enum TimerCommand { 19 - /// Pause the timer for the given duration. 19 + // Pause the timer for the given duration. 20 20 PauseFor(Duration), 21 - /// Resume from manual pause early. 21 + // Resume from manual pause early. 22 22 Resume, 23 - /// Advance to the next scheduled break immediately. 23 + // Advance to the next scheduled break immediately. 24 24 SkipToNextBreak, 25 - /// Called when a break overlay is dismissed (break finished or snoozed). 25 + // Called when a break overlay is dismissed (break finished or snoozed). 26 26 BreakEnded { snoozed: bool }, 27 - /// Called when idle is detected. 27 + // Called when idle is detected. 28 28 IdleDetected { duration: Duration }, 29 - /// Reload profile (after settings change). 29 + // Reload profile (after settings change). 30 30 SetProfile(Profile), 31 - /// Quit. 31 + // Quit. 32 32 Shutdown, 33 33 } 34 34 35 - /// Events sent from the timer task to the UI. 35 + // Events sent from the timer task to the UI. 36 36 #[derive(Debug, Clone)] 37 37 pub enum TimerEvent { 38 - /// A break is starting; show the overlay. 38 + // A break is starting; show the overlay. 39 39 BreakStarting(ScheduledBreak), 40 - /// Snooze period starting; overlay hides temporarily. 40 + // Snooze period starting; overlay hides temporarily. 41 41 SnoozePeriodStarting { remaining_secs: u64 }, 42 - /// Work phase: tick with current countdown to next break. 42 + // Work phase: tick with current countdown to next break. 43 43 WorkTick { secs_until_break: u64 }, 44 - /// Timer is paused (manual or idle). 44 + // Timer is paused (manual or idle). 45 45 Paused, 46 - /// Timer resumed. 46 + // Timer resumed. 47 47 Resumed, 48 48 } 49 49 50 50 #[derive(Debug, Clone)] 51 51 enum TimerState { 52 52 Working, 53 - /// `fired_index` is the scheduler level that triggered this break, so 54 - /// record_break_completed knows which counter to reset. 53 + // `fired_index` is the scheduler level that triggered this break, so 54 + // record_break_completed knows which counter to reset. 55 55 Breaking { 56 56 snooze_used: bool, 57 57 is_long: bool, ··· 66 66 IdlePaused, 67 67 } 68 68 69 - /// Manual PartialEq — Instant does not implement PartialEq. 69 + // Manual PartialEq — Instant does not implement PartialEq. 70 70 impl PartialEq for TimerState { 71 71 fn eq(&self, other: &Self) -> bool { 72 72 match (self, other) { ··· 227 227 let _ = self.event_tx.send(TimerEvent::SnoozePeriodStarting { 228 228 remaining_secs: snooze_dur.as_secs(), 229 229 }); 230 - } else { 231 - let (was_long, fired_index) = match self.state { 232 - TimerState::Breaking { 233 - is_long, 234 - fired_index, 235 - .. 236 - } => (is_long, fired_index), 237 - _ => (false, 0), 238 - }; 239 - self.scheduler.record_break_completed(fired_index, was_long); 230 + } else if let TimerState::Breaking { 231 + is_long, 232 + fired_index, 233 + .. 234 + } = self.state 235 + { 236 + self.scheduler.record_break_completed(fired_index, is_long); 240 237 self.state = TimerState::Working; 241 238 let _ = self.event_tx.send(TimerEvent::Resumed); 242 239 }
+18 -14
src/timer/profile.rs
··· 17 17 18 18 #[derive(Debug, Clone)] 19 19 pub struct LongBreakTrigger { 20 - /// Number of top-level work cycles before a long break. 20 + // Number of top-level work cycles before a long break. 21 21 pub after_cycles: u32, 22 - /// If idle gap exceeds this, cycle counter resets. 22 + // If idle gap exceeds this, cycle counter resets. 23 23 pub max_cycle_gap: Duration, 24 24 pub break_duration: Duration, 25 25 pub label: String, ··· 32 32 pub snooze_duration: Duration, 33 33 pub idle_threshold: Duration, 34 34 pub idle_detection_enabled: bool, 35 - /// Sorted ascending by work_duration. Scheduler fires the highest-priority 36 - /// (longest interval) level whose work elapsed time is due. 35 + // Sorted ascending by work_duration. Scheduler fires the highest-priority 36 + // (longest interval) level whose work elapsed time is due. 37 37 pub levels: Vec<BreakLevel>, 38 38 pub long_break: Option<LongBreakTrigger>, 39 39 } 40 40 41 41 impl Profile { 42 - pub fn from_config(id: &str, cfg: &ProfileConfig) -> Self { 42 + pub fn from_config(cfg: &ProfileConfig) -> Self { 43 43 let mut levels: Vec<BreakLevel> = cfg 44 44 .levels 45 45 .iter() ··· 52 52 // Ensure ascending order so scheduler can use index priority correctly. 53 53 levels.sort_by_key(|l| l.work_duration); 54 54 55 - let long_break = cfg.long_break.as_ref().map(|lb: &LongBreakConfig| LongBreakTrigger { 56 - after_cycles: lb.after_cycles, 57 - max_cycle_gap: Duration::from_secs(lb.max_cycle_gap_secs), 58 - break_duration: Duration::from_secs(lb.break_secs), 59 - label: lb.label.clone(), 60 - }); 55 + let long_break = cfg 56 + .long_break 57 + .as_ref() 58 + .map(|lb: &LongBreakConfig| LongBreakTrigger { 59 + after_cycles: lb.after_cycles, 60 + max_cycle_gap: Duration::from_secs(lb.max_cycle_gap_secs), 61 + break_duration: Duration::from_secs(lb.break_secs), 62 + label: lb.label.clone(), 63 + }); 61 64 62 - let _ = id; 63 65 Self { 64 66 name: cfg.name.clone(), 65 67 mode: match cfg.mode { ··· 74 76 } 75 77 } 76 78 77 - /// The primary (top-level / longest) break level — used for cycle counting. 79 + // The primary (top-level / longest) break level — used for cycle counting. 78 80 pub fn primary_level(&self) -> &BreakLevel { 79 - self.levels.last().expect("profile must have at least one level") 81 + self.levels 82 + .last() 83 + .expect("profile must have at least one level") 80 84 } 81 85 }
+5 -5
src/tray.rs
··· 75 75 }) 76 76 } 77 77 78 - /// Set icon and menu state to reflect paused state. No-op if state unchanged. 78 + // Set icon and menu state to reflect paused state. No-op if state unchanged. 79 79 pub fn set_paused(&self, paused: bool) { 80 80 if self.last_paused.get() == Some(paused) { 81 81 return; ··· 86 86 } else { 87 87 self.icon_active.clone() 88 88 })); 89 - let _ = self.item_resume.set_enabled(paused); 89 + self.item_resume.set_enabled(paused); 90 90 } 91 91 92 - /// Update tray tooltip. No-op if text unchanged. 92 + // Update tray tooltip. No-op if text unchanged. 93 93 pub fn set_tooltip(&self, text: &str) { 94 94 if *self.last_tooltip.borrow() == text { 95 95 return; ··· 98 98 let _ = self._tray.set_tooltip(Some(text)); 99 99 } 100 100 101 - /// Process pending tray/menu events. Call from the main event loop. 102 - /// Returns `true` if a quit was requested. 101 + // Process pending tray/menu events. Call from the main event loop. 102 + // Returns `true` if a quit was requested. 103 103 pub fn process_events( 104 104 &self, 105 105 cmd_tx: &tokio::sync::mpsc::UnboundedSender<TimerCommand>,