diff options
Diffstat (limited to 'src/interactive/app')
-rw-r--r-- | src/interactive/app/common.rs | 40 | ||||
-rw-r--r-- | src/interactive/app/eventloop.rs | 379 | ||||
-rw-r--r-- | src/interactive/app/mod.rs | 5 |
3 files changed, 424 insertions, 0 deletions
diff --git a/src/interactive/app/common.rs b/src/interactive/app/common.rs new file mode 100644 index 0000000..10d9f11 --- /dev/null +++ b/src/interactive/app/common.rs @@ -0,0 +1,40 @@ +use dua::traverse::{EntryData, Tree, TreeIndex}; +use itertools::Itertools; +use petgraph::Direction; + +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Eq)] +pub enum SortMode { + SizeDescending, + SizeAscending, +} + +impl SortMode { + pub fn toggle_size(&mut self) { + use SortMode::*; + *self = match self { + SizeAscending => SizeDescending, + SizeDescending => SizeAscending, + } + } +} + +impl Default for SortMode { + fn default() -> Self { + SortMode::SizeDescending + } +} + +pub fn sorted_entries( + tree: &Tree, + node_idx: TreeIndex, + sorting: SortMode, +) -> Vec<(TreeIndex, &EntryData)> { + use SortMode::*; + tree.neighbors_directed(node_idx, Direction::Outgoing) + .filter_map(|idx| tree.node_weight(idx).map(|w| (idx, w))) + .sorted_by(|(_, l), (_, r)| match sorting { + SizeDescending => r.size.cmp(&l.size), + SizeAscending => l.size.cmp(&r.size), + }) + .collect() +} diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs new file mode 100644 index 0000000..c05ca02 --- /dev/null +++ b/src/interactive/app/eventloop.rs @@ -0,0 +1,379 @@ +use crate::{ + interactive::{ + sorted_entries, + widgets::{DrawState, HelpPaneState, MainWindow}, + SortMode, + }, + ByteFormat, +}; +use dua::{ + path_of, + traverse::{Traversal, TreeIndex}, + WalkOptions, WalkResult, +}; +use failure::Error; +use itertools::Itertools; +use petgraph::Direction; +use std::{fmt, io, path::PathBuf}; +use termion::input::{Keys, TermReadEventsAndRaw}; +use tui::{backend::Backend, widgets::Widget, Terminal}; + +#[derive(Clone, Copy)] +pub enum ByteVisualization { + Percentage, + Bar, + LongBar, + PercentageAndBar, +} + +pub struct DisplayByteVisualization { + format: ByteVisualization, + percentage: f32, +} + +impl Default for ByteVisualization { + fn default() -> Self { + ByteVisualization::PercentageAndBar + } +} + +impl ByteVisualization { + pub fn cycle(&mut self) { + use ByteVisualization::*; + *self = match self { + Bar => LongBar, + LongBar => PercentageAndBar, + PercentageAndBar => Percentage, + Percentage => Bar, + } + } + pub fn display(&self, percentage: f32) -> DisplayByteVisualization { + DisplayByteVisualization { + format: *self, + percentage, + } + } +} + +impl fmt::Display for DisplayByteVisualization { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + use ByteVisualization::*; + let Self { format, percentage } = self; + + const BAR_SIZE: usize = 10; + match format { + Percentage => Self::make_percentage(f, percentage), + PercentageAndBar => { + Self::make_percentage(f, percentage)?; + f.write_str(" ")?; + Self::make_bar(f, percentage, BAR_SIZE) + } + Bar => Self::make_bar(f, percentage, BAR_SIZE), + LongBar => Self::make_bar(f, percentage, 20), + } + } +} + +impl DisplayByteVisualization { + fn make_bar(f: &mut fmt::Formatter, percentage: &f32, length: usize) -> Result<(), fmt::Error> { + let block_length = (length as f32 * percentage).round() as usize; + for _ in 0..block_length { + f.write_str(tui::symbols::block::FULL)?; + } + for _ in 0..length - block_length { + f.write_str(" ")?; + } + Ok(()) + } + fn make_percentage(f: &mut fmt::Formatter, percentage: &f32) -> Result<(), fmt::Error> { + write!(f, " {:>5.02}% ", percentage * 100.0) + } +} + +/// Options to configure how we display things +#[derive(Clone, Copy)] +pub struct DisplayOptions { + pub byte_format: ByteFormat, + pub byte_vis: ByteVisualization, +} + +impl From<WalkOptions> for DisplayOptions { + fn from(WalkOptions { byte_format, .. }: WalkOptions) -> Self { + DisplayOptions { + byte_format, + byte_vis: ByteVisualization::default(), + } + } +} + +#[derive(Copy, Clone)] +pub enum FocussedPane { + Main, + Help, +} + +impl Default for FocussedPane { + fn default() -> Self { + FocussedPane::Main + } +} + +#[derive(Default)] +pub struct AppState { + pub root: TreeIndex, + pub selected: Option<TreeIndex>, + pub sorting: SortMode, + pub message: Option<String>, + pub help_pane: Option<HelpPaneState>, + pub focussed: FocussedPane, +} + +/// 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 draw_state: DrawState, +} + +enum CursorDirection { + PageDown, + Down, + Up, + PageUp, +} + +impl TerminalApp { + fn draw<B>(&mut self, terminal: &mut Terminal<B>) -> Result<(), Error> + where + B: Backend, + { + let Self { + traversal, + display, + state, + ref mut draw_state, + } = self; + + terminal.draw(|mut f| { + let full_screen = f.size(); + MainWindow { + traversal, + display: *display, + state: &state, + draw_state, + } + .render(&mut f, full_screen) + })?; + + Ok(()) + } + pub fn process_events<B, R>( + &mut self, + terminal: &mut Terminal<B>, + keys: Keys<R>, + ) -> Result<WalkResult, Error> + where + B: Backend, + R: io::Read + TermReadEventsAndRaw, + { + use termion::event::Key::{Char, Ctrl}; + use FocussedPane::*; + + self.draw(terminal)?; + for key in keys.filter_map(Result::ok) { + self.update_message(); + match key { + Char('?') => self.toggle_help_pane(), + Char('\t') => { + self.cycle_focus(); + } + Ctrl('c') => break, + Char('q') => match self.state.focussed { + Main => break, + Help => { + self.state.focussed = Main; + self.state.help_pane = None + } + }, + _ => {} + } + + match self.state.focussed { + FocussedPane::Help => match key { + Ctrl('u') => self.scroll_help(CursorDirection::PageUp), + Char('k') => self.scroll_help(CursorDirection::Up), + Char('j') => self.scroll_help(CursorDirection::Down), + Ctrl('d') => self.scroll_help(CursorDirection::PageDown), + _ => {} + }, + FocussedPane::Main => match key { + Char('O') => self.open_that(), + Char('u') => self.exit_node(), + Char('o') => self.enter_node(), + Ctrl('u') => self.change_entry_selection(CursorDirection::PageUp), + Char('k') => self.change_entry_selection(CursorDirection::Up), + Char('j') => self.change_entry_selection(CursorDirection::Down), + Ctrl('d') => self.change_entry_selection(CursorDirection::PageDown), + Char('s') => self.state.sorting.toggle_size(), + Char('g') => self.display.byte_vis.cycle(), + _ => {} + }, + }; + self.draw(terminal)?; + } + Ok(WalkResult { + num_errors: self.traversal.io_errors, + }) + } + + fn cycle_focus(&mut self) { + use FocussedPane::*; + self.state.focussed = match (self.state.focussed, self.state.help_pane) { + (Main, Some(_)) => Help, + (Help, _) => Main, + _ => Main, + }; + } + + fn toggle_help_pane(&mut self) { + use FocussedPane::*; + self.state.focussed = match self.state.focussed { + Main => { + self.state.help_pane = Some(HelpPaneState::default()); + Help + } + Help => { + self.state.help_pane = None; + Main + } + } + } + + fn update_message(&mut self) { + self.state.message = None; + } + + fn exit_node(&mut self) { + match self + .traversal + .tree + .neighbors_directed(self.state.root, Direction::Incoming) + .next() + { + Some(parent_idx) => { + self.state.root = parent_idx; + self.state.selected = + sorted_entries(&self.traversal.tree, parent_idx, self.state.sorting) + .get(0) + .map(|(idx, _)| *idx); + } + None => self.state.message = Some("Top level reached".into()), + } + } + + fn open_that(&mut self) { + match self.state.selected { + Some(ref idx) => { + open::that(path_of(&self.traversal.tree, *idx)).ok(); + } + None => {} + } + } + + fn enter_node(&mut self) { + if let Some(new_root) = self.state.selected { + let entries = sorted_entries(&self.traversal.tree, new_root, self.state.sorting); + match entries.get(0) { + Some((next_selection, _)) => { + self.state.root = new_root; + self.state.selected = Some(*next_selection); + } + None => self.state.message = Some("Entry is a file or an empty directory".into()), + } + } + } + + fn scroll_help(&mut self, direction: CursorDirection) { + use CursorDirection::*; + let scroll = self.draw_state.help_scroll; + self.draw_state.help_scroll = match direction { + Down => scroll.saturating_add(1), + Up => scroll.saturating_sub(1), + PageDown => scroll.saturating_add(10), + PageUp => scroll.saturating_sub(10), + }; + } + + fn change_entry_selection(&mut self, direction: CursorDirection) { + let entries = sorted_entries(&self.traversal.tree, self.state.root, self.state.sorting); + let next_selected_pos = match self.state.selected { + Some(ref selected) => entries + .iter() + .find_position(|(idx, _)| *idx == *selected) + .map(|(idx, _)| match direction { + CursorDirection::PageDown => idx.saturating_add(10), + CursorDirection::Down => idx.saturating_add(1), + CursorDirection::Up => idx.saturating_sub(1), + CursorDirection::PageUp => idx.saturating_sub(10), + }) + .unwrap_or(0), + None => 0, + }; + self.state.selected = entries + .get(next_selected_pos) + .or(entries.last()) + .map(|(idx, _)| *idx) + .or(self.state.selected) + } + + pub fn initialize<B>( + terminal: &mut Terminal<B>, + options: WalkOptions, + input: Vec<PathBuf>, + ) -> Result<TerminalApp, Error> + where + B: Backend, + { + terminal.hide_cursor()?; + let mut display_options: DisplayOptions = options.clone().into(); + display_options.byte_vis = ByteVisualization::Bar; + let traversal = Traversal::from_walk(options, input, move |traversal| { + terminal.draw(|mut f| { + let full_screen = f.size(); + let state = AppState { + root: traversal.root_index, + sorting: Default::default(), + message: Some("-> scanning <-".into()), + ..Default::default() + }; + MainWindow { + traversal, + display: display_options, + state: &state, + draw_state: &mut Default::default(), + } + .render(&mut f, full_screen) + })?; + Ok(()) + })?; + + let sorting = Default::default(); + let root = traversal.root_index; + let selected = sorted_entries(&traversal.tree, root, sorting) + .get(0) + .map(|(idx, _)| *idx); + display_options.byte_vis = ByteVisualization::PercentageAndBar; + Ok(TerminalApp { + state: AppState { + root, + sorting, + selected, + ..Default::default() + }, + display: display_options, + traversal, + draw_state: Default::default(), + }) + } +} diff --git a/src/interactive/app/mod.rs b/src/interactive/app/mod.rs new file mode 100644 index 0000000..912120e --- /dev/null +++ b/src/interactive/app/mod.rs @@ -0,0 +1,5 @@ +mod common; +mod eventloop; + +pub use common::*; +pub use eventloop::*; |