diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1898009..0000000 --- a/.travis.yml +++ /dev/null @@ -1,83 +0,0 @@ -# Based on the "trust" template v0.1.2 -# https://github.com/japaric/trust/tree/v0.1.2 - -# ----------- To do a release --------- -# tag a commit and push: -# git tag v0.4.0.1 -# git push origin v0.4.0.1 -# Remember to do a cargo publish to put it in crates.io - -dist: trusty -language: rust -services: docker -sudo: required - -# TODO Rust builds on stable by default, this can be -# overridden on a case by case basis down below. - -env: - global: - # TODO Update this to match the name of your project. - - CRATE_NAME=dust - -matrix: - # TODO These are all the build jobs. Adjust as necessary. Comment out what you - # don't need - include: - # Linux - - env: TARGET=x86_64-unknown-linux-gnu - - # OSX - - env: TARGET=x86_64-apple-darwin - os: osx - -before_install: - - set -e - - rustup self update - -install: - - sh ci/install.sh - - source ~/.cargo/env || true - -script: - - bash ci/script.sh - -after_script: set +e - -before_deploy: - - sh ci/before_deploy.sh - -deploy: - # TODO update `api_key.secure` - # - Create a `public_repo` GitHub token. Go to: https://github.com/settings/tokens/new - # - Encrypt it: `travis encrypt 0123456789012345678901234567890123456789 - # - Paste the output down here -# api_key: -# secure: UlU73Td7Bkb2N88ws4YGLWR+4U0IMgiou9QQtMnmpouJFjeUNxtLSPMPODVXP7zq4sKt5HR5B3fX9MW4mKm351fvnQEoihETn06pKiXGnY//SlTPTt67MX9ZOYmd9ohJReMDOZDgqhnGLxfymycGtsLAmdjDZnAl+IMqgg0FMyVFj9Cl9aKxnn12lxQyX4zabHKk8TUKD3By8ZoEUnJMHt3gEtOmbDgS4brcTPeHCzqnYFw73LEnkqvz+JP0XwauJY7Cf8lminKm/klmjCkQji8T9SHI52v1g0Fxpx0ucp2o3vulQrLHXaHvZ6Fr7J0cSXXzaFF3rrGLt4t4jU/+9TZm1+n5k5XuPW4x4NTCC9NmIj/z0/z41t82E9qZhzhtm2Jdsg6H2tNk+C774TYqcmR6GCvfRadfjRp3cA5dh0UwDVjH2MJFxlHDVkl6la0mVVRsCGF3oBKZVk0BDl1womfnmI46o/uU+gLknHN6Ed6PHHPPYDViWd3VKdmHKT7XrkMMUF6HjZUtla689DWIOWZSiV++1dVPcl/1TV+6tTmN4bBtPcLuX7SHRuLp2PI2kATvRMECsa7gZRypW4jKpVn7b2yetX9TVI3i1zR5zkQJ3dPg8sATvYPL53aKH/WsqUg4rzoAlbk9so+++R4bQY69LhV3B511B7EAynoZFdM - api_key: $API_KEY - file_glob: true - file: $CRATE_NAME-$TRAVIS_TAG-$TARGET.* - on: - # TODO Here you can pick which targets will generate binary releases - # In this example, there are some targets that are tested using the stable - # and nightly channels. This condition makes sure there is only one release - # for such targets and that's generated using the stable channel - condition: $TRAVIS_RUST_VERSION = stable - tags: true - provider: releases - skip_cleanup: true - -cache: cargo -before_cache: - # Travis can't cache files that are not readable by "others" - - chmod -R a+r $HOME/.cargo - -branches: - only: - # release tags - - /^v\d+\.\d+\.\d+.*$/ - - master - -notifications: - email: - on_success: never diff --git a/Cargo.lock b/Cargo.lock index fbcbc8b..e679baa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,7 +26,7 @@ dependencies = [ "environment 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.47 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -46,7 +46,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "backtrace" -version = "0.3.42" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", @@ -188,6 +188,8 @@ dependencies = [ "jwalk 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "terminal_size 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -206,7 +208,7 @@ name = "failure" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "backtrace 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -217,7 +219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", "synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -241,7 +243,7 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -416,10 +418,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "serde_json" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -431,7 +433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "syn" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -446,7 +448,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -463,6 +465,15 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "terminal_size" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -524,7 +535,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum assert_cli 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a29ab7c0ed62970beb0534d637a8688842506d0ff9157de83286dacd065c8149" "checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" "checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" -"checksum backtrace 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b4b1549d804b6c73f4817df2ba073709e96e426f12987127c48e6745568c350b" +"checksum backtrace 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)" = "e4036b9bf40f3cf16aba72a3d65e8a520fc4bafcdc7079aea8f848c58c5b5536" "checksum backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" @@ -545,7 +556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0bc225b78e0391e4b8683440bf2e63c2deeeb2ce5189eab46e2b68c6d3725d08" "checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" "checksum hermit-abi 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772" -"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" +"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" "checksum jwalk 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2b3dbf0a8f61baee43a2918ff50ac6a2d3b2c105bc08ed53bc298779f1263409" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" @@ -569,11 +580,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" "checksum serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" -"checksum serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)" = "48c575e0cc52bdd09b47f330f646cf59afc586e9c4e3ccd6fc1f625b8ea1dad7" +"checksum serde_json 1.0.47 (registry+https://github.com/rust-lang/crates.io-index)" = "15913895b61e0be854afd32fd4163fcd2a3df34142cf2cb961b310ce694cbf90" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" -"checksum syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1e4ff033220a41d1a57d8125eab57bf5263783dfdcc18688b1dacc6ce9651ef8" +"checksum syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "af6f3550d8dff9ef7dc34d384ac6f107e5d31c8f57d9f28e0081503f547ac8f5" "checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +"checksum terminal_size 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "e25a60e3024df9029a414be05f46318a77c22538861a22170077d0388c0e926e" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" diff --git a/Cargo.toml b/Cargo.toml index fa60fbf..cc85647 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ ansi_term = "=0.12" clap = "=2.33" jwalk = "0.4.0" num_cpus = "1.12" +terminal_size = "0.1.10" +unicode-width = "0.1.7" [target.'cfg(windows)'.dependencies] winapi-util = "0.1" @@ -32,3 +34,8 @@ winapi-util = "0.1" [dev-dependencies] assert_cli = "=0.6" tempfile = "=3" + + +[[test]] +name = "integration" +path = "tests/tests.rs" diff --git a/README.md b/README.md index ce7403e..4212dc4 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ du + rust = dust. Like du but more intuitive Dust is meant to give you an instant overview of which directories are using disk space without requiring sort or head. Dust will print a maximum of 1 'Did not have permissions message'. -Dust will list the 20 biggest sub directories or files and will smartly recurse down the tree to find the larger ones. There is no need for a '-d' flag or a '-h' flag. The largest sub directory will have its size shown in *red* +Dust will list the terminal height - 10 biggest sub directories or files and will smartly recurse down the tree to find the larger ones. There is no need for a '-d' flag or a '-h' flag. The largest sub directory will have its size shown in *red* ## Why? @@ -42,26 +42,43 @@ Usage: dust -d 3 (Shows 3 levels of subdirectories) Usage: dust -r (Reverse order of output, with root at the lowest) Usage: dust -x (Only show directories on same filesystem) Usage: dust -X ignore (Ignore all files and directories with the name 'ignore') +Usage: dust -b (Do not show percentages or draw the ASCII bars) ``` ``` -djin:git/dust> dust - 1.2G target - 622M ├─┬ debug - 445M │ ├── deps - 70M │ ├── incremental - 56M │ └── build - 262M ├─┬ rls - 262M │ └─┬ debug - 203M │ ├── deps - 56M │ └── build - 165M ├─┬ package - 165M │ └─┬ du-dust-0.2.4 - 165M │ └─┬ target - 165M │ └─┬ debug - 131M │ └── deps - 165M └─┬ release - 124M └── deps +$ dust target + 15M ┌── build │ ░█ │ 2% + 25M ├── deps │ ░█ │ 4% + 45M ┌─┴ release │ ██ │ 7% + 84M │ ┌── build │ ▒▒▒▒▒████ │ 13% + 7.6M │ │ ┌── libsynstructure-f7552412787ad339.rlib│ ▒▒▒▓▓▓▓▓█ │ 1% + 16M │ │ ├── libfailure_derive-e18365d3e6be2e2c.so│ ▒▒▒▓▓▓▓▓█ │ 2% + 18M │ │ ├── libsyn-9ad95b745845d5dd.rlib │ ▒▒▒▓▓▓▓▓█ │ 3% + 19M │ │ ├── libsyn-d4a3458fcb1c592c.rlib │ ▒▒▒▓▓▓▓▓█ │ 3% + 135M │ ├─┴ deps │ ▒▒▒██████ │ 20% + 228M │ ┌─┴ debug │ █████████ │ 34% + 228M ├─┴ rls │ █████████ │ 34% + 18M │ ┌── dust │ ░░░░░░░░░░░░░░█ │ 3% + 22M │ ├── dust-a0c31c4633c5fc8b │ ░░░░░░░░░░░░░░█ │ 3% + 7.4M │ │ ┌── s-fkrj3vfncf-19aj951-1fv3o6tzvr348 │ ░░░░░░░░░░░░░▒█ │ 1% + 7.4M │ │ ┌─┴ dust-1i3xquz5fns51 │ ░░░░░░░░░░░░░▒█ │ 1% + 40M │ ├─┴ incremental │ ░░░░░░░░░░░░░██ │ 6% + 41M │ ├── build │ ░░░░░░░░░░░░░██ │ 6% + 7.6M │ │ ┌── libsynstructure-f7552412787ad339.rlib │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 1% + 8.2M │ │ ├── libserde-ab4b407a415bc8fc.rmeta │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 1% + 9.4M │ │ ├── libserde-ab4b407a415bc8fc.rlib │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 1% + 11M │ │ ├── tests_symlinks-bf063461b7be6a99 │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 2% + 11M │ │ ├── integration-08f999d253e3b70c │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 2% + 15M │ │ ├── dust-1c6e63725d641738 │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 2% + 16M │ │ ├── libfailure_derive-e18365d3e6be2e2c.so │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 2% + 18M │ │ ├── dust-3a419f62b84d73c1 │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 3% + 18M │ │ ├── dust-2bdf724d4a721d31 │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 3% + 18M │ │ ├── libsyn-9ad95b745845d5dd.rlib │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 3% + 23M │ │ ├── libclap-0dedc35af3ef0670.rlib │ ░░░░▒▒▒▒▒▒▒▒▒▒█ │ 3% + 267M │ ├─┴ deps │ ░░░░███████████ │ 40% + 392M ├─┴ debug │ ███████████████ │ 59% + 667M ┌─┴ target │█████████████████████████ │ 100% + ``` diff --git a/ci/how2publish.txt b/ci/how2publish.txt new file mode 100644 index 0000000..1c7f038 --- /dev/null +++ b/ci/how2publish.txt @@ -0,0 +1,6 @@ +# ----------- To do a release --------- +# tag a commit and push: +# git tag v0.4.5 +# git push origin v0.4.5 + +# cargo publish to put it in crates.io \ No newline at end of file diff --git a/src/display.rs b/src/display.rs index 43ae0ca..977cd7e 100644 --- a/src/display.rs +++ b/src/display.rs @@ -4,34 +4,32 @@ use self::ansi_term::Colour::Fixed; use self::ansi_term::Style; use crate::utils::Node; +use terminal_size::{terminal_size, Height, Width}; + +use unicode_width::UnicodeWidthStr; + +use std::cmp::max; +use std::cmp::min; +use std::iter::repeat; use std::path::Path; static UNITS: [char; 4] = ['T', 'G', 'M', 'K']; +static BLOCKS: [char; 5] = ['█', '▓', '▒', '░', ' ']; +static DEFAULT_TERMINAL_WIDTH: u16 = 80; pub struct DisplayData { pub short_paths: bool, pub is_reversed: bool, pub colors_on: bool, + pub base_size: u64, + pub longest_string_length: usize, } impl DisplayData { - fn get_first_chars(&self) -> &str { - if self.is_reversed { - "─┴" - } else { - "─┬" - } - } - #[allow(clippy::collapsible_if)] - fn get_tree_chars( - &self, - num_siblings: u64, - max_siblings: u64, - has_children: bool, - ) -> &'static str { + fn get_tree_chars(&self, was_i_last: bool, has_children: bool) -> &'static str { if self.is_reversed { - if num_siblings == max_siblings - 1 { + if was_i_last { if has_children { "┌─┴" } else { @@ -43,7 +41,7 @@ impl DisplayData { "├──" } } else { - if num_siblings == 0 { + if was_i_last { if has_children { "└─┬" } else { @@ -57,22 +55,82 @@ impl DisplayData { } } - fn is_biggest(&self, num_siblings: u64, max_siblings: u64) -> bool { + fn is_biggest(&self, num_siblings: usize, max_siblings: u64) -> bool { if self.is_reversed { - num_siblings == 0 + num_siblings == (max_siblings - 1) as usize } else { - num_siblings == max_siblings - 1 + num_siblings == 0 } } - fn get_children_from_node(&self, node: Node) -> impl Iterator { + fn is_last(&self, num_siblings: usize, max_siblings: u64) -> bool { if self.is_reversed { - let n: Vec = node.children.into_iter().rev().map(|a| a).collect(); - n.into_iter() + num_siblings == 0 } else { - node.children.into_iter() + num_siblings == (max_siblings - 1) as usize } } + + fn percent_size(&self, node: &Node) -> f32 { + let result = node.size as f32 / self.base_size as f32; + if result.is_normal() { + result + } else { + 0.0 + } + } +} + +fn get_children_from_node(node: Node, is_reversed: bool) -> impl Iterator { + if is_reversed { + let n: Vec = node.children.into_iter().rev().map(|a| a).collect(); + n.into_iter() + } else { + node.children.into_iter() + } +} + +struct DrawData<'a> { + indent: String, + percent_bar: String, + display_data: &'a DisplayData, +} + +impl DrawData<'_> { + fn get_new_indent(&self, has_children: bool, was_i_last: bool) -> String { + let chars = self.display_data.get_tree_chars(was_i_last, has_children); + self.indent.to_string() + chars + } + + fn generate_bar(&self, node: &Node, level: usize) -> String { + let chars_in_bar = self.percent_bar.chars().count(); + let num_bars = chars_in_bar as f32 * self.display_data.percent_size(node); + let mut num_not_my_bar = (chars_in_bar as i32) - num_bars as i32; + + let mut new_bar = "".to_string(); + let idx = 5 - min(5, max(1, level)); + + for c in self.percent_bar.chars() { + num_not_my_bar -= 1; + if num_not_my_bar <= 0 { + new_bar.push(BLOCKS[0]); + } else if c == BLOCKS[0] { + new_bar.push(BLOCKS[idx]); + } else { + new_bar.push(c); + } + } + new_bar + } +} + +fn get_width_of_terminal() -> u16 { + // Windows CI runners detect a very low terminal width + if let Some((Width(w), Height(_h))) = terminal_size() { + max(w, DEFAULT_TERMINAL_WIDTH) + } else { + DEFAULT_TERMINAL_WIDTH + } } pub fn draw_it( @@ -80,44 +138,91 @@ pub fn draw_it( use_full_path: bool, is_reversed: bool, no_colors: bool, + no_percents: bool, root_node: Node, ) { if !permissions { eprintln!("Did not have permissions for all directories"); } - let display_data = DisplayData { - short_paths: !use_full_path, - is_reversed, - colors_on: !no_colors, + let longest_string_length = root_node + .children + .iter() + .map(|c| find_longest_dir_name(&c, " ", !use_full_path)) + .fold(0, max); + let terminal_width = get_width_of_terminal() - 16; + + let max_bar_length = if no_percents || longest_string_length >= terminal_width as usize { + 0 + } else { + terminal_width as usize - longest_string_length }; - for c in display_data.get_children_from_node(root_node) { - let first_tree_chars = display_data.get_first_chars(); - display_node(c, true, first_tree_chars, &display_data) + // handle usize error also add do not show fancy output option + let bar_text = repeat(BLOCKS[0]).take(max_bar_length).collect::(); + + for c in get_children_from_node(root_node, is_reversed) { + let display_data = DisplayData { + short_paths: !use_full_path, + is_reversed, + colors_on: !no_colors, + base_size: c.size, + longest_string_length, + }; + let draw_data = DrawData { + indent: "".to_string(), + percent_bar: bar_text.clone(), + display_data: &display_data, + }; + display_node(c, &draw_data, true, true); } } -fn display_node(node: Node, is_biggest: bool, indent: &str, display_data: &DisplayData) { - let mut num_siblings = node.children.len() as u64; - let max_sibling = num_siblings; - let new_indent = clean_indentation_string(indent); - let name = node.name.clone(); - let size = node.size; +// We can probably pass depth instead of indent here. +// It is ugly to feed in ' ' instead of the actual tree characters but we don't need them yet. +fn find_longest_dir_name(node: &Node, indent: &str, long_paths: bool) -> usize { + let longest = UnicodeWidthStr::width(&*get_printable_name(&node.name, long_paths, indent)); - if !display_data.is_reversed { - print_this_node(&name, size, is_biggest, display_data, indent); + // each none root tree drawing is 2 chars + let full_indent: String = indent.to_string() + " "; + node.children + .iter() + .map(|c| find_longest_dir_name(c, &*full_indent, long_paths)) + .fold(longest, max) +} + +fn display_node(node: Node, draw_data: &DrawData, is_biggest: bool, is_last: bool) { + let indent2 = draw_data.get_new_indent(!node.children.is_empty(), is_last); + // hacky way of working out how deep we are in the tree + let level = ((indent2.chars().count() - 1) / 2) - 1; + let bar_text = draw_data.generate_bar(&node, level); + + let to_print = format_string( + &node, + &*indent2, + &*bar_text, + is_biggest, + draw_data.display_data, + ); + + if !draw_data.display_data.is_reversed { + println!("{}", to_print) } - for c in display_data.get_children_from_node(node) { - num_siblings -= 1; - let chars = display_data.get_tree_chars(num_siblings, max_sibling, !c.children.is_empty()); - let is_biggest = display_data.is_biggest(num_siblings, max_sibling); - let full_indent = new_indent.clone() + chars; - display_node(c, is_biggest, &*full_indent, display_data) + let dd = DrawData { + indent: clean_indentation_string(&*indent2), + percent_bar: bar_text, + display_data: draw_data.display_data, + }; + let num_siblings = node.children.len() as u64; + + for (count, c) in get_children_from_node(node, draw_data.display_data.is_reversed).enumerate() { + let is_biggest = dd.display_data.is_biggest(count, num_siblings); + let was_i_last = dd.display_data.is_last(count, num_siblings); + display_node(c, &dd, is_biggest, was_i_last); } - if display_data.is_reversed { - print_this_node(&name, size, is_biggest, display_data, indent); + if draw_data.display_data.is_reversed { + println!("{}", to_print) } } @@ -138,30 +243,10 @@ fn clean_indentation_string(s: &str) -> String { is } -fn print_this_node>( - name: P, - size: u64, - is_biggest: bool, - display_data: &DisplayData, - indentation: &str, -) { - let pretty_size = format!("{:>5}", human_readable_number(size),); - println!( - "{}", - format_string(name, is_biggest, display_data, &*pretty_size, indentation) - ) -} - -pub fn format_string>( - dir_name: P, - is_biggest: bool, - display_data: &DisplayData, - size: &str, - indentation: &str, -) -> String { +fn get_printable_name>(dir_name: &P, long_paths: bool, indentation: &str) -> String { let dir_name = dir_name.as_ref(); let printable_name = { - if display_data.short_paths { + if long_paths { match dir_name.parent() { Some(prefix) => match dir_name.strip_prefix(prefix) { Ok(base) => base, @@ -173,15 +258,44 @@ pub fn format_string>( dir_name } }; + format!("{} {}", indentation, printable_name.display()) +} + +pub fn format_string( + node: &Node, + indent: &str, + percent_bar: &str, + is_biggest: bool, + display_data: &DisplayData, +) -> String { + let pretty_size = format!("{:>5}", human_readable_number(node.size)); + + let percent_size = display_data.percent_size(node); + let percent_size_str = format!("{:.0}%", percent_size * 100.0); + + let tree_and_path = get_printable_name(&node.name, display_data.short_paths, &*indent); + + let printable_chars = UnicodeWidthStr::width(&*tree_and_path); + let tree_and_path = tree_and_path + + &(repeat(" ") + .take(display_data.longest_string_length - printable_chars) + .collect::()); + + let percents = if percent_bar != "" { + format!("│{} │ {:>4}", percent_bar, percent_size_str) + } else { + "".into() + }; + format!( - "{} {} {}", + "{} {}{}", if is_biggest && display_data.colors_on { - Fixed(196).paint(size) + Fixed(196).paint(pretty_size) } else { - Style::new().paint(size) + Style::new().paint(pretty_size) }, - indentation, - printable_name.display(), + tree_and_path, + percents, ) } diff --git a/src/main.rs b/src/main.rs index 583fdd8..69d2681 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,19 @@ #[macro_use] extern crate clap; +extern crate unicode_width; use self::display::draw_it; use crate::utils::is_a_parent_of; use clap::{App, AppSettings, Arg}; +use std::cmp::max; use std::path::PathBuf; +use terminal_size::{terminal_size, Height, Width}; use utils::{find_big_ones, get_dir_tree, simplify_dir_names, sort, trim_deep_ones, Node}; mod display; mod utils; -static DEFAULT_NUMBER_OF_LINES: usize = 20; +static DEFAULT_NUMBER_OF_LINES: usize = 30; #[cfg(windows)] fn init_color() { @@ -20,9 +23,21 @@ fn init_color() { #[cfg(not(windows))] fn init_color() {} +fn get_height_of_terminal() -> usize { + // Windows CI runners detect a terminal height of 0 + if let Some((Width(_w), Height(h))) = terminal_size() { + max(h as usize, DEFAULT_NUMBER_OF_LINES) - 10 + } else { + DEFAULT_NUMBER_OF_LINES - 10 + } +} + fn main() { init_color(); - let def_num_str = DEFAULT_NUMBER_OF_LINES.to_string(); + + let default_height = get_height_of_terminal(); + let def_num_str = default_height.to_string(); + let options = App::new("Dust") .about("Like du but more intuitive") .version(crate_version!()) @@ -80,7 +95,7 @@ fn main() { Arg::with_name("reverse") .short("r") .long("reverse") - .help("If applied tree will be printed upside down (biggest lowest)"), + .help("If applied tree will be printed upside down (biggest highest)"), ) .arg( Arg::with_name("no_colors") @@ -88,6 +103,12 @@ fn main() { .long("no_colors") .help("If applied no colors will be printed (normally largest directories are marked in red"), ) + .arg( + Arg::with_name("no_bars") + .short("b") + .long("no_percent_bars") + .help("If applied no percent bars or percents will be displayed"), + ) .arg(Arg::with_name("inputs").multiple(true)) .get_matches(); @@ -102,7 +123,7 @@ fn main() { Ok(v) => v, Err(_) => { eprintln!("Ignoring bad value for number_of_lines"); - DEFAULT_NUMBER_OF_LINES + default_height } }; @@ -131,7 +152,7 @@ fn main() { .map_err(|_| eprintln!("Ignoring bad value for depth")) .ok() }); - if options.is_present("depth") && number_of_lines != DEFAULT_NUMBER_OF_LINES { + if options.is_present("depth") && number_of_lines != default_height { eprintln!("Use either -n or -d. Not both"); return; } @@ -163,8 +184,9 @@ fn main() { draw_it( permissions, options.is_present("display_full_paths"), - options.is_present("reverse"), + !options.is_present("reverse"), options.is_present("no_colors"), + options.is_present("no_bars"), tree, ); } @@ -200,6 +222,3 @@ fn recursively_build_tree(parent_node: &mut Node, new_node: Node, depth: Option< parent_node.children.push(new_node); } } - -#[cfg(test)] -mod tests; diff --git a/src/test_dir3/ラウトは難しいです!.japan b/src/test_dir3/ラウトは難しいです!.japan new file mode 100644 index 0000000..e69de29 diff --git a/src/test_dir3/👩.unicode b/src/test_dir3/👩.unicode new file mode 100644 index 0000000..e69de29 diff --git a/src/tests.rs b/src/tests.rs deleted file mode 100644 index 1efd4cd..0000000 --- a/src/tests.rs +++ /dev/null @@ -1,412 +0,0 @@ -use super::*; - -use crate::display::DisplayData; -use display::format_string; -use std::fs::File; -use std::io::Write; -use std::panic; -use std::path::PathBuf; -use std::process::Command; -use tempfile::Builder; -use tempfile::TempDir; - -// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable -#[cfg_attr(target_os = "windows", ignore)] -#[test] -pub fn test_main() { - assert_cli::Assert::main_binary() - .with_args(&["src/test_dir"]) - .stdout() - .is(main_output(true).as_str()) - .unwrap(); -} - -// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable -#[cfg_attr(target_os = "windows", ignore)] -#[test] -pub fn test_main_long_paths() { - assert_cli::Assert::main_binary() - .with_args(&["-p", "src/test_dir"]) - .stdout() - .is(main_output(false).as_str()) - .unwrap(); -} - -// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable -#[cfg_attr(target_os = "windows", ignore)] -#[test] -pub fn test_main_multi_arg() { - assert_cli::Assert::main_binary() - .with_args(&["src/test_dir/many/", "src/test_dir/", "src/test_dir"]) - .stdout() - .is(main_output(true).as_str()) - .unwrap(); -} - -#[cfg(target_os = "macos")] -fn main_output(short_paths: bool) -> String { - let d = DisplayData { - short_paths, - is_reversed: false, - colors_on: true, - }; - format!( - "{} -{} -{} -{}", - format_string("src/test_dir", true, &d, " 4.0K", "─┬"), - format_string("src/test_dir/many", true, &d, " 4.0K", " └─┬",), - format_string("src/test_dir/many/hello_file", true, &d, " 4.0K", " ├──",), - format_string("src/test_dir/many/a_file", false, &d, " 0B", " └──",), - ) -} - -#[cfg(target_os = "linux")] -fn main_output(short_paths: bool) -> String { - let d = DisplayData { - short_paths, - is_reversed: false, - colors_on: true, - }; - format!( - "{} -{} -{} -{}", - format_string("src/test_dir", true, &d, " 12K", "─┬"), - format_string("src/test_dir/many", true, &d, " 8.0K", " └─┬",), - format_string("src/test_dir/many/hello_file", true, &d, " 4.0K", " ├──",), - format_string("src/test_dir/many/a_file", false, &d, " 0B", " └──",), - ) -} - -#[cfg(target_os = "windows")] -fn main_output(short_paths: bool) -> String { - let d = DisplayData { - short_paths, - is_reversed: false, - colors_on: true, - }; - format!( - "{} -{} -{} -{}", - format_string("src/test_dir", true, &d, " 6B", "─┬"), - format_string("src/test_dir\\many", true, &d, " 6B", " └─┬",), - format_string( - "src/test_dir\\many\\hello_file", - true, - &d, - " 6B", - " ├──", - ), - format_string("src/test_dir\\many\\a_file", false, &d, " 0B", " └──",), - ) -} - -// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable -#[cfg_attr(target_os = "windows", ignore)] -#[test] -pub fn test_no_color_flag() { - assert_cli::Assert::main_binary() - .with_args(&["-c", "src/test_dir/"]) - .stdout() - .is(no_color_flag_output().as_str()) - .unwrap(); -} - -#[cfg(target_os = "macos")] -fn no_color_flag_output() -> String { - " - 4.0K ─┬ test_dir - 4.0K └─┬ many - 4.0K ├── hello_file - 0B └── a_file - " - .to_string() -} - -#[cfg(target_os = "linux")] -fn no_color_flag_output() -> String { - " - 12K ─┬ test_dir - 8.0K └─┬ many - 4.0K ├── hello_file - 0B └── a_file - " - .to_string() -} - -#[cfg(target_os = "windows")] -fn no_color_flag_output() -> String { - " - 6B ─┬ test_dir - 6B └─┬ many - 6B ├── hello_file - 0B └── a_file - " - .to_string() -} - -// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable -#[cfg_attr(target_os = "windows", ignore)] -#[test] -pub fn test_apparent_size() { - let d = DisplayData { - short_paths: true, - is_reversed: false, - colors_on: true, - }; - let r = format!( - "{}", - format_string("src/test_dir/many/hello_file", true, &d, " 6B", " ├──",), - ); - - assert_cli::Assert::main_binary() - .with_args(&["-s", "src/test_dir"]) - .stdout() - .contains(r.as_str()) - .unwrap(); -} - -#[test] -pub fn test_reverse_flag() { - // variable names the same length make the output easier to read - let a = " ┌── a_file"; - let b = " ├── hello_file"; - let c = " ┌─┴ many"; - let d = " ─┴ test_dir"; - - assert_cli::Assert::main_binary() - .with_args(&["-r", "src/test_dir"]) - .stdout() - .contains(a) - .stdout() - .contains(b) - .stdout() - .contains(c) - .stdout() - .contains(d) - .unwrap(); -} - -#[test] -pub fn test_d_flag_works() { - // We should see the top level directory but not the sub dirs / files: - assert_cli::Assert::main_binary() - .with_args(&["-d", "1", "-s", "src/test_dir"]) - .stdout() - .doesnt_contain("hello_file") - .unwrap(); -} - -fn build_temp_file(dir: &TempDir) -> PathBuf { - let file_path = dir.path().join("notes.txt"); - let mut file = File::create(&file_path).unwrap(); - writeln!(file, "I am a temp file").unwrap(); - file_path -} - -// fix! [rivy; 2020-01-22] possible on "windows"?; `ln` is not usually an available command; creation of symbolic links requires special enhanced permissions -// ... ref: @@ -#[cfg_attr(target_os = "windows", ignore)] -#[test] -pub fn test_soft_sym_link() { - let dir = Builder::new().tempdir().unwrap(); - let file = build_temp_file(&dir); - let dir_s = dir.path().to_str().unwrap(); - let file_path_s = file.to_str().unwrap(); - - let link_name = dir.path().join("the_link"); - let link_name_s = link_name.to_str().unwrap(); - let c = Command::new("ln") - .arg("-s") - .arg(file_path_s) - .arg(link_name_s) - .output(); - assert!(c.is_ok()); - - let a = format!(" ─┬ {}", dir_s); - let b = format!(" ├── {}", file_path_s); - let c = format!(" └── {}", link_name_s); - - assert_cli::Assert::main_binary() - .with_args(&["-p", &dir_s]) - .stdout() - .contains(a.as_str()) - .stdout() - .contains(b.as_str()) - .stdout() - .contains(c.as_str()) - .unwrap(); -} - -// Hard links are ignored as the inode is the same as the file -// fix! [rivy; 2020-01-22] may fail on "usual" windows hosts as `ln` is not usually an available command -#[test] -pub fn test_hard_sym_link() { - let dir = Builder::new().tempdir().unwrap(); - let file = build_temp_file(&dir); - let dir_s = dir.path().to_str().unwrap(); - let file_path_s = file.to_str().unwrap(); - - let link_name = dir.path().join("the_link"); - let link_name_s = link_name.to_str().unwrap(); - let c = Command::new("ln") - .arg(file_path_s) - .arg(link_name_s) - .output(); - assert!(c.is_ok()); - - let a = format!(" ─┬ {}", dir_s); - let b = format!(" └── {}", link_name_s); - let b2 = format!(" └── {}", file_path_s); - - // Because this is a hard link the file and hard link look identical. Therefore - // we cannot guarantee which version will appear first. - let result = panic::catch_unwind(|| { - assert_cli::Assert::main_binary() - .with_args(&["-p", dir_s]) - .stdout() - .contains(a.as_str()) - .stdout() - .contains(b.as_str()) - .unwrap(); - }); - if result.is_err() { - assert_cli::Assert::main_binary() - .with_args(&["-p", dir_s]) - .stdout() - .contains(a.as_str()) - .stdout() - .contains(b2.as_str()) - .unwrap(); - } -} - -// Check we don't recurse down an infinite symlink tree -// fix! [rivy; 2020-01-22] possible on "windows"?; `ln` is not usually an available command; creation of symbolic links requires special enhanced permissions -// ... ref: @@ -#[cfg_attr(target_os = "windows", ignore)] -#[test] -pub fn test_recursive_sym_link() { - let dir = Builder::new().tempdir().unwrap(); - let dir_s = dir.path().to_str().unwrap(); - - let link_name = dir.path().join("the_link"); - let link_name_s = link_name.to_str().unwrap(); - - let c = Command::new("ln") - .arg("-s") - .arg(dir_s) - .arg(link_name_s) - .output(); - assert!(c.is_ok()); - - let a = format!(" ─┬ {}", dir_s); - let b = format!(" └── {}", link_name_s); - - assert_cli::Assert::main_binary() - .with_args(&["-p", dir_s]) - .stdout() - .contains(a.as_str()) - .stdout() - .contains(b.as_str()) - .unwrap(); -} - -// Check against directories and files whos names are substrings of each other -// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable -#[cfg_attr(target_os = "windows", ignore)] -#[test] -pub fn test_substring_of_names() { - assert_cli::Assert::main_binary() - .with_args(&["-c", "src/test_dir2"]) - .stdout() - .is(no_substring_of_names_output().as_str()) - .unwrap(); -} - -#[cfg(target_os = "linux")] -fn no_substring_of_names_output() -> String { - " - 24K ─┬ test_dir2 - 8.0K ├─┬ dir - 4.0K │ └── hello - 8.0K ├─┬ dir_substring - 4.0K │ └── hello - 4.0K └── dir_name_clash - " - .into() -} - -#[cfg(target_os = "macos")] -fn no_substring_of_names_output() -> String { - " - 12K ─┬ test_dir2 - 4.0K ├─┬ dir - 4.0K │ └── hello - 4.0K ├── dir_name_clash - 4.0K └─┬ dir_substring - 4.0K └── hello - " - .into() -} - -#[cfg(target_os = "windows")] -fn no_substring_of_names_output() -> String { - " - 16B ─┬ test_dir2 - 6B ├─┬ dir_substring - 6B │ └── hello - 5B ├─┬ dir - 5B │ └── hello - 5B └── dir_name_clash - " - .into() -} - -// Check against directories and files whos names are substrings of each other -#[test] -pub fn test_ignore_dir() { - assert_cli::Assert::main_binary() - .with_args(&["-c", "-X", "dir_substring", "src/test_dir2"]) - .stdout() - .is(ignore_dir_output().as_str()) - .unwrap(); -} - -#[cfg(target_os = "linux")] -fn ignore_dir_output() -> String { - " - 16K ─┬ test_dir2 - 8.0K ├─┬ dir - 4.0K │ └── hello - 4.0K └── dir_name_clash - " - .into() -} - -#[cfg(target_os = "macos")] -fn ignore_dir_output() -> String { - " - 8.0K ─┬ test_dir2 - 4.0K ├─┬ dir - 4.0K │ └── hello - 4.0K └── dir_name_clash - " - .into() -} - -#[cfg(target_os = "windows")] -fn ignore_dir_output() -> String { - " - 10B ─┬ test_dir2 - 5B ├─┬ dir - 5B │ └── hello - 5B └── dir_name_clash - " - .into() -} diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..4087fce --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,259 @@ +mod tests_symlinks; + +// File sizes differ on both platform and on the format of the disk. +// We can at least test the file names are there +#[test] +pub fn test_basic_output() { + assert_cli::Assert::main_binary() + .with_args(&["src/test_dir/"]) + .stdout() + .contains(" ┌─┴ test_dir ") + .stdout() + .contains(" ┌─┴ many ") + .stdout() + .contains(" ├── hello_file") + .stdout() + .contains(" ┌── a_file ") + .unwrap(); +} + +// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable +#[cfg_attr(target_os = "windows", ignore)] +#[test] +pub fn test_main_basic() { + // -c is no color mode - This makes testing much simpler + assert_cli::Assert::main_binary() + .with_args(&["-c", "src/test_dir"]) + .stdout() + .is(main_output().as_str()) + .unwrap(); +} + +// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable +#[cfg_attr(target_os = "windows", ignore)] +#[test] +pub fn test_main_multi_arg() { + assert_cli::Assert::main_binary() + .with_args(&["-c", "src/test_dir/many/", "src/test_dir/", "src/test_dir"]) + .stdout() + .is(main_output().as_str()) + .unwrap(); +} + +#[cfg(target_os = "macos")] +fn main_output() -> String { + r#" + 0B ┌── a_file │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█ │ 0% + 4.0K ├── hello_file│██████████████████████████████████████████████ │ 100% + 4.0K ┌─┴ many │██████████████████████████████████████████████ │ 100% + 4.0K ┌─┴ test_dir │██████████████████████████████████████████████ │ 100% + "# + .to_string() +} + +#[cfg(target_os = "linux")] +fn main_output() -> String { + r#" + 0B ┌── a_file │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█ │ 0% + 4.0K ├── hello_file│ ░░░░░░░░░░░░░░░████████████████ │ 33% + 8.0K ┌─┴ many │ ███████████████████████████████ │ 67% + 12K ┌─┴ test_dir │██████████████████████████████████████████████ │ 100% + "# + .to_string() +} + +#[cfg(target_os = "windows")] +fn main_output() -> String { + "PRs welcome".to_string() +} + +// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable +#[cfg_attr(target_os = "windows", ignore)] +#[test] +pub fn test_main_long_paths() { + assert_cli::Assert::main_binary() + .with_args(&["-c", "-p", "src/test_dir"]) + .stdout() + .is(main_output_long_paths().as_str()) + .unwrap(); +} + +#[cfg(target_os = "macos")] +fn main_output_long_paths() -> String { + r#" + 0B ┌── src/test_dir/many/a_file │░░░░░░░░░░░░░░░░░░░░░░░░░░░█ │ 0% + 4.0K ├── src/test_dir/many/hello_file│████████████████████████████ │ 100% + 4.0K ┌─┴ src/test_dir/many │████████████████████████████ │ 100% + 4.0K ┌─┴ src/test_dir │████████████████████████████ │ 100% + "# + .to_string() +} + +#[cfg(target_os = "linux")] +fn main_output_long_paths() -> String { + r#" + 0B ┌── src/test_dir/many/a_file │ ░░░░░░░░░░░░░░░░░░█ │ 0% + 4.0K ├── src/test_dir/many/hello_file│ ░░░░░░░░░██████████ │ 33% + 8.0K ┌─┴ src/test_dir/many │ ███████████████████ │ 67% + 12K ┌─┴ src/test_dir │████████████████████████████ │ 100% + "# + .to_string() +} + +#[cfg(target_os = "windows")] +fn main_output_long_paths() -> String { + "PRs welcome".to_string() +} + +// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable +#[cfg_attr(target_os = "windows", ignore)] +#[test] +pub fn test_apparent_size() { + assert_cli::Assert::main_binary() + .with_args(&["-c", "-s", "src/test_dir"]) + .stdout() + .is(output_apparent_size().as_str()) + .unwrap(); +} + +#[cfg(target_os = "linux")] +fn output_apparent_size() -> String { + r#" + 0B ┌── a_file │ ░░░░░░░░░░░░░░░░░░░░░░░█ │ 0% + 6B ├── hello_file│ ░░░░░░░░░░░░░░░░░░░░░░░█ │ 0% + 4.0K ┌─┴ many │ ████████████████████████ │ 50% + 8.0K ┌─┴ test_dir │██████████████████████████████████████████████ │ 100% + "# + .to_string() +} + +#[cfg(target_os = "macos")] +fn output_apparent_size() -> String { + r#" + 0B ┌── a_file │ ░░░░░░░░░░░░░░░░░░░░░░░░░░█ │ 0% + 6B ├── hello_file│ ░░░░░░░░░░░░░░░░░░░░░░░░░██ │ 3% + 134B ┌─┴ many │ ███████████████████████████ │ 58% + 230B ┌─┴ test_dir │██████████████████████████████████████████████ │ 100% + "# + .to_string() +} + +#[cfg(target_os = "windows")] +fn output_apparent_size() -> String { + "".to_string() +} + +#[test] +pub fn test_reverse_flag() { + assert_cli::Assert::main_binary() + .with_args(&["-c", "-r", "src/test_dir/"]) + .stdout() + .contains(" └─┬ test_dir ") + .stdout() + .contains(" └─┬ many ") + .stdout() + .contains(" ├── hello_file") + .stdout() + .contains(" └── a_file ") + .unwrap(); +} + +#[test] +pub fn test_d_flag_works() { + // We should see the top level directory but not the sub dirs / files: + assert_cli::Assert::main_binary() + .with_args(&["-d", "1", "-s", "src/test_dir"]) + .stdout() + .doesnt_contain("hello_file") + .unwrap(); +} + +// Check against directories and files whos names are substrings of each other +// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable +#[cfg_attr(target_os = "windows", ignore)] +#[test] +pub fn test_substring_of_names() { + assert_cli::Assert::main_binary() + .with_args(&["-c", "src/test_dir2"]) + .stdout() + .is(no_substring_of_names_output().as_str()) + .unwrap(); +} + +#[cfg(target_os = "linux")] +fn no_substring_of_names_output() -> String { + " + 4.0K ┌── dir_name_clash│ ████████ │ 17% + 4.0K │ ┌── hello │ ░░░░░░░████████ │ 17% + 8.0K ├─┴ dir_substring │ ███████████████ │ 33% + 4.0K │ ┌── hello │ ░░░░░░░████████ │ 17% + 8.0K ├─┴ dir │ ███████████████ │ 33% + 24K ┌─┴ test_dir2 │████████████████████████████████████████████ │ 100% + " + .into() +} + +#[cfg(target_os = "macos")] +fn no_substring_of_names_output() -> String { + " + 4.0K ┌── hello │ ███████████████ │ 33% + 4.0K ┌─┴ dir_substring │ ███████████████ │ 33% + 4.0K ├── dir_name_clash│ ███████████████ │ 33% + 4.0K │ ┌── hello │ ███████████████ │ 33% + 4.0K ├─┴ dir │ ███████████████ │ 33% + 12K ┌─┴ test_dir2 │████████████████████████████████████████████ │ 100% + " + .into() +} + +#[cfg(target_os = "windows")] +fn no_substring_of_names_output() -> String { + "PRs".into() +} + +// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable +#[cfg_attr(target_os = "windows", ignore)] +#[test] +pub fn test_unicode_directories() { + assert_cli::Assert::main_binary() + .with_args(&["-c", "src/test_dir3"]) + .stdout() + .is(unicode_dir().as_str()) + .unwrap(); +} + +#[cfg(target_os = "linux")] +fn unicode_dir() -> String { + // The way unicode & asian characters are rendered on the terminal should make this line up + " + 0B ┌── 👩.unicode │ █ │ 0% + 0B ├── ラウトは難しいです!.japan│ █ │ 0% + 4.0K ┌─┴ test_dir3 │████████████████████████████████ │ 100% + " + .into() +} + +#[cfg(target_os = "macos")] +fn unicode_dir() -> String { + " + 0B ┌── 👩.unicode │ █ │ 0% + 0B ├── ラウトは難しいです!.japan│ █ │ 0% + 0B ┌─┴ test_dir3 │ █ │ 0% + " + .into() +} + +#[cfg(target_os = "windows")] +fn unicode_dir() -> String { + "".into() +} + +// Check against directories and files whos names are substrings of each other +#[test] +pub fn test_ignore_dir() { + assert_cli::Assert::main_binary() + .with_args(&["-c", "-X", "dir_substring", "src/test_dir2"]) + .stdout() + .doesnt_contain("dir_substring") + .unwrap(); +} diff --git a/tests/tests_symlinks.rs b/tests/tests_symlinks.rs new file mode 100644 index 0000000..6733323 --- /dev/null +++ b/tests/tests_symlinks.rs @@ -0,0 +1,119 @@ +use std::fs::File; +use std::io::Write; +use std::panic; +use std::path::PathBuf; +use std::process::Command; +use tempfile::Builder; +use tempfile::TempDir; + +// File sizes differ on both platform and on the format of the disk. +// Windows: `ln` is not usually an available command; creation of symbolic links requires special enhanced permissions + +fn build_temp_file(dir: &TempDir) -> PathBuf { + let file_path = dir.path().join("notes.txt"); + let mut file = File::create(&file_path).unwrap(); + writeln!(file, "I am a temp file").unwrap(); + file_path +} + +#[cfg_attr(target_os = "windows", ignore)] +#[test] +pub fn test_soft_sym_link() { + let dir = Builder::new().tempdir().unwrap(); + let file = build_temp_file(&dir); + let dir_s = dir.path().to_str().unwrap(); + let file_path_s = file.to_str().unwrap(); + + let link_name = dir.path().join("the_link"); + let link_name_s = link_name.to_str().unwrap(); + let c = Command::new("ln") + .arg("-s") + .arg(file_path_s) + .arg(link_name_s) + .output(); + assert!(c.is_ok()); + + let c = format!(" ┌── {}", link_name_s); + let b = format!(" ├── {}", file_path_s); + let a = format!("─┴ {}", dir_s); + + assert_cli::Assert::main_binary() + .with_args(&["-p", &dir_s]) + .stdout() + .contains(a.as_str()) + .stdout() + .contains(b.as_str()) + .stdout() + .contains(c.as_str()) + .unwrap(); +} + +#[cfg_attr(target_os = "windows", ignore)] +#[test] +pub fn test_hard_sym_link() { + let dir = Builder::new().tempdir().unwrap(); + let file = build_temp_file(&dir); + let dir_s = dir.path().to_str().unwrap(); + let file_path_s = file.to_str().unwrap(); + + let link_name = dir.path().join("the_link"); + let link_name_s = link_name.to_str().unwrap(); + let c = Command::new("ln") + .arg(file_path_s) + .arg(link_name_s) + .output(); + assert!(c.is_ok()); + + let a = format!("─┴ {}", dir_s); + let b = format!(" ┌── {}", link_name_s); + let b2 = format!(" ┌── {}", file_path_s); + + // Because this is a hard link the file and hard link look identical. Therefore + // we cannot guarantee which version will appear first. + let result = panic::catch_unwind(|| { + assert_cli::Assert::main_binary() + .with_args(&["-p", dir_s]) + .stdout() + .contains(a.as_str()) + .stdout() + .contains(b.as_str()) + .unwrap(); + }); + if result.is_err() { + assert_cli::Assert::main_binary() + .with_args(&["-p", dir_s]) + .stdout() + .contains(a.as_str()) + .stdout() + .contains(b2.as_str()) + .unwrap(); + } +} + +#[cfg_attr(target_os = "windows", ignore)] +#[test] +pub fn test_recursive_sym_link() { + let dir = Builder::new().tempdir().unwrap(); + let dir_s = dir.path().to_str().unwrap(); + + let link_name = dir.path().join("the_link"); + let link_name_s = link_name.to_str().unwrap(); + + let c = Command::new("ln") + .arg("-s") + .arg(dir_s) + .arg(link_name_s) + .output(); + assert!(c.is_ok()); + + let a = format!("─┬ {}", dir_s); + let b = format!(" └── {}", link_name_s); + + assert_cli::Assert::main_binary() + .with_args(&["-r", "-p", dir_s]) + .stdout() + .contains(a.as_str()) + .stdout() + .contains(b.as_str()) + .unwrap(); +}