diff --git a/Cargo.lock b/Cargo.lock index bc25b3c5..b33781ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,6 +152,31 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "crossterm" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + [[package]] name = "cxx" version = "1.0.94" @@ -224,6 +249,7 @@ dependencies = [ "chrono", "clap", "clap_complete", + "crossterm", "dirs", "errno 0.3.1", "filesize", @@ -460,6 +486,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -485,6 +521,18 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.45.0", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -532,6 +580,29 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.45.0", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -648,6 +719,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "scratch" version = "1.0.5" @@ -660,6 +737,42 @@ version = "1.0.156" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "314b5b092c0ade17c00142951e50ced110ec27cea304b1037c6969246c2469a4" +[[package]] +name = "signal-hook" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + [[package]] name = "strip-ansi-escapes" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index e179b3fe..833553db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ ansi_term = "0.12.1" chrono = "0.4.24" clap = { version = "4.1.1", features = ["derive"] } clap_complete = "4.1.1" +crossterm = "0.26.1" dirs = "5.0" errno = "0.3.1" filesize = "0.2.0" diff --git a/src/context/mod.rs b/src/context/mod.rs index e4ec857f..ceb94bcc 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -180,6 +180,10 @@ pub struct Context { #[arg(long)] pub no_config: bool, + /// Hides the progress indicator + #[arg(long)] + pub no_progress: bool, + /// Omit disk usage from output #[arg(long)] pub suppress_size: bool, @@ -525,10 +529,17 @@ impl Context { } /// Setter for `window_width` which is set to the current terminal emulator's window width. + #[inline] pub fn set_window_width(&mut self) { self.window_width = crate::tty::get_window_width(self.stdout_is_tty); } + /// Answers whether disk usage is asked to be reported in bytes. + pub const fn byte_metric(&self) -> bool { + matches!(self.disk_usage, DiskUsage::Logical) + || matches!(self.disk_usage, DiskUsage::Physical) + } + /// Do any of the components of a path match the provided glob? This is used for ensuring that /// all children of a directory that a glob targets gets captured. #[inline] diff --git a/src/disk_usage/file_size/byte.rs b/src/disk_usage/file_size/byte.rs index b3baca34..62f52144 100644 --- a/src/disk_usage/file_size/byte.rs +++ b/src/disk_usage/file_size/byte.rs @@ -17,8 +17,8 @@ pub struct Metric { prefix_kind: PrefixKind, /// To prevent allocating the same string twice. We allocate the first time - /// in [`crate::tree::update_column_properties`] in order to compute the max column width for - /// human-readable size and the second time during the actual render. + /// in [`crate::tree::Tree::update_column_properties`] in order to compute the max column width for + /// human-readable size and cache it. It will then be used again when preparing the output. cached_display: RefCell, } diff --git a/src/main.rs b/src/main.rs index e33ae735..b73c8329 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,27 @@ #![cfg_attr(windows, feature(windows_by_handle))] #![warn( clippy::all, - clippy::correctness, - clippy::suspicious, - clippy::style, + clippy::cargo, clippy::complexity, - clippy::perf, - clippy::pedantic, + clippy::correctness, clippy::nursery, - clippy::cargo + clippy::pedantic, + clippy::perf, + clippy::style, + clippy::suspicious )] #![allow( - clippy::struct_excessive_bools, - clippy::too_many_arguments, + clippy::cast_possible_truncation, clippy::cast_precision_loss, clippy::cast_sign_loss, - clippy::cast_possible_truncation + clippy::let_underscore_untyped, + clippy::struct_excessive_bools, + clippy::too_many_arguments )] use clap::CommandFactory; use context::{layout, Context}; +use progress::Message; use render::{Engine, Flat, Inverted, Regular}; use std::{error::Error, io::stdout}; use tree::Tree; @@ -39,6 +41,9 @@ mod fs; /// All things related to icons on how to map certain files to the appropriate icons. mod icons; +/// Concerned with displaying a progress indicator when stdout is a tty. +mod progress; + /// Concerned with taking an initialized [`Tree`] and its [`Node`]s and rendering the output. /// /// [`Tree`]: tree::Tree @@ -68,22 +73,31 @@ fn main() -> Result<(), Box> { styles::init(ctx.no_color()); - let (tree, ctx) = Tree::try_init_and_update_context(ctx)?; + let indicator = (ctx.stdout_is_tty && !ctx.no_progress).then(progress::Indicator::measure); - match ctx.layout { + let (tree, ctx) = Tree::try_init_and_update_context(ctx, indicator.as_ref())?; + + let output = match ctx.layout { layout::Type::Flat => { let render = Engine::::new(tree, ctx); - println!("{render}"); + format!("{render}") } layout::Type::Inverted => { let render = Engine::::new(tree, ctx); - println!("{render}"); + format!("{render}") } layout::Type::Regular => { let render = Engine::::new(tree, ctx); - println!("{render}"); + format!("{render}") } + }; + + if let Some(progress) = indicator { + progress.mailbox().send(Message::RenderReady)?; + progress.join_handle.join().unwrap()?; } + println!("{output}"); + Ok(()) } diff --git a/src/progress.rs b/src/progress.rs new file mode 100644 index 00000000..f8a0da2b --- /dev/null +++ b/src/progress.rs @@ -0,0 +1,171 @@ +use crossterm::{ + cursor, + terminal::{self, ClearType}, + ExecutableCommand, +}; +use std::{ + io::{self, Write}, + sync::mpsc::{self, Sender}, + thread, +}; + +/// Responsible for displying the progress indicator. This struct will be owned by a separate +/// thread that is responsible for displaying the progress text whereas the [`IndicatorHandle`] +/// is how the outside world will interact with it. +pub struct Indicator { + count: u64, + stdout: io::Stdout, + state: IndicatorState, +} + +/// This struct is how the outside world will inform the [`Indicator`] about the progress of the +/// program. The `join_handle` returns the handle to the thread that owns the [`Indicator`] and the +/// `mailbox` is the [`Sender`] channel that allows [`Message`]s to be sent to [`Indicator`]. +pub struct IndicatorHandle { + pub join_handle: thread::JoinHandle>, + mailbox: Sender, +} + +/// The different messages that could be sent to the thread that owns the [`Indicator`]. +#[derive(Debug)] +pub enum Message { + /// Message that indicates that we are currently reading from disk and that a file was indexed. + Index, + + /// Message that indicates that we are done reading from disk and are preparing the output. + DoneIndexing, + + /// Message that indicates that the output is ready to be flushed and that we should cleanup + /// the [`Indicator`] as well as the screen. + RenderReady, +} + +/// All of the different states the [`Indicator`] can be in during its life cycle. +#[derive(Default)] +enum IndicatorState { + /// We are currently reading from disk. + #[default] + Indexing, + + /// No longer reading from disk; preparing output. + Rendering, + + /// Output is prepared and the [`Indicator`] is ready to be torn down. + Done, +} + +/// Errors associated with [`crossterm`]; +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(#[from] io::Error); + +impl Default for Indicator { + /// Default constructor for [`Indicator`]. + fn default() -> Self { + Self { + count: u64::default(), + stdout: io::stdout(), + state: IndicatorState::default(), + } + } +} + +impl IndicatorHandle { + /// The constructor for an [`IndicatorHandle`]. + pub fn new( + join_handle: thread::JoinHandle>, + mailbox: Sender, + ) -> Self { + Self { + join_handle, + mailbox, + } + } + + /// Getter for a cloned `mailbox` wherewith to send [`Message`]s to the [`Indicator`]. + pub fn mailbox(&self) -> Sender { + self.mailbox.clone() + } +} + +impl Indicator { + /// Initializes a worker thread that owns [`Indicator`] that awaits on [`Message`]s to traverse + /// through its internal states. An [`IndicatorHandle`] is returned as a mechanism to allow the + /// outside world to send messages to the worker thread and ultimately to the [`Indicator`]. + pub fn measure() -> IndicatorHandle { + let (tx, rx) = mpsc::channel::(); + + let join_handle = thread::spawn(move || -> Result<(), Error> { + let mut indicator = Self::default(); + + indicator.stdout.execute(cursor::SavePosition)?; + indicator.stdout.execute(cursor::Hide)?; + + while let Ok(msg) = rx.recv() { + if matches!(indicator.state, IndicatorState::Indexing) { + match msg { + Message::Index => indicator.index()?, + Message::DoneIndexing => { + indicator.update_state(IndicatorState::Rendering)?; + } + Message::RenderReady => (), + } + } + + if matches!(indicator.state, IndicatorState::Rendering) + && matches!(msg, Message::RenderReady) + { + indicator.update_state(IndicatorState::Done)?; + break; + } + indicator.stdout.execute(cursor::RestorePosition)?; + } + + Ok(()) + }); + + IndicatorHandle::new(join_handle, tx) + } + + /// Updates the `state` of the [`Indicator`] to `new_state`, immediately running an associated + /// side effect if applicable. + #[inline] + fn update_state(&mut self, new_state: IndicatorState) -> Result<(), Error> { + use IndicatorState::{Done, Indexing, Rendering}; + + match (&self.state, &new_state) { + (Indexing, Rendering) => { + let stdout = &mut self.stdout; + stdout.execute(terminal::Clear(ClearType::CurrentLine))?; + stdout.execute(cursor::RestorePosition)?; + self.rendering(); + } + + (Rendering, Done) => { + let stdout = &mut self.stdout; + stdout.execute(terminal::Clear(ClearType::CurrentLine))?; + stdout.execute(cursor::RestorePosition)?; + stdout.execute(cursor::Show)?; + } + _ => (), + } + + self.state = new_state; + + Ok(()) + } + + /// The user-facing output when the `state` of the [`Indicator`] is `Indexing`. + #[inline] + fn index(&mut self) -> Result<(), Error> { + self.count += 1; + write!(self.stdout, "Indexing {} files...", self.count)?; + Ok(()) + } + + /// The user-facing output when the `state` of the [`Indicator`] is `Rendering`. + #[inline] + fn rendering(&mut self) { + write!(self.stdout, "Preparing output...").unwrap(); + } +} diff --git a/src/tree/mod.rs b/src/tree/mod.rs index 113fcf3c..399de20f 100644 --- a/src/tree/mod.rs +++ b/src/tree/mod.rs @@ -2,6 +2,7 @@ use crate::{ context::{column, file, Context}, disk_usage::file_size::FileSize, fs::inode::Inode, + progress::{self, IndicatorHandle, Message}, utils, }; use count::FileCount; @@ -51,10 +52,13 @@ impl Tree { /// Initiates file-system traversal and [Tree] as well as updates the [Context] object with /// various properties necessary to render output. - pub fn try_init_and_update_context(mut ctx: Context) -> Result<(Self, Context)> { + pub fn try_init_and_update_context( + mut ctx: Context, + indicator: Option<&IndicatorHandle>, + ) -> Result<(Self, Context)> { let mut column_properties = column::Properties::from(&ctx); - let (arena, root_id) = Self::traverse(&ctx, &mut column_properties)?; + let (arena, root_id) = Self::traverse(&ctx, &mut column_properties, indicator)?; ctx.update_column_properties(&column_properties); @@ -98,10 +102,13 @@ impl Tree { fn traverse( ctx: &Context, column_properties: &mut column::Properties, + indicator: Option<&IndicatorHandle>, ) -> Result<(Arena, NodeId)> { let walker = WalkParallel::try_from(ctx)?; let (tx, rx) = mpsc::channel(); + let progress_indicator_mailbox = indicator.map(progress::IndicatorHandle::mailbox); + thread::scope(|s| { let res = s.spawn(move || { let mut tree = Arena::new(); @@ -109,6 +116,10 @@ impl Tree { let mut root_id_id = None; while let Ok(TraversalState::Ongoing(node)) = rx.recv() { + if let Some(ref mailbox) = progress_indicator_mailbox { + let _ = mailbox.send(Message::Index); + } + if node.is_dir() { let node_path = node.path(); @@ -135,6 +146,10 @@ impl Tree { } } + if let Some(ref mailbox) = progress_indicator_mailbox { + let _ = mailbox.send(Message::DoneIndexing); + } + let root_id = root_id_id.ok_or(Error::MissingRoot)?; let node_comparator = node::cmp::comparator(ctx); let mut inodes = HashSet::new(); @@ -306,7 +321,7 @@ impl Tree { #[cfg(unix)] fn update_column_properties(col_props: &mut column::Properties, node: &Node, ctx: &Context) { if let Some(file_size) = node.file_size() { - if ctx.human { + if ctx.byte_metric() && ctx.human { let out = format!("{file_size}"); let [size, unit]: [&str; 2] = out.split(' ').collect::>().try_into().unwrap(); @@ -377,7 +392,7 @@ impl Tree { #[cfg(not(unix))] fn update_column_properties(col_props: &mut column::Properties, node: &Node, ctx: &Context) { if let Some(file_size) = node.file_size() { - if ctx.human { + if ctx.byte_metric() && ctx.human { let out = format!("{file_size}"); let [size, unit]: [&str; 2] = out.split(' ').collect::>().try_into().unwrap(); diff --git a/tests/sort.rs b/tests/sort.rs index 4d38ef57..53592a49 100644 --- a/tests/sort.rs +++ b/tests/sort.rs @@ -66,7 +66,7 @@ fn sort_name_dir_order() { #[test] fn sort_size() { assert_eq!( - utils::run_cmd(&["--sort", "size-rev", "tests/data"]), + utils::run_cmd(&["--sort", "rsize", "tests/data"]), indoc!( "446 B ┌─ lipsum.txt 446 B ┌─ lipsum