diff options
author | Piotr Wach <pwach@bloomberg.net> | 2023-11-23 22:50:51 +0000 |
---|---|---|
committer | Piotr Wach <pwach@bloomberg.net> | 2023-11-24 18:26:24 +0000 |
commit | 2bd06be9ee5ad8e1a747544899b299a53a950940 (patch) | |
tree | 1ca59bbaf5be20329f45198d29a6494abe87a46e | |
parent | adebd00daa409da67d2f252b966e2dba632acda3 (diff) |
Adds keybinding 'm' to toggle sorting by modified time
-rw-r--r-- | Cargo.lock | 19 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/common.rs | 6 | ||||
-rw-r--r-- | src/interactive/app/common.rs | 18 | ||||
-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 | 14 | ||||
-rw-r--r-- | src/interactive/app/tests/utils.rs | 2 | ||||
-rw-r--r-- | src/interactive/widgets/entries.rs | 46 | ||||
-rw-r--r-- | src/interactive/widgets/footer.rs | 12 | ||||
-rw-r--r-- | src/interactive/widgets/help.rs | 1 | ||||
-rw-r--r-- | src/interactive/widgets/main.rs | 2 | ||||
-rw-r--r-- | src/traverse.rs | 77 |
13 files changed, 169 insertions, 35 deletions
@@ -102,6 +102,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "num-traits", +] + +[[package]] name = "clap" version = "4.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -264,6 +273,7 @@ dependencies = [ "anyhow", "atty", "byte-unit", + "chrono", "clap", "crosstermion", "filesize", @@ -461,6 +471,15 @@ dependencies = [ ] [[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -28,6 +28,7 @@ num_cpus = "1.10.0" filesize = "0.2.0" anyhow = "1.0.31" trash = { version = "3.0.0", optional = true, default-features = false, features = ["coinit_apartmentthreaded"] } +chrono = { version = "0.4.31", default-features = false, features = ["std"] } # 'tui' related unicode-segmentation = { version = "1.3.0", optional = true } diff --git a/src/common.rs b/src/common.rs index ccf3652..9b442c4 100644 --- a/src/common.rs +++ b/src/common.rs @@ -192,9 +192,7 @@ impl WalkOptions { if let Ok(dir_entry) = dir_entry_result { let metadata = dir_entry.metadata(); - if dir_entry.file_type.is_file() || dir_entry.file_type().is_symlink() { - dir_entry.client_state = Some(metadata); - } else if dir_entry.file_type.is_dir() { + if dir_entry.file_type.is_dir() { let ok_for_fs = cross_filesystems || metadata .as_ref() @@ -204,6 +202,8 @@ impl WalkOptions { dir_entry.read_children_path = None; } } + + dir_entry.client_state = Some(metadata); } }) } diff --git a/src/interactive/app/common.rs b/src/interactive/app/common.rs index b44874a..805adc2 100644 --- a/src/interactive/app/common.rs +++ b/src/interactive/app/common.rs @@ -9,14 +9,28 @@ pub enum SortMode { #[default] SizeDescending, SizeAscending, + MTimeAscending, + MTimeDescending, } impl SortMode { pub fn toggle_size(&mut self) { use SortMode::*; *self = match self { - SizeAscending => SizeDescending, SizeDescending => SizeAscending, + SizeAscending => SizeDescending, + MTimeAscending => SizeAscending, + MTimeDescending => SizeDescending, + } + } + + pub fn toggle_mtime(&mut self) { + use SortMode::*; + *self = match self { + SizeDescending => MTimeDescending, + SizeAscending => MTimeAscending, + MTimeAscending => MTimeDescending, + MTimeDescending => MTimeAscending, } } } @@ -46,6 +60,8 @@ pub fn sorted_entries(tree: &Tree, node_idx: TreeIndex, sorting: SortMode) -> Ve .sorted_by(|l, r| match sorting { SizeDescending => r.data.size.cmp(&l.data.size), SizeAscending => l.data.size.cmp(&r.data.size), + MTimeAscending => l.data.mtime.cmp(&r.data.mtime), + MTimeDescending => r.data.mtime.cmp(&l.data.mtime), }) .collect() } diff --git a/src/interactive/app/eventloop.rs b/src/interactive/app/eventloop.rs index fb4faa6..478a5b2 100644 --- a/src/interactive/app/eventloop.rs +++ b/src/interactive/app/eventloop.rs @@ -148,6 +148,7 @@ impl AppState { Char('j') | Down => self.change_entry_selection(CursorDirection::Down), Ctrl('d') | PageDown => self.change_entry_selection(CursorDirection::PageDown), Char('s') => self.cycle_sorting(traversal), + Char('m') => self.cycle_mtime_sorting(traversal), Char('g') => display.byte_vis.cycle(), _ => {} }, diff --git a/src/interactive/app/handlers.rs b/src/interactive/app/handlers.rs index eae0e6c..4cc1741 100644 --- a/src/interactive/app/handlers.rs +++ b/src/interactive/app/handlers.rs @@ -156,6 +156,11 @@ impl AppState { self.entries = sorted_entries(&traversal.tree, self.root, self.sorting); } + pub fn cycle_mtime_sorting(&mut self, traversal: &Traversal) { + self.sorting.toggle_mtime(); + 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 76e824e..c220a37 100644 --- a/src/interactive/app/tests/journeys_readonly.rs +++ b/src/interactive/app/tests/journeys_readonly.rs @@ -63,6 +63,20 @@ fn simple_user_journey_read_only() -> Result<()> { // SORTING { + // when hitting the M key + app.process_events(&mut terminal, into_keys(b"m".iter()))?; + assert_eq!( + app.state.sorting, + SortMode::MTimeDescending, + "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, + "it sets the sort mode to ascending by mtime" + ); // when hitting the S key app.process_events(&mut terminal, into_keys(b"s".iter()))?; assert_eq!( diff --git a/src/interactive/app/tests/utils.rs b/src/interactive/app/tests/utils.rs index 2882f6d..7ca7382 100644 --- a/src/interactive/app/tests/utils.rs +++ b/src/interactive/app/tests/utils.rs @@ -13,6 +13,7 @@ use std::{ fs::{copy, create_dir_all, remove_dir, remove_file}, io::ErrorKind, path::{Path, PathBuf}, + time::UNIX_EPOCH, }; use tui::backend::TestBackend; use tui_react::Terminal; @@ -294,6 +295,7 @@ pub fn make_add_node(t: &mut Tree) -> impl FnMut(&str, u128, Option<NodeIndex>) let n = t.add_node(EntryData { name: PathBuf::from(name), size, + mtime: UNIX_EPOCH, metadata_io_error: false, }); if let Some(from) = maybe_from_idx { diff --git a/src/interactive/widgets/entries.rs b/src/interactive/widgets/entries.rs index 3f39a73..aff6c3d 100644 --- a/src/interactive/widgets/entries.rs +++ b/src/interactive/widgets/entries.rs @@ -1,8 +1,9 @@ use crate::interactive::{ path_of, widgets::{entry_color, EntryMarkMap}, - DisplayOptions, EntryDataBundle, + DisplayOptions, EntryDataBundle, SortMode, }; +use chrono::DateTime; use dua::traverse::{Tree, TreeIndex}; use itertools::Itertools; use std::{borrow::Borrow, path::Path}; @@ -29,6 +30,7 @@ pub struct EntriesProps<'a> { pub marked: Option<&'a EntryMarkMap>, pub border_style: Style, pub is_focussed: bool, + pub sort_mode: SortMode, } #[derive(Default)] @@ -52,6 +54,7 @@ impl Entries { marked, border_style, is_focussed, + sort_mode, } = props.borrow(); let list = &mut self.list; @@ -114,6 +117,31 @@ impl Entries { style.add_modifier.insert(Modifier::BOLD); } + let fraction = w.size as f32 / total as f32; + let should_avoid_showing_a_big_reversed_bar = fraction > 0.9; + let local_style = if should_avoid_showing_a_big_reversed_bar { + style.remove_modifier(Modifier::REVERSED) + } else { + style + }; + + let datetime = DateTime::<chrono::Utc>::from(w.mtime); + let formatted_time = datetime.format("%d/%m/%Y %H:%M:%S").to_string(); + let mtime = 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 + }, + ); + + let bar = Span::styled(" | ", local_style); + let bytes = Span::styled( format!( "{:>byte_column_width$}", @@ -121,17 +149,15 @@ impl Entries { byte_column_width = display.byte_format.width() ), Style { - fg: Color::Green.into(), + fg: match sort_mode { + SortMode::SizeAscending | SortMode::SizeDescending => { + Color::Green.into() + } + SortMode::MTimeAscending | SortMode::MTimeDescending => style.fg, + }, ..style }, ); - let fraction = w.size as f32 / total as f32; - let should_avoid_showing_a_big_reversed_bar = fraction > 0.9; - let local_style = if should_avoid_showing_a_big_reversed_bar { - style.remove_modifier(Modifier::REVERSED) - } else { - style - }; let left_bar = Span::styled(" |", local_style); let percentage = Span::styled( @@ -160,7 +186,7 @@ impl Entries { Style { fg, ..style } }, ); - vec![bytes, left_bar, percentage, right_bar, name] + vec![mtime, bar, bytes, left_bar, percentage, right_bar, name] }, ); diff --git a/src/interactive/widgets/footer.rs b/src/interactive/widgets/footer.rs index 453dc9a..6e6413b 100644 --- a/src/interactive/widgets/footer.rs +++ b/src/interactive/widgets/footer.rs @@ -9,6 +9,8 @@ use tui::{ widgets::{Paragraph, Widget}, }; +use crate::interactive::SortMode; + pub struct Footer; pub struct FooterProps { @@ -18,6 +20,7 @@ pub struct FooterProps { pub elapsed: Option<std::time::Duration>, pub format: ByteFormat, pub message: Option<String>, + pub sort_mode: SortMode, } impl Footer { @@ -29,11 +32,18 @@ impl Footer { traversal_start, format, message, + sort_mode, } = props.borrow(); let spans = vec![ Span::from(format!( - " Total disk usage: {} Entries: {} {progress} ", + "Sort mode: {} Total disk usage: {} Entries: {} {progress} ", + match sort_mode { + SortMode::SizeAscending => "size ascending", + SortMode::SizeDescending => "size descending", + SortMode::MTimeAscending => "modified ascending", + SortMode::MTimeDescending => "modified descending", + }, match total_bytes { Some(b) => format!("{}", format.display(*b)), None => "-".to_owned(), diff --git a/src/interactive/widgets/help.rs b/src/interactive/widgets/help.rs index 8bcd202..c1144c7 100644 --- a/src/interactive/widgets/help.rs +++ b/src/interactive/widgets/help.rs @@ -133,6 +133,7 @@ impl HelpPane { title("Keys for display"); { hotkey("s", "toggle sort by size ascending/descending", None); + hotkey("m", "toggle sort by mtime ascending/descending", None); hotkey( "g", "cycle through percentage display and bar options", diff --git a/src/interactive/widgets/main.rs b/src/interactive/widgets/main.rs index 2c9c903..2b5d2c3 100644 --- a/src/interactive/widgets/main.rs +++ b/src/interactive/widgets/main.rs @@ -131,6 +131,7 @@ impl MainWindow { selected: state.selected, border_style: entries_style, is_focussed: matches!(state.focussed, Main), + sort_mode: state.sorting, }; self.entries_pane.render(props, entries_area, buf); @@ -142,6 +143,7 @@ impl MainWindow { message: state.message.clone(), traversal_start: *start, elapsed: *elapsed, + sort_mode: state.sorting, }, footer_area, buf, diff --git a/src/traverse.rs b/src/traverse.rs index b4fbcff..6045d58 100644 --- a/src/traverse.rs +++ b/src/traverse.rs @@ -3,24 +3,48 @@ use anyhow::Result; use filesize::PathExt; use petgraph::{graph::NodeIndex, stable_graph::StableGraph, Directed, Direction}; use std::{ + fmt, fs::Metadata, io, path::{Path, PathBuf}, - time::Duration, + time::{Duration, SystemTime, UNIX_EPOCH}, }; pub type TreeIndex = NodeIndex; pub type Tree = StableGraph<EntryData, (), Directed>; -#[derive(Eq, PartialEq, Debug, Default, Clone)] +#[derive(Eq, PartialEq, Clone)] pub struct EntryData { pub name: PathBuf, /// 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, /// If set, the item meta-data could not be obtained pub metadata_io_error: bool, } +impl EntryData { + pub fn default() -> EntryData { + EntryData { + name: PathBuf::default(), + size: u128::default(), + mtime: UNIX_EPOCH, + metadata_io_error: bool::default(), + } + } +} + +impl fmt::Debug for EntryData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("EntryData") + .field("name", &self.name) + .field("size", &self.size) + // Skip mtime + .field("metadata_io_error", &self.metadata_io_error) + .finish() + } +} + /// The result of the previous filesystem traversal #[derive(Debug)] pub struct Traversal { @@ -117,33 +141,45 @@ impl Traversal { } else { entry.file_name.into() }; - let file_size = match &entry.client_state { - Some(Ok(ref m)) + + let mut file_size = 0u128; + let mut mtime: SystemTime = UNIX_EPOCH; + match &entry.client_state { + Some(Ok(ref m)) => { if !m.is_dir() && (walk_options.count_hard_links || inodes.add(m)) && (walk_options.cross_filesystems - || crossdev::is_same_device(device_id, m)) => - { - if walk_options.apparent_size { - m.len() - } else { - size_on_disk(&entry.parent_path, &data.name, m).unwrap_or_else( - |_| { - t.io_errors += 1; - data.metadata_io_error = true; - 0 - }, - ) + || crossdev::is_same_device(device_id, m)) + { + if walk_options.apparent_size { + file_size = m.len() as u128; + } else { + file_size = size_on_disk(&entry.parent_path, &data.name, m) + .unwrap_or_else(|_| { + t.io_errors += 1; + data.metadata_io_error = true; + 0 + }) + as u128; + } + } + + match m.modified() { + Ok(modified) => { + mtime = modified; + } + Err(_) => { + t.io_errors += 1; + data.metadata_io_error = true; + } } } - Some(Ok(_)) => 0, Some(Err(_)) => { t.io_errors += 1; data.metadata_io_error = true; - 0 } - None => 0, // a directory - } as u128; + None => {} + } match (entry.depth, previous_depth) { (n, p) if n > p => { @@ -174,6 +210,7 @@ impl Traversal { } }; + data.mtime = mtime; data.size = file_size; let entry_index = t.tree.add_node(data); |