summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPiotr Wach <pwach@bloomberg.net>2023-11-23 22:50:51 +0000
committerPiotr Wach <pwach@bloomberg.net>2023-11-24 18:26:24 +0000
commit2bd06be9ee5ad8e1a747544899b299a53a950940 (patch)
tree1ca59bbaf5be20329f45198d29a6494abe87a46e
parentadebd00daa409da67d2f252b966e2dba632acda3 (diff)
Adds keybinding 'm' to toggle sorting by modified time
-rw-r--r--Cargo.lock19
-rw-r--r--Cargo.toml1
-rw-r--r--src/common.rs6
-rw-r--r--src/interactive/app/common.rs18
-rw-r--r--src/interactive/app/eventloop.rs1
-rw-r--r--src/interactive/app/handlers.rs5
-rw-r--r--src/interactive/app/tests/journeys_readonly.rs14
-rw-r--r--src/interactive/app/tests/utils.rs2
-rw-r--r--src/interactive/widgets/entries.rs46
-rw-r--r--src/interactive/widgets/footer.rs12
-rw-r--r--src/interactive/widgets/help.rs1
-rw-r--r--src/interactive/widgets/main.rs2
-rw-r--r--src/traverse.rs77
13 files changed, 169 insertions, 35 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 1f9c11a..9039ee1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 2f0a390..2c69c78 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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);