Merge branch 'rework' into 'main'

Rework

See merge request paris8-rust/rnote!10
This commit is contained in:
Volodymyr Patuta 2020-12-12 16:56:07 +01:00
commit 5e518c3366
4 changed files with 279 additions and 156 deletions

View File

@ -1,21 +1,20 @@
use anyhow::Result; use anyhow::{anyhow, Result};
use dialoguer::{theme::ColorfulTheme, Input};
use rnote::{app, process}; use rnote::{app, process};
mod rnote; mod rnote;
/// Check if variable `EDITOR` is set. /// Check if variable `EDITOR` and `XDG_DATA_HOME` are set.
fn check() -> Result<()> { fn check() -> Result<()> {
let editor = std::env::var("EDITOR").unwrap_or("".to_owned()); let editor = std::env::var("EDITOR").unwrap_or("".to_owned());
if editor.is_empty() { let data_home = std::env::var("XDG_DATA_HOME").unwrap_or("".to_owned());
let editor: String = Input::with_theme(&ColorfulTheme::default()) if editor.is_empty() || data_home.is_empty() {
.with_prompt("Your text editor") Err(anyhow!(
.interact_text()?; "Please make sure variables EDITOR and XDG_DATA_HOME are set.\n\n\texport XDG_DATA_HOME=\"$HOME/.local/share\""
std::env::set_var("EDITOR", editor); ))
} } else {
Ok(()) Ok(())
} }
}
fn main() -> Result<()> { fn main() -> Result<()> {
let mut app = app::make_app(); let mut app = app::make_app();
@ -34,3 +33,14 @@ fn main() -> Result<()> {
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore]
fn check_test() {
assert!(check().is_ok());
}
}

View File

@ -11,15 +11,15 @@ pub fn make_app() -> App<'static, 'static> {
SubCommand::with_name("new") SubCommand::with_name("new")
.alias("n") .alias("n")
.about("Create new note") .about("Create new note")
.arg(
Arg::with_name("header")
.index(1)
.help("Give name to the note."),
)
.arg( .arg(
Arg::with_name("category") Arg::with_name("category")
.help("Create note in category.") .help("Create note in category.")
.index(2), .index(2),
)
.arg(
Arg::with_name("header")
.index(1)
.help("Give name to the file."),
), ),
) )
.subcommand( .subcommand(
@ -31,6 +31,7 @@ pub fn make_app() -> App<'static, 'static> {
Arg::with_name("date") Arg::with_name("date")
.help("Delete all notes created at given date.") .help("Delete all notes created at given date.")
.short("d") .short("d")
.conflicts_with("header")
.long("date"), .long("date"),
), ),
) )
@ -60,6 +61,7 @@ pub fn make_app() -> App<'static, 'static> {
Arg::with_name("word") Arg::with_name("word")
.help("Search by word.") .help("Search by word.")
.short("w") .short("w")
.conflicts_with("header")
.long("word"), .long("word"),
) )
.arg(Arg::with_name("header").help("Name of the note.")), .arg(Arg::with_name("header").help("Name of the note.")),
@ -70,13 +72,15 @@ pub fn make_app() -> App<'static, 'static> {
Arg::with_name("all") Arg::with_name("all")
.help("Show all notes.") .help("Show all notes.")
.short("a") .short("a")
.conflicts_with("header")
.long("all"), .long("all"),
) )
.arg( .arg(
Arg::with_name("category") Arg::with_name("category")
.help("Show all notes from a category/date") .help("Show all notes from a category/date")
.short("c") .short("c")
.long("category"), .long("category")
.conflicts_with("header"),
) )
.arg(Arg::with_name("header").help("Name of the note.")), .arg(Arg::with_name("header").help("Name of the note.")),
) )

View File

@ -7,12 +7,12 @@ use walkdir::WalkDir;
/// Get the path to the root directory of all notes. /// Get the path to the root directory of all notes.
pub fn get_base_path() -> Result<String> { pub fn get_base_path() -> Result<String> {
let home = env::var("HOME")?; let home = env::var("XDG_DATA_HOME")?;
Ok(format!("{}/.rnote/", home)) Ok(format!("{}/rnote/", home))
} }
/// Get path to a category/date directory. /// Get path to a category/date directory.
fn get_path(category: &str) -> Result<String> { fn get_category_path(category: &str) -> Result<String> {
let base = get_base_path()?; let base = get_base_path()?;
let date = Utc::now().format("%Y-%m-%d"); let date = Utc::now().format("%Y-%m-%d");
match category.is_empty() { match category.is_empty() {
@ -21,6 +21,57 @@ fn get_path(category: &str) -> Result<String> {
} }
} }
/// Get all note paths.
pub fn get_all_notes() -> Result<Vec<String>> {
let path = get_base_path()?;
let mut files: Vec<String> = 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<Vec<String>> {
let base = get_base_path()?;
let path = format!("{}{}", base, category);
let mut files: Vec<String> = 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. /// Create directory for a note.
pub fn create_dir(category: &str) -> Result<()> { pub fn create_dir(category: &str) -> Result<()> {
let base = get_base_path()?; let base = get_base_path()?;
@ -45,40 +96,8 @@ pub fn create_dir(category: &str) -> Result<()> {
Ok(()) 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. /// Find a path to desired note.
pub fn find_path(header: &str) -> Result<Option<String>> { pub fn get_note_path(header: &str) -> Result<Vec<String>> {
let mut paths: Vec<String> = Vec::new(); let mut paths: Vec<String> = Vec::new();
let base = get_base_path()?; let base = get_base_path()?;
let header = format!("{}.md", header); let header = format!("{}.md", header);
@ -99,60 +118,12 @@ pub fn find_path(header: &str) -> Result<Option<String>> {
if paths.is_empty() { if paths.is_empty() {
Err(anyhow!("Note not found.")) Err(anyhow!("Note not found."))
} else { } else {
if paths.len() == 1 { Ok(paths)
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. /// Find all notes that contain a given string.
pub fn remove(header: &str) -> Result<()> { pub fn get_files_by_word(word: &str) -> Result<Vec<String>> {
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;
let path = get_base_path()?; let path = get_base_path()?;
let mut paths: Vec<String> = Vec::new(); let mut paths: Vec<String> = Vec::new();
for (_, file) in WalkDir::new(path) for (_, file) in WalkDir::new(path)
@ -174,10 +145,115 @@ 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<Option<String>> {
let mut paths: Vec<String> = get_note_path(header)?;
let mut p: Vec<String> = paths.clone();
let r = p[0].find("rnote").unwrap_or(0);
p = p.into_iter().map(|mut s| s.drain(r..).collect()).collect();
if paths.len() == 1 {
Ok(Some(paths.remove(0)))
} else {
let selection = Select::with_theme(&ColorfulTheme::default()) let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Optionally choose a note") .with_prompt("Optionally choose a note")
.default(0) .default(0)
.items(&paths) .items(&p)
.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 path.is_none() {
return Err(anyhow!("Abort."));
}
if Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(format!("Do you want to delete {}?", header))
.interact()?
{
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<String> = get_files_by_word(word)?;
let mut p: Vec<String> = paths.clone();
let r = p[0].find("rnote").unwrap_or(0);
p = p.into_iter().map(|mut s| s.drain(r..).collect()).collect();
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Optionally choose a note")
.default(0)
.items(&p)
.interact_opt()?; .interact_opt()?;
if let Some(selection) = selection { if let Some(selection) = selection {
let editor = std::env::var("EDITOR")?; let editor = std::env::var("EDITOR")?;
@ -191,9 +267,9 @@ pub fn search_by_word(word: &str) -> Result<()> {
/// Show all notes. /// Show all notes.
pub fn show_all() -> Result<()> { pub fn show_all() -> Result<()> {
let path = get_base_path()?; let base: String = get_base_path()?;
let mut files: Vec<String> = Vec::new(); let mut files: Vec<String> = Vec::new();
for (_, file) in WalkDir::new(path) for (_, file) in WalkDir::new(base)
.into_iter() .into_iter()
.filter_map(|file| file.ok()) .filter_map(|file| file.ok())
.enumerate() .enumerate()
@ -210,7 +286,7 @@ pub fn show_all() -> Result<()> {
/// Show one note. /// Show one note.
pub fn show(header: &str) -> Result<()> { pub fn show(header: &str) -> Result<()> {
let path = find_path(header)?; let path = get_note_path_interractive(header)?;
match path { match path {
Some(s) => { Some(s) => {
let skin = show::make_skin(); let skin = show::make_skin();
@ -240,30 +316,22 @@ pub fn show_category(category: &str) -> Result<()> {
let skin = show::make_skin(); let skin = show::make_skin();
let md = &files.join("---\n"); let md = &files.join("---\n");
show::run_app(skin, md)?; show::run_app(skin, md)?;
}
Ok(()) Ok(())
} else {
Err(anyhow!("Category does not exist."))
}
} }
/// List all notes and optionally open one. /// List all notes and prompt to open one.
pub fn list_all() -> Result<()> { pub fn list_all_notes() -> Result<()> {
let path = get_base_path()?; let mut files: Vec<String> = get_all_notes()?;
let mut files: Vec<String> = Vec::new(); let mut p: Vec<String> = files.clone();
for (_, file) in WalkDir::new(path) let r = p[0].find("rnote").unwrap_or(0);
.into_iter() p = p.into_iter().map(|mut s| s.drain(r..).collect()).collect();
.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()) let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Optionally choose a note") .with_prompt("Optionally choose a note")
.default(0) .default(0)
.items(&files) .items(&p)
.interact_opt()?; .interact_opt()?;
if let Some(selection) = selection { if let Some(selection) = selection {
let editor = std::env::var("EDITOR")?; let editor = std::env::var("EDITOR")?;
@ -276,26 +344,14 @@ pub fn list_all() -> Result<()> {
/// List all notes in the given category and optionally open one. /// List all notes in the given category and optionally open one.
pub fn list_category(category: &str) -> Result<()> { pub fn list_category(category: &str) -> Result<()> {
let base = get_base_path()?; let mut files: Vec<String> = get_notes_in_category(category)?;
let path = format!("{}{}", base, category); let mut p: Vec<String> = files.clone();
let mut files: Vec<String> = Vec::new(); let r = p[0].find("rnote").unwrap_or(0);
if std::path::Path::new(&path).exists() { p = p.into_iter().map(|mut s| s.drain(r..).collect()).collect();
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()) let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Optionally choose a note") .with_prompt("Optionally choose a note")
.default(0) .default(0)
.items(&files) .items(&p)
.interact_opt()?; .interact_opt()?;
if let Some(selection) = selection { if let Some(selection) = selection {
let editor = std::env::var("EDITOR")?; let editor = std::env::var("EDITOR")?;
@ -303,7 +359,6 @@ pub fn list_category(category: &str) -> Result<()> {
.arg(files.remove(selection)) .arg(files.remove(selection))
.status()?; .status()?;
} }
}
Ok(()) Ok(())
} }
@ -346,3 +401,57 @@ fn remove_empty_dirs() -> Result<()> {
Ok(()) Ok(())
} }
// Make sure to remove rnote directory before tests.
#[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!(create("test", "test_word").is_ok());
assert!(get_files_by_word("test").is_ok());
}
#[test]
fn get_note_path_test() {
assert!(create("test", "test_path").is_ok());
assert!(get_note_path("test").is_ok());
}
#[test]
fn get_category_path_create_dir_test() {
assert!(create_dir("test_dir").is_ok());
assert!(get_category_path("test").is_ok());
}
#[test]
fn create_remove_test() {
assert!(create("test1", "test1").is_ok());
let data_home = std::env::var("XDG_DATA_HOME").unwrap_or("".to_owned());
assert!(remove_note(&format!("{}/rnote/test1/test1.md", data_home)).is_ok());
}
#[test]
fn remove_empty_dirs_test() {
assert!(create_dir("test_empty").is_ok());
assert!(remove_empty_dirs().is_ok());
}
#[test]
#[ignore]
fn wipe_date_test() {
assert!(wipe_date("1999-10-10").is_ok());
}
#[test]
fn get_notes_in_category_test() {
assert!(create("test", "test_c").is_ok());
assert!(get_notes_in_category("test_c").is_ok());
}
}

View File

@ -20,7 +20,7 @@ pub fn new(matches: &ArgMatches) -> Result<()> {
/// Process argument `remove`. /// Process argument `remove`.
pub fn remove(matches: &ArgMatches) -> Result<()> { pub fn remove(matches: &ArgMatches) -> Result<()> {
match matches.value_of("header") { match matches.value_of("header") {
Some(s) => notes::remove(s)?, Some(s) => notes::remove_interractive(s)?,
None => match matches.is_present("date") { None => match matches.is_present("date") {
true => { true => {
let date: String = Input::with_theme(&ColorfulTheme::default()) 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()) let header: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Name of your note") .with_prompt("Name of your note")
.interact_text()?; .interact_text()?;
notes::remove(&header)?; notes::remove_interractive(&header)?;
} }
}, },
} }
@ -62,7 +62,7 @@ pub fn list(matches: &ArgMatches) -> Result<()> {
.interact_text()?; .interact_text()?;
notes::list_category(&s)?; notes::list_category(&s)?;
} }
false => notes::list_all()?, false => notes::list_all_notes()?,
} }
Ok(()) Ok(())
} }
@ -71,7 +71,7 @@ pub fn list(matches: &ArgMatches) -> Result<()> {
pub fn search(matches: &ArgMatches) -> Result<()> { pub fn search(matches: &ArgMatches) -> Result<()> {
match matches.value_of("header") { match matches.value_of("header") {
Some(s) => { Some(s) => {
let p = notes::find_path(s)?; let p = notes::get_note_path_interractive(s)?;
match p { match p {
Some(s) => { Some(s) => {
let editor = std::env::var("EDITOR")?; let editor = std::env::var("EDITOR")?;