use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; use serde::Serialize; use tauri::{AppHandle, Emitter}; use crate::audio::{self, AudioEngine}; use crate::state::{TimerPhase, TimerState}; use crate::storage::{self, AppData}; fn persist_timer_state(data: &mut AppData, timer: &TimerState, data_dir: &std::path::PathBuf) { data.current_task_id = timer.current_task_id.clone(); data.timer_snapshot = Some(timer.snapshot()); let _ = storage::save(data_dir, data); } // LOCK ORDERING: When acquiring multiple locks, always take timer_state // before app_data to prevent deadlock. Never acquire app_data first // while timer_state is not held. // ── Event payloads ────────────────────────────────────────────────────────── #[derive(Clone, Serialize)] pub struct TickPayload { pub phase: TimerPhase, pub remaining_secs: u64, pub total_secs: u64, pub session_count: u32, pub current_task_id: Option, } #[derive(Clone, Serialize)] pub struct PhaseChangedPayload { pub phase: TimerPhase, pub session_count: u32, } #[derive(Clone, Serialize)] pub struct CompletedPayload { pub task_id: Option, } // ── Timer thread ──────────────────────────────────────────────────────────── /// Spawns the background timer thread. The thread runs for the lifetime of /// the application; it sleeps while `running == false`. pub fn spawn_timer_thread( app_handle: AppHandle, timer_arc: Arc>, data_arc: Arc>, audio_arc: Arc>>, data_dir: std::path::PathBuf, ) { thread::spawn(move || { loop { thread::sleep(Duration::from_secs(1)); let mut ts = timer_arc.lock().unwrap(); if !ts.running { continue; } // Decrement if ts.remaining_secs > 0 { ts.remaining_secs -= 1; } // Emit tick let tick = TickPayload { phase: ts.phase, remaining_secs: ts.remaining_secs, total_secs: ts.total_secs, session_count: ts.session_count, current_task_id: ts.current_task_id.clone(), }; { let mut data = data_arc.lock().unwrap(); persist_timer_state(&mut data, &ts, &data_dir); } let _ = app_handle.emit("timer-tick", tick); // Check if phase has reached zero if ts.remaining_secs == 0 { ts.running = false; let mut emit_completed = false; let mut completed_task_id = None; match ts.phase { TimerPhase::Work => { // Decrement task remaining sessions let task_id = ts.current_task_id.clone(); { let mut data = data_arc.lock().unwrap(); if let Some(ref tid) = task_id { if let Some(task) = data.tasks.iter_mut().find(|t| &t.id == tid) { if task.remaining_sessions > 0 { task.remaining_sessions -= 1; } if task.remaining_sessions == 0 { task.completed = true; } } } } emit_completed = true; completed_task_id = task_id; ts.session_count += 1; // Determine next phase let (next_phase, next_duration) = { let data = data_arc.lock().unwrap(); let s = &data.settings; if ts.session_count % s.sessions_before_long_break == 0 { (TimerPhase::LongBreak, s.long_break_secs) } else { (TimerPhase::ShortBreak, s.short_break_secs) } }; ts.phase = next_phase; ts.total_secs = next_duration; ts.remaining_secs = next_duration; } TimerPhase::ShortBreak | TimerPhase::LongBreak => { // Transition back to work let work_secs = { let data = data_arc.lock().unwrap(); data.settings.work_duration_secs }; ts.phase = TimerPhase::Work; ts.total_secs = work_secs; ts.remaining_secs = work_secs; } } { let mut data = data_arc.lock().unwrap(); persist_timer_state(&mut data, &ts, &data_dir); } // Emit phase-changed after state is updated if let Some(engine) = audio_arc.lock().unwrap().as_mut() { if audio::should_duck_for_phase(ts.phase) { engine.duck(); } else { engine.unduck(); } } if emit_completed { let completed = CompletedPayload { task_id: completed_task_id, }; let _ = app_handle.emit("timer-completed", completed); } let phase_changed = PhaseChangedPayload { phase: ts.phase, session_count: ts.session_count, }; let _ = app_handle.emit("timer-phase-changed", phase_changed); } } }); }