diff --git a/Cargo.lock b/Cargo.lock index efe10645..7d9fa5f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -35,18 +46,44 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "async-trait" +version = "0.1.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.10", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "1.3.0" @@ -146,12 +183,40 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "config" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "cpufeatures" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +dependencies = [ + "libc", +] + [[package]] name = "crossterm" version = "0.26.1" @@ -177,6 +242,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cxx" version = "1.0.94" @@ -221,6 +296,16 @@ dependencies = [ "syn 2.0.10", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs" version = "5.0.0" @@ -241,6 +326,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + [[package]] name = "erdtree" version = "3.0.2" @@ -249,6 +340,7 @@ dependencies = [ "chrono", "clap", "clap_complete", + "config", "crossterm", "dirs", "errno 0.3.1", @@ -322,6 +414,16 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.9" @@ -346,6 +448,15 @@ dependencies = [ "regex", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + [[package]] name = "heck" version = "0.4.1" @@ -443,6 +554,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + [[package]] name = "js-sys" version = "0.3.61" @@ -452,6 +569,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -473,6 +601,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.1.4" @@ -520,6 +654,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "0.8.6" @@ -532,6 +672,16 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -567,6 +717,16 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown", +] + [[package]] name = "os_str_bytes" version = "6.4.1" @@ -602,6 +762,56 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pest" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16833386b02953ca926d19f64af613b9bf742c48dcd5e09b32fbfc9740bf84e2" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7763190f9406839f99e5197afee8c9e759969f7dbfa40ad3b8dbee8757b745b5" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "249061b22e99973da1f5f5f1410284419e283bb60b79255bf5f42a94b66a2e00" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.10", +] + +[[package]] +name = "pest_meta" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457c310cfc9cf3f22bc58901cc7f0d3410ac5d6298e432a4f9a6138565cb6df6" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -681,6 +891,27 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64", + "bitflags", + "serde", +] + +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustix" version = "0.36.9" @@ -709,6 +940,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + [[package]] name = "same-file" version = "1.0.6" @@ -735,6 +972,42 @@ name = "serde" version = "1.0.156" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "314b5b092c0ade17c00142951e50ced110ec27cea304b1037c6969246c2469a4" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.156" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7e29c4601e36bcec74a223228dce795f4cd3616341a4af93520ca1a837c087d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_json" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf3bf93142acad5821c99197022e170842cdbc1c30482b98750c688c640842a" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] [[package]] name = "signal-hook" @@ -872,6 +1145,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + [[package]] name = "unicode-ident" version = "1.0.8" @@ -1179,3 +1473,12 @@ name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index c6eb1e4d..7982fc68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ documentation = "https://github.com/solidiquis/erdtree" homepage = "https://github.com/solidiquis/erdtree" repository = "https://github.com/solidiquis/erdtree" keywords = ["tree", "find", "ls", "du", "commandline"] -exclude = ["assets/*", "scripts/*"] +exclude = ["assets/*", "scripts/*", "example/*"] readme = "README.md" license = "MIT" rust-version = "1.70.0" @@ -28,6 +28,7 @@ ansi_term = "0.12.1" chrono = "0.4.24" clap = { version = "4.1.1", features = ["derive"] } clap_complete = "4.1.1" +config = { version = "0.13.3", features = ["toml"] } crossterm = "0.26.1" dirs = "5.0" errno = "0.3.1" diff --git a/README.md b/README.md index a37e6fba..85d73185 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ You can think of `erdtree` as a little bit of `du`, `tree`, `find`, `wc` and `ls * [Installation](#installation) * [Documentation](#documentation) - [Configuration file](#configuration-file) + - [Toml file](#toml-file) + - [.erdtreerc](#erdtreerc) - [Hardlinks](#hardlinks) - [Symlinks](#symlinks) - [Disk usage](#disk-usage) @@ -316,6 +318,82 @@ Other means of installation to come. If `erdtree`'s out-of-the-box defaults don't meet your specific requirements, you can set your own defaults using a configuration file. +The configuration file currently comes in two flavors: `.erdtreerc` (to be deprecated) and `.erdtree.toml`. If you have both, +`.erdtreerc` will take precedent and `.erdtree.toml` will be disregarded, but please **note that `.erdtreerc` will be deprecated in the near future.** There is +no reason to have both. + +#### TOML file + +`erdtree` will look for `.erdtree.toml in any of the following locations: + +On Unix-systems: + +``` +$ERDTREE_TOML_PATH +$XDG_CONFIG_HOME/erdtree/.erdtree.toml +$XDG_CONFIG_HOME/.erdtree.toml +$HOME/.config/erdtree/.erdtree.toml +$HOME/.erdtree.toml +``` + +On Windows: + +``` +%APPDATA%\erdtree\.erdtree.toml +``` + +[Here](example/.erdtree.toml) and below is an example of a valid `.erdtree.toml`: + +```toml +icons = true +human = true + +# Compute file sizes like `du` +# e.g. `erd --config du` +[du] +disk_usage = "block" +icons = true +layout = "flat" +no-ignore = true +no-git = true +hidden = true +level = 1 + +# Do as `ls -l` +# e.g. `erd --config ls` +[ls] +icons = true +human = true +level = 1 +suppress-size = true +long = true + +# How many lines of Rust are in this code base? +# e.g. `erd --config rs` +[rs] +disk-usage = "word" +level = 1 +pattern = "\\.rs$" +``` + +`.erdtree.toml` supports multiple configurations. The top-level table is the main config that will be applied without additional arguments. +If you wish to use a separate configuration, create a named table like `du` above, set your arguments, and invoke it like so: + +``` +$ erd --config du + +# equivalent to + +$ erd --disk-usage block --icons --layout flat --no-ignore --no-git --hidden --level 1 +``` + +As far as the arguments go there are only three rules you need to be aware of: +1. `.erdtree.toml` only accepts long-named arguments without the preceding "--". +2. Types are enforced, so numbers are expected to be numbers, booleans are expected to be booleans, strings are expected to be strings, and so on and so forth. +3. `snake_case` and `kebap-case` works. + +#### .erdtreerc + `erdtree` will look for a configuration file in any of the following locations: On Linux/Mac/Unix-like: @@ -333,24 +411,11 @@ The format of a config file is as follows: - Every line is an `erdtree` option/argument. - Lines starting with `#` are considered comments and are thus ignored. -Arguments passed to `erdtree` take precedence. If you have a config that you would like to ignore without deleting you can use `--no-config`. +Arguments passed to `erdtree` on the command-line will override those found in `.erdtreerc`. -Here is an example of a valid configuration file: - -``` -# Long argument ---icons ---human +[Click here](example/.erdtreerc) for an example `.erdtreerc`. -# or short argument --l - -# args can be passed like this --d logical - -# or like this ---unit=si -``` +**If you have a config that you would like to ignore without deleting you can use `--no-config`.** ### Hardlinks diff --git a/example/.erdtree.toml b/example/.erdtree.toml new file mode 100644 index 00000000..a409e1bb --- /dev/null +++ b/example/.erdtree.toml @@ -0,0 +1,26 @@ +icons = true +human = true + +# Compute file sizes like `du` +[du] +disk_usage = "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 + +# How many lines of Rust are in this code base? +[rs] +disk-usage = "word" +level = 1 +pattern = "\\.rs$" diff --git a/example/.erdtreerc b/example/.erdtreerc new file mode 100644 index 00000000..184b15ef --- /dev/null +++ b/example/.erdtreerc @@ -0,0 +1,9 @@ +# Long argument +--icons +--human + +# or short argument +-l + +# args can be passed like this +-d logical diff --git a/src/context/config/mod.rs b/src/context/config/mod.rs new file mode 100644 index 00000000..c28c3a7a --- /dev/null +++ b/src/context/config/mod.rs @@ -0,0 +1,22 @@ +const ERDTREE_CONFIG_TOML: &str = ".erdtree.toml"; +const ERDTREE_TOML_PATH: &str = "ERDTREE_TOML_PATH"; + +const ERDTREE_CONFIG_NAME: &str = ".erdtreerc"; +const ERDTREE_CONFIG_PATH: &str = "ERDTREE_CONFIG_PATH"; + +const ERDTREE_DIR: &str = "erdtree"; + +#[cfg(unix)] +const CONFIG_DIR: &str = ".config"; + +#[cfg(unix)] +const HOME: &str = "HOME"; + +#[cfg(unix)] +const XDG_CONFIG_HOME: &str = "XDG_CONFIG_HOME"; + +/// Concerned with loading `.erdtreerc`. +pub mod rc; + +/// Concerned with loading `.erdtree.toml`. +pub mod toml; diff --git a/src/context/config.rs b/src/context/config/rc.rs similarity index 64% rename from src/context/config.rs rename to src/context/config/rc.rs index 377d48bf..7e230c95 100644 --- a/src/context/config.rs +++ b/src/context/config/rc.rs @@ -1,20 +1,4 @@ -use std::{ - env, fs, - path::{Path, PathBuf}, -}; - -const ERDTREE_CONFIG_NAME: &str = ".erdtreerc"; -const ERDTREE_CONFIG_PATH: &str = "ERDTREE_CONFIG_PATH"; -const ERDTREE_DIR: &str = "erdtree"; - -#[cfg(unix)] -const CONFIG_DIR: &str = ".config"; - -#[cfg(unix)] -const HOME: &str = "HOME"; - -#[cfg(unix)] -const XDG_CONFIG_HOME: &str = "XDG_CONFIG_HOME"; +use std::{env, fs, path::PathBuf}; /// Reads the config file into a `String` if there is one. When `None` is provided then the config /// is looked for in the following locations in order: @@ -25,25 +9,19 @@ const XDG_CONFIG_HOME: &str = "XDG_CONFIG_HOME"; /// - `$HOME/.config/erdtree/.erdtreerc` /// - `$HOME/.erdtreerc` #[cfg(unix)] -pub fn read_config_to_string>(path: Option) -> Option { - path.map(fs::read_to_string) - .and_then(Result::ok) - .or_else(config_from_config_path) +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)) } - -/// Reads the config file into a `String` if there is one. When `None` is provided then the config /// 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>(path: Option) -> Option { - path.map(fs::read_to_string) - .and_then(Result::ok) - .or_else(config_from_config_path) +pub fn read_config_to_string() -> Option { + config_from_config_path() .or_else(config_from_appdata) .map(|e| prepend_arg_prefix(&e)) } @@ -61,13 +39,13 @@ pub fn parse<'a>(config: &'a str) -> Vec<&'a str> { .next() .map_or(true, |ch| ch != '#') }) - .flat_map(str::split_ascii_whitespace) + .flat_map(str::split_whitespace) .collect::>() } /// Try to read in config from `ERDTREE_CONFIG_PATH`. fn config_from_config_path() -> Option { - env::var_os(ERDTREE_CONFIG_PATH) + env::var_os(super::ERDTREE_CONFIG_PATH) .map(PathBuf::from) .map(fs::read_to_string) .and_then(Result::ok) @@ -78,15 +56,15 @@ fn config_from_config_path() -> Option { /// - `$HOME/.erdtreerc` #[cfg(not(windows))] fn config_from_home() -> Option { - let home = env::var_os(HOME).map(PathBuf::from)?; + let home = env::var_os(super::HOME).map(PathBuf::from)?; let config_path = home - .join(CONFIG_DIR) - .join(ERDTREE_DIR) - .join(ERDTREE_CONFIG_NAME); + .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(ERDTREE_CONFIG_NAME); + let config_path = home.join(super::ERDTREE_CONFIG_NAME); fs::read_to_string(config_path).ok() }) } @@ -97,7 +75,9 @@ fn config_from_home() -> Option { fn config_from_appdata() -> Option { let app_data = dirs::config_dir()?; - let config_path = app_data.join(ERDTREE_DIR).join(ERDTREE_CONFIG_NAME); + let config_path = app_data + .join(super::ERDTREE_DIR) + .join(super::ERDTREE_CONFIG_NAME); fs::read_to_string(config_path).ok() } @@ -107,12 +87,14 @@ fn config_from_appdata() -> Option { /// - `$XDG_CONFIG_HOME/.erdtreerc` #[cfg(unix)] fn config_from_xdg_path() -> Option { - let xdg_config = env::var_os(XDG_CONFIG_HOME).map(PathBuf::from)?; + let xdg_config = env::var_os(super::XDG_CONFIG_HOME).map(PathBuf::from)?; - let config_path = xdg_config.join(ERDTREE_DIR).join(ERDTREE_CONFIG_NAME); + 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(ERDTREE_CONFIG_NAME); + let config_path = xdg_config.join(super::ERDTREE_CONFIG_NAME); fs::read_to_string(config_path).ok() }) } diff --git a/src/context/config/toml/error.rs b/src/context/config/toml/error.rs new file mode 100644 index 00000000..3d2e782d --- /dev/null +++ b/src/context/config/toml/error.rs @@ -0,0 +1,19 @@ +use config::ConfigError; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Failed to load .erdtree.toml")] + LoadConfig, + + #[error("The configuration file is improperly formatted")] + 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 new file mode 100644 index 00000000..f27f6dc9 --- /dev/null +++ b/src/context/config/toml/mod.rs @@ -0,0 +1,224 @@ +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 `nested_table` is provided then +/// the top-level table in `.erdtree.toml` is ignored and the configurations specified in the +/// `nested_table` will be used instead. +pub fn parse(config: Config, nested_table: Option<&str>) -> Result, Error> { + let mut args_map = config.cache.into_table()?; + + if let Some(table) = nested_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().ok_or(Error::LoadConfig); + + #[cfg(unix)] + unix::load_toml().ok_or(Error::LoadConfig) +} + +/// Attempts to load in `.erdtree.toml` from `$ERDTREE_TOML_PATH`. Will return `None` for whatever +/// reason. +fn toml_from_env() -> Option { + let config = env::var_os(super::ERDTREE_TOML_PATH) + .map(OsString::into_string) + .and_then(Result::ok)?; + + let file = config.strip_suffix(".toml").map(File::with_name)?; + + Config::builder().add_source(file).build().ok() +} + +/// 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 config::{Config, 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() -> Option { + 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() -> Option { + let config = env::var_os(XDG_CONFIG_HOME).map(PathBuf::from)?; + + 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); + } + + Config::builder().add_source(file?).build().ok() + } + + /// Looks for `.erdtree.toml` in the following locations in order: + /// + /// - `$HOME/.config/erdtree/.erdtree.toml` + /// - `$HOME/.erdtree.toml` + fn toml_from_home() -> Option { + let home = env::var_os(HOME).map(PathBuf::from)?; + + 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); + } + + Config::builder().add_source(file?).build().ok() + } +} + +/// Concerned with how to load `.erdtree.toml` on Windows. +#[cfg(windows)] +mod windows { + use super::super::{ERDTREE_CONFIG_TOML, ERDTREE_DIR}; + use config::{Config, File}; + use std::{env, path::PathBuf}; + + /// Try to read in config from the following location: + /// - `%APPDATA%\erdtree\.erdtree.toml` + pub(super) fn load_toml() -> Option { + 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() -> Option { + let app_data = dirs::config_dir()?; + + let file = app_data + .join(ERDTREE_DIR) + .join(ERDTREE_CONFIG_TOML) + .to_str() + .and_then(|s| s.strip_prefix(".toml")) + .map(File::with_name)?; + + Config::builder().add_source(file).build().ok() + } +} diff --git a/src/context/config/toml/test.rs b/src/context/config/toml/test.rs new file mode 100644 index 00000000..adba50d2 --- /dev/null +++ b/src/context/config/toml/test.rs @@ -0,0 +1,91 @@ +#[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(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/error.rs b/src/context/error.rs index 518133fd..056d9fb5 100644 --- a/src/context/error.rs +++ b/src/context/error.rs @@ -1,14 +1,16 @@ +use super::config::toml::error::Error as TomlError; use clap::Error as ClapError; use ignore::Error as IgnoreError; use regex::Error as RegexError; +use std::convert::From; #[derive(Debug, thiserror::Error)] pub enum Error { #[error("{0}")] - ArgParse(#[source] ClapError), + ArgParse(ClapError), #[error("A configuration file was found but failed to parse: {0}")] - Config(#[source] ClapError), + Config(ClapError), #[error("No glob was provided")] EmptyGlob, @@ -21,4 +23,13 @@ pub enum Error { #[error("Missing '--pattern' argument")] PatternNotProvided, + + #[error("{0}")] + ConfigError(TomlError), +} + +impl From for Error { + fn from(value: TomlError) -> Self { + Self::ConfigError(value) + } } diff --git a/src/context/mod.rs b/src/context/mod.rs index 72765879..3b246930 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -1,6 +1,6 @@ use super::disk_usage::{file_size::DiskUsage, units::PrefixKind}; use crate::tty; -use clap::{parser::ValueSource, ArgMatches, CommandFactory, FromArgMatches, Id, Parser}; +use clap::{parser::ValueSource, ArgMatches, CommandFactory, FromArgMatches, Parser}; use color::Coloring; use error::Error; use ignore::{ @@ -12,7 +12,9 @@ use std::{ borrow::Borrow, convert::From, ffi::{OsStr, OsString}, + num::NonZeroUsize, path::{Path, PathBuf}, + thread::available_parallelism, }; /// Operations to load in defaults from configuration file. @@ -43,10 +45,6 @@ pub mod sort; #[cfg(unix)] pub mod time; -/// Unit tests for [Context] -#[cfg(test)] -mod test; - /// Defines the CLI. #[derive(Parser, Debug)] #[command(name = "erdtree")] @@ -57,6 +55,10 @@ 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, @@ -241,84 +243,79 @@ pub struct Context { pub window_width: Option, } -trait AsVecOfStr { - fn as_vec_of_str(&self) -> Vec<&str>; -} -impl AsVecOfStr for ArgMatches { - fn as_vec_of_str(&self) -> Vec<&str> { - self.ids().map(Id::as_str).collect() - } -} - 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 init() -> Result { + pub fn try_init() -> Result { + // User-provided arguments from command-line. let user_args = Self::command().args_override_self(true).get_matches(); - let no_config = user_args - .get_one::("no_config") - .copied() - .unwrap_or(false); - - if no_config { + // User provides `--no-config`. + if user_args.get_one::("no_config").is_some_and(|b| *b) { return Self::from_arg_matches(&user_args).map_err(Error::ArgParse); } - if let Some(ref config) = config::read_config_to_string::<&str>(None) { - let raw_config_args = config::parse(config); - let config_args = Self::command().get_matches_from(raw_config_args); + // Load in `.erdtreerc` or `.erdtree.toml`. + let config_args = if let Some(config) = config::rc::read_config_to_string() { + let raw_args = config::rc::parse(&config); - // If the user did not provide any arguments just read from config. - if !user_args.args_present() { - return Self::from_arg_matches(&config_args).map_err(Error::Config); - } + Self::command().get_matches_from(raw_args) + } else if let Ok(config) = config::toml::load() { + let named_table = user_args.get_one::("config"); + let raw_args = config::toml::parse(config, named_table.map(String::as_str))?; - // If the user did provide arguments we need to reconcile between config and - // user arguments. - let mut args = vec![OsString::from("--")]; + Self::command().get_matches_from(raw_args) + } else { + return Self::from_arg_matches(&user_args).map_err(Error::ArgParse); + }; - let mut ids = user_args.as_vec_of_str(); + // If the user did not provide any arguments just read from config. + if !user_args.args_present() { + return Self::from_arg_matches(&config_args).map_err(Error::Config); + } - ids.extend(config_args.as_vec_of_str()); + let mut args = vec![OsString::from("--")]; - ids = crate::utils::uniq(ids); + let ids = Self::command() + .get_arguments() + .map(|arg| arg.get_id().clone()) + .collect::>(); - // HACK: - // Going to need to figure out how to handle this better. - for id in ids - .into_iter() - .filter(|&id| id != "Context" && id != "group" && id != "searching") - { - if id == "dir" { - if let Ok(Some(raw)) = user_args.try_get_raw(id) { - let raw_args = raw.map(OsStr::to_owned).collect::>(); + for id in ids { + let id_str = id.as_str(); - args.extend(raw_args); - continue; - } + if id_str == "dir" { + if let Ok(Some(dir)) = user_args.try_get_one::(id_str) { + args.push(dir.as_os_str().to_owned()); + continue; } + } - if let Some(user_arg) = user_args.value_source(id) { - match user_arg { - // prioritize the user arg if user provided a command line argument - ValueSource::CommandLine => Self::pick_args_from(id, &user_args, &mut args), - - // otherwise prioritize argument from the config - _ => Self::pick_args_from(id, &config_args, &mut args), - } - } else { - Self::pick_args_from(id, &config_args, &mut args); + let Some(source) = user_args.value_source(id_str) else { + if let Some(params) = Self::extract_args_from(id_str, &config_args) { + args.extend(params); } - } + continue; + }; + + let higher_precedent = match source { + // User provided argument takes precedent over argument from config + ValueSource::CommandLine => &user_args, - let clargs = Self::command().get_matches_from(args); - return Self::from_arg_matches(&clargs).map_err(Error::Config); + // otherwise prioritize argument from the config + _ => &config_args, + }; + + if let Some(params) = Self::extract_args_from(id_str, higher_precedent) { + args.extend(params); + } } - Self::from_arg_matches(&user_args).map_err(Error::ArgParse) + let clargs = Self::command().get_matches_from(args); + + Self::from_arg_matches(&clargs).map_err(Error::Config) } /// Determines whether or not it's appropriate to display color in output based on @@ -369,20 +366,23 @@ impl Context { } /// Used to pick either from config or user args when constructing [Context]. - fn pick_args_from(id: &str, matches: &ArgMatches, args: &mut Vec) { - if let Ok(Some(raw)) = matches.try_get_raw(id) { - let kebap = id.replace('_', "-"); - - let raw_args = raw - .map(OsStr::to_owned) - .map(|s| vec![OsString::from(format!("--{kebap}")), s]) - .filter(|pair| pair[1] != "false") - .flatten() - .filter(|s| s != "true") - .collect::>(); - - args.extend(raw_args); - } + #[inline] + fn extract_args_from(id: &str, matches: &ArgMatches) -> Option> { + let Ok(Some(raw)) = matches.try_get_raw(id) else { + return None + }; + + let kebap = format!("--{}", id.replace('_', "-")); + + let raw_args = raw + .map(OsStr::to_owned) + .map(|s| [OsString::from(&kebap), s]) + .filter(|[_key, val]| val != "false") + .flatten() + .filter(|s| s != "true") + .collect::>(); + + Some(raw_args) } /// Predicate used for filtering via regular expressions and file-type. When matching regular @@ -564,7 +564,7 @@ impl Context { } /// The default number of threads to use for disk-reads and parallel processing. - const fn num_threads() -> usize { - 3 + fn num_threads() -> usize { + available_parallelism().map(NonZeroUsize::get).unwrap_or(3) } } diff --git a/src/context/test.rs b/src/context/test.rs deleted file mode 100644 index 7aa792ca..00000000 --- a/src/context/test.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::context::sort; -use clap::{CommandFactory, FromArgMatches}; - -use super::{config, Context}; - -const TEST_CONFIG: &str = "./tests/data/.erdtreerc"; - -#[test] -fn config() { - let context = context_from_config().expect("Failed to build Context from config path"); - - let level = context.level(); - - assert_eq!(level, 1, "Failed to properly read 'level' from config"); - - let sort = context.sort; - - assert_eq!( - sort, - sort::Type::Size, - "Failed to properly read 'sort' from config" - ); - - let icons = context.icons; - - assert!(icons, "Failed to properly read 'icons' from config"); -} - -fn context_from_config() -> Option { - config::read_config_to_string(Some(TEST_CONFIG)) - .as_ref() - .and_then(|config| { - let raw_config_args = config::parse(config); - let config_args = Context::command().get_matches_from(raw_config_args); - Context::from_arg_matches(&config_args).ok() - }) -} diff --git a/src/main.rs b/src/main.rs index 8daf7b27..1fb61e53 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,7 +72,7 @@ fn main() -> ExitCode { } fn run() -> Result<(), Box> { - let ctx = Context::init()?; + let ctx = Context::try_init()?; if let Some(shell) = ctx.completions { clap_complete::generate(shell, &mut Context::command(), "erd", &mut stdout()); @@ -83,12 +83,13 @@ fn run() -> Result<(), Box> { let indicator = (ctx.stdout_is_tty && !ctx.no_progress).then(progress::Indicator::measure); - let (tree, ctx) = Tree::try_init_and_update_context(ctx, indicator.as_ref()).map_err(|err| { - if let Some(progress) = &indicator { - progress.mailbox().send(Message::RenderReady).unwrap() - } - err - })?; + let (tree, ctx) = + Tree::try_init_and_update_context(ctx, indicator.as_ref()).map_err(|err| { + if let Some(ref progress) = indicator { + progress.mailbox().send(Message::RenderReady).unwrap(); + } + err + })?; let output = match ctx.layout { layout::Type::Flat => { diff --git a/src/render/grid/cell.rs b/src/render/grid/cell.rs index 7798374f..b0e58493 100644 --- a/src/render/grid/cell.rs +++ b/src/render/grid/cell.rs @@ -278,17 +278,17 @@ impl<'a> Cell<'a> { return write!(f, ""); } - let padding = ctx.max_size_width - + 1 - + match ctx.disk_usage { - DiskUsage::Logical | DiskUsage::Physical => match ctx.unit { - PrefixKind::Si if ctx.human => 2, - PrefixKind::Bin if ctx.human => 3, - PrefixKind::Si => 0, - PrefixKind::Bin => 1, - }, - _ => 0, - }; + let mut padding = ctx.max_size_width + 1; + + match ctx.disk_usage { + DiskUsage::Logical | DiskUsage::Physical => match ctx.unit { + PrefixKind::Si if ctx.human => padding += 2, + PrefixKind::Bin if ctx.human => padding += 3, + PrefixKind::Si => padding += 0, + PrefixKind::Bin => padding += 1, + }, + _ => padding -= 1, + } let formatted_placeholder = format!("{:>padding$}", styles::PLACEHOLDER); diff --git a/src/utils.rs b/src/utils.rs index 13786a7e..f84e8038 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,3 @@ -use std::{cmp::Eq, collections::HashSet, hash::Hash}; - #[macro_export] /// Ruby-like way to crate a hashmap. macro_rules! hash { @@ -21,19 +19,6 @@ macro_rules! hash { }; } -/// Ensure every item in a `Vec` is unique. -#[inline] -pub fn uniq(items: Vec) -> Vec -where - T: Eq + Hash, -{ - items - .into_iter() - .collect::>() - .into_iter() - .collect() -} - /// How many integral digits are there? #[inline] pub const fn num_integral(value: u64) -> usize { diff --git a/tests/data/.erdtreerc b/tests/data/.erdtreerc deleted file mode 100644 index dc8d7f41..00000000 --- a/tests/data/.erdtreerc +++ /dev/null @@ -1,3 +0,0 @@ ---level 1 ---sort size ---icons