1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
use clap::Parser;
use color_print::cprintln;
use crate::{
commands::{Cli, Config, Error, Result, current_job, from_end},
jobber::{JobberGet, JobberWork, Mod},
};
/// End work
///
/// End running work at the specified time (or now).
/// If a subject was not set within the previous start command it must be set
/// at this point.
/// If a subject was already set within the start command, it might be
/// replaced (`-r`) or appended (`-a`) explicitly.
///
/// This function can be undone with `jobber undo`.
#[derive(Parser, Debug)]
pub(crate) struct End {
/// Time when work ended (or current time if not given)
///
/// Available formats:
///
/// | Example | Description |
/// |----------------------|----------------------|
/// | `now` | current time |
/// | `"2026-05-31 13:15"` | date/time |
/// | `13:15` | time behind start |
/// | `15m` | work in minutes |
/// | `8h` | work in hours |
#[clap(default_value = "now")]
#[clap(verbatim_doc_comment)]
end_time: String,
/// Subject of the done work
subject: Option<String>,
/// If subject already was set replace with the given one
#[clap(short, long, conflicts_with = "append_subject", requires = "subject")]
replace_subject: bool,
/// If subject already was set append the given one to it
#[clap(short, long, conflicts_with = "replace_subject", requires = "subject")]
append_subject: bool,
/// 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 end work for (default is current job).
#[clap(short, long)]
job: Option<usize>,
}
impl End {
pub(crate) fn run(&self, cli: &Cli) -> Result<()> {
let config = Config::load()?;
let mut database = cli.open_database()?;
let subject = match (&self.subject, self.replace_subject, self.append_subject) {
(_, true, true) => unreachable!("conflict must be checked before"),
(Some(subject), true, false) => Mod::Replace(subject),
(Some(subject), false, true) => Mod::Append(subject),
(Some(subject), false, false) => Mod::Set(subject),
(None, _, _) => Mod::None,
};
let job_id = self.job.unwrap_or(current_job(&config, &database)?);
if let Some(start) = database.get_job(job_id)?.running.as_ref().map(|r| r.start) {
database.end_work(
job_id,
from_end(&self.end_time, start)?,
subject,
self.tags.clone(),
)?;
cli.close_database(database)?;
cprintln!();
cprintln!("Successfully ended work in job <s>{job_id}</>");
Ok(())
} else {
Err(Error::NoCurrentJob)
}
}
}