mod remote; mod storage; mod utils; use chrono::{DateTime, Local, Utc}; use clap::{Parser, Subcommand}; use colored::Colorize; use const_format::concatcp; use git_version::git_version; use notify_rust::Notification; use regex::Regex; use remote::RemoteSync; use storage::{AuthStorage, Memo, MemoError, MemoStorage, SQLiteStorage, StateStorage, JsonStorage, SUPPORTED_FORMATS}; use utils::{find_by_regex, find_db_file, parse_human_duration, HumanDisplay}; use std::path::PathBuf; const GIT_VERSION: &str = git_version!(); const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); const VERSION: &str = concatcp!(PKG_VERSION, "-", GIT_VERSION); #[derive(Parser, Clone)] #[clap(author, about, long_about = None)] #[clap(version = VERSION)] #[clap(disable_colored_help = true)] #[clap(subcommand_required = false)] #[clap(disable_help_subcommand = true)] struct Cli { #[clap(subcommand)] command: Option, #[clap(short, long, help = "show memos in a notification")] notify: bool, #[clap(long, help = "show completed tasks")] old: bool, #[clap(short, long, help = "synchronize memo db")] sync: bool, #[clap(long, help = "name of db file, without extension")] name: Option, #[clap(short, long, help = "specify location for database file")] path: Option, } #[derive(Subcommand, Clone)] enum Commands { /// create a new memo #[clap(trailing_var_arg = true)] New { #[clap(multiple_values = true)] #[clap(min_values = 1)] #[clap(required = true)] body: Vec, #[clap(short, long, help = "due time relative to now")] due: Option, // TODO allow to pass date }, /// mark existing memo as done Done { search: String, #[clap(long, help = "delete more than one task if matched")] many: bool, }, /// change existing memo Edit { search: String, #[clap(short, long, help = "set memo message")] body: Option, #[clap(short, long, help = "set due time relative to now")] due: Option, // TODO allow to pass date }, } fn main() { let args = Cli::parse(); let args2 = args.clone(); // TODO args is already partially moved so I can't pass it all to run_commands() //might want to move the pathfinding to its own function let mut db_path: PathBuf; let filename = args.name.unwrap_or("memostorage".to_string()); if let Some(db) = args.path { // if we are given a specific path, just use that db_path = PathBuf::from(db); } else if let Some(path) = find_db_file(filename.as_str(), SUPPORTED_FORMATS) { // search up from cwd db_path = path; } else { // default path //TODO: a less "nix-centered" default fallback, possibly configurable //protip: cfg!(windows)/cfg!(unix)/cfg!(target_os = "macos") will give us the info we need db_path = dirs::home_dir().unwrap(); db_path.push(".local/share/"); //default fallback path db_path.push(format!("{}.{}", filename, "db")); // TODO hardcoded default sqlite } //TODO: permissions check, this will panic if it finds an existing db it can't write to if let Some(ext) = db_path.extension() { match ext.to_str().unwrap() { "json" => run_commands(JsonStorage::new(db_path).unwrap(), args2).unwrap(), "db" => run_commands(SQLiteStorage::new(db_path, true).unwrap(), args2).unwrap(), _ => println!("[!] unsupported database"), } } else { println!("[!] no extension on db file"); } } fn run_commands(mut storage: T, args: Cli) -> Result<(), MemoError> where T : MemoStorage + AuthStorage + StateStorage + RemoteSync { if args.sync { if storage.get_key().is_err() { let key = rpassword::prompt_password("[$] new passphrase: ").unwrap(); storage.set_key(key.as_str()).unwrap(); } let res = storage.download( storage.get_hash().unwrap().as_str(), "http://127.0.0.1:8443", ); if res.is_ok() { println!("[v] downloaded remote db"); } else { println!( "[!] could not fetch db : {}", res.err().unwrap().to_string() ); } } match &args.command { Some(Commands::New { body, due }) => { let mut due_date: Option> = None; if let Some(d) = due { if d.len() > 0 { due_date = Some(Utc::now() + parse_human_duration(d.as_str()).unwrap()); } } let txt = body.join(" "); storage.add(txt.as_str(), due_date).unwrap(); println!("{} new memo: {}", "[+]".bold(), txt); } Some(Commands::Done { search, many }) => { let rex = Regex::new(search.as_str()); let mut found = false; let mut to_remove: Option = None; if let Some(re) = rex.ok() { for memo in storage.all(false).unwrap() { if re.is_match(memo.body.as_str()) { if *many { storage.del(&memo.id).unwrap(); println!("[-] done task : {}", memo.body); } else if found { println!("[!] would remove multiple tasks"); to_remove = None; break; } else { to_remove = Some(memo); found = true; } } } if let Some(rm) = to_remove { storage.del(&rm.id).unwrap(); println!("{} done memo: {}", "[-]".bold(), rm.body); } } else { println!("{} invalid regex", format!("[{}]", "!".red()).bold()); } } Some(Commands::Edit { search, body, due }) => { let mut m = find_by_regex( regex::Regex::new(search.as_str()).unwrap(), storage.all(false).unwrap(), ) .unwrap(); if let Some(b) = body { m.body = b.to_owned(); } if let Some(d) = due { if d == "null" || d == "none" { m.due = None } else { m.due = Some(Utc::now() + parse_human_duration(d.as_str()).unwrap()); } } storage.set(&m).unwrap(); println!( "{} updated memo\n{}", format!("[{}]", ">".green()).bold().to_string(), m.colored(), ); } None => { let all = storage.all(args.old).unwrap(); let mut builder = String::new(); let timing = if let Some(state) = storage.get_state().ok() { let now = Local::now(); format!( "last run: {}", state .last_run .with_timezone(&now.timezone()) .format("%a %d/%m %H:%M") ) } else { Local::now().format("%a %d/%m, %H:%M").to_string() }; if args.old { builder.push_str("Archived memos:\n"); } if all.len() < 1 { builder.push_str("[ ] nothing to remember\n"); } for m in all { let tmp = if args.notify { m.human() } else { m.colored() }; builder.push_str(tmp.as_str()); builder.push('\n'); } if args.notify { Notification::new() .summary(format!("memo-cli | {}", timing).as_str()) .body(builder.as_str()) //.icon("") //soon... .show() .unwrap(); } else { println!("{} | {}", "memo-cli".bold(), timing); print!("{}", builder); } if !args.notify { // shitty assumption: notification is run by a daemon TODO storage.set_run_time(Utc::now()).unwrap(); } } } if args.sync { let res = storage.upload( storage.get_hash().unwrap().as_str(), "http://127.0.0.1:8443", ); if res.is_ok() { println!("[^] uploaded local db"); storage.set_sync_time(Utc::now()).unwrap(); } else { println!( "[!] could not upload db : {}", res.err().unwrap().to_string() ); } } Ok(()) }