use crate::interactive::{
DisplayOptions, EntryDataBundle,
app::tree_view::TreeView,
widgets::{Column, GlobPane, HelpPane, MainWindow, MarkMode, MarkPane},
};
use crossterm::event::KeyEvent;
use dua::Config;
use dua::traverse::TreeIndex;
use std::{fs, io, path::PathBuf};
use tui::{Terminal, backend::Backend};
use super::state::{AppState, FocussedPane::*};
#[derive(Copy, Clone)]
pub enum CursorMode {
Advance,
KeepPosition,
}
#[derive(Copy, Clone)]
pub enum MarkEntryMode {
Toggle,
MarkForDeletion,
}
pub enum CursorDirection {
PageDown,
Down,
Up,
PageUp,
ToTop,
ToBottom,
}
impl CursorDirection {
pub fn move_cursor(&self, n: usize) -> usize {
use CursorDirection::*;
match self {
ToTop => 0,
ToBottom => usize::MAX,
Down => n.saturating_add(1),
Up => n.saturating_sub(1),
PageDown => n.saturating_add(10),
PageUp => n.saturating_sub(10),
}
}
}
impl AppState {
pub fn open_that(&self, tree_view: &TreeView<'_>) {
if let Some(idx) = self.navigation().selected {
open::that(tree_view.path_of(idx)).ok();
}
}
pub fn exit_node_with_traversal(&mut self, tree_view: &TreeView<'_>) {
let entries = self.entries_for_exit_node(tree_view);
self.exit_node(entries);
}
fn entries_for_exit_node(
&self,
tree_view: &TreeView<'_>,
) -> Option<(TreeIndex, Vec<EntryDataBundle>)> {
tree_view
.view_parent_of(self.navigation().view_root)
.map(|parent_idx| {
(
parent_idx,
tree_view.sorted_entries(parent_idx, self.sorting, self.entry_check()),
)
})
}
pub fn exit_node(&mut self, entries: Option<(TreeIndex, Vec<EntryDataBundle>)>) {
match entries {
Some((parent_idx, entries)) => {
self.navigation_mut().exit_node(parent_idx, &entries);
self.entries = entries;
}
None => self.message = Some("Top level reached".into()),
}
}
fn entries_for_enter_node(
&self,
tree_view: &TreeView<'_>,
) -> Option<(TreeIndex, Vec<EntryDataBundle>)> {
self.navigation().selected.map(|previously_selected| {
(
previously_selected,
tree_view.sorted_entries(previously_selected, self.sorting, self.entry_check()),
)
})
}
pub fn enter_node_with_traversal(&mut self, tree_view: &TreeView<'_>) {
let new_entries = self.entries_for_enter_node(tree_view);
self.enter_node(new_entries)
}
pub fn enter_node(&mut self, entries_at_selected: Option<(TreeIndex, Vec<EntryDataBundle>)>) {
if let Some((previously_selected, new_entries)) = entries_at_selected {
match self
.navigation()
.previously_selected_index(previously_selected, &new_entries)
{
Some(selected) => {
self.navigation_mut()
.enter_node(previously_selected, selected);
self.entries = new_entries;
}
None => self.message = Some("Entry is a file or an empty directory".into()),
}
}
}
pub fn change_entry_selection(&mut self, direction: CursorDirection) {
let next_index = self.navigation().next_index(direction, &self.entries);
self.navigation_mut().select(next_index);
}
pub fn cycle_sorting(&mut self, tree_view: &TreeView<'_>) {
self.sorting.toggle_size();
self.entries = tree_view.sorted_entries(
self.navigation().view_root,
self.sorting,
self.entry_check(),
);
}
pub fn cycle_mtime_sorting(&mut self, tree_view: &TreeView<'_>) {
self.sorting.toggle_mtime();
self.entries = tree_view.sorted_entries(
self.navigation().view_root,
self.sorting,
self.entry_check(),
);
}
pub fn cycle_count_sorting(&mut self, tree_view: &TreeView<'_>) {
self.sorting.toggle_count();
self.entries = tree_view.sorted_entries(
self.navigation().view_root,
self.sorting,
self.entry_check(),
);
}
pub fn cycle_name_sorting(&mut self, tree_view: &TreeView<'_>) {
self.sorting.toggle_name();
self.entries = tree_view.sorted_entries(
self.navigation().view_root,
self.sorting,
self.entry_check(),
);
}
pub fn toggle_mtime_column(&mut self) {
self.toggle_column(Column::MTime);
}
pub fn toggle_count_column(&mut self) {
self.toggle_column(Column::Count);
}
fn toggle_column(&mut self, column: Column) {
if self.show_columns.contains(&column) {
self.show_columns.remove(&column);
} else {
self.show_columns.insert(column);
}
}
pub fn toggle_glob_search(&mut self, window: &mut MainWindow) {
self.focussed = match self.focussed {
Main | Mark | Help => {
window.glob_pane = Some(GlobPane::default());
Glob
}
Glob => unreachable!("BUG: glob pane must catch the input leading here"),
}
}
pub fn reset_message(&mut self) {
if self.scan.is_some() {
self.message = Some("-> scanning <-".into());
} else {
self.message = None;
}
}
pub fn toggle_help_pane(&mut self, window: &mut MainWindow) {
self.focussed = match self.focussed {
Main | Mark | Glob => {
window.help_pane = Some(HelpPane::default());
Help
}
Help => {
window.help_pane = None;
Main
}
}
}
pub fn cycle_focus(&mut self, window: &mut MainWindow) {
if let Some(p) = window.mark_pane.as_mut() {
p.set_focus(false)
};
self.focussed = match (
self.focussed,
&window.help_pane,
&mut window.mark_pane,
&mut window.glob_pane,
) {
(Main, Some(_), _, _) => Help,
(Help, _, Some(pane), _) => {
pane.set_focus(true);
Mark
}
(Help, _, _, Some(_)) => Glob,
(Help, _, None, None) => Main,
(Mark, _, _, Some(_)) => Glob,
(Mark, _, _, _) => Main,
(Main, None, None, None) => Main,
(Main, None, Some(pane), _) => {
pane.set_focus(true);
Mark
}
(Main, None, None, Some(_)) => Glob,
(Glob, _, _, _) => Main,
};
}
pub fn dispatch_to_mark_pane<B>(
&mut self,
key: KeyEvent,
window: &mut MainWindow,
tree_view: &mut TreeView<'_>,
display: DisplayOptions,
terminal: &mut Terminal<B>,
config: &Config,
) where
B: Backend,
{
let res = window.mark_pane.take().and_then(|p| p.process_events(key));
window.mark_pane = match res {
Some((pane, mode)) => match mode {
Some(MarkMode::Delete) => {
self.message = Some("Deleting items...".to_string());
let mut entries_deleted = 0;
let res = pane.iterate_deletable_items(|mut pane, entry_to_delete| {
window.mark_pane = Some(pane);
self.draw(window, tree_view, display, terminal, config).ok();
pane = window.mark_pane.take().expect("option to be filled");
match self.delete_entry(entry_to_delete, tree_view) {
Ok(ed) => {
entries_deleted += ed;
self.message = Some(format!("Deleted {entries_deleted} items..."));
Ok(pane)
}
Err(c) => Err((pane, c)),
}
});
self.message = None;
res
}
#[cfg(feature = "trash-move")]
Some(MarkMode::Trash) => {
self.message = Some("Trashing items...".to_string());
let mut entries_trashed = 0;
let res = pane.iterate_deletable_items(|mut pane, entry_to_trash| {
window.mark_pane = Some(pane);
self.draw(window, tree_view, display, terminal, config).ok();
pane = window.mark_pane.take().expect("option to be filled");
match self.trash_entry(entry_to_trash, tree_view) {
Ok(ed) => {
entries_trashed += ed;
self.message = Some(format!("Trashed {entries_trashed} items..."));
Ok(pane)
}
Err(c) => Err((pane, c)),
}
});
self.message = None;
res
}
None => Some(pane),
},
None => None,
};
if window.mark_pane.is_none() {
self.focussed = Main;
}
}
pub fn delete_entry(
&mut self,
index: TreeIndex,
tree_view: &mut TreeView<'_>,
) -> Result<usize, usize> {
let mut entries_deleted = 0;
if tree_view.exists(index) {
let path_to_delete = tree_view.path_of(index);
delete_directory_recursively(path_to_delete)?;
entries_deleted = self.delete_entries_in_traversal(index, tree_view);
}
Ok(entries_deleted)
}
#[cfg(feature = "trash-move")]
pub fn trash_entry(
&mut self,
index: TreeIndex,
tree_view: &mut TreeView<'_>,
) -> Result<usize, usize> {
let mut entries_deleted = 0;
if tree_view.exists(index) {
let path_to_delete = tree_view.path_of(index);
if trash::delete(path_to_delete).is_err() {
return Err(1);
}
entries_deleted = self.delete_entries_in_traversal(index, tree_view);
}
Ok(entries_deleted)
}
pub fn delete_entries_in_traversal(
&mut self,
index: TreeIndex,
tree_view: &mut TreeView<'_>,
) -> usize {
let parent_idx = tree_view
.fs_parent_of(index)
.expect("us being unable to delete the root index");
let entries_deleted =
tree_view.remove_entries(index, true );
if !tree_view.exists(self.navigation().view_root) {
self.go_to_root(tree_view);
} else {
self.entries = tree_view.sorted_entries(
self.navigation().view_root,
self.sorting,
self.entry_check(),
);
}
if self
.navigation()
.selected
.and_then(|selected| self.entries.iter().find(|e| e.index == selected))
.is_none()
{
let idx = self.entries.first().map(|e| e.index);
self.navigation_mut().select(idx);
}
tree_view.recompute_sizes_recursively(parent_idx);
entries_deleted
}
pub fn go_to_root(&mut self, tree_view: &TreeView<'_>) {
let root = self.navigation().tree_root;
let entries = tree_view.sorted_entries(root, self.sorting, self.entry_check());
self.navigation_mut().exit_node(root, &entries);
self.entries = entries;
}
pub fn glob_root(&self) -> Option<TreeIndex> {
self.glob_navigation.as_ref().map(|e| e.tree_root)
}
fn mark_entry_by_index(
&mut self,
index: TreeIndex,
mode: MarkEntryMode,
window: &mut MainWindow,
tree_view: &TreeView<'_>,
) {
let is_dir = self
.entries
.iter()
.find(|e| e.index == index)
.unwrap()
.is_dir;
let should_toggle = match mode {
MarkEntryMode::Toggle => true,
MarkEntryMode::MarkForDeletion => false,
};
if let Some(pane) = window.mark_pane.take() {
window.mark_pane = pane.toggle_index(index, tree_view, is_dir, should_toggle);
} else {
window.mark_pane =
MarkPane::default().toggle_index(index, tree_view, is_dir, should_toggle)
}
}
pub fn mark_entry(
&mut self,
cursor: CursorMode,
mode: MarkEntryMode,
window: &mut MainWindow,
tree_view: &TreeView<'_>,
) {
if let Some(index) = self.navigation().selected {
self.mark_entry_by_index(index, mode, window, tree_view);
};
if let CursorMode::Advance = cursor {
self.change_entry_selection(CursorDirection::Down)
}
}
pub fn mark_all_entries(
&mut self,
mode: MarkEntryMode,
window: &mut MainWindow,
tree_view: &TreeView<'_>,
) {
for index in self.entries.iter().map(|e| e.index).collect::<Vec<_>>() {
self.mark_entry_by_index(index, mode, window, tree_view);
}
}
}
fn into_error_count(res: Result<(), io::Error>) -> usize {
match res.map_err(io_err_to_usize) {
Ok(_) => 0,
Err(c) => c,
}
}
fn io_err_to_usize(err: io::Error) -> usize {
if err.kind() == io::ErrorKind::NotFound {
0
} else {
1
}
}
fn delete_directory_recursively(path: PathBuf) -> Result<(), usize> {
let mut files_or_dirs = vec![path];
let mut dirs = Vec::new();
let mut num_errors = 0;
while let Some(path) = files_or_dirs.pop() {
let assume_symlink_to_try_deletion = true;
let is_symlink = path
.symlink_metadata()
.map(|m| m.file_type().is_symlink())
.unwrap_or(assume_symlink_to_try_deletion);
if is_symlink {
num_errors += into_error_count(fs::remove_file(&path));
continue;
}
match fs::read_dir(&path) {
Ok(iterator) => {
dirs.push(path);
for entry in iterator {
match entry.map_err(io_err_to_usize) {
Ok(entry) => files_or_dirs.push(entry.path()),
Err(c) => num_errors += c,
}
}
}
Err(ref e) if e.kind() == io::ErrorKind::NotADirectory => {
num_errors += into_error_count(fs::remove_file(path));
continue;
}
Err(_) => {
num_errors += 1;
continue;
}
};
}
for dir in dirs.into_iter().rev() {
num_errors += into_error_count(fs::remove_dir(&dir).or_else(|_| fs::remove_file(dir)));
}
if num_errors == 0 {
Ok(())
} else {
Err(num_errors)
}
}