From 24527be3cfc07760f2c1a39ee008fb9a97e7cca6 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 5 Dec 2015 16:43:29 +0100 Subject: [PATCH 01/21] Add dep: pulldown-cmark --- Cargo.lock | 14 ++++++++++++++ Cargo.toml | 1 + 2 files changed, 15 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index ebfbeffd..e48fefc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "log 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "open 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "prettytable-rs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pulldown-cmark 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", "rustty 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -74,6 +75,11 @@ dependencies = [ "nom 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "getopts" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "glob" version = "0.2.10" @@ -156,6 +162,14 @@ dependencies = [ "unicode-width 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "pulldown-cmark" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rand" version = "0.3.11" diff --git a/Cargo.toml b/Cargo.toml index ceb9bda9..d58d0158 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,4 +27,5 @@ term = "0.2.12" term_grid = "0.1.2" prettytable-rs = "0.4.0" open = "1.1.0" +pulldown-cmark = "0.0.3" From 1de0796507764bcbd449f593e4076df9b7dae1f6 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 5 Dec 2015 17:20:42 +0100 Subject: [PATCH 02/21] Add interface specification for the notes module --- etc/cli.yml | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/etc/cli.yml b/etc/cli.yml index 21b38e0f..d8c19e0b 100644 --- a/etc/cli.yml +++ b/etc/cli.yml @@ -258,3 +258,78 @@ subcommands: version: 0.1 author: Matthias Beyer + - notes: + about: Notes module + version: 0.1 + author: Matthias Beyer + subcommands: + - add: + about: Add Note + version: 0.1 + author: Matthias Beyer + args: + - name: + short: n + long: name + help: Add a name to the note + required: false + takes_value: true + + - tags: + short: t + long: tags + help: Add these tags to the note + required: false + takes_value: true + + - list: + about: List notes + version: 0.1 + author: Matthias Beyer + args: + - namegrep: + short: n + long: name + help: Filter for name which matches this regex + required: false + takes_value: true + + - grep: + short: g + long: grep + help: grep with regex + required: false + takes_value: true + + - tags: + short: t + long: tags + help: Filter for these tags + required: false + takes_value: true + + - remove: + about: Remove note(s) + version: 0.1 + author: Matthias Beyer + args: + - id: + long: id + help: Delete Note by ID + required: false + takes_value: true + + - match: + short: m + long: match + help: Match for regex + required: false + takes_value: true + + - tags: + short: t + long: tags + help: Filter for these tags + required: false + takes_value: true + From 8a9f4c370e76dfd1b80ee0dfd5b1dc53947c8252 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Wed, 30 Dec 2015 11:34:24 +0100 Subject: [PATCH 03/21] Initial import of code for Notes module --- src/module/mod.rs | 1 + src/module/notes/header.rs | 50 ++++++++++++++++++++ src/module/notes/mod.rs | 95 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 src/module/notes/header.rs create mode 100644 src/module/notes/mod.rs diff --git a/src/module/mod.rs b/src/module/mod.rs index 24fdc9a5..7794719f 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -4,6 +4,7 @@ use clap::ArgMatches; pub mod bm; pub mod helpers; +pub mod notes; /** * Module interface, each module has to implement this. diff --git a/src/module/notes/header.rs b/src/module/notes/header.rs new file mode 100644 index 00000000..029aa698 --- /dev/null +++ b/src/module/notes/header.rs @@ -0,0 +1,50 @@ +/* + * Lets talk about header data first. + * We need: + * + * - tags + * - name (not unique) + * + * So an header could look like this: + * + * ```json + * { + * 'name': "kittennotes", + * 'tags': ['foo', 'bar', 'baz'], + * } + * ``` + * + * Nothing more is required for the header, I guess + * + */ + +use module::helpers; +use module::helpers::header as headerhelpers; +use storage::file::header::data::FileHeaderData as FHD; +use storage::file::header::spec::FileHeaderSpec as FHS; + +pub fn get_spec() -> FHS { + FHS::Map { keys: vec![ helpers::spec::named_text("NAME"), + headerhelpers::tags::spec::tags_key() ] } +} + + +pub fn build_header(name: String, tags: Vec) -> FHD { + FHD::Map { + keys: vec![ + FHD::Key { + name: String::from("NAME"), + value: Box::new(FHD::Text(name.clone())) + }, + FHD::Key { + name: String::from("TAGS"), + value: Box::new(headerhelpers::tags::data::build_tag_array(tags)) + } + ] + } +} + +pub fn get_tags_from_header(header: &FHD) -> Vec { + headerhelpers::tags::data::get_tags_from_header(header) +} + diff --git a/src/module/notes/mod.rs b/src/module/notes/mod.rs new file mode 100644 index 00000000..99793379 --- /dev/null +++ b/src/module/notes/mod.rs @@ -0,0 +1,95 @@ +use std::fmt::{Debug, Formatter}; +use std::fmt::Result as FMTResult; + +mod header; + +use module::Module; +use runtime::Runtime; + +pub struct Notes<'a> { + rt: &'a Runtime<'a>, +} + +impl<'a> Notes<'a> { + + pub fn new(rt: &'a Runtime<'a>) -> Notes<'a> { + Notes { + rt: rt, + } + } + + fn command_add(&self, matches: &ArgMatches) -> bool { + unimplemented!() + } + + fn command_list(&self, matches: &ArgMatches) -> bool { + unimplemented!() + } + + fn command_remove(&self, matches: &ArgMatches) -> bool { + unimplemented!() + } + + fn command_add_tags(&self, matches: &ArgMatches) -> bool { + unimplemented!() + } + + fn command_rm_tags(&self, matches: &ArgMatches) -> bool { + unimplemented!() + } + + fn command_set_tags(&self, matches: &ArgMatches) -> bool { + unimplemented!() + } + +} + +impl<'a> Module<'a> for Notes<'a> { + + fn exec(&self, matches: &ArgMatches) -> bool { + match matches.subcommand_name() { + Some("add") => { + self.command_add(matches.subcommand_matches("add").unwrap()) + }, + + Some("list") => { + self.command_list(matches.subcommand_matches("list").unwrap()) + }, + + Some("remove") => { + self.command_remove(matches.subcommand_matches("remove").unwrap()) + }, + + Some("add_tags") => { + self.command_add_tags(matches.subcommand_matches("add_tags").unwrap()) + }, + + Some("rm_tags") => { + self.command_rm_tags(matches.subcommand_matches("rm_tags").unwrap()) + }, + + Some("set_tags") => { + self.command_set_tags(matches.subcommand_matches("set_tags").unwrap()) + }, + + Some(_) | None => { + info!("No command given, doing nothing"); + false + }, + } + } + + fn name(&self) -> &'static str{ + "notes" + } + +} + +impl<'a> Debug for Notes<'a> { + + fn fmt(&self, fmt: &mut Formatter) -> FMTResult { + write!(fmt, "[Module][Notes]"); + Ok(()) + } + +} From 5cc4b4d0797cdb6a396a7496b52767b1bb82ae0a Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Wed, 30 Dec 2015 11:05:00 +0100 Subject: [PATCH 04/21] Notes: Implement command_add() --- src/module/notes/mod.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/module/notes/mod.rs b/src/module/notes/mod.rs index 99793379..c3284ccf 100644 --- a/src/module/notes/mod.rs +++ b/src/module/notes/mod.rs @@ -1,10 +1,14 @@ use std::fmt::{Debug, Formatter}; use std::fmt::Result as FMTResult; +use clap::ArgMatches; + mod header; use module::Module; use runtime::Runtime; +use storage::parser::Parser; +use storage::json::parser::JsonHeaderParser; pub struct Notes<'a> { rt: &'a Runtime<'a>, @@ -19,7 +23,31 @@ impl<'a> Notes<'a> { } fn command_add(&self, matches: &ArgMatches) -> bool { - unimplemented!() + use std::process::exit; + use self::header::build_header; + + let parser = Parser::new(JsonHeaderParser::new(None)); + let name = matches.value_of("name") + .map(String::from) + .unwrap_or(String::from("")); + let tags = matches.value_of("tags") + .and_then(|s| Some(s.split(",").map(String::from).collect())) + .unwrap_or(vec![]); + + debug!("Building header with"); + debug!(" name = '{:?}'", name); + debug!(" tags = '{:?}'", tags); + let header = build_header(name, tags); + + let fileid = self.rt.store().new_file_with_header(self, header); + self.rt + .store() + .load(self, &parser, &fileid) + .and_then(|file| { + info!("Created file in memory: {}", fileid); + Some(self.rt.store().persist(&parser, file)) + }) + .unwrap_or(false) } fn command_list(&self, matches: &ArgMatches) -> bool { From 591717780a1752eb5989cfd91ad371cd456b97b7 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Wed, 30 Dec 2015 11:32:58 +0100 Subject: [PATCH 05/21] Notes: Add helper to get name from header --- src/module/notes/header.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/module/notes/header.rs b/src/module/notes/header.rs index 029aa698..3f27af38 100644 --- a/src/module/notes/header.rs +++ b/src/module/notes/header.rs @@ -48,3 +48,12 @@ pub fn get_tags_from_header(header: &FHD) -> Vec { headerhelpers::tags::data::get_tags_from_header(header) } +/** + * Get the name from the Header + * + * Returns empty string if there is no NAME field + */ +pub fn get_name_from_header(header: &FHD) -> String { + headerhelpers::data::get_name_from_header(header).unwrap_or(String::from("")) +} + From 852b6244176cd02a69dc1ffff992f81ce77cd4a1 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Wed, 30 Dec 2015 11:33:08 +0100 Subject: [PATCH 06/21] Notes: Implement command_list() --- src/module/notes/mod.rs | 47 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/module/notes/mod.rs b/src/module/notes/mod.rs index c3284ccf..f6c71844 100644 --- a/src/module/notes/mod.rs +++ b/src/module/notes/mod.rs @@ -1,14 +1,23 @@ use std::fmt::{Debug, Formatter}; use std::fmt::Result as FMTResult; +use std::rc::Rc; +use std::cell::RefCell; use clap::ArgMatches; +use regex::Regex; mod header; use module::Module; use runtime::Runtime; +use storage::file::File; use storage::parser::Parser; use storage::json::parser::JsonHeaderParser; +use module::helpers::cli::create_tag_filter; +use module::helpers::cli::create_hash_filter; +use module::helpers::cli::create_text_header_field_grep_filter; +use module::helpers::cli::create_content_grep_filter; +use module::helpers::cli::CliFileFilter; pub struct Notes<'a> { rt: &'a Runtime<'a>, @@ -51,7 +60,43 @@ impl<'a> Notes<'a> { } fn command_list(&self, matches: &ArgMatches) -> bool { - unimplemented!() + use ui::file::{FilePrinter, TablePrinter}; + use std::ops::Deref; + use self::header::get_name_from_header; + use self::header::get_tags_from_header; + use std::process::exit; + use module::helpers::cli::CliFileFilter; + + let parser = Parser::new(JsonHeaderParser::new(None)); + + let filter = { + let hash_filter = create_hash_filter(matches, "id", true); + let head_filter = create_text_header_field_grep_filter(matches, "match", "NAME", true); + let text_filter = create_content_grep_filter(matches, "match", true); + let tags_filter = create_tag_filter(matches, "tags", true); + hash_filter.or(Box::new(head_filter)).and(Box::new(text_filter)).and(Box::new(tags_filter)) + }; + + let printer = TablePrinter::new(self.rt.is_verbose(), self.rt.is_debugging()); + + printer.print_files_custom( + self.rt.store() + .load_for_module(self, &parser) + .into_iter() + .filter(|f| filter.filter_file(f)), + &|file| { + let fl = file.deref().borrow(); + let hdr = fl.header(); + let name = get_name_from_header(hdr); + let tags = get_tags_from_header(hdr); + + debug!("Custom printer field: name = '{:?}'", name); + debug!("Custom printer field: tags = '{:?}'", tags); + + vec![name, tags.join(", ")] + } + ); + true } fn command_remove(&self, matches: &ArgMatches) -> bool { From 7cd429fe7eb30ae1e3c3e25530ce8e54853eb8bf Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 2 Jan 2016 18:49:55 +0100 Subject: [PATCH 07/21] Notes: Implement command_remove --- src/module/notes/mod.rs | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/module/notes/mod.rs b/src/module/notes/mod.rs index f6c71844..4fa5d7b1 100644 --- a/src/module/notes/mod.rs +++ b/src/module/notes/mod.rs @@ -2,6 +2,7 @@ use std::fmt::{Debug, Formatter}; use std::fmt::Result as FMTResult; use std::rc::Rc; use std::cell::RefCell; +use std::ops::Deref; use clap::ArgMatches; use regex::Regex; @@ -61,7 +62,6 @@ impl<'a> Notes<'a> { fn command_list(&self, matches: &ArgMatches) -> bool { use ui::file::{FilePrinter, TablePrinter}; - use std::ops::Deref; use self::header::get_name_from_header; use self::header::get_tags_from_header; use std::process::exit; @@ -100,7 +100,40 @@ impl<'a> Notes<'a> { } fn command_remove(&self, matches: &ArgMatches) -> bool { - unimplemented!() + let parser = Parser::new(JsonHeaderParser::new(None)); + + let filter = { + let hash_filter = create_hash_filter(matches, "id", false); + let text_filter = create_text_header_field_grep_filter(matches, "match", "URL", false); + let tags_filter = create_tag_filter(matches, "tags", false); + hash_filter.or(Box::new(text_filter)).or(Box::new(tags_filter)) + }; + + let result = self.rt + .store() + .load_for_module(self, &parser) + .iter() + .filter(|file| filter.filter_file(file)) + .map(|file| { + debug!("File loaded, can remove now: {:?}", file); + let f = file.deref().borrow(); + self.rt.store().remove(f.id().clone()) + }) + .fold((0, 0), |acc, succeeded| { + let (worked, failed) = acc; + if succeeded { + (worked + 1, failed) + } else { + (worked, failed + 1) + } + }); + + let (worked, failed) = result; + + info!("Removing succeeded for {} files", worked); + info!("Removing failed for {} files", failed); + + return failed == 0; } fn command_add_tags(&self, matches: &ArgMatches) -> bool { From 6d02e3d4868b9c76c80e8e0518e15813813fcc4c Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 2 Jan 2016 19:04:55 +0100 Subject: [PATCH 08/21] Module trait: Module must be able to lend a Runtime object --- src/module/bm/mod.rs | 4 ++++ src/module/mod.rs | 4 ++++ src/module/notes/mod.rs | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/src/module/bm/mod.rs b/src/module/bm/mod.rs index 008df23d..1472127c 100644 --- a/src/module/bm/mod.rs +++ b/src/module/bm/mod.rs @@ -380,6 +380,10 @@ impl<'a> Module<'a> for BM<'a> { fn name(&self) -> &'static str { "bookmark" } + + fn runtime(&self) -> &Runtime { + self.rt + } } impl<'a> Debug for BM<'a> { diff --git a/src/module/mod.rs b/src/module/mod.rs index 7794719f..910995cb 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -2,6 +2,8 @@ use std::fmt::Debug; use clap::ArgMatches; +use runtime::Runtime; + pub mod bm; pub mod helpers; pub mod notes; @@ -12,5 +14,7 @@ pub mod notes; pub trait Module<'a> : Debug { fn exec(&self, matches: &ArgMatches) -> bool; fn name(&self) -> &'static str; + + fn runtime(&self) -> &Runtime; } diff --git a/src/module/notes/mod.rs b/src/module/notes/mod.rs index 4fa5d7b1..69372ccf 100644 --- a/src/module/notes/mod.rs +++ b/src/module/notes/mod.rs @@ -189,6 +189,10 @@ impl<'a> Module<'a> for Notes<'a> { "notes" } + fn runtime(&self) -> &Runtime { + self.rt + } + } impl<'a> Debug for Notes<'a> { From 9227c4bb9a915fe02ae68b4df81c396d6fd5d643 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 2 Jan 2016 19:06:19 +0100 Subject: [PATCH 09/21] Move header-altering helper to module generic helpers This helper is for editing the header tags in some module-independent way, so this piece of code is now moved to the module-generic helpers. --- src/module/bm/mod.rs | 82 ++++++------------------------- src/module/helpers/header/tags.rs | 76 ++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 66 deletions(-) diff --git a/src/module/bm/mod.rs b/src/module/bm/mod.rs index 1472127c..251645f0 100644 --- a/src/module/bm/mod.rs +++ b/src/module/bm/mod.rs @@ -243,94 +243,44 @@ impl<'a> BM<'a> { * Subcommand: add_tags */ fn command_add_tags(&self, matches: &ArgMatches) -> bool { - self.alter_tags_in_files(matches, |old_tags, cli_tags| { + use module::helpers::header::tags::data::alter_tags_in_files; + use self::header::rebuild_header_with_tags; + + let parser = Parser::new(JsonHeaderParser::new(None)); + alter_tags_in_files(self, matches, &parser, |old_tags, cli_tags| { let mut new_tags = old_tags.clone(); new_tags.append(&mut cli_tags.clone()); new_tags - }) + }, rebuild_header_with_tags) } /** * Subcommand: rm_tags */ fn command_rm_tags(&self, matches: &ArgMatches) -> bool { - self.alter_tags_in_files(matches, |old_tags, cli_tags| { + use module::helpers::header::tags::data::alter_tags_in_files; + use self::header::rebuild_header_with_tags; + + let parser = Parser::new(JsonHeaderParser::new(None)); + alter_tags_in_files(self, matches, &parser, |old_tags, cli_tags| { old_tags.clone() .into_iter() .filter(|tag| !cli_tags.contains(tag)) .collect() - }) + }, rebuild_header_with_tags) } /** * Subcommand: set_tags */ fn command_set_tags(&self, matches: &ArgMatches) -> bool { - self.alter_tags_in_files(matches, |old_tags, cli_tags| { - cli_tags.clone() - }) - } - - /** - * Helper function to alter the tags in a file - */ - fn alter_tags_in_files(&self, matches: &ArgMatches, generate_new_tags: F) -> bool - where F: Fn(Vec, &Vec) -> Vec - { + use module::helpers::header::tags::data::alter_tags_in_files; use self::header::rebuild_header_with_tags; - let cli_tags = matches.value_of("tags") - .map(|ts| { - ts.split(",") - .map(String::from) - .collect::>() - }) - .unwrap_or(vec![]); - let parser = Parser::new(JsonHeaderParser::new(None)); - - let filter = { - let hash_filter = create_hash_filter(matches, "with:id", false); - let text_filter = create_text_header_field_grep_filter(matches, "with_match", "URL", false); - let tags_filter = create_tag_filter(matches, "with_tags", false); - hash_filter.or(Box::new(text_filter)).or(Box::new(tags_filter)) - }; - - self.rt - .store() - .load_for_module(self, &parser) - .into_iter() - .filter(|file| filter.filter_file(file)) - .map(|file| { - debug!("Remove tags from file: {:?}", file); - - let hdr = { - let f = file.deref().borrow(); - f.header().clone() - }; - - debug!("Tags:..."); - let old_tags = get_tags_from_header(&hdr); - debug!(" old_tags = {:?}", &old_tags); - debug!(" cli_tags = {:?}", &cli_tags); - - let new_tags = generate_new_tags(old_tags, &cli_tags); - debug!(" new_tags = {:?}", &new_tags); - - let new_header = rebuild_header_with_tags(&hdr, new_tags) - .unwrap_or_else(|| { - error!("Could not rebuild header for file"); - exit(1); - }); - { - let mut f_mut = file.deref().borrow_mut(); - f_mut.set_header(new_header); - } - - self.rt.store().persist(&parser, file); - true - }) - .all(|x| x) + alter_tags_in_files(self, matches, &parser, |old_tags, cli_tags| { + cli_tags.clone() + }, rebuild_header_with_tags) } } diff --git a/src/module/helpers/header/tags.rs b/src/module/helpers/header/tags.rs index e2e390e0..2fd13363 100644 --- a/src/module/helpers/header/tags.rs +++ b/src/module/helpers/header/tags.rs @@ -35,6 +35,10 @@ pub mod spec { pub mod data { use std::ops::Deref; use storage::file::header::data::FileHeaderData as FHD; + use module::Module; + use clap::ArgMatches; + use storage::parser::Parser; + use storage::parser::FileHeaderParser; /** * Use a Vec to build a Tag-Array: @@ -94,5 +98,77 @@ pub mod data { tags } + /** + * Helper function to alter the tags in a file + */ + pub fn alter_tags_in_files(m: &Module, + matches: &ArgMatches, + parser: &Parser, + generate_new_tags: F, + rebuild_header: R) -> bool + where HP: FileHeaderParser, + F: Fn(Vec, &Vec) -> Vec, + R: Fn(&FHD, Vec) -> Option + { + use std::process::exit; + use module::helpers::cli::create_tag_filter; + use module::helpers::cli::create_hash_filter; + use module::helpers::cli::create_text_header_field_grep_filter; + use module::helpers::cli::create_content_grep_filter; + use module::helpers::cli::CliFileFilter; + + let cli_tags = matches.value_of("tags") + .map(|ts| { + ts.split(",") + .map(String::from) + .collect::>() + }) + .unwrap_or(vec![]); + + let filter = { + let hash_filter = create_hash_filter(matches, "with:id", false); + let text_filter = create_text_header_field_grep_filter(matches, "with_match", "URL", false); + let tags_filter = create_tag_filter(matches, "with_tags", false); + hash_filter.or(Box::new(text_filter)).or(Box::new(tags_filter)) + }; + + m.runtime() + .store() + .load_for_module(m, &parser) + .into_iter() + .filter(|file| filter.filter_file(file)) + .map(|file| { + debug!("Alter tags in file: {:?}", file); + + let hdr = { + let f = file.deref().borrow(); + f.header().clone() + }; + + debug!("Tags:..."); + let old_tags = get_tags_from_header(&hdr); + debug!(" old_tags = {:?}", &old_tags); + debug!(" cli_tags = {:?}", &cli_tags); + + let new_tags = generate_new_tags(old_tags, &cli_tags); + debug!(" new_tags = {:?}", &new_tags); + + let new_header = rebuild_header(&hdr, new_tags) + .unwrap_or_else(|| { + error!("Could not rebuild header for file"); + exit(1); + }); + { + let mut f_mut = file.deref().borrow_mut(); + f_mut.set_header(new_header); + } + + m.runtime().store().persist(&parser, file); + true + }) + .all(|x| x) + } + + } From 1bccf067bfeb83e7004b5ff0ac792659e8aca25f Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 2 Jan 2016 19:07:27 +0100 Subject: [PATCH 10/21] Provide header-rebuilder helper --- src/module/notes/header.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/module/notes/header.rs b/src/module/notes/header.rs index 3f27af38..76fc1880 100644 --- a/src/module/notes/header.rs +++ b/src/module/notes/header.rs @@ -57,3 +57,8 @@ pub fn get_name_from_header(header: &FHD) -> String { headerhelpers::data::get_name_from_header(header).unwrap_or(String::from("")) } +pub fn rebuild_header_with_tags(header: &FHD, tags: Vec) -> Option { + let name = get_name_from_header(header); + Some(build_header(name, tags)) +} + From e42d7f7f8d04248ef6bdca4443ebad1383ec6184 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 2 Jan 2016 19:07:40 +0100 Subject: [PATCH 11/21] Notes: Implement command_add_tags() --- src/module/notes/mod.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/module/notes/mod.rs b/src/module/notes/mod.rs index 69372ccf..399f5778 100644 --- a/src/module/notes/mod.rs +++ b/src/module/notes/mod.rs @@ -137,7 +137,15 @@ impl<'a> Notes<'a> { } fn command_add_tags(&self, matches: &ArgMatches) -> bool { - unimplemented!() + use module::helpers::header::tags::data::alter_tags_in_files; + use self::header::rebuild_header_with_tags; + + let parser = Parser::new(JsonHeaderParser::new(None)); + alter_tags_in_files(self, matches, &parser, |old_tags, cli_tags| { + let mut new_tags = old_tags.clone(); + new_tags.append(&mut cli_tags.clone()); + new_tags + }, rebuild_header_with_tags) } fn command_rm_tags(&self, matches: &ArgMatches) -> bool { From 445a9885cfcf096353f052dab8f943495ba5747a Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 2 Jan 2016 19:08:32 +0100 Subject: [PATCH 12/21] Notes: Implement command_rm_tags --- src/module/notes/mod.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/module/notes/mod.rs b/src/module/notes/mod.rs index 399f5778..f08722fb 100644 --- a/src/module/notes/mod.rs +++ b/src/module/notes/mod.rs @@ -149,7 +149,16 @@ impl<'a> Notes<'a> { } fn command_rm_tags(&self, matches: &ArgMatches) -> bool { - unimplemented!() + use module::helpers::header::tags::data::alter_tags_in_files; + use self::header::rebuild_header_with_tags; + + let parser = Parser::new(JsonHeaderParser::new(None)); + alter_tags_in_files(self, matches, &parser, |old_tags, cli_tags| { + old_tags.clone() + .into_iter() + .filter(|tag| !cli_tags.contains(tag)) + .collect() + }, rebuild_header_with_tags) } fn command_set_tags(&self, matches: &ArgMatches) -> bool { From db7a4abbd8ad1e1b60e50ca7db5e6afc9d779cdf Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 2 Jan 2016 19:08:50 +0100 Subject: [PATCH 13/21] Notes: Implement command_set_tags --- src/module/notes/mod.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/module/notes/mod.rs b/src/module/notes/mod.rs index f08722fb..0b421563 100644 --- a/src/module/notes/mod.rs +++ b/src/module/notes/mod.rs @@ -162,7 +162,13 @@ impl<'a> Notes<'a> { } fn command_set_tags(&self, matches: &ArgMatches) -> bool { - unimplemented!() + use module::helpers::header::tags::data::alter_tags_in_files; + use self::header::rebuild_header_with_tags; + + let parser = Parser::new(JsonHeaderParser::new(None)); + alter_tags_in_files(self, matches, &parser, |old_tags, cli_tags| { + cli_tags.clone() + }, rebuild_header_with_tags) } } From d68d0088e80f1735029f80cc09231c5d9980d635 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 2 Jan 2016 19:14:29 +0100 Subject: [PATCH 14/21] main: Include Notes module --- src/main.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 504be825..37ef4914 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,7 @@ pub mod ui; pub mod util; pub use module::bm::BM; +pub use module::notes::Notes; fn main() { let yaml = load_yaml!("../etc/cli.yml"); @@ -43,10 +44,11 @@ fn main() { debug!("Runtime : {:?}", &rt); - if let Some(matches) = rt.config.cli_matches.subcommand_matches("bm") { - let res = BM::new(&rt).exec(matches); - info!("BM exited with {}", res); - } else { - info!("No commandline call...") - } + let res = match rt.config.cli_matches.subcommand_name() { + Some("bm") => BM::new(&rt).exec(rt.config.cli_matches.subcommand_matches("bm").unwrap()), + Some("notes") => Notes::new(&rt).exec(rt.config.cli_matches.subcommand_matches("notes").unwrap()), + _ => false, + }; + + info!("Module execution ended with {}", res); } From ad1a0e7f47ed506aa23c26f063d91ef7fc4ada58 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 2 Jan 2016 19:26:39 +0100 Subject: [PATCH 15/21] configuration should return editor_opts() -> String --- src/configuration.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/configuration.rs b/src/configuration.rs index 7671bef7..b783c734 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -65,8 +65,8 @@ impl Configuration { self.editor.clone() } - pub fn editor_opts(&self) -> &String { - &self.editor_opts + pub fn editor_opts(&self) -> String { + self.editor_opts.clone() } } From 2b8bd86ce16f37b6df956c3480f550056c826a39 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 2 Jan 2016 19:27:13 +0100 Subject: [PATCH 16/21] Runtime::editor() should provide a Command object --- src/runtime.rs | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/runtime.rs b/src/runtime.rs index 9ccce2f2..029e68a8 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1,4 +1,5 @@ use std::fmt::{Debug, Formatter, Error}; +use std::process::Command; extern crate log; use log::{LogRecord, LogLevel, LogLevelFilter, LogMetadata, SetLoggerError}; @@ -112,18 +113,26 @@ impl<'a> Runtime<'a> { } } - pub fn editor(&self) -> String { + pub fn editor(&self) -> Command { use std::env::var; - if let Some(editor) = self.config.editor() { - editor + &self.config.editor_opts()[..] - } else if let Some(editor) = self.configuration.editor() { - editor + &self.configuration.editor_opts()[..] - } else if let Ok(editor) = var("EDITOR") { - editor - } else { - String::from("vim") + let (editor, args) : (String, String) = { + if let Some(editor) = self.config.editor() { + (editor, self.config.editor_opts()) + } else if let Some(editor) = self.configuration.editor() { + (editor, self.configuration.editor_opts()) + } else if let Ok(editor) = var("EDITOR") { + (editor, String::from("")) + } else { + (String::from("vim"), String::from("")) + } + }; + + let mut e = Command::new(editor); + for arg in args.split(" ") { + e.arg(arg); } + e } } From 7ac5111cded78e93f1ee1ea599c1f4b71bb7c73c Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 2 Jan 2016 19:27:18 +0100 Subject: [PATCH 17/21] Fix let_user_provide_content() --- src/ui/external/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/external/editor.rs b/src/ui/external/editor.rs index d6030a5e..08ab6314 100644 --- a/src/ui/external/editor.rs +++ b/src/ui/external/editor.rs @@ -20,7 +20,7 @@ pub fn let_user_provide_content(rt: &Runtime) -> Option { } let output = { - let mut cmd = Command::new(rt.editor()); + let mut cmd = rt.editor(); cmd.arg(filepath); debug!("cmd = {:?}", cmd); cmd.spawn() From ee2d96a9a28bb55baa9d77b18a0b8093b58869a6 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 2 Jan 2016 19:27:43 +0100 Subject: [PATCH 18/21] Notes: command_add() -> let user provide content --- src/module/notes/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/module/notes/mod.rs b/src/module/notes/mod.rs index 0b421563..ee3fd9cb 100644 --- a/src/module/notes/mod.rs +++ b/src/module/notes/mod.rs @@ -35,6 +35,7 @@ impl<'a> Notes<'a> { fn command_add(&self, matches: &ArgMatches) -> bool { use std::process::exit; use self::header::build_header; + use ui::external::editor::let_user_provide_content; let parser = Parser::new(JsonHeaderParser::new(None)); let name = matches.value_of("name") @@ -49,7 +50,9 @@ impl<'a> Notes<'a> { debug!(" tags = '{:?}'", tags); let header = build_header(name, tags); - let fileid = self.rt.store().new_file_with_header(self, header); + let content = let_user_provide_content(self.runtime()).unwrap_or(String::from("")); + + let fileid = self.rt.store().new_file_with_content(self, header, content); self.rt .store() .load(self, &parser, &fileid) From 6b105f41d22557c9a7d38b07e2c1a5638da9edc2 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 2 Jan 2016 19:43:46 +0100 Subject: [PATCH 19/21] Add: ui::external::editor::edit_content() --- src/ui/external/editor.rs | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/ui/external/editor.rs b/src/ui/external/editor.rs index 08ab6314..fb43895c 100644 --- a/src/ui/external/editor.rs +++ b/src/ui/external/editor.rs @@ -49,3 +49,60 @@ pub fn let_user_provide_content(rt: &Runtime) -> Option { Some(contents) }).unwrap_or(None) } + +/** + * Edit some content in a temporary file. If anything failes within this routine, it returns the + * old content and false. + * If the editing succeeded, it returns the new content and true + */ +pub fn edit_content(rt: &Runtime, old_content: String) -> (String, bool) { + use std::io::Read; + use std::io::Write; + use std::fs::File; + use std::process::Command; + use std::process::exit; + + let filepath = "/tmp/imag-tmp.md"; + { + let mut file = match File::create(filepath) { + Ok(f) => f, + Err(e) => { + error!("Error creating file {}", filepath); + debug!("Error creating file at '{}', error = {}", filepath, e); + exit(1); + } + }; + + file.write(old_content.as_ref()); + } + debug!("Ready with putting old content into the file"); + + let output = { + let mut cmd = rt.editor(); + cmd.arg(filepath); + debug!("cmd = {:?}", cmd); + cmd.spawn() + .and_then(|child| child.wait_with_output()) + }; + + let process_out = output.map_err(|e| { + error!("Editor call failed"); + debug!("Editor call failed: {:?}", e); + return None as Option; + }).unwrap(); + + if !process_out.status.success() { + error!("Editor call failed"); + debug!("status = {:?}", process_out.status); + debug!("stdout = {:?}", String::from_utf8(process_out.stdout)); + debug!("stderr = {:?}", String::from_utf8(process_out.stderr)); + return (old_content, false); + } + + let mut contents = String::new(); + File::open(filepath).map(|mut file| { + file.read_to_string(&mut contents); + (contents, true) + }).unwrap_or((old_content, false)) +} + From 8ac798687a1201e7011414d10ce8153f2d4aee80 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 2 Jan 2016 19:44:00 +0100 Subject: [PATCH 20/21] Add File::set_data() --- src/storage/file/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/storage/file/mod.rs b/src/storage/file/mod.rs index 4dde562a..19866a81 100644 --- a/src/storage/file/mod.rs +++ b/src/storage/file/mod.rs @@ -59,6 +59,13 @@ impl File { &self.data } + /** + * Set the data + */ + pub fn set_data(&mut self, new_data: String) { + self.data = new_data; + } + /** * Set the (header, data) of the file */ From 9ac388c2b68f65c3c485b3e6572d6637516efbf2 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 2 Jan 2016 19:44:14 +0100 Subject: [PATCH 21/21] Notes: Add command_edit() --- etc/cli.yml | 32 +++++++++++++++++++++ src/module/notes/mod.rs | 63 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/etc/cli.yml b/etc/cli.yml index d8c19e0b..08fa8293 100644 --- a/etc/cli.yml +++ b/etc/cli.yml @@ -282,6 +282,38 @@ subcommands: required: false takes_value: true + - edit: + about: Edit Note + version: 0.1 + author: Matthias Beyer + args: + - id: + long: id + help: Edit Note with this ID + required: false + takes_value: true + + - namegrep: + short: n + long: name + help: Edit notes where the name matches this regex + required: false + takes_value: true + + - grep: + short: g + long: grep + help: Edit notes where the content matches this regex + required: false + takes_value: true + + - tags: + short: t + long: tags + help: Edit notes with these tags + required: false + takes_value: true + - list: about: List notes version: 0.1 diff --git a/src/module/notes/mod.rs b/src/module/notes/mod.rs index ee3fd9cb..f6736d40 100644 --- a/src/module/notes/mod.rs +++ b/src/module/notes/mod.rs @@ -63,6 +63,65 @@ impl<'a> Notes<'a> { .unwrap_or(false) } + fn command_edit(&self, matches: &ArgMatches) -> bool { + use ui::external::editor::edit_content; + + let parser = Parser::new(JsonHeaderParser::new(None)); + + let filter = { + let hash_filter = create_hash_filter(matches, "id", false); + let head_filter = create_text_header_field_grep_filter(matches, "namematch", "NAME", false); + let text_filter = create_content_grep_filter(matches, "match", false); + let tags_filter = create_tag_filter(matches, "tags", false); + hash_filter.or(Box::new(head_filter)).or(Box::new(text_filter)).or(Box::new(tags_filter)) + }; + + let result = self.rt + .store() + .load_for_module(self, &parser) + .into_iter() + .filter(|f| filter.filter_file(f)) + .map(|file| { + debug!("File loaded, can edit now: {:?}", file); + + let old_content = { + let f = file.deref().borrow(); + f.data().clone() + }; + + debug!("Editing content now..."); + let (new_content, editing_worked) = edit_content(self.runtime(), old_content); + debug!("... ready with editing"); + + if editing_worked { + debug!("Editing worked"); + { + let mut f = file.deref().borrow_mut(); + f.set_data(new_content); + } + self.runtime().store().persist(&parser, file) + } else { + debug!("Editing didn't work"); + false + } + }) + .fold((0, 0), |acc, succeeded| { + let (worked, failed) = acc; + if succeeded { + (worked + 1, failed) + } else { + (worked, failed + 1) + } + }); + + let (worked, failed) = result; + + info!("Editing succeeded for {} files", worked); + info!("Editing failed for {} files", failed); + + return failed == 0; + } + fn command_list(&self, matches: &ArgMatches) -> bool { use ui::file::{FilePrinter, TablePrinter}; use self::header::get_name_from_header; @@ -184,6 +243,10 @@ impl<'a> Module<'a> for Notes<'a> { self.command_add(matches.subcommand_matches("add").unwrap()) }, + Some("edit") => { + self.command_edit(matches.subcommand_matches("edit").unwrap()) + }, + Some("list") => { self.command_list(matches.subcommand_matches("list").unwrap()) },