diff --git a/Cargo.lock b/Cargo.lock index 551612b..7d829e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,7 +118,7 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "c3" -version = "1.3.2" +version = "1.4.0" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index 7aa2d6d..da73465 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "c3" -version = "1.3.2" +version = "1.4.0" edition = "2021" [dependencies] diff --git a/README.md b/README.md index fa35d88..f2b6ab2 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ The default mode of the app is TUI mode. Keybinds are vim-like. Here they are: | P | enable module | | / | search current list for todo | | ? | search the whole tree for todo (tree search) | -| Tab | tree search next | +| n | tree search next | | o | open nnn file picker to choose a file to append to current list | | O | open nnn file picker to choose a file to output current list to | | Ctrl+o | open nnn file picker to choose a file to open | diff --git a/src/main.rs b/src/main.rs index 18bfb29..f4fcfce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ fn main() -> io::Result<()> { let mut app = App::new(args.app_args); if cli_app::run(&mut app, args.cli_args).is_err() { - let output = tui_app::run(&mut app); + let output = tui_app::run(&mut app, args.tui_args); { tui_app::shutdown()?; output @@ -35,6 +35,9 @@ pub struct Args { #[command(flatten)] cli_args: CliArgs, + + #[command(flatten)] + tui_args: TuiArgs, } #[derive(ValueEnum, Clone, Debug)] @@ -94,21 +97,29 @@ struct CliArgs { #[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, +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, - #[command(flatten)] - display_args: DisplayArgs, - /// Enable TUI module at startup #[arg(short = 'm', long)] enable_module: bool, +} + +#[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, + + #[command(flatten)] + display_args: DisplayArgs, /// Path to todo file (and notes sibling directory) #[arg(default_value=get_todo_path().unwrap().into_os_string())] diff --git a/src/todo_app.rs b/src/todo_app.rs index f933abf..dc6e242 100644 --- a/src/todo_app.rs +++ b/src/todo_app.rs @@ -1,3 +1,4 @@ +use std::collections::VecDeque; use std::fmt::Write; use std::fs::create_dir_all; use std::path::Path; @@ -31,7 +32,6 @@ pub struct App { pub(super) args: AppArgs, removed_todos: Vec, tree_search_positions: Vec, - last_query: String, x_index: usize, y_index: usize, restriction: RestrictionFunction, @@ -81,15 +81,11 @@ impl App { #[inline] pub(crate) fn new(args: AppArgs) -> Self { let notes_dir = fileio::append_notes_to_path_parent(&args.todo_path); - let mut todo_list = TodoList::read(&args.todo_path); - if !args.no_tree { - todo_list.read_dependencies(¬es_dir).expect("Failed to read dependencies"); - } + let todo_list = Self::read_a_todo_list(&args.todo_path, ¬es_dir, &args); let mut app = App { notes_dir, x_index: 0, y_index: 0, - last_query: String::new(), tree_search_positions: vec![], removed_todos: vec![], todo_list, @@ -111,17 +107,18 @@ impl App { } #[inline(always)] - fn read_a_todo_list(&self, path: &Path) -> TodoList { + fn read_a_todo_list(path: &Path, notes_dir: &Path, args: &AppArgs) -> TodoList { let mut todo_list = TodoList::read(path); - if !self.args.no_tree { - todo_list.read_dependencies(&self.notes_dir); + if !args.no_tree { + todo_list.read_dependencies(¬es_dir); } todo_list } #[inline] pub fn append_list_from_path(&mut self, path: &Path) { - let todo_list = self.read_a_todo_list(path); + let notes_dir = fileio::append_notes_to_path_parent(path); + let todo_list = Self::read_a_todo_list(path, ¬es_dir, &self.args); self.append_list(todo_list) } @@ -132,7 +129,8 @@ impl App { #[inline] pub fn open_path(&mut self, path: PathBuf) { - self.todo_list = self.read_a_todo_list(&path); + self.notes_dir = fileio::append_notes_to_path_parent(&path); + self.todo_list = Self::read_a_todo_list(&path, &self.notes_dir, &self.args); self.tree_path = vec![]; self.args.todo_path = path; } @@ -164,55 +162,56 @@ impl App { })) } - fn traverse_parents_from_root(&mut self, callback: fn(&mut App, &TodoList, &[usize])) { - self.todo_list.clone().traverse_tree(callback, None, self) - } - - fn add_to_tree_positions(&mut self, list: &TodoList, prior_indices: &[usize]) { - let mut matching_indices: Vec = vec![]; - for (i, todo) in list.todos(&self.restriction).iter().enumerate() { - if todo.matches(self.last_query.as_str()) { - matching_indices.push(i) - } - } - if !matching_indices.is_empty() { - self.tree_search_positions.push(SearchPosition { - tree_path: prior_indices.to_vec(), - matching_indices, - }) - } - } - - pub fn tree_search(&mut self, query: Option) { - if let Some(query) = query { - self.last_query = query; - } + pub fn tree_search(&mut self, query: String) { self.tree_search_positions = vec![]; self.y_index = 0; self.x_index = 0; - if self.last_query.is_empty() { + if query.is_empty() { return; } - let before_position = SearchPosition { - tree_path: self.tree_path.clone(), - matching_indices: vec![self.index], - }; - self.tree_search_positions.push(before_position); - self.traverse_parents_from_root(Self::add_to_tree_positions); - self.search_next(); + let current_not_matches = self.todo().map_or(true, |todo| !todo.matches(&query)); + self.search_tree(query); + + if current_not_matches { + self.search_next(); + } + } + + pub fn search_tree(&mut self, query: String) { + let mut lists: VecDeque<(Vec, &TodoList)> = VecDeque::from([(vec![],&self.todo_list)]); + while let Some((indices, current_list)) = lists.pop_back() { + let mut matching_indices: Vec = vec![]; + for (i,todo) in current_list.filter(&self.restriction).enumerate() { + let mut todo_indices = indices.clone(); + todo_indices.push(i); + if todo.matches(&query) { + matching_indices.push(i) + } + if let Some(list) = todo.dependency.as_ref().and_then(|dep| dep.todo_list()) { + lists.push_back((todo_indices,list)) + } + } + if !matching_indices.is_empty() { + self.tree_search_positions.push(SearchPosition { + tree_path: indices.to_vec(), + matching_indices, + }) + } + } } pub fn batch_editor_messages(&mut self) { let restriction = &self.restriction; - let todos = self.current_list().todos(restriction); - let mut content = if todos.is_empty() { - String::new() + let mut todos_iter = self.current_list().filter(restriction); + let first_item = todos_iter.next(); + let mut content = if let Some(item) = first_item { + format!("# INDEX PRIORITY MESSAGE\n{: <7} {: <8} {}\n",0, item.priority(), item.message) } else { - String::from("# INDEX PRIORITY MESSAGE\n") + String::new() }; - for (i, line) in todos.iter().enumerate() { - writeln!(content, "{i: <7} {: <8} {}", line.priority(), line.message); + for (i, line) in todos_iter.enumerate() { + writeln!(content, "{: <7} {: <8} {}",i+1, line.priority(), line.message); } let new_messages = fileio::open_temp_editor(Some(&content), fileio::temp_path("messages")).unwrap(); let new_messages = new_messages.lines(); @@ -371,17 +370,15 @@ impl App { } else { self.current_list_mut().sort(); } - if self.is_undone_empty() { - while self.traverse_up() && !self.is_undone_empty() { - self.toggle_current_done() - } + while self.is_undone_empty() && self.traverse_up() { + self.toggle_current_done() } } #[inline] pub fn read(&mut self) { self.changed = false; - self.todo_list = self.read_a_todo_list(&self.args.todo_path); + self.todo_list = Self::read_a_todo_list(&self.args.todo_path, &self.notes_dir, &self.args); let len = self.max_tree_length(); self.tree_path.truncate(len); } @@ -705,6 +702,12 @@ impl App { self.display_list(self.current_list()) } + #[inline] + pub fn display_current_slice(&self, min: usize, max: usize) -> Vec { + self.current_list().display_slice(&self.args.display_args, &self.restriction, min, max) + } + + #[inline] pub fn display_list(&self, todo_list: &TodoList) -> Vec { todo_list.display(&self.args.display_args, &self.restriction) @@ -907,8 +910,8 @@ mod tests { let mut app = write_test_todos(&dir)?; remove_dir_all(dir)?; let query = String::from("nod"); - app.tree_search(Some(query)); - let position = &app.tree_search_positions[1]; + app.tree_search(query); + let position = &app.tree_search_positions[0]; assert_eq!(position.tree_path, vec![2, 0]); assert_eq!(position.matching_indices, vec![0]); Ok(()) diff --git a/src/todo_app/todo_list.rs b/src/todo_app/todo_list.rs index bc40b37..fa44b37 100644 --- a/src/todo_app/todo_list.rs +++ b/src/todo_app/todo_list.rs @@ -78,16 +78,15 @@ impl TodoList { pub fn traverse_tree( &self, callback: fn(&mut App, &TodoList, &[usize]), - prior_indices: Option>, + prior_indices: Vec, app: &mut App, ) { - let prior_indices = prior_indices.unwrap_or_default(); - callback(app, self, prior_indices.as_slice()); + callback(app, self, &prior_indices); for (i, todo) in self.todos.iter().enumerate() { if let Some(todo_list) = todo.dependency.as_ref().and_then(|dep| dep.todo_list()) { let mut prior_indices = prior_indices.clone(); prior_indices.push(i); - todo_list.traverse_tree(callback, Some(prior_indices), app); + todo_list.traverse_tree(callback, prior_indices, app); } } } @@ -195,25 +194,40 @@ impl TodoList { } pub fn messages(&self, restriction: &RestrictionFunction) -> Vec<&str> { - self.todos(restriction) - .iter() + self.todos.iter() + .filter(|todo| restriction(todo)) .map(|todo| todo.message.as_str()) .collect() } + pub fn filter<'a>(&'a self, restriction: &'a RestrictionFunction) -> std::iter::Filter, impl FnMut(&&'a Todo) -> bool> { + self.todos.iter().filter(|todo| restriction(todo)) + } + pub fn display(&self, args: &DisplayArgs, restriction: &RestrictionFunction) -> Vec { - self.todos(restriction) - .iter() + self.todos.iter() + .filter(|todo| restriction(todo)) + .map(|todo| todo.display(args)) + .collect() + } + + pub fn display_slice(&self, args: &DisplayArgs, restriction: &RestrictionFunction, min: usize, max: usize) -> Vec { + self.todos.iter() + .filter(|todo| restriction(todo)) + .skip(min) + .take(max) .map(|todo| todo.display(args)) .collect() } pub fn len(&self, restriction: &RestrictionFunction) -> usize { - self.todos(restriction).len() + self.todos.iter() + .filter(|todo| restriction(todo)) + .count() } pub fn is_empty(&self, restriction: &RestrictionFunction) -> bool { - self.todos(restriction).is_empty() + self.len(restriction) == 0 } pub fn true_position_in_list(&self, index: usize, restriction: &RestrictionFunction) -> usize { diff --git a/src/tui_app.rs b/src/tui_app.rs index 6952be5..925af2b 100644 --- a/src/tui_app.rs +++ b/src/tui_app.rs @@ -26,7 +26,7 @@ use tui_textarea::{CursorMove, Input, TextArea}; mod modules; use super::todo_app::{App, Todo}; -use crate::todo_app::RestrictionFunction; +use crate::{todo_app::RestrictionFunction, TuiArgs}; use crate::{date, todo_app::PriorityType}; use modules::{potato::Potato, Module}; // }}} @@ -85,13 +85,13 @@ pub fn restart(terminal: &mut Terminal>) -> io::Res } #[inline] -pub fn run(app: &mut App) -> io::Result<()> { +pub fn run(app: &mut App, args: TuiArgs) -> io::Result<()> { startup()?; let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?; let mut potato_module = Potato::new(None); let mut list_state = ListState::default(); - let mut app = TuiApp::new(app, &mut potato_module, app.args.enable_module); + let mut app = TuiApp::new(app, &mut potato_module, args); loop { terminal.draw(|frame| app.ui(frame, &mut list_state))?; @@ -126,7 +126,7 @@ pub struct TuiApp<'a> { on_submit: Option ()>, on_delete: Option ()>, on_input: Option ()>, - module_enabled: bool, + args: TuiArgs, module: &'a mut dyn Module<'a>, textarea: TextArea<'a>, todo_app: &'a mut App, @@ -134,7 +134,7 @@ pub struct TuiApp<'a> { impl<'a> TuiApp<'a> { #[inline] - pub fn new(app: &'a mut App, module: &'a mut dyn Module<'a>, module_enabled: bool) -> Self { + pub fn new(app: &'a mut App, module: &'a mut dyn Module<'a>, args: TuiArgs) -> Self { let mut textarea = TextArea::default(); textarea.set_cursor_line_style(Style::default()); TuiApp { @@ -146,7 +146,7 @@ impl<'a> TuiApp<'a> { on_delete: None, show_right: true, text_mode: false, - module_enabled, + args, last_restriction: None, } } @@ -254,7 +254,7 @@ impl<'a> TuiApp<'a> { #[inline] fn on_tree_search(&mut self, str: String) { - self.todo_app.tree_search(Some(str)); + self.todo_app.tree_search(str); } #[inline] @@ -457,7 +457,7 @@ impl<'a> TuiApp<'a> { #[inline] pub fn update_editor(&mut self) -> io::Result { - if self.module_enabled { + if self.args.enable_module { if event::poll(std::time::Duration::from_millis( self.module.update_time_ms(), ))? { @@ -516,7 +516,7 @@ impl<'a> TuiApp<'a> { #[inline] fn update_no_editor(&mut self) -> io::Result { - if self.module_enabled { + if self.args.enable_module { if event::poll(std::time::Duration::from_millis( self.module.update_time_ms(), ))? { @@ -575,7 +575,7 @@ impl<'a> TuiApp<'a> { Char('J') => self.todo_app.decrease_current_priority(), Char('K') => self.todo_app.increase_current_priority(), Char(']') => self.show_right = !self.show_right, - Char('P') => self.module_enabled = !self.module_enabled, + Char('P') => self.args.enable_module = !self.args.enable_module, Char('>') => { self.todo_app.edit_or_add_note(); return Ok(Operation::Restart); @@ -587,8 +587,8 @@ impl<'a> TuiApp<'a> { Char('R') => self.todo_app.read(), Char('T') => self.todo_app.remove_current_dependent(), Char(' ') => self.todo_app.toggle_current_done(), - KeyCode::Tab => self.todo_app.search_next(), - Char('n') | Char('a') => self.prepend_prompt(), + Char('n') => self.todo_app.search_next(), + Char('a') => self.prepend_prompt(), Char('/') => self.search_prompt(), Char('?') => self.tree_search_prompt(), Char('A') => self.append_prompt(), @@ -652,7 +652,7 @@ impl<'a> TuiApp<'a> { #[inline] fn highlight_string(&self) -> &str { - self.todo_app.args.highlight_string.as_str() + self.args.highlight_string.as_str() } #[inline] @@ -677,6 +677,7 @@ impl<'a> TuiApp<'a> { dependency_layout, self.todo_app.display_list(todo_list), String::from("Todo dependencies"), + self.todo_app.index(), ) } } @@ -690,13 +691,21 @@ impl<'a> TuiApp<'a> { todo_layout: Rect, ) { let title = self.title(); + let display = if self.args.minimal_render { + let first = self.todo_app.index(); + let last = self.todo_app.len().min(todo_layout.height as usize+first-2); + self.todo_app.display_current_slice(first, last) + } else { + self.todo_app.display_current() + }; Self::render_todos_widget( self.highlight_string(), frame, Some(list_state), todo_layout, - self.todo_app.display_current(), + display, title, + self.todo_app.index(), ) } @@ -708,12 +717,16 @@ impl<'a> TuiApp<'a> { todo_layout: Rect, display_list: Vec, title: String, + index: usize, ) { match create_todo_widget(display_list, title, highlight_symbol) { TodoWidget::Paragraph(widget) => frame.render_widget(widget, todo_layout), TodoWidget::List(widget) => { if let Some(list_state) = list_state { - frame.render_stateful_widget(widget, todo_layout, list_state) + let widget = frame.render_stateful_widget(widget, todo_layout, list_state); + list_state.select(Some(index-list_state.offset())); + widget + } else { frame.render_widget(widget, todo_layout) } @@ -724,11 +737,15 @@ impl<'a> TuiApp<'a> { #[inline] pub fn ui(&mut self, frame: &mut Frame, list_state: &mut ListState) { let todo = self.todo_app.todo(); + if self.args.minimal_render { + list_state.select(Some(0)); + } else { + list_state.select(Some(self.todo_app.index())); + } - list_state.select(Some(self.todo_app.index())); let dependency_width = self.get_dependency_width(todo); - let main_layout = if self.module_enabled { + let main_layout = if self.args.enable_module { self.render_module_widget( frame, Direction::Vertical, @@ -748,7 +765,7 @@ impl<'a> TuiApp<'a> { Constraint::Percentage(100 - dependency_width), Constraint::Percentage(dependency_width), ]) - .split(main_layout[self.module_enabled as usize]); + .split(main_layout[self.args.enable_module as usize]); let todo_and_textarea_layout = Layout::default() .direction(Direction::Vertical)