use crate::database::Database;
use notify_rust::Notification;
use std::fs;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;
use sysinfo::System;
const SECONDS_PER_HOUR: u64 = 3600;
fn pid_file_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
let data_dir = dirs::data_dir().ok_or("Could not find data directory")?;
Ok(data_dir.join("break").join("daemon.pid"))
}
pub fn is_daemon_running() -> Result<bool, Box<dyn std::error::Error>> {
let pid_file = pid_file_path()?;
if !pid_file.exists() {
return Ok(false);
}
let pid_str = fs::read_to_string(&pid_file)?;
let pid: u32 = pid_str.trim().parse().unwrap_or(0);
if pid == 0 {
return Ok(false);
}
let mut system = System::new();
system.refresh_all();
let pid = sysinfo::Pid::from_u32(pid);
Ok(system.process(pid).is_some())
}
pub fn ensure_daemon_running() -> Result<(), Box<dyn std::error::Error>> {
if !is_daemon_running()? {
start_daemon_process()?;
}
Ok(())
}
pub fn start_daemon_process() -> Result<(), Box<dyn std::error::Error>> {
if is_daemon_running()? {
return Ok(());
}
let exe = std::env::current_exe()?;
Command::new(exe)
.arg("--daemon-mode")
.stdin(Stdio::null())
.stdout(Stdio::null())
.spawn()?;
Ok(())
}
pub fn run_daemon() -> Result<(), Box<dyn std::error::Error>> {
let pid_file = pid_file_path()?;
if let Some(parent) = pid_file.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&pid_file, std::process::id().to_string())?;
loop {
let mut db = Database::load()?;
let expired = db.get_expired_timers();
for timer in &expired {
#[cfg(target_os = "linux")]
let notification = {
let mut n = Notification::new();
n.summary(&timer.message)
.body("Break timer completed")
.urgency(if timer.urgent {
notify_rust::Urgency::Critical
} else {
notify_rust::Urgency::Normal
});
if timer.sound {
n.sound_name("message-new-instant");
}
n.finalize()
};
#[cfg(target_os = "macos")]
let notification = {
let mut n = Notification::new();
n.summary(&timer.message).body("Break timer completed");
n.finalize()
};
#[cfg(target_os = "windows")]
let notification = {
let mut n = Notification::new();
n.summary(&timer.message).body("Break timer completed");
n.finalize()
};
if let Err(e) = notification.show() {
eprintln!(
"Warning: Failed to show notification for '{}': {}",
timer.message, e
);
eprintln!("Retrying notification after brief delay...");
thread::sleep(Duration::from_millis(500));
if let Err(e) = notification.show() {
eprintln!(
"Error: Failed to show notification after retry for '{}': {}",
timer.message, e
);
eprintln!("Check that your system notification daemon is running.");
}
}
if timer.recurring {
db.add_to_history(timer.clone());
db.reset_timer(timer.id);
} else {
db.complete_timer(timer.id);
}
}
if !expired.is_empty() {
db.save()?;
}
if db.timers.is_empty() {
break;
}
let now = time::OffsetDateTime::now_utc();
let next_timer = db.timers.iter().min_by_key(|t| t.due_at);
let sleep_duration = if let Some(next) = next_timer {
let time_until = next.due_at - now;
let seconds = time_until.whole_seconds();
if seconds > 0 {
Duration::from_secs((seconds + 1) as u64)
} else {
Duration::from_secs(1)
}
} else {
Duration::from_secs(30)
};
let sleep_duration = sleep_duration.min(Duration::from_secs(SECONDS_PER_HOUR));
thread::sleep(sleep_duration);
}
let _ = fs::remove_file(&pid_file);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pid_file_path_creation() {
let path = pid_file_path();
assert!(path.is_ok());
let path = path.unwrap();
assert!(path.to_string_lossy().contains("break"));
assert!(path.to_string_lossy().ends_with("daemon.pid"));
}
#[test]
fn test_is_daemon_running_no_pid_file() {
let result = is_daemon_running();
assert!(result.is_ok());
}
#[test]
fn test_ensure_daemon_running_idempotent() {
let result = ensure_daemon_running();
let _ = result;
}
}