mirror of
https://git.alemi.dev/memo-cli.git
synced 2024-11-14 05:29:20 +01:00
command overhaul, finished core features, made it modular
This commit is contained in:
parent
ec1e1bf586
commit
0fce795d3a
4 changed files with 278 additions and 112 deletions
|
@ -6,6 +6,7 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argparse = "0.2.2"
|
regex = "1"
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
|
clap = { version = "3.1.6", features = ["derive"] }
|
||||||
rusqlite = { version="0.27.0", features=["chrono"] }
|
rusqlite = { version="0.27.0", features=["chrono"] }
|
||||||
|
|
198
src/main.rs
198
src/main.rs
|
@ -1,128 +1,104 @@
|
||||||
extern crate argparse;
|
mod storage;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
use argparse::{ArgumentParser, Store, StoreTrue};
|
use chrono::{DateTime, Utc};
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use clap::{Parser, Subcommand};
|
||||||
use rusqlite::{params, Connection, Error};
|
use regex::Regex;
|
||||||
|
pub use storage::{open_sqlite_storage, Memo, MemoStorage};
|
||||||
|
use utils::{parse_human_duration, HumanDisplay};
|
||||||
|
|
||||||
struct Memo {
|
#[derive(Parser)]
|
||||||
id: u32,
|
#[clap(author, version, about, long_about = None)]
|
||||||
body: String,
|
#[clap(propagate_version = true, disable_colored_help = true)]
|
||||||
due: DateTime<Utc>,
|
#[clap(subcommand_required = false)]
|
||||||
|
#[clap(disable_help_subcommand = true)]
|
||||||
|
struct Cli {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
command: Option<Commands>,
|
||||||
|
|
||||||
|
#[clap(short, long, help = "location for database file")]
|
||||||
|
db_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_db(path: &str) -> Result<Connection, Error> {
|
#[derive(Subcommand)]
|
||||||
let connection = Connection::open(path)?;
|
enum Commands {
|
||||||
|
#[clap(trailing_var_arg = true)]
|
||||||
connection.execute(
|
New {
|
||||||
"CREATE TABLE IF NOT EXISTS memo (
|
#[clap(multiple_values = true)]
|
||||||
id INTEGER PRIMARY KEY,
|
#[clap(min_values = 1)]
|
||||||
body TEXT NOT NULL,
|
#[clap(required = true)]
|
||||||
due DATETIME
|
body: Vec<String>,
|
||||||
);",
|
#[clap(short, long, help = "due time relative to now")]
|
||||||
[],
|
due: Option<String>,
|
||||||
)?;
|
},
|
||||||
|
Done {
|
||||||
return Ok(connection);
|
search: String,
|
||||||
}
|
#[clap(long)]
|
||||||
|
many: bool,
|
||||||
fn all(conn: Connection) -> Result<Vec<Memo>, rusqlite::Error> {
|
},
|
||||||
let mut statement = conn.prepare("SELECT * FROM memo ORDER BY due, id")?;
|
Del {
|
||||||
let mut rows = statement.query([])?;
|
id: u32,
|
||||||
|
},
|
||||||
let mut results = Vec::new();
|
|
||||||
|
|
||||||
while let Some(row) = rows.next()? {
|
|
||||||
results.push(Memo {
|
|
||||||
id: row.get(0)?,
|
|
||||||
body: row.get(1)?,
|
|
||||||
due: row.get(2)?,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PrettyPrint {
|
|
||||||
fn pretty(&self) -> String;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrettyPrint for Duration {
|
|
||||||
fn pretty(&self) -> String {
|
|
||||||
if self.num_days().abs() > 400 {
|
|
||||||
return format!("{}y {}m", self.num_days().abs() / 365, (self.num_days() % (30*12)) / 30);
|
|
||||||
} else if self.num_days().abs() >= 365 {
|
|
||||||
return format!("{}y", self.num_days().abs() / 365);
|
|
||||||
} else if self.num_days().abs() >= 90 {
|
|
||||||
return format!("{}m", self.num_days().abs() / 30); // sort of
|
|
||||||
} else if self.num_days().abs() >= 1 {
|
|
||||||
return format!("{}d", self.num_days().abs());
|
|
||||||
} else if self.num_hours().abs() > 0 {
|
|
||||||
let h = self.num_minutes().abs() / 60;
|
|
||||||
let m = self.num_minutes().abs() % 60;
|
|
||||||
return format!("{}h {}min", h, m);
|
|
||||||
} else if self.num_minutes().abs() > 0 {
|
|
||||||
return format!("{}min", self.num_minutes().abs());
|
|
||||||
} else if self.num_seconds().abs() > 0 {
|
|
||||||
return format!("{}s", self.num_seconds().abs());
|
|
||||||
}
|
|
||||||
return self.to_string();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
let args = Cli::parse();
|
||||||
let home_path = std::env!("HOME").to_string();
|
let home_path = std::env!("HOME").to_string();
|
||||||
if home_path.len() < 1 {
|
|
||||||
panic!("Cannot work without a home folder");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut version = false;
|
|
||||||
let mut memo: String = "".to_string();
|
|
||||||
let mut db_path: String = home_path + "/.local/share/memo-cli.db";
|
let mut db_path: String = home_path + "/.local/share/memo-cli.db";
|
||||||
|
|
||||||
{
|
if let Some(db) = args.db_path {
|
||||||
// this block limits scope of borrows by ap.refer() method
|
db_path = db;
|
||||||
let mut ap = ArgumentParser::new();
|
|
||||||
ap.set_description("A simple tool to remember things");
|
|
||||||
ap.refer(&mut version).add_option(
|
|
||||||
&["-V", "--version"],
|
|
||||||
StoreTrue,
|
|
||||||
"Show current version and die",
|
|
||||||
);
|
|
||||||
ap.refer(&mut memo)
|
|
||||||
.add_option(&["-n", "--new"], Store, "Add a new memo");
|
|
||||||
ap.refer(&mut db_path)
|
|
||||||
.add_option(&["--path"], Store, "Specify db path");
|
|
||||||
|
|
||||||
ap.parse_args_or_exit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if version {
|
let storage = open_sqlite_storage(&db_path).unwrap();
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
println!("memo-cli v{}", VERSION);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let connection =
|
match args.command {
|
||||||
init_db(&db_path).unwrap_or_else(|err| panic!("Could not open db : {}", err.to_string()));
|
Some(Commands::New { body, due }) => {
|
||||||
|
let mut due_date: Option<DateTime<Utc>> = None;
|
||||||
if memo.len() > 0 {
|
if let Some(d) = due {
|
||||||
connection
|
if d.len() > 0 {
|
||||||
.execute(
|
due_date = Some(Utc::now() + parse_human_duration(d.as_str()).unwrap());
|
||||||
"INSERT INTO memo (body, due) VALUES (?, ?)",
|
}
|
||||||
params![memo, Utc::now()],
|
}
|
||||||
)
|
let txt = body.join(" ");
|
||||||
.unwrap_or_else(|err| panic!("Could not insert into db : {}", err.to_string()));
|
storage.add(txt.as_str(), due_date).unwrap();
|
||||||
println!("New memo : '{}'", memo);
|
println!("new memo: '{}'", txt);
|
||||||
} else {
|
}
|
||||||
let zero_duration = Duration::seconds(0);
|
Some(Commands::Done { search, many }) => {
|
||||||
let memos = all(connection)
|
let rex = Regex::new(search.as_str());
|
||||||
.unwrap_or_else(|err| panic!("Could not read all memos : {}", err.to_string()));
|
let mut found = false;
|
||||||
for m in memos {
|
let mut to_remove: Option<Memo> = None;
|
||||||
let delta = m.due - Utc::now();
|
if let Some(re) = rex.ok() {
|
||||||
if delta.le(&zero_duration) {
|
for memo in storage.all().unwrap() {
|
||||||
println!("[*] {} : +{} \t[{}]", m.body, delta.pretty(), m.id);
|
if re.is_match(memo.body.as_str()) {
|
||||||
|
if many {
|
||||||
|
storage.del(memo.id).unwrap();
|
||||||
|
println!("[-] task #{} done", memo.id);
|
||||||
|
} 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!("[-] task #{} done", rm.id);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
println!(" * {} : -{} \t[{}]", m.body, delta.pretty(), m.id);
|
println!("[!] invalid regex");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Commands::Del { id }) => {
|
||||||
|
storage.del(id).unwrap();
|
||||||
|
println!("[-] task #{} deleted", id);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
for m in storage.all().unwrap() {
|
||||||
|
println!("{}", m.human());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
107
src/storage.rs
Normal file
107
src/storage.rs
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use rusqlite::{params, Connection, Error};
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
pub struct Memo {
|
||||||
|
pub id: u32,
|
||||||
|
pub body: String,
|
||||||
|
pub due: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Memo {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let mut due_str = "null".to_string();
|
||||||
|
if self.due.is_some() {
|
||||||
|
due_str = self.due.unwrap().to_string();
|
||||||
|
}
|
||||||
|
return write!(
|
||||||
|
f,
|
||||||
|
"Memo(id={id}, body={body}, due={due})",
|
||||||
|
id = self.id,
|
||||||
|
body = self.body,
|
||||||
|
due = due_str,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MemoStorage {
|
||||||
|
fn all(&self) -> Result<Vec<Memo>, Error>;
|
||||||
|
fn add(&self, body: &str, due: Option<DateTime<Utc>>) -> Result<(), Error>;
|
||||||
|
fn del(&self, id: u32) -> Result<bool, Error>;
|
||||||
|
fn get(&self, id: u32) -> Result<Memo, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLiteStorage
|
||||||
|
|
||||||
|
pub struct SQLiteStorage {
|
||||||
|
conn: Connection,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_sqlite_storage(path: &str) -> Result<SQLiteStorage, Error> {
|
||||||
|
let connection = Connection::open(path)?;
|
||||||
|
// TODO check if table exist and is valid
|
||||||
|
connection.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS memo (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
body TEXT NOT NULL,
|
||||||
|
due DATETIME
|
||||||
|
);",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
return Ok(SQLiteStorage { conn: connection });
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoStorage for SQLiteStorage {
|
||||||
|
fn all(&self) -> Result<Vec<Memo>, Error> {
|
||||||
|
let mut statement = self.conn.prepare("SELECT * FROM memo ORDER BY due, id")?;
|
||||||
|
let mut rows = statement.query([])?;
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
while let Some(row) = rows.next()? {
|
||||||
|
results.push(Memo {
|
||||||
|
id: row.get(0)?,
|
||||||
|
body: row.get(1)?,
|
||||||
|
due: row.get(2)?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add(&self, body: &str, due: Option<DateTime<Utc>>) -> Result<(), Error> {
|
||||||
|
// TODO join these 2 ifs?
|
||||||
|
if due.is_some() {
|
||||||
|
self.conn.execute(
|
||||||
|
"INSERT INTO memo (body, due) VALUES (?, ?)",
|
||||||
|
params![body, due],
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
self.conn
|
||||||
|
.execute("INSERT INTO memo (body) VALUES (?)", params![body])?;
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn del(&self, id: u32) -> Result<bool, Error> {
|
||||||
|
let count = self
|
||||||
|
.conn
|
||||||
|
.execute("DELETE FROM memo WHERE id = ?", params![id])?;
|
||||||
|
if count > 0 {
|
||||||
|
return Ok(true);
|
||||||
|
} else {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, id: u32) -> Result<Memo, Error> {
|
||||||
|
return Ok(self
|
||||||
|
.conn
|
||||||
|
.query_row("SELECT * FROM memo WHERE id = ?", params![id], |row| {
|
||||||
|
return Ok(Memo {
|
||||||
|
id: row.get(0)?,
|
||||||
|
body: row.get(1)?,
|
||||||
|
due: row.get(2).unwrap_or(None),
|
||||||
|
});
|
||||||
|
})?);
|
||||||
|
}
|
||||||
|
}
|
82
src/utils.rs
Normal file
82
src/utils.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
use crate::storage::Memo;
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
|
use regex::{Error, Regex};
|
||||||
|
|
||||||
|
pub trait HumanDisplay {
|
||||||
|
fn human(&self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HumanDisplay for Duration {
|
||||||
|
fn human(&self) -> String {
|
||||||
|
if self.num_days().abs() > 400 {
|
||||||
|
return format!(
|
||||||
|
"{}y {}m",
|
||||||
|
self.num_days().abs() / 365,
|
||||||
|
(self.num_days() % (30 * 12)) / 30
|
||||||
|
);
|
||||||
|
} else if self.num_days().abs() >= 365 {
|
||||||
|
return format!("{}y", self.num_days().abs() / 365);
|
||||||
|
} else if self.num_days().abs() >= 90 {
|
||||||
|
return format!("{}m", self.num_days().abs() / 30); // sort of
|
||||||
|
} else if self.num_days().abs() >= 1 {
|
||||||
|
return format!("{}d", self.num_days().abs());
|
||||||
|
} else if self.num_hours().abs() > 0 {
|
||||||
|
let h = self.num_minutes().abs() / 60;
|
||||||
|
let m = self.num_minutes().abs() % 60;
|
||||||
|
return format!("{}h {}min", h, m);
|
||||||
|
} else if self.num_minutes().abs() > 0 {
|
||||||
|
return format!("{}min", self.num_minutes().abs());
|
||||||
|
} else if self.num_seconds().abs() > 0 {
|
||||||
|
return format!("{}s", self.num_seconds().abs());
|
||||||
|
}
|
||||||
|
return self.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HumanDisplay for Memo {
|
||||||
|
fn human(&self) -> String {
|
||||||
|
if self.due.is_some() {
|
||||||
|
let delta = self.due.unwrap() - Utc::now();
|
||||||
|
if delta.le(&Duration::seconds(0)) {
|
||||||
|
return format!("[*] {} (+{})", self.body, delta.human());
|
||||||
|
} else {
|
||||||
|
return format!(" * {} (-{})", self.body, delta.human());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return format!(" * {}", self.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_human_duration(input: &str) -> Result<Duration, Error> {
|
||||||
|
let mut secs: i64 = 0;
|
||||||
|
println!("{}", input);
|
||||||
|
|
||||||
|
for (token, mult) in [("d", 60 * 60 * 24), ("h", 60 * 60), ("m", 60)] {
|
||||||
|
let re = Regex::new(format!("((?:-|)[0-9]+){token}", token = token).as_str())?;
|
||||||
|
if let Some(captures) = re.captures(input) {
|
||||||
|
if let Some(cap) = captures.get(1) {
|
||||||
|
secs += mult * cap.as_str().parse::<i64>().unwrap(); // safely unwrap because regex gave only digits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(Duration::seconds(secs));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO is it possible to make this a method?
|
||||||
|
pub fn vec_to_str<T: std::fmt::Display>(arr: &Vec<T>) -> String {
|
||||||
|
let mut out = String::default();
|
||||||
|
let mut first = true;
|
||||||
|
out += "[";
|
||||||
|
for el in arr {
|
||||||
|
if !first {
|
||||||
|
out += ",";
|
||||||
|
}
|
||||||
|
out += " ";
|
||||||
|
out += el.to_string().as_mut_str();
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
out += " ]";
|
||||||
|
return out;
|
||||||
|
}
|
Loading…
Reference in a new issue