v0.2.0 - Performance & Distribution #1
@@ -14,9 +14,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
- Added `MANUAL.md` with comprehensive usage documentation.
|
||||
- Added comprehensive test suite (unit and integration tests).
|
||||
- Added performance benchmarks using `criterion`.
|
||||
|
||||
### Removed
|
||||
- Removed `walkdir` dependency (replaced by `ignore::WalkBuilder` for all traversal).
|
||||
- Removed `humansize` (consolidated to `byte-unit`).
|
||||
- Removed `chrono` (replaced with lightweight `time`).
|
||||
- Removed `atty` (unused).
|
||||
|
||||
|
||||
## [0.1.0] - 2026-01-22
|
||||
|
||||
|
||||
Generated
+350
-140
@@ -8,7 +8,7 @@ version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.2.17",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
]
|
||||
@@ -23,13 +23,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
@@ -88,14 +85,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
name = "assert_cmd"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"anstyle",
|
||||
"bstr",
|
||||
"libc",
|
||||
"winapi",
|
||||
"predicates",
|
||||
"predicates-core",
|
||||
"predicates-tree",
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -152,6 +153,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -202,14 +204,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.53"
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@@ -224,16 +222,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.43"
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -323,10 +335,40 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
name = "criterion"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
@@ -376,6 +418,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.4.0"
|
||||
@@ -397,6 +445,21 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "difflib"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
@@ -435,10 +498,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.8"
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "float-cmp"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
@@ -457,6 +529,18 @@ dependencies = [
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.18"
|
||||
@@ -470,6 +554,17 @@ dependencies = [
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -499,45 +594,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
@@ -565,12 +624,32 @@ dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
@@ -599,12 +678,6 @@ version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.11.0"
|
||||
@@ -638,6 +711,18 @@ version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "normalize-line-endings"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -659,6 +744,12 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -682,6 +773,40 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"plotters-backend",
|
||||
"plotters-svg",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plotters-backend"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
|
||||
|
||||
[[package]]
|
||||
name = "plotters-svg"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
|
||||
dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@@ -691,6 +816,36 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
version = "3.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"difflib",
|
||||
"float-cmp",
|
||||
"normalize-line-endings",
|
||||
"predicates-core",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "predicates-core"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
|
||||
|
||||
[[package]]
|
||||
name = "predicates-tree"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
|
||||
dependencies = [
|
||||
"predicates-core",
|
||||
"termtree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.4.0"
|
||||
@@ -738,6 +893,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
@@ -771,7 +932,7 @@ version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -823,6 +984,18 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
@@ -1001,12 +1174,6 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
@@ -1017,21 +1184,23 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
name = "sized"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"assert_cmd",
|
||||
"byte-unit",
|
||||
"chrono",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"clap_mangen",
|
||||
"colored",
|
||||
"comfy-table",
|
||||
"criterion",
|
||||
"csv",
|
||||
"humansize",
|
||||
"ignore",
|
||||
"predicates",
|
||||
"rayon",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"tempfile",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1096,6 +1265,66 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termtree"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.10.0"
|
||||
@@ -1187,6 +1416,15 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
@@ -1203,6 +1441,15 @@ version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.108"
|
||||
@@ -1248,6 +1495,16 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -1279,65 +1536,12 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
@@ -1429,6 +1633,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.5.1"
|
||||
|
||||
+12
-3
@@ -7,7 +7,6 @@ edition = "2021"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
|
||||
rayon = "1.8"
|
||||
humansize = "2.1"
|
||||
colored = "2.0"
|
||||
comfy-table = "7.0"
|
||||
byte-unit = "5.0"
|
||||
@@ -16,7 +15,17 @@ ignore = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
csv = "1.3"
|
||||
chrono = "0.4"
|
||||
time = { version = "0.3", features = ["macros", "formatting"] }
|
||||
clap_complete = "4.4"
|
||||
atty = "0.2"
|
||||
clap_mangen = "0.2.31"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0"
|
||||
predicates = "3.1"
|
||||
tempfile = "3.10"
|
||||
criterion = "0.5"
|
||||
|
||||
[[bench]]
|
||||
name = "benchmark"
|
||||
harness = false
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use sized::build_tree;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use tempfile::TempDir; // Ensure this is pub in lib.rs
|
||||
|
||||
fn setup_test_dir() -> TempDir {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let base_path = temp_dir.path();
|
||||
|
||||
// Create a moderately deep structure
|
||||
for i in 0..10 {
|
||||
let dir_path = base_path.join(format!("dir_{}", i));
|
||||
fs::create_dir(&dir_path).unwrap();
|
||||
for j in 0..10 {
|
||||
let file_path = dir_path.join(format!("file_{}.txt", j));
|
||||
let mut file = File::create(file_path).unwrap();
|
||||
writeln!(file, "Some content for file {}-{}", i, j).unwrap();
|
||||
}
|
||||
}
|
||||
temp_dir
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
let temp_dir = setup_test_dir();
|
||||
let path = temp_dir.path();
|
||||
|
||||
c.bench_function("build_tree small", |b| {
|
||||
b.iter(|| build_tree(black_box(path), black_box(false)))
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
+552
@@ -0,0 +1,552 @@
|
||||
use byte_unit::{Byte, UnitType};
|
||||
|
||||
use clap::{CommandFactory, Parser, ValueEnum};
|
||||
use clap_complete::{generate, Shell};
|
||||
use colored::*;
|
||||
use comfy_table::presets::UTF8_FULL;
|
||||
use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Table};
|
||||
use csv::WriterBuilder;
|
||||
use ignore::WalkBuilder;
|
||||
use rayon::prelude::*;
|
||||
use serde::Serialize;
|
||||
use std::cmp::Ordering;
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(disable_version_flag = true)]
|
||||
pub struct Args {
|
||||
/// Print version
|
||||
#[arg(short = 'v', long, action = clap::ArgAction::Version)]
|
||||
pub version: Option<bool>,
|
||||
|
||||
/// Directory to analyze
|
||||
#[arg(default_value = ".")]
|
||||
pub path: PathBuf,
|
||||
|
||||
/// Minimum size filter (e.g. "10MB", "1gb")
|
||||
#[arg(short = 'm', long)]
|
||||
pub min_size: Option<String>,
|
||||
|
||||
/// Limit the number of results per table
|
||||
#[arg(short = 'n', long)]
|
||||
pub number: Option<usize>,
|
||||
|
||||
/// Sort by columns (e.g. "type:asc,size:dsc").
|
||||
/// defaults to size:dsc.
|
||||
/// Columns: name, type, size, blocks.
|
||||
#[arg(long)]
|
||||
pub sort: Option<String>,
|
||||
|
||||
/// Depth to traverse (0 = only immediate children)
|
||||
#[arg(short, long, default_value = "0")]
|
||||
pub depth: usize,
|
||||
|
||||
/// Display full absolute paths
|
||||
#[arg(short = 'f', long, conflicts_with = "path_relative")]
|
||||
pub path_full: bool,
|
||||
|
||||
/// Display paths relative to current directory (default)
|
||||
#[arg(long)]
|
||||
pub path_relative: bool,
|
||||
|
||||
/// Number of threads to use (defaults to available logical CPUs)
|
||||
#[arg(short = 'j', long = "threads")]
|
||||
pub threads: Option<usize>,
|
||||
|
||||
/// Generate shell completions
|
||||
#[arg(long, value_enum)]
|
||||
pub completions: Option<Shell>,
|
||||
|
||||
/// Respect .gitignore and .ignore files
|
||||
#[arg(short = 'i', long)]
|
||||
pub ignore: bool,
|
||||
|
||||
/// Output format
|
||||
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
|
||||
pub format: OutputFormat,
|
||||
|
||||
/// Save output to file (optional filename, defaults to timestamped name)
|
||||
#[arg(long, num_args(0..=1), default_missing_value = "_AUTO_")]
|
||||
pub save: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum OutputFormat {
|
||||
Text,
|
||||
Csv,
|
||||
Json,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SortColumn {
|
||||
Name,
|
||||
Type,
|
||||
Size,
|
||||
Blocks,
|
||||
}
|
||||
|
||||
impl FromStr for SortColumn {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"name" | "n" => Ok(SortColumn::Name),
|
||||
"type" | "t" => Ok(SortColumn::Type),
|
||||
"size" | "s" => Ok(SortColumn::Size),
|
||||
"blocks" | "b" => Ok(SortColumn::Blocks),
|
||||
_ => Err(format!("Unknown column: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SortDirection {
|
||||
Asc,
|
||||
Dsc,
|
||||
}
|
||||
|
||||
impl FromStr for SortDirection {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"asc" | "a" => Ok(SortDirection::Asc),
|
||||
"dsc" | "d" | "desc" => Ok(SortDirection::Dsc),
|
||||
_ => Err(format!("Unknown direction: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Debug)]
|
||||
pub struct Node {
|
||||
pub path: PathBuf,
|
||||
pub size_bytes: u64,
|
||||
pub blocks: u64,
|
||||
pub entry_type: EntryType,
|
||||
#[serde(skip)]
|
||||
pub children: Vec<Node>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum EntryType {
|
||||
File,
|
||||
Dir,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for EntryType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
EntryType::File => write!(f, "File"),
|
||||
EntryType::Dir => write!(f, "Dir"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct JsonMetrics {
|
||||
pub size_bytes: u64,
|
||||
pub blocks: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct JsonEntry<'a> {
|
||||
pub path: &'a PathBuf,
|
||||
pub metrics: JsonMetrics,
|
||||
pub entry_type: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct JsonReport<'a> {
|
||||
pub path: &'a PathBuf,
|
||||
pub total_size: u64,
|
||||
pub blocks: u64,
|
||||
pub entries: Vec<JsonEntry<'a>>,
|
||||
}
|
||||
|
||||
pub fn run(args: Args, mut writer: &mut dyn Write) {
|
||||
if let Some(shell) = args.completions {
|
||||
let mut cmd = Args::command();
|
||||
let name = cmd.get_name().to_string();
|
||||
generate(shell, &mut cmd, name, &mut std::io::stdout());
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(threads) = args.threads {
|
||||
rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(threads)
|
||||
.build_global()
|
||||
.ok(); // Ignore error if initialized called multiple times (e.g. in tests)
|
||||
}
|
||||
|
||||
let target_path = args.path.clone();
|
||||
|
||||
if !target_path.exists() {
|
||||
eprintln!("Error: Path '{}' does not exist.", target_path.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let min_bytes = if let Some(size_str) = &args.min_size {
|
||||
match Byte::parse_str(size_str, true) {
|
||||
Ok(byte) => byte.as_u64(),
|
||||
Err(e) => {
|
||||
eprintln!("Error parsing size '{}': {}", size_str, e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let sort_criteria = parse_sort_arg(args.sort.as_deref().unwrap_or("size:dsc"));
|
||||
|
||||
if args.format == OutputFormat::Csv {
|
||||
writeln!(writer, "Path,Type,Size(Bytes),Blocks").expect("Failed to write CSV header");
|
||||
}
|
||||
|
||||
let root_node = build_tree(&target_path, args.ignore);
|
||||
|
||||
if root_node.size_bytes < min_bytes {
|
||||
if args.format == OutputFormat::Text {
|
||||
writeln!(writer, "Root directory is smaller than minimum size.").ok();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
process_node_recursive(&mut writer, &root_node, 0, &args, min_bytes, &sort_criteria);
|
||||
}
|
||||
|
||||
pub fn build_tree(path: &Path, ignore: bool) -> Node {
|
||||
let metadata = path.metadata();
|
||||
|
||||
if let Ok(meta) = metadata {
|
||||
if meta.is_file() {
|
||||
return Node {
|
||||
path: path.to_path_buf(),
|
||||
size_bytes: meta.len(),
|
||||
blocks: meta.blocks(),
|
||||
entry_type: EntryType::File,
|
||||
children: vec![],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let walker = WalkBuilder::new(path)
|
||||
.standard_filters(false)
|
||||
.hidden(false)
|
||||
.git_ignore(ignore)
|
||||
.ignore(ignore)
|
||||
.max_depth(Some(1))
|
||||
.build();
|
||||
|
||||
let child_paths: Vec<PathBuf> = walker
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.path() != path)
|
||||
.map(|e| e.path().to_path_buf())
|
||||
.collect();
|
||||
|
||||
let children: Vec<Node> = child_paths
|
||||
.par_iter()
|
||||
.map(|p| build_tree(p, ignore))
|
||||
.collect();
|
||||
|
||||
let (size_bytes, blocks) = children
|
||||
.iter()
|
||||
.fold((0, 0), |acc, c| (acc.0 + c.size_bytes, acc.1 + c.blocks));
|
||||
|
||||
let (self_size, self_blocks) = path
|
||||
.metadata()
|
||||
.map(|m| (m.len(), m.blocks()))
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
Node {
|
||||
path: path.to_path_buf(),
|
||||
size_bytes: size_bytes + self_size,
|
||||
blocks: blocks + self_blocks,
|
||||
entry_type: EntryType::Dir,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
fn process_node_recursive(
|
||||
writer: &mut dyn Write,
|
||||
node: &Node,
|
||||
current_depth: usize,
|
||||
args: &Args,
|
||||
min_bytes: u64,
|
||||
sort_criteria: &[(SortColumn, SortDirection)],
|
||||
) {
|
||||
if node.children.is_empty() && node.entry_type == EntryType::Dir {}
|
||||
|
||||
let mut display_children: Vec<&Node> = node
|
||||
.children
|
||||
.iter()
|
||||
.filter(|c| c.size_bytes >= min_bytes)
|
||||
.collect();
|
||||
|
||||
display_children.sort_by(|a, b| compare_nodes(a, b, sort_criteria));
|
||||
|
||||
if let Some(n) = args.number {
|
||||
if n < display_children.len() {
|
||||
display_children.truncate(n);
|
||||
}
|
||||
}
|
||||
|
||||
let show_relative = !args.path_full;
|
||||
let relative_path = if show_relative {
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
node.path
|
||||
.strip_prefix(&cwd)
|
||||
.unwrap_or(&node.path)
|
||||
.display()
|
||||
.to_string()
|
||||
} else {
|
||||
node.path
|
||||
.canonicalize()
|
||||
.unwrap_or(node.path.clone())
|
||||
.display()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
print_output(
|
||||
writer,
|
||||
&node,
|
||||
&display_children,
|
||||
args,
|
||||
&relative_path,
|
||||
current_depth,
|
||||
);
|
||||
|
||||
if current_depth < args.depth {
|
||||
for child in display_children {
|
||||
if child.entry_type == EntryType::Dir {
|
||||
process_node_recursive(
|
||||
writer,
|
||||
child,
|
||||
current_depth + 1,
|
||||
args,
|
||||
min_bytes,
|
||||
sort_criteria,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compare_nodes(a: &Node, b: &Node, criteria: &[(SortColumn, SortDirection)]) -> Ordering {
|
||||
for (col, dir) in criteria {
|
||||
let order = match col {
|
||||
SortColumn::Name => a.path.file_name().cmp(&b.path.file_name()),
|
||||
SortColumn::Type => a.entry_type.cmp(&b.entry_type),
|
||||
SortColumn::Size => a.size_bytes.cmp(&b.size_bytes),
|
||||
SortColumn::Blocks => a.blocks.cmp(&b.blocks),
|
||||
};
|
||||
|
||||
if order != Ordering::Equal {
|
||||
return match dir {
|
||||
SortDirection::Asc => order,
|
||||
SortDirection::Dsc => order.reverse(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Ordering::Equal
|
||||
}
|
||||
|
||||
pub fn parse_sort_arg(s: &str) -> Vec<(SortColumn, SortDirection)> {
|
||||
s.split(',')
|
||||
.filter_map(|part| {
|
||||
let parts: Vec<&str> = part.split(':').collect();
|
||||
if parts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let col_str = parts[0];
|
||||
let col = SortColumn::from_str(col_str).ok()?;
|
||||
|
||||
let dir = if parts.len() > 1 {
|
||||
SortDirection::from_str(parts[1]).unwrap_or(SortDirection::Dsc)
|
||||
} else {
|
||||
match col {
|
||||
SortColumn::Name | SortColumn::Type => SortDirection::Asc,
|
||||
_ => SortDirection::Dsc,
|
||||
}
|
||||
};
|
||||
Some((col, dir))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn print_output(
|
||||
writer: &mut dyn Write,
|
||||
parent_node: &Node,
|
||||
children: &[&Node],
|
||||
args: &Args,
|
||||
relative_path: &str,
|
||||
depth: usize,
|
||||
) {
|
||||
match args.format {
|
||||
OutputFormat::Text => print_text(writer, parent_node, children, relative_path, depth),
|
||||
OutputFormat::Csv => print_csv(writer, children),
|
||||
OutputFormat::Json => print_json(writer, parent_node, children),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_text(
|
||||
writer: &mut dyn Write,
|
||||
parent_node: &Node,
|
||||
children: &[&Node],
|
||||
relative_path: &str,
|
||||
depth: usize,
|
||||
) {
|
||||
if depth > 0 {
|
||||
writeln!(writer, "{}", "-".repeat(60).dimmed()).ok();
|
||||
}
|
||||
writeln!(writer, "\n{}", relative_path.bold().blue()).ok();
|
||||
|
||||
let total_byte = Byte::from_u64(parent_node.size_bytes);
|
||||
writeln!(
|
||||
writer,
|
||||
"Total size: {} / {} ({} blocks)",
|
||||
total_byte
|
||||
.get_appropriate_unit(UnitType::Binary)
|
||||
.to_string()
|
||||
.bold()
|
||||
.green(),
|
||||
parent_node.size_bytes.to_string().bold().green(),
|
||||
parent_node.blocks
|
||||
)
|
||||
.ok();
|
||||
|
||||
if children.is_empty() {
|
||||
writeln!(writer, "(No children)").ok();
|
||||
return;
|
||||
}
|
||||
|
||||
let mut table = Table::new();
|
||||
table
|
||||
.load_preset(UTF8_FULL)
|
||||
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||
.set_header(vec![
|
||||
Cell::new("Name").add_attribute(Attribute::Bold),
|
||||
Cell::new("Type").add_attribute(Attribute::Bold),
|
||||
Cell::new("Size (Human)")
|
||||
.add_attribute(Attribute::Bold)
|
||||
.set_alignment(CellAlignment::Right),
|
||||
Cell::new("Size (Bytes)")
|
||||
.add_attribute(Attribute::Bold)
|
||||
.set_alignment(CellAlignment::Right),
|
||||
Cell::new("Blocks")
|
||||
.add_attribute(Attribute::Bold)
|
||||
.set_alignment(CellAlignment::Right),
|
||||
]);
|
||||
|
||||
for child in children {
|
||||
let name = child.path.file_name().unwrap_or_default().to_string_lossy();
|
||||
let color = if child.entry_type == EntryType::Dir {
|
||||
Color::Blue
|
||||
} else {
|
||||
Color::Cyan
|
||||
};
|
||||
|
||||
let child_byte = Byte::from_u64(child.size_bytes);
|
||||
|
||||
table.add_row(vec![
|
||||
Cell::new(name).fg(color),
|
||||
Cell::new(&child.entry_type).fg(Color::Yellow),
|
||||
Cell::new(
|
||||
child_byte
|
||||
.get_appropriate_unit(UnitType::Binary)
|
||||
.to_string(),
|
||||
)
|
||||
.fg(Color::Green)
|
||||
.set_alignment(CellAlignment::Right),
|
||||
Cell::new(child.size_bytes.to_string()) // Raw bytes instead of "Decimal" unit
|
||||
.fg(Color::Green)
|
||||
.set_alignment(CellAlignment::Right),
|
||||
Cell::new(child.blocks)
|
||||
.fg(Color::Green)
|
||||
.set_alignment(CellAlignment::Right),
|
||||
]);
|
||||
}
|
||||
writeln!(writer, "{}", table).ok();
|
||||
}
|
||||
|
||||
fn print_csv(writer: &mut dyn Write, children: &[&Node]) {
|
||||
let mut wtr = WriterBuilder::new().has_headers(false).from_writer(writer);
|
||||
|
||||
for child in children {
|
||||
wtr.write_record(&[
|
||||
child.path.to_string_lossy().as_ref(),
|
||||
&child.entry_type.to_string(),
|
||||
&child.size_bytes.to_string(),
|
||||
&child.blocks.to_string(),
|
||||
])
|
||||
.ok();
|
||||
}
|
||||
wtr.flush().ok();
|
||||
}
|
||||
|
||||
fn print_json(writer: &mut dyn Write, parent_node: &Node, children: &[&Node]) {
|
||||
let entries_view: Vec<JsonEntry> = children
|
||||
.iter()
|
||||
.map(|c| JsonEntry {
|
||||
path: &c.path,
|
||||
metrics: JsonMetrics {
|
||||
size_bytes: c.size_bytes,
|
||||
blocks: c.blocks,
|
||||
},
|
||||
entry_type: c.entry_type.to_string(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let report = JsonReport {
|
||||
path: &parent_node.path,
|
||||
total_size: parent_node.size_bytes,
|
||||
blocks: parent_node.blocks,
|
||||
entries: entries_view,
|
||||
};
|
||||
|
||||
serde_json::to_writer(&mut *writer, &report).ok();
|
||||
writer.write_all(b"\n").ok();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs::File;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_parse_sort_arg() {
|
||||
let criteria = parse_sort_arg("size:asc,name");
|
||||
assert_eq!(criteria.len(), 2);
|
||||
assert_eq!(criteria[0], (SortColumn::Size, SortDirection::Asc));
|
||||
assert_eq!(criteria[1], (SortColumn::Name, SortDirection::Asc)); // Default for Name is Asc
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_defaults() {
|
||||
let criteria = parse_sort_arg("size");
|
||||
assert_eq!(criteria[0], (SortColumn::Size, SortDirection::Dsc)); // Default for Size is Dsc
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_tree() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file_path = temp_dir.path().join("test_file.txt");
|
||||
File::create(&file_path)
|
||||
.unwrap()
|
||||
.write_all(b"Hello")
|
||||
.unwrap();
|
||||
|
||||
let node = build_tree(temp_dir.path(), false);
|
||||
|
||||
assert_eq!(node.entry_type, EntryType::Dir);
|
||||
assert!(!node.children.is_empty());
|
||||
assert_eq!(node.children[0].path, file_path);
|
||||
// Size should be at least 5 bytes
|
||||
assert!(node.children[0].size_bytes >= 5);
|
||||
}
|
||||
}
|
||||
+13
-538
@@ -1,222 +1,23 @@
|
||||
use byte_unit::Byte;
|
||||
use chrono::Local;
|
||||
use clap::{CommandFactory, Parser, ValueEnum};
|
||||
use clap_complete::{generate, Shell};
|
||||
use colored::*;
|
||||
use comfy_table::presets::UTF8_FULL;
|
||||
use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Table};
|
||||
use csv::WriterBuilder;
|
||||
use humansize::{format_size, BINARY, DECIMAL};
|
||||
use ignore::WalkBuilder;
|
||||
use rayon::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use clap::Parser;
|
||||
use sized::{run, Args, OutputFormat};
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(disable_version_flag = true)]
|
||||
struct Args {
|
||||
/// Print version
|
||||
#[arg(short = 'v', long, action = clap::ArgAction::Version)]
|
||||
version: Option<bool>,
|
||||
|
||||
/// Directory to analyze
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
|
||||
/// Minimum size filter (e.g. "10MB", "1gb")
|
||||
#[arg(short = 'm', long)]
|
||||
min_size: Option<String>,
|
||||
|
||||
/// Limit the number of results per table
|
||||
#[arg(short = 'n', long)]
|
||||
number: Option<usize>,
|
||||
|
||||
/// Sort by columns (e.g. "type:asc,size:dsc").
|
||||
/// defaults to size:dsc.
|
||||
/// Columns: name, type, size, blocks.
|
||||
#[arg(long)]
|
||||
sort: Option<String>,
|
||||
|
||||
/// Depth to traverse (0 = only immediate children)
|
||||
#[arg(short, long, default_value = "0")]
|
||||
depth: usize,
|
||||
|
||||
/// Display full absolute paths
|
||||
#[arg(short = 'f', long, conflicts_with = "path_relative")]
|
||||
path_full: bool,
|
||||
|
||||
/// Display paths relative to current directory (default)
|
||||
#[arg(long)]
|
||||
path_relative: bool,
|
||||
|
||||
/// Number of threads to use (defaults to available logical CPUs)
|
||||
#[arg(short = 'j', long = "threads")]
|
||||
threads: Option<usize>,
|
||||
|
||||
/// Generate shell completions
|
||||
#[arg(long, value_enum)]
|
||||
completions: Option<Shell>,
|
||||
|
||||
/// Respect .gitignore and .ignore files
|
||||
#[arg(short = 'i', long)]
|
||||
ignore: bool,
|
||||
|
||||
/// Output format
|
||||
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
|
||||
format: OutputFormat,
|
||||
|
||||
/// Save output to file (optional filename, defaults to timestamped name)
|
||||
#[arg(long, num_args(0..=1), default_missing_value = "_AUTO_")]
|
||||
save: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum OutputFormat {
|
||||
Text,
|
||||
Csv,
|
||||
Json,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum SortColumn {
|
||||
Name,
|
||||
Type,
|
||||
Size,
|
||||
Blocks,
|
||||
}
|
||||
|
||||
impl FromStr for SortColumn {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"name" | "n" => Ok(SortColumn::Name),
|
||||
"type" | "t" => Ok(SortColumn::Type),
|
||||
"size" | "s" => Ok(SortColumn::Size),
|
||||
"blocks" | "b" => Ok(SortColumn::Blocks),
|
||||
_ => Err(format!("Unknown column: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum SortDirection {
|
||||
Asc,
|
||||
Dsc,
|
||||
}
|
||||
|
||||
impl FromStr for SortDirection {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"asc" | "a" => Ok(SortDirection::Asc),
|
||||
"dsc" | "d" | "desc" => Ok(SortDirection::Dsc),
|
||||
_ => Err(format!("Unknown direction: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Debug)]
|
||||
struct Node {
|
||||
path: PathBuf,
|
||||
size_bytes: u64,
|
||||
blocks: u64,
|
||||
entry_type: EntryType,
|
||||
#[serde(skip)]
|
||||
children: Vec<Node>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum EntryType {
|
||||
File,
|
||||
Dir,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for EntryType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
EntryType::File => write!(f, "File"),
|
||||
EntryType::Dir => write!(f, "Dir"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JsonMetrics {
|
||||
size_bytes: u64,
|
||||
blocks: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JsonEntry<'a> {
|
||||
path: &'a PathBuf,
|
||||
metrics: JsonMetrics,
|
||||
entry_type: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JsonReport<'a> {
|
||||
path: &'a PathBuf,
|
||||
total_size: u64,
|
||||
blocks: u64,
|
||||
entries: Vec<JsonEntry<'a>>,
|
||||
}
|
||||
use std::path::PathBuf;
|
||||
use time::macros::format_description;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
// Handle completions
|
||||
if let Some(shell) = args.completions {
|
||||
let mut cmd = Args::command();
|
||||
let name = cmd.get_name().to_string();
|
||||
generate(shell, &mut cmd, name, &mut std::io::stdout());
|
||||
return;
|
||||
}
|
||||
// Determine output writer logic (moved mostly to main to keep lib clean,
|
||||
// or keep in lib? Plan said simplified main. Let's keep file creation in main usually for CLI apps,
|
||||
// but the logic for "auto name" is business logic.
|
||||
// Wait, the lib function `run` takes `writer`. So we need to create the writer here.
|
||||
|
||||
// Configure Rayon
|
||||
if let Some(threads) = args.threads {
|
||||
rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(threads)
|
||||
.build_global()
|
||||
.expect("Failed to build thread pool");
|
||||
}
|
||||
|
||||
let target_path = args.path.clone();
|
||||
|
||||
if !target_path.exists() {
|
||||
eprintln!("Error: Path '{}' does not exist.", target_path.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Parse min_size
|
||||
let min_bytes = if let Some(size_str) = &args.min_size {
|
||||
match Byte::parse_str(size_str, true) {
|
||||
Ok(byte) => byte.as_u64(),
|
||||
Err(e) => {
|
||||
eprintln!("Error parsing size '{}': {}", size_str, e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Parse sort
|
||||
let sort_criteria = parse_sort_arg(args.sort.as_deref().unwrap_or("size:dsc"));
|
||||
|
||||
// Determine output writer
|
||||
let mut writer: Box<dyn Write> = if let Some(path_arg) = &args.save {
|
||||
let output_path = if path_arg.to_string_lossy() == "_AUTO_" {
|
||||
let timestamp = Local::now().format("%Y%m%d-%H%M%S");
|
||||
let dir_name = target_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy();
|
||||
let format = format_description!("[year][month][day]-[hour][minute][second]");
|
||||
let timestamp = OffsetDateTime::now_utc().format(format).unwrap();
|
||||
let dir_name = args.path.file_name().unwrap_or_default().to_string_lossy();
|
||||
let ext = match args.format {
|
||||
OutputFormat::Csv => "csv",
|
||||
OutputFormat::Json => "json",
|
||||
@@ -233,331 +34,5 @@ fn main() {
|
||||
Box::new(std::io::stdout())
|
||||
};
|
||||
|
||||
if args.format == OutputFormat::Csv {
|
||||
// We do NOT write the header here manually anymore if we want to be safe/consistent,
|
||||
// OR we write it manually but treat it as raw bytes.
|
||||
// Actually, if we use csv::Writer, we should use it for the header too, OR assume the writer calls will handle it (no, we need to init).
|
||||
// For simplicity in this logic, let's write the header manually *once* if we are creating a fresh file/stream.
|
||||
// BUT, `process_node_recursive` might be called multiple times?
|
||||
// Wait, `process_node_recursive` is called recursively.
|
||||
// The CSV logic implementation in `print_output` currently printed children.
|
||||
// If we recurse, we print children of children.
|
||||
// So a single global header is correct.
|
||||
writeln!(writer, "Path,Type,Size(Bytes),Blocks").expect("Failed to write CSV header");
|
||||
}
|
||||
|
||||
// Build the tree!
|
||||
let root_node = build_tree(&target_path, args.ignore);
|
||||
|
||||
if root_node.size_bytes < min_bytes {
|
||||
// Even the root is too small?
|
||||
if args.format == OutputFormat::Text {
|
||||
writeln!(writer, "Root directory is smaller than minimum size.").ok();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Render output
|
||||
process_node_recursive(&mut writer, &root_node, 0, &args, min_bytes, &sort_criteria);
|
||||
}
|
||||
|
||||
/// Builds the directory tree in memory, calculating sizes in a single pass.
|
||||
/// Uses Rayon for parallelism.
|
||||
fn build_tree(path: &Path, ignore: bool) -> Node {
|
||||
let metadata = path.metadata();
|
||||
|
||||
if let Ok(meta) = metadata {
|
||||
if meta.is_file() {
|
||||
return Node {
|
||||
path: path.to_path_buf(),
|
||||
size_bytes: meta.len(),
|
||||
blocks: meta.blocks(),
|
||||
entry_type: EntryType::File,
|
||||
children: vec![],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// It's a directory (or error, which we treat as zero-size usually, but here we try to read)
|
||||
// Use ignore::WalkBuilder to get *immediate children only* respecting gitignore
|
||||
let walker = WalkBuilder::new(path)
|
||||
.standard_filters(false) // We manually control ignore
|
||||
.hidden(false) // Show hidden files
|
||||
.git_ignore(ignore)
|
||||
.ignore(ignore)
|
||||
.max_depth(Some(1)) // ONLY immediate children
|
||||
.build();
|
||||
|
||||
let child_paths: Vec<PathBuf> = walker
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.path() != path) // Exclude self
|
||||
.map(|e| e.path().to_path_buf())
|
||||
.collect();
|
||||
|
||||
// Recursively build children in parallel
|
||||
let children: Vec<Node> = child_paths
|
||||
.par_iter()
|
||||
.map(|p| build_tree(p, ignore))
|
||||
.collect();
|
||||
|
||||
// Sum up stats
|
||||
let (size_bytes, blocks) = children
|
||||
.iter()
|
||||
.fold((0, 0), |acc, c| (acc.0 + c.size_bytes, acc.1 + c.blocks));
|
||||
|
||||
// Add self-size (directory entry itself has size)
|
||||
let (self_size, self_blocks) = path
|
||||
.metadata()
|
||||
.map(|m| (m.len(), m.blocks()))
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
Node {
|
||||
path: path.to_path_buf(),
|
||||
size_bytes: size_bytes + self_size,
|
||||
blocks: blocks + self_blocks,
|
||||
entry_type: EntryType::Dir,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
fn process_node_recursive(
|
||||
writer: &mut dyn Write,
|
||||
node: &Node,
|
||||
current_depth: usize,
|
||||
args: &Args,
|
||||
min_bytes: u64,
|
||||
sort_criteria: &[(SortColumn, SortDirection)],
|
||||
) {
|
||||
if node.children.is_empty() && node.entry_type == EntryType::Dir {
|
||||
// Empty directory or leaf in terms of permissions/ignore
|
||||
}
|
||||
|
||||
// Prepare children for display
|
||||
let mut display_children: Vec<&Node> = node
|
||||
.children
|
||||
.iter()
|
||||
.filter(|c| c.size_bytes >= min_bytes)
|
||||
.collect();
|
||||
|
||||
// Sort
|
||||
display_children.sort_by(|a, b| compare_nodes(a, b, sort_criteria));
|
||||
|
||||
// Limit
|
||||
if let Some(n) = args.number {
|
||||
if n < display_children.len() {
|
||||
display_children.truncate(n);
|
||||
}
|
||||
}
|
||||
|
||||
// Default to relative paths unless --path-full is specified
|
||||
let show_relative = !args.path_full;
|
||||
let relative_path = if show_relative {
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
node.path
|
||||
.strip_prefix(&cwd)
|
||||
.unwrap_or(&node.path)
|
||||
.display()
|
||||
.to_string()
|
||||
} else {
|
||||
node.path
|
||||
.canonicalize()
|
||||
.unwrap_or(node.path.clone())
|
||||
.display()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
print_output(
|
||||
writer,
|
||||
&node,
|
||||
&display_children,
|
||||
args,
|
||||
&relative_path,
|
||||
current_depth,
|
||||
);
|
||||
|
||||
// Recurse
|
||||
if current_depth < args.depth {
|
||||
for child in display_children {
|
||||
if child.entry_type == EntryType::Dir {
|
||||
process_node_recursive(
|
||||
writer,
|
||||
child,
|
||||
current_depth + 1,
|
||||
args,
|
||||
min_bytes,
|
||||
sort_criteria,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compare_nodes(a: &Node, b: &Node, criteria: &[(SortColumn, SortDirection)]) -> Ordering {
|
||||
for (col, dir) in criteria {
|
||||
let order = match col {
|
||||
SortColumn::Name => a.path.file_name().cmp(&b.path.file_name()),
|
||||
SortColumn::Type => a.entry_type.cmp(&b.entry_type),
|
||||
SortColumn::Size => a.size_bytes.cmp(&b.size_bytes),
|
||||
SortColumn::Blocks => a.blocks.cmp(&b.blocks),
|
||||
};
|
||||
|
||||
if order != Ordering::Equal {
|
||||
return match dir {
|
||||
SortDirection::Asc => order,
|
||||
SortDirection::Dsc => order.reverse(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Ordering::Equal
|
||||
}
|
||||
|
||||
fn parse_sort_arg(s: &str) -> Vec<(SortColumn, SortDirection)> {
|
||||
s.split(',')
|
||||
.filter_map(|part| {
|
||||
let parts: Vec<&str> = part.split(':').collect();
|
||||
if parts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let col_str = parts[0];
|
||||
let col = SortColumn::from_str(col_str).ok()?;
|
||||
|
||||
let dir = if parts.len() > 1 {
|
||||
SortDirection::from_str(parts[1]).unwrap_or(SortDirection::Dsc)
|
||||
} else {
|
||||
match col {
|
||||
SortColumn::Name | SortColumn::Type => SortDirection::Asc,
|
||||
_ => SortDirection::Dsc,
|
||||
}
|
||||
};
|
||||
Some((col, dir))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn print_output(
|
||||
writer: &mut dyn Write,
|
||||
parent_node: &Node,
|
||||
children: &[&Node],
|
||||
args: &Args,
|
||||
relative_path: &str,
|
||||
depth: usize,
|
||||
) {
|
||||
match args.format {
|
||||
OutputFormat::Text => print_text(writer, parent_node, children, relative_path, depth),
|
||||
OutputFormat::Csv => print_csv(writer, children),
|
||||
OutputFormat::Json => print_json(writer, parent_node, children),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_text(
|
||||
writer: &mut dyn Write,
|
||||
parent_node: &Node,
|
||||
children: &[&Node],
|
||||
relative_path: &str,
|
||||
depth: usize,
|
||||
) {
|
||||
if depth > 0 {
|
||||
writeln!(writer, "{}", "-".repeat(60).dimmed()).ok();
|
||||
}
|
||||
writeln!(writer, "\n{}", relative_path.bold().blue()).ok();
|
||||
writeln!(
|
||||
writer,
|
||||
"Total size: {} / {} ({} blocks)",
|
||||
format_size(parent_node.size_bytes, BINARY).bold().green(),
|
||||
format_size(parent_node.size_bytes, DECIMAL).bold().green(),
|
||||
parent_node.blocks
|
||||
)
|
||||
.ok();
|
||||
|
||||
if children.is_empty() {
|
||||
writeln!(writer, "(No children)").ok();
|
||||
return;
|
||||
}
|
||||
|
||||
let mut table = Table::new();
|
||||
table
|
||||
.load_preset(UTF8_FULL)
|
||||
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||
.set_header(vec![
|
||||
Cell::new("Name").add_attribute(Attribute::Bold),
|
||||
Cell::new("Type").add_attribute(Attribute::Bold),
|
||||
Cell::new("Size (Bin)")
|
||||
.add_attribute(Attribute::Bold)
|
||||
.set_alignment(CellAlignment::Right),
|
||||
Cell::new("Size (Bytes)")
|
||||
.add_attribute(Attribute::Bold)
|
||||
.set_alignment(CellAlignment::Right),
|
||||
Cell::new("Blocks")
|
||||
.add_attribute(Attribute::Bold)
|
||||
.set_alignment(CellAlignment::Right),
|
||||
]);
|
||||
|
||||
for child in children {
|
||||
let name = child.path.file_name().unwrap_or_default().to_string_lossy();
|
||||
let color = if child.entry_type == EntryType::Dir {
|
||||
Color::Blue
|
||||
} else {
|
||||
Color::Cyan
|
||||
};
|
||||
|
||||
table.add_row(vec![
|
||||
Cell::new(name).fg(color),
|
||||
Cell::new(&child.entry_type).fg(Color::Yellow),
|
||||
Cell::new(format_size(child.size_bytes, BINARY))
|
||||
.fg(Color::Green)
|
||||
.set_alignment(CellAlignment::Right),
|
||||
Cell::new(format_size(child.size_bytes, DECIMAL))
|
||||
.fg(Color::Green)
|
||||
.set_alignment(CellAlignment::Right),
|
||||
Cell::new(child.blocks)
|
||||
.fg(Color::Green)
|
||||
.set_alignment(CellAlignment::Right),
|
||||
]);
|
||||
}
|
||||
writeln!(writer, "{}", table).ok();
|
||||
}
|
||||
|
||||
fn print_csv(writer: &mut dyn Write, children: &[&Node]) {
|
||||
// strict csv writing
|
||||
let mut wtr = WriterBuilder::new()
|
||||
.has_headers(false) // We already wrote the header globally
|
||||
.from_writer(writer);
|
||||
|
||||
for child in children {
|
||||
wtr.write_record(&[
|
||||
child.path.to_string_lossy().as_ref(), // This handles escaping automatically
|
||||
&child.entry_type.to_string(),
|
||||
&child.size_bytes.to_string(),
|
||||
&child.blocks.to_string(),
|
||||
])
|
||||
.ok();
|
||||
}
|
||||
wtr.flush().ok();
|
||||
}
|
||||
|
||||
fn print_json(writer: &mut dyn Write, parent_node: &Node, children: &[&Node]) {
|
||||
// Zero-copy serialization
|
||||
let entries_view: Vec<JsonEntry> = children
|
||||
.iter()
|
||||
.map(|c| JsonEntry {
|
||||
path: &c.path,
|
||||
metrics: JsonMetrics {
|
||||
size_bytes: c.size_bytes,
|
||||
blocks: c.blocks,
|
||||
},
|
||||
entry_type: c.entry_type.to_string(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let report = JsonReport {
|
||||
path: &parent_node.path,
|
||||
total_size: parent_node.size_bytes,
|
||||
blocks: parent_node.blocks,
|
||||
entries: entries_view,
|
||||
};
|
||||
|
||||
serde_json::to_writer(&mut *writer, &report).ok();
|
||||
writer.write_all(b"\n").ok();
|
||||
run(args, &mut writer);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
use assert_cmd::prelude::*;
|
||||
use predicates::prelude::*;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_basic_cli_run() {
|
||||
let mut cmd = Command::new(env!("CARGO_BIN_EXE_sized"));
|
||||
cmd.arg("--version")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("sized 0.2.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_on_directory() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file_path = temp_dir.path().join("test_file.txt");
|
||||
File::create(&file_path)
|
||||
.unwrap()
|
||||
.write_all(b"Hello")
|
||||
.unwrap();
|
||||
|
||||
let mut cmd = Command::new(env!("CARGO_BIN_EXE_sized"));
|
||||
cmd.arg(temp_dir.path())
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("test_file.txt")); // Basic check
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_output() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file_path = temp_dir.path().join("data.json");
|
||||
File::create(&file_path).unwrap().write_all(b"{}").unwrap();
|
||||
|
||||
let mut cmd = Command::new(env!("CARGO_BIN_EXE_sized"));
|
||||
cmd.arg(temp_dir.path())
|
||||
.arg("--format")
|
||||
.arg("json")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("\"entry_type\":\"File\""))
|
||||
.stdout(predicate::str::contains("\"path\":"));
|
||||
}
|
||||
Reference in New Issue
Block a user