summaryrefslogtreecommitdiff
path: root/src-tauri/src/lib.rs
diff options
context:
space:
mode:
authorSolstice <solstice@local>2026-06-09 00:13:03 -0700
committerSolstice <solstice@local>2026-06-09 00:13:03 -0700
commit72626524c4a1c7d6642bc170520913273acb1a5c (patch)
treedeeaa45248c6c80f373c2d956cc51e0a4dc7f2cf /src-tauri/src/lib.rs
parent8cf1686e9e9ed20a1c256ccf9e510f4293aa1cf3 (diff)
feat: backend timer logic and data persistence
Diffstat (limited to 'src-tauri/src/lib.rs')
-rw-r--r--src-tauri/src/lib.rs292
1 files changed, 288 insertions, 4 deletions
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 4a277ef..3ca8d99 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -1,14 +1,298 @@
-// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
+mod state;
+mod storage;
+mod timer;
+
+use std::sync::{Arc, Mutex};
+use chrono::Utc;
+use tauri::{AppHandle, Manager, State};
+use uuid::Uuid;
+
+use state::{AppDataWrapper, TimerPhase, TimerState, TimerStateWrapper};
+use storage::{Settings, Task};
+
+// ── Helper ──────────────────────────────────────────────────────────────────
+
+fn app_data_dir(app: &AppHandle) -> std::path::PathBuf {
+ app.path().app_data_dir().expect("Failed to resolve app data dir")
+}
+
+// ── Timer commands ──────────────────────────────────────────────────────────
+
+#[tauri::command]
+fn get_timer_status(timer: State<'_, TimerStateWrapper>) -> serde_json::Value {
+ let ts = timer.0.lock().unwrap();
+ serde_json::json!({
+ "phase": ts.phase,
+ "remaining_secs": ts.remaining_secs,
+ "total_secs": ts.total_secs,
+ "running": ts.running,
+ "session_count": ts.session_count,
+ "current_task_id": ts.current_task_id,
+ })
+}
+
+#[tauri::command]
+fn start_timer(timer: State<'_, TimerStateWrapper>) {
+ let mut ts = timer.0.lock().unwrap();
+ ts.running = true;
+}
+
#[tauri::command]
-fn greet(name: &str) -> String {
- format!("Hello, {}! You've been greeted from Rust!", name)
+fn pause_timer(timer: State<'_, TimerStateWrapper>) {
+ let mut ts = timer.0.lock().unwrap();
+ ts.running = false;
}
+#[tauri::command]
+fn reset_timer(timer: State<'_, TimerStateWrapper>, data: State<'_, AppDataWrapper>) {
+ let mut ts = timer.0.lock().unwrap();
+ let total = {
+ let app_data = data.data.lock().unwrap();
+ match ts.phase {
+ TimerPhase::Work => app_data.settings.work_duration_secs,
+ TimerPhase::ShortBreak => app_data.settings.short_break_secs,
+ TimerPhase::LongBreak => app_data.settings.long_break_secs,
+ }
+ };
+ ts.remaining_secs = total;
+ ts.total_secs = total;
+ ts.running = false;
+}
+
+#[tauri::command]
+fn skip_phase(
+ timer: State<'_, TimerStateWrapper>,
+ data: State<'_, AppDataWrapper>,
+ app: AppHandle,
+) {
+ use tauri::Emitter;
+ let mut ts = timer.0.lock().unwrap();
+ let app_data = data.data.lock().unwrap();
+ let s = &app_data.settings;
+
+ match ts.phase {
+ TimerPhase::Work => {
+ ts.session_count += 1;
+ let (next_phase, next_secs) =
+ 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_secs;
+ ts.remaining_secs = next_secs;
+ }
+ TimerPhase::ShortBreak | TimerPhase::LongBreak => {
+ ts.phase = TimerPhase::Work;
+ ts.total_secs = s.work_duration_secs;
+ ts.remaining_secs = s.work_duration_secs;
+ }
+ }
+
+ ts.running = false;
+
+ let _ = app.emit(
+ "timer-phase-changed",
+ timer::PhaseChangedPayload {
+ phase: ts.phase,
+ session_count: ts.session_count,
+ },
+ );
+}
+
+#[tauri::command]
+fn set_current_task(
+ task_id: Option<String>,
+ timer: State<'_, TimerStateWrapper>,
+ data: State<'_, AppDataWrapper>,
+ app: AppHandle,
+) -> Result<(), String> {
+ {
+ let mut app_data = data.data.lock().unwrap();
+ // Validate task exists if Some
+ if let Some(ref tid) = task_id {
+ if !app_data.tasks.iter().any(|t| &t.id == tid) {
+ return Err(format!("Task {} not found", tid));
+ }
+ }
+ app_data.current_task_id = task_id.clone();
+ storage::save(&app_data_dir(&app), &app_data)?;
+ }
+ {
+ let mut ts = timer.0.lock().unwrap();
+ ts.current_task_id = task_id;
+ }
+ Ok(())
+}
+
+// ── Settings commands ───────────────────────────────────────────────────────
+
+#[tauri::command]
+fn get_settings(data: State<'_, AppDataWrapper>) -> Settings {
+ data.data.lock().unwrap().settings.clone()
+}
+
+#[tauri::command]
+fn update_settings(
+ settings: Settings,
+ data: State<'_, AppDataWrapper>,
+ timer: State<'_, TimerStateWrapper>,
+ app: AppHandle,
+) -> Result<(), String> {
+ {
+ let mut app_data = data.data.lock().unwrap();
+ app_data.settings = settings.clone();
+ storage::save(&app_data_dir(&app), &app_data)?;
+ }
+ // Restart timer at new work duration
+ {
+ let mut ts = timer.0.lock().unwrap();
+ ts.phase = TimerPhase::Work;
+ ts.total_secs = settings.work_duration_secs;
+ ts.remaining_secs = settings.work_duration_secs;
+ ts.running = false;
+ ts.session_count = 0;
+ }
+ Ok(())
+}
+
+// ── Task commands ───────────────────────────────────────────────────────────
+
+#[tauri::command]
+fn get_tasks(data: State<'_, AppDataWrapper>) -> Vec<Task> {
+ data.data.lock().unwrap().tasks.clone()
+}
+
+#[tauri::command]
+fn add_task(
+ name: String,
+ total_sessions: u32,
+ data: State<'_, AppDataWrapper>,
+ app: AppHandle,
+) -> Result<Task, String> {
+ let task = Task {
+ id: Uuid::new_v4().to_string(),
+ name,
+ total_sessions,
+ remaining_sessions: total_sessions,
+ completed: false,
+ created_at: Utc::now().to_rfc3339(),
+ };
+ {
+ let mut app_data = data.data.lock().unwrap();
+ app_data.tasks.push(task.clone());
+ storage::save(&app_data_dir(&app), &app_data)?;
+ }
+ Ok(task)
+}
+
+#[tauri::command]
+fn update_task(
+ id: String,
+ remaining_sessions: Option<u32>,
+ completed: Option<bool>,
+ data: State<'_, AppDataWrapper>,
+ app: AppHandle,
+) -> Result<(), String> {
+ let mut app_data = data.data.lock().unwrap();
+ let task = app_data
+ .tasks
+ .iter_mut()
+ .find(|t| t.id == id)
+ .ok_or_else(|| format!("Task {} not found", id))?;
+ if let Some(r) = remaining_sessions {
+ task.remaining_sessions = r;
+ }
+ if let Some(c) = completed {
+ task.completed = c;
+ }
+ storage::save(&app_data_dir(&app), &app_data)?;
+ Ok(())
+}
+
+#[tauri::command]
+fn delete_task(
+ id: String,
+ data: State<'_, AppDataWrapper>,
+ timer: State<'_, TimerStateWrapper>,
+ app: AppHandle,
+) -> Result<(), String> {
+ {
+ let mut app_data = data.data.lock().unwrap();
+ let before = app_data.tasks.len();
+ app_data.tasks.retain(|t| t.id != id);
+ if app_data.tasks.len() == before {
+ return Err(format!("Task {} not found", id));
+ }
+ // Clear current_task_id if it was this task
+ if app_data.current_task_id.as_deref() == Some(&id) {
+ app_data.current_task_id = None;
+ }
+ storage::save(&app_data_dir(&app), &app_data)?;
+ }
+ {
+ let mut ts = timer.0.lock().unwrap();
+ if ts.current_task_id.as_deref() == Some(&id) {
+ ts.current_task_id = None;
+ }
+ }
+ Ok(())
+}
+
+// ── App entry point ─────────────────────────────────────────────────────────
+
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
- .invoke_handler(tauri::generate_handler![greet])
+ .setup(|app| {
+ let data_dir = app.handle().path().app_data_dir()
+ .expect("Failed to resolve app data dir");
+
+ // Load persisted data
+ let app_data = storage::load(&data_dir);
+ let work_secs = app_data.settings.work_duration_secs;
+ let current_task_id = app_data.current_task_id.clone();
+
+ let data_arc = Arc::new(Mutex::new(app_data));
+ let timer_arc = Arc::new(Mutex::new({
+ let mut ts = TimerState::new(work_secs);
+ ts.current_task_id = current_task_id;
+ ts
+ }));
+
+ // Register managed state
+ app.manage(TimerStateWrapper(Arc::clone(&timer_arc)));
+ app.manage(AppDataWrapper {
+ data: Arc::clone(&data_arc),
+ data_dir: data_dir.clone(),
+ });
+
+ // Spawn background timer thread
+ timer::spawn_timer_thread(
+ app.handle().clone(),
+ Arc::clone(&timer_arc),
+ Arc::clone(&data_arc),
+ data_dir,
+ );
+
+ Ok(())
+ })
+ .invoke_handler(tauri::generate_handler![
+ get_timer_status,
+ start_timer,
+ pause_timer,
+ reset_timer,
+ skip_phase,
+ set_current_task,
+ get_settings,
+ update_settings,
+ get_tasks,
+ add_task,
+ update_task,
+ delete_task,
+ ])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}