jobber 1.1.2-alpha

Minimalistic console work time tracker
use crate::{
    commands::{Cli, Config, Result, current_job, from_start},
    jobber::JobberWork,
    print_tip,
};
use clap::Parser;
use color_print::cprintln;

/// Begin work
///
/// Start working at the specified time (or now) in the job with the given
/// identifier (or current job).
/// A job may contain subject and tags.
/// Any work which is still running must be ended before starting a new one.
///
/// This function can be undone with `jobber undo`.
#[derive(Parser, Debug)]
pub(crate) struct Start {
    /// Time work begins
    ///
    /// Available formats:
    ///
    /// | Example             | Description       |
    /// |---------------------|-------------------|
    /// | `now`               | current time      |
    /// | `2026-05-31 13:15`  | date/time         |
    /// | `13:15`             | time today        |
    /// | `15m`               | minutes in past   |
    /// | `+15m`              | minutes in future |
    /// | `8h`                | hours in past     |
    /// | `+8h`               | hours in future   |
    #[clap(default_value = "now", verbatim_doc_comment)]
    start_time: String,
    /// Subject of the work
    #[clap(short, long)]
    subject: Option<String>,
    /// Comma-separated list of tags (no spaces allowed e.g. support,test)
    #[clap(short, long, value_delimiter = ',')]
    tags: Vec<String>,
    /// ID of the job to start work for. The job will also be selected as current job (unless -n is used).
    #[clap(short, long)]
    job: Option<usize>,
    /// Do not select new job when using --job
    #[clap(short, long, requires = "job")]
    no_select: bool,
}

impl Start {
    pub(crate) fn run(&self, cli: &Cli) -> Result<()> {
        let mut config = Config::load()?;
        let mut database = cli.open_database()?;
        let job_id = self.job.unwrap_or(current_job(&config, &database)?);
        database.start_work(
            job_id,
            from_start(&self.start_time)?,
            self.subject.clone(),
            self.tags.clone(),
        )?;
        cli.close_database(database)?;
        cprintln!("Successfully started work in job {job_id}");
        if !self.no_select && config.current_job != Some(job_id) {
            config.current_job = Some(job_id);
            cprintln!("Selected job <s>{job_id}</>.");
        }
        print_tip!(cli);
        print_tip!(cli, "Use <s>jobber end</> to end work.");
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use crate::{commands::from_start_ex, entities::DateTime};

    #[test]
    fn start() {
        fn from_start(start: &str) -> String {
            let now = DateTime::from_ymd(2026, 6, 1).unwrap();
            from_start_ex(&start, now).unwrap().to_string()
        }

        assert_eq!(from_start("now"), "2026-06-01 00:00:00");
        assert_eq!(from_start("2026-05-31 13:15"), "2026-05-31 13:15:00");
        assert_eq!(from_start("13:15"), "2026-06-01 13:15:00");
        assert_eq!(from_start("15m"), "2026-05-31 23:45:00");
        assert_eq!(from_start("+15m"), "2026-06-01 00:15:00");
        assert_eq!(from_start("8h"), "2026-05-31 16:00:00");
        assert_eq!(from_start("+8h"), "2026-06-01 08:00:00");
    }
}