extern crate termion; extern crate xdg; use clap::{arg, Command}; use std::process::Command as RustCommand; use std::{ env::var, fs::{self, File}, io, path::{Path, PathBuf}, }; use termion::{color, style}; fn cli() -> Command<'static> { Command::new("not") .about("A simple notes taking app") .subcommand_required(false) .arg_required_else_help(false) .allow_external_subcommands(true) .allow_invalid_utf8_for_external_subcommands(true) .subcommand( Command::new("version") .about("Display current not version") .arg_required_else_help(false), ) .subcommand( Command::new("list") .about("List all notes") .arg_required_else_help(false), ) .subcommand( Command::new("add") .about("Add a new note") .arg(arg!([NAME] "The name of the note to add")) .arg_required_else_help(true), ) .subcommand( Command::new("edit") .about("Edit a note") .arg(arg!([NAME] "The name of the note to edit")) .arg_required_else_help(true), ) .subcommand( Command::new("remove") .about("Remove a note") .arg(arg!([NAME] "The name of the note to remove")) .arg_required_else_help(true), ) } fn validate_dir(path: &PathBuf) -> bool { if path.is_dir() { return true; } let dir_created = fs::create_dir(path); return match dir_created { Ok(_) => true, Err(_) => false, }; } fn get_notes_path() -> Result { let xdg_dirs = xdg::BaseDirectories::with_prefix("not").unwrap(); if validate_dir(&xdg_dirs.get_data_home()) { return Ok(xdg_dirs.get_data_home()); } else if validate_dir(&PathBuf::from("/usr/share/not")) { return Ok(PathBuf::from("/usr/share/not")); } Err(()) } fn note_exists(path: &Path) -> Result { return Ok(std::path::Path::new(path).exists()); } fn remove_note_filename_extension(name: &str) -> String { name.replace(".md", "") } fn print_notes(path: &Path) -> Result<(), io::Error> { let mut entries = fs::read_dir(path)? .map(|res| res.map(|e| e.file_name())) .collect::, io::Error>>()?; entries.sort(); for (i, file) in entries.iter().enumerate() { let file = remove_note_filename_extension( &file .clone() .into_string() .expect("Failed to transform note filename into string")[..], ); println!( "\n{}{} - {}{}", color::Fg(color::Green), i + 1, file, style::Reset ); } return Ok(()); } fn create_note(note_path: &Path) -> Result { let file = File::create(note_path)?; return Ok(file); } fn edit_note_act(path: &PathBuf, name: &str) -> Result<(), io::Error> { let mut path_note = path.clone(); path_note.push(name); println!( "{}Edit note: {}{}", color::Fg(color::Blue), name, style::Reset ); if !note_exists(path_note.as_path())? { println!("Note does not exists!"); return Ok(()); } edit_note(&path_note.as_path())?; return Ok(()); } fn remove_note(path: &Path) -> std::io::Result<()> { fs::remove_file(path)?; Ok(()) } fn input_yn(msg: &str) -> io::Result { let mut input = String::new(); println!("{}", msg); io::stdin().read_line(&mut input)?; Ok(&input.trim()[0..1].to_uppercase() == "Y") } fn remove_note_act(path: &PathBuf, name: &str) -> std::io::Result { let mut note_path = path.clone(); note_path.push(name); if !note_exists(¬e_path)? { return Ok(false); } println!( "{}Remove note: {}{}", color::Fg(color::Red), name, style::Reset ); if !input_yn("Are you sure that you want to remove this note? ") .expect("Failed to read input line") { return Ok(false); } remove_note(note_path.as_path())?; println!("\nNote removed"); Ok(true) } fn add_note_act(path: &PathBuf, name: &str) -> std::io::Result<()> { let mut path_note = path.clone(); path_note.push(name); println!( "{}Add note: {}{}", color::Fg(color::Blue), name, style::Reset ); if note_exists(path_note.as_path())? { println!("Note already exists!"); return Ok(()); } create_note(&path_note.as_path())?; edit_note(&path_note.as_path())?; return Ok(()); } fn edit_note(note_path: &Path) -> std::io::Result<()> { let editor = var("EDITOR").unwrap(); RustCommand::new(editor) .arg(¬e_path) .spawn() .expect("Failed to execute editor") .wait()?; return Ok(()); } fn return_note_filename_by_name(name: &str) -> String { let mut name_str = name.to_lowercase().replace(" ", "_").to_string(); name_str.push_str(".md"); return name_str; } fn list_notes_act(notes_path: &Path) { print_notes(notes_path).expect("Failed to display notes"); } fn version_act() -> io::Result<()> { let version = env!("CARGO_PKG_VERSION"); println!( "{}Version: {}{}", color::Fg(color::Green), version, style::Reset ); Ok(()) } fn main() { let notes_path = get_notes_path().expect("Failed to read data folder"); let matches = cli().get_matches(); match matches.subcommand() { Some(("add", sub_matches)) => { let note_name = return_note_filename_by_name( sub_matches.get_one::("NAME").expect("required"), ); add_note_act(¬es_path, ¬e_name).expect("Invalid note"); } Some(("edit", sub_matches)) => { let note_name = return_note_filename_by_name( sub_matches.get_one::("NAME").expect("required"), ); edit_note_act(¬es_path, ¬e_name).expect("Invalid note"); } Some(("version", _)) => version_act().expect("Failed to get the not version!"), Some(("remove", sub_matches)) => { let note_name = return_note_filename_by_name( sub_matches.get_one::("NAME").expect("required"), ); remove_note_act(¬es_path, ¬e_name).expect("Invalid note"); } Some(("list", _)) => list_notes_act(notes_path.as_path()), _ => list_notes_act(notes_path.as_path()), } }