diff --git a/Cargo.lock b/Cargo.lock index 614a58f..3dd5c2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,7 +118,7 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "c3" -version = "1.5.0" +version = "1.5.1" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index ab1b137..c0c7dbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "c3" -version = "1.5.0" +version = "1.5.1" edition = "2021" [dependencies] diff --git a/src/cli_app.rs b/src/cli_app.rs index 40032d6..9d798e5 100644 --- a/src/cli_app.rs +++ b/src/cli_app.rs @@ -1,10 +1,62 @@ -use super::todo_app::{App, Restriction, Todo, TodoList}; -use crate::{Args, TodoDisplay}; -use crate::{CliArgs, DisplayArgs, DoOnSelected}; +use c3::todo_app::{App, Restriction, Todo, TodoList}; +use clap_complete::Shell; +use crate::Args; +use c3::{DisplayArgs, TodoDisplay, DoOnSelected}; use clap::{Command, CommandFactory}; use clap_complete::{generate, Generator}; use std::io; use std::process; +use std::path::PathBuf; +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct CliArgs { + /// Search and select todo. Used for batch change operations + #[arg(short = 'S', long)] + search_and_select: Vec, + + #[arg(long)] + do_on_selected: Option, + + #[arg(short = 'b', long, default_value_t = false)] + batch_edit: bool, + + /// A todo message to append + #[arg(short = 'a', long)] + append_todo: Vec, + + /// A todo message to prepend + #[arg(short = 'A', long)] + prepend_todo: Vec, + + /// A todo file to append to current list + #[arg(long)] + append_file: Option, + + /// A todo file to output to + #[arg(short = 'o', long)] + output_file: Option, + + #[arg(short = 'p', long, default_value_t = false)] + print_path: bool, + + /// Minimal tree with no tree graphics + #[arg(short = 'M', long)] + minimal_tree: bool, + + /// List todos (non interactive) + #[arg(short = 'l', long)] + list: bool, + + /// Write contents of todo file in the stdout (non interactive) + #[arg(short = 's', long)] + stdout: bool, + + /// Generate completion for a certain shell + #[arg(short = 'c', long)] + completion: Option, +} pub struct NotCli; #[inline] diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2b9dd98 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,56 @@ +use clap::{Parser, ValueEnum}; +use todo_app::SortMethod; +use std::path::PathBuf; +use fileio::get_todo_path; +use std::fmt; + +pub mod date; +pub mod fileio; +pub mod todo_app; + +#[derive(ValueEnum, Clone, Debug)] +pub enum DoOnSelected { + Delete, + Done, +} + + +#[derive(Parser, Debug, Default)] +#[command(author, version, about, long_about = None)] +pub struct AppArgs { + /// Performance mode, don't read dependencies + #[arg(short = 'n', long)] + pub no_tree: bool, + + #[command(flatten)] + pub display_args: DisplayArgs, + + /// Path to todo file (and notes sibling directory) + #[arg(default_value=get_todo_path().unwrap().into_os_string())] + pub todo_path: PathBuf, + + /// Sort method, how sortings are done in the app + #[arg(long, default_value = "normal")] + pub sort_method: SortMethod, +} + +#[derive(Parser, Debug, Default)] +#[command(author, version, about, long_about = None)] +pub struct DisplayArgs { + /// Show done todos too + #[arg(short = 'd', long, default_value_t = false)] + show_done: bool, + + /// String before done todos + #[arg(long, default_value_t=String::from("[x] "))] + done_string: String, + + /// String before undone todos + #[arg(long, default_value_t=String::from("[ ] "))] + undone_string: String, +} + +pub trait TodoDisplay: fmt::Display { + fn display_with_args(&self, args: &DisplayArgs) -> String; +} + diff --git a/src/main.rs b/src/main.rs index d426757..5a52bc4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,155 +1,39 @@ // vim:fileencoding=utf-8:foldmethod=marker -use clap::{Parser, ValueEnum}; -use clap_complete::Shell; -use std::{fmt, io}; +use std::io; +use clap::Parser; pub(crate) mod cli_app; -pub(crate) mod date; -pub(crate) mod fileio; -pub(crate) mod todo_app; pub(crate) mod tui_app; -use crate::fileio::get_todo_path; -use std::path::PathBuf; -use todo_app::App; - -fn main() -> io::Result<()> { - let args = Args::parse(); - let mut app = App::new(args.app_args); - - if cli_app::run(&mut app, args.cli_args).is_err() { - let result = tui_app::run(&mut app, args.tui_args); - tui_app::shutdown()?; - result - } else { - Ok(()) - } -} +use c3::{ + todo_app::App, + AppArgs, +}; +use cli_app::CliArgs; +use tui_app::TuiArgs; /// A tree-like todo application that makes you smile #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] pub struct Args { #[command(flatten)] - app_args: AppArgs, - - #[command(flatten)] - cli_args: CliArgs, + pub app_args: AppArgs, #[command(flatten)] - tui_args: TuiArgs, -} - -#[derive(ValueEnum, Clone, Debug)] -pub enum DoOnSelected { - Delete, - Done, -} - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct CliArgs { - /// Search and select todo. Used for batch change operations - #[arg(short = 'S', long)] - search_and_select: Vec, - - #[arg(long)] - do_on_selected: Option, - - #[arg(short = 'b', long, default_value_t = false)] - batch_edit: bool, - - /// A todo message to append - #[arg(short = 'a', long)] - append_todo: Vec, - - /// A todo message to prepend - #[arg(short = 'A', long)] - prepend_todo: Vec, - - /// A todo file to append to current list - #[arg(long)] - append_file: Option, - - /// A todo file to output to - #[arg(short = 'o', long)] - output_file: Option, - - #[arg(short = 'p', long, default_value_t = false)] - print_path: bool, - - /// Minimal tree with no tree graphics - #[arg(short = 'M', long)] - minimal_tree: bool, - - /// List todos (non interactive) - #[arg(short = 'l', long)] - list: bool, - - /// Write contents of todo file in the stdout (non interactive) - #[arg(short = 's', long)] - stdout: bool, - - /// Generate completion for a certain shell - #[arg(short = 'c', long)] - completion: Option, -} - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct TuiArgs { - /// Alternative way of rendering, render minimum amount of todos - #[arg(long)] - minimal_render: bool, - - /// String behind highlighted todo in TUI mode - #[arg(short='H', long, default_value_t=String::from(">>"))] - highlight_string: String, - - /// Enable TUI module at startup - #[arg(short = 'm', long)] - enable_module: bool, -} - -#[derive(ValueEnum, Clone, Debug, PartialEq)] -pub enum SortMethod { - AbandonedFirst, - Normal, -} - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct AppArgs { - /// Performance mode, don't read dependencies - #[arg(short = 'n', long)] - no_tree: bool, + pub cli_args: CliArgs, #[command(flatten)] - display_args: DisplayArgs, - - /// Path to todo file (and notes sibling directory) - #[arg(default_value=get_todo_path().unwrap().into_os_string())] - todo_path: PathBuf, - - /// Sort method, how sortings are done in the app - #[arg(long, default_value = "normal")] - sort_method: SortMethod, + pub tui_args: TuiArgs, } -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -pub struct DisplayArgs { - /// Show done todos too - #[arg(short = 'd', long, default_value_t = false)] - show_done: bool, - - /// String before done todos - #[arg(long, default_value_t=String::from("[x] "))] - done_string: String, +fn main() -> io::Result<()> { + let args = Args::parse(); + let mut app = App::new(args.app_args); - /// String before undone todos - #[arg(long, default_value_t=String::from("[ ] "))] - undone_string: String, + if cli_app::run(&mut app, args.cli_args).is_err() { + let result = tui_app::run(&mut app, args.tui_args); + tui_app::shutdown()?; + result + } else { + Ok(()) + } } -trait TodoDisplay: fmt::Display { - fn display_with_args(&self, args: &DisplayArgs) -> String; -} diff --git a/src/todo_app.rs b/src/todo_app.rs index 0f535c1..c5b860f 100644 --- a/src/todo_app.rs +++ b/src/todo_app.rs @@ -5,11 +5,12 @@ use std::path::Path; use std::str::{FromStr, Lines}; use std::{io, path::PathBuf}; mod clipboard; +use clap::ValueEnum; use clipboard::Clipboard; pub use todo::schedule::Schedule; mod todo; mod todo_list; -use crate::{fileio, AppArgs, SortMethod}; +use crate::{fileio, AppArgs}; use std::rc::Rc; pub use todo::Todo; @@ -21,25 +22,45 @@ struct SearchPosition { matching_indices: Vec, } -pub(crate) fn ord_by_abandonment_coefficient(a: &Todo, b: &Todo) -> cmp::Ordering { - let order = b - .abandonment_coefficient() - .total_cmp(&a.abandonment_coefficient()); - if order.is_eq() { - return a.cmp(b); +#[derive(ValueEnum, Clone, Debug, PartialEq, Default)] +pub enum SortMethod { + #[default] + Normal, + AbandonedFirst, +} + +impl SortMethod { + pub fn cmp_function(&self) -> fn(&Todo, &Todo) -> cmp::Ordering { + match self { + Self::AbandonedFirst => { + |a:&Todo, b:&Todo| { + let order = b + .abandonment_coefficient() + .total_cmp(&a.abandonment_coefficient()); + if order.is_eq() { + return a.cmp(b); + } + order + } + } + Self::Normal => { + |a:&Todo, b:&Todo| { + a.cmp(b) + } + } + } } - order } pub type Restriction = Rc bool>; pub struct App { notes_dir: PathBuf, clipboard: Clipboard, - pub(super) todo_list: TodoList, - pub(super) index: usize, + pub todo_list: TodoList, + pub index: usize, changed: bool, tree_path: Vec, - pub(super) args: AppArgs, + pub args: AppArgs, removed_todos: Vec, tree_search_positions: Vec, x_index: usize, @@ -92,7 +113,7 @@ fn first_word_parse(input: &str) -> (Option, String) { impl App { #[inline] - pub(crate) fn new(args: AppArgs) -> Self { + pub fn new(args: AppArgs) -> Self { let notes_dir = fileio::append_notes_to_path_parent(&args.todo_path); let todo_list = Self::read_a_todo_list(&args.todo_path, ¬es_dir, &args); let mut app = App { @@ -122,11 +143,10 @@ impl App { #[inline(always)] fn read_a_todo_list(path: &Path, notes_dir: &Path, args: &AppArgs) -> TodoList { let mut todo_list = TodoList::read(path); - if args.sort_method == SortMethod::AbandonedFirst { - todo_list.set_todo_cmp(ord_by_abandonment_coefficient); - todo_list.sort(); - todo_list.changed = false; - } + + todo_list.set_todo_cmp(args.sort_method.cmp_function()); + todo_list.sort(); + todo_list.changed = false; if !args.no_tree { todo_list.read_dependencies(notes_dir); } @@ -432,7 +452,7 @@ impl App { } #[inline] - pub fn increment(&mut self) { + pub fn go_down(&mut self) { let size = self.len(); if size == 0 || self.index == size - 1 { self.index = 0; @@ -442,7 +462,7 @@ impl App { } #[inline] - pub fn decrement(&mut self) { + pub fn go_up(&mut self) { if self.index != 0 { self.index -= 1; } else { @@ -833,10 +853,7 @@ mod tests { Ok(path) } - fn write_test_todos(dir: &Path) -> io::Result { - let mut args = AppArgs::parse(); - fs::create_dir_all(dir.join("notes"))?; - args.todo_path = dir.join("todo"); + fn get_test_app(args: AppArgs) -> io::Result{ let mut app = App::new(args); app.append(String::from("Hello")); app.append(String::from("Goodbye")); @@ -857,6 +874,14 @@ mod tests { for _ in 0..3 { app.traverse_up(); } + Ok(app) + } + + fn write_test_todos(dir: &Path) -> io::Result { + let mut args = AppArgs::parse(); + fs::create_dir_all(dir.join("notes"))?; + args.todo_path = dir.join("todo"); + let mut app = get_test_app(args)?; app.write()?; Ok(app) } @@ -1024,4 +1049,19 @@ mod tests { assert_eq!(string, expected_string); Ok(()) } + + #[test] + fn test_sort_method() -> io::Result<()> { + let todo_path = dir("test-sort-method")?.join("todo"); + let mut app = get_test_app(AppArgs { + sort_method: SortMethod::AbandonedFirst, + todo_path, + ..Default::default() + })?; + app.go_down(); + app.toggle_current_daily(); + assert!(app.todo().unwrap().schedule.is_some()); + assert_eq!(app.index(), 0); + Ok(()) + } } diff --git a/src/todo_app/todo.rs b/src/todo_app/todo.rs index d0ea8a9..7bb3b6e 100644 --- a/src/todo_app/todo.rs +++ b/src/todo_app/todo.rs @@ -210,6 +210,9 @@ impl Todo { pub fn abandonment_coefficient(&self) -> f64 { self.schedule.as_ref().map_or(1., |sch| { + if sch.is_reminder() { + return 1.; + } let days_diff = sch.days_diff(); if days_diff == 0 { 1. diff --git a/src/todo_app/todo/dependency.rs b/src/todo_app/todo/dependency.rs index 921a1ed..d6f9ffe 100644 --- a/src/todo_app/todo/dependency.rs +++ b/src/todo_app/todo/dependency.rs @@ -73,7 +73,7 @@ impl Dependency { } #[inline] - pub fn read(&mut self, path: &Path, todo_cmp: Option) -> io::Result<()> { + pub fn read(&mut self, path: &Path, todo_cmp: TodoCmp) -> io::Result<()> { let file_path = path.join(&self.name); let name_todo = format!("{}.todo", self.name); match self.mode { @@ -90,11 +90,9 @@ impl Dependency { self.mode = DependencyMode::TodoList; } self.todo_list = TodoList::read(&path.join(&self.name)); - if let Some(todo_cmp) = todo_cmp { - self.todo_list.set_todo_cmp(todo_cmp); - self.todo_list.sort(); - self.todo_list.changed = false; - } + self.todo_list.set_todo_cmp(todo_cmp); + self.todo_list.sort(); + self.todo_list.changed = false; self.todo_list.read_dependencies(path); } _ => {} diff --git a/src/todo_app/todo_list.rs b/src/todo_app/todo_list.rs index 084fbf1..57ce941 100644 --- a/src/todo_app/todo_list.rs +++ b/src/todo_app/todo_list.rs @@ -3,15 +3,25 @@ use std::io::{stdout, BufRead, BufWriter, Write}; use std::path::Path; use std::{cmp, io}; -use super::{App, Restriction, Todo}; +use super::{App, Restriction, SortMethod, Todo}; use crate::{DisplayArgs, TodoDisplay}; pub type TodoCmp = fn(&Todo, &Todo) -> cmp::Ordering; -#[derive(Debug, Eq, PartialEq, Clone, Default)] +#[derive(Debug, Eq, PartialEq, Clone)] pub struct TodoList { pub todos: Vec, pub(super) changed: bool, - pub todo_cmp: Option, + pub todo_cmp: TodoCmp, +} + +impl Default for TodoList { + fn default() -> Self { + Self { + todos: Vec::new(), + changed: false, + todo_cmp: SortMethod::default().cmp_function(), + } + } } type Output = Todo; @@ -119,21 +129,18 @@ impl TodoList { return Self::new(); } let file_data = read(filename).unwrap(); - let mut todolist = Self { + Self { todos: file_data .lines() .map_while(Result::ok) .flat_map(|line| line.parse()) .collect(), ..Default::default() - }; - todolist.sort_normal(); - todolist.changed = false; - todolist + } } pub fn set_todo_cmp(&mut self, sort: TodoCmp) { - self.todo_cmp = Some(sort); + self.todo_cmp = sort; } pub fn read_dependencies(&mut self, folder_name: &Path) -> io::Result<()> { @@ -278,16 +285,8 @@ impl TodoList { self.todos.push(item); } - fn todo_cmp(&self) -> TodoCmp { - if let Some(todo_cmp) = self.todo_cmp { - todo_cmp - } else { - Todo::cmp - } - } - fn compare_todos(&self, a: &Todo, b: &Todo) -> cmp::Ordering { - let todo_cmp = self.todo_cmp(); + let todo_cmp = self.todo_cmp; todo_cmp(a, b) } @@ -306,7 +305,6 @@ impl TodoList { #[inline(always)] pub fn reorder_last(&mut self) -> usize { - self.changed = true; self.reorder(self.todos.len() - 1) } @@ -357,14 +355,7 @@ impl TodoList { } pub fn sort(&mut self) { - let todo_cmp = self.todo_cmp(); - self.sort_by(todo_cmp); - } - - #[inline(always)] - fn sort_normal(&mut self) { - self.changed = true; - self.todos.sort() + self.sort_by(self.todo_cmp); } #[inline(always)] @@ -386,9 +377,11 @@ mod tests { fn get_todo_list() -> TodoList { let path = PathBuf::from("tests/TODO_LIST"); let mut todolist = TodoList::read(&path); + todolist.sort(); todolist .read_dependencies(&path) .expect("reading todo dependencies failed"); + todolist.changed = false; todolist } @@ -486,7 +479,7 @@ mod tests { fn test_initially_sorted() { let todo_list = get_todo_list(); let mut sorted_list = todo_list.clone(); - sorted_list.sort_normal(); + sorted_list.sort(); sorted_list.changed = false; assert_eq!(todo_list, sorted_list) diff --git a/src/tui_app.rs b/src/tui_app.rs index 2403fc5..236c635 100644 --- a/src/tui_app.rs +++ b/src/tui_app.rs @@ -8,6 +8,7 @@ use std::{ }; // }}} // lib {{{ +use clap::Parser; use crossterm::{ event::{ self, @@ -24,12 +25,11 @@ use tui_textarea::{CursorMove, Input, TextArea}; // mod {{{ mod modules; -use super::todo_app::{App, Restriction, Todo}; -use crate::{ +use c3::{ + todo_app::{App, Restriction, Todo, Schedule}, date, - todo_app::{ord_by_abandonment_coefficient, Schedule}, - TuiArgs, }; + use modules::{potato::Potato, Module}; // }}} #[derive(Debug)] @@ -66,6 +66,22 @@ pub struct TuiApp<'a> { todo_app: &'a mut App, } +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct TuiArgs { + /// Alternative way of rendering, render minimum amount of todos + #[arg(long)] + minimal_render: bool, + + /// String behind highlighted todo in TUI mode + #[arg(short='H', long, default_value_t=String::from(">>"))] + highlight_string: String, + + /// Enable TUI module at startup + #[arg(short = 'm', long)] + enable_module: bool, +} + impl<'a> TuiApp<'a> { #[inline] pub fn new(app: &'a mut App, args: TuiArgs) -> Self { @@ -456,10 +472,10 @@ impl<'a> TuiApp<'a> { if let event::Event::Mouse(mouse) = event { match mouse.kind { event::MouseEventKind::ScrollUp => { - self.todo_app.decrement(); + self.todo_app.go_up(); } event::MouseEventKind::ScrollDown => { - self.todo_app.increment(); + self.todo_app.go_down(); } _ => {} } @@ -471,10 +487,6 @@ impl<'a> TuiApp<'a> { self.nnn_open(); return Ok(HandlerOperation::Restart); } - Char('d') if key.modifiers == KeyModifiers::CONTROL => self - .todo_app - .current_list_mut() - .sort_by(ord_by_abandonment_coefficient), Char('x') => self.todo_app.cut_todo(), Char('d') => self.todo_app.toggle_current_daily(), Char('W') => self.todo_app.toggle_current_weekly(), @@ -495,8 +507,8 @@ impl<'a> TuiApp<'a> { self.nnn_output_todo(); return Ok(HandlerOperation::Restart); } - KeyCode::Down | Char('j') => self.todo_app.increment(), - KeyCode::Up | Char('k') => self.todo_app.decrement(), + KeyCode::Down | Char('j') => self.todo_app.go_down(), + KeyCode::Up | Char('k') => self.todo_app.go_up(), KeyCode::Right | Char('l') => self.todo_app.add_dependency_traverse_down(), KeyCode::Enter => self.todo_app.traverse_down(), KeyCode::Left | Char('h') => {