Compare commits
13 Commits
3c132528e3
...
b1d1c5d2b2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1d1c5d2b2 | ||
|
|
4d8fd0597b | ||
|
|
8f8202282f | ||
|
|
e81f221910 | ||
|
|
7ad6dad214 | ||
|
|
c7163268d4 | ||
|
|
e90cf02664 | ||
|
|
7129723442 | ||
|
|
74b02118bc | ||
|
|
c7b0fc8dba | ||
|
|
50e9c80473 | ||
|
|
c5ed06b76c | ||
|
|
039c4dad04 |
138
pttodoer/Cargo.lock
generated
138
pttodoer/Cargo.lock
generated
@@ -26,6 +26,55 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.3.0"
|
||||
@@ -64,6 +113,52 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.6"
|
||||
@@ -91,6 +186,12 @@ version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.60"
|
||||
@@ -124,6 +225,12 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
@@ -186,8 +293,10 @@ name = "pttodoer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"croner",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
]
|
||||
|
||||
@@ -237,18 +346,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.200"
|
||||
version = "1.0.202"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
|
||||
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.200"
|
||||
version = "1.0.202"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
|
||||
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -268,6 +377,12 @@ dependencies = [
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.61"
|
||||
@@ -291,6 +406,12 @@ version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.92"
|
||||
@@ -354,6 +475,15 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.5"
|
||||
|
||||
@@ -7,6 +7,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.38"
|
||||
clap = { version = "4.4.8", features = ["derive"] }
|
||||
croner = "2.0.4"
|
||||
regex = "1.10.4"
|
||||
serde = { version = "1.0.202", features = [ "serde_derive" ] }
|
||||
serde_yaml = "0.9.34"
|
||||
|
||||
@@ -1,29 +1,391 @@
|
||||
use serde_yaml;
|
||||
use serde::ser::{Serialize};
|
||||
use serde;
|
||||
use chrono::{DateTime, Local};
|
||||
use chrono::naive::NaiveDateTime;
|
||||
use regex::Regex;
|
||||
use croner;
|
||||
use clap::Parser;
|
||||
|
||||
fn main() {
|
||||
println!("{:?}", Task::new())
|
||||
let flags = Flags::new();
|
||||
|
||||
let mut db = DB::new(flags.path).unwrap();
|
||||
|
||||
match flags.add {
|
||||
Some(s) => {
|
||||
let t = match flags.add_schedule {
|
||||
Some(sch) => Task::from_string_schedule(s, sch),
|
||||
None => Task::from_string(s),
|
||||
};
|
||||
db.0[0].tasks.0.push(t);
|
||||
// TODO save
|
||||
},
|
||||
_ => {},
|
||||
};
|
||||
|
||||
println!("{}", db.due().to_string());
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Flags {
|
||||
#[arg(short = 'f', long = "path", default_value="$PTTODO_FILE")]
|
||||
path: String,
|
||||
|
||||
#[arg(short = 'a', long = "add")]
|
||||
add: Option<String>,
|
||||
|
||||
#[arg(short = 's', long = "add-schedule")]
|
||||
add_schedule: Option<String>,
|
||||
|
||||
#[arg(short = 'e', long = "edit", default_value="false")]
|
||||
edit: bool,
|
||||
}
|
||||
|
||||
impl Flags {
|
||||
fn new() -> Flags {
|
||||
let mut result = Flags::parse();
|
||||
|
||||
if result.path.get(..1) == Some("$") {
|
||||
result.path = std::env::var(
|
||||
result.path.get(1..).unwrap()
|
||||
).expect(format!("'{}' unset", result.path).as_str());
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Task(serde_yaml::Mapping);
|
||||
//pub todo: String,
|
||||
//pub detail: Option<String>,
|
||||
//pub sub: Vec<Task>,
|
||||
pub struct DB(Vec<TasksAndMetadata>);
|
||||
|
||||
impl DB {
|
||||
pub fn new(path: String) -> Result<DB, String> {
|
||||
let metadata = match std::fs::metadata(path.clone()) {
|
||||
Ok(v) => Ok(v),
|
||||
Err(msg) => Err(format!("failed to load {}: {}", path, msg)),
|
||||
}?;
|
||||
let mut files = vec![];
|
||||
if metadata.is_file() {
|
||||
files.push(path.clone());
|
||||
} else if metadata.is_dir() {
|
||||
match std::fs::read_dir(path.clone()) {
|
||||
Ok(paths) => {
|
||||
files.extend(paths
|
||||
.filter(|x| x.is_ok())
|
||||
.map(|x| x.unwrap())
|
||||
.filter(|x| x.metadata().unwrap().is_file())
|
||||
.map(|x| x.path().display().to_string())
|
||||
);
|
||||
Ok(())
|
||||
},
|
||||
Err(msg) => Err(format!("failed to read {}: {}", path.clone(), msg)),
|
||||
}?;
|
||||
}
|
||||
|
||||
let mut result = vec![];
|
||||
for file in files {
|
||||
let item = TasksAndMetadata::new(file)?;
|
||||
result.push(item);
|
||||
}
|
||||
Ok(DB(result))
|
||||
}
|
||||
|
||||
pub fn incomplete(&self) -> Tasks {
|
||||
let mut result = Tasks::new();
|
||||
for set in &self.0 {
|
||||
result.0.extend(set.tasks.incomplete().0);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn due(&self) -> Tasks {
|
||||
let mut result = Tasks::new();
|
||||
for set in &self.0 {
|
||||
result.0.extend(set.tasks.due().0);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_taskss {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn read_dir_files() {
|
||||
let db = DB::new("./src/testdata/taskss.d/files.d".to_string()).expect("failed to construct from dir of files");
|
||||
assert_eq!(2, db.0.len());
|
||||
assert_eq!(2, db.due().len());
|
||||
assert_eq!(2, db.incomplete().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_dir_file() {
|
||||
let db = DB::new("./src/testdata/taskss.d/file.d".to_string()).expect("failed to construct from dir of a single file");
|
||||
assert_eq!(1, db.0.len());
|
||||
assert_eq!(1, db.due().len());
|
||||
assert_eq!(1, db.incomplete().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_single_file() {
|
||||
let db = DB::new("./src/testdata/taskss.d/single_file.yaml".to_string()).expect("failed to construct from single file");
|
||||
assert_eq!(1, db.0.len());
|
||||
assert_eq!(1, db.due().len());
|
||||
assert_eq!(2, db.incomplete().len());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TasksAndMetadata {
|
||||
tasks: Tasks,
|
||||
file: String,
|
||||
version: TS,
|
||||
}
|
||||
|
||||
impl TasksAndMetadata {
|
||||
pub fn new(file: String) -> Result<TasksAndMetadata, String> {
|
||||
let version = match std::fs::metadata(file.clone()) {
|
||||
Ok(m) => Ok(TS::from_system_time(m.modified().unwrap())),
|
||||
Err(msg) => Err(format!("couldnt get version from {}: {}", file, msg)),
|
||||
}?;
|
||||
match Tasks::from_file(file.clone()) {
|
||||
Ok(tasks) => Ok(TasksAndMetadata{
|
||||
tasks: tasks,
|
||||
file: file,
|
||||
version: version,
|
||||
}),
|
||||
Err(msg) => Err(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Tasks(Vec<Task>);
|
||||
|
||||
impl Tasks {
|
||||
pub fn new() -> Tasks {
|
||||
Tasks(vec![])
|
||||
}
|
||||
|
||||
pub fn from_file(path: String) -> Result<Tasks, String> {
|
||||
let r = match std::fs::File::open(path.clone()) {
|
||||
Ok(f) => Ok(f),
|
||||
Err(msg) => Err(format!("could not open {}: {}", path, msg)),
|
||||
}?;
|
||||
Tasks::from_reader(r)
|
||||
}
|
||||
|
||||
pub fn from_reader(r: impl std::io::Read) -> Result<Tasks, String> {
|
||||
let result = Tasks::_from_reader(r)?;
|
||||
if !result.is_legacy() {
|
||||
return Ok(result);
|
||||
}
|
||||
let mut v2 = Tasks::new();
|
||||
for k in vec!["todo", "scheduled", "done"] {
|
||||
v2.0.extend(
|
||||
result.0[0]
|
||||
.get_value(k.to_string())
|
||||
.or(Some(serde_yaml::Value::Null))
|
||||
.unwrap()
|
||||
.as_sequence()
|
||||
.or(Some(&vec![]))
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|v| {
|
||||
Task::from_value(match k {
|
||||
"done" => {
|
||||
let mut t = Task::new();
|
||||
t.set_value(
|
||||
"_done".to_string(),
|
||||
v.clone(),
|
||||
);
|
||||
serde_yaml::Value::from(t.0)
|
||||
},
|
||||
_ => v.clone(),
|
||||
})
|
||||
})
|
||||
);
|
||||
}
|
||||
Ok(v2)
|
||||
}
|
||||
|
||||
fn _from_reader(mut r: impl std::io::Read) -> Result<Tasks, String> {
|
||||
let mut buff = String::new();
|
||||
match r.read_to_string(&mut buff) {
|
||||
Err(msg) => {
|
||||
return Err(format!("failed to read body: {}", msg));
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let mut result = Tasks::new();
|
||||
match serde_yaml::from_str::<Vec<serde_yaml::Value>>(&buff) {
|
||||
Ok(v) => {
|
||||
result.0.extend(v.iter().map(|x| {
|
||||
Task::from_value(x.clone())
|
||||
}));
|
||||
Ok(result)
|
||||
},
|
||||
Err(msg) => match Task::from_str(buff) {
|
||||
Ok(t) => {
|
||||
result.0.push(t);
|
||||
Ok(result)
|
||||
},
|
||||
Err(_) => Err(format!("failed to parse yaml: {}", msg)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
let mut buffer = Vec::new();
|
||||
let mut serializer = serde_yaml::Serializer::new(&mut buffer);
|
||||
_ = serde_yaml::Value::Sequence(
|
||||
self.0.iter()
|
||||
.map(|x| {
|
||||
let mut x = x.clone();
|
||||
if x.is_due() {
|
||||
x.unset("ts".to_string());
|
||||
}
|
||||
x
|
||||
})
|
||||
.map(|x| {
|
||||
let is = x.get_value("is".to_string());
|
||||
if x.0.len() == 1 && is.is_some() {
|
||||
return is.unwrap();
|
||||
}
|
||||
serde_yaml::Value::from(x.0.clone())
|
||||
})
|
||||
.map(|x| serde_yaml::Value::from(x.clone()))
|
||||
.collect()
|
||||
).serialize(&mut serializer)
|
||||
.expect("failed to serialize");
|
||||
String::from_utf8(buffer).expect("illegal utf8 characters found")
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn incomplete(&self) -> Tasks {
|
||||
Tasks(self.0.iter()
|
||||
.filter(|x| !x.is_done())
|
||||
.map(|x| x.clone())
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn due(&self) -> Tasks {
|
||||
Tasks(self.0.iter()
|
||||
.filter(|x| x.is_due())
|
||||
.map(|x| x.clone())
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
|
||||
fn is_legacy(&self) -> bool {
|
||||
self.len() == 1
|
||||
&& self.0[0].get_value("done".to_string()).is_some()
|
||||
&& self.0[0].get_value("scheduled".to_string()).is_some()
|
||||
&& self.0[0].get_value("todo".to_string()).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_tasks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn due() {
|
||||
let tasks = Tasks::from_file("./src/testdata/tasks_due_scheduled_done.yaml".to_string()).expect("failed to open file");
|
||||
eprintln!("{:?}", tasks);
|
||||
assert_eq!(2, tasks.due().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_reader_legacy() {
|
||||
let tasks = Tasks::from_reader(
|
||||
std::fs::File::open("./src/testdata/legacy.yaml").expect("failed to open file")
|
||||
).expect("failed to read file");
|
||||
assert_eq!(8, tasks.len());
|
||||
assert_eq!(5, tasks.due().len());
|
||||
assert_eq!("a".to_string(), tasks.due().0[0].get("is".to_string()).expect("missing 0th is"));
|
||||
assert_eq!("b".to_string(), tasks.due().0[1].get("todo".to_string()).expect("missing 1st todo"));
|
||||
assert_eq!("c".to_string(), tasks.due().0[2].get("is".to_string()).expect("missing 2nd is"));
|
||||
assert_eq!("d".to_string(), tasks.due().0[3].get("todo".to_string()).expect("missing 3rd todo"));
|
||||
assert_eq!("e".to_string(), tasks.due().0[4].get("todo".to_string()).expect("missing 4th todo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_reader() {
|
||||
let tasks = Tasks::from_reader(
|
||||
std::fs::File::open("./src/testdata/mvp.yaml").expect("failed to open file")
|
||||
).expect("failed to read file");
|
||||
assert_eq!(2, tasks.0.len());
|
||||
assert_eq!(1, tasks.0[0].0.len());
|
||||
assert!(tasks.0[0].get("is".to_string()).is_some());
|
||||
assert_eq!("x".to_string(), tasks.0[0].get("is".to_string()).unwrap());
|
||||
assert_eq!(1, tasks.0[1].0.len());
|
||||
assert_eq!("y and z".to_string(), tasks.0[1].get("is".to_string()).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
|
||||
pub struct Task(serde_yaml::Mapping);
|
||||
|
||||
impl Task {
|
||||
fn new() -> Task {
|
||||
pub fn new() -> Task {
|
||||
Task(serde_yaml::Mapping::new())
|
||||
}
|
||||
|
||||
fn is_due(&self) -> bool {
|
||||
self.is_due_now(TS::now())
|
||||
pub fn from_string(s: String) -> Task {
|
||||
let mut t = Task::new();
|
||||
t.set("is".to_string(), s);
|
||||
t
|
||||
}
|
||||
|
||||
fn is_due_now(&self, now: TS) -> bool {
|
||||
pub fn from_string_schedule(s: String, schedule: String) -> Task {
|
||||
let mut t = Task::new();
|
||||
t.set("is".to_string(), s);
|
||||
t.set("schedule".to_string(), schedule);
|
||||
t
|
||||
}
|
||||
|
||||
pub fn from_reader(mut r: impl std::io::Read) -> Result<Task, String> {
|
||||
let mut buff = String::new();
|
||||
match r.read_to_string(&mut buff) {
|
||||
Err(msg) => Err(format!("failed to read body: {}", msg)),
|
||||
_ => Task::from_str(buff),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: String) -> Result<Task, String> {
|
||||
match serde_yaml::from_str::<serde_yaml::Value>(&s) {
|
||||
Ok(v) => Ok(Task::from_value(v)),
|
||||
Err(msg) => Err(format!("failed to read value: {}", msg)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_value(v: serde_yaml::Value) -> Task {
|
||||
let mut result = Task::new();
|
||||
match v.as_mapping() {
|
||||
Some(m) => { result.0 = m.clone(); },
|
||||
None => { result.set_value("is".to_string(), v); },
|
||||
};
|
||||
result
|
||||
}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.get_value("_done".to_string()).is_some()
|
||||
}
|
||||
|
||||
pub fn is_due(&self) -> bool {
|
||||
self.is_due_at(TS::now()) && !self.is_done()
|
||||
}
|
||||
|
||||
fn is_due_at(&self, now: TS) -> bool {
|
||||
match self.when() {
|
||||
Some(when) => {
|
||||
now.unix() >= when.next(self.ts()).unix()
|
||||
@@ -56,24 +418,38 @@ impl Task {
|
||||
}
|
||||
|
||||
fn get(&self, k: String) -> Option<String> {
|
||||
match self.get_value(k) {
|
||||
None => None,
|
||||
Some(v) => match v.as_str() {
|
||||
Some(s) => Some(s.to_string()),
|
||||
None => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_value(&self, k: String) -> Option<serde_yaml::Value> {
|
||||
match self.0.get(k) {
|
||||
Some(v) => Some(
|
||||
serde_yaml::to_string(v)
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.trim()
|
||||
.trim_start_matches('\'')
|
||||
.trim_end_matches( '\'')
|
||||
.to_string()
|
||||
),
|
||||
Some(v) => Some(v.clone()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unset(&mut self, k: String) {
|
||||
self.0.remove(serde_yaml::Value::String(k));
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // used in test
|
||||
fn set(&mut self, k: String, v: String) {
|
||||
self.0.insert(
|
||||
serde_yaml::Value::String(k),
|
||||
serde_yaml::Value::String(v)
|
||||
serde_yaml::Value::String(v),
|
||||
);
|
||||
}
|
||||
|
||||
fn set_value(&mut self, k: String, v: serde_yaml::Value) {
|
||||
self.0.insert(
|
||||
serde_yaml::Value::String(k),
|
||||
v
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -82,6 +458,49 @@ impl Task {
|
||||
mod test_task {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert!(Task::from_str("{ invalid".to_string()).is_err());
|
||||
assert!(Task::from_str("1".to_string()).is_ok());
|
||||
assert!(Task::from_str("'1'".to_string()).is_ok());
|
||||
assert!(Task::from_str("null".to_string()).is_ok());
|
||||
assert!(Task::from_str("true".to_string()).is_ok());
|
||||
assert!(Task::from_str("[]".to_string()).is_ok());
|
||||
assert!(Task::from_str("{}".to_string()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_reader() {
|
||||
let task = Task::from_reader(
|
||||
std::fs::File::open("./src/testdata/mvp.uuid-123-456-xyz.yaml").expect("failed to open file")
|
||||
).expect("failed to read 123...");
|
||||
assert_eq!(1, task.0.len());
|
||||
assert!(task.get("is".to_string()).is_some());
|
||||
assert_eq!("plaintext".to_string(), task.get("is".to_string()).unwrap());
|
||||
|
||||
let task = Task::from_reader(
|
||||
std::fs::File::open("./src/testdata/mvp.uuid-789-012-abc.yaml").expect("failed to open file")
|
||||
).expect("failed to read 789...");
|
||||
assert_eq!(3, task.0.len());
|
||||
assert!(task.get("is".to_string()).is_none());
|
||||
assert_eq!("todo here".to_string(), task.get("todo".to_string()).unwrap());
|
||||
assert_eq!("* * * * *".to_string(), task.get("schedule".to_string()).unwrap());
|
||||
assert_eq!("hello world\n".to_string(), task.get("details".to_string()).unwrap());
|
||||
assert!(task.is_due());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_done() {
|
||||
let mut t = Task::new();
|
||||
t.set("_done".to_string(), "anything".to_string());
|
||||
eprintln!("{:?}", t.0);
|
||||
assert!(t.is_done());
|
||||
|
||||
t.set_value("_done".to_string(), serde_yaml::Value::Null);
|
||||
eprintln!("{:?}", t.0);
|
||||
assert!(t.is_done());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crud() {
|
||||
let mut t = Task::new();
|
||||
@@ -100,9 +519,9 @@ mod test_task {
|
||||
let now = TS::new("2000-01-02T16:00Z".to_string()).unwrap();
|
||||
t.set("ts".to_string(), then.to_string());
|
||||
t.set("schedule".to_string(), "1h".to_string());
|
||||
assert!(!t.is_due_now(TS::from_unix(now.unix()-1)));
|
||||
assert!(t.is_due_now(TS::from_unix(now.unix())));
|
||||
assert!(t.is_due_now(TS::from_unix(now.unix()+1)));
|
||||
assert!(!t.is_due_at(TS::from_unix(now.unix()-1)));
|
||||
assert!(t.is_due_at(TS::from_unix(now.unix())));
|
||||
assert!(t.is_due_at(TS::from_unix(now.unix()+1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -113,9 +532,9 @@ mod test_task {
|
||||
let now = TS::new("2000-01-02T16:00Z".to_string()).unwrap();
|
||||
t.set("ts".to_string(), then.to_string());
|
||||
t.set("schedule".to_string(), "2000-01-02T16:00Z".to_string());
|
||||
assert!(!t.is_due_now(TS::from_unix(now.unix()-1)));
|
||||
assert!(t.is_due_now(TS::from_unix(now.unix())));
|
||||
assert!(t.is_due_now(TS::from_unix(now.unix()+1)));
|
||||
assert!(!t.is_due_at(TS::from_unix(now.unix()-1)));
|
||||
assert!(t.is_due_at(TS::from_unix(now.unix())));
|
||||
assert!(t.is_due_at(TS::from_unix(now.unix()+1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -126,9 +545,9 @@ mod test_task {
|
||||
let now = TS::new("2000-01-02T16:00Z".to_string()).unwrap();
|
||||
t.set("ts".to_string(), then.to_string());
|
||||
t.set("schedule".to_string(), "0 16 * * *".to_string());
|
||||
assert!(!t.is_due_now(TS::from_unix(now.unix()-1)));
|
||||
assert!(t.is_due_now(TS::from_unix(now.unix())));
|
||||
assert!(t.is_due_now(TS::from_unix(now.unix()+1)));
|
||||
assert!(!t.is_due_at(TS::from_unix(now.unix()-1)));
|
||||
assert!(t.is_due_at(TS::from_unix(now.unix())));
|
||||
assert!(t.is_due_at(TS::from_unix(now.unix()+1)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,7 +694,7 @@ mod test_cron {
|
||||
#[test]
|
||||
fn parse() {
|
||||
match Cron::new("* * * * *".to_string()) {
|
||||
Ok(c) => {}
|
||||
Ok(_) => {}
|
||||
Err(err) => assert!(false, "failed to parse cron: {}", err),
|
||||
};
|
||||
match Cron::new("1 * * * *".to_string()) {
|
||||
@@ -359,11 +778,16 @@ impl TS {
|
||||
Self::from_unix(Local::now().timestamp() as u64)
|
||||
}
|
||||
|
||||
fn from_system_time(st: std::time::SystemTime) -> TS {
|
||||
TS::from_unix(st.duration_since(std::time::SystemTime::UNIX_EPOCH).unwrap().as_secs())
|
||||
}
|
||||
|
||||
fn from_unix(src: u64) -> TS {
|
||||
TS{0: src}
|
||||
}
|
||||
|
||||
fn new(src: String) -> Result<TS, String> {
|
||||
// %Y-%m-%dT%H:%MZ
|
||||
match DateTime::parse_from_str(
|
||||
&format!("{} +0000", src),
|
||||
"%Y-%m-%dT%H:%MZ %z",
|
||||
@@ -371,20 +795,56 @@ impl TS {
|
||||
Ok(v) => { return Ok(TS(v.timestamp() as u64)) },
|
||||
_ => {},
|
||||
};
|
||||
|
||||
// %Y-%m-%dT%H
|
||||
match NaiveDateTime::parse_from_str(
|
||||
&format!("{}:00", src),
|
||||
"%Y-%m-%dT%H:%M",
|
||||
) {
|
||||
Ok(v) => { return Ok(TS(v.timestamp() as u64)) },
|
||||
Ok(v) => { return Ok(TS(v.and_utc().timestamp() as u64)) },
|
||||
_ => {},
|
||||
};
|
||||
Err(format!("cannot parse date format from {}", src))
|
||||
|
||||
// %Y-%m-%d
|
||||
match NaiveDateTime::parse_from_str(
|
||||
&format!("{}T00:00", src),
|
||||
"%Y-%m-%dT%H:%M",
|
||||
) {
|
||||
Ok(v) => { return Ok(TS(v.and_utc().timestamp() as u64)) },
|
||||
_ => {},
|
||||
};
|
||||
|
||||
// Sun Dec 3 23:29:27 EST 2023
|
||||
match DateTime::parse_from_str(
|
||||
&format!("{}", src)
|
||||
.replace("PDT", "-0800")
|
||||
.replace("MDT", "-0700")
|
||||
.replace("EDT", "-0400")
|
||||
.replace("PST", "-0700")
|
||||
.replace("MST", "-0600")
|
||||
.replace("EST", "-0500")
|
||||
.replace(" 1 ", " 01 ")
|
||||
.replace(" 2 ", " 02 ")
|
||||
.replace(" 3 ", " 03 ")
|
||||
.replace(" 4 ", " 04 ")
|
||||
.replace(" 5 ", " 05 ")
|
||||
.replace(" 6 ", " 06 ")
|
||||
.replace(" 7 ", " 07 ")
|
||||
.replace(" 8 ", " 08 ")
|
||||
.replace(" 9 ", " 09 ")
|
||||
.replace(" ", " "),
|
||||
"%a %b %d %H:%M:%S %z %Y",
|
||||
) {
|
||||
Ok(v) => Ok(TS(v.timestamp() as u64)),
|
||||
Err(msg) => Err(format!("failed to parse legacy golang time: {}", msg)),
|
||||
}
|
||||
}
|
||||
|
||||
fn unix(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // used in test
|
||||
fn to_string(&self) -> String {
|
||||
DateTime::from_timestamp(self.0 as i64, 0)
|
||||
.unwrap()
|
||||
@@ -399,6 +859,42 @@ mod test_ts {
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
match TS::new("Tue Nov 7 07:33:11 PST 2023".to_string()) {
|
||||
Ok(ts) => {
|
||||
assert_eq!("2023-11-07T14:33Z", ts.to_string());
|
||||
},
|
||||
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
||||
};
|
||||
match TS::new("Sun Jun 4 05:24:32 PDT 2023".to_string()) {
|
||||
Ok(ts) => {
|
||||
assert_eq!("2023-06-04T13:24Z", ts.to_string());
|
||||
},
|
||||
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
||||
};
|
||||
match TS::new("Sat Nov 4 08:36:01 MDT 2023".to_string()) {
|
||||
Ok(ts) => {
|
||||
assert_eq!("2023-11-04T15:36Z", ts.to_string());
|
||||
},
|
||||
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
||||
};
|
||||
match TS::new("Sun Nov 5 22:27:17 MST 2023".to_string()) {
|
||||
Ok(ts) => {
|
||||
assert_eq!("2023-11-06T04:27Z", ts.to_string());
|
||||
},
|
||||
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
||||
};
|
||||
match TS::new("Thu Apr 18 16:17:41 EDT 2024".to_string()) {
|
||||
Ok(ts) => {
|
||||
assert_eq!("2024-04-18T20:17Z", ts.to_string());
|
||||
},
|
||||
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
||||
};
|
||||
match TS::new("Sun Dec 3 23:29:27 EST 2023".to_string()) {
|
||||
Ok(ts) => {
|
||||
assert_eq!("2023-12-04T04:29Z", ts.to_string());
|
||||
},
|
||||
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
||||
};
|
||||
match TS::new("2024-05-01T00:00Z".to_string()) {
|
||||
Ok(ts) => {
|
||||
assert_eq!(1714521600, ts.unix());
|
||||
@@ -413,5 +909,12 @@ mod test_ts {
|
||||
},
|
||||
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
||||
};
|
||||
match TS::new("2024-05-01".to_string()) {
|
||||
Ok(ts) => {
|
||||
assert_eq!(1714521600, ts.unix());
|
||||
assert_eq!("2024-05-01T00:00Z", ts.to_string());
|
||||
},
|
||||
Err(err) => assert!(false, "failed to parse ts: {}", err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
17
pttodoer/src/testdata/legacy.yaml
vendored
Normal file
17
pttodoer/src/testdata/legacy.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
todo:
|
||||
- a
|
||||
- todo: b
|
||||
schedule: 2000-01-01
|
||||
scheduled:
|
||||
- c
|
||||
- todo: d
|
||||
schedule: 2000-02-01
|
||||
- todo: e
|
||||
ts: Sun Dec 3 23:29:27 EST 2023
|
||||
schedule: '0 0 1 1 *'
|
||||
- todo: f
|
||||
schedule: 2099-02-02
|
||||
done:
|
||||
- g
|
||||
- todo: h
|
||||
ts: Sun Dec 3 23:29:27 EST 2023
|
||||
@@ -1,4 +1,4 @@
|
||||
todo: todo here
|
||||
schedule: * * * * *
|
||||
schedule: '* * * * *'
|
||||
details: |
|
||||
hello world
|
||||
|
||||
10
pttodoer/src/testdata/tasks_due_scheduled_done.yaml
vendored
Normal file
10
pttodoer/src/testdata/tasks_due_scheduled_done.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
- 1do: due
|
||||
schedule: 2006-04-05
|
||||
- 2do: scheduled
|
||||
schedule: 2099-04-05
|
||||
- 3do: repeating scheduled
|
||||
schedule: 0 0 1 1 *
|
||||
ts: 2001-02-03T04:05:06Z
|
||||
- _done:
|
||||
do: done
|
||||
schedule: 2006-04-05
|
||||
1
pttodoer/src/testdata/taskss.d/file.d/file.yaml
vendored
Normal file
1
pttodoer/src/testdata/taskss.d/file.d/file.yaml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
- file
|
||||
1
pttodoer/src/testdata/taskss.d/files.d/a.yaml
vendored
Normal file
1
pttodoer/src/testdata/taskss.d/files.d/a.yaml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
- a
|
||||
1
pttodoer/src/testdata/taskss.d/files.d/b.yaml
vendored
Normal file
1
pttodoer/src/testdata/taskss.d/files.d/b.yaml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
- b
|
||||
4
pttodoer/src/testdata/taskss.d/single_file.yaml
vendored
Normal file
4
pttodoer/src/testdata/taskss.d/single_file.yaml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
- todo
|
||||
- do: scheduled
|
||||
schedule: 2099-01-01
|
||||
- _done: any
|
||||
Reference in New Issue
Block a user