memo-cli/src/main.rs

243 lines
6.7 KiB
Rust
Raw Normal View History

mod remote;
mod storage;
mod utils;
2022-03-16 02:07:02 +01:00
use chrono::{DateTime, Local, Utc};
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;
use notify_rust::Notification;
use regex::Regex;
2022-03-20 23:48:33 +01:00
use remote::RemoteSync;
use storage::{AuthStorage, Memo, MemoStorage, SQLiteStorage, StateStorage, JsonStorage};
use utils::{find_by_regex, parse_human_duration, HumanDisplay};
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);
#[derive(Parser)]
2022-03-15 22:17:24 +01:00
#[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<Commands>,
//#[clap(short, long, help = "name of db file")]
//name: Option<String>,
#[clap(short, long, help = "show memos in a notification")]
notify: bool,
#[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,
#[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
}
#[derive(Subcommand)]
enum Commands {
2022-03-15 22:17:24 +01:00
/// create a new memo
#[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")]
due: Option<String>, // TODO allow to pass date
},
2022-03-15 22:17:24 +01:00
/// mark existing memo as done
Done {
search: String,
#[clap(long, help = "delete more than one task if matched")]
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() {
let args = Cli::parse();
2022-03-13 20:13:13 +01:00
//might want to move the pathfinding to its own function
let mut db_path: PathBuf;
let filename = "memostorage.db"; //TODO: make this configurable
if let Some(db) = args.path { //if we are given a starting path from command line, set it to that
db_path = PathBuf::from(db);
} else { //else use the current path, also should not browse up if path is specified
db_path = std::env::current_dir().unwrap();
db_path.push(filename); //check if file exists in current folder
while !db_path.exists() && !db_path.parent().is_none() {
db_path.pop().pop(); //browse parent folders until the given filename (or root) is found
db_path.push(filename); //we want to know about the db file, not the folder itself
}
if (!db_path.exists()) { //fallback to default
//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
}
2022-03-13 20:13:13 +01:00
}
//TODO: permissions check, this will panic if it finds an existing db it can't write to
let mut storage = SQLiteStorage::new(db_path.to_str().unwrap(), true).unwrap();
2022-03-20 23:48:33 +01:00
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",
);
2022-03-20 23:48:33 +01:00
if res.is_ok() {
println!("[v] downloaded remote db");
} else {
println!(
"[!] could not fetch db : {}",
res.err().unwrap().to_string()
);
2022-03-20 23:48:33 +01:00
}
}
match args.command {
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);
}
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() {
for memo in storage.all(false).unwrap() {
if re.is_match(memo.body.as_str()) {
if many {
storage.del(&memo.id).unwrap();
2022-03-17 01:58:22 +01:00
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();
2022-03-17 01:58:22 +01:00
println!("{} done memo: {}", "[-]".bold(), rm.body);
}
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-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 {
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();
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
}
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");
}
2022-03-14 03:49:35 +01:00
if all.len() < 1 {
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());
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);
2022-03-13 20:13:13 +01:00
}
if !args.notify {
// shitty assumption: notification is run by a daemon TODO
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 {
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");
storage.set_sync_time(Utc::now()).unwrap();
2022-03-20 23:48:33 +01:00
} else {
println!(
"[!] could not upload db : {}",
res.err().unwrap().to_string()
);
2022-03-20 23:48:33 +01:00
}
}
2022-03-13 20:13:13 +01:00
}