diff options
Diffstat (limited to 'src/interactive')
-rw-r--r-- | src/interactive/app/app_state.rs | 70 | ||||
-rw-r--r-- | src/interactive/app/eventloop.rs | 412 | ||||
-rw-r--r-- | src/interactive/app/terminal_app.rs | 190 |
3 files changed, 480 insertions, 192 deletions
diff --git a/src/interactive/app/app_state.rs b/src/interactive/app/app_state.rs index dd0f9a0..8986f09 100644 --- a/src/interactive/app/app_state.rs +++ b/src/interactive/app/app_state.rs @@ -1,4 +1,5 @@ -use dua::WalkResult; +use dua::{WalkResult, traverse::{TreeIndex, Tree}, inodefilter::InodeFilter}; +use petgraph::Direction; use super::{navigation::Navigation, EntryDataBundle, SortMode}; @@ -27,6 +28,73 @@ pub struct AppState { pub message: Option<String>, pub focussed: FocussedPane, pub is_scanning: bool, + pub traversal_state: TraversalState, +} + + +#[derive(Default)] +pub struct TraversalState { + pub previous_node_idx: TreeIndex, + pub parent_node_idx: TreeIndex, + pub directory_info_per_depth_level: Vec<EntryInfo>, + pub current_directory_at_depth: EntryInfo, + pub previous_depth: usize, + pub inodes: InodeFilter, +} + +impl TraversalState { + pub fn new(root_idx: TreeIndex) -> Self { + Self { + previous_node_idx: root_idx, + parent_node_idx: root_idx, + directory_info_per_depth_level: Vec::new(), + current_directory_at_depth: EntryInfo::default(), + previous_depth: 0, + inodes: InodeFilter::default(), + } + } +} + +#[derive(Default, Copy, Clone)] +pub struct EntryInfo { + pub size: u128, + pub entries_count: Option<u64>, +} + +impl EntryInfo { + pub fn add_count(&mut self, other: &Self) { + self.entries_count = match (self.entries_count, other.entries_count) { + (Some(a), Some(b)) => Some(a + b), + (None, Some(b)) => Some(b), + (Some(a), None) => Some(a), + (None, None) => None, + }; + } +} + +pub fn set_entry_info_or_panic( + tree: &mut Tree, + node_idx: TreeIndex, + EntryInfo { + size, + entries_count, + }: EntryInfo, +) { + let node = tree + .node_weight_mut(node_idx) + .expect("node for parent index we just retrieved"); + node.size = size; + node.entry_count = entries_count; +} + +pub fn parent_or_panic(tree: &mut Tree, parent_node_idx: TreeIndex) -> TreeIndex { + tree.neighbors_directed(parent_node_idx, Direction::Incoming) + .next() + .expect("every node in the iteration has a parent") +} + +pub fn pop_or_panic(v: &mut Vec<EntryInfo>) -> EntryInfo { + v.pop().expect("sizes per level to be in sync with graph") } pub enum ProcessingResult { diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index df297e3..014ce36 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -1,24 +1,24 @@ -use crate::interactive::{ +use crate::{interactive::{ app::navigation::Navigation, app_state::FocussedPane, sorted_entries, widgets::{glob_search, MainWindow, MainWindowProps}, ByteVisualization, CursorDirection, CursorMode, DisplayOptions, EntryDataBundle, MarkEntryMode, SortMode, -}; +}, crossdev}; use anyhow::Result; use crossbeam::channel::Receiver; use crosstermion::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use crosstermion::input::Event; use dua::{ - traverse::{EntryData, Traversal, Tree}, + traverse::{EntryData, Traversal, Tree, size_on_disk}, WalkOptions, WalkResult, }; -use std::path::PathBuf; +use std::{path::PathBuf, time::{SystemTime, UNIX_EPOCH}}; use tui::backend::Backend; use tui_react::Terminal; -use super::tree_view::TreeView; +use super::{tree_view::TreeView, terminal_app::TraversalEvent, app_state::{EntryInfo, set_entry_info_or_panic, pop_or_panic, parent_or_panic}}; use super::{ app_state::{AppState, Cursor, ProcessingResult}, input::input_channel, @@ -74,132 +74,326 @@ impl AppState { traversal: &mut Traversal, display: &mut DisplayOptions, terminal: &mut Terminal<B>, - events: impl Iterator<Item = Event>, + walk_options: &WalkOptions, + events: Receiver<Event>, + traversal_events: Receiver<TraversalEvent>, ) -> Result<ProcessingResult> where B: Backend, { - use crosstermion::crossterm::event::KeyCode::*; - use FocussedPane::*; - { let tree_view = self.tree_view(traversal); self.draw(window, &tree_view, *display, terminal)?; } - for event in events { - let key = match event { - Event::Key(key) if key.kind != KeyEventKind::Release => key, - Event::Resize(_, _) => refresh_key(), - _ => continue, - }; + loop { + crossbeam::select! { + recv(events) -> event => { + let Ok(event) = event else { + continue; + }; + let result = self.process_event( + window, + traversal, + display, + terminal, + event)?; + if let Some(processing_result) = result { + return Ok(processing_result); + } + }, + recv(traversal_events) -> event => { + let Ok(event) = event else { + continue; + }; + self.process_traversal_event(traversal, walk_options, event); + } + } + } + // TODO: do we need this? + // Ok(ProcessingResult::Finished(WalkResult { + // num_errors: traversal.io_errors, + // })) + } + + // TODO: + // default(Duration::from_millis(250)) => { + // // No events or new entries received, but we still need + // // to keep updating the status message regularly. + // if update(&mut t, None)? { + // return Ok(None); + // } + // } + // } + // } + + fn process_traversal_event<'a>(&mut self, t: &'a mut Traversal, walk_options: &'a WalkOptions, event: TraversalEvent) { + match event { + TraversalEvent::Entry(entry, root_path, device_id) => { + t.entries_traversed += 1; + let mut data = EntryData::default(); + match entry { + Ok(entry) => { + data.name = if entry.depth < 1 { + (*root_path).clone() + } else { + entry.file_name.into() + }; + + let mut file_size = 0u128; + let mut mtime: SystemTime = UNIX_EPOCH; + match &entry.client_state { + Some(Ok(ref m)) => { + if !m.is_dir() + && (walk_options.count_hard_links || self.traversal_state.inodes.add(m)) + && (walk_options.cross_filesystems + || crossdev::is_same_device(device_id, m)) + { + if walk_options.apparent_size { + file_size = m.len() as u128; + } else { + file_size = size_on_disk(&entry.parent_path, &data.name, m) + .unwrap_or_else(|_| { + t.io_errors += 1; + data.metadata_io_error = true; + 0 + }) + as u128; + } + } else { + data.entry_count = Some(0); + data.is_dir = true; + } + + match m.modified() { + Ok(modified) => { + mtime = modified; + } + Err(_) => { + t.io_errors += 1; + data.metadata_io_error = true; + } + } + } + Some(Err(_)) => { + t.io_errors += 1; + data.metadata_io_error = true; + } + None => {} + } + + match (entry.depth, self.traversal_state.previous_depth) { + (n, p) if n > p => { + self.traversal_state.directory_info_per_depth_level.push(self.traversal_state.current_directory_at_depth); + self.traversal_state.current_directory_at_depth = EntryInfo { + size: file_size, + entries_count: Some(1), + }; + self.traversal_state.parent_node_idx = self.traversal_state.previous_node_idx; + } + (n, p) if n < p => { + for _ in n..p { + set_entry_info_or_panic( + &mut t.tree, + self.traversal_state.parent_node_idx, + self.traversal_state.current_directory_at_depth, + ); + let dir_info = + pop_or_panic(&mut self.traversal_state.directory_info_per_depth_level); + + self.traversal_state.current_directory_at_depth.size += dir_info.size; + self.traversal_state.current_directory_at_depth.add_count(&dir_info); + + self.traversal_state.parent_node_idx = parent_or_panic(&mut t.tree, self.traversal_state.parent_node_idx); + } + self.traversal_state.current_directory_at_depth.size += file_size; + *self.traversal_state.current_directory_at_depth.entries_count.get_or_insert(0) += 1; + set_entry_info_or_panic( + &mut t.tree, + self.traversal_state.parent_node_idx, + self.traversal_state.current_directory_at_depth, + ); + } + _ => { + self.traversal_state.current_directory_at_depth.size += file_size; + *self.traversal_state.current_directory_at_depth.entries_count.get_or_insert(0) += 1; + } + }; - self.reset_message(); + data.mtime = mtime; + data.size = file_size; + let entry_index = t.tree.add_node(data); - let glob_focussed = self.focussed == Glob; - let mut tree_view = self.tree_view(traversal); - let mut handled = true; - match key.code { - Esc => { - if let Some(value) = self.handle_quit(&mut tree_view, window) { - return value; + t.tree.add_edge(self.traversal_state.parent_node_idx, entry_index, ()); + self.traversal_state.previous_node_idx = entry_index; + self.traversal_state.previous_depth = entry.depth; + } + Err(_) => { + if self.traversal_state.previous_depth == 0 { + data.name = (*root_path).clone(); + let entry_index = t.tree.add_node(data); + t.tree.add_edge(self.traversal_state.parent_node_idx, entry_index, ()); + } + + t.io_errors += 1 } } - Tab => { - self.cycle_focus(window); - } - Char('/') if !glob_focussed => { - self.toggle_glob_search(window); - } - Char('?') if !glob_focussed => self.toggle_help_pane(window), - Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) && !glob_focussed => { - return Ok(ProcessingResult::ExitRequested(WalkResult { - num_errors: tree_view.traversal.io_errors, - })) + + // TODO: + // if throttle.can_update() && update(&mut t, None)? { + // return Ok(None); + // } + }, + TraversalEvent::Finished(io_errors) => { + self.traversal_state.directory_info_per_depth_level.push(self.traversal_state.current_directory_at_depth); + self.traversal_state.current_directory_at_depth = EntryInfo::default(); + for _ in 0..self.traversal_state.previous_depth { + let dir_info = pop_or_panic(&mut self.traversal_state.directory_info_per_depth_level); + self.traversal_state.current_directory_at_depth.size += dir_info.size; + self.traversal_state.current_directory_at_depth.add_count(&dir_info); + + set_entry_info_or_panic(&mut t.tree, self.traversal_state.parent_node_idx, self.traversal_state.current_directory_at_depth); + self.traversal_state.parent_node_idx = parent_or_panic(&mut t.tree, self.traversal_state.parent_node_idx); } - Char('q') if !glob_focussed => { - if let Some(value) = self.handle_quit(&mut tree_view, window) { - return value; - } + let root_size = t.recompute_root_size(); + set_entry_info_or_panic( + &mut t.tree, + t.root_index, + EntryInfo { + size: root_size, + entries_count: (t.entries_traversed > 0).then_some(t.entries_traversed), + }, + ); + t.total_bytes = Some(root_size); + t.elapsed = Some(t.start.elapsed()); + // Ok(Some(t)) + } + } + } + + fn process_event<B>(&mut self, + window: &mut MainWindow, + traversal: &mut Traversal, + display: &mut DisplayOptions, + terminal: &mut Terminal<B>, + event: Event + ) -> Result<Option<ProcessingResult>> + where + B: Backend, + { + use crosstermion::crossterm::event::KeyCode::*; + use FocussedPane::*; + + let key = match event { + Event::Key(key) if key.kind != KeyEventKind::Release => key, + Event::Resize(_, _) => refresh_key(), + _ => return Ok(None), + }; + + self.reset_message(); + + let glob_focussed = self.focussed == Glob; + let mut tree_view = self.tree_view(traversal); + let mut handled = true; + match key.code { + Esc => { + if let Some(value) = self.handle_quit(&mut tree_view, window) { + return Ok(Some(value?)); } - _ => { - handled = false; + } + Tab => { + self.cycle_focus(window); + } + Char('/') if !glob_focussed => { + self.toggle_glob_search(window); + } + Char('?') if !glob_focussed => self.toggle_help_pane(window), + Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) && !glob_focussed => { + return Ok(Some(ProcessingResult::ExitRequested(WalkResult { + num_errors: tree_view.traversal.io_errors, + }))) + } + Char('q') if !glob_focussed => { + if let Some(result) = self.handle_quit(&mut tree_view, window) { + return Ok(Some(result?)); } } + _ => { + handled = false; + } + } - if !handled { - match self.focussed { - Mark => { - self.dispatch_to_mark_pane(key, window, &mut tree_view, *display, terminal) + if !handled { + match self.focussed { + Mark => { + self.dispatch_to_mark_pane(key, window, &mut tree_view, *display, terminal) + } + Help => { + window + .help_pane + .as_mut() + .expect("help pane") + .process_events(key); + } + Glob => { + let glob_pane = window.glob_pane.as_mut().expect("glob pane"); + match key.code { + Enter => self.search_glob_pattern(&mut tree_view, &glob_pane.input), + _ => glob_pane.process_events(key), } - Help => { - window - .help_pane - .as_mut() - .expect("help pane") - .process_events(key); + } + Main => match key.code { + Char('O') => self.open_that(&tree_view), + Char(' ') => self.mark_entry( + CursorMode::KeepPosition, + MarkEntryMode::Toggle, + window, + &tree_view, + ), + Char('x') => self.mark_entry( + CursorMode::Advance, + MarkEntryMode::MarkForDeletion, + window, + &tree_view, + ), + Char('a') => { + self.mark_all_entries(MarkEntryMode::Toggle, window, &tree_view) } - Glob => { - let glob_pane = window.glob_pane.as_mut().expect("glob pane"); - match key.code { - Enter => self.search_glob_pattern(&mut tree_view, &glob_pane.input), - _ => glob_pane.process_events(key), - } + Char('o') | Char('l') | Enter | Right => { + self.enter_node_with_traversal(&tree_view) } - Main => match key.code { - Char('O') => self.open_that(&tree_view), - Char(' ') => self.mark_entry( - CursorMode::KeepPosition, - MarkEntryMode::Toggle, - window, - &tree_view, - ), - Char('x') => self.mark_entry( - CursorMode::Advance, - MarkEntryMode::MarkForDeletion, - window, - &tree_view, - ), - Char('a') => { - self.mark_all_entries(MarkEntryMode::Toggle, window, &tree_view) - } - Char('o') | Char('l') | Enter | Right => { - self.enter_node_with_traversal(&tree_view) - } - Char('H') | Home => self.change_entry_selection(CursorDirection::ToTop), - Char('G') | End => self.change_entry_selection(CursorDirection::ToBottom), - PageUp => self.change_entry_selection(CursorDirection::PageUp), - Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.change_entry_selection(CursorDirection::PageUp) - } - Char('k') | Up => self.change_entry_selection(CursorDirection::Up), - Char('j') | Down => self.change_entry_selection(CursorDirection::Down), - PageDown => self.change_entry_selection(CursorDirection::PageDown), - Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.change_entry_selection(CursorDirection::PageDown) - } - Char('s') => self.cycle_sorting(&tree_view), - Char('m') => self.cycle_mtime_sorting(&tree_view), - Char('c') => self.cycle_count_sorting(&tree_view), - Char('g') => display.byte_vis.cycle(), - Char('d') => self.mark_entry( - CursorMode::Advance, - MarkEntryMode::Toggle, - window, - &tree_view, - ), - Char('u') | Char('h') | Backspace | Left => { - self.exit_node_with_traversal(&tree_view) - } - _ => {} - }, - }; - } - self.draw(window, &tree_view, *display, terminal)?; + Char('H') | Home => self.change_entry_selection(CursorDirection::ToTop), + Char('G') | End => self.change_entry_selection(CursorDirection::ToBottom), + PageUp => self.change_entry_selection(CursorDirection::PageUp), + Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => { + self.change_entry_selection(CursorDirection::PageUp) + } + Char('k') | Up => self.change_entry_selection(CursorDirection::Up), + Char('j') | Down => self.change_entry_selection(CursorDirection::Down), + PageDown => self.change_entry_selection(CursorDirection::PageDown), + Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => { + self.change_entry_selection(CursorDirection::PageDown) + } + Char('s') => self.cycle_sorting(&tree_view), + Char('m') => self.cycle_mtime_sorting(&tree_view), + Char('c') => self.cycle_count_sorting(&tree_view), + Char('g') => display.byte_vis.cycle(), + Char('d') => self.mark_entry( + CursorMode::Advance, + MarkEntryMode::Toggle, + window, + &tree_view, + ), + Char('u') | Char('h') | Backspace | Left => { + self.exit_node_with_traversal(&tree_view) + } + _ => {} + }, + }; } - Ok(ProcessingResult::Finished(WalkResult { - num_errors: traversal.io_errors, - })) + self.draw(window, &tree_view, *display, terminal)?; + + Ok(None) } fn tree_view<'a>(&mut self, traversal: &'a mut Traversal) -> TreeView<'a> { diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal_app.rs index 0c96140..01398df 100644 --- a/src/interactive/app/terminal_app.rs +++ b/src/interactive/app/terminal_app.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; use anyhow::Result; use crossbeam::channel::Receiver; @@ -10,10 +10,10 @@ use dua::{ use tui::prelude::Backend; use tui_react::Terminal; -use crate::interactive::widgets::MainWindow; +use crate::{interactive::widgets::MainWindow, crossdev}; use super::{ - app_state::{AppState, ProcessingResult}, + app_state::{AppState, ProcessingResult, TraversalState}, refresh_key, sorted_entries, ByteVisualization, DisplayOptions, }; @@ -23,73 +23,108 @@ pub struct TerminalApp { pub display: DisplayOptions, pub state: AppState, pub window: MainWindow, + pub walk_options: WalkOptions, } -type KeyboardInputAndApp = (crossbeam::channel::Receiver<Event>, TerminalApp); +pub type TraversalEntry = Result<jwalk::DirEntry<((), Option<Result<std::fs::Metadata, jwalk::Error>>)>, jwalk::Error>; -impl TerminalApp { - pub fn refresh_view<B>(&mut self, terminal: &mut Terminal<B>) - where - B: Backend, - { - // Use an event that does nothing to trigger a refresh - self.state - .process_events( - &mut self.window, - &mut self.traversal, - &mut self.display, - terminal, - std::iter::once(Event::Key(refresh_key())), - ) - .ok(); - } - - pub fn process_events<B>( - &mut self, - terminal: &mut Terminal<B>, - events: impl Iterator<Item = Event>, - ) -> Result<WalkResult> - where - B: Backend, - { - match self.state.process_events( - &mut self.window, - &mut self.traversal, - &mut self.display, - terminal, - events, - )? { - ProcessingResult::Finished(res) | ProcessingResult::ExitRequested(res) => Ok(res), - } - } +pub enum TraversalEvent { + Entry(TraversalEntry, Arc<PathBuf>, u64), + Finished(u64), +} - pub fn initialize<B>(terminal: &mut Terminal<B>, byte_format: ByteFormat) -> Result<TerminalApp> +impl TerminalApp { + pub fn initialize<B>(terminal: &mut Terminal<B>, walk_options: WalkOptions, byte_format: ByteFormat) -> Result<TerminalApp> where B: Backend, { terminal.hide_cursor()?; terminal.clear()?; - let mut display = DisplayOptions::new(byte_format); - let mut window = MainWindow::default(); - - // #[inline] - // fn fetch_buffered_key_events(keys_rx: &Receiver<Event>) -> Vec<Event> { - // let mut keys = Vec::new(); - // while let Ok(key) = keys_rx.try_recv() { - // keys.push(key); - // } - // keys - // } + let display = DisplayOptions::new(byte_format); + let window = MainWindow::default(); let mut state = AppState { is_scanning: false, ..Default::default() }; + let traversal = { + let mut tree = Tree::new(); + let root_index = tree.add_node(EntryData::default()); + Traversal { + tree, + root_index, + entries_traversed: 0, + start: std::time::Instant::now(), + elapsed: None, + io_errors: 0, + total_bytes: None, + } + }; + + state.navigation_mut().view_root = traversal.root_index; + state.entries = sorted_entries( + &traversal.tree, + state.navigation().view_root, + state.sorting, + state.glob_root(), + ); + state.navigation_mut().selected = state.entries.first().map(|b| b.index); + + let mut app = TerminalApp { + state, + display, + traversal, + window, + walk_options, + }; + Ok(app) + } + + pub fn scan<'a>(&mut self, input: Vec<PathBuf>) -> Result<Receiver<TraversalEvent>> { + self.state.traversal_state = TraversalState::new(self.traversal.root_index); + + let (entry_tx, entry_rx) = crossbeam::channel::bounded(100); + std::thread::Builder::new() + .name("dua-fs-walk-dispatcher".to_string()) + .spawn({ + let walk_options = self.walk_options.clone(); + let mut io_errors: u64 = 0; + move || { + for root_path in input.into_iter() { + let device_id = match crossdev::init(root_path.as_ref()) { + Ok(id) => id, + Err(_) => { + io_errors += 1; + continue; + } + }; + + let root_path = Arc::new(root_path); + for entry in walk_options + .iter_from_path(root_path.as_ref(), device_id) + .into_iter() + { + if entry_tx + .send(TraversalEvent::Entry(entry, Arc::clone(&root_path), device_id)) + .is_err() + { + // The channel is closed, this means the user has + // requested to quit the app. Abort the walking. + return; + } + } + } + if entry_tx.send(TraversalEvent::Finished(io_errors)).is_err() { + log::error!("Failed to send TraversalEvents::Finished event"); + } + } + })?; + // let mut received_events = false; // let traversal = - // Traversal::from_walk(options, input_paths, &keys_rx, |traversal, event| { + // Traversal::from_walk(options, input_paths, |traversal, event| { // if !received_events { // state.navigation_mut().view_root = traversal.root_index; // } @@ -135,37 +170,28 @@ impl TerminalApp { // if !received_events { // } - let traversal = { - let mut tree = Tree::new(); - let root_index = tree.add_node(EntryData::default()); - Traversal { - tree, - root_index, - entries_traversed: 0, - start: std::time::Instant::now(), - elapsed: None, - io_errors: 0, - total_bytes: None, - } - }; - - state.navigation_mut().view_root = traversal.root_index; - state.entries = sorted_entries( - &traversal.tree, - state.navigation().view_root, - state.sorting, - state.glob_root(), - ); - state.navigation_mut().selected = state.entries.first().map(|b| b.index); + Ok(entry_rx) + } - let mut app = TerminalApp { - state, - display, + pub fn process_events<B>( + &mut self, + terminal: &mut Terminal<B>, + events: Receiver<Event>, + traversal: Receiver<TraversalEvent>, + ) -> Result<WalkResult> + where + B: Backend, + { + match self.state.process_events( + &mut self.window, + &mut self.traversal, + &mut self.display, + terminal, + &self.walk_options, + events, traversal, - window, - }; - app.refresh_view(terminal); - - Ok(app) + )? { + ProcessingResult::Finished(res) | ProcessingResult::ExitRequested(res) => Ok(res), + } } } |