├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── assets ├── broken_icons.png ├── colorization.png ├── du_match.png ├── flat_human_long.png ├── h_readable.png ├── human_readable_flat.png ├── inhuman_readable.png ├── inhuman_readable_flat.png ├── symfollow.png ├── top_demo.png ├── top_showcase.png ├── trunc.png └── untrunc.png ├── example ├── .erdtree.toml └── .erdtreerc ├── rustfmt.toml ├── scripts └── performance_metrics.sh ├── src ├── ansi.rs ├── context │ ├── args.rs │ ├── color.rs │ ├── column.rs │ ├── config │ │ ├── mod.rs │ │ ├── rc.rs │ │ └── toml │ │ │ ├── error.rs │ │ │ ├── mod.rs │ │ │ └── test.rs │ ├── dir.rs │ ├── error.rs │ ├── file.rs │ ├── layout.rs │ ├── mod.rs │ ├── sort.rs │ └── time.rs ├── disk_usage │ ├── file_size │ │ ├── block.rs │ │ ├── byte.rs │ │ ├── line_count.rs │ │ ├── mod.rs │ │ └── word_count.rs │ ├── mod.rs │ └── units.rs ├── fs │ ├── inode.rs │ ├── mod.rs │ ├── permissions │ │ ├── class.rs │ │ ├── error.rs │ │ ├── file_type.rs │ │ ├── mod.rs │ │ └── test.rs │ ├── ug.rs │ └── xattr.rs ├── icons │ ├── fs.rs │ └── mod.rs ├── main.rs ├── progress.rs ├── render │ ├── grid │ │ ├── cell.rs │ │ └── mod.rs │ ├── layout │ │ ├── flat.rs │ │ ├── flat_inverted.rs │ │ ├── inverted.rs │ │ ├── mod.rs │ │ └── regular.rs │ ├── long │ │ └── mod.rs │ ├── mod.rs │ └── theme.rs ├── styles │ ├── error.rs │ └── mod.rs ├── tree │ ├── count.rs │ ├── error.rs │ ├── mod.rs │ ├── node │ │ ├── cmp.rs │ │ ├── mod.rs │ │ └── unix.rs │ └── visitor.rs ├── tty.rs └── utils.rs └── tests ├── data ├── .dagon ├── dream_cycle │ └── polaris.txt ├── lipsum │ └── lipsum.txt ├── necronomicon.txt ├── nemesis.txt ├── nylarlathotep.txt └── the_yellow_king │ └── cassildas_song.md ├── dirs_only.rs ├── flat.rs ├── glob.rs ├── hardlink.rs ├── hardlinks └── kadath.txt ├── level.rs ├── line_count.rs ├── prune.rs ├── regex.rs ├── sort.rs ├── suppress_size.rs ├── symlink.rs └── utils └── mod.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.buymeacoffee.com/O3nsHqb7A9 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | push: 5 | pull_request: 6 | 7 | name: ci 8 | 9 | jobs: 10 | check: 11 | name: Check 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v2 16 | 17 | - name: Install stable toolchain 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: stable 22 | override: true 23 | 24 | - name: Run cargo check 25 | uses: actions-rs/cargo@v1 26 | with: 27 | command: check 28 | 29 | test: 30 | name: Test Suite 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout sources 34 | uses: actions/checkout@v2 35 | 36 | - name: Install stable toolchain 37 | uses: actions-rs/toolchain@v1 38 | with: 39 | profile: minimal 40 | toolchain: stable 41 | override: true 42 | 43 | - name: Run cargo test 44 | uses: actions-rs/cargo@v1 45 | with: 46 | command: test 47 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | jobs: 10 | publish: 11 | name: ${{ matrix.target }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | include: 16 | - os: ubuntu-20.04 17 | target: x86_64-unknown-linux-gnu 18 | toolchain: stable 19 | use-cross: false 20 | 21 | - os: ubuntu-20.04 22 | target: x86_64-unknown-linux-musl 23 | toolchain: stable 24 | use-cross: false 25 | 26 | - os: ubuntu-20.04 27 | target: aarch64-unknown-linux-gnu 28 | toolchain: stable 29 | use-cross: true 30 | 31 | - os: ubuntu-20.04 32 | target: aarch64-unknown-linux-musl 33 | toolchain: stable 34 | use-cross: true 35 | 36 | - os: macos-latest 37 | target: x86_64-apple-darwin 38 | toolchain: stable 39 | use-cross: false 40 | 41 | - os: macos-latest 42 | target: aarch64-apple-darwin 43 | toolchain: stable 44 | use-cross: false 45 | 46 | - os: windows-latest 47 | target: x86_64-pc-windows-msvc 48 | toolchain: nightly-2023-06-11 49 | use-cross: false 50 | 51 | steps: 52 | - name: Checkout repository 53 | uses: actions/checkout@v3 54 | with: 55 | fetch-depth: 1 56 | 57 | - name: Install Rust 58 | uses: actions-rs/toolchain@v1 59 | with: 60 | toolchain: ${{ matrix.toolchain }} 61 | profile: minimal 62 | override: true 63 | target: ${{ matrix.target }} 64 | 65 | - name: rust cache restore 66 | uses: actions/cache/restore@v3 67 | with: 68 | path: | 69 | ~/.cargo/bin/ 70 | ~/.cargo/registry/index/ 71 | ~/.cargo/registry/cache/ 72 | ~/.cargo/git/db/ 73 | target/ 74 | key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} 75 | 76 | - name: Build 77 | uses: actions-rs/cargo@v1 78 | with: 79 | use-cross: ${{ matrix.use-cross }} 80 | command: build 81 | args: --target ${{ matrix.target }} --release --locked 82 | 83 | - name: Upload files (only for Mac/Linux) 84 | if: matrix.target != 'x86_64-pc-windows-msvc' 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | UPLOADTOOL_ISPRERELEASE: true 88 | VERSION: ${{ github.ref_name }} 89 | run: | 90 | curl -L https://github.com/probonopd/uploadtool/raw/master/upload.sh --output upload.sh 91 | mv target/${{ matrix.target }}/release/erd erd 92 | tar -cavf erd-${VERSION}-${{ matrix.target }}.tar.gz erd CHANGELOG.md README.md LICENSE 93 | bash upload.sh erd-${VERSION}-${{ matrix.target }}.tar.gz 94 | 95 | - name: Rename files (only for Windows) 96 | if: matrix.target == 'x86_64-pc-windows-msvc' 97 | env: 98 | VERSION: ${{ github.ref_name }} 99 | run: | 100 | mkdir output/ 101 | mv target/${{ matrix.target }}/release/erd.exe output/erd-$env:VERSION-${{ matrix.target }}.exe 102 | 103 | - name: Upload files (only for Windows) 104 | uses: ncipollo/release-action@v1 105 | if: matrix.target == 'x86_64-pc-windows-msvc' 106 | with: 107 | allowUpdates: true 108 | artifacts: "output/*" 109 | token: ${{ secrets.GITHUB_TOKEN }} 110 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test App With Cache 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "tag version" 8 | required: true 9 | default: "v0.0.1" 10 | 11 | jobs: 12 | publish: 13 | name: ${{ matrix.target }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | include: 18 | - os: ubuntu-20.04 19 | target: x86_64-unknown-linux-gnu 20 | use-cross: false 21 | 22 | - os: ubuntu-20.04 23 | target: x86_64-unknown-linux-musl 24 | use-cross: false 25 | 26 | - os: ubuntu-20.04 27 | target: aarch64-unknown-linux-gnu 28 | use-cross: true 29 | 30 | - os: ubuntu-20.04 31 | target: aarch64-unknown-linux-musl 32 | use-cross: true 33 | 34 | - os: macos-latest 35 | target: x86_64-apple-darwin 36 | use-cross: false 37 | 38 | - os: macos-latest 39 | target: aarch64-apple-darwin 40 | use-cross: false 41 | 42 | - os: windows-latest 43 | target: x86_64-pc-windows-msvc 44 | use-cross: false 45 | 46 | steps: 47 | - name: Checkout repository 48 | uses: actions/checkout@v2 49 | with: 50 | fetch-depth: 1 51 | 52 | - name: Install Rust 53 | uses: actions-rs/toolchain@v1 54 | with: 55 | toolchain: stable 56 | profile: minimal 57 | override: true 58 | target: ${{ matrix.target }} 59 | 60 | - name: rust cache restore 61 | uses: actions/cache/restore@v3 62 | with: 63 | path: | 64 | ~/.cargo/bin/ 65 | ~/.cargo/registry/index/ 66 | ~/.cargo/registry/cache/ 67 | ~/.cargo/git/db/ 68 | target/ 69 | key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} 70 | 71 | 72 | - name: Build 73 | uses: actions-rs/cargo@v1 74 | with: 75 | use-cross: ${{ matrix.use-cross }} 76 | command: build 77 | args: --target ${{ matrix.target }} --release --locked 78 | 79 | - name: Rename files (only for Mac/Linux) 80 | if: matrix.target != 'x86_64-pc-windows-msvc' 81 | env: 82 | VERSION: ${{ inputs.version }} 83 | run: | 84 | mkdir output/ 85 | mv target/${{ matrix.target }}/release/et et 86 | tar -cavf output/et-${VERSION}-${{ matrix.target }}.tar.gz et CHANGELOG.md README.md LICENSE 87 | 88 | - name: Rename files (only for Windows) 89 | if: matrix.target == 'x86_64-pc-windows-msvc' 90 | env: 91 | VERSION: ${{ inputs.version }} 92 | run: | 93 | mkdir output/ 94 | mv target/${{ matrix.target }}/release/et.exe output/et-$env:VERSION-${{ matrix.target }}.exe 95 | 96 | - name: Upload files 97 | # arg info: https://github.com/ncipollo/release-action#release-action 98 | uses: ncipollo/release-action@v1 99 | with: 100 | allowUpdates: true 101 | prerelease: true 102 | artifacts: "output/*" 103 | tag: ${{ inputs.version }} 104 | token: ${{ secrets.GITHUB_TOKEN }} 105 | 106 | 107 | - name: rust cache store 108 | uses: actions/cache/save@v3 109 | with: 110 | path: | 111 | ~/.cargo/bin/ 112 | ~/.cargo/registry/index/ 113 | ~/.cargo/registry/cache/ 114 | ~/.cargo/git/db/ 115 | target/ 116 | key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | benjamin.van.nguyen@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to *erdtree* 2 | 3 | Thank you for taking an interest in *erdtree*! If you'd like to play a part in the development of *erdtree*, the following forms of participation are acceptable: 4 | * New issues: feature requests, bug reports, questions, ideas, etc.. 5 | * Pull requests: documentation improvements, bug fixes, code improvements, new features, etc.. 6 | 7 | The following are the general rules for contributing code: 8 | * Every function, struct, traits, etc. no matter how trivial, should be documented in `cargo doc` fashion. 9 | * When adding or amending a new feature and if warranted, the `usage` as well as the `documentation` section of the README should be updated appropriately. 10 | * Integration is usually required when adding or amending a feature that alters the output. 11 | * All existing tests must pass. 12 | * Unit testing is encouraged but not required. 13 | 14 | **Note**: Before you take the time to open a pull request, please open an issue first to open the floor for discussion. 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "erdtree" 3 | version = "3.1.2" 4 | edition = "2021" 5 | authors = ["Benjamin Nguyen "] 6 | description = """ 7 | erdtree (erd) is a cross-platform, multi-threaded, and general purpose 8 | filesystem and disk usage utility that is aware of .gitignore and hidden file rules. 9 | """ 10 | categories = ["command-line-utilities"] 11 | documentation = "https://github.com/solidiquis/erdtree" 12 | homepage = "https://github.com/solidiquis/erdtree" 13 | repository = "https://github.com/solidiquis/erdtree" 14 | keywords = ["tree", "find", "ls", "du", "commandline"] 15 | exclude = ["assets/*", "scripts/*", "example/*"] 16 | readme = "README.md" 17 | license = "MIT" 18 | rust-version = "1.70.0" 19 | 20 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 21 | 22 | [[bin]] 23 | name = "erd" 24 | path = "src/main.rs" 25 | 26 | [dependencies] 27 | ansi_term = "0.12.1" 28 | chrono = "0.4.24" 29 | clap = { version = "4.1.1", features = ["derive"] } 30 | clap_complete = "4.1.1" 31 | config = { version = "0.13.3", default-features = false, features = ["toml"] } 32 | crossterm = "0.26.1" 33 | ctrlc = "3.4.0" 34 | dirs = "5.0" 35 | errno = "0.3.1" 36 | filesize = "0.2.0" 37 | ignore = "0.4.2" 38 | indextree = "4.6.0" 39 | lscolors = { version = "0.13.0", features = ["ansi_term"] } 40 | once_cell = "1.17.0" 41 | regex = "1.7.3" 42 | terminal_size = "0.2.6" 43 | thiserror = "1.0.40" 44 | 45 | [target.'cfg(unix)'.dependencies] 46 | libc = "0.2.141" 47 | 48 | [target.'cfg(windows)'.dependencies] 49 | winapi = "0.3.9" 50 | 51 | [dev-dependencies] 52 | indoc = "2.0.0" 53 | strip-ansi-escapes = "0.1.1" 54 | tempfile = "3.4.0" 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Benjamin Nguyen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | 6 | | Version | Supported | 7 | | ------- | ------------------ | 8 | | 3.x.x | :white_check_mark: | 9 | | 2.x.x | :x: | 10 | | 1.8.x | :x: | 11 | | < 1.8.1 | :x: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | If you come across a security vulnerability please contact `benjamin.van.nguyen@gmail.com` or [submit a private report](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability). 16 | -------------------------------------------------------------------------------- /assets/broken_icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidiquis/erdtree/77199d97c3f14cfaf66a3c0c54c06d1f28d7e7ca/assets/broken_icons.png -------------------------------------------------------------------------------- /assets/colorization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidiquis/erdtree/77199d97c3f14cfaf66a3c0c54c06d1f28d7e7ca/assets/colorization.png -------------------------------------------------------------------------------- /assets/du_match.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidiquis/erdtree/77199d97c3f14cfaf66a3c0c54c06d1f28d7e7ca/assets/du_match.png -------------------------------------------------------------------------------- /assets/flat_human_long.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidiquis/erdtree/77199d97c3f14cfaf66a3c0c54c06d1f28d7e7ca/assets/flat_human_long.png -------------------------------------------------------------------------------- /assets/h_readable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidiquis/erdtree/77199d97c3f14cfaf66a3c0c54c06d1f28d7e7ca/assets/h_readable.png -------------------------------------------------------------------------------- /assets/human_readable_flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidiquis/erdtree/77199d97c3f14cfaf66a3c0c54c06d1f28d7e7ca/assets/human_readable_flat.png -------------------------------------------------------------------------------- /assets/inhuman_readable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidiquis/erdtree/77199d97c3f14cfaf66a3c0c54c06d1f28d7e7ca/assets/inhuman_readable.png -------------------------------------------------------------------------------- /assets/inhuman_readable_flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidiquis/erdtree/77199d97c3f14cfaf66a3c0c54c06d1f28d7e7ca/assets/inhuman_readable_flat.png -------------------------------------------------------------------------------- /assets/symfollow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidiquis/erdtree/77199d97c3f14cfaf66a3c0c54c06d1f28d7e7ca/assets/symfollow.png -------------------------------------------------------------------------------- /assets/top_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidiquis/erdtree/77199d97c3f14cfaf66a3c0c54c06d1f28d7e7ca/assets/top_demo.png -------------------------------------------------------------------------------- /assets/top_showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidiquis/erdtree/77199d97c3f14cfaf66a3c0c54c06d1f28d7e7ca/assets/top_showcase.png -------------------------------------------------------------------------------- /assets/trunc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidiquis/erdtree/77199d97c3f14cfaf66a3c0c54c06d1f28d7e7ca/assets/trunc.png -------------------------------------------------------------------------------- /assets/untrunc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidiquis/erdtree/77199d97c3f14cfaf66a3c0c54c06d1f28d7e7ca/assets/untrunc.png -------------------------------------------------------------------------------- /example/.erdtree.toml: -------------------------------------------------------------------------------- 1 | icons = true 2 | human = true 3 | 4 | # Compute file sizes like `du` 5 | [du] 6 | disk_usage = "block" 7 | icons = true 8 | layout = "flat" 9 | no-ignore = true 10 | no-git = true 11 | hidden = true 12 | level = 1 13 | 14 | # Do as `ls -l` 15 | [ls] 16 | icons = true 17 | human = true 18 | level = 1 19 | suppress-size = true 20 | long = true 21 | no-ignore = true 22 | hidden = true 23 | 24 | # How many lines of Rust are in this code base? 25 | [rs] 26 | disk-usage = "line" 27 | level = 1 28 | pattern = "\\.rs$" 29 | -------------------------------------------------------------------------------- /example/.erdtreerc: -------------------------------------------------------------------------------- 1 | # Long argument 2 | --icons 3 | --human 4 | 5 | # or short argument 6 | -l 7 | 8 | # args can be passed like this 9 | -d logical 10 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | match_block_trailing_comma = true 2 | -------------------------------------------------------------------------------- /scripts/performance_metrics.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ ! "$OSTYPE" =~ "darwin" ]]; then 4 | printf "Error: Script requires a darwin operating system.\n" 5 | exit 1 6 | fi 7 | 8 | if [[ $(/usr/bin/id -u) != 0 ]]; then 9 | printf "Error: Script requires root privilege.\n" 10 | exit 1 11 | fi 12 | 13 | printf "This script will purge your disk cache. Continue? [y/n]: " 14 | 15 | read -r proceed 16 | 17 | if [[ "$proceed" != "y" ]]; then 18 | echo "Aborted." 19 | exit 0 20 | fi 21 | 22 | cargo build --release 23 | 24 | # Clear disk cache 25 | purge 26 | 27 | fifo="$TMPDIR" 28 | fifo+="erd_performance" 29 | 30 | if [[ -f "$fifo" ]]; then 31 | rm "$fifo" 32 | fi 33 | 34 | exec 3<>$fifo 35 | 36 | trap "rm -f $fifo" SIGINT 37 | 38 | iostat_output= 39 | while read -r line; do 40 | iostat_output+="$line\n" 41 | 42 | read -r -u3 -t1 finished 43 | 44 | if [[ "$finished" == "1" ]]; then 45 | printf "$iostat_output" 46 | rm "$fifo" 47 | exit 0 48 | fi 49 | done < <(iostat -w1) & 50 | 51 | iostat_job="$!" 52 | 53 | trap "kill $iostat_job 2> /dev/null" SIGINT 54 | 55 | echo "Executing command: target/release/erd ${@}" 56 | echo 57 | 58 | /usr/bin/time -p target/release/erd "$@" 1> /dev/null 59 | 60 | echo 61 | 62 | echo "1" >> "$fifo" 63 | 64 | wait "$iostat_job" 65 | -------------------------------------------------------------------------------- /src/ansi.rs: -------------------------------------------------------------------------------- 1 | /// Trait that provides functionality to ANSI escaped strings to be truncated in a manner that 2 | /// preserves the ANSI color/style escape sequences. Consider the following: 3 | /// 4 | /// ``` 5 | /// // "\u{1b}[1;31mHello World\u{1b}[0m" 6 | /// ansi_term::Color::Red.bold().paint("Hello") 7 | /// ``` 8 | /// 9 | /// Truncating the above to a length of 5 would result in: 10 | /// 11 | /// `"\u{1b}[1;31mHello\u{1b}[0m"` 12 | /// 13 | /// NOTE: This is being used for a very particular use-case and isn't comprehensive enough to 14 | /// handle all types of ANSI escaped sequences, only color/style related ones. It also makes some 15 | /// assumptions that are valid only for this program, namely that all relevant grapheme clusters 16 | /// are at most sized to a single `char`, so truncating to any arbitrary length will always result 17 | /// in a coherent output. 18 | pub trait Escaped: AsRef { 19 | fn truncate(&self, new_len: usize) -> String { 20 | let mut open_sequence = false; 21 | let mut resultant = String::new(); 22 | let mut char_count = 0; 23 | let mut chars = self.as_ref().chars(); 24 | 25 | 'outer: while let Some(ch) = chars.next() { 26 | resultant.push(ch); 27 | 28 | if ch == '\u{1b}' { 29 | for code in chars.by_ref() { 30 | resultant.push(code); 31 | 32 | if code == 'm' { 33 | open_sequence = !open_sequence; 34 | continue 'outer; 35 | } 36 | } 37 | } 38 | char_count += 1; 39 | 40 | if char_count == new_len { 41 | break; 42 | } 43 | } 44 | 45 | if open_sequence { 46 | resultant.push_str("\u{1b}[0m"); 47 | } 48 | 49 | resultant 50 | } 51 | } 52 | 53 | impl Escaped for str {} 54 | 55 | #[test] 56 | fn truncate() { 57 | use ansi_term::Color::Red; 58 | 59 | let control = Red.bold().paint("Hello").to_string(); 60 | let base = format!("{}!!!", Red.bold().paint("Hello World")); 61 | let trunc = ::truncate(&base, 5); 62 | 63 | assert_eq!(control, trunc); 64 | } 65 | -------------------------------------------------------------------------------- /src/context/args.rs: -------------------------------------------------------------------------------- 1 | use super::{config, error::Error, Context}; 2 | use clap::{ 3 | builder::ArgAction, parser::ValueSource, ArgMatches, Command, CommandFactory, FromArgMatches, 4 | }; 5 | use std::{ 6 | ffi::{OsStr, OsString}, 7 | path::PathBuf, 8 | }; 9 | 10 | /// Allows the implementor to compute [`ArgMatches`] that reconciles arguments from both the 11 | /// command-line as well as the config file that gets loaded. 12 | pub trait Reconciler: CommandFactory + FromArgMatches { 13 | /// Loads in arguments from both the command-line as well as the config file and reconciles 14 | /// identical arguments between the two using these rules: 15 | /// 16 | /// 1. If no config file is present, use arguments strictly from the command-line. 17 | /// 2. If an argument was provided via the CLI then override the argument from the config. 18 | /// 3. If an argument is sourced from its default value because a user didn't provide it via 19 | /// the CLI, then select the argument from the config if it exists. 20 | fn compute_args() -> Result { 21 | let cmd = Self::command().args_override_self(true); 22 | 23 | let user_args = Command::clone(&cmd).get_matches(); 24 | 25 | if user_args.get_one::("no_config").is_some_and(|b| *b) { 26 | return Ok(user_args); 27 | } 28 | 29 | let maybe_config_args = { 30 | let named_table = user_args.get_one::("config"); 31 | 32 | if let Some(rc) = load_rc_config_args() { 33 | if named_table.is_some() { 34 | return Err(Error::Rc); 35 | } 36 | 37 | Some(rc) 38 | } else { 39 | let toml = load_toml_config_args(named_table.map(String::as_str))?; 40 | 41 | if named_table.is_some() && toml.is_none() { 42 | return Err(Error::NoToml); 43 | } 44 | 45 | toml 46 | } 47 | }; 48 | 49 | let Some(config_args) = maybe_config_args else { 50 | return Ok(user_args); 51 | }; 52 | 53 | let mut final_args = init_empty_args(); 54 | 55 | for arg in cmd.get_arguments() { 56 | let arg_id = arg.get_id(); 57 | let id_str = arg_id.as_str(); 58 | 59 | if id_str == "dir" { 60 | if let Some(dir) = user_args.try_get_one::(id_str)? { 61 | final_args.push(OsString::from(dir)); 62 | } 63 | continue; 64 | } 65 | 66 | let argument_source = user_args 67 | .value_source(id_str) 68 | .map_or(&config_args, |source| { 69 | if source == ValueSource::CommandLine { 70 | &user_args 71 | } else { 72 | &config_args 73 | } 74 | }); 75 | 76 | let Some(key) = arg.get_long().map(|l| format!("--{l}")).map(OsString::from) else { 77 | continue 78 | }; 79 | 80 | match arg.get_action() { 81 | ArgAction::SetTrue => { 82 | if argument_source 83 | .try_get_one::(id_str)? 84 | .is_some_and(|b| *b) 85 | { 86 | final_args.push(key); 87 | }; 88 | }, 89 | ArgAction::SetFalse => continue, 90 | _ => { 91 | let Ok(Some(raw)) = argument_source.try_get_raw(id_str) else { 92 | continue; 93 | }; 94 | final_args.push(key); 95 | final_args.extend(raw.map(OsStr::to_os_string)); 96 | }, 97 | } 98 | } 99 | 100 | Ok(cmd.get_matches_from(final_args)) 101 | } 102 | } 103 | 104 | impl Reconciler for Context {} 105 | 106 | /// Creates a properly formatted `Vec` that [`clap::Command`] would understand. 107 | #[inline] 108 | fn init_empty_args() -> Vec { 109 | vec![OsString::from("--")] 110 | } 111 | 112 | /// Loads an [`ArgMatches`] from `.erdtreerc`. 113 | #[inline] 114 | fn load_rc_config_args() -> Option { 115 | config::rc::read_config_to_string().map(|rc_config| { 116 | let parsed_args = config::rc::parse(&rc_config); 117 | Context::command().get_matches_from(parsed_args) 118 | }) 119 | } 120 | 121 | /// Loads an [`ArgMatches`] from `.erdtree.toml`. 122 | #[inline] 123 | fn load_toml_config_args(named_table: Option<&str>) -> Result, Error> { 124 | let toml_config = config::toml::load()?; 125 | let parsed_args = config::toml::parse(toml_config, named_table)?; 126 | let config_args = Context::command().get_matches_from(parsed_args); 127 | 128 | Ok(Some(config_args)) 129 | } 130 | -------------------------------------------------------------------------------- /src/context/color.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | use once_cell::sync::OnceCell; 3 | use std::{env, ffi::OsString}; 4 | 5 | pub static NO_COLOR: OnceCell> = OnceCell::new(); 6 | 7 | /// Reads in the `NO_COLOR` environment variable to determine whether or not to display color in 8 | /// the output. 9 | pub fn no_color_env() { 10 | let _ = NO_COLOR.set(env::var_os("NO_COLOR")); 11 | } 12 | 13 | /// Enum to determine how the output should be colorized. 14 | #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, Default)] 15 | pub enum Coloring { 16 | /// Print plainly without ANSI escapes 17 | None, 18 | 19 | /// Attempt to colorize output 20 | #[default] 21 | Auto, 22 | 23 | /// Turn on colorization always 24 | Force, 25 | } 26 | -------------------------------------------------------------------------------- /src/context/column.rs: -------------------------------------------------------------------------------- 1 | use super::{Context, PrefixKind}; 2 | use std::convert::From; 3 | 4 | /// Utility struct to help store maximum column widths for attributes of each node. Each width is 5 | /// measured as the number of columns of the tty's window. 6 | #[derive(Default)] 7 | pub struct Properties { 8 | pub max_size_width: usize, 9 | pub max_size_unit_width: usize, 10 | 11 | #[cfg(unix)] 12 | pub max_nlink_width: usize, 13 | 14 | #[cfg(unix)] 15 | pub max_ino_width: usize, 16 | 17 | #[cfg(unix)] 18 | pub max_block_width: usize, 19 | 20 | #[cfg(unix)] 21 | pub max_owner_width: usize, 22 | 23 | #[cfg(unix)] 24 | pub max_group_width: usize, 25 | } 26 | 27 | impl From<&Context> for Properties { 28 | fn from(ctx: &Context) -> Self { 29 | let unit_width = match ctx.unit { 30 | PrefixKind::Bin if ctx.human => 3, 31 | PrefixKind::Si if ctx.human => 2, 32 | _ => 1, 33 | }; 34 | 35 | Self { 36 | max_size_unit_width: unit_width, 37 | ..Default::default() 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/context/config/mod.rs: -------------------------------------------------------------------------------- 1 | const ERDTREE_CONFIG_TOML: &str = ".erdtree.toml"; 2 | const ERDTREE_TOML_PATH: &str = "ERDTREE_TOML_PATH"; 3 | 4 | const ERDTREE_CONFIG_NAME: &str = ".erdtreerc"; 5 | const ERDTREE_CONFIG_PATH: &str = "ERDTREE_CONFIG_PATH"; 6 | 7 | const ERDTREE_DIR: &str = "erdtree"; 8 | 9 | #[cfg(unix)] 10 | const CONFIG_DIR: &str = ".config"; 11 | 12 | #[cfg(unix)] 13 | const HOME: &str = "HOME"; 14 | 15 | #[cfg(unix)] 16 | const XDG_CONFIG_HOME: &str = "XDG_CONFIG_HOME"; 17 | 18 | /// Concerned with loading `.erdtreerc`. 19 | pub mod rc; 20 | 21 | /// Concerned with loading `.erdtree.toml`. 22 | pub mod toml; 23 | -------------------------------------------------------------------------------- /src/context/config/rc.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs, path::PathBuf}; 2 | 3 | /// Reads the config file into a `String` if there is one, otherwise returns `None`. 4 | /// is looked for in the following locations in order: 5 | /// 6 | /// - `$ERDTREE_CONFIG_PATH` 7 | /// - `$XDG_CONFIG_HOME/erdtree/.erdtreerc` 8 | /// - `$XDG_CONFIG_HOME/.erdtreerc` 9 | /// - `$HOME/.config/erdtree/.erdtreerc` 10 | /// - `$HOME/.erdtreerc` 11 | #[cfg(unix)] 12 | pub fn read_config_to_string() -> Option { 13 | config_from_config_path() 14 | .or_else(config_from_xdg_path) 15 | .or_else(config_from_home) 16 | .map(|e| prepend_arg_prefix(&e)) 17 | } 18 | /// is looked for in the following locations in order (Windows specific): 19 | /// 20 | /// - `$ERDTREE_CONFIG_PATH` 21 | /// - `%APPDATA%/erdtree/.erdtreerc` 22 | #[cfg(windows)] 23 | pub fn read_config_to_string() -> Option { 24 | config_from_config_path() 25 | .or_else(config_from_appdata) 26 | .map(|e| prepend_arg_prefix(&e)) 27 | } 28 | 29 | /// Parses the config `str`, removing comments and preparing it as a format understood by 30 | /// [`get_matches_from`]. 31 | /// 32 | /// [`get_matches_from`]: clap::builder::Command::get_matches_from 33 | pub fn parse<'a>(config: &'a str) -> Vec<&'a str> { 34 | config 35 | .lines() 36 | .filter(|line| { 37 | line.trim_start() 38 | .chars() 39 | .next() 40 | .map_or(true, |ch| ch != '#') 41 | }) 42 | .flat_map(str::split_whitespace) 43 | .collect::>() 44 | } 45 | 46 | /// Try to read in config from `ERDTREE_CONFIG_PATH`. 47 | fn config_from_config_path() -> Option { 48 | env::var_os(super::ERDTREE_CONFIG_PATH) 49 | .map(PathBuf::from) 50 | .map(fs::read_to_string) 51 | .and_then(Result::ok) 52 | } 53 | 54 | /// Try to read in config from either one of the following locations: 55 | /// - `$HOME/.config/erdtree/.erdtreerc` 56 | /// - `$HOME/.erdtreerc` 57 | #[cfg(not(windows))] 58 | fn config_from_home() -> Option { 59 | let home = env::var_os(super::HOME).map(PathBuf::from)?; 60 | 61 | let config_path = home 62 | .join(super::CONFIG_DIR) 63 | .join(super::ERDTREE_DIR) 64 | .join(super::ERDTREE_CONFIG_NAME); 65 | 66 | fs::read_to_string(config_path).ok().or_else(|| { 67 | let config_path = home.join(super::ERDTREE_CONFIG_NAME); 68 | fs::read_to_string(config_path).ok() 69 | }) 70 | } 71 | 72 | /// Windows specific: Try to read in config from the following location: 73 | /// - `%APPDATA%/erdtree/.erdtreerc` 74 | #[cfg(windows)] 75 | fn config_from_appdata() -> Option { 76 | let app_data = dirs::config_dir()?; 77 | 78 | let config_path = app_data 79 | .join(super::ERDTREE_DIR) 80 | .join(super::ERDTREE_CONFIG_NAME); 81 | 82 | fs::read_to_string(config_path).ok() 83 | } 84 | 85 | /// Try to read in config from either one of the following locations: 86 | /// - `$XDG_CONFIG_HOME/erdtree/.erdtreerc` 87 | /// - `$XDG_CONFIG_HOME/.erdtreerc` 88 | #[cfg(unix)] 89 | fn config_from_xdg_path() -> Option { 90 | let xdg_config = env::var_os(super::XDG_CONFIG_HOME).map(PathBuf::from)?; 91 | 92 | let config_path = xdg_config 93 | .join(super::ERDTREE_DIR) 94 | .join(super::ERDTREE_CONFIG_NAME); 95 | 96 | fs::read_to_string(config_path).ok().or_else(|| { 97 | let config_path = xdg_config.join(super::ERDTREE_CONFIG_NAME); 98 | fs::read_to_string(config_path).ok() 99 | }) 100 | } 101 | 102 | /// Prepends "--\n" to the config string which is required for proper parsing by 103 | /// [`get_matches_from`]. 104 | /// 105 | /// [`get_matches_from`]: clap::builder::Command::get_matches_from 106 | fn prepend_arg_prefix(config: &str) -> String { 107 | format!("--\n{config}") 108 | } 109 | -------------------------------------------------------------------------------- /src/context/config/toml/error.rs: -------------------------------------------------------------------------------- 1 | use config::ConfigError; 2 | 3 | #[derive(Debug, thiserror::Error)] 4 | pub enum Error { 5 | #[error("Failed to load .erdtree.toml")] 6 | LoadConfig, 7 | 8 | #[error("The configuration file is improperly formatted: {0}")] 9 | InvalidFormat(#[from] ConfigError), 10 | 11 | #[error("Named table '{0}' was not found in '.erdtree.toml'")] 12 | MissingAltConfig(String), 13 | 14 | #[error("'#{0}' is required to be a pointer-sized unsigned integer type")] 15 | InvalidInteger(String), 16 | 17 | #[error("'#{0}' has a type that is invalid")] 18 | InvalidArgument(String), 19 | } 20 | -------------------------------------------------------------------------------- /src/context/config/toml/mod.rs: -------------------------------------------------------------------------------- 1 | use config::{Config, File, Value, ValueKind}; 2 | use error::Error; 3 | use std::{env, ffi::OsString}; 4 | 5 | /// Errors associated with loading and parsing the toml config file. 6 | pub mod error; 7 | 8 | /// Testing related to `.erdtree.toml`. 9 | pub mod test; 10 | 11 | /// Represents an instruction on how to handle a single key-value pair, which makes up a single 12 | /// command-line argument, when constructing the arguments vector. 13 | enum ArgInstructions { 14 | /// Used for bool arguments such as `--icons`. When `icons = true` is set in `.erdtree.toml`, 15 | /// we only want `--icons` to be pushed into the ultimate arguments vector. 16 | PushKeyOnly, 17 | 18 | /// Used for arguments such as `--threads 10`. 19 | PushKeyValue { parsed_value: OsString }, 20 | 21 | /// If a bool field is set to false in `.erdtree.toml` (e.g. `icons = false`) then we want to 22 | /// completely omit the key-value pair from the arguments that we ultimately use. 23 | Pass, 24 | } 25 | 26 | /// Takes in a `Config` that is generated from [`load`] returning a `Vec` which 27 | /// represents command-line arguments from `.erdtree.toml`. If a `named_table` is provided then 28 | /// the top-level table in `.erdtree.toml` is ignored and the configurations specified in the 29 | /// `named_table` will be used instead. 30 | pub fn parse(config: Config, named_table: Option<&str>) -> Result, Error> { 31 | let mut args_map = config.cache.into_table()?; 32 | 33 | if let Some(table) = named_table { 34 | let new_conf = args_map 35 | .get(table) 36 | .and_then(|conf| conf.clone().into_table().ok()) 37 | .ok_or_else(|| Error::MissingAltConfig(table.to_owned()))?; 38 | 39 | args_map = new_conf; 40 | } else { 41 | args_map.retain(|_k, v| !matches!(v.kind, ValueKind::Table(_))); 42 | } 43 | 44 | let mut parsed_args = vec![OsString::from("--")]; 45 | 46 | let process_key = |s| OsString::from(format!("--{s}").replace('_', "-")); 47 | 48 | for (k, v) in &args_map { 49 | match parse_argument(k, v)? { 50 | ArgInstructions::PushKeyValue { parsed_value } => { 51 | let fmt_key = process_key(k); 52 | parsed_args.push(fmt_key); 53 | parsed_args.push(parsed_value); 54 | }, 55 | 56 | ArgInstructions::PushKeyOnly => { 57 | let fmt_key = process_key(k); 58 | parsed_args.push(fmt_key); 59 | }, 60 | 61 | ArgInstructions::Pass => continue, 62 | } 63 | } 64 | 65 | Ok(parsed_args) 66 | } 67 | 68 | /// Reads in `.erdtree.toml` file. 69 | pub fn load() -> Result { 70 | #[cfg(windows)] 71 | return windows::load_toml(); 72 | 73 | #[cfg(unix)] 74 | unix::load_toml() 75 | } 76 | 77 | /// Attempts to load in `.erdtree.toml` from `$ERDTREE_TOML_PATH`. 78 | fn toml_from_env() -> Result { 79 | let config = env::var_os(super::ERDTREE_TOML_PATH) 80 | .map(OsString::into_string) 81 | .transpose() 82 | .map_err(|_| Error::LoadConfig)? 83 | .ok_or(Error::LoadConfig)?; 84 | 85 | let file = config 86 | .strip_suffix(".toml") 87 | .map(File::with_name) 88 | .ok_or(Error::LoadConfig)?; 89 | 90 | Config::builder() 91 | .add_source(file) 92 | .build() 93 | .map_err(Error::from) 94 | } 95 | 96 | /// Simple utility used to extract the underlying value from the [`Value`] enum that we get when 97 | /// loading in the values from `.erdtree.toml`, returning instructions on how the argument should 98 | /// be processed into the ultimate arguments vector. 99 | fn parse_argument(keyword: &str, arg: &Value) -> Result { 100 | macro_rules! try_parse_num { 101 | ($n:expr) => { 102 | usize::try_from($n) 103 | .map_err(|_e| Error::InvalidInteger(keyword.to_owned())) 104 | .map(|num| { 105 | let parsed = OsString::from(format!("{num}")); 106 | ArgInstructions::PushKeyValue { 107 | parsed_value: parsed, 108 | } 109 | }) 110 | }; 111 | } 112 | 113 | match &arg.kind { 114 | ValueKind::Boolean(val) => { 115 | if *val { 116 | Ok(ArgInstructions::PushKeyOnly) 117 | } else { 118 | Ok(ArgInstructions::Pass) 119 | } 120 | }, 121 | ValueKind::String(val) => Ok(ArgInstructions::PushKeyValue { 122 | parsed_value: OsString::from(val), 123 | }), 124 | ValueKind::I64(val) => try_parse_num!(*val), 125 | ValueKind::I128(val) => try_parse_num!(*val), 126 | ValueKind::U64(val) => try_parse_num!(*val), 127 | ValueKind::U128(val) => try_parse_num!(*val), 128 | _ => Err(Error::InvalidArgument(keyword.to_owned())), 129 | } 130 | } 131 | 132 | /// Concerned with how to load `.erdtree.toml` on Unix systems. 133 | #[cfg(unix)] 134 | mod unix { 135 | use super::super::{CONFIG_DIR, ERDTREE_CONFIG_TOML, ERDTREE_DIR, HOME, XDG_CONFIG_HOME}; 136 | use super::Error; 137 | use config::{Config, ConfigError, File}; 138 | use std::{env, path::PathBuf}; 139 | 140 | /// Looks for `.erdtree.toml` in the following locations in order: 141 | /// 142 | /// - `$ERDTREE_TOML_PATH` 143 | /// - `$XDG_CONFIG_HOME/erdtree/.erdtree.toml` 144 | /// - `$XDG_CONFIG_HOME/.erdtree.toml` 145 | /// - `$HOME/.config/erdtree/.erdtree.toml` 146 | /// - `$HOME/.erdtree.toml` 147 | pub(super) fn load_toml() -> Result { 148 | super::toml_from_env() 149 | .or_else(|_| toml_from_xdg_path()) 150 | .or_else(|_| toml_from_home()) 151 | } 152 | 153 | /// Looks for `.erdtree.toml` in the following locations in order: 154 | /// 155 | /// - `$XDG_CONFIG_HOME/erdtree/.erdtree.toml` 156 | /// - `$XDG_CONFIG_HOME/.erdtree.toml` 157 | fn toml_from_xdg_path() -> Result { 158 | let config = env::var_os(XDG_CONFIG_HOME) 159 | .map(PathBuf::from) 160 | .ok_or(Error::LoadConfig)?; 161 | 162 | let mut file = config 163 | .join(ERDTREE_DIR) 164 | .join(ERDTREE_CONFIG_TOML) 165 | .to_str() 166 | .and_then(|s| s.strip_suffix(".toml")) 167 | .map(File::with_name); 168 | 169 | if file.is_none() { 170 | file = config 171 | .join(ERDTREE_CONFIG_TOML) 172 | .to_str() 173 | .and_then(|s| s.strip_suffix(".toml")) 174 | .map(File::with_name); 175 | } 176 | 177 | file.map_or_else( 178 | || Err(Error::LoadConfig), 179 | |f| Config::builder().add_source(f).build().map_err(Error::from), 180 | ) 181 | } 182 | 183 | /// Looks for `.erdtree.toml` in the following locations in order: 184 | /// 185 | /// - `$HOME/.config/erdtree/.erdtree.toml` 186 | /// - `$HOME/.erdtree.toml` 187 | fn toml_from_home() -> Result { 188 | let home = env::var_os(HOME) 189 | .map(PathBuf::from) 190 | .ok_or(Error::LoadConfig)?; 191 | 192 | let mut file = home 193 | .join(CONFIG_DIR) 194 | .join(ERDTREE_DIR) 195 | .join(ERDTREE_CONFIG_TOML) 196 | .to_str() 197 | .and_then(|s| s.strip_suffix(".toml")) 198 | .map(File::with_name); 199 | 200 | if file.is_none() { 201 | file = home 202 | .join(ERDTREE_CONFIG_TOML) 203 | .to_str() 204 | .and_then(|s| s.strip_suffix(".toml")) 205 | .map(File::with_name); 206 | } 207 | 208 | file.map_or_else( 209 | || Err(Error::LoadConfig), 210 | |f| Config::builder() 211 | .add_source(f) 212 | .build() 213 | .map_err(|err| match err { 214 | ConfigError::FileParse { .. } | ConfigError::Type { .. } => Error::from(err), 215 | _ => Error::LoadConfig, 216 | }), 217 | ) 218 | } 219 | } 220 | 221 | /// Concerned with how to load `.erdtree.toml` on Windows. 222 | #[cfg(windows)] 223 | mod windows { 224 | use super::super::{ERDTREE_CONFIG_TOML, ERDTREE_DIR}; 225 | use super::Error; 226 | use config::{Config, File}; 227 | 228 | /// Try to read in config from the following location: 229 | /// - `%APPDATA%\erdtree\.erdtree.toml` 230 | pub(super) fn load_toml() -> Result { 231 | super::toml_from_env().or_else(toml_from_appdata) 232 | } 233 | 234 | /// Try to read in config from the following location: 235 | /// - `%APPDATA%\erdtree\.erdtree.toml` 236 | fn toml_from_appdata() -> Result { 237 | let app_data = dirs::config_dir()?; 238 | 239 | let file = app_data 240 | .join(ERDTREE_DIR) 241 | .join(ERDTREE_CONFIG_TOML) 242 | .to_str() 243 | .and_then(|s| s.strip_suffix(".toml")) 244 | .map(File::with_name)?; 245 | 246 | Config::builder() 247 | .add_source(file) 248 | .build() 249 | .map_err(Error::from) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/context/config/toml/test.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn parse_toml() -> Result<(), Box> { 3 | use config::{Config, File}; 4 | use std::{ffi::OsString, io::Write}; 5 | use tempfile::Builder; 6 | 7 | let mut config_file = Builder::new() 8 | .prefix(".erdtree") 9 | .suffix(".toml") 10 | .tempfile()?; 11 | 12 | let toml_contents = r#" 13 | icons = true 14 | human = true 15 | threads = 10 16 | 17 | [grogoroth] 18 | disk_usage = "block" 19 | icons = true 20 | human = false 21 | threads = 10 22 | "#; 23 | 24 | config_file.write_all(toml_contents.as_bytes())?; 25 | 26 | let file = config_file 27 | .path() 28 | .to_str() 29 | .and_then(|s| s.strip_suffix(".toml")) 30 | .map(File::with_name) 31 | .unwrap(); 32 | 33 | let config = Config::builder().add_source(file).build()?; 34 | 35 | // TOP-LEVEL TABLE 36 | let mut toml = super::parse(config.clone(), None)?; 37 | 38 | let expected = vec![ 39 | OsString::from("--"), 40 | OsString::from("--icons"), 41 | OsString::from("--human"), 42 | OsString::from("--threads"), 43 | OsString::from("10"), 44 | ]; 45 | 46 | for (i, outer_item) in expected.iter().enumerate() { 47 | for j in 0..toml.len() { 48 | let inner_item = &toml[j]; 49 | 50 | if outer_item == inner_item { 51 | toml.swap(i, j); 52 | } 53 | } 54 | } 55 | 56 | assert_eq!(toml.len(), expected.len()); 57 | 58 | for (lhs, rhs) in toml.iter().zip(expected.iter()) { 59 | assert_eq!(lhs, rhs); 60 | } 61 | 62 | // NAMED-TABLE 63 | let mut toml = super::parse(config, Some("grogoroth"))?; 64 | 65 | let expected = vec![ 66 | OsString::from("--"), 67 | OsString::from("--disk-usage"), 68 | OsString::from("block"), 69 | OsString::from("--icons"), 70 | OsString::from("--threads"), 71 | OsString::from("10"), 72 | ]; 73 | 74 | for (i, outer_item) in expected.iter().enumerate() { 75 | for j in 0..toml.len() { 76 | let inner_item = &toml[j]; 77 | 78 | if outer_item == inner_item { 79 | toml.swap(i, j); 80 | } 81 | } 82 | } 83 | 84 | assert_eq!(toml.len(), expected.len()); 85 | 86 | for (lhs, rhs) in toml.iter().zip(expected.iter()) { 87 | assert_eq!(lhs, rhs); 88 | } 89 | 90 | Ok(()) 91 | } 92 | -------------------------------------------------------------------------------- /src/context/dir.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | 3 | /// Enum to determine how directories should be ordered relative to regular files in output. 4 | #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, Default)] 5 | pub enum Order { 6 | /// Directories are ordered as if they were regular nodes. 7 | #[default] 8 | None, 9 | 10 | /// Sort directories above files 11 | First, 12 | 13 | /// Sort directories below files 14 | Last, 15 | } 16 | -------------------------------------------------------------------------------- /src/context/error.rs: -------------------------------------------------------------------------------- 1 | use super::config::toml::error::Error as TomlError; 2 | use clap::{parser::MatchesError, Error as ClapError}; 3 | use ignore::Error as IgnoreError; 4 | use regex::Error as RegexError; 5 | 6 | #[derive(Debug, thiserror::Error)] 7 | pub enum Error { 8 | #[error("{0}")] 9 | ArgParse(ClapError), 10 | 11 | #[error("A configuration file was found but failed to parse: {0}")] 12 | Config(ClapError), 13 | 14 | #[error("No glob was provided")] 15 | EmptyGlob, 16 | 17 | #[error("{0}")] 18 | IgnoreError(#[from] IgnoreError), 19 | 20 | #[error("{0}")] 21 | InvalidRegularExpression(#[from] RegexError), 22 | 23 | #[error("Missing '--pattern' argument")] 24 | PatternNotProvided, 25 | 26 | #[error("{0}")] 27 | ConfigError(#[from] TomlError), 28 | 29 | #[error("{0}")] 30 | MatchError(#[from] MatchesError), 31 | 32 | #[error("'--config' was specified but a `.erdtree.toml` file could not be found")] 33 | NoToml, 34 | 35 | #[error("Please migrate from `erdtreerc` to `.erdtree.toml` to make use of `--config`")] 36 | Rc, 37 | } 38 | -------------------------------------------------------------------------------- /src/context/file.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | 3 | /// File-types found in both Unix and Windows. 4 | #[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default)] 5 | pub enum Type { 6 | /// A regular file. 7 | #[default] 8 | File, 9 | 10 | /// A directory. 11 | Dir, 12 | 13 | /// A symlink. 14 | Link, 15 | } 16 | -------------------------------------------------------------------------------- /src/context/layout.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | 3 | /// Which layout to use when rendering the tree. 4 | #[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default)] 5 | pub enum Type { 6 | /// Outputs the tree with the root node at the bottom of the output 7 | #[default] 8 | Regular, 9 | 10 | /// Outputs the tree with the root node at the top of the output 11 | Inverted, 12 | 13 | /// Outputs a flat layout using paths rather than an ASCII tree 14 | Flat, 15 | 16 | /// Outputs an inverted flat layout with the root at the top of the output 17 | Iflat, 18 | } 19 | -------------------------------------------------------------------------------- /src/context/sort.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | 3 | /// Order in which to print nodes. 4 | #[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default)] 5 | pub enum Type { 6 | /// Sort entries by file name in lexicographical order. 7 | Name, 8 | /// Sort entries by file name in reversed lexicographical order. 9 | Rname, 10 | 11 | /// Sort entries by size smallest to largest, top to bottom 12 | #[default] 13 | Size, 14 | 15 | /// Sort entries by size largest to smallest, bottom to top 16 | Rsize, 17 | 18 | /// Sort entries by newer to older Accessing Date 19 | #[value(alias("atime"))] 20 | Access, 21 | 22 | /// Sort entries by older to newer Accessing Date 23 | #[value(alias("ratime"))] 24 | Raccess, 25 | 26 | /// Sort entries by newer to older Creation Date 27 | #[value(alias("ctime"))] 28 | Create, 29 | 30 | /// Sort entries by older to newer Creation Date 31 | #[value(alias("rctime"))] 32 | Rcreate, 33 | 34 | /// Sort entries by newer to older Alteration Date 35 | #[value(alias("mtime"))] 36 | Mod, 37 | 38 | /// Sort entries by older to newer Alteration Date 39 | #[value(alias("rmtime"))] 40 | Rmod, 41 | } 42 | -------------------------------------------------------------------------------- /src/context/time.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | 3 | /// Different types of timestamps available in long-view. 4 | #[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default)] 5 | pub enum Stamp { 6 | /// Time created (alias: ctime) 7 | #[value(alias("ctime"))] 8 | Create, 9 | 10 | /// Time last accessed (alias: atime) 11 | #[value(alias("atime"))] 12 | Access, 13 | 14 | /// Time last modified (alias: mtime) 15 | #[default] 16 | #[value(alias("mtime"))] 17 | Mod, 18 | } 19 | 20 | /// Different formatting options for timestamps 21 | #[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default)] 22 | pub enum Format { 23 | /// Timestamp formatted following the iso8601, with slight differences and the time-zone omitted 24 | Iso, 25 | 26 | /// Timestamp formatted following the exact iso8601 specifications 27 | IsoStrict, 28 | 29 | /// Timestamp only shows date without time in YYYY-MM-DD format 30 | Short, 31 | 32 | /// Timestamp is shown in DD MMM HH:MM format 33 | #[default] 34 | Default, 35 | } 36 | -------------------------------------------------------------------------------- /src/disk_usage/file_size/block.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{self, Display}, 3 | fs::Metadata, 4 | os::unix::fs::MetadataExt, 5 | }; 6 | 7 | #[derive(Default)] 8 | pub struct Metric { 9 | pub value: u64, 10 | } 11 | 12 | impl Metric { 13 | pub fn init(md: &Metadata) -> Self { 14 | Self { value: md.blocks() } 15 | } 16 | } 17 | 18 | impl Display for Metric { 19 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 20 | ::fmt(&self.value, f) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/disk_usage/file_size/byte.rs: -------------------------------------------------------------------------------- 1 | use super::super::units::{BinPrefix, PrefixKind, SiPrefix, UnitPrefix}; 2 | use filesize::PathExt; 3 | use std::{ 4 | cell::{Ref, RefCell}, 5 | fmt::{self, Display}, 6 | fs::Metadata, 7 | path::Path, 8 | }; 9 | 10 | /// Concerned with measuring file size in bytes, whether logical or physical determined by `kind`. 11 | /// Binary or SI units used for reporting determined by `prefix_kind`. 12 | pub struct Metric { 13 | pub value: u64, 14 | pub human_readable: bool, 15 | #[allow(dead_code)] 16 | kind: MetricKind, 17 | prefix_kind: PrefixKind, 18 | 19 | /// To prevent allocating the same string twice. We allocate the first time 20 | /// in [`crate::tree::Tree::update_column_properties`] in order to compute the max column width for 21 | /// human-readable size and cache it. It will then be used again when preparing the output. 22 | cached_display: RefCell, 23 | } 24 | 25 | /// Represents the appropriate method in which to compute bytes. `Logical` represent the total amount 26 | /// of bytes in a file; `Physical` represents how many bytes are actually used to store the file on 27 | /// disk. 28 | pub enum MetricKind { 29 | Logical, 30 | Physical, 31 | } 32 | 33 | impl Metric { 34 | /// Initializes a [Metric] that stores the total amount of bytes in a file. 35 | pub fn init_logical( 36 | metadata: &Metadata, 37 | prefix_kind: PrefixKind, 38 | human_readable: bool, 39 | ) -> Self { 40 | let value = metadata.len(); 41 | let kind = MetricKind::Logical; 42 | 43 | Self { 44 | value, 45 | human_readable, 46 | kind, 47 | prefix_kind, 48 | cached_display: RefCell::default(), 49 | } 50 | } 51 | 52 | /// Initializes an empty [Metric] used to represent the total amount of bytes of a file. 53 | pub fn init_empty_logical(human_readable: bool, prefix_kind: PrefixKind) -> Self { 54 | Self { 55 | value: 0, 56 | human_readable, 57 | kind: MetricKind::Logical, 58 | prefix_kind, 59 | cached_display: RefCell::default(), 60 | } 61 | } 62 | 63 | /// Initializes an empty [Metric] used to represent the total disk space of a file in bytes. 64 | pub fn init_empty_physical(human_readable: bool, prefix_kind: PrefixKind) -> Self { 65 | Self { 66 | value: 0, 67 | human_readable, 68 | kind: MetricKind::Physical, 69 | prefix_kind, 70 | cached_display: RefCell::default(), 71 | } 72 | } 73 | 74 | /// Initializes a [Metric] that stores the total amount of bytes used to store a file on disk. 75 | pub fn init_physical( 76 | path: &Path, 77 | metadata: &Metadata, 78 | prefix_kind: PrefixKind, 79 | human_readable: bool, 80 | ) -> Self { 81 | let value = path.size_on_disk_fast(metadata).unwrap_or(metadata.len()); 82 | let kind = MetricKind::Physical; 83 | 84 | Self { 85 | value, 86 | human_readable, 87 | kind, 88 | prefix_kind, 89 | cached_display: RefCell::default(), 90 | } 91 | } 92 | 93 | /// Returns an immutable borrow of the `cached_display`. 94 | pub fn cached_display(&self) -> Ref<'_, String> { 95 | self.cached_display.borrow() 96 | } 97 | } 98 | 99 | impl Display for Metric { 100 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 101 | { 102 | let cached_display = self.cached_display(); 103 | 104 | if cached_display.len() > 0 { 105 | return write!(f, "{cached_display}"); 106 | } 107 | } 108 | 109 | let value = self.value as f64; 110 | 111 | let display = match self.prefix_kind { 112 | PrefixKind::Si => { 113 | if self.human_readable { 114 | let unit = SiPrefix::from(self.value); 115 | 116 | if unit == SiPrefix::Base { 117 | format!("{} {unit}", self.value) 118 | } else { 119 | let base_value = unit.base_value(); 120 | let size = value / (base_value as f64); 121 | format!("{size:.1} {unit}") 122 | } 123 | } else { 124 | format!("{} {}", self.value, SiPrefix::Base) 125 | } 126 | }, 127 | PrefixKind::Bin => { 128 | if self.human_readable { 129 | let unit = BinPrefix::from(self.value); 130 | 131 | if unit == BinPrefix::Base { 132 | format!("{} {unit}", self.value) 133 | } else { 134 | let base_value = unit.base_value(); 135 | let size = value / (base_value as f64); 136 | format!("{size:.1} {unit}") 137 | } 138 | } else { 139 | format!("{} {}", self.value, BinPrefix::Base) 140 | } 141 | }, 142 | }; 143 | 144 | write!(f, "{display}")?; 145 | 146 | self.cached_display.replace(display); 147 | 148 | Ok(()) 149 | } 150 | } 151 | 152 | #[test] 153 | fn test_metric() { 154 | let metric = Metric { 155 | value: 100, 156 | kind: MetricKind::Logical, 157 | human_readable: false, 158 | prefix_kind: PrefixKind::Bin, 159 | cached_display: RefCell::::default(), 160 | }; 161 | assert_eq!(format!("{metric}"), "100 B"); 162 | 163 | let metric = Metric { 164 | value: 1000, 165 | kind: MetricKind::Logical, 166 | human_readable: true, 167 | prefix_kind: PrefixKind::Si, 168 | cached_display: RefCell::::default(), 169 | }; 170 | assert_eq!(format!("{metric}"), "1.0 KB"); 171 | 172 | let metric = Metric { 173 | value: 1000, 174 | kind: MetricKind::Logical, 175 | human_readable: true, 176 | prefix_kind: PrefixKind::Bin, 177 | cached_display: RefCell::::default(), 178 | }; 179 | assert_eq!(format!("{metric}"), "1000 B"); 180 | 181 | let metric = Metric { 182 | value: 1024, 183 | kind: MetricKind::Logical, 184 | human_readable: true, 185 | prefix_kind: PrefixKind::Bin, 186 | cached_display: RefCell::::default(), 187 | }; 188 | assert_eq!(format!("{metric}"), "1.0 KiB"); 189 | 190 | let metric = Metric { 191 | value: 2_u64.pow(20), 192 | kind: MetricKind::Logical, 193 | human_readable: true, 194 | prefix_kind: PrefixKind::Bin, 195 | cached_display: RefCell::::default(), 196 | }; 197 | assert_eq!(format!("{metric}"), "1.0 MiB"); 198 | 199 | let metric = Metric { 200 | value: 123_454, 201 | kind: MetricKind::Logical, 202 | human_readable: false, 203 | prefix_kind: PrefixKind::Bin, 204 | cached_display: RefCell::::default(), 205 | }; 206 | assert_eq!(format!("{metric}"), "123454 B"); 207 | } 208 | -------------------------------------------------------------------------------- /src/disk_usage/file_size/line_count.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::{AsRef, From}, 3 | fmt::{self, Display}, 4 | fs, 5 | path::Path, 6 | }; 7 | 8 | /// Concerned with measuring file size using line count as a metric. 9 | #[derive(Default)] 10 | pub struct Metric { 11 | pub value: u64, 12 | } 13 | 14 | impl Metric { 15 | /// Reads in contents of a file given by `path` and attempts to compute the total number of 16 | /// lines in that file. If a file is not UTF-8 encoded as in the case of a binary jpeg file 17 | /// then `None` will be returned. 18 | pub fn init(path: impl AsRef) -> Option { 19 | let data = fs::read_to_string(path.as_ref()).ok()?; 20 | 21 | let lines = data.lines().count(); 22 | 23 | u64::try_from(lines).map(|value| Self { value }).ok() 24 | } 25 | } 26 | 27 | impl From for Metric { 28 | fn from(value: u64) -> Self { 29 | Self { value } 30 | } 31 | } 32 | 33 | impl Display for Metric { 34 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 35 | ::fmt(&self.value, f) 36 | } 37 | } 38 | 39 | #[test] 40 | fn test_line_count() { 41 | let metric = 42 | Metric::init("tests/data/nemesis.txt").expect("Expected 'tests/data/nemesis.txt' to exist"); 43 | 44 | assert_eq!(metric.value, 4); 45 | } 46 | -------------------------------------------------------------------------------- /src/disk_usage/file_size/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::context::Context; 2 | use clap::ValueEnum; 3 | use std::{ 4 | convert::From, 5 | fmt::{self, Display}, 6 | ops::AddAssign, 7 | }; 8 | 9 | /// Concerned with measuring file size in blocks. 10 | #[cfg(unix)] 11 | pub mod block; 12 | 13 | /// Concerned with measuring file size in bytes, logical or physical. 14 | pub mod byte; 15 | 16 | /// Concerned with measuring file size by line count. 17 | pub mod line_count; 18 | 19 | /// Concerned with measuring file size by word count. 20 | pub mod word_count; 21 | 22 | #[cfg(unix)] 23 | pub const BLOCK_SIZE_BYTES: u16 = 512; 24 | 25 | /// Represents all the different ways in which a filesize could be reported using various metrics. 26 | pub enum FileSize { 27 | Word(word_count::Metric), 28 | Line(line_count::Metric), 29 | Byte(byte::Metric), 30 | #[cfg(unix)] 31 | Block(block::Metric), 32 | } 33 | 34 | /// Determines between logical or physical size for display 35 | #[derive(Copy, Clone, Debug, ValueEnum, Default)] 36 | pub enum DiskUsage { 37 | /// How many bytes does a file contain 38 | Logical, 39 | 40 | /// How many actual bytes on disk, taking into account blocks, sparse files, and compression. 41 | #[default] 42 | Physical, 43 | 44 | /// How many total lines a file contains 45 | Line, 46 | 47 | /// How many total words a file contains 48 | Word, 49 | 50 | /// How many blocks are allocated to store the file 51 | #[cfg(unix)] 52 | Block, 53 | } 54 | 55 | impl FileSize { 56 | /// Extracts the inner value of [`FileSize`] which represents the file size for various metrics. 57 | #[inline] 58 | pub const fn value(&self) -> u64 { 59 | match self { 60 | Self::Byte(metric) => metric.value, 61 | Self::Line(metric) => metric.value, 62 | Self::Word(metric) => metric.value, 63 | 64 | #[cfg(unix)] 65 | Self::Block(metric) => metric.value, 66 | } 67 | } 68 | } 69 | 70 | impl AddAssign<&Self> for FileSize { 71 | fn add_assign(&mut self, rhs: &Self) { 72 | match self { 73 | Self::Byte(metric) => metric.value += rhs.value(), 74 | Self::Line(metric) => metric.value += rhs.value(), 75 | Self::Word(metric) => metric.value += rhs.value(), 76 | 77 | #[cfg(unix)] 78 | Self::Block(metric) => metric.value += rhs.value(), 79 | } 80 | } 81 | } 82 | 83 | impl From<&Context> for FileSize { 84 | fn from(ctx: &Context) -> Self { 85 | use DiskUsage::{Line, Logical, Physical, Word}; 86 | 87 | match ctx.disk_usage { 88 | Logical => Self::Byte(byte::Metric::init_empty_logical(ctx.human, ctx.unit)), 89 | Physical => Self::Byte(byte::Metric::init_empty_physical(ctx.human, ctx.unit)), 90 | Line => Self::Line(line_count::Metric::default()), 91 | Word => Self::Word(word_count::Metric::default()), 92 | 93 | #[cfg(unix)] 94 | DiskUsage::Block => Self::Block(block::Metric::default()), 95 | } 96 | } 97 | } 98 | 99 | impl Display for FileSize { 100 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 101 | match self { 102 | Self::Word(metric) => write!(f, "{metric}"), 103 | Self::Line(metric) => write!(f, "{metric}"), 104 | Self::Byte(metric) => write!(f, "{metric}"), 105 | 106 | #[cfg(unix)] 107 | Self::Block(metric) => write!(f, "{metric}"), 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/disk_usage/file_size/word_count.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::{AsRef, From}, 3 | fmt::{self, Display}, 4 | fs, 5 | path::Path, 6 | }; 7 | 8 | /// Concerned with measuring file size using word count as a metric. 9 | #[derive(Default)] 10 | pub struct Metric { 11 | pub value: u64, 12 | } 13 | 14 | impl Metric { 15 | /// Reads in contents of a file given by `path` and attempts to compute the total number of 16 | /// words in that file. If a file is not UTF-8 encoded as in the case of a binary jpeg file 17 | /// then `None` will be returned. 18 | /// 19 | /// Words are UTF-8 encoded byte sequences delimited by Unicode Derived Core Property `White_Space`. 20 | pub fn init(path: impl AsRef) -> Option { 21 | let data = fs::read_to_string(path.as_ref()).ok()?; 22 | 23 | let words = data.split_whitespace().count(); 24 | 25 | u64::try_from(words).map(|value| Self { value }).ok() 26 | } 27 | } 28 | 29 | impl From for Metric { 30 | fn from(value: u64) -> Self { 31 | Self { value } 32 | } 33 | } 34 | 35 | impl Display for Metric { 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | ::fmt(&self.value, f) 38 | } 39 | } 40 | 41 | #[test] 42 | fn test_line_count() { 43 | let metric = 44 | Metric::init("tests/data/nemesis.txt").expect("Expected 'tests/data/nemesis.txt' to exist"); 45 | 46 | assert_eq!(metric.value, 27); 47 | } 48 | -------------------------------------------------------------------------------- /src/disk_usage/mod.rs: -------------------------------------------------------------------------------- 1 | /// Binary and SI prefixes 2 | pub mod units; 3 | 4 | /// Concerned with all of the different ways to measure file size: bytes, word-count, line-count, 5 | /// blocks (unix), etc.. 6 | pub mod file_size; 7 | -------------------------------------------------------------------------------- /src/disk_usage/units.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | use std::{ 3 | convert::From, 4 | fmt::{self, Display}, 5 | }; 6 | 7 | /// Determines whether to use SI prefixes or binary prefixes. 8 | #[derive(Copy, Clone, Debug, ValueEnum, Default)] 9 | pub enum PrefixKind { 10 | /// Displays disk usage using binary prefixes. 11 | #[default] 12 | Bin, 13 | 14 | /// Displays disk usage using SI prefixes. 15 | Si, 16 | } 17 | 18 | /// Binary prefixes. 19 | #[derive(Debug, PartialEq, Eq)] 20 | pub enum BinPrefix { 21 | Base, 22 | Kibi, 23 | Mebi, 24 | Gibi, 25 | Tebi, 26 | } 27 | 28 | /// SI prefixes. 29 | #[derive(Debug, PartialEq, Eq)] 30 | pub enum SiPrefix { 31 | Base, 32 | Kilo, 33 | Mega, 34 | Giga, 35 | Tera, 36 | } 37 | 38 | impl SiPrefix { 39 | /// Returns the human readable representation of the SI prefix. 40 | pub const fn as_str(&self) -> &str { 41 | match self { 42 | Self::Base => "B", 43 | Self::Kilo => "KB", 44 | Self::Mega => "MB", 45 | Self::Giga => "GB", 46 | Self::Tera => "TB", 47 | } 48 | } 49 | } 50 | 51 | impl BinPrefix { 52 | /// Returns the human readable representation of the binary prefix. 53 | pub const fn as_str(&self) -> &str { 54 | match self { 55 | Self::Base => "B", 56 | Self::Kibi => "KiB", 57 | Self::Mebi => "MiB", 58 | Self::Gibi => "GiB", 59 | Self::Tebi => "TiB", 60 | } 61 | } 62 | } 63 | 64 | pub trait UnitPrefix { 65 | fn base_value(&self) -> u64; 66 | } 67 | 68 | impl UnitPrefix for SiPrefix { 69 | fn base_value(&self) -> u64 { 70 | match self { 71 | Self::Base => 1, 72 | Self::Kilo => 10_u64.pow(3), 73 | Self::Mega => 10_u64.pow(6), 74 | Self::Giga => 10_u64.pow(9), 75 | Self::Tera => 10_u64.pow(12), 76 | } 77 | } 78 | } 79 | 80 | impl UnitPrefix for BinPrefix { 81 | fn base_value(&self) -> u64 { 82 | match self { 83 | Self::Base => 1, 84 | Self::Kibi => 2_u64.pow(10), 85 | Self::Mebi => 2_u64.pow(20), 86 | Self::Gibi => 2_u64.pow(30), 87 | Self::Tebi => 2_u64.pow(40), 88 | } 89 | } 90 | } 91 | 92 | /// Get the closest human-readable unit prefix for value. 93 | impl From for BinPrefix { 94 | fn from(value: u64) -> Self { 95 | let log = (value as f64).log2(); 96 | 97 | if log < 10. { 98 | Self::Base 99 | } else if log < 20. { 100 | Self::Kibi 101 | } else if log < 30. { 102 | Self::Mebi 103 | } else if log < 40. { 104 | Self::Gibi 105 | } else { 106 | Self::Tebi 107 | } 108 | } 109 | } 110 | 111 | /// Get the closest human-readable unit prefix for value. 112 | impl From for SiPrefix { 113 | fn from(value: u64) -> Self { 114 | let log = (value as f64).log10(); 115 | 116 | if log < 3. { 117 | Self::Base 118 | } else if log < 6. { 119 | Self::Kilo 120 | } else if log < 9. { 121 | Self::Mega 122 | } else if log < 12. { 123 | Self::Giga 124 | } else { 125 | Self::Tera 126 | } 127 | } 128 | } 129 | 130 | impl Display for BinPrefix { 131 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 132 | write!(f, "{}", self.as_str()) 133 | } 134 | } 135 | 136 | impl Display for SiPrefix { 137 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 138 | write!(f, "{}", self.as_str()) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/fs/inode.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::TryFrom, fs::Metadata}; 2 | 3 | /// Represents a file's underlying inode. 4 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 5 | pub struct Inode { 6 | pub ino: u64, 7 | pub dev: u64, 8 | pub nlink: u64, 9 | } 10 | 11 | impl Inode { 12 | /// Initializer for an inode given all the properties that make it unique. 13 | pub const fn new(ino: u64, dev: u64, nlink: u64) -> Self { 14 | Self { ino, dev, nlink } 15 | } 16 | } 17 | 18 | #[derive(Debug, thiserror::Error)] 19 | #[error("Insufficient information to compute inode")] 20 | pub struct Error; 21 | 22 | impl TryFrom<&Metadata> for Inode { 23 | type Error = Error; 24 | 25 | #[cfg(unix)] 26 | fn try_from(md: &Metadata) -> Result { 27 | use std::os::unix::fs::MetadataExt; 28 | 29 | Ok(Self::new(md.ino(), md.dev(), md.nlink())) 30 | } 31 | 32 | #[cfg(windows)] 33 | fn try_from(md: &Metadata) -> Result { 34 | use std::os::windows::fs::MetadataExt; 35 | 36 | if let (Some(dev), Some(ino), Some(nlink)) = ( 37 | md.volume_serial_number(), 38 | md.file_index(), 39 | md.number_of_links(), 40 | ) { 41 | return Ok(Self::new(ino, dev.into(), nlink.into())); 42 | } 43 | 44 | Err(Error {}) 45 | } 46 | 47 | #[cfg(not(any(unix, windows)))] 48 | fn try_from(md: &Metadata) -> Result { 49 | Err(Error {}) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/fs/mod.rs: -------------------------------------------------------------------------------- 1 | use ignore::DirEntry; 2 | use std::{fs, path::PathBuf}; 3 | 4 | /// Operations pertaining to underlying inodes of files. 5 | pub mod inode; 6 | 7 | /// Unix file permissions. 8 | #[cfg(unix)] 9 | pub mod permissions; 10 | 11 | /// Determining whether or not a file has extended attributes. 12 | #[cfg(unix)] 13 | pub mod xattr; 14 | 15 | /// Concerned with determining group and owner of file. 16 | #[cfg(unix)] 17 | pub mod ug; 18 | 19 | /// Returns the path to the target of the soft link. Returns `None` if provided `dir_entry` isn't a 20 | /// symlink. 21 | pub fn symlink_target(dir_entry: &DirEntry) -> Option { 22 | dir_entry 23 | .path_is_symlink() 24 | .then(|| fs::read_link(dir_entry.path())) 25 | .transpose() 26 | .ok() 27 | .flatten() 28 | } 29 | -------------------------------------------------------------------------------- /src/fs/permissions/class.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display}; 2 | 3 | /// The set of permissions for a particular class i.e. user, group, or other. 4 | #[derive(Debug)] 5 | pub struct Permissions { 6 | class: Class, 7 | attr: Option, 8 | pub(super) triad: PermissionsTriad, 9 | } 10 | 11 | /// The class type that is associated with a permissions triad. 12 | #[derive(Debug)] 13 | pub enum Class { 14 | User, 15 | Group, 16 | Other, 17 | } 18 | 19 | /// Represents the special attributes that exist on the overall file corresponding to the setuid, 20 | /// setgid, and the sticky bit. 21 | #[derive(Debug, PartialEq, Eq)] 22 | #[allow(clippy::upper_case_acronyms)] 23 | pub enum Attribute { 24 | SUID, 25 | SGID, 26 | Sticky, 27 | } 28 | 29 | /// Read, write, execute permissions. 30 | #[derive(Debug, PartialEq, Eq)] 31 | pub enum PermissionsTriad { 32 | Read, 33 | Write, 34 | Execute, 35 | ReadWrite, 36 | ReadExecute, 37 | WriteExecute, 38 | ReadWriteExecute, 39 | None, 40 | } 41 | 42 | /// All `permissions_mask` arguments represents the bits of `st_mode` which excludes the file-type 43 | /// and the setuid, setgid, and sticky bit. 44 | impl Permissions { 45 | /// Computes user permissions. 46 | pub fn user_permissions_from(st_mode: u32) -> Self { 47 | let read = Self::enabled(st_mode, libc::S_IRUSR); 48 | let write = Self::enabled(st_mode, libc::S_IWUSR); 49 | let execute = Self::enabled(st_mode, libc::S_IXUSR); 50 | let suid = Self::enabled(st_mode, libc::S_ISUID).then_some(Attribute::SUID); 51 | 52 | Self::permissions_from_rwx(Class::User, read, write, execute, suid) 53 | } 54 | 55 | /// Computes group permissions. 56 | pub fn group_permissions_from(st_mode: u32) -> Self { 57 | let read = Self::enabled(st_mode, libc::S_IRGRP); 58 | let write = Self::enabled(st_mode, libc::S_IWGRP); 59 | let execute = Self::enabled(st_mode, libc::S_IXGRP); 60 | let sgid = Self::enabled(st_mode, libc::S_ISGID).then_some(Attribute::SGID); 61 | 62 | Self::permissions_from_rwx(Class::Group, read, write, execute, sgid) 63 | } 64 | 65 | /// Computes other permissions. 66 | pub fn other_permissions_from(st_mode: u32) -> Self { 67 | let read = Self::enabled(st_mode, libc::S_IROTH); 68 | let write = Self::enabled(st_mode, libc::S_IWOTH); 69 | let execute = Self::enabled(st_mode, libc::S_IXOTH); 70 | let sticky = Self::enabled(st_mode, libc::S_ISVTX).then_some(Attribute::Sticky); 71 | 72 | Self::permissions_from_rwx(Class::Other, read, write, execute, sticky) 73 | } 74 | 75 | /// Checks if a particular mode (read, write, or execute) is enabled. 76 | fn enabled(st_mode: u32, mask: N) -> bool 77 | where 78 | N: Copy + Into, 79 | { 80 | st_mode & mask.into() == mask.into() 81 | } 82 | 83 | /// Returns `true` if sticky bit is enabled. 84 | pub fn attr_is_sticky(&self) -> bool { 85 | self.attr 86 | .as_ref() 87 | .map_or(false, |attr| attr == &Attribute::Sticky) 88 | } 89 | 90 | /// Helper function to compute permissions. 91 | const fn permissions_from_rwx( 92 | class: Class, 93 | r: bool, 94 | w: bool, 95 | x: bool, 96 | attr: Option, 97 | ) -> Self { 98 | let triad = match (r, w, x) { 99 | (true, false, false) => PermissionsTriad::Read, 100 | (false, true, false) => PermissionsTriad::Write, 101 | (false, false, true) => PermissionsTriad::Execute, 102 | (true, true, false) => PermissionsTriad::ReadWrite, 103 | (true, false, true) => PermissionsTriad::ReadExecute, 104 | (false, true, true) => PermissionsTriad::WriteExecute, 105 | (true, true, true) => PermissionsTriad::ReadWriteExecute, 106 | (false, false, false) => PermissionsTriad::None, 107 | }; 108 | 109 | Self { class, attr, triad } 110 | } 111 | } 112 | 113 | /// The symbolic representation of a [`PermissionsTriad`]. 114 | impl Display for Permissions { 115 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 116 | match self.class { 117 | Class::Other if self.attr_is_sticky() => match self.triad { 118 | PermissionsTriad::Read => write!(f, "r-T"), 119 | PermissionsTriad::Write => write!(f, "-wT"), 120 | PermissionsTriad::Execute => write!(f, "--t"), 121 | PermissionsTriad::ReadWrite => write!(f, "rwT"), 122 | PermissionsTriad::ReadExecute => write!(f, "r-t"), 123 | PermissionsTriad::WriteExecute => write!(f, "-wt"), 124 | PermissionsTriad::ReadWriteExecute => write!(f, "rwt"), 125 | PermissionsTriad::None => write!(f, "--T"), 126 | }, 127 | 128 | _ if self.attr.is_some() => match self.triad { 129 | PermissionsTriad::Read => write!(f, "r-S"), 130 | PermissionsTriad::Write => write!(f, "-wS"), 131 | PermissionsTriad::Execute => write!(f, "--s"), 132 | PermissionsTriad::ReadWrite => write!(f, "rwS"), 133 | PermissionsTriad::ReadExecute => write!(f, "r-s"), 134 | PermissionsTriad::WriteExecute => write!(f, "-ws"), 135 | PermissionsTriad::ReadWriteExecute => write!(f, "rws"), 136 | PermissionsTriad::None => write!(f, "--S"), 137 | }, 138 | 139 | _ => match self.triad { 140 | PermissionsTriad::Read => write!(f, "r--"), 141 | PermissionsTriad::Write => write!(f, "-w-"), 142 | PermissionsTriad::Execute => write!(f, "--x"), 143 | PermissionsTriad::ReadWrite => write!(f, "rw-"), 144 | PermissionsTriad::ReadExecute => write!(f, "r-x"), 145 | PermissionsTriad::WriteExecute => write!(f, "-wx"), 146 | PermissionsTriad::ReadWriteExecute => write!(f, "rwx"), 147 | PermissionsTriad::None => write!(f, "---"), 148 | }, 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/fs/permissions/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum Error { 5 | #[error("Encountered an unknown file type.")] 6 | UnknownFileType, 7 | } 8 | -------------------------------------------------------------------------------- /src/fs/permissions/file_type.rs: -------------------------------------------------------------------------------- 1 | use super::error::Error; 2 | 3 | /// Unix file types. 4 | #[derive(Debug, PartialEq, Eq)] 5 | pub enum FileType { 6 | Directory, 7 | File, 8 | Symlink, 9 | Fifo, 10 | Socket, 11 | CharDevice, 12 | BlockDevice, 13 | } 14 | 15 | impl FileType { 16 | /// Unix file identifiers that you'd find in the `ls -l` command. 17 | pub const fn identifier(&self) -> char { 18 | match self { 19 | Self::Directory => 'd', 20 | Self::File => '.', 21 | Self::Symlink => 'l', 22 | Self::Fifo => 'p', 23 | Self::Socket => 's', 24 | Self::CharDevice => 'c', 25 | Self::BlockDevice => 'b', 26 | } 27 | } 28 | } 29 | 30 | /// The argument `mode` is meant to come from the `mode` method of [`std::fs::Permissions`]. 31 | impl TryFrom for FileType { 32 | type Error = Error; 33 | 34 | fn try_from(mode: u32) -> Result { 35 | let file_mask = mode & u32::from(libc::S_IFMT); 36 | 37 | if file_mask == u32::from(libc::S_IFIFO) { 38 | Ok(Self::Fifo) 39 | } else if file_mask == u32::from(libc::S_IFCHR) { 40 | Ok(Self::CharDevice) 41 | } else if file_mask == u32::from(libc::S_IFDIR) { 42 | Ok(Self::Directory) 43 | } else if file_mask == u32::from(libc::S_IFBLK) { 44 | Ok(Self::BlockDevice) 45 | } else if file_mask == u32::from(libc::S_IFREG) { 46 | Ok(Self::File) 47 | } else if file_mask == u32::from(libc::S_IFLNK) { 48 | Ok(Self::Symlink) 49 | } else if file_mask == u32::from(libc::S_IFSOCK) { 50 | Ok(Self::Socket) 51 | } else { 52 | Err(Error::UnknownFileType) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/fs/permissions/mod.rs: -------------------------------------------------------------------------------- 1 | use error::Error; 2 | use file_type::FileType; 3 | use std::{ 4 | convert::TryFrom, 5 | fmt::{self, Display, Octal}, 6 | os::unix::fs::PermissionsExt, 7 | }; 8 | 9 | /// For working with permissions for a particular class i.e. user, group, or other. 10 | pub mod class; 11 | 12 | /// File permission related errors. 13 | pub mod error; 14 | 15 | /// For working with Unix file identifiers. 16 | pub mod file_type; 17 | 18 | #[cfg(test)] 19 | mod test; 20 | 21 | impl SymbolicNotation for std::fs::Permissions {} 22 | 23 | /// Trait that is used to extend [`std::fs::Permissions`] behavior such that it allows for `mode` to 24 | /// be expressed in Unix's symbolic notation for file permissions. 25 | pub trait SymbolicNotation: PermissionsExt { 26 | /// Attempts to return a [`FileMode`] which implements [Display] allowing it to be presented in 27 | /// symbolic notation for file permissions. 28 | fn try_mode_symbolic_notation(&self) -> Result { 29 | let mode = self.mode(); 30 | FileMode::try_from(mode) 31 | } 32 | } 33 | 34 | /// A struct which holds information about the permissions of a particular file. [`FileMode`] 35 | /// implements [Display] which allows it to be conveniently presented in symbolic notation when 36 | /// expressing file permissions. 37 | pub struct FileMode { 38 | pub st_mode: u32, 39 | file_type: FileType, 40 | user_permissions: class::Permissions, 41 | group_permissions: class::Permissions, 42 | other_permissions: class::Permissions, 43 | } 44 | 45 | /// Implements [Display] which presents symbolic notation of file permissions with the extended 46 | /// attributes. 47 | pub struct FileModeXAttrs<'a>(pub &'a FileMode); 48 | 49 | impl FileMode { 50 | /// Constructor for [`FileMode`]. 51 | pub const fn new( 52 | st_mode: u32, 53 | file_type: FileType, 54 | user_permissions: class::Permissions, 55 | group_permissions: class::Permissions, 56 | other_permissions: class::Permissions, 57 | ) -> Self { 58 | Self { 59 | st_mode, 60 | file_type, 61 | user_permissions, 62 | group_permissions, 63 | other_permissions, 64 | } 65 | } 66 | 67 | /// Returns a reference to `file_type`. 68 | pub const fn file_type(&self) -> &FileType { 69 | &self.file_type 70 | } 71 | 72 | /// Returns a reference to a [`class::Permissions`] which represents the permissions of the user class. 73 | pub const fn user_permissions(&self) -> &class::Permissions { 74 | &self.user_permissions 75 | } 76 | 77 | /// Returns a reference to a [`class::Permissions`] which represents the permissions of the group class. 78 | pub const fn group_permissions(&self) -> &class::Permissions { 79 | &self.group_permissions 80 | } 81 | 82 | /// Returns a reference to a [`class::Permissions`] which represents the permissions of the other class. 83 | pub const fn other_permissions(&self) -> &class::Permissions { 84 | &self.other_permissions 85 | } 86 | } 87 | 88 | /// For representing [`FileMode`] in symbolic notation. 89 | impl Display for FileMode { 90 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 91 | let file_iden = self.file_type().identifier(); 92 | let user_permissions = self.user_permissions(); 93 | let group_permissions = self.group_permissions(); 94 | let other_permissions = self.other_permissions(); 95 | 96 | write!( 97 | f, 98 | "{file_iden}{user_permissions}{group_permissions}{other_permissions}" 99 | ) 100 | } 101 | } 102 | 103 | /// For representing file permissions with extended attributes in symbolic notation. 104 | impl Display for FileModeXAttrs<'_> { 105 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 106 | let mode = self.0; 107 | write!(f, "{mode}@") 108 | } 109 | } 110 | 111 | /// For the octal representation of permissions 112 | impl Octal for FileMode { 113 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 114 | let modes_mask = self.st_mode & !u32::from(libc::S_IFMT); 115 | fmt::Octal::fmt(&modes_mask, f) 116 | } 117 | } 118 | 119 | /// The argument `st_mode` is meant to come from the `mode` method of [`std::fs::Permissions`]. 120 | impl TryFrom for FileMode { 121 | type Error = Error; 122 | 123 | fn try_from(st_mode: u32) -> Result { 124 | let file_type = FileType::try_from(st_mode)?; 125 | let user_permissions = class::Permissions::user_permissions_from(st_mode); 126 | let group_permissions = class::Permissions::group_permissions_from(st_mode); 127 | let other_permissions = class::Permissions::other_permissions_from(st_mode); 128 | 129 | Ok(Self::new( 130 | st_mode, 131 | file_type, 132 | user_permissions, 133 | group_permissions, 134 | other_permissions, 135 | )) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/fs/permissions/test.rs: -------------------------------------------------------------------------------- 1 | use super::{class::PermissionsTriad, file_type::FileType, SymbolicNotation}; 2 | use std::{error::Error, fs::File, os::unix::fs::PermissionsExt}; 3 | 4 | #[test] 5 | fn test_symbolic_notation() -> Result<(), Box> { 6 | let temp = std::env::temp_dir().join("yogsothoth.hpl"); 7 | 8 | // File is created with read + write for user and read-only for all others. 9 | let file = File::create(temp)?; 10 | let metadata = file.metadata()?; 11 | 12 | let permissions = metadata.permissions(); 13 | 14 | let file_mode = permissions.try_mode_symbolic_notation()?; 15 | 16 | let file_type = file_mode.file_type(); 17 | let user = file_mode.user_permissions(); 18 | let group = file_mode.group_permissions(); 19 | let other = file_mode.other_permissions(); 20 | 21 | assert_eq!(file_type, &FileType::File); 22 | assert_eq!(&user.triad, &PermissionsTriad::ReadWrite); 23 | assert_eq!(&group.triad, &PermissionsTriad::Read); 24 | assert_eq!(&other.triad, &PermissionsTriad::Read); 25 | 26 | let rwx = format!("{file_mode}"); 27 | assert_eq!(rwx, ".rw-r--r--"); 28 | 29 | let octal = format!("{file_mode:o}"); 30 | assert_eq!(octal, "644"); 31 | 32 | Ok(()) 33 | } 34 | 35 | #[test] 36 | fn test_symbolic_notation_special_attr() -> Result<(), Box> { 37 | let temp = std::env::temp_dir().join("sub-niggurath.hpl"); 38 | 39 | // File is created with read + write for user and read-only for all others. 40 | let file = File::create(temp)?; 41 | 42 | let metadata = file.metadata()?; 43 | let mut permissions = metadata.permissions(); 44 | 45 | // Set the sticky bit 46 | permissions.set_mode(0o101_644); 47 | 48 | let file_mode = permissions.try_mode_symbolic_notation()?; 49 | let rwx = format!("{file_mode}"); 50 | assert_eq!(rwx, ".rw-r--r-T"); 51 | 52 | let octal = format!("{file_mode:o}"); 53 | assert_eq!(octal, "1644"); 54 | 55 | // Set the getuid bit 56 | permissions.set_mode(0o102_644); 57 | 58 | let file_mode = permissions.try_mode_symbolic_notation()?; 59 | let rwx = format!("{file_mode}"); 60 | assert_eq!(rwx, ".rw-r-Sr--"); 61 | 62 | let octal = format!("{file_mode:o}"); 63 | assert_eq!(octal, "2644"); 64 | 65 | // Set the setuid bit 66 | permissions.set_mode(0o104_644); 67 | 68 | let file_mode = permissions.try_mode_symbolic_notation()?; 69 | let rwx = format!("{file_mode}"); 70 | assert_eq!(rwx, ".rwSr--r--"); 71 | 72 | let octal = format!("{file_mode:o}"); 73 | assert_eq!(octal, "4644"); 74 | 75 | // Set the all the attr bits 76 | permissions.set_mode(0o107_644); 77 | 78 | let file_mode = permissions.try_mode_symbolic_notation()?; 79 | let rwx = format!("{file_mode}"); 80 | assert_eq!(rwx, ".rwSr-Sr-T"); 81 | 82 | let octal = format!("{file_mode:o}"); 83 | assert_eq!(octal, "7644"); 84 | 85 | // Set all the attr bits and give all classes execute permissions 86 | permissions.set_mode(0o107_777); 87 | 88 | let file_mode = permissions.try_mode_symbolic_notation()?; 89 | let rwx = format!("{file_mode}"); 90 | assert_eq!(rwx, ".rwsrwsrwt"); 91 | 92 | let octal = format!("{file_mode:o}"); 93 | assert_eq!(octal, "7777"); 94 | 95 | Ok(()) 96 | } 97 | -------------------------------------------------------------------------------- /src/fs/ug.rs: -------------------------------------------------------------------------------- 1 | use errno::{errno, set_errno, Errno}; 2 | use std::{ffi::CStr, fs::Metadata, os::unix::fs::MetadataExt}; 3 | 4 | type Owner = String; 5 | type Group = String; 6 | 7 | impl UserGroupInfo for Metadata {} 8 | 9 | /// Trait that allows for files to query their owner and group. 10 | pub trait UserGroupInfo: MetadataExt { 11 | /// Attemps to query the owner of the implementor. 12 | fn try_get_owner(&self) -> Result { 13 | unsafe { 14 | let uid = self.uid(); 15 | try_get_user(uid) 16 | } 17 | } 18 | 19 | /// Attempts to query both the owner and group of the implementor. 20 | fn try_get_owner_and_group(&self) -> Result<(Owner, Group), Error> { 21 | unsafe { 22 | let uid = self.uid(); 23 | let gid = self.gid(); 24 | let user = try_get_user(uid)?; 25 | let group = try_get_group(gid)?; 26 | 27 | Ok((user, group)) 28 | } 29 | } 30 | } 31 | 32 | #[derive(Debug, thiserror::Error)] 33 | pub enum Error { 34 | #[error("libc error")] 35 | LibC(Errno), 36 | 37 | #[error("Invalid user")] 38 | InvalidUser, 39 | 40 | #[error("Invalid group")] 41 | InvalidGroup, 42 | } 43 | 44 | /// Attempts to return the name of the group associated with `gid`. 45 | unsafe fn try_get_group(gid: libc::gid_t) -> Result { 46 | set_errno(Errno(0)); 47 | 48 | let group = libc::getgrgid(gid); 49 | 50 | let errno = errno(); 51 | 52 | if errno.0 != 0 { 53 | return Err(Error::LibC(errno)); 54 | } 55 | 56 | if group.is_null() { 57 | return Err(Error::InvalidGroup); 58 | } 59 | 60 | let libc::group { gr_name, .. } = *group; 61 | 62 | Ok(CStr::from_ptr(gr_name).to_string_lossy().to_string()) 63 | } 64 | 65 | /// Attempts to return the name of the user associated with `uid`. 66 | unsafe fn try_get_user(uid: libc::uid_t) -> Result { 67 | set_errno(Errno(0)); 68 | 69 | let pwd = libc::getpwuid(uid); 70 | 71 | let errno = errno(); 72 | 73 | if errno.0 != 0 { 74 | return Err(Error::LibC(errno)); 75 | } 76 | 77 | if pwd.is_null() { 78 | return Err(Error::InvalidUser); 79 | } 80 | 81 | let libc::passwd { pw_name, .. } = *pwd; 82 | 83 | Ok(CStr::from_ptr(pw_name).to_string_lossy().to_string()) 84 | } 85 | -------------------------------------------------------------------------------- /src/fs/xattr.rs: -------------------------------------------------------------------------------- 1 | use ignore::DirEntry; 2 | use std::{os::unix::ffi::OsStrExt, path::Path, ptr}; 3 | 4 | /// Allow extended attributes to be queried directly from the directory entry. 5 | impl ExtendedAttr for DirEntry { 6 | fn path(&self) -> &Path { 7 | self.path() 8 | } 9 | } 10 | 11 | /// Simple trait that allows files to query extended attributes if it exists. 12 | pub trait ExtendedAttr { 13 | fn path(&self) -> &Path; 14 | 15 | /// Queries the filesystem to check if there exists extended attributes for the implementor's 16 | /// path. 17 | fn has_xattrs(&self) -> bool { 18 | unsafe { has_xattrs(self.path()) } 19 | } 20 | } 21 | 22 | /// Checks to see if a directory entry referred to by `path` has extended attributes. If the file 23 | /// at the provided `path` is symlink the file it points to is interrogated. 24 | unsafe fn has_xattrs(path: &Path) -> bool { 25 | use libc::{c_char, listxattr}; 26 | 27 | let path_ptr = { 28 | let slice = path.as_os_str().as_bytes(); 29 | let slice_ptr = slice.as_ptr(); 30 | slice_ptr.cast::() 31 | }; 32 | 33 | #[cfg(not(target_os = "macos"))] 34 | return 0 < listxattr(path_ptr, ptr::null_mut::(), 0); 35 | 36 | #[cfg(target_os = "macos")] 37 | return 0 < listxattr(path_ptr, ptr::null_mut::(), 0, 0); 38 | } 39 | -------------------------------------------------------------------------------- /src/icons/fs.rs: -------------------------------------------------------------------------------- 1 | use ansi_term::{ANSIGenericString, Style}; 2 | use ignore::DirEntry; 3 | use std::{borrow::Cow, path::Path}; 4 | 5 | /// Computes a plain, colorless icon with given parameters. 6 | /// 7 | /// The precedent from highest to lowest in terms of which parameters determine the icon used 8 | /// is as followed: file-type, file-extension, and then file-name. If an icon cannot be 9 | /// computed the fall-back default icon is used. 10 | /// 11 | /// If a directory entry is a link and the link target is provided, the link target will be 12 | /// used to determine the icon. 13 | pub fn compute(entry: &DirEntry, link_target: Option<&Path>) -> Cow<'static, str> { 14 | let icon = entry 15 | .file_type() 16 | .and_then(super::icon_from_file_type) 17 | .map(Cow::from); 18 | 19 | if let Some(i) = icon { 20 | return i; 21 | } 22 | 23 | let ext = match link_target { 24 | Some(target) if entry.path_is_symlink() => target.extension(), 25 | _ => entry.path().extension(), 26 | }; 27 | 28 | let icon = ext 29 | .and_then(super::icon_from_ext) 30 | .map(|(_, i)| Cow::from(i)); 31 | 32 | if let Some(i) = icon { 33 | return i; 34 | } 35 | 36 | let icon = super::icon_from_file_name(entry.file_name()).map(Cow::from); 37 | 38 | if let Some(i) = icon { 39 | return i; 40 | } 41 | 42 | Cow::from(super::get_default_icon().1) 43 | } 44 | 45 | /// Computes a plain, colored icon with given parameters. See [compute] for more details. 46 | pub fn compute_with_color( 47 | entry: &DirEntry, 48 | link_target: Option<&Path>, 49 | style: Option