diff --git a/.gitignore b/.gitignore index 05923927..6bc848eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .DS_Store +foobar diff --git a/Cargo.lock b/Cargo.lock index 9e60f9fb..510e1503 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,11 +2,24 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -30,22 +43,66 @@ dependencies = [ ] [[package]] -name = "arrayvec" -version = "0.5.2" +name = "anstream" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] -name = "async-trait" -version = "0.1.68" +name = "anstyle-parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.10", + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", ] +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "autocfg" version = "1.1.0" @@ -58,11 +115,17 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "bstr" -version = "1.3.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffdb39cb703212f3c11973452c2861b972f757b021158f3516ba10f2fa8b2c1" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", "serde", @@ -70,9 +133,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "cc" @@ -86,6 +149,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" version = "0.4.24" @@ -93,27 +162,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ "iana-time-zone", - "js-sys", "num-integer", "num-traits", - "time", - "wasm-bindgen", "winapi", ] [[package]] name = "clap" -version = "4.1.9" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a9d6ada83c1edcce028902ea27dd929069c70df4c7600b131b4d9a1ad2879cc" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ - "bitflags", + "clap_builder", "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", "clap_lex", - "is-terminal", - "once_cell", "strsim", - "termcolor", ] [[package]] @@ -127,43 +200,34 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.1.9" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fddf67631444a3a3e3e5ac51c36a5e01335302de677bd78759eaa90ab1f46644" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck", - "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] name = "clap_lex" -version = "0.3.3" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646" -dependencies = [ - "os_str_bytes", -] +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] -name = "codespan-reporting" -version = "0.11.1" +name = "colorchoice" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "config" -version = "0.13.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" dependencies = [ - "async-trait", "lazy_static", "nom", "pathdiff", @@ -178,82 +242,63 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] -name = "crossterm" -version = "0.26.1" +name = "crossbeam-deque" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "bitflags", - "crossterm_winapi", - "libc", - "mio", - "parking_lot", - "signal-hook", - "signal-hook-mio", - "winapi", + "crossbeam-epoch", + "crossbeam-utils", ] [[package]] -name = "crossterm_winapi" -version = "0.9.0" +name = "crossbeam-epoch" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "winapi", + "crossbeam-utils", ] [[package]] -name = "ctrlc" -version = "3.4.0" +name = "crossbeam-utils" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a011bbe2c35ce9c1f143b7af6f94f29a167beb4cd1d29e6740ce836f723120e" -dependencies = [ - "nix", - "windows-sys 0.48.0", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] -name = "cxx" -version = "1.0.94" +name = "crossterm" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", + "bitflags 2.5.0", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", ] [[package]] -name = "cxx-build" -version = "1.0.94" +name = "crossterm_winapi" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.10", + "winapi", ] [[package]] -name = "cxxbridge-flags" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.94" +name = "ctrlc" +version = "3.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" +checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.10", + "nix", + "windows-sys 0.52.0", ] [[package]] @@ -276,11 +321,19 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "erdtree" version = "3.1.2" dependencies = [ + "ahash", "ansi_term", + "anyhow", "chrono", "clap", "clap_complete", @@ -301,6 +354,7 @@ dependencies = [ "tempfile", "terminal_size", "thiserror", + "toml", "winapi", ] @@ -354,12 +408,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "getrandom" version = "0.2.9" @@ -368,27 +416,33 @@ checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "globset" -version = "0.4.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ "aho-corasick", "bstr", - "fnv", "log", - "regex", + "regex-automata", + "regex-syntax", ] +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -398,9 +452,9 @@ checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] name = "iana-time-zone" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -412,31 +466,39 @@ dependencies = [ [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] name = "ignore" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" dependencies = [ + "crossbeam-deque", "globset", - "lazy_static", "log", "memchr", - "regex", + "regex-automata", "same-file", - "thread_local", "walkdir", "winapi-util", ] +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "indextree" version = "4.6.0" @@ -470,22 +532,16 @@ dependencies = [ ] [[package]] -name = "is-terminal" -version = "0.4.6" +name = "is_terminal_polyfill" +version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256017f749ab3117e93acb91063009e1f1bb56d03965b14c2c8df4eb02c524d8" -dependencies = [ - "hermit-abi", - "io-lifetimes", - "rustix 0.37.7", - "windows-sys 0.45.0", -] +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -498,18 +554,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.141" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" - -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "linux-raw-sys" @@ -519,9 +566,9 @@ checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] name = "linux-raw-sys" -version = "0.3.1" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" @@ -535,18 +582,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "lscolors" -version = "0.13.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dedc85d67baf5327114fad78ab9418f8893b1121c17d5538dd11005ad1ddf2" +checksum = "53304fff6ab1e597661eee37e42ea8c47a146fca280af902bb76bff8a896e523" dependencies = [ "ansi_term", "nu-ansi-term", @@ -554,9 +598,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "minimal-lexical" @@ -572,20 +616,20 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.45.0", ] [[package]] name = "nix" -version = "0.26.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags", + "bitflags 2.5.0", "cfg-if", + "cfg_aliases", "libc", - "static_assertions", ] [[package]] @@ -600,12 +644,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "dd2800e1520bdc966782168a627aa5d1ad92e33b984bf7c7615d31280c83ff14" dependencies = [ - "overload", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -629,21 +672,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" - -[[package]] -name = "os_str_bytes" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "parking_lot" @@ -674,44 +705,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" -version = "1.0.52" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -722,7 +729,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -738,9 +745,21 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -749,9 +768,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rustix" @@ -759,7 +778,7 @@ version = "0.36.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno 0.2.8", "io-lifetimes", "libc", @@ -769,16 +788,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.7" +version = "0.38.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" +checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" dependencies = [ - "bitflags", + "bitflags 2.5.0", "errno 0.3.1", - "io-lifetimes", "libc", - "linux-raw-sys 0.3.1", - "windows-sys 0.45.0", + "linux-raw-sys 0.4.14", + "windows-sys 0.48.0", ] [[package]] @@ -797,22 +815,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] -name = "scratch" -version = "1.0.5" +name = "serde" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] [[package]] -name = "serde" -version = "1.0.156" +name = "serde_derive" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "314b5b092c0ade17c00142951e50ced110ec27cea304b1037c6969246c2469a4" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] [[package]] name = "signal-hook" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", @@ -844,12 +879,6 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strip-ansi-escapes" version = "0.1.1" @@ -861,26 +890,15 @@ dependencies = [ [[package]] name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.109" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.10" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aad1363ed6d37b84299588d62d3a7d95b5a5c2d9aad5c85609fda12afaa1f40" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", @@ -900,22 +918,13 @@ dependencies = [ "windows-sys 0.42.0", ] -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminal_size" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "rustix 0.37.7", + "rustix 0.38.25", "windows-sys 0.48.0", ] @@ -936,37 +945,41 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.10", + "syn", ] [[package]] -name = "thread_local" -version = "1.1.7" +name = "toml" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" dependencies = [ - "cfg-if", - "once_cell", + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", ] [[package]] -name = "time" -version = "0.1.45" +name = "toml_datetime" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", + "serde", ] [[package]] -name = "toml" -version = "0.5.11" +name = "toml_edit" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ + "indexmap", "serde", + "serde_spanned", + "toml_datetime", + "winnow", ] [[package]] @@ -975,12 +988,6 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - [[package]] name = "utf8parse" version = "0.2.1" @@ -1016,20 +1023,14 @@ dependencies = [ [[package]] name = "walkdir" -version = "2.3.3" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1038,9 +1039,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1048,24 +1049,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1073,22 +1074,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "winapi" @@ -1163,6 +1164,15 @@ dependencies = [ "windows-targets 0.48.0", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -1193,6 +1203,22 @@ dependencies = [ "windows_x86_64_msvc 0.48.0", ] +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -1205,6 +1231,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -1217,6 +1249,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -1229,6 +1267,18 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -1241,6 +1291,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -1253,6 +1309,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -1265,6 +1327,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -1276,3 +1344,38 @@ name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "winnow" +version = "0.5.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2" +dependencies = [ + "memchr", +] + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index dfd6d96e..6e6ef1fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ keywords = ["tree", "find", "ls", "du", "commandline"] exclude = ["assets/*", "scripts/*", "example/*"] readme = "README.md" license = "MIT" -rust-version = "1.70.0" +rust-version = "1.78.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -23,24 +23,33 @@ rust-version = "1.70.0" name = "erd" path = "src/main.rs" +[lints.clippy] +cast_precision_loss = "allow" +struct_excessive_bools = "allow" +wildcard_imports = "allow" +obfuscated_if_else = "allow" + [dependencies] +ahash = "0.8.11" ansi_term = "0.12.1" -chrono = "0.4.24" -clap = { version = "4.1.1", features = ["derive"] } +anyhow = "1.0.75" +chrono = { version = "0.4.24", default-features = false, features = ["clock", "std"] } +clap = { version = "4.5.4", features = ["derive"] } clap_complete = "4.1.1" -config = { version = "0.13.3", default-features = false, features = ["toml"] } -crossterm = "0.26.1" -ctrlc = "3.4.0" +config = { version = "0.14.0", default-features = false, features = ["toml"] } +crossterm = "0.27.0" +ctrlc = "3.4.4" dirs = "5.0" errno = "0.3.1" filesize = "0.2.0" -ignore = "0.4.2" +ignore = "0.4.22" indextree = "4.6.0" -lscolors = { version = "0.13.0", features = ["ansi_term"] } -once_cell = "1.17.0" -regex = "1.7.3" -terminal_size = "0.2.6" +lscolors = { version = "0.17.0", features = ["ansi_term"] } +once_cell = "1.19.0" +regex = "1.10.4" +terminal_size = "0.3.0" thiserror = "1.0.40" +toml = "0.8.8" [target.'cfg(unix)'.dependencies] libc = "0.2.141" diff --git a/example/.erdtree.toml b/example/.erdtree.toml index 006cb2ac..30e1a66c 100644 --- a/example/.erdtree.toml +++ b/example/.erdtree.toml @@ -1,28 +1,26 @@ +# Please becareful modifying this file as some tests may assume +# this file to look a certain way. + icons = true -human = true # Compute file sizes like `du` [du] -disk_usage = "block" +metric = "block" icons = true layout = "flat" -no-ignore = true -no-git = true -hidden = true level = 1 # Do as `ls -l` [ls] icons = true -human = true level = 1 suppress-size = true long = true -no-ignore = true -hidden = true +gitignore = true +no_hidden = true # How many lines of Rust are in this code base? [rs] -disk-usage = "line" +metric = "line" level = 1 pattern = "\\.rs$" diff --git a/example/.erdtreerc b/example/.erdtreerc deleted file mode 100644 index 184b15ef..00000000 --- a/example/.erdtreerc +++ /dev/null @@ -1,9 +0,0 @@ -# Long argument ---icons ---human - -# or short argument --l - -# args can be passed like this --d logical diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index 8c795ae5..00000000 --- a/rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -match_block_trailing_comma = true diff --git a/scripts/performance_metrics.sh b/scripts/performance_metrics.sh deleted file mode 100755 index 9d23bd7c..00000000 --- a/scripts/performance_metrics.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash - -if [[ ! "$OSTYPE" =~ "darwin" ]]; then - printf "Error: Script requires a darwin operating system.\n" - exit 1 -fi - -if [[ $(/usr/bin/id -u) != 0 ]]; then - printf "Error: Script requires root privilege.\n" - exit 1 -fi - -printf "This script will purge your disk cache. Continue? [y/n]: " - -read -r proceed - -if [[ "$proceed" != "y" ]]; then - echo "Aborted." - exit 0 -fi - -cargo build --release - -# Clear disk cache -purge - -fifo="$TMPDIR" -fifo+="erd_performance" - -if [[ -f "$fifo" ]]; then - rm "$fifo" -fi - -exec 3<>$fifo - -trap "rm -f $fifo" SIGINT - -iostat_output= -while read -r line; do - iostat_output+="$line\n" - - read -r -u3 -t1 finished - - if [[ "$finished" == "1" ]]; then - printf "$iostat_output" - rm "$fifo" - exit 0 - fi -done < <(iostat -w1) & - -iostat_job="$!" - -trap "kill $iostat_job 2> /dev/null" SIGINT - -echo "Executing command: target/release/erd ${@}" -echo - -/usr/bin/time -p target/release/erd "$@" 1> /dev/null - -echo - -echo "1" >> "$fifo" - -wait "$iostat_job" diff --git a/src/ansi.rs b/src/ansi.rs deleted file mode 100644 index a5682e3c..00000000 --- a/src/ansi.rs +++ /dev/null @@ -1,64 +0,0 @@ -/// Trait that provides functionality to ANSI escaped strings to be truncated in a manner that -/// preserves the ANSI color/style escape sequences. Consider the following: -/// -/// ``` -/// // "\u{1b}[1;31mHello World\u{1b}[0m" -/// ansi_term::Color::Red.bold().paint("Hello") -/// ``` -/// -/// Truncating the above to a length of 5 would result in: -/// -/// `"\u{1b}[1;31mHello\u{1b}[0m"` -/// -/// NOTE: This is being used for a very particular use-case and isn't comprehensive enough to -/// handle all types of ANSI escaped sequences, only color/style related ones. It also makes some -/// assumptions that are valid only for this program, namely that all relevant grapheme clusters -/// are at most sized to a single `char`, so truncating to any arbitrary length will always result -/// in a coherent output. -pub trait Escaped: AsRef { - fn truncate(&self, new_len: usize) -> String { - let mut open_sequence = false; - let mut resultant = String::new(); - let mut char_count = 0; - let mut chars = self.as_ref().chars(); - - 'outer: while let Some(ch) = chars.next() { - resultant.push(ch); - - if ch == '\u{1b}' { - for code in chars.by_ref() { - resultant.push(code); - - if code == 'm' { - open_sequence = !open_sequence; - continue 'outer; - } - } - } - char_count += 1; - - if char_count == new_len { - break; - } - } - - if open_sequence { - resultant.push_str("\u{1b}[0m"); - } - - resultant - } -} - -impl Escaped for str {} - -#[test] -fn truncate() { - use ansi_term::Color::Red; - - let control = Red.bold().paint("Hello").to_string(); - let base = format!("{}!!!", Red.bold().paint("Hello World")); - let trunc = ::truncate(&base, 5); - - assert_eq!(control, trunc); -} diff --git a/src/context/args.rs b/src/context/args.rs deleted file mode 100644 index ce202b14..00000000 --- a/src/context/args.rs +++ /dev/null @@ -1,129 +0,0 @@ -use super::{config, error::Error, Context}; -use clap::{ - builder::ArgAction, parser::ValueSource, ArgMatches, Command, CommandFactory, FromArgMatches, -}; -use std::{ - ffi::{OsStr, OsString}, - path::PathBuf, -}; - -/// Allows the implementor to compute [`ArgMatches`] that reconciles arguments from both the -/// command-line as well as the config file that gets loaded. -pub trait Reconciler: CommandFactory + FromArgMatches { - /// Loads in arguments from both the command-line as well as the config file and reconciles - /// identical arguments between the two using these rules: - /// - /// 1. If no config file is present, use arguments strictly from the command-line. - /// 2. If an argument was provided via the CLI then override the argument from the config. - /// 3. If an argument is sourced from its default value because a user didn't provide it via - /// the CLI, then select the argument from the config if it exists. - fn compute_args() -> Result { - let cmd = Self::command().args_override_self(true); - - let user_args = Command::clone(&cmd).get_matches(); - - if user_args.get_one::("no_config").is_some_and(|b| *b) { - return Ok(user_args); - } - - let maybe_config_args = { - let named_table = user_args.get_one::("config"); - - if let Some(rc) = load_rc_config_args() { - if named_table.is_some() { - return Err(Error::Rc); - } - - Some(rc) - } else { - let toml = load_toml_config_args(named_table.map(String::as_str))?; - - if named_table.is_some() && toml.is_none() { - return Err(Error::NoToml); - } - - toml - } - }; - - let Some(config_args) = maybe_config_args else { - return Ok(user_args); - }; - - let mut final_args = init_empty_args(); - - for arg in cmd.get_arguments() { - let arg_id = arg.get_id(); - let id_str = arg_id.as_str(); - - if id_str == "dir" { - if let Some(dir) = user_args.try_get_one::(id_str)? { - final_args.push(OsString::from(dir)); - } - continue; - } - - let argument_source = user_args - .value_source(id_str) - .map_or(&config_args, |source| { - if source == ValueSource::CommandLine { - &user_args - } else { - &config_args - } - }); - - let Some(key) = arg.get_long().map(|l| format!("--{l}")).map(OsString::from) else { - continue - }; - - match arg.get_action() { - ArgAction::SetTrue => { - if argument_source - .try_get_one::(id_str)? - .is_some_and(|b| *b) - { - final_args.push(key); - }; - }, - ArgAction::SetFalse => continue, - _ => { - let Ok(Some(raw)) = argument_source.try_get_raw(id_str) else { - continue; - }; - final_args.push(key); - final_args.extend(raw.map(OsStr::to_os_string)); - }, - } - } - - Ok(cmd.get_matches_from(final_args)) - } -} - -impl Reconciler for Context {} - -/// Creates a properly formatted `Vec` that [`clap::Command`] would understand. -#[inline] -fn init_empty_args() -> Vec { - vec![OsString::from("--")] -} - -/// Loads an [`ArgMatches`] from `.erdtreerc`. -#[inline] -fn load_rc_config_args() -> Option { - config::rc::read_config_to_string().map(|rc_config| { - let parsed_args = config::rc::parse(&rc_config); - Context::command().get_matches_from(parsed_args) - }) -} - -/// Loads an [`ArgMatches`] from `.erdtree.toml`. -#[inline] -fn load_toml_config_args(named_table: Option<&str>) -> Result, Error> { - let toml_config = config::toml::load()?; - let parsed_args = config::toml::parse(toml_config, named_table)?; - let config_args = Context::command().get_matches_from(parsed_args); - - Ok(Some(config_args)) -} diff --git a/src/context/color.rs b/src/context/color.rs deleted file mode 100644 index caf39c73..00000000 --- a/src/context/color.rs +++ /dev/null @@ -1,25 +0,0 @@ -use clap::ValueEnum; -use once_cell::sync::OnceCell; -use std::{env, ffi::OsString}; - -pub static NO_COLOR: OnceCell> = OnceCell::new(); - -/// Reads in the `NO_COLOR` environment variable to determine whether or not to display color in -/// the output. -pub fn no_color_env() { - let _ = NO_COLOR.set(env::var_os("NO_COLOR")); -} - -/// Enum to determine how the output should be colorized. -#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, Default)] -pub enum Coloring { - /// Print plainly without ANSI escapes - None, - - /// Attempt to colorize output - #[default] - Auto, - - /// Turn on colorization always - Force, -} diff --git a/src/context/column.rs b/src/context/column.rs deleted file mode 100644 index c66daa40..00000000 --- a/src/context/column.rs +++ /dev/null @@ -1,40 +0,0 @@ -use super::{Context, PrefixKind}; -use std::convert::From; - -/// Utility struct to help store maximum column widths for attributes of each node. Each width is -/// measured as the number of columns of the tty's window. -#[derive(Default)] -pub struct Properties { - pub max_size_width: usize, - pub max_size_unit_width: usize, - - #[cfg(unix)] - pub max_nlink_width: usize, - - #[cfg(unix)] - pub max_ino_width: usize, - - #[cfg(unix)] - pub max_block_width: usize, - - #[cfg(unix)] - pub max_owner_width: usize, - - #[cfg(unix)] - pub max_group_width: usize, -} - -impl From<&Context> for Properties { - fn from(ctx: &Context) -> Self { - let unit_width = match ctx.unit { - PrefixKind::Bin if ctx.human => 3, - PrefixKind::Si if ctx.human => 2, - _ => 1, - }; - - Self { - max_size_unit_width: unit_width, - ..Default::default() - } - } -} diff --git a/src/context/config/rc.rs b/src/context/config/rc.rs deleted file mode 100644 index 27192b75..00000000 --- a/src/context/config/rc.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::{env, fs, path::PathBuf}; - -/// Reads the config file into a `String` if there is one, otherwise returns `None`. -/// is looked for in the following locations in order: -/// -/// - `$ERDTREE_CONFIG_PATH` -/// - `$XDG_CONFIG_HOME/erdtree/.erdtreerc` -/// - `$XDG_CONFIG_HOME/.erdtreerc` -/// - `$HOME/.config/erdtree/.erdtreerc` -/// - `$HOME/.erdtreerc` -#[cfg(unix)] -pub fn read_config_to_string() -> Option { - config_from_config_path() - .or_else(config_from_xdg_path) - .or_else(config_from_home) - .map(|e| prepend_arg_prefix(&e)) -} -/// is looked for in the following locations in order (Windows specific): -/// -/// - `$ERDTREE_CONFIG_PATH` -/// - `%APPDATA%/erdtree/.erdtreerc` -#[cfg(windows)] -pub fn read_config_to_string() -> Option { - config_from_config_path() - .or_else(config_from_appdata) - .map(|e| prepend_arg_prefix(&e)) -} - -/// Parses the config `str`, removing comments and preparing it as a format understood by -/// [`get_matches_from`]. -/// -/// [`get_matches_from`]: clap::builder::Command::get_matches_from -pub fn parse<'a>(config: &'a str) -> Vec<&'a str> { - config - .lines() - .filter(|line| { - line.trim_start() - .chars() - .next() - .map_or(true, |ch| ch != '#') - }) - .flat_map(str::split_whitespace) - .collect::>() -} - -/// Try to read in config from `ERDTREE_CONFIG_PATH`. -fn config_from_config_path() -> Option { - env::var_os(super::ERDTREE_CONFIG_PATH) - .map(PathBuf::from) - .map(fs::read_to_string) - .and_then(Result::ok) -} - -/// Try to read in config from either one of the following locations: -/// - `$HOME/.config/erdtree/.erdtreerc` -/// - `$HOME/.erdtreerc` -#[cfg(not(windows))] -fn config_from_home() -> Option { - let home = env::var_os(super::HOME).map(PathBuf::from)?; - - let config_path = home - .join(super::CONFIG_DIR) - .join(super::ERDTREE_DIR) - .join(super::ERDTREE_CONFIG_NAME); - - fs::read_to_string(config_path).ok().or_else(|| { - let config_path = home.join(super::ERDTREE_CONFIG_NAME); - fs::read_to_string(config_path).ok() - }) -} - -/// Windows specific: Try to read in config from the following location: -/// - `%APPDATA%/erdtree/.erdtreerc` -#[cfg(windows)] -fn config_from_appdata() -> Option { - let app_data = dirs::config_dir()?; - - let config_path = app_data - .join(super::ERDTREE_DIR) - .join(super::ERDTREE_CONFIG_NAME); - - fs::read_to_string(config_path).ok() -} - -/// Try to read in config from either one of the following locations: -/// - `$XDG_CONFIG_HOME/erdtree/.erdtreerc` -/// - `$XDG_CONFIG_HOME/.erdtreerc` -#[cfg(unix)] -fn config_from_xdg_path() -> Option { - let xdg_config = env::var_os(super::XDG_CONFIG_HOME).map(PathBuf::from)?; - - let config_path = xdg_config - .join(super::ERDTREE_DIR) - .join(super::ERDTREE_CONFIG_NAME); - - fs::read_to_string(config_path).ok().or_else(|| { - let config_path = xdg_config.join(super::ERDTREE_CONFIG_NAME); - fs::read_to_string(config_path).ok() - }) -} - -/// Prepends "--\n" to the config string which is required for proper parsing by -/// [`get_matches_from`]. -/// -/// [`get_matches_from`]: clap::builder::Command::get_matches_from -fn prepend_arg_prefix(config: &str) -> String { - format!("--\n{config}") -} diff --git a/src/context/config/toml/error.rs b/src/context/config/toml/error.rs deleted file mode 100644 index 3757c98e..00000000 --- a/src/context/config/toml/error.rs +++ /dev/null @@ -1,19 +0,0 @@ -use config::ConfigError; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("Failed to load .erdtree.toml")] - LoadConfig, - - #[error("The configuration file is improperly formatted: {0}")] - InvalidFormat(#[from] ConfigError), - - #[error("Named table '{0}' was not found in '.erdtree.toml'")] - MissingAltConfig(String), - - #[error("'#{0}' is required to be a pointer-sized unsigned integer type")] - InvalidInteger(String), - - #[error("'#{0}' has a type that is invalid")] - InvalidArgument(String), -} diff --git a/src/context/config/toml/mod.rs b/src/context/config/toml/mod.rs deleted file mode 100644 index 95b8b2f5..00000000 --- a/src/context/config/toml/mod.rs +++ /dev/null @@ -1,251 +0,0 @@ -use config::{Config, File, Value, ValueKind}; -use error::Error; -use std::{env, ffi::OsString}; - -/// Errors associated with loading and parsing the toml config file. -pub mod error; - -/// Testing related to `.erdtree.toml`. -pub mod test; - -/// Represents an instruction on how to handle a single key-value pair, which makes up a single -/// command-line argument, when constructing the arguments vector. -enum ArgInstructions { - /// Used for bool arguments such as `--icons`. When `icons = true` is set in `.erdtree.toml`, - /// we only want `--icons` to be pushed into the ultimate arguments vector. - PushKeyOnly, - - /// Used for arguments such as `--threads 10`. - PushKeyValue { parsed_value: OsString }, - - /// If a bool field is set to false in `.erdtree.toml` (e.g. `icons = false`) then we want to - /// completely omit the key-value pair from the arguments that we ultimately use. - Pass, -} - -/// Takes in a `Config` that is generated from [`load`] returning a `Vec` which -/// represents command-line arguments from `.erdtree.toml`. If a `named_table` is provided then -/// the top-level table in `.erdtree.toml` is ignored and the configurations specified in the -/// `named_table` will be used instead. -pub fn parse(config: Config, named_table: Option<&str>) -> Result, Error> { - let mut args_map = config.cache.into_table()?; - - if let Some(table) = named_table { - let new_conf = args_map - .get(table) - .and_then(|conf| conf.clone().into_table().ok()) - .ok_or_else(|| Error::MissingAltConfig(table.to_owned()))?; - - args_map = new_conf; - } else { - args_map.retain(|_k, v| !matches!(v.kind, ValueKind::Table(_))); - } - - let mut parsed_args = vec![OsString::from("--")]; - - let process_key = |s| OsString::from(format!("--{s}").replace('_', "-")); - - for (k, v) in &args_map { - match parse_argument(k, v)? { - ArgInstructions::PushKeyValue { parsed_value } => { - let fmt_key = process_key(k); - parsed_args.push(fmt_key); - parsed_args.push(parsed_value); - }, - - ArgInstructions::PushKeyOnly => { - let fmt_key = process_key(k); - parsed_args.push(fmt_key); - }, - - ArgInstructions::Pass => continue, - } - } - - Ok(parsed_args) -} - -/// Reads in `.erdtree.toml` file. -pub fn load() -> Result { - #[cfg(windows)] - return windows::load_toml(); - - #[cfg(unix)] - unix::load_toml() -} - -/// Attempts to load in `.erdtree.toml` from `$ERDTREE_TOML_PATH`. -fn toml_from_env() -> Result { - let config = env::var_os(super::ERDTREE_TOML_PATH) - .map(OsString::into_string) - .transpose() - .map_err(|_| Error::LoadConfig)? - .ok_or(Error::LoadConfig)?; - - let file = config - .strip_suffix(".toml") - .map(File::with_name) - .ok_or(Error::LoadConfig)?; - - Config::builder() - .add_source(file) - .build() - .map_err(Error::from) -} - -/// Simple utility used to extract the underlying value from the [`Value`] enum that we get when -/// loading in the values from `.erdtree.toml`, returning instructions on how the argument should -/// be processed into the ultimate arguments vector. -fn parse_argument(keyword: &str, arg: &Value) -> Result { - macro_rules! try_parse_num { - ($n:expr) => { - usize::try_from($n) - .map_err(|_e| Error::InvalidInteger(keyword.to_owned())) - .map(|num| { - let parsed = OsString::from(format!("{num}")); - ArgInstructions::PushKeyValue { - parsed_value: parsed, - } - }) - }; - } - - match &arg.kind { - ValueKind::Boolean(val) => { - if *val { - Ok(ArgInstructions::PushKeyOnly) - } else { - Ok(ArgInstructions::Pass) - } - }, - ValueKind::String(val) => Ok(ArgInstructions::PushKeyValue { - parsed_value: OsString::from(val), - }), - ValueKind::I64(val) => try_parse_num!(*val), - ValueKind::I128(val) => try_parse_num!(*val), - ValueKind::U64(val) => try_parse_num!(*val), - ValueKind::U128(val) => try_parse_num!(*val), - _ => Err(Error::InvalidArgument(keyword.to_owned())), - } -} - -/// Concerned with how to load `.erdtree.toml` on Unix systems. -#[cfg(unix)] -mod unix { - use super::super::{CONFIG_DIR, ERDTREE_CONFIG_TOML, ERDTREE_DIR, HOME, XDG_CONFIG_HOME}; - use super::Error; - use config::{Config, ConfigError, File}; - use std::{env, path::PathBuf}; - - /// Looks for `.erdtree.toml` in the following locations in order: - /// - /// - `$ERDTREE_TOML_PATH` - /// - `$XDG_CONFIG_HOME/erdtree/.erdtree.toml` - /// - `$XDG_CONFIG_HOME/.erdtree.toml` - /// - `$HOME/.config/erdtree/.erdtree.toml` - /// - `$HOME/.erdtree.toml` - pub(super) fn load_toml() -> Result { - super::toml_from_env() - .or_else(|_| toml_from_xdg_path()) - .or_else(|_| toml_from_home()) - } - - /// Looks for `.erdtree.toml` in the following locations in order: - /// - /// - `$XDG_CONFIG_HOME/erdtree/.erdtree.toml` - /// - `$XDG_CONFIG_HOME/.erdtree.toml` - fn toml_from_xdg_path() -> Result { - let config = env::var_os(XDG_CONFIG_HOME) - .map(PathBuf::from) - .ok_or(Error::LoadConfig)?; - - let mut file = config - .join(ERDTREE_DIR) - .join(ERDTREE_CONFIG_TOML) - .to_str() - .and_then(|s| s.strip_suffix(".toml")) - .map(File::with_name); - - if file.is_none() { - file = config - .join(ERDTREE_CONFIG_TOML) - .to_str() - .and_then(|s| s.strip_suffix(".toml")) - .map(File::with_name); - } - - file.map_or_else( - || Err(Error::LoadConfig), - |f| Config::builder().add_source(f).build().map_err(Error::from), - ) - } - - /// Looks for `.erdtree.toml` in the following locations in order: - /// - /// - `$HOME/.config/erdtree/.erdtree.toml` - /// - `$HOME/.erdtree.toml` - fn toml_from_home() -> Result { - let home = env::var_os(HOME) - .map(PathBuf::from) - .ok_or(Error::LoadConfig)?; - - let mut file = home - .join(CONFIG_DIR) - .join(ERDTREE_DIR) - .join(ERDTREE_CONFIG_TOML) - .to_str() - .and_then(|s| s.strip_suffix(".toml")) - .map(File::with_name); - - if file.is_none() { - file = home - .join(ERDTREE_CONFIG_TOML) - .to_str() - .and_then(|s| s.strip_suffix(".toml")) - .map(File::with_name); - } - - file.map_or_else( - || Err(Error::LoadConfig), - |f| Config::builder() - .add_source(f) - .build() - .map_err(|err| match err { - ConfigError::FileParse { .. } | ConfigError::Type { .. } => Error::from(err), - _ => Error::LoadConfig, - }), - ) - } -} - -/// Concerned with how to load `.erdtree.toml` on Windows. -#[cfg(windows)] -mod windows { - use super::super::{ERDTREE_CONFIG_TOML, ERDTREE_DIR}; - use super::Error; - use config::{Config, File}; - - /// Try to read in config from the following location: - /// - `%APPDATA%\erdtree\.erdtree.toml` - pub(super) fn load_toml() -> Result { - super::toml_from_env().or_else(toml_from_appdata) - } - - /// Try to read in config from the following location: - /// - `%APPDATA%\erdtree\.erdtree.toml` - fn toml_from_appdata() -> Result { - let app_data = dirs::config_dir()?; - - let file = app_data - .join(ERDTREE_DIR) - .join(ERDTREE_CONFIG_TOML) - .to_str() - .and_then(|s| s.strip_suffix(".toml")) - .map(File::with_name)?; - - Config::builder() - .add_source(file) - .build() - .map_err(Error::from) - } -} diff --git a/src/context/config/toml/test.rs b/src/context/config/toml/test.rs deleted file mode 100644 index 368c9db7..00000000 --- a/src/context/config/toml/test.rs +++ /dev/null @@ -1,91 +0,0 @@ -#[test] -fn parse_toml() -> Result<(), Box> { - use config::{Config, File}; - use std::{ffi::OsString, io::Write}; - use tempfile::Builder; - - let mut config_file = Builder::new() - .prefix(".erdtree") - .suffix(".toml") - .tempfile()?; - - let toml_contents = r#" - icons = true - human = true - threads = 10 - - [grogoroth] - disk_usage = "block" - icons = true - human = false - threads = 10 - "#; - - config_file.write_all(toml_contents.as_bytes())?; - - let file = config_file - .path() - .to_str() - .and_then(|s| s.strip_suffix(".toml")) - .map(File::with_name) - .unwrap(); - - let config = Config::builder().add_source(file).build()?; - - // TOP-LEVEL TABLE - let mut toml = super::parse(config.clone(), None)?; - - let expected = vec![ - OsString::from("--"), - OsString::from("--icons"), - OsString::from("--human"), - OsString::from("--threads"), - OsString::from("10"), - ]; - - for (i, outer_item) in expected.iter().enumerate() { - for j in 0..toml.len() { - let inner_item = &toml[j]; - - if outer_item == inner_item { - toml.swap(i, j); - } - } - } - - assert_eq!(toml.len(), expected.len()); - - for (lhs, rhs) in toml.iter().zip(expected.iter()) { - assert_eq!(lhs, rhs); - } - - // NAMED-TABLE - let mut toml = super::parse(config, Some("grogoroth"))?; - - let expected = vec![ - OsString::from("--"), - OsString::from("--disk-usage"), - OsString::from("block"), - OsString::from("--icons"), - OsString::from("--threads"), - OsString::from("10"), - ]; - - for (i, outer_item) in expected.iter().enumerate() { - for j in 0..toml.len() { - let inner_item = &toml[j]; - - if outer_item == inner_item { - toml.swap(i, j); - } - } - } - - assert_eq!(toml.len(), expected.len()); - - for (lhs, rhs) in toml.iter().zip(expected.iter()) { - assert_eq!(lhs, rhs); - } - - Ok(()) -} diff --git a/src/context/dir.rs b/src/context/dir.rs deleted file mode 100644 index 0a87dd32..00000000 --- a/src/context/dir.rs +++ /dev/null @@ -1,15 +0,0 @@ -use clap::ValueEnum; - -/// Enum to determine how directories should be ordered relative to regular files in output. -#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, Default)] -pub enum Order { - /// Directories are ordered as if they were regular nodes. - #[default] - None, - - /// Sort directories above files - First, - - /// Sort directories below files - Last, -} diff --git a/src/context/error.rs b/src/context/error.rs deleted file mode 100644 index 89fbb740..00000000 --- a/src/context/error.rs +++ /dev/null @@ -1,37 +0,0 @@ -use super::config::toml::error::Error as TomlError; -use clap::{parser::MatchesError, Error as ClapError}; -use ignore::Error as IgnoreError; -use regex::Error as RegexError; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("{0}")] - ArgParse(ClapError), - - #[error("A configuration file was found but failed to parse: {0}")] - Config(ClapError), - - #[error("No glob was provided")] - EmptyGlob, - - #[error("{0}")] - IgnoreError(#[from] IgnoreError), - - #[error("{0}")] - InvalidRegularExpression(#[from] RegexError), - - #[error("Missing '--pattern' argument")] - PatternNotProvided, - - #[error("{0}")] - ConfigError(#[from] TomlError), - - #[error("{0}")] - MatchError(#[from] MatchesError), - - #[error("'--config' was specified but a `.erdtree.toml` file could not be found")] - NoToml, - - #[error("Please migrate from `erdtreerc` to `.erdtree.toml` to make use of `--config`")] - Rc, -} diff --git a/src/context/file.rs b/src/context/file.rs deleted file mode 100644 index 01c29e3c..00000000 --- a/src/context/file.rs +++ /dev/null @@ -1,15 +0,0 @@ -use clap::ValueEnum; - -/// File-types found in both Unix and Windows. -#[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default)] -pub enum Type { - /// A regular file. - #[default] - File, - - /// A directory. - Dir, - - /// A symlink. - Link, -} diff --git a/src/context/layout.rs b/src/context/layout.rs deleted file mode 100644 index 7e88bcc2..00000000 --- a/src/context/layout.rs +++ /dev/null @@ -1,18 +0,0 @@ -use clap::ValueEnum; - -/// Which layout to use when rendering the tree. -#[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default)] -pub enum Type { - /// Outputs the tree with the root node at the bottom of the output - #[default] - Regular, - - /// Outputs the tree with the root node at the top of the output - Inverted, - - /// Outputs a flat layout using paths rather than an ASCII tree - Flat, - - /// Outputs an inverted flat layout with the root at the top of the output - Iflat, -} diff --git a/src/context/mod.rs b/src/context/mod.rs deleted file mode 100644 index fbcfd1e2..00000000 --- a/src/context/mod.rs +++ /dev/null @@ -1,500 +0,0 @@ -use super::disk_usage::{file_size::DiskUsage, units::PrefixKind}; - -use args::Reconciler; -use clap::{FromArgMatches, Parser}; -use color::Coloring; -use error::Error; -use ignore::{ - overrides::{Override, OverrideBuilder}, - DirEntry, -}; -use regex::Regex; -use std::{ - borrow::Borrow, - convert::From, - io::{stdin, stdout, IsTerminal}, - num::NonZeroUsize, - path::{Path, PathBuf}, - thread::available_parallelism, -}; - -/// Concerned with figuring out how to reconcile arguments provided via the command-line with -/// arguments that come from a config file. -pub mod args; - -/// Operations to load in defaults from configuration file. -pub mod config; - -/// Controlling color of output. -pub mod color; - -/// Controlling order of directories in output. -pub mod dir; - -/// [Context] related errors. -pub mod error; - -/// Common cross-platform file-types. -pub mod file; - -/// For determining the output layout. -pub mod layout; - -/// Utilities to print output. -pub mod column; - -/// Printing order kinds. -pub mod sort; - -/// Different types of timestamps available in long view. -#[cfg(unix)] -pub mod time; - -/// Defines the CLI. -#[derive(Parser, Debug)] -#[command(name = "erdtree")] -#[command(author = "Benjamin Nguyen. ")] -#[command(version = "3.1.2")] -#[command(about = "erdtree (erd) is a cross-platform, multi-threaded, and general purpose filesystem and disk usage utility.", long_about = None)] -pub struct Context { - /// Directory to traverse; defaults to current working directory - dir: Option, - - /// Use configuration of named table rather than the top-level table in .erdtree.toml - #[arg(short = 'c', long)] - pub config: Option, - - /// Mode of coloring output - #[arg(short = 'C', long, value_enum, default_value_t)] - pub color: Coloring, - - /// Print physical or logical file size - #[arg(short, long, value_enum, default_value_t)] - pub disk_usage: DiskUsage, - - /// Follow symlinks - #[arg(short = 'f', long)] - pub follow: bool, - - /// Print disk usage in human-readable format - #[arg(short = 'H', long)] - pub human: bool, - - /// Do not respect .gitignore files - #[arg(short = 'i', long)] - pub no_ignore: bool, - - /// Display file icons - #[arg(short = 'I', long)] - pub icons: bool, - - /// Show extended metadata and attributes - #[cfg(unix)] - #[arg(short, long)] - pub long: bool, - - /// Show file's groups - #[cfg(unix)] - #[arg(long)] - pub group: bool, - - /// Show each file's ino - #[cfg(unix)] - #[arg(long)] - pub ino: bool, - - /// Show the total number of hardlinks to the underlying inode - #[cfg(unix)] - #[arg(long)] - pub nlink: bool, - - /// Show permissions in numeric octal format instead of symbolic - #[cfg(unix)] - #[arg(long, requires = "long")] - pub octal: bool, - - /// Which kind of timestamp to use; modified by default - #[cfg(unix)] - #[arg(long, value_enum, requires = "long")] - pub time: Option, - - /// Which format to use for the timestamp; default by default - #[cfg(unix)] - #[arg(long = "time-format", value_enum, requires = "long")] - pub time_format: Option, - - /// Maximum depth to display - #[arg(short = 'L', long, value_name = "NUM")] - level: Option, - - /// Regular expression (or glob if '--glob' or '--iglob' is used) used to match files - #[arg(short, long)] - pub pattern: Option, - - /// Enables glob based searching - #[arg(group = "searching", long, requires = "pattern")] - pub glob: bool, - - /// Enables case-insensitive glob based searching - #[arg(group = "searching", long, requires = "pattern")] - pub iglob: bool, - - /// Restrict regex or glob search to a particular file-type - #[arg(short = 't', long, requires = "pattern", value_enum)] - pub file_type: Option, - - /// Remove empty directories from output - #[arg(short = 'P', long)] - pub prune: bool, - - /// How to sort entries - #[arg(short, long, value_enum, default_value_t)] - pub sort: sort::Type, - - /// Sort directories before or after all other file types - #[arg(long, value_enum, default_value_t)] - pub dir_order: dir::Order, - - /// Number of threads to use - #[arg(short = 'T', long, default_value_t = Context::num_threads())] - pub threads: usize, - - /// Report disk usage in binary or SI units - #[arg(short, long, value_enum, default_value_t)] - pub unit: PrefixKind, - - /// Prevent traversal into directories that are on different filesystems - #[arg(short = 'x', long = "one-file-system")] - pub same_fs: bool, - - /// Which kind of layout to use when rendering the output - #[arg(short = 'y', long, value_enum, default_value_t)] - pub layout: layout::Type, - - /// Show hidden files - #[arg(short = '.', long)] - pub hidden: bool, - - /// Disable traversal of .git directory when traversing hidden files - #[arg(long, requires = "hidden")] - pub no_git: bool, - - #[arg(long)] - /// Print completions for a given shell to stdout - pub completions: Option, - - /// Only print directories - #[arg(long)] - pub dirs_only: bool, - - /// Don't read configuration file - #[arg(long)] - pub no_config: bool, - - /// Hides the progress indicator - #[arg(long)] - pub no_progress: bool, - - /// Omit disk usage from output - #[arg(long)] - pub suppress_size: bool, - - /// Truncate output to fit terminal emulator window - #[arg(long)] - pub truncate: bool, - - ////////////////////////// - /* INTERNAL USAGE BELOW */ - ////////////////////////// - /// Is stdin in a tty? - #[clap(skip = stdin().is_terminal())] - pub stdin_is_tty: bool, - - /// Is stdin in a tty? - #[clap(skip = stdout().is_terminal())] - pub stdout_is_tty: bool, - - /// Restricts column width of size not including units - #[clap(skip = usize::default())] - pub max_size_width: usize, - - /// Restricts column width of disk_usage units - #[clap(skip = usize::default())] - pub max_size_unit_width: usize, - - /// Restricts column width of nlink for long view - #[clap(skip = usize::default())] - #[cfg(unix)] - pub max_nlink_width: usize, - - /// Restricts column width of ino for long view - #[clap(skip = usize::default())] - #[cfg(unix)] - pub max_ino_width: usize, - - /// Restricts column width of block for long view - #[clap(skip = usize::default())] - #[cfg(unix)] - pub max_block_width: usize, - - /// Restricts column width of file owner for long view - #[clap(skip = usize::default())] - #[cfg(unix)] - pub max_owner_width: usize, - - /// Restricts column width of file group for long view - #[clap(skip = usize::default())] - #[cfg(unix)] - pub max_group_width: usize, - - /// Width of the terminal emulator's window - #[clap(skip)] - pub window_width: Option, -} - -type Predicate = Result bool + Send + Sync + 'static>, Error>; - -impl Context { - /// Initializes [Context], optionally reading in the configuration file to override defaults. - /// Arguments provided will take precedence over config. - pub fn try_init() -> Result { - Self::compute_args().and_then(|args| { - color::no_color_env(); - Self::from_arg_matches(&args).map_err(Error::Config) - }) - } - - /// Determines whether or not it's appropriate to display color in output based on - /// the Coloring, and whether or not stdout is connected to a tty. - /// - /// If Coloring is Force then this will always evaluate to `false`. - pub fn no_color(&self) -> bool { - if let Some(Some(var)) = color::NO_COLOR.get() { - return !var.is_empty(); - } - - match self.color { - Coloring::Auto if !self.stdout_is_tty => true, - Coloring::None => true, - Coloring::Auto | Coloring::Force => false, - } - } - - /// Returns [Path] of the root directory to be traversed. - pub fn dir(&self) -> &Path { - self.dir - .as_ref() - .map_or_else(|| Path::new("."), |pb| pb.as_path()) - } - - /// Returns canonical [Path] of the root directory to be traversed. - pub fn dir_canonical(&self) -> PathBuf { - std::fs::canonicalize(self.dir()).unwrap_or_else(|_| self.dir().to_path_buf()) - } - - /// The max depth to print. Note that all directories are fully traversed to compute file - /// sizes; this just determines how much to print. - pub fn level(&self) -> usize { - self.level.unwrap_or(usize::MAX) - } - - /// Which timestamp type to use for long view; defaults to modified. - #[cfg(unix)] - pub fn time(&self) -> time::Stamp { - self.time.unwrap_or_default() - } - - /// Which format to use for the timestamp; default by default - #[cfg(unix)] - pub fn time_format(&self) -> time::Format { - self.time_format.unwrap_or_default() - } - - /// Which `FileType` to filter on; defaults to regular file. - pub fn file_type(&self) -> file::Type { - self.file_type.unwrap_or_default() - } - - /// Predicate used for filtering via regular expressions and file-type. When matching regular - /// files, directories will always be included since matched files will need to be bridged back - /// to the root node somehow. Empty sets not producing an output is handled by [`Tree`]. - /// - /// [`Tree`]: crate::tree::Tree - pub fn regex_predicate(&self) -> Predicate { - let Some(pattern) = self.pattern.as_ref() else { - return Err(Error::PatternNotProvided); - }; - - let re = Regex::new(pattern)?; - - let file_type = self.file_type(); - - Ok(match file_type { - file::Type::Dir => Box::new(move |dir_entry| { - let is_dir = dir_entry.file_type().map_or(false, |ft| ft.is_dir()); - if is_dir { - return Self::ancestor_regex_match(dir_entry.path(), &re, 0); - } - - Self::ancestor_regex_match(dir_entry.path(), &re, 1) - }), - - _ => Box::new(move |dir_entry| { - let entry_type = dir_entry.file_type(); - let is_dir = entry_type.map_or(false, |ft| ft.is_dir()); - - if is_dir { - return true; - } - - match file_type { - file::Type::File if entry_type.map_or(true, |ft| !ft.is_file()) => { - return false - }, - file::Type::Link if entry_type.map_or(true, |ft| !ft.is_symlink()) => { - return false - }, - _ => {}, - } - let file_name = dir_entry.file_name().to_string_lossy(); - re.is_match(&file_name) - }), - }) - } - - /// Predicate used for filtering via globs and file-types. - pub fn glob_predicate(&self) -> Predicate { - let mut builder = OverrideBuilder::new(self.dir()); - - let mut negated_glob = false; - - let overrides = { - if self.iglob { - builder.case_insensitive(true)?; - } - - if let Some(ref glob) = self.pattern { - let trim = glob.trim_start(); - negated_glob = trim.starts_with('!'); - - if negated_glob { - builder.add(trim.trim_start_matches('!'))?; - } else { - builder.add(trim)?; - } - } - - builder.build()? - }; - - let file_type = self.file_type(); - - match file_type { - file::Type::Dir => Ok(Box::new(move |dir_entry| { - let is_dir = dir_entry.file_type().map_or(false, |ft| ft.is_dir()); - - if is_dir { - if negated_glob { - return !Self::ancestor_glob_match(dir_entry.path(), &overrides, 0); - } - return Self::ancestor_glob_match(dir_entry.path(), &overrides, 0); - } - let matched = Self::ancestor_glob_match(dir_entry.path(), &overrides, 1); - - if negated_glob { - !matched - } else { - matched - } - })), - - _ => Ok(Box::new(move |dir_entry| { - let entry_type = dir_entry.file_type(); - let is_dir = entry_type.map_or(false, |ft| ft.is_dir()); - - if is_dir { - return true; - } - - match file_type { - file::Type::File if entry_type.map_or(true, |ft| !ft.is_file()) => { - return false - }, - file::Type::Link if entry_type.map_or(true, |ft| !ft.is_symlink()) => { - return false - }, - _ => {}, - } - - let matched = overrides.matched(dir_entry.path(), false); - - if negated_glob { - !matched.is_whitelist() - } else { - matched.is_whitelist() - } - })), - } - } - - /// Special override to toggle the visibility of the git directory. - pub fn no_git_override(&self) -> Result { - let mut builder = OverrideBuilder::new(self.dir()); - - if self.no_git { - builder.add("!.git")?; - } - - Ok(builder.build()?) - } - - /// Update column width properties. - pub fn update_column_properties(&mut self, col_props: &column::Properties) { - self.max_size_width = col_props.max_size_width; - self.max_size_unit_width = col_props.max_size_unit_width; - - #[cfg(unix)] - { - self.max_owner_width = col_props.max_owner_width; - self.max_group_width = col_props.max_group_width; - self.max_nlink_width = col_props.max_nlink_width; - self.max_block_width = col_props.max_block_width; - self.max_ino_width = col_props.max_ino_width; - } - } - - /// Setter for `window_width` which is set to the current terminal emulator's window width. - #[inline] - pub fn set_window_width(&mut self) { - self.window_width = crate::tty::get_window_width(); - } - - /// Answers whether disk usage is asked to be reported in bytes. - pub const fn byte_metric(&self) -> bool { - matches!(self.disk_usage, DiskUsage::Logical | DiskUsage::Physical) - } - - /// Do any of the components of a path match the provided glob? This is used for ensuring that - /// all children of a directory that a glob targets gets captured. - #[inline] - fn ancestor_glob_match(path: &Path, ovr: &Override, skip: usize) -> bool { - path.components() - .rev() - .skip(skip) - .any(|c| ovr.matched(c, false).is_whitelist()) - } - - /// Like [`Self::ancestor_glob_match`] except uses [Regex] rather than [Override]. - #[inline] - fn ancestor_regex_match(path: &Path, re: &Regex, skip: usize) -> bool { - path.components() - .rev() - .skip(skip) - .any(|comp| re.is_match(comp.as_os_str().to_string_lossy().borrow())) - } - - /// The default number of threads to use for disk-reads and parallel processing. - fn num_threads() -> usize { - available_parallelism().map(NonZeroUsize::get).unwrap_or(3) - } -} diff --git a/src/context/sort.rs b/src/context/sort.rs deleted file mode 100644 index b7d9043f..00000000 --- a/src/context/sort.rs +++ /dev/null @@ -1,41 +0,0 @@ -use clap::ValueEnum; - -/// Order in which to print nodes. -#[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default)] -pub enum Type { - /// Sort entries by file name in lexicographical order. - Name, - /// Sort entries by file name in reversed lexicographical order. - Rname, - - /// Sort entries by size smallest to largest, top to bottom - #[default] - Size, - - /// Sort entries by size largest to smallest, bottom to top - Rsize, - - /// Sort entries by newer to older Accessing Date - #[value(alias("atime"))] - Access, - - /// Sort entries by older to newer Accessing Date - #[value(alias("ratime"))] - Raccess, - - /// Sort entries by newer to older Creation Date - #[value(alias("ctime"))] - Create, - - /// Sort entries by older to newer Creation Date - #[value(alias("rctime"))] - Rcreate, - - /// Sort entries by newer to older Alteration Date - #[value(alias("mtime"))] - Mod, - - /// Sort entries by older to newer Alteration Date - #[value(alias("rmtime"))] - Rmod, -} diff --git a/src/context/time.rs b/src/context/time.rs deleted file mode 100644 index 88f9657c..00000000 --- a/src/context/time.rs +++ /dev/null @@ -1,35 +0,0 @@ -use clap::ValueEnum; - -/// Different types of timestamps available in long-view. -#[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default)] -pub enum Stamp { - /// Time created (alias: ctime) - #[value(alias("ctime"))] - Create, - - /// Time last accessed (alias: atime) - #[value(alias("atime"))] - Access, - - /// Time last modified (alias: mtime) - #[default] - #[value(alias("mtime"))] - Mod, -} - -/// Different formatting options for timestamps -#[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default)] -pub enum Format { - /// Timestamp formatted following the iso8601, with slight differences and the time-zone omitted - Iso, - - /// Timestamp formatted following the exact iso8601 specifications - IsoStrict, - - /// Timestamp only shows date without time in YYYY-MM-DD format - Short, - - /// Timestamp is shown in DD MMM HH:MM format - #[default] - Default, -} diff --git a/src/disk/mod.rs b/src/disk/mod.rs new file mode 100644 index 00000000..70bfecf8 --- /dev/null +++ b/src/disk/mod.rs @@ -0,0 +1,331 @@ +use crate::user::args::BytePresentation; +use ignore::DirEntry; +use std::{ + fmt::{self, Display}, + fs::{self, Metadata}, + io, + ops::AddAssign, +}; + +/// Binary and SI prefixes. +pub mod prefix; + +/// +#[cfg(unix)] +const BLOCK_SIZE: u64 = 512; + +/// Padding between the numerical value the byte-unit, "B" used when reporting bytes. +pub const RAW_PADDING: usize = 2; + +/// Padding between the numerical value and the SI units used when reporting bytes. +pub const SI_PADDING: usize = 3; + +/// Padding between the numerical value and the binary units used when reporting bytes. +pub const BIN_PADDING: usize = 4; + +/// Precision to use when reporting bytes in human-readable format. +pub const FLOAT_PRECISION: usize = 1; + +/// Different metrics for reporting file size. +#[derive(Debug)] +pub enum Usage { + /// Apparent size in bytes rather than actual disk usage. + Logical { + value: u64, + presentation: BytePresentation, + }, + + /// The amount of bytes used to store the relevant file on disk. + Physical { + value: u64, + presentation: BytePresentation, + }, + + /// The amount of blocks used to store the relevant file on disk. + #[cfg(unix)] + Blocks(u64), + + /// The total amount of words in a file + WordCount(u64), + + /// The total amount of lines in a file + LineCount(u64), +} + +impl Usage { + /// Gets the actual bytes stored on disk for a particular file. Directory sizes must be + /// recursively computed so they will be initialized to a size of 0. + #[cfg(unix)] + pub fn init_physical(metadata: &Metadata, presentation: BytePresentation) -> Self { + use std::os::unix::fs::MetadataExt; + + let value = metadata + .is_dir() + .then_some(0) + .unwrap_or_else(|| metadata.blocks() * BLOCK_SIZE); + + Self::Physical { + value, + presentation, + } + } + + /// Gets the actual bytes stored on disk for a particular file. Directory sizes must be + /// recursively computed so they will be initialized to a size of 0. + #[cfg(windows)] + pub fn init_physical(metadata: &Metadata, presentation: BytePresentation) -> Self { + use std::os::windows::fs::MetadataExt; + + let value = metadata + .is_dir() + .then_some(0) + .unwrap_or_else(|| metadata.file_size()); + + Self::Physical { + value, + presentation, + } + } + + #[cfg(not(any(windows, unix)))] + pub fn init_physical(metadata: &Metadata, presentation: BytePresentation) -> Self { + Self::init_logical(metadata, presentation) + } + + /// Gets the apparent file size rather than disk usage. Refer to `--apparent-size` in the man + /// pages of `du`: + pub fn init_logical(metadata: &Metadata, presentation: BytePresentation) -> Self { + let value = metadata.is_dir().then_some(0).unwrap_or(metadata.len()); + + Self::Logical { + value, + presentation, + } + } + + /// Gets the word count. Words are delimited by a whitespace or a sequence of whitespaces. + /// Directories are initialized to 0. The `follow` argument determines whether or not to query the + /// symlink target, otherwise the symlink will have a word count of 0. + pub fn init_word_count( + data: &DirEntry, + metadata: &Metadata, + follow: bool, + ) -> Result { + if metadata.is_dir() || (metadata.is_symlink() && !follow) { + return Ok(Self::WordCount(0)); + } + + let word_count = + std::fs::read_to_string(data.path()).map(|data| data.split_whitespace().count())?; + + let word_count = u64::try_from(word_count) + .map_or_else(|_| Self::WordCount(word_count as u64), Self::WordCount); + + Ok(word_count) + } + + /// Gets the line count. Lines are delimited by the new-line ASCII char. Directories are + /// initialized to 0. The `follow` argument determines whether or not to query the symlink + /// target, otherwise the symlink will have a count of 0. + pub fn init_line_count( + data: &DirEntry, + metadata: &Metadata, + follow: bool, + ) -> Result { + if metadata.is_dir() || (metadata.is_symlink() && !follow) { + return Ok(Self::LineCount(0)); + } + + let line_count = fs::read_to_string(data.path()).map(|data| data.lines().count())?; + + let line_count = u64::try_from(line_count) + .map_or_else(|_| Self::WordCount(line_count as u64), Self::LineCount); + + Ok(line_count) + } + + /// Gets the underlying numeric value representing the disk usage + pub fn value(&self) -> u64 { + match self { + Self::WordCount(count) => *count, + Self::LineCount(count) => *count, + Self::Logical { value, .. } => *value, + Self::Physical { value, .. } => *value, + + #[cfg(unix)] + Self::Blocks(blocks) => *blocks, + } + } + + /// Gets the actual amount of blocks allocated to a particular file. Directories are + /// initialized to 0. + #[cfg(unix)] + pub fn init_blocks(metadata: &Metadata) -> Self { + use std::os::unix::fs::MetadataExt; + let value = metadata.blocks(); + Self::Blocks(value) + } +} + +impl Display for Usage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + macro_rules! byte_display { + ($p:expr, $v:expr) => { + match $p { + BytePresentation::Raw => write!(f, "{}{:>RAW_PADDING$}", $v, "B"), + BytePresentation::Bin => { + let prefix = prefix::Binary::from($v); + + if matches!(prefix, prefix::Binary::Base) { + write!(f, "{}{:>BIN_PADDING$}", $v, "B") + } else { + let bytes = ($v as f64) / prefix.base_value(); + write!(f, "{bytes:.FLOAT_PRECISION$} {prefix}B") + } + } + BytePresentation::Si => { + let prefix = prefix::Si::from($v); + + if matches!(prefix, prefix::Si::Base) { + write!(f, "{}{:>SI_PADDING$}", $v, "B") + } else { + let bytes = ($v as f64) / prefix.base_value(); + write!(f, "{bytes:.1} {prefix}B") + } + } + } + }; + } + + match self { + Self::WordCount(count) => ::fmt(count, f), + Self::LineCount(count) => ::fmt(count, f), + Self::Logical { + value, + presentation, + } => byte_display!(presentation, *value), + Self::Physical { + value, + presentation, + } => byte_display!(presentation, *value), + + #[cfg(unix)] + Self::Blocks(blocks) => ::fmt(blocks, f), + } + } +} + +impl AddAssign for Usage { + fn add_assign(&mut self, rhs: u64) { + match self { + Self::WordCount(count) => *count += rhs, + Self::LineCount(count) => *count += rhs, + Self::Logical { value, .. } => *value += rhs, + Self::Physical { value, .. } => *value += rhs, + + #[cfg(unix)] + Self::Blocks(blocks) => *blocks += rhs, + } + } +} + +#[test] +fn test_bytes_display() { + let size = Usage::Physical { + value: 998, + presentation: BytePresentation::Bin, + }; + + assert_eq!(String::from("998 B"), format!("{size}")); + + let size = Usage::Physical { + value: 2_u64.pow(10), + presentation: BytePresentation::Bin, + }; + + assert_eq!(String::from("1.0 KiB"), format!("{size}")); + + let size = Usage::Physical { + value: 2_u64.pow(20), + presentation: BytePresentation::Bin, + }; + + assert_eq!(String::from("1.0 MiB"), format!("{size}")); + + let size = Usage::Physical { + value: 2_u64.pow(30), + presentation: BytePresentation::Bin, + }; + + assert_eq!(String::from("1.0 GiB"), format!("{size}")); + + let size = Usage::Physical { + value: 2_u64.pow(40), + presentation: BytePresentation::Bin, + }; + + assert_eq!(String::from("1.0 TiB"), format!("{size}")); + + let size = Usage::Physical { + value: 2_u64.pow(50), + presentation: BytePresentation::Bin, + }; + + assert_eq!(String::from("1.0 PiB"), format!("{size}")); + + let size = Usage::Physical { + value: 2_u64.pow(30), + presentation: BytePresentation::Bin, + }; + + assert_eq!(String::from("1.0 GiB"), format!("{size}")); + + let size = Usage::Physical { + value: 10_u64.pow(3), + presentation: BytePresentation::Si, + }; + + assert_eq!(String::from("1.0 KB"), format!("{size}")); + + let size = Usage::Physical { + value: 10_u64.pow(6), + presentation: BytePresentation::Si, + }; + + assert_eq!(String::from("1.0 MB"), format!("{size}")); + + let size = Usage::Physical { + value: 10_u64.pow(9), + presentation: BytePresentation::Si, + }; + + assert_eq!(String::from("1.0 GB"), format!("{size}")); + + let size = Usage::Physical { + value: 10_u64.pow(12), + presentation: BytePresentation::Si, + }; + + assert_eq!(String::from("1.0 TB"), format!("{size}")); + + let size = Usage::Physical { + value: 10_u64.pow(15), + presentation: BytePresentation::Si, + }; + + assert_eq!(String::from("1.0 PB"), format!("{size}")); + + let size = Usage::Physical { + value: 998, + presentation: BytePresentation::Si, + }; + + assert_eq!(String::from("998 B"), format!("{size}")); + + let size = Usage::Physical { + value: 1000, + presentation: BytePresentation::Raw, + }; + + assert_eq!(String::from("1000 B"), format!("{size}")); +} diff --git a/src/disk/prefix.rs b/src/disk/prefix.rs new file mode 100644 index 00000000..ab13120e --- /dev/null +++ b/src/disk/prefix.rs @@ -0,0 +1,135 @@ +use std::{ + convert::From, + fmt::{self, Display}, +}; + +/// +#[derive(Debug, PartialEq)] +pub enum Binary { + Base, + Kibi, + Mebi, + Gibi, + Tebi, + Pebi, +} + +/// +#[derive(Debug, PartialEq)] +pub enum Si { + Base, + Kilo, + Mega, + Giga, + Tera, + Peta, +} + +impl Binary { + pub fn base_value(&self) -> f64 { + match self { + Self::Base => 1., + Self::Kibi => 2_u64.pow(10) as f64, + Self::Mebi => 2_u64.pow(20) as f64, + Self::Gibi => 2_u64.pow(30) as f64, + Self::Tebi => 2_u64.pow(40) as f64, + Self::Pebi => 2_u64.pow(50) as f64, + } + } +} + +impl Si { + pub fn base_value(&self) -> f64 { + match self { + Self::Base => 1., + Self::Kilo => 10_u64.pow(3) as f64, + Self::Mega => 10_u64.pow(6) as f64, + Self::Giga => 10_u64.pow(9) as f64, + Self::Tera => 10_u64.pow(12) as f64, + Self::Peta => 10_u64.pow(15) as f64, + } + } +} + +impl From for Binary { + fn from(bytes: u64) -> Self { + let bytes = (bytes as f64).log2(); + + if bytes < 10. { + Self::Base + } else if bytes < 20. { + Self::Kibi + } else if bytes < 30. { + Self::Mebi + } else if bytes < 40. { + Self::Gibi + } else if bytes < 50. { + Self::Tebi + } else { + Self::Pebi + } + } +} + +impl From for Si { + fn from(bytes: u64) -> Self { + let bytes = (bytes as f64).log10(); + + if bytes < 3. { + Self::Base + } else if bytes < 6. { + Self::Kilo + } else if bytes < 9. { + Self::Mega + } else if bytes < 12. { + Self::Giga + } else if bytes < 15. { + Self::Tera + } else { + Self::Peta + } + } +} + +impl Display for Binary { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Base => write!(f, ""), + Self::Kibi => write!(f, "Ki"), + Self::Mebi => write!(f, "Mi"), + Self::Gibi => write!(f, "Gi"), + Self::Tebi => write!(f, "Ti"), + Self::Pebi => write!(f, "Pi"), + } + } +} + +impl Display for Si { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Base => write!(f, ""), + Self::Kilo => write!(f, "K"), + Self::Mega => write!(f, "M"), + Self::Giga => write!(f, "G"), + Self::Tera => write!(f, "T"), + Self::Peta => write!(f, "P"), + } + } +} + +#[test] +fn test_prefixes() { + assert_eq!(Binary::from(100), Binary::Base); + assert_eq!(Binary::from(2_u64.pow(10)), Binary::Kibi); + assert_eq!(Binary::from(2_u64.pow(20)), Binary::Mebi); + assert_eq!(Binary::from(2_u64.pow(30)), Binary::Gibi); + assert_eq!(Binary::from(2_u64.pow(40)), Binary::Tebi); + assert_eq!(Binary::from(2_u64.pow(50)), Binary::Pebi); + + assert_eq!(Si::from(100), Si::Base); + assert_eq!(Si::from(10_u64.pow(3)), Si::Kilo); + assert_eq!(Si::from(10_u64.pow(6)), Si::Mega); + assert_eq!(Si::from(10_u64.pow(9)), Si::Giga); + assert_eq!(Si::from(10_u64.pow(12)), Si::Tera); + assert_eq!(Si::from(10_u64.pow(15)), Si::Peta); +} diff --git a/src/disk_usage/file_size/block.rs b/src/disk_usage/file_size/block.rs deleted file mode 100644 index bcb982ec..00000000 --- a/src/disk_usage/file_size/block.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::{ - fmt::{self, Display}, - fs::Metadata, - os::unix::fs::MetadataExt, -}; - -#[derive(Default)] -pub struct Metric { - pub value: u64, -} - -impl Metric { - pub fn init(md: &Metadata) -> Self { - Self { value: md.blocks() } - } -} - -impl Display for Metric { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - ::fmt(&self.value, f) - } -} diff --git a/src/disk_usage/file_size/byte.rs b/src/disk_usage/file_size/byte.rs deleted file mode 100644 index 3301a890..00000000 --- a/src/disk_usage/file_size/byte.rs +++ /dev/null @@ -1,207 +0,0 @@ -use super::super::units::{BinPrefix, PrefixKind, SiPrefix, UnitPrefix}; -use filesize::PathExt; -use std::{ - cell::{Ref, RefCell}, - fmt::{self, Display}, - fs::Metadata, - path::Path, -}; - -/// Concerned with measuring file size in bytes, whether logical or physical determined by `kind`. -/// Binary or SI units used for reporting determined by `prefix_kind`. -pub struct Metric { - pub value: u64, - pub human_readable: bool, - #[allow(dead_code)] - kind: MetricKind, - prefix_kind: PrefixKind, - - /// To prevent allocating the same string twice. We allocate the first time - /// in [`crate::tree::Tree::update_column_properties`] in order to compute the max column width for - /// human-readable size and cache it. It will then be used again when preparing the output. - cached_display: RefCell, -} - -/// Represents the appropriate method in which to compute bytes. `Logical` represent the total amount -/// of bytes in a file; `Physical` represents how many bytes are actually used to store the file on -/// disk. -pub enum MetricKind { - Logical, - Physical, -} - -impl Metric { - /// Initializes a [Metric] that stores the total amount of bytes in a file. - pub fn init_logical( - metadata: &Metadata, - prefix_kind: PrefixKind, - human_readable: bool, - ) -> Self { - let value = metadata.len(); - let kind = MetricKind::Logical; - - Self { - value, - human_readable, - kind, - prefix_kind, - cached_display: RefCell::default(), - } - } - - /// Initializes an empty [Metric] used to represent the total amount of bytes of a file. - pub fn init_empty_logical(human_readable: bool, prefix_kind: PrefixKind) -> Self { - Self { - value: 0, - human_readable, - kind: MetricKind::Logical, - prefix_kind, - cached_display: RefCell::default(), - } - } - - /// Initializes an empty [Metric] used to represent the total disk space of a file in bytes. - pub fn init_empty_physical(human_readable: bool, prefix_kind: PrefixKind) -> Self { - Self { - value: 0, - human_readable, - kind: MetricKind::Physical, - prefix_kind, - cached_display: RefCell::default(), - } - } - - /// Initializes a [Metric] that stores the total amount of bytes used to store a file on disk. - pub fn init_physical( - path: &Path, - metadata: &Metadata, - prefix_kind: PrefixKind, - human_readable: bool, - ) -> Self { - let value = path.size_on_disk_fast(metadata).unwrap_or(metadata.len()); - let kind = MetricKind::Physical; - - Self { - value, - human_readable, - kind, - prefix_kind, - cached_display: RefCell::default(), - } - } - - /// Returns an immutable borrow of the `cached_display`. - pub fn cached_display(&self) -> Ref<'_, String> { - self.cached_display.borrow() - } -} - -impl Display for Metric { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - { - let cached_display = self.cached_display(); - - if cached_display.len() > 0 { - return write!(f, "{cached_display}"); - } - } - - let value = self.value as f64; - - let display = match self.prefix_kind { - PrefixKind::Si => { - if self.human_readable { - let unit = SiPrefix::from(self.value); - - if unit == SiPrefix::Base { - format!("{} {unit}", self.value) - } else { - let base_value = unit.base_value(); - let size = value / (base_value as f64); - format!("{size:.1} {unit}") - } - } else { - format!("{} {}", self.value, SiPrefix::Base) - } - }, - PrefixKind::Bin => { - if self.human_readable { - let unit = BinPrefix::from(self.value); - - if unit == BinPrefix::Base { - format!("{} {unit}", self.value) - } else { - let base_value = unit.base_value(); - let size = value / (base_value as f64); - format!("{size:.1} {unit}") - } - } else { - format!("{} {}", self.value, BinPrefix::Base) - } - }, - }; - - write!(f, "{display}")?; - - self.cached_display.replace(display); - - Ok(()) - } -} - -#[test] -fn test_metric() { - let metric = Metric { - value: 100, - kind: MetricKind::Logical, - human_readable: false, - prefix_kind: PrefixKind::Bin, - cached_display: RefCell::::default(), - }; - assert_eq!(format!("{metric}"), "100 B"); - - let metric = Metric { - value: 1000, - kind: MetricKind::Logical, - human_readable: true, - prefix_kind: PrefixKind::Si, - cached_display: RefCell::::default(), - }; - assert_eq!(format!("{metric}"), "1.0 KB"); - - let metric = Metric { - value: 1000, - kind: MetricKind::Logical, - human_readable: true, - prefix_kind: PrefixKind::Bin, - cached_display: RefCell::::default(), - }; - assert_eq!(format!("{metric}"), "1000 B"); - - let metric = Metric { - value: 1024, - kind: MetricKind::Logical, - human_readable: true, - prefix_kind: PrefixKind::Bin, - cached_display: RefCell::::default(), - }; - assert_eq!(format!("{metric}"), "1.0 KiB"); - - let metric = Metric { - value: 2_u64.pow(20), - kind: MetricKind::Logical, - human_readable: true, - prefix_kind: PrefixKind::Bin, - cached_display: RefCell::::default(), - }; - assert_eq!(format!("{metric}"), "1.0 MiB"); - - let metric = Metric { - value: 123_454, - kind: MetricKind::Logical, - human_readable: false, - prefix_kind: PrefixKind::Bin, - cached_display: RefCell::::default(), - }; - assert_eq!(format!("{metric}"), "123454 B"); -} diff --git a/src/disk_usage/file_size/line_count.rs b/src/disk_usage/file_size/line_count.rs deleted file mode 100644 index 377a5ea3..00000000 --- a/src/disk_usage/file_size/line_count.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::{ - convert::{AsRef, From}, - fmt::{self, Display}, - fs, - path::Path, -}; - -/// Concerned with measuring file size using line count as a metric. -#[derive(Default)] -pub struct Metric { - pub value: u64, -} - -impl Metric { - /// Reads in contents of a file given by `path` and attempts to compute the total number of - /// lines in that file. If a file is not UTF-8 encoded as in the case of a binary jpeg file - /// then `None` will be returned. - pub fn init(path: impl AsRef) -> Option { - let data = fs::read_to_string(path.as_ref()).ok()?; - - let lines = data.lines().count(); - - u64::try_from(lines).map(|value| Self { value }).ok() - } -} - -impl From for Metric { - fn from(value: u64) -> Self { - Self { value } - } -} - -impl Display for Metric { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - ::fmt(&self.value, f) - } -} - -#[test] -fn test_line_count() { - let metric = - Metric::init("tests/data/nemesis.txt").expect("Expected 'tests/data/nemesis.txt' to exist"); - - assert_eq!(metric.value, 4); -} diff --git a/src/disk_usage/file_size/mod.rs b/src/disk_usage/file_size/mod.rs deleted file mode 100644 index fc47b48f..00000000 --- a/src/disk_usage/file_size/mod.rs +++ /dev/null @@ -1,110 +0,0 @@ -use crate::context::Context; -use clap::ValueEnum; -use std::{ - convert::From, - fmt::{self, Display}, - ops::AddAssign, -}; - -/// Concerned with measuring file size in blocks. -#[cfg(unix)] -pub mod block; - -/// Concerned with measuring file size in bytes, logical or physical. -pub mod byte; - -/// Concerned with measuring file size by line count. -pub mod line_count; - -/// Concerned with measuring file size by word count. -pub mod word_count; - -#[cfg(unix)] -pub const BLOCK_SIZE_BYTES: u16 = 512; - -/// Represents all the different ways in which a filesize could be reported using various metrics. -pub enum FileSize { - Word(word_count::Metric), - Line(line_count::Metric), - Byte(byte::Metric), - #[cfg(unix)] - Block(block::Metric), -} - -/// Determines between logical or physical size for display -#[derive(Copy, Clone, Debug, ValueEnum, Default)] -pub enum DiskUsage { - /// How many bytes does a file contain - Logical, - - /// How many actual bytes on disk, taking into account blocks, sparse files, and compression. - #[default] - Physical, - - /// How many total lines a file contains - Line, - - /// How many total words a file contains - Word, - - /// How many blocks are allocated to store the file - #[cfg(unix)] - Block, -} - -impl FileSize { - /// Extracts the inner value of [`FileSize`] which represents the file size for various metrics. - #[inline] - pub const fn value(&self) -> u64 { - match self { - Self::Byte(metric) => metric.value, - Self::Line(metric) => metric.value, - Self::Word(metric) => metric.value, - - #[cfg(unix)] - Self::Block(metric) => metric.value, - } - } -} - -impl AddAssign<&Self> for FileSize { - fn add_assign(&mut self, rhs: &Self) { - match self { - Self::Byte(metric) => metric.value += rhs.value(), - Self::Line(metric) => metric.value += rhs.value(), - Self::Word(metric) => metric.value += rhs.value(), - - #[cfg(unix)] - Self::Block(metric) => metric.value += rhs.value(), - } - } -} - -impl From<&Context> for FileSize { - fn from(ctx: &Context) -> Self { - use DiskUsage::{Line, Logical, Physical, Word}; - - match ctx.disk_usage { - Logical => Self::Byte(byte::Metric::init_empty_logical(ctx.human, ctx.unit)), - Physical => Self::Byte(byte::Metric::init_empty_physical(ctx.human, ctx.unit)), - Line => Self::Line(line_count::Metric::default()), - Word => Self::Word(word_count::Metric::default()), - - #[cfg(unix)] - DiskUsage::Block => Self::Block(block::Metric::default()), - } - } -} - -impl Display for FileSize { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Word(metric) => write!(f, "{metric}"), - Self::Line(metric) => write!(f, "{metric}"), - Self::Byte(metric) => write!(f, "{metric}"), - - #[cfg(unix)] - Self::Block(metric) => write!(f, "{metric}"), - } - } -} diff --git a/src/disk_usage/file_size/word_count.rs b/src/disk_usage/file_size/word_count.rs deleted file mode 100644 index 05abe9b9..00000000 --- a/src/disk_usage/file_size/word_count.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::{ - convert::{AsRef, From}, - fmt::{self, Display}, - fs, - path::Path, -}; - -/// Concerned with measuring file size using word count as a metric. -#[derive(Default)] -pub struct Metric { - pub value: u64, -} - -impl Metric { - /// Reads in contents of a file given by `path` and attempts to compute the total number of - /// words in that file. If a file is not UTF-8 encoded as in the case of a binary jpeg file - /// then `None` will be returned. - /// - /// Words are UTF-8 encoded byte sequences delimited by Unicode Derived Core Property `White_Space`. - pub fn init(path: impl AsRef) -> Option { - let data = fs::read_to_string(path.as_ref()).ok()?; - - let words = data.split_whitespace().count(); - - u64::try_from(words).map(|value| Self { value }).ok() - } -} - -impl From for Metric { - fn from(value: u64) -> Self { - Self { value } - } -} - -impl Display for Metric { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - ::fmt(&self.value, f) - } -} - -#[test] -fn test_line_count() { - let metric = - Metric::init("tests/data/nemesis.txt").expect("Expected 'tests/data/nemesis.txt' to exist"); - - assert_eq!(metric.value, 27); -} diff --git a/src/disk_usage/mod.rs b/src/disk_usage/mod.rs deleted file mode 100644 index c855943f..00000000 --- a/src/disk_usage/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -/// Binary and SI prefixes -pub mod units; - -/// Concerned with all of the different ways to measure file size: bytes, word-count, line-count, -/// blocks (unix), etc.. -pub mod file_size; diff --git a/src/disk_usage/units.rs b/src/disk_usage/units.rs deleted file mode 100644 index 365668ff..00000000 --- a/src/disk_usage/units.rs +++ /dev/null @@ -1,140 +0,0 @@ -use clap::ValueEnum; -use std::{ - convert::From, - fmt::{self, Display}, -}; - -/// Determines whether to use SI prefixes or binary prefixes. -#[derive(Copy, Clone, Debug, ValueEnum, Default)] -pub enum PrefixKind { - /// Displays disk usage using binary prefixes. - #[default] - Bin, - - /// Displays disk usage using SI prefixes. - Si, -} - -/// Binary prefixes. -#[derive(Debug, PartialEq, Eq)] -pub enum BinPrefix { - Base, - Kibi, - Mebi, - Gibi, - Tebi, -} - -/// SI prefixes. -#[derive(Debug, PartialEq, Eq)] -pub enum SiPrefix { - Base, - Kilo, - Mega, - Giga, - Tera, -} - -impl SiPrefix { - /// Returns the human readable representation of the SI prefix. - pub const fn as_str(&self) -> &str { - match self { - Self::Base => "B", - Self::Kilo => "KB", - Self::Mega => "MB", - Self::Giga => "GB", - Self::Tera => "TB", - } - } -} - -impl BinPrefix { - /// Returns the human readable representation of the binary prefix. - pub const fn as_str(&self) -> &str { - match self { - Self::Base => "B", - Self::Kibi => "KiB", - Self::Mebi => "MiB", - Self::Gibi => "GiB", - Self::Tebi => "TiB", - } - } -} - -pub trait UnitPrefix { - fn base_value(&self) -> u64; -} - -impl UnitPrefix for SiPrefix { - fn base_value(&self) -> u64 { - match self { - Self::Base => 1, - Self::Kilo => 10_u64.pow(3), - Self::Mega => 10_u64.pow(6), - Self::Giga => 10_u64.pow(9), - Self::Tera => 10_u64.pow(12), - } - } -} - -impl UnitPrefix for BinPrefix { - fn base_value(&self) -> u64 { - match self { - Self::Base => 1, - Self::Kibi => 2_u64.pow(10), - Self::Mebi => 2_u64.pow(20), - Self::Gibi => 2_u64.pow(30), - Self::Tebi => 2_u64.pow(40), - } - } -} - -/// Get the closest human-readable unit prefix for value. -impl From for BinPrefix { - fn from(value: u64) -> Self { - let log = (value as f64).log2(); - - if log < 10. { - Self::Base - } else if log < 20. { - Self::Kibi - } else if log < 30. { - Self::Mebi - } else if log < 40. { - Self::Gibi - } else { - Self::Tebi - } - } -} - -/// Get the closest human-readable unit prefix for value. -impl From for SiPrefix { - fn from(value: u64) -> Self { - let log = (value as f64).log10(); - - if log < 3. { - Self::Base - } else if log < 6. { - Self::Kilo - } else if log < 9. { - Self::Mega - } else if log < 12. { - Self::Giga - } else { - Self::Tera - } - } -} - -impl Display for BinPrefix { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.as_str()) - } -} - -impl Display for SiPrefix { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.as_str()) - } -} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..e0a19a34 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,143 @@ +use ansi_term::{Color, Style}; +use std::{convert::From, error::Error as StdError, fmt, result::Result as StdResult}; + +/// Meant to be a convenient wild-card import to allow access to the [`crate::error`] module's facilities +/// error-handling facilities. +pub mod prelude { + pub use super::{Error, ErrorCategory, ErrorReport, Result, WithContext}; + + macro_rules! error_source { + () => { + format!("{}:{}", file!(), line!()) + }; + } + pub(crate) use error_source; +} + +/// General result type to be used through the application. +pub type Result = std::result::Result; + +/// General error type to be used throughout the application. Depending on the `category`, a +/// different format of the error will be presented to the end-user. +#[derive(Debug)] +pub struct Error { + source: anyhow::Error, + category: ErrorCategory, + help_text: Option, +} + +/// ErrorCategory of errors with which to generate a report. +#[derive(Debug)] +pub enum ErrorCategory { + /// Errors due to logical errors within the application. When creating an [`Error`] via + /// [`ErrorReport`], an `Internal` error will come with a default help text to guide the user + /// to the Github issues page to file a new issue. Becareful when overriding help texts under + /// these circumstances. + Internal, + + /// User-specific errors to be used in relation to command-line arguments and configs. + User, + + /// Errors related to environment such as the missing of the `$HOME` environment variable. + System, + + /// Errors that are meant to be recoverable. + Warning, +} + +impl Error { + pub fn new(category: ErrorCategory, source: anyhow::Error, help_text: Option) -> Self { + Self { + source, + category, + help_text, + } + } + + fn internal_error_help_message() -> String { + format!( + "Please submit the error output to {}", + Style::default() + .bold() + .paint("https://github.com/solidiquis/erdtree/issues") + ) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let icon = Color::Red.bold().paint("\u{2715}"); + let prefix = Style::default().bold().paint(format!("{}", self.category)); + let help = Color::Cyan.bold().paint("help"); + + if let Some(ref help_txt) = self.help_text { + writeln!( + f, + "{icon} {prefix}: {:?}\n\n{help}: {help_txt}", + self.source + ) + } else { + writeln!(f, "{} {:?}", icon, self.source) + } + } +} + +impl fmt::Display for ErrorCategory { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Internal => write!(f, "Internal error"), + Self::User => write!(f, "Error"), + Self::System => write!(f, "System error"), + Self::Warning => write!(f, ""), + } + } +} + +/// Convenience trait to generate a [`Result`] from any type that implements [`std::error::Error`]. +pub trait ErrorReport { + fn into_report(self, category: ErrorCategory) -> Result; +} + +/// Allows the chaining of contexts to [`Error`]'s underlying [`anyhow::Error`]. +pub trait WithContext +where + C: fmt::Display + Send + Sync + 'static, +{ + /// Chain together contexts. + fn context(self, ctx: C) -> Result; + + /// Set some help text to display with the error output. + fn set_help(self, msg: C) -> Result; +} + +impl ErrorReport for StdResult { + fn into_report(self, category: ErrorCategory) -> Result { + self.map_err(|e| { + let help_text = matches!(category, ErrorCategory::Internal) + .then(Error::internal_error_help_message); + + let anyhow_err = anyhow::Error::from(e); + Error::new(category, anyhow_err, help_text) + }) + } +} + +impl WithContext for Result +where + C: fmt::Display + Send + Sync + 'static, +{ + fn context(self, ctx: C) -> Self { + self.map_err(|mut err| { + let cause = err.source.context(ctx); + err.source = cause; + err + }) + } + + fn set_help(self, msg: C) -> Self { + self.map_err(|mut err| { + err.help_text = Some(format!("{msg}")); + err + }) + } +} diff --git a/src/fs/inode.rs b/src/file/inode.rs similarity index 83% rename from src/fs/inode.rs rename to src/file/inode.rs index 8f9ff6b9..f0b25b90 100644 --- a/src/fs/inode.rs +++ b/src/file/inode.rs @@ -10,17 +10,17 @@ pub struct Inode { impl Inode { /// Initializer for an inode given all the properties that make it unique. - pub const fn new(ino: u64, dev: u64, nlink: u64) -> Self { + pub fn new(ino: u64, dev: u64, nlink: u64) -> Self { Self { ino, dev, nlink } } } #[derive(Debug, thiserror::Error)] -#[error("Insufficient information to compute inode")] -pub struct Error; +#[error("Insufficient information to compute inode.")] +pub struct INodeError; impl TryFrom<&Metadata> for Inode { - type Error = Error; + type Error = INodeError; #[cfg(unix)] fn try_from(md: &Metadata) -> Result { @@ -41,11 +41,11 @@ impl TryFrom<&Metadata> for Inode { return Ok(Self::new(ino, dev.into(), nlink.into())); } - Err(Error {}) + Err(Self::Error {}) } #[cfg(not(any(unix, windows)))] fn try_from(md: &Metadata) -> Result { - Err(Error {}) + Err(Self::Error {}) } } diff --git a/src/file/mod.rs b/src/file/mod.rs new file mode 100644 index 00000000..b3fae8d4 --- /dev/null +++ b/src/file/mod.rs @@ -0,0 +1,333 @@ +use crate::{ + disk, icon, + user::{args::Metric, Context}, +}; +use ignore::DirEntry; +use std::{ + fmt::{self, Display}, + fs::{self, Metadata}, + io, + ops::Deref, + path::{Path, PathBuf}, +}; + +#[cfg(unix)] +use crate::{ + file::unix::permissions::{file_type::FileType, SymbolicNotation}, + user::args::{TimeFormat, TimeStamp}, +}; + +/// Concerned with querying information about a file's underlying inode. +pub mod inode; +use inode::{INodeError, Inode}; + +/// Rules on how to order entries relative to their siblings or all other files. +pub mod order; + +/// Concerned with the tree data structure that is used to produce the program output. +pub mod tree; +pub use tree::Tree; + +/// File attributes specific to Unix systems. +#[cfg(unix)] +pub mod unix; + +/// Erdtree's wrapper around [`DirEntry`], it's metadata ([`Metadata`]). Also contains disk usage +/// information of files. Directories will always be initialized to have a size of zero as they +/// must be recursively computed. +#[derive(Debug)] +pub struct File { + data: DirEntry, + metadata: Metadata, + size: disk::Usage, + symlink_target: Option, + + #[cfg(unix)] + unix_attrs: unix::Attrs, +} + +// For keeping track of the count of file-types while loading from disk. +#[derive(Default)] +pub struct Accumulator { + num_file: usize, + num_dir: usize, + num_link: usize, +} + +/// [`Display`] implementation concerned with human-readable presentation of the file-name. +pub struct DisplayName<'a> { + file: &'a File, +} + +/// [`Display`] implementation concerned with human-readable presentation of the file-path. +pub struct DisplayPath<'a> { + file: &'a File, + path_prefix: Option<&'a Path>, +} + +impl File { + /// Plain Jane constructor for [`File`]. + pub fn new( + data: DirEntry, + metadata: Metadata, + size: disk::Usage, + symlink_target: Option, + #[cfg(unix)] unix_attrs: unix::Attrs, + ) -> Self { + Self { + data, + metadata, + size, + symlink_target, + #[cfg(unix)] + unix_attrs, + } + } + + /// Initializes [`File`] from the given [`DirEntry`] and [`Context`]. + pub fn init( + data: DirEntry, + Context { + metric, + byte_units, + follow, + #[cfg(unix)] + long, + .. + }: &Context, + ) -> Result { + let path = data.path(); + + let (symlink_target, metadata) = if data.file_type().is_some_and(|ft| ft.is_symlink()) { + if *follow { + (fs::read_link(path).ok(), fs::metadata(path)?) + } else { + (fs::read_link(path).ok(), fs::symlink_metadata(path)?) + } + } else { + (None, fs::symlink_metadata(path)?) + }; + + let size = match metric { + Metric::Physical => disk::Usage::init_physical(&metadata, *byte_units), + Metric::Logical => disk::Usage::init_logical(&metadata, *byte_units), + Metric::Word => disk::Usage::init_word_count(&data, &metadata, *follow)?, + Metric::Line => disk::Usage::init_line_count(&data, &metadata, *follow)?, + + #[cfg(unix)] + Metric::Block => disk::Usage::init_blocks(&metadata), + }; + + #[cfg(unix)] + let unix_attrs = long + .long + .then(|| unix::Attrs::from((&metadata, &data))) + .unwrap_or_else(unix::Attrs::default); + + Ok(Self::new( + data, + metadata, + size, + symlink_target, + #[cfg(unix)] + unix_attrs, + )) + } + + /// Attempts to query the [`File`]'s underlying inode which is represented by [`Inode`]. + pub fn inode(&self) -> Result { + Inode::try_from(&self.metadata) + } + + /// Reader for `metadata` field. + pub fn metadata(&self) -> &Metadata { + &self.metadata + } + + /// Gets a mutable reference to the `size` field. + pub fn size_mut(&mut self) -> &mut disk::Usage { + &mut self.size + } + + /// Gets an immmutable reference to the `size` field. + pub fn size(&self) -> &disk::Usage { + &self.size + } + + pub fn symlink_target(&self) -> Option<&Path> { + self.symlink_target.as_deref() + } + + pub fn display_name(&self) -> DisplayName<'_> { + DisplayName { file: self } + } + + pub fn display_path<'a>(&'a self, path_prefix: Option<&'a Path>) -> DisplayPath<'a> { + DisplayPath { + file: self, + path_prefix, + } + } + + pub fn icon(&self) -> &str { + icon::compute(self) + } + + #[cfg(unix)] + pub fn unix_attrs(&self) -> &unix::Attrs { + &self.unix_attrs + } + + #[cfg(unix)] + pub fn timestamp_from_ctx(&self, ctx: &Context) -> Option { + use chrono::{DateTime, Local}; + + let system_time = match ctx.long.time.unwrap_or_default() { + TimeStamp::Mod => self.metadata().accessed().ok(), + TimeStamp::Create => self.metadata().created().ok(), + TimeStamp::Access => self.metadata().accessed().ok(), + }; + + system_time + .map(DateTime::::from) + .map( + |local_time| match ctx.long.time_format.unwrap_or_default() { + TimeFormat::Default => local_time.format("%d %h %H:%M %g"), + TimeFormat::Iso => local_time.format("%Y-%m-%d %H:%M:%S"), + TimeFormat::IsoStrict => local_time.format("%Y-%m-%dT%H:%M:%S%Z"), + TimeFormat::Short => local_time.format("%Y-%m-%d"), + }, + ) + .map(|dt| format!("{dt}")) + } + + #[cfg(unix)] + pub fn is_fifo(&self) -> bool { + self.metadata() + .permissions() + .try_mode_symbolic_notation() + .map_or(false, |mode| mode.file_type() == &FileType::Fifo) + } + + #[cfg(unix)] + pub fn is_socket(&self) -> bool { + self.metadata() + .permissions() + .try_mode_symbolic_notation() + .map_or(false, |mode| mode.file_type() == &FileType::Socket) + } + + #[cfg(unix)] + pub fn is_char_device(&self) -> bool { + self.metadata() + .permissions() + .try_mode_symbolic_notation() + .map_or(false, |mode| mode.file_type() == &FileType::CharDevice) + } + + #[cfg(unix)] + pub fn is_block_device(&self) -> bool { + self.metadata() + .permissions() + .try_mode_symbolic_notation() + .map_or(false, |mode| mode.file_type() == &FileType::BlockDevice) + } + + pub fn is_dir(&self) -> bool { + self.file_type().is_some_and(|ft| ft.is_dir()) + } + + pub fn is_file(&self) -> bool { + self.file_type().is_some_and(|ft| ft.is_file()) + } + + pub fn is_symlink(&self) -> bool { + self.file_type().is_some_and(|ft| ft.is_symlink()) + } +} + +impl Deref for File { + type Target = DirEntry; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl Accumulator { + pub fn total(&self) -> usize { + self.num_dir + self.num_file + self.num_link + } + + pub fn increment(&mut self, ft: Option) { + let Some(file_type) = ft else { return }; + + if file_type.is_file() { + self.num_file += 1; + } else if file_type.is_dir() { + self.num_dir += 1; + } else if file_type.is_symlink() { + self.num_link += 1; + } + } +} + +impl Display for DisplayName<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let file_name = self.file.file_name().to_string_lossy(); + let link_target = self.file.symlink_target().map(|p| p.canonicalize()); + + if let Some(Ok(target)) = link_target { + write!(f, "{file_name} \u{2192} {}", target.display()) + } else { + write!(f, "{file_name}") + } + } +} + +impl Display for DisplayPath<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let display = match self.path_prefix { + Some(prefix) => { + let path = self.file.path(); + path.strip_prefix(prefix) + .map_or_else(|_| path.display(), |p| p.display()) + } + None => self.file.path().display(), + }; + + let link_target = self.file.symlink_target().map(|p| p.canonicalize()); + + if let Some(Ok(target)) = link_target { + write!(f, "{display} \u{2192} {}", target.display()) + } else { + write!(f, "{display}") + } + } +} + +impl Display for Accumulator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut displayed_count = Vec::new(); + + match self.num_dir { + n if n > 1 => displayed_count.push(format!("{} directories", n)), + 1 => displayed_count.push("1 directory".to_string()), + _ => (), + } + + match self.num_file { + n if n > 1 => displayed_count.push(format!("{} files", n)), + 1 => displayed_count.push("1 file".to_string()), + _ => (), + } + + match self.num_link { + n if n > 1 => displayed_count.push(format!("{} links", n)), + 1 => displayed_count.push("1 link".to_string()), + _ => (), + } + + writeln!(f, "{}", displayed_count.join(", ")) + } +} diff --git a/src/file/order.rs b/src/file/order.rs new file mode 100644 index 00000000..b3cd8a12 --- /dev/null +++ b/src/file/order.rs @@ -0,0 +1,180 @@ +use super::File; +use crate::user::args::{DirOrder, Sort}; +use std::cmp::Ordering; + +/// Comparator type used to sort [File]s. +pub type FileComparator = dyn Fn(&File, &File) -> Ordering; + +/// Yields function pointer to the appropriate `File` comparator. +pub fn comparator(sort: Sort, dir_order: DirOrder) -> Option> { + match dir_order { + DirOrder::First if matches!(sort, Sort::None) => Some(Box::new(dir_first_comparator)), + DirOrder::First => Some(Box::new(move |a, b| { + dir_first_comparator_with_fallback(a, b, base_comparator(sort)) + })), + DirOrder::Last if matches!(sort, Sort::None) => Some(Box::new(dir_last_comparator)), + DirOrder::Last => Some(Box::new(move |a, b| { + dir_last_comparator_with_fallback(a, b, base_comparator(sort)) + })), + DirOrder::None if matches!(sort, Sort::None) => None, + DirOrder::None => Some(base_comparator(sort)), + } +} + +/// Orders directories first. Provides a fallback if inputs are not directories. +fn dir_first_comparator_with_fallback( + a: &File, + b: &File, + fallback: impl Fn(&File, &File) -> Ordering, +) -> Ordering { + match (a.is_dir(), b.is_dir()) { + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + _ => fallback(a, b), + } +} + +/// Orders directories first relative to all other file-types. +fn dir_first_comparator(a: &File, b: &File) -> Ordering { + match (a.is_dir(), b.is_dir()) { + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + _ => Ordering::Equal, + } +} + +/// Orders directories last. Provides a fallback if inputs are not directories. +fn dir_last_comparator_with_fallback( + a: &File, + b: &File, + fallback: impl Fn(&File, &File) -> Ordering, +) -> Ordering { + match (a.is_dir(), b.is_dir()) { + (true, false) => Ordering::Less, + (false, true) => Ordering::Greater, + _ => fallback(a, b), + } +} + +/// Orders directories last relative to all other file-types. +fn dir_last_comparator(a: &File, b: &File) -> Ordering { + match (a.is_dir(), b.is_dir()) { + (true, false) => Ordering::Less, + (false, true) => Ordering::Greater, + _ => Ordering::Equal, + } +} + +/// Grabs the comparator for two non-dir type [File]s. +fn base_comparator(sort_type: Sort) -> Box { + Box::new(match sort_type { + Sort::Name => naming::comparator, + Sort::Rname => naming::rev_comparator, + Sort::Size => sizing::comparator, + Sort::Rsize => sizing::rev_comparator, + Sort::Access => time_stamping::accessed::comparator, + Sort::Raccess => time_stamping::accessed::rev_comparator, + Sort::Create => time_stamping::created::comparator, + Sort::Rcreate => time_stamping::created::rev_comparator, + Sort::Mod => time_stamping::modified::comparator, + Sort::Rmod => time_stamping::modified::rev_comparator, + Sort::None => |_: &File, _: &File| Ordering::Equal, + }) +} + +mod time_stamping { + use super::File; + use core::cmp::Ordering; + use std::time::SystemTime; + + pub mod accessed { + use super::*; + + /// Comparator that sorts [File]s by Last Access timestamp, newer to older. + pub fn comparator(a: &File, b: &File) -> Ordering { + let a_stamp = a + .metadata() + .accessed() + .unwrap_or_else(|_| SystemTime::now()); + let b_stamp = b + .metadata() + .accessed() + .unwrap_or_else(|_| SystemTime::now()); + b_stamp.cmp(&a_stamp) + } + + /// Comparator that sorts [File]s by Access timestamp, older to newer. + pub fn rev_comparator(a: &File, b: &File) -> Ordering { + comparator(b, a) + } + } + + pub mod created { + use super::*; + + /// Comparator that sorts [File]s by Creation timestamp, newer to older. + pub fn comparator(a: &File, b: &File) -> Ordering { + let a_stamp = a.metadata().created().unwrap_or_else(|_| SystemTime::now()); + let b_stamp = b.metadata().created().unwrap_or_else(|_| SystemTime::now()); + b_stamp.cmp(&a_stamp) + } + + /// Comparator that sorts [File]s by Creation timestamp, older to newer. + pub fn rev_comparator(a: &File, b: &File) -> Ordering { + comparator(b, a) + } + } + + pub mod modified { + use super::*; + + /// Comparator that sorts [File]s by Alteration timestamp, newer to older. + pub fn comparator(a: &File, b: &File) -> Ordering { + let a_stamp = a + .metadata() + .modified() + .unwrap_or_else(|_| SystemTime::now()); + let b_stamp = b + .metadata() + .modified() + .unwrap_or_else(|_| SystemTime::now()); + b_stamp.cmp(&a_stamp) + } + + /// Comparator that sorts [File]s by Alteration timestamp, older to newer. + pub fn rev_comparator(a: &File, b: &File) -> Ordering { + comparator(b, a) + } + } +} + +mod sizing { + use super::File; + use std::cmp::Ordering; + + /// Comparator that sorts [File]s by size, largest to smallest. + pub fn comparator(a: &File, b: &File) -> Ordering { + let a_size = a.size().value(); + let b_size = b.size().value(); + b_size.cmp(&a_size) + } + /// Comparator that sorts [File]s by size, smallest to largest. + pub fn rev_comparator(a: &File, b: &File) -> Ordering { + comparator(b, a) + } +} + +mod naming { + use super::File; + use std::cmp::Ordering; + + /// Comparator based on [File] file names in lexicographical order. + pub fn comparator(a: &File, b: &File) -> Ordering { + a.file_name().cmp(b.file_name()) + } + + /// Comparator based on [File] file names in reversed lexicographical order. + pub fn rev_comparator(a: &File, b: &File) -> Ordering { + comparator(b, a) + } +} diff --git a/src/file/tree/filter.rs b/src/file/tree/filter.rs new file mode 100644 index 00000000..09b0e2fd --- /dev/null +++ b/src/file/tree/filter.rs @@ -0,0 +1,259 @@ +use crate::{ + error::prelude::*, + file::{tree::Tree, File}, + user::{ + args::{FileType, Layout}, + Context, Search, + }, +}; +use ahash::HashSet; +use ignore::overrides::OverrideBuilder; +use indextree::NodeId; +use regex::Regex; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("A regex pattern was not provided")] + MissingRegexPattern, + + #[error("{0}")] + InvalidRegex(regex::Error), +} + +/// Predicate used for filtering a [`File`] based on [`FileType`]. +pub type FileTypeFilter = dyn Fn(&File) -> bool; + +impl Tree { + /// Filter [`File`]s in the [`indextree::Arena`] based on provided [`Context`]. The order in + /// which the filters are applied matters. + pub fn filter_nodes(&mut self, ctx: &Context) -> Result<()> { + if !ctx.file_type.is_empty() { + self.filter_file_type(ctx); + } + + if ctx.search.pattern.is_some() { + let Search { glob, iglob, .. } = ctx.search; + + if glob || iglob { + self.filter_glob(ctx)?; + } else { + self.filter_regex(ctx)?; + } + } + + if ctx.prune { + self.prune(); + } + + Ok(()) + } + + /// Remove all directories that have no children. + fn prune(&mut self) { + let mut pruning = true; + + while pruning { + let mut to_remove = vec![]; + + for n in self.root_id.descendants(&self.arena).skip(1) { + if !n.is_removed(&self.arena) + && self.arena[n].get().is_dir() + && n.children(&self.arena).count() == 0 + { + to_remove.push(n); + } + } + + if !to_remove.is_empty() { + to_remove + .into_iter() + .for_each(|n| n.remove_subtree(&mut self.arena)); + continue; + } + pruning = false; + } + } + + /// Updates the [`Tree`]'s inner [`indextree::Arena`] to only contain files of certain + /// file-types. This should not affect disk-usage calculations. + /// + /// TODO: Consider using Rayon for parallel filtering. + pub fn filter_file_type( + &mut self, + Context { + layout, file_type, .. + }: &Context, + ) { + let mut filters = Vec::>::new(); + + for ft in HashSet::from_iter(file_type) { + match ft { + FileType::Dir if matches!(layout, Layout::Tree | Layout::InvertedTree) => { + filters.push(Box::new(|f| f.is_dir())) + } + FileType::Dir => filters.push(Box::new(|f| f.is_dir())), + FileType::File => filters.push(Box::new(|f| f.is_file())), + FileType::Symlink => filters.push(Box::new(|f| f.is_symlink())), + + #[cfg(unix)] + FileType::Pipe => filters.push(Box::new(|f| f.is_fifo())), + #[cfg(unix)] + FileType::Socket => filters.push(Box::new(|f| f.is_socket())), + #[cfg(unix)] + FileType::Char => filters.push(Box::new(|f| f.is_char_device())), + #[cfg(unix)] + FileType::Block => filters.push(Box::new(|f| f.is_block_device())), + } + } + + let no_match = |node_id: &NodeId| !filters.iter().any(|f| f(self.arena[*node_id].get())); + + let to_remove = self + .root_id + .descendants(&self.arena) + .filter(no_match) + .collect::>(); + + to_remove + .into_iter() + .for_each(|n| n.remove_subtree(&mut self.arena)); + } + + /// Remove nodes/sub-trees that don't match the provided regular expression, `pattern`. + pub fn filter_regex( + &mut self, + Context { + search: Search { pattern, .. }, + layout, + .. + }: &Context, + ) -> Result<()> { + let re_pattern = pattern + .as_ref() + .ok_or(Error::MissingRegexPattern) + .into_report(ErrorCategory::User)?; + + let regex = Regex::new(re_pattern) + .map_err(Error::InvalidRegex) + .into_report(ErrorCategory::User)?; + + match layout { + Layout::Flat => { + let to_remove = self + .root_id + .descendants(&self.arena) + .skip(1) + .filter(|node_id| { + !regex + .is_match(self.arena[*node_id].get().path().to_string_lossy().as_ref()) + }) + .collect::>(); + + to_remove + .into_iter() + .for_each(|n| n.remove(&mut self.arena)); + } + _ => { + let to_remove = self + .root_id + .descendants(&self.arena) + .skip(1) + .filter(|node_id| { + let node = self.arena[*node_id].get(); + !node.is_dir() && !regex.is_match(node.path().to_string_lossy().as_ref()) + }) + .collect::>(); + + to_remove + .into_iter() + .for_each(|n| n.remove_subtree(&mut self.arena)); + } + }; + + Ok(()) + } + + /// Filtering via globbing + fn filter_glob(&mut self, ctx: &Context) -> Result<()> { + let Context { + search: Search { pattern, iglob, .. }, + layout, + .. + } = ctx; + + let dir = ctx.dir_canonical()?; + let mut override_builder = OverrideBuilder::new(dir); + + let mut negated_glob = false; + + let overrides = { + if *iglob { + override_builder + .case_insensitive(true) + .into_report(ErrorCategory::Internal) + .context(error_source!())?; + } + + if let Some(ref glob) = pattern { + let trim = glob.trim_start(); + negated_glob = trim.starts_with('!'); + + if negated_glob { + override_builder + .add(trim.trim_start_matches('!')) + .into_report(ErrorCategory::Internal) + .context(error_source!())?; + } else { + override_builder + .add(trim) + .into_report(ErrorCategory::Internal) + .context(error_source!())?; + } + } + + override_builder.build().into_report(ErrorCategory::User)? + }; + + match layout { + Layout::Flat => { + let to_remove = self + .root_id + .descendants(&self.arena) + .skip(1) + .filter(|node_id| { + let dirent = self.arena[*node_id].get(); + let matched = overrides.matched(dirent.path(), dirent.is_dir()); + !(negated_glob ^ matched.is_whitelist()) + }) + .collect::>(); + + to_remove + .into_iter() + .for_each(|n| n.remove(&mut self.arena)); + } + _ => { + let to_remove = self + .root_id + .descendants(&self.arena) + .skip(1) + .filter(|node_id| { + let dirent = self.arena[*node_id].get(); + + if dirent.is_dir() { + false + } else { + let matched = overrides.matched(dirent.path(), dirent.is_dir()); + !(negated_glob ^ matched.is_whitelist()) + } + }) + .collect::>(); + + to_remove + .into_iter() + .for_each(|n| n.remove_subtree(&mut self.arena)); + } + } + + Ok(()) + } +} diff --git a/src/file/tree/mod.rs b/src/file/tree/mod.rs new file mode 100644 index 00000000..650fba2b --- /dev/null +++ b/src/file/tree/mod.rs @@ -0,0 +1,330 @@ +use super::order; +use crate::{ + error::prelude::*, + file::File, + progress, + user::{ + args::{Layout, Sort, SortType}, + column, Context, + }, +}; +use ahash::{HashMap, HashSet}; +use indextree::{Arena, NodeId}; +use std::{ops::Deref, path::PathBuf}; + +/// Concerned with pruning and filtering via file-type, globbing, and regular expressions. +mod filter; + +/// Parallel disk reading. +mod traverse; + +/// Representation of the file-tree that is traversed starting from the root directory whose index +/// in the underlying `arena` is `root_id`. +pub struct Tree { + root_id: NodeId, + arena: Arena, +} + +/// Intermediate components that will be used to construct the final [`Tree`] data structure. +pub struct TransitionState { + arena: Arena, + branches: HashMap>, + column_metadata: column::Metadata, + root_id: NodeId, +} + +/// Errors associated with [`Tree`]. +#[derive(Debug, thiserror::Error)] +pub enum TreeError { + #[error("Failed to extrapolate the root directory")] + RootDir, +} + +impl Tree { + /// Like [`Tree::init`] but leverages parallelism for disk-reads and [`File`] initialization. + pub fn init(ctx: &Context) -> Result<(Self, super::Accumulator, column::Metadata)> { + let ( + TransitionState { + mut arena, + mut branches, + mut column_metadata, + root_id, + }, + accumulator, + ) = Self::load(ctx)?; + + let mut dir_stack = vec![root_id]; + let mut inode_set = HashSet::default(); + + // Keeps track of which directory entry we're at for each directory while doing depth first + // traversal. The key is the NodeID of a directory with the value being an index into a + // slice of the directory's children. + let mut dirent_cursor_map = HashMap::::default(); + + // Map of dynamically computed directory sizes. + let mut dirsize_map = HashMap::::default(); + + 'outer: while let Some(node_id) = dir_stack.last() { + let current_dir = *node_id; + + let current_node_path = arena[current_dir].get().path(); + + let Some(dirents) = branches.get_mut(current_node_path) else { + dir_stack.pop(); + continue; + }; + + let current_dirsize = dirsize_map.entry(current_dir).or_insert(0); + let dirent_cursor = dirent_cursor_map.entry(current_dir).or_insert(0); + + for dirent_node_id in &dirents[*dirent_cursor..] { + *dirent_cursor += 1; + let dirent_node_id = *dirent_node_id; + let dirent_node = arena[dirent_node_id].get(); + + if dirent_node.is_dir() { + dir_stack.push(dirent_node_id); + continue 'outer; + } + + if let Ok(inode) = dirent_node.inode() { + #[cfg(unix)] + column_metadata.update_inode_attr_widths(&inode); + + *current_dirsize += inode_set + .insert(inode) + .then(|| dirent_node.size().value()) + .unwrap_or(0); + } + } + + dir_stack.pop(); + + // To play nicely with aliasing rules around mutable refs + let current_dirsize = *current_dirsize; + + if let Some(parent_dir) = dir_stack.last() { + if let Some(parent_dirsize) = dirsize_map.get_mut(parent_dir) { + *parent_dirsize += current_dirsize; + } + } + } + + match order::comparator(ctx.sort, ctx.dir_order) { + Some(comparator) => match ctx.sort_type { + SortType::Flat if matches!(ctx.layout, Layout::Flat) => { + for (dir_id, dirsize) in dirsize_map.into_iter() { + let dir = arena[dir_id].get_mut(); + *dir.size_mut() += dirsize; + } + + let mut all_dirents = branches + .values() + .flatten() + .filter_map(|n| (*n != root_id).then_some(*n)) + .collect::>(); + + all_dirents.sort_by(|id_a, id_b| { + let node_a = arena[*id_a].get(); + let node_b = arena[*id_b].get(); + comparator(node_a, node_b) + }); + + all_dirents + .into_iter() + .for_each(|n| root_id.append(n, &mut arena)); + } + _ => { + for (dir_id, dirsize) in dirsize_map.into_iter() { + let dir = arena[dir_id].get_mut(); + *dir.size_mut() += dirsize; + + if let Some(mut dirents) = branches.remove(dir.path()) { + dirents.sort_by(|id_a, id_b| { + let node_a = arena[*id_a].get(); + let node_b = arena[*id_b].get(); + comparator(node_a, node_b) + }); + + for dirent_id in dirents { + dir_id.append(dirent_id, &mut arena); + } + } + } + } + }, + None => { + for (dir_id, dirsize) in dirsize_map.into_iter() { + let dir = arena[dir_id].get_mut(); + *dir.size_mut() += dirsize; + + if let Some(dirents) = branches.remove(dir.path()) { + for dirent_id in dirents { + dir_id.append(dirent_id, &mut arena); + } + } + } + } + } + + column_metadata.update_size_width(arena[root_id].get(), ctx); + + let tree = Self { root_id, arena }; + + Ok((tree, accumulator, column_metadata)) + } + + pub fn init_without_disk_usage( + ctx: &Context, + ) -> Result<(Self, super::Accumulator, column::Metadata)> { + let ( + TransitionState { + mut arena, + mut branches, + mut column_metadata, + root_id, + }, + accumulator, + ) = Self::load(ctx)?; + + #[cfg(unix)] + macro_rules! update_metadata { + ($dirent_id:expr) => { + let dirent = arena[$dirent_id].get(); + if let Ok(inode) = dirent.inode() { + column_metadata.update_inode_attr_widths(&inode); + } + }; + } + + match ctx.sort_type { + SortType::Flat if matches!(ctx.layout, Layout::Flat) => { + let mut all_dirents = branches + .values() + .flatten() + .filter_map(|n| (*n != root_id).then_some(*n)) + .collect::>(); + + if let Some(comparator) = order::comparator(Sort::None, ctx.dir_order) { + all_dirents.sort_by(|id_a, id_b| { + let node_a = arena[*id_a].get(); + let node_b = arena[*id_b].get(); + comparator(node_a, node_b) + }); + } + + for dirent_id in all_dirents { + root_id.append(dirent_id, &mut arena); + + #[cfg(unix)] + update_metadata!(dirent_id); + } + } + _ => { + let dirs = arena + .iter() + .filter_map(|node| { + if node.get().is_dir() { + arena + .get_node_id(node) + .map(|n| (node.get().path().to_path_buf(), n)) + } else { + None + } + }) + .collect::>(); + + dirs.into_iter().for_each(|(path, dir)| { + if let Some(mut dirents) = branches.remove(&path) { + if let Some(comparator) = order::comparator(Sort::None, ctx.dir_order) { + dirents.sort_by(|id_a, id_b| { + let node_a = arena[*id_a].get(); + let node_b = arena[*id_b].get(); + comparator(node_a, node_b) + }); + } + + for dirent_id in dirents { + dir.append(dirent_id, &mut arena); + + #[cfg(unix)] + update_metadata!(dirent_id); + } + } + }); + } + } + + let tree = Self { root_id, arena }; + + Ok((tree, accumulator, column_metadata)) + } + + /// Reads data from disk and aggregates data along with metadata into a [`TransitionState`] + /// which callers would then consume to construct a [`Tree`]. + fn load(ctx: &Context) -> Result<(TransitionState, super::Accumulator)> { + let mut arena = Arena::new(); + let mut branches = HashMap::>::default(); + let mut column_metadata = column::Metadata::default(); + let mut maybe_root_id = None; + let mut accumulator = super::Accumulator::default(); + + // To notify the progress indicator + let notifier = progress::Indicator::use_notifier(); + + traverse::run(ctx, |file| { + #[cfg(unix)] + column_metadata.update_unix_attrs_widths(&file, ctx); + + let node_id = arena.new_node(file); + let file = arena[node_id].get(); + let file_path = file.path(); + + accumulator.increment(file.file_type()); + progress::Indicator::notify(¬ifier, accumulator.total()); + + maybe_root_id = (file.depth() == 0).then_some(node_id).or(maybe_root_id); + + if let Some(parent) = file_path.parent() { + if let Some(nodes) = branches.get_mut(parent) { + nodes.push(node_id); + } else { + branches.insert(parent.to_path_buf(), vec![node_id]); + } + } + Ok(()) + })?; + + progress::Indicator::finish(¬ifier); + + let root_id = maybe_root_id + .ok_or(TreeError::RootDir) + .into_report(ErrorCategory::Internal) + .context(error_source!())?; + + let ts = TransitionState { + arena, + branches, + column_metadata, + root_id, + }; + + Ok((ts, accumulator)) + } + + pub fn root_id(&self) -> NodeId { + self.root_id + } + + pub fn arena(&self) -> &Arena { + &self.arena + } +} + +impl Deref for Tree { + type Target = Arena; + + fn deref(&self) -> &Self::Target { + &self.arena + } +} diff --git a/src/file/tree/traverse.rs b/src/file/tree/traverse.rs new file mode 100644 index 00000000..4bb3e091 --- /dev/null +++ b/src/file/tree/traverse.rs @@ -0,0 +1,154 @@ +use crate::{error::prelude::*, file::File, user::Context}; +use ignore::{DirEntry, ParallelVisitor, ParallelVisitorBuilder, WalkState}; +use std::{ + ops::Deref, + result::Result as StdResult, + sync::mpsc::{self, Sender}, + thread, +}; + +/// Parallel traversal algorithm. `op` takes in a single argument which is the [`File`] that is +/// retrieved from disk, returning a [`Result`]. If `op` returns an `Err` then traversal will +/// immediately conclude. +pub fn run(ctx: &Context, mut op: F) -> Result<()> +where + F: FnMut(File) -> Result<()> + Send, +{ + let parallel_walker = walker::init(ctx)?; + + let (tx, rx) = mpsc::channel::(); + let mut builder = VisitorBuilder::new(tx.clone(), ctx); + + thread::scope(move |scope| { + let handle = scope.spawn(move || { + loop { + match rx.recv().into_report(ErrorCategory::Internal) { + Ok(TraversalState::Ongoing(file)) => op(file)?, + Ok(TraversalState::Done) => break, + Err(e) => return Err(e), + _ => (), + } + } + Ok(()) + }); + + parallel_walker.visit(&mut builder); + let _ = tx.send(TraversalState::Done); + + handle.join().unwrap() + })?; + + Ok(()) +} + +pub enum TraversalState { + Error(Error), + Ongoing(File), + Done, +} + +pub struct Visitor<'a> { + tx: Sender, + ctx: &'a Context, +} + +pub struct VisitorBuilder<'a> { + tx: Sender, + ctx: &'a Context, +} + +impl<'a> VisitorBuilder<'a> { + pub fn new(tx: Sender, ctx: &'a Context) -> Self { + Self { tx, ctx } + } +} + +impl<'a> Visitor<'a> { + pub fn new(tx: Sender, ctx: &'a Context) -> Self { + Self { tx, ctx } + } +} + +impl ParallelVisitor for Visitor<'_> { + fn visit(&mut self, entry: StdResult) -> WalkState { + let entry = match entry.into_report(ErrorCategory::Warning) { + Ok(entry) => entry, + Err(e) => { + let _ = self.send(TraversalState::Error(e)); + return WalkState::Continue; + } + }; + + match File::init(entry, self.ctx).into_report(ErrorCategory::Warning) { + Ok(file) => { + let _ = self.send(TraversalState::Ongoing(file)); + } + Err(e) => { + let _ = self.send(TraversalState::Error(e)); + } + } + + WalkState::Continue + } +} + +impl<'a> ParallelVisitorBuilder<'a> for VisitorBuilder<'a> { + fn build(&mut self) -> Box { + Box::new(Visitor::new(Sender::clone(&self.tx), self.ctx)) + } +} + +impl Deref for Visitor<'_> { + type Target = Sender; + + fn deref(&self) -> &Self::Target { + &self.tx + } +} + +mod walker { + use crate::{error::prelude::*, user::Context}; + use ignore::{ + overrides::{Override, OverrideBuilder}, + WalkBuilder, WalkParallel, + }; + use std::path::Path; + + pub fn init(ctx: &Context) -> Result { + let path = ctx.dir_canonical()?; + let mut builder = WalkBuilder::new(&path); + + let walker = builder + .follow_links(ctx.follow) + .git_ignore(ctx.gitignore) + .git_global(ctx.global_gitignore) + .threads(ctx.threads) + .hidden(ctx.no_hidden) + .same_file_system(ctx.same_fs); + + if ctx.suppress_size { + walker.max_depth(ctx.level); + } + + let overrides = build_overrides(ctx, &path)?; + walker.overrides(overrides); + + Ok(walker.build_parallel()) + } + + pub fn build_overrides(ctx: &Context, path: &Path) -> Result { + let mut builder = OverrideBuilder::new(path); + + if ctx.no_git { + builder + .add("!.git") + .into_report(ErrorCategory::Internal) + .context(error_source!())?; + } + + builder + .build() + .into_report(ErrorCategory::Internal) + .context(error_source!()) + } +} diff --git a/src/tree/node/unix.rs b/src/file/unix/mod.rs similarity index 75% rename from src/tree/node/unix.rs rename to src/file/unix/mod.rs index 9160ccd1..355f08c2 100644 --- a/src/tree/node/unix.rs +++ b/src/file/unix/mod.rs @@ -1,9 +1,22 @@ -use crate::fs::{ug::UserGroupInfo, xattr::ExtendedAttr}; use ignore::DirEntry; use std::{convert::From, fs::Metadata}; +/// Unix file permissions in symbolic and octal notation for presentation. +#[cfg(unix)] +pub mod permissions; + +/// Concerned with determining group and owner of file. +#[cfg(unix)] +pub mod ug; +use ug::UserGroupInfo; + +/// Determining whether or not a file has extended attributes. +#[cfg(unix)] +pub mod xattr; +use xattr::ExtendedAttr; + /// File attributes that are optionally computed and specific to Unix-like systems. -#[derive(Default)] +#[derive(Debug, Default)] pub struct Attrs { pub has_xattrs: bool, owner: Option, diff --git a/src/fs/permissions/class.rs b/src/file/unix/permissions/class.rs similarity index 100% rename from src/fs/permissions/class.rs rename to src/file/unix/permissions/class.rs diff --git a/src/fs/permissions/error.rs b/src/file/unix/permissions/error.rs similarity index 100% rename from src/fs/permissions/error.rs rename to src/file/unix/permissions/error.rs diff --git a/src/fs/permissions/file_type.rs b/src/file/unix/permissions/file_type.rs similarity index 100% rename from src/fs/permissions/file_type.rs rename to src/file/unix/permissions/file_type.rs diff --git a/src/fs/permissions/mod.rs b/src/file/unix/permissions/mod.rs similarity index 100% rename from src/fs/permissions/mod.rs rename to src/file/unix/permissions/mod.rs diff --git a/src/fs/permissions/test.rs b/src/file/unix/permissions/test.rs similarity index 100% rename from src/fs/permissions/test.rs rename to src/file/unix/permissions/test.rs diff --git a/src/fs/ug.rs b/src/file/unix/ug.rs similarity index 89% rename from src/fs/ug.rs rename to src/file/unix/ug.rs index 5d7e5343..7d08bbf8 100644 --- a/src/fs/ug.rs +++ b/src/file/unix/ug.rs @@ -8,14 +8,6 @@ impl UserGroupInfo for Metadata {} /// Trait that allows for files to query their owner and group. pub trait UserGroupInfo: MetadataExt { - /// Attemps to query the owner of the implementor. - fn try_get_owner(&self) -> Result { - unsafe { - let uid = self.uid(); - try_get_user(uid) - } - } - /// Attempts to query both the owner and group of the implementor. fn try_get_owner_and_group(&self) -> Result<(Owner, Group), Error> { unsafe { diff --git a/src/fs/xattr.rs b/src/file/unix/xattr.rs similarity index 100% rename from src/fs/xattr.rs rename to src/file/unix/xattr.rs diff --git a/src/fs/mod.rs b/src/fs/mod.rs deleted file mode 100644 index 9c262071..00000000 --- a/src/fs/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -use ignore::DirEntry; -use std::{fs, path::PathBuf}; - -/// Operations pertaining to underlying inodes of files. -pub mod inode; - -/// Unix file permissions. -#[cfg(unix)] -pub mod permissions; - -/// Determining whether or not a file has extended attributes. -#[cfg(unix)] -pub mod xattr; - -/// Concerned with determining group and owner of file. -#[cfg(unix)] -pub mod ug; - -/// Returns the path to the target of the soft link. Returns `None` if provided `dir_entry` isn't a -/// symlink. -pub fn symlink_target(dir_entry: &DirEntry) -> Option { - dir_entry - .path_is_symlink() - .then(|| fs::read_link(dir_entry.path())) - .transpose() - .ok() - .flatten() -} diff --git a/src/icon.rs b/src/icon.rs new file mode 100644 index 00000000..fa52eb64 --- /dev/null +++ b/src/icon.rs @@ -0,0 +1,331 @@ +use crate::file::File; +use ahash::HashMap; +use once_cell::sync::Lazy; +use std::ffi::OsString; + +/// The precedent from highest to lowest in terms of which parameters determine the icon used +/// is as followed: file-type, file-extension, and then file-name. If an icon cannot be +/// computed the fall-back default icon is used. +pub fn compute(file: &File) -> &str { + from_file_type(file) + .or_else(|| from_ext(file)) + .or_else(|| from_file_name(file)) + .unwrap_or(DEFAULT_ICON) +} + +fn from_file_type(file: &File) -> Option<&str> { + if file.is_dir() { + FILE_TYPE_ICON_MAP.get("dir").copied() + } else if file.is_symlink() { + FILE_TYPE_ICON_MAP.get("symlink").copied() + } else { + None + } +} + +fn from_ext(file: &File) -> Option<&str> { + file.path() + .extension() + .and_then(|ext| EXT_ICON_MAP.get(ext).copied()) +} + +fn from_file_name(file: &File) -> Option<&str> { + FILE_NAME_ICON_MAP.get(file.file_name()).copied() +} + +/// Ruby-like way to crate a hashmap. +macro_rules! hash { + ( $( $( $k:literal)|* => $v:expr ),* ) => { + { + let mut hash = HashMap::default(); + $( + $( hash.insert($k, $v); )* + )* + hash + } + }; + ( $( $k:expr => $v:expr ),* ) => { + { + let mut hash = HashMap::default(); + $( hash.insert($k, $v); )* + hash + } + }; +} + +/// Default fallback icon. +const DEFAULT_ICON: &str = "\u{f15b}"; + +/// Lazily evaluated static hash-map of special file-types and their corresponding styled icons. +/// These icons will take on the color properties of their associated file which is based on +/// `LS_COLORS`. +/// +/// Dev icons sourced from [`exa`](https://github.com/ogham/exa/blob/master/src/output/icons.rs) +static FILE_TYPE_ICON_MAP: Lazy> = Lazy::new(|| { + hash!( + "dir" => "\u{f413}", //  + "symlink" => "\u{f482}" //  + ) +}); + +/// Lazily evaluated static hash-map of special named and their corresponding icons. These icons +/// will take on the color properties of their associated file which is based on `LS_COLORS`. +/// +/// Dev icons sourced from [`exa`](https://github.com/ogham/exa/blob/master/src/output/icons.rs) +static FILE_NAME_ICON_MAP: Lazy> = Lazy::new(|| { + hash!( + OsString::from(".Trash") => "\u{f1f8}", //  + OsString::from(".atom") => "\u{e764}", //  + OsString::from(".bashprofile") => "\u{e615}", //  + OsString::from(".bashrc") => "\u{f489}", //  + OsString::from(".git") => "\u{f1d3}", //  + OsString::from(".gitattributes") => "\u{f1d3}", //  + OsString::from(".gitconfig") => "\u{f1d3}", //  + OsString::from(".github") => "\u{f408}", //  + OsString::from(".gitignore") => "\u{f1d3}", //  + OsString::from(".gitmodules") => "\u{f1d3}", //  + OsString::from(".rvm") => "\u{e21e}", //  + OsString::from(".vimrc") => "\u{e62b}", //  + OsString::from(".vscode") => "\u{e70c}", //  + OsString::from(".zshrc") => "\u{f489}", //  + OsString::from("Cargo.lock") => "\u{e7a8}", //  + OsString::from("bin") => "\u{e5fc}", //  + OsString::from("config") => "\u{e5fc}", //  + OsString::from("docker-compose.yml") => "\u{f308}", //  + OsString::from("Dockerfile") => "\u{f308}", //  + OsString::from(".DS_Store") => "\u{f179}", //  + OsString::from("gitignore_global") => "\u{f1d3}", //  + OsString::from("go.mod") => "\u{e626}", //  + OsString::from("go.sum") => "\u{e626}", //  + OsString::from("gradle") => "\u{e256}", //  + OsString::from("gruntfile.coffee") => "\u{e611}", //  + OsString::from("gruntfile.js") => "\u{e611}", //  + OsString::from("gruntfile.ls") => "\u{e611}", //  + OsString::from("gulpfile.coffee") => "\u{e610}", //  + OsString::from("gulpfile.js") => "\u{e610}", //  + OsString::from("gulpfile.ls") => "\u{e610}", //  + OsString::from("hidden") => "\u{f023}", //  + OsString::from("include") => "\u{e5fc}", //  + OsString::from("lib") => "\u{f121}", //  + OsString::from("license") => "\u{e60a}", //  + OsString::from("LICENSE") => "\u{e60a}", //  + OsString::from("licence") => "\u{e60a}", //  + OsString::from("LICENCE") => "\u{e60a}", //  + OsString::from("localized") => "\u{f179}", //  + OsString::from("Makefile") => "\u{f489}", //  + OsString::from("node_modules") => "\u{e718}", //  + OsString::from("npmignore") => "\u{e71e}", //  + OsString::from("PKGBUILD") => "\u{f303}", //  + OsString::from("rubydoc") => "\u{e73b}", //  + OsString::from("yarn.lock") => "\u{e718}" //  + ) +}); + +/// Lazily evaluated static hash-map of various file extensions and their corresponding icons. The +/// key is the file extension while the associated value Unicode scalar value for the corresponding icon. +/// +/// Dev icons sourced from [`nvim-web-devicons`](https://github.com/nvim-tree/nvim-web-devicons/blob/master/lua/nvim-web-devicons.lua). +static EXT_ICON_MAP: Lazy> = Lazy::new(|| { + hash!( + OsString::from("ai") => "\u{e7b4}", //  + OsString::from("awk") => "\u{e795}", //  + OsString::from("bash") => "\u{e795}", //  + OsString::from("bat") => "\u{e615}", //  + OsString::from("bmp") => "\u{e60d}", //  + OsString::from("cbl") => "\u{2699}", // ⚙ + OsString::from("c++") => "\u{e61d}", //  + OsString::from("c") => "\u{e61e}", //  + OsString::from("cc") => "\u{e61d}", //  + OsString::from("cfg") => "\u{e7a3}", //  + OsString::from("cljc") => "\u{e768}", //  + OsString::from("clj") => "\u{e768}", //  + OsString::from("cljd") => "\u{e76a}", //  + OsString::from("cljs") => "\u{e76a}", //  + OsString::from("cmake") => "\u{e615}", //  + OsString::from("cob") => "\u{2699}", // ⚙ + OsString::from("cobol") => "\u{2699}", // ⚙ + OsString::from("coffee") => "\u{e61b}", //  + OsString::from("conf") => "\u{e615}", //  + OsString::from("config.ru") => "\u{e791}", //  + OsString::from("cp") => "\u{e61d}", //  + OsString::from("cpp") => "\u{e61d}", //  + OsString::from("cpy") => "\u{2699}", // ⚙ + OsString::from("cr") => "\u{e24f}", //  + OsString::from("cs") => "\u{f031b}", // 󰌛 + OsString::from("csh") => "\u{e795}", //  + OsString::from("cson") => "\u{e60b}", //  + OsString::from("css") => "\u{e749}", //  + OsString::from("csv") => "\u{f0219}", // 󰈙 + OsString::from("cxx") => "\u{e61d}", //  + OsString::from("dart") => "\u{e798}", //  + OsString::from("db") => "\u{e706}", //  + OsString::from("d") => "\u{e7af}", //  + OsString::from("desktop") => "\u{f108}", //  + OsString::from("diff") => "\u{e728}", //  + OsString::from("doc") => "\u{f022c}", // 󰈬 + OsString::from("drl") => "\u{e28c}", //  + OsString::from("dropbox") => "\u{e707}", //  + OsString::from("dump") => "\u{e706}", //  + OsString::from("edn") => "\u{e76a}", //  + OsString::from("eex") => "\u{e62d}", //  + OsString::from("ejs") => "\u{e60e}", //  + OsString::from("elm") => "\u{e62c}", //  + OsString::from("epp") => "\u{e631}", //  + OsString::from("erb") => "\u{e60e}", //  + OsString::from("erl") => "\u{e7b1}", //  + OsString::from("ex") => "\u{e62d}", //  + OsString::from("exs") => "\u{e62d}", //  + OsString::from("f#") => "\u{e7a7}", //  + OsString::from("fish") => "\u{e795}", //  + OsString::from("fnl") => "\u{1f31c}", // 🌜 + OsString::from("fs") => "\u{e7a7}", //  + OsString::from("fsi") => "\u{e7a7}", //  + OsString::from("fsscript") => "\u{e7a7}", //  + OsString::from("fsx") => "\u{e7a7}", //  + OsString::from("GNUmakefile") => "\u{e779}", //  + OsString::from("gd") => "\u{e615}", //  + OsString::from("gemspec") => "\u{e791}", //  + OsString::from("gif") => "\u{e60d}", //  + OsString::from("git") => "\u{e702}", //  + OsString::from("glb") => "\u{f1b2}", //  + OsString::from("go") => "\u{e627}", //  + OsString::from("godot") => "\u{e7a3}", //  + OsString::from("gql") => "\u{f20e}", //  + OsString::from("graphql") => "\u{f20e}", //  + OsString::from("haml") => "\u{e60e}", //  + OsString::from("hbs") => "\u{e60f}", //  + OsString::from("h") => "\u{f0fd}", //  + OsString::from("heex") => "\u{e62d}", //  + OsString::from("hh") => "\u{f0fd}", //  + OsString::from("hpp") => "\u{f0fd}", //  + OsString::from("hrl") => "\u{e7b1}", //  + OsString::from("hs") => "\u{e61f}", //  + OsString::from("htm") => "\u{e60e}", //  + OsString::from("html") => "\u{e736}", //  + OsString::from("hxx") => "\u{f0fd}", //  + OsString::from("ico") => "\u{e60d}", //  + OsString::from("import") => "\u{f0c6}", //  + OsString::from("ini") => "\u{e615}", //  + OsString::from("java") => "\u{e738}", //  + OsString::from("jl") => "\u{e624}", //  + OsString::from("jpeg") => "\u{e60d}", //  + OsString::from("jpg") => "\u{e60d}", //  + OsString::from("js") => "\u{e60c}", //  + OsString::from("json5") => "\u{f0626}", // 󰘦 + OsString::from("json") => "\u{e60b}", //  + OsString::from("jsx") => "\u{e625}", //  + OsString::from("ksh") => "\u{e795}", //  + OsString::from("kt") => "\u{e634}", //  + OsString::from("kts") => "\u{e634}", //  + OsString::from("leex") => "\u{e62d}", //  + OsString::from("less") => "\u{e614}", //  + OsString::from("lhs") => "\u{e61f}", //  + OsString::from("license") => "\u{e60a}", //  + OsString::from("licence") => "\u{e60a}", //  + OsString::from("lock") => "\u{f13e}", //  + OsString::from("log") => "\u{f0331}", // 󰮩 + OsString::from("lua") => "\u{e620}", //  + OsString::from("luau") => "\u{e620}", //  + OsString::from("makefile") => "\u{e779}", //  + OsString::from("markdown") => "\u{e609}", //  + OsString::from("Makefile") => "\u{e779}", //  + OsString::from("material") => "\u{f0509}", // 󰔉 + OsString::from("md") => "\u{f48a}", //  + OsString::from("mdx") => "\u{f48a}", //  + OsString::from("mint") => "\u{f032a}", // 󰌪 + OsString::from("mjs") => "\u{e60c}", //  + OsString::from("mk") => "\u{e779}", //  + OsString::from("ml") => "\u{3bb}", // λ + OsString::from("mli") => "\u{3bb}", // λ + OsString::from("mo") => "\u{221e}", // ∞ + OsString::from("mustache") => "\u{e60f}", //  + OsString::from("nim") => "\u{e677}", //  + OsString::from("nix") => "\u{f313}", //  + OsString::from("opus") => "\u{f0223}", // 󰈣 + OsString::from("otf") => "\u{f031}", //  + OsString::from("pck") => "\u{f487}", //  + OsString::from("pdf") => "\u{f0226}", // 󰈦 + OsString::from("php") => "\u{e608}", //  + OsString::from("pl") => "\u{e769}", //  + OsString::from("pm") => "\u{e769}", //  + OsString::from("png") => "\u{e60d}", //  + OsString::from("pp") => "\u{e631}", //  + OsString::from("ppt") => "\u{f0227}", // 󰈧 + OsString::from("prisma") => "\u{e684}", //  + OsString::from("pro") => "\u{e7a1}", //  + OsString::from("ps1") => "\u{f0a0a}", // 󰨊 + OsString::from("psb") => "\u{e7b8}", //  + OsString::from("psd1") => "\u{f0a0a}", // 󰨊 + OsString::from("psd") => "\u{e7b8}", //  + OsString::from("psm1") => "\u{f0a0a}", // 󰨊 + OsString::from("pyc") => "\u{e606}", //  + OsString::from("py") => "\u{e606}", //  + OsString::from("pyd") => "\u{e606}", //  + OsString::from("pyo") => "\u{e606}", //  + OsString::from("query") => "\u{e21c}", //  + OsString::from("rake") => "\u{e791}", //  + OsString::from("rb") => "\u{e791}", //  + OsString::from("r") => "\u{f07d4}", // 󰟔 + OsString::from("rlib") => "\u{e7a8}", //  + OsString::from("rmd") => "\u{e609}", //  + OsString::from("rproj") => "\u{f07d4}", // 󰟔 + OsString::from("rs") => "\u{e7a8}", //  + OsString::from("rss") => "\u{e619}", //  + OsString::from("sass") => "\u{e603}", //  + OsString::from("sbt") => "\u{e737}", //  + OsString::from("scala") => "\u{e737}", //  + OsString::from("scm") => "\u{f0627}", // 󰘧 + OsString::from("scss") => "\u{e603}", //  + OsString::from("sh") => "\u{e795}", //  + OsString::from("sig") => "\u{3bb}", // λ + OsString::from("slim") => "\u{e60e}", //  + OsString::from("sln") => "\u{e70c}", //  + OsString::from("sml") => "\u{3bb}", // λ + OsString::from("sol") => "\u{f07bb}", // 󰞻 + OsString::from("sql") => "\u{e706}", //  + OsString::from("sqlite3") => "\u{e706}", //  + OsString::from("sqlite") => "\u{e706}", //  + OsString::from("styl") => "\u{e600}", //  + OsString::from("sublime") => "\u{e7aa}", //  + OsString::from("suo") => "\u{e70c}", //  + OsString::from("sv") => "\u{f035b}", // 󰍛 + OsString::from("svelte") => "\u{f260}", //  + OsString::from("svg") => "\u{f0721}", // 󰜡 + OsString::from("svh") => "\u{f035b}", // 󰍛 + OsString::from("swift") => "\u{e755}", //  + OsString::from("tbc") => "\u{f06d3}", // 󰛓 + OsString::from("t") => "\u{e769}", //  + OsString::from("tcl") => "\u{f06d3}", // 󰛓 + OsString::from("terminal") => "\u{f489}", //  + OsString::from("test.js") => "\u{e60c}", //  + OsString::from("tex") => "\u{f0669}", // 󰙩 + OsString::from("tf") => "\u{e2a6}", //  + OsString::from("tfvars") => "\u{f15b}", //  + OsString::from("toml") => "\u{e615}", //  + OsString::from("tres") => "\u{e706}", //  + OsString::from("ts") => "\u{e628}", //  + OsString::from("tscn") => "\u{f0381}", // 󰎁 + OsString::from("tsx") => "\u{e7ba}", //  + OsString::from("twig") => "\u{e61c}", //  + OsString::from("txt") => "\u{f0219}", // 󰈙 + OsString::from("vala") => "\u{e69e}", //  + OsString::from("v") => "\u{f035b}", // 󰍛 + OsString::from("vh") => "\u{f035b}", // 󰍛 + OsString::from("vhd") => "\u{f035b}", // 󰍛 + OsString::from("vhdl") => "\u{f035b}", // 󰍛 + OsString::from("vim") => "\u{e62b}", //  + OsString::from("vue") => "\u{f0844}", // 󰡄 + OsString::from("wasm") => "\u{e6a1}", //  + OsString::from("webmanifest") => "\u{e60b}", //  + OsString::from("webpack") => "\u{f072b}", // 󰜫 + OsString::from("webp") => "\u{e60d}", //  + OsString::from("xcplayground") => "\u{e755}", //  + OsString::from("xls") => "\u{f021b}", // 󰈛 + OsString::from("xml") => "\u{f05c0}", // 󰗀 + OsString::from("xul") => "\u{e745}", //  + OsString::from("yaml") => "\u{e615}", //  + OsString::from("yml") => "\u{e615}", //  + OsString::from("zig") => "\u{f0e7}", //  + OsString::from("zsh") => "\u{e795}" //  + ) +}); diff --git a/src/icons/fs.rs b/src/icons/fs.rs deleted file mode 100644 index aa56ac70..00000000 --- a/src/icons/fs.rs +++ /dev/null @@ -1,95 +0,0 @@ -use ansi_term::{ANSIGenericString, Style}; -use ignore::DirEntry; -use std::{borrow::Cow, path::Path}; - -/// Computes a plain, colorless icon with given parameters. -/// -/// The precedent from highest to lowest in terms of which parameters determine the icon used -/// is as followed: file-type, file-extension, and then file-name. If an icon cannot be -/// computed the fall-back default icon is used. -/// -/// If a directory entry is a link and the link target is provided, the link target will be -/// used to determine the icon. -pub fn compute(entry: &DirEntry, link_target: Option<&Path>) -> Cow<'static, str> { - let icon = entry - .file_type() - .and_then(super::icon_from_file_type) - .map(Cow::from); - - if let Some(i) = icon { - return i; - } - - let ext = match link_target { - Some(target) if entry.path_is_symlink() => target.extension(), - _ => entry.path().extension(), - }; - - let icon = ext - .and_then(super::icon_from_ext) - .map(|(_, i)| Cow::from(i)); - - if let Some(i) = icon { - return i; - } - - let icon = super::icon_from_file_name(entry.file_name()).map(Cow::from); - - if let Some(i) = icon { - return i; - } - - Cow::from(super::get_default_icon().1) -} - -/// Computes a plain, colored icon with given parameters. See [compute] for more details. -pub fn compute_with_color( - entry: &DirEntry, - link_target: Option<&Path>, - style: Option