From cd0dce4168250a3e60ae6fa27c1ffad4efd9831d Mon Sep 17 00:00:00 2001 From: aaron-jack-manning Date: Sun, 21 Aug 2022 16:43:42 +1000 Subject: [PATCH] name to id index and validation on names --- Cargo.lock | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + README.md | 1 - src/edit.rs | 20 +++++--- src/main.rs | 18 ++++--- src/state.rs | 51 +++++++++++++++++-- src/tasks.rs | 67 +++++++++++++------------ src/vault.rs | 3 +- 8 files changed, 242 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 78a92a3..c24bc2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "bitflags" version = "1.3.2" @@ -63,7 +69,7 @@ dependencies = [ "num-integer", "num-traits", "serde", - "time", + "time 0.1.44", "wasm-bindgen", "winapi 0.3.9", ] @@ -172,6 +178,41 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "darling" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "directories" version = "2.0.2" @@ -193,6 +234,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -235,6 +282,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "iana-time-zone" version = "0.1.46" @@ -248,6 +301,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -267,8 +326,15 @@ checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", + "serde", ] +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + [[package]] name = "js-sys" version = "0.3.59" @@ -365,6 +431,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "numtoa" version = "0.1.0" @@ -498,6 +573,12 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + [[package]] name = "scopeguard" version = "1.1.0" @@ -524,6 +605,45 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89df7a26519371a3cce44fbb914c2819c84d9b897890987fa3ab096491cc0ea8" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap", + "serde", + "serde_json", + "serde_with_macros", + "time 0.3.13", +] + +[[package]] +name = "serde_with_macros" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de337f322382fcdfbb21a014f7c224ee041a23785651db67b9827403178f698f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "signal-hook" version = "0.3.14" @@ -667,6 +787,18 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "time" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db76ff9fa4b1458b3c7f077f3ff9887394058460d21e634355b273aaf11eea45" +dependencies = [ + "itoa", + "libc", + "num_threads", + "serde", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -701,6 +833,7 @@ dependencies = [ "comfy-table", "confy", "serde", + "serde_with", "termsize", "toml", "trash", diff --git a/Cargo.toml b/Cargo.toml index 1054150..1821a50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ colored = "2.0.0" comfy-table = "6.0.0" confy = "0.4.0" serde = { version = "1.0.143", features = ["derive"] } +serde_with = "2.0.0" termsize = "0.1.6" toml = "0.5.9" trash = "2.1.5" diff --git a/README.md b/README.md index 91db59e..8b6b25a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ A (currently in development) to do app for the command line. - Options for which columns to include - If no values given, read a set of defaults from a `list.toml` file, which can be edited from a similar command - Ability to view, edit, delete, etc. using name - - Have a file containing a serialized `HashMap>` - Disallow numerical names and have command automatically identify if it is a name or Id - Error on operation if two tasks exist with the same name - Dependency tracker diff --git a/src/edit.rs b/src/edit.rs index 54cc1bd..da2de41 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -5,6 +5,7 @@ use std::process; use crate::tasks; use crate::error; +use crate::state; use crate::tasks::Id; pub fn open_editor(path : &path::Path, editor : &str) -> Result { @@ -15,15 +16,15 @@ pub fn open_editor(path : &path::Path, editor : &str) -> Result Result<(), error::Error> { - let mut task = tasks::Task::load(id, vault_folder.clone(), false)?; + let mut task = tasks::Task::load(id, &vault_folder, false)?; let temp_path = vault_folder.join("temp.md"); - fs::write(&temp_path, &task.data.info.unwrap_or(String::new()).as_bytes())?; + fs::write(&temp_path, &task.data.info.unwrap_or_default().as_bytes())?; let status = open_editor(&temp_path, editor)?; @@ -49,9 +50,9 @@ pub fn edit_info(id : Id, vault_folder : path::PathBuf, editor : &str) -> Result } } -pub fn edit_raw(id : Id, vault_folder : path::PathBuf, editor : &str) -> Result<(), error::Error> { +pub fn edit_raw(id : Id, vault_folder : path::PathBuf, editor : &str, state : &mut state::State) -> Result<(), error::Error> { - let mut task = tasks::Task::load(id, vault_folder.clone(), false)?; + let mut task = tasks::Task::load(id, &vault_folder, false)?; let temp_path = vault_folder.join("temp.toml"); @@ -66,19 +67,22 @@ pub fn edit_raw(id : Id, vault_folder : path::PathBuf, editor : &str) -> Result< } } else { - let file_contents = fs::read_to_string(&temp_path)?; - let mut edited_task = tasks::Task::load_direct(temp_path.clone(), true)?; if edited_task.data.id != task.data.id { Err(error::Error::Generic(String::from("You cannot change the ID of a task in a direct edit"))) } + else if edited_task.data.name.chars().all(|c| c.is_numeric()) { + Err(error::Error::Generic(String::from("Name must not be purely numeric"))) + } else { if edited_task.data.dependencies != task.data.dependencies { // This is where the other dependencies graph needs to be updated. } + // Name change means index needs to be updated. if edited_task.data.name != task.data.name { - // This is where the hashmap from id to string needs to be updated. + state.index_remove(task.data.name.clone(), id); + state.index_insert(edited_task.data.name.clone(), id); } mem::swap(&mut edited_task.data, &mut task.data); diff --git a/src/main.rs b/src/main.rs index 6724bc4..854315f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -#![allow(dead_code, unused_variables)] +//#![allow(dead_code, unused_variables)] mod git; mod edit; @@ -164,11 +164,15 @@ fn program() -> Result<(), error::Error> { println!("Created task {} (ID: {})", colour::task_name(&task.data.name), colour::id(&task.data.id.to_string())); }, Delete { id } => { - tasks::Task::delete_by_id(id, vault_folder)?; - println!("Deleted task {}", colour::id(&id.to_string())); + let task = tasks::Task::load(id, vault_folder, false)?; + let name = task.data.name.clone(); + state.index_remove(task.data.name.clone(), task.data.id); + task.delete()?; + + println!("Deleted task {} (ID: {})", colour::task_name(&name), colour::id(&id.to_string())); }, View { id } => { - let task = tasks::Task::load(id, vault_folder.clone(), true)?; + let task = tasks::Task::load(id, vault_folder, true)?; task.display()?; }, Edit { id, info } => { @@ -176,18 +180,18 @@ fn program() -> Result<(), error::Error> { edit::edit_info(id, vault_folder.clone(), "nvim")?; } else { - edit::edit_raw(id, vault_folder.clone(), "nvim")?; + edit::edit_raw(id, vault_folder.clone(), "nvim", &mut state)?; } println!("Updated task {}", colour::id(&id.to_string())); }, Discard { id } => { - let mut task = tasks::Task::load(id, vault_folder.clone(), false)?; + let mut task = tasks::Task::load(id, vault_folder, false)?; task.data.discarded = true; task.save()?; println!("Discarded task {}", colour::id(&id.to_string())); }, Complete { id } => { - let mut task = tasks::Task::load(id, vault_folder.clone(), false)?; + let mut task = tasks::Task::load(id, vault_folder, false)?; task.data.complete = true; task.save()?; println!("Marked task {} as complete", colour::id(&id.to_string())); diff --git a/src/state.rs b/src/state.rs index 19bc20b..3f8f75f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,19 +1,26 @@ +use crate::error; +use crate::tasks; +use crate::tasks::Id; + use std::fs; use std::path; use std::io; use std::io::{Write, Seek}; +use std::collections::HashMap; -use crate::error; -use crate::tasks::Id; +use serde_with::{serde_as, DisplayFromStr}; pub struct State { file : fs::File, pub data : InternalState, } +#[serde_as] #[derive(serde::Serialize, serde::Deserialize)] pub struct InternalState { pub next_id : Id, + #[serde_as(as = "HashMap")] + pub index : HashMap>, } impl State { @@ -39,8 +46,8 @@ impl State { } else { + // Calculating the next ID if necessary. let mut max_id : i128 = -1; - for id in vault_location.join("notes").read_dir()?.filter_map(|p| p.ok()).map(|p| p.path()).filter(|p| p.extension().map(|s| s.to_str()) == Some(Some("toml"))).filter_map(|p| p.file_stem().map(|x| x.to_str().map(|y| y.to_string()))).flatten().filter_map(|p| p.parse::().ok()) { if i128::try_from(id).unwrap() > max_id { @@ -48,8 +55,23 @@ impl State { } } + // Calculating out the index. + let tasks = tasks::Task::load_all(vault_location, true)?; + let mut index : HashMap> = HashMap::with_capacity(tasks.len()); + for task in tasks { + match index.get_mut(&task.data.name) { + Some(ids) => { + ids.push(task.data.id); + }, + None => { + index.insert(task.data.name.clone(), vec![task.data.id]); + }, + } + } + let data = InternalState { next_id : u64::try_from(max_id + 1).unwrap(), + index, }; let mut file = fs::File::options() @@ -83,4 +105,27 @@ impl State { Ok(()) } + + pub fn index_insert(&mut self, name : String, id : Id) { + match self.data.index.get_mut(&name) { + Some(ids) => { + ids.push(id); + }, + None => { + self.data.index.insert(name, vec![id]); + } + } + } + + pub fn index_remove(&mut self, name : String, id : Id) { + if let Some(mut ids) = self.data.index.remove(&name) { + if let Some(index) = ids.iter().position(|i| i == &id) { + ids.swap_remove(index); + + if !ids.is_empty() { + self.data.index.insert(name, ids); + } + } + } + } } diff --git a/src/tasks.rs b/src/tasks.rs index 9844650..8c89187 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -75,6 +75,10 @@ pub struct InternalTask { impl Task { pub fn new(name : String, info : Option, tags : Vec, dependencies : Vec, priority : Option, vault_folder : &path::Path, state : &mut state::State) -> Result { + if name.chars().all(|c| c.is_numeric()) { + return Err(error::Error::Generic(String::from("Name must not be purely numeric"))); + }; + let id = state.data.next_id; state.data.next_id += 1; @@ -98,11 +102,12 @@ impl Task { discarded : false, }; - file.set_len(0)?; file.seek(io::SeekFrom::Start(0))?; file.write_all(toml::to_string(&data)?.as_bytes())?; + state.index_insert(data.name.clone(), id); + Ok(Task { path, file, @@ -136,12 +141,30 @@ impl Task { /// The read_only flag is so that the file will not be truncated, and therefore doesn't need to /// be saved when finished. - pub fn load(id : Id, vault_folder : path::PathBuf, read_only : bool) -> Result { - let path = Task::check_exists(id, &vault_folder)?; + pub fn load(id : Id, vault_folder : &path::Path, read_only : bool) -> Result { + let path = Task::check_exists(id, vault_folder)?; Task::load_direct(path, read_only) } + pub fn load_all(vault_folder : &path::Path, read_only : bool) -> Result, error::Error> { + let ids : Vec = + fs::read_dir(vault_folder.join("notes")) + .unwrap() + .map(|entry| entry.unwrap().path()) + .filter(|p| p.is_file()) + .map(|p| p.file_stem().unwrap().to_str().unwrap().to_string()) + .filter_map(|n| n.parse::().ok()) + .collect(); + + let mut tasks = Vec::with_capacity(ids.len()); + for id in ids { + tasks.push(Task::load(id, vault_folder, read_only)?); + } + + Ok(tasks) + } + pub fn path(&self) -> &path::Path { &self.path } @@ -158,7 +181,7 @@ impl Task { pub fn save(self) -> Result<(), error::Error> { let Self { - path, + path : _, mut file, data, } = self; @@ -174,7 +197,7 @@ impl Task { let Self { path, file, - data, + data : _, } = self; mem::drop(file); @@ -183,12 +206,6 @@ impl Task { Ok(()) } - pub fn delete_by_id(id : Id, vault_folder : &path::Path) -> Result<(), error::Error> { - let path = Task::check_exists(id, vault_folder)?; - fs::remove_file(&path)?; - Ok(()) - } - pub fn display(&self) -> Result<(), error::Error> { fn line(len : usize) { @@ -200,7 +217,7 @@ impl Task { let id = &self.data.id.to_string(); let discarded = if self.data.discarded { String::from(" (discarded)") } else { String::new() }; - let heading = format!("[{}] {} {}{}", if self.data.complete {"X"} else {" "}, colour::id(&id), colour::task_name(&self.data.name), colour::greyed_out(&discarded)); + let heading = format!("[{}] {} {}{}", if self.data.complete {"X"} else {" "}, colour::id(id), colour::task_name(&self.data.name), colour::greyed_out(&discarded)); println!("{}", heading); line(5 + self.data.name.chars().count() + id.chars().count() + discarded.chars().count()); @@ -212,11 +229,11 @@ impl Task { let mut max_line_width = 0; println!("Info:"); - while info.ends_with("\n") { + while info.ends_with('\n') { info.pop(); } - let info_lines : Vec<&str> = info.split("\n").collect(); + let info_lines : Vec<&str> = info.split('\n').collect(); for line in info_lines { max_line_width = usize::max(max_line_width, line.chars().count() + 4); println!(" {}", line); @@ -240,7 +257,8 @@ fn format_hash_set(set : &HashSet) -> Result(set : &HashSet) -> Result Result<(), error::Error> { - let ids : Vec = - fs::read_dir(vault_folder.join("notes")) - .unwrap() - .map(|entry| entry.unwrap().path()) - .filter(|p| p.is_file()) - .map(|p| p.file_stem().unwrap().to_str().unwrap().to_string()) - .filter_map(|n| n.parse::().ok()) - .collect(); let mut table = comfy_table::Table::new(); - table .load_preset(comfy_table::presets::UTF8_FULL) .apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS) .set_content_arrangement(comfy_table::ContentArrangement::Dynamic); - table.set_header(vec!["Id", "Name", "Tags", "Priority"]); - let mut tasks = Vec::with_capacity(ids.len()); - - for id in ids { - tasks.push(Task::load(id, vault_folder.to_path_buf(), true)?); - } - + let mut tasks = Task::load_all(vault_folder, true)?; tasks.sort_by(|t1, t2| t2.data.priority.cmp(&t1.data.priority)); for task in tasks { @@ -282,7 +285,7 @@ pub fn list(vault_folder : &path::Path) -> Result<(), error::Error> { task.data.id.to_string(), task.data.name, format_hash_set(&task.data.tags)?, - task.data.priority.to_string() + task.data.priority.to_string(), ] ); } diff --git a/src/vault.rs b/src/vault.rs index 429e197..35b5318 100644 --- a/src/vault.rs +++ b/src/vault.rs @@ -10,8 +10,7 @@ pub fn new(name : String, path : path::PathBuf, config : &mut config::Config) -> fn create_all_metadata(path : &path::Path) -> Result<(), error::Error> { fs::create_dir(path.join("notes"))?; - let state = state::State::load(path)?; - //state.save()?; + let _ = state::State::load(path)?; Ok(()) }