use std::iter::once;
use std::num::NonZeroU32;
use std::path::PathBuf;
use std::result;
use clap::{Args, Parser, Subcommand};
use once_cell::sync::Lazy;
use regex::Regex;
use crate::date::Time;
#[derive(Args, Default)]
struct VecString {
args: Vec<String>
}
#[derive(Args, Default)]
struct OptString {
arg: Option<String>
}
use crate::config::{Config, DEFAULT_CONF};
#[doc(inline)]
use crate::error::Error;
#[doc(inline)]
use crate::error::PathError;
pub mod args;
pub mod cmd;
pub use args::DateRangeArgs;
pub use args::FilterArgs;
const BIN_NAME: &str = "rtimelog";
static SUBST_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\{\}").expect("Template pattern must be correct"));
#[derive(Clone, Copy)]
enum ExpandAlias {
Yes,
No
}
const TASK_DESC: &str = "The command takes a 'task description' consisting of an optional project \
formatted with a leading '+', an optional task name formatted with a \
leading '@', and potentially more text adding details to the task. If no \
task name starting with '@' is supplied, any extra text is treated as \
the task.";
const DATE_DESC: &str = "The 'date range description' consists of a single date string or a pair \
of date strings of the form 'YYYY-MM-DD', or one of a set of relative \
date strings including: today, yesterday, sunday, monday, tuesday, \
wednesday, thursday, friday, or saturday. The first two are obvious. The \
others refer to the previous instance of that day. The range can also be \
described by a month name (like january), the string 'ytd', or a range \
specified by 'this' or 'last' followed by 'week', 'month', or 'year'.";
static FULL_DESC: Lazy<String> = Lazy::new(|| format!("{TASK_DESC}\n\n{DATE_DESC}"));
#[derive(Subcommand)]
enum StackCommands {
Clear,
Drop {
#[arg(name = "num", default_value = "1")]
num: NonZeroU32
},
Keep {
#[arg(name = "num", default_value = "10")]
num: NonZeroU32
},
Ls,
Top
}
#[derive(Subcommand)]
enum EntryCommands {
Discard,
Now,
Ignore,
#[command(after_help = TASK_DESC)]
Rewrite {
#[arg(name = "task_desc")]
task: Vec<String>
},
Was {
time: Time
},
Rewind {
minutes: NonZeroU32
}
}
#[derive(Subcommand)]
enum ReportCommands {
#[command(after_help = DATE_DESC)]
Detail {
#[arg(name = "proj", short, long = "proj")]
projs: Vec<String>,
#[arg(name = "date_range")]
dates: Vec<String>
},
#[command(after_help = DATE_DESC)]
Summary {
#[arg(name = "proj", short, long = "proj")]
projs: Vec<String>,
#[arg(name = "date_range")]
dates: Vec<String>
},
#[command(after_help = DATE_DESC)]
Hours {
#[arg(name = "proj", short, long = "proj")]
projs: Vec<String>,
#[arg(name = "date_range")]
dates: Vec<String>
},
#[command(after_help = DATE_DESC)]
Events {
#[arg(short)]
compact: bool,
#[arg(name = "proj", short, long = "proj")]
projs: Vec<String>,
#[arg(name = "date_range")]
dates: Vec<String>
},
#[command(after_help = DATE_DESC)]
Intervals {
#[arg(name = "proj", short, long = "proj")]
projs: Vec<String>,
#[arg(name = "date_range")]
dates: Vec<String>
},
#[command(after_help = DATE_DESC)]
Chart {
#[arg(name = "date_range")]
dates: Vec<String>
}
}
#[derive(Subcommand)]
enum Subcommands {
Init {
#[arg(name = "dir")]
dir: Option<String>
},
#[command(after_help = TASK_DESC)]
Start {
#[arg(name = "task_desc")]
task: Vec<String>
},
Stop,
#[command(after_help = TASK_DESC)]
Push {
#[arg(name = "task_desc")]
task: Vec<String>
},
Resume,
Pause,
Swap,
#[command(after_help = DATE_DESC)]
Ls {
#[arg(short)]
all: bool,
#[arg(name = "date_desc")]
date: Option<String>
},
Comment(VecString),
Event(VecString),
Lsproj,
Edit,
Curr,
Check,
Archive,
Aliases,
#[command(subcommand)]
Report(ReportCommands),
#[command(subcommand)]
Stack(StackCommands),
#[command(subcommand)]
Entry(EntryCommands),
#[command(external_subcommand)]
Other(Vec<String>)
}
#[derive(Parser)]
#[command(author, name = "rtimelog", version, about, long_about = None, after_help = FULL_DESC.as_str())]
pub struct Cli {
#[arg(long, name = "dir")]
dir: Option<PathBuf>,
#[arg(long)]
editor: Option<PathBuf>,
#[arg(long, name = "filepath")]
conf: Option<PathBuf>,
#[arg(long)]
browser: Option<String>,
#[command(subcommand)]
cmd: Option<Subcommands>
}
impl Cli {
pub fn run(&self) -> crate::Result<()> {
let config = self.config()?;
match self.cmd {
Some(ref cmd) => cmd.run(&config, ExpandAlias::Yes),
None => Subcommands::default_command(&config).run(&config, ExpandAlias::No)
}
}
fn run_alias(&self, config: &Config, expand: ExpandAlias) -> crate::Result<()> {
match self.cmd {
Some(ref cmd) => cmd.run(config, expand),
None => Subcommands::default_command(config).run(config, ExpandAlias::No)
}
}
fn config(&self) -> result::Result<Config, PathError> {
let mut config = match self.conf {
Some(ref conf_file) => {
Config::from_file(conf_file.to_str().ok_or(PathError::FilenameMissing)?)
}
None => Config::from_file(&DEFAULT_CONF)
}
.unwrap_or_default();
if let Some(dir) = self.dir.as_ref() {
config.set_dir(
dir.to_str()
.ok_or_else(|| PathError::InvalidPath(String::new(), String::new()))?
);
}
if let Some(editor) = self.editor.as_ref() {
config.set_editor(editor.to_str().ok_or(PathError::FilenameMissing)?);
}
if let Some(browser) = self.browser.as_ref() {
config.set_browser(browser);
}
Ok(config)
}
}
impl Subcommands {
pub fn run(&self, config: &Config, expand: ExpandAlias) -> crate::Result<()> {
use Subcommands as C;
match self {
C::Init { dir } => Ok(cmd::initialize(config, dir.as_ref().map(String::as_str))?),
C::Start { task } => cmd::start_task(config, task),
C::Stop => cmd::stop_task(config),
C::Comment(VecString { args }) => cmd::add_comment(config, args),
C::Event(VecString { args }) => cmd::add_event(config, args),
C::Push { task } => cmd::push_task(config, task),
C::Resume => cmd::resume_task(config),
C::Pause => cmd::pause_task(config),
C::Swap => cmd::swap_entry(config),
C::Ls { all, date } => cmd::list_entries(config, date.as_ref().map(String::as_str), *all),
C::Lsproj => cmd::list_projects(config),
C::Edit => cmd::edit(config),
C::Curr => cmd::current_task(config),
C::Check => cmd::check_logfile(config),
C::Archive => cmd::archive_year(config),
C::Aliases => {
cmd::list_aliases(config);
Ok(())
}
C::Report(cmd) => cmd.run(config),
C::Stack(cmd) => cmd.run(config),
C::Entry(cmd) => cmd.run(config),
C::Other(args) => match expand {
ExpandAlias::Yes => Self::expand_alias(config, args),
ExpandAlias::No => Err(Error::InvalidCommand(args[0].clone()))
}
}
}
fn default_command(config: &Config) -> Self {
match config.defcmd() {
"init" => Self::Init { dir: None },
"start" => Self::Start { task: Vec::new() },
"stop" => Self::Stop,
"push" => Self::Push { task: Vec::new() },
"resume" => Self::Resume,
"pause" => Self::Pause,
"swap" => Self::Swap,
"ls" => Self::Ls { all: false, date: None },
"lsproj" => Self::Lsproj,
"edit" => Self::Edit,
"curr" => Self::Curr,
"archive" => Self::Archive,
"aliases" => Self::Aliases,
_ => Self::Curr
}
}
fn expand_alias(config: &Config, args: &[String]) -> crate::Result<()> {
let alias = &args[0];
let mut args_iter = args[1..].iter().map(String::as_str);
let expand: Vec<String> = config
.alias(alias)
.ok_or_else(|| Error::InvalidCommand(alias.clone()))?
.split(' ')
.map(|w| {
if SUBST_RE.is_match(w) {
args_iter.next().map_or_else(
|| w.to_string(),
|val| SUBST_RE.replace(w, val).into_owned()
)
}
else {
w.to_string()
}
})
.collect();
let cmd = Cli::parse_from(
once(BIN_NAME)
.chain(expand.iter().map(String::as_str))
.chain(args_iter)
);
cmd.run_alias(config, ExpandAlias::No)
}
}
impl StackCommands {
pub fn run(&self, config: &Config) -> crate::Result<()> {
use StackCommands as SC;
match self {
SC::Clear => cmd::stack_clear(config),
SC::Drop { num } => cmd::stack_drop(config, *num),
SC::Keep { num } => cmd::stack_keep(config, *num),
SC::Ls => cmd::list_stack(config),
SC::Top => cmd::stack_top(config)
}
}
}
impl EntryCommands {
pub fn run(&self, config: &Config) -> crate::Result<()> {
use EntryCommands as EC;
match self {
EC::Discard => cmd::discard_last_entry(config),
EC::Now => cmd::reset_last_entry(config),
EC::Ignore => cmd::ignore_last_entry(config),
EC::Rewrite { task } => cmd::rewrite_last_entry(config, task),
EC::Was { time } => cmd::retime_last_entry(config, *time),
EC::Rewind { minutes } => cmd::rewind_last_entry(config, *minutes)
}
}
}
impl ReportCommands {
pub fn run(&self, config: &Config) -> crate::Result<()> {
use ReportCommands as RC;
match self {
RC::Detail { projs, dates } => cmd::report_daily(config, dates, projs),
RC::Summary { projs, dates } => cmd::report_summary(config, dates, projs),
RC::Hours { projs, dates } => cmd::report_hours(config, dates, projs),
RC::Events { compact, projs, dates } => {
cmd::report_events(config, dates, projs, *compact)
}
RC::Intervals { projs, dates } => cmd::report_intervals(config, dates, projs),
RC::Chart { dates } => cmd::chart_daily(config, dates)
}
}
}