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
27/// Session management commands
28#[derive(Subcommand)]
29pub enum SessionCommands {
30    /// Start a new timer session
31    Start {
32        /// Task name
33        task: String,
34
35        /// Optional task description
36        #[arg(short, long)]
37        description: Option<String>,
38    },
39
40    /// Stop the running timer session
41    Stop,
42
43    /// Pause the running timer session
44    Pause,
45
46    /// Resume the paused timer session
47    Resume,
48
49    /// Show status of running timer session
50    Status,
51}
52
53/// Handle CLI command execution
54pub fn handle_command(cmd: Commands, storage: Storage) -> Result<()> {
55    match cmd {
56        Commands::Session { command } => match command {
57            SessionCommands::Start { task, description } => {
58                handle_start(task, description, storage)
59            }
60            SessionCommands::Stop => handle_stop(storage),
61            SessionCommands::Pause => handle_pause(storage),
62            SessionCommands::Resume => handle_resume(storage),
63            SessionCommands::Status => handle_status(storage),
64        },
65    }
66}
67
68/// Start a new session
69fn handle_start(task: String, description: Option<String>, storage: Storage) -> Result<()> {
70    let timer_manager = TimerManager::new(storage);
71
72    // Trim task name
73    let task = task.trim().to_string();
74    if task.is_empty() {
75        return Err(anyhow::anyhow!("Task name cannot be empty"));
76    }
77
78    let timer = timer_manager.start(task, description, None, None)?;
79
80    let start_time = format_time(timer.start_time);
81    println!("✓ Session started");
82    println!("  Task: {}", timer.task_name);
83    if let Some(desc) = &timer.description {
84        println!("  Description: {}", desc);
85    }
86    println!("  Started at: {}", start_time);
87
88    Ok(())
89}
90
91/// Stop the running session
92fn handle_stop(storage: Storage) -> Result<()> {
93    let timer_manager = TimerManager::new(storage);
94
95    // Load and validate timer exists
96    let timer = timer_manager
97        .status()?
98        .ok_or_else(|| anyhow::anyhow!("No session is running"))?;
99
100    let elapsed = timer_manager.get_elapsed_duration(&timer);
101    let formatted_duration = format_duration(elapsed);
102
103    let start_time = format_time(timer.start_time);
104
105    // Stop the timer and get the work record
106    let record = timer_manager.stop()?;
107
108    // Format end time from the work record (HH:MM format)
109    let end_time = format!("{:02}:{:02}:{:02}", record.end.hour, record.end.minute, 0);
110
111    println!("✓ Session stopped");
112    println!("  Task: {}", timer.task_name);
113    println!("  Duration: {}", formatted_duration);
114    println!("  Started at: {}", start_time);
115    println!("  Ended at: {}", end_time);
116
117    Ok(())
118}
119
120/// Pause the running session
121fn handle_pause(storage: Storage) -> Result<()> {
122    let timer_manager = TimerManager::new(storage);
123
124    let timer = timer_manager
125        .status()?
126        .ok_or_else(|| anyhow::anyhow!("No session is running"))?;
127
128    let _paused_timer = timer_manager.pause()?;
129    let elapsed = timer_manager.get_elapsed_duration(&timer);
130    let formatted_duration = format_duration(elapsed);
131
132    println!("⏸ Session paused");
133    println!("  Task: {}", timer.task_name);
134    println!("  Elapsed: {}", formatted_duration);
135
136    Ok(())
137}
138
139/// Resume the paused session
140fn handle_resume(storage: Storage) -> Result<()> {
141    let timer_manager = TimerManager::new(storage);
142
143    let timer = timer_manager
144        .status()?
145        .ok_or_else(|| anyhow::anyhow!("No session is running"))?;
146
147    let _resumed_timer = timer_manager.resume()?;
148    let elapsed = timer_manager.get_elapsed_duration(&timer);
149    let formatted_duration = format_duration(elapsed);
150
151    println!("▶ Session resumed");
152    println!("  Task: {}", timer.task_name);
153    println!("  Total elapsed (before pause): {}", formatted_duration);
154
155    Ok(())
156}
157
158/// Show status of running session
159fn handle_status(storage: Storage) -> Result<()> {
160    let timer_manager = TimerManager::new(storage);
161
162    match timer_manager.status()? {
163        Some(timer) => {
164            let elapsed = timer_manager.get_elapsed_duration(&timer);
165            let formatted_duration = format_duration(elapsed);
166            let start_time = format_time(timer.start_time);
167
168            println!("⏱ Session Status");
169            println!("  Task: {}", timer.task_name);
170            println!(
171                "  Status: {}",
172                match timer.status {
173                    crate::timer::TimerStatus::Running => "Running",
174                    crate::timer::TimerStatus::Paused => "Paused",
175                    crate::timer::TimerStatus::Stopped => "Stopped",
176                }
177            );
178            println!("  Elapsed: {}", formatted_duration);
179            println!("  Started at: {}", start_time);
180            if let Some(desc) = &timer.description {
181                println!("  Description: {}", desc);
182            }
183        }
184        None => {
185            println!("No session is currently running");
186        }
187    }
188
189    Ok(())
190}
191
192/// Format time::OffsetDateTime for display (HH:MM:SS)
193fn format_time(dt: time::OffsetDateTime) -> String {
194    format!("{:02}:{:02}:{:02}", dt.hour(), dt.minute(), dt.second())
195}
196
197/// Format Duration for display (h:mm:ss or mm:ss)
198fn format_duration(duration: Duration) -> String {
199    let total_secs = duration.as_secs();
200    let hours = total_secs / 3600;
201    let minutes = (total_secs % 3600) / 60;
202    let seconds = total_secs % 60;
203
204    if hours > 0 {
205        format!("{}h {:02}m {:02}s", hours, minutes, seconds)
206    } else {
207        format!("{}m {:02}s", minutes, seconds)
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_format_duration_hours_minutes_seconds() {
217        let duration = Duration::from_secs(3661); // 1h 1m 1s
218        assert_eq!(format_duration(duration), "1h 01m 01s");
219    }
220
221    #[test]
222    fn test_format_duration_minutes_seconds() {
223        let duration = Duration::from_secs(125); // 2m 5s
224        assert_eq!(format_duration(duration), "2m 05s");
225    }
226
227    #[test]
228    fn test_format_duration_seconds_only() {
229        let duration = Duration::from_secs(45);
230        assert_eq!(format_duration(duration), "0m 45s");
231    }
232
233    #[test]
234    fn test_format_duration_zero() {
235        let duration = Duration::from_secs(0);
236        assert_eq!(format_duration(duration), "0m 00s");
237    }
238
239    #[test]
240    fn test_format_time() {
241        use time::macros::datetime;
242        let dt = datetime!(2025-01-15 14:30:45 UTC);
243        assert_eq!(format_time(dt), "14:30:45");
244    }
245
246    #[test]
247    fn test_cli_has_version() {
248        use clap::CommandFactory;
249        let cmd = Cli::command();
250        let version = cmd.get_version();
251        assert!(version.is_some(), "CLI should have version configured");
252        // Version comes from Cargo.toml
253        assert_eq!(version.unwrap(), env!("CARGO_PKG_VERSION"));
254    }
255}