1use crate::storage::Storage;
2use crate::timer::TimerManager;
3use anyhow::Result;
4use clap::{Parser, Subcommand};
5use std::time::Duration;
6
7#[derive(Parser)]
9#[command(name = "work-tuimer")]
10#[command(about = "Automatic time tracking with CLI commands and TUI", long_about = None)]
11#[command(version)]
12pub struct Cli {
13 #[command(subcommand)]
14 pub command: Commands,
15}
16
17#[derive(Subcommand)]
19pub enum Commands {
20 Session {
22 #[command(subcommand)]
23 command: SessionCommands,
24 },
25
26 Doctor,
28}
29
30#[derive(Subcommand)]
32pub enum SessionCommands {
33 Start {
35 task: String,
37
38 #[arg(short, long)]
40 description: Option<String>,
41 },
42
43 Stop,
45
46 Pause,
48
49 Resume,
51
52 Status,
54}
55
56pub fn handle_command(cmd: Commands, storage: Storage) -> Result<()> {
58 match cmd {
59 Commands::Session { command } => match command {
60 SessionCommands::Start { task, description } => {
61 handle_start(task, description, storage)
62 }
63 SessionCommands::Stop => handle_stop(storage),
64 SessionCommands::Pause => handle_pause(storage),
65 SessionCommands::Resume => handle_resume(storage),
66 SessionCommands::Status => handle_status(storage),
67 },
68 Commands::Doctor => handle_doctor(storage),
69 }
70}
71
72fn handle_doctor(storage: Storage) -> Result<()> {
73 let diagnostics = storage.diagnostics()?;
74
75 println!("WorkTimer Doctor");
76 println!(" Database: {}", diagnostics.database_path.display());
77 println!(
78 " Migration marker: {}",
79 diagnostics
80 .migration_marker
81 .as_deref()
82 .unwrap_or("<not-set>")
83 );
84 println!(" Days stored: {}", diagnostics.days_count);
85 println!(" Work records: {}", diagnostics.work_records_count);
86 println!(
87 " Active timer: {}",
88 if diagnostics.active_timer_present {
89 "present"
90 } else {
91 "none"
92 }
93 );
94 println!(
95 " Legacy JSON backups: {} day files, {} timer files",
96 diagnostics.legacy_day_json_files, diagnostics.legacy_timer_json_files
97 );
98
99 if diagnostics.migration_marker.is_some() {
100 println!(" Status: OK (SQLite migration completed)");
101 } else {
102 println!(
103 " Status: WARN (unexpected missing migration marker; possible failed migration or DB issue)"
104 );
105 }
106
107 Ok(())
108}
109
110fn handle_start(task: String, description: Option<String>, storage: Storage) -> Result<()> {
112 let timer_manager = TimerManager::new(storage);
113
114 let task = task.trim().to_string();
116 if task.is_empty() {
117 return Err(anyhow::anyhow!("Task name cannot be empty"));
118 }
119
120 let timer = timer_manager.start(task, description, None, None)?;
121
122 let start_time = format_time(timer.start_time);
123 println!("✓ Session started");
124 println!(" Task: {}", timer.task_name);
125 if let Some(desc) = &timer.description {
126 println!(" Description: {}", desc);
127 }
128 println!(" Started at: {}", start_time);
129
130 Ok(())
131}
132
133fn handle_stop(storage: Storage) -> Result<()> {
135 let timer_manager = TimerManager::new(storage);
136
137 let timer = timer_manager
139 .status()?
140 .ok_or_else(|| anyhow::anyhow!("No session is running"))?;
141
142 let elapsed = timer_manager.get_elapsed_duration(&timer);
143 let formatted_duration = format_duration(elapsed);
144
145 let start_time = format_time(timer.start_time);
146
147 let record = timer_manager.stop()?;
149
150 let end_time = format!("{:02}:{:02}:{:02}", record.end.hour, record.end.minute, 0);
152
153 println!("✓ Session stopped");
154 println!(" Task: {}", timer.task_name);
155 println!(" Duration: {}", formatted_duration);
156 println!(" Started at: {}", start_time);
157 println!(" Ended at: {}", end_time);
158
159 Ok(())
160}
161
162fn handle_pause(storage: Storage) -> Result<()> {
164 let timer_manager = TimerManager::new(storage);
165
166 let timer = timer_manager
167 .status()?
168 .ok_or_else(|| anyhow::anyhow!("No session is running"))?;
169
170 let _paused_timer = timer_manager.pause()?;
171 let elapsed = timer_manager.get_elapsed_duration(&timer);
172 let formatted_duration = format_duration(elapsed);
173
174 println!("⏸ Session paused");
175 println!(" Task: {}", timer.task_name);
176 println!(" Elapsed: {}", formatted_duration);
177
178 Ok(())
179}
180
181fn handle_resume(storage: Storage) -> Result<()> {
183 let timer_manager = TimerManager::new(storage);
184
185 let timer = timer_manager
186 .status()?
187 .ok_or_else(|| anyhow::anyhow!("No session is running"))?;
188
189 let _resumed_timer = timer_manager.resume()?;
190 let elapsed = timer_manager.get_elapsed_duration(&timer);
191 let formatted_duration = format_duration(elapsed);
192
193 println!("▶ Session resumed");
194 println!(" Task: {}", timer.task_name);
195 println!(" Total elapsed (before pause): {}", formatted_duration);
196
197 Ok(())
198}
199
200fn handle_status(storage: Storage) -> Result<()> {
202 let timer_manager = TimerManager::new(storage);
203
204 match timer_manager.status()? {
205 Some(timer) => {
206 let elapsed = timer_manager.get_elapsed_duration(&timer);
207 let formatted_duration = format_duration(elapsed);
208 let start_time = format_time(timer.start_time);
209
210 println!("⏱ Session Status");
211 println!(" Task: {}", timer.task_name);
212 println!(
213 " Status: {}",
214 match timer.status {
215 crate::timer::TimerStatus::Running => "Running",
216 crate::timer::TimerStatus::Paused => "Paused",
217 crate::timer::TimerStatus::Stopped => "Stopped",
218 }
219 );
220 println!(" Elapsed: {}", formatted_duration);
221 println!(" Started at: {}", start_time);
222 if let Some(desc) = &timer.description {
223 println!(" Description: {}", desc);
224 }
225 }
226 None => {
227 println!("No session is currently running");
228 }
229 }
230
231 Ok(())
232}
233
234fn format_time(dt: time::OffsetDateTime) -> String {
236 format!("{:02}:{:02}:{:02}", dt.hour(), dt.minute(), dt.second())
237}
238
239fn format_duration(duration: Duration) -> String {
241 let total_secs = duration.as_secs();
242 let hours = total_secs / 3600;
243 let minutes = (total_secs % 3600) / 60;
244 let seconds = total_secs % 60;
245
246 if hours > 0 {
247 format!("{}h {:02}m {:02}s", hours, minutes, seconds)
248 } else {
249 format!("{}m {:02}s", minutes, seconds)
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_format_duration_hours_minutes_seconds() {
259 let duration = Duration::from_secs(3661); assert_eq!(format_duration(duration), "1h 01m 01s");
261 }
262
263 #[test]
264 fn test_format_duration_minutes_seconds() {
265 let duration = Duration::from_secs(125); assert_eq!(format_duration(duration), "2m 05s");
267 }
268
269 #[test]
270 fn test_format_duration_seconds_only() {
271 let duration = Duration::from_secs(45);
272 assert_eq!(format_duration(duration), "0m 45s");
273 }
274
275 #[test]
276 fn test_format_duration_zero() {
277 let duration = Duration::from_secs(0);
278 assert_eq!(format_duration(duration), "0m 00s");
279 }
280
281 #[test]
282 fn test_format_time() {
283 use time::macros::datetime;
284 let dt = datetime!(2025-01-15 14:30:45 UTC);
285 assert_eq!(format_time(dt), "14:30:45");
286 }
287
288 #[test]
289 fn test_cli_has_version() {
290 use clap::CommandFactory;
291 let cmd = Cli::command();
292 let version = cmd.get_version();
293 assert!(version.is_some(), "CLI should have version configured");
294 assert_eq!(version.unwrap(), env!("CARGO_PKG_VERSION"));
296 }
297
298 #[test]
299 fn test_cli_doctor_command_parse() {
300 let cli = Cli::try_parse_from(["work-tuimer", "doctor"]).unwrap();
301 assert!(matches!(cli.command, Commands::Doctor));
302 }
303}