diff options
author | Piotr Wach <pwach@bloomberg.net> | 2023-12-08 10:49:13 +0000 |
---|---|---|
committer | Piotr Wach <pwach@bloomberg.net> | 2023-12-08 22:34:56 +0000 |
commit | 8df0b4c5dc5ee3f512f8812dff709a77cfb18f2f (patch) | |
tree | 552730382e6750ad37b4a955be79e2725a4cccbe | |
parent | bf4da4e1c4444fb490f85516efc518bb238e1652 (diff) |
Adds keybinding for 'c' to toggle sorting by number of items
-rw-r--r-- | Cargo.lock | 7 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/interactive/app/common.rs | 19 | ||||
-rw-r--r-- | src/interactive/app/eventloop.rs | 1 | ||||
-rw-r--r-- | src/interactive/app/handlers.rs | 5 | ||||
-rw-r--r-- | src/interactive/app/tests/journeys_readonly.rs | 4 | ||||
-rw-r--r-- | src/interactive/app/tests/utils.rs | 63 | ||||
-rw-r--r-- | src/interactive/widgets/entries.rs | 86 | ||||
-rw-r--r-- | src/interactive/widgets/footer.rs | 2 | ||||
-rw-r--r-- | src/interactive/widgets/help.rs | 1 | ||||
-rw-r--r-- | src/traverse.rs | 79 |
11 files changed, 184 insertions, 84 deletions
@@ -277,6 +277,7 @@ dependencies = [ "clap", "crosstermion", "filesize", + "human_format", "itertools", "jwalk", "num_cpus", @@ -361,6 +362,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] +name = "human_format" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86cce260d758a9aa3d7c4b99d55c815a540f8a37514ba6046ab6be402a157cb0" + +[[package]] name = "idna" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -38,6 +38,7 @@ tui-react = { version = "0.19.0", optional = true } open = { version = "5.0", optional = true } wild = "2.0.4" owo-colors = "3.5.0" +human_format = "1.0.3" [[bin]] name="dua" diff --git a/src/interactive/app/common.rs b/src/interactive/app/common.rs index 805adc2..c977f4c 100644 --- a/src/interactive/app/common.rs +++ b/src/interactive/app/common.rs @@ -11,6 +11,8 @@ pub enum SortMode { SizeAscending, MTimeAscending, MTimeDescending, + CountAscending, + CountDescending, } impl SortMode { @@ -19,18 +21,25 @@ impl SortMode { *self = match self { SizeDescending => SizeAscending, SizeAscending => SizeDescending, - MTimeAscending => SizeAscending, - MTimeDescending => SizeDescending, + _ => SizeAscending, } } pub fn toggle_mtime(&mut self) { use SortMode::*; *self = match self { - SizeDescending => MTimeDescending, - SizeAscending => MTimeAscending, MTimeAscending => MTimeDescending, MTimeDescending => MTimeAscending, + _ => MTimeAscending, + } + } + + pub fn toggle_count(&mut self) { + use SortMode::*; + *self = match self { + CountAscending => CountDescending, + CountDescending => CountAscending, + _ => CountAscending, } } } @@ -62,6 +71,8 @@ pub fn sorted_entries(tree: &Tree, node_idx: TreeIndex, sorting: SortMode) -> Ve SizeAscending => l.data.size.cmp(&r.data.size), MTimeAscending => l.data.mtime.cmp(&r.data.mtime), MTimeDescending => r.data.mtime.cmp(&l.data.mtime), + CountAscending => l.data.entry_count.cmp(&r.data.entry_count), + CountDescending => r.data.entry_count.cmp(&l.data.entry_count), }) .collect() } diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index a8108bc..0502ddd 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -149,6 +149,7 @@ impl AppState { Ctrl('d') | PageDown => self.change_entry_selection(CursorDirection::PageDown), Char('s') => self.cycle_sorting(traversal), Char('m') => self.cycle_mtime_sorting(traversal), + Char('c') => self.cycle_count_sorting(traversal), Char('g') => display.byte_vis.cycle(), _ => {} }, diff --git a/src/interactive/app/handlers.rs b/src/interactive/app/handlers.rs index 4948c95..d3761e9 100644 --- a/src/interactive/app/handlers.rs +++ b/src/interactive/app/handlers.rs @@ -161,6 +161,11 @@ impl AppState { self.entries = sorted_entries(&traversal.tree, self.root, self.sorting); } + pub fn cycle_count_sorting(&mut self, traversal: &Traversal) { + self.sorting.toggle_count(); + 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()); diff --git a/src/interactive/app/tests/journeys_readonly.rs b/src/interactive/app/tests/journeys_readonly.rs index c220a37..9bb4e27 100644 --- a/src/interactive/app/tests/journeys_readonly.rs +++ b/src/interactive/app/tests/journeys_readonly.rs @@ -67,14 +67,14 @@ fn simple_user_journey_read_only() -> Result<()> { app.process_events(&mut terminal, into_keys(b"m".iter()))?; assert_eq!( app.state.sorting, - SortMode::MTimeDescending, + SortMode::MTimeAscending, "it sets the sort mode to descending by mtime" ); // when hitting the M key again app.process_events(&mut terminal, into_keys(b"m".iter()))?; assert_eq!( app.state.sorting, - SortMode::MTimeAscending, + SortMode::MTimeDescending, "it sets the sort mode to ascending by mtime" ); // when hitting the S key diff --git a/src/interactive/app/tests/utils.rs b/src/interactive/app/tests/utils.rs index 7ca7382..68fc85b 100644 --- a/src/interactive/app/tests/utils.rs +++ b/src/interactive/app/tests/utils.rs @@ -224,32 +224,32 @@ pub fn sample_01_tree() -> Tree { let root_size = 1259070; #[cfg(windows)] let root_size = 1259069; - let rn = add_node("", root_size, None); + let rn = add_node("", root_size, 14, None); { - let sn = add_node(&fixture_str("sample-01"), root_size, Some(rn)); + let sn = add_node(&fixture_str("sample-01"), root_size, 13, Some(rn)); { - add_node(".hidden.666", 666, Some(sn)); - add_node("a", 256, Some(sn)); - add_node("b.empty", 0, Some(sn)); + add_node(".hidden.666", 666, 0, Some(sn)); + add_node("a", 256, 0, Some(sn)); + add_node("b.empty", 0, 0, Some(sn)); #[cfg(not(windows))] - add_node("c.lnk", 1, Some(sn)); + add_node("c.lnk", 1, 0, Some(sn)); #[cfg(windows)] - add_node("c.lnk", 0, Some(sn)); - let dn = add_node("dir", 1258024, Some(sn)); + add_node("c.lnk", 0, 0, Some(sn)); + let dn = add_node("dir", 1258024, 7, Some(sn)); { - add_node("1000bytes", 1000, Some(dn)); - add_node("dir-a.1mb", 1_000_000, Some(dn)); - add_node("dir-a.kb", 1024, Some(dn)); - let en = add_node("empty-dir", 0, Some(dn)); + add_node("1000bytes", 1000, 0, Some(dn)); + add_node("dir-a.1mb", 1_000_000, 0, Some(dn)); + add_node("dir-a.kb", 1024, 0, Some(dn)); + let en = add_node("empty-dir", 0, 1, Some(dn)); { - add_node(".gitkeep", 0, Some(en)); + add_node(".gitkeep", 0, 0, Some(en)); } - let sub = add_node("sub", 256_000, Some(dn)); + let sub = add_node("sub", 256_000, 1, Some(dn)); { - add_node("dir-sub-a.256kb", 256_000, Some(sub)); + add_node("dir-sub-a.256kb", 256_000, 0, Some(sub)); } } - add_node("z123.b", 123, Some(sn)); + add_node("z123.b", 123, 0, Some(sn)); } } } @@ -261,27 +261,28 @@ pub fn sample_02_tree() -> Tree { { let mut add_node = make_add_node(&mut tree); let root_size = 1540; - let rn = add_node("", root_size, None); + let rn = add_node("", root_size, 10, None); { let sn = add_node( Path::new(FIXTURE_PATH).join("sample-02").to_str().unwrap(), root_size, + 9, Some(rn), ); { - add_node("a", 256, Some(sn)); - add_node("b", 1, Some(sn)); - let dn = add_node("dir", 1283, Some(sn)); + add_node("a", 256, 0, Some(sn)); + add_node("b", 1, 0, Some(sn)); + let dn = add_node("dir", 1283, 6, Some(sn)); { - add_node("c", 257, Some(dn)); - add_node("d", 2, Some(dn)); - let en = add_node("empty-dir", 0, Some(dn)); + add_node("c", 257, 0, Some(dn)); + add_node("d", 2, 0, Some(dn)); + let en = add_node("empty-dir", 0, 1, Some(dn)); { - add_node(".gitkeep", 0, Some(en)); + add_node(".gitkeep", 0, 0, Some(en)); } - let sub = add_node("sub", 1024, Some(dn)); + let sub = add_node("sub", 1024, 1, Some(dn)); { - add_node("e", 1024, Some(sub)); + add_node("e", 1024, 0, Some(sub)); } } } @@ -290,13 +291,15 @@ pub fn sample_02_tree() -> Tree { tree } -pub fn make_add_node(t: &mut Tree) -> impl FnMut(&str, u128, Option<NodeIndex>) -> NodeIndex + '_ { - move |name, size, maybe_from_idx| { +pub fn make_add_node( + t: &mut Tree, +) -> impl FnMut(&str, u128, u64, Option<NodeIndex>) -> NodeIndex + '_ { + move |name, size, entry_count, maybe_from_idx| { let n = t.add_node(EntryData { name: PathBuf::from(name), size, - mtime: UNIX_EPOCH, - metadata_io_error: false, + entry_count, + ..Default::default() }); if let Some(from) = maybe_from_idx { t.add_edge(from, n, ()); diff --git a/src/interactive/widgets/entries.rs b/src/interactive/widgets/entries.rs index 4df8efb..8db9409 100644 --- a/src/interactive/widgets/entries.rs +++ b/src/interactive/widgets/entries.rs @@ -5,6 +5,7 @@ use crate::interactive::{ }; use chrono::DateTime; use dua::traverse::{EntryData, Tree, TreeIndex}; +use human_format; use itertools::Itertools; use std::time::SystemTime; use std::{borrow::Borrow, path::Path}; @@ -89,15 +90,24 @@ impl Entries { let mut columns = Vec::new(); if should_show_mtime_column(sort_mode) { - columns.push(mtime_column(entry_data.mtime, *sort_mode, text_style)); + columns.push(mtime_column( + entry_data.mtime, + column_style(Column::MTime, *sort_mode, text_style), + )); } columns.push(bytes_column( *display, entry_data.size, - *sort_mode, - text_style, + column_style(Column::Bytes, *sort_mode, text_style), )); columns.push(percentage_column(*display, fraction, percentage_style)); + if should_show_count_column(sort_mode) { + columns.push(count_column( + entry_data.entry_count, + *is_dir, + column_style(Column::Count, *sort_mode, text_style), + )); + } columns.push(name_column( &entry_data.name, *is_dir, @@ -229,18 +239,27 @@ fn columns_with_separators(columns: Vec<Span<'_>>, style: Style) -> Vec<Span<'_> columns_with_separators } -fn mtime_column(entry_mtime: SystemTime, sort_mode: SortMode, style: Style) -> Span<'static> { +fn mtime_column(entry_mtime: SystemTime, style: Style) -> Span<'static> { let datetime = DateTime::<chrono::Utc>::from(entry_mtime); let formatted_time = datetime.format("%d/%m/%Y %H:%M:%S").to_string(); + Span::styled(format!("{:>20}", formatted_time), style) +} + +fn count_column(entry_count: u64, is_dir: bool, style: Style) -> Span<'static> { + let count_in_units = human_format::Formatter::new() + .with_decimals(0) + .with_separator("") + .format(entry_count as f64); Span::styled( - format!("{:>20}", formatted_time), - Style { - fg: match sort_mode { - SortMode::SizeAscending | SortMode::SizeDescending => style.fg, - SortMode::MTimeAscending | SortMode::MTimeDescending => Color::Green.into(), - }, - ..style - }, + format!( + "{:>4}", + if is_dir { + count_in_units + } else { + "".to_string() + } + ), + style, ) } @@ -279,31 +298,48 @@ fn percentage_column(display: DisplayOptions, fraction: f32, style: Style) -> Sp Span::styled(format!("{}", display.byte_vis.display(fraction)), style) } -fn bytes_column( - display: DisplayOptions, - entry_size: u128, - sort_mode: SortMode, - style: Style, -) -> Span<'static> { +fn bytes_column(display: DisplayOptions, entry_size: u128, style: Style) -> Span<'static> { Span::styled( format!( "{:>byte_column_width$}", display.byte_format.display(entry_size).to_string(), // we would have to impl alignment/padding ourselves otherwise... byte_column_width = display.byte_format.width() ), - Style { - fg: match sort_mode { - SortMode::SizeAscending | SortMode::SizeDescending => Color::Green.into(), - SortMode::MTimeAscending | SortMode::MTimeDescending => style.fg, - }, - ..style - }, + style, ) } +#[derive(PartialEq)] +enum Column { + Bytes, + MTime, + Count, +} + +fn column_style(column: Column, sort_mode: SortMode, style: Style) -> Style { + Style { + fg: match (sort_mode, column) { + (SortMode::SizeAscending | SortMode::SizeDescending, Column::Bytes) + | (SortMode::MTimeAscending | SortMode::MTimeDescending, Column::MTime) + | (SortMode::CountAscending | SortMode::CountDescending, Column::Count) => { + Color::Green.into() + } + _ => style.fg, + }, + ..style + } +} + fn should_show_mtime_column(sort_mode: &SortMode) -> bool { matches!( sort_mode, SortMode::MTimeAscending | SortMode::MTimeDescending ) } + +fn should_show_count_column(sort_mode: &SortMode) -> bool { + matches!( + sort_mode, + SortMode::CountAscending | SortMode::CountDescending + ) +} diff --git a/src/interactive/widgets/footer.rs b/src/interactive/widgets/footer.rs index 6e6413b..911a266 100644 --- a/src/interactive/widgets/footer.rs +++ b/src/interactive/widgets/footer.rs @@ -43,6 +43,8 @@ impl Footer { SortMode::SizeDescending => "size descending", SortMode::MTimeAscending => "modified ascending", SortMode::MTimeDescending => "modified descending", + SortMode::CountAscending => "items ascending", + SortMode::CountDescending => "items descending", }, match total_bytes { Some(b) => format!("{}", format.display(*b)), diff --git a/src/interactive/widgets/help.rs b/src/interactive/widgets/help.rs index c1144c7..82d3725 100644 --- a/src/interactive/widgets/help.rs +++ b/src/interactive/widgets/help.rs @@ -134,6 +134,7 @@ impl HelpPane { { hotkey("s", "toggle sort by size ascending/descending", None); hotkey("m", "toggle sort by mtime ascending/descending", None); + hotkey("c", "toggle sort by items ascending/descending", None); hotkey( "g", "cycle through percentage display and bar options", diff --git a/src/traverse.rs b/src/traverse.rs index d0f5cc6..a157994 100644 --- a/src/traverse.rs +++ b/src/traverse.rs @@ -19,6 +19,7 @@ pub struct EntryData { /// The entry's size in bytes. If it's a directory, the size is the aggregated file size of all children pub size: u128, pub mtime: SystemTime, + pub entry_count: u64, /// If set, the item meta-data could not be obtained pub metadata_io_error: bool, } @@ -29,6 +30,7 @@ impl Default for EntryData { name: PathBuf::default(), size: u128::default(), mtime: UNIX_EPOCH, + entry_count: u64::default(), metadata_io_error: bool::default(), } } @@ -39,6 +41,7 @@ impl fmt::Debug for EntryData { f.debug_struct("EntryData") .field("name", &self.name) .field("size", &self.size) + .field("entry_count", &self.entry_count) // Skip mtime .field("metadata_io_error", &self.metadata_io_error) .finish() @@ -70,17 +73,28 @@ impl Traversal { input: Vec<PathBuf>, mut update: impl FnMut(&mut Traversal) -> Result<bool>, ) -> Result<Option<Traversal>> { - fn set_size_or_panic(tree: &mut Tree, node_idx: TreeIndex, current_size_at_depth: u128) { - tree.node_weight_mut(node_idx) - .expect("node for parent index we just retrieved") - .size = current_size_at_depth; + #[derive(Default)] + struct DirectoryInfo { + size: u128, + entries_count: u64, + } + fn set_directory_info_or_panic( + tree: &mut Tree, + node_idx: TreeIndex, + current_directory_at_depth: &DirectoryInfo, + ) { + let node = tree + .node_weight_mut(node_idx) + .expect("node for parent index we just retrieved"); + node.size = current_directory_at_depth.size; + node.entry_count = current_directory_at_depth.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<u128>) -> u128 { + fn pop_or_panic(v: &mut Vec<DirectoryInfo>) -> DirectoryInfo { v.pop().expect("sizes per level to be in sync with graph") } @@ -99,8 +113,8 @@ impl Traversal { }; let (mut previous_node_idx, mut parent_node_idx) = (t.root_index, t.root_index); - let mut sizes_per_depth_level = Vec::new(); - let mut current_size_at_depth: u128 = 0; + let mut directory_info_per_depth_level = Vec::new(); + let mut current_directory_at_depth = DirectoryInfo::default(); let mut previous_depth = 0; let mut inodes = InodeFilter::default(); @@ -183,30 +197,39 @@ impl Traversal { match (entry.depth, previous_depth) { (n, p) if n > p => { - sizes_per_depth_level.push(current_size_at_depth); - current_size_at_depth = file_size; + directory_info_per_depth_level.push(current_directory_at_depth); + current_directory_at_depth = DirectoryInfo { + size: file_size, + entries_count: 1, + }; parent_node_idx = previous_node_idx; } (n, p) if n < p => { for _ in n..p { - set_size_or_panic( + set_directory_info_or_panic( &mut t.tree, parent_node_idx, - current_size_at_depth, + ¤t_directory_at_depth, ); - current_size_at_depth += - pop_or_panic(&mut sizes_per_depth_level); + let dir_info = + pop_or_panic(&mut directory_info_per_depth_level); + current_directory_at_depth.size += dir_info.size; + current_directory_at_depth.entries_count += + dir_info.entries_count; + parent_node_idx = parent_or_panic(&mut t.tree, parent_node_idx); } - current_size_at_depth += file_size; - set_size_or_panic( + current_directory_at_depth.size += file_size; + current_directory_at_depth.entries_count += 1; + set_directory_info_or_panic( &mut t.tree, parent_node_idx, - current_size_at_depth, + ¤t_directory_at_depth, ); } _ => { - current_size_at_depth += file_size; + current_directory_at_depth.size += file_size; + current_directory_at_depth.entries_count += 1; } }; @@ -235,15 +258,25 @@ impl Traversal { } } - sizes_per_depth_level.push(current_size_at_depth); - current_size_at_depth = 0; + directory_info_per_depth_level.push(current_directory_at_depth); + current_directory_at_depth = DirectoryInfo::default(); for _ in 0..previous_depth { - current_size_at_depth += pop_or_panic(&mut sizes_per_depth_level); - set_size_or_panic(&mut t.tree, parent_node_idx, current_size_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.entries_count += dir_info.entries_count; + + set_directory_info_or_panic(&mut t.tree, parent_node_idx, ¤t_directory_at_depth); parent_node_idx = parent_or_panic(&mut t.tree, parent_node_idx); } let root_size = t.recompute_root_size(); - set_size_or_panic(&mut t.tree, t.root_index, root_size); + set_directory_info_or_panic( + &mut t.tree, + t.root_index, + &DirectoryInfo { + size: root_size, + entries_count: t.entries_traversed, + }, + ); t.total_bytes = Some(root_size); t.elapsed = Some(t.start.elapsed()); @@ -266,7 +299,7 @@ mod tests { fn size_of_entry_data() { assert_eq!( std::mem::size_of::<EntryData>(), - 64, + if cfg!(target_os = "macos") { 80 } else { 72 }, "the size of this should not change unexpectedly as it affects overall memory consumption" ); } |