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 #[arg(long)]
44 project: Option<String>,
45
46 #[arg(long)]
48 customer: Option<String>,
49 },
50
51 Stop,
53
54 Pause,
56
57 Resume,
59
60 Status,
62}
63
64pub fn handle_command(cmd: Commands, storage: Storage) -> Result<()> {
66 match cmd {
67 Commands::Session { command } => match command {
68 SessionCommands::Start {
69 task,
70 description,
71 project,
72 customer,
73 } => handle_start(task, description, project, customer, storage),
74 SessionCommands::Stop => handle_stop(storage),
75 SessionCommands::Pause => handle_pause(storage),
76 SessionCommands::Resume => handle_resume(storage),
77 SessionCommands::Status => handle_status(storage),
78 },
79 Commands::Doctor => handle_doctor(storage),
80 }
81}
82
83fn handle_doctor(storage: Storage) -> Result<()> {
84 let diagnostics = storage.diagnostics()?;
85
86 println!("WorkTimer Doctor");
87 println!(" Database: {}", diagnostics.database_path.display());
88 println!(
89 " Migration marker: {}",
90 diagnostics
91 .migration_marker
92 .as_deref()
93 .unwrap_or("<not-set>")
94 );
95 println!(" Days stored: {}", diagnostics.days_count);
96 println!(" Work records: {}", diagnostics.work_records_count);
97 println!(
98 " Active timer: {}",
99 if diagnostics.active_timer_present {
100 "present"
101 } else {
102 "none"
103 }
104 );
105 println!(
106 " Legacy JSON backups: {} day files, {} timer files",
107 diagnostics.legacy_day_json_files, diagnostics.legacy_timer_json_files
108 );
109
110 if diagnostics.migration_marker.is_some() {
111 println!(" Status: OK (SQLite migration completed)");
112 } else {
113 println!(
114 " Status: WARN (unexpected missing migration marker; possible failed migration or DB issue)"
115 );
116 }
117
118 Ok(())
119}
120
121fn handle_start(
123 task: String,
124 description: Option<String>,
125 project: Option<String>,
126 customer: Option<String>,
127 storage: Storage,
128) -> Result<()> {
129 let timer_manager = TimerManager::new(storage);
130
131 let task = task.trim().to_string();
133 if task.is_empty() {
134 return Err(anyhow::anyhow!("Task name cannot be empty"));
135 }
136
137 let project = project
138 .map(|value| value.trim().to_string())
139 .filter(|value| !value.is_empty());
140 let customer = customer
141 .map(|value| value.trim().to_string())
142 .filter(|value| !value.is_empty());
143
144 let timer = timer_manager.start(task, description, project, customer, None, None)?;
145
146 let start_time = format_time(timer.start_time);
147 println!("✓ Session started");
148 println!(" Task: {}", timer.task_name);
149 if let Some(project) = &timer.project {
150 println!(" Project: {}", project);
151 }
152 if let Some(customer) = &timer.customer {
153 println!(" Customer: {}", customer);
154 }
155 if let Some(desc) = &timer.description {
156 println!(" Description: {}", desc);
157 }
158 println!(" Started at: {}", start_time);
159
160 Ok(())
161}
162
163fn handle_stop(storage: Storage) -> Result<()> {
165 let timer_manager = TimerManager::new(storage);
166
167 let timer = timer_manager
169 .status()?
170 .ok_or_else(|| anyhow::anyhow!("No session is running"))?;
171
172 let elapsed = timer_manager.get_elapsed_duration(&timer);
173 let formatted_duration = format_duration(elapsed);
174
175 let start_time = format_time(timer.start_time);
176
177 let record = timer_manager.stop()?;
179
180 let end_time = format!("{:02}:{:02}:{:02}", record.end.hour, record.end.minute, 0);
182
183 println!("✓ Session stopped");
184 println!(" Task: {}", timer.task_name);
185 if let Some(project) = &timer.project {
186 println!(" Project: {}", project);
187 }
188 if let Some(customer) = &timer.customer {
189 println!(" Customer: {}", customer);
190 }
191 println!(" Duration: {}", formatted_duration);
192 println!(" Started at: {}", start_time);
193 println!(" Ended at: {}", end_time);
194
195 Ok(())
196}
197
198fn handle_pause(storage: Storage) -> Result<()> {
200 let timer_manager = TimerManager::new(storage);
201
202 let timer = timer_manager
203 .status()?
204 .ok_or_else(|| anyhow::anyhow!("No session is running"))?;
205
206 let _paused_timer = timer_manager.pause()?;
207 let elapsed = timer_manager.get_elapsed_duration(&timer);
208 let formatted_duration = format_duration(elapsed);
209
210 println!("⏸ Session paused");
211 println!(" Task: {}", timer.task_name);
212 println!(" Elapsed: {}", formatted_duration);
213
214 Ok(())
215}
216
217fn handle_resume(storage: Storage) -> Result<()> {
219 let timer_manager = TimerManager::new(storage);
220
221 let timer = timer_manager
222 .status()?
223 .ok_or_else(|| anyhow::anyhow!("No session is running"))?;
224
225 let _resumed_timer = timer_manager.resume()?;
226 let elapsed = timer_manager.get_elapsed_duration(&timer);
227 let formatted_duration = format_duration(elapsed);
228
229 println!("▶ Session resumed");
230 println!(" Task: {}", timer.task_name);
231 println!(" Total elapsed (before pause): {}", formatted_duration);
232
233 Ok(())
234}
235
236fn handle_status(storage: Storage) -> Result<()> {
238 let timer_manager = TimerManager::new(storage);
239
240 match timer_manager.status()? {
241 Some(timer) => {
242 let elapsed = timer_manager.get_elapsed_duration(&timer);
243 let formatted_duration = format_duration(elapsed);
244 let start_time = format_time(timer.start_time);
245
246 println!("⏱ Session Status");
247 println!(" Task: {}", timer.task_name);
248 println!(
249 " Status: {}",
250 match timer.status {
251 crate::timer::TimerStatus::Running => "Running",
252 crate::timer::TimerStatus::Paused => "Paused",
253 crate::timer::TimerStatus::Stopped => "Stopped",
254 }
255 );
256 println!(" Elapsed: {}", formatted_duration);
257 println!(" Started at: {}", start_time);
258 if let Some(project) = &timer.project {
259 println!(" Project: {}", project);
260 }
261 if let Some(customer) = &timer.customer {
262 println!(" Customer: {}", customer);
263 }
264 if let Some(desc) = &timer.description {
265 println!(" Description: {}", desc);
266 }
267 }
268 None => {
269 println!("No session is currently running");
270 }
271 }
272
273 Ok(())
274}
275
276fn format_time(dt: time::OffsetDateTime) -> String {
278 format!("{:02}:{:02}:{:02}", dt.hour(), dt.minute(), dt.second())
279}
280
281fn format_duration(duration: Duration) -> String {
283 let total_secs = duration.as_secs();
284 let hours = total_secs / 3600;
285 let minutes = (total_secs % 3600) / 60;
286 let seconds = total_secs % 60;
287
288 if hours > 0 {
289 format!("{}h {:02}m {:02}s", hours, minutes, seconds)
290 } else {
291 format!("{}m {:02}s", minutes, seconds)
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn test_format_duration_hours_minutes_seconds() {
301 let duration = Duration::from_secs(3661); assert_eq!(format_duration(duration), "1h 01m 01s");
303 }
304
305 #[test]
306 fn test_format_duration_minutes_seconds() {
307 let duration = Duration::from_secs(125); assert_eq!(format_duration(duration), "2m 05s");
309 }
310
311 #[test]
312 fn test_format_duration_seconds_only() {
313 let duration = Duration::from_secs(45);
314 assert_eq!(format_duration(duration), "0m 45s");
315 }
316
317 #[test]
318 fn test_format_duration_zero() {
319 let duration = Duration::from_secs(0);
320 assert_eq!(format_duration(duration), "0m 00s");
321 }
322
323 #[test]
324 fn test_format_time() {
325 use time::macros::datetime;
326 let dt = datetime!(2025-01-15 14:30:45 UTC);
327 assert_eq!(format_time(dt), "14:30:45");
328 }
329
330 #[test]
331 fn test_cli_has_version() {
332 use clap::CommandFactory;
333 let cmd = Cli::command();
334 let version = cmd.get_version();
335 assert!(version.is_some(), "CLI should have version configured");
336 assert_eq!(version.unwrap(), env!("CARGO_PKG_VERSION"));
338 }
339
340 #[test]
341 fn test_cli_doctor_command_parse() {
342 let cli = Cli::try_parse_from(["work-tuimer", "doctor"]).unwrap();
343 assert!(matches!(cli.command, Commands::Doctor));
344 }
345
346 #[test]
347 fn test_cli_session_start_parses_project_and_customer() {
348 let cli = Cli::try_parse_from([
349 "work-tuimer",
350 "session",
351 "start",
352 "My Task",
353 "--project",
354 "Internal Platform",
355 "--customer",
356 "ACME",
357 ])
358 .unwrap();
359
360 let Commands::Session { command } = cli.command else {
361 panic!("Expected session command");
362 };
363
364 match command {
365 SessionCommands::Start {
366 task,
367 project,
368 customer,
369 ..
370 } => {
371 assert_eq!(task, "My Task");
372 assert_eq!(project.as_deref(), Some("Internal Platform"));
373 assert_eq!(customer.as_deref(), Some("ACME"));
374 }
375 _ => panic!("Expected session start command"),
376 }
377 }
378}