diff --git a/doc/src/06100-module-notes.md b/doc/src/06100-module-notes.md index 432b2df6..723f56ca 100644 --- a/doc/src/06100-module-notes.md +++ b/doc/src/06100-module-notes.md @@ -1,4 +1,16 @@ ## Notes {#sec:modules:notes} -The Notes module. +The Notes module is intended to keep notes. These notes can be inserted as plain +text, markdown or other markup languages. + +The notes module offers: + +* adding, removing and settings of tags +* listing notes, optionally filtered by + * tags + * `grep`ping through note content and listing + * the matches + * files with matches +* opening a note via `xdg-open` (rendered as HTML if content is written in + a markup language) diff --git a/imag-notes/Cargo.toml b/imag-notes/Cargo.toml new file mode 100644 index 00000000..8faaab35 --- /dev/null +++ b/imag-notes/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "imag-notes" +version = "0.1.0" +authors = ["Matthias Beyer "] + +[dependencies] +semver = "0.2.1" +clap = "2.1.1" +log = "0.3.5" +version = "2.0.1" + +[dependencies.libimagrt] +path = "../libimagrt" + +[dependencies.libimagnotes] +path = "../libimagnotes" + +[dependencies.libimagtag] +path = "../libimagtag" + +[dependencies.libimagutil] +path = "../libimagutil" + diff --git a/imag-notes/src/main.rs b/imag-notes/src/main.rs new file mode 100644 index 00000000..25d5fcf2 --- /dev/null +++ b/imag-notes/src/main.rs @@ -0,0 +1,138 @@ +extern crate clap; +#[macro_use] extern crate log; +extern crate semver; +#[macro_use] extern crate version; + +extern crate libimagnotes; +extern crate libimagrt; +extern crate libimagtag; +extern crate libimagutil; + +use std::process::exit; + +use libimagrt::edit::Edit; +use libimagrt::runtime::Runtime; +use libimagnotes::note::Note; +use libimagutil::trace::trace_error; + +mod ui; +use ui::build_ui; + +fn main() { + let name = "imag-notes"; + let version = &version!()[..]; + let about = "Note taking helper"; + let ui = build_ui(Runtime::get_default_cli_builder(name, version, about)); + let rt = { + let rt = Runtime::new(ui); + if rt.is_ok() { + rt.unwrap() + } else { + println!("Could not set up Runtime"); + println!("{:?}", rt.err().unwrap()); + exit(1); + } + }; + + debug!("Hello. Logging was just enabled"); + debug!("I already set up the Runtime object and build the commandline interface parser."); + debug!("Lets get rollin' ..."); + + rt.cli() + .subcommand_name() + .map(|name| { + debug!("Call: {}", name); + match name { + "create" => create(&rt), + "delete" => delete(&rt), + "edit" => edit(&rt), + "list" => list(&rt), + _ => { + debug!("Unknown command"); // More error handling + }, + }; + }); +} + +fn name_from_cli(rt: &Runtime, subcmd: &str) -> String { + rt.cli().subcommand_matches(subcmd).unwrap().value_of("name").map(String::from).unwrap() +} + +fn create(rt: &Runtime) { + let name = name_from_cli(rt, "create"); + Note::new(rt.store(), name.clone(), String::new()) + .map_err(|e| trace_error(&e)) + .ok(); + + if rt.cli().subcommand_matches("create").unwrap().is_present("edit") { + if !edit_entry(rt, name) { + exit(1); + } + } +} + +fn delete(rt: &Runtime) { + Note::delete(rt.store(), String::from(name_from_cli(rt, "delete"))) + .map_err(|e| trace_error(&e)) + .map(|_| println!("Ok")) + .ok(); +} + +fn edit(rt: &Runtime) { + edit_entry(rt, name_from_cli(rt, "edit")); +} + +fn edit_entry(rt: &Runtime, name: String) -> bool { + let note = Note::retrieve(rt.store(), name); + if note.is_err() { + trace_error(¬e.err().unwrap()); + warn!("Cannot edit nonexistent Note"); + return false + } + + let mut note = note.unwrap(); + if let Err(e) = note.edit_content(rt) { + trace_error(&e); + warn!("Editing failed"); + return false + } + true +} + +fn list(rt: &Runtime) { + use std::cmp::Ordering; + + let iter = Note::all_notes(rt.store()); + if iter.is_err() { + trace_error(&iter.err().unwrap()); + exit(1); + } + + let mut iter = iter.unwrap() + .filter_map(|note| { + match note { + Err(e) => { + trace_error(&e); + None + }, + Ok(e) => Some(e) + } + }) + .collect::>(); + + iter.sort_by(|note_a, note_b| { + if let (Ok(a), Ok(b)) = (note_a.get_name(), note_b.get_name()) { + return a.cmp(&b) + } else { + return Ordering::Greater; + } + }); + + for note in iter { + note.get_name() + .map(|name| println!("{}", name)) + .map_err(|e| trace_error(&e)) + .ok(); + } +} + diff --git a/imag-notes/src/ui.rs b/imag-notes/src/ui.rs new file mode 100644 index 00000000..e68d5f88 --- /dev/null +++ b/imag-notes/src/ui.rs @@ -0,0 +1,57 @@ +use clap::{Arg, ArgGroup, App, SubCommand}; + +use libimagtag::ui::tag_argument; +use libimagtag::ui::tag_argument_name; + +pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { + app + .subcommand(SubCommand::with_name("create") + .about("Create a note") + .version("0.1") + .arg(Arg::with_name("name") + .long("name") + .short("n") + .takes_value(true) + .required(true) + .help("Create Note with this name")) + .arg(Arg::with_name("edit") + .long("edit") + .short("e") + .takes_value(false) + .required(false) + .help("Edit after creating")) + ) + + .subcommand(SubCommand::with_name("delete") + .about("Delete a Note") + .version("0.1") + .arg(Arg::with_name("name") + .long("name") + .short("n") + .takes_value(true) + .required(true) + .help("Delete Note with this name"))) + + .subcommand(SubCommand::with_name("edit") + .about("Edit a Note") + .version("0.1") + .arg(Arg::with_name("name") + .long("name") + .short("n") + .takes_value(true) + .required(true) + .help("Edit Note with this name")) + + .arg(tag_argument()) + .group(ArgGroup::with_name("editargs") + .args(&[tag_argument_name(), "name"]) + .required(true)) + ) + + .subcommand(SubCommand::with_name("list") + .about("List Notes") + .version("0.1")) + +} + + diff --git a/libimagnotes/Cargo.toml b/libimagnotes/Cargo.toml new file mode 100644 index 00000000..7998a296 --- /dev/null +++ b/libimagnotes/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "libimagnotes" +version = "0.1.0" +authors = ["Matthias Beyer "] + +[dependencies] +semver = "0.2" +log = "0.3.5" +toml = "0.1.25" + +[dependencies.libimagstore] +path = "../libimagstore" + +[dependencies.libimagrt] +path = "../libimagrt" + +[dependencies.libimagtag] +path = "../libimagtag" + diff --git a/libimagnotes/src/error.rs b/libimagnotes/src/error.rs new file mode 100644 index 00000000..b55659f6 --- /dev/null +++ b/libimagnotes/src/error.rs @@ -0,0 +1,86 @@ +use std::error::Error; +use std::fmt::Error as FmtError; +use std::clone::Clone; +use std::fmt::{Display, Formatter}; + +/** + * Kind of error + */ +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum NoteErrorKind { + StoreWriteError, + StoreReadError, + HeaderTypeError, + NoteToEntryConversion, + // Nothing here yet +} + +fn note_error_type_as_str(e: &NoteErrorKind) -> &'static str { + match e { + &NoteErrorKind::StoreWriteError => "Error writing store", + &NoteErrorKind::StoreReadError => "Error reading store", + &NoteErrorKind::HeaderTypeError => "Header type error", + &NoteErrorKind::NoteToEntryConversion => "Error converting Note instance to Entry instance", + } +} + +impl Display for NoteErrorKind { + + fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> { + try!(write!(fmt, "{}", note_error_type_as_str(self))); + Ok(()) + } + +} + +/** + * Store error type + */ +#[derive(Debug)] +pub struct NoteError { + err_type: NoteErrorKind, + cause: Option>, +} + +impl NoteError { + + /** + * Build a new NoteError from an NoteErrorKind, optionally with cause + */ + pub fn new(errtype: NoteErrorKind, cause: Option>) -> NoteError { + NoteError { + err_type: errtype, + cause: cause, + } + } + + /** + * Get the error type of this NoteError + */ + pub fn err_type(&self) -> NoteErrorKind { + self.err_type.clone() + } + +} + +impl Display for NoteError { + + fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> { + try!(write!(fmt, "[{}]", note_error_type_as_str(&self.err_type.clone()))); + Ok(()) + } + +} + +impl Error for NoteError { + + fn description(&self) -> &str { + note_error_type_as_str(&self.err_type.clone()) + } + + fn cause(&self) -> Option<&Error> { + self.cause.as_ref().map(|e| &**e) + } + +} + diff --git a/libimagnotes/src/lib.rs b/libimagnotes/src/lib.rs new file mode 100644 index 00000000..f9319e8e --- /dev/null +++ b/libimagnotes/src/lib.rs @@ -0,0 +1,14 @@ +#[macro_use] extern crate log; +extern crate semver; +extern crate toml; + +extern crate libimagrt; +#[macro_use] extern crate libimagstore; +extern crate libimagtag; + +module_entry_path_mod!("notes", "0.1.0"); + +pub mod error; +pub mod note; +pub mod result; + diff --git a/libimagnotes/src/note.rs b/libimagnotes/src/note.rs new file mode 100644 index 00000000..46ac6c95 --- /dev/null +++ b/libimagnotes/src/note.rs @@ -0,0 +1,190 @@ +use std::collections::BTreeMap; +use std::ops::{DerefMut, Deref}; + +use toml::Value; + +use libimagrt::runtime::Runtime; +use libimagrt::edit::{Edit, EditResult}; +use libimagstore::storeid::IntoStoreId; +use libimagstore::storeid::StoreId; +use libimagstore::storeid::StoreIdIterator; +use libimagstore::store::FileLockEntry; +use libimagstore::store::Store; +use libimagtag::tag::Tag; +use libimagtag::tagable::Tagable; +use libimagtag::result::Result as TagResult; + +use module_path::ModuleEntryPath; +use result::Result; +use error::NoteError as NE; +use error::NoteErrorKind as NEK; + +pub struct Note<'a> { + entry: FileLockEntry<'a>, +} + +impl<'a> Note<'a> { + + pub fn new(store: &Store, name: String, text: String) -> Result { + use std::ops::DerefMut; + + debug!("Creating new Note: '{}'", name); + let fle = { + let lockentry = store.create(ModuleEntryPath::new(name.clone()).into_storeid()); + if lockentry.is_err() { + return Err(NE::new(NEK::StoreWriteError, Some(Box::new(lockentry.err().unwrap())))); + } + let mut lockentry = lockentry.unwrap(); + + { + let mut entry = lockentry.deref_mut(); + + { + let mut header = entry.get_header_mut(); + let setres = header.set("note", Value::Table(BTreeMap::new())); + if setres.is_err() { + let kind = NEK::StoreWriteError; + return Err(NE::new(kind, Some(Box::new(setres.err().unwrap())))); + } + + let setres = header.set("note.name", Value::String(name)); + if setres.is_err() { + let kind = NEK::StoreWriteError; + return Err(NE::new(kind, Some(Box::new(setres.err().unwrap())))); + } + } + + *entry.get_content_mut() = text; + } + + lockentry + }; + + Ok(Note { entry: fle }) + } + + pub fn set_name(&mut self, n: String) -> Result<()> { + let mut header = self.entry.deref_mut().get_header_mut(); + header.set("note.name", Value::String(n)) + .map_err(|e| NE::new(NEK::StoreWriteError, Some(Box::new(e)))) + .map(|_| ()) + } + + pub fn get_name(&self) -> Result { + let header = self.entry.deref().get_header(); + match header.read("note.name") { + Ok(Some(Value::String(s))) => Ok(String::from(s)), + Ok(_) => { + let e = NE::new(NEK::HeaderTypeError, None); + Err(NE::new(NEK::StoreReadError, Some(Box::new(e)))) + }, + Err(e) => Err(NE::new(NEK::StoreReadError, Some(Box::new(e)))) + } + } + + pub fn set_text(&mut self, n: String) { + *self.entry.deref_mut().get_content_mut() = n + } + + pub fn get_text(&self) -> &String { + self.entry.deref().get_content() + } + + pub fn delete(store: &Store, name: String) -> Result<()> { + store.delete(ModuleEntryPath::new(name).into_storeid()) + .map_err(|e| NE::new(NEK::StoreWriteError, Some(Box::new(e)))) + } + + pub fn retrieve(store: &Store, name: String) -> Result { + store.retrieve(ModuleEntryPath::new(name).into_storeid()) + .map_err(|e| NE::new(NEK::StoreWriteError, Some(Box::new(e)))) + .map(|entry| Note { entry: entry }) + } + + pub fn all_notes(store: &Store) -> Result { + store.retrieve_for_module("notes") + .map(|iter| NoteIterator::new(store, iter)) + .map_err(|e| NE::new(NEK::StoreReadError, Some(Box::new(e)))) + } + +} + +impl<'a> Edit for Note<'a> { + + fn edit_content(&mut self, rt: &Runtime) -> EditResult<()> { + self.entry.edit_content(rt) + } + +} + +impl<'a> Tagable for Note<'a> { + + fn get_tags(&self) -> TagResult> { + self.entry.deref().get_tags() + } + + fn set_tags(&mut self, ts: Vec) -> TagResult<()> { + self.entry.deref_mut().set_tags(ts) + } + + fn add_tag(&mut self, t: Tag) -> TagResult<()> { + self.entry.deref_mut().add_tag(t) + } + + fn remove_tag(&mut self, t: Tag) -> TagResult<()> { + self.entry.deref_mut().remove_tag(t) + } + + fn has_tag(&self, t: &Tag) -> TagResult { + self.entry.deref().has_tag(t) + } + + fn has_tags(&self, ts: &Vec) -> TagResult { + self.entry.deref().has_tags(ts) + } + +} + +trait FromStoreId { + fn from_storeid<'a>(&'a Store, StoreId) -> Result>; +} + +impl<'a> FromStoreId for Note<'a> { + + fn from_storeid<'b>(store: &'b Store, id: StoreId) -> Result> { + debug!("Loading note from storeid: '{:?}'", id); + match store.retrieve(id) { + Err(e) => Err(NE::new(NEK::StoreReadError, Some(Box::new(e)))), + Ok(entry) => Ok(Note { entry: entry }), + } + } + +} + +pub struct NoteIterator<'a> { + store: &'a Store, + iditer: StoreIdIterator, +} + +impl<'a> NoteIterator<'a> { + + pub fn new(store: &'a Store, iditer: StoreIdIterator) -> NoteIterator<'a> { + NoteIterator { + store: store, + iditer: iditer, + } + } + +} + +impl<'a> Iterator for NoteIterator<'a> { + type Item = Result>; + + fn next(&mut self) -> Option>> { + self.iditer + .next() + .map(|id| Note::from_storeid(self.store, id)) + } + +} + diff --git a/libimagnotes/src/result.rs b/libimagnotes/src/result.rs new file mode 100644 index 00000000..df40d8c7 --- /dev/null +++ b/libimagnotes/src/result.rs @@ -0,0 +1,6 @@ +use std::result::Result as RResult; + +use error::NoteError; + +pub type Result = RResult; +