2022-03-26 21:48:18 +01:00
|
|
|
mod remote;
|
2022-03-14 03:41:26 +01:00
|
|
|
mod storage;
|
|
|
|
mod utils;
|
|
|
|
|
2022-03-16 02:07:02 +01:00
|
|
|
use chrono::{DateTime, Local, Utc};
|
2022-03-14 03:41:26 +01:00
|
|
|
use clap::{Parser, Subcommand};
|
2022-03-16 02:10:22 +01:00
|
|
|
use colored::Colorize;
|
2022-03-16 02:07:02 +01:00
|
|
|
use const_format::concatcp;
|
|
|
|
use git_version::git_version;
|
2022-03-26 21:48:18 +01:00
|
|
|
use notify_rust::Notification;
|
2022-03-14 03:41:26 +01:00
|
|
|
use regex::Regex;
|
2022-03-20 23:48:33 +01:00
|
|
|
use remote::RemoteSync;
|
2022-03-28 01:52:05 +02:00
|
|
|
use storage::{AuthStorage, Memo, MemoError, MemoStorage, SQLiteStorage, StateStorage, JsonStorage, SUPPORTED_FORMATS};
|
|
|
|
use utils::{find_by_regex, find_db_file, parse_human_duration, HumanDisplay};
|
2022-03-27 17:12:13 +02:00
|
|
|
use std::path::PathBuf;
|
2022-03-15 22:17:24 +01:00
|
|
|
|
|
|
|
const GIT_VERSION: &str = git_version!();
|
|
|
|
const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
|
|
const VERSION: &str = concatcp!(PKG_VERSION, "-", GIT_VERSION);
|
2022-03-14 03:41:26 +01:00
|
|
|
|
2022-03-28 01:52:05 +02:00
|
|
|
#[derive(Parser, Clone)]
|
2022-03-15 22:17:24 +01:00
|
|
|
#[clap(author, about, long_about = None)]
|
|
|
|
#[clap(version = VERSION)]
|
2022-03-14 23:12:41 +01:00
|
|
|
#[clap(disable_colored_help = true)]
|
2022-03-14 03:41:26 +01:00
|
|
|
#[clap(subcommand_required = false)]
|
|
|
|
#[clap(disable_help_subcommand = true)]
|
|
|
|
struct Cli {
|
|
|
|
#[clap(subcommand)]
|
|
|
|
command: Option<Commands>,
|
2022-03-14 23:12:41 +01:00
|
|
|
#[clap(short, long, help = "show memos in a notification")]
|
|
|
|
notify: bool,
|
2022-03-14 22:41:32 +01:00
|
|
|
#[clap(long, help = "show completed tasks")]
|
|
|
|
old: bool,
|
2022-03-20 23:48:33 +01:00
|
|
|
#[clap(short, long, help = "synchronize memo db")]
|
|
|
|
sync: bool,
|
2022-03-28 01:52:05 +02:00
|
|
|
#[clap(long, help = "name of db file, without extension")]
|
|
|
|
name: Option<String>,
|
2022-03-26 21:48:18 +01:00
|
|
|
#[clap(short, long, help = "specify location for database file")]
|
2022-03-15 22:17:24 +01:00
|
|
|
path: Option<String>,
|
2022-03-13 20:13:13 +01:00
|
|
|
}
|
|
|
|
|
2022-03-28 01:52:05 +02:00
|
|
|
#[derive(Subcommand, Clone)]
|
2022-03-14 03:41:26 +01:00
|
|
|
enum Commands {
|
2022-03-15 22:17:24 +01:00
|
|
|
/// create a new memo
|
2022-03-14 03:41:26 +01:00
|
|
|
#[clap(trailing_var_arg = true)]
|
|
|
|
New {
|
|
|
|
#[clap(multiple_values = true)]
|
|
|
|
#[clap(min_values = 1)]
|
|
|
|
#[clap(required = true)]
|
|
|
|
body: Vec<String>,
|
|
|
|
#[clap(short, long, help = "due time relative to now")]
|
2022-03-14 23:12:41 +01:00
|
|
|
due: Option<String>, // TODO allow to pass date
|
2022-03-14 03:41:26 +01:00
|
|
|
},
|
2022-03-15 22:17:24 +01:00
|
|
|
/// mark existing memo as done
|
2022-03-14 03:41:26 +01:00
|
|
|
Done {
|
|
|
|
search: String,
|
2022-03-14 23:12:41 +01:00
|
|
|
#[clap(long, help = "delete more than one task if matched")]
|
2022-03-14 03:41:26 +01:00
|
|
|
many: bool,
|
2022-03-16 02:07:02 +01:00
|
|
|
},
|
|
|
|
/// change existing memo
|
|
|
|
Edit {
|
|
|
|
search: String,
|
|
|
|
#[clap(short, long, help = "set memo message")]
|
|
|
|
body: Option<String>,
|
|
|
|
#[clap(short, long, help = "set due time relative to now")]
|
|
|
|
due: Option<String>, // TODO allow to pass date
|
|
|
|
},
|
2022-03-13 20:13:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
2022-03-14 03:41:26 +01:00
|
|
|
let args = Cli::parse();
|
2022-03-28 01:52:05 +02:00
|
|
|
let args2 = args.clone(); // TODO args is already partially moved so I can't pass it all to run_commands()
|
2022-03-13 20:13:13 +01:00
|
|
|
|
2022-03-27 17:12:13 +02:00
|
|
|
//might want to move the pathfinding to its own function
|
|
|
|
let mut db_path: PathBuf;
|
2022-03-28 01:52:05 +02:00
|
|
|
let filename = args.name.unwrap_or("memostorage".to_string());
|
2022-03-27 17:12:13 +02:00
|
|
|
|
2022-03-28 01:52:05 +02:00
|
|
|
if let Some(db) = args.path { // if we are given a specific path, just use that
|
2022-03-27 17:12:13 +02:00
|
|
|
db_path = PathBuf::from(db);
|
2022-03-28 01:52:05 +02:00
|
|
|
} 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
|
2022-03-13 20:13:13 +01:00
|
|
|
}
|
|
|
|
|
2022-03-27 17:12:13 +02:00
|
|
|
//TODO: permissions check, this will panic if it finds an existing db it can't write to
|
|
|
|
|
2022-03-23 02:42:08 +01:00
|
|
|
|
2022-03-28 01:52:05 +02:00
|
|
|
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<T>(mut storage: T, args: Cli) -> Result<(), MemoError>
|
|
|
|
where T : MemoStorage + AuthStorage + StateStorage + RemoteSync
|
|
|
|
{
|
2022-03-20 23:48:33 +01:00
|
|
|
if args.sync {
|
2022-03-23 02:42:08 +01:00
|
|
|
if storage.get_key().is_err() {
|
|
|
|
let key = rpassword::prompt_password("[$] new passphrase: ").unwrap();
|
|
|
|
storage.set_key(key.as_str()).unwrap();
|
|
|
|
}
|
2022-03-26 21:48:18 +01:00
|
|
|
let res = storage.download(
|
|
|
|
storage.get_hash().unwrap().as_str(),
|
|
|
|
"http://127.0.0.1:8443",
|
|
|
|
);
|
2022-03-20 23:48:33 +01:00
|
|
|
if res.is_ok() {
|
|
|
|
println!("[v] downloaded remote db");
|
|
|
|
} else {
|
2022-03-26 21:48:18 +01:00
|
|
|
println!(
|
|
|
|
"[!] could not fetch db : {}",
|
|
|
|
res.err().unwrap().to_string()
|
|
|
|
);
|
2022-03-20 23:48:33 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-28 01:52:05 +02:00
|
|
|
match &args.command {
|
2022-03-14 03:41:26 +01:00
|
|
|
Some(Commands::New { body, due }) => {
|
|
|
|
let mut due_date: Option<DateTime<Utc>> = 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();
|
2022-03-16 02:07:02 +01:00
|
|
|
println!("{} new memo: {}", "[+]".bold(), txt);
|
2022-03-14 03:41:26 +01:00
|
|
|
}
|
|
|
|
Some(Commands::Done { search, many }) => {
|
|
|
|
let rex = Regex::new(search.as_str());
|
|
|
|
let mut found = false;
|
|
|
|
let mut to_remove: Option<Memo> = None;
|
|
|
|
if let Some(re) = rex.ok() {
|
2022-03-14 22:41:32 +01:00
|
|
|
for memo in storage.all(false).unwrap() {
|
2022-03-14 03:41:26 +01:00
|
|
|
if re.is_match(memo.body.as_str()) {
|
2022-03-28 01:52:05 +02:00
|
|
|
if *many {
|
2022-03-23 02:42:08 +01:00
|
|
|
storage.del(&memo.id).unwrap();
|
2022-03-17 01:58:22 +01:00
|
|
|
println!("[-] done task : {}", memo.body);
|
2022-03-14 03:41:26 +01:00
|
|
|
} 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 {
|
2022-03-23 02:42:08 +01:00
|
|
|
storage.del(&rm.id).unwrap();
|
2022-03-17 01:58:22 +01:00
|
|
|
println!("{} done memo: {}", "[-]".bold(), rm.body);
|
2022-03-14 03:41:26 +01:00
|
|
|
}
|
2022-03-13 20:13:13 +01:00
|
|
|
} else {
|
2022-03-17 01:58:22 +01:00
|
|
|
println!("{} invalid regex", format!("[{}]", "!".red()).bold());
|
2022-03-14 03:41:26 +01:00
|
|
|
}
|
|
|
|
}
|
2022-03-16 02:07:02 +01:00
|
|
|
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 {
|
2022-03-28 01:52:05 +02:00
|
|
|
m.body = b.to_owned();
|
2022-03-16 02:07:02 +01:00
|
|
|
}
|
|
|
|
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();
|
2022-03-17 01:58:22 +01:00
|
|
|
println!(
|
|
|
|
"{} updated memo\n{}",
|
|
|
|
format!("[{}]", ">".green()).bold().to_string(),
|
|
|
|
m.colored(),
|
|
|
|
);
|
2022-03-16 02:07:02 +01:00
|
|
|
}
|
2022-03-14 03:41:26 +01:00
|
|
|
None => {
|
2022-03-14 22:41:32 +01:00
|
|
|
let all = storage.all(args.old).unwrap();
|
2022-03-14 23:12:41 +01:00
|
|
|
let mut builder = String::new();
|
2022-03-22 01:54:05 +01:00
|
|
|
let timing = if let Some(state) = storage.get_state().ok() {
|
|
|
|
let now = Local::now();
|
2022-03-26 21:48:18 +01:00
|
|
|
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()
|
|
|
|
};
|
2022-03-14 23:12:41 +01:00
|
|
|
if args.old {
|
|
|
|
builder.push_str("Archived memos:\n");
|
|
|
|
}
|
2022-03-14 03:49:35 +01:00
|
|
|
if all.len() < 1 {
|
2022-03-14 23:12:41 +01:00
|
|
|
builder.push_str("[ ] nothing to remember\n");
|
2022-03-14 03:49:35 +01:00
|
|
|
}
|
|
|
|
for m in all {
|
2022-03-17 01:58:22 +01:00
|
|
|
let tmp = if args.notify { m.human() } else { m.colored() };
|
|
|
|
builder.push_str(tmp.as_str());
|
2022-03-14 23:12:41 +01:00
|
|
|
builder.push('\n');
|
|
|
|
}
|
|
|
|
if args.notify {
|
2022-03-23 20:01:24 +01:00
|
|
|
Notification::new()
|
|
|
|
.summary(format!("memo-cli | {}", timing).as_str())
|
|
|
|
.body(builder.as_str())
|
|
|
|
//.icon("") //soon...
|
2022-03-28 01:52:05 +02:00
|
|
|
.show()
|
|
|
|
.unwrap();
|
2022-03-14 23:12:41 +01:00
|
|
|
} else {
|
2022-03-22 01:54:05 +01:00
|
|
|
println!("{} | {}", "memo-cli".bold(), timing);
|
2022-03-14 23:12:41 +01:00
|
|
|
print!("{}", builder);
|
2022-03-13 20:13:13 +01:00
|
|
|
}
|
2022-03-23 02:42:08 +01:00
|
|
|
|
2022-03-26 21:48:18 +01:00
|
|
|
if !args.notify {
|
|
|
|
// shitty assumption: notification is run by a daemon TODO
|
2022-03-23 02:42:08 +01:00
|
|
|
storage.set_run_time(Utc::now()).unwrap();
|
|
|
|
}
|
2022-03-13 20:13:13 +01:00
|
|
|
}
|
|
|
|
}
|
2022-03-20 23:48:33 +01:00
|
|
|
|
|
|
|
if args.sync {
|
2022-03-26 21:48:18 +01:00
|
|
|
let res = storage.upload(
|
|
|
|
storage.get_hash().unwrap().as_str(),
|
|
|
|
"http://127.0.0.1:8443",
|
|
|
|
);
|
2022-03-20 23:48:33 +01:00
|
|
|
if res.is_ok() {
|
|
|
|
println!("[^] uploaded local db");
|
2022-03-22 01:54:05 +01:00
|
|
|
storage.set_sync_time(Utc::now()).unwrap();
|
2022-03-20 23:48:33 +01:00
|
|
|
} else {
|
2022-03-26 21:48:18 +01:00
|
|
|
println!(
|
|
|
|
"[!] could not upload db : {}",
|
|
|
|
res.err().unwrap().to_string()
|
|
|
|
);
|
2022-03-20 23:48:33 +01:00
|
|
|
}
|
|
|
|
}
|
2022-03-28 01:52:05 +02:00
|
|
|
|
|
|
|
Ok(())
|
2022-03-13 20:13:13 +01:00
|
|
|
}
|