├── .github └── workflows │ └── CICD.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md └── src ├── bin.rs ├── fs.rs ├── lib.rs ├── style.rs └── suffix.rs /.github/workflows/CICD.yml: -------------------------------------------------------------------------------- 1 | name: CICD 2 | 3 | env: 4 | CICD_INTERMEDIATES_DIR: "_cicd-intermediates" 5 | MSRV_FEATURES: "" 6 | 7 | on: 8 | workflow_dispatch: 9 | pull_request: 10 | push: 11 | branches: 12 | - master 13 | tags: 14 | - '*' 15 | 16 | jobs: 17 | crate_metadata: 18 | name: Extract crate metadata 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Extract crate information 23 | id: crate_metadata 24 | run: | 25 | cargo metadata --no-deps --format-version 1 | jq -r '"name=" + .packages[0].name' | tee -a $GITHUB_OUTPUT 26 | cargo metadata --no-deps --format-version 1 | jq -r '"version=" + .packages[0].version' | tee -a $GITHUB_OUTPUT 27 | cargo metadata --no-deps --format-version 1 | jq -r '"maintainer=" + .packages[0].authors[0]' | tee -a $GITHUB_OUTPUT 28 | cargo metadata --no-deps --format-version 1 | jq -r '"homepage=" + .packages[0].homepage' | tee -a $GITHUB_OUTPUT 29 | cargo metadata --no-deps --format-version 1 | jq -r '"msrv=" + .packages[0].rust_version' | tee -a $GITHUB_OUTPUT 30 | outputs: 31 | name: ${{ steps.crate_metadata.outputs.name }} 32 | version: ${{ steps.crate_metadata.outputs.version }} 33 | maintainer: ${{ steps.crate_metadata.outputs.maintainer }} 34 | homepage: ${{ steps.crate_metadata.outputs.homepage }} 35 | msrv: ${{ steps.crate_metadata.outputs.msrv }} 36 | 37 | ensure_cargo_fmt: 38 | name: Ensure 'cargo fmt' has been run 39 | runs-on: ubuntu-20.04 40 | steps: 41 | - uses: dtolnay/rust-toolchain@stable 42 | with: 43 | components: rustfmt 44 | - uses: actions/checkout@v4 45 | - run: cargo fmt -- --check 46 | 47 | min_version: 48 | name: Minimum supported rust version 49 | runs-on: ubuntu-20.04 50 | needs: crate_metadata 51 | steps: 52 | - name: Checkout source code 53 | uses: actions/checkout@v4 54 | 55 | - name: Install rust toolchain (v${{ needs.crate_metadata.outputs.msrv }}) 56 | uses: dtolnay/rust-toolchain@master 57 | with: 58 | toolchain: ${{ needs.crate_metadata.outputs.msrv }} 59 | components: clippy 60 | - name: Run clippy (on minimum supported rust version to prevent warnings we can't fix) 61 | run: | 62 | cargo clippy --all-targets --features=gnu_legacy 63 | cargo clippy --all-targets --features=crossterm,ansi_term,nu-ansi-term 64 | - name: Run tests 65 | run: | 66 | cargo test --features=gnu_legacy 67 | cargo test --features=crossterm,ansi_term,nu-ansi-term 68 | 69 | documentation: 70 | name: Documentation 71 | runs-on: ubuntu-20.04 72 | steps: 73 | - name: Git checkout 74 | uses: actions/checkout@v2 75 | - name: Install Rust toolchain 76 | uses: dtolnay/rust-toolchain@master 77 | with: 78 | toolchain: stable 79 | - name: Check documentation 80 | env: 81 | RUSTDOCFLAGS: -D warnings 82 | run: | 83 | cargo doc --no-deps --document-private-items --features=gnu_legacy 84 | cargo doc --no-deps --document-private-items --features=crossterm,ansi_term,nu-ansi-term 85 | 86 | build: 87 | name: ${{ matrix.job.target }} (${{ matrix.job.os }} with ${{ matrix.terminal }}) 88 | runs-on: ${{ matrix.job.os }} 89 | needs: crate_metadata 90 | strategy: 91 | fail-fast: false 92 | matrix: 93 | job: 94 | - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } 95 | - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } 96 | - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } 97 | - { target: i686-pc-windows-msvc , os: windows-2019 } 98 | - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } 99 | - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } 100 | - { target: x86_64-apple-darwin , os: macos-12 } 101 | - { target: x86_64-pc-windows-gnu , os: windows-2019 } 102 | - { target: x86_64-pc-windows-msvc , os: windows-2019 } 103 | - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } 104 | - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } 105 | terminal: 106 | - ansi_term 107 | - crossterm 108 | - nu-ansi-term 109 | - gnu_legacy 110 | env: 111 | BUILD_CMD: cargo 112 | steps: 113 | - name: Checkout source code 114 | uses: actions/checkout@v4 115 | 116 | - name: Install prerequisites 117 | shell: bash 118 | run: | 119 | case ${{ matrix.job.target }} in 120 | arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; 121 | aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; 122 | esac 123 | 124 | - name: Install Rust toolchain 125 | uses: dtolnay/rust-toolchain@stable 126 | with: 127 | targets: ${{ matrix.job.target }} 128 | 129 | - name: Install cross 130 | if: matrix.job.use-cross 131 | uses: taiki-e/install-action@v2 132 | with: 133 | tool: cross 134 | 135 | - name: Overwrite build command env variable 136 | if: matrix.job.use-cross 137 | shell: bash 138 | run: echo "BUILD_CMD=cross" >> $GITHUB_ENV 139 | 140 | - name: Show version information (Rust, cargo, GCC) 141 | shell: bash 142 | run: | 143 | gcc --version || true 144 | rustup -V 145 | rustup toolchain list 146 | rustup default 147 | cargo -V 148 | rustc -V 149 | 150 | - name: Build 151 | shell: bash 152 | run: $BUILD_CMD build --release --target=${{ matrix.job.target }} --features=${{ matrix.terminal }} 153 | 154 | - name: Set binary name & path 155 | id: bin 156 | shell: bash 157 | run: | 158 | # Figure out suffix of binary 159 | EXE_suffix="" 160 | case ${{ matrix.job.target }} in 161 | *-pc-windows-*) EXE_suffix=".exe" ;; 162 | esac; 163 | 164 | # Setup paths 165 | BIN_NAME="${{ needs.crate_metadata.outputs.name }}${EXE_suffix}" 166 | BIN_PATH="target/${{ matrix.job.target }}/release/${BIN_NAME}" 167 | 168 | # Let subsequent steps know where to find the binary 169 | echo "BIN_PATH=${BIN_PATH}" >> $GITHUB_OUTPUT 170 | echo "BIN_NAME=${BIN_NAME}" >> $GITHUB_OUTPUT 171 | 172 | - name: Run tests for all other targets 173 | shell: bash 174 | if: ${{ !startsWith(matrix.job.target, 'a') }} 175 | run: $BUILD_CMD test --target=${{ matrix.job.target }} --features=${{ matrix.terminal }} --lib --bin ${{ needs.crate_metadata.outputs.name }} 176 | 177 | - name: Run tests for arm and aarch64 178 | shell: bash 179 | if: startsWith(matrix.job.target, 'a') 180 | run: $BUILD_CMD test --target=${{ matrix.job.target }} --features=${{ matrix.terminal }} --lib --bin ${{ needs.crate_metadata.outputs.name }} 181 | 182 | - name: Run lscolors 183 | shell: bash 184 | run: $BUILD_CMD run --target=${{ matrix.job.target }} --features ${{ matrix.terminal }} -- Cargo.toml Cargo.lock LICENSE-APACHE LICENSE-MIT README.md src/lib.rs 185 | 186 | - name: "Feature check: ${{ matrix.terminal }}" 187 | shell: bash 188 | run: $BUILD_CMD check --target=${{ matrix.job.target }} --verbose --lib --features ${{ matrix.terminal }} 189 | 190 | - name: Create tarball 191 | id: package 192 | shell: bash 193 | run: | 194 | PKG_suffix=".tar.gz" ; case ${{ matrix.job.target }} in *-pc-windows-*) PKG_suffix=".zip" ;; esac; 195 | PKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-v${{ needs.crate_metadata.outputs.version }}-${{ matrix.job.target }} 196 | PKG_NAME=${PKG_BASENAME}${PKG_suffix} 197 | echo "PKG_NAME=${PKG_NAME}" >> $GITHUB_OUTPUT 198 | 199 | PKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/package" 200 | ARCHIVE_DIR="${PKG_STAGING}/${PKG_BASENAME}/" 201 | mkdir -p "${ARCHIVE_DIR}" 202 | 203 | # Binary 204 | cp "${{ steps.bin.outputs.BIN_PATH }}" "$ARCHIVE_DIR" 205 | 206 | # README, LICENSE files 207 | cp "README.md" "LICENSE-MIT" "LICENSE-APACHE" "$ARCHIVE_DIR" 208 | 209 | # base compressed package 210 | pushd "${PKG_STAGING}/" >/dev/null 211 | case ${{ matrix.job.target }} in 212 | *-pc-windows-*) 7z -y a "${PKG_NAME}" "${PKG_BASENAME}"/* | tail -2 ;; 213 | *) tar czf "${PKG_NAME}" "${PKG_BASENAME}"/* ;; 214 | esac; 215 | popd >/dev/null 216 | 217 | # Let subsequent steps know where to find the compressed package 218 | echo "PKG_PATH=${PKG_STAGING}/${PKG_NAME}" >> $GITHUB_OUTPUT 219 | 220 | - name: Create Debian package 221 | id: debian-package 222 | shell: bash 223 | if: startsWith(matrix.job.os, 'ubuntu') 224 | run: | 225 | COPYRIGHT_YEARS="2018 - "$(date "+%Y") 226 | DPKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/debian-package" 227 | DPKG_DIR="${DPKG_STAGING}/dpkg" 228 | mkdir -p "${DPKG_DIR}" 229 | 230 | DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }} 231 | DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }}-musl 232 | case ${{ matrix.job.target }} in *-musl*) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac; 233 | DPKG_VERSION=${{ needs.crate_metadata.outputs.version }} 234 | 235 | unset DPKG_ARCH 236 | case ${{ matrix.job.target }} in 237 | aarch64-*-linux-*) DPKG_ARCH=arm64 ;; 238 | arm-*-linux-*hf) DPKG_ARCH=armhf ;; 239 | i686-*-linux-*) DPKG_ARCH=i686 ;; 240 | x86_64-*-linux-*) DPKG_ARCH=amd64 ;; 241 | *) DPKG_ARCH=notset ;; 242 | esac; 243 | 244 | DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb" 245 | echo "DPKG_NAME=${DPKG_NAME}" >> $GITHUB_OUTPUT 246 | 247 | # Binary 248 | install -Dm755 "${{ steps.bin.outputs.BIN_PATH }}" "${DPKG_DIR}/usr/bin/${{ steps.bin.outputs.BIN_NAME }}" 249 | 250 | # README and LICENSE 251 | install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md" 252 | install -Dm644 "LICENSE-MIT" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE-MIT" 253 | install -Dm644 "LICENSE-APACHE" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE-APACHE" 254 | 255 | cat > "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright" < "${DPKG_DIR}/DEBIAN/control" <> $GITHUB_OUTPUT 313 | 314 | # build dpkg 315 | fakeroot dpkg-deb --build "${DPKG_DIR}" "${DPKG_PATH}" 316 | 317 | - name: "Artifact upload: tarball" 318 | uses: actions/upload-artifact@master 319 | if: matrix.terminal == 'ansi_term' 320 | with: 321 | name: ${{ steps.package.outputs.PKG_NAME }} 322 | path: ${{ steps.package.outputs.PKG_PATH }} 323 | 324 | - name: "Artifact upload: Debian package" 325 | uses: actions/upload-artifact@master 326 | if: steps.debian-package.outputs.DPKG_NAME && matrix.terminal == 'ansi_term' 327 | with: 328 | name: ${{ steps.debian-package.outputs.DPKG_NAME }} 329 | path: ${{ steps.debian-package.outputs.DPKG_PATH }} 330 | 331 | - name: Check for release 332 | id: is-release 333 | shell: bash 334 | run: | 335 | unset IS_RELEASE ; if [[ $GITHUB_REF =~ ^refs/tags/v[0-9].* ]]; then IS_RELEASE='true' ; fi 336 | echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT 337 | 338 | - name: Publish archives and packages 339 | uses: softprops/action-gh-release@v1 340 | if: steps.is-release.outputs.IS_RELEASE 341 | with: 342 | files: | 343 | ${{ steps.package.outputs.PKG_PATH }} 344 | ${{ steps.debian-package.outputs.DPKG_PATH }} 345 | env: 346 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 347 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lscolors" 3 | description = "Colorize paths using the LS_COLORS environment variable" 4 | categories = ["command-line-interface"] 5 | homepage = "https://github.com/sharkdp/lscolors" 6 | repository = "https://github.com/sharkdp/lscolors" 7 | keywords = [ 8 | "cli", 9 | "linux", 10 | "terminal", 11 | "filesystem", 12 | "color", 13 | ] 14 | license = "MIT/Apache-2.0" 15 | version = "0.20.0" 16 | readme = "README.md" 17 | edition = "2021" 18 | authors = ["David Peter "] 19 | rust-version = "1.70.0" 20 | 21 | [features] 22 | default = ["nu-ansi-term"] 23 | gnu_legacy = ["nu-ansi-term/gnu_legacy"] 24 | 25 | [dependencies] 26 | ansi_term = { version = "0.12", optional = true } 27 | nu-ansi-term = { version = "0.50", optional = true } 28 | crossterm = { version = "0.28", optional = true } 29 | owo-colors = { version = "4.0", optional = true } 30 | aho-corasick = "1.1.3" 31 | 32 | [dev-dependencies] 33 | tempfile = "^3" 34 | 35 | [[bin]] 36 | name = "lscolors" 37 | path = "src/bin.rs" 38 | 39 | [profile.release] 40 | lto = true 41 | strip = true 42 | codegen-units = 1 43 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lscolors 2 | 3 | [![CICD](https://github.com/sharkdp/lscolors/actions/workflows/CICD.yml/badge.svg)](https://github.com/sharkdp/lscolors/actions/workflows/CICD.yml) 4 | [![Crates.io](https://img.shields.io/crates/v/lscolors.svg)](https://crates.io/crates/lscolors) 5 | [![Documentation](https://docs.rs/lscolors/badge.svg)](https://docs.rs/lscolors) 6 | 7 | A cross-platform library for colorizing paths according to the `LS_COLORS` environment variable (like `ls`). 8 | 9 | ## Usage 10 | 11 | 12 | 13 | ```rust 14 | use lscolors::{LsColors, Style}; 15 | 16 | let lscolors = LsColors::from_env().unwrap_or_default(); 17 | 18 | let path = "some/folder/test.tar.gz"; 19 | let style = lscolors.style_for_path(path); 20 | 21 | // If you want to use `ansi_term`: 22 | let ansi_style = style.map(Style::to_ansi_term_style) 23 | .unwrap_or_default(); 24 | println!("{}", ansi_style.paint(path)); 25 | 26 | // If you want to use `nu-ansi-term` (fork of ansi_term) or `gnu_legacy`: 27 | let nu_ansi_style = style.map(Style::to_nu_ansi_term_style) 28 | .unwrap_or_default(); 29 | println!("{}", nu_ansi_style.paint(path)); 30 | 31 | // If you want to use `crossterm`: 32 | let crossterm_style = style.map(Style::to_crossterm_style) 33 | .unwrap_or_default(); 34 | println!("{}", crossterm_style.apply(path)); 35 | ``` 36 | 37 | ## Command-line application 38 | 39 | This crate also comes with a small command-line program `lscolors` that 40 | can be used to colorize the output of other commands: 41 | 42 | ```bash 43 | > find . -maxdepth 2 | lscolors 44 | 45 | > rg foo -l | lscolors 46 | ``` 47 | 48 | You can install it by running `cargo install lscolors` or by downloading one 49 | of the prebuilt binaries from the [release page](https://github.com/sharkdp/lscolors/releases). 50 | If you want to build the application from source, you can run 51 | 52 | ```rs 53 | cargo build --release --features=nu-ansi-term --locked 54 | ``` 55 | 56 | ## Features 57 | 58 | ```rust 59 | // Cargo.toml 60 | 61 | [dependencies] 62 | // use ansi-term coloring 63 | lscolors = { version = "v0.14.0", features = ["ansi_term"] } 64 | // use crossterm coloring 65 | lscolors = { version = "v0.14.0", features = ["crossterm"] } 66 | // use nu-ansi-term coloring 67 | lscolors = { version = "v0.14.0", features = ["nu-ansi-term"] } 68 | // use nu-ansi-term coloring in gnu legacy mode with double digit styles 69 | lscolors = { version = "v0.14.0", features = ["gnu_legacy"] } 70 | ``` 71 | 72 | ## License 73 | 74 | Licensed under either of 75 | 76 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 77 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 78 | 79 | at your option. 80 | 81 | ## References 82 | 83 | Information about the `LS_COLORS` environment variable is sparse. Here is a short list of useful references: 84 | 85 | * [`LS_COLORS` implementation in the GNU coreutils version of `ls`](https://github.com/coreutils/coreutils/blob/17983b2cb3bccbb4fa69691178caddd99269bda9/src/ls.c#L2507-L2647) (the reference implementation) 86 | * [`LS_COLORS` implementation in `bfs`](https://github.com/tavianator/bfs/blob/2.6/src/color.c#L556) by [**@tavianator**](https://github.com/tavianator) 87 | * [The `DIR_COLORS(5)` man page](https://linux.die.net/man/5/dir_colors) 88 | -------------------------------------------------------------------------------- /src/bin.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::io; 3 | use std::io::prelude::*; 4 | use std::path::Path; 5 | 6 | use lscolors::{LsColors, Style}; 7 | 8 | #[cfg(all( 9 | not(feature = "nu-ansi-term"), 10 | not(feature = "gnu_legacy"), 11 | not(feature = "ansi_term"), 12 | not(feature = "crossterm"), 13 | not(feature = "owo-colors") 14 | ))] 15 | compile_error!( 16 | "one feature must be enabled: ansi_term, nu-ansi-term, crossterm, gnu_legacy, owo-colors" 17 | ); 18 | 19 | fn print_path(handle: &mut dyn Write, ls_colors: &LsColors, path: &str) -> io::Result<()> { 20 | for (component, style) in ls_colors.style_for_path_components(Path::new(path)) { 21 | #[cfg(any(feature = "nu-ansi-term", feature = "gnu_legacy"))] 22 | { 23 | let ansi_style = style.map(Style::to_nu_ansi_term_style).unwrap_or_default(); 24 | write!(handle, "{}", ansi_style.paint(component.to_string_lossy()))?; 25 | } 26 | 27 | #[cfg(feature = "ansi_term")] 28 | { 29 | let ansi_style = style.map(Style::to_ansi_term_style).unwrap_or_default(); 30 | write!(handle, "{}", ansi_style.paint(component.to_string_lossy()))?; 31 | } 32 | 33 | #[cfg(feature = "crossterm")] 34 | { 35 | let ansi_style = style.map(Style::to_crossterm_style).unwrap_or_default(); 36 | write!(handle, "{}", ansi_style.apply(component.to_string_lossy()))?; 37 | } 38 | #[cfg(feature = "owo-colors")] 39 | { 40 | use owo_colors::OwoColorize; 41 | let ansi_style = style.map(Style::to_owo_colors_style).unwrap_or_default(); 42 | write!(handle, "{}", component.to_string_lossy().style(ansi_style))?; 43 | } 44 | } 45 | writeln!(handle)?; 46 | 47 | Ok(()) 48 | } 49 | 50 | fn run() -> io::Result<()> { 51 | let ls_colors = LsColors::from_env().unwrap_or_default(); 52 | 53 | let stdout = io::stdout(); 54 | let mut stdout = stdout.lock(); 55 | 56 | let mut args = env::args(); 57 | 58 | if args.len() >= 2 { 59 | // Skip program name 60 | args.next(); 61 | 62 | for arg in args { 63 | print_path(&mut stdout, &ls_colors, &arg)?; 64 | } 65 | } else { 66 | let stdin = io::stdin(); 67 | let mut buf = vec![]; 68 | 69 | while let Ok(size) = stdin.lock().read_until(b'\n', &mut buf) { 70 | if size == 0 { 71 | break; 72 | } 73 | 74 | let path_str = String::from_utf8_lossy(&buf[..(buf.len() - 1)]); 75 | #[cfg(windows)] 76 | let path_str = path_str.trim_end_matches('\r'); 77 | print_path(&mut stdout, &ls_colors, path_str.as_ref())?; 78 | 79 | buf.clear(); 80 | } 81 | } 82 | 83 | Ok(()) 84 | } 85 | 86 | fn main() { 87 | run().ok(); 88 | } 89 | -------------------------------------------------------------------------------- /src/fs.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | #[cfg(any(unix, target_os = "redox"))] 4 | use std::os::unix::fs::MetadataExt; 5 | 6 | /// Get the UNIX-style mode bits from some metadata if available, otherwise 0. 7 | #[allow(unused_variables)] 8 | pub fn mode(md: &fs::Metadata) -> u32 { 9 | #[cfg(any(unix, target_os = "redox"))] 10 | return md.mode(); 11 | 12 | #[cfg(not(any(unix, target_os = "redox")))] 13 | return 0; 14 | } 15 | 16 | /// Get the number of hard links to a file, or 1 if unknown. 17 | #[allow(unused_variables)] 18 | pub fn nlink(md: &fs::Metadata) -> u64 { 19 | #[cfg(any(unix, target_os = "redox"))] 20 | return md.nlink(); 21 | 22 | #[cfg(not(any(unix, target_os = "redox")))] 23 | return 1; 24 | } 25 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A library for colorizing paths according to the `LS_COLORS` environment variable. 2 | //! 3 | //! # Example 4 | //! ``` 5 | //! use lscolors::{LsColors, Style}; 6 | //! 7 | //! let lscolors = LsColors::from_env().unwrap_or_default(); 8 | //! 9 | //! let path = "some/folder/archive.zip"; 10 | //! let style = lscolors.style_for_path(path); 11 | //! 12 | //! // If you want to use `nu_ansi_term`: 13 | //! # #[cfg(features = "nu-ansi-term")] 14 | //! # { 15 | //! let ansi_style = style.map(Style::to_nu_ansi_term_style).unwrap_or_default(); 16 | //! println!("{}", ansi_style.paint(path)); 17 | //! # } 18 | //! 19 | //! // If you want to use `ansi_term`: 20 | //! # #[cfg(features = "ansi_term")] 21 | //! # { 22 | //! let ansi_style = style.map(Style::to_ansi_term_style).unwrap_or_default(); 23 | //! println!("{}", ansi_style.paint(path)); 24 | //! # } 25 | //! ``` 26 | 27 | mod fs; 28 | pub mod style; 29 | mod suffix; 30 | 31 | use std::collections::HashMap; 32 | use std::env; 33 | use std::ffi::OsString; 34 | use std::fs::{DirEntry, FileType, Metadata}; 35 | use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR}; 36 | 37 | use crate::suffix::{SuffixMap, SuffixMapBuilder}; 38 | 39 | pub use crate::style::{Color, FontStyle, Style}; 40 | 41 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 42 | pub enum Indicator { 43 | /// `no`: Normal (non-filename) text 44 | Normal, 45 | 46 | /// `fi`: Regular file 47 | RegularFile, 48 | 49 | /// `di`: Directory 50 | Directory, 51 | 52 | /// `ln`: Symbolic link 53 | SymbolicLink, 54 | 55 | /// `pi`: Named pipe or FIFO 56 | FIFO, 57 | 58 | /// `so`: Socket 59 | Socket, 60 | 61 | /// `do`: Door (IPC connection to another program) 62 | Door, 63 | 64 | /// `bd`: Block-oriented device 65 | BlockDevice, 66 | 67 | /// `cd`: Character-oriented device 68 | CharacterDevice, 69 | 70 | /// `or`: A broken symbolic link 71 | OrphanedSymbolicLink, 72 | 73 | /// `su`: A file that is setuid (`u+s`) 74 | Setuid, 75 | 76 | /// `sg`: A file that is setgid (`g+s`) 77 | Setgid, 78 | 79 | /// `st`: A directory that is sticky and other-writable (`+t`, `o+w`) 80 | Sticky, 81 | 82 | /// `ow`: A directory that is not sticky and other-writeable (`o+w`) 83 | OtherWritable, 84 | 85 | /// `tw`: A directory that is sticky and other-writable (`+t`, `o+w`) 86 | StickyAndOtherWritable, 87 | 88 | /// `ex`: Executable file 89 | ExecutableFile, 90 | 91 | /// `mi`: Missing file 92 | MissingFile, 93 | 94 | /// `ca`: File with capabilities set 95 | Capabilities, 96 | 97 | /// `mh`: File with multiple hard links 98 | MultipleHardLinks, 99 | 100 | /// `lc`: Code that is printed before the color sequence 101 | LeftCode, 102 | 103 | /// `rc`: Code that is printed after the color sequence 104 | RightCode, 105 | 106 | /// `ec`: End code 107 | EndCode, 108 | 109 | /// `rs`: Code to reset to ordinary colors 110 | Reset, 111 | 112 | /// `cl`: Code to clear to the end of the line 113 | ClearLine, 114 | } 115 | 116 | impl Indicator { 117 | pub fn from(indicator: &str) -> Option { 118 | match indicator { 119 | "no" => Some(Indicator::Normal), 120 | "fi" => Some(Indicator::RegularFile), 121 | "di" => Some(Indicator::Directory), 122 | "ln" => Some(Indicator::SymbolicLink), 123 | "pi" => Some(Indicator::FIFO), 124 | "so" => Some(Indicator::Socket), 125 | "do" => Some(Indicator::Door), 126 | "bd" => Some(Indicator::BlockDevice), 127 | "cd" => Some(Indicator::CharacterDevice), 128 | "or" => Some(Indicator::OrphanedSymbolicLink), 129 | "su" => Some(Indicator::Setuid), 130 | "sg" => Some(Indicator::Setgid), 131 | "st" => Some(Indicator::Sticky), 132 | "ow" => Some(Indicator::OtherWritable), 133 | "tw" => Some(Indicator::StickyAndOtherWritable), 134 | "ex" => Some(Indicator::ExecutableFile), 135 | "mi" => Some(Indicator::MissingFile), 136 | "ca" => Some(Indicator::Capabilities), 137 | "mh" => Some(Indicator::MultipleHardLinks), 138 | "lc" => Some(Indicator::LeftCode), 139 | "rc" => Some(Indicator::RightCode), 140 | "ec" => Some(Indicator::EndCode), 141 | "rs" => Some(Indicator::Reset), 142 | "cl" => Some(Indicator::ClearLine), 143 | _ => None, 144 | } 145 | } 146 | } 147 | 148 | /// Iterator over the path components with their respective style. 149 | pub struct StyledComponents<'a> { 150 | /// Reference to the underlying LsColors object 151 | lscolors: &'a LsColors, 152 | 153 | /// Full path to the current component 154 | component_path: PathBuf, 155 | 156 | /// Underlying iterator over the path components 157 | components: std::iter::Peekable>, 158 | } 159 | 160 | impl<'a> Iterator for StyledComponents<'a> { 161 | type Item = (OsString, Option<&'a Style>); 162 | 163 | fn next(&mut self) -> Option { 164 | if let Some(component) = self.components.next() { 165 | let mut component_str = component.as_os_str().to_os_string(); 166 | 167 | self.component_path.push(&component_str); 168 | let style = self.lscolors.style_for_path(&self.component_path); 169 | 170 | if self.components.peek().is_some() { 171 | match component { 172 | // Prefix needs no separator, as it is always followed by RootDir. 173 | // RootDir is already a separator. 174 | Component::Prefix(_) | Component::RootDir => {} 175 | // Everything else uses a separator that is painted the same way as the component. 176 | Component::CurDir | Component::ParentDir | Component::Normal(_) => { 177 | component_str.push(MAIN_SEPARATOR.to_string()); 178 | } 179 | } 180 | } 181 | 182 | Some((component_str, style)) 183 | } else { 184 | None 185 | } 186 | } 187 | } 188 | 189 | /// A colorable file path. 190 | pub trait Colorable { 191 | /// Get the full path to this file. 192 | fn path(&self) -> PathBuf; 193 | 194 | /// Get the name of this file. 195 | fn file_name(&self) -> OsString; 196 | 197 | /// Try to get the type of this file. 198 | fn file_type(&self) -> Option; 199 | 200 | /// Try to get the metadata for this file. 201 | fn metadata(&self) -> Option; 202 | } 203 | 204 | impl Colorable for DirEntry { 205 | fn path(&self) -> PathBuf { 206 | self.path() 207 | } 208 | 209 | fn file_name(&self) -> OsString { 210 | self.file_name() 211 | } 212 | 213 | fn file_type(&self) -> Option { 214 | self.file_type().ok() 215 | } 216 | 217 | fn metadata(&self) -> Option { 218 | self.metadata().ok() 219 | } 220 | } 221 | 222 | /// Builder for [LsColors]. 223 | struct LsColorsBuilder { 224 | indicator_mapping: HashMap, 225 | 226 | /// Whether Indicator::RegularFile falls back to Indicator::Normal 227 | /// (see ) 228 | file_normal_fallback: bool, 229 | 230 | suffixes: SuffixMapBuilder, 231 | } 232 | 233 | impl LsColorsBuilder { 234 | fn empty() -> Self { 235 | Self { 236 | indicator_mapping: HashMap::new(), 237 | file_normal_fallback: true, 238 | suffixes: SuffixMapBuilder::default(), 239 | } 240 | } 241 | 242 | fn add_from_string(&mut self, input: &str) { 243 | for entry in input.split(':') { 244 | let parts: Vec<_> = entry.split('=').collect(); 245 | 246 | if let Some([entry, ansi_style]) = parts.get(0..2) { 247 | let style = Style::from_ansi_sequence(ansi_style); 248 | if let Some(suffix) = entry.strip_prefix('*') { 249 | self.suffixes.push(suffix, style); 250 | } else if let Some(indicator) = Indicator::from(entry) { 251 | if let Some(style) = style { 252 | self.indicator_mapping.insert(indicator, style); 253 | } else { 254 | self.indicator_mapping.remove(&indicator); 255 | if indicator == Indicator::RegularFile { 256 | self.file_normal_fallback = false; 257 | } 258 | } 259 | } 260 | } 261 | } 262 | } 263 | 264 | fn build(self) -> LsColors { 265 | LsColors { 266 | indicator_mapping: self.indicator_mapping, 267 | file_normal_fallback: self.file_normal_fallback, 268 | suffixes: self.suffixes.build(), 269 | } 270 | } 271 | } 272 | 273 | const LS_COLORS_DEFAULT: &str = "rs=0:lc=\x1b[:rc=m:cl=\x1b[K:ex=01;32:sg=30;43:su=37;41:di=01;34:st=37;44:ow=34;42:tw=30;42:ln=01;36:bd=01;33:cd=01;33:do=01;35:pi=33:so=01;35:"; 274 | 275 | impl Default for LsColorsBuilder { 276 | fn default() -> Self { 277 | let mut builder = Self::empty(); 278 | builder.add_from_string(LS_COLORS_DEFAULT); 279 | builder 280 | } 281 | } 282 | 283 | /// Holds information about how different file system entries should be colorized / styled. 284 | #[derive(Debug, Clone)] 285 | pub struct LsColors { 286 | indicator_mapping: HashMap, 287 | 288 | /// Whether Indicator::RegularFile falls back to Indicator::Normal 289 | /// (see ) 290 | file_normal_fallback: bool, 291 | 292 | suffixes: SuffixMap, 293 | } 294 | 295 | impl Default for LsColors { 296 | /// Constructs a default `LsColors` instance with some default styles. See `man dircolors` for 297 | /// information about the default styles and colors. 298 | fn default() -> Self { 299 | LsColorsBuilder::default().build() 300 | } 301 | } 302 | 303 | impl LsColors { 304 | /// Construct an empty [`LsColors`](struct.LsColors.html) instance with no pre-defined styles. 305 | pub fn empty() -> Self { 306 | LsColorsBuilder::empty().build() 307 | } 308 | 309 | /// Creates a new [`LsColors`](struct.LsColors.html) instance from the `LS_COLORS` environment 310 | /// variable. The basis for this is a default style as constructed via the `Default` 311 | /// implementation. 312 | pub fn from_env() -> Option { 313 | env::var("LS_COLORS") 314 | .ok() 315 | .as_ref() 316 | .map(|s| Self::from_string(s)) 317 | } 318 | 319 | /// Creates a new [`LsColors`](struct.LsColors.html) instance from the given string. 320 | pub fn from_string(input: &str) -> Self { 321 | let mut builder = LsColorsBuilder::default(); 322 | builder.add_from_string(input); 323 | builder.build() 324 | } 325 | 326 | /// Get the ANSI style for a given path. 327 | /// 328 | /// *Note:* this function calls `Path::symlink_metadata` internally. If you already happen to 329 | /// have the `Metadata` available, use [`style_for_path_with_metadata`](#method.style_for_path_with_metadata). 330 | pub fn style_for_path>(&self, path: P) -> Option<&Style> { 331 | let metadata = path.as_ref().symlink_metadata().ok(); 332 | self.style_for_path_with_metadata(path, metadata.as_ref()) 333 | } 334 | 335 | /// Check if an indicator has an associated color. 336 | fn has_color_for(&self, indicator: Indicator) -> bool { 337 | self.indicator_mapping.contains_key(&indicator) 338 | } 339 | 340 | /// Check if we need metadata to color a regular file. 341 | fn needs_file_metadata(&self) -> bool { 342 | self.has_color_for(Indicator::Setuid) 343 | || self.has_color_for(Indicator::Setgid) 344 | || self.has_color_for(Indicator::ExecutableFile) 345 | || self.has_color_for(Indicator::MultipleHardLinks) 346 | } 347 | 348 | /// Check if we need metadata to color a directory. 349 | fn needs_dir_metadata(&self) -> bool { 350 | self.has_color_for(Indicator::StickyAndOtherWritable) 351 | || self.has_color_for(Indicator::OtherWritable) 352 | || self.has_color_for(Indicator::Sticky) 353 | } 354 | 355 | /// Get the indicator type for a path with corresponding metadata. 356 | fn indicator_for(&self, file: &F) -> Indicator { 357 | let file_type = file.file_type(); 358 | 359 | if let Some(file_type) = file_type { 360 | if file_type.is_file() { 361 | if self.needs_file_metadata() { 362 | if let Some(metadata) = file.metadata() { 363 | let mode = crate::fs::mode(&metadata); 364 | let nlink = crate::fs::nlink(&metadata); 365 | 366 | if self.has_color_for(Indicator::Setuid) && mode & 0o4000 != 0 { 367 | return Indicator::Setuid; 368 | } else if self.has_color_for(Indicator::Setgid) && mode & 0o2000 != 0 { 369 | return Indicator::Setgid; 370 | } else if self.has_color_for(Indicator::ExecutableFile) 371 | && mode & 0o0111 != 0 372 | { 373 | return Indicator::ExecutableFile; 374 | } else if self.has_color_for(Indicator::MultipleHardLinks) && nlink > 1 { 375 | return Indicator::MultipleHardLinks; 376 | } 377 | } 378 | } 379 | 380 | Indicator::RegularFile 381 | } else if file_type.is_dir() { 382 | if self.needs_dir_metadata() { 383 | if let Some(metadata) = file.metadata() { 384 | let mode = crate::fs::mode(&metadata); 385 | 386 | if self.has_color_for(Indicator::StickyAndOtherWritable) 387 | && mode & 0o1002 == 0o1002 388 | { 389 | return Indicator::StickyAndOtherWritable; 390 | } else if self.has_color_for(Indicator::OtherWritable) && mode & 0o0002 != 0 391 | { 392 | return Indicator::OtherWritable; 393 | } else if self.has_color_for(Indicator::Sticky) && mode & 0o1000 != 0 { 394 | return Indicator::Sticky; 395 | } 396 | } 397 | } 398 | 399 | Indicator::Directory 400 | } else if file_type.is_symlink() { 401 | // This works because `Path::exists` traverses symlinks. 402 | if self.has_color_for(Indicator::OrphanedSymbolicLink) && !file.path().exists() { 403 | return Indicator::OrphanedSymbolicLink; 404 | } 405 | 406 | Indicator::SymbolicLink 407 | } else { 408 | #[cfg(unix)] 409 | { 410 | use std::os::unix::fs::FileTypeExt; 411 | 412 | if file_type.is_fifo() { 413 | return Indicator::FIFO; 414 | } 415 | if file_type.is_socket() { 416 | return Indicator::Socket; 417 | } 418 | if file_type.is_block_device() { 419 | return Indicator::BlockDevice; 420 | } 421 | if file_type.is_char_device() { 422 | return Indicator::CharacterDevice; 423 | } 424 | } 425 | 426 | // Treat files of unknown type as errors 427 | Indicator::MissingFile 428 | } 429 | } else { 430 | // Default to a regular file, so we still try the suffix map when no metadata is available 431 | Indicator::RegularFile 432 | } 433 | } 434 | 435 | /// Get the ANSI style for a colorable path. 436 | pub fn style_for(&self, file: &F) -> Option<&Style> { 437 | let indicator = self.indicator_for(file); 438 | 439 | if indicator == Indicator::RegularFile { 440 | // Note: using '.to_str()' here means that filename 441 | // matching will not work with invalid-UTF-8 paths. 442 | let filename = file.file_name(); 443 | if let Some(style) = self.style_for_str(filename.to_str()?) { 444 | return Some(style); 445 | } 446 | } 447 | 448 | self.style_for_indicator(indicator) 449 | } 450 | 451 | /// Get the ANSI style for a string. This does not have to be a valid filepath. 452 | pub fn style_for_str(&self, file_str: &str) -> Option<&Style> { 453 | self.suffixes.get(file_str) 454 | } 455 | 456 | /// Get the ANSI style for a path, given the corresponding `Metadata` struct. 457 | /// 458 | /// *Note:* The `Metadata` struct must have been acquired via `Path::symlink_metadata` in 459 | /// order to colorize symbolic links correctly. 460 | pub fn style_for_path_with_metadata>( 461 | &self, 462 | path: P, 463 | metadata: Option<&std::fs::Metadata>, 464 | ) -> Option<&Style> { 465 | struct PathWithMetadata<'a> { 466 | path: &'a Path, 467 | metadata: Option<&'a Metadata>, 468 | } 469 | 470 | impl Colorable for PathWithMetadata<'_> { 471 | fn path(&self) -> PathBuf { 472 | self.path.to_owned() 473 | } 474 | 475 | fn file_name(&self) -> OsString { 476 | // Path::file_name() only works if the last component is Normal, but 477 | // we want it for all component types, so we open code it 478 | 479 | self.path 480 | .components() 481 | .last() 482 | .map(|c| c.as_os_str()) 483 | .unwrap_or_else(|| self.path.as_os_str()) 484 | .to_owned() 485 | } 486 | 487 | fn file_type(&self) -> Option { 488 | self.metadata.map(|m| m.file_type()) 489 | } 490 | 491 | fn metadata(&self) -> Option { 492 | self.metadata.cloned() 493 | } 494 | } 495 | 496 | let path = path.as_ref(); 497 | self.style_for(&PathWithMetadata { path, metadata }) 498 | } 499 | 500 | /// Get ANSI styles for each component of a given path. Components already include the path 501 | /// separator symbol, if required. For a path like `foo/bar/test.md`, this would return an 502 | /// iterator over three pairs for the three path components `foo/`, `bar/` and `test.md` 503 | /// together with their respective styles. 504 | pub fn style_for_path_components<'a>(&'a self, path: &'a Path) -> StyledComponents<'a> { 505 | StyledComponents { 506 | lscolors: self, 507 | component_path: PathBuf::new(), 508 | components: path.components().peekable(), 509 | } 510 | } 511 | 512 | /// Get the ANSI style for a certain `Indicator` (regular file, directory, symlink, ...). Note 513 | /// that this function implements a fallback logic for some of the indicators (just like `ls`). 514 | /// For example, the style for `mi` (missing file) falls back to `or` (orphaned symbolic link) 515 | /// if it has not been specified explicitly. 516 | pub fn style_for_indicator(&self, indicator: Indicator) -> Option<&Style> { 517 | self.indicator_mapping 518 | .get(&indicator) 519 | .or_else(|| { 520 | self.indicator_mapping.get(&match indicator { 521 | Indicator::Setuid 522 | | Indicator::Setgid 523 | | Indicator::ExecutableFile 524 | | Indicator::MultipleHardLinks => Indicator::RegularFile, 525 | 526 | Indicator::StickyAndOtherWritable 527 | | Indicator::OtherWritable 528 | | Indicator::Sticky => Indicator::Directory, 529 | 530 | Indicator::OrphanedSymbolicLink => Indicator::SymbolicLink, 531 | 532 | Indicator::MissingFile => Indicator::OrphanedSymbolicLink, 533 | 534 | _ => indicator, 535 | }) 536 | }) 537 | .or_else(|| { 538 | if indicator == Indicator::RegularFile && !self.file_normal_fallback { 539 | None 540 | } else { 541 | self.indicator_mapping.get(&Indicator::Normal) 542 | } 543 | }) 544 | } 545 | } 546 | 547 | #[cfg(test)] 548 | mod tests { 549 | use crate::style::{Color, FontStyle, Style}; 550 | use crate::{Indicator, LsColors}; 551 | 552 | use std::fs::{self, File}; 553 | use std::path::{Path, PathBuf}; 554 | 555 | #[test] 556 | fn basic_usage() { 557 | let lscolors = LsColors::from_string("*.wav=00;36:"); 558 | 559 | let style_dir = lscolors.style_for_indicator(Indicator::Directory).unwrap(); 560 | assert_eq!(FontStyle::bold(), style_dir.font_style); 561 | assert_eq!(Some(Color::Blue), style_dir.foreground); 562 | assert_eq!(None, style_dir.background); 563 | 564 | let style_symlink = lscolors 565 | .style_for_indicator(Indicator::SymbolicLink) 566 | .unwrap(); 567 | assert_eq!(FontStyle::bold(), style_symlink.font_style); 568 | assert_eq!(Some(Color::Cyan), style_symlink.foreground); 569 | assert_eq!(None, style_symlink.background); 570 | 571 | let style_rs = lscolors.style_for_path("test.wav").unwrap(); 572 | assert_eq!(FontStyle::default(), style_rs.font_style); 573 | assert_eq!(Some(Color::Cyan), style_rs.foreground); 574 | assert_eq!(None, style_rs.background); 575 | } 576 | 577 | #[test] 578 | fn style_for_path_uses_correct_ordering() { 579 | let lscolors = LsColors::from_string("*.foo=01;35:*README.foo=33;44"); 580 | 581 | // dummy.foo matches to *.foo without getting overriden. 582 | let style_foo = lscolors.style_for_path("some/folder/dummy.foo").unwrap(); 583 | assert_eq!(FontStyle::bold(), style_foo.font_style); 584 | assert_eq!(Some(Color::Magenta), style_foo.foreground); 585 | assert_eq!(None, style_foo.background); 586 | 587 | // README.foo matches to *README.foo by overriding *.foo 588 | let style_readme = lscolors 589 | .style_for_path("some/other/folder/README.foo") 590 | .unwrap(); 591 | assert_eq!(FontStyle::default(), style_readme.font_style); 592 | assert_eq!(Some(Color::Yellow), style_readme.foreground); 593 | assert_eq!(Some(Color::Blue), style_readme.background); 594 | 595 | let lscolors = LsColors::from_string("*README.foo=33;44:*.foo=01;35"); 596 | 597 | let style_foo = lscolors.style_for_path("some/folder/dummy.foo").unwrap(); 598 | assert_eq!(FontStyle::bold(), style_foo.font_style); 599 | assert_eq!(Some(Color::Magenta), style_foo.foreground); 600 | assert_eq!(None, style_foo.background); 601 | 602 | // README.foo matches to *.foo because *.foo overrides *README.foo 603 | let style_readme = lscolors 604 | .style_for_path("some/other/folder/README.foo") 605 | .unwrap(); 606 | assert_eq!(FontStyle::bold(), style_readme.font_style); 607 | assert_eq!(Some(Color::Magenta), style_readme.foreground); 608 | assert_eq!(None, style_readme.background); 609 | } 610 | 611 | #[test] 612 | fn style_for_path_uses_lowercase_matching() { 613 | let lscolors = LsColors::from_string("*.O=01;35"); 614 | 615 | let style_artifact = lscolors.style_for_path("artifact.o").unwrap(); 616 | assert_eq!(FontStyle::bold(), style_artifact.font_style); 617 | assert_eq!(Some(Color::Magenta), style_artifact.foreground); 618 | assert_eq!(None, style_artifact.background); 619 | } 620 | 621 | #[test] 622 | fn default_styles_should_be_preserved() { 623 | // Setting an unrelated style should not influence the default 624 | // style for "directory" (below) 625 | let lscolors = LsColors::from_string("ex=01:"); 626 | 627 | let style_dir = lscolors.style_for_indicator(Indicator::Directory).unwrap(); 628 | assert_eq!(FontStyle::bold(), style_dir.font_style); 629 | assert_eq!(Some(Color::Blue), style_dir.foreground); 630 | assert_eq!(None, style_dir.background); 631 | } 632 | 633 | fn temp_dir() -> tempfile::TempDir { 634 | tempfile::tempdir().expect("temporary directory") 635 | } 636 | 637 | fn create_file>(path: P) -> PathBuf { 638 | File::create(&path).expect("temporary file"); 639 | path.as_ref().to_path_buf() 640 | } 641 | 642 | fn create_dir>(path: P) -> PathBuf { 643 | fs::create_dir(&path).expect("temporary directory"); 644 | path.as_ref().to_path_buf() 645 | } 646 | 647 | fn get_default_style>(path: P) -> Option