pub(crate) use config::Config;
pub(crate) use info::Info;
use crate::{
database::Database,
jobber::{JobberGet, JobberIter},
};
use clap::{Parser, Subcommand};
use color_print::cformat;
pub(crate) use error::*;
pub(crate) use interpret::*;
use std::{fs::OpenOptions, path::PathBuf};
mod add;
mod calendar;
mod cancel;
mod completions;
mod config;
mod create;
mod delete;
mod edit;
mod end;
mod error;
mod export;
mod info;
mod init;
mod interpret;
mod list;
mod markdown_doc;
mod restore;
mod select;
mod show;
mod sort;
mod start;
mod undo;
use add::Add;
use calendar::Calendar;
use cancel::Cancel;
use completions::Completions;
use create::Create;
use delete::Delete;
use edit::Edit;
use end::End;
use export::Export;
use init::Init;
use list::List;
use markdown_doc::MarkdownDoc;
use restore::Restore;
use select::Select;
use show::Show;
use sort::Sort;
use start::Start;
use undo::Undo;
#[derive(Parser)]
#[clap(verbatim_doc_comment)]
pub(crate) struct Cli {
#[command(subcommand)]
pub(crate) command: Option<Command>,
#[clap(short, long)]
file: Option<PathBuf>,
#[clap(short = 'a', long)]
no_ansi: bool,
#[clap(short = 't', long)]
pub no_tips: bool,
}
impl Cli {
fn database_file(&self) -> Result<std::path::PathBuf> {
if let Some(file) = &self.file {
Ok(file.clone())
} else {
Config::load()?.database_path()
}
}
pub(crate) fn open_database(&self) -> Result<Database> {
let database_file = self.database_file()?;
let target = &database_file.canonicalize()?;
match OpenOptions::new().read(true).open(target) {
Ok(file) => match serde_json::from_reader(file) {
Ok(database) => Ok(database),
Err(err) => Err(Error::CannotReadDatabase(database_file, err)),
},
Err(err) => Err(Error::CannotOpenDatabase(database_file, err)),
}
}
pub(crate) fn close_database(&self, mut database: Database) -> Result<()> {
let database_file = self.database_file()?;
let target = &database_file.canonicalize()?;
let tmp = target.with_extension("tmp");
let bak = target.with_extension("bak");
database.reset_version();
match OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&tmp)
{
Ok(file) => match serde_json::to_writer_pretty(file, &database) {
Ok(database) => {
std::fs::rename(target, bak)?;
std::fs::rename(tmp, target)?;
eprintln!("Database has been updated.");
Ok(database)
}
Err(err) => Err(Error::CannotWriteDatabase(database_file, err)),
},
Err(err) => Err(Error::CannotOpenDatabase(database_file, err)),
}
}
pub(crate) fn backup_database(&self) -> Result<()> {
let target = &self.database_file()?.canonicalize()?;
let bak = target.with_extension("bak");
if std::fs::exists(target)? {
std::fs::copy(target, bak)?;
}
Ok(())
}
pub(crate) fn restore_database(&self) -> Result<()> {
let target = &self.database_file()?.canonicalize()?;
let tmp = &target.with_extension("tmp");
let bak = &target.with_extension("bak");
std::fs::rename(target, tmp)?;
std::fs::rename(bak, target)?;
std::fs::rename(tmp, bak)?;
Ok(())
}
pub(crate) fn ansi(&self) -> bool {
!self.no_ansi
}
}
#[derive(Subcommand)]
pub(crate) enum Command {
Init(Init),
Create(Create),
Select(Select),
Start(Start),
End(End),
Add(Add),
Cancel(Cancel),
List(List),
Show(Show),
Calendar(Calendar),
Export(Export),
Info(Info),
Edit(Edit),
Delete(Delete),
Restore(Restore),
Sort(Sort),
Undo(Undo),
Completions(Completions),
#[clap(hide = true)]
MarkdownDoc(MarkdownDoc),
}
impl Command {
pub(crate) fn run(&self, cli: &Cli) -> Result<()> {
match self {
Command::Init(init) => init.run(cli),
Command::Create(create) => create.run(cli),
Command::Select(select) => select.run(cli),
Command::Start(start) => start.run(cli),
Command::End(end) => end.run(cli),
Command::Add(add) => add.run(cli),
Command::Cancel(cancel) => cancel.run(cli),
Command::List(list) => list.run(cli),
Command::Show(show) => show.run(cli),
Command::Calendar(calendar) => calendar.run(cli),
Command::Export(export) => export.run(cli),
Command::Info(info) => info.run(cli),
Command::Edit(edit) => edit.run(cli),
Command::Delete(delete) => delete.run(cli),
Command::Restore(restore) => restore.run(cli),
Command::Sort(sort) => sort.run(cli),
Command::Undo(undo) => undo.run(cli),
Command::Completions(completions) => completions.run(cli),
Command::MarkdownDoc(markdown_doc) => markdown_doc.run(cli),
}
}
}
pub(crate) trait RunCommand<T = ()> {
fn run(&self, cli: &Cli) -> Result<T>;
}
pub(crate) fn current_job(config: &Config, database: &impl JobberGet) -> Result<usize> {
match config.current_job {
Some(pos) => {
if database.get_job(pos)?.deleted {
Err(Error::CurrentJobDeleted(pos))
} else {
Ok(pos)
}
}
None => Err(Error::NoCurrentJob),
}
}
pub(crate) fn current_client(
config: &Config,
database: &(impl JobberGet + JobberIter),
ansi: bool,
) -> Result<String> {
let current_job = current_job(config, database).ok();
if let Some(client_id) =
current_job.and_then(|j| database.get_job(j).ok().map(|j| j.client.clone()))
{
Ok(client_id.to_string())
} else {
Ok(not_available(ansi))
}
}
pub(crate) fn current_worker(
config: &Config,
database: &(impl JobberGet + JobberIter),
ansi: bool,
) -> Result<String> {
let current_job = current_job(config, database).ok();
if let Some(worker_id) =
current_job.and_then(|j| database.get_job(j).ok().map(|j| j.worker.clone()))
{
Ok(worker_id.to_string())
} else {
Ok(not_available(ansi))
}
}
pub(crate) fn not_available(ansi: bool) -> String {
if ansi {
cformat!("<dim>-</>")
} else {
"-".to_string()
}
}
pub(crate) fn is_available(value: &str) -> bool {
value != not_available(true) && value != not_available(false)
}
fn shorten_date(start: &mut String, prev: &str) {
let s: Vec<_> = start.split(' ').collect();
let p: Vec<_> = prev.split(' ').collect();
if s[0] == p[0] {
if s[2] == p[2] && s[3] == p[3] {
*start = s[4..].to_vec().join(" ");
} else {
*start = s[1..].to_vec().join(" ");
}
}
}
#[macro_export]
macro_rules! print_tip {
($cli:ident) => {
if !$cli.no_tips {
color_print::ceprintln!();
}
};
($cli:ident, $($arg:tt)*) => {
if !$cli.no_tips {
color_print::ceprintln!($($arg)*);
}
};
}