mod config; #[cfg(all(windows, target_env = "gnu"))] mod msvc_compat; mod stats; mod timer; use config::Config; use stats::Stats; use std::sync::{Arc, Mutex}; use std::time::Duration; use tauri::{ image::Image, menu::{MenuBuilder, MenuItemBuilder}, tray::{TrayIcon, TrayIconBuilder}, AppHandle, Emitter, Manager, State, }; use tauri_plugin_notification::NotificationExt; use timer::{AppView, MicrobreakTickResult, TickResult, TimerManager, TimerSnapshot}; pub struct AppState { pub timer: Arc>, pub stats: Arc>, } // ── Tauri Commands ────────────────────────────────────────────────────────── #[tauri::command] fn get_config(state: State) -> Config { let timer = state.timer.lock().unwrap(); timer.pending_config.clone() } #[tauri::command] fn save_config(state: State, config: Config) -> Result<(), String> { let mut timer = state.timer.lock().unwrap(); timer.update_config(config); timer.save_config() } #[tauri::command] fn update_pending_config(app: AppHandle, state: State, config: Config) { let mut timer = state.timer.lock().unwrap(); timer.update_config(config); // Notify all windows (break, mini) so they can pick up live config changes let _ = app.emit("config-changed", &()); } #[tauri::command] fn reset_config(state: State) -> Config { let mut timer = state.timer.lock().unwrap(); timer.reset_config(); timer.pending_config.clone() } #[tauri::command] fn toggle_timer(state: State) -> TimerSnapshot { let mut timer = state.timer.lock().unwrap(); timer.toggle_timer(); timer.snapshot() } #[tauri::command] fn start_break_now(app: AppHandle, state: State) -> TimerSnapshot { let (snapshot, fullscreen_mode) = { let mut timer = state.timer.lock().unwrap(); let payload = timer.start_break_now(); let fm = payload.as_ref().map(|p| p.fullscreen_mode); (timer.snapshot(), fm) }; // Window creation must happen after dropping the mutex if let Some(fm) = fullscreen_mode { handle_break_start(&app, fm); } snapshot } #[tauri::command] fn cancel_break(app: AppHandle, state: State) -> TimerSnapshot { let (snapshot, should_end) = { let mut timer = state.timer.lock().unwrap(); let was_break = timer.state == timer::TimerState::BreakActive; timer.cancel_break(); let ended = was_break && timer.state != timer::TimerState::BreakActive; if ended { let mut s = state.stats.lock().unwrap(); s.record_break_skipped(); } (timer.snapshot(), ended) }; if should_end { handle_break_end(&app); } snapshot } #[tauri::command] fn snooze(app: AppHandle, state: State) -> TimerSnapshot { let (snapshot, should_end) = { let mut timer = state.timer.lock().unwrap(); let was_break = timer.state == timer::TimerState::BreakActive; timer.snooze(); let ended = was_break && timer.state != timer::TimerState::BreakActive; if ended { let mut s = state.stats.lock().unwrap(); s.record_break_snoozed(); } (timer.snapshot(), ended) }; if should_end { handle_break_end(&app); } snapshot } #[tauri::command] fn get_timer_state(state: State) -> TimerSnapshot { let timer = state.timer.lock().unwrap(); timer.snapshot() } #[tauri::command] fn set_view(state: State, view: AppView) { let mut timer = state.timer.lock().unwrap(); timer.set_view(view); } #[tauri::command] fn get_stats(state: State) -> stats::StatsSnapshot { let s = state.stats.lock().unwrap(); let timer = state.timer.lock().unwrap(); let daily_goal = if timer.config.daily_goal_enabled { timer.config.daily_goal_breaks } else { 0 }; s.snapshot(daily_goal) } #[tauri::command] fn get_daily_history(state: State, days: u32) -> Vec { let s = state.stats.lock().unwrap(); s.recent_days(days) } // F7: Weekly summary command #[tauri::command] fn get_weekly_summary(state: State, weeks: u32) -> Vec { let s = state.stats.lock().unwrap(); s.weekly_summary(weeks) } // F8: Auto-start on Windows login #[tauri::command] fn set_auto_start(enabled: bool) -> Result<(), String> { #[cfg(windows)] { use std::ffi::OsStr; use std::os::windows::ffi::OsStrExt; use winapi::um::winreg::{RegOpenKeyExW, RegSetValueExW, RegDeleteValueW, HKEY_CURRENT_USER}; use winapi::um::winnt::{KEY_SET_VALUE, REG_SZ}; let sub_key: Vec = OsStr::new("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run") .encode_wide() .chain(std::iter::once(0)) .collect(); let value_name: Vec = OsStr::new("CoreCooldown") .encode_wide() .chain(std::iter::once(0)) .collect(); unsafe { let mut hkey = std::ptr::null_mut(); let result = RegOpenKeyExW( HKEY_CURRENT_USER, sub_key.as_ptr(), 0, KEY_SET_VALUE, &mut hkey, ); if result != 0 { return Err("Failed to open registry key".to_string()); } if enabled { let exe_path = std::env::current_exe() .map_err(|e| e.to_string())?; let path_str = exe_path.to_string_lossy(); let value_data: Vec = OsStr::new(&*path_str) .encode_wide() .chain(std::iter::once(0)) .collect(); let res = RegSetValueExW( hkey, value_name.as_ptr(), 0, REG_SZ, value_data.as_ptr() as *const u8, (value_data.len() * 2) as u32, ); winapi::um::winreg::RegCloseKey(hkey); if res != 0 { return Err("Failed to set registry value".to_string()); } } else { let _res = RegDeleteValueW(hkey, value_name.as_ptr()); winapi::um::winreg::RegCloseKey(hkey); } } Ok(()) } #[cfg(not(windows))] { Err("Auto-start is only supported on Windows".to_string()) } } #[tauri::command] fn get_auto_start_status() -> bool { #[cfg(windows)] { use std::ffi::OsStr; use std::os::windows::ffi::OsStrExt; use winapi::um::winreg::{RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER}; use winapi::um::winnt::KEY_READ; let sub_key: Vec = OsStr::new("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run") .encode_wide() .chain(std::iter::once(0)) .collect(); let value_name: Vec = OsStr::new("CoreCooldown") .encode_wide() .chain(std::iter::once(0)) .collect(); unsafe { let mut hkey = std::ptr::null_mut(); let result = RegOpenKeyExW( HKEY_CURRENT_USER, sub_key.as_ptr(), 0, KEY_READ, &mut hkey, ); if result != 0 { return false; } let res = RegQueryValueExW( hkey, value_name.as_ptr(), std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut(), ); winapi::um::winreg::RegCloseKey(hkey); res == 0 } } #[cfg(not(windows))] { false } } // ── Cursor / Window Position Commands ──────────────────────────────────── #[tauri::command] fn get_cursor_position() -> (i32, i32) { #[cfg(windows)] { use winapi::shared::windef::POINT; use winapi::um::winuser::GetCursorPos; let mut point = POINT { x: 0, y: 0 }; unsafe { GetCursorPos(&mut point); } (point.x, point.y) } #[cfg(not(windows))] { (0, 0) } } #[tauri::command] fn save_window_position( state: State, label: String, x: i32, y: i32, width: u32, height: u32, ) { let mut timer = state.timer.lock().unwrap(); match label.as_str() { "main" => { timer.pending_config.main_window_x = Some(x); timer.pending_config.main_window_y = Some(y); timer.pending_config.main_window_width = Some(width); timer.pending_config.main_window_height = Some(height); } "mini" => { timer.pending_config.mini_window_x = Some(x); timer.pending_config.mini_window_y = Some(y); } _ => {} } let _ = timer.save_config(); } // ── Dynamic Tray Icon Rendering ──────────────────────────────────────────── /// Parse a "#RRGGBB" hex color string into (r, g, b). Falls back to fallback on invalid input. fn parse_hex_color(hex: &str, fallback: (u8, u8, u8)) -> (u8, u8, u8) { if hex.len() == 7 && hex.starts_with('#') { let r = u8::from_str_radix(&hex[1..3], 16).unwrap_or(fallback.0); let g = u8::from_str_radix(&hex[3..5], 16).unwrap_or(fallback.1); let b = u8::from_str_radix(&hex[5..7], 16).unwrap_or(fallback.2); (r, g, b) } else { fallback } } /// Render a 32x32 RGBA icon with a progress arc using the given accent/break colors. /// F10: Optionally renders a green checkmark when daily goal is met. fn render_tray_icon( progress: f64, is_break: bool, is_paused: bool, accent: (u8, u8, u8), break_color: (u8, u8, u8), goal_met: bool, ) -> Vec { let size: usize = 32; let mut rgba = vec![0u8; size * size * 4]; let center = size as f64 / 2.0; let outer_r = center - 1.0; let inner_r = outer_r - 4.0; let arc_color = if is_break { break_color } else { accent }; for y in 0..size { for x in 0..size { let dx = x as f64 - center; let dy = y as f64 - center; let dist = (dx * dx + dy * dy).sqrt(); let idx = (y * size + x) * 4; if dist >= inner_r && dist <= outer_r { // Determine angle (0 at top, clockwise) let angle = (dx.atan2(-dy) + std::f64::consts::PI) / (2.0 * std::f64::consts::PI); let in_arc = angle <= progress; if in_arc { rgba[idx] = arc_color.0; rgba[idx + 1] = arc_color.1; rgba[idx + 2] = arc_color.2; rgba[idx + 3] = 255; } else { // Background ring rgba[idx] = 60; rgba[idx + 1] = 60; rgba[idx + 2] = 60; rgba[idx + 3] = if is_paused { 100 } else { 180 }; } } } } // F10: Draw a small green dot in the bottom-right corner when goal is met if goal_met { let dot_cx = 25.0_f64; let dot_cy = 25.0_f64; let dot_r = 4.0_f64; for y in 20..32 { for x in 20..32 { let dx = x as f64 - dot_cx; let dy = y as f64 - dot_cy; let dist = (dx * dx + dy * dy).sqrt(); if dist <= dot_r { let idx = (y * size + x) * 4; rgba[idx] = 63; // green rgba[idx + 1] = 185; rgba[idx + 2] = 80; rgba[idx + 3] = 255; } } } } rgba } fn update_tray( tray: &TrayIcon, snapshot: &TimerSnapshot, accent: (u8, u8, u8), break_color: (u8, u8, u8), goal_met: bool, ) { // Update tooltip let tooltip = match snapshot.state { timer::TimerState::Running => { if snapshot.deferred_break_pending { "Core Cooldown - Break deferred (fullscreen)".to_string() } else { let m = snapshot.time_remaining / 60; let s = snapshot.time_remaining % 60; format!("Core Cooldown - {:02}:{:02} until break", m, s) } } timer::TimerState::Paused => { if snapshot.idle_paused { "Core Cooldown - Paused (idle)".to_string() } else { "Core Cooldown - Paused".to_string() } } timer::TimerState::BreakActive => { let m = snapshot.break_time_remaining / 60; let s = snapshot.break_time_remaining % 60; format!("Core Cooldown - Break {:02}:{:02}", m, s) } }; let _ = tray.set_tooltip(Some(&tooltip)); // Update icon let (progress, is_break, is_paused) = match snapshot.state { timer::TimerState::Running => (snapshot.progress, false, false), timer::TimerState::Paused => (snapshot.progress, false, true), timer::TimerState::BreakActive => (snapshot.break_progress, true, false), }; let icon_data = render_tray_icon(progress, is_break, is_paused, accent, break_color, goal_met); let icon = Image::new_owned(icon_data, 32, 32); let _ = tray.set_icon(Some(icon)); } // ── App Builder ───────────────────────────────────────────────────────────── #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_global_shortcut::Builder::new().build()) .setup(|app| { // Portable data directory for WebView2 data (next to the exe) let data_dir = std::env::current_exe() .ok() .and_then(|p| p.parent().map(|d| d.join("data"))) .unwrap_or_else(|| std::path::PathBuf::from("data")); // Create main window (was previously in tauri.conf.json) tauri::WebviewWindowBuilder::new( app, "main", tauri::WebviewUrl::App("index.html".into()), ) .title("Core Cooldown") .inner_size(480.0, 700.0) .decorations(false) .transparent(true) .shadow(true) .data_directory(data_dir.clone()) .build()?; // Create break window (hidden, was previously in tauri.conf.json) tauri::WebviewWindowBuilder::new( app, "break", tauri::WebviewUrl::App("index.html?break=1".into()), ) .title("Core Cooldown - Break") .inner_size(900.0, 540.0) .decorations(false) .transparent(true) .shadow(false) .always_on_top(true) .skip_taskbar(true) .resizable(false) .visible(false) .center() .data_directory(data_dir.clone()) .build()?; let timer_manager = TimerManager::new(); let loaded_stats = Stats::load_or_default(); let state = AppState { timer: Arc::new(Mutex::new(timer_manager)), stats: Arc::new(Mutex::new(loaded_stats)), }; app.manage(state); // Restore saved window position { let st = app.state::(); let timer = st.timer.lock().unwrap(); let cfg = &timer.pending_config; if let (Some(x), Some(y)) = (cfg.main_window_x, cfg.main_window_y) { if let Some(win) = app.get_webview_window("main") { let _ = win.set_position(tauri::Position::Physical( tauri::PhysicalPosition::new(x, y), )); } } if let (Some(w), Some(h)) = (cfg.main_window_width, cfg.main_window_height) { if let Some(win) = app.get_webview_window("main") { let _ = win.set_size(tauri::Size::Physical(tauri::PhysicalSize::new(w, h))); } } } // Set up system tray let tray = setup_tray(app.handle())?; // Set up global shortcuts setup_shortcuts(app.handle()); // Start the timer tick thread let handle = app.handle().clone(); let timer_ref = app.state::().timer.clone(); let stats_ref = app.state::().stats.clone(); let data_dir_clone = data_dir.clone(); std::thread::spawn(move || { let mut dim_window_open = false; let mut microbreak_window_open = false; let mut break_deferred_notified = false; loop { std::thread::sleep(Duration::from_secs(1)); let (tick_result, mb_result, snapshot, accent_hex, break_hex, daily_goal, daily_goal_enabled, goal_met) = { let mut timer = timer_ref.lock().unwrap(); let result = timer.tick(); let mb = timer.tick_microbreak(); let snap = timer.snapshot(); let ac = timer.config.accent_color.clone(); let bc = timer.config.break_color.clone(); let dg = timer.config.daily_goal_breaks; let dge = timer.config.daily_goal_enabled; // Check goal status let s = stats_ref.lock().unwrap(); let goal_target = if dge { dg } else { 0 }; let ss = s.snapshot(goal_target); (result, mb, snap, ac, bc, dg, dge, ss.daily_goal_met) }; // Update tray icon and tooltip with configured colors let accent = parse_hex_color(&accent_hex, (255, 77, 0)); let break_c = parse_hex_color(&break_hex, (124, 106, 239)); update_tray(&tray, &snapshot, accent, break_c, goal_met); // Emit tick event with full snapshot let _ = handle.emit("timer-tick", &snapshot); // F5: Screen dim window management if snapshot.screen_dim_active && !dim_window_open { open_dim_overlay(&handle, &data_dir_clone); dim_window_open = true; } else if !snapshot.screen_dim_active && dim_window_open { close_dim_overlay(&handle); dim_window_open = false; } if snapshot.screen_dim_active { let max_opacity = { let t = timer_ref.lock().unwrap(); t.config.screen_dim_max_opacity }; let _ = handle.emit("screen-dim-update", &serde_json::json!({ "progress": snapshot.screen_dim_progress, "maxOpacity": max_opacity })); } // F1: Microbreak window management match mb_result { MicrobreakTickResult::MicrobreakStarted => { open_microbreak_window(&handle, &data_dir_clone); microbreak_window_open = true; let _ = handle.emit("microbreak-started", &()); } MicrobreakTickResult::MicrobreakEnded => { close_microbreak_window(&handle); microbreak_window_open = false; let _ = handle.emit("microbreak-ended", &()); } MicrobreakTickResult::None => {} } // Emit specific events for state transitions match tick_result { TickResult::BreakStarted(payload) => { // Close dim overlay if it was open if dim_window_open { close_dim_overlay(&handle); dim_window_open = false; } // Close microbreak if active if microbreak_window_open { close_microbreak_window(&handle); microbreak_window_open = false; } handle_break_start(&handle, payload.fullscreen_mode); // F9: Multi-monitor overlays { let timer = timer_ref.lock().unwrap(); if timer.config.fullscreen_mode && timer.config.multi_monitor_break { open_multi_monitor_overlays(&handle, &data_dir_clone); } } let _ = handle.emit("break-started", &payload); break_deferred_notified = false; } TickResult::BreakEnded => { // Restore normal window state and close break window handle_break_end(&handle); // F9: Close multi-monitor overlays close_multi_monitor_overlays(&handle); // Record completed break in stats let break_result = { let timer = timer_ref.lock().unwrap(); let goal = if daily_goal_enabled { daily_goal } else { 0 }; let mut s = stats_ref.lock().unwrap(); s.record_break_completed(timer.break_total_duration, goal) }; // F10: Emit milestone/goal events if let Some(streak) = break_result.milestone_reached { let _ = handle.emit("milestone-reached", &streak); let timer = timer_ref.lock().unwrap(); if timer.config.streak_notifications { let _ = handle .notification() .builder() .title("Streak milestone!") .body(&format!("{}-day streak! Keep it up!", streak)) .show(); } } if break_result.daily_goal_just_met { let _ = handle.emit("daily-goal-met", &()); let timer = timer_ref.lock().unwrap(); if timer.config.streak_notifications { let _ = handle .notification() .builder() .title("Daily goal reached!") .body("You've hit your break goal for today.") .show(); } } let _ = handle .notification() .builder() .title("Break complete") .body("Great job! Back to work.") .show(); let _ = handle.emit("break-ended", &()); } TickResult::PreBreakWarning { seconds_until_break, } => { let secs = seconds_until_break; let msg = if secs >= 60 { format!( "Break in {} minute{}", secs / 60, if secs / 60 == 1 { "" } else { "s" } ) } else { format!("Break in {} seconds", secs) }; let _ = handle .notification() .builder() .title("Core Cooldown") .body(&msg) .show(); let _ = handle.emit("prebreak-warning", &secs); } TickResult::NaturalBreakDetected { duration_seconds } => { // Record natural break in stats if enabled { let timer = timer_ref.lock().unwrap(); if timer.config.smart_break_count_stats { let mut s = stats_ref.lock().unwrap(); s.record_natural_break(duration_seconds); } } let mins = duration_seconds / 60; let msg = if mins >= 1 { format!( "You've been away for {} minute{}. Break timer has been reset.", mins, if mins == 1 { "" } else { "s" } ) } else { format!( "You've been away for {} seconds. Break timer has been reset.", duration_seconds ) }; let _ = handle .notification() .builder() .title("Natural break detected") .body(&msg) .show(); let _ = handle.emit("natural-break-detected", &duration_seconds); } TickResult::BreakDeferred => { // F2: Notify once when break gets deferred if !break_deferred_notified { break_deferred_notified = true; let timer = timer_ref.lock().unwrap(); if timer.config.presentation_mode_notification { let _ = handle .notification() .builder() .title("Break deferred") .body("Fullscreen app detected - break will start when you exit.") .show(); } let _ = handle.emit("break-deferred", &()); } } TickResult::None => {} } } }); Ok(()) }) // Handle window close events - only exit when the main window is closed .on_window_event(|window, event| { if let tauri::WindowEvent::CloseRequested { .. } = event { if window.label() == "main" { window.app_handle().exit(0); } // Mini and break windows just close normally without killing the app } }) .invoke_handler(tauri::generate_handler![ get_config, save_config, update_pending_config, reset_config, toggle_timer, start_break_now, cancel_break, snooze, get_timer_state, set_view, get_stats, get_daily_history, get_weekly_summary, set_auto_start, get_auto_start_status, get_cursor_position, save_window_position, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } fn setup_tray(app: &AppHandle) -> Result> { let show_i = MenuItemBuilder::with_id("show", "Show").build(app)?; let pause_i = MenuItemBuilder::with_id("pause", "Pause/Resume").build(app)?; let mini_i = MenuItemBuilder::with_id("mini", "Mini Mode").build(app)?; let quit_i = MenuItemBuilder::with_id("quit", "Quit").build(app)?; let menu = MenuBuilder::new(app) .item(&show_i) .item(&pause_i) .item(&mini_i) .separator() .item(&quit_i) .build()?; let tray = TrayIconBuilder::new() .menu(&menu) .tooltip("Core Cooldown") .on_menu_event(move |app, event| match event.id().as_ref() { "show" => { if let Some(window) = app.get_webview_window("main") { let _ = window.show(); let _ = window.unminimize(); let _ = window.set_focus(); } } "pause" => { let state: State = app.state(); let mut timer = state.timer.lock().unwrap(); timer.toggle_timer(); } "mini" => { toggle_mini_window(app); } "quit" => { app.exit(0); } _ => {} }) .on_tray_icon_event(|tray, event| { if let tauri::tray::TrayIconEvent::Click { button: tauri::tray::MouseButton::Left, .. } = event { let app = tray.app_handle(); if let Some(window) = app.get_webview_window("main") { let _ = window.show(); let _ = window.unminimize(); let _ = window.set_focus(); } } }) .build(app)?; Ok(tray) } // ── Global Shortcuts ─────────────────────────────────────────────────────── fn setup_shortcuts(app: &AppHandle) { use tauri_plugin_global_shortcut::GlobalShortcutExt; let _ = app .global_shortcut() .on_shortcut("Ctrl+Shift+P", move |_app, _shortcut, event| { if event.state == tauri_plugin_global_shortcut::ShortcutState::Pressed { let state: State = _app.state(); let mut timer = state.timer.lock().unwrap(); timer.toggle_timer(); } }); let _ = app .global_shortcut() .on_shortcut("Ctrl+Shift+B", move |_app, _shortcut, event| { if event.state == tauri_plugin_global_shortcut::ShortcutState::Pressed { let state: State = _app.state(); let mut timer = state.timer.lock().unwrap(); let payload = timer.start_break_now(); if let Some(ref p) = payload { handle_break_start(_app, p.fullscreen_mode); } } }); let _ = app .global_shortcut() .on_shortcut("Ctrl+Shift+S", move |_app, _shortcut, event| { if event.state == tauri_plugin_global_shortcut::ShortcutState::Pressed { if let Some(window) = _app.get_webview_window("main") { if window.is_visible().unwrap_or(false) { let _ = window.hide(); } else { let _ = window.show(); let _ = window.unminimize(); let _ = window.set_focus(); } } } }); } // ── Break Window ──────────────────────────────────────────────────────────── fn open_break_window(app: &AppHandle) { if let Some(win) = app.get_webview_window("break") { let _ = win.show(); let _ = win.set_focus(); } } fn close_break_window(app: &AppHandle) { if let Some(win) = app.get_webview_window("break") { let _ = win.hide(); } } /// Handle break start: either fullscreen on main window, or open a separate modal break window. fn handle_break_start(app: &AppHandle, fullscreen_mode: bool) { if fullscreen_mode { // Fullscreen: show break inside the main window if let Some(window) = app.get_webview_window("main") { let _ = window.set_always_on_top(true); let _ = window.show(); let _ = window.set_focus(); let _ = window.set_fullscreen(true); } } else { // Modal: open a separate centered break window open_break_window(app); } } /// Handle break end: restore main window state and close break window if open. fn handle_break_end(app: &AppHandle) { if let Some(window) = app.get_webview_window("main") { let _ = window.set_always_on_top(false); let _ = window.set_fullscreen(false); } close_break_window(app); } // ── Mini Mode ────────────────────────────────────────────────────────────── fn toggle_mini_window(app: &AppHandle) { if let Some(mini) = app.get_webview_window("mini") { // Close existing mini window let _ = mini.close(); } else { // Read saved position from config let (mx, my) = { let st: State = app.state(); let timer = st.timer.lock().unwrap(); ( timer.pending_config.mini_window_x, timer.pending_config.mini_window_y, ) }; // Portable data directory for WebView2 let data_dir = std::env::current_exe() .ok() .and_then(|p| p.parent().map(|d| d.join("data"))) .unwrap_or_else(|| std::path::PathBuf::from("data")); // Create mini window let mut builder = tauri::WebviewWindowBuilder::new( app, "mini", tauri::WebviewUrl::App("index.html?mini=1".into()), ) .title("Core Cooldown") .inner_size(184.0, 92.0) .decorations(false) .transparent(true) .shadow(false) .always_on_top(true) .skip_taskbar(true) .resizable(false) .data_directory(data_dir); if let (Some(x), Some(y)) = (mx, my) { builder = builder.position(x as f64, y as f64); } let _ = builder.build(); } } // ── F1: Microbreak Window ────────────────────────────────────────────────── fn open_microbreak_window(app: &AppHandle, data_dir: &std::path::Path) { if app.get_webview_window("microbreak").is_some() { return; // already open } let _ = tauri::WebviewWindowBuilder::new( app, "microbreak", tauri::WebviewUrl::App("index.html?microbreak=1".into()), ) .title("Eye Break") .inner_size(400.0, 180.0) .decorations(false) .transparent(true) .shadow(false) .always_on_top(true) .skip_taskbar(true) .resizable(false) .center() .data_directory(data_dir.to_path_buf()) .build(); } fn close_microbreak_window(app: &AppHandle) { if let Some(win) = app.get_webview_window("microbreak") { let _ = win.close(); } } // ── F5: Dim Overlay Window ────────────────────────────────────────────────── fn open_dim_overlay(app: &AppHandle, data_dir: &std::path::Path) { if app.get_webview_window("dim").is_some() { return; } let builder = tauri::WebviewWindowBuilder::new( app, "dim", tauri::WebviewUrl::App("index.html?dim=1".into()), ) .title("") .decorations(false) .transparent(true) .shadow(false) .always_on_top(true) .skip_taskbar(true) .resizable(false) .maximized(true) .data_directory(data_dir.to_path_buf()); if let Ok(win) = builder.build() { let _ = win.set_ignore_cursor_events(true); } } fn close_dim_overlay(app: &AppHandle) { if let Some(win) = app.get_webview_window("dim") { let _ = win.close(); } } // ── F9: Multi-Monitor Break Overlays ──────────────────────────────────────── fn open_multi_monitor_overlays(app: &AppHandle, data_dir: &std::path::Path) { let monitors = timer::get_all_monitors(); for (i, mon) in monitors.iter().enumerate() { if mon.is_primary { continue; // Primary is handled by the main break window } if i > 5 { break; // Cap at 6 monitors } let label = format!("break-overlay-{}", i); if app.get_webview_window(&label).is_some() { continue; } let _ = tauri::WebviewWindowBuilder::new( app, &label, tauri::WebviewUrl::App("index.html?breakoverlay=1".into()), ) .title("") .position(mon.x as f64, mon.y as f64) .inner_size(mon.width as f64, mon.height as f64) .decorations(false) .transparent(true) .shadow(false) .always_on_top(true) .skip_taskbar(true) .resizable(false) .data_directory(data_dir.to_path_buf()) .build(); } } fn close_multi_monitor_overlays(app: &AppHandle) { // Close any window with label starting with "break-overlay-" for i in 0..6 { let label = format!("break-overlay-{}", i); if let Some(win) = app.get_webview_window(&label) { let _ = win.close(); } } }