1use crate::notification::send_notification;
2use crate::storage::Storage;
3use anyhow::{Context, Result};
4use chrono::Local;
5use std::fs::{self, OpenOptions};
6use std::io::Write;
7use std::process::{Command, Stdio};
8use std::thread;
9use std::time::Duration;
10
11const POLL_INTERVAL_SECS: u64 = 10;
12
13pub fn start_daemon() -> Result<()> {
14 let pid_file = Storage::pid_file_path()?;
15
16 if is_daemon_running()? {
17 println!("Daemon is already running");
18 return Ok(());
19 }
20
21 let exe = std::env::current_exe()?;
22
23 let child = Command::new(exe)
24 .arg("daemon")
25 .arg("run")
26 .stdin(Stdio::null())
27 .stdout(Stdio::null())
28 .stderr(Stdio::null())
29 .spawn()
30 .context("Failed to start daemon process")?;
31
32 fs::write(&pid_file, child.id().to_string())?;
33 println!("Daemon started with PID: {}", child.id());
34
35 Ok(())
36}
37
38pub fn stop_daemon() -> Result<()> {
39 let pid_file = Storage::pid_file_path()?;
40
41 if !pid_file.exists() {
42 println!("Daemon is not running");
43 return Ok(());
44 }
45
46 let pid_str = fs::read_to_string(&pid_file)?;
47 let pid: i32 = pid_str.trim().parse()?;
48
49 #[cfg(unix)]
50 {
51 let _ = Command::new("kill").arg(pid.to_string()).status();
52 }
53
54 #[cfg(windows)]
55 {
56 let _ = Command::new("taskkill")
57 .args(["/PID", &pid.to_string(), "/F"])
58 .status();
59 }
60
61 fs::remove_file(&pid_file)?;
62 println!("Daemon stopped");
63
64 Ok(())
65}
66
67pub fn daemon_status() -> Result<()> {
68 if is_daemon_running()? {
69 let pid_file = Storage::pid_file_path()?;
70 let pid = fs::read_to_string(&pid_file)?;
71 println!("Daemon is running (PID: {})", pid.trim());
72 } else {
73 println!("Daemon is not running");
74 }
75 Ok(())
76}
77
78pub fn is_daemon_running() -> Result<bool> {
79 let pid_file = Storage::pid_file_path()?;
80
81 if !pid_file.exists() {
82 return Ok(false);
83 }
84
85 let pid_str = fs::read_to_string(&pid_file)?;
86 let pid: u32 = match pid_str.trim().parse() {
87 Ok(p) => p,
88 Err(_) => {
89 fs::remove_file(&pid_file)?;
90 return Ok(false);
91 }
92 };
93
94 #[cfg(unix)]
95 {
96 let output = Command::new("kill")
97 .args(["-0", &pid.to_string()])
98 .output();
99
100 match output {
101 Ok(o) => Ok(o.status.success()),
102 Err(_) => {
103 fs::remove_file(&pid_file)?;
104 Ok(false)
105 }
106 }
107 }
108
109 #[cfg(windows)]
110 {
111 let output = Command::new("tasklist")
112 .args(["/FI", &format!("PID eq {}", pid)])
113 .output();
114
115 match output {
116 Ok(o) => {
117 let stdout = String::from_utf8_lossy(&o.stdout);
118 Ok(stdout.contains(&pid.to_string()))
119 }
120 Err(_) => {
121 fs::remove_file(&pid_file)?;
122 Ok(false)
123 }
124 }
125 }
126}
127
128fn log_daemon(message: &str) {
129 if let Ok(log_path) = Storage::log_file_path() {
130 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(log_path) {
131 let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
132 let _ = writeln!(file, "[{}] {}", timestamp, message);
133 }
134 }
135}
136
137pub fn run_daemon_loop() -> Result<()> {
138 let storage = Storage::new()?;
139 log_daemon("Daemon started");
140
141 loop {
142 match storage.load() {
143 Ok(mut reminders) => {
144 let mut updated = false;
145
146 for reminder in reminders.iter_mut() {
147 if reminder.is_due() {
148 log_daemon(&format!("Triggering reminder: {}", reminder.title));
149
150 if let Err(e) = send_notification(reminder) {
151 log_daemon(&format!("Failed to send notification: {}", e));
152 }
153 reminder.calculate_next_trigger();
154 updated = true;
155 }
156 }
157
158 if updated {
159 if let Err(e) = storage.save(&reminders) {
160 log_daemon(&format!("Failed to save reminders: {}", e));
161 }
162 }
163 }
164 Err(e) => {
165 log_daemon(&format!("Failed to load reminders: {}", e));
166 }
167 }
168
169 thread::sleep(Duration::from_secs(POLL_INTERVAL_SECS));
170 }
171}
172
173#[cfg(target_os = "macos")]
175pub fn generate_launchd_plist() -> Result<String> {
176 let exe = std::env::current_exe()?;
177 let plist = format!(
178 r#"<?xml version="1.0" encoding="UTF-8"?>
179<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
180<plist version="1.0">
181<dict>
182 <key>Label</key>
183 <string>com.reminder-cli.daemon</string>
184 <key>ProgramArguments</key>
185 <array>
186 <string>{}</string>
187 <string>daemon</string>
188 <string>run</string>
189 </array>
190 <key>RunAtLoad</key>
191 <true/>
192 <key>KeepAlive</key>
193 <true/>
194</dict>
195</plist>"#,
196 exe.display()
197 );
198 Ok(plist)
199}
200
201#[cfg(target_os = "linux")]
203pub fn generate_systemd_service() -> Result<String> {
204 let exe = std::env::current_exe()?;
205 let service = format!(
206 r#"[Unit]
207Description=Reminder CLI Daemon
208After=network.target
209
210[Service]
211Type=simple
212ExecStart={} daemon run
213Restart=always
214RestartSec=10
215
216[Install]
217WantedBy=default.target"#,
218 exe.display()
219 );
220 Ok(service)
221}
222
223pub fn install_autostart() -> Result<()> {
224 #[cfg(target_os = "macos")]
225 {
226 let plist = generate_launchd_plist()?;
227 let plist_path = dirs::home_dir()
228 .context("Failed to get home directory")?
229 .join("Library/LaunchAgents/com.reminder-cli.daemon.plist");
230
231 fs::write(&plist_path, plist)?;
232 println!("Created launchd plist at: {}", plist_path.display());
233 println!("To enable: launchctl load {}", plist_path.display());
234 }
235
236 #[cfg(target_os = "linux")]
237 {
238 let service = generate_systemd_service()?;
239 let service_path = dirs::home_dir()
240 .context("Failed to get home directory")?
241 .join(".config/systemd/user/reminder-cli.service");
242
243 if let Some(parent) = service_path.parent() {
244 fs::create_dir_all(parent)?;
245 }
246
247 fs::write(&service_path, service)?;
248 println!("Created systemd service at: {}", service_path.display());
249 println!("To enable: systemctl --user enable --now reminder-cli");
250 }
251
252 #[cfg(target_os = "windows")]
253 {
254 println!("Windows auto-start: Add a shortcut to 'reminder daemon start' in your Startup folder");
255 println!(
256 "Startup folder: {}",
257 dirs::data_local_dir()
258 .map(|p| p
259 .parent()
260 .unwrap_or(&p)
261 .join("Roaming/Microsoft/Windows/Start Menu/Programs/Startup")
262 .display()
263 .to_string())
264 .unwrap_or_else(|| "Unknown".to_string())
265 );
266 }
267
268 Ok(())
269}