mod storage; mod utils; mod remote; use chrono::{DateTime, Local, Utc}; use clap::{Parser, Subcommand}; use colored::Colorize; use const_format::concatcp; use git_version::git_version; use regex::Regex; use storage::{open_sqlite_storage, Memo, MemoStorage, AuthStorage, StateStorage, SQLiteStorage}; use utils::{find_by_regex, parse_human_duration, HumanDisplay}; use remote::RemoteSync; use notify_rust::Notification; const GIT_VERSION: &str = git_version!(); const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); const VERSION: &str = concatcp!(PKG_VERSION, "-", GIT_VERSION); #[derive(Parser)] #[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(short, long, help = "location for database file")] path: Option, } #[derive(Subcommand)] 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 home_path = std::env!("HOME").to_string(); //this is linux-only let mut db_path: std::path::PathBuf = dirs::home_dir().unwrap(); db_path.push(".local/share/memo-cli.db"); //+ "/.local/share/memo-cli.db"; //WARNING: this is only for testing, it has no proper error handling and will panic is HOME is invalid or None if let Some(db) = args.path { db_path = std::path::PathBuf::from(db); } let storage = open_sqlite_storage(db_path.to_str().unwrap(), true).unwrap(); 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.fetch(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; } 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(); } 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.store(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()); } } }