Skip to main content

work_tuimer/cli/
mod.rs

1use crate::storage::Storage;
2use crate::timer::TimerManager;
3use anyhow::Result;
4use clap::{Parser, Subcommand};
5use std::time::Duration;
6
7/// WorkTimer CLI - Automatic time tracking
8#[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/// Available CLI commands
18#[derive(Subcommand)]
19pub enum Commands {
20    /// Manage timer sessions (start/stop/pause/resume/status)
21    Session {
22        #[command(subcommand)]
23        command: SessionCommands,
24    },
25
26    /// Show storage and migration diagnostics
27    Doctor,
28}
29
30/// Session management commands
31#[derive(Subcommand)]
32pub enum SessionCommands {
33    /// Start a new timer session
34    Start {
35        /// Task name
36        task: String,
37
38        /// Optional task description
39        #[arg(short, long)]
40        description: Option<String>,
41    },
42
43    /// Stop the running timer session
44    Stop,
45
46    /// Pause the running timer session
47    Pause,
48
49    /// Resume the paused timer session
50    Resume,
51
52    /// Show status of running timer session
53    Status,
54}
55
56/// Handle CLI command execution
57pub 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
110/// Start a new session
111fn handle_start(task: String, description: Option<String>, storage: Storage) -> Result<()> {
112    let timer_manager = TimerManager::new(storage);
113
114    // Trim task name
115    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
133/// Stop the running session
134fn handle_stop(storage: Storage) -> Result<()> {
135    let timer_manager = TimerManager::new(storage);
136
137    // Load and validate timer exists
138    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    // Stop the timer and get the work record
148    let record = timer_manager.stop()?;
149
150    // Format end time from the work record (HH:MM format)
151    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
162/// Pause the running session
163fn 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
181/// Resume the paused session
182fn 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
200/// Show status of running session
201fn 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
234/// Format time::OffsetDateTime for display (HH:MM:SS)
235fn format_time(dt: time::OffsetDateTime) -> String {
236    format!("{:02}:{:02}:{:02}", dt.hour(), dt.minute(), dt.second())
237}
238
239/// Format Duration for display (h:mm:ss or mm:ss)
240fn 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); // 1h 1m 1s
260        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); // 2m 5s
266        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        // Version comes from Cargo.toml
295        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}