diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index 5ed73059..f338c6e5 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -1,5 +1,5 @@ use crate::interactive::{ - app::navigation::NavigationState, + app::navigation::Navigation, sorted_entries, widgets::{glob_search, MainWindow, MainWindowProps}, ByteVisualization, CursorDirection, CursorMode, DisplayOptions, EntryDataBundle, MarkEntryMode, @@ -35,8 +35,8 @@ pub struct Cursor { #[derive(Default)] pub struct AppState { - pub normal_mode: NavigationState, - pub glob_mode: Option, + pub navigation: Navigation, + pub glob_navigation: Option, pub entries: Vec, pub sorting: SortMode, pub message: Option, @@ -50,12 +50,14 @@ pub enum ProcessingResult { } impl AppState { - pub fn navigation_mut(&mut self) -> &mut NavigationState { - self.glob_mode.as_mut().unwrap_or(&mut self.normal_mode) + pub fn navigation_mut(&mut self) -> &mut Navigation { + self.glob_navigation + .as_mut() + .unwrap_or(&mut self.navigation) } - pub fn navigation(&self) -> &NavigationState { - self.glob_mode.as_ref().unwrap_or(&self.normal_mode) + pub fn navigation(&self) -> &Navigation { + self.glob_navigation.as_ref().unwrap_or(&self.navigation) } pub fn draw( @@ -77,16 +79,16 @@ impl AppState { display, state: self, }; - let mut cursor = Cursor::default(); + let mut cursor = Cursor::default(); let result = draw_window(window, props, terminal, &mut cursor); if cursor.show { _ = terminal.show_cursor(); + _ = terminal.set_cursor(cursor.x, cursor.y); } else { _ = terminal.hide_cursor(); } - _ = terminal.set_cursor(cursor.x, cursor.y); result } @@ -119,26 +121,28 @@ impl AppState { let mut tree_view = self.tree_view(traversal); self.reset_message(); + + let glob_focussed = self.focussed == Glob; let mut handled = true; match key { - Char('?') if self.focussed != FocussedPane::Glob => self.toggle_help_pane(window), - Char('/') if self.focussed != FocussedPane::Glob => { - self.toggle_glob_search(window); + Esc => { + if let Some(value) = self.handle_quit(tree_view.as_mut(), window) { + return value; + } } Char('\t') => { self.cycle_focus(window); } - Ctrl('c') if self.focussed != FocussedPane::Glob => { + Char('/') if !glob_focussed => { + self.toggle_glob_search(window); + } + Char('?') if !glob_focussed => self.toggle_help_pane(window), + Ctrl('c') if !glob_focussed => { return Ok(ProcessingResult::ExitRequested(WalkResult { num_errors: tree_view.traversal().io_errors, })) } - Char('q') if self.focussed != FocussedPane::Glob => { - if let Some(value) = self.handle_quit(tree_view.as_mut(), window) { - return value; - } - } - Esc => { + Char('q') if !glob_focussed => { if let Some(value) = self.handle_quit(tree_view.as_mut(), window) { return value; } @@ -226,7 +230,7 @@ impl AppState { } fn tree_view<'a>(&mut self, traversal: &'a mut Traversal) -> Box { - let tree_view: Box = if let Some(glob_source) = &self.glob_mode { + let tree_view: Box = if let Some(glob_source) = &self.glob_navigation { Box::new(GlobTreeView { traversal, glob_tree_root: glob_source.tree_root, @@ -239,47 +243,43 @@ impl AppState { fn search_glob_pattern(&mut self, tree_view: &mut dyn TreeView, glob_pattern: &str) { use FocussedPane::*; - let search_results = - glob_search(tree_view.tree(), self.normal_mode.view_root, glob_pattern); - match search_results { - Ok(search_results) => { - if search_results.is_empty() { - self.message = Some("Not found".into()); - } else { - if let Some(glob_source) = &self.glob_mode { - tree_view.tree_as_mut().remove_node(glob_source.tree_root); - } + match glob_search(tree_view.tree(), self.navigation.view_root, glob_pattern) { + Ok(matches) if matches.is_empty() => { + self.message = Some("No match found".into()); + } + Ok(matches) => { + if let Some(glob_source) = &self.glob_navigation { + tree_view.tree_as_mut().remove_node(glob_source.tree_root); + } - let tree_root = tree_view.tree_as_mut().add_node(EntryData::default()); - let glob_source = NavigationState { - tree_root, - view_root: tree_root, - selected: Some(tree_root), - ..Default::default() - }; - self.glob_mode = Some(glob_source); - - for idx in search_results { - tree_view.tree_as_mut().add_edge(tree_root, idx, ()); - } + let tree_root = tree_view.tree_as_mut().add_node(EntryData::default()); + let glob_source = Navigation { + tree_root, + view_root: tree_root, + selected: Some(tree_root), + ..Default::default() + }; + self.glob_navigation = Some(glob_source); - let glob_tree_view = GlobTreeView { - traversal: tree_view.traversal_as_mut(), - glob_tree_root: tree_root, - }; + for idx in matches { + tree_view.tree_as_mut().add_edge(tree_root, idx, ()); + } - let new_entries = glob_tree_view.sorted_entries(tree_root, self.sorting); + let glob_tree_view = GlobTreeView { + traversal: tree_view.traversal_as_mut(), + glob_tree_root: tree_root, + }; + let new_entries = glob_tree_view.sorted_entries(tree_root, self.sorting); - let new_entries = self - .navigation_mut() - .selected - .map(|previously_selected| (previously_selected, new_entries)); + let new_entries = self + .navigation_mut() + .selected + .map(|previously_selected| (previously_selected, new_entries)); - self.enter_node(new_entries); - self.focussed = Main; - } + self.enter_node(new_entries); + self.focussed = Main; } - _ => self.message = Some("Search error, try again".into()), + Err(err) => self.message = Some(err.to_string()), } } @@ -291,7 +291,7 @@ impl AppState { use FocussedPane::*; match self.focussed { Main => { - if self.glob_mode.is_some() { + if self.glob_navigation.is_some() { self.handle_glob_quit(tree_view, window); } else { return Some(Ok(ProcessingResult::ExitRequested(WalkResult { @@ -314,17 +314,16 @@ impl AppState { fn handle_glob_quit(&mut self, tree_view: &mut dyn TreeView, window: &mut MainWindow) { use FocussedPane::*; self.focussed = Main; - if let Some(glob_source) = &self.glob_mode { + if let Some(glob_source) = &self.glob_navigation { tree_view.tree_as_mut().remove_node(glob_source.tree_root); } - self.glob_mode = None; + self.glob_navigation = None; window.glob_pane = None; - let normal_tree_view = NormalTreeView { + let tree_view = NormalTreeView { traversal: tree_view.traversal_as_mut(), }; - - self.entries = normal_tree_view.sorted_entries(self.navigation().view_root, self.sorting); + self.entries = tree_view.sorted_entries(self.navigation().view_root, self.sorting); } } diff --git a/src/interactive/app/handlers.rs b/src/interactive/app/handlers.rs index ee982989..62fff820 100644 --- a/src/interactive/app/handlers.rs +++ b/src/interactive/app/handlers.rs @@ -140,10 +140,7 @@ impl AppState { window.glob_pane = Some(GlobPane::default()); Glob } - Glob => { - window.glob_pane = None; - Main - } + Glob => unreachable!("BUG: glob pane must catch the input leading here"), } } @@ -329,7 +326,7 @@ impl AppState { } pub fn glob_root(&self) -> Option { - self.glob_mode.as_ref().map(|e| e.tree_root) + self.glob_navigation.as_ref().map(|e| e.tree_root) } fn mark_entry_by_index( diff --git a/src/interactive/app/navigation.rs b/src/interactive/app/navigation.rs index 8e307ba5..8bb2e3e8 100644 --- a/src/interactive/app/navigation.rs +++ b/src/interactive/app/navigation.rs @@ -5,14 +5,14 @@ use std::collections::BTreeMap; use super::{CursorDirection, EntryDataBundle}; #[derive(Default)] -pub struct NavigationState { +pub struct Navigation { pub tree_root: TreeIndex, pub view_root: TreeIndex, pub selected: Option, pub bookmarks: BTreeMap, } -impl NavigationState { +impl Navigation { pub fn get_previously_selected_index( &self, view_root: TreeIndex, @@ -61,19 +61,17 @@ impl NavigationState { None => 0, }; - let selected = entries + entries .get(next_selected_pos) .or_else(|| entries.last()) - .map(|b| b.index); - - selected.or(self.selected) + .map(|b| b.index) + .or(self.selected) } pub fn select(&mut self, selected: Option) { self.selected = selected; if let Some(selected) = selected { - let view_root = self.view_root; - self.bookmarks.insert(view_root, selected); + self.bookmarks.insert(self.view_root, selected); } } } diff --git a/src/interactive/app/tree_view.rs b/src/interactive/app/tree_view.rs index 5718fa9a..be6f5293 100644 --- a/src/interactive/app/tree_view.rs +++ b/src/interactive/app/tree_view.rs @@ -41,6 +41,14 @@ pub struct NormalTreeView<'a> { } impl<'a> TreeView for NormalTreeView<'a> { + fn traversal(&self) -> &Traversal { + self.traversal + } + + fn traversal_as_mut(&mut self) -> &mut Traversal { + self.traversal + } + fn tree(&self) -> &Tree { &self.traversal.tree } @@ -60,14 +68,6 @@ impl<'a> TreeView for NormalTreeView<'a> { remove_entries(self.traversal, index) } - fn traversal(&self) -> &Traversal { - self.traversal - } - - fn traversal_as_mut(&mut self) -> &mut Traversal { - self.traversal - } - fn recompute_sizes_recursively(&mut self, mut index: TreeIndex) { loop { self.traversal @@ -95,6 +95,14 @@ pub struct GlobTreeView<'a> { } impl<'a> TreeView for GlobTreeView<'a> { + fn traversal(&self) -> &Traversal { + self.traversal + } + + fn traversal_as_mut(&mut self) -> &mut Traversal { + self.traversal + } + fn tree(&self) -> &Tree { &self.traversal.tree } @@ -135,10 +143,6 @@ impl<'a> TreeView for GlobTreeView<'a> { parent } - fn remove_entries(&mut self, index: TreeIndex) -> usize { - remove_entries(self.traversal, index) - } - fn path_of(&self, node_idx: TreeIndex) -> PathBuf { path_of(&self.traversal.tree, node_idx, Some(self.glob_tree_root)) } @@ -156,12 +160,8 @@ impl<'a> TreeView for GlobTreeView<'a> { current_path(&self.traversal.tree, view_root, Some(self.glob_tree_root)) } - fn traversal(&self) -> &Traversal { - self.traversal - } - - fn traversal_as_mut(&mut self) -> &mut Traversal { - self.traversal + fn remove_entries(&mut self, index: TreeIndex) -> usize { + remove_entries(self.traversal, index) } fn recompute_sizes_recursively(&mut self, mut index: TreeIndex) { diff --git a/src/interactive/widgets/glob.rs b/src/interactive/widgets/glob.rs index 69f932e9..b02ac137 100644 --- a/src/interactive/widgets/glob.rs +++ b/src/interactive/widgets/glob.rs @@ -15,6 +15,8 @@ use tui::{ }; use tui_react::util::{block_width, rect}; use tui_react::{draw_text_nowrap_fn, Terminal}; +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::UnicodeWidthStr; use crate::interactive::Cursor; @@ -26,7 +28,11 @@ pub struct GlobPaneProps { #[derive(Default)] pub struct GlobPane { pub input: String, - cursor_position: usize, + /// The index of the grapheme the cursor currently points to. + /// This hopefully rightfully assumes that a grapheme will be matching the block size on screen + /// and is treated as 'one character'. If not, it will be off, which isn't the end of the world. + // TODO: use `tui-textarea` for proper cursor handling, needs native crossterm events. + cursor_grapheme_idx: usize, } impl GlobPane { @@ -51,45 +57,45 @@ impl GlobPane { } fn move_cursor_left(&mut self) { - let cursor_moved_left = self.cursor_position.saturating_sub(1); - self.cursor_position = self.clamp_cursor(cursor_moved_left); + let cursor_moved_left = self.cursor_grapheme_idx.saturating_sub(1); + self.cursor_grapheme_idx = self.clamp_cursor(cursor_moved_left); } fn move_cursor_right(&mut self) { - let cursor_moved_right = self.cursor_position.saturating_add(1); - self.cursor_position = self.clamp_cursor(cursor_moved_right); + let cursor_moved_right = self.cursor_grapheme_idx.saturating_add(1); + self.cursor_grapheme_idx = self.clamp_cursor(cursor_moved_right); } fn enter_char(&mut self, new_char: char) { - self.input.insert(self.cursor_position, new_char); + self.input.insert( + self.input + .graphemes(true) + .take(self.cursor_grapheme_idx) + .map(|g| g.as_bytes().len()) + .sum::(), + new_char, + ); - self.move_cursor_right(); + for _ in 0..new_char.to_string().graphemes(true).count() { + self.move_cursor_right(); + } } fn delete_char(&mut self) { - let is_not_cursor_leftmost = self.cursor_position != 0; - if is_not_cursor_leftmost { - // Method "remove" is not used on the saved text for deleting the selected char. - // Reason: Using remove on String works on bytes instead of the chars. - // Using remove would require special care because of char boundaries. - - let current_index = self.cursor_position; - let from_left_to_current_index = current_index - 1; - - // // Getting all characters before the selected character. - let before_char_to_delete = self.input.chars().take(from_left_to_current_index); - // // Getting all characters after selected character. - let after_char_to_delete = self.input.chars().skip(current_index); - - // Put all characters together except the selected one. - // By leaving the selected one out, it is forgotten and therefore deleted. - self.input = before_char_to_delete.chain(after_char_to_delete).collect(); - self.move_cursor_left(); + if self.cursor_grapheme_idx == 0 { + return; } + + let cur_idx = self.cursor_grapheme_idx; + let before_char_to_delete = self.input.graphemes(true).take(cur_idx - 1); + let after_char_to_delete = self.input.graphemes(true).skip(cur_idx); + + self.input = before_char_to_delete.chain(after_char_to_delete).collect(); + self.move_cursor_left(); } fn clamp_cursor(&self, new_cursor_pos: usize) -> usize { - new_cursor_pos.clamp(0, self.input.len()) + new_cursor_pos.clamp(0, self.input.graphemes(true).count()) } pub fn render( @@ -106,7 +112,7 @@ impl GlobPane { has_focus, } = props.borrow(); - let title = "Glob search"; + let title = "Glob search from top"; let block = Block::default() .title(title) .border_style(*border_style) @@ -126,7 +132,14 @@ impl GlobPane { draw_top_right_help(area, title, terminal.current_buffer_mut()); cursor.show = true; - cursor.x = inner_block_area.x + self.cursor_position as u16 + 1; + cursor.x = inner_block_area.x + + self + .input + .graphemes(true) + .take(self.cursor_grapheme_idx) + .map(|g| g.width()) + .sum::() as u16 + + 1; cursor.y = inner_block_area.y; } else { cursor.show = false; @@ -168,13 +181,10 @@ fn glob_search_neighbours( glob: &GlobMatcher, path: &mut PathBuf, ) { - let iter = tree.neighbors_directed(root_index, Direction::Outgoing); - for node_index in iter { + for node_index in tree.neighbors_directed(root_index, Direction::Outgoing) { if let Some(node) = tree.node_weight(node_index) { path.push(&node.name); - // println!("{path:?}"); if glob.is_match(&path) { - // println!("match"); results.push(node_index); } else { glob_search_neighbours(results, tree, node_index, glob, path);