1use std::iter::once;
20use std::num::NonZeroU32;
21use std::path::PathBuf;
22use std::result;
23
24use clap::{Args, Parser, Subcommand};
25use once_cell::sync::Lazy;
26use regex::Regex;
27
28use crate::date::Time;
29
30#[derive(Args, Default)]
32struct VecString {
33 args: Vec<String>
34}
35
36#[derive(Args, Default)]
38struct OptString {
39 arg: Option<String>
40}
41
42use crate::config::{Config, DEFAULT_CONF};
43#[doc(inline)]
44use crate::error::Error;
45#[doc(inline)]
46use crate::error::PathError;
47
48pub mod args;
49pub mod cmd;
50
51pub use args::DateRangeArgs;
52pub use args::FilterArgs;
53
54const BIN_NAME: &str = "rtimelog";
56
57static SUBST_RE: Lazy<Regex> =
59 Lazy::new(|| Regex::new(r"\{\}").expect("Template pattern must be correct"));
60
61#[derive(Clone, Copy)]
63enum ExpandAlias {
64 Yes,
65 No
66}
67
68const TASK_DESC: &str = "The command takes a 'task description' consisting of an optional project \
70 formatted with a leading '+', an optional task name formatted with a \
71 leading '@', and potentially more text adding details to the task. If no \
72 task name starting with '@' is supplied, any extra text is treated as \
73 the task.";
74
75const DATE_DESC: &str = "The 'date range description' consists of a single date string or a pair \
77 of date strings of the form 'YYYY-MM-DD', or one of a set of relative \
78 date strings including: today, yesterday, sunday, monday, tuesday, \
79 wednesday, thursday, friday, or saturday. The first two are obvious. The \
80 others refer to the previous instance of that day. The range can also be \
81 described by a month name (like january), the string 'ytd', or a range \
82 specified by 'this' or 'last' followed by 'week', 'month', or 'year'.";
83
84static FULL_DESC: Lazy<String> = Lazy::new(|| format!("{TASK_DESC}\n\n{DATE_DESC}"));
85
86#[derive(Subcommand)]
88enum StackCommands {
89 Clear,
91
92 Drop {
94 #[arg(name = "num", default_value = "1")]
99 num: NonZeroU32
100 },
101
102 Keep {
104 #[arg(name = "num", default_value = "10")]
109 num: NonZeroU32
110 },
111
112 Ls,
114
115 Top
117}
118
119#[derive(Subcommand)]
121enum EntryCommands {
122 Discard,
124
125 Now,
127
128 Ignore,
130
131 #[command(after_help = TASK_DESC)]
133 Rewrite {
134 #[arg(name = "task_desc")]
135 task: Vec<String>
136 },
137
138 Was {
140 time: Time
142 },
143
144 Rewind {
146 minutes: NonZeroU32
148 }
149}
150
151#[derive(Subcommand)]
153enum ReportCommands {
154 #[command(after_help = DATE_DESC)]
156 Detail {
157 #[arg(name = "proj", short, long = "proj")]
159 projs: Vec<String>,
160
161 #[arg(name = "date_range")]
163 dates: Vec<String>
164 },
165
166 #[command(after_help = DATE_DESC)]
168 Summary {
169 #[arg(name = "proj", short, long = "proj")]
171 projs: Vec<String>,
172
173 #[arg(name = "date_range")]
175 dates: Vec<String>
176 },
177
178 #[command(after_help = DATE_DESC)]
180 Hours {
181 #[arg(name = "proj", short, long = "proj")]
183 projs: Vec<String>,
184
185 #[arg(name = "date_range")]
187 dates: Vec<String>
188 },
189
190 #[command(after_help = DATE_DESC)]
192 Events {
193 #[arg(short)]
195 compact: bool,
196
197 #[arg(name = "proj", short, long = "proj")]
199 projs: Vec<String>,
200
201 #[arg(name = "date_range")]
203 dates: Vec<String>
204 },
205
206 #[command(after_help = DATE_DESC)]
209 Intervals {
210 #[arg(name = "proj", short, long = "proj")]
212 projs: Vec<String>,
213
214 #[arg(name = "date_range")]
216 dates: Vec<String>
217 },
218
219 #[command(after_help = DATE_DESC)]
221 Chart {
222 #[arg(name = "date_range")]
224 dates: Vec<String>
225 }
226}
227
228#[derive(Subcommand)]
230enum Subcommands {
231 Init {
233 #[arg(name = "dir")]
236 dir: Option<String>
237 },
238
239 #[command(after_help = TASK_DESC)]
243 Start {
244 #[arg(name = "task_desc")]
245 task: Vec<String>
246 },
247
248 Stop,
250
251 #[command(after_help = TASK_DESC)]
257 Push {
258 #[arg(name = "task_desc")]
259 task: Vec<String>
260 },
261
262 Resume,
264
265 Pause,
267
268 Swap,
270
271 #[command(after_help = DATE_DESC)]
274 Ls {
275 #[arg(short)]
277 all: bool,
278
279 #[arg(name = "date_desc")]
280 date: Option<String>
281 },
282
283 Comment(VecString),
285
286 Event(VecString),
288
289 Lsproj,
291
292 Edit,
294
295 Curr,
297
298 Check,
300
301 Archive,
303
304 Aliases,
306
307 #[command(subcommand)]
309 Report(ReportCommands),
310
311 #[command(subcommand)]
313 Stack(StackCommands),
314
315 #[command(subcommand)]
317 Entry(EntryCommands),
318
319 #[command(external_subcommand)]
322 Other(Vec<String>)
323}
324
325#[derive(Parser)]
327#[command(author, name = "rtimelog", version, about, long_about = None, after_help = FULL_DESC.as_str())]
328pub struct Cli {
329 #[arg(long, name = "dir")]
331 dir: Option<PathBuf>,
332
333 #[arg(long)]
335 editor: Option<PathBuf>,
336
337 #[arg(long, name = "filepath")]
339 conf: Option<PathBuf>,
340
341 #[arg(long)]
343 browser: Option<String>,
344
345 #[command(subcommand)]
347 cmd: Option<Subcommands>
348}
349
350impl Cli {
351 pub fn run(&self) -> crate::Result<()> {
360 let config = self.config()?;
361 match &self.cmd {
362 Some(cmd) => cmd.run(&config, ExpandAlias::Yes),
363 None => Subcommands::default_command(&config).run(&config, ExpandAlias::No)
364 }
365 }
366
367 fn run_alias(&self, config: &Config, expand: ExpandAlias) -> crate::Result<()> {
376 match &self.cmd {
377 Some(cmd) => cmd.run(config, expand),
378 None => Subcommands::default_command(config).run(config, ExpandAlias::No)
379 }
380 }
381
382 fn config(&self) -> result::Result<Config, PathError> {
390 let mut config = match &self.conf {
391 Some(conf_file) => {
392 Config::from_file(conf_file.to_str().ok_or(PathError::FilenameMissing)?)
393 }
394 None => Config::from_file(&DEFAULT_CONF)
395 }
396 .unwrap_or_default();
397
398 if let Some(dir) = &self.dir {
399 config.set_dir(
400 dir.to_str()
401 .ok_or_else(|| PathError::InvalidPath(String::new(), String::new()))?
402 );
403 }
404 if let Some(editor) = &self.editor {
405 config.set_editor(editor.to_str().ok_or(PathError::FilenameMissing)?);
406 }
407 if let Some(browser) = &self.browser {
408 config.set_browser(browser);
409 }
410 Ok(config)
411 }
412}
413
414impl Subcommands {
415 pub fn run(&self, config: &Config, expand: ExpandAlias) -> crate::Result<()> {
423 use Subcommands as C;
424
425 match &self {
426 C::Init { dir } => Ok(cmd::initialize(config, dir.as_ref().map(String::as_str))?),
427 C::Start { task } => cmd::start_task(config, task),
428 C::Stop => cmd::stop_task(config),
429 C::Comment(VecString { args }) => cmd::add_comment(config, args),
430 C::Event(VecString { args }) => cmd::add_event(config, args),
431 C::Push { task } => cmd::push_task(config, task),
432 C::Resume => cmd::resume_task(config),
433 C::Pause => cmd::pause_task(config),
434 C::Swap => cmd::swap_entry(config),
435 C::Ls { all, date } => cmd::list_entries(config, date.as_ref().map(String::as_str), *all),
436 C::Lsproj => cmd::list_projects(config),
437 C::Edit => cmd::edit(config),
438 C::Curr => cmd::current_task(config),
439 C::Check => cmd::check_logfile(config),
440 C::Archive => cmd::archive_year(config),
441 C::Aliases => {
442 cmd::list_aliases(config);
443 Ok(())
444 }
445 C::Report(cmd) => cmd.run(config),
446 C::Stack(cmd) => cmd.run(config),
447 C::Entry(cmd) => cmd.run(config),
448 C::Other(args) => match expand {
449 ExpandAlias::Yes => Self::expand_alias(config, args),
450 ExpandAlias::No => Err(Error::InvalidCommand(args[0].clone()))
451 }
452 }
453 }
454
455 fn default_command(config: &Config) -> Self {
456 match config.defcmd() {
457 "init" => Self::Init { dir: None },
458 "start" => Self::Start { task: Vec::new() },
459 "stop" => Self::Stop,
460 "push" => Self::Push { task: Vec::new() },
461 "resume" => Self::Resume,
462 "pause" => Self::Pause,
463 "swap" => Self::Swap,
464 "ls" => Self::Ls { all: false, date: None },
465 "lsproj" => Self::Lsproj,
466 "edit" => Self::Edit,
467 "curr" => Self::Curr,
468 "archive" => Self::Archive,
469 "aliases" => Self::Aliases,
470 _ => Self::Curr
471 }
472 }
473
474 fn expand_alias(config: &Config, args: &[String]) -> crate::Result<()> {
476 let alias = &args[0];
477 let mut args_iter = args[1..].iter().map(String::as_str);
478
479 let expand: Vec<String> = config
480 .alias(alias)
481 .ok_or_else(|| Error::InvalidCommand(alias.clone()))?
482 .split(' ')
483 .map(|w| {
484 if SUBST_RE.is_match(w) {
485 args_iter.next().map_or_else(
486 || w.to_string(),
487 |val| SUBST_RE.replace(w, val).into_owned()
488 )
489 }
490 else {
491 w.to_string()
492 }
493 })
494 .collect();
495
496 let cmd = Cli::parse_from(
497 once(BIN_NAME)
498 .chain(expand.iter().map(String::as_str))
499 .chain(args_iter)
500 );
501 cmd.run_alias(config, ExpandAlias::No)
502 }
503}
504
505impl StackCommands {
506 pub fn run(&self, config: &Config) -> crate::Result<()> {
514 use StackCommands as SC;
515 match &self {
516 SC::Clear => cmd::stack_clear(config),
517 SC::Drop { num } => cmd::stack_drop(config, *num),
518 SC::Keep { num } => cmd::stack_keep(config, *num),
519 SC::Ls => cmd::list_stack(config),
520 SC::Top => cmd::stack_top(config)
521 }
522 }
523}
524
525impl EntryCommands {
526 pub fn run(&self, config: &Config) -> crate::Result<()> {
534 use EntryCommands as EC;
535 match &self {
536 EC::Discard => cmd::discard_last_entry(config),
537 EC::Now => cmd::reset_last_entry(config),
538 EC::Ignore => cmd::ignore_last_entry(config),
539 EC::Rewrite { task } => cmd::rewrite_last_entry(config, task),
540 EC::Was { time } => cmd::retime_last_entry(config, *time),
541 EC::Rewind { minutes } => cmd::rewind_last_entry(config, *minutes)
542 }
543 }
544}
545
546impl ReportCommands {
547 pub fn run(&self, config: &Config) -> crate::Result<()> {
555 use ReportCommands as RC;
556 match &self {
557 RC::Detail { projs, dates } => cmd::report_daily(config, dates, projs),
558 RC::Summary { projs, dates } => cmd::report_summary(config, dates, projs),
559 RC::Hours { projs, dates } => cmd::report_hours(config, dates, projs),
560 RC::Events { compact, projs, dates } => {
561 cmd::report_events(config, dates, projs, *compact)
562 }
563 RC::Intervals { projs, dates } => cmd::report_intervals(config, dates, projs),
564 RC::Chart { dates } => cmd::chart_daily(config, dates)
565 }
566 }
567}