diff --git a/Cargo.lock b/Cargo.lock index d48d8b5..703a25d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,28 +1,127 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] -name = "aho-corasick" -version = "1.1.3" +name = "anstream" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ - "memchr", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", ] +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "console" version = "0.15.8" @@ -32,7 +131,8 @@ dependencies = [ "encode_unicode 0.3.6", "lazy_static", "libc", - "windows-sys", + "unicode-width 0.1.14", + "windows-sys 0.52.0", ] [[package]] @@ -124,10 +224,16 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" name = "explain" version = "0.1.3" dependencies = [ + "anyhow", + "clap", "csv", + "humansize", "indicatif", "prettytable-rs", "rayon", + "serde", + "serde_json", + "walkdir", ] [[package]] @@ -141,22 +247,39 @@ dependencies = [ "wasi", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "indicatif" -version = "0.16.2" +version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" dependencies = [ "console", - "lazy_static", "number_prefix", - "regex", + "portable-atomic", + "rayon", + "unicode-width 0.2.2", + "web-time", ] [[package]] @@ -167,15 +290,31 @@ checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -188,6 +327,12 @@ version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.3" @@ -210,17 +355,36 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "prettytable-rs" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" dependencies = [ + "csv", "encode_unicode 1.0.0", "is-terminal", "lazy_static", "term", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -272,35 +436,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "regex" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - [[package]] name = "rustversion" version = "1.0.18" @@ -313,26 +448,64 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" -version = "1.0.213" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.213" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.83" @@ -387,12 +560,89 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -409,12 +659,27 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.52.0" @@ -424,6 +689,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -487,3 +761,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zmij" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" diff --git a/Cargo.toml b/Cargo.toml index a79a221..174c47c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,10 +6,15 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -indicatif = "0.16" -csv = "1.1" -rayon = "1.10.0" +anyhow = "1" +clap = { version = "4", features = ["derive"] } +walkdir = "2" +indicatif = { version = "0.17", features = ["rayon"] } +rayon = "1" +prettytable-rs = "0.10" +csv = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +humansize = "2" + -[dependencies.prettytable-rs] -version = "0.10" -default-features = false diff --git a/src/main.rs b/src/main.rs index 0246daa..e373708 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,354 +1,356 @@ +use anyhow::{Context, Result}; +use clap::{Parser, ValueEnum}; +use indicatif::{ProgressBar, ProgressStyle}; +use prettytable::{Cell, Row, Table}; +use rayon::prelude::*; +use serde::Serialize; use std::env; -use csv::ReaderBuilder; use std::fs::{self, File}; use std::io::{self, Read}; -use std::error::Error; use std::path::{Path, PathBuf}; -use std::collections::HashMap; -use prettytable::{Table, Row, Cell}; -use indicatif::{ProgressBar, ProgressStyle}; -use rayon::prelude::*; -use std::fs::DirEntry; +use walkdir::{DirEntry, WalkDir}; +#[derive(Debug, Clone, ValueEnum)] +enum SortBy { + Size, + Name, + Type, +} +#[derive(Parser, Debug)] +#[command(name = "rdu", about = "Fast folder size + preview tool")] +struct Args { + /// Path to scan (file or directory) + #[arg(default_value = ".")] + path: PathBuf, -fn calculate_total_size(path: &Path) -> io::Result { - let mut total_size = 0; - let entries = match fs::read_dir(path) { - Ok(entries) => entries, - Err(e) if e.kind() == io::ErrorKind::PermissionDenied => { - eprintln!("Skipping directory due to permission error: {}", path.display()); - return Ok(0); - } - Err(e) => return Err(e), - }; - - for entry in entries { - match entry { - Ok(entry) => { - let metadata = match entry.metadata() { - Ok(metadata) => metadata, - Err(e) if e.kind() == io::ErrorKind::PermissionDenied => { - eprintln!("Skipping file due to permission error: {}", entry.path().display()); - continue; - } - Err(e) => return Err(e), - }; + /// Show only top N entries + #[arg(long, default_value_t = 50)] + top: usize, - if metadata.is_dir() { - total_size += calculate_total_size(&entry.path())?; - } else { - total_size += metadata.len(); - } - } - Err(e) if e.kind() == io::ErrorKind::PermissionDenied => { - eprintln!("Skipping entry due to permission error: {}", path.display()); - continue; - } - Err(e) => return Err(e), - } - } + /// Sorting strategy + #[arg(long, value_enum, default_value_t = SortBy::Size)] + sort: SortBy, - Ok(total_size) -} -// updated first layer full size -fn get_first_layer_full_sizes(start_path: &Path) -> io::Result> { - let entries: Vec = fs::read_dir(start_path)? - .filter_map(|entry| entry.ok()) - .collect(); + /// Maximum depth for scanning (0 = first layer only, 1 = full subtree sizes for first layer items) + #[arg(long, default_value_t = 1)] + depth: usize, - let pb = ProgressBar::new(entries.len() as u64); - pb.set_style( - ProgressStyle::default_bar() - .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}") - .progress_chars("##-"), - ); - pb.enable_steady_tick(100); + /// Follow symlinks (can be dangerous if cycles exist) + #[arg(long, default_value_t = false)] + follow_symlinks: bool, - let folder_sizes: HashMap = entries - .into_par_iter() // Converts Vec to a Rayon parallel iterator - .filter_map(|entry| { - let metadata = entry.metadata().ok()?; - if metadata.is_dir() { - let size = calculate_total_size(&entry.path()).ok()?; - Some((entry.path(), size)) - } else { - Some((entry.path(), metadata.len())) - } - }) - .inspect(|_| pb.inc(1)) - .collect(); + /// Skip common heavy folders (.git, node_modules, target, dist, build) + #[arg(long, default_value_t = true)] + smart_ignore: bool, - pb.finish_and_clear(); - Ok(folder_sizes) + /// Output JSON instead of table + #[arg(long, default_value_t = false)] + json: bool, + + /// Print extension summary (top 20) + #[arg(long, default_value_t = false)] + ext: bool, + + /// Minimum size filter in bytes + #[arg(long)] + min_size: Option, + + /// Print only first N lines when previewing a file + #[arg(long, default_value_t = 400)] + head: usize, } -fn format_size(size: u64) -> String { - const KB: u64 = 1_024; - const MB: u64 = KB * 1_024; - const GB: u64 = MB * 1_024; - - if size >= GB { - format!("{:.2} GB", size as f64 / GB as f64) - } else if size >= MB { - format!("{:.2} MB", size as f64 / MB as f64) - } else if size >= KB { - format!("{:.2} KB", size as f64 / KB as f64) - } else { - format!("{} B", size) - } +#[derive(Debug, Clone, Serialize)] +struct EntryInfo { + path: PathBuf, + is_dir: bool, + size: u64, + file_type: String, } +fn format_root_name(path: &Path) -> String { + // Handle roots like "C:\" on Windows + if path.parent().is_none() { + return path.display().to_string(); + } + path.file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| path.display().to_string()) +} -fn get_file_type(path: &Path) -> String { - let extension = path.extension().and_then(|s| s.to_str()).unwrap_or(""); - match extension { - "rs" => "Rust".to_uppercase(), - "py" => "Python".to_uppercase(), - "js" => "JavaScript".to_uppercase(), - "ts" => "TypeScript".to_uppercase(), - "cpp" => "C++".to_uppercase(), - "c" => "C".to_uppercase(), - "cs" => "C#".to_uppercase(), - "rb" => "Ruby".to_uppercase(), - "sh" => "Shell".to_uppercase(), - "toml" => "Cargo".to_uppercase(), - "exe" => "Application".to_uppercase(), - "pem" => "Key".to_uppercase(), - "md" => "Markdown".to_uppercase(), - "mp3" => "Mp3 Audio".to_uppercase(), - "m4a" => "M4a Audio".to_uppercase(), - "mp4" => "MP4 Video".to_uppercase(), - "mkv" => "MKV Video".to_uppercase(), - "jpg" => "JPEG Image".to_uppercase(), - "png" => "PNG Image".to_uppercase(), - "pdf" => "PDF".to_uppercase(), - "txt" => "Palin Text".to_uppercase(), - "" => "Dot File".to_uppercase(), - _ => extension.to_uppercase(), +fn is_ignored(entry: &DirEntry, smart_ignore: bool) -> bool { + if !smart_ignore { + return false; } + let name = entry.file_name().to_string_lossy().to_lowercase(); + matches!( + name.as_str(), + ".git" | "node_modules" | "target" | "dist" | "build" | ".idea" | ".vscode" + ) } +fn calculate_tree_size(root: &Path, follow_symlinks: bool, smart_ignore: bool) -> u64 { + WalkDir::new(root) + .follow_links(follow_symlinks) + .into_iter() + .filter_entry(|e| !is_ignored(e, smart_ignore)) + .filter_map(|e| e.ok()) + .filter_map(|e| e.metadata().ok().map(|m| (e, m))) + .filter(|(_, m)| m.is_file()) + .map(|(_, m)| m.len()) + .sum() +} -fn format_root_name(path: &Path) -> String { - let normalized_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); - match normalized_path.file_name() { - Some(name) => name.to_string_lossy().to_string(), - None => "UNKNOWN".to_string(), +fn get_file_type(path: &std::path::Path) -> String { + // Treat dotfiles like ".env" as DOT FILE + if let Some(name) = path.file_name().and_then(|s| s.to_str()) { + if name.starts_with('.') { + return "DOT FILE".to_string(); + } + } + + let ext = path + .extension() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_lowercase(); + + match ext.as_str() { + "rs" => "RUST".to_string(), + "py" => "PYTHON".to_string(), + "js" => "JAVASCRIPT".to_string(), + "ts" => "TYPESCRIPT".to_string(), + "cpp" => "C++".to_string(), + "c" => "C".to_string(), + "cs" => "C#".to_string(), + "rb" => "RUBY".to_string(), + "sh" => "SHELL".to_string(), + "toml" => "CARGO".to_string(), + "exe" => "APPLICATION".to_string(), + "pem" => "KEY".to_string(), + "md" => "MARKDOWN".to_string(), + "mp3" => "MP3 AUDIO".to_string(), + "m4a" => "M4A AUDIO".to_string(), + "mp4" => "MP4 VIDEO".to_string(), + "mkv" => "MKV VIDEO".to_string(), + "jpg" | "jpeg" => "JPEG IMAGE".to_string(), + "png" => "PNG IMAGE".to_string(), + "pdf" => "PDF".to_string(), + "txt" => "PLAIN TEXT".to_string(), + "" => "NO EXT".to_string(), + _=> ext.to_uppercase(), // <-- return the String, don't borrow it } } -fn print_csv_table(file_path: &Path) -> Result<(), Box> { - let file = File::open(file_path)?; - let mut rdr = ReaderBuilder::new().has_headers(true).from_reader(file); +fn collect_first_layer_entries(start: &Path, args: &Args) -> Result> { + let read_dir = fs::read_dir(start) + .with_context(|| format!("Failed to read directory {}", start.display()))?; - // Create a table for pretty printing - let mut table = Table::new(); + let entries: Vec = read_dir.filter_map(|e| e.ok()).collect(); - // Get headers and add them to the table - let headers = rdr.headers()?; - let header_row: Vec = headers.iter().map(|h| Cell::new(h)).collect(); - table.add_row(Row::new(header_row)); + let pb = ProgressBar::new(entries.len() as u64); + pb.set_style( + ProgressStyle::with_template( + "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}", + ) + .unwrap() + .progress_chars("##-"), + ); + pb.enable_steady_tick(std::time::Duration::from_millis(100)); + let pb = std::sync::Arc::new(pb); - // Read and add the top 10 rows to the table - for result in rdr.records().take(10) { - let record = result?; - let cells: Vec = record.iter().map(|field| Cell::new(field)).collect(); - table.add_row(Row::new(cells)); - } + let results: Vec = entries + .into_par_iter() + .filter_map(|entry| { + let path = entry.path(); + let md = entry.metadata().ok()?; + + // ignore at first layer too + if args.smart_ignore { + if let Some(name) = path.file_name().and_then(|s| s.to_str()) { + let n = name.to_lowercase(); + if matches!( + n.as_str(), + ".git" | "node_modules" | "target" | "dist" | "build" | ".idea" | ".vscode" + ) { + pb.inc(1); + return None; + } + } + } - // Print the table - table.printstd(); + let is_dir = md.is_dir(); + let size = if is_dir { + if args.depth == 0 { + 0 + } else { + calculate_tree_size(&path, args.follow_symlinks, args.smart_ignore) + } + } else { + md.len() + }; - Ok(()) + pb.inc(1); + + Some(EntryInfo { + file_type: if is_dir { "-".into() } else { get_file_type(&path) }, + path, + is_dir, + size, + }) + }) + .collect(); + + pb.finish_and_clear(); + Ok(results) } -fn print_file_content(path: &Path) -> io::Result<()> { - let mut file = fs::File::open(path)?; - let mut content = String::new(); - file.read_to_string(&mut content)?; - //let extension = path.extension().and_then(|s| s.to_str()).unwrap_or(""); - println!("{}", &content); +fn apply_filters(mut v: Vec, args: &Args) -> Vec { + if let Some(min) = args.min_size { + v.retain(|e| e.size >= min); + } + + match args.sort { + SortBy::Size => v.sort_by_key(|e| std::cmp::Reverse(e.size)), + SortBy::Name => v.sort_by_key(|e| e.path.to_string_lossy().to_lowercase()), + SortBy::Type => v.sort_by_key(|e| e.file_type.clone()), + } - Ok(()) + v.truncate(args.top); + v } -fn main() -> io::Result<()> { - // Get input or use the current directory - let args: Vec = env::args().collect(); - let folder_path = if args.len() > 1 { - Path::new(&args[1]) - } else { - Path::new(".") - }; +fn print_table(entries: &[EntryInfo], root: &Path) { + #[derive(Clone)] + struct RowData { + icon: String, + path: String, + typ: String, + size: String, + } - if folder_path.is_file() { - // If input is a file, print its content - if folder_path.extension().and_then(|e| e.to_str()) == Some("csv") { + // 1) Prepare owned row strings that will live long enough + let rows: Vec = entries + .iter() + .map(|e| { + let icon = if e.is_dir { "📁" } else { "📄" }.to_string(); - if let Err(e) = print_csv_table(&folder_path) { - eprintln!("Error reading file: {}", e); - } - } else { - if let Err(e) = print_file_content(&folder_path) { - eprintln!("Error reading file: {}", e); - } - } - return Ok(()); - } + let path = e + .path + .strip_prefix(root) + .unwrap_or(&e.path) + .display() + .to_string(); - let folder_sizes = match get_first_layer_full_sizes(folder_path) { - Ok(folder_sizes) => folder_sizes, - Err(e) => { - eprintln!("Error calculating folder size: {}", e); - return Err(e); - } - }; + let typ = e.file_type.clone(); - // Calculate and display the total size of the root folder and its contents - let total_size: u64 = folder_sizes.values().sum(); - println!("\nDir: 📂{}\tSize: {}\n", format_root_name(folder_path),format_size(total_size)); + let size = humansize::format_size(e.size, humansize::BINARY); - // Separate into folders and files - let mut folders: Vec<_> = folder_sizes.iter() - .filter(|(path, _)| path.is_dir()) - .collect(); - let mut files: Vec<_> = folder_sizes.iter() - .filter(|(path, _)| path.is_file()) + RowData { icon, path, typ, size } + }) .collect(); - // Sort folders and files by size in descending order - folders.sort_by_key(|(_, size)| std::cmp::Reverse(*size)); - files.sort_by_key(|(_, size)| std::cmp::Reverse(*size)); - - // Create and configure the table + // 2) Build the table using references to the owned strings let mut table = Table::new(); table.add_row(Row::new(vec![ Cell::new("#"), Cell::new("Path"), - Cell::new("Type"), + Cell::new("Type"), Cell::new("Size"), - ])); - // Function to remove the current directory prefix - let remove_prefix = |path: &Path| { - path.strip_prefix(".\\") - .or_else(|_| path.strip_prefix("./")) - .unwrap_or(path) - .display().to_string() - }; - - // Add folders to the table - for (folder, size) in folders { + for r in &rows { table.add_row(Row::new(vec![ - Cell::new("📁"), - Cell::new(&remove_prefix(folder)), - Cell::new("-"), - Cell::new(&format_size(*size)), - + Cell::new(&r.icon), + Cell::new(&r.path), + Cell::new(&r.typ), + Cell::new(&r.size), ])); } - // Add files to the table - for (file, size) in files { - table.add_row(Row::new(vec![ - Cell::new("📄"), - Cell::new(&remove_prefix(file)), - Cell::new(&get_file_type(file)), - Cell::new(&format_size(*size)), - - ])); - } - - // Print the table table.printstd(); - println!(""); - - Ok(()) } -#[cfg(test)] -mod tests { - use super::*; - use std::fs::{self, File}; - use std::io::Write; - use std::path::Path; - use std::env; - - #[test] - fn test_format_size() { - assert_eq!(format_size(0), "0 B"); - assert_eq!(format_size(1), "1 B"); - assert_eq!(format_size(1023), "1023 B"); - assert_eq!(format_size(1024), "1.00 KB"); - assert_eq!(format_size(1024 * 1024), "1.00 MB"); - assert_eq!(format_size(1024 * 1024 * 1024), "1.00 GB"); +fn print_ext_summary(all: &[EntryInfo]) { + use std::collections::BTreeMap; + let mut map: BTreeMap = BTreeMap::new(); // ext -> (count, bytes) + + for e in all.iter().filter(|e| !e.is_dir) { + let ext = e + .path + .extension() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_lowercase(); + let key = if ext.is_empty() { "(none)".into() } else { ext }; + let entry = map.entry(key).or_insert((0, 0)); + entry.0 += 1; + entry.1 += e.size; } - #[test] - fn test_get_file_type() { - assert_eq!(get_file_type(Path::new("test.rs")), "RUST"); - assert_eq!(get_file_type(Path::new("test.py")), "PYTHON"); - assert_eq!(get_file_type(Path::new("test.unknown")), "UNKNOWN"); - assert_eq!(get_file_type(Path::new("file")), "DOT FILE"); // No extension + println!("\nBy extension (top 20):"); + for (ext, (count, bytes)) in map.iter().rev().take(20) { + println!( + "{:>10} {:>6} files {}", + ext, + count, + humansize::format_size(*bytes, humansize::BINARY) + ); } +} - #[test] - fn test_format_root_name() { - assert_eq!(format_root_name(Path::new("/path/to/file.txt")), "file.txt"); - assert_eq!(format_root_name(Path::new("/path/to/directory")), "directory"); - assert_eq!(format_root_name(Path::new("/path/to/unknown")), "unknown"); - } +fn print_json(entries: &[EntryInfo]) -> Result<()> { + println!("{}", serde_json::to_string_pretty(entries)?); + Ok(()) +} - #[test] - fn test_calculate_total_size() { - let temp_dir = env::temp_dir().join("test_calculate_total_size"); +fn print_file_head(path: &Path, head: usize) -> io::Result<()> { + let mut file = fs::File::open(path)?; + let mut content = String::new(); + file.read_to_string(&mut content)?; - // Create temp directory and files - fs::create_dir(&temp_dir).unwrap(); - let file_path = temp_dir.join("file1.txt"); - let mut file = File::create(&file_path).unwrap(); - file.write_all(b"Hello, world!").unwrap(); // 13 bytes + for (i, line) in content.lines().take(head).enumerate() { + println!("{:>5} {}", i + 1, line); + } + Ok(()) +} - let sub_dir = temp_dir.join("sub_dir"); - fs::create_dir(&sub_dir).unwrap(); - let sub_file_path = sub_dir.join("file2.txt"); - let mut sub_file = File::create(&sub_file_path).unwrap(); - sub_file.write_all(b"Hello, sub world!").unwrap(); // 17 bytes +fn main() -> Result<()> { + // Keep your old behavior: allow passing path as first argument + // But clap already handles it; this is just to keep compatibility. + let _ = env::args(); - // Calculate the size of the temp directory - let size = calculate_total_size(&temp_dir).unwrap(); - assert_eq!(size, 13 + 17); // 13 bytes + 17 bytes + let args = Args::parse(); + let path = args.path.clone(); - // Clean up - fs::remove_dir_all(temp_dir).unwrap(); + if path.is_file() { + // file preview (csv preview can be re-added; keeping simple here) + print_file_head(&path, args.head) + .with_context(|| format!("Failed to read file {}", path.display()))?; + return Ok(()); } - #[test] - fn test_get_first_layer_full_sizes() { - let temp_dir = env::temp_dir().join("test_get_first_layer_full_sizes"); - - // Create temp directory and files - fs::create_dir(&temp_dir).unwrap(); - let file_path = temp_dir.join("file1.txt"); - let mut file = File::create(&file_path).unwrap(); - file.write_all(b"Hello, world!").unwrap(); // 13 bytes + let entries = collect_first_layer_entries(&path, &args)?; - let sub_dir = temp_dir.join("sub_dir"); - fs::create_dir(&sub_dir).unwrap(); - let sub_file_path = sub_dir.join("file2.txt"); - let mut sub_file = File::create(&sub_file_path).unwrap(); - sub_file.write_all(b"Hello, sub world!").unwrap(); // 17 bytes + let total: u64 = entries.iter().map(|e| e.size).sum(); + println!( + "\nDir: 📂{}\tSize: {}\n", + format_root_name(&path), + humansize::format_size(total, humansize::BINARY) + ); - // Get the first layer folder sizes - let folder_sizes = get_first_layer_full_sizes(&temp_dir).unwrap(); + if args.ext { + print_ext_summary(&entries); + } - assert_eq!(folder_sizes.get(&file_path).unwrap(), &13); // File size - assert_eq!(folder_sizes.get(&sub_dir).unwrap(), &(17)); // Size of sub_dir's content + let entries = apply_filters(entries, &args); - // Clean up - fs::remove_dir_all(temp_dir).unwrap(); + if args.json { + print_json(&entries)?; + } else { + print_table(&entries, &path); } -} + Ok(()) +}