use crate::interactive::state::FilesystemScan;
use crate::interactive::{
CursorDirection, CursorMode, DisplayOptions, EntryCheck, MarkEntryMode,
app::navigation::Navigation,
state::FocussedPane,
widgets::{MainWindow, MainWindowProps, glob_search},
};
use anyhow::Result;
use crossbeam::channel::Receiver;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use dua::{
Config, WalkResult,
traverse::{BackgroundTraversal, EntryData, Traversal, TreeIndex},
};
use std::path::PathBuf;
use tui::{Terminal, backend::Backend, buffer::Buffer, layout::Rect, widgets::Widget};
use super::state::{AppState, Cursor};
use super::tree_view::TreeView;
impl AppState {
pub fn navigation_mut(&mut self) -> &mut Navigation {
self.glob_navigation
.as_mut()
.unwrap_or(&mut self.navigation)
}
pub fn navigation(&self) -> &Navigation {
self.glob_navigation.as_ref().unwrap_or(&self.navigation)
}
pub fn draw<B>(
&mut self,
window: &mut MainWindow,
tree_view: &TreeView<'_>,
display: DisplayOptions,
terminal: &mut Terminal<B>,
config: &Config,
) -> Result<()>
where
B: Backend,
{
let props = MainWindowProps {
current_path: tree_view.current_path(self.navigation().view_root),
entries_traversed: self.stats.entries_traversed,
total_bytes: tree_view.total_size(),
start: self.stats.start,
elapsed: self.stats.elapsed,
display,
state: self,
config,
};
let mut cursor = Cursor::default();
let result = draw_window(window, props, terminal, &mut cursor);
if cursor.show {
_ = terminal.show_cursor();
_ = terminal.set_cursor(cursor.x, cursor.y);
} else {
_ = terminal.hide_cursor();
}
result
}
pub fn traverse(&mut self, traversal: &Traversal) -> Result<()> {
let bg_traversal = BackgroundTraversal::start(
traversal.root_index,
&self.walk_options,
self.root_paths.clone(),
false,
true,
)?;
self.navigation_mut().view_root = traversal.root_index;
self.scan = Some(FilesystemScan {
active_traversal: bg_traversal,
previous_selection: None,
});
Ok(())
}
fn recompute_sizes_recursively(&mut self, traversal: &mut Traversal, node_index: TreeIndex) {
let mut tree_view = self.tree_view(traversal);
tree_view.recompute_sizes_recursively(node_index);
}
fn refresh_screen<B>(
&mut self,
window: &mut MainWindow,
traversal: &mut Traversal,
display: &mut DisplayOptions,
terminal: &mut Terminal<B>,
config: &Config,
) -> Result<()>
where
B: Backend,
{
let tree_view = self.tree_view(traversal);
self.draw(window, &tree_view, *display, terminal, config)?;
Ok(())
}
pub fn process_events<B>(
&mut self,
window: &mut MainWindow,
traversal: &mut Traversal,
display: &mut DisplayOptions,
terminal: &mut Terminal<B>,
events: Receiver<Event>,
config: &Config,
) -> Result<WalkResult>
where
B: Backend,
{
self.refresh_screen(window, traversal, display, terminal, config)?;
loop {
if let Some(result) =
self.process_event(window, traversal, display, terminal, &events, config)?
{
return Ok(result);
}
}
}
pub fn process_event<B>(
&mut self,
window: &mut MainWindow,
traversal: &mut Traversal,
display: &mut DisplayOptions,
terminal: &mut Terminal<B>,
events: &Receiver<Event>,
config: &Config,
) -> Result<Option<WalkResult>>
where
B: Backend,
{
if let Some(FilesystemScan {
active_traversal,
previous_selection,
}) = self.scan.as_mut()
{
crossbeam::select! {
recv(events) -> event => {
let Ok(event) = event else {
return Ok(Some(WalkResult { num_errors: self.stats.io_errors }));
};
let res = self.process_terminal_event(
window,
traversal,
display,
terminal,
event,
config,
)?;
if let Some(res) = res {
return Ok(Some(res));
}
},
recv(&active_traversal.event_rx) -> event => {
let Ok(event) = event else {
return Ok(None);
};
if let Some(is_finished) = active_traversal.integrate_traversal_event(traversal, event) {
self.stats = active_traversal.stats;
let previous_selection = previous_selection.clone();
if is_finished {
let root_index = active_traversal.root_idx;
self.recompute_sizes_recursively(traversal, root_index);
self.scan = None;
traversal.cost = Some(traversal.start_time.elapsed());
}
self.update_state_during_traversal(traversal, previous_selection.as_ref(), is_finished);
self.refresh_screen(window, traversal, display, terminal, config)?;
};
}
}
} else {
let Ok(event) = events.recv() else {
return Ok(Some(WalkResult {
num_errors: self.stats.io_errors,
}));
};
let result =
self.process_terminal_event(window, traversal, display, terminal, event, config)?;
if let Some(processing_result) = result {
return Ok(Some(processing_result));
}
}
Ok(None)
}
fn update_state_during_traversal(
&mut self,
traversal: &mut Traversal,
previous_selection: Option<&(PathBuf, usize)>,
is_finished: bool,
) {
let tree_view = self.tree_view(traversal);
self.entries = tree_view.sorted_entries(
self.navigation().view_root,
self.sorting,
self.entry_check(),
);
if !self.received_events {
let previously_selected_entry =
previous_selection.and_then(|(selected_name, selected_idx)| {
self.entries
.iter()
.find(|e| e.name == *selected_name)
.or_else(|| self.entries.get(*selected_idx))
});
if let Some(selected_entry) = previously_selected_entry {
self.navigation_mut().selected = Some(selected_entry.index);
} else if is_finished {
self.navigation_mut().selected = self.entries.first().map(|b| b.index);
}
}
self.reset_message(); }
pub(crate) fn entry_check(&self) -> EntryCheck {
EntryCheck::new(self.scan.is_some(), self.allow_entry_check)
}
fn process_terminal_event<B>(
&mut self,
window: &mut MainWindow,
traversal: &mut Traversal,
display: &mut DisplayOptions,
terminal: &mut Terminal<B>,
event: Event,
config: &Config,
) -> Result<Option<WalkResult>>
where
B: Backend,
{
use FocussedPane::*;
use crossterm::event::KeyCode::*;
let key = match event {
Event::Key(key) if key.kind != KeyEventKind::Release => {
if key != refresh_key() {
self.received_events = true;
}
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 esc_navigates_back_in_main =
config.keys.esc_navigates_back && key.code == Esc && self.focussed == Main;
if esc_navigates_back_in_main {
self.pending_exit = false;
self.exit_node_with_traversal(&tree_view);
} else {
match (key.code, glob_focussed) {
(Esc, _) | (Char('q'), false) => {
if let Some(result) = self.handle_quit(&mut tree_view, window) {
return Ok(Some(result?));
}
}
_ => {
self.pending_exit = false;
}
}
}
let mut handled = true;
match key.code {
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(WalkResult {
num_errors: self.stats.io_errors,
}));
}
_ => {
handled = false;
}
}
if !handled {
match self.focussed {
Mark => self.dispatch_to_mark_pane(
key,
window,
&mut tree_view,
*display,
terminal,
config,
),
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.case,
),
_ => glob_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),
Char('o') | Char('l') | Enter | Right => {
self.enter_node_with_traversal(&tree_view)
}
Char('r') => self.refresh(&mut tree_view, window, Refresh::Selected)?,
Char('R') => self.refresh(&mut tree_view, window, Refresh::AllInView)?,
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('M') => self.toggle_mtime_column(),
Char('c') => self.cycle_count_sorting(&tree_view),
Char('C') => self.toggle_count_column(),
Char('n') => self.cycle_name_sorting(&tree_view),
Char('g') | Char('S') => 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, config)?;
Ok(None)
}
fn refresh(
&mut self,
tree: &mut TreeView<'_>,
window: &mut MainWindow,
what: Refresh,
) -> anyhow::Result<()> {
if self.scan.is_some() {
self.message = Some("Traversal already running".into());
return Ok(());
}
let previous_selection = self.navigation().selected.and_then(|sel_index| {
tree.tree().node_weight(sel_index).map(|w| {
(
w.name.clone(),
self.entries
.iter()
.enumerate()
.find_map(|(idx, e)| (e.index == sel_index).then_some(idx))
.expect("selected item is always in entries"),
)
})
});
if let Some(glob_tree_root) = tree.glob_tree_root
&& glob_tree_root == self.navigation().view_root
{
self.quit_glob_mode(tree, window)
}
let (paths, remove_root_node, skip_root, use_root_path, index, parent_index) = match what {
Refresh::Selected => {
let Some(selected) = self.navigation().selected else {
return Ok(());
};
let parent_index = tree
.fs_parent_of(selected)
.expect("there is always a parent to a selection");
let mut path = tree.path_of(selected);
if path.to_str() == Some("") {
path = PathBuf::from(".");
}
let (paths, use_root_path, skip_root) = if self.navigation().view_root
== tree.traversal.root_index
&& self.root_paths.len() > 1
{
(vec![path], true, false)
} else {
(vec![path], false, false)
};
(
paths,
true,
skip_root,
use_root_path,
selected,
parent_index,
)
}
Refresh::AllInView => {
let (paths, use_root_path, skip_root) = if self.navigation().view_root
== tree.traversal.root_index
&& self.root_paths.len() > 1
{
(self.root_paths.clone(), true, false)
} else {
let mut path = tree.path_of(self.navigation().view_root);
if path.to_str() == Some("") {
path = PathBuf::from(".");
}
(vec![path], false, true)
};
(
paths,
false,
skip_root,
use_root_path,
self.navigation().view_root,
self.navigation().view_root,
)
}
};
tree.remove_entries(index, remove_root_node);
tree.recompute_sizes_recursively(parent_index);
self.entries = tree.sorted_entries(
self.navigation().view_root,
self.sorting,
self.entry_check(),
);
self.navigation_mut().selected = self.entries.first().map(|e| e.index);
self.scan = Some(FilesystemScan {
active_traversal: BackgroundTraversal::start(
parent_index,
&self.walk_options,
paths,
skip_root,
use_root_path,
)?,
previous_selection,
});
self.received_events = false;
Ok(())
}
fn tree_view<'a>(&mut self, traversal: &'a mut Traversal) -> TreeView<'a> {
TreeView {
traversal,
glob_tree_root: self.glob_navigation.as_ref().map(|n| n.tree_root),
}
}
fn search_glob_pattern(
&mut self,
tree_view: &mut TreeView<'_>,
glob_pattern: &str,
case: gix_glob::pattern::Case,
) {
use FocussedPane::*;
match glob_search(
tree_view.tree(),
self.navigation.view_root,
glob_pattern,
case,
) {
Ok(matches) if matches.is_empty() => {
self.message = Some("No match found".into());
}
Ok(matches) => {
if let Some(glob_source) = &self.glob_navigation {
tree_view.tree_mut().remove_node(glob_source.tree_root);
}
let tree_root = tree_view.tree_mut().add_node(EntryData::default());
let glob_source = Navigation {
tree_root,
view_root: tree_root,
selected: Some(tree_root),
..Default::default()
};
self.glob_navigation = Some(glob_source);
for idx in matches {
tree_view.tree_mut().add_edge(tree_root, idx, ());
}
let glob_tree_view = TreeView {
traversal: tree_view.traversal,
glob_tree_root: Some(tree_root),
};
let new_entries =
glob_tree_view.sorted_entries(tree_root, self.sorting, self.entry_check());
let new_entries = self
.navigation_mut()
.selected
.map(|previously_selected| (previously_selected, new_entries));
self.enter_node(new_entries);
self.focussed = Main;
}
Err(err) => self.message = Some(err.to_string()),
}
}
fn handle_quit(
&mut self,
tree_view: &mut TreeView<'_>,
window: &mut MainWindow,
) -> Option<std::result::Result<WalkResult, anyhow::Error>> {
use FocussedPane::*;
match self.focussed {
Main => {
if self.glob_navigation.is_some() {
self.quit_glob_mode(tree_view, window);
} else if window.mark_pane.is_none() && !tree_view.traversal.is_costly() {
return Some(Ok(WalkResult {
num_errors: self.stats.io_errors,
}));
} else if !self.pending_exit {
self.pending_exit = true;
} else {
return Some(Ok(WalkResult {
num_errors: self.stats.io_errors,
}));
}
}
Mark => self.focussed = Main,
Help => {
self.focussed = Main;
window.help_pane = None
}
Glob => {
self.quit_glob_mode(tree_view, window);
}
}
None
}
fn quit_glob_mode(&mut self, tree_view: &mut TreeView<'_>, window: &mut MainWindow) {
use FocussedPane::*;
self.focussed = Main;
if let Some(glob_source) = &self.glob_navigation {
tree_view.tree_mut().remove_node(glob_source.tree_root);
}
self.glob_navigation = None;
window.glob_pane = None;
tree_view.glob_tree_root.take();
self.entries = tree_view.sorted_entries(
self.navigation().view_root,
self.sorting,
self.entry_check(),
);
}
}
enum Refresh {
AllInView,
Selected,
}
struct FunctionWidget<F>
where
F: FnOnce(Rect, &mut Buffer),
{
render: F,
}
impl<F> FunctionWidget<F>
where
F: FnOnce(Rect, &mut Buffer),
{
fn new(function: F) -> FunctionWidget<F>
where
F: FnOnce(Rect, &mut Buffer),
{
FunctionWidget { render: function }
}
}
impl<F> Widget for FunctionWidget<F>
where
F: FnOnce(Rect, &mut Buffer),
{
fn render(self, area: Rect, buf: &mut Buffer) {
(self.render)(area, buf);
}
}
pub fn draw_window<B>(
window: &mut MainWindow,
props: MainWindowProps<'_>,
terminal: &mut Terminal<B>,
cursor: &mut Cursor,
) -> Result<()>
where
B: Backend,
{
terminal.draw(|frame| {
frame.render_widget(
FunctionWidget::new(|area, buf| {
window.render(props, area, buf, cursor);
}),
frame.size(),
);
})?;
Ok(())
}
pub fn refresh_key() -> KeyEvent {
KeyEvent::new(KeyCode::Char('\r'), KeyModifiers::ALT)
}