summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSebastian Thiel <sebastian.thiel@icloud.com>2023-12-22 09:35:37 +0100
committerSebastian Thiel <sebastian.thiel@icloud.com>2023-12-23 17:36:05 +0100
commitb945a1e2613b5b0b2eed85f7c9f34942ab3c4a29 (patch)
tree2facab982dcb44f95960b6e8cc373e5673777ee0 /src
parentbc566649e6941340c2bdbcd178ac73a6a6512f68 (diff)
refactor glob widget
* handle unicode well enough to not crash (and luckily it doesn't work too terribly even though I doubt its correct) * show glob error * name changes
Diffstat (limited to 'src')
-rw-r--r--src/interactive/app/eventloop.rs121
-rw-r--r--src/interactive/app/handlers.rs7
-rw-r--r--src/interactive/app/navigation.rs14
-rw-r--r--src/interactive/app/tree_view.rs36
-rw-r--r--src/interactive/widgets/glob.rs74
5 files changed, 128 insertions, 124 deletions
diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs
index 5ed7305..f338c6e 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<NavigationState>,
+ pub navigation: Navigation,
+ pub glob_navigation: Option<Navigation>,
pub entries: Vec<EntryDataBundle>,
pub sorting: SortMode,
pub message: Option<String>,
@@ -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<B>(
@@ -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<dyn TreeView + 'a> {
- let tree_view: Box<dyn TreeView> = if let Some(glob_source) = &self.glob_mode {
+ let tree_view: Box<dyn TreeView> = 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 ee98298..62fff82 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<TreeIndex> {
- 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 8e307ba..8bb2e3e 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<TreeIndex>,
pub bookmarks: BTreeMap<TreeIndex, TreeIndex>,
}
-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<TreeIndex>) {
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 5718fa9..be6f529 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 69f932e..b02ac13 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::<usize>(),
+ 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<B>(
@@ -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::<usize>() 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);