From 807916ced6e4ec195e0c3805181f3ccd78d69ce3 Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Sat, 6 Jan 2024 20:57:06 +0000 Subject: [PATCH 01/20] No Interactive enum --- src/interactive/app/eventloop.rs | 15 +-------------- src/interactive/app/mod.rs | 2 +- src/interactive/app/tests/utils.rs | 5 +++-- src/main.rs | 8 ++++++-- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index cec4506e..10b5c145 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -387,7 +387,7 @@ impl TerminalApp { terminal: &mut Terminal, options: WalkOptions, input_paths: Vec, - mode: Interaction, + keys_rx: Receiver, ) -> Result> where B: Backend, @@ -397,13 +397,6 @@ impl TerminalApp { let mut display: DisplayOptions = options.clone().into(); display.byte_vis = ByteVisualization::PercentageAndBar; let mut window = MainWindow::default(); - let keys_rx = match mode { - Interaction::None => { - let (_, keys_rx) = crossbeam::channel::unbounded(); - keys_rx - } - Interaction::Full => input_channel(), - }; #[inline] fn fetch_buffered_key_events(keys_rx: &Receiver) -> Vec { @@ -490,12 +483,6 @@ impl TerminalApp { } } -pub enum Interaction { - Full, - #[allow(dead_code)] - None, -} - fn refresh_key() -> KeyEvent { KeyEvent::new(KeyCode::Char('\r'), KeyModifiers::ALT) } diff --git a/src/interactive/app/mod.rs b/src/interactive/app/mod.rs index f1e29252..8d27e466 100644 --- a/src/interactive/app/mod.rs +++ b/src/interactive/app/mod.rs @@ -2,7 +2,7 @@ mod bytevis; mod common; mod eventloop; mod handlers; -mod input; +pub mod input; mod navigation; pub mod tree_view; diff --git a/src/interactive/app/tests/utils.rs b/src/interactive/app/tests/utils.rs index 79defabd..f8657507 100644 --- a/src/interactive/app/tests/utils.rs +++ b/src/interactive/app/tests/utils.rs @@ -18,7 +18,7 @@ use std::{ use tui::backend::TestBackend; use tui_react::Terminal; -use crate::interactive::{app::tests::FIXTURE_PATH, Interaction, TerminalApp}; +use crate::interactive::{app::tests::FIXTURE_PATH, TerminalApp}; pub fn into_keys<'a>( codes: impl IntoIterator + 'a, @@ -175,6 +175,7 @@ pub fn initialized_app_and_terminal_with_closure( let mut terminal = new_test_terminal()?; std::env::set_current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))?; + let (_, keys_rx) = crossbeam::channel::unbounded(); let input_paths = fixture_paths.iter().map(|c| convert(c.as_ref())).collect(); let app = TerminalApp::initialize( &mut terminal, @@ -188,7 +189,7 @@ pub fn initialized_app_and_terminal_with_closure( ignore_dirs: Default::default(), }, input_paths, - Interaction::None, + keys_rx, )? .map(|(_, app)| app); Ok(( diff --git a/src/main.rs b/src/main.rs index 9943270f..6b9ffa33 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,8 @@ use simplelog::{Config, LevelFilter, WriteLogger}; use std::fs::OpenOptions; use std::{fs, io, io::Write, path::PathBuf, process}; +use crate::interactive::input::input_channel; + mod crossdev; #[cfg(feature = "tui-crossplatform")] mod interactive; @@ -51,7 +53,7 @@ fn main() -> Result<()> { let res = match opt.command { #[cfg(feature = "tui-crossplatform")] Some(Interactive { input }) => { - use crate::interactive::{Interaction, TerminalApp}; + use crate::interactive::{TerminalApp}; use anyhow::{anyhow, Context}; use crosstermion::terminal::{tui::new_terminal, AlternateRawScreen}; @@ -64,11 +66,13 @@ fn main() -> Result<()> { AlternateRawScreen::try_from(io::stderr()).with_context(|| no_tty_msg)?, ) .with_context(|| "Could not instantiate terminal")?; + + let keys_rx = input_channel(); let res = TerminalApp::initialize( &mut terminal, walk_options, extract_paths_maybe_set_cwd(input, !opt.stay_on_filesystem)?, - Interaction::Full, + keys_rx, )? .map(|(keys_rx, mut app)| { let res = app.process_events(&mut terminal, keys_rx.into_iter()); From cf3c507bb43221066acf96cde778b66bbd578669 Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Sat, 6 Jan 2024 21:28:01 +0000 Subject: [PATCH 02/20] Scan disabled --- src/interactive/app/eventloop.rs | 134 +++++++++++++++++-------------- 1 file changed, 74 insertions(+), 60 deletions(-) diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index 10b5c145..8132f6b9 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -10,7 +10,7 @@ use crossbeam::channel::Receiver; use crosstermion::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use crosstermion::input::Event; use dua::{ - traverse::{EntryData, Traversal}, + traverse::{EntryData, Traversal, Tree}, WalkOptions, WalkResult, }; use std::path::PathBuf; @@ -394,82 +394,96 @@ impl TerminalApp { { terminal.hide_cursor()?; terminal.clear()?; + let mut display: DisplayOptions = options.clone().into(); display.byte_vis = ByteVisualization::PercentageAndBar; + let mut window = MainWindow::default(); - #[inline] - fn fetch_buffered_key_events(keys_rx: &Receiver) -> Vec { - let mut keys = Vec::new(); - while let Ok(key) = keys_rx.try_recv() { - keys.push(key); - } - keys - } + // #[inline] + // fn fetch_buffered_key_events(keys_rx: &Receiver) -> Vec { + // let mut keys = Vec::new(); + // while let Ok(key) = keys_rx.try_recv() { + // keys.push(key); + // } + // keys + // } let mut state = AppState { - is_scanning: true, + is_scanning: false, ..Default::default() }; - let mut received_events = false; - let traversal = - Traversal::from_walk(options, input_paths, &keys_rx, |traversal, event| { - if !received_events { - state.navigation_mut().view_root = traversal.root_index; - } - state.entries = sorted_entries( - &traversal.tree, - state.navigation().view_root, - state.sorting, - state.glob_root(), - ); - if !received_events { - state.navigation_mut().selected = state.entries.first().map(|b| b.index); - } - state.reset_message(); // force "scanning" to appear - - let mut events = fetch_buffered_key_events(&keys_rx); - if let Some(event) = event { - // This update is triggered by a user event, insert it - // before any events fetched later. - events.insert(0, event); - } - received_events |= !events.is_empty(); - - let should_exit = match state.process_events( - &mut window, - traversal, - &mut display, - terminal, - events.into_iter(), - )? { - ProcessingResult::ExitRequested(_) => true, - ProcessingResult::Finished(_) => false, - }; - - Ok(should_exit) - })?; - let traversal = match traversal { - Some(t) => t, - None => return Ok(None), + // let mut received_events = false; + // let traversal = + // Traversal::from_walk(options, input_paths, &keys_rx, |traversal, event| { + // if !received_events { + // state.navigation_mut().view_root = traversal.root_index; + // } + // state.entries = sorted_entries( + // &traversal.tree, + // state.navigation().view_root, + // state.sorting, + // state.glob_root(), + // ); + // if !received_events { + // state.navigation_mut().selected = state.entries.first().map(|b| b.index); + // } + // state.reset_message(); // force "scanning" to appear + + // let mut events = fetch_buffered_key_events(&keys_rx); + // if let Some(event) = event { + // // This update is triggered by a user event, insert it + // // before any events fetched later. + // events.insert(0, event); + // } + // received_events |= !events.is_empty(); + + // let should_exit = match state.process_events( + // &mut window, + // traversal, + // &mut display, + // terminal, + // events.into_iter(), + // )? { + // ProcessingResult::ExitRequested(_) => true, + // ProcessingResult::Finished(_) => false, + // }; + + // Ok(should_exit) + // })?; + + // let traversal = match traversal { + // Some(t) => t, + // None => return Ok(None), + // }; + + // state.is_scanning = false; + // 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.is_scanning = false; - if !received_events { - state.navigation_mut().view_root = traversal.root_index; - } + 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 - .navigation() - .selected - .filter(|_| received_events) - .or_else(|| state.entries.first().map(|b| b.index)); + state.navigation_mut().selected = state.entries.first().map(|b| b.index); let mut app = TerminalApp { state, From 5123cf584ab68c0a2f491580289c7243e8651bfa Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Sun, 7 Jan 2024 09:12:46 +0000 Subject: [PATCH 03/20] Move TerminalApp to separate file --- src/interactive/app/eventloop.rs | 162 +------------------------- src/interactive/app/mod.rs | 1 + src/interactive/app/terminal_app.rs | 173 ++++++++++++++++++++++++++++ src/interactive/app/tests/utils.rs | 2 +- src/main.rs | 2 +- 5 files changed, 177 insertions(+), 163 deletions(-) create mode 100644 src/interactive/app/terminal_app.rs diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index 8132f6b9..2d2459cb 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -337,166 +337,6 @@ where Ok(()) } -/// State and methods representing the interactive disk usage analyser for the terminal -pub struct TerminalApp { - pub traversal: Traversal, - pub display: DisplayOptions, - pub state: AppState, - pub window: MainWindow, -} - -type KeyboardInputAndApp = (crossbeam::channel::Receiver, TerminalApp); - -impl TerminalApp { - pub fn refresh_view(&mut self, terminal: &mut Terminal) - 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( - &mut self, - terminal: &mut Terminal, - events: impl Iterator, - ) -> Result - 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 fn initialize( - terminal: &mut Terminal, - options: WalkOptions, - input_paths: Vec, - keys_rx: Receiver, - ) -> Result> - where - B: Backend, - { - terminal.hide_cursor()?; - terminal.clear()?; - - let mut display: DisplayOptions = options.clone().into(); - display.byte_vis = ByteVisualization::PercentageAndBar; - - let mut window = MainWindow::default(); - - // #[inline] - // fn fetch_buffered_key_events(keys_rx: &Receiver) -> Vec { - // let mut keys = Vec::new(); - // while let Ok(key) = keys_rx.try_recv() { - // keys.push(key); - // } - // keys - // } - - let mut state = AppState { - is_scanning: false, - ..Default::default() - }; - - // let mut received_events = false; - // let traversal = - // Traversal::from_walk(options, input_paths, &keys_rx, |traversal, event| { - // if !received_events { - // state.navigation_mut().view_root = traversal.root_index; - // } - // state.entries = sorted_entries( - // &traversal.tree, - // state.navigation().view_root, - // state.sorting, - // state.glob_root(), - // ); - // if !received_events { - // state.navigation_mut().selected = state.entries.first().map(|b| b.index); - // } - // state.reset_message(); // force "scanning" to appear - - // let mut events = fetch_buffered_key_events(&keys_rx); - // if let Some(event) = event { - // // This update is triggered by a user event, insert it - // // before any events fetched later. - // events.insert(0, event); - // } - // received_events |= !events.is_empty(); - - // let should_exit = match state.process_events( - // &mut window, - // traversal, - // &mut display, - // terminal, - // events.into_iter(), - // )? { - // ProcessingResult::ExitRequested(_) => true, - // ProcessingResult::Finished(_) => false, - // }; - - // Ok(should_exit) - // })?; - - // let traversal = match traversal { - // Some(t) => t, - // None => return Ok(None), - // }; - - // state.is_scanning = false; - // 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); - - let mut app = TerminalApp { - state, - display, - traversal, - window, - }; - app.refresh_view(terminal); - - Ok(Some((keys_rx, app))) - } -} - -fn refresh_key() -> KeyEvent { +pub fn refresh_key() -> KeyEvent { KeyEvent::new(KeyCode::Char('\r'), KeyModifiers::ALT) } diff --git a/src/interactive/app/mod.rs b/src/interactive/app/mod.rs index 8d27e466..73eb2f26 100644 --- a/src/interactive/app/mod.rs +++ b/src/interactive/app/mod.rs @@ -5,6 +5,7 @@ mod handlers; pub mod input; mod navigation; pub mod tree_view; +pub mod terminal_app; pub use bytevis::*; pub use common::*; diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal_app.rs new file mode 100644 index 00000000..b19bf1b0 --- /dev/null +++ b/src/interactive/app/terminal_app.rs @@ -0,0 +1,173 @@ +use std::path::PathBuf; + +use crossbeam::channel::Receiver; +use crosstermion::input::Event; +use dua::{traverse::{Traversal, Tree, EntryData}, WalkResult, WalkOptions}; +use tui::prelude::Backend; +use tui_react::Terminal; +use anyhow::Result; + +use crate::interactive::widgets::MainWindow; + +use super::{DisplayOptions, AppState, ProcessingResult, ByteVisualization, sorted_entries, refresh_key}; + + +/// State and methods representing the interactive disk usage analyser for the terminal +pub struct TerminalApp { + pub traversal: Traversal, + pub display: DisplayOptions, + pub state: AppState, + pub window: MainWindow, +} + +type KeyboardInputAndApp = (crossbeam::channel::Receiver, TerminalApp); + +impl TerminalApp { + pub fn refresh_view(&mut self, terminal: &mut Terminal) + 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( + &mut self, + terminal: &mut Terminal, + events: impl Iterator, + ) -> Result + 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 fn initialize( + terminal: &mut Terminal, + options: WalkOptions, + input_paths: Vec, + keys_rx: Receiver, + ) -> Result> + where + B: Backend, + { + terminal.hide_cursor()?; + terminal.clear()?; + + let mut display: DisplayOptions = options.clone().into(); + display.byte_vis = ByteVisualization::PercentageAndBar; + + let mut window = MainWindow::default(); + + // #[inline] + // fn fetch_buffered_key_events(keys_rx: &Receiver) -> Vec { + // let mut keys = Vec::new(); + // while let Ok(key) = keys_rx.try_recv() { + // keys.push(key); + // } + // keys + // } + + let mut state = AppState { + is_scanning: false, + ..Default::default() + }; + + // let mut received_events = false; + // let traversal = + // Traversal::from_walk(options, input_paths, &keys_rx, |traversal, event| { + // if !received_events { + // state.navigation_mut().view_root = traversal.root_index; + // } + // state.entries = sorted_entries( + // &traversal.tree, + // state.navigation().view_root, + // state.sorting, + // state.glob_root(), + // ); + // if !received_events { + // state.navigation_mut().selected = state.entries.first().map(|b| b.index); + // } + // state.reset_message(); // force "scanning" to appear + + // let mut events = fetch_buffered_key_events(&keys_rx); + // if let Some(event) = event { + // // This update is triggered by a user event, insert it + // // before any events fetched later. + // events.insert(0, event); + // } + // received_events |= !events.is_empty(); + + // let should_exit = match state.process_events( + // &mut window, + // traversal, + // &mut display, + // terminal, + // events.into_iter(), + // )? { + // ProcessingResult::ExitRequested(_) => true, + // ProcessingResult::Finished(_) => false, + // }; + + // Ok(should_exit) + // })?; + + // let traversal = match traversal { + // Some(t) => t, + // None => return Ok(None), + // }; + + // state.is_scanning = false; + // 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); + + let mut app = TerminalApp { + state, + display, + traversal, + window, + }; + app.refresh_view(terminal); + + Ok(Some((keys_rx, app))) + } +} diff --git a/src/interactive/app/tests/utils.rs b/src/interactive/app/tests/utils.rs index f8657507..82cf00bc 100644 --- a/src/interactive/app/tests/utils.rs +++ b/src/interactive/app/tests/utils.rs @@ -18,7 +18,7 @@ use std::{ use tui::backend::TestBackend; use tui_react::Terminal; -use crate::interactive::{app::tests::FIXTURE_PATH, TerminalApp}; +use crate::interactive::{app::tests::FIXTURE_PATH, terminal_app::TerminalApp}; pub fn into_keys<'a>( codes: impl IntoIterator + 'a, diff --git a/src/main.rs b/src/main.rs index 6b9ffa33..636b01a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use std::fs::OpenOptions; use std::{fs, io, io::Write, path::PathBuf, process}; use crate::interactive::input::input_channel; +use crate::interactive::terminal_app::TerminalApp; mod crossdev; #[cfg(feature = "tui-crossplatform")] @@ -53,7 +54,6 @@ fn main() -> Result<()> { let res = match opt.command { #[cfg(feature = "tui-crossplatform")] Some(Interactive { input }) => { - use crate::interactive::{TerminalApp}; use anyhow::{anyhow, Context}; use crosstermion::terminal::{tui::new_terminal, AlternateRawScreen}; From feec3eb37d50c4b927ae3f948159693f134edf4b Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Sun, 7 Jan 2024 09:19:09 +0000 Subject: [PATCH 04/20] Move AppState to separate file --- src/interactive/app/app_state.rs | 35 ++++++++++++++++++++++++++++ src/interactive/app/eventloop.rs | 36 ++--------------------------- src/interactive/app/handlers.rs | 5 ++-- src/interactive/app/mod.rs | 1 + src/interactive/app/terminal_app.rs | 2 +- src/interactive/widgets/glob.rs | 2 +- src/interactive/widgets/main.rs | 2 +- 7 files changed, 44 insertions(+), 39 deletions(-) create mode 100644 src/interactive/app/app_state.rs diff --git a/src/interactive/app/app_state.rs b/src/interactive/app/app_state.rs new file mode 100644 index 00000000..7983374a --- /dev/null +++ b/src/interactive/app/app_state.rs @@ -0,0 +1,35 @@ +use dua::WalkResult; + +use super::{navigation::Navigation, EntryDataBundle, SortMode}; + +#[derive(Default, Copy, Clone, PartialEq)] +pub enum FocussedPane { + #[default] + Main, + Help, + Mark, + Glob, +} + +#[derive(Default)] +pub struct Cursor { + pub show: bool, + pub x: u16, + pub y: u16, +} + +#[derive(Default)] +pub struct AppState { + pub navigation: Navigation, + pub glob_navigation: Option, + pub entries: Vec, + pub sorting: SortMode, + pub message: Option, + pub focussed: FocussedPane, + pub is_scanning: bool, +} + +pub enum ProcessingResult { + Finished(WalkResult), + ExitRequested(WalkResult), +} \ No newline at end of file diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index 2d2459cb..98e13c20 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -3,7 +3,7 @@ use crate::interactive::{ sorted_entries, widgets::{glob_search, MainWindow, MainWindowProps}, ByteVisualization, CursorDirection, CursorMode, DisplayOptions, EntryDataBundle, MarkEntryMode, - SortMode, + SortMode, app_state::FocussedPane, }; use anyhow::Result; use crossbeam::channel::Receiver; @@ -17,41 +17,9 @@ use std::path::PathBuf; use tui::backend::Backend; use tui_react::Terminal; -use super::input::input_channel; +use super::{input::input_channel, app_state::{AppState, Cursor, ProcessingResult}}; use super::tree_view::TreeView; -#[derive(Default, Copy, Clone, PartialEq)] -pub enum FocussedPane { - #[default] - Main, - Help, - Mark, - Glob, -} - -#[derive(Default)] -pub struct Cursor { - pub show: bool, - pub x: u16, - pub y: u16, -} - -#[derive(Default)] -pub struct AppState { - pub navigation: Navigation, - pub glob_navigation: Option, - pub entries: Vec, - pub sorting: SortMode, - pub message: Option, - pub focussed: FocussedPane, - pub is_scanning: bool, -} - -pub enum ProcessingResult { - Finished(WalkResult), - ExitRequested(WalkResult), -} - impl AppState { pub fn navigation_mut(&mut self) -> &mut Navigation { self.glob_navigation diff --git a/src/interactive/app/handlers.rs b/src/interactive/app/handlers.rs index 6d5a7dd4..611c2a00 100644 --- a/src/interactive/app/handlers.rs +++ b/src/interactive/app/handlers.rs @@ -1,8 +1,7 @@ use crate::interactive::{ app::tree_view::TreeView, - app::FocussedPane::*, widgets::{GlobPane, HelpPane, MainWindow, MarkMode, MarkPane}, - AppState, DisplayOptions, EntryDataBundle, + DisplayOptions, EntryDataBundle, }; use crosstermion::input::Key; use dua::traverse::TreeIndex; @@ -10,6 +9,8 @@ use std::{fs, io, path::PathBuf}; use tui::backend::Backend; use tui_react::Terminal; +use super::app_state::{AppState, FocussedPane::*}; + #[derive(Copy, Clone)] pub enum CursorMode { Advance, diff --git a/src/interactive/app/mod.rs b/src/interactive/app/mod.rs index 73eb2f26..742127a8 100644 --- a/src/interactive/app/mod.rs +++ b/src/interactive/app/mod.rs @@ -6,6 +6,7 @@ pub mod input; mod navigation; pub mod tree_view; pub mod terminal_app; +pub mod app_state; pub use bytevis::*; pub use common::*; diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal_app.rs index b19bf1b0..13677241 100644 --- a/src/interactive/app/terminal_app.rs +++ b/src/interactive/app/terminal_app.rs @@ -9,7 +9,7 @@ use anyhow::Result; use crate::interactive::widgets::MainWindow; -use super::{DisplayOptions, AppState, ProcessingResult, ByteVisualization, sorted_entries, refresh_key}; +use super::{DisplayOptions, ByteVisualization, sorted_entries, refresh_key, app_state::{ProcessingResult, AppState}}; /// State and methods representing the interactive disk usage analyser for the terminal diff --git a/src/interactive/widgets/glob.rs b/src/interactive/widgets/glob.rs index bad9fd92..9f385a2c 100644 --- a/src/interactive/widgets/glob.rs +++ b/src/interactive/widgets/glob.rs @@ -18,7 +18,7 @@ use tui_react::{draw_text_nowrap_fn, Terminal}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; -use crate::interactive::Cursor; +use crate::interactive::app_state::Cursor; pub struct GlobPaneProps { pub border_style: Style, diff --git a/src/interactive/widgets/main.rs b/src/interactive/widgets/main.rs index 46e67240..6a4bfb85 100644 --- a/src/interactive/widgets/main.rs +++ b/src/interactive/widgets/main.rs @@ -3,7 +3,7 @@ use crate::interactive::{ Entries, EntriesProps, Footer, FooterProps, GlobPane, GlobPaneProps, Header, HelpPane, HelpPaneProps, MarkPane, MarkPaneProps, COLOR_MARKED, }, - AppState, Cursor, DisplayOptions, FocussedPane, + DisplayOptions, app_state::{AppState, FocussedPane, Cursor}, }; use std::borrow::Borrow; use tui::backend::Backend; From e53036ad84b71e1121588929fe4653a7ababbf67 Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Sun, 7 Jan 2024 09:34:16 +0000 Subject: [PATCH 05/20] Move ByteFormat out of WalkOptions --- src/aggregate.rs | 11 ++++++++--- src/common.rs | 1 - src/interactive/app/bytevis.rs | 4 ++-- src/interactive/app/terminal_app.rs | 8 +++----- src/interactive/app/tests/utils.rs | 20 +++++++++++--------- src/main.rs | 7 +++++-- 6 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/aggregate.rs b/src/aggregate.rs index f8672479..d314571e 100644 --- a/src/aggregate.rs +++ b/src/aggregate.rs @@ -1,4 +1,4 @@ -use crate::{crossdev, InodeFilter, Throttle, WalkOptions, WalkResult}; +use crate::{crossdev, InodeFilter, Throttle, WalkOptions, WalkResult, ByteFormat}; use anyhow::Result; use filesize::PathExt; use owo_colors::{AnsiColors as Color, OwoColorize}; @@ -14,6 +14,7 @@ pub fn aggregate( walk_options: WalkOptions, compute_total: bool, sort_by_size_in_bytes: bool, + byte_format: ByteFormat, paths: impl IntoIterator>, ) -> Result<(WalkResult, Statistics)> { let mut res = WalkResult::default(); @@ -94,6 +95,7 @@ pub fn aggregate( num_bytes, num_errors, path_color_of(&path), + byte_format )?; } total += num_bytes; @@ -114,6 +116,7 @@ pub fn aggregate( num_bytes, num_errors, path_color_of(&path), + byte_format )?; } } @@ -126,6 +129,7 @@ pub fn aggregate( total, res.num_errors, None, + byte_format )?; } Ok((res, stats)) @@ -142,10 +146,11 @@ fn output_colored_path( num_bytes: u128, num_errors: u64, path_color: Option, + byte_format: ByteFormat, ) -> std::result::Result<(), io::Error> { - let size = options.byte_format.display(num_bytes).to_string(); + let size = byte_format.display(num_bytes).to_string(); let size = size.green(); - let size_width = options.byte_format.width(); + let size_width = byte_format.width(); let path = path.as_ref().display(); let errors = (num_errors != 0) diff --git a/src/common.rs b/src/common.rs index 7f4f89ea..47c0b100 100644 --- a/src/common.rs +++ b/src/common.rs @@ -166,7 +166,6 @@ pub struct WalkOptions { /// The amount of threads to use. Refer to [`WalkDir::num_threads()`](https://docs.rs/jwalk/0.4.0/jwalk/struct.WalkDir.html#method.num_threads) /// for more information. pub threads: usize, - pub byte_format: ByteFormat, pub count_hard_links: bool, pub apparent_size: bool, pub sorting: TraversalSorting, diff --git a/src/interactive/app/bytevis.rs b/src/interactive/app/bytevis.rs index 1d1bc332..26a99e74 100644 --- a/src/interactive/app/bytevis.rs +++ b/src/interactive/app/bytevis.rs @@ -106,8 +106,8 @@ pub struct DisplayOptions { pub byte_vis: ByteVisualization, } -impl From for DisplayOptions { - fn from(WalkOptions { byte_format, .. }: WalkOptions) -> Self { +impl DisplayOptions { + pub fn new(byte_format: ByteFormat) -> Self { DisplayOptions { byte_format, byte_vis: ByteVisualization::default(), diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal_app.rs index 13677241..9d97ae78 100644 --- a/src/interactive/app/terminal_app.rs +++ b/src/interactive/app/terminal_app.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use crossbeam::channel::Receiver; use crosstermion::input::Event; -use dua::{traverse::{Traversal, Tree, EntryData}, WalkResult, WalkOptions}; +use dua::{traverse::{Traversal, Tree, EntryData}, WalkResult, WalkOptions, ByteFormat}; use tui::prelude::Backend; use tui_react::Terminal; use anyhow::Result; @@ -60,7 +60,7 @@ impl TerminalApp { pub fn initialize( terminal: &mut Terminal, - options: WalkOptions, + byte_format: ByteFormat, input_paths: Vec, keys_rx: Receiver, ) -> Result> @@ -70,9 +70,7 @@ impl TerminalApp { terminal.hide_cursor()?; terminal.clear()?; - let mut display: DisplayOptions = options.clone().into(); - display.byte_vis = ByteVisualization::PercentageAndBar; - + let mut display = DisplayOptions::new(byte_format); let mut window = MainWindow::default(); // #[inline] diff --git a/src/interactive/app/tests/utils.rs b/src/interactive/app/tests/utils.rs index 82cf00bc..e5c432a5 100644 --- a/src/interactive/app/tests/utils.rs +++ b/src/interactive/app/tests/utils.rs @@ -179,15 +179,7 @@ pub fn initialized_app_and_terminal_with_closure( let input_paths = fixture_paths.iter().map(|c| convert(c.as_ref())).collect(); let app = TerminalApp::initialize( &mut terminal, - WalkOptions { - threads: 1, - byte_format: ByteFormat::Metric, - apparent_size: true, - count_hard_links: false, - sorting: TraversalSorting::AlphabeticalByFileName, - cross_filesystems: false, - ignore_dirs: Default::default(), - }, + ByteFormat::Metric, input_paths, keys_rx, )? @@ -196,6 +188,16 @@ pub fn initialized_app_and_terminal_with_closure( terminal, app.expect("app that didn't try to abort iteration"), )) + + // WalkOptions { + // threads: 1, + // byte_format: , + // apparent_size: true, + // count_hard_links: false, + // sorting: TraversalSorting::AlphabeticalByFileName, + // cross_filesystems: false, + // ignore_dirs: Default::default(), + // } } pub fn new_test_terminal() -> std::io::Result> { diff --git a/src/main.rs b/src/main.rs index 636b01a3..7694cf1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use std::{fs, io, io::Write, path::PathBuf, process}; use crate::interactive::input::input_channel; use crate::interactive::terminal_app::TerminalApp; +use crate::options::ByteFormat; mod crossdev; #[cfg(feature = "tui-crossplatform")] @@ -42,9 +43,9 @@ fn main() -> Result<()> { info!("dua options={opt:#?}"); } + let byte_format: dua::ByteFormat = opt.format.into(); let walk_options = dua::WalkOptions { threads: opt.threads, - byte_format: opt.format.into(), apparent_size: opt.apparent_size, count_hard_links: opt.count_hard_links, sorting: TraversalSorting::None, @@ -70,7 +71,7 @@ fn main() -> Result<()> { let keys_rx = input_channel(); let res = TerminalApp::initialize( &mut terminal, - walk_options, + byte_format, extract_paths_maybe_set_cwd(input, !opt.stay_on_filesystem)?, keys_rx, )? @@ -123,6 +124,7 @@ fn main() -> Result<()> { walk_options, !no_total, !no_sort, + byte_format, extract_paths_maybe_set_cwd(input, !opt.stay_on_filesystem)?, )?; if statistics { @@ -139,6 +141,7 @@ fn main() -> Result<()> { walk_options, true, true, + byte_format, extract_paths_maybe_set_cwd(opt.input, !opt.stay_on_filesystem)?, )? .0 From 13c381bebc6a64e553ec11793ec8880f868e712c Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Sun, 7 Jan 2024 09:44:10 +0000 Subject: [PATCH 06/20] Clean-up init function --- src/aggregate.rs | 8 ++-- src/interactive/app/app_state.rs | 2 +- src/interactive/app/eventloop.rs | 8 +++- src/interactive/app/mod.rs | 4 +- src/interactive/app/terminal_app.rs | 26 ++++++------- src/interactive/app/tests/utils.rs | 18 +++------ src/interactive/widgets/main.rs | 3 +- src/main.rs | 57 +++++++++++++---------------- 8 files changed, 59 insertions(+), 67 deletions(-) diff --git a/src/aggregate.rs b/src/aggregate.rs index d314571e..163a7eba 100644 --- a/src/aggregate.rs +++ b/src/aggregate.rs @@ -1,4 +1,4 @@ -use crate::{crossdev, InodeFilter, Throttle, WalkOptions, WalkResult, ByteFormat}; +use crate::{crossdev, ByteFormat, InodeFilter, Throttle, WalkOptions, WalkResult}; use anyhow::Result; use filesize::PathExt; use owo_colors::{AnsiColors as Color, OwoColorize}; @@ -95,7 +95,7 @@ pub fn aggregate( num_bytes, num_errors, path_color_of(&path), - byte_format + byte_format, )?; } total += num_bytes; @@ -116,7 +116,7 @@ pub fn aggregate( num_bytes, num_errors, path_color_of(&path), - byte_format + byte_format, )?; } } @@ -129,7 +129,7 @@ pub fn aggregate( total, res.num_errors, None, - byte_format + byte_format, )?; } Ok((res, stats)) diff --git a/src/interactive/app/app_state.rs b/src/interactive/app/app_state.rs index 7983374a..dd0f9a0d 100644 --- a/src/interactive/app/app_state.rs +++ b/src/interactive/app/app_state.rs @@ -32,4 +32,4 @@ pub struct AppState { pub enum ProcessingResult { Finished(WalkResult), ExitRequested(WalkResult), -} \ No newline at end of file +} diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index 98e13c20..df297e3e 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -1,9 +1,10 @@ use crate::interactive::{ app::navigation::Navigation, + app_state::FocussedPane, sorted_entries, widgets::{glob_search, MainWindow, MainWindowProps}, ByteVisualization, CursorDirection, CursorMode, DisplayOptions, EntryDataBundle, MarkEntryMode, - SortMode, app_state::FocussedPane, + SortMode, }; use anyhow::Result; use crossbeam::channel::Receiver; @@ -17,8 +18,11 @@ use std::path::PathBuf; use tui::backend::Backend; use tui_react::Terminal; -use super::{input::input_channel, app_state::{AppState, Cursor, ProcessingResult}}; use super::tree_view::TreeView; +use super::{ + app_state::{AppState, Cursor, ProcessingResult}, + input::input_channel, +}; impl AppState { pub fn navigation_mut(&mut self) -> &mut Navigation { diff --git a/src/interactive/app/mod.rs b/src/interactive/app/mod.rs index 742127a8..2dd74eb3 100644 --- a/src/interactive/app/mod.rs +++ b/src/interactive/app/mod.rs @@ -1,12 +1,12 @@ +pub mod app_state; mod bytevis; mod common; mod eventloop; mod handlers; pub mod input; mod navigation; -pub mod tree_view; pub mod terminal_app; -pub mod app_state; +pub mod tree_view; pub use bytevis::*; pub use common::*; diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal_app.rs index 9d97ae78..0c961405 100644 --- a/src/interactive/app/terminal_app.rs +++ b/src/interactive/app/terminal_app.rs @@ -1,16 +1,21 @@ use std::path::PathBuf; +use anyhow::Result; use crossbeam::channel::Receiver; use crosstermion::input::Event; -use dua::{traverse::{Traversal, Tree, EntryData}, WalkResult, WalkOptions, ByteFormat}; +use dua::{ + traverse::{EntryData, Traversal, Tree}, + ByteFormat, WalkOptions, WalkResult, +}; use tui::prelude::Backend; use tui_react::Terminal; -use anyhow::Result; use crate::interactive::widgets::MainWindow; -use super::{DisplayOptions, ByteVisualization, sorted_entries, refresh_key, app_state::{ProcessingResult, AppState}}; - +use super::{ + app_state::{AppState, ProcessingResult}, + refresh_key, sorted_entries, ByteVisualization, DisplayOptions, +}; /// State and methods representing the interactive disk usage analyser for the terminal pub struct TerminalApp { @@ -58,18 +63,13 @@ impl TerminalApp { } } - pub fn initialize( - terminal: &mut Terminal, - byte_format: ByteFormat, - input_paths: Vec, - keys_rx: Receiver, - ) -> Result> + pub fn initialize(terminal: &mut Terminal, byte_format: ByteFormat) -> Result where B: Backend, { terminal.hide_cursor()?; terminal.clear()?; - + let mut display = DisplayOptions::new(byte_format); let mut window = MainWindow::default(); @@ -133,7 +133,7 @@ impl TerminalApp { // state.is_scanning = false; // if !received_events { - // } + // } let traversal = { let mut tree = Tree::new(); @@ -166,6 +166,6 @@ impl TerminalApp { }; app.refresh_view(terminal); - Ok(Some((keys_rx, app))) + Ok(app) } } diff --git a/src/interactive/app/tests/utils.rs b/src/interactive/app/tests/utils.rs index e5c432a5..64d561c6 100644 --- a/src/interactive/app/tests/utils.rs +++ b/src/interactive/app/tests/utils.rs @@ -175,19 +175,11 @@ pub fn initialized_app_and_terminal_with_closure( let mut terminal = new_test_terminal()?; std::env::set_current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))?; - let (_, keys_rx) = crossbeam::channel::unbounded(); - let input_paths = fixture_paths.iter().map(|c| convert(c.as_ref())).collect(); - let app = TerminalApp::initialize( - &mut terminal, - ByteFormat::Metric, - input_paths, - keys_rx, - )? - .map(|(_, app)| app); - Ok(( - terminal, - app.expect("app that didn't try to abort iteration"), - )) + // let (_, keys_rx) = crossbeam::channel::unbounded(); + // let input_paths = fixture_paths.iter().map(|c| convert(c.as_ref())).collect(); + let app = TerminalApp::initialize(&mut terminal, ByteFormat::Metric)?; + + Ok((terminal, app)) // WalkOptions { // threads: 1, diff --git a/src/interactive/widgets/main.rs b/src/interactive/widgets/main.rs index 6a4bfb85..3df375ae 100644 --- a/src/interactive/widgets/main.rs +++ b/src/interactive/widgets/main.rs @@ -1,9 +1,10 @@ use crate::interactive::{ + app_state::{AppState, Cursor, FocussedPane}, widgets::{ Entries, EntriesProps, Footer, FooterProps, GlobPane, GlobPaneProps, Header, HelpPane, HelpPaneProps, MarkPane, MarkPaneProps, COLOR_MARKED, }, - DisplayOptions, app_state::{AppState, FocussedPane, Cursor}, + DisplayOptions, }; use std::borrow::Borrow; use tui::backend::Backend; diff --git a/src/main.rs b/src/main.rs index 7694cf1d..e579aad6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,46 +68,41 @@ fn main() -> Result<()> { ) .with_context(|| "Could not instantiate terminal")?; + // TODO: use + // extract_paths_maybe_set_cwd(input, !opt.stay_on_filesystem)?, + let keys_rx = input_channel(); - let res = TerminalApp::initialize( - &mut terminal, - byte_format, - extract_paths_maybe_set_cwd(input, !opt.stay_on_filesystem)?, - keys_rx, - )? - .map(|(keys_rx, mut app)| { - let res = app.process_events(&mut terminal, keys_rx.into_iter()); - - let res = res.map(|r| { - ( - r, - app.window - .mark_pane - .take() - .map(|marked| marked.into_paths()), - ) - }); - // Leak app memory to avoid having to wait for the hashmap to deallocate, - // which causes a noticeable delay shortly before the the program exits anyway. - std::mem::forget(app); - res + let mut app = TerminalApp::initialize(&mut terminal, byte_format)?; + + let res = app.process_events(&mut terminal, keys_rx.into_iter()); + + let res = res.map(|r| { + ( + r, + app.window + .mark_pane + .take() + .map(|marked| marked.into_paths()), + ) }); + // Leak app memory to avoid having to wait for the hashmap to deallocate, + // which causes a noticeable delay shortly before the the program exits anyway. + std::mem::forget(app); drop(terminal); io::stderr().flush().ok(); // Exit 'quickly' to avoid having to not have to deal with slightly different types in the other match branches std::process::exit( - res.transpose()? - .map(|(walk_result, paths)| { - if let Some(paths) = paths { - for path in paths { - println!("{}", path.display()) - } + res.map(|(walk_result, paths)| { + if let Some(paths) = paths { + for path in paths { + println!("{}", path.display()) } - walk_result.to_exit_code() - }) - .unwrap_or(0), + } + walk_result.to_exit_code() + }) + .unwrap_or(0), ); } Some(Aggregate { From 51b67ff9d009a56272448d1fee1951f30b1de678 Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Sun, 7 Jan 2024 19:36:43 +0000 Subject: [PATCH 07/20] wip --- src/common.rs | 2 +- src/interactive/app/app_state.rs | 70 +++- src/interactive/app/eventloop.rs | 412 ++++++++++++++++------ src/interactive/app/terminal_app.rs | 190 +++++----- src/lib.rs | 2 +- src/main.rs | 18 +- src/traverse.rs | 521 ++++++++++++++-------------- 7 files changed, 745 insertions(+), 470 deletions(-) diff --git a/src/common.rs b/src/common.rs index 47c0b100..254fbbf7 100644 --- a/src/common.rs +++ b/src/common.rs @@ -176,7 +176,7 @@ pub struct WalkOptions { type WalkDir = jwalk::WalkDirGeneric<((), Option>)>; impl WalkOptions { - pub(crate) fn iter_from_path(&self, root: &Path, root_device_id: u64) -> WalkDir { + pub fn iter_from_path(&self, root: &Path, root_device_id: u64) -> WalkDir { let ignore_dirs = self.ignore_dirs.clone(); let cwd = std::env::current_dir().unwrap_or_else(|_| root.to_owned()); WalkDir::new(root) diff --git a/src/interactive/app/app_state.rs b/src/interactive/app/app_state.rs index dd0f9a0d..8986f094 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, 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, + 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, +} + +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 { + 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 df297e3e..014ce36d 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, - events: impl Iterator, + walk_options: &WalkOptions, + events: Receiver, + traversal_events: Receiver, ) -> Result 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(&mut self, + window: &mut MainWindow, + traversal: &mut Traversal, + display: &mut DisplayOptions, + terminal: &mut Terminal, + event: Event + ) -> Result> + 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 0c961405..01398dff 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, TerminalApp); +pub type TraversalEntry = Result>)>, jwalk::Error>; -impl TerminalApp { - pub fn refresh_view(&mut self, terminal: &mut Terminal) - 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( - &mut self, - terminal: &mut Terminal, - events: impl Iterator, - ) -> Result - 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, u64), + Finished(u64), +} - pub fn initialize(terminal: &mut Terminal, byte_format: ByteFormat) -> Result +impl TerminalApp { + pub fn initialize(terminal: &mut Terminal, walk_options: WalkOptions, byte_format: ByteFormat) -> Result 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) -> Vec { - // 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) -> Result> { + 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( + &mut self, + terminal: &mut Terminal, + events: Receiver, + traversal: Receiver, + ) -> Result + 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), + } } } diff --git a/src/lib.rs b/src/lib.rs index bd46b4a4..6b08772e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ mod aggregate; mod common; mod crossdev; -mod inodefilter; +pub mod inodefilter; pub mod traverse; diff --git a/src/main.rs b/src/main.rs index e579aad6..a3351bc7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,7 +44,7 @@ fn main() -> Result<()> { } let byte_format: dua::ByteFormat = opt.format.into(); - let walk_options = dua::WalkOptions { + let mut walk_options = dua::WalkOptions { threads: opt.threads, apparent_size: opt.apparent_size, count_hard_links: opt.count_hard_links, @@ -52,6 +52,13 @@ fn main() -> Result<()> { cross_filesystems: !opt.stay_on_filesystem, ignore_dirs: canonicalize_ignore_dirs(&opt.ignore_dirs), }; + + if walk_options.threads == 0 { + // avoid using the global rayon pool, as it will keep a lot of threads alive after we are done. + // Also means that we will spin up a bunch of threads per root path, instead of reusing them. + walk_options.threads = num_cpus::get(); + } + let res = match opt.command { #[cfg(feature = "tui-crossplatform")] Some(Interactive { input }) => { @@ -68,13 +75,12 @@ fn main() -> Result<()> { ) .with_context(|| "Could not instantiate terminal")?; - // TODO: use - // extract_paths_maybe_set_cwd(input, !opt.stay_on_filesystem)?, - let keys_rx = input_channel(); - let mut app = TerminalApp::initialize(&mut terminal, byte_format)?; + let mut app = TerminalApp::initialize(&mut terminal, walk_options, byte_format)?; - let res = app.process_events(&mut terminal, keys_rx.into_iter()); + let traversal_rx = app.scan(extract_paths_maybe_set_cwd(input, !opt.stay_on_filesystem)?)?; + + let res = app.process_events(&mut terminal, keys_rx, traversal_rx); let res = res.map(|r| { ( diff --git a/src/traverse.rs b/src/traverse.rs index 69c16395..88384384 100644 --- a/src/traverse.rs +++ b/src/traverse.rs @@ -71,283 +71,254 @@ pub struct Traversal { } impl Traversal { - pub fn from_walk( - mut walk_options: WalkOptions, - input: Vec, - waker_rx: &crossbeam::channel::Receiver, - mut update: impl FnMut(&mut Traversal, Option) -> Result, - ) -> Result> { - #[derive(Default, Copy, Clone)] - struct EntryInfo { - size: u128, - entries_count: Option, - } - impl EntryInfo { - 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, - }; - } - } - 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; - } - 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") - } - fn pop_or_panic(v: &mut Vec) -> EntryInfo { - v.pop().expect("sizes per level to be in sync with graph") - } + // pub fn from_walk( + // mut walk_options: WalkOptions, + // input: Vec, + // mut update: impl FnMut(&mut Traversal, Option) -> Result, + // ) -> Result> { + - let mut t = { - 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, - } - }; - - let (mut previous_node_idx, mut parent_node_idx) = (t.root_index, t.root_index); - let mut directory_info_per_depth_level = Vec::new(); - let mut current_directory_at_depth = EntryInfo::default(); - let mut previous_depth = 0; - let mut inodes = InodeFilter::default(); - - let throttle = Throttle::new(Duration::from_millis(250), None); - if walk_options.threads == 0 { - // avoid using the global rayon pool, as it will keep a lot of threads alive after we are done. - // Also means that we will spin up a bunch of threads per root path, instead of reusing them. - walk_options.threads = num_cpus::get(); - } + // let mut t = { + // 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, + // } + // }; - #[cfg(not(windows))] - fn size_on_disk(_parent: &Path, name: &Path, meta: &Metadata) -> io::Result { - name.size_on_disk_fast(meta) - } - #[cfg(windows)] - fn size_on_disk(parent: &Path, name: &Path, meta: &Metadata) -> io::Result { - parent.join(name).size_on_disk_fast(meta) - } + // let (mut previous_node_idx, mut parent_node_idx) = (t.root_index, t.root_index); + // let mut directory_info_per_depth_level = Vec::new(); + // let mut current_directory_at_depth = EntryInfo::default(); + // let mut previous_depth = 0; + // let mut inodes = InodeFilter::default(); - let (entry_tx, entry_rx) = crossbeam::channel::bounded(100); - std::thread::Builder::new() - .name("dua-fs-walk-dispatcher".to_string()) - .spawn({ - let walk_options = walk_options.clone(); - move || { - for root_path in input.into_iter() { - let device_id = match crossdev::init(root_path.as_ref()) { - Ok(id) => id, - Err(_) => { - t.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((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; - } - } - } - } - })?; - - loop { - crossbeam::select! { - recv(entry_rx) -> entry => { - let Ok((entry, root_path, device_id)) = entry else { - break; - }; - - 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 || 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, previous_depth) { - (n, p) if n > p => { - directory_info_per_depth_level.push(current_directory_at_depth); - current_directory_at_depth = EntryInfo { - size: file_size, - entries_count: Some(1), - }; - parent_node_idx = previous_node_idx; - } - (n, p) if n < p => { - for _ in n..p { - set_entry_info_or_panic( - &mut t.tree, - parent_node_idx, - current_directory_at_depth, - ); - let dir_info = - pop_or_panic(&mut directory_info_per_depth_level); - - current_directory_at_depth.size += dir_info.size; - current_directory_at_depth.add_count(&dir_info); - - parent_node_idx = parent_or_panic(&mut t.tree, parent_node_idx); - } - current_directory_at_depth.size += file_size; - *current_directory_at_depth.entries_count.get_or_insert(0) += 1; - set_entry_info_or_panic( - &mut t.tree, - parent_node_idx, - current_directory_at_depth, - ); - } - _ => { - current_directory_at_depth.size += file_size; - *current_directory_at_depth.entries_count.get_or_insert(0) += 1; - } - }; - - data.mtime = mtime; - data.size = file_size; - let entry_index = t.tree.add_node(data); - - t.tree.add_edge(parent_node_idx, entry_index, ()); - previous_node_idx = entry_index; - previous_depth = entry.depth; - } - Err(_) => { - if previous_depth == 0 { - data.name = (*root_path).clone(); - let entry_index = t.tree.add_node(data); - t.tree.add_edge(parent_node_idx, entry_index, ()); - } - - t.io_errors += 1 - } - } - - if throttle.can_update() && update(&mut t, None)? { - return Ok(None); - } - }, - recv(waker_rx) -> waker_value => { - let Ok(waker_value) = waker_value else { - continue; - }; - if update(&mut t, Some(waker_value))? { - return Ok(None); - } - }, - 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); - } - } - } - } + // let throttle = Throttle::new(Duration::from_millis(250), None); + // // if walk_options.threads == 0 { + // // // avoid using the global rayon pool, as it will keep a lot of threads alive after we are done. + // // // Also means that we will spin up a bunch of threads per root path, instead of reusing them. + // // walk_options.threads = num_cpus::get(); + // // } - directory_info_per_depth_level.push(current_directory_at_depth); - current_directory_at_depth = EntryInfo::default(); - for _ in 0..previous_depth { - let dir_info = pop_or_panic(&mut directory_info_per_depth_level); - current_directory_at_depth.size += dir_info.size; - current_directory_at_depth.add_count(&dir_info); + // // #[cfg(not(windows))] + // // fn size_on_disk(_parent: &Path, name: &Path, meta: &Metadata) -> io::Result { + // // name.size_on_disk_fast(meta) + // // } + // // #[cfg(windows)] + // // fn size_on_disk(parent: &Path, name: &Path, meta: &Metadata) -> io::Result { + // // parent.join(name).size_on_disk_fast(meta) + // // } - set_entry_info_or_panic(&mut t.tree, parent_node_idx, current_directory_at_depth); - parent_node_idx = parent_or_panic(&mut t.tree, parent_node_idx); - } - 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); + // // enum TraversalEvents { + // // Entry(Result>)>, jwalk::Error>, Arc, u64), + // // Finished, + // // } - t.elapsed = Some(t.start.elapsed()); - Ok(Some(t)) - } + // let (entry_tx, entry_rx) = crossbeam::channel::bounded(100); + // std::thread::Builder::new() + // .name("dua-fs-walk-dispatcher".to_string()) + // .spawn({ + // let walk_options = walk_options.clone(); + // move || { + // for root_path in input.into_iter() { + // let device_id = match crossdev::init(root_path.as_ref()) { + // Ok(id) => id, + // Err(_) => { + // t.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(TraversalEvents::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(TraversalEvents::Finished).is_err() { + // log::error!("Failed to send TraversalEvents::Finished event"); + // } + // } + // })?; + + // // loop { + // // crossbeam::select! { + // // recv(entry_rx) -> entry => { + // // let Ok((entry, root_path, device_id)) = entry else { + // // break; + // // }; + + // // 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 || 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; + // // } - fn recompute_root_size(&self) -> u128 { + // // 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, previous_depth) { + // // (n, p) if n > p => { + // // directory_info_per_depth_level.push(current_directory_at_depth); + // // current_directory_at_depth = EntryInfo { + // // size: file_size, + // // entries_count: Some(1), + // // }; + // // parent_node_idx = previous_node_idx; + // // } + // // (n, p) if n < p => { + // // for _ in n..p { + // // set_entry_info_or_panic( + // // &mut t.tree, + // // parent_node_idx, + // // current_directory_at_depth, + // // ); + // // let dir_info = + // // pop_or_panic(&mut directory_info_per_depth_level); + + // // current_directory_at_depth.size += dir_info.size; + // // current_directory_at_depth.add_count(&dir_info); + + // // parent_node_idx = parent_or_panic(&mut t.tree, parent_node_idx); + // // } + // // current_directory_at_depth.size += file_size; + // // *current_directory_at_depth.entries_count.get_or_insert(0) += 1; + // // set_entry_info_or_panic( + // // &mut t.tree, + // // parent_node_idx, + // // current_directory_at_depth, + // // ); + // // } + // // _ => { + // // current_directory_at_depth.size += file_size; + // // *current_directory_at_depth.entries_count.get_or_insert(0) += 1; + // // } + // // }; + + // // data.mtime = mtime; + // // data.size = file_size; + // // let entry_index = t.tree.add_node(data); + + // // t.tree.add_edge(parent_node_idx, entry_index, ()); + // // previous_node_idx = entry_index; + // // previous_depth = entry.depth; + // // } + // // Err(_) => { + // // if previous_depth == 0 { + // // data.name = (*root_path).clone(); + // // let entry_index = t.tree.add_node(data); + // // t.tree.add_edge(parent_node_idx, entry_index, ()); + // // } + + // // t.io_errors += 1 + // // } + // // } + + // // if throttle.can_update() && update(&mut t, None)? { + // // return Ok(None); + // // } + // // }, + // // recv(waker_rx) -> waker_value => { + // // let Ok(waker_value) = waker_value else { + // // continue; + // // }; + // // if update(&mut t, Some(waker_value))? { + // // return Ok(None); + // // } + // // }, + // // 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); + // // } + // // } + // // } + // // } + + // directory_info_per_depth_level.push(current_directory_at_depth); + // current_directory_at_depth = EntryInfo::default(); + // for _ in 0..previous_depth { + // let dir_info = pop_or_panic(&mut directory_info_per_depth_level); + // current_directory_at_depth.size += dir_info.size; + // current_directory_at_depth.add_count(&dir_info); + + // set_entry_info_or_panic(&mut t.tree, parent_node_idx, current_directory_at_depth); + // parent_node_idx = parent_or_panic(&mut t.tree, parent_node_idx); + // } + // 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)) + // } + + pub fn recompute_root_size(&self) -> u128 { self.tree .neighbors_directed(self.root_index, Direction::Outgoing) .map(|idx| get_size_or_panic(&self.tree, idx)) @@ -355,6 +326,16 @@ impl Traversal { } } +#[cfg(not(windows))] +pub fn size_on_disk(_parent: &Path, name: &Path, meta: &Metadata) -> io::Result { + name.size_on_disk_fast(meta) +} + +#[cfg(windows)] +pub fn size_on_disk(parent: &Path, name: &Path, meta: &Metadata) -> io::Result { + parent.join(name).size_on_disk_fast(meta) +} + #[cfg(test)] mod tests { use super::*; From bb511b538c7d75b02d598d495b307a83a11f53c0 Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Sun, 7 Jan 2024 20:02:45 +0000 Subject: [PATCH 08/20] Update entries --- src/interactive/app/app_state.rs | 6 ++++- src/interactive/app/eventloop.rs | 34 +++++++++++++++++++++++----- src/interactive/app/terminal_app.rs | 35 +---------------------------- 3 files changed, 34 insertions(+), 41 deletions(-) diff --git a/src/interactive/app/app_state.rs b/src/interactive/app/app_state.rs index 8986f094..f4a8d202 100644 --- a/src/interactive/app/app_state.rs +++ b/src/interactive/app/app_state.rs @@ -1,4 +1,6 @@ -use dua::{WalkResult, traverse::{TreeIndex, Tree}, inodefilter::InodeFilter}; +use std::time::Duration; + +use dua::{WalkResult, traverse::{TreeIndex, Tree}, inodefilter::InodeFilter, Throttle}; use petgraph::Direction; use super::{navigation::Navigation, EntryDataBundle, SortMode}; @@ -40,6 +42,7 @@ pub struct TraversalState { pub current_directory_at_depth: EntryInfo, pub previous_depth: usize, pub inodes: InodeFilter, + pub throttle: Option } impl TraversalState { @@ -51,6 +54,7 @@ impl TraversalState { current_directory_at_depth: EntryInfo::default(), previous_depth: 0, inodes: InodeFilter::default(), + throttle: Some(Throttle::new(Duration::from_millis(250), None)), } } } diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index 014ce36d..0cc38e9e 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -116,7 +116,7 @@ impl AppState { // })) } - // TODO: + // TODO: do we need this? // default(Duration::from_millis(250)) => { // // No events or new entries received, but we still need // // to keep updating the status message regularly. @@ -239,12 +239,16 @@ impl AppState { } } - // TODO: - // if throttle.can_update() && update(&mut t, None)? { - // return Ok(None); - // } + if let Some(throttle) = &self.traversal_state.throttle { + if throttle.can_update() { + self.update_state(t); + } + } }, TraversalEvent::Finished(io_errors) => { + t.io_errors += io_errors; + + self.traversal_state.throttle = None; 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 { @@ -266,11 +270,29 @@ impl AppState { ); t.total_bytes = Some(root_size); t.elapsed = Some(t.start.elapsed()); - // Ok(Some(t)) + + self.update_state(t); } } } + fn update_state<'a>(&mut self, traversal: &'a Traversal) { + let mut received_events = false; + if !received_events { + self.navigation_mut().view_root = traversal.root_index; + } + self.entries = sorted_entries( + &traversal.tree, + self.navigation().view_root, + self.sorting, + self.glob_root(), + ); + if !received_events { + self.navigation_mut().selected = self.entries.first().map(|b| b.index); + } + self.reset_message(); // force "scanning" to appear + } + fn process_event(&mut self, window: &mut MainWindow, traversal: &mut Traversal, diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal_app.rs index 01398dff..21ee66f2 100644 --- a/src/interactive/app/terminal_app.rs +++ b/src/interactive/app/terminal_app.rs @@ -125,40 +125,7 @@ impl TerminalApp { // let mut received_events = false; // let traversal = // Traversal::from_walk(options, input_paths, |traversal, event| { - // if !received_events { - // state.navigation_mut().view_root = traversal.root_index; - // } - // state.entries = sorted_entries( - // &traversal.tree, - // state.navigation().view_root, - // state.sorting, - // state.glob_root(), - // ); - // if !received_events { - // state.navigation_mut().selected = state.entries.first().map(|b| b.index); - // } - // state.reset_message(); // force "scanning" to appear - - // let mut events = fetch_buffered_key_events(&keys_rx); - // if let Some(event) = event { - // // This update is triggered by a user event, insert it - // // before any events fetched later. - // events.insert(0, event); - // } - // received_events |= !events.is_empty(); - - // let should_exit = match state.process_events( - // &mut window, - // traversal, - // &mut display, - // terminal, - // events.into_iter(), - // )? { - // ProcessingResult::ExitRequested(_) => true, - // ProcessingResult::Finished(_) => false, - // }; - - // Ok(should_exit) + // })?; // let traversal = match traversal { From 0cd5ea9612005ff724226ba502c2bea8ff4f0486 Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Sun, 7 Jan 2024 20:05:07 +0000 Subject: [PATCH 09/20] cargo fmt --- src/interactive/app/app_state.rs | 9 +- src/interactive/app/eventloop.rs | 125 +++++++++++++++++++--------- src/interactive/app/terminal_app.rs | 17 +++- src/main.rs | 7 +- src/traverse.rs | 1 - 5 files changed, 107 insertions(+), 52 deletions(-) diff --git a/src/interactive/app/app_state.rs b/src/interactive/app/app_state.rs index f4a8d202..74989fe1 100644 --- a/src/interactive/app/app_state.rs +++ b/src/interactive/app/app_state.rs @@ -1,6 +1,10 @@ use std::time::Duration; -use dua::{WalkResult, traverse::{TreeIndex, Tree}, inodefilter::InodeFilter, Throttle}; +use dua::{ + inodefilter::InodeFilter, + traverse::{Tree, TreeIndex}, + Throttle, WalkResult, +}; use petgraph::Direction; use super::{navigation::Navigation, EntryDataBundle, SortMode}; @@ -33,7 +37,6 @@ pub struct AppState { pub traversal_state: TraversalState, } - #[derive(Default)] pub struct TraversalState { pub previous_node_idx: TreeIndex, @@ -42,7 +45,7 @@ pub struct TraversalState { pub current_directory_at_depth: EntryInfo, pub previous_depth: usize, pub inodes: InodeFilter, - pub throttle: Option + pub throttle: Option, } impl TraversalState { diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index 0cc38e9e..70df427c 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -1,24 +1,34 @@ -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 crate::{ + crossdev, + interactive::{ + app::navigation::Navigation, + app_state::FocussedPane, + sorted_entries, + widgets::{glob_search, MainWindow, MainWindowProps}, + ByteVisualization, CursorDirection, CursorMode, DisplayOptions, EntryDataBundle, + MarkEntryMode, SortMode, + }, +}; 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, size_on_disk}, + traverse::{size_on_disk, EntryData, Traversal, Tree}, WalkOptions, WalkResult, }; -use std::{path::PathBuf, time::{SystemTime, UNIX_EPOCH}}; +use std::{ + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, +}; use tui::backend::Backend; use tui_react::Terminal; -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::{parent_or_panic, pop_or_panic, set_entry_info_or_panic, EntryInfo}, + terminal_app::TraversalEvent, + tree_view::TreeView, +}; use super::{ app_state::{AppState, Cursor, ProcessingResult}, input::input_channel, @@ -125,9 +135,14 @@ impl AppState { // } // } // } - // } + // } - fn process_traversal_event<'a>(&mut self, t: &'a mut Traversal, walk_options: &'a WalkOptions, event: TraversalEvent) { + 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; @@ -145,7 +160,8 @@ impl AppState { 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.count_hard_links + || self.traversal_state.inodes.add(m)) && (walk_options.cross_filesystems || crossdev::is_same_device(device_id, m)) { @@ -184,12 +200,15 @@ impl AppState { 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 + .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; + self.traversal_state.parent_node_idx = + self.traversal_state.previous_node_idx; } (n, p) if n < p => { for _ in n..p { @@ -198,16 +217,27 @@ impl AppState { 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); + 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.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.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; + *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, @@ -216,7 +246,11 @@ impl AppState { } _ => { 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 + .traversal_state + .current_directory_at_depth + .entries_count + .get_or_insert(0) += 1; } }; @@ -224,7 +258,8 @@ impl AppState { data.size = file_size; let entry_index = t.tree.add_node(data); - t.tree.add_edge(self.traversal_state.parent_node_idx, entry_index, ()); + 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; } @@ -232,7 +267,8 @@ impl AppState { 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.tree + .add_edge(self.traversal_state.parent_node_idx, entry_index, ()); } t.io_errors += 1 @@ -244,20 +280,30 @@ impl AppState { self.update_state(t); } } - }, + } TraversalEvent::Finished(io_errors) => { t.io_errors += io_errors; self.traversal_state.throttle = None; - self.traversal_state.directory_info_per_depth_level.push(self.traversal_state.current_directory_at_depth); + 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); + 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); + 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); } let root_size = t.recompute_root_size(); set_entry_info_or_panic( @@ -270,7 +316,7 @@ impl AppState { ); t.total_bytes = Some(root_size); t.elapsed = Some(t.start.elapsed()); - + self.update_state(t); } } @@ -293,12 +339,13 @@ impl AppState { self.reset_message(); // force "scanning" to appear } - fn process_event(&mut self, + fn process_event( + &mut self, window: &mut MainWindow, traversal: &mut Traversal, display: &mut DisplayOptions, terminal: &mut Terminal, - event: Event + event: Event, ) -> Result> where B: Backend, @@ -347,9 +394,7 @@ impl AppState { if !handled { match self.focussed { - Mark => { - self.dispatch_to_mark_pane(key, window, &mut tree_view, *display, terminal) - } + Mark => self.dispatch_to_mark_pane(key, window, &mut tree_view, *display, terminal), Help => { window .help_pane @@ -378,9 +423,7 @@ impl AppState { window, &tree_view, ), - Char('a') => { - self.mark_all_entries(MarkEntryMode::Toggle, 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) } diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal_app.rs index 21ee66f2..ce1d5c64 100644 --- a/src/interactive/app/terminal_app.rs +++ b/src/interactive/app/terminal_app.rs @@ -10,7 +10,7 @@ use dua::{ use tui::prelude::Backend; use tui_react::Terminal; -use crate::{interactive::widgets::MainWindow, crossdev}; +use crate::{crossdev, interactive::widgets::MainWindow}; use super::{ app_state::{AppState, ProcessingResult, TraversalState}, @@ -26,7 +26,8 @@ pub struct TerminalApp { pub walk_options: WalkOptions, } -pub type TraversalEntry = Result>)>, jwalk::Error>; +pub type TraversalEntry = + Result>)>, jwalk::Error>; pub enum TraversalEvent { Entry(TraversalEntry, Arc, u64), @@ -34,7 +35,11 @@ pub enum TraversalEvent { } impl TerminalApp { - pub fn initialize(terminal: &mut Terminal, walk_options: WalkOptions, byte_format: ByteFormat) -> Result + pub fn initialize( + terminal: &mut Terminal, + walk_options: WalkOptions, + byte_format: ByteFormat, + ) -> Result where B: Backend, { @@ -107,7 +112,11 @@ impl TerminalApp { .into_iter() { if entry_tx - .send(TraversalEvent::Entry(entry, Arc::clone(&root_path), device_id)) + .send(TraversalEvent::Entry( + entry, + Arc::clone(&root_path), + device_id, + )) .is_err() { // The channel is closed, this means the user has diff --git a/src/main.rs b/src/main.rs index a3351bc7..a3b314cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,7 +52,7 @@ fn main() -> Result<()> { cross_filesystems: !opt.stay_on_filesystem, ignore_dirs: canonicalize_ignore_dirs(&opt.ignore_dirs), }; - + if walk_options.threads == 0 { // avoid using the global rayon pool, as it will keep a lot of threads alive after we are done. // Also means that we will spin up a bunch of threads per root path, instead of reusing them. @@ -78,8 +78,9 @@ fn main() -> Result<()> { let keys_rx = input_channel(); let mut app = TerminalApp::initialize(&mut terminal, walk_options, byte_format)?; - let traversal_rx = app.scan(extract_paths_maybe_set_cwd(input, !opt.stay_on_filesystem)?)?; - + let traversal_rx = + app.scan(extract_paths_maybe_set_cwd(input, !opt.stay_on_filesystem)?)?; + let res = app.process_events(&mut terminal, keys_rx, traversal_rx); let res = res.map(|r| { diff --git a/src/traverse.rs b/src/traverse.rs index 88384384..fe862e37 100644 --- a/src/traverse.rs +++ b/src/traverse.rs @@ -76,7 +76,6 @@ impl Traversal { // input: Vec, // mut update: impl FnMut(&mut Traversal, Option) -> Result, // ) -> Result> { - // let mut t = { // let mut tree = Tree::new(); From b52f66e4cd48bc670b1b98a4a713e280b63d9432 Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Sun, 7 Jan 2024 20:15:55 +0000 Subject: [PATCH 10/20] first working version --- src/interactive/app/app_state.rs | 2 ++ src/interactive/app/eventloop.rs | 31 +++++++++++++++++++++++------ src/interactive/app/terminal_app.rs | 16 +-------------- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/interactive/app/app_state.rs b/src/interactive/app/app_state.rs index 74989fe1..45784385 100644 --- a/src/interactive/app/app_state.rs +++ b/src/interactive/app/app_state.rs @@ -46,6 +46,7 @@ pub struct TraversalState { pub previous_depth: usize, pub inodes: InodeFilter, pub throttle: Option, + pub received_event: bool, } impl TraversalState { @@ -58,6 +59,7 @@ impl TraversalState { previous_depth: 0, inodes: InodeFilter::default(), throttle: Some(Throttle::new(Duration::from_millis(250), None)), + received_event: false, } } } diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index 70df427c..02918132 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -116,7 +116,18 @@ impl AppState { let Ok(event) = event else { continue; }; - self.process_traversal_event(traversal, walk_options, event); + if self.process_traversal_event(traversal, walk_options, event) { + self.update_state(traversal); + let result = self.process_event( + window, + traversal, + display, + terminal, + Event::Key(refresh_key()))?; + if let Some(processing_result) = result { + return Ok(processing_result); + } + } } } } @@ -142,7 +153,7 @@ impl AppState { t: &'a mut Traversal, walk_options: &'a WalkOptions, event: TraversalEvent, - ) { + ) -> bool { match event { TraversalEvent::Entry(entry, root_path, device_id) => { t.entries_traversed += 1; @@ -277,7 +288,7 @@ impl AppState { if let Some(throttle) = &self.traversal_state.throttle { if throttle.can_update() { - self.update_state(t); + return true; } } } @@ -317,13 +328,16 @@ impl AppState { t.total_bytes = Some(root_size); t.elapsed = Some(t.start.elapsed()); - self.update_state(t); + self.is_scanning = false; + + return true; } } + return false; } fn update_state<'a>(&mut self, traversal: &'a Traversal) { - let mut received_events = false; + let received_events = self.traversal_state.received_event; if !received_events { self.navigation_mut().view_root = traversal.root_index; } @@ -354,7 +368,12 @@ impl AppState { use FocussedPane::*; let key = match event { - Event::Key(key) if key.kind != KeyEventKind::Release => key, + Event::Key(key) if key.kind != KeyEventKind::Release => { + if key.code != KeyCode::Char('\r') { + self.traversal_state.received_event = true; + } + key + }, Event::Resize(_, _) => refresh_key(), _ => return Ok(None), }; diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal_app.rs index ce1d5c64..f3e3369e 100644 --- a/src/interactive/app/terminal_app.rs +++ b/src/interactive/app/terminal_app.rs @@ -89,6 +89,7 @@ impl TerminalApp { pub fn scan<'a>(&mut self, input: Vec) -> Result> { self.state.traversal_state = TraversalState::new(self.traversal.root_index); + self.state.is_scanning = true; let (entry_tx, entry_rx) = crossbeam::channel::bounded(100); std::thread::Builder::new() @@ -131,21 +132,6 @@ impl TerminalApp { } })?; - // let mut received_events = false; - // let traversal = - // Traversal::from_walk(options, input_paths, |traversal, event| { - - // })?; - - // let traversal = match traversal { - // Some(t) => t, - // None => return Ok(None), - // }; - - // state.is_scanning = false; - // if !received_events { - // } - Ok(entry_rx) } From 7378bd8bb1887379688eafe00a773521a7c32c9b Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Sun, 7 Jan 2024 20:18:11 +0000 Subject: [PATCH 11/20] remove commented out code --- src/interactive/app/eventloop.rs | 15 -- src/traverse.rs | 246 ------------------------------- 2 files changed, 261 deletions(-) diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index 02918132..56442368 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -131,23 +131,8 @@ impl AppState { } } } - // TODO: do we need this? - // Ok(ProcessingResult::Finished(WalkResult { - // num_errors: traversal.io_errors, - // })) } - // TODO: do we need this? - // 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, diff --git a/src/traverse.rs b/src/traverse.rs index fe862e37..98cf41c0 100644 --- a/src/traverse.rs +++ b/src/traverse.rs @@ -71,252 +71,6 @@ pub struct Traversal { } impl Traversal { - // pub fn from_walk( - // mut walk_options: WalkOptions, - // input: Vec, - // mut update: impl FnMut(&mut Traversal, Option) -> Result, - // ) -> Result> { - - // let mut t = { - // 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, - // } - // }; - - // let (mut previous_node_idx, mut parent_node_idx) = (t.root_index, t.root_index); - // let mut directory_info_per_depth_level = Vec::new(); - // let mut current_directory_at_depth = EntryInfo::default(); - // let mut previous_depth = 0; - // let mut inodes = InodeFilter::default(); - - // let throttle = Throttle::new(Duration::from_millis(250), None); - // // if walk_options.threads == 0 { - // // // avoid using the global rayon pool, as it will keep a lot of threads alive after we are done. - // // // Also means that we will spin up a bunch of threads per root path, instead of reusing them. - // // walk_options.threads = num_cpus::get(); - // // } - - // // #[cfg(not(windows))] - // // fn size_on_disk(_parent: &Path, name: &Path, meta: &Metadata) -> io::Result { - // // name.size_on_disk_fast(meta) - // // } - // // #[cfg(windows)] - // // fn size_on_disk(parent: &Path, name: &Path, meta: &Metadata) -> io::Result { - // // parent.join(name).size_on_disk_fast(meta) - // // } - - // // enum TraversalEvents { - // // Entry(Result>)>, jwalk::Error>, Arc, u64), - // // Finished, - // // } - - // let (entry_tx, entry_rx) = crossbeam::channel::bounded(100); - // std::thread::Builder::new() - // .name("dua-fs-walk-dispatcher".to_string()) - // .spawn({ - // let walk_options = walk_options.clone(); - // move || { - // for root_path in input.into_iter() { - // let device_id = match crossdev::init(root_path.as_ref()) { - // Ok(id) => id, - // Err(_) => { - // t.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(TraversalEvents::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(TraversalEvents::Finished).is_err() { - // log::error!("Failed to send TraversalEvents::Finished event"); - // } - // } - // })?; - - // // loop { - // // crossbeam::select! { - // // recv(entry_rx) -> entry => { - // // let Ok((entry, root_path, device_id)) = entry else { - // // break; - // // }; - - // // 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 || 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, previous_depth) { - // // (n, p) if n > p => { - // // directory_info_per_depth_level.push(current_directory_at_depth); - // // current_directory_at_depth = EntryInfo { - // // size: file_size, - // // entries_count: Some(1), - // // }; - // // parent_node_idx = previous_node_idx; - // // } - // // (n, p) if n < p => { - // // for _ in n..p { - // // set_entry_info_or_panic( - // // &mut t.tree, - // // parent_node_idx, - // // current_directory_at_depth, - // // ); - // // let dir_info = - // // pop_or_panic(&mut directory_info_per_depth_level); - - // // current_directory_at_depth.size += dir_info.size; - // // current_directory_at_depth.add_count(&dir_info); - - // // parent_node_idx = parent_or_panic(&mut t.tree, parent_node_idx); - // // } - // // current_directory_at_depth.size += file_size; - // // *current_directory_at_depth.entries_count.get_or_insert(0) += 1; - // // set_entry_info_or_panic( - // // &mut t.tree, - // // parent_node_idx, - // // current_directory_at_depth, - // // ); - // // } - // // _ => { - // // current_directory_at_depth.size += file_size; - // // *current_directory_at_depth.entries_count.get_or_insert(0) += 1; - // // } - // // }; - - // // data.mtime = mtime; - // // data.size = file_size; - // // let entry_index = t.tree.add_node(data); - - // // t.tree.add_edge(parent_node_idx, entry_index, ()); - // // previous_node_idx = entry_index; - // // previous_depth = entry.depth; - // // } - // // Err(_) => { - // // if previous_depth == 0 { - // // data.name = (*root_path).clone(); - // // let entry_index = t.tree.add_node(data); - // // t.tree.add_edge(parent_node_idx, entry_index, ()); - // // } - - // // t.io_errors += 1 - // // } - // // } - - // // if throttle.can_update() && update(&mut t, None)? { - // // return Ok(None); - // // } - // // }, - // // recv(waker_rx) -> waker_value => { - // // let Ok(waker_value) = waker_value else { - // // continue; - // // }; - // // if update(&mut t, Some(waker_value))? { - // // return Ok(None); - // // } - // // }, - // // 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); - // // } - // // } - // // } - // // } - - // directory_info_per_depth_level.push(current_directory_at_depth); - // current_directory_at_depth = EntryInfo::default(); - // for _ in 0..previous_depth { - // let dir_info = pop_or_panic(&mut directory_info_per_depth_level); - // current_directory_at_depth.size += dir_info.size; - // current_directory_at_depth.add_count(&dir_info); - - // set_entry_info_or_panic(&mut t.tree, parent_node_idx, current_directory_at_depth); - // parent_node_idx = parent_or_panic(&mut t.tree, parent_node_idx); - // } - // 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)) - // } - pub fn recompute_root_size(&self) -> u128 { self.tree .neighbors_directed(self.root_index, Direction::Outgoing) From 5abb9d7e8d18799caa4a2f3823e06b77bdb27133 Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Sun, 7 Jan 2024 20:29:24 +0000 Subject: [PATCH 12/20] started fixing tests... --- .../app/tests/journeys_readonly.rs | 47 ++++++++++--------- src/interactive/app/tests/utils.rs | 35 +++++++------- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/src/interactive/app/tests/journeys_readonly.rs b/src/interactive/app/tests/journeys_readonly.rs index 28e0529c..669effd7 100644 --- a/src/interactive/app/tests/journeys_readonly.rs +++ b/src/interactive/app/tests/journeys_readonly.rs @@ -27,9 +27,12 @@ fn init_from_pdu_results() -> Result<()> { fn simple_user_journey_read_only() -> Result<()> { let long_root = "sample-02/dir"; let short_root = "sample-01"; - let (mut terminal, mut app) = + let (mut terminal, mut app, traversal_events) = initialized_app_and_terminal_from_fixture(&[short_root, long_root])?; + // TODO: process traversal events before continuing? + // let (key_send, key_receive) = crossbeam::channel::bounded(0); + // POST-INIT // after initialization, we expect that... { @@ -67,28 +70,28 @@ fn simple_user_journey_read_only() -> Result<()> { // SORTING { // when hitting the M key - app.process_events(&mut terminal, into_codes("m"))?; + app.process_events(&mut terminal, into_codes("m"), traversal_events)?; assert_eq!( app.state.sorting, SortMode::MTimeDescending, "it sets the sort mode to descending by mtime" ); // when hitting the M key again - app.process_events(&mut terminal, into_codes("m"))?; + app.process_events(&mut terminal, into_codes("m"), traversal_events)?; assert_eq!( app.state.sorting, SortMode::MTimeAscending, "it sets the sort mode to ascending by mtime" ); // when hitting the C key - app.process_events(&mut terminal, into_codes("c"))?; + app.process_events(&mut terminal, into_codes("c"), traversal_events)?; assert_eq!( app.state.sorting, SortMode::CountDescending, "it sets the sort mode to descending by count" ); // when hitting the C key again - app.process_events(&mut terminal, into_codes("c"))?; + app.process_events(&mut terminal, into_codes("c"), traversal_events)?; assert_eq!( app.state.sorting, SortMode::CountAscending, @@ -100,7 +103,7 @@ fn simple_user_journey_read_only() -> Result<()> { "it recomputes the cached items" ); // when hitting the S key - app.process_events(&mut terminal, into_codes("s"))?; + app.process_events(&mut terminal, into_codes("s"), traversal_events)?; assert_eq!( app.state.sorting, SortMode::SizeDescending, @@ -112,14 +115,14 @@ fn simple_user_journey_read_only() -> Result<()> { "it recomputes the cached items" ); // when hitting the S key again - app.process_events(&mut terminal, into_codes("s"))?; + app.process_events(&mut terminal, into_codes("s"), traversal_events)?; assert_eq!( app.state.sorting, SortMode::SizeAscending, "it sets the sort mode to ascending by size" ); // hit the S key again to get Descending - the rest depends on it - app.process_events(&mut terminal, into_codes("s"))?; + app.process_events(&mut terminal, into_codes("s"), traversal_events)?; assert_eq!(app.state.sorting, SortMode::SizeDescending,); assert_eq!( @@ -132,35 +135,35 @@ fn simple_user_journey_read_only() -> Result<()> { // Entry-Navigation { // when hitting the j key - app.process_events(&mut terminal, into_codes("j"))?; + app.process_events(&mut terminal, into_codes("j"), traversal_events)?; assert_eq!( node_by_name(&app, fixture_str(long_root)), node_by_index(&app, *app.state.navigation().selected.as_ref().unwrap()), "it moves the cursor down and selects the next item based on the current sort mode" ); // when hitting it while there is nowhere to go - app.process_events(&mut terminal, into_codes("j"))?; + app.process_events(&mut terminal, into_codes("j"), traversal_events)?; assert_eq!( node_by_name(&app, fixture_str(long_root)), node_by_index(&app, *app.state.navigation().selected.as_ref().unwrap()), "it stays at the previous position" ); // when hitting the k key - app.process_events(&mut terminal, into_codes("k"))?; + app.process_events(&mut terminal, into_codes("k"), traversal_events)?; assert_eq!( node_by_name(&app, fixture_str(short_root)), node_by_index(&app, *app.state.navigation().selected.as_ref().unwrap()), "it moves the cursor up and selects the next item based on the current sort mode" ); // when hitting the k key again - app.process_events(&mut terminal, into_codes("k"))?; + app.process_events(&mut terminal, into_codes("k"), traversal_events)?; assert_eq!( node_by_name(&app, fixture_str(short_root)), node_by_index(&app, *app.state.navigation().selected.as_ref().unwrap()), "it stays at the current cursor position as there is nowhere to go" ); // when hitting the o key with a directory selected - app.process_events(&mut terminal, into_codes("o"))?; + app.process_events(&mut terminal, into_codes("o"), traversal_events)?; { let new_root_idx = index_by_name(&app, fixture_str(short_root)); assert_eq!( @@ -175,7 +178,7 @@ fn simple_user_journey_read_only() -> Result<()> { ); // when hitting the u key while inside a sub-directory - app.process_events(&mut terminal, into_codes("u"))?; + app.process_events(&mut terminal, into_codes("u"), traversal_events)?; { assert_eq!( app.traversal.root_index, @@ -191,7 +194,7 @@ fn simple_user_journey_read_only() -> Result<()> { } // when hitting the u key while inside of the root directory // We are moving the cursor down just to have a non-default selection - app.process_events(&mut terminal, into_codes("ju"))?; + app.process_events(&mut terminal, into_codes("ju"), traversal_events)?; { assert_eq!( app.traversal.root_index, @@ -209,9 +212,9 @@ fn simple_user_journey_read_only() -> Result<()> { // Deletion { // when hitting the 'd' key (also move cursor back to start) - app.process_events(&mut terminal, into_codes("k"))?; + app.process_events(&mut terminal, into_codes("k"), traversal_events)?; let previously_selected_index = *app.state.navigation().selected.as_ref().unwrap(); - app.process_events(&mut terminal, into_codes("d"))?; + app.process_events(&mut terminal, into_codes("d"), traversal_events)?; { assert_eq!( Some(1), @@ -233,7 +236,7 @@ fn simple_user_journey_read_only() -> Result<()> { // when hitting the 'd' key again { - app.process_events(&mut terminal, into_codes("d"))?; + app.process_events(&mut terminal, into_codes("d"), traversal_events)?; assert_eq!( Some(2), @@ -250,7 +253,7 @@ fn simple_user_journey_read_only() -> Result<()> { // when hitting the 'd' key once again { - app.process_events(&mut terminal, into_codes("d"))?; + app.process_events(&mut terminal, into_codes("d"), traversal_events)?; assert_eq!( Some(1), @@ -267,7 +270,7 @@ fn simple_user_journey_read_only() -> Result<()> { } // when hitting the spacebar (after moving up to the first entry) { - app.process_events(&mut terminal, into_codes("k "))?; + app.process_events(&mut terminal, into_codes("k "), traversal_events)?; assert_eq!( None, @@ -286,7 +289,7 @@ fn simple_user_journey_read_only() -> Result<()> { // Marking { // select something - app.process_events(&mut terminal, into_codes(" j "))?; + app.process_events(&mut terminal, into_codes(" j "), traversal_events)?; assert_eq!( Some(false), app.window.mark_pane.as_ref().map(|p| p.has_focus()), @@ -300,7 +303,7 @@ fn simple_user_journey_read_only() -> Result<()> { ); // when advancing the selection to the marker pane - app.process_events(&mut terminal, into_keys(Some(KeyCode::Tab)))?; + app.process_events(&mut terminal, into_keys(Some(KeyCode::Tab)), traversal_events)?; { assert_eq!( Some(true), diff --git a/src/interactive/app/tests/utils.rs b/src/interactive/app/tests/utils.rs index 64d561c6..a9d5fcc6 100644 --- a/src/interactive/app/tests/utils.rs +++ b/src/interactive/app/tests/utils.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Error, Result}; +use crossbeam::channel::Receiver; use crosstermion::crossterm::event::KeyCode; use dua::{ traverse::{EntryData, Tree, TreeIndex}, @@ -18,7 +19,7 @@ use std::{ use tui::backend::TestBackend; use tui_react::Terminal; -use crate::interactive::{app::tests::FIXTURE_PATH, terminal_app::TerminalApp}; +use crate::interactive::{app::tests::FIXTURE_PATH, terminal_app::{TerminalApp, TraversalEvent}}; pub fn into_keys<'a>( codes: impl IntoIterator + 'a, @@ -171,25 +172,25 @@ pub fn fixture_str(p: impl AsRef) -> String { pub fn initialized_app_and_terminal_with_closure( fixture_paths: &[impl AsRef], mut convert: impl FnMut(&Path) -> PathBuf, -) -> Result<(Terminal, TerminalApp), Error> { +) -> Result<(Terminal, TerminalApp, Receiver), Error> { let mut terminal = new_test_terminal()?; std::env::set_current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))?; - // let (_, keys_rx) = crossbeam::channel::unbounded(); - // let input_paths = fixture_paths.iter().map(|c| convert(c.as_ref())).collect(); - let app = TerminalApp::initialize(&mut terminal, ByteFormat::Metric)?; + let walk_options = WalkOptions { + threads: 1, + apparent_size: true, + count_hard_links: false, + sorting: TraversalSorting::AlphabeticalByFileName, + cross_filesystems: false, + ignore_dirs: Default::default(), + }; - Ok((terminal, app)) + let mut app = TerminalApp::initialize(&mut terminal, walk_options, ByteFormat::Metric)?; - // WalkOptions { - // threads: 1, - // byte_format: , - // apparent_size: true, - // count_hard_links: false, - // sorting: TraversalSorting::AlphabeticalByFileName, - // cross_filesystems: false, - // ignore_dirs: Default::default(), - // } + let input_paths = fixture_paths.iter().map(|c| convert(c.as_ref())).collect(); + let traversal_rx = app.scan(input_paths)?; + + Ok((terminal, app, traversal_rx)) } pub fn new_test_terminal() -> std::io::Result> { @@ -198,7 +199,7 @@ pub fn new_test_terminal() -> std::io::Result> { pub fn initialized_app_and_terminal_from_paths( fixture_paths: &[PathBuf], -) -> Result<(Terminal, TerminalApp), Error> { +) -> Result<(Terminal, TerminalApp, Receiver), Error> { fn to_path_buf(p: &Path) -> PathBuf { p.to_path_buf() } @@ -207,7 +208,7 @@ pub fn initialized_app_and_terminal_from_paths( pub fn initialized_app_and_terminal_from_fixture( fixture_paths: &[&str], -) -> Result<(Terminal, TerminalApp), Error> { +) -> Result<(Terminal, TerminalApp, Receiver), Error> { #[allow(clippy::redundant_closure)] // doesn't actually work that way due to borrowchk - probably a bug initialized_app_and_terminal_with_closure(fixture_paths, |p| fixture(p)) From 8aaa05ada6169860cd083a24764bc2c5915b220b Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Sun, 7 Jan 2024 20:39:10 +0000 Subject: [PATCH 13/20] clippy --- src/aggregate.rs | 4 ---- src/interactive/app/app_state.rs | 1 - src/interactive/app/bytevis.rs | 2 +- src/interactive/app/eventloop.rs | 13 +++++-------- src/interactive/app/mod.rs | 1 - src/interactive/app/terminal_app.rs | 9 ++++----- src/main.rs | 2 +- src/traverse.rs | 7 +++---- 8 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/aggregate.rs b/src/aggregate.rs index 163a7eba..ec4d5aed 100644 --- a/src/aggregate.rs +++ b/src/aggregate.rs @@ -90,7 +90,6 @@ pub fn aggregate( } else { output_colored_path( &mut out, - &walk_options, &path, num_bytes, num_errors, @@ -111,7 +110,6 @@ pub fn aggregate( for (path, num_bytes, num_errors) in aggregates.into_iter() { output_colored_path( &mut out, - &walk_options, &path, num_bytes, num_errors, @@ -124,7 +122,6 @@ pub fn aggregate( if num_roots > 1 && compute_total { output_colored_path( &mut out, - &walk_options, Path::new("total"), total, res.num_errors, @@ -141,7 +138,6 @@ fn path_color_of(path: impl AsRef) -> Option { fn output_colored_path( out: &mut impl io::Write, - options: &WalkOptions, path: impl AsRef, num_bytes: u128, num_errors: u64, diff --git a/src/interactive/app/app_state.rs b/src/interactive/app/app_state.rs index 45784385..8d245db3 100644 --- a/src/interactive/app/app_state.rs +++ b/src/interactive/app/app_state.rs @@ -107,6 +107,5 @@ pub fn pop_or_panic(v: &mut Vec) -> EntryInfo { } pub enum ProcessingResult { - Finished(WalkResult), ExitRequested(WalkResult), } diff --git a/src/interactive/app/bytevis.rs b/src/interactive/app/bytevis.rs index 26a99e74..96f934ea 100644 --- a/src/interactive/app/bytevis.rs +++ b/src/interactive/app/bytevis.rs @@ -1,4 +1,4 @@ -use dua::{ByteFormat, WalkOptions}; +use dua::{ByteFormat}; use std::fmt; #[derive(Default, Clone, Copy)] diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index 56442368..bd6b7117 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -4,9 +4,8 @@ use crate::{ app::navigation::Navigation, app_state::FocussedPane, sorted_entries, - widgets::{glob_search, MainWindow, MainWindowProps}, - ByteVisualization, CursorDirection, CursorMode, DisplayOptions, EntryDataBundle, - MarkEntryMode, SortMode, + widgets::{glob_search, MainWindow, MainWindowProps}, CursorDirection, CursorMode, DisplayOptions, + MarkEntryMode, }, }; use anyhow::Result; @@ -14,11 +13,10 @@ use crossbeam::channel::Receiver; use crosstermion::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use crosstermion::input::Event; use dua::{ - traverse::{size_on_disk, EntryData, Traversal, Tree}, + traverse::{size_on_disk, EntryData, Traversal}, WalkOptions, WalkResult, }; use std::{ - path::PathBuf, time::{SystemTime, UNIX_EPOCH}, }; use tui::backend::Backend; @@ -31,7 +29,6 @@ use super::{ }; use super::{ app_state::{AppState, Cursor, ProcessingResult}, - input::input_channel, }; impl AppState { @@ -318,10 +315,10 @@ impl AppState { return true; } } - return false; + false } - fn update_state<'a>(&mut self, traversal: &'a Traversal) { + fn update_state(&mut self, traversal: &Traversal) { let received_events = self.traversal_state.received_event; if !received_events { self.navigation_mut().view_root = traversal.root_index; diff --git a/src/interactive/app/mod.rs b/src/interactive/app/mod.rs index 2dd74eb3..f4f6d3e3 100644 --- a/src/interactive/app/mod.rs +++ b/src/interactive/app/mod.rs @@ -10,7 +10,6 @@ pub mod tree_view; pub use bytevis::*; pub use common::*; -pub use eventloop::*; pub use handlers::*; #[cfg(test)] diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal_app.rs index f3e3369e..8728f140 100644 --- a/src/interactive/app/terminal_app.rs +++ b/src/interactive/app/terminal_app.rs @@ -13,8 +13,7 @@ use tui_react::Terminal; use crate::{crossdev, interactive::widgets::MainWindow}; use super::{ - app_state::{AppState, ProcessingResult, TraversalState}, - refresh_key, sorted_entries, ByteVisualization, DisplayOptions, + app_state::{AppState, ProcessingResult, TraversalState}, sorted_entries, DisplayOptions, }; /// State and methods representing the interactive disk usage analyser for the terminal @@ -77,7 +76,7 @@ impl TerminalApp { ); state.navigation_mut().selected = state.entries.first().map(|b| b.index); - let mut app = TerminalApp { + let app = TerminalApp { state, display, traversal, @@ -87,7 +86,7 @@ impl TerminalApp { Ok(app) } - pub fn scan<'a>(&mut self, input: Vec) -> Result> { + pub fn scan(&mut self, input: Vec) -> Result> { self.state.traversal_state = TraversalState::new(self.traversal.root_index); self.state.is_scanning = true; @@ -153,7 +152,7 @@ impl TerminalApp { events, traversal, )? { - ProcessingResult::Finished(res) | ProcessingResult::ExitRequested(res) => Ok(res), + ProcessingResult::ExitRequested(res) => Ok(res), } } } diff --git a/src/main.rs b/src/main.rs index a3b314cc..fcee61f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use std::{fs, io, io::Write, path::PathBuf, process}; use crate::interactive::input::input_channel; use crate::interactive::terminal_app::TerminalApp; -use crate::options::ByteFormat; + mod crossdev; #[cfg(feature = "tui-crossplatform")] diff --git a/src/traverse.rs b/src/traverse.rs index 98cf41c0..6ed7a605 100644 --- a/src/traverse.rs +++ b/src/traverse.rs @@ -1,5 +1,5 @@ -use crate::{crossdev, get_size_or_panic, InodeFilter, Throttle, WalkOptions}; -use anyhow::Result; +use crate::{get_size_or_panic}; + use filesize::PathExt; use petgraph::{graph::NodeIndex, stable_graph::StableGraph, Directed, Direction}; use std::{ @@ -7,8 +7,7 @@ use std::{ fs::Metadata, io, path::{Path, PathBuf}, - sync::Arc, - time::{Duration, SystemTime, UNIX_EPOCH}, + time::{SystemTime, UNIX_EPOCH}, }; pub type TreeIndex = NodeIndex; From b3236dcb3db927f3709e9355b218f42327a66a99 Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Sun, 7 Jan 2024 20:39:26 +0000 Subject: [PATCH 14/20] fmt --- src/interactive/app/bytevis.rs | 2 +- src/interactive/app/eventloop.rs | 14 +++++--------- src/interactive/app/terminal_app.rs | 3 ++- src/interactive/app/tests/journeys_readonly.rs | 6 +++++- src/interactive/app/tests/utils.rs | 5 ++++- src/main.rs | 1 - src/traverse.rs | 2 +- 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/interactive/app/bytevis.rs b/src/interactive/app/bytevis.rs index 96f934ea..19efa08f 100644 --- a/src/interactive/app/bytevis.rs +++ b/src/interactive/app/bytevis.rs @@ -1,4 +1,4 @@ -use dua::{ByteFormat}; +use dua::ByteFormat; use std::fmt; #[derive(Default, Clone, Copy)] diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index bd6b7117..efb4cd1f 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -4,8 +4,8 @@ use crate::{ app::navigation::Navigation, app_state::FocussedPane, sorted_entries, - widgets::{glob_search, MainWindow, MainWindowProps}, CursorDirection, CursorMode, DisplayOptions, - MarkEntryMode, + widgets::{glob_search, MainWindow, MainWindowProps}, + CursorDirection, CursorMode, DisplayOptions, MarkEntryMode, }, }; use anyhow::Result; @@ -16,20 +16,16 @@ use dua::{ traverse::{size_on_disk, EntryData, Traversal}, WalkOptions, WalkResult, }; -use std::{ - time::{SystemTime, UNIX_EPOCH}, -}; +use std::time::{SystemTime, UNIX_EPOCH}; use tui::backend::Backend; use tui_react::Terminal; +use super::app_state::{AppState, Cursor, ProcessingResult}; use super::{ app_state::{parent_or_panic, pop_or_panic, set_entry_info_or_panic, EntryInfo}, terminal_app::TraversalEvent, tree_view::TreeView, }; -use super::{ - app_state::{AppState, Cursor, ProcessingResult}, -}; impl AppState { pub fn navigation_mut(&mut self) -> &mut Navigation { @@ -355,7 +351,7 @@ impl AppState { self.traversal_state.received_event = true; } key - }, + } Event::Resize(_, _) => refresh_key(), _ => return Ok(None), }; diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal_app.rs index 8728f140..537cb6f4 100644 --- a/src/interactive/app/terminal_app.rs +++ b/src/interactive/app/terminal_app.rs @@ -13,7 +13,8 @@ use tui_react::Terminal; use crate::{crossdev, interactive::widgets::MainWindow}; use super::{ - app_state::{AppState, ProcessingResult, TraversalState}, sorted_entries, DisplayOptions, + app_state::{AppState, ProcessingResult, TraversalState}, + sorted_entries, DisplayOptions, }; /// State and methods representing the interactive disk usage analyser for the terminal diff --git a/src/interactive/app/tests/journeys_readonly.rs b/src/interactive/app/tests/journeys_readonly.rs index 669effd7..72daf03c 100644 --- a/src/interactive/app/tests/journeys_readonly.rs +++ b/src/interactive/app/tests/journeys_readonly.rs @@ -303,7 +303,11 @@ fn simple_user_journey_read_only() -> Result<()> { ); // when advancing the selection to the marker pane - app.process_events(&mut terminal, into_keys(Some(KeyCode::Tab)), traversal_events)?; + app.process_events( + &mut terminal, + into_keys(Some(KeyCode::Tab)), + traversal_events, + )?; { assert_eq!( Some(true), diff --git a/src/interactive/app/tests/utils.rs b/src/interactive/app/tests/utils.rs index a9d5fcc6..6be28ab9 100644 --- a/src/interactive/app/tests/utils.rs +++ b/src/interactive/app/tests/utils.rs @@ -19,7 +19,10 @@ use std::{ use tui::backend::TestBackend; use tui_react::Terminal; -use crate::interactive::{app::tests::FIXTURE_PATH, terminal_app::{TerminalApp, TraversalEvent}}; +use crate::interactive::{ + app::tests::FIXTURE_PATH, + terminal_app::{TerminalApp, TraversalEvent}, +}; pub fn into_keys<'a>( codes: impl IntoIterator + 'a, diff --git a/src/main.rs b/src/main.rs index fcee61f6..52abdc61 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,6 @@ use std::{fs, io, io::Write, path::PathBuf, process}; use crate::interactive::input::input_channel; use crate::interactive::terminal_app::TerminalApp; - mod crossdev; #[cfg(feature = "tui-crossplatform")] mod interactive; diff --git a/src/traverse.rs b/src/traverse.rs index 6ed7a605..e1e4b9ac 100644 --- a/src/traverse.rs +++ b/src/traverse.rs @@ -1,4 +1,4 @@ -use crate::{get_size_or_panic}; +use crate::get_size_or_panic; use filesize::PathExt; use petgraph::{graph::NodeIndex, stable_graph::StableGraph, Directed, Direction}; From 9eaa96144bc72de6515c30fc32961a2807b247c7 Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Mon, 8 Jan 2024 22:00:44 +0000 Subject: [PATCH 15/20] New Traversal --- src/interactive/app/app_state.rs | 72 +------ src/interactive/app/eventloop.rs | 282 ++++++-------------------- src/interactive/app/terminal_app.rs | 63 +----- src/interactive/app/tests/utils.rs | 7 +- src/main.rs | 6 +- src/traverse.rs | 303 +++++++++++++++++++++++++++- 6 files changed, 373 insertions(+), 360 deletions(-) diff --git a/src/interactive/app/app_state.rs b/src/interactive/app/app_state.rs index 8d245db3..14164d27 100644 --- a/src/interactive/app/app_state.rs +++ b/src/interactive/app/app_state.rs @@ -2,7 +2,7 @@ use std::time::Duration; use dua::{ inodefilter::InodeFilter, - traverse::{Tree, TreeIndex}, + traverse::{RunningTraversal, Tree, TreeIndex}, Throttle, WalkResult, }; use petgraph::Direction; @@ -34,76 +34,8 @@ pub struct AppState { pub message: Option, 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, - pub current_directory_at_depth: EntryInfo, - pub previous_depth: usize, - pub inodes: InodeFilter, - pub throttle: Option, pub received_event: bool, -} - -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(), - throttle: Some(Throttle::new(Duration::from_millis(250), None)), - received_event: false, - } - } -} - -#[derive(Default, Copy, Clone)] -pub struct EntryInfo { - pub size: u128, - pub entries_count: Option, -} - -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 { - v.pop().expect("sizes per level to be in sync with graph") + pub running_traversal: Option, } pub enum ProcessingResult { diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index efb4cd1f..594164d8 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -9,23 +9,22 @@ use crate::{ }, }; use anyhow::Result; -use crossbeam::channel::Receiver; +use crossbeam::channel::{Receiver, Select}; use crosstermion::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use crosstermion::input::Event; use dua::{ - traverse::{size_on_disk, EntryData, Traversal}, + traverse::{size_on_disk, EntryData, ProcessEventResult, RunningTraversal, Traversal}, WalkOptions, WalkResult, }; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, +}; use tui::backend::Backend; use tui_react::Terminal; use super::app_state::{AppState, Cursor, ProcessingResult}; -use super::{ - app_state::{parent_or_panic, pop_or_panic, set_entry_info_or_panic, EntryInfo}, - terminal_app::TraversalEvent, - tree_view::TreeView, -}; +use super::tree_view::TreeView; impl AppState { pub fn navigation_mut(&mut self) -> &mut Navigation { @@ -71,251 +70,92 @@ impl AppState { result } + pub fn traverse( + &mut self, + traversal: &Traversal, + walk_options: &WalkOptions, + input: Vec, + ) -> Result<()> { + let running_traversal = RunningTraversal::new(traversal.root_index, walk_options, input)?; + self.running_traversal = Some(running_traversal); + Ok(()) + } + + fn refresh_screen( + &mut self, + window: &mut MainWindow, + traversal: &mut Traversal, + display: &mut DisplayOptions, + terminal: &mut Terminal, + ) -> Result<()> + where + B: Backend, + { + let tree_view = self.tree_view(traversal); + self.draw(window, &tree_view, *display, terminal)?; + Ok(()) + } + pub fn process_events( &mut self, window: &mut MainWindow, traversal: &mut Traversal, display: &mut DisplayOptions, terminal: &mut Terminal, - walk_options: &WalkOptions, events: Receiver, - traversal_events: Receiver, ) -> Result where B: Backend, { - { - let tree_view = self.tree_view(traversal); - self.draw(window, &tree_view, *display, terminal)?; - } + self.refresh_screen(window, traversal, display, terminal)?; 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; - }; - if self.process_traversal_event(traversal, walk_options, event) { - self.update_state(traversal); + if let Some(running_traversal) = &mut self.running_traversal { + crossbeam::select! { + recv(events) -> event => { + let Ok(event) = event else { + continue; + }; let result = self.process_event( window, traversal, display, terminal, - Event::Key(refresh_key()))?; + event)?; if let Some(processing_result) = result { return Ok(processing_result); } - } - } - } - } - } - - fn process_traversal_event<'a>( - &mut self, - t: &'a mut Traversal, - walk_options: &'a WalkOptions, - event: TraversalEvent, - ) -> bool { - 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() + }, + recv(&running_traversal.event_rx) -> event => { + let Ok(event) = event else { + continue; }; - 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; + let result = running_traversal.process_event(traversal, event); + if result != ProcessEventResult::NoOp { + if result == ProcessEventResult::Finished { + self.is_scanning = false; + self.running_traversal = None; } - None => {} + self.update_state(traversal); + self.refresh_screen(window, traversal, display, terminal)?; } - - 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; - } - }; - - data.mtime = mtime; - data.size = file_size; - let entry_index = t.tree.add_node(data); - - 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 - } - } - - if let Some(throttle) = &self.traversal_state.throttle { - if throttle.can_update() { - return true; } } - } - TraversalEvent::Finished(io_errors) => { - t.io_errors += io_errors; - - self.traversal_state.throttle = None; - 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); + } else { + let Ok(event) = events.recv() else { + continue; + }; + let result = self.process_event(window, traversal, display, terminal, event)?; + if let Some(processing_result) = result { + return Ok(processing_result); } - 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()); - - self.is_scanning = false; - - return true; } } - false } fn update_state(&mut self, traversal: &Traversal) { - let received_events = self.traversal_state.received_event; + let received_events = self.received_event; if !received_events { self.navigation_mut().view_root = traversal.root_index; } @@ -348,7 +188,7 @@ impl AppState { let key = match event { Event::Key(key) if key.kind != KeyEventKind::Release => { if key.code != KeyCode::Char('\r') { - self.traversal_state.received_event = true; + self.received_event = true; } key } diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal_app.rs index 537cb6f4..5d57cf16 100644 --- a/src/interactive/app/terminal_app.rs +++ b/src/interactive/app/terminal_app.rs @@ -13,7 +13,7 @@ use tui_react::Terminal; use crate::{crossdev, interactive::widgets::MainWindow}; use super::{ - app_state::{AppState, ProcessingResult, TraversalState}, + app_state::{AppState, ProcessingResult}, sorted_entries, DisplayOptions, }; @@ -26,14 +26,6 @@ pub struct TerminalApp { pub walk_options: WalkOptions, } -pub type TraversalEntry = - Result>)>, jwalk::Error>; - -pub enum TraversalEvent { - Entry(TraversalEntry, Arc, u64), - Finished(u64), -} - impl TerminalApp { pub fn initialize( terminal: &mut Terminal, @@ -87,59 +79,16 @@ impl TerminalApp { Ok(app) } - pub fn scan(&mut self, input: Vec) -> Result> { - self.state.traversal_state = TraversalState::new(self.traversal.root_index); - self.state.is_scanning = true; - - 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"); - } - } - })?; - - Ok(entry_rx) + pub fn traverse(&mut self, input: Vec) -> Result<()> { + self.state + .traverse(&self.traversal, &self.walk_options, input)?; + Ok(()) } pub fn process_events( &mut self, terminal: &mut Terminal, events: Receiver, - traversal: Receiver, ) -> Result where B: Backend, @@ -149,9 +98,7 @@ impl TerminalApp { &mut self.traversal, &mut self.display, terminal, - &self.walk_options, events, - traversal, )? { ProcessingResult::ExitRequested(res) => Ok(res), } diff --git a/src/interactive/app/tests/utils.rs b/src/interactive/app/tests/utils.rs index 6be28ab9..133dd9a8 100644 --- a/src/interactive/app/tests/utils.rs +++ b/src/interactive/app/tests/utils.rs @@ -19,10 +19,7 @@ use std::{ use tui::backend::TestBackend; use tui_react::Terminal; -use crate::interactive::{ - app::tests::FIXTURE_PATH, - terminal_app::{TerminalApp, TraversalEvent}, -}; +use crate::interactive::{app::tests::FIXTURE_PATH, terminal_app::TerminalApp}; pub fn into_keys<'a>( codes: impl IntoIterator + 'a, @@ -191,7 +188,7 @@ pub fn initialized_app_and_terminal_with_closure( let mut app = TerminalApp::initialize(&mut terminal, walk_options, ByteFormat::Metric)?; let input_paths = fixture_paths.iter().map(|c| convert(c.as_ref())).collect(); - let traversal_rx = app.scan(input_paths)?; + let traversal_rx = app.traverse(input_paths)?; Ok((terminal, app, traversal_rx)) } diff --git a/src/main.rs b/src/main.rs index 52abdc61..7e28c130 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,11 +76,9 @@ fn main() -> Result<()> { let keys_rx = input_channel(); let mut app = TerminalApp::initialize(&mut terminal, walk_options, byte_format)?; + app.traverse(extract_paths_maybe_set_cwd(input, !opt.stay_on_filesystem)?)?; - let traversal_rx = - app.scan(extract_paths_maybe_set_cwd(input, !opt.stay_on_filesystem)?)?; - - let res = app.process_events(&mut terminal, keys_rx, traversal_rx); + let res = app.process_events(&mut terminal, keys_rx); let res = res.map(|r| { ( diff --git a/src/traverse.rs b/src/traverse.rs index e1e4b9ac..e83751cb 100644 --- a/src/traverse.rs +++ b/src/traverse.rs @@ -1,5 +1,6 @@ -use crate::get_size_or_panic; +use crate::{crossdev, get_size_or_panic, inodefilter::InodeFilter, Throttle, WalkOptions}; +use crossbeam::channel::Receiver; use filesize::PathExt; use petgraph::{graph::NodeIndex, stable_graph::StableGraph, Directed, Direction}; use std::{ @@ -7,7 +8,8 @@ use std::{ fs::Metadata, io, path::{Path, PathBuf}, - time::{SystemTime, UNIX_EPOCH}, + sync::Arc, + time::{Duration, SystemTime, UNIX_EPOCH}, }; pub type TreeIndex = NodeIndex; @@ -78,6 +80,303 @@ impl Traversal { } } +#[derive(Default, Copy, Clone)] +pub struct EntryInfo { + pub size: u128, + pub entries_count: Option, +} + +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 { + v.pop().expect("sizes per level to be in sync with graph") +} + +pub type TraversalEntry = + Result>)>, jwalk::Error>; + +pub enum TraversalEvent { + Entry(TraversalEntry, Arc, u64), + Finished(u64), +} + +pub struct RunningTraversal { + walk_options: WalkOptions, + previous_node_idx: TreeIndex, + parent_node_idx: TreeIndex, + directory_info_per_depth_level: Vec, + current_directory_at_depth: EntryInfo, + previous_depth: usize, + inodes: InodeFilter, + throttle: Option, + pub event_rx: Receiver, +} + +#[derive(PartialEq)] +pub enum ProcessEventResult { + NoOp, + UpdateIsReady, + Finished, +} + +impl RunningTraversal { + pub fn new( + root_idx: TreeIndex, + walk_options: &WalkOptions, + input: Vec, + ) -> anyhow::Result { + let (entry_tx, entry_rx) = crossbeam::channel::bounded(100); + std::thread::Builder::new() + .name("dua-fs-walk-dispatcher".to_string()) + .spawn({ + let walk_options = 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"); + } + } + })?; + + Ok(Self { + walk_options: walk_options.clone(), + 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(), + throttle: Some(Throttle::new(Duration::from_millis(250), None)), + event_rx: entry_rx, + }) + } + + pub fn process_event<'a>( + &mut self, + t: &'a mut Traversal, + event: TraversalEvent, + ) -> ProcessEventResult { + 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() + && (self.walk_options.count_hard_links || self.inodes.add(m)) + && (self.walk_options.cross_filesystems + || crossdev::is_same_device(device_id, m)) + { + if self.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.previous_depth) { + (n, p) if n > p => { + self.directory_info_per_depth_level + .push(self.current_directory_at_depth); + self.current_directory_at_depth = EntryInfo { + size: file_size, + entries_count: Some(1), + }; + self.parent_node_idx = self.previous_node_idx; + } + (n, p) if n < p => { + for _ in n..p { + set_entry_info_or_panic( + &mut t.tree, + self.parent_node_idx, + self.current_directory_at_depth, + ); + let dir_info = + pop_or_panic(&mut self.directory_info_per_depth_level); + + self.current_directory_at_depth.size += dir_info.size; + self.current_directory_at_depth.add_count(&dir_info); + + self.parent_node_idx = + parent_or_panic(&mut t.tree, self.parent_node_idx); + } + self.current_directory_at_depth.size += file_size; + *self + .current_directory_at_depth + .entries_count + .get_or_insert(0) += 1; + set_entry_info_or_panic( + &mut t.tree, + self.parent_node_idx, + self.current_directory_at_depth, + ); + } + _ => { + self.current_directory_at_depth.size += file_size; + *self + .current_directory_at_depth + .entries_count + .get_or_insert(0) += 1; + } + }; + + data.mtime = mtime; + data.size = file_size; + let entry_index = t.tree.add_node(data); + + t.tree.add_edge(self.parent_node_idx, entry_index, ()); + self.previous_node_idx = entry_index; + self.previous_depth = entry.depth; + } + Err(_) => { + if self.previous_depth == 0 { + data.name = (*root_path).clone(); + let entry_index = t.tree.add_node(data); + t.tree.add_edge(self.parent_node_idx, entry_index, ()); + } + + t.io_errors += 1 + } + } + + if let Some(throttle) = &self.throttle { + if throttle.can_update() { + return ProcessEventResult::UpdateIsReady; + } + } + } + TraversalEvent::Finished(io_errors) => { + t.io_errors += io_errors; + + self.throttle = None; + self.directory_info_per_depth_level + .push(self.current_directory_at_depth); + self.current_directory_at_depth = EntryInfo::default(); + for _ in 0..self.previous_depth { + let dir_info = pop_or_panic(&mut self.directory_info_per_depth_level); + self.current_directory_at_depth.size += dir_info.size; + self.current_directory_at_depth.add_count(&dir_info); + + set_entry_info_or_panic( + &mut t.tree, + self.parent_node_idx, + self.current_directory_at_depth, + ); + self.parent_node_idx = parent_or_panic(&mut t.tree, self.parent_node_idx); + } + 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()); + + return ProcessEventResult::Finished; + } + } + ProcessEventResult::NoOp + } +} + #[cfg(not(windows))] pub fn size_on_disk(_parent: &Path, name: &Path, meta: &Metadata) -> io::Result { name.size_on_disk_fast(meta) From f74a40a7212bde94bae9ff0ee1947a5b1478fb93 Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Mon, 8 Jan 2024 22:01:29 +0000 Subject: [PATCH 16/20] clippy --- src/interactive/app/app_state.rs | 8 +++----- src/interactive/app/eventloop.rs | 6 ++---- src/interactive/app/terminal_app.rs | 4 ++-- src/traverse.rs | 4 ++-- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/interactive/app/app_state.rs b/src/interactive/app/app_state.rs index 14164d27..e0404e34 100644 --- a/src/interactive/app/app_state.rs +++ b/src/interactive/app/app_state.rs @@ -1,11 +1,9 @@ -use std::time::Duration; + use dua::{ - inodefilter::InodeFilter, - traverse::{RunningTraversal, Tree, TreeIndex}, - Throttle, WalkResult, + traverse::{RunningTraversal}, WalkResult, }; -use petgraph::Direction; + use super::{navigation::Navigation, EntryDataBundle, SortMode}; diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index 594164d8..1b68f266 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -1,5 +1,4 @@ use crate::{ - crossdev, interactive::{ app::navigation::Navigation, app_state::FocussedPane, @@ -9,16 +8,15 @@ use crate::{ }, }; use anyhow::Result; -use crossbeam::channel::{Receiver, Select}; +use crossbeam::channel::{Receiver}; use crosstermion::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use crosstermion::input::Event; use dua::{ - traverse::{size_on_disk, EntryData, ProcessEventResult, RunningTraversal, Traversal}, + traverse::{EntryData, ProcessEventResult, RunningTraversal, Traversal}, WalkOptions, WalkResult, }; use std::{ path::PathBuf, - time::{SystemTime, UNIX_EPOCH}, }; use tui::backend::Backend; use tui_react::Terminal; diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal_app.rs index 5d57cf16..4a395665 100644 --- a/src/interactive/app/terminal_app.rs +++ b/src/interactive/app/terminal_app.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, sync::Arc}; +use std::{path::PathBuf}; use anyhow::Result; use crossbeam::channel::Receiver; @@ -10,7 +10,7 @@ use dua::{ use tui::prelude::Backend; use tui_react::Terminal; -use crate::{crossdev, interactive::widgets::MainWindow}; +use crate::{interactive::widgets::MainWindow}; use super::{ app_state::{AppState, ProcessingResult}, diff --git a/src/traverse.rs b/src/traverse.rs index e83751cb..0aba859f 100644 --- a/src/traverse.rs +++ b/src/traverse.rs @@ -209,9 +209,9 @@ impl RunningTraversal { }) } - pub fn process_event<'a>( + pub fn process_event( &mut self, - t: &'a mut Traversal, + t: &mut Traversal, event: TraversalEvent, ) -> ProcessEventResult { match event { From 6c63bf5a33ebb6b98516ca9a96796facfdab2277 Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Mon, 8 Jan 2024 22:01:43 +0000 Subject: [PATCH 17/20] fmt --- src/interactive/app/app_state.rs | 7 +------ src/interactive/app/eventloop.rs | 20 ++++++++------------ src/interactive/app/terminal_app.rs | 4 ++-- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/interactive/app/app_state.rs b/src/interactive/app/app_state.rs index e0404e34..3d32f806 100644 --- a/src/interactive/app/app_state.rs +++ b/src/interactive/app/app_state.rs @@ -1,9 +1,4 @@ - - -use dua::{ - traverse::{RunningTraversal}, WalkResult, -}; - +use dua::{traverse::RunningTraversal, WalkResult}; use super::{navigation::Navigation, EntryDataBundle, SortMode}; diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index 1b68f266..1a8539dd 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -1,23 +1,19 @@ -use crate::{ - interactive::{ - app::navigation::Navigation, - app_state::FocussedPane, - sorted_entries, - widgets::{glob_search, MainWindow, MainWindowProps}, - CursorDirection, CursorMode, DisplayOptions, MarkEntryMode, - }, +use crate::interactive::{ + app::navigation::Navigation, + app_state::FocussedPane, + sorted_entries, + widgets::{glob_search, MainWindow, MainWindowProps}, + CursorDirection, CursorMode, DisplayOptions, MarkEntryMode, }; use anyhow::Result; -use crossbeam::channel::{Receiver}; +use crossbeam::channel::Receiver; use crosstermion::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use crosstermion::input::Event; use dua::{ traverse::{EntryData, ProcessEventResult, RunningTraversal, Traversal}, WalkOptions, WalkResult, }; -use std::{ - path::PathBuf, -}; +use std::path::PathBuf; use tui::backend::Backend; use tui_react::Terminal; diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal_app.rs index 4a395665..757ac768 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; use anyhow::Result; use crossbeam::channel::Receiver; @@ -10,7 +10,7 @@ use dua::{ use tui::prelude::Backend; use tui_react::Terminal; -use crate::{interactive::widgets::MainWindow}; +use crate::interactive::widgets::MainWindow; use super::{ app_state::{AppState, ProcessingResult}, From d903ea67a4f77c9483aed7bda1ef6694ee4465da Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Mon, 8 Jan 2024 23:17:53 +0000 Subject: [PATCH 18/20] Fixed tests --- src/interactive/app/eventloop.rs | 103 +++++++++++------- src/interactive/app/terminal_app.rs | 25 +++++ .../app/tests/journeys_readonly.rs | 48 ++++---- .../app/tests/journeys_with_writes.rs | 6 +- src/interactive/app/tests/utils.rs | 34 ++++-- src/traverse.rs | 14 +-- 6 files changed, 144 insertions(+), 86 deletions(-) diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index 1a8539dd..2de51b63 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -10,7 +10,7 @@ use crossbeam::channel::Receiver; use crosstermion::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use crosstermion::input::Event; use dua::{ - traverse::{EntryData, ProcessEventResult, RunningTraversal, Traversal}, + traverse::{EntryData, TraversalProcessingEvent, RunningTraversal, Traversal}, WalkOptions, WalkResult, }; use std::path::PathBuf; @@ -70,7 +70,7 @@ impl AppState { walk_options: &WalkOptions, input: Vec, ) -> Result<()> { - let running_traversal = RunningTraversal::new(traversal.root_index, walk_options, input)?; + let running_traversal = RunningTraversal::start(traversal.root_index, walk_options, input)?; self.running_traversal = Some(running_traversal); Ok(()) } @@ -104,48 +104,71 @@ impl AppState { self.refresh_screen(window, traversal, display, terminal)?; loop { - if let Some(running_traversal) = &mut self.running_traversal { - 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(&running_traversal.event_rx) -> event => { - let Ok(event) = event else { - continue; - }; - - let result = running_traversal.process_event(traversal, event); - if result != ProcessEventResult::NoOp { - if result == ProcessEventResult::Finished { - self.is_scanning = false; - self.running_traversal = None; - } - self.update_state(traversal); - self.refresh_screen(window, traversal, display, terminal)?; + if let Some(result) = self.process_event( + window, + traversal, + display, + terminal, + &events, + )? { + return Ok(result); + } + } + } + + pub fn process_event( + &mut self, + window: &mut MainWindow, + traversal: &mut Traversal, + display: &mut DisplayOptions, + terminal: &mut Terminal, + events: &Receiver, + ) -> Result> + where + B: Backend, + { + if let Some(running_traversal) = &mut self.running_traversal { + crossbeam::select! { + recv(events) -> event => { + let Ok(event) = event else { + return Ok(Some(ProcessingResult::ExitRequested(WalkResult { num_errors: 0 }))); + }; + let result = self.process_terminal_event( + window, + traversal, + display, + terminal, + event)?; + if let Some(processing_result) = result { + return Ok(Some(processing_result)); + } + }, + recv(&running_traversal.event_rx) -> event => { + let Ok(event) = event else { + return Ok(None); + }; + + let result = running_traversal.process_event(traversal, event); + if result != TraversalProcessingEvent::None { + if result == TraversalProcessingEvent::Finished { + self.is_scanning = false; + self.running_traversal = None; } + self.update_state(traversal); + self.refresh_screen(window, traversal, display, terminal)?; } } - } else { - let Ok(event) = events.recv() else { - continue; - }; - let result = self.process_event(window, traversal, display, terminal, event)?; - if let Some(processing_result) = result { - return Ok(processing_result); - } + } + } else { + let Ok(event) = events.recv() else { + return Ok(Some(ProcessingResult::ExitRequested(WalkResult { num_errors: 0 }))); + }; + let result = self.process_terminal_event(window, traversal, display, terminal, event)?; + if let Some(processing_result) = result { + return Ok(Some(processing_result)); } } + Ok(None) } fn update_state(&mut self, traversal: &Traversal) { @@ -165,7 +188,7 @@ impl AppState { self.reset_message(); // force "scanning" to appear } - fn process_event( + fn process_terminal_event( &mut self, window: &mut MainWindow, traversal: &mut Traversal, diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal_app.rs index 757ac768..82d7199b 100644 --- a/src/interactive/app/terminal_app.rs +++ b/src/interactive/app/terminal_app.rs @@ -103,4 +103,29 @@ impl TerminalApp { ProcessingResult::ExitRequested(res) => Ok(res), } } + + pub fn run_until_traversed( + &mut self, + terminal: &mut Terminal, + events: Receiver, + ) -> Result + where + B: Backend + { + while self.state.running_traversal.is_some() { + match self.state.process_event( + &mut self.window, + &mut self.traversal, + &mut self.display, + terminal, + &events, + )? { + Some(ProcessingResult::ExitRequested(res)) => { + return Ok(res); + } + _ => {} + } + } + Ok(WalkResult { num_errors: 0 }) + } } diff --git a/src/interactive/app/tests/journeys_readonly.rs b/src/interactive/app/tests/journeys_readonly.rs index 72daf03c..48211453 100644 --- a/src/interactive/app/tests/journeys_readonly.rs +++ b/src/interactive/app/tests/journeys_readonly.rs @@ -27,12 +27,9 @@ fn init_from_pdu_results() -> Result<()> { fn simple_user_journey_read_only() -> Result<()> { let long_root = "sample-02/dir"; let short_root = "sample-01"; - let (mut terminal, mut app, traversal_events) = + let (mut terminal, mut app) = initialized_app_and_terminal_from_fixture(&[short_root, long_root])?; - - // TODO: process traversal events before continuing? - // let (key_send, key_receive) = crossbeam::channel::bounded(0); - + // POST-INIT // after initialization, we expect that... { @@ -70,28 +67,28 @@ fn simple_user_journey_read_only() -> Result<()> { // SORTING { // when hitting the M key - app.process_events(&mut terminal, into_codes("m"), traversal_events)?; + app.process_events(&mut terminal, into_codes("m"))?; assert_eq!( app.state.sorting, SortMode::MTimeDescending, "it sets the sort mode to descending by mtime" ); // when hitting the M key again - app.process_events(&mut terminal, into_codes("m"), traversal_events)?; + app.process_events(&mut terminal, into_codes("m"))?; assert_eq!( app.state.sorting, SortMode::MTimeAscending, "it sets the sort mode to ascending by mtime" ); // when hitting the C key - app.process_events(&mut terminal, into_codes("c"), traversal_events)?; + app.process_events(&mut terminal, into_codes("c"))?; assert_eq!( app.state.sorting, SortMode::CountDescending, "it sets the sort mode to descending by count" ); // when hitting the C key again - app.process_events(&mut terminal, into_codes("c"), traversal_events)?; + app.process_events(&mut terminal, into_codes("c"))?; assert_eq!( app.state.sorting, SortMode::CountAscending, @@ -103,7 +100,7 @@ fn simple_user_journey_read_only() -> Result<()> { "it recomputes the cached items" ); // when hitting the S key - app.process_events(&mut terminal, into_codes("s"), traversal_events)?; + app.process_events(&mut terminal, into_codes("s"))?; assert_eq!( app.state.sorting, SortMode::SizeDescending, @@ -115,14 +112,14 @@ fn simple_user_journey_read_only() -> Result<()> { "it recomputes the cached items" ); // when hitting the S key again - app.process_events(&mut terminal, into_codes("s"), traversal_events)?; + app.process_events(&mut terminal, into_codes("s"))?; assert_eq!( app.state.sorting, SortMode::SizeAscending, "it sets the sort mode to ascending by size" ); // hit the S key again to get Descending - the rest depends on it - app.process_events(&mut terminal, into_codes("s"), traversal_events)?; + app.process_events(&mut terminal, into_codes("s"))?; assert_eq!(app.state.sorting, SortMode::SizeDescending,); assert_eq!( @@ -135,35 +132,35 @@ fn simple_user_journey_read_only() -> Result<()> { // Entry-Navigation { // when hitting the j key - app.process_events(&mut terminal, into_codes("j"), traversal_events)?; + app.process_events(&mut terminal, into_codes("j"))?; assert_eq!( node_by_name(&app, fixture_str(long_root)), node_by_index(&app, *app.state.navigation().selected.as_ref().unwrap()), "it moves the cursor down and selects the next item based on the current sort mode" ); // when hitting it while there is nowhere to go - app.process_events(&mut terminal, into_codes("j"), traversal_events)?; + app.process_events(&mut terminal, into_codes("j"))?; assert_eq!( node_by_name(&app, fixture_str(long_root)), node_by_index(&app, *app.state.navigation().selected.as_ref().unwrap()), "it stays at the previous position" ); // when hitting the k key - app.process_events(&mut terminal, into_codes("k"), traversal_events)?; + app.process_events(&mut terminal, into_codes("k"))?; assert_eq!( node_by_name(&app, fixture_str(short_root)), node_by_index(&app, *app.state.navigation().selected.as_ref().unwrap()), "it moves the cursor up and selects the next item based on the current sort mode" ); // when hitting the k key again - app.process_events(&mut terminal, into_codes("k"), traversal_events)?; + app.process_events(&mut terminal, into_codes("k"))?; assert_eq!( node_by_name(&app, fixture_str(short_root)), node_by_index(&app, *app.state.navigation().selected.as_ref().unwrap()), "it stays at the current cursor position as there is nowhere to go" ); // when hitting the o key with a directory selected - app.process_events(&mut terminal, into_codes("o"), traversal_events)?; + app.process_events(&mut terminal, into_codes("o"))?; { let new_root_idx = index_by_name(&app, fixture_str(short_root)); assert_eq!( @@ -178,7 +175,7 @@ fn simple_user_journey_read_only() -> Result<()> { ); // when hitting the u key while inside a sub-directory - app.process_events(&mut terminal, into_codes("u"), traversal_events)?; + app.process_events(&mut terminal, into_codes("u"))?; { assert_eq!( app.traversal.root_index, @@ -194,7 +191,7 @@ fn simple_user_journey_read_only() -> Result<()> { } // when hitting the u key while inside of the root directory // We are moving the cursor down just to have a non-default selection - app.process_events(&mut terminal, into_codes("ju"), traversal_events)?; + app.process_events(&mut terminal, into_codes("ju"))?; { assert_eq!( app.traversal.root_index, @@ -212,9 +209,9 @@ fn simple_user_journey_read_only() -> Result<()> { // Deletion { // when hitting the 'd' key (also move cursor back to start) - app.process_events(&mut terminal, into_codes("k"), traversal_events)?; + app.process_events(&mut terminal, into_codes("k"))?; let previously_selected_index = *app.state.navigation().selected.as_ref().unwrap(); - app.process_events(&mut terminal, into_codes("d"), traversal_events)?; + app.process_events(&mut terminal, into_codes("d"))?; { assert_eq!( Some(1), @@ -236,7 +233,7 @@ fn simple_user_journey_read_only() -> Result<()> { // when hitting the 'd' key again { - app.process_events(&mut terminal, into_codes("d"), traversal_events)?; + app.process_events(&mut terminal, into_codes("d"))?; assert_eq!( Some(2), @@ -253,7 +250,7 @@ fn simple_user_journey_read_only() -> Result<()> { // when hitting the 'd' key once again { - app.process_events(&mut terminal, into_codes("d"), traversal_events)?; + app.process_events(&mut terminal, into_codes("d"))?; assert_eq!( Some(1), @@ -270,7 +267,7 @@ fn simple_user_journey_read_only() -> Result<()> { } // when hitting the spacebar (after moving up to the first entry) { - app.process_events(&mut terminal, into_codes("k "), traversal_events)?; + app.process_events(&mut terminal, into_codes("k "))?; assert_eq!( None, @@ -289,7 +286,7 @@ fn simple_user_journey_read_only() -> Result<()> { // Marking { // select something - app.process_events(&mut terminal, into_codes(" j "), traversal_events)?; + app.process_events(&mut terminal, into_codes(" j "))?; assert_eq!( Some(false), app.window.mark_pane.as_ref().map(|p| p.has_focus()), @@ -306,7 +303,6 @@ fn simple_user_journey_read_only() -> Result<()> { app.process_events( &mut terminal, into_keys(Some(KeyCode::Tab)), - traversal_events, )?; { assert_eq!( diff --git a/src/interactive/app/tests/journeys_with_writes.rs b/src/interactive/app/tests/journeys_with_writes.rs index 80e65d79..09ca1be3 100644 --- a/src/interactive/app/tests/journeys_with_writes.rs +++ b/src/interactive/app/tests/journeys_with_writes.rs @@ -9,6 +9,8 @@ use pretty_assertions::assert_eq; #[test] #[cfg(not(target_os = "windows"))] // it stopped working here, don't know if it's truly broken or if it's the test. Let's wait for windows users to report. fn basic_user_journey_with_deletion() -> Result<()> { + use crate::interactive::app::tests::utils::{into_keys, into_events}; + let fixture = WritableFixture::from("sample-02"); let (mut terminal, mut app) = initialized_app_and_terminal_from_paths(&[fixture.root.clone()])?; @@ -26,11 +28,11 @@ fn basic_user_journey_with_deletion() -> Result<()> { // When selecting the marker window and pressing the combination to delete entries app.process_events( &mut terminal, - vec![ + into_events(vec![ Event::Key(KeyCode::Tab.into()), Event::Key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL)), ] - .into_iter(), + .into_iter()), )?; assert!( app.window.mark_pane.is_none(), diff --git a/src/interactive/app/tests/utils.rs b/src/interactive/app/tests/utils.rs index 133dd9a8..4f8c6fd5 100644 --- a/src/interactive/app/tests/utils.rs +++ b/src/interactive/app/tests/utils.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Error, Result}; -use crossbeam::channel::Receiver; -use crosstermion::crossterm::event::KeyCode; +use crossbeam::channel::{Receiver, Sender}; +use crosstermion::{crossterm::{event::{KeyCode, KeyModifiers, KeyEvent}}, input::Event}; use dua::{ traverse::{EntryData, Tree, TreeIndex}, ByteFormat, TraversalSorting, WalkOptions, @@ -21,15 +21,25 @@ use tui_react::Terminal; use crate::interactive::{app::tests::FIXTURE_PATH, terminal_app::TerminalApp}; +pub fn into_events<'a>( + events: impl IntoIterator + 'a, +) -> Receiver { + let (key_send, key_receive) = crossbeam::channel::unbounded(); + for event in events { + _ = key_send.send(event); + } + key_receive +} + pub fn into_keys<'a>( codes: impl IntoIterator + 'a, -) -> impl Iterator + 'a { - codes +) -> Receiver { + into_events(codes .into_iter() - .map(|code| crosstermion::input::Event::Key(code.into())) + .map(|code| crosstermion::input::Event::Key(code.into()))) } -pub fn into_codes(input: &str) -> impl Iterator + '_ { +pub fn into_codes<'a>(input: &'a str) -> Receiver { into_keys(input.chars().map(KeyCode::Char)) } @@ -172,7 +182,7 @@ pub fn fixture_str(p: impl AsRef) -> String { pub fn initialized_app_and_terminal_with_closure( fixture_paths: &[impl AsRef], mut convert: impl FnMut(&Path) -> PathBuf, -) -> Result<(Terminal, TerminalApp, Receiver), Error> { +) -> Result<(Terminal, TerminalApp), Error> { let mut terminal = new_test_terminal()?; std::env::set_current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))?; @@ -185,12 +195,14 @@ pub fn initialized_app_and_terminal_with_closure( ignore_dirs: Default::default(), }; + let (key_send, key_receive) = crossbeam::channel::bounded(0); let mut app = TerminalApp::initialize(&mut terminal, walk_options, ByteFormat::Metric)?; let input_paths = fixture_paths.iter().map(|c| convert(c.as_ref())).collect(); - let traversal_rx = app.traverse(input_paths)?; + app.traverse(input_paths)?; + app.run_until_traversed(&mut terminal, key_receive); - Ok((terminal, app, traversal_rx)) + Ok((terminal, app)) } pub fn new_test_terminal() -> std::io::Result> { @@ -199,7 +211,7 @@ pub fn new_test_terminal() -> std::io::Result> { pub fn initialized_app_and_terminal_from_paths( fixture_paths: &[PathBuf], -) -> Result<(Terminal, TerminalApp, Receiver), Error> { +) -> Result<(Terminal, TerminalApp), Error> { fn to_path_buf(p: &Path) -> PathBuf { p.to_path_buf() } @@ -208,7 +220,7 @@ pub fn initialized_app_and_terminal_from_paths( pub fn initialized_app_and_terminal_from_fixture( fixture_paths: &[&str], -) -> Result<(Terminal, TerminalApp, Receiver), Error> { +) -> Result<(Terminal, TerminalApp), Error> { #[allow(clippy::redundant_closure)] // doesn't actually work that way due to borrowchk - probably a bug initialized_app_and_terminal_with_closure(fixture_paths, |p| fixture(p)) diff --git a/src/traverse.rs b/src/traverse.rs index 0aba859f..f221ace5 100644 --- a/src/traverse.rs +++ b/src/traverse.rs @@ -143,14 +143,14 @@ pub struct RunningTraversal { } #[derive(PartialEq)] -pub enum ProcessEventResult { - NoOp, +pub enum TraversalProcessingEvent { + None, UpdateIsReady, Finished, } impl RunningTraversal { - pub fn new( + pub fn start( root_idx: TreeIndex, walk_options: &WalkOptions, input: Vec, @@ -213,7 +213,7 @@ impl RunningTraversal { &mut self, t: &mut Traversal, event: TraversalEvent, - ) -> ProcessEventResult { + ) -> TraversalProcessingEvent { match event { TraversalEvent::Entry(entry, root_path, device_id) => { t.entries_traversed += 1; @@ -335,7 +335,7 @@ impl RunningTraversal { if let Some(throttle) = &self.throttle { if throttle.can_update() { - return ProcessEventResult::UpdateIsReady; + return TraversalProcessingEvent::UpdateIsReady; } } } @@ -370,10 +370,10 @@ impl RunningTraversal { t.total_bytes = Some(root_size); t.elapsed = Some(t.start.elapsed()); - return ProcessEventResult::Finished; + return TraversalProcessingEvent::Finished; } } - ProcessEventResult::NoOp + TraversalProcessingEvent::None } } From c4efba87179636afeb26e472353a029a4030086c Mon Sep 17 00:00:00 2001 From: Piotr Wach Date: Mon, 8 Jan 2024 23:34:14 +0000 Subject: [PATCH 19/20] clippy --- src/interactive/app/eventloop.rs | 19 ++++--- src/interactive/app/terminal_app.rs | 49 +++++++++++-------- .../app/tests/journeys_readonly.rs | 7 +-- .../app/tests/journeys_with_writes.rs | 14 +++--- src/interactive/app/tests/utils.rs | 26 +++++----- src/main.rs | 2 + src/traverse.rs | 1 + 7 files changed, 63 insertions(+), 55 deletions(-) diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index 2de51b63..ad326a0f 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -10,7 +10,7 @@ use crossbeam::channel::Receiver; use crosstermion::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use crosstermion::input::Event; use dua::{ - traverse::{EntryData, TraversalProcessingEvent, RunningTraversal, Traversal}, + traverse::{EntryData, RunningTraversal, Traversal, TraversalProcessingEvent}, WalkOptions, WalkResult, }; use std::path::PathBuf; @@ -104,13 +104,9 @@ impl AppState { self.refresh_screen(window, traversal, display, terminal)?; loop { - if let Some(result) = self.process_event( - window, - traversal, - display, - terminal, - &events, - )? { + if let Some(result) = + self.process_event(window, traversal, display, terminal, &events)? + { return Ok(result); } } @@ -161,9 +157,12 @@ impl AppState { } } else { let Ok(event) = events.recv() else { - return Ok(Some(ProcessingResult::ExitRequested(WalkResult { num_errors: 0 }))); + return Ok(Some(ProcessingResult::ExitRequested(WalkResult { + num_errors: 0, + }))); }; - let result = self.process_terminal_event(window, traversal, display, terminal, event)?; + let result = + self.process_terminal_event(window, traversal, display, terminal, event)?; if let Some(processing_result) = result { return Ok(Some(processing_result)); } diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal_app.rs index 82d7199b..ae1c130e 100644 --- a/src/interactive/app/terminal_app.rs +++ b/src/interactive/app/terminal_app.rs @@ -103,29 +103,38 @@ impl TerminalApp { ProcessingResult::ExitRequested(res) => Ok(res), } } +} - pub fn run_until_traversed( - &mut self, - terminal: &mut Terminal, - events: Receiver, - ) -> Result - where - B: Backend - { - while self.state.running_traversal.is_some() { - match self.state.process_event( - &mut self.window, - &mut self.traversal, - &mut self.display, - terminal, - &events, - )? { - Some(ProcessingResult::ExitRequested(res)) => { - return Ok(res); +#[cfg(test)] +mod tests { + use super::*; + + use super::TerminalApp; + + impl TerminalApp { + pub fn run_until_traversed( + &mut self, + terminal: &mut Terminal, + events: Receiver, + ) -> Result + where + B: Backend, + { + while self.state.running_traversal.is_some() { + match self.state.process_event( + &mut self.window, + &mut self.traversal, + &mut self.display, + terminal, + &events, + )? { + Some(ProcessingResult::ExitRequested(res)) => { + return Ok(res); + } + _ => {} } - _ => {} } + Ok(WalkResult { num_errors: 0 }) } - Ok(WalkResult { num_errors: 0 }) } } diff --git a/src/interactive/app/tests/journeys_readonly.rs b/src/interactive/app/tests/journeys_readonly.rs index 48211453..28e0529c 100644 --- a/src/interactive/app/tests/journeys_readonly.rs +++ b/src/interactive/app/tests/journeys_readonly.rs @@ -29,7 +29,7 @@ fn simple_user_journey_read_only() -> Result<()> { let short_root = "sample-01"; let (mut terminal, mut app) = initialized_app_and_terminal_from_fixture(&[short_root, long_root])?; - + // POST-INIT // after initialization, we expect that... { @@ -300,10 +300,7 @@ fn simple_user_journey_read_only() -> Result<()> { ); // when advancing the selection to the marker pane - app.process_events( - &mut terminal, - into_keys(Some(KeyCode::Tab)), - )?; + app.process_events(&mut terminal, into_keys(Some(KeyCode::Tab)))?; { assert_eq!( Some(true), diff --git a/src/interactive/app/tests/journeys_with_writes.rs b/src/interactive/app/tests/journeys_with_writes.rs index 09ca1be3..52431da8 100644 --- a/src/interactive/app/tests/journeys_with_writes.rs +++ b/src/interactive/app/tests/journeys_with_writes.rs @@ -9,7 +9,7 @@ use pretty_assertions::assert_eq; #[test] #[cfg(not(target_os = "windows"))] // it stopped working here, don't know if it's truly broken or if it's the test. Let's wait for windows users to report. fn basic_user_journey_with_deletion() -> Result<()> { - use crate::interactive::app::tests::utils::{into_keys, into_events}; + use crate::interactive::app::tests::utils::into_events; let fixture = WritableFixture::from("sample-02"); let (mut terminal, mut app) = initialized_app_and_terminal_from_paths(&[fixture.root.clone()])?; @@ -28,11 +28,13 @@ fn basic_user_journey_with_deletion() -> Result<()> { // When selecting the marker window and pressing the combination to delete entries app.process_events( &mut terminal, - into_events(vec![ - Event::Key(KeyCode::Tab.into()), - Event::Key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL)), - ] - .into_iter()), + into_events( + vec![ + Event::Key(KeyCode::Tab.into()), + Event::Key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL)), + ] + .into_iter(), + ), )?; assert!( app.window.mark_pane.is_none(), diff --git a/src/interactive/app/tests/utils.rs b/src/interactive/app/tests/utils.rs index 4f8c6fd5..8f7bf97a 100644 --- a/src/interactive/app/tests/utils.rs +++ b/src/interactive/app/tests/utils.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Error, Result}; -use crossbeam::channel::{Receiver, Sender}; -use crosstermion::{crossterm::{event::{KeyCode, KeyModifiers, KeyEvent}}, input::Event}; +use crossbeam::channel::Receiver; +use crosstermion::{crossterm::event::KeyCode, input::Event}; use dua::{ traverse::{EntryData, Tree, TreeIndex}, ByteFormat, TraversalSorting, WalkOptions, @@ -21,9 +21,7 @@ use tui_react::Terminal; use crate::interactive::{app::tests::FIXTURE_PATH, terminal_app::TerminalApp}; -pub fn into_events<'a>( - events: impl IntoIterator + 'a, -) -> Receiver { +pub fn into_events<'a>(events: impl IntoIterator + 'a) -> Receiver { let (key_send, key_receive) = crossbeam::channel::unbounded(); for event in events { _ = key_send.send(event); @@ -31,15 +29,15 @@ pub fn into_events<'a>( key_receive } -pub fn into_keys<'a>( - codes: impl IntoIterator + 'a, -) -> Receiver { - into_events(codes - .into_iter() - .map(|code| crosstermion::input::Event::Key(code.into()))) +pub fn into_keys<'a>(codes: impl IntoIterator + 'a) -> Receiver { + into_events( + codes + .into_iter() + .map(|code| crosstermion::input::Event::Key(code.into())), + ) } -pub fn into_codes<'a>(input: &'a str) -> Receiver { +pub fn into_codes(input: &str) -> Receiver { into_keys(input.chars().map(KeyCode::Char)) } @@ -195,12 +193,12 @@ pub fn initialized_app_and_terminal_with_closure( ignore_dirs: Default::default(), }; - let (key_send, key_receive) = crossbeam::channel::bounded(0); + let (_key_send, key_receive) = crossbeam::channel::bounded(0); let mut app = TerminalApp::initialize(&mut terminal, walk_options, ByteFormat::Metric)?; let input_paths = fixture_paths.iter().map(|c| convert(c.as_ref())).collect(); app.traverse(input_paths)?; - app.run_until_traversed(&mut terminal, key_receive); + app.run_until_traversed(&mut terminal, key_receive)?; Ok((terminal, app)) } diff --git a/src/main.rs b/src/main.rs index 7e28c130..27433775 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,9 @@ use simplelog::{Config, LevelFilter, WriteLogger}; use std::fs::OpenOptions; use std::{fs, io, io::Write, path::PathBuf, process}; +#[cfg(feature = "tui-crossplatform")] use crate::interactive::input::input_channel; +#[cfg(feature = "tui-crossplatform")] use crate::interactive::terminal_app::TerminalApp; mod crossdev; diff --git a/src/traverse.rs b/src/traverse.rs index f221ace5..3b6b291c 100644 --- a/src/traverse.rs +++ b/src/traverse.rs @@ -125,6 +125,7 @@ pub fn pop_or_panic(v: &mut Vec) -> EntryInfo { pub type TraversalEntry = Result>)>, jwalk::Error>; +#[allow(clippy::large_enum_variant)] pub enum TraversalEvent { Entry(TraversalEntry, Arc, u64), Finished(u64), From 30da672a83c1063eb6f4c5483cb47f5d69c1dc35 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 9 Jan 2024 09:53:50 +0100 Subject: [PATCH 20/20] refactor - avoid `app_` prefix in `app` module. - cleanup root-index logic a little --- src/interactive/app/eventloop.rs | 65 +++++++++---------- src/interactive/app/handlers.rs | 4 +- src/interactive/app/mod.rs | 4 +- .../app/{app_state.rs => state.rs} | 11 +--- .../app/{terminal_app.rs => terminal.rs} | 25 ++----- .../app/tests/journeys_readonly.rs | 4 +- .../app/tests/journeys_with_writes.rs | 11 ++-- src/interactive/app/tests/utils.rs | 6 +- src/interactive/widgets/glob.rs | 2 +- src/interactive/widgets/main.rs | 2 +- src/main.rs | 2 +- src/traverse.rs | 37 ++++++----- 12 files changed, 76 insertions(+), 97 deletions(-) rename src/interactive/app/{app_state.rs => state.rs} (70%) rename src/interactive/app/{terminal_app.rs => terminal.rs} (83%) diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index ad326a0f..cd91a270 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -1,7 +1,7 @@ use crate::interactive::{ app::navigation::Navigation, - app_state::FocussedPane, sorted_entries, + state::FocussedPane, widgets::{glob_search, MainWindow, MainWindowProps}, CursorDirection, CursorMode, DisplayOptions, MarkEntryMode, }; @@ -10,14 +10,14 @@ use crossbeam::channel::Receiver; use crosstermion::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use crosstermion::input::Event; use dua::{ - traverse::{EntryData, RunningTraversal, Traversal, TraversalProcessingEvent}, + traverse::{BackgroundTraversal, EntryData, Traversal}, WalkOptions, WalkResult, }; use std::path::PathBuf; use tui::backend::Backend; use tui_react::Terminal; -use super::app_state::{AppState, Cursor, ProcessingResult}; +use super::state::{AppState, Cursor}; use super::tree_view::TreeView; impl AppState { @@ -70,8 +70,10 @@ impl AppState { walk_options: &WalkOptions, input: Vec, ) -> Result<()> { - let running_traversal = RunningTraversal::start(traversal.root_index, walk_options, input)?; - self.running_traversal = Some(running_traversal); + let background_traversal = + BackgroundTraversal::start(traversal.root_index, walk_options, input)?; + self.navigation_mut().view_root = traversal.root_index; + self.active_traversal = Some(background_traversal); Ok(()) } @@ -90,6 +92,7 @@ impl AppState { Ok(()) } + /// This method ends once the user quits the application or there are no more inputs to process. pub fn process_events( &mut self, window: &mut MainWindow, @@ -97,7 +100,7 @@ impl AppState { display: &mut DisplayOptions, terminal: &mut Terminal, events: Receiver, - ) -> Result + ) -> Result where B: Backend, { @@ -119,47 +122,43 @@ impl AppState { display: &mut DisplayOptions, terminal: &mut Terminal, events: &Receiver, - ) -> Result> + ) -> Result> where B: Backend, { - if let Some(running_traversal) = &mut self.running_traversal { + if let Some(active_traversal) = &mut self.active_traversal { crossbeam::select! { recv(events) -> event => { let Ok(event) = event else { - return Ok(Some(ProcessingResult::ExitRequested(WalkResult { num_errors: 0 }))); + return Ok(Some(WalkResult { num_errors: 0 })); }; - let result = self.process_terminal_event( + let res = self.process_terminal_event( window, traversal, display, terminal, event)?; - if let Some(processing_result) = result { - return Ok(Some(processing_result)); + if let Some(res) = res { + return Ok(Some(res)); } }, - recv(&running_traversal.event_rx) -> event => { + recv(&active_traversal.event_rx) -> event => { let Ok(event) = event else { return Ok(None); }; - let result = running_traversal.process_event(traversal, event); - if result != TraversalProcessingEvent::None { - if result == TraversalProcessingEvent::Finished { - self.is_scanning = false; - self.running_traversal = None; + if let Some(is_finished) = active_traversal.integrate_traversal_event(traversal, event) { + if is_finished { + self.active_traversal = None; } self.update_state(traversal); self.refresh_screen(window, traversal, display, terminal)?; - } + }; } } } else { let Ok(event) = events.recv() else { - return Ok(Some(ProcessingResult::ExitRequested(WalkResult { - num_errors: 0, - }))); + return Ok(Some(WalkResult { num_errors: 0 })); }; let result = self.process_terminal_event(window, traversal, display, terminal, event)?; @@ -171,17 +170,13 @@ impl AppState { } fn update_state(&mut self, traversal: &Traversal) { - let received_events = self.received_event; - if !received_events { - self.navigation_mut().view_root = traversal.root_index; - } self.entries = sorted_entries( &traversal.tree, self.navigation().view_root, self.sorting, self.glob_root(), ); - if !received_events { + if !self.received_events { self.navigation_mut().selected = self.entries.first().map(|b| b.index); } self.reset_message(); // force "scanning" to appear @@ -194,7 +189,7 @@ impl AppState { display: &mut DisplayOptions, terminal: &mut Terminal, event: Event, - ) -> Result> + ) -> Result> where B: Backend, { @@ -203,8 +198,8 @@ impl AppState { let key = match event { Event::Key(key) if key.kind != KeyEventKind::Release => { - if key.code != KeyCode::Char('\r') { - self.received_event = true; + if key != refresh_key() { + self.received_events = true; } key } @@ -231,9 +226,9 @@ impl AppState { } 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 { + return Ok(Some(WalkResult { num_errors: tree_view.traversal.io_errors, - }))) + })) } Char('q') if !glob_focussed => { if let Some(result) = self.handle_quit(&mut tree_view, window) { @@ -367,16 +362,16 @@ impl AppState { &mut self, tree_view: &mut TreeView<'_>, window: &mut MainWindow, - ) -> Option> { + ) -> Option> { use FocussedPane::*; match self.focussed { Main => { if self.glob_navigation.is_some() { self.handle_glob_quit(tree_view, window); } else { - return Some(Ok(ProcessingResult::ExitRequested(WalkResult { + return Some(Ok(WalkResult { num_errors: tree_view.traversal.io_errors, - }))); + })); } } Mark => self.focussed = Main, diff --git a/src/interactive/app/handlers.rs b/src/interactive/app/handlers.rs index 611c2a00..d6b65823 100644 --- a/src/interactive/app/handlers.rs +++ b/src/interactive/app/handlers.rs @@ -9,7 +9,7 @@ use std::{fs, io, path::PathBuf}; use tui::backend::Backend; use tui_react::Terminal; -use super::app_state::{AppState, FocussedPane::*}; +use super::state::{AppState, FocussedPane::*}; #[derive(Copy, Clone)] pub enum CursorMode { @@ -146,7 +146,7 @@ impl AppState { } pub fn reset_message(&mut self) { - if self.is_scanning { + if self.active_traversal.is_some() { self.message = Some("-> scanning <-".into()); } else { self.message = None; diff --git a/src/interactive/app/mod.rs b/src/interactive/app/mod.rs index f4f6d3e3..355d5879 100644 --- a/src/interactive/app/mod.rs +++ b/src/interactive/app/mod.rs @@ -1,11 +1,11 @@ -pub mod app_state; mod bytevis; mod common; mod eventloop; mod handlers; pub mod input; mod navigation; -pub mod terminal_app; +pub mod state; +pub mod terminal; pub mod tree_view; pub use bytevis::*; diff --git a/src/interactive/app/app_state.rs b/src/interactive/app/state.rs similarity index 70% rename from src/interactive/app/app_state.rs rename to src/interactive/app/state.rs index 3d32f806..9157da1b 100644 --- a/src/interactive/app/app_state.rs +++ b/src/interactive/app/state.rs @@ -1,4 +1,4 @@ -use dua::{traverse::RunningTraversal, WalkResult}; +use dua::traverse::BackgroundTraversal; use super::{navigation::Navigation, EntryDataBundle, SortMode}; @@ -26,11 +26,6 @@ pub struct AppState { pub sorting: SortMode, pub message: Option, pub focussed: FocussedPane, - pub is_scanning: bool, - pub received_event: bool, - pub running_traversal: Option, -} - -pub enum ProcessingResult { - ExitRequested(WalkResult), + pub received_events: bool, + pub active_traversal: Option, } diff --git a/src/interactive/app/terminal_app.rs b/src/interactive/app/terminal.rs similarity index 83% rename from src/interactive/app/terminal_app.rs rename to src/interactive/app/terminal.rs index ae1c130e..2109e003 100644 --- a/src/interactive/app/terminal_app.rs +++ b/src/interactive/app/terminal.rs @@ -12,10 +12,7 @@ use tui_react::Terminal; use crate::interactive::widgets::MainWindow; -use super::{ - app_state::{AppState, ProcessingResult}, - sorted_entries, DisplayOptions, -}; +use super::{sorted_entries, state::AppState, DisplayOptions}; /// State and methods representing the interactive disk usage analyser for the terminal pub struct TerminalApp { @@ -41,10 +38,7 @@ impl TerminalApp { let display = DisplayOptions::new(byte_format); let window = MainWindow::default(); - let mut state = AppState { - is_scanning: false, - ..Default::default() - }; + let mut state = AppState::default(); let traversal = { let mut tree = Tree::new(); @@ -93,15 +87,13 @@ impl TerminalApp { where B: Backend, { - match self.state.process_events( + self.state.process_events( &mut self.window, &mut self.traversal, &mut self.display, terminal, events, - )? { - ProcessingResult::ExitRequested(res) => Ok(res), - } + ) } } @@ -120,18 +112,15 @@ mod tests { where B: Backend, { - while self.state.running_traversal.is_some() { - match self.state.process_event( + while self.state.active_traversal.is_some() { + if let Some(res) = self.state.process_event( &mut self.window, &mut self.traversal, &mut self.display, terminal, &events, )? { - Some(ProcessingResult::ExitRequested(res)) => { - return Ok(res); - } - _ => {} + return Ok(res); } } Ok(WalkResult { num_errors: 0 }) diff --git a/src/interactive/app/tests/journeys_readonly.rs b/src/interactive/app/tests/journeys_readonly.rs index 28e0529c..b6dc5792 100644 --- a/src/interactive/app/tests/journeys_readonly.rs +++ b/src/interactive/app/tests/journeys_readonly.rs @@ -40,8 +40,8 @@ fn simple_user_journey_read_only() -> Result<()> { ); assert!( - !app.state.is_scanning, - "it will not think it is still scanning" + app.state.active_traversal.is_none(), + "it will not think it is still scanning as there is no traversal" ); let first_selected_path = OsString::from(format!("{}/{}", FIXTURE_PATH, long_root)); diff --git a/src/interactive/app/tests/journeys_with_writes.rs b/src/interactive/app/tests/journeys_with_writes.rs index 52431da8..cbe18f1e 100644 --- a/src/interactive/app/tests/journeys_with_writes.rs +++ b/src/interactive/app/tests/journeys_with_writes.rs @@ -28,13 +28,10 @@ fn basic_user_journey_with_deletion() -> Result<()> { // When selecting the marker window and pressing the combination to delete entries app.process_events( &mut terminal, - into_events( - vec![ - Event::Key(KeyCode::Tab.into()), - Event::Key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL)), - ] - .into_iter(), - ), + into_events([ + Event::Key(KeyCode::Tab.into()), + Event::Key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL)), + ]), )?; assert!( app.window.mark_pane.is_none(), diff --git a/src/interactive/app/tests/utils.rs b/src/interactive/app/tests/utils.rs index 8f7bf97a..a54fa8f5 100644 --- a/src/interactive/app/tests/utils.rs +++ b/src/interactive/app/tests/utils.rs @@ -19,12 +19,14 @@ use std::{ use tui::backend::TestBackend; use tui_react::Terminal; -use crate::interactive::{app::tests::FIXTURE_PATH, terminal_app::TerminalApp}; +use crate::interactive::{app::tests::FIXTURE_PATH, terminal::TerminalApp}; pub fn into_events<'a>(events: impl IntoIterator + 'a) -> Receiver { let (key_send, key_receive) = crossbeam::channel::unbounded(); for event in events { - _ = key_send.send(event); + key_send + .send(event) + .expect("event is stored in the channel for later retrieval"); } key_receive } diff --git a/src/interactive/widgets/glob.rs b/src/interactive/widgets/glob.rs index 9f385a2c..bafc69e7 100644 --- a/src/interactive/widgets/glob.rs +++ b/src/interactive/widgets/glob.rs @@ -18,7 +18,7 @@ use tui_react::{draw_text_nowrap_fn, Terminal}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; -use crate::interactive::app_state::Cursor; +use crate::interactive::state::Cursor; pub struct GlobPaneProps { pub border_style: Style, diff --git a/src/interactive/widgets/main.rs b/src/interactive/widgets/main.rs index 3df375ae..cd3c330a 100644 --- a/src/interactive/widgets/main.rs +++ b/src/interactive/widgets/main.rs @@ -1,5 +1,5 @@ use crate::interactive::{ - app_state::{AppState, Cursor, FocussedPane}, + state::{AppState, Cursor, FocussedPane}, widgets::{ Entries, EntriesProps, Footer, FooterProps, GlobPane, GlobPaneProps, Header, HelpPane, HelpPaneProps, MarkPane, MarkPaneProps, COLOR_MARKED, diff --git a/src/main.rs b/src/main.rs index 27433775..105cf58b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use std::{fs, io, io::Write, path::PathBuf, process}; #[cfg(feature = "tui-crossplatform")] use crate::interactive::input::input_channel; #[cfg(feature = "tui-crossplatform")] -use crate::interactive::terminal_app::TerminalApp; +use crate::interactive::terminal::TerminalApp; mod crossdev; #[cfg(feature = "tui-crossplatform")] diff --git a/src/traverse.rs b/src/traverse.rs index 3b6b291c..6c247883 100644 --- a/src/traverse.rs +++ b/src/traverse.rs @@ -131,7 +131,8 @@ pub enum TraversalEvent { Finished(u64), } -pub struct RunningTraversal { +/// An in-progress traversal which exposes newly obtained entries +pub struct BackgroundTraversal { walk_options: WalkOptions, previous_node_idx: TreeIndex, parent_node_idx: TreeIndex, @@ -143,19 +144,14 @@ pub struct RunningTraversal { pub event_rx: Receiver, } -#[derive(PartialEq)] -pub enum TraversalProcessingEvent { - None, - UpdateIsReady, - Finished, -} - -impl RunningTraversal { +impl BackgroundTraversal { + /// Start a background thread to perform the actual tree walk, and dispatch the results + /// as events to be received on [BackgroundTraversal::event_rx]. pub fn start( root_idx: TreeIndex, walk_options: &WalkOptions, input: Vec, - ) -> anyhow::Result { + ) -> anyhow::Result { let (entry_tx, entry_rx) = crossbeam::channel::bounded(100); std::thread::Builder::new() .name("dua-fs-walk-dispatcher".to_string()) @@ -210,11 +206,18 @@ impl RunningTraversal { }) } - pub fn process_event( + /// Integrate `event` into traversal `t` so its information is represented by it. + /// This builds the traversal tree from a directory-walk. + /// + /// Returns + /// * `Some(true)` if the traversal is finished + /// * `Some(false)` if the caller may update its state after throttling kicked in + /// * `None` - the event was written into the traversal, but there is nothing else to do + pub fn integrate_traversal_event( &mut self, t: &mut Traversal, event: TraversalEvent, - ) -> TraversalProcessingEvent { + ) -> Option { match event { TraversalEvent::Entry(entry, root_path, device_id) => { t.entries_traversed += 1; @@ -334,10 +337,8 @@ impl RunningTraversal { } } - if let Some(throttle) = &self.throttle { - if throttle.can_update() { - return TraversalProcessingEvent::UpdateIsReady; - } + if self.throttle.as_ref().map_or(false, |t| t.can_update()) { + return Some(false); } } TraversalEvent::Finished(io_errors) => { @@ -371,10 +372,10 @@ impl RunningTraversal { t.total_bytes = Some(root_size); t.elapsed = Some(t.start.elapsed()); - return TraversalProcessingEvent::Finished; + return Some(true); } } - TraversalProcessingEvent::None + None } }