diff --git a/src/main.rs b/src/main.rs index 3a41e13..77a93cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,19 @@ -use anyhow::Result; -use dialoguer::{theme::ColorfulTheme, Input}; +use anyhow::{anyhow, Result}; use rnote::{app, process}; mod rnote; -/// Check if variable `EDITOR` is set. +/// Check if variable `EDITOR` and `XDG_DATA_HOME` are set. fn check() -> Result<()> { let editor = std::env::var("EDITOR").unwrap_or("".to_owned()); - if editor.is_empty() { - let editor: String = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Your text editor") - .interact_text()?; - std::env::set_var("EDITOR", editor); + let data_home = std::env::var("XDG_DATA_HOME").unwrap_or("".to_owned()); + if editor.is_empty() || data_home.is_empty() { + Err(anyhow!( + "Please make sure variables EDITOR and XDG_DATA_HOME are set." + )) + } else { + Ok(()) } - - Ok(()) } fn main() -> Result<()> { @@ -34,3 +33,14 @@ fn main() -> Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[ignore] + fn check_test() { + assert!(check().is_ok()); + } +} diff --git a/src/rnote/notes.rs b/src/rnote/notes.rs index 448bd4a..b10a01a 100644 --- a/src/rnote/notes.rs +++ b/src/rnote/notes.rs @@ -7,12 +7,12 @@ use walkdir::WalkDir; /// Get the path to the root directory of all notes. pub fn get_base_path() -> Result { - let home = env::var("HOME")?; + let home = env::var("XDG_DATA_HOME")?; Ok(format!("{}/.rnote/", home)) } /// Get path to a category/date directory. -fn get_path(category: &str) -> Result { +fn get_category_path(category: &str) -> Result { let base = get_base_path()?; let date = Utc::now().format("%Y-%m-%d"); match category.is_empty() { @@ -21,6 +21,57 @@ fn get_path(category: &str) -> Result { } } +/// Get all note paths. +pub fn get_all_notes() -> Result> { + let path = get_base_path()?; + let mut files: Vec = Vec::new(); + for (_, file) in WalkDir::new(path) + .into_iter() + .filter_map(|file| file.ok()) + .enumerate() + { + if file.metadata()?.is_file() { + let p = file.path().to_str().unwrap_or(""); + if !p.is_empty() { + files.push(p.to_owned()); + } + } + } + if files.is_empty() { + Err(anyhow!("No notes found.")) + } else { + Ok(files) + } +} + +/// Get all notes in category. +pub fn get_notes_in_category(category: &str) -> Result> { + let base = get_base_path()?; + let path = format!("{}{}", base, category); + let mut files: Vec = Vec::new(); + if std::path::Path::new(&path).exists() { + for (_, file) in WalkDir::new(path) + .into_iter() + .filter_map(|file| file.ok()) + .enumerate() + { + if file.metadata()?.is_file() { + let p = file.path().to_str().unwrap_or(""); + if !p.is_empty() { + files.push(p.to_owned()); + } + } + } + if files.is_empty() { + Err(anyhow!("Category is empty.")) + } else { + Ok(files) + } + } else { + Err(anyhow!("Category no found.")) + } +} + /// Create directory for a note. pub fn create_dir(category: &str) -> Result<()> { let base = get_base_path()?; @@ -45,40 +96,8 @@ pub fn create_dir(category: &str) -> Result<()> { Ok(()) } -/// Create a new note. -pub fn create(header: &str, category: &str) -> Result<()> { - let editor = env::var("EDITOR")?; - let file = format!("{}{}.md", get_path(category)?, header); - create_dir(category)?; - is_duplicate(header, category)?; - let mut f = fs::File::create(&file)?; - f.set_permissions(fs::Permissions::from_mode(0o600))?; - f.write(format!("# {}\n", header).as_bytes())?; - Command::new(editor).arg(&file).status()?; - Ok(()) -} - -/// Check if potentially new note name already exists. -fn is_duplicate(header: &str, category: &str) -> Result<()> { - let file = format!("{}{}.md", get_path(category)?, header); - let path = format!("{}", get_path(category)?); - for entry in WalkDir::new(path) { - let entry = entry?; - let p: &str = match entry.path().to_str() { - Some(s) => s, - None => "", - }; - if p == file { - return Err(anyhow!( - "Duplicate in the same category/date. Choose another name." - )); - } - } - Ok(()) -} - /// Find a path to desired note. -pub fn find_path(header: &str) -> Result> { +pub fn get_note_path(header: &str) -> Result> { let mut paths: Vec = Vec::new(); let base = get_base_path()?; let header = format!("{}.md", header); @@ -99,60 +118,12 @@ pub fn find_path(header: &str) -> Result> { if paths.is_empty() { Err(anyhow!("Note not found.")) } else { - if paths.len() == 1 { - Ok(Some(paths.remove(0))) - } else { - let selection = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Optionally choose a note") - .default(0) - .items(&paths) - .interact_opt()?; - match selection { - Some(s) => Ok(Some(paths.remove(s))), - None => Ok(None), - } - } + Ok(paths) } } -/// Delete a note. -pub fn remove(header: &str) -> Result<()> { - let path = find_path(header)?; - if Confirm::with_theme(&ColorfulTheme::default()) - .with_prompt(format!("Do you want to delete {}?", header)) - .interact()? - && path.is_some() - { - println!("Deleting..."); - fs::remove_file(path.unwrap())?; - remove_empty_dirs()?; - println!("Successfully deleted."); - Ok(()) - } else { - Err(anyhow!("Abort.")) - } -} - -/// Modify a note. -pub fn modify(header: &str) -> Result<()> { - let editor = env::var("EDITOR")?; - let file = find_path(header)?; - match file { - Some(f) => { - Command::new(editor).arg(f).status()?; - println!("Edited successfully!"); - Ok(()) - } - None => { - println!("Abort."); - Ok(()) - } - } -} - -/// Search all notes that contain a given string. -pub fn search_by_word(word: &str) -> Result<()> { - extern crate fstream; +/// Find all notes that contain a given string. +pub fn get_files_by_word(word: &str) -> Result> { let path = get_base_path()?; let mut paths: Vec = Vec::new(); for (_, file) in WalkDir::new(path) @@ -174,6 +145,103 @@ pub fn search_by_word(word: &str) -> Result<()> { } } } + Ok(paths) +} + +/// Create a new note. +pub fn create(header: &str, category: &str) -> Result<()> { + let editor = env::var("EDITOR")?; + let file = format!("{}{}.md", get_category_path(category)?, header); + create_dir(category)?; + is_duplicate(header, category)?; + let mut f = fs::File::create(&file)?; + f.set_permissions(fs::Permissions::from_mode(0o600))?; + f.write(format!("# {}\n", header).as_bytes())?; + Command::new(editor).arg(&file).status()?; + Ok(()) +} + +/// Check if potentially new note name already exists. +fn is_duplicate(header: &str, category: &str) -> Result<()> { + let file = format!("{}{}.md", get_category_path(category)?, header); + let path = format!("{}", get_category_path(category)?); + for entry in WalkDir::new(path) { + let entry = entry?; + let p: &str = match entry.path().to_str() { + Some(s) => s, + None => "", + }; + if p == file { + return Err(anyhow!( + "Duplicate in the same category/date. Choose another name." + )); + } + } + Ok(()) +} + +/// Find a path to desired note and prompt to choose one to open. +pub fn get_note_path_interractive(header: &str) -> Result> { + let mut paths: Vec = get_note_path(header)?; + if paths.len() == 1 { + Ok(Some(paths.remove(0))) + } else { + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Optionally choose a note") + .default(0) + .items(&paths) + .interact_opt()?; + match selection { + Some(s) => Ok(Some(paths.remove(s))), + None => Ok(None), + } + } +} + +/// Delete a note. +pub fn remove_note(path: &str) -> Result<()> { + println!("Deleting..."); + fs::remove_file(path)?; + remove_empty_dirs()?; + println!("Successfully deleted."); + Ok(()) +} + +/// Prompt user to delete a note. +pub fn remove_interractive(header: &str) -> Result<()> { + let path = get_note_path_interractive(header)?; + if Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(format!("Do you want to delete {}?", header)) + .interact()? + && path.is_some() + { + remove_note(&path.unwrap())?; + Ok(()) + } else { + Err(anyhow!("Abort.")) + } +} + +/// Modify a note. +pub fn modify(header: &str) -> Result<()> { + let editor = env::var("EDITOR")?; + let file = get_note_path_interractive(header)?; + match file { + Some(f) => { + Command::new(editor).arg(f).status()?; + println!("Edited successfully!"); + Ok(()) + } + None => { + println!("Abort."); + Ok(()) + } + } +} + +/// Prompt user to open one of found notes by word. +pub fn search_by_word(word: &str) -> Result<()> { + let mut paths: Vec = get_files_by_word(word)?; let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Optionally choose a note") .default(0) @@ -191,17 +259,7 @@ pub fn search_by_word(word: &str) -> Result<()> { /// Show all notes. pub fn show_all() -> Result<()> { - let path = get_base_path()?; - let mut files: Vec = Vec::new(); - for (_, file) in WalkDir::new(path) - .into_iter() - .filter_map(|file| file.ok()) - .enumerate() - { - if file.metadata()?.is_file() { - files.push(fs::read_to_string(file.path())?); - } - } + let mut files: Vec = get_all_notes()?; let skin = show::make_skin(); let md = &files.join("---\n"); show::run_app(skin, md)?; @@ -210,7 +268,7 @@ pub fn show_all() -> Result<()> { /// Show one note. pub fn show(header: &str) -> Result<()> { - let path = find_path(header)?; + let path = get_note_path_interractive(header)?; match path { Some(s) => { let skin = show::make_skin(); @@ -224,42 +282,16 @@ pub fn show(header: &str) -> Result<()> { /// Show all notes in the given category. pub fn show_category(category: &str) -> Result<()> { - let base = get_base_path()?; - let path = format!("{}{}", base, category); - let mut files: Vec = Vec::new(); - if std::path::Path::new(&path).exists() { - for (_, file) in WalkDir::new(path) - .into_iter() - .filter_map(|file| file.ok()) - .enumerate() - { - if file.metadata()?.is_file() { - files.push(fs::read_to_string(file.path())?); - } - } - let skin = show::make_skin(); - let md = &files.join("---\n"); - show::run_app(skin, md)?; - } + let mut files: Vec = get_notes_in_category(category)?; + let skin = show::make_skin(); + let md = &files.join("---\n"); + show::run_app(skin, md)?; Ok(()) } -/// List all notes and optionally open one. -pub fn list_all() -> Result<()> { - let path = get_base_path()?; - let mut files: Vec = Vec::new(); - for (_, file) in WalkDir::new(path) - .into_iter() - .filter_map(|file| file.ok()) - .enumerate() - { - if file.metadata()?.is_file() { - let p = file.path().to_str().unwrap_or(""); - if !p.is_empty() { - files.push(p.to_owned()); - } - } - } +/// List all notes and prompt to open one. +pub fn list_all_notes() -> Result<()> { + let mut files: Vec = get_all_notes()?; let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Optionally choose a note") .default(0) @@ -276,33 +308,17 @@ pub fn list_all() -> Result<()> { /// List all notes in the given category and optionally open one. pub fn list_category(category: &str) -> Result<()> { - let base = get_base_path()?; - let path = format!("{}{}", base, category); - let mut files: Vec = Vec::new(); - if std::path::Path::new(&path).exists() { - for (_, file) in WalkDir::new(path) - .into_iter() - .filter_map(|file| file.ok()) - .enumerate() - { - if file.metadata()?.is_file() { - let p = file.path().to_str().unwrap_or(""); - if !p.is_empty() { - files.push(p.to_owned()); - } - } - } - let selection = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Optionally choose a note") - .default(0) - .items(&files) - .interact_opt()?; - if let Some(selection) = selection { - let editor = std::env::var("EDITOR")?; - std::process::Command::new(editor) - .arg(files.remove(selection)) - .status()?; - } + let mut files: Vec = get_notes_in_category(category)?; + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Optionally choose a note") + .default(0) + .items(&files) + .interact_opt()?; + if let Some(selection) = selection { + let editor = std::env::var("EDITOR")?; + std::process::Command::new(editor) + .arg(files.remove(selection)) + .status()?; } Ok(()) } @@ -346,3 +362,50 @@ fn remove_empty_dirs() -> Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_base_path_test() { + assert!(get_base_path().is_ok()); + } + + #[test] + fn find_by_word_test() { + assert!(get_files_by_word("test").is_ok()); + } + + #[test] + fn get_note_path_test() { + assert!(get_note_path("test").is_ok()); + } + + #[test] + fn get_category_path_create_dir_test() { + assert!(create_dir("test").is_ok()); + assert!(get_category_path("test").is_ok()); + } + + #[test] + fn create_remove_test() { + assert!(create("test", "test").is_ok()); + assert!(remove_note("test").is_ok()); + } + + #[test] + fn remove_empty_dirs_test() { + assert!(remove_empty_dirs().is_ok()); + } + + #[test] + fn wipe_date_test() { + assert!(wipe_date("1999-10-10").is_ok()); + } + + #[test] + fn get_notes_in_category_test() { + assert!(get_notes_in_category("test").is_ok()); + } +} diff --git a/src/rnote/process.rs b/src/rnote/process.rs index 05b83e1..b9fe746 100644 --- a/src/rnote/process.rs +++ b/src/rnote/process.rs @@ -20,7 +20,7 @@ pub fn new(matches: &ArgMatches) -> Result<()> { /// Process argument `remove`. pub fn remove(matches: &ArgMatches) -> Result<()> { match matches.value_of("header") { - Some(s) => notes::remove(s)?, + Some(s) => notes::remove_interractive(s)?, None => match matches.is_present("date") { true => { let date: String = Input::with_theme(&ColorfulTheme::default()) @@ -33,7 +33,7 @@ pub fn remove(matches: &ArgMatches) -> Result<()> { let header: String = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Name of your note") .interact_text()?; - notes::remove(&header)?; + notes::remove_interractive(&header)?; } }, } @@ -62,7 +62,7 @@ pub fn list(matches: &ArgMatches) -> Result<()> { .interact_text()?; notes::list_category(&s)?; } - false => notes::list_all()?, + false => notes::list_all_notes()?, } Ok(()) } @@ -71,7 +71,7 @@ pub fn list(matches: &ArgMatches) -> Result<()> { pub fn search(matches: &ArgMatches) -> Result<()> { match matches.value_of("header") { Some(s) => { - let p = notes::find_path(s)?; + let p = notes::get_note_path_interractive(s)?; match p { Some(s) => { let editor = std::env::var("EDITOR")?;