du-dust-1.2.4/.cargo_vcs_info.json0000644000000001360000000000100124250ustar { "git": { "sha1": "3a16a6a234321d40d2c2423136911db8ec8b07c2" }, "path_in_vcs": "" }du-dust-1.2.4/.github/workflows/CICD.yml000064400000000000000000000422421046102023000160430ustar 00000000000000name: CICD # spell-checker:ignore CICD CODECOV MSVC MacOS Peltoche SHAs buildable clippy esac fakeroot gnueabihf halium libssl mkdir musl popd printf pushd rustfmt softprops toolchain env: PROJECT_NAME: dust PROJECT_DESC: "du + rust = dust" PROJECT_AUTH: "bootandy" RUST_MIN_SRV: "1.31.0" on: [push, pull_request] jobs: style: name: Style runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: job: - { os: ubuntu-latest } - { os: macos-latest } - { os: windows-latest } steps: - uses: actions/checkout@v1 - name: Initialize workflow variables id: vars shell: bash run: | # 'windows-latest' `cargo fmt` is bugged for this project (see reasons @ GH:rust-lang/rustfmt #3324, #3590, #3688 ; waiting for repair) JOB_DO_FORMAT_TESTING="true" case ${{ matrix.job.os }} in windows-latest) unset JOB_DO_FORMAT_TESTING ;; esac; echo set-output name=JOB_DO_FORMAT_TESTING::${JOB_DO_FORMAT_TESTING:-/false} echo ::set-output name=JOB_DO_FORMAT_TESTING::${JOB_DO_FORMAT_TESTING} # target-specific options # * CARGO_FEATURES_OPTION CARGO_FEATURES_OPTION='' ; if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features "${{ matrix.job.features }}"' ; fi echo set-output name=CARGO_FEATURES_OPTION::${CARGO_FEATURES_OPTION} echo ::set-output name=CARGO_FEATURES_OPTION::${CARGO_FEATURES_OPTION} - name: Install `rust` toolchain uses: actions-rs/toolchain@v1 with: toolchain: stable override: true profile: minimal # minimal component installation (ie, no documentation) components: rustfmt, clippy - name: Install wget for Windows if: matrix.job.os == 'windows-latest' run: choco install wget --no-progress - name: typos-action uses: crate-ci/typos@v1.28.4 - name: "`fmt` testing" if: steps.vars.outputs.JOB_DO_FORMAT_TESTING uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check - name: "`clippy` testing" if: success() || failure() # run regardless of prior step ("`fmt` testing") success/failure uses: actions-rs/cargo@v1 with: command: clippy args: ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} -- -D warnings min_version: name: MinSRV # Minimum supported rust version runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Install `rust` toolchain (v${{ env.RUST_MIN_SRV }}) uses: actions-rs/toolchain@v1 with: toolchain: ${{ env.RUST_MIN_SRV }} profile: minimal # minimal component installation (ie, no documentation) - name: Test uses: actions-rs/cargo@v1 with: command: test build: name: Build runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: job: # { os, target, cargo-options, features, use-cross, toolchain } - { os: ubuntu-latest, target: aarch64-unknown-linux-gnu, use-cross: use-cross, } - { os: ubuntu-latest, target: aarch64-unknown-linux-musl, use-cross: use-cross, } - { os: ubuntu-latest, target: arm-unknown-linux-gnueabihf, use-cross: use-cross, } - { os: ubuntu-latest, target: arm-unknown-linux-musleabi, use-cross: use-cross, } - { os: ubuntu-latest, target: i686-unknown-linux-gnu, use-cross: use-cross, } - { os: ubuntu-latest, target: i686-unknown-linux-musl, use-cross: use-cross, } - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu, use-cross: use-cross, } - { os: ubuntu-latest, target: x86_64-unknown-linux-musl, use-cross: use-cross, } - { os: macos-latest, target: x86_64-apple-darwin } - { os: windows-latest, target: i686-pc-windows-gnu } - { os: windows-latest, target: i686-pc-windows-msvc } - { os: windows-latest, target: x86_64-pc-windows-gnu } ## !maint: [rivy; 2020-01-21] may break due to rust bug; follow possible solution from GH:rust-lang/rust#47048 (refs: GH:rust-lang/rust#47048 , GH:rust-lang/rust#53454 , GH:bike-barn/hermit#172 ) - { os: windows-latest, target: x86_64-pc-windows-msvc } steps: - uses: actions/checkout@v1 - name: Install any prerequisites shell: bash run: | case ${{ matrix.job.target }} in arm-unknown-linux-gnueabihf) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install binutils-aarch64-linux-gnu ;; esac - name: Initialize workflow variables id: vars shell: bash run: | # toolchain TOOLCHAIN="stable" ## default to "stable" toolchain # * specify alternate TOOLCHAIN for *-pc-windows-gnu targets; gnu targets on Windows are broken for the standard *-pc-windows-msvc toolchain (refs: , , ) case ${{ matrix.job.target }} in *-pc-windows-gnu) TOOLCHAIN="stable-${{ matrix.job.target }}" ;; esac; # * use requested TOOLCHAIN if specified if [ -n "${{ matrix.job.toolchain }}" ]; then TOOLCHAIN="${{ matrix.job.toolchain }}" ; fi echo set-output name=TOOLCHAIN::${TOOLCHAIN} echo ::set-output name=TOOLCHAIN::${TOOLCHAIN} # staging directory STAGING='_staging' echo set-output name=STAGING::${STAGING} echo ::set-output name=STAGING::${STAGING} # determine EXE suffix EXE_suffix="" ; case ${{ matrix.job.target }} in *-pc-windows-*) EXE_suffix=".exe" ;; esac; echo set-output name=EXE_suffix::${EXE_suffix} echo ::set-output name=EXE_suffix::${EXE_suffix} # parse commit reference info REF_NAME=${GITHUB_REF#refs/*/} unset REF_BRANCH ; case ${GITHUB_REF} in refs/heads/*) REF_BRANCH=${GITHUB_REF#refs/heads/} ;; esac; unset REF_TAG ; case ${GITHUB_REF} in refs/tags/*) REF_TAG=${GITHUB_REF#refs/tags/} ;; esac; REF_SHAS=${GITHUB_SHA:0:8} echo set-output name=REF_NAME::${REF_NAME} echo set-output name=REF_BRANCH::${REF_BRANCH} echo set-output name=REF_TAG::${REF_TAG} echo set-output name=REF_SHAS::${REF_SHAS} echo ::set-output name=REF_NAME::${REF_NAME} echo ::set-output name=REF_BRANCH::${REF_BRANCH} echo ::set-output name=REF_TAG::${REF_TAG} echo ::set-output name=REF_SHAS::${REF_SHAS} # parse target unset TARGET_ARCH ; case ${{ matrix.job.target }} in arm-unknown-linux-gnueabihf) TARGET_ARCH=arm ;; aarch-*) TARGET_ARCH=aarch64 ;; i686-*) TARGET_ARCH=i686 ;; x86_64-*) TARGET_ARCH=x86_64 ;; esac; echo set-output name=TARGET_ARCH::${TARGET_ARCH} echo ::set-output name=TARGET_ARCH::${TARGET_ARCH} unset TARGET_OS ; case ${{ matrix.job.target }} in *-linux-*) TARGET_OS=linux ;; *-apple-*) TARGET_OS=macos ;; *-windows-*) TARGET_OS=windows ;; esac; echo set-output name=TARGET_OS::${TARGET_OS} echo ::set-output name=TARGET_OS::${TARGET_OS} # package name PKG_suffix=".tar.gz" ; case ${{ matrix.job.target }} in *-pc-windows-*) PKG_suffix=".zip" ;; esac; PKG_BASENAME=${PROJECT_NAME}-${REF_TAG:-$REF_SHAS}-${{ matrix.job.target }} PKG_NAME=${PKG_BASENAME}${PKG_suffix} echo set-output name=PKG_suffix::${PKG_suffix} echo set-output name=PKG_BASENAME::${PKG_BASENAME} echo set-output name=PKG_NAME::${PKG_NAME} echo ::set-output name=PKG_suffix::${PKG_suffix} echo ::set-output name=PKG_BASENAME::${PKG_BASENAME} echo ::set-output name=PKG_NAME::${PKG_NAME} # deployable tag? (ie, leading "vM" or "M"; M == version number) unset DEPLOY ; if [[ $REF_TAG =~ ^[vV]?[0-9].* ]]; then DEPLOY='true' ; fi echo set-output name=DEPLOY::${DEPLOY:-/false} echo ::set-output name=DEPLOY::${DEPLOY} # target-specific options # * CARGO_FEATURES_OPTION CARGO_FEATURES_OPTION='' ; if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features "${{ matrix.job.features }}"' ; fi echo set-output name=CARGO_FEATURES_OPTION::${CARGO_FEATURES_OPTION} echo ::set-output name=CARGO_FEATURES_OPTION::${CARGO_FEATURES_OPTION} # * CARGO_USE_CROSS (truthy) CARGO_USE_CROSS='true' ; case '${{ matrix.job.use-cross }}' in ''|0|f|false|n|no) unset CARGO_USE_CROSS ;; esac; echo set-output name=CARGO_USE_CROSS::${CARGO_USE_CROSS:-/false} echo ::set-output name=CARGO_USE_CROSS::${CARGO_USE_CROSS} # # * `arm` cannot be tested on ubuntu-* hosts (b/c testing is currently primarily done via comparison of target outputs with built-in outputs and the `arm` target is not executable on the host) JOB_DO_TESTING="true" case ${{ matrix.job.target }} in arm-*|aarch64-*) unset JOB_DO_TESTING ;; esac; echo set-output name=JOB_DO_TESTING::${JOB_DO_TESTING:-/false} echo ::set-output name=JOB_DO_TESTING::${JOB_DO_TESTING} # # * test only binary for arm-type targets unset CARGO_TEST_OPTIONS unset CARGO_TEST_OPTIONS ; case ${{ matrix.job.target }} in arm-*|aarch64-*) CARGO_TEST_OPTIONS="--bin ${PROJECT_NAME}" ;; esac; echo set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS} echo ::set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS} # * strip executable? STRIP="strip" ; case ${{ matrix.job.target }} in arm-unknown-linux-gnueabihf) STRIP="arm-linux-gnueabihf-strip" ;; *-pc-windows-msvc) STRIP="" ;; aarch64-unknown-linux-gnu) STRIP="aarch64-linux-gnu-strip" ;; aarch64-unknown-linux-musl) STRIP="" ;; armv7-unknown-linux-musleabi) STRIP="" ;; arm-unknown-linux-musleabi) STRIP="" ;; esac; echo set-output name=STRIP::${STRIP} echo ::set-output name=STRIP::${STRIP} - name: Create all needed build/work directories shell: bash run: | mkdir -p '${{ steps.vars.outputs.STAGING }}' mkdir -p '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}' - name: rust toolchain ~ install uses: actions-rs/toolchain@v1 with: toolchain: ${{ steps.vars.outputs.TOOLCHAIN }} target: ${{ matrix.job.target }} override: true profile: minimal # minimal component installation (ie, no documentation) - name: Info shell: bash run: | gcc --version || true rustup -V rustup toolchain list rustup default cargo -V rustc -V - name: Build uses: actions-rs/cargo@v1 with: use-cross: ${{ steps.vars.outputs.CARGO_USE_CROSS }} command: build args: --release --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} - name: Install cargo-deb uses: actions-rs/cargo@v1 with: command: install args: cargo-deb if: matrix.job.target == 'i686-unknown-linux-musl' || matrix.job.target == 'x86_64-unknown-linux-musl' - name: Build deb uses: actions-rs/cargo@v1 with: command: deb args: --no-build --target=${{ matrix.job.target }} if: matrix.job.target == 'i686-unknown-linux-musl' || matrix.job.target == 'x86_64-unknown-linux-musl' - name: Test uses: actions-rs/cargo@v1 with: use-cross: ${{ steps.vars.outputs.CARGO_USE_CROSS }} command: test args: --target=${{ matrix.job.target }} ${{ steps.vars.outputs.CARGO_TEST_OPTIONS}} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} - name: Archive executable artifacts uses: actions/upload-artifact@master with: name: ${{ env.PROJECT_NAME }}-${{ matrix.job.target }} path: target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }} - name: Archive deb artifacts uses: actions/upload-artifact@master with: name: ${{ env.PROJECT_NAME }}-${{ matrix.job.target }}.deb path: target/${{ matrix.job.target }}/debian if: matrix.job.target == 'i686-unknown-linux-musl' || matrix.job.target == 'x86_64-unknown-linux-musl' - name: Package shell: bash run: | # binary cp 'target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }}' '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/' # `strip` binary (if needed) if [ -n "${{ steps.vars.outputs.STRIP }}" ]; then "${{ steps.vars.outputs.STRIP }}" '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }}' ; fi # README and LICENSE cp README.md '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/' cp LICENSE '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/' # base compressed package pushd '${{ steps.vars.outputs.STAGING }}/' >/dev/null case ${{ matrix.job.target }} in *-pc-windows-*) 7z -y a '${{ steps.vars.outputs.PKG_NAME }}' '${{ steps.vars.outputs.PKG_BASENAME }}'/* | tail -2 ;; *) tar czf '${{ steps.vars.outputs.PKG_NAME }}' '${{ steps.vars.outputs.PKG_BASENAME }}'/* ;; esac; popd >/dev/null - name: Publish uses: softprops/action-gh-release@v1 if: steps.vars.outputs.DEPLOY with: files: | ${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_NAME }} target/${{ matrix.job.target }}/debian/*.deb env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ## fix! [rivy; 2020-22-01] `cargo tarpaulin` is unable to test this repo at the moment; alternate recipe or another testing framework? # coverage: # name: Code Coverage # runs-on: ${{ matrix.job.os }} # strategy: # fail-fast: true # matrix: # # job: [ { os: ubuntu-latest }, { os: macos-latest }, { os: windows-latest } ] # job: [ { os: ubuntu-latest } ] ## cargo-tarpaulin is currently only available on linux # steps: # - uses: actions/checkout@v1 # # - name: Reattach HEAD ## may be needed for accurate code coverage info # # run: git checkout ${{ github.head_ref }} # - name: Initialize workflow variables # id: vars # shell: bash # run: | # # staging directory # STAGING='_staging' # echo set-output name=STAGING::${STAGING} # echo ::set-output name=STAGING::${STAGING} # # check for CODECOV_TOKEN availability (work-around for inaccessible 'secrets' object for 'if'; see ) # unset HAS_CODECOV_TOKEN # if [ -n $CODECOV_TOKEN ]; then HAS_CODECOV_TOKEN='true' ; fi # echo set-output name=HAS_CODECOV_TOKEN::${HAS_CODECOV_TOKEN} # echo ::set-output name=HAS_CODECOV_TOKEN::${HAS_CODECOV_TOKEN} # env: # CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}" # - name: Create all needed build/work directories # shell: bash # run: | # mkdir -p '${{ steps.vars.outputs.STAGING }}/work' # - name: Install required packages # run: | # sudo apt-get -y install libssl-dev # pushd '${{ steps.vars.outputs.STAGING }}/work' >/dev/null # wget --no-verbose https://github.com/xd009642/tarpaulin/releases/download/0.9.3/cargo-tarpaulin-0.9.3-travis.tar.gz # tar xf cargo-tarpaulin-0.9.3-travis.tar.gz # cp cargo-tarpaulin "$(dirname -- "$(which cargo)")"/ # popd >/dev/null # - name: Generate coverage # run: | # cargo tarpaulin --out Xml # - name: Upload coverage results (CodeCov.io) # # CODECOV_TOKEN (aka, "Repository Upload Token" for REPO from CodeCov.io) ## set via REPO/Settings/Secrets # # if: secrets.CODECOV_TOKEN (not supported {yet?}; see ) # if: steps.vars.outputs.HAS_CODECOV_TOKEN # run: | # # CodeCov.io # cargo tarpaulin --out Xml # bash <(curl -s https://codecov.io/bash) # env: # CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}" du-dust-1.2.4/.gitignore000064400000000000000000000002721046102023000132060ustar 00000000000000# Generated by Cargo # will have compiled files and executables /target/ # These are backup files generated by rustfmt **/*.rs.bk *.swp .vscode/* *.idea/* #ignore macos files .DS_Storedu-dust-1.2.4/.pre-commit-config.yaml000064400000000000000000000004121046102023000154730ustar 00000000000000repos: - repo: https://github.com/doublify/pre-commit-rust rev: v1.0 hooks: - id: cargo-check stages: [commit] - id: fmt stages: [commit] - id: clippy args: [--all-targets, --all-features] stages: [commit] du-dust-1.2.4/Cargo.lock0000644000000744270000000000100104160ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstream" version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys 0.61.2", ] [[package]] name = "assert_cmd" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" dependencies = [ "anstyle", "bstr", "libc", "predicates", "predicates-core", "predicates-tree", "wait-timeout", ] [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "block2" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ "objc2", ] [[package]] name = "bstr" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", "serde", ] [[package]] name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "cc" version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-link 0.2.1", ] [[package]] name = "clap" version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_complete" version = "4.5.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" dependencies = [ "clap", ] [[package]] name = "clap_derive" version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "clap_mangen" version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ea63a92086df93893164221ad4f24142086d535b3a0957b9b9bea2dc86301" dependencies = [ "clap", "roff", ] [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "config-file" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df51e72c150781d2c7d4cbcb0b803277caaa80476786994a62961a8f1010dafb" dependencies = [ "serde", "thiserror", "toml", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "ctrlc" version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" dependencies = [ "dispatch2", "nix", "windows-sys 0.61.2", ] [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "dispatch2" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags", "block2", "libc", "objc2", ] [[package]] name = "du-dust" version = "1.2.4" dependencies = [ "assert_cmd", "chrono", "clap", "clap_complete", "clap_mangen", "config-file", "ctrlc", "filesize", "lscolors", "nu-ansi-term", "portable-atomic", "rayon", "regex", "serde", "serde_json", "stfu8", "sysinfo", "tempfile", "terminal_size", "thousands", "unicode-width", "winapi-util", ] [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.61.2", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filesize" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12d741e2415d4e2e5bd1c1d00409d1a8865a57892c2d689b504365655d237d43" dependencies = [ "winapi", ] [[package]] name = "find-msvc-tools" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", ] [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "iana-time-zone" version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core 0.62.2", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "libc" version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lscolors" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d60e266dfb1426eb2d24792602e041131fdc0236bb7007abc0e589acafd60929" dependencies = [ "aho-corasick", "nu-ansi-term", ] [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "nix" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags", "cfg-if", "cfg_aliases", "libc", ] [[package]] name = "ntapi" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" dependencies = [ "winapi", ] [[package]] name = "nu-ansi-term" version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "objc2" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", ] [[package]] name = "objc2-core-foundation" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags", ] [[package]] name = "objc2-encode" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-io-kit" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" dependencies = [ "libc", "objc2-core-foundation", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "portable-atomic" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "predicates" version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "difflib", "predicates-core", ] [[package]] name = "predicates-core" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", ] [[package]] name = "proc-macro2" version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rayon" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "regex" version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "roff" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" [[package]] name = "rustix" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.2", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", "serde", "serde_core", "zmij", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "stfu8" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51f1e89f093f99e7432c491c382b88a6860a5adbe6bf02574bf0a08efff1978" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sysinfo" version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" dependencies = [ "libc", "memchr", "ntapi", "objc2-core-foundation", "objc2-io-kit", "windows", ] [[package]] name = "tempfile" version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom", "once_cell", "rustix", "windows-sys 0.61.2", ] [[package]] name = "terminal_size" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ "rustix", "windows-sys 0.60.2", ] [[package]] name = "termtree" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thousands" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" [[package]] name = "toml" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "serde", ] [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "wait-timeout" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", "windows-core 0.61.2", "windows-future", "windows-link 0.1.3", "windows-numerics", ] [[package]] name = "windows-collections" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ "windows-core 0.61.2", ] [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", ] [[package]] name = "windows-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", ] [[package]] name = "windows-future" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", "windows-threading", ] [[package]] name = "windows-implement" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-link" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", ] [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link 0.1.3", ] [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link 0.2.1", ] [[package]] name = "windows-strings" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link 0.1.3", ] [[package]] name = "windows-strings" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link 0.2.1", ] [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link 0.2.1", ] [[package]] name = "windows-targets" version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link 0.2.1", "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows-threading" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ "windows-link 0.1.3", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "zmij" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" du-dust-1.2.4/Cargo.toml0000644000000066350000000000100104350ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2024" name = "du-dust" version = "1.2.4" authors = [ "bootandy ", "nebkor ", ] build = "build.rs" autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "A more intuitive version of du" homepage = "https://github.com/bootandy/dust" documentation = "https://github.com/bootandy/dust" readme = "README.md" keywords = [ "du", "command-line", "disk", "disk-usage", ] categories = ["command-line-utilities"] license = "Apache-2.0" repository = "https://github.com/bootandy/dust" [package.metadata.binstall] pkg-url = "{ repo }/releases/download/v{ version }/dust-v{ version }-{ target }{ archive-suffix }" bin-dir = "dust-v{ version }-{ target }/{ bin }{ binary-ext }" [package.metadata.deb] section = "utils" assets = [ [ "target/release/dust", "usr/bin/", "755", ], [ "LICENSE", "usr/share/doc/du-dust/", "644", ], [ "README.md", "usr/share/doc/du-dust/README", "644", ], [ "man-page/dust.1", "usr/share/man/man1/dust.1", "644", ], [ "completions/dust.bash", "usr/share/bash-completion/completions/dust", "644", ], ] extended-description = """ 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 one 'Did not have permissions message'. """ [badges.travis-ci] repository = "https://travis-ci.org/bootandy/dust" [[bin]] name = "dust" path = "src/main.rs" [[test]] name = "integration" path = "tests/tests.rs" [[test]] name = "test_exact_output" path = "tests/test_exact_output.rs" [[test]] name = "test_flags" path = "tests/test_flags.rs" [[test]] name = "tests_symlinks" path = "tests/tests_symlinks.rs" [dependencies.chrono] version = "0.4" [dependencies.clap] version = "4" features = ["derive"] [dependencies.config-file] version = "0.2" [dependencies.ctrlc] version = "3" [dependencies.lscolors] version = "0.21" [dependencies.nu-ansi-term] version = "0.50" [dependencies.rayon] version = "1" [dependencies.regex] version = "1" [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.serde_json] version = "1.0" [dependencies.stfu8] version = "0.2" [dependencies.sysinfo] version = "0.37" [dependencies.terminal_size] version = "0.4" [dependencies.thousands] version = "0.2" [dependencies.unicode-width] version = "0.2" [dev-dependencies.assert_cmd] version = "2" [dev-dependencies.tempfile] version = "=3" [build-dependencies.clap] version = "4.4" features = ["derive"] [build-dependencies.clap_complete] version = "4.4" [build-dependencies.clap_mangen] version = "0.2" [target.'cfg(not(target_has_atomic = "64"))'.dependencies.portable-atomic] version = "1.4" [target."cfg(windows)".dependencies.filesize] version = "0.2.0" [target."cfg(windows)".dependencies.winapi-util] version = "0.1" [profile.release] lto = true codegen-units = 1 strip = true du-dust-1.2.4/Cargo.toml.orig000064400000000000000000000042561046102023000141130ustar 00000000000000[package] name = "du-dust" description = "A more intuitive version of du" version = "1.2.4" authors = ["bootandy ", "nebkor "] edition = "2024" readme = "README.md" documentation = "https://github.com/bootandy/dust" homepage = "https://github.com/bootandy/dust" repository = "https://github.com/bootandy/dust" keywords = ["du", "command-line", "disk", "disk-usage"] categories = ["command-line-utilities"] license = "Apache-2.0" [badges] travis-ci = { repository = "https://travis-ci.org/bootandy/dust" } [[bin]] name = "dust" path = "src/main.rs" [profile.release] codegen-units = 1 lto = true strip = true [dependencies] clap = { version = "4", features = ["derive"] } lscolors = "0.21" nu-ansi-term = "0.50" terminal_size = "0.4" unicode-width = "0.2" rayon = "1" thousands = "0.2" stfu8 = "0.2" regex = "1" config-file = "0.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sysinfo = "0.37" ctrlc = "3" chrono = "0.4" [target.'cfg(not(target_has_atomic = "64"))'.dependencies] portable-atomic = "1.4" [target.'cfg(windows)'.dependencies] winapi-util = "0.1" filesize = "0.2.0" [dev-dependencies] assert_cmd = "2" tempfile = "=3" [build-dependencies] clap = { version = "4.4", features = ["derive"] } clap_complete = "4.4" clap_mangen = "0.2" [[test]] name = "integration" path = "tests/tests.rs" [package.metadata.binstall] pkg-url = "{ repo }/releases/download/v{ version }/dust-v{ version }-{ target }{ archive-suffix }" bin-dir = "dust-v{ version }-{ target }/{ bin }{ binary-ext }" [package.metadata.deb] section = "utils" assets = [ [ "target/release/dust", "usr/bin/", "755", ], [ "LICENSE", "usr/share/doc/du-dust/", "644", ], [ "README.md", "usr/share/doc/du-dust/README", "644", ], [ "man-page/dust.1", "usr/share/man/man1/dust.1", "644", ], [ "completions/dust.bash", "usr/share/bash-completion/completions/dust", "644", ], ] extended-description = """\ 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 one 'Did not have permissions message'. """ du-dust-1.2.4/LICENSE000064400000000000000000000261211046102023000122240ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [2023] [andrew boot] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. du-dust-1.2.4/README.md000064400000000000000000000156001046102023000124760ustar 00000000000000[![Build Status](https://github.com/bootandy/dust/actions/workflows/CICD.yml/badge.svg)](https://github.com/bootandy/dust/actions) # Dust du + rust = dust. Like du but more intuitive. # Why Because I want an easy way to see where my disk is being used. # Demo ![Example](media/snap.png) Study the above picture. * We see `target` has 1.8G * `target/debug` is the same size as `target` - so we know nearly all the disk usage of the 1.5G is in this folder * `target/debug/deps` this is 1.2G - Note the bar jumps down to 70% to indicate that most disk usage is here but not all. * `target/debug/deps/dust-e78c9f87a17f24f3` - This is the largest file in this folder, but it is only 46M - Note the bar jumps down to 3% to indicate the file is small. From here we can conclude: * `target/debug/deps` takes the majority of the space in `target` and that `target/debug/deps` has a large number of relatively small files. ## Install #### Cargo Packaging status - `cargo install du-dust` #### 🍺 Homebrew (Mac OS) - `brew install dust` #### 🍺 Homebrew (Linux) - `brew install dust` #### [Snap](https://ubuntu.com/core/services/guide/snaps-intro) Ubuntu and [supported systems](https://snapcraft.io/docs/installing-snapd) - `snap install dust` Note: `dust` installed through `snap` can only access files stored in the `/home` directory. See daniejstriata/dust-snap#2 for more information. #### [Pacstall](https://github.com/pacstall/pacstall) (Debian/Ubuntu) - `pacstall -I dust-bin` #### Anaconda (conda-forge) - `conda install -c conda-forge dust` #### [deb-get](https://github.com/wimpysworld/deb-get) (Debian/Ubuntu) - `deb-get install du-dust` #### [x-cmd](https://www.x-cmd.com/pkg/#VPContent) - `x env use dust` #### Windows: - `scoop install dust` - Windows GNU version - works - Windows MSVC - requires: [VCRUNTIME140.dll](https://docs.microsoft.com/en-gb/cpp/windows/latest-supported-vc-redist?view=msvc-170) #### Download - Download Linux/Mac binary from [Releases](https://github.com/bootandy/dust/releases) - unzip file: `tar -xvf _downloaded_file.tar.gz` - move file to executable path: `sudo mv dust /usr/local/bin/` ## Overview 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 one 'Did not have permissions message'. Dust will list a slightly-less-than-the-terminal-height number of the biggest subdirectories 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 subdirectories will be colored. The different colors on the bars: These represent the combined tree hierarchy & disk usage. The shades of grey are used to indicate which parent folder a subfolder belongs to. For instance, look at the above screenshot. `.steam` is a folder taking 44% of the space. From the `.steam` bar is a light grey line that goes up. All these folders are inside `.steam` so if you delete `.steam` all that stuff will be gone too. If you are new to the tool I recommend to try tweaking the `-n` parameter. `dust -n 10`, `dust -n 50`. ## Usage ``` Usage: dust Usage: dust Usage: dust Usage: dust -p (full-path - Show fullpath of the subdirectories) Usage: dust -s (apparent-size - shows the length of the file as opposed to the amount of disk space it uses) Usage: dust -n 30 (Shows 30 directories instead of the default [default is terminal height]) Usage: dust -d 3 (Shows 3 levels of subdirectories) Usage: dust -D (Show only directories (eg dust -D)) Usage: dust -F (Show only files - finds your largest files) Usage: dust -r (reverse order of output) Usage: dust -o si/b/kb/kib/mb/mib/gb/gib (si - prints sizes in powers of 1000. Others print size in that format). Usage: dust -X ignore (ignore all files and directories with the name 'ignore') Usage: dust -x (Only show directories on the same filesystem) Usage: dust -b (Do not show percentages or draw ASCII bars) Usage: dust -B (--bars-on-right - Percent bars moved to right side of screen) Usage: dust -i (Do not show hidden files) Usage: dust -c (No colors [monochrome]) Usage: dust -C (Force colors) Usage: dust -f (Count files instead of diskspace [Counts by inode, to include duplicate inodes use dust -f -s]) Usage: dust -t (Group by filetype) Usage: dust -z 10M (min-size, Only include files larger than 10M) Usage: dust -e regex (Only include files matching this regex (eg dust -e "\.png$" would match png files)) Usage: dust -v regex (Exclude files matching this regex (eg dust -v "\.png$" would ignore png files)) Usage: dust -L (dereference-links - Treat sym links as directories and go into them) Usage: dust -P (Disable the progress indicator) Usage: dust -R (For screen readers. Removes bars/symbols. Adds new column: depth level. (May want to use -p for full path too)) Usage: dust -S (Custom Stack size - Use if you see: 'fatal runtime error: stack overflow' (default allocation: low memory=1048576, high memory=1073741824)"), Usage: dust --skip-total (No total row will be displayed) Usage: dust -z 40000/30MB/20kib (Exclude output files/directories below size 40000 bytes / 30MB / 20KiB) Usage: dust -j (Prints JSON representation of directories, try: dust -j | jq) Usage: dust --files0-from=FILE (Read NUL-terminated file paths from FILE; if FILE is '-', read from stdin) Usage: dust --files-from=FILE (Read newline-terminated file paths from FILE; if FILE is '-', read from stdin) Usage: dust --collapse=node-modules will keep the node-modules folder collapsed in display instead of recursively opening it ``` ## Config file Dust has a config file where the above options can be set. Either: `~/.config/dust/config.toml` or `~/.dust.toml` ``` $ cat ~/.config/dust/config.toml reverse=true ``` ## Alternatives - [NCDU](https://dev.yorhel.nl/ncdu) - [dutree](https://github.com/nachoparker/dutree) - [dua](https://github.com/Byron/dua-cli/) - [pdu](https://github.com/KSXGitHub/parallel-disk-usage) - [dirstat-rs](https://github.com/scullionw/dirstat-rs) - `du -d 1 -h | sort -h` ## Why to use Dust over the Alternatives Dust simply Does The Right Thing when handling lots of small files & directories. Dust keeps the output simple by only showing large entries. Tools like ncdu & baobab, give you a view of directory sizes but you have no idea where the largest files are. For example directory A could have a size larger than directory B, but in fact the largest file is in B and not A. Finding this out via these other tools is not trivial whereas Dust will show the large file clearly in the tree hierarchy Dust will not count hard links multiple times (unless you want to `-s`). Typing `dust -n 90` will show you your 90 largest entries. `-n` is not quite like `head -n` or `tail -n`, dust is intelligent and chooses the largest entries du-dust-1.2.4/build.rs000064400000000000000000000013721046102023000126650ustar 00000000000000use clap::CommandFactory; use clap_complete::{generate_to, shells::*}; use clap_mangen::Man; use std::fs::File; use std::io::Error; use std::path::Path; include!("src/cli.rs"); fn main() -> Result<(), Error> { let outdir = "completions"; let app_name = "dust"; let mut cmd = Cli::command(); generate_to(Bash, &mut cmd, app_name, outdir)?; generate_to(Zsh, &mut cmd, app_name, outdir)?; generate_to(Fish, &mut cmd, app_name, outdir)?; generate_to(PowerShell, &mut cmd, app_name, outdir)?; generate_to(Elvish, &mut cmd, app_name, outdir)?; let file = Path::new("man-page").join("dust.1"); std::fs::create_dir_all("man-page")?; let mut file = File::create(file)?; Man::new(cmd).render(&mut file)?; Ok(()) } du-dust-1.2.4/ci/before_deploy.ps1000064400000000000000000000010731046102023000150540ustar 00000000000000# This script takes care of packaging the build artifacts that will go in the # release zipfile $SRC_DIR = $PWD.Path $STAGE = [System.Guid]::NewGuid().ToString() Set-Location $ENV:Temp New-Item -Type Directory -Name $STAGE Set-Location $STAGE $ZIP = "$SRC_DIR\$($Env:CRATE_NAME)-$($Env:APPVEYOR_REPO_TAG_NAME)-$($Env:TARGET).zip" # TODO Update this to package the right artifacts Copy-Item "$SRC_DIR\target\$($Env:TARGET)\release\dust" '.\' 7z a "$ZIP" * Push-AppveyorArtifact "$ZIP" Remove-Item *.* -Force Set-Location .. Remove-Item $STAGE Set-Location $SRC_DIR du-dust-1.2.4/ci/before_deploy.sh000064400000000000000000000013101046102023000147550ustar 00000000000000#!/usr/bin/env bash # This script takes care of building your crate and packaging it for release set -ex main() { local src=$(pwd) \ stage= case $TRAVIS_OS_NAME in linux) stage=$(mktemp -d) ;; osx) stage=$(mktemp -d -t tmp) ;; esac test -f Cargo.lock || cargo generate-lockfile # TODO Update this to build the artifacts that matter to you cross rustc --bin dust --target $TARGET --release -- -C lto # TODO Update this to package the right artifacts cp target/$TARGET/release/dust $stage/ cd $stage tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz * cd $src rm -rf $stage } main du-dust-1.2.4/ci/how2publish.txt000064400000000000000000000010521046102023000146150ustar 00000000000000# ----------- To do a release --------- # ----------- Pre release --------- # Compare times of runs to check no drastic slow down: # hyperfine 'target/release/dust /home/andy' # hyperfine 'dust /home/andy' # ----------- Release --------- # inc version in cargo.toml # cargo build --release # commit changed files # merge to master in github # tag a commit and push (increment version in Cargo.toml first): # git tag v0.4.5 # git push origin v0.4.5 # cargo publish to put it in crates.io # Optional: To install locally #cargo install --path . du-dust-1.2.4/ci/install.sh000064400000000000000000000014151046102023000136130ustar 00000000000000#!/usr/bin/env bash set -ex main() { local target= if [ $TRAVIS_OS_NAME = linux ]; then target=x86_64-unknown-linux-musl sort=sort else target=x86_64-apple-darwin sort=gsort # for `sort --sort-version`, from brew's coreutils. fi # This fetches latest stable release local tag=$(git ls-remote --tags --refs --exit-code https://github.com/japaric/cross \ | cut -d/ -f3 \ | grep -E '^v[0.1.0-9.]+$' \ | $sort --version-sort \ | tail -n1) curl -LSfs https://japaric.github.io/trust/install.sh | \ sh -s -- \ --force \ --git japaric/cross \ --tag $tag \ --target $target } main du-dust-1.2.4/ci/script.sh000064400000000000000000000010111046102023000134410ustar 00000000000000#!/usr/bin/env bash # This script takes care of testing your crate set -ex # TODO This is the "test phase", tweak it as you see fit main() { cross build --target $TARGET cross build --target $TARGET --release if [ ! -z $DISABLE_TESTS ]; then return fi cross test --target $TARGET cross test --target $TARGET --release cross run --target $TARGET cross run --target $TARGET --release } # we don't run the "test phase" when doing deploys if [ -z $TRAVIS_TAG ]; then main fi du-dust-1.2.4/completions/_dust000064400000000000000000000171041046102023000146150ustar 00000000000000#compdef dust autoload -U is-at-least _dust() { typeset -A opt_args typeset -a _arguments_options local ret=1 if is-at-least 5.2; then _arguments_options=(-s -S -C) else _arguments_options=(-s -C) fi local context curcontext="$curcontext" state line _arguments "${_arguments_options[@]}" : \ '-d+[Depth to show]:DEPTH:_default' \ '--depth=[Depth to show]:DEPTH:_default' \ '-T+[Number of threads to use]:THREADS:_default' \ '--threads=[Number of threads to use]:THREADS:_default' \ '--config=[Specify a config file to use]:FILE:_files' \ '-n+[Display the '\''n'\'' largest entries. (Default is terminal_height)]:NUMBER:_default' \ '--number-of-lines=[Display the '\''n'\'' largest entries. (Default is terminal_height)]:NUMBER:_default' \ '*-X+[Exclude any file or directory with this path]:PATH:_files' \ '*--ignore-directory=[Exclude any file or directory with this path]:PATH:_files' \ '-I+[Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter]:FILE:_files' \ '--ignore-all-in-file=[Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter]:FILE:_files' \ '-z+[Minimum size file to include in output]:MIN_SIZE:_default' \ '--min-size=[Minimum size file to include in output]:MIN_SIZE:_default' \ '(-e --filter -t --file-types)*-v+[Exclude filepaths matching this regex. To ignore png files type\: -v "\\.png\$"]:REGEX:_default' \ '(-e --filter -t --file-types)*--invert-filter=[Exclude filepaths matching this regex. To ignore png files type\: -v "\\.png\$"]:REGEX:_default' \ '(-t --file-types)*-e+[Only include filepaths matching this regex. For png files type\: -e "\\.png\$"]:REGEX:_default' \ '(-t --file-types)*--filter=[Only include filepaths matching this regex. For png files type\: -e "\\.png\$"]:REGEX:_default' \ '-w+[Specify width of output overriding the auto detection of terminal width]:WIDTH:_default' \ '--terminal-width=[Specify width of output overriding the auto detection of terminal width]:WIDTH:_default' \ '-o+[Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size]:FORMAT:((si\:"SI prefix (powers of 1000)" b\:"byte (B)" k\:"kibibyte (KiB)" m\:"mebibyte (MiB)" g\:"gibibyte (GiB)" t\:"tebibyte (TiB)" kb\:"kilobyte (kB)" mb\:"megabyte (MB)" gb\:"gigabyte (GB)" tb\:"terabyte (TB)"))' \ '--output-format=[Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size]:FORMAT:((si\:"SI prefix (powers of 1000)" b\:"byte (B)" k\:"kibibyte (KiB)" m\:"mebibyte (MiB)" g\:"gibibyte (GiB)" t\:"tebibyte (TiB)" kb\:"kilobyte (kB)" mb\:"megabyte (MB)" gb\:"gigabyte (GB)" tb\:"terabyte (TB)"))' \ '-S+[Specify memory to use as stack size - use if you see\: '\''fatal runtime error\: stack overflow'\'' (default low memory=1048576, high memory=1073741824)]:STACK_SIZE:_default' \ '--stack-size=[Specify memory to use as stack size - use if you see\: '\''fatal runtime error\: stack overflow'\'' (default low memory=1048576, high memory=1073741824)]:STACK_SIZE:_default' \ '-M+[+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr−(n+1)), n => \[curr−(n+1), curr−n), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)]:MTIME:_default' \ '--mtime=[+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr−(n+1)), n => \[curr−(n+1), curr−n), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)]:MTIME:_default' \ '-A+[just like -mtime, but based on file access time]:ATIME:_default' \ '--atime=[just like -mtime, but based on file access time]:ATIME:_default' \ '-y+[just like -mtime, but based on file change time]:CTIME:_default' \ '--ctime=[just like -mtime, but based on file change time]:CTIME:_default' \ '(--files-from)--files0-from=[Read NUL-terminated paths from FILE (use \`-\` for stdin)]:FILES0_FROM:_files' \ '(--files0-from)--files-from=[Read newline-terminated paths from FILE (use \`-\` for stdin)]:FILES_FROM:_files' \ '*--collapse=[Keep these directories collapsed]:COLLAPSE:_files' \ '-m+[Directory '\''size'\'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time]:FILETIME:((a\:"last accessed time" c\:"last changed time" m\:"last modified time"))' \ '--filetime=[Directory '\''size'\'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time]:FILETIME:((a\:"last accessed time" c\:"last changed time" m\:"last modified time"))' \ '-p[Subdirectories will not have their path shortened]' \ '--full-paths[Subdirectories will not have their path shortened]' \ '-L[dereference sym links - Treat sym links as directories and go into them]' \ '--dereference-links[dereference sym links - Treat sym links as directories and go into them]' \ '-x[Only count the files and directories on the same filesystem as the supplied directory]' \ '--limit-filesystem[Only count the files and directories on the same filesystem as the supplied directory]' \ '-s[Use file length instead of blocks]' \ '--apparent-size[Use file length instead of blocks]' \ '-r[Print tree upside down (biggest highest)]' \ '--reverse[Print tree upside down (biggest highest)]' \ '-c[No colors will be printed (Useful for commands like\: watch)]' \ '--no-colors[No colors will be printed (Useful for commands like\: watch)]' \ '-C[Force colors print]' \ '--force-colors[Force colors print]' \ '-b[No percent bars or percentages will be displayed]' \ '--no-percent-bars[No percent bars or percentages will be displayed]' \ '-B[percent bars moved to right side of screen]' \ '--bars-on-right[percent bars moved to right side of screen]' \ '-R[For screen readers. Removes bars. Adds new column\: depth level (May want to use -p too for full path)]' \ '--screen-reader[For screen readers. Removes bars. Adds new column\: depth level (May want to use -p too for full path)]' \ '--skip-total[No total row will be displayed]' \ '-f[Directory '\''size'\'' is number of child files instead of disk size]' \ '--filecount[Directory '\''size'\'' is number of child files instead of disk size]' \ '-i[Do not display hidden files]' \ '--ignore-hidden[Do not display hidden files]' \ '(-d --depth -D --only-dir)-t[show only these file types]' \ '(-d --depth -D --only-dir)--file-types[show only these file types]' \ '-P[Disable the progress indication]' \ '--no-progress[Disable the progress indication]' \ '--print-errors[Print path with errors]' \ '(-F --only-file -t --file-types)-D[Only directories will be displayed]' \ '(-F --only-file -t --file-types)--only-dir[Only directories will be displayed]' \ '(-D --only-dir)-F[Only files will be displayed. (Finds your largest files)]' \ '(-D --only-dir)--only-file[Only files will be displayed. (Finds your largest files)]' \ '-j[Output the directory tree as json to the current directory]' \ '--output-json[Output the directory tree as json to the current directory]' \ '-h[Print help (see more with '\''--help'\'')]' \ '--help[Print help (see more with '\''--help'\'')]' \ '-V[Print version]' \ '--version[Print version]' \ '*::params -- Input files or directories:_files' \ && ret=0 } (( $+functions[_dust_commands] )) || _dust_commands() { local commands; commands=() _describe -t commands 'dust commands' commands "$@" } if [ "$funcstack[1]" = "_dust" ]; then _dust "$@" else compdef _dust dust fi du-dust-1.2.4/completions/_dust.ps1000064400000000000000000000314721046102023000153230ustar 00000000000000 using namespace System.Management.Automation using namespace System.Management.Automation.Language Register-ArgumentCompleter -Native -CommandName 'dust' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) $commandElements = $commandAst.CommandElements $command = @( 'dust' for ($i = 1; $i -lt $commandElements.Count; $i++) { $element = $commandElements[$i] if ($element -isnot [StringConstantExpressionAst] -or $element.StringConstantType -ne [StringConstantType]::BareWord -or $element.Value.StartsWith('-') -or $element.Value -eq $wordToComplete) { break } $element.Value }) -join ';' $completions = @(switch ($command) { 'dust' { [CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'Depth to show') [CompletionResult]::new('--depth', '--depth', [CompletionResultType]::ParameterName, 'Depth to show') [CompletionResult]::new('-T', '-T ', [CompletionResultType]::ParameterName, 'Number of threads to use') [CompletionResult]::new('--threads', '--threads', [CompletionResultType]::ParameterName, 'Number of threads to use') [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Specify a config file to use') [CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'Display the ''n'' largest entries. (Default is terminal_height)') [CompletionResult]::new('--number-of-lines', '--number-of-lines', [CompletionResultType]::ParameterName, 'Display the ''n'' largest entries. (Default is terminal_height)') [CompletionResult]::new('-X', '-X ', [CompletionResultType]::ParameterName, 'Exclude any file or directory with this path') [CompletionResult]::new('--ignore-directory', '--ignore-directory', [CompletionResultType]::ParameterName, 'Exclude any file or directory with this path') [CompletionResult]::new('-I', '-I ', [CompletionResultType]::ParameterName, 'Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter') [CompletionResult]::new('--ignore-all-in-file', '--ignore-all-in-file', [CompletionResultType]::ParameterName, 'Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter') [CompletionResult]::new('-z', '-z', [CompletionResultType]::ParameterName, 'Minimum size file to include in output') [CompletionResult]::new('--min-size', '--min-size', [CompletionResultType]::ParameterName, 'Minimum size file to include in output') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$"') [CompletionResult]::new('--invert-filter', '--invert-filter', [CompletionResultType]::ParameterName, 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$"') [CompletionResult]::new('-e', '-e', [CompletionResultType]::ParameterName, 'Only include filepaths matching this regex. For png files type: -e "\.png$"') [CompletionResult]::new('--filter', '--filter', [CompletionResultType]::ParameterName, 'Only include filepaths matching this regex. For png files type: -e "\.png$"') [CompletionResult]::new('-w', '-w', [CompletionResultType]::ParameterName, 'Specify width of output overriding the auto detection of terminal width') [CompletionResult]::new('--terminal-width', '--terminal-width', [CompletionResultType]::ParameterName, 'Specify width of output overriding the auto detection of terminal width') [CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size') [CompletionResult]::new('--output-format', '--output-format', [CompletionResultType]::ParameterName, 'Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size') [CompletionResult]::new('-S', '-S ', [CompletionResultType]::ParameterName, 'Specify memory to use as stack size - use if you see: ''fatal runtime error: stack overflow'' (default low memory=1048576, high memory=1073741824)') [CompletionResult]::new('--stack-size', '--stack-size', [CompletionResultType]::ParameterName, 'Specify memory to use as stack size - use if you see: ''fatal runtime error: stack overflow'' (default low memory=1048576, high memory=1073741824)') [CompletionResult]::new('-M', '-M ', [CompletionResultType]::ParameterName, '+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr−(n+1)), n => [curr−(n+1), curr−n), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)') [CompletionResult]::new('--mtime', '--mtime', [CompletionResultType]::ParameterName, '+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr−(n+1)), n => [curr−(n+1), curr−n), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)') [CompletionResult]::new('-A', '-A ', [CompletionResultType]::ParameterName, 'just like -mtime, but based on file access time') [CompletionResult]::new('--atime', '--atime', [CompletionResultType]::ParameterName, 'just like -mtime, but based on file access time') [CompletionResult]::new('-y', '-y', [CompletionResultType]::ParameterName, 'just like -mtime, but based on file change time') [CompletionResult]::new('--ctime', '--ctime', [CompletionResultType]::ParameterName, 'just like -mtime, but based on file change time') [CompletionResult]::new('--files0-from', '--files0-from', [CompletionResultType]::ParameterName, 'Read NUL-terminated paths from FILE (use `-` for stdin)') [CompletionResult]::new('--files-from', '--files-from', [CompletionResultType]::ParameterName, 'Read newline-terminated paths from FILE (use `-` for stdin)') [CompletionResult]::new('--collapse', '--collapse', [CompletionResultType]::ParameterName, 'Keep these directories collapsed') [CompletionResult]::new('-m', '-m', [CompletionResultType]::ParameterName, 'Directory ''size'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time') [CompletionResult]::new('--filetime', '--filetime', [CompletionResultType]::ParameterName, 'Directory ''size'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time') [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Subdirectories will not have their path shortened') [CompletionResult]::new('--full-paths', '--full-paths', [CompletionResultType]::ParameterName, 'Subdirectories will not have their path shortened') [CompletionResult]::new('-L', '-L ', [CompletionResultType]::ParameterName, 'dereference sym links - Treat sym links as directories and go into them') [CompletionResult]::new('--dereference-links', '--dereference-links', [CompletionResultType]::ParameterName, 'dereference sym links - Treat sym links as directories and go into them') [CompletionResult]::new('-x', '-x', [CompletionResultType]::ParameterName, 'Only count the files and directories on the same filesystem as the supplied directory') [CompletionResult]::new('--limit-filesystem', '--limit-filesystem', [CompletionResultType]::ParameterName, 'Only count the files and directories on the same filesystem as the supplied directory') [CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'Use file length instead of blocks') [CompletionResult]::new('--apparent-size', '--apparent-size', [CompletionResultType]::ParameterName, 'Use file length instead of blocks') [CompletionResult]::new('-r', '-r', [CompletionResultType]::ParameterName, 'Print tree upside down (biggest highest)') [CompletionResult]::new('--reverse', '--reverse', [CompletionResultType]::ParameterName, 'Print tree upside down (biggest highest)') [CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'No colors will be printed (Useful for commands like: watch)') [CompletionResult]::new('--no-colors', '--no-colors', [CompletionResultType]::ParameterName, 'No colors will be printed (Useful for commands like: watch)') [CompletionResult]::new('-C', '-C ', [CompletionResultType]::ParameterName, 'Force colors print') [CompletionResult]::new('--force-colors', '--force-colors', [CompletionResultType]::ParameterName, 'Force colors print') [CompletionResult]::new('-b', '-b', [CompletionResultType]::ParameterName, 'No percent bars or percentages will be displayed') [CompletionResult]::new('--no-percent-bars', '--no-percent-bars', [CompletionResultType]::ParameterName, 'No percent bars or percentages will be displayed') [CompletionResult]::new('-B', '-B ', [CompletionResultType]::ParameterName, 'percent bars moved to right side of screen') [CompletionResult]::new('--bars-on-right', '--bars-on-right', [CompletionResultType]::ParameterName, 'percent bars moved to right side of screen') [CompletionResult]::new('-R', '-R ', [CompletionResultType]::ParameterName, 'For screen readers. Removes bars. Adds new column: depth level (May want to use -p too for full path)') [CompletionResult]::new('--screen-reader', '--screen-reader', [CompletionResultType]::ParameterName, 'For screen readers. Removes bars. Adds new column: depth level (May want to use -p too for full path)') [CompletionResult]::new('--skip-total', '--skip-total', [CompletionResultType]::ParameterName, 'No total row will be displayed') [CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'Directory ''size'' is number of child files instead of disk size') [CompletionResult]::new('--filecount', '--filecount', [CompletionResultType]::ParameterName, 'Directory ''size'' is number of child files instead of disk size') [CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'Do not display hidden files') [CompletionResult]::new('--ignore-hidden', '--ignore-hidden', [CompletionResultType]::ParameterName, 'Do not display hidden files') [CompletionResult]::new('-t', '-t', [CompletionResultType]::ParameterName, 'show only these file types') [CompletionResult]::new('--file-types', '--file-types', [CompletionResultType]::ParameterName, 'show only these file types') [CompletionResult]::new('-P', '-P ', [CompletionResultType]::ParameterName, 'Disable the progress indication') [CompletionResult]::new('--no-progress', '--no-progress', [CompletionResultType]::ParameterName, 'Disable the progress indication') [CompletionResult]::new('--print-errors', '--print-errors', [CompletionResultType]::ParameterName, 'Print path with errors') [CompletionResult]::new('-D', '-D ', [CompletionResultType]::ParameterName, 'Only directories will be displayed') [CompletionResult]::new('--only-dir', '--only-dir', [CompletionResultType]::ParameterName, 'Only directories will be displayed') [CompletionResult]::new('-F', '-F ', [CompletionResultType]::ParameterName, 'Only files will be displayed. (Finds your largest files)') [CompletionResult]::new('--only-file', '--only-file', [CompletionResultType]::ParameterName, 'Only files will be displayed. (Finds your largest files)') [CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, 'Output the directory tree as json to the current directory') [CompletionResult]::new('--output-json', '--output-json', [CompletionResultType]::ParameterName, 'Output the directory tree as json to the current directory') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') break } }) $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | Sort-Object -Property ListItemText } du-dust-1.2.4/completions/dust.bash000064400000000000000000000166221046102023000153760ustar 00000000000000_dust() { local i cur prev opts cmd COMPREPLY=() if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then cur="$2" else cur="${COMP_WORDS[COMP_CWORD]}" fi prev="$3" cmd="" opts="" for i in "${COMP_WORDS[@]:0:COMP_CWORD}" do case "${cmd},${i}" in ",$1") cmd="dust" ;; *) ;; esac done case "${cmd}" in dust) opts="-d -T -n -p -X -I -L -x -s -r -c -C -b -B -z -R -f -i -v -e -t -w -P -D -F -o -S -j -M -A -y -m -h -V --depth --threads --config --number-of-lines --full-paths --ignore-directory --ignore-all-in-file --dereference-links --limit-filesystem --apparent-size --reverse --no-colors --force-colors --no-percent-bars --bars-on-right --min-size --screen-reader --skip-total --filecount --ignore-hidden --invert-filter --filter --file-types --terminal-width --no-progress --print-errors --only-dir --only-file --output-format --stack-size --output-json --mtime --atime --ctime --files0-from --files-from --collapse --filetime --help --version [PATH]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in --depth) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -d) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --threads) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -T) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --config) local oldifs if [ -n "${IFS+x}" ]; then oldifs="$IFS" fi IFS=$'\n' COMPREPLY=($(compgen -f "${cur}")) if [ -n "${oldifs+x}" ]; then IFS="$oldifs" fi if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then compopt -o filenames fi return 0 ;; --number-of-lines) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -n) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --ignore-directory) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -X) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --ignore-all-in-file) local oldifs if [ -n "${IFS+x}" ]; then oldifs="$IFS" fi IFS=$'\n' COMPREPLY=($(compgen -f "${cur}")) if [ -n "${oldifs+x}" ]; then IFS="$oldifs" fi if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then compopt -o filenames fi return 0 ;; -I) local oldifs if [ -n "${IFS+x}" ]; then oldifs="$IFS" fi IFS=$'\n' COMPREPLY=($(compgen -f "${cur}")) if [ -n "${oldifs+x}" ]; then IFS="$oldifs" fi if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then compopt -o filenames fi return 0 ;; --min-size) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -z) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --invert-filter) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -v) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --filter) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -e) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --terminal-width) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -w) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --output-format) COMPREPLY=($(compgen -W "si b k m g t kb mb gb tb" -- "${cur}")) return 0 ;; -o) COMPREPLY=($(compgen -W "si b k m g t kb mb gb tb" -- "${cur}")) return 0 ;; --stack-size) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -S) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --mtime) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -M) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --atime) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -A) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --ctime) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -y) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --files0-from) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --files-from) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --collapse) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --filetime) COMPREPLY=($(compgen -W "a c m" -- "${cur}")) return 0 ;; -m) COMPREPLY=($(compgen -W "a c m" -- "${cur}")) return 0 ;; *) COMPREPLY=() ;; esac COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; esac } if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then complete -F _dust -o nosort -o bashdefault -o default dust else complete -F _dust -o bashdefault -o default dust fi du-dust-1.2.4/completions/dust.elv000064400000000000000000000160711046102023000152450ustar 00000000000000 use builtin; use str; set edit:completion:arg-completer[dust] = {|@words| fn spaces {|n| builtin:repeat $n ' ' | str:join '' } fn cand {|text desc| edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc } var command = 'dust' for word $words[1..-1] { if (str:has-prefix $word '-') { break } set command = $command';'$word } var completions = [ &'dust'= { cand -d 'Depth to show' cand --depth 'Depth to show' cand -T 'Number of threads to use' cand --threads 'Number of threads to use' cand --config 'Specify a config file to use' cand -n 'Display the ''n'' largest entries. (Default is terminal_height)' cand --number-of-lines 'Display the ''n'' largest entries. (Default is terminal_height)' cand -X 'Exclude any file or directory with this path' cand --ignore-directory 'Exclude any file or directory with this path' cand -I 'Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter' cand --ignore-all-in-file 'Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter' cand -z 'Minimum size file to include in output' cand --min-size 'Minimum size file to include in output' cand -v 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$"' cand --invert-filter 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$"' cand -e 'Only include filepaths matching this regex. For png files type: -e "\.png$"' cand --filter 'Only include filepaths matching this regex. For png files type: -e "\.png$"' cand -w 'Specify width of output overriding the auto detection of terminal width' cand --terminal-width 'Specify width of output overriding the auto detection of terminal width' cand -o 'Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size' cand --output-format 'Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size' cand -S 'Specify memory to use as stack size - use if you see: ''fatal runtime error: stack overflow'' (default low memory=1048576, high memory=1073741824)' cand --stack-size 'Specify memory to use as stack size - use if you see: ''fatal runtime error: stack overflow'' (default low memory=1048576, high memory=1073741824)' cand -M '+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr−(n+1)), n => [curr−(n+1), curr−n), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)' cand --mtime '+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr−(n+1)), n => [curr−(n+1), curr−n), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)' cand -A 'just like -mtime, but based on file access time' cand --atime 'just like -mtime, but based on file access time' cand -y 'just like -mtime, but based on file change time' cand --ctime 'just like -mtime, but based on file change time' cand --files0-from 'Read NUL-terminated paths from FILE (use `-` for stdin)' cand --files-from 'Read newline-terminated paths from FILE (use `-` for stdin)' cand --collapse 'Keep these directories collapsed' cand -m 'Directory ''size'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time' cand --filetime 'Directory ''size'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time' cand -p 'Subdirectories will not have their path shortened' cand --full-paths 'Subdirectories will not have their path shortened' cand -L 'dereference sym links - Treat sym links as directories and go into them' cand --dereference-links 'dereference sym links - Treat sym links as directories and go into them' cand -x 'Only count the files and directories on the same filesystem as the supplied directory' cand --limit-filesystem 'Only count the files and directories on the same filesystem as the supplied directory' cand -s 'Use file length instead of blocks' cand --apparent-size 'Use file length instead of blocks' cand -r 'Print tree upside down (biggest highest)' cand --reverse 'Print tree upside down (biggest highest)' cand -c 'No colors will be printed (Useful for commands like: watch)' cand --no-colors 'No colors will be printed (Useful for commands like: watch)' cand -C 'Force colors print' cand --force-colors 'Force colors print' cand -b 'No percent bars or percentages will be displayed' cand --no-percent-bars 'No percent bars or percentages will be displayed' cand -B 'percent bars moved to right side of screen' cand --bars-on-right 'percent bars moved to right side of screen' cand -R 'For screen readers. Removes bars. Adds new column: depth level (May want to use -p too for full path)' cand --screen-reader 'For screen readers. Removes bars. Adds new column: depth level (May want to use -p too for full path)' cand --skip-total 'No total row will be displayed' cand -f 'Directory ''size'' is number of child files instead of disk size' cand --filecount 'Directory ''size'' is number of child files instead of disk size' cand -i 'Do not display hidden files' cand --ignore-hidden 'Do not display hidden files' cand -t 'show only these file types' cand --file-types 'show only these file types' cand -P 'Disable the progress indication' cand --no-progress 'Disable the progress indication' cand --print-errors 'Print path with errors' cand -D 'Only directories will be displayed' cand --only-dir 'Only directories will be displayed' cand -F 'Only files will be displayed. (Finds your largest files)' cand --only-file 'Only files will be displayed. (Finds your largest files)' cand -j 'Output the directory tree as json to the current directory' cand --output-json 'Output the directory tree as json to the current directory' cand -h 'Print help (see more with ''--help'')' cand --help 'Print help (see more with ''--help'')' cand -V 'Print version' cand --version 'Print version' } ] $completions[$command] } du-dust-1.2.4/completions/dust.fish000064400000000000000000000105301046102023000154020ustar 00000000000000complete -c dust -s d -l depth -d 'Depth to show' -r complete -c dust -s T -l threads -d 'Number of threads to use' -r complete -c dust -l config -d 'Specify a config file to use' -r -F complete -c dust -s n -l number-of-lines -d 'Display the \'n\' largest entries. (Default is terminal_height)' -r complete -c dust -s X -l ignore-directory -d 'Exclude any file or directory with this path' -r -F complete -c dust -s I -l ignore-all-in-file -d 'Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter' -r -F complete -c dust -s z -l min-size -d 'Minimum size file to include in output' -r complete -c dust -s v -l invert-filter -d 'Exclude filepaths matching this regex. To ignore png files type: -v "\\.png$"' -r complete -c dust -s e -l filter -d 'Only include filepaths matching this regex. For png files type: -e "\\.png$"' -r complete -c dust -s w -l terminal-width -d 'Specify width of output overriding the auto detection of terminal width' -r complete -c dust -s o -l output-format -d 'Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size' -r -f -a "si\t'SI prefix (powers of 1000)' b\t'byte (B)' k\t'kibibyte (KiB)' m\t'mebibyte (MiB)' g\t'gibibyte (GiB)' t\t'tebibyte (TiB)' kb\t'kilobyte (kB)' mb\t'megabyte (MB)' gb\t'gigabyte (GB)' tb\t'terabyte (TB)'" complete -c dust -s S -l stack-size -d 'Specify memory to use as stack size - use if you see: \'fatal runtime error: stack overflow\' (default low memory=1048576, high memory=1073741824)' -r complete -c dust -s M -l mtime -d '+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr−(n+1)), n => [curr−(n+1), curr−n), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)' -r complete -c dust -s A -l atime -d 'just like -mtime, but based on file access time' -r complete -c dust -s y -l ctime -d 'just like -mtime, but based on file change time' -r complete -c dust -l files0-from -d 'Read NUL-terminated paths from FILE (use `-` for stdin)' -r -F complete -c dust -l files-from -d 'Read newline-terminated paths from FILE (use `-` for stdin)' -r -F complete -c dust -l collapse -d 'Keep these directories collapsed' -r -F complete -c dust -s m -l filetime -d 'Directory \'size\' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time' -r -f -a "a\t'last accessed time' c\t'last changed time' m\t'last modified time'" complete -c dust -s p -l full-paths -d 'Subdirectories will not have their path shortened' complete -c dust -s L -l dereference-links -d 'dereference sym links - Treat sym links as directories and go into them' complete -c dust -s x -l limit-filesystem -d 'Only count the files and directories on the same filesystem as the supplied directory' complete -c dust -s s -l apparent-size -d 'Use file length instead of blocks' complete -c dust -s r -l reverse -d 'Print tree upside down (biggest highest)' complete -c dust -s c -l no-colors -d 'No colors will be printed (Useful for commands like: watch)' complete -c dust -s C -l force-colors -d 'Force colors print' complete -c dust -s b -l no-percent-bars -d 'No percent bars or percentages will be displayed' complete -c dust -s B -l bars-on-right -d 'percent bars moved to right side of screen' complete -c dust -s R -l screen-reader -d 'For screen readers. Removes bars. Adds new column: depth level (May want to use -p too for full path)' complete -c dust -l skip-total -d 'No total row will be displayed' complete -c dust -s f -l filecount -d 'Directory \'size\' is number of child files instead of disk size' complete -c dust -s i -l ignore-hidden -d 'Do not display hidden files' complete -c dust -s t -l file-types -d 'show only these file types' complete -c dust -s P -l no-progress -d 'Disable the progress indication' complete -c dust -l print-errors -d 'Print path with errors' complete -c dust -s D -l only-dir -d 'Only directories will be displayed' complete -c dust -s F -l only-file -d 'Only files will be displayed. (Finds your largest files)' complete -c dust -s j -l output-json -d 'Output the directory tree as json to the current directory' complete -c dust -s h -l help -d 'Print help (see more with \'--help\')' complete -c dust -s V -l version -d 'Print version' du-dust-1.2.4/config/config.toml000064400000000000000000000012321046102023000146220ustar 00000000000000# Sample Config file, works with toml and yaml # Place in either: # ~/.config/dust/config.toml # ~/.dust.toml # Print tree upside down (biggest highest) reverse=true # Subdirectories will not have their path shortened display-full-paths=true # Use file length instead of blocks display-apparent-size=true # No colors will be printed no-colors=true # No percent bars or percentages will be displayed no-bars=true # No total row will be displayed skip-total=true # Do not display hidden files ignore-hidden=true # print sizes in powers of 1000 (e.g., 1.1G) output-format="si" number-of-lines=5 # To keep the .git directory collapsed collapse=[".git"] du-dust-1.2.4/man-page/dust.1000064400000000000000000000141541046102023000137500ustar 00000000000000.ie \n(.g .ds Aq \(aq .el .ds Aq ' .TH Dust 1 "Dust 1.2.4" .SH NAME Dust \- Like du but more intuitive .SH SYNOPSIS \fBdust\fR [\fB\-d\fR|\fB\-\-depth\fR] [\fB\-T\fR|\fB\-\-threads\fR] [\fB\-\-config\fR] [\fB\-n\fR|\fB\-\-number\-of\-lines\fR] [\fB\-p\fR|\fB\-\-full\-paths\fR] [\fB\-X\fR|\fB\-\-ignore\-directory\fR] [\fB\-I\fR|\fB\-\-ignore\-all\-in\-file\fR] [\fB\-L\fR|\fB\-\-dereference\-links\fR] [\fB\-x\fR|\fB\-\-limit\-filesystem\fR] [\fB\-s\fR|\fB\-\-apparent\-size\fR] [\fB\-r\fR|\fB\-\-reverse\fR] [\fB\-c\fR|\fB\-\-no\-colors\fR] [\fB\-C\fR|\fB\-\-force\-colors\fR] [\fB\-b\fR|\fB\-\-no\-percent\-bars\fR] [\fB\-B\fR|\fB\-\-bars\-on\-right\fR] [\fB\-z\fR|\fB\-\-min\-size\fR] [\fB\-R\fR|\fB\-\-screen\-reader\fR] [\fB\-\-skip\-total\fR] [\fB\-f\fR|\fB\-\-filecount\fR] [\fB\-i\fR|\fB\-\-ignore\-hidden\fR] [\fB\-v\fR|\fB\-\-invert\-filter\fR] [\fB\-e\fR|\fB\-\-filter\fR] [\fB\-t\fR|\fB\-\-file\-types\fR] [\fB\-w\fR|\fB\-\-terminal\-width\fR] [\fB\-P\fR|\fB\-\-no\-progress\fR] [\fB\-\-print\-errors\fR] [\fB\-D\fR|\fB\-\-only\-dir\fR] [\fB\-F\fR|\fB\-\-only\-file\fR] [\fB\-o\fR|\fB\-\-output\-format\fR] [\fB\-S\fR|\fB\-\-stack\-size\fR] [\fB\-j\fR|\fB\-\-output\-json\fR] [\fB\-M\fR|\fB\-\-mtime\fR] [\fB\-A\fR|\fB\-\-atime\fR] [\fB\-y\fR|\fB\-\-ctime\fR] [\fB\-\-files0\-from\fR] [\fB\-\-files\-from\fR] [\fB\-\-collapse\fR] [\fB\-m\fR|\fB\-\-filetime\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIPATH\fR] .SH DESCRIPTION Like du but more intuitive .SH OPTIONS .TP \fB\-d\fR, \fB\-\-depth\fR \fI\fR Depth to show .TP \fB\-T\fR, \fB\-\-threads\fR \fI\fR Number of threads to use .TP \fB\-\-config\fR \fI\fR Specify a config file to use .TP \fB\-n\fR, \fB\-\-number\-of\-lines\fR \fI\fR Display the \*(Aqn\*(Aq largest entries. (Default is terminal_height) .TP \fB\-p\fR, \fB\-\-full\-paths\fR Subdirectories will not have their path shortened .TP \fB\-X\fR, \fB\-\-ignore\-directory\fR \fI\fR Exclude any file or directory with this path .TP \fB\-I\fR, \fB\-\-ignore\-all\-in\-file\fR \fI\fR Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by \-\-invert_filter .TP \fB\-L\fR, \fB\-\-dereference\-links\fR dereference sym links \- Treat sym links as directories and go into them .TP \fB\-x\fR, \fB\-\-limit\-filesystem\fR Only count the files and directories on the same filesystem as the supplied directory .TP \fB\-s\fR, \fB\-\-apparent\-size\fR Use file length instead of blocks .TP \fB\-r\fR, \fB\-\-reverse\fR Print tree upside down (biggest highest) .TP \fB\-c\fR, \fB\-\-no\-colors\fR No colors will be printed (Useful for commands like: watch) .TP \fB\-C\fR, \fB\-\-force\-colors\fR Force colors print .TP \fB\-b\fR, \fB\-\-no\-percent\-bars\fR No percent bars or percentages will be displayed .TP \fB\-B\fR, \fB\-\-bars\-on\-right\fR percent bars moved to right side of screen .TP \fB\-z\fR, \fB\-\-min\-size\fR \fI\fR Minimum size file to include in output .TP \fB\-R\fR, \fB\-\-screen\-reader\fR For screen readers. Removes bars. Adds new column: depth level (May want to use \-p too for full path) .TP \fB\-\-skip\-total\fR No total row will be displayed .TP \fB\-f\fR, \fB\-\-filecount\fR Directory \*(Aqsize\*(Aq is number of child files instead of disk size .TP \fB\-i\fR, \fB\-\-ignore\-hidden\fR Do not display hidden files .TP \fB\-v\fR, \fB\-\-invert\-filter\fR \fI\fR Exclude filepaths matching this regex. To ignore png files type: \-v "\\.png$" .TP \fB\-e\fR, \fB\-\-filter\fR \fI\fR Only include filepaths matching this regex. For png files type: \-e "\\.png$" .TP \fB\-t\fR, \fB\-\-file\-types\fR show only these file types .TP \fB\-w\fR, \fB\-\-terminal\-width\fR \fI\fR Specify width of output overriding the auto detection of terminal width .TP \fB\-P\fR, \fB\-\-no\-progress\fR Disable the progress indication .TP \fB\-\-print\-errors\fR Print path with errors .TP \fB\-D\fR, \fB\-\-only\-dir\fR Only directories will be displayed .TP \fB\-F\fR, \fB\-\-only\-file\fR Only files will be displayed. (Finds your largest files) .TP \fB\-o\fR, \fB\-\-output\-format\fR \fI\fR Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size .br .br \fIPossible values:\fR .RS 14 .IP \(bu 2 si: SI prefix (powers of 1000) .IP \(bu 2 b: byte (B) .IP \(bu 2 k: kibibyte (KiB) .IP \(bu 2 m: mebibyte (MiB) .IP \(bu 2 g: gibibyte (GiB) .IP \(bu 2 t: tebibyte (TiB) .IP \(bu 2 kb: kilobyte (kB) .IP \(bu 2 mb: megabyte (MB) .IP \(bu 2 gb: gigabyte (GB) .IP \(bu 2 tb: terabyte (TB) .RE .TP \fB\-S\fR, \fB\-\-stack\-size\fR \fI\fR Specify memory to use as stack size \- use if you see: \*(Aqfatal runtime error: stack overflow\*(Aq (default low memory=1048576, high memory=1073741824) .TP \fB\-j\fR, \fB\-\-output\-json\fR Output the directory tree as json to the current directory .TP \fB\-M\fR, \fB\-\-mtime\fR \fI\fR +/\-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr−(n+1)), n => [curr−(n+1), curr−n), and \-n => (𝑐𝑢𝑟𝑟−𝑛, +∞) .TP \fB\-A\fR, \fB\-\-atime\fR \fI\fR just like \-mtime, but based on file access time .TP \fB\-y\fR, \fB\-\-ctime\fR \fI\fR just like \-mtime, but based on file change time .TP \fB\-\-files0\-from\fR \fI\fR Read NUL\-terminated paths from FILE (use `\-` for stdin) .TP \fB\-\-files\-from\fR \fI\fR Read newline\-terminated paths from FILE (use `\-` for stdin) .TP \fB\-\-collapse\fR \fI\fR Keep these directories collapsed .TP \fB\-m\fR, \fB\-\-filetime\fR \fI\fR Directory \*(Aqsize\*(Aq is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time .br .br \fIPossible values:\fR .RS 14 .IP \(bu 2 a: last accessed time .IP \(bu 2 c: last changed time .IP \(bu 2 m: last modified time .RE .TP \fB\-h\fR, \fB\-\-help\fR Print help (see a summary with \*(Aq\-h\*(Aq) .TP \fB\-V\fR, \fB\-\-version\fR Print version .TP [\fIPATH\fR] Input files or directories .SH VERSION v1.2.4 du-dust-1.2.4/media/snap.png000064400000000000000000003262671046102023000137630ustar 00000000000000PNG  IHDRv&Ev cHRMz&u0`:pQ<bKGDcaNvppfJ&IDATxw|l^UNݒ,Y\1n`cc{Ls!@~BLPBHS(I0@%CB %{o%wtΧw*eǟvvv޾Y7@F̅sN4E>r FCɴ{7j1}^a<%=ln7nퟪdh8(N6cEg5ܾo페#7=c0 <מp9O\~U:?ydPi|hd ܥEd4 \ ԠvqW\{` _rpeH/~Z {8Q.yƜ܁5~_ 'ɷ֨QXVG;@w#yrKʖ,̺BwԌ̇^xӷ><+{RUIcY* `~ ً_ܳy/u,e$ЬJBp|oojUeH8nK8WߋOR9͠id}}RbU3 L5QO*H*,I*P,4!$TP4(cB 9✙`|5 ',ff+o6|mJf4cא~ƟY+Z5TFB8EH 2@D{'H6JjRr<*I،8YXzo*|$?Bm;.~93se Uuy0My򲃾 Cn4__gw^av洇vo깞j3\}_!)Rd+eQ}c}?<{ھ-GKfdte&c3>HO^6о.@V[`0ah&'/{Yv'篸b~2~_~{o=3Ya]t6jtޝ7:Z7>>gƗh>P*Ȫͪd{ߗ8Y"ibIgk3v8} mW|9g$~srE1w.ek]w"ΖaO豫 `0ĹYgܦOOzgַOַ?\޶O~v9l 'dݹS0/ɟ?zQg_4a~?s,8wXoV'WAшdkr.fW'@IlZj E$HE""*5 A{t#(*u$@xnl T3 m@Uxt'LuPD!s}Y $Fdx+6[_H6p%j!&&$S'PT>a-F>xEwE!kP81"wɹ¢u0YU!,](=]{@5䂼g<ϟDqrS*)hjIA'PUԞDKr=4^ :aEto8vR/w#!O=3}򡷟}%ߙwڸ@Q}AfWg\?W7ݳ3 K'ʛax|+f_|aɢ Ø Ly+.ߛ_2P,)sTӐ?~\*wKo.se 3{f;n[ϻO_r-s-iY1 S-zj`0L*./}h'MM|{}Ojm9r<_Y9 j+o}5^W]k./-CjۯO996wo7l%b 6l%ȎĝWM+-9@, ]&)*[RMeėFs-BˌTlz:3eFBs^U }8=IFHDLv<ɚ:q %7+&ν|V&H@y9/e]ס('Λ%hi빈 F˖r7;T >PhN9ݏOr‚W[ɺF˧Ҕdm=Nӄ8PGjAU㗥#d˳ZYz:tq '8еqLzWro}'Ww?Ͻ? `\|V=/o߾{)) [>ߨ}:Z;ZSy3h8RWY]g>tC;&7{kAi߽V  &w/:# XTy͚b5~v˧o}/Y}[`0fb9Nkeh r:ۖ]qA]ןg~ɸzEQ.GԲ]뷝},!e) ˊIy}?1ןJNL t%Qr 4/I. AAU ?/]fP,y=a`#z^jb+.}}Tg|2S-f aV<(B Ѩ,B-M/RMm7fyԡ2``>[wT]5Kܜƴs .xXV8s JvVo’>sWT@n% :Smd}4u\̆: ahJ%K?]z7~9iFR:'_K䳷?׽ۯ~ `:lyv/ vemHWr튕}mm}79TTua6 '=8_iߢ `C3A0,C-rfNٽqSjSql.;qK8½#4OW\:ub]eM$*jҳ3V:Э;& Q8jD/*x { QP$RUAl ^.Q~ghtEL(0\Ry@$$DVл g+Yޱw'M DgxtG7 Q_j2jZjtœdejA>&d!_fp<%3sOְ/; CACՏ7<=Oȶ5\7"b[c ]w/|Cw[iwwKl ]8qv+Cٙ] Ѥgt=q#K.;{ou; ŻEsx?TY\0 O䑺E3=0o75WJoe?쁞6+[ {(,]_ H6OFy\>BrnqLJpXV$;Ϝ Cv_v]\4|#-gg܄KNKpح?}쟺峍N;%~i3i,ބ$?m߿:&EoѱY  |]kۚM!C,5>r!G#P+.yD^رn"ˮ wŮp OO|VT@͟ hnww.;Y!Kr|Hx-^u.e_i,hX*~"wfIoMQDG.ff ^ J\dKbd A]l`._Dwxt@5Tq6qdI@fI|D!mgLhq)jKo%Κ.1O9 |_=Dُ>/<[V: 2*Ø~;1k6u:&޵O\dV8^,?o\XQS-&-NF|EQ 6T|4M;##͹%_)E~W~߼;[3r<_G=dħjl@+.GȚD/`"M oݗ_o 枪MZ`AQ0$,]U lL9Qb>__t68fՇ޴MΘ,,`(ljU<(#̧_һѲl[܊oF"}٤M}6T//[55=_W\r媻c@w_|~vٍ=poXI%4HDRd$~G f8Q+-$)/^?ǟ냏೿B򆻇t7Y/I%޷yQ웑[tl`0_WPQQdR`.^|xҳ2ngW{gWw I9voB.ѐEQTu5߯eTB+6E$"$&#$B! 'цݑˊr ``sX1DA q)E1>*=qS`P3AȦmuh>yo+'PܮIsw?Dr$IBSVbA_#֟=hKNT'T1~o9&o) 嫪Rפ(JnqO=lYw(F`0 3*Ps`{}=;#mc HtǷn9>HlJ@'ó3[~q$E-m_#Fڳ8['ܓQ߽'3ֹ7{"+ FV{c `01pXDO<ޢHTIG8$ {/CR& [e5$='k'To̴LZ- ` `0 `0LV`0Ǎ=15X + 뷥xe7 ` `0 s&`0 xCbLI(?PagB1֎ZA2${b?hй` &'1Q? `0 &E0 ˊ]C#iHފSB@9:!4O:3"e ћb9O{[dSX `0$⍎Pu|*}& N9x}_'_0w/?/_Ǻ3r<)^zwedgj?Ϲ/;wN> 7QG! ګlU}`0 ״((-L1ryް,ɛ<31X YT%eK{f]wXjFvC/<[=҉g˿.\([<3EI0[~d~y_y6TT `0 c`oO{Itysʦچ% 9yWGxx$)߬^uۃP}_t"KoRGQ;[;V)sgL=-Ԍ\ώ[N?L g9om,tͮ+O0OJQb I4~)f5[uJ 9-#̑iO`0 䠸O5w?zpHCչEyrh$jYt5?gi3 V\5^%jUKϞ2g M5 ǥLsuٴc9 hͻrQv1(.-D ;s=w\u?$ PAp|N'A&9L(?_tF:lISyӷ>ZxY3u篸2.3m"}UqGv ոW?\MvgMT[yxJ!y`0 ͯ~{o}gΙ7;lb]y˵+V^շ7I@xgz"B}J3l9~}5GkAڭ^GΚ#[0T{*4 ,Z@x `0 O/ggߴwe 6n=0.~˛~ӣ+yE C%]uIRה4UeQ. {}nrRX)zFB{cfbDxWUBpO`0 |Ѫ{,I˗qaI\uꢹwyhm*8f¦a 2gx%?>֎sǃ.GU>˸:FXO<,⶷VAD̆:EERѲWGWW*A"El񭥧@/,Cm!1&d'$ YS&,0B*>'RT(j);Sؙ-ΦǺan-=#V9lc_vE0 s,(,+ve;jUq &caYq͡j.rtX"eIvYC0 (ڤ (P[ex)Lx f0rX舓tV=Ϩ6@S $B2 c1' &{zf )Uɝ T5ZFKl [Ks5N1Z2;?1>j;nǿBZ=ZZjZ cEw.gӾW- _5Y5皘y{k ؈hPXRM Iq46}Ka[{2mp`0̱`?}ҩe3W8 v_>ppBg^ ~8Sn;{.q&Vky]@ $\cV.1K!XfPj^TG0AM2µK?YXg#hvNmCM!VQ8 _X15O':v($ZzFȝ;#SC IX۫GEɚ}eJA09: g1Q!Y¡jRyit܉%FȪX3f, y=~3f‰ `0Lj,}+nl(Tۗtן~K KWϿ{#o=s9yW/Y1`bXD 2lYzgdpEZnJ1 P9 xP 9ph&Jȱ!({lI*"8kB@ ` b Y2:IEHnQEhDF|B (!†ڼ$LGiT!%EN|X?5"jB/v_ĴڞĘDUXRٰ{nPBҠ)Gf@ Q*SΉس,4 I+R *4  $:6&JR3Q! GVIJ|mRS&gMG†:)h?vvBSq&V0m\w!kb!(ʀKbci)JxH+W09,I@UKbɩ$)rtOJB*-GGW);XLeFޭRl9I M_]60 CPs:(V߬94wa@:dx1LKKB6BģKq&C vlb%Tl0.AR嵘`zQ0H˘V-C0ښʗ+_Y/Cu7r3yX2@TJɴ@M?%d+i/+m\FkS RgBOά\T%A)ƎKC?U_cNp5B-ڋiT4̍]ObZvGAs U:YRC3@<X;k"vsT)OMf}]%m3!w>𩹻)!U!y",Hݦ'Al0|JTQ8ۗUi̎kkUM6~>R$.^&JH)rzVGOPn  n{S{⹁̒&]%-W6ux*ګڋ"Ee}_ޭ6`0w) "RfR4M|GXpz~#Kf1#(P+2ZH> fS|XISv d~Oi²Sm5)r&_ )Fn/Yl;f '#)E\㶿uhMu-LfRcgbu~ ]%EQ|8.h-=;<]V2?Q |-֎f-"dCڿ&;eO&_1Ц"+w& hcǸYBΚy>xcx dyri.[nJ9~Lok2iUru~ˡ#? 0[| ͠6`0A筸dGkh;mY|e3g.gΕ~DB#]1d/FuR5,$@u2 Gʨeo(mc 7wǚ2anZ '`$|dr@(q48[s *]ۧR7cbjg^.ӆ#0o.Ќ[+M!o޾̺BbLcHS! (A]Y'qd2K]{5̆@kRa F{4>) P9l;ۿo֗}vmM{#/e&8SDz&r%]M0D.C{lrsڙ[+lGV L\Yܱ]4)>Ls!`3Zn`(5gr$D(!2mH Q BU%ĘR(FʜM{zԤy_l8M%ȤѓT(}E[{US9k 92笗D0G7ZB1-0w76{^ Fâ͟.lݹjfW ;jF&9k6_N!rP S^4$֌T>O[GHBҝ3 aHb~_ m|63?*YӵYm:{ rJk9Zot6DmVyFfI6`0•\bw_}[{Xo>OƷV^|ޕU/ڸ} ĊP/A%tU)aTL) KaQ* ͬ$$> pvgLWhkqi]UY0+3!5 zafC1#Ndb/"3 Iʱj)-ڐQ|aIgě`nݝ[ϗX3͇<_9u])Dqoh dh^oL!(%tf{keؙVfTgȴ)r[/ʬ\FD*'% vrfrEMOycJ]}pF NΒL/4bSflT$`3_ևM??Zu{/%9w>vy2. ᇻh!}b I$6~dawȝ(2әqDKTQ  ĚSHZ ollڏdyYڢxzB 2<"$ո*M΄XI7Nw55:&_ orj5' %U g"UQѕ?ݘ0O, CuhM0wyIYDkdjį籍R4^z^Y$[T7iV-ja9T r*"8[=!oGyKmoWm`}7Xו~)Ack;Y҂EڴFmBl_ԡFBmR_* N]4GO~OtS&?{^{>Y-6q劣+nˡX&OPІ ?@c3G09HV@?d6G6p1[U+2jPcä8 O+i㣓) iۜ}&!E2Z+wJؕzpb4&)V"$iͭ'_Vx={B^mĘ4׊b^EDk#cj/ƽ`Pu5hQUCH I\=O`yF^4mi)b k,^Ds!C Eǔ"ΜxhиoiIm5 x[ wD}ǴkkJmJ B$ѕ.)Ac8BȢLZLkgmb{  =aKһڤT 9XzϾoOYU?[P`6| L᯶kLa5aȞ9ިa5&g*,m}=i5[PLӤY"dA!O2#l5kMWܴI: $A֒3RQ%̓qO˄E"ΚPLZkGɐԶ2Zuc5rG09j%mwSWɔEБ[0C֝;%TZtp42YUDȴA jVڦdIp7,8%I1BR?xp7ͮK .b!x'AƛiR"ퟦWGcڵ)!bj2KNru5*}P`oК1Ur&g!BSy3$ RT 9v,gW-(-w|pmuu|RT4q J j]L 4g͢B-2<"X1+Y7[U@*a!@(i bx;Y}6IIgYS9>m+YYZ0@0 VgӋIe~gᬈ#K!)-o:,.5kz=rT%jC h r(>,lϘ[7UkGZ3DE˕U6a ! 2 тƅ9HӏlJwuΝbovn!J^z Fxk3H:z[_ڳda#)*$=Ȣ-Rlݤʝ*SP$=f}iagf#UVvx4)1FJJ)kɨJgh9̈#1&R ZRҖTZNIJ[=+^N͐/JX*`0ƸB"|>xfn5+yw~Ϭ:[HKDEEEўҸEN@2v8hӏ[؉Y.B"kVHE h ER cDB%rGd 1룐=KY`Rd*AO ^_Eh9TӼI(2WE {[eZ61BdiiX2+ LN`]3Ev{|cΒB^T7"vdURњC"Ň,46hC&piRb"kFJs!;7r$ƤP  Z.1&WF}] rzuΙL moSc^*m2ExnSb_'`0{[Wu*= wվJE3p TfP!HA\f* !KL40t夹,JOXLܞ5A}f5 KTt_A.jr. DG*"UL42LOHCkt kA7}MR|8X=,caZNȕQ4)62nf M1o'J* "oHWP_ U`0D4XekOIc=PE*Iw֊XKgMm`0 lba0' +$}g(H+LD\('URQ0e"㰢kCKiQ c'm h` 9i޴Óz,j1C"_~좪ldl H`0 `0L`0 `0QX `0 3j``юUr\χ9BS `0 0p²bg=T=eI(($ICTy9x*׎0 "k BW@ do-=}a鬍8ʗDov68Tm ě]2PB`0 f0 ˊ]Cj`2"GĎT5e]f]q{SMCeˮ8vޥu5W[e79W]x_aU3 ceOL4?ٜ5]Dʵn5R(VHJ|GQrbzHyI8S U]yScD1_Vŗ'A8 `0cęSg lSMý+n>p.3\}pϼ*Mpiv]& 64;mgMK1Yw5}o=9Z\^PU=x%X1v֙vU>ꨈi I_B$cb?5D79UD y$ esȌQRćAYUԖARؙTTذ#yLם3vc!E)V%){H'dI%H7R)'NSZ妨I1 &JQ?) ior$E<}SFlh4DT{P),mu~`0 ˳YX}_Ѧf{jUUw5/Jo=lY;Ag}[v(q'?GحSRs6#'W"mMD/izhnɴհ;n2aa]JіH ;s'.9z)ƎK#To[mA+L4XGdҋvNʛp}|ݹr_xn}C#Al0| IR,7&^<7Yons5ۋҋKT}mF[Q[Fِ`׻@s!yt!ba0 fKD6+؝,0m7=Q/?Xyaߖ݃eq:Zl1 ]O(gw;q.-+RC!ʙr|-o=9Y %D+)>LsAP%Ν HULɶ Ťle¾Ij@X:@XΚr"'eiYe)ឩ9~Lo"Ue`m%ŀbIH׆Z"vOύ dؕlkb`0LA ѢYXeg(p;W? GXʐX?}x%~iN>}룅U>s͏]wK,r޷g1UJ-n<^TռkS/Hk{s}9b3LmoFm S}V'x)4Rotp0{J#,r+7MM jhr.h3 NK9l`zX{c|M[Ў[KOdV{"vA,G#g[+26WJIKX*涪v\%xh#FvPUmb)iY1R̪ ZUb0 `{o-(-|wj?/x/=93ښZG^ʐMg~ߟ0}uwduj4C&׾ٝWSqQ݊yl$xk 19%v [RebWpr64vԸ{K($Y0ҏlֹB1-0w7vv甇iu;x79vxT:X:ptmnހQyGKM@saͷH$ 3&T qʹ;`0 Ǖ\bw_}\ͧ_~闳_ʋoZۻ r_}]!Xn-mٽs6u|3,V (>o7HhƘXާѢ'&LKwnf}YeQ8`qo4lizOژkşY"m% @ #t2+Ŧ/fy*w*sC094'!0僕L=]w^e:u6t $! m SϓicwvyƑM(`0L/_ևM??Zu{/%9w>vy2. ᇻ8rY\^'봓/\٣+X4GPGQ#] C@FIԖ͟D֎#+zo4@O8ڜ=`kn?,޺/p$ *A{TDpJ-YJT%Кg셉 Κ, d-Ra')WGܳ^K L[p֞&m (`07Sͽ_GS.]0}מ|Ou{KMܻiΆDZ.{w76 }@QfQorfT &GđR=]ag͇Zio9[Y1+v3fWeD ͛P !u q-,KНSNHBݍ%^4{ܭZY}ٓTD"g;j(4HUM{ru4 /VC%悢^47j˔iLR׆>5 wihjkbF`07zS}4ܾgx4 FBlvC0f.=*j~imrk4vAV~6c`ݮ r 4SJ|8h&hBsAkǑ`z/,^GSOօ]y^=*B,7:g $C2jqq PLkɂ΂ I(RtQ{&($]=IB7LJ|H@МEݹSb[Enw6׎m՝fj\9!p0L`8䬞TŢ'-WG1w5]y]y% IuvYkϒ) LRm˗$E!BCERd{k `,gW-(-w|pmuu|RT4q J j]LyNFRU w|~O5'w?z6y@(G $3KdO =C{[`rb~Rbp`t(S.-q_SJfՆі Q|XY$w/,5hR"lH; fl90N VgӋmBksx}KٙGbL#U n}=jϒ)#,͇yS4ش3LԟQiWa0 +-$)/^?ǟ냏gf]}G;̪3/^DTTTT6Y%hnkoԊ6}x!+Fwب%,ĹAX3 %r=!$fdYЀ)7Sp4f&bM+e 1>&$ƤP  Z.1ĂDnvs,?LѦ""8 36wA{V=5[q3`\-+O9Eܞtwj_H@C[ ܏)Vj@Y4UU.Aڼ7zؕGRdmޣk`3iRs d5L9~Z wFI'Z`0 Ioku)k fYD-{ku0F6(5C;c g^?%d `01 60_[*T6 FBLw56sH+LD\hH;c0 `>|!dX޼D `0IU`0 `060 `0 f&`0 x-f#ov!E6[^,mD|C rxtđ-&Bp Wb' 'aᵍY Ixi@=KK-VtPdSvHÎ@ $H !l{E7dȀFY-eg;^4 )ÛHpR;ĚyEC#흳[Kψ]co0ojP*"C}ᵍkaR%Tϩ􅑴ؑP[{URkZXso͓,n{kŀ&Voc`01ؕ9T &caYq͡j.r43I4=u`bPpI PRs9QǂDGTyF¯¨WԿ&&59 ɨ-p6gM~2рrm]ySc{j1_Vŗ-KI!)Β}Jɗ=I0ӫ7|hHOڝ3 "ʵ΂St~˪Xk5͇mUvN`P5Xh1ЖpYK٢`Z!)ś^>BTyحnl2B#`0ogNcN-81M5 a|rÅo

pIْŞYםv9B?BKZ]feSmÒ¼+nw<< *b6ȰGgT蝉E S"h)/j Ӱ A {+orAz^"v w%쨫")$F|*,2du;UBҤR̪LwBܦ2ņ9'J]rPTQ 6:Ui`rh'dgȨ*Rc 1JpRjV"jB/v_ĴmЕ;\{jS{denO~dyujAͼ,`M"6;g0`mC *A"?Q I@S H%f14 ui*TZlr%Yӑ'ՒDUXRٰt&{`RgeUI )r|;ԪesRi]ktfh4 1@0 sBx7c3~u?=T{@͡ܢ<XwPHJ~ P%!Ëa^\,&]bT6:eck,d˄!wA R-In0(^eL[#mMD/,ɡvwiΜK7|[uo]؃Dyi.Ds=/R z 0q+Pb ƽ@ȢP{ќ#; @ csw#`{&DmI&i^ZV:!El*m#Ezũ:2aQđsٌ&L!ZH*USҰ3hPڭ惩/:rUDteiiVIKbZvG2fW UD8ZfToJCo2wNʝJJ|W?(U7b~l`zNbYL QY>~۬}-CEG!owj`0' ;YDEyG#Q vi9 'L wX9&Si4V;:oJ)QNa35>ʙ3Z,z&#$!u "kfve"`EbR B&À7Fl!4|To *A]y~Oހ y N d悔.i.d MM>L4E =QG[%mX_b76S.I$˫_ 1\G@fI|l Ťle¾!:A aPIΚ A%Dk!T )@;)*AΉXԑV_n縙쉀k6TDtNg+r&_ )Fn/}!֪[KOWaj89/TY_fKgY5s ®\㗁 -!6ܥ}@ w甧Rk=j`0' [qɆh,v۲ҳf\8{+W#,e^Fs$:2KߞcĚC<j>%khYj[+36@#@fmKs}d ƖI @nւ{s_[ңN3ow v(TdU^!H~]bReR@0b/X yC|ΒV;R{kH 9;]]s&Ňi.$c^;,Z@MaWDE"dQ ugBQjܗB] 2cN 9ORMRbl74dToj,_&1qדOҼ:`~_ m|63, .ԟWmh=ځ`rh"Qɂ4"UmCJM~gn:nr9k9QH;T쟗E[{US9k qHr.L&sR ֞evv甇iu;x79gOYyRkgMđ-m*A&d0X ͅc5`0ȕ\bw_}[{o>OƷV^|ޕU/ڸ} ĊP/A%tU)aTL) KaQ*hH}-܍e F{G΂ƽQIFB :4΂]y0izF~5ʵ>N^ $! m SϓicwvyƑMǮ HYb I(Y,j+(rwd=ykx2{ˡYd6 En+Y~Qfo4д!2@U#,!IʡJbHB*n1s7vG"fS$ٓhĚN&:2K=lo)L׆w}b%$1fq~*`0 g??G~ŷ^$=o<./_E9{.d*!a_iJF@R*z1 ͫ1jr2J ot&v=kThBkRO!)llڏdyYQ[F"Km|x*L\_DIDl;0Fk [q0M2̀5mb|@EDkO۹:NjH ovɔzJRY*I?~` #^rD9cbRԖT#DGo:wRn֡5 ; {4! o- d7/fׂRҞudf">C3K`o98Jh> $a$M"&1 s9u;?go?uN}?{ɗd]wdĽv6X&OP%bMDؙCa&⳷V[+kNhrw҆C'ʝQUBLb k a;jotbYT?Z"5 wiˆ!>>dXk&ov>IR.ʛUL|Ye`CHd͜5;{&Rzքݚ[ӴYO3iIk!^xB3Q[Y=s I~l= r($mD IY?FȠzAGv*AӋ26jkBTw-q`yJɬ[\pswS]s/~^ZP(nES|X%HmJvY00)|ޖ)bm 9kmIkAGR2:;:EdGk֖wI$$`NK.;Ǚw J c']>_g7\sum6M{¦a8k YE@ υx/wJV|*VmJ.(.DÆCr)}񻆇zR%2j(R)P՜,,MHسNw⇾+%b[?N^fi>̛HLԟQiu *UW(F[DafvԈś?C4X\YkcK}~(>lnU8ev8b j o(%˫[ "kĘbMR 1` Gm֎*&Rd` 2sX}\ I1ڕ;U4Xh"i.$4u6 w9ZvgwLW1~-ˬ/~{N*3Rdg΂,:[X5I[, pJPV4LْƑg (ƕğ3sYy˾|uxfՙ/WG]"***vC*W VƮG-xNmHd HsѵU mOѹL$ƈJbCUΒB^T7"vdU9O$Sh  V |dNBp60iIҚj$,f4ni#ocyhƃf0Ǿ84L=?j˰Ul-h0Ty2ɣbUYJPǢj]V=uuTع~0Loޖ'`2nO;]RQo m-B<U%T5FC)NR{]y!EfYł Uex3Y)#IΊocyPBDJ$yjAGy2ɣb"oq׻=c0 ֑|m!dJlšw `0 M,̘&Kj`)D|',2 渑yxBң>ˌ`00&fCȢ!z`t`Xf `FU`0 `060 `0 f&`0 x-hLN&;<%{b?h89 X;j `0'C3Bet[{zPd%!#"PI"e u Q v\;L,H !l{,Ȭ\TPUn-=#vV9&NqđT $zn `0#ؕ9TGbL²C\^#4U`bM;gWex$AY{WTӐpٲ+οwi]eMU?և~r٤mpUׇoXtU.<B9!K4v%Ňi.ݹ2r#vOcRhU5&_3Z*\dQ|ψ- v&'jZVIo d{`KE*r5=\0609~du\P!qIk`0 A ѢYXeg(p;W? GXʐX?}x%~iN>}룅U>s͏]wK,r!u޷gT.pLչy2oW)$U5 }kc˥LmoFm S}'5Z=޼ۡ0̝Nh.8{t!Y*v,SLeqd'D)C.59u9 v8I)W3O)(fgp5'{35`0 ީVx㽷Shkjy)C6}O^Iqiwj{h6M<ǯ};Hsm VW(⭅R yC|ΒV;R{k)FS2q1+ww_LhX;j\F.B1lןv&5#ʑhH,ʴAkĞB|C*1^GӴhΚ#2Œ?`0 zs-׮XyWߦ9x|6VWxa.h&Vۿo}[v\oq/L?a>}{ Z1&i񭪄, #qB2h(Y0ո7L6q=Ugrm1c$Fg̮~G}Ib $ki4'^R]BHpzd%% `0I/;~y6Vދo/`Izx]^s]9Xm,./MxuIV Q'Bk0Пf ($=p~UM?հ;dDmLo8FnO8ڜ'kn?,޺/cZDfJ79])$ݕ;M,5O:+jH,KW},aUT@CQR\ҿ\Rzi9Tj`0 ȩ}ɯ?{s.t䇾kO'۽%&4Oİ5[-vÛJV(F (bj];ʹykGV-#LR쭕y{>.G-ͮX* ؿy!dS.!fHdFd@W+/I Ӭf &% b4Z,]3yk {T)iiq۵Ȅ&fzuL}l"'A `0o Si}V?h$6.!8ŚpN8:N.ʭyYٌv*okG(L)ﳁF 6F Z;Ӌ|Ye"YuaW`WϹA?xs Bh>$SĚIP(dAgLTDl|)aQLPHzձ"ozy2livB1M"ddh>X?4Ě[J0dOB.2:r&g!B䋶"tJ\)679U!dh9uNeM dѢY'A `0o K.;Ǚw J c']>_g7\sum6M{¦a8k޲ӯTU%zߺ9~k߾'h|Iݏ|ŷx`4( sx=) bEASmU*SHZzG\,$bxʥet6tnT%jC H(>,lҰ= ‚ F&sWޮ; gEY I)$2e 9i[zv`C?1敢ЀGYBhJ B,&T[gӋ»wg(!~dS1 `+-$)/^?ǟ냏gf]}G;̪3/^DTTTT6Y%hnkoU Y1F%(`QA\l3 IKT9B6$fdYЀ)7?2\@II%(͇$1"E!d9bEiIm#Uj$_! F;Rd&K\ʕB b0K {<ꩯvߊdm)^yBF)3U*GZH(|h~܀8HoTBMUU-UX'R $&:Vȍp]R/T;$B b0 `m: k `0 ` `0 `F lba0 `0̨M, `0 5`0 `060 `0 fԠ 0#Gbمo<`r(}ǚ6BPUKW=%ƦTՑ7; E6+DRҳBGĚI`lk!!mD|b efb! ,H ]2@=KK-VyM,HUo+`!$ wlU:01 &;!LO(jCD̆7@ ڔ;0R9&0\ǎIgqxXZzF짽2$R㳅zqd)SlLzg6`#ě]2PB$Iw kg^ʵ`0@aY+UsZU0e5}PI%ye *jb'@@m07BMcA#NY<WNa+L_D Q `8WQ[FGlΚdOQ6@x'!]Φ}89; NQ (!\ PLI~92j1Ж4cTTc ګ ̕iΕ!w~ c|)v܎=r]OI2aLș,ix1%3_Vŗ9`01ęSg lSMý+n>p.3\}pϼ*Mpiv]bu)GAEpWC yFuIS*DI4B JǜDYu3.K8dŜ5%&U 6ΙY&+)$Q8+iTd~R$<,5'\foHۑV^5)r_*59 & l\\GsA:jy⒐@sȣbG~%FȪX\}󢭯|# 2mLK!`0o& YT%eK{f]w -i*v皕M K.?r ۏHJ+FOT ytAyGY$`Q:ex+mv%TN&(%zDBkc!ɩ%rl;Y[ AE@$"hBĘ8k:R6I GSUDJPN4HeWHP98圈=k@a"gV}ewv6/)6ȱv yeV,݀P9kz0m!Em FW4VdVmTH~˚V T mm~HOEW hCͯ$% %D"@U)!{QȌH!TE~毊 gvBI{w*S*"8kB1=b0 &u_]60 CsIDATPs:(V߬9T=€ gIub mGU#B(MNK2aa];ThkL$^L/2nPQ8ۗU 8k6C c|y*A%DTdl>]S4#}k߹IkgMѾcYuql:6fW UD8ZfTomR8RU5sB&6t#ovq/h9^4'N3y,@{ޞc}]i.-EY9?1w7Nv˚~d&GGI,ŵ`ݰ]K,{&:3+kTD dK*vV/-G0Ϗ2!owj#. O#i/+mGUoawܩozE{fW݌ `N)@J~sBvv@smo~*AfZYRC3I>ͅh.5b_TL4X&`0ANQ{;E#Q vi9 'L wX9&Si4V;KbFPVFe:}"xX+gkx뙈+@ TDt%zf'hSѕ;Es_ȴA%(JPBDbL*Anԕ?]KJ<Dewݎq:&~0s'ReHϫdk*"] G6t_3tL+t[Gu*A4E =QG[=" Y#*.pv $>%@%i;s8k(椚ԗ*dpM&i.dnx(> yg-(K$Ć!x ;GEEs *"o3#?6"SP|8.h-=;<-{˗ F!5hKgY5c®\MQ'Gׯ~-$:z&d悱Fs4^ϓXsđ"ETL}_%Ȱ+)B!!ؙJ֯}mH4X`A ѢYXeg(p;W? GXʐXy [CHH 4Sq*K=x7}kf%R [Gu k>2k9 v82meO=6~SL2[+M!o޾̺BbLci2 =>){Jy]#6(jt.C#Кg]09Zig<׳!oRmkR&`OsB&_S7\ǢDhJbL6ERƑJ(}'$aij$դT-GGI"dp[O2μ\ GfT^fh|vđ,u5i3zkWjR1)`p Xs*w* Ds}ŭB`uR|Xb!WY W$֢ul5n{3kto<=p8rB3$v"$2hwɐO߻־Nr62oW)$U5Z `7{kAi߽Sͯ~{o}gΙ7;RlbC;EKg /Q$@Ta.1)2t) k,!w>gIy~$aAEŜ9@4 V1gM׾*$՝]*A`v%DA*)D%ƤGB1-0w7s=Ɵ֎Wԟ"12@!r`X3R&צ<owFmIJRB {a}d0]ߗiov="ki? vivv CI5d*Rq֌+GM"(ӆx~*Κ#[0TڌnC)4|:-Bx"l֝;9vB1 ٴ cP۝Sr.[2R9isK!Er' 3&T qʹ;߉W݀;i/-Z@x =+ڛFT~|6VWxa.h&VDz %T-ۭJ9 $@͔*Fާݸ[Y_V`w,j;m9.bڄeB AlvMmKC/GP0m\ϨS6>o*ϫhĚcTDtf{keؙVfT׆>,h#$hk6$d1LWē0w5[Zt lV`U"vL,:6L*Ug̮~w~j$j#ahRM<#壥SB@@25ݟY({N&mI`zNEQ!w?TSKW4sw!a6qE5;yĚ)~1 3hǟ?:wIL~>K_}[2mM:P,'`h 1 &X"LR쭕y{>fb6G )X` \0;HK0HIHlN$19l;ԽPy!d1D~HϫYtKy['$\iAsȴU=+:zlqk(>LȒqd˴!uZ_~^}MK%Ӭh0)Si/kmHUbA4_J Rl9:hF&oM|tZZB0k)aW^GLO[Y˞$Rl&_>SIۆ~s*dx*Z5gB] gMEUkW:;iAWqz`0ح?}J~G#lw a8/xJT, NUTa_\ 'PE(TbŐT`q%9kFX{qVMIp7,8%I1BR?xKU0Cӝ;;gv2v6 dσix]Ss&SbTgBs>*"oz9EX[?BΚ-8%vf]4q!%S6DvԌ\{Z&, q eBTJ"Oa 5J3ذ7j$%kDhncaT:R|X Rz!{Rx#G?5F 8ȖX6vJ®icFB䬞s5 D|| jˈ2jf-W!-+fKôit!K,($C.uTwt4*Wm€zNJ[b-:kg19#]yS}5/SIC, &֊;i@sAkǑ`z/,^8M1 sLYr9tׇ[PZ;:ۮlh64!Xh*EZdx.D{cVzכ\oVԷ"h4oUt FFeIf4`݂ѡP Oxg>wqrUe˶ٞM6lz( T@DDDW@E^#?)^ BAAZ{ٔfz~dl |?d{'s]?M;Y5[4KKw &6_Հr'B(a+Jxpm_Auo ga!/n3žBsNj?հmrtթd*?a[HLd6Nj5o˭8Q%u95}-y V{ZzEiw:,ٳμxۆ8b+U* ^pRǂ9uVߡmUD\>Ml:M8qs;g]{J!XMίy eއ;^.;57XΌ'az&i]:UO۾̟HMj9 =))$sdpJ4qGWi}E3]3Fzu^l$NJLqڧ;wh-)yStN4u)A3GGŧ|w=Wk?[gZ:H[$)e[$G߀>bO[؉UvQE ЄUNg ?yǫp"}cml0.{bl*zH/6._Pǂ:,58%&Xa0©A~n)R)ê2 |4Pwe`6NhE,C[7XNz@ hyU" qBD⤄HA3CTN êaߏ mO}Dc9?c#Bdޒa͘,_NVn= cQ.܅LY? ELݮ0G~f˪ TTatuX+AOɜUcM4{(ޯprxq٫qJ2uX)nWv'Uƌat-CFRz89fAXE mP8ASDO{WOxwp Q FW 5~G AAD*|j?qxbq(D'IRߦށCpA1*|`tՌ⍽6L9gA AAAd@ AAAd@ AAAdXeSz_4>h<˒  IT,BHYU'S_S;R6e T0$"˱glG(Ͽ|?<T,Ukd177l)ä!@M{g0̏m~n̞j?*  RVU꭫I Bj)EW9NUe*sztUnOST^j5 v.{ZxIpw+ӗwv?sٗرӂ&X+v#/ը3촘#*D&WMY}Չ?Fk:;]jrņ?qQm<+=۴3a?  'ig;+gU)1YĖۮQCk*Zs.Bp?uƼ.Θ[^_(>c]an $ovpyE>08z[)Jޢ!o eKQm4atMRiU,n8%*{ 5u2 dPSe"'m  rbY|wjRߩWq)!v-M. ˊv78 +VoG-y/OՕצ ]=|Emn:s9/= p-| &6X^%ih V $DCScbL RX 7Nby?:1T/91.ttץNMnV6w~ȝ^y*c,)ԝ@0{R_ጡ3X>Y3a15gCMRJN͂B4`my~ĕV6&h`*9Tɮs"kRUc AAɣP3vߡgZ{o5] JڢbXW_S7L{ u#pWܳigꡙ}9/rZ8nm{89 MlcK+AAaW^h>ڴ/Tͭ]\u˟~y_$g+b_W_ֳgXʝQe\sϼ/b\ݳRٳd{Z4Xt>٘ mSϦdE>?yR{w]SnYM>zo¦,s^frϝ..P,NʰfC1Gn[V]yIO{  7\uknMfs5yz_/)xb +/n~_Z̟ر~߳iM;/ťn"h,vՏ=})sb>M+P:ZZwϯR,ӺK{wRL0{R\p9QSShJw@8fyK_Au ySt^"1e1> o弃%'/D/|>Hkqĕnbo93:0']SnJb`H6L1 @(zJzZjӲh?Z[Œn@ޔ0zhV_ Nm~>9@ ".D۸x9aMU%L'>b  $+{rǫe~w\q]o?շOsʲ18 kyg~$ 2 S_>h=3?/Zbɲd1 '~ZjJ(&snoÊϯ2X.X5 ={I5똼sk'u^9pMbIgaASS mUr9hnHm@ä/Z)ќ#Lj$53 ^pf|,Sj')-Nǔ1{V?S  |ޘTYr#S>+ʿb5ן-3Vs}|q-r2mJLDs2+fO1CN0K*`yM5858jv h3XU>U|$}f 3[2yBV:x@ w k$ꈩRUN>A.^v)< @ҬKk9DAA@O{WOxgB   'b!  Lb!  Lb!  Lb!  Lb!  L?`m^bCrSX#D'$;rbg9n`m-'h 4h" q_h@:n:  ȰN", Rҩ0$^V4uuLȦ [?Rp$2,1y5X] ~FWGշ((5'V1t1;cUkd1~Vr)mDl2*Y#X#˙aO۪91q%ngW88/% t;GANeUܬ޺ZdUU"EcumPJ9p1p@ux2 Y eV@Jՙ"]a 0<~J'ȴM@˧sm/#S|F wլ=T8 VtʩˡQZ gtΣ o3ܮb ~iC+ NHR럖@ljU.[<̪Z-T{gp$9,x7旂]%;_A#L%:'jb\̞ #/ )yil a+ fO6n6! (͟UdA[nGwljwϹ<u`n;cnq{z P&w33cqVXeV:R7`? f tc|eӖJZp ]eřT;Jޢ!oLԈR.*;2g՘&X[6 :mȋJzY'1_GL3Wؖ@'Dt^jԜԜY4b&C26a6[ O5Gbw#Xpl w~܋ٓ# '}p羋-zB~_I/[q`Ǿ[XRߴ k7|Ob X/LtX/s% ',\k7ux!B.@Fy G` Ghč6N>T![=a85&zFYg0\Š^V&'QˋYr+ ej[9@)ݰ۴iuȃ̞dmJAyT@8%"zLeUɡJv]Bݬau^JfhFCŬK,OS(5~ z|IӲC;qԙոѵzΉawN%ʰ} g4%Ѻry $k̑C!9%tIԣ V $Գ *6c$i-f"n v124ðb:'R#q`4Pz{fywvOΉ4"141jy= f8^dB?4ljqSșѵ)W'-[(HY JyIUŕ js 7Lj6?j>ɩ)(5gquAȣP3vߡgZ{o5] JڢbXW_SWS;B"JҤtXf5(baKh'C7b 2:tBd2l%BM&/CS)攛$vb(gKy;BY(oM|Qe.)5+ztqvV,P,K{-g5riVsYN[HiyAsޢ OE{2ܧQ 4"<m j:O `is|6;p-vO9ӺO8;'/:s7ވ%LWi*s!Niٛ]%.Y\V@/eHG }XbA|ZvwLf?xfb5b?+m{|],)f_%MXMwFyfaOчs7[}N1ik|g@N9G&[̻Ť?75Uwps2ݓt~S[/Qͯy;+*PwCdY5˶I=5kȩ_:+COh./]qL\r> rǮ?5E5 h$js:q5,:q8-J`ObG IOG% D eVQM\b-kҩ_M=[RWhQp(̾dtM:5]so] zbqRo@g&eh? Z= BzJ暛d]cuNg^a59q%aJO=0ǂʨgeST}E3P`0D{@7tO/VKh9q~jyS%@WD ur [g?x sba]g?a"@$+w%j2eβӇƠL;ގ)@3{b%Mý*SWXŲfoKH,ntN^WI w#tXh@V#J]3'͛?.k;1OhR2d5l}i8 ȉaW^h>ڴ/Tͭ]\u˟~y_$g+b]d E MTIЀT JXH9EZRֿkk[잕:l)S- @{XM9jDۑS/BBwk f2k;y` J.s&oU%;^7_-~X\Vu;V 'Pў7VW2uYFʶ>S,ɋX`;_Fdpxd5l3=̥p͐єqr$E{޴: ~ytL^Muuv9M2 -FWAS~THrjq~)O;)+99 5ajS>)R;f*;M팮k4ĥq`i4ǫ`v$jݗ`u< wA ̜Bɘ>v0UVy&[LY%| w3O/ =7i)6eHQD> rK+Oͯ[?C{ۍOvQX*)b9"0I'bѩCX'b'UgϿ~ydЎiKc׬Yۇ0 Cl0졧 kPw[TS2G}E3̽ D7J{O)'F'Oc^ǛHQs䆽oe UFz?,'M=גxvDHS=031 ptyäj'D]>bu= ¦-!#Ǵ,WP a@yFrd:/6oRa8}dFJuzS}աm+[=Ḽ\}&X'rtJdxiȃ1 l{Uwt`uwbqR%BΉs!ژbֲ:3M="C=%st^:s9%bjȶ֥e8 @~UWoz4v rn__U%_\S{yKϋEcckk.:tcaOHi@1 @B꤂Ҝ#M;CY%)QgnO!:bO7LQqhmh/VW5(aN\aX\f gamn &)Q @dgPe - ֽDZknHH@ޔP$BQjI ~oc:r!sIm!ɱChTq8;J#BMfrWm/Ag@R'c2snͫsfgBj.rJt@g?-B/b)jj۟jݺ,B(PL=^K3?m|Zf98TeU7vol[_s-6}rD8gsMGDLl\NːoXgA9r?/94u͸{=_}ɏgi7l[sc%2 A u2*V7\wazU%w рM%eNex0JnM1X.-ubL0S!f@o̰8aoPIYVE3%aVcG &4Y~:xw>76RJU!4ϯ}9'0jHnÌp9MwKb*%i?SBRK vj,ڙ7Cĝ)eN;'!RYfo!k+ dt5g_0]%V9^%} DY+9eS9! 1g|A3Z- %T::S9^̇=qj1e1|hm4@ KeR:'N\#۴#ŃeH8fA9Q];G}5CSyQѪ_1+ۼ-ӖI^ERb;JNݦ2\F]1X^N]fʳГxϣsWݥ IS:44]Ӻ7m6`Ny̑kpΉfv35kOFch1u汚laL㌘єݥBY%VR7X`F4ŊU{匮fƤ_fUÔau<-{zf󧅽:+WS~&;j'7C[ұS~ L3FVvw bӍ\aTȭ 4f**ilؼ:<1eΉGw%SY )H;ikf4~|,A&sdfJeo!*o~.J oJGBp-W8Se> rX=9ʲ^{[ʧM޹~kieYK][5EajTMBL[6#/Df֪Aq4% 9>68Ajycwh-)yStN4 89z{Oт}vN>#)4U B W%-zw٩wre)-Nǔ1{VLb>lyuL^dp:/jL1(az! O948TS2Wf?Llol;vwGK33y[ZX5R"yjM;Y5[4K=]ǟ_e[j,)PED5)n~DZ)ќ#fbֽݥ] ^%}t)NZ\3qLzSW vm~.9Af^`|b# ^pǂ9u=Bi ϐ(IaO!1#.e5*?h`m~)Fa ^"`NyrFWr)B(a# 'Ie,>?/>c+V^sy2/>_m9-G")//Ό%"8''3xNc* V`t&V JvJV ],X= {d.سS +e8 yY4B N =Fav R(]Y^yCýCt\1"rО5X&2mQPǂÞfpbqC"aco޴c0x[Oe3MX2*(ñ[l_36"A-<܌dfs0ƮngLY? EL?`HCKƗ5idDV^2f{mٴ5jȝ GiH='QMI-?ޛv?Fo >O kn:m`CO!rG4b[cvöS#Va% |>iiN3! A2iQ#;vO 1G"6vf|6  rê-kt^RE;B)%q2< Aԁ* ȧϑobgA AAA& TAAA& TAAA& ܋YF8G?&XH   '3S@. vBt+0$^X(Ͽ|?<T,Ukaw [=pomUK\g*&dU)54&XT!?FS6DdWgNLԌ$IAA3eUܬ޺ZJidUU"Gkc?e*V)߰.thO!KJ2]I:S+,w'ЯY<Ӄ&X+v#/3촘#*D&EW8teX۬Kώ? j,V0VʰN62ոӲ'jS65ų u^K#?9$AA>L?gY9Jɂ$5vj<]PԊsy(n3tw(bLg~g 2"49G`^fC+u3@ynP%Go̐\geiKcFׄh?PX- N03˺N +DY5 ij-L䔈KӖ&0u$k,Uvj[I)ySI[-B_x$  YegܹeK}^}ƥWfVر+V7-(,+ ߼{(XA`erDWYntB\f;H5dL1q&6X^%ih V $DCScbL R 7Nby?:1T/91.tt#^o|/)?Zv7a-[tN?:'@nm 4κ0 fO6 5/LWeMA]DFפPw" J9%]D 5F@3UeIn)CO>ԑMft-q%f"n v19*I" |NxJx;C|ﭦ+`Aiѡ5PWS[T^ +Wkjj]HBITLE, V -B$vЭ UF8[S%< M.T%@/۴3aPm$UOQYV=Ӗgϕk7m= %a~05%M6xw!6o/Q¸nyI`kVSQg Y5jJd`UL-0LP-5m٘=qΗ4iGVP[/Qͯy!;+*PwXF,BhFo,V+6|daEPu)E YWi`t-~mh%  *㛝в%SgO[X+Y<=Cn IOG% 2M=[Rb*[83U ZmF!g440ǂ 'Xmf T ]c~_e3w%ga+Ϥ6hV@"񯦲5`vY̑q+N(A2Feh? Z= BzJ暛H_N&蜼PmwAG 2l[j< P{wsբ8%bZ*NM R X̞4*I" |nafŕ{}3Ŷ6- Us/9m+W_ ʨXYB&lS2U4 ;:>cNbݳRٳd{ZyN9VFb;Fb$ڍbѩCX#ݪZ Jb8;2m@ qGV GWyph>NrJT pwE] !(obnh>OݓN9ZՀMh#"WiiH޿dpBԳ)al}-Ӗ9rB2K:'&Zs\u4uzt*R%6 ϸԁhDAr n?w_|sh勧ϛ~xޙQ_gϔU,9&Hc/jX`2qSfeSXݬs,9=#?DXyB4 {  18:ըi1B:/ n9H&X[-d%DY+9eS9&w;Jit4N>9mY @''YMNDH1e1|hm4mb>9Mk0kNWALyUcή#uk@ v2$AA>W];G}5CSyQѪ_tt 9eɧ]oXjOD=91G.)94Yt>`4)K,G 3eiТ<0Xo$.2Vi6jzv0<58^D3J|8K1GNO<3| xjd5nw3>1+ۼ-ӖI^LÔ.>ҹAh@vVOey9lh/bM[Ȋ3TN ê+l5Xrl=BTf|,4K JʰV>f mp/ЉԻu^2mPB?Բ7:/i+˼:''% Dp E ]7oI"2! !z3&k7m IݒaBΐ,_NVn= c" uNvk9FWGL4ySSxDP`tuhЈ Ucilc+;YMf4E$  gqV9AAA*   Ȅ*   ȄA;36.V,AAA U,AAA ORžB]x8Hh-% рAA9)bBʪ*<9ޞΞQd*gOEvIǞBC ^,5<w~Ky=P gW8*  eR>}2}@eUu5H4qq13]'ֿ~jKO8TX|KG0\?ّ[g~=/}н}x >|kSZ/SE{x9<<<擅AA};V ~ɵ_{{x//{s]^/|=vg|!z?x𕿍ǨX-uM'P|Rߩ-j|}s Ndڹ_Լ"aVm~=s Hd= 9G6f5W_ٳ|)jڑոoAAĤpWܳigꡩaGwpGQUв%SgO[ٟჶ;=|_0dʤ/<}*+-uM%'loW[ dg;h̕b| 1`gb5d52lgW"._srJP*D-N3UoQK'LV%A [4?o DS"+e81@zK-x9%9c![_@HOܘ#oAADӮ3,k~i|r}mnmmZzV_rڮWO/ ?X~ۿݯy3IejSaYA/UVy%_>z+/O*Z )=+ENlh!:M92 $-t{)@ y3iThϛ@'Py_0yw@E~TW!34B1S#~M`%H[v>:f=o 2Vx'9~:/E<AAɄ_{?~7&ϘzR~هVYhièX󧷟{}W|?M*#qEB$^-6?|ݟw{݁#͊8\qM*94'Ze ¦,-e`j K#D͆RVj`9ʤ}΄#7-x+ch44AA/ܽaymxrWU7e*k<_RV^t/ĄonVlJB,[4<{=-{N?7"+:ZY!  οo<<5ky k͸{=_}ɏgi7l[L4z2IgAcy7c#ڜvUEج8#aɎld!p$Y[43-NؙFW%i9|,$`b5%~uʦl'6/h*]@''YM6 Ȋa@AAk:/W!~^~y$l^?!2b_rz8:4;s]Voםooޱ~-TYInK;X#b'VO+o~.mMM/48e1b/K )YM;K煲JBY% o\w=ǯlΎM18}QLY$/99ʳcm}-R=@ʙ AA̐_R Hyn~i!w!XK;1,s5L᝵f+niwZZYR<b-<̯}!Raj'JuǷnK !w@DyCU,Y,0as8qs 6~.%{w]vjĝoYV2:۴UETaC3xB:&/28<:/jp;SYQg5b*IV"`NLd  |fn>8ΐr팯­v[ɔI(tv5ʝ:I2sj,{33afB%Mj,qrT,g`hʠQiKrr8TPFaW%M`tScêO:o ɜ<ޣ ACOϡ Fnw6@$n0 Y]#)Xdrr98;pPfޑrN%&ªD0Q P"&,H۷2FYAAĤ#mʠAAA& TAAA& TAAA& E  '-z36OiE>a݌B@>IЊ   2a   2a pZ;ruDW{vb)60(ǨZa#LTb*ITZls1O^ |UVU꭫#J1c"$_d9Vt8:B߮J|׷UԩfKd^yZKq\T, b Q4?^3b@yh4Y{4%ʶ CuNu'vGúVO7FfЫƛ;i\}4ct5g#1툐X?VCoIDAf"Y-eUu5VǩBŚ6rVIlk;4(y_Gj% /7L_nu_\{vÙX [ZdYfzc7t2KyN"sn9Z\m5(y7q7mʝN&9?Q5N3oW)ss,o6Z^;6Tao/YC V=?[IW3~, LP=ߗ"R>-,KdFNsrTdX;]\lԫϸ?*9 7'?[\m^0E+6y9ap'#[^ʢix5lklkHy[hm>yzNEf$2wb9٦(τWMrYg#`P=qf.Nnfn8}_8Fc\RH3՘ƖF/ˆ}t,X>=~=BE6 ufVC:fHn AAN$#)/[q`Ǿ[XRߴ k7|Osxy? w虇V[F_k{P"߶7✗}VSY}_q>ByxP` x71~9p Zhm 2eP%F(0AmQ<\ ,vMY %s$Ha{7G`H!-^xPt9Lꈝw2 _IZ8aV"˶&RfgN-]lgÍCdzj7xLjak&m]*+g5Y7"AE͉[0|Q{CuTN|C"_U-#l}P _3M}FznԴ5(,5G6Ss#WTRm `8`]s8ƪA#׷۞:!xLڛJ4a m,>h>{hҰr@TcX49s*s@~_TaG; $a[6eZF~>tEݿd~67+jۻӎyG~7_ 7*xS5 {zD4a[8_pY2,%! ulu')z/~;@2veꠈ  q DmN<E/:{]20̊+/^}`ٴsloW[ dg;h̕)@3]фY镙g^\\;kb=-VdA}®n-L9Eٴ(f:*~heNjP@yWl*lkD/$3YuEH!$HDHXjccBWWK==_ SaBij43ȱA#fHT2~Y*h$I3yz7>+'f핏v쪩Y_`#LO+f20$z$m3}ҍ7EYŵ&W>=Wʕ9$V j;xL=|MlSgd60]m |S}i%_[=i6n_?H(<Ǹn,,?0;ۣ;/VϟyW_xIE]5-vJIpn?JF|BZ'++E|q};vBX9uٴ.+ٕjTp w~ݶk B/sAƐ0R~6&&0By&"a} bVbۓčT`<&)#ʋNϓ8>Q^'MY3};C@>#_t⇭/ *Nˍ4Un>WiǛ,j\CP+uD.0<":+ L`ښ,z-ttm)>zq909BeE f:A>:}nGKr5|&3<`ZxجrLwzu7+p9p^?12'Țws^`73 !:yMܙɦ]V]QJVdd{+ +EkELldK%=S`;v\MJ7,oC{SE>+dܑ^cw)ϘN6~_k(s,sȋߨ–LB䥅Jl20 s;^%ExWǛvDgoe%剋_Τf#bx$ #V$Q :念ՐJlI  '-k<_RV^t}2\aPA-o5R;;@Y#VD/*jJ,Ssʲae6BƳ|6X!j<1Y0xD6R4iʅUwHrjw$1oxg1`6w`Z䅾y9qmdkQ<BD3WMsp_̳>_.tcWiƛnD\}#;̐USAq]:fV }jQTb9-f" rU%_\S{yKϋExLTS^p}'}73/Wb2|v<ܖ"+6s<W41ǮM7Z?lW2;[^ʝ KΈ3uMAZC>%w TjM#x#\f$7tz51݈l᨝s֘^h3lA!G<$f$Lm$%++'$\H11v9`V"K%6jFOn3Qgi{z ({-U@4Cԑfg5r,@A^KGBL1Ō.p r`f|>o3ex(vGyg_}|ͨڰ9 jYqF' ň^dH(2=1.؄N>+ 6,b{ښ3)e͝$Sgu rD|Dnof\8AyY1p$LjKְBU=`FK?"36ʡ^dO(EZ]ϳFD@y%tƠ\c3 !T`6 ZQJ-5_a7 c$~mQoZ~O&amP|J3޴#b%)n ֗NSi%Vyox(38a3NMX rˏV;’P dY\(X˾r'ZZYwǗ%$]@O{W(2U)E>U-vT&3j _55zqY`GkITr2;[*Td+RIxk.f4m-K-vnpl[dt^eN^`{BB GS)ss[Y%Q=95-D52O|LF4:UW<@ f +*lTN͕*PYT\z9T>]+wY 1L5mGTdq{~S D+[Di$V$Rj!1y5bfcJ٫M3rߪ_4D(5-a9ك߯C7%u{Q au`zbn13q=oA97]so@[}K;o-,kQX*X}$_||׃=3-V UdYH >7+3߬3}2q$Zldjb#b&)nd[zZ86Eš坦x+ogێfJb:Wu*}j}.q:JZ1Sy,oDAg$=%(\޷̤gZ:H㘛#)G >);.Xg[#$ _nj牬3~$u!GD5W~H5gRv$F*K9عELs8Z @ P;oae5F! # `zoV` KV/%46Y7D4&JM=ޑ Ϙk6m l{xؓnD4/ Ɍ/>Udl_yK9O 9|9YY; L:e;FtcvjGyxqmeՙW'}VR7f (0ryR̎ΔL$9RWb8Ɗ(z.$poLPVM5ۉg^! gqV*r|1܂^ŢV>uYm x  r*r|uғzJ,%keBAAXePaP5ّ  9A   Lb!  Lb!  L lbEҝtE eNR+'lUNA/AOdĪU-Ƶ֯ZU+AAOQXeUܬ޺Z:ªaC?"BaHEcuM @Z#Wq}PK `&X^K敇U I2\vDzkx%#K"am xCPQ }w:}kj4gR;i\}(<i`&b1dA {5v +Z΃E4:GAANCj)Ew9NUe*ִ3~,HbK]mWvߡAڅw-u;qeC+wF:Y?Q5TG3oW)ss,o6Z^;r8RTd1M_*~Tm پ3i{5Ė%m ϶3seV O6uf-S\Y@A1TOqyӵM9!:cM~g b/>sU˖NK !?㯒3xo~rÿe XlcFngz1-ʬN: k>pM!;:RagÖ7wOnG˧϶GFR3UkDTϵoI-GCu'p|enΰkv)lO}@_ښcK WeWmx@4#/~]i{5NI>+L@a[`P=I-U_6krNMY@Ag$=ee+w+[ꛖ]zo=in/={kˆVQ]m:uw*[3Wң x9 +@(Po8C0|fI46,I/Qa 22y2|Y1<0!4 0 ,ѳ$3J ZIX:dnB[4"\sGh5 qy᫫,I+s r6RfgN-]l`OM9  R)Ev@]MmQy1\u+OP5ّpWܳige}9/rZ8nm< D Q~<;{ |"vVA]Aw/13k)s֨SϭeW#5`{F&ptrjnJʛ kX5hDvS3oV{_fX^<"?Lz%gWJT "! ڝ[tk@B*PuiS\]h{LWMk6{zD4a[8_pYZ74r,fGe l2 *:1=qwV#C&rVd+=InY@A䄐BOF6\w[CˢL=kogcbfŕ{>0UVl9R~Os׆m_X4O D֙h¬LW3D..]5}~`Hly:b+s>aW7&Q ֜lZRed3VU2'pD5C<+_6\Y5 Nd  $qG$R|B7d8&tuc}@+mCrS*7kl}[9|WN[3Z+ L 3ȱA#fa347)mR߱SQgfg]\cke"g/+sWWŖ%{ n7,  rBHlhKP5zvmܾr-}xqXZYva~wGw^xsɗϭ?ʋ퓊>'ֿkBY잕H0IDAT"sjV <ڕ59`}NzWVʋvK5w-L]siI]V-+^!@V~oO" kҊEe[M73e[æ E6m%kzqJLjNdܪ٤`cCBWMkumtoZ3k,ʼ7*xf>fe~m͈[ C|oиF N7;6 $9n1BlYi<``6w`Z>$LEASW|qYNǟ.>/1ESN9{̼T\Ŋb80yhc1ׅ>evЧ;asg3*teŏ۵R|J.@=դF $hI2SǴ'ь!qs֣Fd{l?GldjX}֗f:WVaGO>mL@k¦0EC #oCv$kOM;6e[†S0wӥ31 eI2{A`f|>o3eQ,-.ңϾQas@>V TbN8 #z#ĸW`Jb:ݫ$ڜnЊikΤl Җ6wNmtm[K+Zƾ}*֤2c}z?wOq?<0wECU,Y,0aO$:=u0Jjcy>8R-o62A5z^c1b϶|+pm5;MjN_vd2)+m5U,qmh!oW)FՓ̈Jr@_9$}[Z;HζE. Jb:k}@GQ0 @& # ;$_P.#KuꚉjYgd~om;&$) }Q$]ndR߱)R ;z-gmX;lV.H8$۟WAA#)yEWe&=_m9-G)//N E?fOq=!!uhr%P;Odˠ% 1<"p$1F9#1RYBϑT-bםM7b8HH6X9KVVcd<0Chd[6c`H!zͰpL2dW2$Vٶ l$馨yhS hTFA9QHVKYUE]Mm,M\2F|]W͟UdA[nGW/l/ڳ/?c%ĖyVsѝΠcn6};ͨfLW琰ʢ_(U}?:$|Mq$ {{j%Dô<v27fպ%E?1m ϶īʝN&9⩝Q5Giy戈[ިiN[3پ3WYʂ;]\lԫϸ?*W3 cъͯg^xa>OF%X59W)" D3@Rͤ%AcK WeWmx { =Qc^}J 6_3Zgv (lK ۂߟa(τWMrYg#`P=qfB./OmP|Bft$yz%6_աW+sM AA$v-M. ˊv78Ee|~(aqw虇V[&"߶7✗}VSY}_q>NIEm|!$cKrkȺ jI+JHV/}jWWYVlD5&,EWgR+N[@^ku7!&9y945+% 0,lH\ _]J5 [HGA9H]u5Er-b2J]6MFf%Q ~{rp^$фm]|eI׫xW @A֥MqvcczNlǀC>)aG;R>nT4jv4 a5B rq[nʜlev6Pp<׋  "R)HtnkhYtu-lqt_/[1eִi*eW^<ޮ]-: ~wbј+˃S>gcrGLHUعʜl0(OͶIT#!3]ф镙I WBɩcg!|Mw[^ldҖM WMB-w!ş4Wr>ΓWqiǍZLO+g+l}#X8F%Yvp!!4|h_75}J2?GЪ@A1Yz C8㗕9᫫bKKAAO#mmZzV_rڮWO/ QlyqK+OWUVy%_>z+/O*Z g{VTءqcؾg^^j)͍Ce4_&y8khB+%bB*L{"6v>vN!X_j|yq=uٴj??l}$$q A,;F) ufVo oԽJѰrf@4E JZ}u36FUslY;$YpK nOAA> S~هVYhi+PɵvRg W][q<xجrLwzu7+p9p'n~whj !:yMܙ'*-MJNPkEvyJ+*ٶ0w$9%@ETHe@/-:Jl2`}>{(x,%D.)O\$2UjR9-D5=%hv1rݳ57*30\@3؎^h3s ;IT˰f#[RoM(Ϙ6~_k(s,sIAANr)/O%OaE׭K;osCXN+]9+n8lyC"E\v7_l|g]9 ]cOhjܨXO%XXjv>vVF.^T&ՔP \X~wa *qGu XC)ʦY^vhF`.y0Wtz2?ȶf-~H۫17#qE5#{Y9S-+>Wy9qjk񚣊4CBWMkumtoZ3k2̀$>qN\ЧAAdcX=%W|qYNǟ.>/LUދ,q%'?7(ڼ_{y唳|o}q+&@Ȋad0C  M%v>evЧ;asgo3eL}NR&c{7J=lw9x䞗} *ks@>V Tb|O @y%g |6M(ƠTu鍱q]ӌVlOX 3mm6Ƿ#ЋlKȌ< lIjPQNɵhdK$-:UUޕ胼(?D5wcrg"0_KGUjR95$'#Ǣ9͉RjHL왑 U^~OYI@r5ǞlP1;3 ͝c6CA'(h/H(,,~BVe-NXXe_9ߓ?^-,K\LUJ1k(1bGk"aN\e~.ŽnuFg  1ã+Jr'jLO1cL(j…,0󣵩[:UW<@ fդ5D\Vfe3A,Y SMSsXe1 SM:ioUœ jy^zWg!u@^\1(@عE$oI퍁6` imv36)랟g[lsH/_5 +odK l5^h7 SIz9T>]+wY 1:AAN~R)pMWwwtVR>m[K+Zƾԙ0kRe˱O=؟zbPK%0a8$) }Q$`\zM>T LwĶ|+pzHXaSS߬Ƒ %hvo}P"F&F+6r,fD1@bҖM[sjjYgd~om;&:g^&}XBzBB8 ;z-ǶGN Ͱ@ b:QS޸oNZeR3YRG=ݎ]V'1IX?,d?  ȉ%Wk?[gZ:H[$)G >);f^ XBBP}ALPMa#Q镇7OD϶7|#axD#LO4~6<.y"_fX6G2W5R?9`/v7l P`Ã>gT`BP`[BD7#2F`V=BCq}f'"-0;?ʌ v׸j?^7w9lیtu J{z6^j5pO[Ώ/u2!BG)v[0D3Ě`֊|G-#'#w"mU_pcɮbtuKsFͫ;X ~\6{ɞ-z+Q>)Lq8^G%&lb6Nvs?ZNMц2'sӓ哳g a<ڪ:W {"=0};U :/sk~df7(k<<%jΕͽQd'e2on? t4B!~PQ┘ا?{dWqYkw\W8}.w?Ug.w뮳 枔z !piJfi3ζޞhixɏ!f$R 珍2="|;c6 EW}WV'Z`L ׀:))pN)W^͌}W6cmr>M Ɍd{QBۍ^Mȳ2 /i-|W6l@hUdF{՘@z4@!aES杻pϖ]bS #7\ >szxᑿ "?3n%_y|կ-<ŖWOX8g^LԬ4䃏=lWy@ u"`ƈ@߆INQUJUʜM0h:(ћck:D1LF D0Jqc=W78 l/evmD#!phb}XnQ mtڱkM\W F hc׌z3,Rxk?s-rbNi= 32]f]0m2i:eJi`׳\oz4@!:ZESs2@iQIf^,[o >X=vߦ/ֽkBZWIo.6twIž˚1$niy$?pndzDrR`_2Rq:w7ħi}~_ M|->{۬OT%;]MEG42 }RD6l_ ;L/v4[BU84GIgCC<&Zx{qS}x_6n!M@]23M#ԣ0eFrB? DN6ʴ1 Yw=h B͢)@q@L\l f7˖ƾw#z8) /8k͇_Kw3̚y/D1 qx$ Lo>OG'gdžfq[W'A4 A +ڤFYǶDhl5)i2-hO/-O 3S@diFmtS(jʓgg!BQ3(eYE$N0ffr#أgP's3{[SmlrB{4@!:vtS[O)0zҬm^bcw=XKfnu߼{˿ksso[b4EgqIc~d r \˯[ yfF)ճz /챭e"ޟq_sv+]P{z}hLضx{=ZԧYu66{<%rmamևgSfLIQf{L8 Hu/ȧrP<5Y3 ǻA2#Mf[[+6pm&A?s܁C8hV᥀Qz>mm_{Bmd2=qEoJMo3tX=h BC:)\.~pʄڪ߁҇뢛.+޾盏WGk.`%q<QEVϧy=UqeDFz!nn0QqumM^^]9Jnf&PZl$K#I x}M+|VL"쟄r0' Q0]ǚvmx,DF=bf8;/*Xs 굑qFJ4RF#.S1l <\2!BNJq KoqzRҎ=u߆w;su'kݱP~ ~2!V٧bɈ֟,G%'3sU&L|hZLP'm3YR@:0@zmcX {ԉIjSkeQx})PJHP{?|7 $3QݔOBqk=zR;buk*H77=<7ھ!h B!tL6N aV{is2RY/>x|9(ކXCμ𖤴̴ǜ7aSo~莿Oa䱡KV < *‹=OČѦnY鶙x9^'}p@LK^\8|Mwk&,s{=_bw>[ϼ7:@j&9xp(i^p~vQ}DlFlk=lгP002K W "l2 @ 9u\"p>= s t\$ȆsUQ6kG=.6|i_Fj=f[K6=3Pb&n $j:#Z}]s-v-Շx]Y3Is=G B\8x[owJM-k9RksY7644[tͭʊk0N=>Y91]L3mLht"[, .<$FBǭ,Y LlPw}Om^yZ n'nJ.ܒa\{_aKMҥ]>9ۿ7JUv=B܂ ME%ȱ;܂E%rѣ UbM_0kſV>y)]rkQ%^g. 3FO4S&G<%jΕ͇>A29*%!_frUpA|R&s /Rn@ۓ?OgR/-|I롪Ow61gNCt:)߮ŏ=B!u#'#w"mU_pcɮb}Jvz%6\qu'^~H{Ymm}iJfi3ζޞhixɏ!f$R ط<+# Vڢnwh3fe)SR@ 3T03Gg:Km*WiNUclo?4z.E쌽[aO4pv:) ?!ј`ޭ*{RK/&{y.ܳem/*T5>|ϳXX/<xAK3Sj ̫viޘ|Qzj:OȘ(!nL3cD oC$؍ '(\*beNt&|[4B[[$'3)+'K2-JˁߌׇxԉIS0ҜFhWx`=9h8+y/meJi`׳\onFˌ`E?s6Q0cD=Ctƕ{i$ coYiuTGiaLyuB|ra/azB!t4{W*~U~6k)`zNf"(-*e+Ңź}pIo.6twIžH׎5$nniy$?pndzDrR`_2Rq:w7ħi}~_ M2݂k^$+3Ӵ1XKWL|:>62.pp#Yjz㙣mich#W{hmI,eFrB?XAE3Giso^${V: 9x fW𖉡A޳>t f )B!,+U-!":=n>P5cF{طk޹ x9}-Kw37}~|0_:Ld|L[zI>9K&[bIO>BڤaQz3mƄC`&5Y^ ljR-?{XLWH@7lBIkghTg乙U@FC"?Qc 8+O fS}XXtY=|(HN"+_M0$=c Q=Mhɢ6GozmD3@/[4:tqU>+2R$z\WoiL=LetB!bO<3򍮥Mqŭp;֦N5GWV(*~"0L>(nSOVNLW'%vqK6&Hqi: !^pa!A6ʇn=6lPw=ޓLN_\zc2?eϫBs>#j4_3hqc㍒Ndf Q Qga4^?$biNM>SSX38GisR/ U%ԫ2lɝ&M"~kR@T^Jwk\DtYB!t4{΂^~7'?7ο~iCmǯeUy#m])'?5!AKQ쒝Rj&A|q T䙓Љ74ojtrbW 'o9. ~]"b0{3nס sħEoLYv6py0s?pmT|h:Sniqf3E=s0]9r㋏>住d쒫_lm|U ,v3n&؉_ўe5C|6#Ed/nXBcWnҢ r ȁ0Tg5}K[住tuKsFͫ;X ~\6{ɞ-z+Q>)Lq8^G>+<ʱٸGfv}(k< 2K}ͪgwsUNmx,s1oy?q :/K:WFw(uRkq];# <=7xJ6՜+w {5!BǮ TYҊ/dW1>K%sG |__tikkϬ4%34 g[oOX44T3Qe LF8' PfƈFڈ'Ѫ(F?cihUdF{՘ȩf|j:>9y`L+GCD1H׎J#S'% Z{<+# Vڢnwh3WB!t욹`ޭ*{RK/&{y.ܳem/*T5>|ϳXX/<xAK3Sj ̫viޘ|Qzj:O(!NdH> v#8J*W鳊@S0ݡ)Cmuε-h8qZ~lzӱQ%wژ5c@k2'eJi`׳\i:. fRX"xX#[FTCv,sڨ8qS}FuB|rZnƈz+67G,Rxk?s-rbNiFkB!?<^XW۬9ۋ$3/ X7JJRcCo~nCÕ&&n~NWp%ʒ3R.kLfd~hmË͘@T'F iڨx߯F¯2zLhd8@lؾ:8w^4` ~W1=bzD pV.ꭅs@eZ {ZD`~b- ▆5X}L/')lW'wPP=ZO+oXE;qG.~WLӆHhB!: 7V`ֈq#lk[5܅ G^<휾%Wm f>w?IHMIK>/e&I`2k&ڣ B>9K&[bIO>~& 5S@di\mm/Lz 7=rlict'=`*QmQ\M ݎ*n E-흺hx+L;MʓggT#j5=U=7@ڬ v4 !BXA)]xYk>fxҬ[ !Wpokilb3N=iՏ,]tY!?ku˶A9(z[v=uL9.+|n >$5޳^vxp?kB?3yiVx$H@3R i~b0;cQأ(XoxO+6p=rޫXn:^>JfM0%Eyp3kgPfO{e4-?Vb|Jv6yN8-T i4+efȮ="A?s܁C8hVkDwlB!:V\v9w7M_{U~ ?S8eBmU^kCuMoǫZ*t(>CwRUUw/ 2\ qsaSwzlk u}w(=J/܁M+|GGLkck@4SbD=x꽱0MQfelk]F$/1əwg~i_qcJk#s3s}]z#l|R&GHwbf8;JJB!~p]s.Y$7z鍧^JN~μboI|緬zbybιb:nЖXOjVqnX(AvEnil]\?+`mdDOrӍTG`*:I@:0@zmcX c4^er  A](j|nM|ɒGf:^& u0%eZB6ջVkT2@t0ous>ZA@I/Queޘ6D tS>9 ŭ=;(B!ayɹ}Wܺ5]KoXUuܤwV,=_!V3/97%)-9%3m1MX4b TEuq6M?+7so2iz'( B;D  ZN<D@e5FIP*$ H2: Ì/s#f4r(ѳQ_h럻:בL6ypķnk{^\8| vm~ބE[;7l.q1 TMr:z"Pl+]wG2s mT)6pJ?B)Upc*+ CD:dtuR2Q:4hcG˟0yi\փ,\]u&Fe6(I.ܒay)i/[_y|=Os S5<Hr)+rl1viEƙ60jDgcO:mR∑(ɣNL{x{9y#IkL '0|D)0cDb=`GeB!&55< .A!BA!B!B  B!Bh`B!BC,B!4b!B!РHFO* BXpTJ!Fzdp,C=od8A3 _G3?{twxȱfLU>W*zi?[յ7֞KHuYjD6fe^a!B?܂ ME%@;܂E%r 8P!V/m>ڛ%-5`-,XrO>p%{}0cDL3xmmt|rV!@[U"aOs}Duj6<97LAV~6L}^V1nem7G|R'%۵B+SŐ/R>$xznlz5f#BX7rҘ>rw~a*+VUZq7*ا?{dWqYkw\W8}.wֶޗd9cl 昦b&J,0}ب1rwh3ށ9߯FOdN"ff0˪w,7Q &&3%cVE/PX$(pv:),eJ {[qsjcRyVFpA6%E1݂f ?!:\0{]g=)Bn=VѼsٲUU]|*df Y,wݏ]ə}szUn;~δ_oLJK>(s=q5N%GJ[+H> v#8J*W鳊@S0݂sChc r^fږO4R K q`0z} xk[-k.l~K4J>,Fz6:{XQqiQVjc׌z#uB|rVʴT+>,J~3^Q'& @O*ogwSWNL)m`$ڙS {WïB! ]ů<f-L,^E%yYlwVQZT2b}Xub415vCwb܅&.W|pY)f-:m77$Vlv$#JKĈӱ۾!>Mz\u5_Fo 'HT W D`~bn#ȱqKCKrhhoJҪvg>`c|zurs  LF|:[&M@]23M#nubD5L!B:.U-!":=n>P5cF{ط_]8pÿC_K}݌f:w?rLB^D(3Iۨo YD0YUIP'> b`xŠ6)>hfl2#MWH~fWhO/-Oʌ\q: l2-9w vZz.H"hct' {u,E#;,;'8 pN:)I bm]-=#.qzh,j4( .w!BJ ZV6V?S &4kuX]|҇Y,u7ޠ?8MQ?yYg4823 `J2+K @2 T96L<5u]ٵt $qkF?V {[l_OtB+Fd9} }gW*|@:q6L0seD5bZgfɦ[ b6@!:v\v9w7M_{U~ ?S8eBmU^kCuMoǫZ*t(>CwRUUw/ 2\ qs>';D=zq5:ڦpߕ *@z,rڪ6EndT dI^c$I3Pyp3s zKjs~~[ȱef@@Xמ|/j@ZLkc2;838^m_dɌnrAk23M@:8^-^_hƈ‡Rh&`!v72er&SfTUuܪ~xDB,]C,EKvJi{HPW2'ϬqxC#oHw*'3lPFBO\Hs072]@IE(g`/goCIOōu8[!fhzZ%nw6NV=3FT3Fԭ =-WF@COoOH-ۋ^:V`^9s?pmT|(@2}ABfŠ͊ɶUj2;n}Xh} X!B͆rL:͜t`\m?Ȥy]lD.JD~w ~WVE1c60ED|!Bǫ~'8!B!4X0B!BA!B!B  B!Bh`B!BC,B!4Ht'W80;ę6*?W)X BJC, >M?ٔ0@.&{'keQbQ'ۚ1$:<0x"ΗyAF.Ŝ_%*{mmtxs/Cf5iUԱL4%{0B![04.)$r ȁ`x#sn~X#v}l9em7A29Q 2'2A)ֱԫ9Wv7GTC0'O|8=G6@$}X.[шf=0};WC]|Q7t&t:)߮hd'e2:snʏ_ BNp3+3RuUlbɮ/bc쥒]Ź#^>gWq]/5'`]'Xb#B(lb)0F b)2^c(@z{8x `>^yYʔ` m1=3 {<<+# %f݌ Yv3\G?w {ZB=F 3R}(mf]9D1hb$KFvl]HPҥ]mM,^E%yYlwVQq1a_9:sT{M|ʾUY³Z kYf}Am77$@mL^d:>1t,oOF~5<~͗=7φ'Zx{;qS}x98-t ~-|62.pp#YjLzb+Nw4 w4mhйGL >{z։Lħz}x5kԽ.gqKChJZC(|zurJtf(P6n!B!1q57YU31n佗-`} Ƌ,cxƉր:yj Gvh3 aFmp_:Ld|LG߅|r:>L&0$-D1h}j-IJgɳ3Pxeqښݴ1 FL7 fu|b<"Q bm]mXc0_#݌ %@ό >yUJe+"嚔\M6\m>mڂ;6S&WZeк6w)<.$J1x5ӛ3Tg乙l!BO\q{O֖Vo}ǓfMõwݏP7b,7DR# |*P"٪1ds '2OfmrQJ, {lj5?:'7r\WqǤC(jg>4~l[fW4+8KTy. n uZnL̀Q_(E럻:*qc@HuI0aWKFyU`tr6ypķnk{Fo3sA+gO+jxӥ=2l 򁀸evO#g=l\*$ t!:߽ap5uQ폾W7nk>no7ކXCu,7=\aQBH``NtCA%J"|Q6ҜX&RvWM2Wno$Yp^#p5 `dI\]O6LqɎŇfW1o;MX hŽFj j {qY\_-8Rb<<'z ZmfN:/i:=G0'\U)ʌ4yn& ?ob?}n$_:BsnWL|Ls}?x[owJM-k9d~ںV!md;T&`x*aph+Lg`+̾?6{.pNVs h&Q ڬ;lkk^hm6 l>,>f*G1!L [mF 7tI1ҝᕁ\Mھ&B!l~~Յdޛvh̴]r=_y|=Os S5<HJxiSKY1cԌGO1g`ԫEcO:mR∑(ɣ3cDbf݂(oe]eΌAH@r(ppcѮK0="tӺ> œ$GiK. Ϯxmz獽BvÇ6`mu({&&%$'۱4~WnuQ( bpՇ)5YϿ 0ڪBЁ^z]6lAMFkO|XB!б(mٛ=kkXO!B! !B! X!B!4hxBuW ԣOtY,B!4b!B!РhJ_B*P B!t;XOJE:I95g re@ޯ`+B!BPJ %62L=Kq#@xC-ZZT"i݇XDv=>sg;tt#ljlF YUT݄vow~LjI -ZFhS֔Vɉiӂ&' 2r1GK\ P] !Bhe;m-Ç>eVVd߭^٧L=im6/[ l}tl%q/o{L&j1mr({ʮŕ)kۯ}g Lx3?Ěicy?S`cY<ǩ(\e@:.IP099}40J@uu=54Oݾ!D } ٲ+l15{ k05Mzє5Qg9啀28ӕʭ`kI+P 9+'oo#B!wO qXi>k-x>Py +߳Nxţ~P3!0`oȶ.i-d(,T?NWmN^ 4|TZ,_aq Xu[} mÃb]G!Br d:m?4o[&ff}y3GMs??^ |qۿTD_<;'vt'}j7q 8!0@QI5֋n&$o1 & {9MTF9ō irLެm CKB!:v?g+ng?];nDZ6~$Az+W|~Sjmj_ݒγXqx@&;_sښ2mV Wl`Sffwԭi0` SmfU5yQUp5#Bc]!%no}O/]Lٻu}|vKW9g7uGAbD qrhf\?2.5i2CSCFEAOrLҍT?Վh6ɉL# ޸Kq_o?ȯkn>vKv[BjRBr¾{Ά^]4 WA~{FL] DrjJ(sF' !BsCXSX3h1$!B!t,q~LR69!!B!ta){;a,IDATB!B !B? G8} B!BA!B!B \(x$Guĉ+GMCjC!BB\H ,D \$SbFI s/cB;StWd @bEfp Yd"-B!;xig[}꼙 O~ѥnd'Pb:X_gt'Z`jMrO+O* B!B=k~w}Y7?89}#Of?t޹ lu˪*.>2r~~ͅ@ڡ}i19fX'`|,I[!1i,ٝl"~!B!1m74qޮg_~/5?~cN3y\9ۋ$3/ X7JJ  X:drt"`# K1DSL[j|`GZ;%̸e#iMoMaԗ0* ivw|vk0)bk䄺ڒ bibD!BǨ}e]lFw-=~δ-k6bw?gڎ[A 1qf,5b{/[>v;K B\;5PL5gODٚMiqܩaȤ@rPje6emMa'Bl&+a[ѽv@]_jEeB!:hjK>ht)c"{ؐK[O)0zҬm^bcw=AKھS#,MHǞp 7V*_l3DvJP\ RS}. %}$}_ jhMoMS[-foʾ5P9@lz[J~3QM̅i% w!BXQJ.՛3"weQމޖ6wM_{U~ ?S8eBmU^4h!6d.9rl,?©-+Wc ew#LoN RPqO:PMW%1exTCKˆB!~Bi_#o<O7yŲ?ޒcoYmk%pTc^q"jM#c1BmwȈ>4XLN`,4x)k\(Ev52i(?}t!BL{}.[24k;S}ҰV,oտg67)#?#GϗrDoEU%ڡ[URgC *9] D9)B*5I DVW zI/B!84od{^&ff+M)L7s1'0?uǍw{bsl9^2:=ȇڢA#D%s֔ͤ䭳$oR zd+E' G[ ivҺæy+B!~v=XlI(lmoJx[owJM-k9h9ĊA,eOYK&;[`qݥ6N:9s(^#0+x ]Z J_6# []2eIɔ%]Tz:M=%-=g'\{ |xT)dʒ͂[SGr"S#B!tJNMJ\ekW,KԬ~󖵛vmޛ_P[k@uYUa(VVgB.vif.q2flP7 g>I2f "'I '#L?djkpY7_q«%[9-ؔYVDMݤBMv3[doRzǡ}#%k ؽu^ Ƥh T=no C!BǗ:lL豱y}oտ΋o?t1!dE<*JiJf.zW_u+9oT/wSI^^$aEYKeOAf`D :@پRta4W]wE!ة?F,F9dtWIorns3yMѲX1je# ƤV=wj,BG "!t4/闹h ;B%;4,!5)!9aߎGN'`k5{xUASS-F_ tzmQ+ƞ VF8+UG!B?nܰa55l  7kBL wEjK):ӴB!BCNs7avBiAB!$ ~l8M?z !B!tQB!,b!B!Р !B! X!B!4hh  dq6D \$B!u%{ ޗȉc)r#sia`xi}_4ڙ@$ {P.2#j娎tђ>Ju$ !B/.vۧΛy+.'hνw}]UM1ORɮC/+p]Fvy %vuk8N ~@nQyf; һ[8g4)s/>Q[B!EۮMw]gSOg<1v?ԛy.ܳem/*T5>|ijqp؛>?D8b Lxf0IdN0`FJ26!ح(6'fwbӼCq=#8i4)"2{ &%M^t.:| 9,]*B!~dsGcZz;ZCMw=8SN)jUiE!=~zIgR0aYۼlz(m8xz(I?Xީp&Lf$cOzi8Y@9*n6g[eOG܍o=)  sGbPz׭/MZl@l[[:`,O91mUB ޒ> ˾f淦-U!BPJ.՛3"weQމޖ6wM_{U~ ?S8eBmU^oj9‡UO(bqYaJF "0 -7<Ʋxx n ]pPSfvuG4 <Ѥ8Ιd}ߜ>:B!Pi_XoJx7z)-;o.;e%-'Ǟ߲vSHb͵j ֫0N_8^ i| b;fPHn^٧#!gRSVaK桤AZSָGPkI!B!Bbsy\6ߒYݱU=ݰb{=봹I|Y)6pJnO+B@s%]X>4"(~^@u +ǜ"z}V+NWL^lMKȉP| %SLYoE,cۅB!~ҲssS -'#'?qYEo==5+߼e]6P]V7r 9Unj煂;ʹCQ8{3@6?ߛL`3R$n3`Ɂ/i0G`Q,̔}kj0yz0 irƮOjϔ] AOJhg`oNknx-f-R1Tl,n+ 5u i~B!Џ_1#y}oտ΋o?t1!dE<*JiJf.zW_u+9oT/wSI^^${LKeOAf`D :@پG>#T8M?bv]iRC5X:b@uKU0&zpO]..: N W]]DiBVH:W^ۆ!䒝{Zo^U9.tbZ]4V_Tpl!<8MQS!gF%(>gsUx5%v*B!~jTYٹa[Jkkh=kbJ w(VRuf B!Zbi&n|wp!B!~b0Aכ@}#B!u˶T; Bdu$!B!b!B!Р5i֔y3̊]:n,T`_<#ԙS#?;]~|fr6~tPq88y1G9!B?r n۽~ĸXrWG w>a LN]9vi_g/ ^x.ҡHrL/5l((g-Rd6[7+.2r2h?j48}I'B!ԟKs)~y׵44w*-^_GOy 3jEv9V. ~edњ:?Vc"O&=*gUDO|u y>ڦ*`GrLp,ƣ̙i$_65+}|aRzʘ9k~~!BE<5tte]ooNbМa;3??LGu ڤqًim=X'xR36&}ρi3h1WҦ#9&G^31^7K/LJF9z|_^!BG>b]uϲm6GkR)Gk*٩(&!66!N *uU5i鴪s}ov)'^:mve_p;of.a9P_]o;?Sΰ^]q=?[m M=住]şó.972ҳmNldC?O׎FLJ nJo"H׎5$ȺLTp>_d[W ʌχ͌rhSA7OvwXΞ=ܾ6S@ߗ@dE3#C,?J3xc["3(p<߽`8@*5I ]`Jŧ'N 9f⧫|'R&{ŐFFǹ_Dž7r+ 6la e3{|G0 _5"p~vGkUa T+6C!BGP?gEzʓ/ܿ{||Z hONRFh5TYS|o>^}ꐙ rfg{Çw]?Ąg8{ϒ}3Ow, ]1)m]Mڢ%EC!9 FigC +"t$mav6+\P:=V xȅ/B#Y ư\̤N{Ew>2SV8 xP?hu-?T ^zɕ @CNUgBbn_)PΛM=-9FN(k>f$Vz8}x^d|7QNoEkIPM)*o$&hS o |4Vd亥Ç1 B`65ttޭE]W@]UM]U[[E,%o{쾒ů=bZvjuU5p7]b꼙 ?jm)Ulhm{۷FM*!W~}ߺ/m)ƌزfc(Ոk9{&nA+G[/1l-OJUz[v=uLY1RG; 1 ]Lsw\N9Pd)I fǓ+m@HdJ Jh'iN"lޮwEn72b6QزCl5 ڴṶ}IkC;Q6FVҋ c}Xrw?},VOVgNM-f|>v$WVqFvNwFr2`,L*{ƪ^]>h jp9̔$3>z~hUYQÿy!^52˩͜ 0F nNە3"3afC\1&l"X@y9.7DfwmZ§764q\i9hM{6sq:Pg`n ;t0nl}0F֮w>w+iqh`h @>^[);M9\畽!Bh 6o}3?=E[v{O8~ﶢ8{^tY<߭`F|rž-t OyO>BUi`PZݛv~9O8~70R}לԺ,廟n/)ԳݽZ7ԩ'+'w݇uər՝3Ef씴]زCIOUgN? @σ7a9$"WS؛ɉa]XQOI۵ cYy)GQذE;~z4m8(!hSSq6ma|Mhk"(HزCS`$V:ޖ,k`60ʓb5`IRŸg.(h|=6"ov]KKD1 $%&&6@b&$BxC65VyH"`CŽnwgRzm)篶{3;3śWdxjOorI/MynD$(+xs̜7f؍,{b߸tM+Kh9Qz C7PULa.}糆Wl]ithاƗv]EƆJO(1ڭ |d"ZGeӹHS7ϰ\jC4TaynNB!/{H7xGRGkwـiB묣Xg(>` (ŭrVn4z(qAǾ u{Jta.]:H7UD4~pvV}^q C8I1?CÛ|f|ԙ.ϓF pH~XWUq:g#[o=7#GlԨQD۬&y+=hH[c jq=  %k;3,͉%g=YaN}7`DZo֦M{Ų6L "Kל(_Ǜ'#"ul VCV1K^04~z=Q0Fu9`hHݮ.rs:>M2$bHQYr 3G>@DR[{rbR)량1C%r!7ۛ7e2/!+sUP4}_b0 IhTX2&AR#LgIENDB`du-dust-1.2.4/src/cli.rs000064400000000000000000000171671046102023000131350ustar 00000000000000use std::fmt; use clap::{Parser, ValueEnum, ValueHint}; // For single thread mode set this variable on your command line: // export RAYON_NUM_THREADS=1 /// Like du but more intuitive #[derive(Debug, Parser)] #[command(name("Dust"), version)] pub struct Cli { /// Depth to show #[arg(short, long)] pub depth: Option, /// Number of threads to use #[arg(short('T'), long)] pub threads: Option, /// Specify a config file to use #[arg(long, value_name("FILE"), value_hint(ValueHint::FilePath))] pub config: Option, /// Display the 'n' largest entries. (Default is terminal_height) #[arg(short, long, value_name("NUMBER"))] pub number_of_lines: Option, /// Subdirectories will not have their path shortened #[arg(short('p'), long)] pub full_paths: bool, /// Exclude any file or directory with this path #[arg(short('X'), long, value_name("PATH"), value_hint(ValueHint::AnyPath))] pub ignore_directory: Option>, /// Exclude any file or directory with a regex matching that listed in this /// file, the file entries will be added to the ignore regexs provided by /// --invert_filter #[arg(short('I'), long, value_name("FILE"), value_hint(ValueHint::FilePath))] pub ignore_all_in_file: Option, /// dereference sym links - Treat sym links as directories and go into them #[arg(short('L'), long)] pub dereference_links: bool, /// Only count the files and directories on the same filesystem as the /// supplied directory #[arg(short('x'), long)] pub limit_filesystem: bool, /// Use file length instead of blocks #[arg(short('s'), long)] pub apparent_size: bool, /// Print tree upside down (biggest highest) #[arg(short, long)] pub reverse: bool, /// No colors will be printed (Useful for commands like: watch) #[arg(short('c'), long)] pub no_colors: bool, /// Force colors print #[arg(short('C'), long)] pub force_colors: bool, /// No percent bars or percentages will be displayed #[arg(short('b'), long)] pub no_percent_bars: bool, /// percent bars moved to right side of screen #[arg(short('B'), long)] pub bars_on_right: bool, /// Minimum size file to include in output #[arg(short('z'), long)] pub min_size: Option, /// For screen readers. Removes bars. Adds new column: depth level (May want /// to use -p too for full path) #[arg(short('R'), long)] pub screen_reader: bool, /// No total row will be displayed #[arg(long)] pub skip_total: bool, /// Directory 'size' is number of child files instead of disk size #[arg(short, long)] pub filecount: bool, /// Do not display hidden files // Do not use 'h' this is used by 'help' #[arg(short, long)] pub ignore_hidden: bool, /// Exclude filepaths matching this regex. To ignore png files type: -v /// "\.png$" #[arg( short('v'), long, value_name("REGEX"), conflicts_with("filter"), conflicts_with("file_types") )] pub invert_filter: Option>, /// Only include filepaths matching this regex. For png files type: -e /// "\.png$" #[arg(short('e'), long, value_name("REGEX"), conflicts_with("file_types"))] pub filter: Option>, /// show only these file types #[arg(short('t'), long, conflicts_with("depth"), conflicts_with("only_dir"))] pub file_types: bool, /// Specify width of output overriding the auto detection of terminal width #[arg(short('w'), long, value_name("WIDTH"))] pub terminal_width: Option, /// Disable the progress indication. #[arg(short('P'), long)] pub no_progress: bool, /// Print path with errors. #[arg(long)] pub print_errors: bool, /// Only directories will be displayed. #[arg( short('D'), long, conflicts_with("only_file"), conflicts_with("file_types") )] pub only_dir: bool, /// Only files will be displayed. (Finds your largest files) #[arg(short('F'), long, conflicts_with("only_dir"))] pub only_file: bool, /// Changes output display size. si will print sizes in powers of 1000. b k /// m g t kb mb gb tb will print the whole tree in that size. #[arg(short, long, value_enum, value_name("FORMAT"), ignore_case(true))] pub output_format: Option, /// Specify memory to use as stack size - use if you see: 'fatal runtime /// error: stack overflow' (default low memory=1048576, high /// memory=1073741824) #[arg(short('S'), long)] pub stack_size: Option, /// Input files or directories. #[arg(value_name("PATH"), value_hint(ValueHint::AnyPath))] pub params: Option>, /// Output the directory tree as json to the current directory #[arg(short('j'), long)] pub output_json: bool, /// +/-n matches files modified more/less than n days ago , and n matches /// files modified exactly n days ago, days are rounded down.That is +n => /// (−∞, curr−(n+1)), n => [curr−(n+1), curr−n), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞) #[arg(short('M'), long, allow_hyphen_values(true))] pub mtime: Option, /// just like -mtime, but based on file access time #[arg(short('A'), long, allow_hyphen_values(true))] pub atime: Option, /// just like -mtime, but based on file change time #[arg(short('y'), long, allow_hyphen_values(true))] pub ctime: Option, /// Read NUL-terminated paths from FILE (use `-` for stdin). #[arg(long, value_hint(ValueHint::AnyPath), conflicts_with("files_from"))] pub files0_from: Option, /// Read newline-terminated paths from FILE (use `-` for stdin). #[arg(long, value_hint(ValueHint::AnyPath), conflicts_with("files0_from"))] pub files_from: Option, /// Keep these directories collapsed #[arg(long, value_hint(ValueHint::AnyPath))] pub collapse: Option>, /// Directory 'size' is max filetime of child files instead of disk size. /// while a/c/m for last accessed/changed/modified time #[arg(short('m'), long, value_enum)] pub filetime: Option, } #[derive(Clone, Copy, Debug, ValueEnum)] #[value(rename_all = "lower")] pub enum OutputFormat { /// SI prefix (powers of 1000) SI, /// byte (B) B, /// kibibyte (KiB) #[value(name = "k", alias("kib"))] KiB, /// mebibyte (MiB) #[value(name = "m", alias("mib"))] MiB, /// gibibyte (GiB) #[value(name = "g", alias("gib"))] GiB, /// tebibyte (TiB) #[value(name = "t", alias("tib"))] TiB, /// kilobyte (kB) KB, /// megabyte (MB) MB, /// gigabyte (GB) GB, /// terabyte (TB) TB, } impl fmt::Display for OutputFormat { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::SI => write!(f, "si"), Self::B => write!(f, "b"), Self::KiB => write!(f, "k"), Self::MiB => write!(f, "m"), Self::GiB => write!(f, "g"), Self::TiB => write!(f, "t"), Self::KB => write!(f, "kb"), Self::MB => write!(f, "mb"), Self::GB => write!(f, "gb"), Self::TB => write!(f, "tb"), } } } #[derive(Clone, Copy, Debug, ValueEnum)] pub enum FileTime { /// last accessed time #[value(name = "a", alias("accessed"))] Accessed, /// last changed time #[value(name = "c", alias("changed"))] Changed, /// last modified time #[value(name = "m", alias("modified"))] Modified, } du-dust-1.2.4/src/config.rs000064400000000000000000000351421046102023000136240ustar 00000000000000use crate::node::FileTime; use chrono::{Local, TimeZone}; use config_file::FromConfigFile; use regex::Regex; use serde::Deserialize; use std::path::Path; use std::path::PathBuf; use crate::cli::Cli; use crate::dir_walker::Operator; use crate::display::get_number_format; pub static DAY_SECONDS: i64 = 24 * 60 * 60; #[derive(Deserialize, Default)] #[serde(rename_all = "kebab-case")] pub struct Config { pub display_full_paths: Option, pub display_apparent_size: Option, pub reverse: Option, pub no_colors: Option, pub force_colors: Option, pub no_bars: Option, pub skip_total: Option, pub screen_reader: Option, pub ignore_hidden: Option, pub output_format: Option, pub min_size: Option, pub only_dir: Option, pub only_file: Option, pub disable_progress: Option, pub depth: Option, pub bars_on_right: Option, pub stack_size: Option, pub threads: Option, pub output_json: Option, pub print_errors: Option, pub files0_from: Option, pub number_of_lines: Option, pub files_from: Option, pub collapse: Option>, } impl Config { pub fn get_files0_from(&self, options: &Cli) -> Option { let from_file = &options.files0_from; match from_file { None => self.files0_from.as_ref().map(|x| x.to_string()), Some(x) => Some(x.to_string()), } } pub fn get_files_from(&self, options: &Cli) -> Option { let from_file = &options.files_from; match from_file { None => self.files_from.as_ref().map(|x| x.to_string()), Some(x) => Some(x.to_string()), } } pub fn get_no_colors(&self, options: &Cli) -> bool { Some(true) == self.no_colors || options.no_colors } pub fn get_force_colors(&self, options: &Cli) -> bool { Some(true) == self.force_colors || options.force_colors } pub fn get_disable_progress(&self, options: &Cli) -> bool { Some(true) == self.disable_progress || options.no_progress } pub fn get_apparent_size(&self, options: &Cli) -> bool { Some(true) == self.display_apparent_size || options.apparent_size } pub fn get_ignore_hidden(&self, options: &Cli) -> bool { Some(true) == self.ignore_hidden || options.ignore_hidden } pub fn get_full_paths(&self, options: &Cli) -> bool { Some(true) == self.display_full_paths || options.full_paths } pub fn get_reverse(&self, options: &Cli) -> bool { Some(true) == self.reverse || options.reverse } pub fn get_no_bars(&self, options: &Cli) -> bool { Some(true) == self.no_bars || options.no_percent_bars } pub fn get_output_format(&self, options: &Cli) -> String { let out_fmt = options.output_format; (match out_fmt { None => match &self.output_format { None => "".to_string(), Some(x) => x.to_string(), }, Some(x) => x.to_string(), }) .to_lowercase() } pub fn get_filetime(&self, options: &Cli) -> Option { options.filetime.map(FileTime::from) } pub fn get_skip_total(&self, options: &Cli) -> bool { Some(true) == self.skip_total || options.skip_total } pub fn get_screen_reader(&self, options: &Cli) -> bool { Some(true) == self.screen_reader || options.screen_reader } pub fn get_depth(&self, options: &Cli) -> usize { if let Some(v) = options.depth { return v; } self.depth.unwrap_or(usize::MAX) } pub fn get_min_size(&self, options: &Cli) -> Option { let size_from_param = options.min_size.as_ref(); self._get_min_size(size_from_param) } fn _get_min_size(&self, min_size: Option<&String>) -> Option { let size_from_param = min_size.and_then(|a| convert_min_size(a)); if size_from_param.is_none() { self.min_size .as_ref() .and_then(|a| convert_min_size(a.as_ref())) } else { size_from_param } } pub fn get_only_dir(&self, options: &Cli) -> bool { Some(true) == self.only_dir || options.only_dir } pub fn get_print_errors(&self, options: &Cli) -> bool { Some(true) == self.print_errors || options.print_errors } pub fn get_only_file(&self, options: &Cli) -> bool { Some(true) == self.only_file || options.only_file } pub fn get_bars_on_right(&self, options: &Cli) -> bool { Some(true) == self.bars_on_right || options.bars_on_right } pub fn get_custom_stack_size(&self, options: &Cli) -> Option { let from_cmd_line = options.stack_size; if from_cmd_line.is_none() { self.stack_size } else { from_cmd_line } } pub fn get_threads(&self, options: &Cli) -> Option { let from_cmd_line = options.threads; if from_cmd_line.is_none() { self.threads } else { from_cmd_line } } pub fn get_output_json(&self, options: &Cli) -> bool { Some(true) == self.output_json || options.output_json } pub fn get_number_of_lines(&self, options: &Cli) -> Option { let from_cmd_line = options.number_of_lines; if from_cmd_line.is_none() { self.number_of_lines } else { from_cmd_line } } pub fn get_modified_time_operator(&self, options: &Cli) -> Option<(Operator, i64)> { get_filter_time_operator(options.mtime.as_ref(), get_current_date_epoch_seconds()) } pub fn get_accessed_time_operator(&self, options: &Cli) -> Option<(Operator, i64)> { get_filter_time_operator(options.atime.as_ref(), get_current_date_epoch_seconds()) } pub fn get_changed_time_operator(&self, options: &Cli) -> Option<(Operator, i64)> { get_filter_time_operator(options.ctime.as_ref(), get_current_date_epoch_seconds()) } pub fn get_collapse(&self, options: &Cli) -> Option> { if self.collapse.is_none() { options.collapse.clone() } else { self.collapse.clone() } } } fn get_current_date_epoch_seconds() -> i64 { // calculate current date epoch seconds let now = Local::now(); let current_date = now.date_naive(); let current_date_time = current_date.and_hms_opt(0, 0, 0).unwrap(); Local .from_local_datetime(¤t_date_time) .unwrap() .timestamp() } fn get_filter_time_operator( option_value: Option<&String>, current_date_epoch_seconds: i64, ) -> Option<(Operator, i64)> { match option_value { Some(val) => { let time = current_date_epoch_seconds - val .parse::() .unwrap_or_else(|_| panic!("invalid data format")) .abs() * DAY_SECONDS; match val.chars().next().expect("Value should not be empty") { '+' => Some((Operator::LessThan, time - DAY_SECONDS)), '-' => Some((Operator::GreaterThan, time)), _ => Some((Operator::Equal, time - DAY_SECONDS)), } } None => None, } } fn convert_min_size(input: &str) -> Option { let re = Regex::new(r"([0-9]+)(\w*)").unwrap(); if let Some(cap) = re.captures(input) { let (_, [digits, letters]) = cap.extract(); // Failure to parse should be impossible due to regex match let digits_as_usize: Option = digits.parse().ok(); match digits_as_usize { Some(parsed_digits) => { let number_format = get_number_format(&letters.to_lowercase()); match number_format { Some((multiple, _)) => Some(parsed_digits * (multiple as usize)), None => { if letters.is_empty() { Some(parsed_digits) } else { eprintln!("Ignoring invalid min-size: {input}"); None } } } } None => None, } } else { None } } fn get_config_locations(base: PathBuf) -> Vec { vec![ base.join(".dust.toml"), base.join(".config").join("dust").join("config.toml"), ] } pub fn get_config(conf_path: Option<&String>) -> Config { match conf_path { Some(path_str) => { let path = Path::new(path_str); if path.exists() { match Config::from_config_file(path) { Ok(config) => return config, Err(e) => { eprintln!("Ignoring invalid config file '{}': {}", &path.display(), e) } } } else { eprintln!("Config file {:?} doesn't exists", &path.display()); } } None => { if let Some(home) = std::env::home_dir() { for path in get_config_locations(home) { if path.exists() && let Ok(config) = Config::from_config_file(&path) { return config; } } } } } Config { ..Default::default() } } #[cfg(test)] mod tests { #[allow(unused_imports)] use super::*; use chrono::{Datelike, Timelike}; use clap::Parser; #[test] fn test_get_current_date_epoch_seconds() { let epoch_seconds = get_current_date_epoch_seconds(); let dt = Local.timestamp_opt(epoch_seconds, 0).unwrap(); assert_eq!(dt.hour(), 0); assert_eq!(dt.minute(), 0); assert_eq!(dt.second(), 0); assert_eq!(dt.date_naive().day(), Local::now().date_naive().day()); assert_eq!(dt.date_naive().month(), Local::now().date_naive().month()); assert_eq!(dt.date_naive().year(), Local::now().date_naive().year()); } #[test] fn test_conversion() { assert_eq!(convert_min_size("55"), Some(55)); assert_eq!(convert_min_size("12344321"), Some(12344321)); assert_eq!(convert_min_size("95RUBBISH"), None); assert_eq!(convert_min_size("10Ki"), Some(10 * 1024)); assert_eq!(convert_min_size("10MiB"), Some(10 * 1024usize.pow(2))); assert_eq!(convert_min_size("10M"), Some(10 * 1024usize.pow(2))); assert_eq!(convert_min_size("10Mb"), Some(10 * 1000usize.pow(2))); assert_eq!(convert_min_size("2Gi"), Some(2 * 1024usize.pow(3))); } #[test] fn test_min_size_from_config_applied_or_overridden() { let c = Config { min_size: Some("1KiB".to_owned()), ..Default::default() }; assert_eq!(c._get_min_size(None), Some(1024)); assert_eq!(c._get_min_size(Some(&"2KiB".into())), Some(2048)); assert_eq!(c._get_min_size(Some(&"1kb".into())), Some(1000)); assert_eq!(c._get_min_size(Some(&"2KB".into())), Some(2000)); } #[test] fn test_get_depth() { // No config and no flag. let c = Config::default(); let args = get_args(vec![]); assert_eq!(c.get_depth(&args), usize::MAX); // Config is not defined and flag is defined. let c = Config::default(); let args = get_args(vec!["dust", "--depth", "5"]); assert_eq!(c.get_depth(&args), 5); // Config is defined and flag is not defined. let c = Config { depth: Some(3), ..Default::default() }; let args = get_args(vec![]); assert_eq!(c.get_depth(&args), 3); // Both config and flag are defined. let c = Config { depth: Some(3), ..Default::default() }; let args = get_args(vec!["dust", "--depth", "5"]); assert_eq!(c.get_depth(&args), 5); } fn get_args(args: Vec<&str>) -> Cli { Cli::parse_from(args) } #[test] fn test_get_filetime() { // No config and no flag. let c = Config::default(); let args = get_filetime_args(vec!["dust"]); assert_eq!(c.get_filetime(&args), None); // Config is not defined and flag is defined as access time let c = Config::default(); let args = get_filetime_args(vec!["dust", "--filetime", "a"]); assert_eq!(c.get_filetime(&args), Some(FileTime::Accessed)); let c = Config::default(); let args = get_filetime_args(vec!["dust", "--filetime", "accessed"]); assert_eq!(c.get_filetime(&args), Some(FileTime::Accessed)); // Config is not defined and flag is defined as modified time let c = Config::default(); let args = get_filetime_args(vec!["dust", "--filetime", "m"]); assert_eq!(c.get_filetime(&args), Some(FileTime::Modified)); let c = Config::default(); let args = get_filetime_args(vec!["dust", "--filetime", "modified"]); assert_eq!(c.get_filetime(&args), Some(FileTime::Modified)); // Config is not defined and flag is defined as changed time let c = Config::default(); let args = get_filetime_args(vec!["dust", "--filetime", "c"]); assert_eq!(c.get_filetime(&args), Some(FileTime::Changed)); let c = Config::default(); let args = get_filetime_args(vec!["dust", "--filetime", "changed"]); assert_eq!(c.get_filetime(&args), Some(FileTime::Changed)); } fn get_filetime_args(args: Vec<&str>) -> Cli { Cli::parse_from(args) } #[test] fn test_get_number_of_lines() { // No config and no flag. let c = Config::default(); let args = get_args(vec![]); assert_eq!(c.get_number_of_lines(&args), None); // Config is not defined and flag is defined. let c = Config::default(); let args = get_args(vec!["dust", "--number-of-lines", "5"]); assert_eq!(c.get_number_of_lines(&args), Some(5)); // Config is defined and flag is not defined. let c = Config { number_of_lines: Some(3), ..Default::default() }; let args = get_args(vec![]); assert_eq!(c.get_number_of_lines(&args), Some(3)); // Both config and flag are defined. let c = Config { number_of_lines: Some(3), ..Default::default() }; let args = get_args(vec!["dust", "--number-of-lines", "5"]); assert_eq!(c.get_number_of_lines(&args), Some(5)); } } du-dust-1.2.4/src/dir_walker.rs000064400000000000000000000337431046102023000145070ustar 00000000000000use std::cmp::Ordering; use std::fs; use std::io::Error; use std::sync::Arc; use std::sync::Mutex; use crate::node::Node; use crate::progress::ORDERING; use crate::progress::Operation; use crate::progress::PAtomicInfo; use crate::progress::RuntimeErrors; use crate::utils::is_filtered_out_due_to_file_time; use crate::utils::is_filtered_out_due_to_invert_regex; use crate::utils::is_filtered_out_due_to_regex; use rayon::iter::ParallelBridge; use rayon::prelude::ParallelIterator; use regex::Regex; use std::path::Path; use std::path::PathBuf; use std::collections::HashSet; use crate::node::build_node; use std::fs::DirEntry; use crate::node::FileTime; use crate::platform::get_metadata; #[derive(Debug)] pub enum Operator { Equal = 0, LessThan = 1, GreaterThan = 2, } pub struct WalkData<'a> { pub ignore_directories: HashSet, pub filter_regex: &'a [Regex], pub invert_filter_regex: &'a [Regex], pub allowed_filesystems: HashSet, pub filter_modified_time: Option<(Operator, i64)>, pub filter_accessed_time: Option<(Operator, i64)>, pub filter_changed_time: Option<(Operator, i64)>, pub use_apparent_size: bool, pub by_filecount: bool, pub by_filetime: &'a Option, pub ignore_hidden: bool, pub follow_links: bool, pub progress_data: Arc, pub errors: Arc>, } pub fn walk_it(dirs: HashSet, walk_data: &WalkData) -> Vec { let mut inodes = HashSet::new(); let top_level_nodes: Vec<_> = dirs .into_iter() .filter_map(|d| { let prog_data = &walk_data.progress_data; prog_data.clear_state(&d); let node = walk(d, walk_data, 0)?; prog_data.state.store(Operation::PREPARING, ORDERING); clean_inodes(node, &mut inodes, walk_data) }) .collect(); top_level_nodes } // Remove files which have the same inode, we don't want to double count them. fn clean_inodes(x: Node, inodes: &mut HashSet<(u64, u64)>, walk_data: &WalkData) -> Option { if !walk_data.use_apparent_size && let Some(id) = x.inode_device && !inodes.insert(id) { return None; } // Sort Nodes so iteration order is predictable let mut tmp: Vec<_> = x.children; tmp.sort_by(sort_by_inode); let new_children: Vec<_> = tmp .into_iter() .filter_map(|c| clean_inodes(c, inodes, walk_data)) .collect(); let actual_size = if walk_data.by_filetime.is_some() { // If by_filetime is Some, directory 'size' is the maximum filetime among child files instead of disk size new_children .iter() .map(|c| c.size) .chain(std::iter::once(x.size)) .max() .unwrap_or(0) } else { // If by_filetime is None, directory 'size' is the sum of disk sizes or file counts of child files x.size + new_children.iter().map(|c| c.size).sum::() }; Some(Node { name: x.name, size: actual_size, children: new_children, inode_device: x.inode_device, depth: x.depth, }) } fn sort_by_inode(a: &Node, b: &Node) -> std::cmp::Ordering { // Sorting by inode is quicker than by sorting by name/size match (a.inode_device, b.inode_device) { (Some(x), Some(y)) => { if x.0 != y.0 { x.0.cmp(&y.0) } else if x.1 != y.1 { x.1.cmp(&y.1) } else { a.name.cmp(&b.name) } } (Some(_), None) => Ordering::Greater, (None, Some(_)) => Ordering::Less, (None, None) => a.name.cmp(&b.name), } } // Check if `path` is inside ignored directory fn is_ignored_path(path: &Path, walk_data: &WalkData) -> bool { if walk_data.ignore_directories.contains(path) { return true; } // Entry is inside an ignored absolute path // Absolute paths should be canonicalized before being added to `WalkData.ignore_directories` for ignored_path in walk_data.ignore_directories.iter() { if !ignored_path.is_absolute() { continue; } let absolute_entry_path = std::fs::canonicalize(path).unwrap_or_default(); if absolute_entry_path.starts_with(ignored_path) { return true; } } false } fn ignore_file(entry: &DirEntry, walk_data: &WalkData) -> bool { if is_ignored_path(&entry.path(), walk_data) { return true; } let is_dot_file = entry.file_name().to_str().unwrap_or("").starts_with('.'); let follow_links = walk_data.follow_links && entry.file_type().is_ok_and(|ft| ft.is_symlink()); if !walk_data.allowed_filesystems.is_empty() { let size_inode_device = get_metadata(entry.path(), false, follow_links); if let Some((_size, Some((_id, dev)), _gunk)) = size_inode_device && !walk_data.allowed_filesystems.contains(&dev) { return true; } } if walk_data.filter_accessed_time.is_some() || walk_data.filter_modified_time.is_some() || walk_data.filter_changed_time.is_some() { let size_inode_device = get_metadata(entry.path(), false, follow_links); if let Some((_, _, (modified_time, accessed_time, changed_time))) = size_inode_device && entry.path().is_file() && [ (&walk_data.filter_modified_time, modified_time), (&walk_data.filter_accessed_time, accessed_time), (&walk_data.filter_changed_time, changed_time), ] .iter() .any(|(filter_time, actual_time)| { is_filtered_out_due_to_file_time(filter_time, *actual_time) }) { return true; } } // Keeping `walk_data.filter_regex.is_empty()` is important for performance reasons, it stops unnecessary work if !walk_data.filter_regex.is_empty() && entry.path().is_file() && is_filtered_out_due_to_regex(walk_data.filter_regex, &entry.path()) { return true; } if !walk_data.invert_filter_regex.is_empty() && entry.path().is_file() && is_filtered_out_due_to_invert_regex(walk_data.invert_filter_regex, &entry.path()) { return true; } is_dot_file && walk_data.ignore_hidden } fn walk(dir: PathBuf, walk_data: &WalkData, depth: usize) -> Option { let prog_data = &walk_data.progress_data; let errors = &walk_data.errors; let children = if dir.is_dir() { let read_dir = fs::read_dir(&dir); match read_dir { Ok(entries) => { entries .into_iter() .par_bridge() .filter_map(|entry| { match entry { Ok(ref entry) => { // uncommenting the below line gives simpler code but // rayon doesn't parallelize as well giving a 3X performance drop // hence we unravel the recursion a bit // return walk(entry.path(), walk_data, depth) if !ignore_file(entry, walk_data) && let Ok(data) = entry.file_type() { if data.is_dir() || (walk_data.follow_links && data.is_symlink()) { return walk(entry.path(), walk_data, depth + 1); } let node = build_node( entry.path(), vec![], data.is_symlink(), data.is_file(), depth, walk_data, ); prog_data.num_files.fetch_add(1, ORDERING); if let Some(ref file) = node { prog_data.total_file_size.fetch_add(file.size, ORDERING); } return node; } } Err(ref failed) => { if handle_error_and_retry(failed, &dir, walk_data) { return walk(dir.clone(), walk_data, depth); } } } None }) .collect() } Err(failed) => { if handle_error_and_retry(&failed, &dir, walk_data) { return walk(dir, walk_data, depth); } else { vec![] } } } } else { if !dir.is_file() { let mut editable_error = errors.lock().unwrap(); let bad_file = dir.as_os_str().to_string_lossy().into(); editable_error.file_not_found.insert(bad_file); } vec![] }; let is_symlink = if walk_data.follow_links { match fs::symlink_metadata(&dir) { Ok(metadata) => metadata.file_type().is_symlink(), Err(_) => false, } } else { false }; build_node(dir, children, is_symlink, false, depth, walk_data) } fn handle_error_and_retry(failed: &Error, dir: &Path, walk_data: &WalkData) -> bool { let mut editable_error = walk_data.errors.lock().unwrap(); match failed.kind() { std::io::ErrorKind::PermissionDenied => { editable_error .no_permissions .insert(dir.to_string_lossy().into()); } std::io::ErrorKind::InvalidInput => { editable_error .no_permissions .insert(dir.to_string_lossy().into()); } std::io::ErrorKind::NotFound => { editable_error.file_not_found.insert(failed.to_string()); } std::io::ErrorKind::Interrupted => { editable_error.interrupted_error += 1; // This does happen on some systems. It was set to 3 but sometimes dust runs would exceed this // However, if there is no limit this results in infinite retrys and dust never finishes if editable_error.interrupted_error > 999 { panic!("Multiple Interrupted Errors occurred while scanning filesystem. Aborting"); } else { return true; } } _ => { editable_error.unknown_error.insert(failed.to_string()); } } false } mod tests { #[allow(unused_imports)] use super::*; #[cfg(test)] fn create_node() -> Node { Node { name: PathBuf::new(), size: 10, children: vec![], inode_device: Some((5, 6)), depth: 0, } } #[cfg(test)] fn create_walker<'a>(use_apparent_size: bool) -> WalkData<'a> { use crate::PIndicator; let indicator = PIndicator::build_me(); WalkData { ignore_directories: HashSet::new(), filter_regex: &[], invert_filter_regex: &[], allowed_filesystems: HashSet::new(), filter_modified_time: Some((Operator::GreaterThan, 0)), filter_accessed_time: Some((Operator::GreaterThan, 0)), filter_changed_time: Some((Operator::GreaterThan, 0)), use_apparent_size, by_filecount: false, by_filetime: &None, ignore_hidden: false, follow_links: false, progress_data: indicator.data.clone(), errors: Arc::new(Mutex::new(RuntimeErrors::default())), } } #[test] #[allow(clippy::redundant_clone)] fn test_should_ignore_file() { let mut inodes = HashSet::new(); let n = create_node(); let walkdata = create_walker(false); // First time we insert the node assert_eq!( clean_inodes(n.clone(), &mut inodes, &walkdata), Some(n.clone()) ); // Second time is a duplicate - we ignore it assert_eq!(clean_inodes(n.clone(), &mut inodes, &walkdata), None); } #[test] #[allow(clippy::redundant_clone)] fn test_should_not_ignore_files_if_using_apparent_size() { let mut inodes = HashSet::new(); let n = create_node(); let walkdata = create_walker(true); // If using apparent size we include Nodes, even if duplicate inodes assert_eq!( clean_inodes(n.clone(), &mut inodes, &walkdata), Some(n.clone()) ); assert_eq!( clean_inodes(n.clone(), &mut inodes, &walkdata), Some(n.clone()) ); } #[test] fn test_total_ordering_of_sort_by_inode() { use std::str::FromStr; let a = Node { name: PathBuf::from_str("a").unwrap(), size: 0, children: vec![], inode_device: Some((3, 66310)), depth: 0, }; let b = Node { name: PathBuf::from_str("b").unwrap(), size: 0, children: vec![], inode_device: None, depth: 0, }; let c = Node { name: PathBuf::from_str("c").unwrap(), size: 0, children: vec![], inode_device: Some((1, 66310)), depth: 0, }; assert_eq!(sort_by_inode(&a, &b), Ordering::Greater); assert_eq!(sort_by_inode(&a, &c), Ordering::Greater); assert_eq!(sort_by_inode(&c, &b), Ordering::Greater); assert_eq!(sort_by_inode(&b, &a), Ordering::Less); assert_eq!(sort_by_inode(&c, &a), Ordering::Less); assert_eq!(sort_by_inode(&b, &c), Ordering::Less); } } du-dust-1.2.4/src/display.rs000064400000000000000000000546351046102023000140340ustar 00000000000000use crate::display_node::DisplayNode; use crate::node::FileTime; use lscolors::{LsColors, Style}; use nu_ansi_term::Color::Red; use unicode_width::UnicodeWidthStr; use stfu8::encode_u8; use chrono::{DateTime, Local, TimeZone, Utc}; use std::cmp::max; use std::cmp::min; use std::fs; use std::iter::repeat_n; use std::path::Path; use thousands::Separable; pub static UNITS: [char; 5] = ['P', 'T', 'G', 'M', 'K']; static BLOCKS: [char; 5] = ['█', '▓', '▒', '░', ' ']; const FILETIME_SHOW_LENGTH: usize = 19; pub struct InitialDisplayData { pub short_paths: bool, pub is_reversed: bool, pub colors_on: bool, pub by_filecount: bool, pub by_filetime: Option, pub is_screen_reader: bool, pub output_format: String, pub bars_on_right: bool, } pub struct DisplayData { pub initial: InitialDisplayData, pub num_chars_needed_on_left_most: usize, pub base_size: u64, pub longest_string_length: usize, pub ls_colors: LsColors, } impl DisplayData { fn get_tree_chars(&self, was_i_last: bool, has_children: bool) -> &'static str { match (self.initial.is_reversed, was_i_last, has_children) { (true, true, true) => "┌─┴", (true, true, false) => "┌──", (true, false, true) => "├─┴", (true, false, false) => "├──", (false, true, true) => "└─┬", (false, true, false) => "└──", (false, false, true) => "├─┬", (false, false, false) => "├──", } } fn is_biggest(&self, num_siblings: usize, max_siblings: u64) -> bool { if self.initial.is_reversed { num_siblings == (max_siblings - 1) as usize } else { num_siblings == 0 } } fn is_last(&self, num_siblings: usize, max_siblings: u64) -> bool { if self.initial.is_reversed { num_siblings == 0 } else { num_siblings == (max_siblings - 1) as usize } } fn percent_size(&self, node: &DisplayNode) -> f32 { let result = node.size as f32 / self.base_size as f32; if result.is_normal() { result } else { 0.0 } } } 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 } // TODO: can we test this? fn generate_bar(&self, node: &DisplayNode, level: usize) -> String { if self.display_data.initial.is_screen_reader { return level.to_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 - level.clamp(1, 4); let itr: Box> = if self.display_data.initial.bars_on_right { Box::new(self.percent_bar.chars()) } else { Box::new(self.percent_bar.chars().rev()) }; for c in itr { 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); } } if self.display_data.initial.bars_on_right { new_bar } else { new_bar.chars().rev().collect() } } } pub fn draw_it( idd: InitialDisplayData, root_node: &DisplayNode, no_percent_bars: bool, terminal_width: usize, skip_total: bool, ) { let num_chars_needed_on_left_most = if idd.by_filecount { let max_size = root_node.size; max_size.separate_with_commas().chars().count() } else if idd.by_filetime.is_some() { FILETIME_SHOW_LENGTH } else { find_biggest_size_str(root_node, &idd.output_format) }; assert!( terminal_width > num_chars_needed_on_left_most + 2, "Not enough terminal width" ); let allowed_width = terminal_width - num_chars_needed_on_left_most - 2; let num_indent_chars = 3; let longest_string_length = find_longest_dir_name(root_node, num_indent_chars, allowed_width, &idd); let max_bar_length = if no_percent_bars || longest_string_length + 7 >= allowed_width { 0 } else { allowed_width - longest_string_length - 7 }; let first_size_bar = repeat_n(BLOCKS[0], max_bar_length).collect(); let display_data = DisplayData { initial: idd, num_chars_needed_on_left_most, base_size: root_node.size, longest_string_length, ls_colors: LsColors::from_env().unwrap_or_default(), }; let draw_data = DrawData { indent: "".to_string(), percent_bar: first_size_bar, display_data: &display_data, }; if !skip_total { display_node(root_node, &draw_data, true, true); } else { for (count, c) in root_node .get_children_from_node(draw_data.display_data.initial.is_reversed) .enumerate() { let is_biggest = display_data.is_biggest(count, root_node.num_siblings()); let was_i_last = display_data.is_last(count, root_node.num_siblings()); display_node(c, &draw_data, is_biggest, was_i_last); } } } fn find_biggest_size_str(node: &DisplayNode, output_format: &str) -> usize { let mut mx = human_readable_number(node.size, output_format) .chars() .count(); for n in node.children.iter() { mx = max(mx, find_biggest_size_str(n, output_format)); } mx } fn find_longest_dir_name( node: &DisplayNode, indent: usize, terminal: usize, idd: &InitialDisplayData, ) -> usize { let printable_name = get_printable_name(&node.name, idd.short_paths); let longest = if idd.is_screen_reader { UnicodeWidthStr::width(&*printable_name) + 1 } else { min( UnicodeWidthStr::width(&*printable_name) + 1 + indent, terminal, ) }; // each none root tree drawing is 2 more chars, hence we increment indent by 2 node.children .iter() .map(|c| find_longest_dir_name(c, indent + 2, terminal, idd)) .fold(longest, max) } fn display_node(node: &DisplayNode, draw_data: &DrawData, is_biggest: bool, is_last: bool) { // hacky way of working out how deep we are in the tree let indent = draw_data.get_new_indent(!node.children.is_empty(), is_last); let level = ((indent.chars().count() - 1) / 2) - 1; let bar_text = draw_data.generate_bar(node, level); let to_print = format_string(node, &indent, &bar_text, is_biggest, draw_data.display_data); if !draw_data.display_data.initial.is_reversed { println!("{to_print}") } let dd = DrawData { indent: clean_indentation_string(&indent), percent_bar: bar_text, display_data: draw_data.display_data, }; let num_siblings = node.num_siblings(); for (count, c) in node .get_children_from_node(draw_data.display_data.initial.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 draw_data.display_data.initial.is_reversed { println!("{to_print}") } } fn clean_indentation_string(s: &str) -> String { let mut is: String = s.into(); // For reversed: is = is.replace("┌─┴", " "); is = is.replace("┌──", " "); is = is.replace("├─┴", "│ "); is = is.replace("─┴", " "); // For normal is = is.replace("└─┬", " "); is = is.replace("└──", " "); is = is.replace("├─┬", "│ "); is = is.replace("─┬", " "); // For both is = is.replace("├──", "│ "); is } pub fn get_printable_name>(dir_name: &P, short_paths: bool) -> String { let dir_name = dir_name.as_ref(); let printable_name = { if short_paths { match dir_name.parent() { Some(prefix) => match dir_name.strip_prefix(prefix) { Ok(base) => base, Err(_) => dir_name, }, None => dir_name, } } else { dir_name } }; encode_u8(printable_name.display().to_string().as_bytes()) } fn pad_or_trim_filename(node: &DisplayNode, indent: &str, display_data: &DisplayData) -> String { let name = get_printable_name(&node.name, display_data.initial.short_paths); let indent_and_name = format!("{indent} {name}"); let width = UnicodeWidthStr::width(&*indent_and_name); assert!( display_data.longest_string_length >= width, "Terminal width not wide enough to draw directory tree" ); // Add spaces after the filename so we can draw the % used bar chart. name + " " .repeat(display_data.longest_string_length - width) .as_str() } fn maybe_trim_filename(name_in: String, indent: &str, display_data: &DisplayData) -> String { let indent_length = UnicodeWidthStr::width(indent); assert!( display_data.longest_string_length >= indent_length + 2, "Terminal width not wide enough to draw directory tree" ); let max_size = display_data.longest_string_length - indent_length; if UnicodeWidthStr::width(&*name_in) > max_size { let name = name_in.chars().take(max_size - 2).collect::(); name + ".." } else { name_in } } pub fn format_string( node: &DisplayNode, indent: &str, bars: &str, is_biggest: bool, display_data: &DisplayData, ) -> String { let (percent, name_and_padding) = get_name_percent(node, indent, bars, display_data); let pretty_size = get_pretty_size(node, is_biggest, display_data); let pretty_name = get_pretty_name(node, name_and_padding, display_data); // we can clean this and the method below somehow, not sure yet if display_data.initial.is_screen_reader { // if screen_reader then bars is 'depth' format!("{pretty_name} {bars} {pretty_size}{percent}") } else if display_data.initial.by_filetime.is_some() { format!("{pretty_size} {indent}{pretty_name}") } else { format!("{pretty_size} {indent} {pretty_name}{percent}") } } fn get_name_percent( node: &DisplayNode, indent: &str, bar_chart: &str, display_data: &DisplayData, ) -> (String, String) { if display_data.initial.is_screen_reader { let percent = display_data.percent_size(node) * 100.0; let percent_size_str = format!("{percent:.0}%"); let percents = format!(" {percent_size_str:>4}",); let name = pad_or_trim_filename(node, "", display_data); (percents, name) // Bar chart being empty may come from either config or the screen not being wide enough } else if !bar_chart.is_empty() { let percent = display_data.percent_size(node) * 100.0; let percent_size_str = format!("{percent:.0}%"); let percents = format!("│{bar_chart} │ {percent_size_str:>4}"); let name_and_padding = pad_or_trim_filename(node, indent, display_data); (percents, name_and_padding) } else { let n = get_printable_name(&node.name, display_data.initial.short_paths); let name = maybe_trim_filename(n, indent, display_data); ("".into(), name) } } fn get_pretty_size(node: &DisplayNode, is_biggest: bool, display_data: &DisplayData) -> String { let output = if display_data.initial.by_filecount { node.size.separate_with_commas() } else if display_data.initial.by_filetime.is_some() { get_pretty_file_modified_time(node.size as i64) } else { human_readable_number(node.size, &display_data.initial.output_format) }; let spaces_to_add = display_data.num_chars_needed_on_left_most - output.chars().count(); let output = " ".repeat(spaces_to_add) + output.as_str(); if is_biggest && display_data.initial.colors_on { format!("{}", Red.paint(output)) } else { output } } fn get_pretty_file_modified_time(timestamp: i64) -> String { let datetime: DateTime = Utc.timestamp_opt(timestamp, 0).unwrap(); let local_datetime = datetime.with_timezone(&Local); local_datetime.format("%Y-%m-%dT%H:%M:%S").to_string() } fn get_pretty_name( node: &DisplayNode, name_and_padding: String, display_data: &DisplayData, ) -> String { if display_data.initial.colors_on { let meta_result = fs::metadata(&node.name); let directory_color = display_data .ls_colors .style_for_path_with_metadata(&node.name, meta_result.as_ref().ok()); let ansi_style = directory_color .map(Style::to_nu_ansi_term_style) .unwrap_or_default(); let out = ansi_style.paint(name_and_padding); format!("{out}") } else { name_and_padding } } // If we are working with SI units or not pub fn get_type_of_thousand(output_str: &str) -> u64 { if output_str.is_empty() { 1024 } else if output_str == "si" { 1000 } else if output_str.contains('i') || output_str.len() == 1 { 1024 } else { 1000 } } pub fn get_number_format(output_str: &str) -> Option<(u64, char)> { if output_str.starts_with('b') { return Some((1, 'B')); } for (i, u) in UNITS.iter().enumerate() { if output_str.starts_with((*u).to_ascii_lowercase()) { let marker = get_type_of_thousand(output_str).pow((UNITS.len() - i) as u32); return Some((marker, *u)); } } None } pub fn human_readable_number(size: u64, output_str: &str) -> String { if output_str == "count" { return size.to_string(); }; match get_number_format(output_str) { Some((x, u)) => { format!("{}{}", (size / x), u) } None => { for (i, u) in UNITS.iter().enumerate() { let marker = get_type_of_thousand(output_str).pow((UNITS.len() - i) as u32); if size >= marker { if size / marker < 10 { return format!("{:.1}{}", (size as f32 / marker as f32), u); } else { return format!("{}{}", (size / marker), u); } } } format!("{size}B") } } } mod tests { #[allow(unused_imports)] use super::*; #[allow(unused_imports)] use std::path::PathBuf; #[cfg(test)] fn get_fake_display_data(longest_string_length: usize) -> DisplayData { let initial = InitialDisplayData { short_paths: true, is_reversed: false, colors_on: false, by_filecount: false, by_filetime: None, is_screen_reader: false, output_format: "".into(), bars_on_right: false, }; DisplayData { initial, num_chars_needed_on_left_most: 5, base_size: 2_u64.pow(12), // 4.0K longest_string_length, ls_colors: LsColors::from_env().unwrap_or_default(), } } #[test] fn test_format_str() { let n = DisplayNode { name: PathBuf::from("/short"), size: 2_u64.pow(12), // This is 4.0K children: vec![], }; let indent = "┌─┴"; let percent_bar = ""; let is_biggest = false; let data = get_fake_display_data(20); let s = format_string(&n, indent, percent_bar, is_biggest, &data); assert_eq!(s, " 4.0K ┌─┴ short"); } #[test] fn test_format_str_long_name() { let name = "very_long_name_longer_than_the_eighty_character_limit_very_long_name_this_bit_will_truncate"; let n = DisplayNode { name: PathBuf::from(name), size: 2_u64.pow(12), // This is 4.0K children: vec![], }; let indent = "┌─┴"; let percent_bar = ""; let is_biggest = false; let data = get_fake_display_data(64); let s = format_string(&n, indent, percent_bar, is_biggest, &data); assert_eq!( s, " 4.0K ┌─┴ very_long_name_longer_than_the_eighty_character_limit_very_.." ); } #[test] fn test_format_str_screen_reader() { let n = DisplayNode { name: PathBuf::from("/short"), size: 2_u64.pow(12), // This is 4.0K children: vec![], }; let indent = ""; let percent_bar = "3"; let is_biggest = false; let mut data = get_fake_display_data(20); data.initial.is_screen_reader = true; let s = format_string(&n, indent, percent_bar, is_biggest, &data); assert_eq!(s, "short 3 4.0K 100%"); } #[test] fn test_machine_readable_filecount() { assert_eq!(human_readable_number(1, "count"), "1"); assert_eq!(human_readable_number(1000, "count"), "1000"); assert_eq!(human_readable_number(1024, "count"), "1024"); } #[test] fn test_human_readable_number() { assert_eq!(human_readable_number(1, ""), "1B"); assert_eq!(human_readable_number(956, ""), "956B"); assert_eq!(human_readable_number(1004, ""), "1004B"); assert_eq!(human_readable_number(1024, ""), "1.0K"); assert_eq!(human_readable_number(1536, ""), "1.5K"); assert_eq!(human_readable_number(1024 * 512, ""), "512K"); assert_eq!(human_readable_number(1024 * 1024, ""), "1.0M"); assert_eq!(human_readable_number(1024 * 1024 * 1024 - 1, ""), "1023M"); assert_eq!(human_readable_number(1024 * 1024 * 1024 * 20, ""), "20G"); assert_eq!(human_readable_number(1024 * 1024 * 1024 * 1024, ""), "1.0T"); assert_eq!( human_readable_number(1024 * 1024 * 1024 * 1024 * 234, ""), "234T" ); assert_eq!( human_readable_number(1024 * 1024 * 1024 * 1024 * 1024, ""), "1.0P" ); } #[test] fn test_human_readable_number_si() { assert_eq!(human_readable_number(1024 * 100, ""), "100K"); assert_eq!(human_readable_number(1024 * 100, "si"), "102K"); } // Refer to https://en.wikipedia.org/wiki/Byte#Multiple-byte_units #[test] fn test_human_readable_number_kb() { let hrn = human_readable_number; assert_eq!(hrn(1023, "b"), "1023B"); assert_eq!(hrn(1000 * 1000, "bytes"), "1000000B"); assert_eq!(hrn(1023, "kb"), "1K"); assert_eq!(hrn(1023, "k"), "0K"); assert_eq!(hrn(1023, "kib"), "0K"); assert_eq!(hrn(1024, "kib"), "1K"); assert_eq!(hrn(1024 * 512, "kib"), "512K"); assert_eq!(hrn(1024 * 1024, "kib"), "1024K"); assert_eq!(hrn(1024 * 1000 * 1000 * 20, "kib"), "20000000K"); assert_eq!(hrn(1024 * 1024 * 1000 * 20, "mib"), "20000M"); assert_eq!(hrn(1024 * 1024 * 1024 * 20, "gib"), "20G"); } #[cfg(test)] fn build_draw_data(disp: &DisplayData, size: u32) -> (DrawData<'_>, DisplayNode) { let n = DisplayNode { name: PathBuf::from("/short"), size: 2_u64.pow(size), children: vec![], }; let first_size_bar = repeat_n(BLOCKS[0], 13).collect(); let dd = DrawData { indent: "".into(), percent_bar: first_size_bar, display_data: disp, }; (dd, n) } #[test] fn test_draw_data() { let disp = &get_fake_display_data(20); let (dd, n) = build_draw_data(disp, 12); let bar = dd.generate_bar(&n, 1); assert_eq!(bar, "█████████████"); } #[test] fn test_draw_data2() { let disp = &get_fake_display_data(20); let (dd, n) = build_draw_data(disp, 11); let bar = dd.generate_bar(&n, 2); assert_eq!(bar, "███████░░░░░░"); } #[test] fn test_draw_data3() { let mut disp = get_fake_display_data(20); let (dd, n) = build_draw_data(&disp, 11); let bar = dd.generate_bar(&n, 3); assert_eq!(bar, "███████▒▒▒▒▒▒"); disp.initial.bars_on_right = true; let (dd, n) = build_draw_data(&disp, 11); let bar = dd.generate_bar(&n, 3); assert_eq!(bar, "▒▒▒▒▒▒███████") } #[test] fn test_draw_data4() { let disp = &get_fake_display_data(20); let (dd, n) = build_draw_data(disp, 10); // After 4 we have no more levels of shading so 4+ is the same let bar = dd.generate_bar(&n, 4); assert_eq!(bar, "████▓▓▓▓▓▓▓▓▓"); let bar = dd.generate_bar(&n, 5); assert_eq!(bar, "████▓▓▓▓▓▓▓▓▓"); } #[test] fn test_get_pretty_file_modified_time() { // Create a timestamp for 2023-07-12 00:00:00 in local time let local_dt = Local.with_ymd_and_hms(2023, 7, 12, 0, 0, 0).unwrap(); let timestamp = local_dt.timestamp(); // Format expected output let expected_output = local_dt.format("%Y-%m-%dT%H:%M:%S").to_string(); assert_eq!(get_pretty_file_modified_time(timestamp), expected_output); // Test another timestamp let local_dt = Local.with_ymd_and_hms(2020, 1, 1, 12, 0, 0).unwrap(); let timestamp = local_dt.timestamp(); let expected_output = local_dt.format("%Y-%m-%dT%H:%M:%S").to_string(); assert_eq!(get_pretty_file_modified_time(timestamp), expected_output); // Test timestamp for epoch start (1970-01-01T00:00:00) let local_dt = Local.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap(); let timestamp = local_dt.timestamp(); let expected_output = local_dt.format("%Y-%m-%dT%H:%M:%S").to_string(); assert_eq!(get_pretty_file_modified_time(timestamp), expected_output); // Test a future timestamp let local_dt = Local.with_ymd_and_hms(2030, 12, 25, 6, 30, 0).unwrap(); let timestamp = local_dt.timestamp(); let expected_output = local_dt.format("%Y-%m-%dT%H:%M:%S").to_string(); assert_eq!(get_pretty_file_modified_time(timestamp), expected_output); } } du-dust-1.2.4/src/display_node.rs000064400000000000000000000035711046102023000150320ustar 00000000000000use std::cell::RefCell; use std::path::PathBuf; use serde::ser::SerializeStruct; use serde::{Serialize, Serializer}; use crate::display::human_readable_number; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] pub struct DisplayNode { // Note: the order of fields in important here, for PartialEq and PartialOrd pub size: u64, pub name: PathBuf, pub children: Vec, } impl DisplayNode { pub fn num_siblings(&self) -> u64 { self.children.len() as u64 } pub fn get_children_from_node(&self, is_reversed: bool) -> impl Iterator { // we box to avoid the clippy lint warning let out: Box> = if is_reversed { Box::new(self.children.iter().rev()) } else { Box::new(self.children.iter()) }; out } } // Only used for -j 'json' flag combined with -o 'output_type' flag // Used to pass the output_type into the custom Serde serializer thread_local! { pub static OUTPUT_TYPE: RefCell = const { RefCell::new(String::new()) }; } /* We need the custom Serialize incase someone uses the -o flag to pass a custom output type in (show size in Mb / Gb etc). Sadly this also necessitates a global variable OUTPUT_TYPE as we can not pass the output_type flag into the serialize method */ impl Serialize for DisplayNode { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let readable_size = OUTPUT_TYPE .with(|output_type| human_readable_number(self.size, output_type.borrow().as_str())); let mut state = serializer.serialize_struct("DisplayNode", 2)?; state.serialize_field("size", &(readable_size))?; state.serialize_field("name", &self.name)?; state.serialize_field("children", &self.children)?; state.end() } } du-dust-1.2.4/src/filter.rs000064400000000000000000000155101046102023000136410ustar 00000000000000use stfu8::encode_u8; use crate::display::get_printable_name; use crate::display_node::DisplayNode; use crate::node::FileTime; use crate::node::Node; use std::collections::BinaryHeap; use std::collections::HashMap; use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; pub struct AggregateData { pub min_size: Option, pub only_dir: bool, pub only_file: bool, pub number_of_lines: usize, pub depth: usize, pub using_a_filter: bool, pub short_paths: bool, } pub fn get_biggest( top_level_nodes: Vec, display_data: AggregateData, by_filetime: &Option, keep_collapsed: HashSet, ) -> DisplayNode { let mut heap = BinaryHeap::new(); let number_top_level_nodes = top_level_nodes.len(); let root; if number_top_level_nodes == 0 { root = total_node_builder(0, vec![]) } else if number_top_level_nodes > 1 { let size = if by_filetime.is_some() { top_level_nodes .iter() .map(|node| node.size) .max() .unwrap_or(0) } else { top_level_nodes.iter().map(|node| node.size).sum() }; let nodes = handle_duplicate_top_level_names(top_level_nodes, display_data.short_paths); root = total_node_builder(size, nodes); heap = always_add_children(&display_data, &root, heap); } else { root = top_level_nodes.into_iter().next().unwrap(); heap = add_children(&display_data, &root, heap); } fill_remaining_lines(heap, &root, display_data, keep_collapsed) } fn total_node_builder(size: u64, children: Vec) -> Node { Node { name: PathBuf::from("(total)"), size, children, inode_device: None, depth: 0, } } pub fn fill_remaining_lines<'a>( mut heap: BinaryHeap<&'a Node>, root: &'a Node, display_data: AggregateData, keep_collapsed: HashSet, ) -> DisplayNode { let mut allowed_nodes = HashMap::new(); while allowed_nodes.len() < display_data.number_of_lines { let line = heap.pop(); match line { Some(line) => { // If we are not doing only_file OR if we are doing // only_file and it has no children (ie is a file not a dir) if !display_data.only_file || line.children.is_empty() { allowed_nodes.insert(line.name.as_path(), line); } if !keep_collapsed.contains(&line.name) { heap = add_children(&display_data, line, heap); } } None => break, } } if display_data.only_file { flat_rebuilder(allowed_nodes, root) } else { recursive_rebuilder(&allowed_nodes, root) } } fn add_children<'a>( display_data: &AggregateData, file_or_folder: &'a Node, heap: BinaryHeap<&'a Node>, ) -> BinaryHeap<&'a Node> { if display_data.depth > file_or_folder.depth { always_add_children(display_data, file_or_folder, heap) } else { heap } } fn always_add_children<'a>( display_data: &AggregateData, file_or_folder: &'a Node, mut heap: BinaryHeap<&'a Node>, ) -> BinaryHeap<&'a Node> { heap.extend( file_or_folder .children .iter() .filter(|c| match display_data.min_size { Some(ms) => c.size > ms as u64, None => !display_data.using_a_filter || c.name.is_file() || c.size > 0, }) .filter(|c| { if display_data.only_dir { c.name.is_dir() } else { true } }), ); heap } // Finds children of current, if in allowed_nodes adds them as children to new DisplayNode fn recursive_rebuilder(allowed_nodes: &HashMap<&Path, &Node>, current: &Node) -> DisplayNode { let new_children: Vec<_> = current .children .iter() .filter(|c| allowed_nodes.contains_key(c.name.as_path())) .map(|c| recursive_rebuilder(allowed_nodes, c)) .collect(); build_display_node(new_children, current) } // Applies all allowed nodes as children to current node fn flat_rebuilder(allowed_nodes: HashMap<&Path, &Node>, current: &Node) -> DisplayNode { let new_children: Vec = allowed_nodes .into_values() .map(|v| DisplayNode { name: v.name.clone(), size: v.size, children: vec![], }) .collect::>(); build_display_node(new_children, current) } fn build_display_node(mut new_children: Vec, current: &Node) -> DisplayNode { new_children.sort_by(|lhs, rhs| lhs.cmp(rhs).reverse()); DisplayNode { name: current.name.clone(), size: current.size, children: new_children, } } fn names_have_dup(top_level_nodes: &Vec) -> bool { let mut stored = HashSet::new(); for node in top_level_nodes { let name = get_printable_name(&node.name, true); if stored.contains(&name) { return true; } stored.insert(name); } false } fn handle_duplicate_top_level_names(top_level_nodes: Vec, short_paths: bool) -> Vec { // If we have top level names that are the same - we need to tweak them: if short_paths && names_have_dup(&top_level_nodes) { let mut new_top_nodes = top_level_nodes.clone(); let mut dir_walk_up_count = 0; while names_have_dup(&new_top_nodes) && dir_walk_up_count < 10 { dir_walk_up_count += 1; let mut newer = vec![]; for node in new_top_nodes.iter() { let mut folders = node.name.iter().rev(); // Get parent folder (if second time round get grandparent and so on) for _ in 0..dir_walk_up_count { folders.next(); } match folders.next() { // Add (parent_name) to path of Node Some(data) => { let parent = encode_u8(data.as_encoded_bytes()); let current_node = node.name.display(); let n = Node { name: PathBuf::from(format!("{current_node}({parent})")), size: node.size, children: node.children.clone(), inode_device: node.inode_device, depth: node.depth, }; newer.push(n) } // Node does not have a parent None => newer.push(node.clone()), } } new_top_nodes = newer; } new_top_nodes } else { top_level_nodes } } du-dust-1.2.4/src/filter_type.rs000064400000000000000000000052071046102023000147040ustar 00000000000000use crate::display_node::DisplayNode; use crate::node::FileTime; use crate::node::Node; use std::collections::HashMap; use std::ffi::OsStr; use std::path::PathBuf; #[derive(PartialEq, Eq, PartialOrd, Ord)] struct ExtensionNode<'a> { size: u64, extension: Option<&'a OsStr>, } pub fn get_all_file_types( top_level_nodes: &[Node], n: usize, by_filetime: &Option, ) -> DisplayNode { let ext_nodes = { let mut extension_cumulative_sizes = HashMap::new(); build_by_all_file_types(top_level_nodes, &mut extension_cumulative_sizes); let mut extension_cumulative_sizes: Vec> = extension_cumulative_sizes .iter() .map(|(&extension, &size)| ExtensionNode { extension, size }) .collect(); extension_cumulative_sizes.sort_by(|lhs, rhs| lhs.cmp(rhs).reverse()); extension_cumulative_sizes }; let mut ext_nodes_iter = ext_nodes.iter(); // First, collect the first N - 1 nodes... let mut displayed: Vec = ext_nodes_iter .by_ref() .take(if n > 1 { n - 1 } else { 1 }) .map(|node| DisplayNode { name: PathBuf::from( node.extension .map(|ext| format!(".{}", ext.to_string_lossy())) .unwrap_or_else(|| "(no extension)".to_owned()), ), size: node.size, children: vec![], }) .collect(); // ...then, aggregate the remaining nodes (if any) into a single "(others)" node if ext_nodes_iter.len() > 0 { let actual_size = if by_filetime.is_some() { ext_nodes_iter.map(|node| node.size).max().unwrap_or(0) } else { ext_nodes_iter.map(|node| node.size).sum() }; displayed.push(DisplayNode { name: PathBuf::from("(others)"), size: actual_size, children: vec![], }); } let actual_size: u64 = if by_filetime.is_some() { displayed.iter().map(|node| node.size).max().unwrap_or(0) } else { displayed.iter().map(|node| node.size).sum() }; DisplayNode { name: PathBuf::from("(total)"), size: actual_size, children: displayed, } } fn build_by_all_file_types<'a>( top_level_nodes: &'a [Node], counter: &mut HashMap, u64>, ) { for node in top_level_nodes { if node.name.is_file() { let ext = node.name.extension(); let cumulative_size = counter.entry(ext).or_default(); *cumulative_size += node.size; } build_by_all_file_types(&node.children, counter) } } du-dust-1.2.4/src/main.rs000064400000000000000000000347051046102023000133070ustar 00000000000000mod cli; mod config; mod dir_walker; mod display; mod display_node; mod filter; mod filter_type; mod node; mod platform; mod progress; mod utils; use crate::cli::Cli; use crate::config::Config; use crate::display_node::DisplayNode; use crate::progress::RuntimeErrors; use clap::Parser; use dir_walker::WalkData; use display::InitialDisplayData; use filter::AggregateData; use progress::PIndicator; use regex::Error; use std::collections::HashSet; use std::env; use std::fs::{read, read_to_string}; use std::io; use std::io::Read; use std::panic; use std::process; use std::sync::Arc; use std::sync::Mutex; use sysinfo::System; use utils::canonicalize_absolute_path; use self::display::draw_it; use config::get_config; use dir_walker::walk_it; use display_node::OUTPUT_TYPE; use filter::get_biggest; use filter_type::get_all_file_types; use regex::Regex; use std::cmp::max; use std::path::PathBuf; use terminal_size::{Height, Width, terminal_size}; use utils::get_filesystem_devices; use utils::simplify_dir_names; static DEFAULT_NUMBER_OF_LINES: usize = 30; static DEFAULT_TERMINAL_WIDTH: usize = 80; fn should_init_color(no_color: bool, force_color: bool) -> bool { if force_color { return true; } if no_color { return false; } // check if NO_COLOR is set // https://no-color.org/ if env::var_os("NO_COLOR").is_some() { return false; } if terminal_size().is_none() { // we are not in a terminal, color may not be needed return false; } // we are in a terminal #[cfg(windows)] { // Required for windows 10 // Fails to resolve for windows 8 so disable color match nu_ansi_term::enable_ansi_support() { Ok(_) => true, Err(_) => { eprintln!("This version of Windows does not support ANSI colors"); false } } } #[cfg(not(windows))] { true } } fn get_height_of_terminal() -> usize { terminal_size() // Windows CI runners detect a terminal height of 0 .map(|(_, Height(h))| max(h.into(), DEFAULT_NUMBER_OF_LINES)) .unwrap_or(DEFAULT_NUMBER_OF_LINES) - 10 } fn get_width_of_terminal() -> usize { terminal_size() .map(|(Width(w), _)| match cfg!(windows) { // Windows CI runners detect a very low terminal width true => max(w.into(), DEFAULT_TERMINAL_WIDTH), false => w.into(), }) .unwrap_or(DEFAULT_TERMINAL_WIDTH) } fn get_regex_value(maybe_value: Option<&Vec>) -> Vec { maybe_value .unwrap_or(&Vec::new()) .iter() .map(|reg| { Regex::new(reg).unwrap_or_else(|err| { eprintln!("Ignoring bad value for regex {err:?}"); process::exit(1) }) }) .collect() } fn main() { let options = Cli::parse(); let config = get_config(options.config.as_ref()); let errors = RuntimeErrors::default(); let error_listen_for_ctrlc = Arc::new(Mutex::new(errors)); let errors_for_rayon = error_listen_for_ctrlc.clone(); ctrlc::set_handler(move || { println!("\nAborting"); process::exit(1); }) .expect("Error setting Ctrl-C handler"); let target_dirs = if let Some(path) = config.get_files0_from(&options) { read_paths_from_source(&path, true) } else if let Some(path) = config.get_files_from(&options) { read_paths_from_source(&path, false) } else { match options.params { Some(ref values) => values.clone(), None => vec![".".to_owned()], } }; let summarize_file_types = options.file_types; let filter_regexs = get_regex_value(options.filter.as_ref()); let invert_filter_regexs = get_regex_value(options.invert_filter.as_ref()); let terminal_width: usize = match options.terminal_width { Some(val) => val, None => get_width_of_terminal(), }; let depth = config.get_depth(&options); // If depth is set, then we set the default number_of_lines to be max // instead of screen height let number_of_lines = match config.get_number_of_lines(&options) { Some(val) => val, None => { if depth != usize::MAX { usize::MAX } else { get_height_of_terminal() } } }; let is_colors = should_init_color( config.get_no_colors(&options), config.get_force_colors(&options), ); let ignore_directories = match options.ignore_directory { Some(ref values) => values .iter() .map(PathBuf::from) .map(canonicalize_absolute_path) .collect::>(), None => vec![], }; let ignore_from_file_result = match options.ignore_all_in_file { Some(ref val) => read_to_string(val) .unwrap() .lines() .map(Regex::new) .collect::>>(), None => vec![], }; let ignore_from_file = ignore_from_file_result .into_iter() .filter_map(|x| x.ok()) .collect::>(); let invert_filter_regexs = invert_filter_regexs .into_iter() .chain(ignore_from_file) .collect::>(); let by_filecount = options.filecount; let by_filetime = config.get_filetime(&options); let limit_filesystem = options.limit_filesystem; let follow_links = options.dereference_links; let allowed_filesystems = if limit_filesystem { get_filesystem_devices(&target_dirs, follow_links) } else { Default::default() }; let simplified_dirs = simplify_dir_names(&target_dirs); let ignored_full_path: HashSet = ignore_directories .into_iter() .flat_map(|x| simplified_dirs.iter().map(move |d| d.join(&x))) .collect(); let output_format = config.get_output_format(&options); let ignore_hidden = config.get_ignore_hidden(&options); let mut indicator = PIndicator::build_me(); if !config.get_disable_progress(&options) { indicator.spawn(output_format.clone()) } let keep_collapsed: HashSet = match config.get_collapse(&options) { Some(ref collapse) => { let mut combined_dirs = HashSet::new(); for collapse_dir in collapse { for target_dir in target_dirs.iter() { combined_dirs.insert(PathBuf::from(target_dir).join(collapse_dir)); } } combined_dirs } None => HashSet::new(), }; let filter_modified_time = config.get_modified_time_operator(&options); let filter_accessed_time = config.get_accessed_time_operator(&options); let filter_changed_time = config.get_changed_time_operator(&options); let walk_data = WalkData { ignore_directories: ignored_full_path, filter_regex: &filter_regexs, invert_filter_regex: &invert_filter_regexs, allowed_filesystems, filter_modified_time, filter_accessed_time, filter_changed_time, use_apparent_size: config.get_apparent_size(&options), by_filecount, by_filetime: &by_filetime, ignore_hidden, follow_links, progress_data: indicator.data.clone(), errors: errors_for_rayon, }; let threads_to_use = config.get_threads(&options); let stack_size = config.get_custom_stack_size(&options); init_rayon(&stack_size, &threads_to_use).install(|| { let top_level_nodes = walk_it(simplified_dirs, &walk_data); let tree = match summarize_file_types { true => get_all_file_types(&top_level_nodes, number_of_lines, walk_data.by_filetime), false => { let agg_data = AggregateData { min_size: config.get_min_size(&options), only_dir: config.get_only_dir(&options), only_file: config.get_only_file(&options), number_of_lines, depth, using_a_filter: !filter_regexs.is_empty() || !invert_filter_regexs.is_empty(), short_paths: !config.get_full_paths(&options), }; get_biggest( top_level_nodes, agg_data, walk_data.by_filetime, keep_collapsed, ) } }; // Must have stopped indicator before we print to stderr indicator.stop(); let print_errors = config.get_print_errors(&options); let final_errors = walk_data.errors.lock().unwrap(); print_any_errors(print_errors, &final_errors); if tree.children.is_empty() && !final_errors.file_not_found.is_empty() { std::process::exit(1) } else { print_output( config, options, tree, walk_data.by_filecount, is_colors, terminal_width, ) } }); } fn print_output( config: Config, options: Cli, tree: DisplayNode, by_filecount: bool, is_colors: bool, terminal_width: usize, ) { let output_format = config.get_output_format(&options); if config.get_output_json(&options) { OUTPUT_TYPE.with(|wrapped| { if by_filecount { wrapped.replace("count".to_string()); } else { wrapped.replace(output_format); } }); println!("{}", serde_json::to_string(&tree).unwrap()); } else { let idd = InitialDisplayData { short_paths: !config.get_full_paths(&options), is_reversed: !config.get_reverse(&options), colors_on: is_colors, by_filecount, by_filetime: config.get_filetime(&options), is_screen_reader: config.get_screen_reader(&options), output_format, bars_on_right: config.get_bars_on_right(&options), }; draw_it( idd, &tree, config.get_no_bars(&options), terminal_width, config.get_skip_total(&options), ) } } fn print_any_errors(print_errors: bool, final_errors: &RuntimeErrors) { if !final_errors.file_not_found.is_empty() { let err = final_errors .file_not_found .iter() .map(|a| a.as_ref()) .collect::>() .join(", "); eprintln!("No such file or directory: {err}"); } if !final_errors.no_permissions.is_empty() { if print_errors { let err = final_errors .no_permissions .iter() .map(|a| a.as_ref()) .collect::>() .join(", "); eprintln!("Did not have permissions for directories: {err}"); } else { eprintln!( "Did not have permissions for all directories (add --print-errors to see errors)" ); } } if !final_errors.unknown_error.is_empty() { let err = final_errors .unknown_error .iter() .map(|a| a.as_ref()) .collect::>() .join(", "); eprintln!("Unknown Error: {err}"); } } fn read_paths_from_source(path: &str, null_terminated: bool) -> Vec { let from_stdin = path == "-"; let result: Result, Option> = (|| { // 1) read bytes let bytes = if from_stdin { let mut b = Vec::new(); io::stdin().lock().read_to_end(&mut b).map_err(|_| None)?; b } else { read(path).map_err(|e| Some(e.to_string()))? }; let text = std::str::from_utf8(&bytes).map_err(|e| { if from_stdin { None } else { Some(e.to_string()) } })?; let items: Vec = if null_terminated { text.split('\0') .filter(|s| !s.is_empty()) .map(str::to_owned) .collect() } else { text.lines().map(str::to_owned).collect() }; if from_stdin && items.is_empty() { return Err(None); } Ok(items) })(); match result { Ok(v) => v, Err(None) => { eprintln!("No files provided, defaulting to current directory"); vec![".".to_owned()] } Err(Some(msg)) => { eprintln!("Failed to read file: {msg}"); vec![".".to_owned()] } } } fn init_rayon(stack: &Option, threads: &Option) -> rayon::ThreadPool { let stack_size = match stack { Some(s) => Some(*s), None => { // Do not increase the stack size on a 32 bit system, it will fail if cfg!(target_pointer_width = "32") { None } else { let large_stack = usize::pow(1024, 3); let mut sys = System::new_all(); sys.refresh_memory(); // Larger stack size if possible to handle cases with lots of nested directories let available = sys.available_memory(); if available > (large_stack * threads.unwrap_or(1)).try_into().unwrap() { Some(large_stack) } else { None } } } }; match build_thread_pool(stack_size, threads) { Ok(pool) => pool, Err(err) => { eprintln!("Problem initializing rayon, try: export RAYON_NUM_THREADS=1"); if stack.is_none() && stack_size.is_some() { // stack parameter was none, try with default stack size if let Ok(pool) = build_thread_pool(None, threads) { eprintln!("WARNING: not using large stack size, got error: {err}"); return pool; } } panic!("{err}"); } } } fn build_thread_pool( stack_size: Option, threads: &Option, ) -> Result { let mut pool_builder = rayon::ThreadPoolBuilder::new(); if let Some(stack_size_param) = stack_size { pool_builder = pool_builder.stack_size(stack_size_param); } if let Some(thread_count) = threads { pool_builder = pool_builder.num_threads(*thread_count); } pool_builder.build() } du-dust-1.2.4/src/node.rs000064400000000000000000000060271046102023000133040ustar 00000000000000use crate::dir_walker::WalkData; use crate::platform::get_metadata; use crate::utils::is_filtered_out_due_to_file_time; use crate::utils::is_filtered_out_due_to_invert_regex; use crate::utils::is_filtered_out_due_to_regex; use std::cmp::Ordering; use std::path::PathBuf; #[derive(Debug, Eq, Clone)] pub struct Node { pub name: PathBuf, pub size: u64, pub children: Vec, pub inode_device: Option<(u64, u64)>, pub depth: usize, } #[derive(Debug, PartialEq)] pub enum FileTime { Modified, Accessed, Changed, } impl From for FileTime { fn from(time: crate::cli::FileTime) -> Self { match time { crate::cli::FileTime::Modified => Self::Modified, crate::cli::FileTime::Accessed => Self::Accessed, crate::cli::FileTime::Changed => Self::Changed, } } } #[allow(clippy::too_many_arguments)] pub fn build_node( dir: PathBuf, children: Vec, is_symlink: bool, is_file: bool, depth: usize, walk_data: &WalkData, ) -> Option { let use_apparent_size = walk_data.use_apparent_size; let by_filecount = walk_data.by_filecount; let by_filetime = &walk_data.by_filetime; get_metadata( &dir, use_apparent_size, walk_data.follow_links && is_symlink, ) .map(|data| { let inode_device = data.1; let size = if is_filtered_out_due_to_regex(walk_data.filter_regex, &dir) || is_filtered_out_due_to_invert_regex(walk_data.invert_filter_regex, &dir) || by_filecount && !is_file || [ (&walk_data.filter_modified_time, data.2.0), (&walk_data.filter_accessed_time, data.2.1), (&walk_data.filter_changed_time, data.2.2), ] .iter() .any(|(filter_time, actual_time)| { is_filtered_out_due_to_file_time(filter_time, *actual_time) }) { 0 } else if by_filecount { 1 } else if by_filetime.is_some() { match by_filetime { Some(FileTime::Modified) => data.2.0.unsigned_abs(), Some(FileTime::Accessed) => data.2.1.unsigned_abs(), Some(FileTime::Changed) => data.2.2.unsigned_abs(), None => unreachable!(), } } else { data.0 }; Node { name: dir, size, children, inode_device, depth, } }) } impl PartialEq for Node { fn eq(&self, other: &Self) -> bool { self.name == other.name && self.size == other.size && self.children == other.children } } impl Ord for Node { fn cmp(&self, other: &Self) -> Ordering { self.size .cmp(&other.size) .then_with(|| self.name.cmp(&other.name)) .then_with(|| self.children.cmp(&other.children)) } } impl PartialOrd for Node { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } du-dust-1.2.4/src/platform.rs000064400000000000000000000210101046102023000141700ustar 00000000000000#[allow(unused_imports)] use std::fs; use std::path::Path; #[cfg(target_family = "unix")] fn get_block_size() -> u64 { // All os specific implementations of MetadataExt seem to define a block as 512 bytes // https://doc.rust-lang.org/std/os/linux/fs/trait.MetadataExt.html#tymethod.st_blocks 512 } type InodeAndDevice = (u64, u64); type FileTime = (i64, i64, i64); #[cfg(target_family = "unix")] pub fn get_metadata>( path: P, use_apparent_size: bool, follow_links: bool, ) -> Option<(u64, Option, FileTime)> { use std::os::unix::fs::MetadataExt; let metadata = if follow_links { path.as_ref().metadata() } else { path.as_ref().symlink_metadata() }; match metadata { Ok(md) => { let file_size = md.len(); if use_apparent_size { Some(( file_size, Some((md.ino(), md.dev())), (md.mtime(), md.atime(), md.ctime()), )) } else { // On NTFS mounts, the reported block count can be unexpectedly large. // To avoid overestimating disk usage, cap the allocated size to what the // file should occupy based on the file system I/O block size (blksize). // Related: https://github.com/bootandy/dust/issues/295 let blksize = md.blksize(); let target_size = file_size.div_ceil(blksize) * blksize; let reported_size = md.blocks() * get_block_size(); // File systems can pre-allocate more space for a file than what would be necessary let pre_allocation_buffer = blksize * 65536; let max_size = target_size + pre_allocation_buffer; let allocated_size = if reported_size > max_size { target_size } else { reported_size }; Some(( allocated_size, Some((md.ino(), md.dev())), (md.mtime(), md.atime(), md.ctime()), )) } } Err(_e) => None, } } #[cfg(target_family = "windows")] pub fn get_metadata>( path: P, use_apparent_size: bool, follow_links: bool, ) -> Option<(u64, Option, FileTime)> { // On windows opening the file to get size, file ID and volume can be very // expensive because 1) it causes a few system calls, and more importantly 2) it can cause // windows defender to scan the file. // Therefore we try to avoid doing that for common cases, mainly those of // plain files: // The idea is to make do with the file size that we get from the OS for // free as part of iterating a folder. Therefore we want to make sure that // it makes sense to use that free size information: // Volume boundaries: // The user can ask us not to cross volume boundaries. If the DirEntry is a // plain file and not a reparse point or other non-trivial stuff, we assume // that the file is located on the same volume as the directory that // contains it. // File ID: // This optimization does deprive us of access to a file ID. As a // workaround, we just make one up that hopefully does not collide with real // file IDs. // Hard links: Unresolved. We don't get inode/file index, so hard links // count once for each link. Hopefully they are not too commonly in use on // windows. // Size: // We assume (naively?) that for the common cases the free size info is the // same as one would get by doing the expensive thing. Sparse, encrypted and // compressed files are not included in the common cases, as one can image // there being more than view on their size. // Savings in orders of magnitude in terms of time, io and cpu have been // observed on hdd, windows 10, some 100Ks files taking up some hundreds of // GBs: // Consistently opening the file: 30 minutes. // With this optimization: 8 sec. use std::io; use winapi_util::Handle; fn handle_from_path_limited(path: &Path) -> io::Result { use std::fs::OpenOptions; use std::os::windows::fs::OpenOptionsExt; const FILE_READ_ATTRIBUTES: u32 = 0x0080; // So, it seems that it does does have to be that expensive to open // files to get their info: Avoiding opening the file with the full // GENERIC_READ is key: // https://docs.microsoft.com/en-us/windows/win32/secauthz/generic-access-rights: // "For example, a Windows file object maps the GENERIC_READ bit to the // READ_CONTROL and SYNCHRONIZE standard access rights and to the // FILE_READ_DATA, FILE_READ_EA, and FILE_READ_ATTRIBUTES // object-specific access rights" // The flag FILE_READ_DATA seems to be the expensive one, so we'll avoid // that, and a most of the other ones. Simply because it seems that we // don't need them. let file = OpenOptions::new() .access_mode(FILE_READ_ATTRIBUTES) .open(path)?; Ok(Handle::from_file(file)) } fn get_metadata_expensive( path: &Path, use_apparent_size: bool, ) -> Option<(u64, Option, FileTime)> { use winapi_util::file::information; let h = handle_from_path_limited(path).ok()?; let info = information(&h).ok()?; if use_apparent_size { use filesize::PathExt; Some(( path.size_on_disk().ok()?, Some((info.file_index(), info.volume_serial_number())), ( info.last_write_time().unwrap() as i64, info.last_access_time().unwrap() as i64, info.creation_time().unwrap() as i64, ), )) } else { Some(( info.file_size(), Some((info.file_index(), info.volume_serial_number())), ( info.last_write_time().unwrap() as i64, info.last_access_time().unwrap() as i64, info.creation_time().unwrap() as i64, ), )) } } use std::os::windows::fs::MetadataExt; let path = path.as_ref(); let metadata = if follow_links { path.metadata() } else { path.symlink_metadata() }; match metadata { Ok(ref md) => { const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x20; const FILE_ATTRIBUTE_READONLY: u32 = 0x01; const FILE_ATTRIBUTE_HIDDEN: u32 = 0x02; const FILE_ATTRIBUTE_SYSTEM: u32 = 0x04; const FILE_ATTRIBUTE_NORMAL: u32 = 0x80; const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10; const FILE_ATTRIBUTE_SPARSE_FILE: u32 = 0x00000200; const FILE_ATTRIBUTE_PINNED: u32 = 0x00080000; const FILE_ATTRIBUTE_UNPINNED: u32 = 0x00100000; const FILE_ATTRIBUTE_RECALL_ON_OPEN: u32 = 0x00040000; const FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS: u32 = 0x00400000; const FILE_ATTRIBUTE_OFFLINE: u32 = 0x00001000; // normally FILE_ATTRIBUTE_SPARSE_FILE would be enough, however Windows sometimes likes to mask it out. see: https://stackoverflow.com/q/54560454 const IS_PROBABLY_ONEDRIVE: u32 = FILE_ATTRIBUTE_SPARSE_FILE | FILE_ATTRIBUTE_PINNED | FILE_ATTRIBUTE_UNPINNED | FILE_ATTRIBUTE_RECALL_ON_OPEN | FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | FILE_ATTRIBUTE_OFFLINE; let attr_filtered = md.file_attributes() & !(FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_SYSTEM); if ((attr_filtered & FILE_ATTRIBUTE_ARCHIVE) != 0 || (attr_filtered & FILE_ATTRIBUTE_DIRECTORY) != 0 || md.file_attributes() == FILE_ATTRIBUTE_NORMAL) && !((attr_filtered & IS_PROBABLY_ONEDRIVE != 0) && use_apparent_size) { Some(( md.len(), None, ( md.last_write_time() as i64, md.last_access_time() as i64, md.creation_time() as i64, ), )) } else { get_metadata_expensive(path, use_apparent_size) } } _ => get_metadata_expensive(path, use_apparent_size), } } du-dust-1.2.4/src/progress.rs000064400000000000000000000117741046102023000142300ustar 00000000000000use std::{ collections::HashSet, io::Write, path::Path, sync::{ Arc, RwLock, atomic::{AtomicU8, AtomicUsize, Ordering}, mpsc::{self, RecvTimeoutError, Sender}, }, thread::JoinHandle, time::Duration, }; #[cfg(not(target_has_atomic = "64"))] use portable_atomic::AtomicU64; #[cfg(target_has_atomic = "64")] use std::sync::atomic::AtomicU64; use crate::display::human_readable_number; /* -------------------------------------------------------------------------- */ pub const ORDERING: Ordering = Ordering::Relaxed; const SPINNER_SLEEP_TIME: u64 = 100; const PROGRESS_CHARS: [char; 4] = ['-', '\\', '|', '/']; const PROGRESS_CHARS_LEN: usize = PROGRESS_CHARS.len(); pub trait ThreadSyncTrait { fn set(&self, val: T); fn get(&self) -> T; } #[derive(Default)] pub struct ThreadStringWrapper { inner: RwLock, } impl ThreadSyncTrait for ThreadStringWrapper { fn set(&self, val: String) { *self.inner.write().unwrap() = val; } fn get(&self) -> String { (*self.inner.read().unwrap()).clone() } } /* -------------------------------------------------------------------------- */ // creating an enum this way allows to have simpler syntax compared to a Mutex or a RwLock #[allow(non_snake_case)] pub mod Operation { pub const INDEXING: u8 = 0; pub const PREPARING: u8 = 1; } #[derive(Default)] pub struct PAtomicInfo { pub num_files: AtomicUsize, pub total_file_size: AtomicU64, pub state: AtomicU8, pub current_path: ThreadStringWrapper, } impl PAtomicInfo { pub fn clear_state(&self, dir: &Path) { self.state.store(Operation::INDEXING, ORDERING); let dir_name = dir.to_string_lossy().to_string(); self.current_path.set(dir_name); self.total_file_size.store(0, ORDERING); self.num_files.store(0, ORDERING); } } #[derive(Default)] pub struct RuntimeErrors { pub no_permissions: HashSet, pub file_not_found: HashSet, pub unknown_error: HashSet, pub interrupted_error: i32, } /* -------------------------------------------------------------------------- */ fn format_preparing_str(prog_char: char, data: &PAtomicInfo, output_display: &str) -> String { let path_in = data.current_path.get(); let size = human_readable_number(data.total_file_size.load(ORDERING), output_display); format!("Preparing: {path_in} {size} ... {prog_char}") } fn format_indexing_str(prog_char: char, data: &PAtomicInfo, output_display: &str) -> String { let path_in = data.current_path.get(); let file_count = data.num_files.load(ORDERING); let size = human_readable_number(data.total_file_size.load(ORDERING), output_display); let file_str = format!("{file_count} files, {size}"); format!("Indexing: {path_in} {file_str} ... {prog_char}") } pub struct PIndicator { pub thread: Option<(Sender<()>, JoinHandle<()>)>, pub data: Arc, } impl PIndicator { pub fn build_me() -> Self { Self { thread: None, data: Arc::new(PAtomicInfo { ..Default::default() }), } } pub fn spawn(&mut self, output_display: String) { let data = self.data.clone(); let (stop_handler, receiver) = mpsc::channel::<()>(); let time_info_thread = std::thread::spawn(move || { let mut progress_char_i: usize = 0; let mut stderr = std::io::stderr(); let mut msg = "".to_string(); // While the timeout triggers we go round the loop // If we disconnect or the sender sends its message we exit the while loop while let Err(RecvTimeoutError::Timeout) = receiver.recv_timeout(Duration::from_millis(SPINNER_SLEEP_TIME)) { // Clear the text written by 'write!'& Return at the start of line let clear = format!("\r{:width$}", " ", width = msg.len()); write!(stderr, "{clear}").unwrap(); let prog_char = PROGRESS_CHARS[progress_char_i]; msg = match data.state.load(ORDERING) { Operation::INDEXING => format_indexing_str(prog_char, &data, &output_display), Operation::PREPARING => format_preparing_str(prog_char, &data, &output_display), _ => panic!("Unknown State"), }; write!(stderr, "\r{msg}").unwrap(); stderr.flush().unwrap(); progress_char_i += 1; progress_char_i %= PROGRESS_CHARS_LEN; } let clear = format!("\r{:width$}", " ", width = msg.len()); write!(stderr, "{clear}").unwrap(); write!(stderr, "\r").unwrap(); stderr.flush().unwrap(); }); self.thread = Some((stop_handler, time_info_thread)) } pub fn stop(self) { if let Some((stop_handler, thread)) = self.thread { stop_handler.send(()).unwrap(); thread.join().unwrap(); } } } du-dust-1.2.4/src/utils.rs000064400000000000000000000145271046102023000135230ustar 00000000000000use platform::get_metadata; use std::collections::HashSet; use std::path::{Path, PathBuf}; use crate::config::DAY_SECONDS; use crate::dir_walker::Operator; use crate::platform; use regex::Regex; pub fn simplify_dir_names>(dirs: &[P]) -> HashSet { let mut top_level_names: HashSet = HashSet::with_capacity(dirs.len()); for t in dirs { let top_level_name = normalize_path(t); let mut can_add = true; let mut to_remove: Vec = Vec::new(); for tt in top_level_names.iter() { if is_a_parent_of(&top_level_name, tt) { to_remove.push(tt.to_path_buf()); } else if is_a_parent_of(tt, &top_level_name) { can_add = false; } } for r in to_remove { top_level_names.remove(&r); } if can_add { top_level_names.insert(top_level_name); } } top_level_names } pub fn get_filesystem_devices>(paths: &[P], follow_links: bool) -> HashSet { use std::fs; // Gets the device ids for the filesystems which are used by the argument paths paths .iter() .filter_map(|p| { let follow_links = if follow_links { // slow path: If dereference-links is set, then we check if the file is a symbolic link match fs::symlink_metadata(p) { Ok(metadata) => metadata.file_type().is_symlink(), Err(_) => false, } } else { false }; match get_metadata(p, false, follow_links) { Some((_size, Some((_id, dev)), _time)) => Some(dev), _ => None, } }) .collect() } pub fn normalize_path>(path: P) -> PathBuf { // normalize path ... // 1. removing repeated separators // 2. removing interior '.' ("current directory") path segments // 3. removing trailing extra separators and '.' ("current directory") path segments // * `Path.components()` does all the above work; ref: // 4. changing to os preferred separator (automatically done by recollecting components back into a PathBuf) path.as_ref().components().collect() } // Canonicalize the path only if it is an absolute path pub fn canonicalize_absolute_path(path: PathBuf) -> PathBuf { if !path.is_absolute() { return path; } match std::fs::canonicalize(&path) { Ok(canonicalized_path) => canonicalized_path, Err(_) => path, } } pub fn is_filtered_out_due_to_regex(filter_regex: &[Regex], dir: &Path) -> bool { if filter_regex.is_empty() { false } else { filter_regex .iter() .all(|f| !f.is_match(&dir.as_os_str().to_string_lossy())) } } pub fn is_filtered_out_due_to_file_time( filter_time: &Option<(Operator, i64)>, actual_time: i64, ) -> bool { match filter_time { None => false, Some((Operator::Equal, bound_time)) => { !(actual_time >= *bound_time && actual_time < *bound_time + DAY_SECONDS) } Some((Operator::GreaterThan, bound_time)) => actual_time < *bound_time, Some((Operator::LessThan, bound_time)) => actual_time > *bound_time, } } pub fn is_filtered_out_due_to_invert_regex(filter_regex: &[Regex], dir: &Path) -> bool { filter_regex .iter() .any(|f| f.is_match(&dir.as_os_str().to_string_lossy())) } fn is_a_parent_of>(parent: P, child: P) -> bool { let parent = parent.as_ref(); let child = child.as_ref(); child.starts_with(parent) && !parent.starts_with(child) } mod tests { #[allow(unused_imports)] use super::*; #[test] fn test_simplify_dir() { let mut correct = HashSet::new(); correct.insert(PathBuf::from("a")); assert_eq!(simplify_dir_names(&["a"]), correct); } #[test] fn test_simplify_dir_rm_subdir() { let mut correct = HashSet::new(); correct.insert(["a", "b"].iter().collect::()); assert_eq!(simplify_dir_names(&["a/b/c", "a/b", "a/b/d/f"]), correct); assert_eq!(simplify_dir_names(&["a/b", "a/b/c", "a/b/d/f"]), correct); } #[test] fn test_simplify_dir_duplicates() { let mut correct = HashSet::new(); correct.insert(["a", "b"].iter().collect::()); correct.insert(PathBuf::from("c")); assert_eq!( simplify_dir_names(&[ "a/b", "a/b//", "a/././b///", "c", "c/", "c/.", "c/././", "c/././." ]), correct ); } #[test] fn test_simplify_dir_rm_subdir_and_not_substrings() { let mut correct = HashSet::new(); correct.insert(PathBuf::from("b")); correct.insert(["c", "a", "b"].iter().collect::()); correct.insert(["a", "b"].iter().collect::()); assert_eq!(simplify_dir_names(&["a/b", "c/a/b/", "b"]), correct); } #[test] fn test_simplify_dir_dots() { let mut correct = HashSet::new(); correct.insert(PathBuf::from("src")); assert_eq!(simplify_dir_names(&["src/."]), correct); } #[test] fn test_simplify_dir_substring_names() { let mut correct = HashSet::new(); correct.insert(PathBuf::from("src")); correct.insert(PathBuf::from("src_v2")); assert_eq!(simplify_dir_names(&["src/", "src_v2"]), correct); } #[test] fn test_is_a_parent_of() { assert!(is_a_parent_of("/usr", "/usr/andy")); assert!(is_a_parent_of("/usr", "/usr/andy/i/am/descendant")); assert!(!is_a_parent_of("/usr", "/usr/.")); assert!(!is_a_parent_of("/usr", "/usr/")); assert!(!is_a_parent_of("/usr", "/usr")); assert!(!is_a_parent_of("/usr/", "/usr")); assert!(!is_a_parent_of("/usr/andy", "/usr")); assert!(!is_a_parent_of("/usr/andy", "/usr/sibling")); assert!(!is_a_parent_of("/usr/folder", "/usr/folder_not_a_child")); } #[test] fn test_is_a_parent_of_root() { assert!(is_a_parent_of("/", "/usr/andy")); assert!(is_a_parent_of("/", "/usr")); assert!(!is_a_parent_of("/", "/")); } } du-dust-1.2.4/tests/test_dir/many/a_file000064400000000000000000000000001046102023000162700ustar 00000000000000du-dust-1.2.4/tests/test_dir/many/hello_file000064400000000000000000000000061046102023000171610ustar 00000000000000hello du-dust-1.2.4/tests/test_dir2/dir/hello000064400000000000000000000000051046102023000160550ustar 00000000000000hellodu-dust-1.2.4/tests/test_dir2/dir_name_clash000064400000000000000000000000051046102023000171240ustar 00000000000000hellodu-dust-1.2.4/tests/test_dir2/dir_substring/hello000064400000000000000000000000061046102023000201560ustar 00000000000000hello ././@LongLink00006440000000000000000000000176000000000000007777Lustar du-dust-1.2.4/tests/test_dir2/long_dir_name_what_a_very_long_dir_name_what_happens_when_this_goes_over_80_characters_i_wonderdu-dust-1.2.4/tests/test_dir2/long_dir_name_what_a_very_long_dir_name_what_happens_when_this_goes_ov000064400000000000000000000000001046102023000325630ustar 00000000000000du-dust-1.2.4/tests/test_dir_files_from/a_file000064400000000000000000000000001046102023000175310ustar 00000000000000du-dust-1.2.4/tests/test_dir_files_from/files0_from.txt000064400000000000000000000001061046102023000213440ustar 00000000000000tests/test_dir_files_from/a_filetests/test_dir_files_from/hello_filedu-dust-1.2.4/tests/test_dir_files_from/files_from.txt000064400000000000000000000001061046102023000212640ustar 00000000000000tests/test_dir_files_from/a_file tests/test_dir_files_from/hello_file du-dust-1.2.4/tests/test_dir_files_from/hello_file000064400000000000000000000000061046102023000204220ustar 00000000000000hello du-dust-1.2.4/tests/test_dir_hidden_entries/.hidden_file000064400000000000000000000000221046102023000214650ustar 00000000000000something .secret du-dust-1.2.4/tests/test_dir_hidden_entries/.secret000064400000000000000000000000001046102023000205140ustar 00000000000000du-dust-1.2.4/tests/test_dir_matching/andy/dup_name/hello000064400000000000000000000000001046102023000216050ustar 00000000000000du-dust-1.2.4/tests/test_dir_matching/dave/dup_name/hello000064400000000000000000000000001046102023000215710ustar 00000000000000du-dust-1.2.4/tests/test_dir_unicode/ラウトは難しいです!.japan000064400000000000000000000000001046102023000311130ustar 00000000000000du-dust-1.2.4/tests/test_dir_unicode/👩.unicode000064400000000000000000000000001046102023000206300ustar 00000000000000du-dust-1.2.4/tests/test_exact_output.rs000064400000000000000000000242421046102023000165140ustar 00000000000000use assert_cmd::{Command, cargo_bin_cmd}; use std::ffi::OsStr; use std::process::Output; use std::sync::Once; use std::{io, str}; static INIT: Once = Once::new(); static UNREADABLE_DIR_PATH: &str = "/tmp/unreadable_dir"; /** * This file contains tests that verify the exact output of the command. * This output differs on Linux / Mac so the tests are harder to write and debug * Windows is ignored here because the results vary by host making exact testing impractical * * Despite the above problems, these tests are good as they are the closest to 'the real thing'. */ // Warning: File sizes differ on both platform and on the format of the disk. /// Copy to /tmp dir - we assume that the formatting of the /tmp partition /// is consistent. If the tests fail your /tmp filesystem probably differs fn copy_test_data(dir: &str) { // First remove the existing directory - just in case it is there and has incorrect data let last_slash = dir.rfind('/').unwrap(); let last_part_of_dir = dir.chars().skip(last_slash).collect::(); let _ = Command::new("rm") .arg("-rf") .arg("/tmp/".to_owned() + &*last_part_of_dir) .ok(); let _ = Command::new("cp") .arg("-r") .arg(dir) .arg("/tmp/") .ok() .map_err(|err| eprintln!("Error copying directory for test setup\n{:?}", err)); } fn create_unreadable_directory() -> io::Result<()> { #[cfg(unix)] { use std::fs; use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; fs::create_dir_all(UNREADABLE_DIR_PATH)?; fs::set_permissions(UNREADABLE_DIR_PATH, Permissions::from_mode(0))?; } Ok(()) } fn initialize() { INIT.call_once(|| { copy_test_data("tests/test_dir"); copy_test_data("tests/test_dir2"); copy_test_data("tests/test_dir_unicode"); if let Err(e) = create_unreadable_directory() { panic!("Failed to create unreadable directory: {}", e); } }); } fn run_cmd>(command_args: &[T]) -> Output { initialize(); let mut to_run = cargo_bin_cmd!("dust"); // Hide progress bar to_run.arg("-P"); for p in command_args { to_run.arg(p); } to_run.unwrap() } fn exact_stdout_test>(command_args: &[T], valid_stdout: Vec) { let to_run = run_cmd(command_args); let stdout_output = str::from_utf8(&to_run.stdout).unwrap().to_owned(); let will_fail = valid_stdout.iter().any(|i| stdout_output.contains(i)); if !will_fail { eprintln!( "output(stdout):\n{}\ndoes not contain any of:\n{}", stdout_output, valid_stdout.join("\n\n") ); } assert!(will_fail); } fn exact_stderr_test>(command_args: &[T], valid_stderr: String) { let to_run = run_cmd(command_args); let stderr_output = str::from_utf8(&to_run.stderr).unwrap().trim(); assert_eq!(stderr_output, valid_stderr); } // "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 exact_stdout_test(&["-c", "-B", "/tmp/test_dir/"], main_output()); } #[cfg_attr(target_os = "windows", ignore)] #[test] pub fn test_main_multi_arg() { let command_args = [ "-c", "-B", "/tmp/test_dir/many/", "/tmp/test_dir", "/tmp/test_dir", ]; exact_stdout_test(&command_args, main_output()); } fn main_output() -> Vec { // Some linux currently thought to be Manjaro, Arch // Although probably depends on how drive is formatted let mac_and_some_linux = r#" 0B ┌── a_file │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█ │ 0% 4.0K ├── hello_file│█████████████████████████████████████████████████ │ 100% 4.0K ┌─┴ many │█████████████████████████████████████████████████ │ 100% 4.0K ┌─┴ test_dir │█████████████████████████████████████████████████ │ 100% "# .trim() .to_string(); let ubuntu = r#" 0B ┌── a_file │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█ │ 0% 4.0K ├── hello_file│ ░░░░░░░░░░░░░░░░█████████████████ │ 33% 8.0K ┌─┴ many │ █████████████████████████████████ │ 67% 12K ┌─┴ test_dir │█████████████████████████████████████████████████ │ 100% "# .trim() .to_string(); vec![mac_and_some_linux, ubuntu] } #[cfg_attr(target_os = "windows", ignore)] #[test] pub fn test_main_long_paths() { let command_args = ["-c", "-p", "-B", "/tmp/test_dir/"]; exact_stdout_test(&command_args, main_output_long_paths()); } fn main_output_long_paths() -> Vec { let mac_and_some_linux = r#" 0B ┌── /tmp/test_dir/many/a_file │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█ │ 0% 4.0K ├── /tmp/test_dir/many/hello_file│██████████████████████████████ │ 100% 4.0K ┌─┴ /tmp/test_dir/many │██████████████████████████████ │ 100% 4.0K ┌─┴ /tmp/test_dir │██████████████████████████████ │ 100% "# .trim() .to_string(); let ubuntu = r#" 0B ┌── /tmp/test_dir/many/a_file │ ░░░░░░░░░░░░░░░░░░░░█ │ 0% 4.0K ├── /tmp/test_dir/many/hello_file│ ░░░░░░░░░░███████████ │ 33% 8.0K ┌─┴ /tmp/test_dir/many │ █████████████████████ │ 67% 12K ┌─┴ /tmp/test_dir │██████████████████████████████ │ 100% "# .trim() .to_string(); vec![mac_and_some_linux, ubuntu] } // Check against directories and files whose names are substrings of each other #[cfg_attr(target_os = "windows", ignore)] #[test] pub fn test_substring_of_names_and_long_names() { let command_args = ["-c", "-B", "/tmp/test_dir2"]; exact_stdout_test(&command_args, no_substring_of_names_output()); } fn no_substring_of_names_output() -> Vec { let ubuntu = " 0B ┌── long_dir_name_what_a_very_long_dir_name_what_happens_when_this_goes.. 4.0K ├── dir_name_clash 4.0K │ ┌── hello 8.0K ├─┴ dir 4.0K │ ┌── hello 8.0K ├─┴ dir_substring 24K ┌─┴ test_dir2 " .trim() .into(); let mac_and_some_linux = " 0B ┌── long_dir_name_what_a_very_long_dir_name_what_happens_when_this_goes.. 4.0K │ ┌── hello 4.0K ├─┴ dir 4.0K ├── dir_name_clash 4.0K │ ┌── hello 4.0K ├─┴ dir_substring 12K ┌─┴ test_dir2 " .trim() .into(); vec![mac_and_some_linux, ubuntu] } #[cfg_attr(target_os = "windows", ignore)] #[test] pub fn test_unicode_directories() { let command_args = ["-c", "-B", "/tmp/test_dir_unicode"]; exact_stdout_test(&command_args, unicode_dir()); } fn unicode_dir() -> Vec { // The way unicode & asian characters are rendered on the terminal should make this line up let ubuntu = " 0B ┌── ラウトは難しいです!.japan│ █ │ 0% 0B ├── 👩.unicode │ █ │ 0% 4.0K ┌─┴ test_dir_unicode │███████████████████████████████████ │ 100% " .trim() .into(); let mac_and_some_linux = " 0B ┌── ラウトは難しいです!.japan│ █ │ 0% 0B ├── 👩.unicode │ █ │ 0% 0B ┌─┴ test_dir_unicode │ █ │ 0% " .trim() .into(); vec![mac_and_some_linux, ubuntu] } #[cfg_attr(target_os = "windows", ignore)] #[test] pub fn test_apparent_size() { let command_args = ["-c", "-s", "-b", "/tmp/test_dir"]; exact_stdout_test(&command_args, apparent_size_output()); } fn apparent_size_output() -> Vec { // The apparent directory sizes are too unpredictable and system dependent to try and match let one_space_before = r#" 0B ┌── a_file 6B ├── hello_file "# .trim() .to_string(); let two_space_before = r#" 0B ┌── a_file 6B ├── hello_file "# .trim() .to_string(); vec![one_space_before, two_space_before] } #[cfg_attr(target_os = "windows", ignore)] #[test] pub fn test_permission_normal() { let command_args = [UNREADABLE_DIR_PATH]; let permission_msg = r#"Did not have permissions for all directories (add --print-errors to see errors)"# .trim() .to_string(); exact_stderr_test(&command_args, permission_msg); } #[cfg_attr(target_os = "windows", ignore)] #[test] pub fn test_permission_flag() { // add the flag to CLI let command_args = ["--print-errors", UNREADABLE_DIR_PATH]; let permission_msg = format!( "Did not have permissions for directories: {}", UNREADABLE_DIR_PATH ); exact_stderr_test(&command_args, permission_msg); } du-dust-1.2.4/tests/test_flags.rs000064400000000000000000000252341046102023000150660ustar 00000000000000use assert_cmd::cargo_bin_cmd; use std::ffi::OsStr; use std::str; /** * This file contains tests that test a substring of the output using '.contains' * * These tests should be the same cross platform */ fn build_command>(command_args: Vec) -> String { let mut cmd = cargo_bin_cmd!("dust"); // Hide progress bar cmd.arg("-P"); for p in command_args { cmd.arg(p); } let finished = &cmd.unwrap(); assert_eq!(str::from_utf8(&finished.stderr).unwrap(), ""); str::from_utf8(&finished.stdout).unwrap().into() } // We can at least test the file names are there #[test] pub fn test_basic_output() { let output = build_command(vec!["tests/test_dir/"]); assert!(output.contains(" ┌─┴ ")); assert!(output.contains("test_dir ")); assert!(output.contains(" ┌─┴ ")); assert!(output.contains("many ")); assert!(output.contains(" ├── ")); assert!(output.contains("hello_file")); assert!(output.contains(" ┌── ")); assert!(output.contains("a_file ")); } #[test] pub fn test_output_no_bars_means_no_excess_spaces() { let output = build_command(vec!["-b", "tests/test_dir/"]); // If bars are not being shown we don't need to pad the output with spaces assert!(output.contains("many")); assert!(!output.contains("many ")); } #[test] pub fn test_reverse_flag() { let output = build_command(vec!["-r", "-c", "tests/test_dir/"]); assert!(output.contains(" └─┬ test_dir ")); assert!(output.contains(" └─┬ many ")); assert!(output.contains(" ├── hello_file")); assert!(output.contains(" └── a_file ")); } #[test] pub fn test_d_flag_works() { // We should see the top level directory but not the sub dirs / files: let output = build_command(vec!["-d", "1", "tests/test_dir/"]); assert!(!output.contains("hello_file")); } #[test] pub fn test_d0_works_on_multiple() { // We should see the top level directory but not the sub dirs / files: let output = build_command(vec!["-d", "0", "tests/test_dir/", "tests/test_dir2"]); assert!(output.contains("test_dir ")); assert!(output.contains("test_dir2")); } #[test] pub fn test_threads_flag_works() { let output = build_command(vec!["-T", "1", "tests/test_dir/"]); assert!(output.contains("hello_file")); } #[test] pub fn test_d_flag_works_and_still_recurses_down() { // We had a bug where running with '-d 1' would stop at the first directory and the code // would fail to recurse down let output = build_command(vec!["-d", "1", "-f", "-c", "tests/test_dir2/"]); assert!(output.contains("1 ┌── dir")); assert!(output.contains("4 ┌─┴ test_dir2")); } // Check against directories and files whose names are substrings of each other #[test] pub fn test_ignore_dir() { let output = build_command(vec!["-c", "-X", "dir_substring", "tests/test_dir2/"]); assert!(!output.contains("dir_substring")); } #[test] pub fn test_ignore_all_in_file() { let output = build_command(vec![ "-c", "-I", "tests/test_dir_hidden_entries/.hidden_file", "tests/test_dir_hidden_entries/", ]); assert!(output.contains(" test_dir_hidden_entries")); assert!(!output.contains(".secret")); } #[test] pub fn test_files_from_flag_file() { let output = build_command(vec![ "--files-from", "tests/test_dir_files_from/files_from.txt", ]); assert!(output.contains("a_file")); assert!(output.contains("hello_file")); } #[test] pub fn test_files0_from_flag_file() { let output = build_command(vec![ "--files0-from", "tests/test_dir_files_from/files0_from.txt", ]); assert!(output.contains("a_file")); assert!(output.contains("hello_file")); } #[test] pub fn test_files_from_flag_stdin() { let mut cmd = cargo_bin_cmd!("dust"); cmd.arg("-P").arg("--files-from").arg("-"); let input = b"tests/test_dir_files_from/a_file\ntests/test_dir_files_from/hello_file\n"; cmd.write_stdin(input.as_ref()); let finished = &cmd.unwrap(); let stderr = std::str::from_utf8(&finished.stderr).unwrap(); assert_eq!(stderr, ""); let output = std::str::from_utf8(&finished.stdout).unwrap(); assert!(output.contains("a_file")); assert!(output.contains("hello_file")); } #[test] pub fn test_files0_from_flag_stdin() { let mut cmd = cargo_bin_cmd!("dust"); cmd.arg("-P").arg("--files0-from").arg("-"); let input = b"tests/test_dir_files_from/a_file\0tests/test_dir_files_from/hello_file\0"; cmd.write_stdin(input.as_ref()); let finished = &cmd.unwrap(); let stderr = std::str::from_utf8(&finished.stderr).unwrap(); assert_eq!(stderr, ""); let output = std::str::from_utf8(&finished.stdout).unwrap(); assert!(output.contains("a_file")); assert!(output.contains("hello_file")); } #[test] pub fn test_with_bad_param() { let mut cmd = cargo_bin_cmd!("dust"); cmd.arg("-P").arg("bad_place"); let output_error = cmd.unwrap_err(); let result = output_error.as_output().unwrap(); let stderr = str::from_utf8(&result.stderr).unwrap(); assert!(stderr.contains("No such file or directory")); } #[test] pub fn test_hidden_flag() { // Check we can see the hidden file normally let output = build_command(vec!["-c", "tests/test_dir_hidden_entries/"]); assert!(output.contains(".hidden_file")); assert!(output.contains("┌─┴ test_dir_hidden_entries")); // Check that adding the '-h' flag causes us to not see hidden files let output = build_command(vec!["-c", "-i", "tests/test_dir_hidden_entries/"]); assert!(!output.contains(".hidden_file")); assert!(output.contains("┌── test_dir_hidden_entries")); } #[test] pub fn test_number_of_files() { // Check we can see the hidden file normally let output = build_command(vec!["-c", "-f", "tests/test_dir"]); assert!(output.contains("1 ┌── a_file ")); assert!(output.contains("1 ├── hello_file")); assert!(output.contains("2 ┌─┴ many")); assert!(output.contains("2 ┌─┴ test_dir")); } #[test] pub fn test_show_files_by_type() { // Check we can list files by type let output = build_command(vec!["-c", "-t", "tests"]); assert!(output.contains(" .unicode")); assert!(output.contains(" .japan")); assert!(output.contains(" .rs")); assert!(output.contains(" (no extension)")); assert!(output.contains("┌─┴ (total)")); } #[test] #[cfg(target_family = "unix")] pub fn test_show_files_only() { let output = build_command(vec!["-c", "-F", "tests/test_dir"]); assert!(output.contains("a_file")); assert!(output.contains("hello_file")); assert!(!output.contains("many")); } #[test] pub fn test_output_skip_total() { let output = build_command(vec![ "--skip-total", "tests/test_dir/many/hello_file", "tests/test_dir/many/a_file", ]); assert!(output.contains("hello_file")); assert!(!output.contains("(total)")); } #[test] pub fn test_output_screen_reader() { let output = build_command(vec!["--screen-reader", "-c", "tests/test_dir/"]); println!("{}", output); assert!(output.contains("test_dir 0")); assert!(output.contains("many 1")); assert!(output.contains("hello_file 2")); assert!(output.contains("a_file 2")); // Verify no 'symbols' reported by screen reader assert!(!output.contains('│')); for block in ['█', '▓', '▒', '░'] { assert!(!output.contains(block)); } } #[test] pub fn test_show_files_by_regex_match_lots() { // Check we can see '.rs' files in the tests directory let output = build_command(vec!["-c", "-e", "\\.rs$", "tests"]); assert!(output.contains(" ┌─┴ tests")); assert!(!output.contains("0B ┌── tests")); assert!(!output.contains("0B ┌─┴ tests")); } #[test] pub fn test_show_files_by_regex_match_nothing() { // Check there are no files named: '.match_nothing' in the tests directory let output = build_command(vec!["-c", "-e", "match_nothing$", "tests"]); assert!(output.contains("0B ┌── tests")); } #[test] pub fn test_show_files_by_regex_match_multiple() { let output = build_command(vec![ "-c", "-e", "test_dir_hidden", "-e", "test_dir2", "-n", "100", "tests", ]); assert!(output.contains("test_dir2")); assert!(output.contains("test_dir_hidden")); assert!(!output.contains("many")); // We do not find the 'many' folder in the 'test_dir' folder } #[test] pub fn test_show_files_by_invert_regex() { let output = build_command(vec!["-c", "-f", "-v", "e", "tests/test_dir2"]); // There are 0 files without 'e' in the name assert!(output.contains("0 ┌── test_dir2")); let output = build_command(vec!["-c", "-f", "-v", "a", "tests/test_dir2"]); // There are 2 files without 'a' in the name assert!(output.contains("2 ┌─┴ test_dir2")); // There are 4 files in the test_dir2 hierarchy let output = build_command(vec!["-c", "-f", "-v", "match_nothing$", "tests/test_dir2"]); assert!(output.contains("4 ┌─┴ test_dir2")); } #[test] pub fn test_show_files_by_invert_regex_match_multiple() { // We ignore test_dir2 & test_dir_unicode, leaving the test_dir folder // which has the 'many' folder inside let output = build_command(vec![ "-c", "-v", "test_dir2", "-v", "test_dir_unicode", "-n", "100", "tests", ]); assert!(!output.contains("test_dir2")); assert!(!output.contains("test_dir_unicode")); assert!(output.contains("many")); } #[test] pub fn test_no_color() { let output = build_command(vec!["-c"]); // Red is 31 assert!(!output.contains("\x1B[31m")); assert!(!output.contains("\x1B[0m")); } #[test] pub fn test_force_color() { let output = build_command(vec!["-C"]); // Red is 31 assert!(output.contains("\x1B[31m")); assert!(output.contains("\x1B[0m")); } #[test] pub fn test_collapse() { let output = build_command(vec!["--collapse", "many", "tests/test_dir/"]); assert!(output.contains("many")); assert!(!output.contains("hello_file")); } #[test] pub fn test_handle_duplicate_names() { // Check that even if we run on a multiple directories with the same name // we still show the distinct parent dir in the output let output = build_command(vec![ "tests/test_dir_matching/dave/dup_name", "tests/test_dir_matching/andy/dup_name", "ci", ]); assert!(output.contains("andy")); assert!(output.contains("dave")); assert!(output.contains("ci")); assert!(output.contains("dup_name")); assert!(!output.contains("test_dir_matching")); } du-dust-1.2.4/tests/tests.rs000064400000000000000000000000011046102023000140560ustar 00000000000000 du-dust-1.2.4/tests/tests_symlinks.rs000064400000000000000000000106751046102023000160310ustar 00000000000000use assert_cmd::{Command, cargo_bin_cmd}; use std::fs::File; use std::io::Write; use std::path::PathBuf; use std::str; 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 } fn link_it(link_path: PathBuf, file_path_s: &str, is_soft: bool) -> String { let link_name_s = link_path.to_str().unwrap(); let mut c = Command::new("ln"); if is_soft { c.arg("-s"); } c.arg(file_path_s); c.arg(link_name_s); assert!(c.output().is_ok()); link_name_s.into() } #[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_it(link_name, file_path_s, true); let c = format!(" ├── {}", link_name_s); let b = format!(" ┌── {}", file_path_s); let a = format!("─┴ {}", dir_s); let mut cmd = cargo_bin_cmd!("dust"); // Mac test runners create long filenames in tmp directories let output = cmd .args(["-p", "-c", "-s", "-w", "999", dir_s]) .unwrap() .stdout; let output = str::from_utf8(&output).unwrap(); assert!(output.contains(a.as_str())); assert!(output.contains(b.as_str())); assert!(output.contains(c.as_str())); } #[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"); link_it(link_name, file_path_s, false); let file_output = format!(" ┌── {}", file_path_s); let dirs_output = format!("─┴ {}", dir_s); let mut cmd = cargo_bin_cmd!("dust"); // Mac test runners create long filenames in tmp directories let output = cmd.args(["-p", "-c", "-w", "999", dir_s]).unwrap().stdout; // The link should not appear in the output because multiple inodes are now ordered // then filtered. let output = str::from_utf8(&output).unwrap(); assert!(output.contains(dirs_output.as_str())); assert!(output.contains(file_output.as_str())); } #[cfg_attr(target_os = "windows", ignore)] #[test] pub fn test_hard_sym_link_no_dup_multi_arg() { let dir = Builder::new().tempdir().unwrap(); let dir_link = Builder::new().tempdir().unwrap(); let file = build_temp_file(&dir); let dir_s = dir.path().to_str().unwrap(); let dir_link_s = dir_link.path().to_str().unwrap(); let file_path_s = file.to_str().unwrap(); let link_name = dir_link.path().join("the_link"); let link_name_s = link_it(link_name, file_path_s, false); let mut cmd = cargo_bin_cmd!("dust"); // Mac test runners create long filenames in tmp directories let output = cmd .args(["-p", "-c", "-w", "999", "-b", dir_link_s, dir_s]) .unwrap() .stdout; // The link or the file should appear but not both let output = str::from_utf8(&output).unwrap(); let has_file_only = output.contains(file_path_s) && !output.contains(&link_name_s); let has_link_only = !output.contains(file_path_s) && output.contains(&link_name_s); assert!(has_file_only || has_link_only); } #[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_it(link_name, dir_s, true); let a = format!("─┬ {}", dir_s); let b = format!(" └── {}", link_name_s); let mut cmd = cargo_bin_cmd!("dust"); let output = cmd .arg("-p") .arg("-c") .arg("-r") .arg("-s") .arg("-w") .arg("999") .arg(dir_s) .unwrap() .stdout; let output = str::from_utf8(&output).unwrap(); assert!(output.contains(a.as_str())); assert!(output.contains(b.as_str())); }