tomate 0.5.0

A command-line tool for the Pomodoro productivity system
use anyhow::{Context, Result};
use chrono::TimeDelta;
use clap::{Parser, Subcommand};
use regex::Regex;
use std::path::PathBuf;

const LONG_ABOUT: &str = r#"
tomate sets timers and keeps a log of completed tasks for the Pomodoro productivity system.

There are three types of timers in tomate: pomodoros, short breaks, and long breaks.
Each timer has its own configured duration, with these defaults:

    - pomodoros are 25 minutes long
    - short breaks are 5 minutes long
    - long breaks are 30 minutes long

You can change these settings by editing ~/.config/tomate/config.toml

"#;

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = LONG_ABOUT)]
pub struct Args {
    #[command(subcommand)]
    pub command: Command,
    /// Config file to use. [default: ${XDG_CONFIG_DIR}/tomate/config.toml]
    #[arg(short, long)]
    pub config: Option<PathBuf>,
    #[command(flatten)]
    pub verbose: clap_verbosity_flag::Verbosity,
}

#[derive(Debug, Subcommand)]
pub enum Command {
    /// Get the current Pomodoro or break timer
    Status {
        /// Print a custom-formatted status for the current timer
        ///
        /// Recognizes the following tokens:
        ///
        /// %d - description
        ///
        /// %t - tags, comma-separated
        ///
        /// %r - remaining time, in mm:ss format (or hh:mm:ss if longer than an hour)
        ///
        /// %R - remaining time in seconds
        ///
        /// %s - start time in RFC 3339 format
        ///
        /// %S - start time as a Unix timestamp
        ///
        /// %e - end time in RFC 3339 format
        ///
        /// %E - end time as a Unix timestamp
        #[arg(short, long)]
        format: Option<String>,
    },
    /// Interact with Pomodoro timers
    #[command(visible_alias("pom"))]
    Pomodoro {
        #[command(subcommand)]
        command: PomodoroCommand,
    },
    /// Interact with short break timers
    #[command(visible_alias("short"))]
    ShortBreak {
        #[command(subcommand)]
        command: ShortBreakCommand,
    },
    /// Interact with long break timers
    #[command(visible_alias("long"))]
    LongBreak {
        #[command(subcommand)]
        command: LongBreakCommand,
    },
    /// Interact with system timers
    Timer {
        #[command(subcommand)]
        command: TimerCommand,
    },
    /// Print a list of all logged Pomodoros
    History {
        /// Print history data in JSON format
        #[arg(long, default_value_t = false)]
        json: bool,
    },
    /// Delete all state and configuration files
    Purge,
}

#[derive(Debug, Subcommand)]
pub enum PomodoroCommand {
    /// Show the Pomodoro timer that is currently running
    Show {
        /// Customize how the current Pomodoro is printed.
        ///
        /// Recognizes the following tokens:
        ///
        /// %d - description
        ///
        /// %t - tags, comma-separated
        ///
        /// %r - remaining time, in mm:ss format (or hh:mm:ss if longer than an hour)
        ///
        /// %R - remaining time in seconds
        ///
        /// %s - start time in RFC 3339 format
        ///
        /// %S - start time as a Unix timestamp
        ///
        /// %e - end time in RFC 3339 format
        ///
        /// %E - end time as a Unix timestamp
        #[arg(short, long)]
        format: Option<String>,
    },
    /// Start a new Pomodoro timer
    Start {
        /// Length of the Pomodoro to start like 2m30s
        #[arg(short, long, value_parser = duration_from_human)]
        duration: Option<TimeDelta>,
        /// Description of the task you're focusing on
        description: Option<String>,
        /// Tag to categorize the work you're doing. Can be provided multiple times, one for each
        /// tag
        #[arg(short, long)]
        tag: Vec<String>,
    },
    /// Stop the current Pomodoro timer and log it to the history file
    Stop,
}

#[derive(Debug, Subcommand)]
pub enum ShortBreakCommand {
    /// Start a short break
    Start {
        /// Length of the short break to start like 2m30s
        #[arg(short, long, value_parser = duration_from_human)]
        duration: Option<TimeDelta>,
    },
    /// Stop a short break
    Stop,
}

#[derive(Debug, Subcommand)]
pub enum LongBreakCommand {
    /// Start a long break
    Start {
        /// Length of the long break to start like 2m30s
        #[arg(short, long, value_parser = duration_from_human)]
        duration: Option<TimeDelta>,
    },
    /// Stop a long break
    Stop,
}

#[derive(Debug, Subcommand)]
pub enum TimerCommand {
    /// Check and execute any completed timers
    Check,
}

fn duration_from_human(input: &str) -> Result<TimeDelta> {
    let re = Regex::new(r"^(?:([0-9])h)?(?:([0-9]+)m)?(?:([0-9]+)s)?$").unwrap();
    let caps = re.captures(input)
    .with_context(|| "Failed to parse duration string, format is <HOURS>h<MINUTES>m<SECONDS>s (each section is optional) example: 22m30s")?;

    let hours: i64 = caps.get(1).map_or("0", |c| c.as_str()).parse()?;
    let minutes: i64 = caps.get(2).map_or("0", |c| c.as_str()).parse()?;
    let seconds: i64 = caps.get(3).map_or("0", |c| c.as_str()).parse()?;

    let total_seconds = (hours * 3600) + (minutes * 60) + seconds;

    Ok(TimeDelta::new(total_seconds, 0).expect("Expected duration to be nonzero."))
}