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,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.\n\n\texport XDG_DATA_HOME=\"$HOME/.local/share\""
))
} 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());
}
}

View File

@ -11,15 +11,15 @@ pub fn make_app() -> App<'static, 'static> {
SubCommand::with_name("new")
.alias("n")
.about("Create new note")
.arg(
Arg::with_name("header")
.index(1)
.help("Give name to the note."),
)
.arg(
Arg::with_name("category")
.help("Create note in category.")
.index(2),
)
.arg(
Arg::with_name("header")
.index(1)
.help("Give name to the file."),
),
)
.subcommand(
@ -31,6 +31,7 @@ pub fn make_app() -> App<'static, 'static> {
Arg::with_name("date")
.help("Delete all notes created at given date.")
.short("d")
.conflicts_with("header")
.long("date"),
),
)
@ -60,6 +61,7 @@ pub fn make_app() -> App<'static, 'static> {
Arg::with_name("word")
.help("Search by word.")
.short("w")
.conflicts_with("header")
.long("word"),
)
.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")
.help("Show all notes.")
.short("a")
.conflicts_with("header")
.long("all"),
)
.arg(
Arg::with_name("category")
.help("Show all notes from a category/date")
.short("c")
.long("category"),
.long("category")
.conflicts_with("header"),
)
.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.
pub fn get_base_path() -> Result<String> {
let home = env::var("HOME")?;
Ok(format!("{}/.rnote/", 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<String> {
fn get_category_path(category: &str) -> Result<String> {
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<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.
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<Option<String>> {
pub fn get_note_path(header: &str) -> Result<Vec<String>> {
let mut paths: Vec<String> = Vec::new();
let base = get_base_path()?;
let header = format!("{}.md", header);
@ -99,60 +118,12 @@ pub fn find_path(header: &str) -> Result<Option<String>> {
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<Vec<String>> {
let path = get_base_path()?;
let mut paths: Vec<String> = Vec::new();
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())
.with_prompt("Optionally choose a note")
.default(0)
.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(&paths)
.items(&p)
.interact_opt()?;
if let Some(selection) = selection {
let editor = std::env::var("EDITOR")?;
@ -191,9 +267,9 @@ pub fn search_by_word(word: &str) -> Result<()> {
/// Show all notes.
pub fn show_all() -> Result<()> {
let path = get_base_path()?;
let base: String = get_base_path()?;
let mut files: Vec<String> = Vec::new();
for (_, file) in WalkDir::new(path)
for (_, file) in WalkDir::new(base)
.into_iter()
.filter_map(|file| file.ok())
.enumerate()
@ -210,7 +286,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();
@ -240,30 +316,22 @@ pub fn show_category(category: &str) -> Result<()> {
let skin = show::make_skin();
let md = &files.join("---\n");
show::run_app(skin, md)?;
Ok(())
} else {
Err(anyhow!("Category does not exist."))
}
Ok(())
}
/// List all notes and optionally open one.
pub fn list_all() -> Result<()> {
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());
}
}
}
/// List all notes and prompt to open one.
pub fn list_all_notes() -> Result<()> {
let mut files: Vec<String> = get_all_notes()?;
let mut p: Vec<String> = files.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(&files)
.items(&p)
.interact_opt()?;
if let Some(selection) = selection {
let editor = std::env::var("EDITOR")?;
@ -276,33 +344,20 @@ 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<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());
}
}
}
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<String> = get_notes_in_category(category)?;
let mut p: Vec<String> = files.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()?;
if let Some(selection) = selection {
let editor = std::env::var("EDITOR")?;
std::process::Command::new(editor)
.arg(files.remove(selection))
.status()?;
}
Ok(())
}
@ -346,3 +401,57 @@ fn remove_empty_dirs() -> Result<()> {
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`.
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")?;