Auto merge of #33 - matthiasbeyer:add-notes, r=matthiasbeyer
Add module: notes (journal) Module: Notes/journal Closes #14 Tracking branch for the module implementation.
This commit is contained in:
commit
670f0e16e9
13 changed files with 674 additions and 84 deletions
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
107
etc/cli.yml
107
etc/cli.yml
|
@ -258,3 +258,110 @@ subcommands:
|
|||
version: 0.1
|
||||
author: Matthias Beyer <mail@beyermatthias.de>
|
||||
|
||||
- notes:
|
||||
about: Notes module
|
||||
version: 0.1
|
||||
author: Matthias Beyer <mail@beyermatthias.de>
|
||||
subcommands:
|
||||
- add:
|
||||
about: Add Note
|
||||
version: 0.1
|
||||
author: Matthias Beyer <mail@beyermatthias.de>
|
||||
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
|
||||
|
||||
- edit:
|
||||
about: Edit Note
|
||||
version: 0.1
|
||||
author: Matthias Beyer <mail@beyermatthias.de>
|
||||
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
|
||||
author: Matthias Beyer <mail@beyermatthias.de>
|
||||
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 <mail@beyermatthias.de>
|
||||
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
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
14
src/main.rs
14
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);
|
||||
}
|
||||
|
|
|
@ -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<F>(&self, matches: &ArgMatches, generate_new_tags: F) -> bool
|
||||
where F: Fn(Vec<String>, &Vec<String>) -> Vec<String>
|
||||
{
|
||||
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::<Vec<String>>()
|
||||
})
|
||||
.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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -380,6 +330,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> {
|
||||
|
|
|
@ -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<String> 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<HP, F, R>(m: &Module,
|
||||
matches: &ArgMatches,
|
||||
parser: &Parser<HP>,
|
||||
generate_new_tags: F,
|
||||
rebuild_header: R) -> bool
|
||||
where HP: FileHeaderParser,
|
||||
F: Fn(Vec<String>, &Vec<String>) -> Vec<String>,
|
||||
R: Fn(&FHD, Vec<String>) -> Option<FHD>
|
||||
{
|
||||
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::<Vec<String>>()
|
||||
})
|
||||
.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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -2,8 +2,11 @@ use std::fmt::Debug;
|
|||
|
||||
use clap::ArgMatches;
|
||||
|
||||
use runtime::Runtime;
|
||||
|
||||
pub mod bm;
|
||||
pub mod helpers;
|
||||
pub mod notes;
|
||||
|
||||
/**
|
||||
* Module interface, each module has to implement this.
|
||||
|
@ -11,5 +14,7 @@ pub mod helpers;
|
|||
pub trait Module<'a> : Debug {
|
||||
fn exec(&self, matches: &ArgMatches) -> bool;
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
fn runtime(&self) -> &Runtime;
|
||||
}
|
||||
|
||||
|
|
64
src/module/notes/header.rs
Normal file
64
src/module/notes/header.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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<String>) -> 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<String> {
|
||||
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(""))
|
||||
}
|
||||
|
||||
pub fn rebuild_header_with_tags(header: &FHD, tags: Vec<String>) -> Option<FHD> {
|
||||
let name = get_name_from_header(header);
|
||||
Some(build_header(name, tags))
|
||||
}
|
||||
|
294
src/module/notes/mod.rs
Normal file
294
src/module/notes/mod.rs
Normal file
|
@ -0,0 +1,294 @@
|
|||
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;
|
||||
|
||||
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>,
|
||||
}
|
||||
|
||||
impl<'a> Notes<'a> {
|
||||
|
||||
pub fn new(rt: &'a Runtime<'a>) -> Notes<'a> {
|
||||
Notes {
|
||||
rt: rt,
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
.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 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)
|
||||
.and_then(|file| {
|
||||
info!("Created file in memory: {}", fileid);
|
||||
Some(self.rt.store().persist(&parser, file))
|
||||
})
|
||||
.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;
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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("edit") => {
|
||||
self.command_edit(matches.subcommand_matches("edit").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"
|
||||
}
|
||||
|
||||
fn runtime(&self) -> &Runtime {
|
||||
self.rt
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<'a> Debug for Notes<'a> {
|
||||
|
||||
fn fmt(&self, fmt: &mut Formatter) -> FMTResult {
|
||||
write!(fmt, "[Module][Notes]");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
let (editor, args) : (String, String) = {
|
||||
if let Some(editor) = self.config.editor() {
|
||||
editor + &self.config.editor_opts()[..]
|
||||
(editor, self.config.editor_opts())
|
||||
} else if let Some(editor) = self.configuration.editor() {
|
||||
editor + &self.configuration.editor_opts()[..]
|
||||
(editor, self.configuration.editor_opts())
|
||||
} else if let Ok(editor) = var("EDITOR") {
|
||||
editor
|
||||
(editor, String::from(""))
|
||||
} else {
|
||||
String::from("vim")
|
||||
(String::from("vim"), String::from(""))
|
||||
}
|
||||
};
|
||||
|
||||
let mut e = Command::new(editor);
|
||||
for arg in args.split(" ") {
|
||||
e.arg(arg);
|
||||
}
|
||||
e
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
59
src/ui/external/editor.rs
vendored
59
src/ui/external/editor.rs
vendored
|
@ -20,7 +20,7 @@ pub fn let_user_provide_content(rt: &Runtime) -> Option<String> {
|
|||
}
|
||||
|
||||
let output = {
|
||||
let mut cmd = Command::new(rt.editor());
|
||||
let mut cmd = rt.editor();
|
||||
cmd.arg(filepath);
|
||||
debug!("cmd = {:?}", cmd);
|
||||
cmd.spawn()
|
||||
|
@ -49,3 +49,60 @@ pub fn let_user_provide_content(rt: &Runtime) -> Option<String> {
|
|||
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<String>;
|
||||
}).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))
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue