use crate::interactive::{ app::FocussedPane::*, path_of, sorted_entries, widgets::{HelpPane, MainWindow, MarkMode, MarkPane}, AppState, DisplayOptions, EntryDataBundle, }; use crosstermion::input::Key; use dua::traverse::{Traversal, TreeIndex}; use itertools::Itertools; use petgraph::{visit::Bfs, Direction}; use std::{fs, io, path::PathBuf}; use tui::backend::Backend; use tui_react::Terminal; #[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, traversal: &Traversal) { if let Some(idx) = self.selected { open::that(path_of(&traversal.tree, idx)).ok(); } } pub fn exit_node_with_traversal(&mut self, traversal: &Traversal) { let entries = self.entries_for_exit_node(traversal); self.exit_node(entries); } fn entries_for_exit_node( &self, traversal: &Traversal, ) -> Option<(TreeIndex, Vec)> { traversal .tree .neighbors_directed(self.root, Direction::Incoming) .next() .map(|parent_idx| { ( parent_idx, sorted_entries(&traversal.tree, parent_idx, self.sorting), ) }) } pub fn exit_node(&mut self, entries: Option<(TreeIndex, Vec)>) { match entries { Some((parent_idx, entries)) => { self.root = parent_idx; self.entries = entries; self.selected = self .bookmarks .get(&parent_idx) .copied() .or_else(|| self.entries.get(0).map(|b| b.index)); } None => self.message = Some("Top level reached".into()), } } fn entries_for_enter_node( &self, traversal: &Traversal, ) -> Option<(TreeIndex, Vec)> { self.selected.map(|previously_selected| { ( previously_selected, sorted_entries(&traversal.tree, previously_selected, self.sorting), ) }) } pub fn enter_node_with_traversal(&mut self, traversal: &Traversal) { let new_entries = self.entries_for_enter_node(traversal); self.enter_node(new_entries) } pub fn enter_node(&mut self, entries_at_selected: Option<(TreeIndex, Vec)>) { if let Some((previously_selected, new_entries)) = entries_at_selected { match new_entries.get( self.bookmarks .get(&previously_selected) .and_then(|selected| { new_entries .iter() .find_position(|b| b.index == *selected) .map(|(pos, _)| pos) }) .unwrap_or(0), ) { Some(b) => { self.bookmarks.insert(self.root, previously_selected); self.root = previously_selected; self.selected = Some(b.index); 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 entries = &self.entries; let next_selected_pos = match self.selected { Some(ref selected) => entries .iter() .find_position(|b| b.index == *selected) .map(|(idx, _)| direction.move_cursor(idx)) .unwrap_or(0), None => 0, }; self.selected = entries .get(next_selected_pos) .or_else(|| entries.last()) .map(|b| b.index) .or(self.selected); if let Some(selected) = self.selected { self.bookmarks.insert(self.root, selected); } } pub fn cycle_sorting(&mut self, traversal: &Traversal) { self.sorting.toggle_size(); self.entries = sorted_entries(&traversal.tree, self.root, self.sorting); } pub fn reset_message(&mut self) { if self.is_scanning { 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 => { 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) { (Main, Some(_), _) => Help, (Help, _, Some(ref mut pane)) => { pane.set_focus(true); Mark } (Help, _, None) => Main, (Mark, _, _) => Main, (Main, None, None) => Main, (Main, None, Some(ref mut pane)) => { pane.set_focus(true); Mark } }; } pub fn dispatch_to_mark_pane( &mut self, key: Key, window: &mut MainWindow, traversal: &mut Traversal, display: DisplayOptions, terminal: &mut Terminal, ) 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 entries...".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, traversal, display, terminal).ok(); pane = window.mark_pane.take().expect("option to be filled"); match self.delete_entry(entry_to_delete, traversal) { Ok(ed) => { entries_deleted += ed; self.message = Some(format!("Deleted {} entries...", entries_deleted)); Ok(pane) } Err(c) => Err((pane, c)), } }); self.message = None; res } Some(MarkMode::Trash) => { self.message = Some("Trashing entries...".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, traversal, display, terminal).ok(); pane = window.mark_pane.take().expect("option to be filled"); match self.trash_entry(entry_to_trash, traversal) { Ok(ed) => { entries_trashed += ed; self.message = Some(format!("Trashed {} entries...", entries_trashed)); 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, traversal: &mut Traversal, ) -> Result { let mut entries_deleted = 0; if let Some(_entry) = traversal.tree.node_weight(index) { let path_to_delete = path_of(&traversal.tree, index); delete_directory_recursively(path_to_delete)?; entries_deleted = self.delete_entries_in_traversal(index, traversal); } Ok(entries_deleted) } pub fn trash_entry( &mut self, index: TreeIndex, traversal: &mut Traversal, ) -> Result { let mut entries_deleted = 0; if let Some(_entry) = traversal.tree.node_weight(index) { let path_to_delete = path_of(&traversal.tree, index); if trash::delete(path_to_delete).is_err() { return Err(1); } entries_deleted = self.delete_entries_in_traversal(index, traversal); } Ok(entries_deleted) } pub fn delete_entries_in_traversal( &mut self, index: TreeIndex, traversal: &mut Traversal, ) -> usize { let mut entries_deleted = 0; let parent_idx = traversal .tree .neighbors_directed(index, Direction::Incoming) .next() .expect("us being unable to delete the root index"); let mut bfs = Bfs::new(&traversal.tree, index); while let Some(nx) = bfs.next(&traversal.tree) { traversal.tree.remove_node(nx); traversal.entries_traversed -= 1; entries_deleted += 1; } self.entries = sorted_entries(&traversal.tree, self.root, self.sorting); if traversal.tree.node_weight(self.root).is_none() { self.set_root(traversal.root_index, traversal); } if self .selected .and_then(|selected| self.entries.iter().find(|e| e.index == selected)) .is_none() { self.selected = self.entries.get(0).map(|e| e.index); } self.recompute_sizes_recursively(parent_idx, traversal); entries_deleted } fn set_root(&mut self, root: TreeIndex, traversal: &Traversal) { self.root = root; self.entries = sorted_entries(&traversal.tree, root, self.sorting); } fn recompute_sizes_recursively(&mut self, mut index: TreeIndex, traversal: &mut Traversal) { loop { traversal .tree .node_weight_mut(index) .expect("valid index") .size = traversal .tree .neighbors_directed(index, Direction::Outgoing) .filter_map(|idx| traversal.tree.node_weight(idx).map(|w| w.size)) .sum(); match traversal .tree .neighbors_directed(index, Direction::Incoming) .next() { None => break, Some(parent) => index = parent, } } traversal.total_bytes = traversal .tree .node_weight(traversal.root_index) .map(|w| w.size); } pub fn mark_entry( &mut self, cursor: CursorMode, mode: MarkEntryMode, window: &mut MainWindow, traversal: &Traversal, ) { if let Some(index) = self.selected { 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, &traversal.tree, is_dir, should_toggle); } else { window.mark_pane = MarkPane::default().toggle_index(index, &traversal.tree, is_dir, should_toggle) } }; if let CursorMode::Advance = cursor { self.change_entry_selection(CursorDirection::Down) } } } 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 } } // TODO: could use jwalk for this // see https://github.com/Byron/dua-cli/issues/43 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 { // do not follow symlinks 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::Other => { // assume file, save IOps 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) } }