Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE:Backports:SLE-15-SP4:RebuildFactoryUpdates
lsd
lsd-1.1.5.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File lsd-1.1.5.obscpio of Package lsd
07070100000000000041ED00000000000000000000000266C4C37900000000000000000000000000000000000000000000001200000000lsd-1.1.5/.github07070100000001000081A400000000000000000000000166C4C379000001AA000000000000000000000000000000000000001E00000000lsd-1.1.5/.github/FUNDING.yml# These are supported funding model platforms github: zwpaper patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: kweizh tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry custom: # Replace with a single custom sponsorship URL 07070100000002000041ED00000000000000000000000266C4C37900000000000000000000000000000000000000000000002100000000lsd-1.1.5/.github/ISSUE_TEMPLATE07070100000003000081A400000000000000000000000166C4C3790000098B000000000000000000000000000000000000002900000000lsd-1.1.5/.github/ISSUE_TEMPLATE/bug.ymlname: Bug Report Form description: Create a report to help us improve, by the new GitHub form title: "[Bug]: " labels: ["bug"] assignees: - zwpaper body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! - type: checkboxes id: latest-version attributes: label: Version description: Please make sure you can reproduce in the [latest release](https://github.com/lsd-rs/lsd/releases/latest) options: - label: latest required: true - type: textarea id: version attributes: label: version description: "`lsd --version` output" placeholder: lsd --version validations: required: true - type: dropdown id: os attributes: label: What OS are you seeing the problem on? multiple: true options: - Windows - Linux - macOS - Others - type: textarea id: installation attributes: label: installation description: "how do you install lsd?" placeholder: "how do you install lsd?" validations: required: true - type: textarea id: term attributes: label: term description: "`echo $TERM` output" placeholder: echo $TERM validations: required: false - type: textarea id: ls-colors attributes: label: ls-colors description: "`echo $LS_COLORS` output" placeholder: echo $LS_COLORS validations: required: false - type: textarea id: what-happened attributes: label: What happened? description: Tell us what happen? placeholder: | If applicable, add the output of the classic ls command (`\ls -la`) in order to show the buggy file/directory. render: markdown validations: required: true - type: textarea id: what-expected attributes: label: What expected? description: What did you expect to happen? placeholder: | If the application panics run the command with the trace (`RUST_BACKTRACE=1 lsd ...`). In case of graphical errors, add a screenshot if possible." render: markdown validations: required: true - type: textarea id: others attributes: label: What else? description: Is there anything else you want to tell us? placeholder: "Others" render: markdown validations: required: false 07070100000004000081A400000000000000000000000166C4C379000001D6000000000000000000000000000000000000002F00000000lsd-1.1.5/.github/ISSUE_TEMPLATE/bug_report.md--- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- - os: - `lsd --version`: - `echo $TERM`: - `echo $LS_COLORS`: ## Expected behavior If applicable, add the output of the classic ls command (`\ls -la`) in order to show the buggy file/directory. ## Actual behavior If the application panics run the command with the trace (`RUST_BACKTRACE=1 lsd ...`). In case of graphical errors, add a screenshot if possible. 07070100000005000081A400000000000000000000000166C4C379000000CF000000000000000000000000000000000000002800000000lsd-1.1.5/.github/PULL_REQUEST_TEMPLATE <!--- PR Description ---> --- #### TODO - [ ] Use `cargo fmt` - [ ] Add necessary tests - [ ] Update default config/theme in README (if applicable) - [ ] Update man page at lsd/doc/lsd.md (if applicable) 07070100000006000041ED00000000000000000000000266C4C37900000000000000000000000000000000000000000000001C00000000lsd-1.1.5/.github/workflows07070100000007000081A400000000000000000000000166C4C37900004FBE000000000000000000000000000000000000002500000000lsd-1.1.5/.github/workflows/CICD.ymlname: CICD # spell-checker:ignore CICD CODECOV MSVC MacOS Peltoche SHAs buildable clippy dpkg esac fakeroot gnueabihf halium libssl mkdir musl popd printf pushd rustfmt softprops toolchain env: PROJECT_NAME: lsd PROJECT_DESC: "An ls command with a lot of pretty colors." PROJECT_AUTH: "Peltoche <peltoche@halium.fr>" RUST_MIN_SRV: "1.74.0" on: [push, pull_request] jobs: style: name: Style runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: job: [ { os: ubuntu-latest }, { os: macos-latest }, { os: windows-latest } ] steps: - uses: actions/checkout@v1 - name: Install `rust` toolchain uses: actions-rs/toolchain@v1 with: toolchain: ${{ env.RUST_MIN_SRV }} override: true profile: minimal # minimal component installation (ie, no documentation) components: rustfmt, clippy - name: "`fmt` testing" uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check - name: "`clippy` testing" if: success() || failure() # run regardless of prior step ("`fmt` testing") success/failure uses: actions-rs/cargo@v1 with: command: clippy args: --tests -- -D warnings - name: "`clap` deprecated checks" if: success() || failure() # run regardless of prior step ("`fmt` testing") success/failure uses: actions-rs/cargo@v1 with: command: check args: --features clap/deprecated min_version: name: MinSRV # Minimum supported rust version runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Install `rust` toolchain (v${{ env.RUST_MIN_SRV }}) uses: actions-rs/toolchain@v1 with: toolchain: ${{ env.RUST_MIN_SRV }} profile: minimal # minimal component installation (ie, no documentation) - name: Test uses: actions-rs/cargo@v1 with: command: test build: name: Build runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: job: - { os: ubuntu-latest , target: arm-unknown-linux-gnueabihf , use-cross: use-cross } - { os: ubuntu-latest , target: aarch64-unknown-linux-gnu , use-cross: use-cross } - { os: ubuntu-latest , target: aarch64-unknown-linux-musl , use-cross: use-cross } - { os: ubuntu-latest , target: i686-unknown-linux-gnu , use-cross: use-cross } - { os: ubuntu-latest , target: i686-unknown-linux-musl , use-cross: use-cross } - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , use-cross: use-cross } - { os: ubuntu-latest , target: x86_64-unknown-linux-musl , use-cross: use-cross } - { os: macos-latest , target: x86_64-apple-darwin } - { os: macos-latest , target: aarch64-apple-darwin } - { os: windows-latest , target: i686-pc-windows-gnu } - { os: windows-latest , target: i686-pc-windows-msvc } - { os: windows-latest , target: x86_64-pc-windows-gnu } - { os: windows-latest , target: x86_64-pc-windows-msvc } outputs: DEPLOY: ${{ steps.vars.outputs.DEPLOY }} steps: - uses: actions/checkout@v1 - name: Install any prerequisites shell: bash run: | case ${{ matrix.job.target }} in arm-*-linux-*hf) sudo apt-get -y update ; sudo apt-get -y install binutils-arm-linux-gnueabihf ;; aarch64-*-linux-*) sudo apt-get -y update ; sudo apt-get -y install binutils-aarch64-linux-gnu ;; esac - name: Initialize workflow variables id: vars shell: bash run: | # toolchain TOOLCHAIN="stable" ## default to "stable" toolchain # * specify alternate TOOLCHAIN for *-pc-windows-gnu targets; gnu targets on Windows are broken for the standard *-pc-windows-msvc toolchain (refs: <https://github.com/rust-lang/rust/issues/47048>, <https://github.com/rust-lang/rust/issues/53454>, <https://github.com/rust-lang/cargo/issues/6754>) case ${{ matrix.job.target }} in *-pc-windows-gnu) TOOLCHAIN="stable-${{ matrix.job.target }}" ;; esac; # * use requested TOOLCHAIN if specified if [ -n "${{ matrix.job.toolchain }}" ]; then TOOLCHAIN="${{ matrix.job.toolchain }}" ; fi echo set-output name=TOOLCHAIN::${TOOLCHAIN} echo ::set-output name=TOOLCHAIN::${TOOLCHAIN} # staging directory STAGING='_staging' echo set-output name=STAGING::${STAGING} echo ::set-output name=STAGING::${STAGING} # determine EXE suffix EXE_suffix="" ; case ${{ matrix.job.target }} in *-pc-windows-*) EXE_suffix=".exe" ;; esac; echo set-output name=EXE_suffix::${EXE_suffix} echo ::set-output name=EXE_suffix::${EXE_suffix} # parse commit reference info REF_NAME=${GITHUB_REF#refs/*/} unset REF_BRANCH ; case ${GITHUB_REF} in refs/heads/*) REF_BRANCH=${GITHUB_REF#refs/heads/} ;; esac; unset REF_TAG ; case ${GITHUB_REF} in refs/tags/*) REF_TAG=${GITHUB_REF#refs/tags/} ;; esac; REF_SHAS=${GITHUB_SHA:0:8} echo set-output name=REF_NAME::${REF_NAME} echo set-output name=REF_BRANCH::${REF_BRANCH} echo set-output name=REF_TAG::${REF_TAG} echo set-output name=REF_SHAS::${REF_SHAS} echo ::set-output name=REF_NAME::${REF_NAME} echo ::set-output name=REF_BRANCH::${REF_BRANCH} echo ::set-output name=REF_TAG::${REF_TAG} echo ::set-output name=REF_SHAS::${REF_SHAS} # package name PKG_suffix=".tar.gz" ; case ${{ matrix.job.target }} in *-pc-windows-*) PKG_suffix=".zip" ;; esac; PKG_BASENAME=${PROJECT_NAME}-${REF_TAG:-$REF_SHAS}-${{ matrix.job.target }} PKG_NAME=${PKG_BASENAME}${PKG_suffix} echo set-output name=PKG_suffix::${PKG_suffix} echo set-output name=PKG_BASENAME::${PKG_BASENAME} echo set-output name=PKG_NAME::${PKG_NAME} echo ::set-output name=PKG_suffix::${PKG_suffix} echo ::set-output name=PKG_BASENAME::${PKG_BASENAME} echo ::set-output name=PKG_NAME::${PKG_NAME} # deployable tag? (ie, leading "vM" or "M"; M == version number) unset DEPLOY ; if [[ $REF_TAG =~ ^[vV]?[0-9].* ]]; then DEPLOY='true' ; fi echo set-output name=DEPLOY::${DEPLOY:-<empty>/false} echo ::set-output name=DEPLOY::${DEPLOY} # DPKG architecture? unset DPKG_ARCH ; case ${{ matrix.job.target }} in aarch64-*-linux-*) DPKG_ARCH=arm64 ;; i686-*-linux-*) DPKG_ARCH=i686 ;; x86_64-*-linux-*) DPKG_ARCH=amd64 ;; esac; echo set-output name=DPKG_ARCH::${DPKG_ARCH} echo ::set-output name=DPKG_ARCH::${DPKG_ARCH} # DPKG version? unset DPKG_VERSION ; if [[ $REF_TAG =~ ^[vV]?[0-9].* ]]; then DPKG_VERSION=${REF_TAG/#[vV]/} ; fi echo set-output name=DPKG_VERSION::${DPKG_VERSION} echo ::set-output name=DPKG_VERSION::${DPKG_VERSION} # DPKG base name/conflicts? DPKG_BASENAME=${PROJECT_NAME} DPKG_CONFLICTS=${PROJECT_NAME}-musl case ${{ matrix.job.target }} in *-musl) DPKG_BASENAME=${PROJECT_NAME}-musl ; DPKG_CONFLICTS=${PROJECT_NAME} ;; esac; echo set-output name=DPKG_BASENAME::${DPKG_BASENAME} echo set-output name=DPKG_CONFLICTS::${DPKG_CONFLICTS} echo ::set-output name=DPKG_BASENAME::${DPKG_BASENAME} echo ::set-output name=DPKG_CONFLICTS::${DPKG_CONFLICTS} # DPKG name unset DPKG_NAME; if [[ -n $DPKG_ARCH && -n $DPKG_VERSION ]]; then DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb" ; fi echo set-output name=DPKG_NAME::${DPKG_NAME} echo ::set-output name=DPKG_NAME::${DPKG_NAME} # target-specific options # * CARGO_USE_CROSS (truthy) CARGO_USE_CROSS='true' ; case '${{ matrix.job.use-cross }}' in ''|0|f|false|n|no) unset CARGO_USE_CROSS ;; esac; echo set-output name=CARGO_USE_CROSS::${CARGO_USE_CROSS:-<empty>/false} echo ::set-output name=CARGO_USE_CROSS::${CARGO_USE_CROSS} # * test only binary for arm-type targets unset CARGO_TEST_OPTIONS ; case ${{ matrix.job.target }} in arm-* | aarch64-*-linux-*) CARGO_TEST_OPTIONS="--bin ${PROJECT_NAME}" ;; esac; echo set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS} echo ::set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS} # * strip executable? STRIP="strip" ; case ${{ matrix.job.target }} in arm-*-linux-*hf) STRIP="arm-linux-gnueabihf-strip" ;; aarch64-*-linux-*) STRIP="aarch64-linux-gnu-strip" ;; *-pc-windows-msvc) STRIP="" ;; esac; echo set-output name=STRIP::${STRIP} echo ::set-output name=STRIP::${STRIP} - name: Create all needed build/work directories shell: bash run: | mkdir -p '${{ steps.vars.outputs.STAGING }}' mkdir -p '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}' mkdir -p '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/autocomplete' mkdir -p '${{ steps.vars.outputs.STAGING }}/dpkg' - name: Update manpage placeholders shell: bash run: | LSD_VERSION="$(if echo "$GITHUB_REF" | grep -q '^refs/tags'; then echo "${GITHUB_REF#refs/*/}"; else echo; fi)" sed -i.bk "s|footer: lsd <version>|footer: lsd $LSD_VERSION|" doc/lsd.md sed -i.bk "s|date: <date>|date: $(date '+%Y-%m-%d')|" doc/lsd.md rm doc/lsd.md.bk - name: Setup pandoc uses: r-lib/actions/setup-pandoc@v1 - name: Generate Manpage run: pandoc --standalone --to man doc/lsd.md -o lsd.1 - name: Install `rust` toolchain uses: actions-rs/toolchain@v1 with: toolchain: ${{ steps.vars.outputs.TOOLCHAIN }} target: ${{ matrix.job.target }} override: true profile: minimal # minimal component installation (ie, no documentation) - name: Build uses: actions-rs/cargo@v1 with: use-cross: ${{ steps.vars.outputs.CARGO_USE_CROSS }} command: build args: --release --target=${{ matrix.job.target }} --locked - name: Test if: matrix.job.target != 'aarch64-apple-darwin' uses: actions-rs/cargo@v1 with: use-cross: ${{ steps.vars.outputs.CARGO_USE_CROSS }} command: test args: --target=${{ matrix.job.target }} ${{ steps.vars.outputs.CARGO_TEST_OPTIONS}} - name: Archive executable artifacts uses: actions/upload-artifact@master with: name: ${{ env.PROJECT_NAME }}-${{ matrix.job.target }} path: target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }} - name: Package shell: bash run: | # binary cp 'target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }}' '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/' # `strip` binary (if needed) if [ -n "${{ steps.vars.outputs.STRIP }}" ]; then "${{ steps.vars.outputs.STRIP }}" '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }}' ; fi # README and LICENSE cp README.md '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/' cp LICENSE '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/' # manpage cp lsd.1 '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/' # autocomplete cp 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}-'*/'out/${{ env.PROJECT_NAME }}.bash' '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/autocomplete/${{ env.PROJECT_NAME }}.bash-completion' cp 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}-'*/'out/${{ env.PROJECT_NAME }}.fish' '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/autocomplete/' cp 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}-'*/'out/_${{ env.PROJECT_NAME }}.ps1' '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/autocomplete/' cp 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}-'*/'out/_${{ env.PROJECT_NAME }}' '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/autocomplete/' # base compressed package pushd '${{ steps.vars.outputs.STAGING }}/' >/dev/null case ${{ matrix.job.target }} in *-pc-windows-*) 7z -y a '${{ steps.vars.outputs.PKG_NAME }}' '${{ steps.vars.outputs.PKG_BASENAME }}'/* | tail -2 ;; *) tar czf '${{ steps.vars.outputs.PKG_NAME }}' '${{ steps.vars.outputs.PKG_BASENAME }}'/* ;; esac; popd >/dev/null # dpkg if [ -n "${{ steps.vars.outputs.DPKG_NAME }}" ]; then DPKG_DIR="${{ steps.vars.outputs.STAGING }}/dpkg" # binary install -Dm755 'target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }}' "${DPKG_DIR}/usr/bin/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }}" if [ -n "${{ steps.vars.outputs.STRIP }}" ]; then "${{ steps.vars.outputs.STRIP }}" "${DPKG_DIR}/usr/bin/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }}" ; fi # README and LICENSE install -Dm644 README.md "${DPKG_DIR}/usr/share/doc/${{ env.PROJECT_NAME }}/README.md" install -Dm644 LICENSE "${DPKG_DIR}/usr/share/doc/${{ env.PROJECT_NAME }}/LICENSE" # (auto-)completions install -Dm644 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}-'*/'out/${{ env.PROJECT_NAME }}.bash' "${DPKG_DIR}/usr/share/bash-completion/completions/${{ env.PROJECT_NAME }}" install -Dm644 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}-'*/'out/${{ env.PROJECT_NAME }}.fish' "${DPKG_DIR}/usr/share/fish/vendor_completions.d/${{ env.PROJECT_NAME }}.fish" install -Dm644 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}-'*/'out/_${{ env.PROJECT_NAME }}' "${DPKG_DIR}/usr/share/zsh/vendor-completions/_${{ env.PROJECT_NAME }}" # control file mkdir -p "${DPKG_DIR}/DEBIAN" printf "Package: ${{ steps.vars.outputs.DPKG_BASENAME }}\nVersion: ${{ steps.vars.outputs.DPKG_VERSION }}\nSection: utils\nPriority: optional\nMaintainer: ${{ env.PROJECT_AUTH }}\nArchitecture: ${{ steps.vars.outputs.DPKG_ARCH }}\nProvides: ${{ env.PROJECT_NAME }}\nConflicts: ${{ steps.vars.outputs.DPKG_CONFLICTS }}\nDescription: ${{ env.PROJECT_DESC }}\n" > "${DPKG_DIR}/DEBIAN/control" ## cat "${DPKG_DIR}/DEBIAN/control" # build dpkg fakeroot dpkg-deb --build "${DPKG_DIR}" "${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.DPKG_NAME }}" # build a deb not using zst # check https://github.com/lsd-rs/lsd/issues/891 ar x "${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.DPKG_NAME }}" # Uncompress zstd files an re-compress them using xz zstd -d < control.tar.zst | xz > control.tar.xz zstd -d < data.tar.zst | xz > data.tar.xz # Re-create the Debian package in /tmp/ xz_deb="$(echo ${{ steps.vars.outputs.DPKG_NAME }} | sed 's/.deb/_xz.deb/g')" ar -m -c -a sdsd ${xz_deb} debian-binary control.tar.xz data.tar.xz mv ${xz_deb} ${{ steps.vars.outputs.STAGING }}/ # Clean up rm debian-binary control.tar.xz data.tar.xz control.tar.zst data.tar.zst fi - name: Publish uses: softprops/action-gh-release@v1 if: steps.vars.outputs.DEPLOY with: files: | ${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_NAME }} ${{ steps.vars.outputs.STAGING }}/*.deb env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} container_build: name: Container Build runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: job: - { os: ubuntu-latest, target: loongarch64-unknown-linux-gnu, platform: loong64 } steps: - uses: actions/checkout@v1 - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: image: tonistiigi/binfmt:master - name: Containerized Build # containerized build is slow, but easy to setup and cross compile # currently only used for: # - loongarch64 # # tests should be done previously shell: bash run: | docker run --platform linux/${{ matrix.job.platform }} \ -v `pwd`:/src \ kweizh/loongarch-rust:v0.1.0 \ build --release --target ${{ matrix.job.target }} # determine EXE suffix EXE_suffix="" ; case ${{ matrix.job.target }} in *-pc-windows-*) EXE_suffix=".exe" ;; esac; echo "EXE_suffix=${EXE_suffix}" >> $GITHUB_OUTPUT - name: Archive executable artifacts uses: actions/upload-artifact@master with: name: ${{ env.PROJECT_NAME }}-${{ matrix.job.target }} path: target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }} - name: Release uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: files: | target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} winget: runs-on: ubuntu-latest needs: build if: ${{ needs.build.outputs.DEPLOY }} steps: - name: Publish to Winget uses: vedantmgoyal9/winget-releaser@main with: identifier: lsd-rs.lsd installers-regex: 'pc-windows-msvc\.zip$' token: ${{ secrets.WINGET_TOKEN }} coverage: name: Code Coverage runs-on: ${{ matrix.job.os }} strategy: fail-fast: true matrix: # job: [ { os: ubuntu-latest }, { os: macos-latest }, { os: windows-latest } ] job: [ { os: ubuntu-latest } ] ## cargo-tarpaulin is currently only available on linux steps: - uses: actions/checkout@v1 # - name: Reattach HEAD ## may be needed for accurate code coverage info # run: git checkout ${{ github.head_ref }} - name: Initialize workflow variables id: vars shell: bash run: | # staging directory STAGING='_staging' echo set-output name=STAGING::${STAGING} echo ::set-output name=STAGING::${STAGING} # check for CODECOV_TOKEN availability (work-around for inaccessible 'secrets' object for 'if'; see <https://github.community/t5/GitHub-Actions/jobs-lt-job-id-gt-if-does-not-work-with-env-secrets/m-p/38549>) unset HAS_CODECOV_TOKEN if [ -n $CODECOV_TOKEN ]; then HAS_CODECOV_TOKEN='true' ; fi echo set-output name=HAS_CODECOV_TOKEN::${HAS_CODECOV_TOKEN} echo ::set-output name=HAS_CODECOV_TOKEN::${HAS_CODECOV_TOKEN} env: CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}" - name: Create all needed build/work directories shell: bash run: | mkdir -p '${{ steps.vars.outputs.STAGING }}/work' - name: Install required packages run: | sudo apt-get -y install libssl-dev pushd '${{ steps.vars.outputs.STAGING }}/work' >/dev/null wget --no-verbose https://github.com/xd009642/tarpaulin/releases/download/0.13.3/cargo-tarpaulin-0.13.3-travis.tar.gz tar xf cargo-tarpaulin-0.13.3-travis.tar.gz cp cargo-tarpaulin "$(dirname -- "$(which cargo)")"/ popd >/dev/null - name: Generate coverage run: | cargo tarpaulin --out Xml - name: Upload coverage results (CodeCov.io) # CODECOV_TOKEN (aka, "Repository Upload Token" for REPO from CodeCov.io) ## set via REPO/Settings/Secrets # if: secrets.CODECOV_TOKEN (not supported {yet?}; see <https://github.community/t5/GitHub-Actions/jobs-lt-job-id-gt-if-does-not-work-with-env-secrets/m-p/38549>) if: steps.vars.outputs.HAS_CODECOV_TOKEN run: | # CodeCov.io cargo tarpaulin --out Xml bash <(curl -s https://codecov.io/bash) env: CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}" 07070100000008000081A400000000000000000000000166C4C3790000001A000000000000000000000000000000000000001500000000lsd-1.1.5/.gitignore/target out.md **/*.rs.bk 07070100000009000081A400000000000000000000000166C4C37900000259000000000000000000000000000000000000001800000000lsd-1.1.5/.release.tomlsign-commit = true sign-tag = true dev-version = false pre-release-commit-message = "Release {{version}}" tag-prefix = "" tag-name = "{{version}}" pre-release-replacements = [ {file="CHANGELOG.md", search="## \\[Unreleased\\]", replace="## [Unreleased]\n\n## [{{version}}] - {{date}}"}, {file="CHANGELOG.md", search="HEAD", replace="{{version}}"}, {file="CHANGELOG.md", search="\\[Unreleased\\]:", replace="[Unreleased]: https://github.com/Peltoche/lsd/compare/{{version}}...HEAD\n[{{version}}]: "}, {file="README.md", search="lsd_[0-9\\.]+_amd64.deb", replace="lsd_{{version}}_amd64.deb"}, ] 0707010000000A000081A400000000000000000000000166C4C3790000002A000000000000000000000000000000000000001500000000lsd-1.1.5/CODEOWNERS* @zwpaper # Retired # @meain @Peltoche 0707010000000B000081A400000000000000000000000166C4C3790000AA82000000000000000000000000000000000000001500000000lsd-1.1.5/Cargo.lock# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstream" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is-terminal", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anstyle-parse" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" dependencies = [ "anstyle", "windows-sys 0.48.0", ] [[package]] name = "assert_cmd" version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" dependencies = [ "anstyle", "bstr", "doc-comment", "predicates", "predicates-core", "predicates-tree", "wait-timeout", ] [[package]] name = "assert_fs" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cd762e110c8ed629b11b6cde59458cc1c71de78ebbcc30099fc8e0403a2a2ec" dependencies = [ "anstyle", "doc-comment", "globwalk", "predicates", "predicates-core", "predicates-tree", "tempfile", ] [[package]] name = "autocfg" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" dependencies = [ "serde", ] [[package]] name = "bstr" version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", "regex-automata", "serde", ] [[package]] name = "bumpalo" version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ "jobserver", "libc", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "pure-rust-locales", "wasm-bindgen", "windows-targets 0.52.4", ] [[package]] name = "chrono-humanize" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799627e6b4d27827a814e837b9d8a504832086081806d45b1afa34dc982b023b" dependencies = [ "chrono", ] [[package]] name = "clap" version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb690e81c7840c0d7aade59f242ea3b41b9bc27bcd5997890e7702ae4b32e487" dependencies = [ "clap_builder", "clap_derive", "once_cell", ] [[package]] name = "clap_builder" version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", "terminal_size 0.2.6", ] [[package]] name = "clap_complete" version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "885e4d7d5af40bfb99ae6f9433e292feac98d452dcb3ec3d25dfe7552b77da8c" dependencies = [ "clap", ] [[package]] name = "clap_derive" version = "4.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" [[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "crossbeam-deque" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crossterm" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ "bitflags 2.5.0", "crossterm_winapi", "libc", "mio", "parking_lot", "serde", "signal-hook", "signal-hook-mio", "winapi", ] [[package]] name = "crossterm_winapi" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ "winapi", ] [[package]] name = "dashmap" version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", "hashbrown", "lock_api", "once_cell", "parking_lot_core", ] [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", "redox_users", "windows-sys 0.48.0", ] [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "fastrand" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "float-cmp" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" dependencies = [ "num-traits", ] [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "getrandom" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "git2" version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" dependencies = [ "bitflags 2.5.0", "libc", "libgit2-sys", "log", "url", ] [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ "aho-corasick", "bstr", "log", "regex-automata", "regex-syntax", ] [[package]] name = "globwalk" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ "bitflags 2.5.0", "ignore", "walkdir", ] [[package]] name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "human-sort" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "140a09c9305e6d5e557e2ed7cbc68e05765a7d4213975b87cb04920689cc6219" [[package]] name = "iana-time-zone" version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "idna" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "ignore" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" dependencies = [ "crossbeam-deque", "globset", "log", "memchr", "regex-automata", "same-file", "walkdir", "winapi-util", ] [[package]] name = "indexmap" version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", "windows-sys 0.48.0", ] [[package]] name = "is-terminal" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ "hermit-abi", "libc", "windows-sys 0.52.0", ] [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" dependencies = [ "libc", ] [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libgit2-sys" version = "0.16.2+1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" dependencies = [ "cc", "libc", "libz-sys", "pkg-config", ] [[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.5.0", "libc", ] [[package]] name = "libz-sys" version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "lscolors" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab0b209ec3976527806024406fe765474b9a1750a0ed4b8f0372364741f50e7b" dependencies = [ "nu-ansi-term", ] [[package]] name = "lsd" version = "1.1.5" dependencies = [ "assert_cmd", "assert_fs", "chrono", "chrono-humanize", "clap", "clap_complete", "crossterm", "dirs", "git2", "globset", "human-sort", "libc", "lscolors", "once_cell", "predicates", "serde", "serde_yaml", "serial_test", "sys-locale", "tempfile", "term_grid", "terminal_size 0.3.0", "thiserror", "unicode-width", "url", "uzers", "version_check", "vsort", "wild", "windows", "xattr", "xdg", "yaml-rust", ] [[package]] name = "memchr" version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", "wasi", "windows-sys 0.48.0", ] [[package]] name = "normalize-line-endings" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "nu-ansi-term" version = "0.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" dependencies = [ "windows-sys 0.48.0", ] [[package]] name = "num-traits" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.48.5", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "predicates" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" dependencies = [ "anstyle", "difflib", "float-cmp", "normalize-line-endings", "predicates-core", "regex", ] [[package]] name = "predicates-core" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" [[package]] name = "predicates-tree" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" dependencies = [ "predicates-core", "termtree", ] [[package]] name = "proc-macro2" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] [[package]] name = "pure-rust-locales" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1190fd18ae6ce9e137184f207593877e70f39b015040156b1e05081cdfe3733a" [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_users" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", "thiserror", ] [[package]] name = "regex" version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rustix" version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys 0.3.8", "windows-sys 0.48.0", ] [[package]] name = "rustix" version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ "bitflags 2.5.0", "errno", "libc", "linux-raw-sys 0.4.13", "windows-sys 0.52.0", ] [[package]] name = "ryu" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ "indexmap", "itoa", "ryu", "serde", "unsafe-libyaml", ] [[package]] name = "serial_test" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" dependencies = [ "dashmap", "futures", "lazy_static", "log", "parking_lot", "serial_test_derive", ] [[package]] name = "serial_test_derive" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "signal-hook" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-mio" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" dependencies = [ "libc", "mio", "signal-hook", ] [[package]] name = "signal-hook-registry" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" version = "2.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sys-locale" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0" dependencies = [ "libc", ] [[package]] name = "tempfile" version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", "rustix 0.38.32", "windows-sys 0.52.0", ] [[package]] name = "term_grid" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230d3e804faaed5a39b08319efb797783df2fd9671b39b7596490cb486d702cf" dependencies = [ "unicode-width", ] [[package]] name = "terminal_size" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ "rustix 0.37.27", "windows-sys 0.48.0", ] [[package]] name = "terminal_size" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ "rustix 0.38.32", "windows-sys 0.48.0", ] [[package]] name = "termtree" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tinyvec" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "unicode-bidi" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "url" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uzers" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d283dc7e8c901e79e32d077866eaf599156cbf427fffa8289aecc52c5c3f63" dependencies = [ "libc", "log", ] [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vsort" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11331a1b48f99ea6bb27faae41399d08783d055ddee131e1b1b70a854207ebf8" [[package]] name = "wait-timeout" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" dependencies = [ "libc", ] [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wild" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3131afc8c575281e1e80f36ed6a092aa502c08b18ed7524e86fbbb12bb410e1" dependencies = [ "glob", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets 0.52.4", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.4", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ "windows_aarch64_gnullvm 0.52.4", "windows_aarch64_msvc 0.52.4", "windows_i686_gnu 0.52.4", "windows_i686_msvc 0.52.4", "windows_x86_64_gnu 0.52.4", "windows_x86_64_gnullvm 0.52.4", "windows_x86_64_msvc 0.52.4", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "xattr" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", "linux-raw-sys 0.4.13", "rustix 0.38.32", ] [[package]] name = "xdg" version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" [[package]] name = "yaml-rust" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] 0707010000000C000081A400000000000000000000000166C4C37900000823000000000000000000000000000000000000001500000000lsd-1.1.5/Cargo.toml[package] authors = ["Peltoche <dev@halium.fr>"] build = "build.rs" categories = ["command-line-utilities"] description = "An ls command with a lot of pretty colors and some other stuff." keywords = ["ls"] license = "Apache-2.0" name = "lsd" readme = "./README.md" repository = "https://github.com/lsd-rs/lsd" version = "1.1.5" edition = "2021" rust-version = "1.74" [[bin]] name = "lsd" path = "src/main.rs" [build-dependencies] clap = { version = "4.3.*", features = ["derive"] } clap_complete = "4.3" version_check = "0.9.*" [dependencies] crossterm = { version = "0.27.0", features = ["serde"] } dirs = "5" libc = "0.2.*" human-sort = "0.2.2" # should stick to 0.1, the 0.2 needs some adaptation # check https://github.com/lsd-rs/lsd/issues/1014 term_grid = "0.1" terminal_size = "0.3" thiserror = "1.0" sys-locale = "0.3" once_cell = "1.17.1" chrono = { version = "0.4.19", features = ["unstable-locales"] } chrono-humanize = "0.2" # incompatible with v0.1.11 unicode-width = "0.1.13" lscolors = "0.16.0" wild = "2.0" globset = "0.4.*" yaml-rust = "0.4.*" serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.9" url = "2.1" vsort = "0.2" xdg = "2.5" [target."cfg(not(all(windows, target_arch = \"x86\", target_env = \"gnu\")))".dependencies] # if ssl feature is enabled compilation will fail on arm-unknown-linux-gnueabihf and i686-pc-windows-gnu git2 = { version = "0.18", optional = true, default-features = false } [target.'cfg(unix)'.dependencies] users = { version = "0.11.3", package = "uzers" } xattr = "1" [target.'cfg(windows)'.dependencies] windows = { version = "0.43.0", features = ["Win32_Foundation", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Memory"] } [dependencies.clap] features = ["derive", "wrap_help"] version = "4.3.*" [dev-dependencies] assert_cmd = "2" assert_fs = "1" predicates = "3" tempfile = "3" serial_test = "2.0" [features] default = ["git2"] sudo = [] no-git = [] # force disabling git even if available by default [profile.release] lto = true codegen-units = 1 strip = true debug = false 0707010000000D000081A400000000000000000000000166C4C37900002C5F000000000000000000000000000000000000001200000000lsd-1.1.5/LICENSE Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 0707010000000E000081A400000000000000000000000166C4C37900000048000000000000000000000000000000000000001100000000lsd-1.1.5/OWNERS# See the OWNERS docs at https://go.k8s.io/owners approvers: - zwpaper 0707010000000F000081A400000000000000000000000166C4C37900005801000000000000000000000000000000000000001400000000lsd-1.1.5/README.md<div align="center"> <p> <sup> <a href="https://github.com/sponsors/zwpaper">LSD is supported by the community.</a> </sup> </p> <sup>Special thanks to:</sup> <br> <br> <a href="https://www.warp.dev/?utm_source=github&utm_medium=referral&utm_campaign=lsd_20231001"> <div> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://github.com/lsd-rs/lsd/assets/3764335/61c7471f-ade1-42ea-9829-ac381c92b28a"> <source media="(prefers-color-scheme: light)" srcset="https://github.com/lsd-rs/lsd/assets/3764335/40e5d173-603c-45c8-90fa-50ba6c48a813"> <img alt="Warp" width="230" src="https://github.com/lsd-rs/lsd/assets/3764335/40e5d173-603c-45c8-90fa-50ba6c48a813"> </picture> </div> <b>Warp is a blazingly fast, Rust-based terminal reimagined to work like a modern app.</b> <div> <sup>Get more done in the CLI with real text editing, block-based output, and AI command search.</sup> </div> </a> <hr> </div> **IMPORTANT**: This is the development documents, please check the docs in [Tags](https://github.com/lsd-rs/lsd/tags) if you installed from the released ones. The current newest release is: [v1.1.5](https://github.com/lsd-rs/lsd/tree/v1.1.5) --- # LSD (LSDeluxe) [![license](http://img.shields.io/badge/license-Apache%20v2-blue.svg)](https://raw.githubusercontent.com/lsd-rs/lsd/master/LICENSE) [![Latest version](https://img.shields.io/crates/v/lsd.svg)](https://crates.io/crates/lsd) [![build](https://github.com/lsd-rs/lsd/workflows/CICD/badge.svg)](https://github.com/lsd-rs/lsd/actions) [![codecov](https://codecov.io/gh/lsd-rs/lsd/branch/master/graph/badge.svg)](https://codecov.io/gh/lsd-rs/lsd) [![versions](https://img.shields.io/repology/repositories/lsd)](https://repology.org/project/lsd/versions) ![image](https://raw.githubusercontent.com/lsd-rs/lsd/assets/screen_lsd.png) This project is a rewrite of GNU `ls` with lots of added features like colors, icons, tree-view, more formatting options etc. The project is heavily inspired by the super [colorls](https://github.com/athityakumar/colorls) project. ## Installation <details> <summary>Packaging status</summary> <a href="https://repology.org/project/lsd/versions"> <img src="https://repology.org/badge/vertical-allrepos/lsd.svg?columns=3" alt="Packaging status"> </a> </details> ### Prerequisites Install the patched fonts of powerline nerd-font and/or font-awesome. Have a look at the [Nerd Font README](https://github.com/ryanoasis/nerd-fonts/blob/master/readme.md) for more installation instructions. Don't forget to setup your terminal in order to use the correct font. | OS/Distro | Command | | ------------------------------- | -------------------------------------------------------------------------------| | Archlinux | `pacman -S lsd` | | Fedora | `dnf install lsd` | | Gentoo | `sudo emerge sys-apps/lsd` | | macOS | `brew install lsd` or `sudo port install lsd` | | NixOS | `nix-env -iA nixos.lsd` | | FreeBSD | `pkg install lsd` | | NetBSD or any `pkgsrc` platform | `pkgin install lsd` or `cd /usr/pkgsrc/sysutils/lsd && make install` | | OpenBSD | `pkg_add lsd` | | Windows | `scoop install lsd` or `winget install --id lsd-rs.lsd` or `choco install lsd` | | Android (via Termux) | `pkg install lsd` | | Debian sid and bookworm | `apt install lsd` | | Ubuntu 23.04 (Lunar Lobster) | `apt install lsd` | | Earlier Ubuntu/Debian versions | **snap discontinued**, use [From Binaries](#from-binaries) | | Solus | `eopkg it lsd` | | Void Linux | `sudo xbps-install lsd` | | openSUSE | `sudo zypper install lsd` | ### From source With Rust's package manager cargo, you can install lsd via: ```sh cargo install lsd ``` If you want to install the latest master branch commit: ```sh cargo install --git https://github.com/lsd-rs/lsd.git --branch master ``` ### From Binaries The [release page](https://github.com/lsd-rs/lsd/releases) includes precompiled binaries for Linux, macOS and Windows for every release. You can also get the latest binary of `master` branch from the [GitHub action build artifacts](https://github.com/lsd-rs/lsd/actions?query=branch%3Amaster+is%3Asuccess+event%3Apush) (choose the top action and scroll down to the artifacts section). ## Configuration `lsd` can be configured with a configuration file to set the default options. Check [Config file content](#config-file-content) for details. ### Config file location ### Non-Windows On non-Windows systems `lsd` follows the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) convention for the location of the configuration file. A `config.yaml` or `config.yml` file will be searched for in these locations, in order: - `$HOME/.config/lsd` - `$XDG_CONFIG_HOME/lsd` On most systems these are mapped to the same location, which is `~/.config/lsd/config.yaml`. ### Windows On Windows systems `lsd` searches for `config.yaml` or `config.yml` in the following locations, in order: - `%USERPROFILE%\.config\lsd` - `%APPDATA%\lsd` These are usually something like `C:\Users\username\AppData\Roaming\lsd\config.yaml` and `C:\Users\username\.config\lsd\config.yaml` respectively. ### Custom You can also provide a configuration file from a non-standard location: `lsd --config-file [PATH]` ### Config file content <details open> <summary>This is an example config file with the default values and some additional remarks.</summary> ```yaml # == Classic == # This is a shorthand to override some of the options to be backwards compatible # with `ls`. It affects the "color"->"when", "sorting"->"dir-grouping", "date" # and "icons"->"when" options. # Possible values: false, true classic: false # == Blocks == # This specifies the columns and their order when using the long and the tree # layout. # Possible values: permission, user, group, context, size, date, name, inode, links, git blocks: - permission - user - group - size - date - name # == Color == # This has various color options. (Will be expanded in the future.) color: # When to colorize the output. # When "classic" is set, this is set to "never". # Possible values: never, auto, always when: auto # How to colorize the output. # When "classic" is set, this is set to "no-color". # Possible values: default, custom # When "custom" is set, lsd will look in the config directory for `colors.yaml`. theme: default # == Date == # This specifies the date format for the date column. The freeform format # accepts a strftime like string. # When "classic" is set, this is set to "date". # Possible values: date, locale, relative, '+<date_format>' # `date_format` will be a `strftime` formatted value. e.g. `date: '+%d %b %y %X'` will give you a date like this: 17 Jun 21 20:14:55 date: date # == Dereference == # Whether to dereference symbolic links. # Possible values: false, true dereference: false # == Display == # What items to display. Do not specify this for the default behavior. # Possible values: all, almost-all, directory-only # display: all # == Icons == icons: # When to use icons. # When "classic" is set, this is set to "never". # Possible values: always, auto, never when: auto # Which icon theme to use. # Possible values: fancy, unicode theme: fancy # Separator between icon and the name # Default to 1 space separator: " " # == Ignore Globs == # A list of globs to ignore when listing. # ignore-globs: # - .git # == Indicators == # Whether to add indicator characters to certain listed files. # Possible values: false, true indicators: false # == Layout == # Which layout to use. "oneline" might be a bit confusing here and should be # called "one-per-line". It might be changed in the future. # Possible values: grid, tree, oneline layout: grid # == Recursion == recursion: # Whether to enable recursion. # Possible values: false, true enabled: false # How deep the recursion should go. This has to be a positive integer. Leave # it unspecified for (virtually) infinite. # depth: 3 # == Size == # Specifies the format of the size column. # Possible values: default, short, bytes size: default # == Permission == # Specify the format of the permission column # Possible value: rwx, octal, attributes (windows only), disable # permission: rwx # == Sorting == sorting: # Specify what to sort by. # Possible values: extension, name, time, size, version column: name # Whether to reverse the sorting. # Possible values: false, true reverse: false # Whether to group directories together and where. # When "classic" is set, this is set to "none". # Possible values: first, last, none dir-grouping: none # == No Symlink == # Whether to omit showing symlink targets # Possible values: false, true no-symlink: false # == Total size == # Whether to display the total size of directories. # Possible values: false, true total-size: false # == Hyperlink == # Attach hyperlink to filenames # Possible values: always, auto, never hyperlink: never # == Symlink arrow == # Specifies how the symlink arrow display, chars in both ascii and utf8 symlink-arrow: ⇒ # == Header == # Whether to display block headers. # Possible values: false, true header: false # == Literal == # Whether to show quotes on filenames. # Possible values: false, true literal: false # == Truncate owner == # How to truncate the username and group names for a file if they exceed a certain # number of characters. truncate-owner: # Number of characters to keep. By default, no truncation is done (empty value). after: # String to be appended to a name if truncated. marker: "" ``` </details> ## Theme `lsd` can be configured with theme files to set the colors or icons. ### Color Theme Color theme can be configured in the [configuration file](#configuration)(color.theme). The valid theme configurations are: - `default`: the default color scheme shipped in `lsd` - `custom`: use a custom color scheme defined in `colors.yaml` - *(deprecated) theme_file_name(yaml): use the theme file to specify colors (without the `yaml` extension)* When set to `custom`, `lsd` will look for `colors.yaml` in the XDG Base Directory, e.g. ~/.config/lsd/colors.yaml When configured with the `theme-file-name` which is a `yaml` file, `lsd` will look up the theme file in the following way: - relative name: check the XDG Base Directory, e.g. ~/.config/lsd/themes/<theme-file-name>.yaml - absolute name: use the file path and name to find theme file Check [Color Theme file content](#color-theme-file-content) for details. #### Color Theme file content Theme file use the [crossterm](https://crates.io/crates/crossterm) to configure the colors, check [crossterm](https://docs.rs/crossterm/0.20.0/crossterm/style/enum.Color.html) for supported colors. Color table: https://upload.wikimedia.org/wikipedia/commons/1/15/Xterm_256color_chart.svg Please notice that color values would ignore the case, both lowercase and UPPERCASE is supported. This is the default theme scheme shipped with `lsd`. ```yaml user: 230 group: 187 permission: read: dark_green write: dark_yellow exec: dark_red exec-sticky: 5 no-access: 245 octal: 6 acl: dark_cyan context: cyan date: hour-old: 40 day-old: 42 older: 36 size: none: 245 small: 229 medium: 216 large: 172 inode: valid: 13 invalid: 245 links: valid: 13 invalid: 245 tree-edge: 245 git-status: default: 245 unmodified: 245 ignored: 245 new-in-index: dark_green new-in-workdir: dark_green typechange: dark_yellow deleted: dark_red renamed: dark_green modified: dark_yellow conflicted: dark_red ``` When creating a theme for `lsd`, you can specify any part of the default theme, and then change its colors, the items missed would fall back to use the default colors. ### Icon Theme Icon theme can be configured in a fixed location, `$XDG_CONFIG_DIR/lsd/icons.yaml`, for example, `~/.config/lsd/icons.yaml` on macOS, please check [Config file location](#config-file-location) to make sure where is `$XDG_CONFIG_DIR`. As the file name indicated, the icon theme file is a `yaml` file. Check [Icon Theme file content](#icon-theme-file-content) for details. #### Icon Theme file content `lsd` support 3 kinds of icon overrides, by `name`, by `filetype` and by `extension`. The final set of icons used will be a combination of what is shipped with in `lsd` with overrides from config applied on top of it. *You can find the default set of icons [here](src/theme/icon.rs).* Both nerd font glyphs and Unicode emojis can be used for icons. You can find an example of icons customization below. ```yaml name: .trash: .cargo: .emacs.d: a.out: extension: go: hs: rs: 🦀 filetype: dir: 📂 file: 📄 pipe: 📩 socket: executable: symlink-dir: symlink-file: device-char: device-block: special: ``` ## External Configurations ### Required Enable nerd fonts for your terminal, URxvt for example in `.Xresources`: ```sh URxvt*font: xft:Hack Nerd Font:style=Regular:size=11 ``` ### Optional In order to use lsd when entering the `ls` command, you need to add this to your shell configuration file (~/.bashrc, ~/.zshrc, etc.): ```sh alias ls='lsd' ``` Some further examples of useful aliases: ```sh alias l='ls -l' alias la='ls -a' alias lla='ls -la' alias lt='ls --tree' ``` ## F.A.Q ### Uses unknown compression for member 'control.tar.zst' when using deb Zst compression is supported starting from `Debian 12` and `Ubuntu 21.10`, Please use the `_xz.deb` released starting from `lsd v1.1.0`. Please check https://github.com/lsd-rs/lsd/issues/891 for details or manual fixes. ### Custom Color Schemes for Windows For `lsd` currently, it reads a system environment variable called LS_COLORS. Please look at the marked solution in [this post](https://github.com/orgs/lsd-rs/discussions/958#discussioncomment-7659375), which contains a guide on how to set a color scheme. ### Icons not showing up For `lsd` to be able to display icons, the font has to include special font glyphs. This might not be the case for most fonts that you download. Thankfully, you can patch most fonts using [NerdFont](https://www.nerdfonts.com/) and add these icons. Or you can just download an already patched version of your favorite font from [NerdFont font download page](https://www.nerdfonts.com/font-downloads). Here is a guide on how to set up fonts on [macOS](https://github.com/lsd-rs/lsd/issues/199#issuecomment-494218334) and [Android](https://github.com/lsd-rs/lsd/issues/423). To check if the font you are using is set up correctly, try running the following snippet in a shell and see if that [prints a folder icon](https://github.com/lsd-rs/lsd/issues/510#issuecomment-860000306). If it prints a box, or question mark or something else, then you might have some issues in how you set up the font or how your terminal emulator renders the font. ```sh echo $'\uf115' ``` ### Icons missing or not rendering correctly using PuTTY/KiTTY on Windows First of all, make sure a patched font is installed and PuTTY/KiTTY is configured to use it, please check [Prerequisites](#prerequisites). There are problems for PuTTY/KiTTY to show 2 char wide icons, make sure using a 1 char wide font like [Hack Regular Nerd Font Complete Mono Windows Compatible](https://github.com/ryanoasis/nerd-fonts/blob/master/patched-fonts/Hack/Regular/complete/Hack%20Regular%20Nerd%20Font%20Complete%20Mono%20Windows%20Compatible.ttf), check [this issue](https://github.com/lsd-rs/lsd/issues/331) for detail. ### Colors You can customize filetype colors using `LS_COLORS` and other colors using the theme. The default colors are: | User/Group | Permission | File Type (changes based on your terminal colorscheme) | Date | File Size | | :-------------------------------------------------------------------- | :------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------- | :-------------------------------------------------------------------------- | | ![#ffffd7](https://via.placeholder.com/15/ffffd7/000000?text=+) User | ![#00d700](https://via.placeholder.com/15/00d700/000000?text=+) Read | ![#0087ff](https://via.placeholder.com/15/0087ff/000000?text=+) Directory | ![#00d700](https://via.placeholder.com/15/00d700/000000?text=+) within the last hour | ![#ffffaf](https://via.placeholder.com/15/ffffaf/000000?text=+) Small File | | ![#d7d7af](https://via.placeholder.com/15/d7d7af/000000?text=+) Group | ![#d7ff87](https://via.placeholder.com/15/d7ff87/000000?text=+) Write | ![#00d700](https://via.placeholder.com/15/00d700/000000?text=+) Executable File | ![#00d787](https://via.placeholder.com/15/00d787/000000?text=+) within the last day | ![#ffaf87](https://via.placeholder.com/15/ffaf87/000000?text=+) Medium File | | | ![#af0000](https://via.placeholder.com/15/af0000/000000?text=+) Execute | ![#ffffff](https://via.placeholder.com/15/ffffff/000000?text=+) Non-Executable File | ![#00af87](https://via.placeholder.com/15/00af87/000000?text=+) older | ![#d78700](https://via.placeholder.com/15/d78700/000000?text=+) Large File | | | ![#ff00ff](https://via.placeholder.com/15/ff00ff/000000?text=+) Execute with Stickybit | ![#af0000](https://via.placeholder.com/15/af0000/000000?text=+) Broken Symlink | | ![#ffffff](https://via.placeholder.com/15/ffffff/000000?text=+) Non File | | | ![#d75f87](https://via.placeholder.com/15/d75f87/000000?text=+) No Access | ![#00d7d7](https://via.placeholder.com/15/00d7d7/000000?text=+) Pipe/Symlink/Blockdevice/Socket/Special | | | | | | ![#d78700](https://via.placeholder.com/15/d78700/000000?text=+) CharDevice | | | _Checkout [trapd00r/LS_COLORS](https://github.com/trapd00r/LS_COLORS) and [sharkdp/vivid](https://github.com/sharkdp/vivid) for help in theming using `LS_COLORS`._ ### First char of folder/file getting trimmed Workaround for Konsole: ㅤEdit the config file (or [create it](#config-file-location) if it doesn't already exist) and paste the following into it (contains invisible Unicode characters): ```yml icons: separator: " ㅤ" ``` This is a known issue in a few terminal emulators. Try using a different terminal emulator like. [Alacritty](https://github.com/alacritty/alacritty) and [Kitty](https://github.com/kovidgoyal/kitty) are really good alternatives. You might also want to check if your font is responsible for causing this. To verify this, try running lsd with icons disabled and if it still does not have the first character, then this is an lsd bug: ```sh lsd --icon never --ignore-config ``` ### UTF-8 Chars `lsd` will try to display the UTF-8 chars in file name, A `U+FFFD REPLACEMENT CHARACTER`(�) is used to represent the invalid UTF-8 chars. ### Icons are showing up strangely Nerd Fonts is moving the code points of the Material Design Icons in 3.0, so lsd has updated the icons in #830. If your icons look weird, use fonts that have been patched using Nerd Fonts v2.3.0 or later. See also: <https://github.com/ryanoasis/nerd-fonts/releases/tag/v2.3.3> ## Contributors Everyone can contribute to this project, improving the code or adding functions. If anyone wants something to be added we will try to do it. As this is being updated regularly, don't forget to rebase your fork before creating a pull-request. ## Credits Special thanks to: - [meain](https://github.com/meain) for all his contributions and reviews - [danieldulaney](https://github.com/danieldulaney) for the Windows integration - [sharkdp](https://github.com/sharkdp) and his superb [fd](https://github.com/sharkdp/fd) from which I have stolen a lot of CI stuff. - [athityakumar](https://github.com/athityakumar) for the project [colorls](https://github.com/athityakumar/colorls) - [All the other contributors](https://github.com/lsd-rs/lsd/graphs/contributors) 07070100000010000081A400000000000000000000000166C4C37900000627000000000000000000000000000000000000001300000000lsd-1.1.5/build.rs// Copyright (c) 2017 fd developers // Licensed under the Apache License, Version 2.0 // <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> // or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>, // at your option. All files in the project carrying such // notice may not be copied, modified, or distributed except // according to those terms. use clap::CommandFactory; use clap_complete::generate_to; use clap_complete::shells::*; use std::fs; use std::process::exit; include!("src/app.rs"); fn main() { let outdir = std::env::var_os("SHELL_COMPLETIONS_DIR") .or_else(|| std::env::var_os("OUT_DIR")) .unwrap_or_else(|| exit(0)); fs::create_dir_all(&outdir).unwrap(); let mut app = Cli::command(); let bin_name = "lsd"; generate_to(Bash, &mut app, bin_name, &outdir).expect("Failed to generate Bash completions"); generate_to(Fish, &mut app, bin_name, &outdir).expect("Failed to generate Fish completions"); generate_to(Zsh, &mut app, bin_name, &outdir).expect("Failed to generate Zsh completions"); generate_to(PowerShell, &mut app, bin_name, &outdir) .expect("Failed to generate PowerShell completions"); // Disable git feature for these target where git2 is not well supported if !std::env::var("CARGO_FEATURE_GIT2") .map(|flag| flag == "1") .unwrap_or(false) || std::env::var("TARGET") .map(|target| target == "i686-pc-windows-gnu") .unwrap_or(false) { println!(r#"cargo:rustc-cfg=feature="no-git""#); } } 07070100000011000041ED00000000000000000000000266C4C37900000000000000000000000000000000000000000000000D00000000lsd-1.1.5/ci07070100000012000081A400000000000000000000000166C4C37900000D62000000000000000000000000000000000000002000000000lsd-1.1.5/ci/before_deploy.bash#!/usr/bin/env bash # Building and packaging for release set -ex build() { cargo build --target "$TARGET" --features="$FEATURES" --release --verbose } pack() { local tempdir local out_dir local package_name local gcc_prefix tempdir=$(mktemp -d 2>/dev/null || mktemp -d -t tmp) out_dir=$(pwd) package_name="$PROJECT_NAME-$TRAVIS_TAG-$TARGET" if [[ $TARGET == arm-unknown-linux-* ]]; then gcc_prefix="arm-linux-gnueabihf-" else gcc_prefix="" fi # create a "staging" directory mkdir "$tempdir/$package_name" mkdir "$tempdir/$package_name/autocomplete" # copying the main binary cp "target/$TARGET/release/$PROJECT_NAME" "$tempdir/$package_name/" "${gcc_prefix}"strip "$tempdir/$package_name/$PROJECT_NAME" # manpage, readme and license cp README.md "$tempdir/$package_name" cp LICENSE "$tempdir/$package_name" # various autocomplete cp target/"$TARGET"/release/build/"$PROJECT_NAME"-*/out/"$PROJECT_NAME".bash "$tempdir/$package_name/autocomplete/${PROJECT_NAME}.bash-completion" cp target/"$TARGET"/release/build/"$PROJECT_NAME"-*/out/"$PROJECT_NAME".fish "$tempdir/$package_name/autocomplete" cp target/"$TARGET"/release/build/"$PROJECT_NAME"-*/out/_"$PROJECT_NAME" "$tempdir/$package_name/autocomplete" # archiving pushd "$tempdir" tar czf "$out_dir/$package_name.tar.gz" "$package_name"/* popd rm -r "$tempdir" } make_deb() { local tempdir local architecture local version local dpkgname local conflictname case $TARGET in x86_64*) architecture=amd64 ;; i686*) architecture=i386 ;; *) echo "make_deb: skipping target '${TARGET}'" >&2 return 0 ;; esac version=${TRAVIS_TAG#v} if [[ $TARGET = *musl* ]]; then dpkgname=$PROJECT_NAME-musl conflictname=$PROJECT_NAME else dpkgname=$PROJECT_NAME conflictname=$PROJECT_NAME-musl fi tempdir=$(mktemp -d 2>/dev/null || mktemp -d -t tmp) # copy the main binary install -Dm755 "target/$TARGET/release/$PROJECT_NAME" "$tempdir/usr/bin/$PROJECT_NAME" strip "$tempdir/usr/bin/$PROJECT_NAME" # readme and license install -Dm644 README.md "$tempdir/usr/share/doc/$PROJECT_NAME/README.md" install -Dm644 LICENSE "$tempdir/usr/share/doc/$PROJECT_NAME/LICENSE" # completions install -Dm644 target/$TARGET/release/build/$PROJECT_NAME-*/out/$PROJECT_NAME.bash "$tempdir/usr/share/bash-completion/completions/${PROJECT_NAME}" install -Dm644 target/$TARGET/release/build/$PROJECT_NAME-*/out/$PROJECT_NAME.fish "$tempdir/usr/share/fish/completions/$PROJECT_NAME.fish" install -Dm644 target/$TARGET/release/build/$PROJECT_NAME-*/out/_$PROJECT_NAME "$tempdir/usr/share/zsh/vendor-completions/_$PROJECT_NAME" # Control file mkdir "$tempdir/DEBIAN" cat > "$tempdir/DEBIAN/control" <<EOF Package: $dpkgname Version: $version Section: utils Priority: optional Maintainer: Peltoche <peltoche@halium.fr> Architecture: $architecture Provides: $PROJECT_NAME Conflicts: $conflictname Description: A ls command with a lot of pretty colors. EOF fakeroot dpkg-deb --build "$tempdir" "${dpkgname}_${version}_${architecture}.deb" } main() { build pack if [[ $TARGET = *linux* ]]; then make_deb fi } main 07070100000013000081A400000000000000000000000166C4C37900000227000000000000000000000000000000000000002100000000lsd-1.1.5/ci/before_install.bash#!/usr/bin/env bash set -ex if [ "$TRAVIS_OS_NAME" != linux ]; then exit 0 fi sudo apt-get update # needed to build deb packages sudo apt-get install -y fakeroot # needed for i686 linux gnu target if [[ $TARGET == i686-unknown-linux-gnu ]]; then sudo apt-get install -y gcc-multilib fi # needed for cross-compiling for arm if [[ $TARGET == arm-unknown-linux-* ]]; then sudo apt-get install -y \ gcc-4.8-arm-linux-gnueabihf \ binutils-arm-linux-gnueabihf \ libc6-armhf-cross \ libc6-dev-armhf-cross fi 07070100000014000081A400000000000000000000000166C4C37900000109000000000000000000000000000000000000001900000000lsd-1.1.5/ci/script.bash#!/usr/bin/env bash set -ex # Incorporate TARGET env var to the build and test process cargo build --target "$TARGET" --verbose # We cannot run arm executables on linux if [[ $TARGET != arm-unknown-linux-* ]]; then cargo test --target "$TARGET" --verbose fi 07070100000015000041ED00000000000000000000000266C4C37900000000000000000000000000000000000000000000000E00000000lsd-1.1.5/doc07070100000016000081A400000000000000000000000166C4C37900001248000000000000000000000000000000000000001500000000lsd-1.1.5/doc/lsd.md--- title: lsd section: 1 header: User Manual footer: lsd <version> date: <date> --- # NAME lsd - LSDeluxe # SYNOPSIS `lsd [FLAGS] [OPTIONS] [--] [FILE]...` # DESCRIPTION lsd is a ls command with a lot of pretty colours and some other stuff to enrich and enhance the directory listing experience. # OPTIONS `-a`, `--all` : Do not ignore entries starting with **.** `-A`, `--almost-all` : Do not list implied **.** and **..** `--classic` : Enable classic mode (no colours or icons) `-L`, `--dereference` : When showing file information for a symbolic link, show information for the file the link references rather than for the link itself `-d`, `--directory-only` : Display directories themselves, and not their contents (recursively when used with --tree) `-X`, `--extensionsort` : Sort by file extension `--git` : Display git status. Directory git status is a reduction of included file statuses (recursively). `--help` : Prints help information `-h`, `--human-readable` : For ls compatibility purposes ONLY, currently set by default `--ignore-config` : Ignore the configuration file `--config-file <path>` : Provide the config file from a custom location `-F`, `--classify` : Append indicator (one of \*/=>@|) at the end of the file names `-i`, `--inode` : Display the index number of each file `-l`, `--long` : Display extended file metadata as a table `--no-symlink` : Do not display symlink target `-1`, `--oneline` : Display one entry per line `-R`, `--recursive` : Recurse into directories `-r`, `--reverse` : Reverse the order of the sort `-S`, `--sizesort` : Sort by size `-t`, `--timesort` : Sort by time modified `--total-size` : Display the total size of directories `--tree` : Recurse into directories and present the result as a tree `-V`, `--version` : Prints version information `-v`, `--versionsort` : Natural sort of (version) numbers within text `--blocks <blocks>...` : Specify the blocks that will be displayed and in what order [possible values: permission, user, group, size, date, name, inode, git] `--color <color>...` : When to use terminal colours [default: auto] [possible values: always, auto, never] `--date <date>...` : How to display date [possible values: date, locale, relative, +date-time-format] [default: date] `--depth <num>...` : Stop recursing into directories after reaching specified depth `--group-dirs <group-dirs>...` : Sort the directories then the files [default: none] [possible values: none, first, last] `--group-directories-first` : Groups the directories at the top before the files. Same as `--group-dirs=first` `--hyperlink <hyperlink>...` : Attach hyperlink to filenames [default: never] [possible values: always, auto, never] `--icon <icon>...` : When to print the icons [default: auto] [possible values: always, auto, never] `--icon-theme <icon-theme>...` : Whether to use fancy or unicode icons [default: fancy] [possible values: fancy, unicode] `-I, --ignore-glob <pattern>...` : Do not display files/directories with names matching the glob pattern(s). More than one can be specified by repeating the argument [default: ] `--permission <permission>...` : How to display permissions [default: rwx for linux, attributes for windows] [possible values: rwx, octal, attributes, disable] `--size <size>...` : How to display size [default: default] [possible values: default, short, bytes] `--sort <WORD>...` : Sort by WORD instead of name [possible values: size, time, version, extension, git] `-U`, `--no-sort` : Do not sort. List entries in directory order `-Z` `--context` : Display SELinux or SMACK security context `--header` : Display block headers `-N --literal` : Print entry names without quoting `--truncate-owner-after` : Truncate the user and group names if they exceed a certain number of characters `--truncate-owner-marker` : Truncation marker appended to a truncated user or group name # ARGS `<FILE>...` : A file or directory to list [default: .] # EXAMPLES `lsd` : Display listing for current directory `lsd /etc` : Display listing of /etc `lsd -la` : Display listing of current directory, including files starting with `.` and the current directory's entry. # ENVIRONMENT `LS_COLORS` : Used to determine color for displaying filenames. See **dir_colors**. `XDG_CONFIG_HOME` : Used to locate optional config file. If `XDG_CONFIG_HOME` is set, use `$XDG_CONFIG_HOME/lsd/config.yaml` else `$HOME/.config/lsd/config.yaml`. `SHELL_COMPLETIONS_DIR` or `OUT_DIR` : Used to specify the directory for generating a shell completions file. If neither are set, no completions file will be generated. The directory will be created if it does not exist. 07070100000017000081A400000000000000000000000166C4C3790000031D000000000000000000000000000000000000001300000000lsd-1.1.5/lsd.specName: lsd Version: 1.1.5 Release: 1%{?dist} Summary: The next gen ls command License: MIT URL: https://github.com/lsd-rs/lsd Source0: https://github.com/lsd-rs/lsd/archive/refs/tags/v%{version}.tar.gz BuildRequires: rust BuildRequires: cargo %description This project is a rewrite of GNU ls with lots of added features like colors, icons, tree-view, more formatting options etc. The project is heavily inspired by the super colorls project. %global debug_package %{nil} %prep %setup -q %build cargo build --release %install %global _build_id_links none mkdir -p %{buildroot}/%{_bindir} # upx "target/release/lsd" install -m 755 target/release/%{name} %{buildroot}/%{_bindir}/%{name} %files %defattr(-,root,root,-) %{_bindir}/%{name} 07070100000018000081A400000000000000000000000166C4C379000000B2000000000000000000000000000000000000001700000000lsd-1.1.5/rustfmt.toml# use empty config file to ensure that `rustfmt` will always use # the default configuration when formatting code # even if there is a `rustfmt.toml` file in parent directories 07070100000019000041ED00000000000000000000000266C4C37900000000000000000000000000000000000000000000000E00000000lsd-1.1.5/src0707010000001A000081A400000000000000000000000166C4C37900002724000000000000000000000000000000000000001500000000lsd-1.1.5/src/app.rsuse std::path::PathBuf; use clap::{ArgAction, Parser, ValueHint}; #[derive(Debug, Parser)] #[command(about, version, args_override_self = true, disable_help_flag = true)] pub struct Cli { #[arg(value_name = "FILE", default_value = ".", value_hint = ValueHint::AnyPath)] pub inputs: Vec<PathBuf>, /// Do not ignore entries starting with . . #[arg(short, long, overrides_with = "almost_all")] pub all: bool, /// Do not list implied . and .. #[arg(short = 'A', long)] pub almost_all: bool, /// When to use terminal colours [default: auto] #[arg(long, value_name = "MODE", value_parser = ["always", "auto", "never"])] pub color: Option<String>, /// When to print the icons [default: auto] #[arg(long, value_name = "MODE", value_parser = ["always", "auto", "never"])] pub icon: Option<String>, /// Whether to use fancy or unicode icons [default: fancy] #[arg(long, value_name = "THEME", value_parser = ["fancy", "unicode"])] pub icon_theme: Option<String>, /// Append indicator (one of */=>@|) at the end of the file names #[arg(short = 'F', long = "classify")] pub indicators: bool, /// Display extended file metadata as a table #[arg(short, long)] pub long: bool, /// Ignore the configuration file #[arg(long)] pub ignore_config: bool, /// Provide a custom lsd configuration file #[arg(long, value_name = "PATH")] pub config_file: Option<PathBuf>, /// Display one entry per line #[arg(short = '1', long)] pub oneline: bool, /// Recurse into directories #[arg(short = 'R', long, conflicts_with = "tree")] pub recursive: bool, /// For ls compatibility purposes ONLY, currently set by default #[arg(short, long)] human_readable: bool, /// Recurse into directories and present the result as a tree #[arg(long)] pub tree: bool, /// Stop recursing into directories after reaching specified depth #[arg(long, value_name = "NUM")] pub depth: Option<usize>, /// Display directories themselves, and not their contents (recursively when used with --tree) #[arg(short, long, conflicts_with_all = ["depth", "recursive"])] pub directory_only: bool, /// How to display permissions [default: rwx for linux, attributes for windows] #[arg(long, value_name = "MODE", value_parser = ["rwx", "octal", "attributes", "disable"])] pub permission: Option<String>, /// How to display size [default: default] #[arg(long, value_name = "MODE", value_parser = ["default", "short", "bytes"])] pub size: Option<String>, /// Display the total size of directories #[arg(long)] pub total_size: bool, /// How to display date [default: date] [possible values: date, locale, relative, +date-time-format] #[arg(long, value_parser = validate_date_argument)] pub date: Option<String>, /// Sort by time modified #[arg(short = 't', long)] pub timesort: bool, /// Sort by size #[arg(short = 'S', long)] pub sizesort: bool, /// Sort by file extension #[arg(short = 'X', long)] pub extensionsort: bool, /// Sort by git status #[arg(short = 'G', long)] pub gitsort: bool, /// Natural sort of (version) numbers within text #[arg(short = 'v', long)] pub versionsort: bool, /// Sort by TYPE instead of name #[arg( long, value_name = "TYPE", value_parser = ["size", "time", "version", "extension", "git", "none"], overrides_with_all = ["timesort", "sizesort", "extensionsort", "versionsort", "gitsort", "no_sort"] )] pub sort: Option<String>, /// Do not sort. List entries in directory order #[arg(short = 'U', long, overrides_with_all = ["timesort", "sizesort", "extensionsort", "versionsort", "gitsort", "sort"])] pub no_sort: bool, /// Reverse the order of the sort #[arg(short, long)] pub reverse: bool, /// Sort the directories then the files #[arg(long, value_name = "MODE", value_parser = ["none", "first", "last"])] pub group_dirs: Option<String>, /// Groups the directories at the top before the files. Same as --group-dirs=first #[arg(long)] pub group_directories_first: bool, /// Specify the blocks that will be displayed and in what order #[arg( long, value_delimiter = ',', value_parser = ["permission", "user", "group", "context", "size", "date", "name", "inode", "links", "git"], )] pub blocks: Vec<String>, /// Enable classic mode (display output similar to ls) #[arg(long)] pub classic: bool, /// Do not display symlink target #[arg(long)] pub no_symlink: bool, /// Do not display files/directories with names matching the glob pattern(s). /// More than one can be specified by repeating the argument #[arg(short = 'I', long, value_name = "PATTERN")] pub ignore_glob: Vec<String>, /// Display the index number of each file #[arg(short, long)] pub inode: bool, /// Show git status on file and directory" /// Only when used with --long option #[arg(short, long)] pub git: bool, /// When showing file information for a symbolic link, /// show information for the file the link references rather than for the link itself #[arg(short = 'L', long)] pub dereference: bool, /// Print security context (label) of each file #[arg(short = 'Z', long)] pub context: bool, /// Attach hyperlink to filenames [default: never] #[arg(long, value_name = "MODE", value_parser = ["always", "auto", "never"])] pub hyperlink: Option<String>, /// Display block headers #[arg(long)] pub header: bool, /// Truncate the user and group names if they exceed a certain number of characters #[arg(long, value_name = "NUM")] pub truncate_owner_after: Option<usize>, /// Truncation marker appended to a truncated user or group name #[arg(long, value_name = "STR")] pub truncate_owner_marker: Option<String>, /// Includes files with the windows system protection flag set. /// This is the same as --all on other platforms #[arg(long, hide = !cfg!(windows))] pub system_protected: bool, /// Print entry names without quoting #[arg(short = 'N', long)] pub literal: bool, /// Print help information #[arg(long, action = ArgAction::Help)] help: (), } fn validate_date_argument(arg: &str) -> Result<String, String> { if arg.starts_with('+') { validate_time_format(arg) } else if arg == "date" || arg == "relative" || arg == "locale" { Result::Ok(arg.to_owned()) } else { Result::Err("possible values: date, locale, relative, +date-time-format".to_owned()) } } pub fn validate_time_format(formatter: &str) -> Result<String, String> { let mut chars = formatter.chars(); loop { match chars.next() { Some('%') => match chars.next() { Some('.') => match chars.next() { Some('f') => (), Some(n @ ('3' | '6' | '9')) => match chars.next() { Some('f') => (), Some(c) => return Err(format!("invalid format specifier: %.{n}{c}")), None => return Err("missing format specifier".to_owned()), }, Some(c) => return Err(format!("invalid format specifier: %.{c}")), None => return Err("missing format specifier".to_owned()), }, Some(n @ (':' | '#')) => match chars.next() { Some('z') => (), Some(c) => return Err(format!("invalid format specifier: %{n}{c}")), None => return Err("missing format specifier".to_owned()), }, Some(n @ ('-' | '_' | '0')) => match chars.next() { Some( 'C' | 'd' | 'e' | 'f' | 'G' | 'g' | 'H' | 'I' | 'j' | 'k' | 'l' | 'M' | 'm' | 'S' | 's' | 'U' | 'u' | 'V' | 'W' | 'w' | 'Y' | 'y', ) => (), Some(c) => return Err(format!("invalid format specifier: %{n}{c}")), None => return Err("missing format specifier".to_owned()), }, Some( 'A' | 'a' | 'B' | 'b' | 'C' | 'c' | 'D' | 'd' | 'e' | 'F' | 'f' | 'G' | 'g' | 'H' | 'h' | 'I' | 'j' | 'k' | 'l' | 'M' | 'm' | 'n' | 'P' | 'p' | 'R' | 'r' | 'S' | 's' | 'T' | 't' | 'U' | 'u' | 'V' | 'v' | 'W' | 'w' | 'X' | 'x' | 'Y' | 'y' | 'Z' | 'z' | '+' | '%', ) => (), Some(n @ ('3' | '6' | '9')) => match chars.next() { Some('f') => (), Some(c) => return Err(format!("invalid format specifier: %{n}{c}")), None => return Err("missing format specifier".to_owned()), }, Some(c) => return Err(format!("invalid format specifier: %{c}")), None => return Err("missing format specifier".to_owned()), }, None => break, _ => continue, } } Ok(formatter.to_owned()) } // Wrapper for value_parser to simply remove non supported option (mainly git flag) // required since value_parser requires impl Into<ValueParser> that Vec do not support // should be located here, since this file is included by build.rs struct LabelFilter<Filter: Fn(&'static str) -> bool, const C: usize>([&'static str; C], Filter); impl<Filter: Fn(&'static str) -> bool, const C: usize> From<LabelFilter<Filter, C>> for clap::builder::ValueParser { fn from(label_filter: LabelFilter<Filter, C>) -> Self { let filter = label_filter.1; let values = label_filter.0.into_iter().filter(|x| filter(x)); let inner = clap::builder::PossibleValuesParser::from(values); Self::from(inner) } } 0707010000001B000081A400000000000000000000000166C4C37900003F1C000000000000000000000000000000000000001700000000lsd-1.1.5/src/color.rsuse crossterm::style::Color; use crossterm::style::{Attribute, ContentStyle, StyledContent, Stylize}; use lscolors::{Indicator, LsColors}; use std::path::Path; pub use crate::flags::color::ThemeOption; use crate::git::GitStatus; use crate::print_output; use crate::theme::{color::ColorTheme, Theme}; #[allow(dead_code)] #[derive(Hash, Debug, Eq, PartialEq, Clone)] pub enum Elem { /// Node type File { exec: bool, uid: bool, }, SymLink, BrokenSymLink, MissingSymLinkTarget, Dir { uid: bool, }, Pipe, BlockDevice, CharDevice, Socket, Special, /// Permission Read, Write, Exec, ExecSticky, NoAccess, Octal, Acl, Context, /// Attributes Archive, AttributeRead, Hidden, System, /// Last Time Modified DayOld, HourOld, Older, /// User / Group Name User, Group, /// File Size NonFile, FileLarge, FileMedium, FileSmall, /// INode INode { valid: bool, }, Links { valid: bool, }, TreeEdge, GitStatus { status: GitStatus, }, } impl Elem { fn has_suid(&self) -> bool { matches!(self, Elem::Dir { uid: true } | Elem::File { uid: true, .. }) } pub fn get_color(&self, theme: &ColorTheme) -> Color { match self { Elem::File { exec: true, uid: true, } => theme.file_type.file.exec_uid, Elem::File { exec: false, uid: true, } => theme.file_type.file.uid_no_exec, Elem::File { exec: true, uid: false, } => theme.file_type.file.exec_no_uid, Elem::File { exec: false, uid: false, } => theme.file_type.file.no_exec_no_uid, Elem::SymLink => theme.file_type.symlink.default, Elem::BrokenSymLink => theme.file_type.symlink.broken, Elem::MissingSymLinkTarget => theme.file_type.symlink.missing_target, Elem::Dir { uid: true } => theme.file_type.dir.uid, Elem::Dir { uid: false } => theme.file_type.dir.no_uid, Elem::Pipe => theme.file_type.pipe, Elem::BlockDevice => theme.file_type.block_device, Elem::CharDevice => theme.file_type.char_device, Elem::Socket => theme.file_type.socket, Elem::Special => theme.file_type.special, Elem::Read => theme.permission.read, Elem::Write => theme.permission.write, Elem::Exec => theme.permission.exec, Elem::ExecSticky => theme.permission.exec_sticky, Elem::NoAccess => theme.permission.no_access, Elem::Octal => theme.permission.octal, Elem::Acl => theme.permission.acl, Elem::Context => theme.permission.context, Elem::Archive => theme.attributes.archive, Elem::AttributeRead => theme.attributes.read, Elem::Hidden => theme.attributes.hidden, Elem::System => theme.attributes.system, Elem::DayOld => theme.date.day_old, Elem::HourOld => theme.date.hour_old, Elem::Older => theme.date.older, Elem::User => theme.user, Elem::Group => theme.group, Elem::NonFile => theme.size.none, Elem::FileLarge => theme.size.large, Elem::FileMedium => theme.size.medium, Elem::FileSmall => theme.size.small, Elem::INode { valid: true } => theme.inode.valid, Elem::INode { valid: false } => theme.inode.invalid, Elem::TreeEdge => theme.tree_edge, Elem::Links { valid: false } => theme.links.invalid, Elem::Links { valid: true } => theme.links.valid, Elem::GitStatus { status: GitStatus::Default, } => theme.git_status.default, Elem::GitStatus { status: GitStatus::Unmodified, } => theme.git_status.unmodified, Elem::GitStatus { status: GitStatus::Ignored, } => theme.git_status.ignored, Elem::GitStatus { status: GitStatus::NewInIndex, } => theme.git_status.new_in_index, Elem::GitStatus { status: GitStatus::NewInWorkdir, } => theme.git_status.new_in_workdir, Elem::GitStatus { status: GitStatus::Typechange, } => theme.git_status.typechange, Elem::GitStatus { status: GitStatus::Deleted, } => theme.git_status.deleted, Elem::GitStatus { status: GitStatus::Renamed, } => theme.git_status.renamed, Elem::GitStatus { status: GitStatus::Modified, } => theme.git_status.modified, Elem::GitStatus { status: GitStatus::Conflicted, } => theme.git_status.conflicted, } } } pub type ColoredString = StyledContent<String>; pub struct Colors { theme: Option<ColorTheme>, lscolors: Option<LsColors>, } impl Colors { pub fn new(t: ThemeOption) -> Self { let theme = match t { ThemeOption::NoColor => None, ThemeOption::Default | ThemeOption::NoLscolors => Some(Theme::default().color), ThemeOption::Custom => Some( Theme::from_path::<ColorTheme>(Path::new("colors").to_str().unwrap()) .unwrap_or_default(), ), ThemeOption::CustomLegacy(ref file) => { print_output!( "Warning: the 'themes' directory is deprecated, use 'colors.yaml' instead.\n\n" ); // TODO: drop the `themes` dir prefix, adding it here only for backwards compatibility Some( Theme::from_path::<ColorTheme>( Path::new("themes").join(file).to_str().unwrap_or(file), ) .unwrap_or_default(), ) } }; let lscolors = match t { ThemeOption::Default | ThemeOption::Custom | ThemeOption::CustomLegacy(_) => { Some(LsColors::from_env().unwrap_or_default()) } _ => None, }; Self { theme, lscolors } } pub fn colorize<S: Into<String>>(&self, input: S, elem: &Elem) -> ColoredString { self.style(elem).apply(input.into()) } pub fn colorize_using_path(&self, input: String, path: &Path, elem: &Elem) -> ColoredString { let style_from_path = self.style_from_path(path); match style_from_path { Some(style_from_path) => style_from_path.apply(input), None => self.colorize(input, elem), } } pub fn default_style() -> ContentStyle { ContentStyle::default() } fn style_from_path(&self, path: &Path) -> Option<ContentStyle> { match &self.lscolors { Some(lscolors) => lscolors.style_for_path(path).map(to_content_style), None => None, } } fn style(&self, elem: &Elem) -> ContentStyle { match &self.lscolors { Some(lscolors) => match self.get_indicator_from_elem(elem) { Some(style) => { let style = lscolors.style_for_indicator(style); style.map(to_content_style).unwrap_or_default() } None => self.style_default(elem), }, None => self.style_default(elem), } } fn style_default(&self, elem: &Elem) -> ContentStyle { if let Some(t) = &self.theme { let style_fg = ContentStyle::default().with(elem.get_color(t)); if elem.has_suid() { style_fg.on(Color::AnsiValue(124)) // Red3 } else { style_fg } } else { ContentStyle::default() } } fn get_indicator_from_elem(&self, elem: &Elem) -> Option<Indicator> { let indicator_string = match elem { Elem::File { exec, uid } => match (exec, uid) { (_, true) => None, (true, false) => Some("ex"), (false, false) => Some("fi"), }, Elem::Dir { uid } => { if *uid { None } else { Some("di") } } Elem::SymLink => Some("ln"), Elem::Pipe => Some("pi"), Elem::Socket => Some("so"), Elem::BlockDevice => Some("bd"), Elem::CharDevice => Some("cd"), Elem::BrokenSymLink => Some("or"), Elem::MissingSymLinkTarget => Some("mi"), _ => None, }; match indicator_string { Some(ids) => Indicator::from(ids), None => None, } } } fn to_content_style(ls: &lscolors::Style) -> ContentStyle { let to_crossterm_color = |c: &lscolors::Color| match c { lscolors::style::Color::RGB(r, g, b) => Color::Rgb { r: *r, g: *g, b: *b, }, lscolors::style::Color::Fixed(n) => Color::AnsiValue(*n), lscolors::style::Color::Black => Color::Black, lscolors::style::Color::Red => Color::DarkRed, lscolors::style::Color::Green => Color::DarkGreen, lscolors::style::Color::Yellow => Color::DarkYellow, lscolors::style::Color::Blue => Color::DarkBlue, lscolors::style::Color::Magenta => Color::DarkMagenta, lscolors::style::Color::Cyan => Color::DarkCyan, lscolors::style::Color::White => Color::Grey, lscolors::style::Color::BrightBlack => Color::DarkGrey, lscolors::style::Color::BrightRed => Color::Red, lscolors::style::Color::BrightGreen => Color::Green, lscolors::style::Color::BrightYellow => Color::Yellow, lscolors::style::Color::BrightBlue => Color::Blue, lscolors::style::Color::BrightMagenta => Color::Magenta, lscolors::style::Color::BrightCyan => Color::Cyan, lscolors::style::Color::BrightWhite => Color::White, }; let mut style = ContentStyle { foreground_color: ls.foreground.as_ref().map(to_crossterm_color), background_color: ls.background.as_ref().map(to_crossterm_color), ..ContentStyle::default() }; if ls.font_style.bold { style.attributes.set(Attribute::Bold); } if ls.font_style.dimmed { style.attributes.set(Attribute::Dim); } if ls.font_style.italic { style.attributes.set(Attribute::Italic); } if ls.font_style.underline { style.attributes.set(Attribute::Underlined); } if ls.font_style.rapid_blink { style.attributes.set(Attribute::RapidBlink); } if ls.font_style.slow_blink { style.attributes.set(Attribute::SlowBlink); } if ls.font_style.reverse { style.attributes.set(Attribute::Reverse); } if ls.font_style.hidden { style.attributes.set(Attribute::Hidden); } if ls.font_style.strikethrough { style.attributes.set(Attribute::CrossedOut); } style } #[cfg(test)] mod tests { use super::Colors; use crate::color::ThemeOption; use crate::theme::color::ColorTheme; #[test] fn test_color_new_no_color_theme() { assert!(Colors::new(ThemeOption::NoColor).theme.is_none()); } #[test] fn test_color_new_custom_theme() { assert_eq!( Colors::new(ThemeOption::Custom).theme, Some(ColorTheme::default_dark()), ); } #[test] fn test_color_new_custom_no_file_theme() { assert_eq!( Colors::new(ThemeOption::Custom).theme, Some(ColorTheme::default_dark()), ); } #[test] fn test_color_new_bad_legacy_custom_theme() { assert_eq!( Colors::new(ThemeOption::CustomLegacy("not-existed".to_string())).theme, Some(ColorTheme::default_dark()), ); } } #[cfg(test)] mod elem { use super::Elem; use crate::theme::{color, color::ColorTheme}; use crossterm::style::Color; #[cfg(test)] fn test_theme() -> ColorTheme { ColorTheme { user: Color::AnsiValue(230), // Cornsilk1 group: Color::AnsiValue(187), // LightYellow3 permission: color::Permission { read: Color::Green, write: Color::Yellow, exec: Color::Red, exec_sticky: Color::Magenta, no_access: Color::AnsiValue(245), // Grey octal: Color::AnsiValue(6), acl: Color::DarkCyan, context: Color::Cyan, }, attributes: color::Attributes { read: Color::Green, archive: Color::Yellow, hidden: Color::Red, system: Color::Magenta, }, file_type: color::FileType { file: color::File { exec_uid: Color::AnsiValue(40), // Green3 uid_no_exec: Color::AnsiValue(184), // Yellow3 exec_no_uid: Color::AnsiValue(40), // Green3 no_exec_no_uid: Color::AnsiValue(184), // Yellow3 }, dir: color::Dir { uid: Color::AnsiValue(33), // DodgerBlue1 no_uid: Color::AnsiValue(33), // DodgerBlue1 }, pipe: Color::AnsiValue(44), // DarkTurquoise symlink: color::Symlink { default: Color::AnsiValue(44), // DarkTurquoise broken: Color::AnsiValue(124), // Red3 missing_target: Color::AnsiValue(124), // Red3 }, block_device: Color::AnsiValue(44), // DarkTurquoise char_device: Color::AnsiValue(172), // Orange3 socket: Color::AnsiValue(44), // DarkTurquoise special: Color::AnsiValue(44), // DarkTurquoise }, date: color::Date { hour_old: Color::AnsiValue(40), // Green3 day_old: Color::AnsiValue(42), // SpringGreen2 older: Color::AnsiValue(36), // DarkCyan }, size: color::Size { none: Color::AnsiValue(245), // Grey small: Color::AnsiValue(229), // Wheat1 medium: Color::AnsiValue(216), // LightSalmon1 large: Color::AnsiValue(172), // Orange3 }, inode: color::INode { valid: Color::AnsiValue(13), // Pink invalid: Color::AnsiValue(245), // Grey }, links: color::Links { valid: Color::AnsiValue(13), // Pink invalid: Color::AnsiValue(245), // Grey }, tree_edge: Color::AnsiValue(245), // Grey git_status: Default::default(), } } #[test] fn test_default_theme_color() { assert_eq!( Elem::File { exec: true, uid: true } .get_color(&test_theme()), Color::AnsiValue(40), ); assert_eq!( Elem::File { exec: false, uid: true } .get_color(&test_theme()), Color::AnsiValue(184), ); assert_eq!( Elem::File { exec: true, uid: false } .get_color(&test_theme()), Color::AnsiValue(40), ); assert_eq!( Elem::File { exec: false, uid: false } .get_color(&test_theme()), Color::AnsiValue(184), ); } } 0707010000001C000081A400000000000000000000000166C4C37900003629000000000000000000000000000000000000001D00000000lsd-1.1.5/src/config_file.rs//! This module provides methods to handle the program's config files and //! operations related to this. use crate::flags::display::Display; use crate::flags::icons::{IconOption, IconTheme}; use crate::flags::layout::Layout; use crate::flags::permission::PermissionFlag; use crate::flags::size::SizeFlag; use crate::flags::sorting::{DirGrouping, SortColumn}; use crate::flags::HyperlinkOption; use crate::flags::{ColorOption, ThemeOption}; use crate::print_error; use std::path::{Path, PathBuf}; use serde::Deserialize; use std::fs; use std::io; /// A struct to hold an optional configuration items, and provides methods /// around error handling in a config file. #[derive(Eq, PartialEq, Debug, Deserialize)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] pub struct Config { pub classic: Option<bool>, pub blocks: Option<Vec<String>>, pub color: Option<Color>, pub date: Option<String>, pub dereference: Option<bool>, pub display: Option<Display>, pub icons: Option<Icons>, pub ignore_globs: Option<Vec<String>>, pub indicators: Option<bool>, pub layout: Option<Layout>, pub recursion: Option<Recursion>, pub size: Option<SizeFlag>, pub permission: Option<PermissionFlag>, pub sorting: Option<Sorting>, pub no_symlink: Option<bool>, pub total_size: Option<bool>, pub symlink_arrow: Option<String>, pub hyperlink: Option<HyperlinkOption>, pub header: Option<bool>, pub literal: Option<bool>, pub truncate_owner: Option<TruncateOwner>, } #[derive(Eq, PartialEq, Debug, Deserialize)] pub struct Color { pub when: Option<ColorOption>, pub theme: Option<ThemeOption>, } #[derive(Eq, PartialEq, Debug, Deserialize)] pub struct Icons { pub when: Option<IconOption>, pub theme: Option<IconTheme>, pub separator: Option<String>, } #[derive(Eq, PartialEq, Debug, Deserialize)] pub struct Recursion { pub enabled: Option<bool>, pub depth: Option<usize>, } #[derive(Eq, PartialEq, Debug, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Sorting { pub column: Option<SortColumn>, pub reverse: Option<bool>, pub dir_grouping: Option<DirGrouping>, } #[derive(Eq, PartialEq, Debug, Deserialize)] pub struct TruncateOwner { pub after: Option<usize>, pub marker: Option<String>, } /// This expand the `~` in path to HOME dir /// returns the origin one if no `~` found; /// returns None if error happened when getting home dir /// /// Implementing this to reuse the `dirs` dependency, avoid adding new one pub fn expand_home<P: AsRef<Path>>(path: P) -> Option<PathBuf> { let p = path.as_ref(); if !p.starts_with("~") { return Some(p.to_path_buf()); } if p == Path::new("~") { return dirs::home_dir(); } dirs::home_dir().map(|mut h| { if h == Path::new("/") { // Corner case: `h` root directory; // don't prepend extra `/`, just drop the tilde. p.strip_prefix("~").unwrap().to_path_buf() } else { h.push(p.strip_prefix("~/").unwrap()); h } }) } impl Config { /// This constructs a Config struct with all None pub fn with_none() -> Self { Self { classic: None, blocks: None, color: None, date: None, dereference: None, display: None, icons: None, ignore_globs: None, indicators: None, layout: None, recursion: None, size: None, permission: None, sorting: None, no_symlink: None, total_size: None, symlink_arrow: None, hyperlink: None, header: None, literal: None, truncate_owner: None, } } /// This constructs a Config struct with a passed file path. pub fn from_file<P: AsRef<Path>>(file: P) -> Option<Self> { let file = file.as_ref(); match fs::read(file) { Ok(f) => match Self::from_yaml(&String::from_utf8_lossy(&f)) { Ok(c) => Some(c), Err(e) => { print_error!( "Configuration file {} format error, {}.", file.to_string_lossy(), e ); None } }, Err(e) => { if e.kind() != io::ErrorKind::NotFound { print_error!( "Can not open config file {}: {}.", file.to_string_lossy(), e ); } None } } } /// This constructs a Config struct with a passed [Yaml] str. /// If error happened, return the [serde_yaml::Error]. fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> { serde_yaml::from_str::<Self>(yaml) } /// Config paths for non-Windows platforms will be read from /// `$XDG_CONFIG_HOME/lsd` or `$HOME/.config/lsd` /// (usually, those are the same) in that order. /// The default paths for Windows will be read from /// `%APPDATA%\lsd` or `%USERPROFILE%\.config\lsd` in that order. /// This will apply both to the config file and the theme file. pub fn config_paths() -> impl Iterator<Item = PathBuf> { #[cfg(not(windows))] use xdg::BaseDirectories; [ dirs::home_dir().map(|h| h.join(".config")), dirs::config_dir(), #[cfg(not(windows))] BaseDirectories::with_prefix("") .ok() .map(|p| p.get_config_home()), ] .iter() .filter_map(|p| p.as_ref().map(|p| p.join("lsd"))) .collect::<Vec<_>>() .into_iter() } } impl Default for Config { /// Try to find either config.yaml or config.yml in the config directories /// and use the first one that is found. If none are found, or the parsing fails, /// use the default from DEFAULT_CONFIG. fn default() -> Self { Config::config_paths() .find_map(|p| { let yaml = p.join("config.yaml"); let yml = p.join("config.yml"); if yaml.is_file() { Config::from_file(yaml) } else if yml.is_file() { Config::from_file(yml) } else { None } }) .or(Self::from_yaml(DEFAULT_CONFIG).ok()) .expect("Failed to read both config file and default config") } } const DEFAULT_CONFIG: &str = r#"--- # == Classic == # This is a shorthand to override some of the options to be backwards compatible # with `ls`. It affects the "color"->"when", "sorting"->"dir-grouping", "date" # and "icons"->"when" options. # Possible values: false, true classic: false # == Blocks == # This specifies the columns and their order when using the long and the tree # layout. # Possible values: permission, user, group, context, size, date, name, inode, git blocks: - permission - user - group - size - date - name # == Color == # This has various color options. (Will be expanded in the future.) color: # When to colorize the output. # When "classic" is set, this is set to "never". # Possible values: never, auto, always when: auto # How to colorize the output. # When "classic" is set, this is set to "no-color". # Possible values: default, no-color, no-lscolors, <theme-file-name> # when specifying <theme-file-name>, lsd will look up theme file in # XDG Base Directory if relative # The file path if absolute theme: default # == Date == # This specifies the date format for the date column. The freeform format # accepts an strftime like string. # When "classic" is set, this is set to "date". # Possible values: date, locale, relative, +<date_format> # date: date # == Dereference == # Whether to dereference symbolic links. # Possible values: false, true dereference: false # == Display == # What items to display. Do not specify this for the default behavior. # Possible values: all, almost-all, directory-only # display: all # == Icons == icons: # When to use icons. # When "classic" is set, this is set to "never". # Possible values: always, auto, never when: auto # Which icon theme to use. # Possible values: fancy, unicode theme: fancy # The string between the icons and the name. # Possible values: any string (eg: " |") separator: " " # == Ignore Globs == # A list of globs to ignore when listing. # ignore-globs: # - .git # == Indicators == # Whether to add indicator characters to certain listed files. # Possible values: false, true indicators: false # == Layout == # Which layout to use. "oneline" might be a bit confusing here and should be # called "one-per-line". It might be changed in the future. # Possible values: grid, tree, oneline layout: grid # == Recursion == recursion: # Whether to enable recursion. # Possible values: false, true enabled: false # How deep the recursion should go. This has to be a positive integer. Leave # it unspecified for (virtually) infinite. # depth: 3 # == Size == # Specifies the format of the size column. # Possible values: default, short, bytes size: default # == Permission == # Specify the format of the permission column. # Possible value: rwx, octal, attributes, disable # permission: rwx # == Sorting == sorting: # Specify what to sort by. # Possible values: extension, name, time, size, version column: name # Whether to reverse the sorting. # Possible values: false, true reverse: false # Whether to group directories together and where. # When "classic" is set, this is set to "none". # Possible values: first, last, none dir-grouping: none # == No Symlink == # Whether to omit showing symlink targets # Possible values: false, true no-symlink: false # == Total size == # Whether to display the total size of directories. # Possible values: false, true total-size: false # == Hyperlink == # Whether to display the total size of directories. # Possible values: always, auto, never hyperlink: never # == Symlink arrow == # Specifies how the symlink arrow display, chars in both ascii and utf8 symlink-arrow: ⇒ # == Literal == # Whether to print entry names without quoting # Possible values: false, true literal: false # == Truncate owner == # How to truncate the username and group name for the file if they exceed a # certain number of characters. truncate-owner: # Number of characters to keep. By default, no truncation is done (empty value). after: # String to be appended to a name if truncated. marker: "" "#; #[cfg(test)] impl Config { pub fn builtin() -> Self { Self::from_yaml(DEFAULT_CONFIG).unwrap() } } #[cfg(test)] mod tests { use super::Config; use crate::config_file; use crate::flags::color::{ColorOption, ThemeOption}; use crate::flags::icons::{IconOption, IconTheme}; use crate::flags::layout::Layout; use crate::flags::size::SizeFlag; use crate::flags::sorting::{DirGrouping, SortColumn}; use crate::flags::HyperlinkOption; #[test] fn test_read_default() { let c = Config::from_yaml(config_file::DEFAULT_CONFIG).unwrap(); assert_eq!( Config { classic: Some(false), blocks: Some(vec![ "permission".into(), "user".into(), "group".into(), "size".into(), "date".into(), "name".into(), ]), color: Some(config_file::Color { when: Some(ColorOption::Auto), theme: Some(ThemeOption::Default) }), date: None, dereference: Some(false), display: None, icons: Some(config_file::Icons { when: Some(IconOption::Auto), theme: Some(IconTheme::Fancy), separator: Some(" ".to_string()), }), ignore_globs: None, indicators: Some(false), layout: Some(Layout::Grid), recursion: Some(config_file::Recursion { enabled: Some(false), depth: None, }), size: Some(SizeFlag::Default), permission: None, sorting: Some(config_file::Sorting { column: Some(SortColumn::Name), reverse: Some(false), dir_grouping: Some(DirGrouping::None), }), no_symlink: Some(false), total_size: Some(false), symlink_arrow: Some("⇒".into()), hyperlink: Some(HyperlinkOption::Never), header: None, literal: Some(false), truncate_owner: Some(config_file::TruncateOwner { after: None, marker: Some("".to_string()), }), }, c ); } #[test] fn test_read_config_ok() { let c = Config::from_yaml("classic: true").unwrap(); assert!(c.classic.unwrap()) } #[test] fn test_read_config_bad_bool() { let c = Config::from_yaml("classic: notbool"); assert!(c.is_err()) } #[test] fn test_read_config_file_not_found() { let c = Config::from_file("not-existed"); assert!(c.is_none()) } #[test] fn test_read_bad_display() { assert!(Config::from_yaml("display: bad").is_err()) } } 0707010000001D000081A400000000000000000000000166C4C379000019A7000000000000000000000000000000000000001600000000lsd-1.1.5/src/core.rsuse crate::color::Colors; use crate::display; use crate::flags::{ ColorOption, Display, Flags, HyperlinkOption, Layout, Literal, SortOrder, ThemeOption, }; use crate::git::GitCache; use crate::icon::Icons; use crate::meta::Meta; use crate::{print_error, print_output, sort, ExitCode}; use std::path::PathBuf; #[cfg(not(target_os = "windows"))] use std::io; #[cfg(not(target_os = "windows"))] use std::os::unix::io::AsRawFd; use crate::flags::blocks::Block; use crate::git_theme::GitTheme; #[cfg(target_os = "windows")] use terminal_size::terminal_size; pub struct Core { flags: Flags, icons: Icons, colors: Colors, git_theme: GitTheme, sorters: Vec<(SortOrder, sort::SortFn)>, } impl Core { pub fn new(mut flags: Flags) -> Self { // Check through libc if stdout is a tty. Unix specific so not on windows. // Determine color output availability (and initialize color output (for Windows 10)) #[cfg(not(target_os = "windows"))] let tty_available = unsafe { libc::isatty(io::stdout().as_raw_fd()) == 1 }; #[cfg(not(target_os = "windows"))] let console_color_ok = true; #[cfg(target_os = "windows")] let tty_available = terminal_size().is_some(); // terminal_size allows us to know if the stdout is a tty or not. #[cfg(target_os = "windows")] let console_color_ok = crossterm::ansi_support::supports_ansi(); let color_theme = match (tty_available && console_color_ok, flags.color.when) { (_, ColorOption::Never) | (false, ColorOption::Auto) => ThemeOption::NoColor, _ => flags.color.theme.clone(), }; let icon_when = flags.icons.when; let icon_theme = flags.icons.theme.clone(); // TODO: Rework this so that flags passed downstream does not // have Auto option for any (icon, color, hyperlink). if matches!(flags.hyperlink, HyperlinkOption::Auto) { flags.hyperlink = if tty_available { HyperlinkOption::Always } else { HyperlinkOption::Never } } let icon_separator = flags.icons.separator.0.clone(); // The output is not a tty, this means the command is piped. e.g. // // lsd -l | less // // Most of the programs does not handle correctly the ansi colors // or require a raw output (like the `wc` command). if !tty_available { // we should not overwrite the tree layout if flags.layout != Layout::Tree { flags.layout = Layout::OneLine; } flags.literal = Literal(true); }; let sorters = sort::assemble_sorters(&flags); Self { flags, colors: Colors::new(color_theme), icons: Icons::new(tty_available, icon_when, icon_theme, icon_separator), git_theme: GitTheme::new(), sorters, } } pub fn run(self, paths: Vec<PathBuf>) -> ExitCode { let (mut meta_list, exit_code) = self.fetch(paths); self.sort(&mut meta_list); self.display(&meta_list); exit_code } fn fetch(&self, paths: Vec<PathBuf>) -> (Vec<Meta>, ExitCode) { let mut exit_code = ExitCode::OK; let mut meta_list = Vec::with_capacity(paths.len()); let depth = match self.flags.layout { Layout::Tree { .. } => self.flags.recursion.depth, _ if self.flags.recursion.enabled => self.flags.recursion.depth, _ => 1, }; #[cfg(target_os = "windows")] use crate::config_file; #[cfg(target_os = "windows")] let paths: Vec<PathBuf> = paths .into_iter() .filter_map(config_file::expand_home) .collect(); for path in paths { let mut meta = match Meta::from_path(&path, self.flags.dereference.0, self.flags.permission) { Ok(meta) => meta, Err(err) => { print_error!("{}: {}.", path.display(), err); exit_code.set_if_greater(ExitCode::MajorIssue); continue; } }; let cache = if self.flags.blocks.0.contains(&Block::GitStatus) { Some(GitCache::new(&path)) } else { None }; let recurse = self.flags.layout == Layout::Tree || self.flags.display != Display::DirectoryOnly; if recurse { match meta.recurse_into(depth, &self.flags, cache.as_ref()) { Ok((content, path_exit_code)) => { meta.content = content; meta.git_status = cache.and_then(|cache| cache.get(&meta.path, true)); meta_list.push(meta); exit_code.set_if_greater(path_exit_code); } Err(err) => { print_error!("lsd: {}: {}\n", path.display(), err); exit_code.set_if_greater(ExitCode::MinorIssue); continue; } }; } else { meta.git_status = cache.and_then(|cache| cache.get(&meta.path, true)); meta_list.push(meta); }; } // Only calculate the total size of a directory if it will be displayed if self.flags.total_size.0 && self.flags.blocks.displays_size() { for meta in &mut meta_list.iter_mut() { meta.calculate_total_size(); } } (meta_list, exit_code) } fn sort(&self, metas: &mut Vec<Meta>) { metas.sort_unstable_by(|a, b| sort::by_meta(&self.sorters, a, b)); for meta in metas { if let Some(ref mut content) = meta.content { self.sort(content); } } } fn display(&self, metas: &[Meta]) { let output = if self.flags.layout == Layout::Tree { display::tree( metas, &self.flags, &self.colors, &self.icons, &self.git_theme, ) } else { display::grid( metas, &self.flags, &self.colors, &self.icons, &self.git_theme, ) }; print_output!("{}", output); } } 0707010000001E000081A400000000000000000000000166C4C37900007C2F000000000000000000000000000000000000001900000000lsd-1.1.5/src/display.rsuse crate::color::{Colors, Elem}; use crate::flags::blocks::Block; use crate::flags::{Display, Flags, HyperlinkOption, Layout}; use crate::git_theme::GitTheme; use crate::icon::Icons; use crate::meta::name::DisplayOption; use crate::meta::{FileType, Meta, OwnerCache}; use std::collections::HashMap; use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; use terminal_size::terminal_size; use unicode_width::UnicodeWidthStr; const EDGE: &str = "\u{251c}\u{2500}\u{2500}"; // "├──" const LINE: &str = "\u{2502} "; // "│ " const CORNER: &str = "\u{2514}\u{2500}\u{2500}"; // "└──" const BLANK: &str = " "; pub fn grid( metas: &[Meta], flags: &Flags, colors: &Colors, icons: &Icons, git_theme: &GitTheme, ) -> String { let term_width = terminal_size().map(|(w, _)| w.0 as usize); let owner_cache = OwnerCache::default(); inner_display_grid( &DisplayOption::None, metas, &owner_cache, flags, colors, icons, git_theme, 0, term_width, ) } pub fn tree( metas: &[Meta], flags: &Flags, colors: &Colors, icons: &Icons, git_theme: &GitTheme, ) -> String { let mut grid = Grid::new(GridOptions { filling: Filling::Spaces(1), direction: Direction::LeftToRight, }); let padding_rules = get_padding_rules(metas, flags); let mut index = 0; for (i, block) in flags.blocks.0.iter().enumerate() { if block == &Block::Name { index = i; break; } } let owner_cache = OwnerCache::default(); for cell in inner_display_tree( metas, &owner_cache, flags, colors, icons, git_theme, (0, ""), &padding_rules, index, ) { grid.add(cell); } grid.fit_into_columns(flags.blocks.0.len()).to_string() } #[allow(clippy::too_many_arguments)] // should wrap flags, colors, icons, git_theme into one struct fn inner_display_grid( display_option: &DisplayOption, metas: &[Meta], owner_cache: &OwnerCache, flags: &Flags, colors: &Colors, icons: &Icons, git_theme: &GitTheme, depth: usize, term_width: Option<usize>, ) -> String { let mut output = String::new(); let mut cells = Vec::new(); let padding_rules = get_padding_rules(metas, flags); let mut grid = match flags.layout { Layout::OneLine => Grid::new(GridOptions { filling: Filling::Spaces(1), direction: Direction::LeftToRight, }), _ => Grid::new(GridOptions { filling: Filling::Spaces(2), direction: Direction::TopToBottom, }), }; // The first iteration (depth == 0) corresponds to the inputs given by the // user. We defer displaying directories given by the user unless we've been // asked to display the directory itself (rather than its contents). let skip_dirs = (depth == 0) && (flags.display != Display::DirectoryOnly); // print the files first. for meta in metas { // Maybe skip showing the directory meta now; show its contents later. if skip_dirs && (matches!(meta.file_type, FileType::Directory { .. }) || (matches!(meta.file_type, FileType::SymLink { is_dir: true })) && flags.blocks.0.len() == 1) { continue; } let blocks = get_output( meta, owner_cache, colors, icons, git_theme, flags, display_option, &padding_rules, (0, ""), ); for block in blocks { cells.push(Cell { width: get_visible_width(&block, flags.hyperlink == HyperlinkOption::Always), contents: block, }); } } // Print block headers if flags.header.0 && flags.layout == Layout::OneLine && !cells.is_empty() { add_header(flags, &cells, &mut grid); } for cell in cells { grid.add(cell); } if flags.layout == Layout::Grid { if let Some(tw) = term_width { if let Some(gridded_output) = grid.fit_into_width(tw) { output += &gridded_output.to_string(); } else { //does not fit into grid, usually because (some) filename(s) //are longer or almost as long as term_width //print line by line instead! output += &grid.fit_into_columns(1).to_string(); } } else { output += &grid.fit_into_columns(1).to_string(); } } else { output += &grid.fit_into_columns(flags.blocks.0.len()).to_string(); } let should_display_folder_path = should_display_folder_path(depth, metas); // print the folder content for meta in metas { if let Some(content) = &meta.content { if should_display_folder_path { output += &display_folder_path(meta); } let display_option = DisplayOption::Relative { base_path: &meta.path, }; output += &inner_display_grid( &display_option, content, owner_cache, flags, colors, icons, git_theme, depth + 1, term_width, ); } } output } fn add_header(flags: &Flags, cells: &[Cell], grid: &mut Grid) { let num_columns: usize = flags.blocks.0.len(); let mut widths = flags .blocks .0 .iter() .map(|b| get_visible_width(b.get_header(), flags.hyperlink == HyperlinkOption::Always)) .collect::<Vec<usize>>(); // find max widths of each column for (index, cell) in cells.iter().enumerate() { let index = index % num_columns; widths[index] = std::cmp::max(widths[index], cell.width); } for (idx, block) in flags.blocks.0.iter().enumerate() { // center and underline header let underlined_header = crossterm::style::Stylize::attribute( format!("{: ^1$}", block.get_header(), widths[idx]), crossterm::style::Attribute::Underlined, ) .to_string(); grid.add(Cell { width: widths[idx], contents: underlined_header, }); } } #[allow(clippy::too_many_arguments)] fn inner_display_tree( metas: &[Meta], owner_cache: &OwnerCache, flags: &Flags, colors: &Colors, icons: &Icons, git_theme: &GitTheme, tree_depth_prefix: (usize, &str), padding_rules: &HashMap<Block, usize>, tree_index: usize, ) -> Vec<Cell> { let mut cells = Vec::new(); let last_idx = metas.len(); for (idx, meta) in metas.iter().enumerate() { let current_prefix = if tree_depth_prefix.0 > 0 { if idx + 1 != last_idx { // is last folder elem format!("{}{} ", tree_depth_prefix.1, EDGE) } else { format!("{}{} ", tree_depth_prefix.1, CORNER) } } else { tree_depth_prefix.1.to_string() }; for block in get_output( meta, owner_cache, colors, icons, git_theme, flags, &DisplayOption::FileName, padding_rules, (tree_index, ¤t_prefix), ) { cells.push(Cell { width: get_visible_width(&block, flags.hyperlink == HyperlinkOption::Always), contents: block, }); } if let Some(content) = &meta.content { let new_prefix = if tree_depth_prefix.0 > 0 { if idx + 1 != last_idx { // is last folder elem format!("{}{} ", tree_depth_prefix.1, LINE) } else { format!("{}{} ", tree_depth_prefix.1, BLANK) } } else { tree_depth_prefix.1.to_string() }; cells.extend(inner_display_tree( content, owner_cache, flags, colors, icons, git_theme, (tree_depth_prefix.0 + 1, &new_prefix), padding_rules, tree_index, )); } } cells } fn should_display_folder_path(depth: usize, metas: &[Meta]) -> bool { if depth > 0 { true } else { let folder_number = metas .iter() .filter(|x| { matches!(x.file_type, FileType::Directory { .. }) || (matches!(x.file_type, FileType::SymLink { is_dir: true })) }) .count(); folder_number > 1 || folder_number < metas.len() } } fn display_folder_path(meta: &Meta) -> String { format!("\n{}:\n", meta.path.to_string_lossy()) } #[allow(clippy::too_many_arguments)] fn get_output( meta: &Meta, owner_cache: &OwnerCache, colors: &Colors, icons: &Icons, git_theme: &GitTheme, flags: &Flags, display_option: &DisplayOption, padding_rules: &HashMap<Block, usize>, tree: (usize, &str), ) -> Vec<String> { let mut strings: Vec<String> = Vec::new(); let colorize_missing = |string: &str| colors.colorize(string, &Elem::NoAccess); for (i, block) in flags.blocks.0.iter().enumerate() { let mut block_vec = if Layout::Tree == flags.layout && tree.0 == i { vec![colors.colorize(tree.1, &Elem::TreeEdge)] } else { Vec::new() }; match block { Block::INode => block_vec.push(match &meta.inode { Some(inode) => inode.render(colors), None => colorize_missing("?"), }), Block::Links => block_vec.push(match &meta.links { Some(links) => links.render(colors), None => colorize_missing("?"), }), Block::Permission => { block_vec.extend([ meta.file_type.render(colors), match &meta.permissions_or_attributes { Some(permissions_or_attributes) => { permissions_or_attributes.render(colors, flags) } None => colorize_missing("?????????"), }, match &meta.access_control { Some(access_control) => access_control.render_method(colors), None => colorize_missing(""), }, ]); } Block::User => block_vec.push(match &meta.owner { Some(owner) => owner.render_user(colors, owner_cache, flags), None => colorize_missing("?"), }), Block::Group => block_vec.push(match &meta.owner { Some(owner) => owner.render_group(colors, owner_cache, flags), None => colorize_missing("?"), }), Block::Context => block_vec.push(match &meta.access_control { Some(access_control) => access_control.render_context(colors), None => colorize_missing("?"), }), Block::Size => { let pad = if Layout::Tree == flags.layout && 0 == tree.0 && 0 == i { None } else { Some(padding_rules[&Block::SizeValue]) }; block_vec.push(match &meta.size { Some(size) => size.render(colors, flags, pad), None => colorize_missing("?"), }) } Block::SizeValue => block_vec.push(match &meta.size { Some(size) => size.render_value(colors, flags), None => colorize_missing("?"), }), Block::Date => block_vec.push(match &meta.date { Some(date) => date.render(colors, flags), None => colorize_missing("?"), }), Block::Name => { block_vec.extend([ meta.name.render( colors, icons, display_option, flags.hyperlink, flags.literal.0, ), meta.indicator.render(flags), ]); if !(flags.no_symlink.0 || flags.dereference.0 || flags.layout == Layout::Grid) { block_vec.push(meta.symlink.render(colors, flags)) } } Block::GitStatus => { if let Some(_s) = &meta.git_status { block_vec.push(_s.render(colors, git_theme)); } } }; strings.push( block_vec .into_iter() .map(|s| s.to_string()) .collect::<Vec<String>>() .join(""), ); } strings } fn get_visible_width(input: &str, hyperlink: bool) -> usize { let mut nb_invisible_char = 0; // If the input has color, do not compute the length contributed by the color to the actual length for (idx, _) in input.match_indices("\u{1b}[") { let (_, s) = input.split_at(idx); let m_pos = s.find('m'); if let Some(len) = m_pos { // len points to the 'm' character, we must include 'm' to invisible characters nb_invisible_char += len + 1; } } if hyperlink { for (idx, _) in input.match_indices("\x1B]8;;") { let (_, s) = input.split_at(idx); let m_pos = s.find("\x1B\x5C"); if let Some(len) = m_pos { // len points to the '\x1B' character, we must include both '\x1B' and '\x5C' to invisible characters nb_invisible_char += len + 2 } } } // `UnicodeWidthStr::width` counts all unicode characters including escape '\u{1b}' and hyperlink '\x1B' UnicodeWidthStr::width(input) - nb_invisible_char } fn detect_size_lengths(metas: &[Meta], flags: &Flags) -> usize { let mut max_value_length: usize = 0; for meta in metas { let value_len = match &meta.size { Some(size) => size.value_string(flags).len(), None => 0, }; if value_len > max_value_length { max_value_length = value_len; } if Layout::Tree == flags.layout { if let Some(subs) = &meta.content { let sub_length = detect_size_lengths(subs, flags); if sub_length > max_value_length { max_value_length = sub_length; } } } } max_value_length } fn get_padding_rules(metas: &[Meta], flags: &Flags) -> HashMap<Block, usize> { let mut padding_rules: HashMap<Block, usize> = HashMap::new(); if flags.blocks.0.contains(&Block::Size) { let size_val = detect_size_lengths(metas, flags); padding_rules.insert(Block::SizeValue, size_val); } padding_rules } #[cfg(test)] mod tests { use super::*; use crate::app::Cli; use crate::color; use crate::color::Colors; use crate::flags::{HyperlinkOption, IconOption, IconTheme as FlagTheme, PermissionFlag}; use crate::icon::Icons; use crate::meta::{FileType, Name}; use crate::Config; use crate::{flags, sort}; use assert_fs::prelude::*; use clap::Parser; use std::path::Path; use tempfile::tempdir; #[test] fn test_display_get_visible_width_without_icons() { for (s, l) in [ ("Hello,world!", 22), ("ASCII1234-_", 11), ("制作样本。", 10), ("日本語", 6), ("샘플은 무료로 드리겠습니다", 28), ("👩🐩", 4), ("🔬", 2), ] { let path = Path::new(s); let name = Name::new( path, FileType::File { exec: false, uid: false, }, ); let output = name .render( &Colors::new(color::ThemeOption::NoColor), &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), &DisplayOption::FileName, HyperlinkOption::Never, false, ) .to_string(); assert_eq!(get_visible_width(&output, false), l); } } #[test] fn test_display_get_visible_width_with_icons() { for (s, l) in [ // Add 3 characters for the icons. ("Hello,world!", 24), ("ASCII1234-_", 13), ("File with space", 19), ("制作样本。", 12), ("日本語", 8), ("샘플은 무료로 드리겠습니다", 30), ("👩🐩", 6), ("🔬", 4), ] { let path = Path::new(s); let name = Name::new( path, FileType::File { exec: false, uid: false, }, ); let output = name .render( &Colors::new(color::ThemeOption::NoColor), &Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()), &DisplayOption::FileName, HyperlinkOption::Never, false, ) .to_string(); assert_eq!(get_visible_width(&output, false), l); } } #[test] fn test_display_get_visible_width_with_colors() { for (s, l) in [ ("Hello,world!", 22), ("ASCII1234-_", 11), ("File with space", 17), ("制作样本。", 10), ("日本語", 6), ("샘플은 무료로 드리겠습니다", 28), ("👩🐩", 4), ("🔬", 2), ] { let path = Path::new(s); let name = Name::new( path, FileType::File { exec: false, uid: false, }, ); let output = name .render( &Colors::new(color::ThemeOption::NoLscolors), &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), &DisplayOption::FileName, HyperlinkOption::Never, false, ) .to_string(); // check if the color is present. assert!( output.starts_with("\u{1b}[38;5;"), "{output:?} should start with color" ); assert!(output.ends_with("[39m"), "reset foreground color"); assert_eq!(get_visible_width(&output, false), l, "visible match"); } } #[test] fn test_display_get_visible_width_without_colors() { for (s, l) in [ ("Hello,world!", 22), ("ASCII1234-_", 11), ("File with space", 17), ("制作样本。", 10), ("日本語", 6), ("샘플은 무료로 드리겠습니다", 28), ("👩🐩", 4), ("🔬", 2), ] { let path = Path::new(s); let name = Name::new( path, FileType::File { exec: false, uid: false, }, ); let output = name .render( &Colors::new(color::ThemeOption::NoColor), &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), &DisplayOption::FileName, HyperlinkOption::Never, false, ) .to_string(); // check if the color is present. assert!(!output.starts_with("\u{1b}[38;5;")); assert!(!output.ends_with("[0m")); assert_eq!(get_visible_width(&output, false), l); } } #[test] fn test_display_get_visible_width_hypelink_simple() { for (s, l) in [ ("Hello,world!", 22), ("ASCII1234-_", 11), ("File with space", 15), ("制作样本。", 10), ("日本語", 6), ("샘플은 무료로 드리겠습니다", 26), ("👩🐩", 4), ("🔬", 2), ] { // rending name require actual file, so we are mocking that let output = format!("\x1B]8;;{}\x1B\x5C{}\x1B]8;;\x1B\x5C", "url://fake-url", s); assert_eq!(get_visible_width(&output, true), l); } } fn sort(metas: &mut Vec<Meta>, sorters: &Vec<(flags::SortOrder, sort::SortFn)>) { metas.sort_unstable_by(|a, b| sort::by_meta(sorters, a, b)); for meta in metas { if let Some(ref mut content) = meta.content { sort(content, sorters); } } } #[test] fn test_display_tree_with_all() { let argv = ["lsd", "--tree", "--all"]; let cli = Cli::try_parse_from(argv).unwrap(); let flags = Flags::configure_from(&cli, &Config::with_none()).unwrap(); let dir = assert_fs::TempDir::new().unwrap(); dir.child("one.d").create_dir_all().unwrap(); dir.child("one.d/two").touch().unwrap(); dir.child("one.d/.hidden").touch().unwrap(); let mut metas = Meta::from_path(Path::new(dir.path()), false, PermissionFlag::Rwx) .unwrap() .recurse_into(42, &flags, None) .unwrap() .0 .unwrap(); sort(&mut metas, &sort::assemble_sorters(&flags)); let output = tree( &metas, &flags, &Colors::new(color::ThemeOption::NoColor), &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), &GitTheme::new(), ); assert_eq!("one.d\n├── .hidden\n└── two\n", output); } /// Different level of folder may form a different width /// we must make sure it is aligned in all level /// /// dir has a bytes size /// empty file has an empty size /// `---blocks size,name` can help us for this case #[test] fn test_tree_align_subfolder() { let argv = ["lsd", "--tree", "--blocks", "size,name"]; let cli = Cli::try_parse_from(argv).unwrap(); let flags = Flags::configure_from(&cli, &Config::with_none()).unwrap(); let dir = assert_fs::TempDir::new().unwrap(); dir.child("dir").create_dir_all().unwrap(); dir.child("dir/file").touch().unwrap(); let metas = Meta::from_path(Path::new(dir.path()), false, PermissionFlag::Rwx) .unwrap() .recurse_into(42, &flags, None) .unwrap() .0 .unwrap(); let output = tree( &metas, &flags, &Colors::new(color::ThemeOption::NoColor), &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), &GitTheme::new(), ); let length_before_b = |i| -> usize { output .lines() .nth(i) .unwrap() .split(|c| c == 'K' || c == 'B') .next() .unwrap() .len() }; assert_eq!(length_before_b(0), length_before_b(1)); assert_eq!( output.lines().next().unwrap().find('d'), output.lines().nth(1).unwrap().find('└') ); } #[test] #[cfg(unix)] fn test_tree_size_first_without_name() { let argv = ["lsd", "--tree", "--blocks", "size,permission"]; let cli = Cli::try_parse_from(argv).unwrap(); let flags = Flags::configure_from(&cli, &Config::with_none()).unwrap(); let dir = assert_fs::TempDir::new().unwrap(); dir.child("dir").create_dir_all().unwrap(); dir.child("dir/file").touch().unwrap(); let metas = Meta::from_path(Path::new(dir.path()), false, PermissionFlag::Rwx) .unwrap() .recurse_into(42, &flags, None) .unwrap() .0 .unwrap(); let output = tree( &metas, &flags, &Colors::new(color::ThemeOption::NoColor), &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), &GitTheme::new(), ); assert_eq!(output.lines().nth(1).unwrap().chars().next().unwrap(), '└'); assert_eq!( output .lines() .next() .unwrap() .chars() .position(|x| x == 'd'), output .lines() .nth(1) .unwrap() .chars() .position(|x| x == '.'), ); } #[test] fn test_tree_edge_before_name() { let argv = ["lsd", "--tree", "--long"]; let cli = Cli::try_parse_from(argv).unwrap(); let flags = Flags::configure_from(&cli, &Config::with_none()).unwrap(); let dir = assert_fs::TempDir::new().unwrap(); dir.child("one.d").create_dir_all().unwrap(); dir.child("one.d/two").touch().unwrap(); let metas = Meta::from_path(Path::new(dir.path()), false, PermissionFlag::Rwx) .unwrap() .recurse_into(42, &flags, None) .unwrap() .0 .unwrap(); let output = tree( &metas, &flags, &Colors::new(color::ThemeOption::NoColor), &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), &GitTheme::new(), ); assert!(output.ends_with("└── two\n")); } #[test] fn test_grid_all_block_headers() { let argv = [ "lsd", "--header", "--blocks", "permission,user,group,size,date,name,inode,links", ]; let cli = Cli::try_parse_from(argv).unwrap(); let flags = Flags::configure_from(&cli, &Config::with_none()).unwrap(); let dir = assert_fs::TempDir::new().unwrap(); dir.child("testdir").create_dir_all().unwrap(); dir.child("test").touch().unwrap(); let metas = Meta::from_path(Path::new(dir.path()), false, PermissionFlag::Rwx) .unwrap() .recurse_into(1, &flags, None) .unwrap() .0 .unwrap(); let output = grid( &metas, &flags, &Colors::new(color::ThemeOption::NoColor), &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), &GitTheme::new(), ); dir.close().unwrap(); assert!(output.contains("Permissions")); assert!(output.contains("User")); assert!(output.contains("Group")); assert!(output.contains("Size")); assert!(output.contains("Date Modified")); assert!(output.contains("Name")); assert!(output.contains("INode")); assert!(output.contains("Links")); } #[test] fn test_grid_no_header_with_empty_meta() { let argv = ["lsd", "--header", "-l"]; let cli = Cli::try_parse_from(argv).unwrap(); let flags = Flags::configure_from(&cli, &Config::with_none()).unwrap(); let dir = assert_fs::TempDir::new().unwrap(); dir.child("testdir").create_dir_all().unwrap(); let metas = Meta::from_path(Path::new(dir.path()), false, PermissionFlag::Rwx) .unwrap() .recurse_into(1, &flags, None) .unwrap() .0 .unwrap(); let output = grid( &metas, &flags, &Colors::new(color::ThemeOption::NoColor), &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), &GitTheme::new(), ); dir.close().unwrap(); assert!(!output.contains("Permissions")); assert!(!output.contains("User")); assert!(!output.contains("Group")); assert!(!output.contains("Size")); assert!(!output.contains("Date Modified")); assert!(!output.contains("Name")); } #[test] fn test_folder_path() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path().join("file"); std::fs::File::create(&file_path).expect("failed to create the file"); let file = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); let dir_path = tmp_dir.path().join("dir"); std::fs::create_dir(&dir_path).expect("failed to create the dir"); let dir = Meta::from_path(&dir_path, false, PermissionFlag::Rwx).unwrap(); assert_eq!( display_folder_path(&dir), format!( "\n{}{}dir:\n", tmp_dir.path().to_string_lossy(), std::path::MAIN_SEPARATOR ) ); const YES: bool = true; const NO: bool = false; assert_eq!( should_display_folder_path(0, &[file.clone()]), YES // doesn't matter since there is no folder ); assert_eq!(should_display_folder_path(0, &[dir.clone()]), NO); assert_eq!( should_display_folder_path(0, &[file.clone(), dir.clone()]), YES ); assert_eq!( should_display_folder_path(0, &[dir.clone(), dir.clone()]), YES ); assert_eq!( should_display_folder_path(0, &[file.clone(), file.clone()]), YES // doesn't matter since there is no folder ); drop(dir); // to avoid clippy complains about previous .clone() drop(file); } #[cfg(unix)] #[test] fn test_folder_path_with_links() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path().join("file"); std::fs::File::create(&file_path).expect("failed to create the file"); let file = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); let dir_path = tmp_dir.path().join("dir"); std::fs::create_dir(&dir_path).expect("failed to create the dir"); let dir = Meta::from_path(&dir_path, false, PermissionFlag::Rwx).unwrap(); let link_path = tmp_dir.path().join("link"); std::os::unix::fs::symlink("dir", &link_path).unwrap(); let link = Meta::from_path(&link_path, false, PermissionFlag::Rwx).unwrap(); const YES: bool = true; const NO: bool = false; assert_eq!(should_display_folder_path(0, &[link.clone()]), NO); assert_eq!( should_display_folder_path(0, &[file.clone(), link.clone()]), YES ); assert_eq!( should_display_folder_path(0, &[dir.clone(), link.clone()]), YES ); drop(dir); // to avoid clippy complains about previous .clone() drop(file); drop(link); } } 0707010000001F000041ED00000000000000000000000266C4C37900000000000000000000000000000000000000000000001400000000lsd-1.1.5/src/flags07070100000020000081A400000000000000000000000166C4C3790000141B000000000000000000000000000000000000001700000000lsd-1.1.5/src/flags.rspub mod blocks; pub mod color; pub mod date; pub mod dereference; pub mod display; pub mod header; pub mod hyperlink; pub mod icons; pub mod ignore_globs; pub mod indicators; pub mod layout; pub mod literal; pub mod permission; pub mod recursion; pub mod size; pub mod sorting; pub mod symlink_arrow; pub mod symlinks; pub mod total_size; pub mod truncate_owner; pub use blocks::Blocks; pub use color::Color; pub use color::{ColorOption, ThemeOption}; pub use date::DateFlag; pub use dereference::Dereference; pub use display::Display; pub use header::Header; pub use hyperlink::HyperlinkOption; pub use icons::IconOption; pub use icons::IconTheme; pub use icons::Icons; pub use ignore_globs::IgnoreGlobs; pub use indicators::Indicators; pub use layout::Layout; pub use literal::Literal; pub use permission::PermissionFlag; pub use recursion::Recursion; pub use size::SizeFlag; pub use sorting::DirGrouping; pub use sorting::SortColumn; pub use sorting::SortOrder; pub use sorting::Sorting; pub use symlink_arrow::SymlinkArrow; pub use symlinks::NoSymlink; pub use total_size::TotalSize; pub use truncate_owner::TruncateOwner; use crate::app::Cli; use crate::config_file::Config; use clap::Error; #[cfg(doc)] use yaml_rust::Yaml; /// A struct to hold all set configuration flags for the application. #[derive(Clone, Debug, Default)] pub struct Flags { pub blocks: Blocks, pub color: Color, pub date: DateFlag, pub dereference: Dereference, pub display: Display, pub display_indicators: Indicators, pub icons: Icons, pub ignore_globs: IgnoreGlobs, pub layout: Layout, pub no_symlink: NoSymlink, pub recursion: Recursion, pub size: SizeFlag, pub permission: PermissionFlag, pub sorting: Sorting, pub total_size: TotalSize, pub symlink_arrow: SymlinkArrow, pub hyperlink: HyperlinkOption, pub header: Header, pub literal: Literal, pub truncate_owner: TruncateOwner, } impl Flags { /// Set up the `Flags` from either [Cli], a [Config] or its [Default] value. /// /// # Errors /// /// This can return an [Error], when either the building of the ignore globs or the parsing of /// the recursion depth parameter fails. pub fn configure_from(cli: &Cli, config: &Config) -> Result<Self, Error> { Ok(Self { blocks: Blocks::configure_from(cli, config), color: Color::configure_from(cli, config), date: DateFlag::configure_from(cli, config), dereference: Dereference::configure_from(cli, config), display: Display::configure_from(cli, config), layout: Layout::configure_from(cli, config), size: SizeFlag::configure_from(cli, config), permission: PermissionFlag::configure_from(cli, config), display_indicators: Indicators::configure_from(cli, config), icons: Icons::configure_from(cli, config), ignore_globs: IgnoreGlobs::configure_from(cli, config)?, no_symlink: NoSymlink::configure_from(cli, config), recursion: Recursion::configure_from(cli, config), sorting: Sorting::configure_from(cli, config), total_size: TotalSize::configure_from(cli, config), symlink_arrow: SymlinkArrow::configure_from(cli, config), hyperlink: HyperlinkOption::configure_from(cli, config), header: Header::configure_from(cli, config), literal: Literal::configure_from(cli, config), truncate_owner: TruncateOwner::configure_from(cli, config), }) } } /// A trait to allow a type to be configured by either command line parameters, a configuration /// file or a [Default] value. pub trait Configurable<T> where T: std::default::Default, { /// Returns a value from either [Cli], a [Config], a [Default] or the environment value. /// The first value that is not [None] is used. The order of precedence for the value used is: /// - [from_cli](Configurable::from_cli) /// - [from_environment](Configurable::from_environment) /// - [from_config](Configurable::from_config) /// - [Default::default] /// /// # Note /// /// The configuration file's Yaml is read in any case, to be able to check for errors and print /// out warnings. fn configure_from(cli: &Cli, config: &Config) -> T { if let Some(value) = Self::from_cli(cli) { return value; } if let Some(value) = Self::from_environment() { return value; } if let Some(value) = Self::from_config(config) { return value; } Default::default() } /// The method to implement the value fetching from command line parameters. fn from_cli(cli: &Cli) -> Option<T>; /// The method to implement the value fetching from a configuration file. This should return /// [None], if the [Config] does not have a [Yaml]. fn from_config(config: &Config) -> Option<T>; /// The method to implement the value fetching from environment variables. fn from_environment() -> Option<T> { None } } 07070100000021000081A400000000000000000000000166C4C379000047BF000000000000000000000000000000000000001E00000000lsd-1.1.5/src/flags/blocks.rs//! This module defines the [Blocks] struct. To set it up from [Cli], a [Config] and its //! [Default] value, use its [configure_from](Blocks::configure_from) method. use super::Configurable; use crate::app::Cli; use crate::config_file::Config; use crate::print_error; use std::convert::TryFrom; /// A struct to hold a [Vec] of [Block]s and to provide methods to create it. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Blocks(pub Vec<Block>); impl Blocks { /// This returns a Blocks struct for the long format. /// /// It contains the [Block]s [Permission](Block::Permission), [User](Block::User), /// [Group](Block::Group), [Size](Block::Size), [Date](Block::Date) and [Name](Block::Name). fn long() -> Self { Self(vec![ Block::Permission, Block::User, Block::Group, Block::Size, Block::Date, Block::Name, ]) } /// Checks whether `self` already contains a [Block] of variant [INode](Block::INode). fn contains_inode(&self) -> bool { self.0.contains(&Block::INode) } /// Prepends a [Block] of variant [INode](Block::INode) to `self`. fn prepend_inode(&mut self) { self.0.insert(0, Block::INode); } /// Prepends a [Block] of variant [INode](Block::INode), if `self` does not already contain a /// Block of that variant. fn optional_prepend_inode(&mut self) { if !self.contains_inode() { self.prepend_inode() } } pub fn displays_size(&self) -> bool { self.0.contains(&Block::Size) } /// Inserts a [Block] of variant [INode](Block::Context), if `self` does not already contain a /// [Block] of that variant. The positioning will be best-effort approximation of coreutils /// ls position for a security context fn optional_insert_context(&mut self) { if self.0.contains(&Block::Context) { return; } let mut pos = self.0.iter().position(|elem| *elem == Block::Group); if pos.is_none() { pos = self.0.iter().position(|elem| *elem == Block::User); } match pos { Some(pos) => self.0.insert(pos + 1, Block::Context), None => self.0.insert(0, Block::Context), } } /// Checks whether `self` already contains a [Block] of variant [GitStatus](Block::GitStatus). fn contains_git_status(&self) -> bool { self.0.contains(&Block::GitStatus) } /// Put a [Block] of variant [GitStatus](Block::GitStatus) on the left of [GitStatus](Block::Name) to `self`. fn add_git_status(&mut self) { if let Some(position) = self.0.iter().position(|&b| b == Block::Name) { self.0.insert(position, Block::GitStatus); } else { self.0.push(Block::GitStatus); } } /// Prepends a [Block] of variant [GitStatus](Block::GitStatus), if `self` does not already contain a /// Block of that variant. fn optional_add_git_status(&mut self) { if !self.contains_git_status() { self.add_git_status() } } } impl Configurable<Self> for Blocks { /// Returns a value from either [Cli], a [Config] or a default value. /// Unless the "long" argument is passed, this returns [Default::default]. Otherwise the first /// value, that is not [None], is used. The order of precedence for the value used is: /// - [from_cli](Blocks::from_cli) /// - [from_config](Blocks::from_config) /// - [long](Blocks::long) /// /// No matter if the "long" argument was passed, if the "inode" argument is passed and the /// `Blocks` does not contain a [Block] of variant [INode](Block::INode) yet, one is prepended /// to the returned value. fn configure_from(cli: &Cli, config: &Config) -> Self { let mut blocks = if cli.long { Self::long() } else { Default::default() }; if cli.long { if let Some(value) = Self::from_config(config) { blocks = value; } } if let Some(value) = Self::from_cli(cli) { blocks = value; } if cli.context { blocks.optional_insert_context(); } if cli.inode { blocks.optional_prepend_inode(); } if !cfg!(feature = "no-git") && cli.git && cli.long { blocks.optional_add_git_status(); } blocks } /// Get a potential `Blocks` struct from [Cli]. /// /// If the "blocks" argument is passed, then this returns a `Blocks` containing the parameter /// values in a [Some]. Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.blocks.is_empty() { return None; } let blocks = cli .blocks .iter() .map(|b| Block::try_from(b.as_str()).unwrap()) .collect(); Some(Self(blocks)) } /// Get a potential `Blocks` struct from a [Config]. /// /// If the [Config] contains an array of blocks values, /// its [String] values is returned as `Blocks` in a [Some]. /// Otherwise it returns [None]. fn from_config(config: &Config) -> Option<Self> { if let Some(c) = &config.blocks { let mut blocks: Vec<Block> = vec![]; for b in c.iter() { match Block::try_from(b.as_str()) { Ok(block) => blocks.push(block), Err(err) => print_error!("{}.", err), } } if blocks.is_empty() { None } else { Some(Self(blocks)) } } else { None } } } /// The default value for `Blocks` contains a [Vec] of [Name](Block::Name). impl Default for Blocks { fn default() -> Self { Self(vec![Block::Name]) } } /// A block of data to show. #[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum Block { Permission, User, Group, Context, Size, SizeValue, Date, Name, INode, Links, GitStatus, } impl Block { pub fn get_header(&self) -> &'static str { match self { Block::INode => "INode", Block::Links => "Links", Block::Permission => "Permissions", Block::User => "User", Block::Group => "Group", Block::Context => "Context", Block::Size => "Size", Block::SizeValue => "SizeValue", Block::Date => "Date Modified", Block::Name => "Name", Block::GitStatus => "Git", } } } impl TryFrom<&str> for Block { type Error = String; fn try_from(string: &str) -> Result<Self, Self::Error> { match string { "permission" => Ok(Self::Permission), "user" => Ok(Self::User), "group" => Ok(Self::Group), "context" => Ok(Self::Context), "size" => Ok(Self::Size), "size_value" => Ok(Self::SizeValue), "date" => Ok(Self::Date), "name" => Ok(Self::Name), "inode" => Ok(Self::INode), "links" => Ok(Self::Links), "git" => Ok(Self::GitStatus), _ => Err(format!("Not a valid block name: {string}")), } } } #[cfg(test)] mod test_blocks { use clap::Parser; use super::Block; use super::Blocks; use crate::app::Cli; use crate::config_file::Config; use crate::flags::Configurable; #[test] fn test_configure_from_without_long() { let argv = ["lsd"]; let target = Blocks::default(); let cli = Cli::try_parse_from(argv).unwrap(); let result = Blocks::configure_from(&cli, &Config::with_none()); assert_eq!(result, target); } #[test] fn test_configure_from_with_long() { let argv = ["lsd", "--long"]; let target = Blocks::long(); let cli = Cli::try_parse_from(argv).unwrap(); let result = Blocks::configure_from(&cli, &Config::with_none()); assert_eq!(result, target); } #[test] fn test_configure_from_with_blocks_and_without_long() { let argv = ["lsd", "--blocks", "permission"]; let target = Blocks(vec![Block::Permission]); let cli = Cli::try_parse_from(argv).unwrap(); let result = Blocks::configure_from(&cli, &Config::with_none()); assert_eq!(result, target); } #[test] fn test_configure_from_with_blocks_and_long() { let argv = ["lsd", "--long", "--blocks", "permission"]; let target = Blocks(vec![Block::Permission]); let cli = Cli::try_parse_from(argv).unwrap(); let result = Blocks::configure_from(&cli, &Config::with_none()); assert_eq!(result, target); } #[test] fn test_configure_from_with_inode() { let argv = ["lsd", "--inode"]; let target = Blocks(vec![Block::INode, Block::Name]); let cli = Cli::try_parse_from(argv).unwrap(); let result = Blocks::configure_from(&cli, &Config::with_none()); assert_eq!(result, target); } #[test] fn test_configure_from_prepend_inode_without_long() { let argv = ["lsd", "--blocks", "permission", "--inode"]; let target = Blocks(vec![Block::INode, Block::Permission]); let cli = Cli::try_parse_from(argv).unwrap(); let result = Blocks::configure_from(&cli, &Config::with_none()); assert_eq!(result, target); } #[test] fn test_configure_from_prepend_inode_with_long() { let argv = ["lsd", "--long", "--blocks", "permission", "--inode"]; let target = Blocks(vec![Block::INode, Block::Permission]); let cli = Cli::try_parse_from(argv).unwrap(); let result = Blocks::configure_from(&cli, &Config::with_none()); assert_eq!(result, target); } #[test] fn test_configure_from_ignore_prepend_inode_without_long() { let argv = ["lsd", "--blocks", "permission,inode", "--inode"]; let target = Blocks(vec![Block::Permission, Block::INode]); let cli = Cli::try_parse_from(argv).unwrap(); let result = Blocks::configure_from(&cli, &Config::with_none()); assert_eq!(result, target); } #[test] fn test_configure_from_ignore_prepend_inode_with_long() { let argv = ["lsd", "--long", "--blocks", "permission,inode", "--inode"]; let target = Blocks(vec![Block::Permission, Block::INode]); let cli = Cli::try_parse_from(argv).unwrap(); let result = Blocks::configure_from(&cli, &Config::with_none()); assert_eq!(result, target); } #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert!(Blocks::from_cli(&cli).is_none()); } #[test] fn test_from_cli_one() { let argv = ["lsd", "--blocks", "permission"]; let cli = Cli::try_parse_from(argv).unwrap(); let test_blocks = Blocks(vec![Block::Permission]); assert_eq!(Blocks::from_cli(&cli), Some(test_blocks)); } #[test] fn test_from_cli_multi_occurences() { let argv = ["lsd", "--blocks", "permission", "--blocks", "name"]; let cli = Cli::try_parse_from(argv).unwrap(); let test_blocks = Blocks(vec![Block::Permission, Block::Name]); assert_eq!(Blocks::from_cli(&cli), Some(test_blocks)); } #[test] fn test_from_cli_multi_values() { let argv = ["lsd", "--blocks", "permission,name"]; let cli = Cli::try_parse_from(argv).unwrap(); let test_blocks = Blocks(vec![Block::Permission, Block::Name]); assert_eq!(Blocks::from_cli(&cli), Some(test_blocks)); } #[test] fn test_from_cli_reversed_default() { let argv = ["lsd", "--blocks", "name,date,size,group,user,permission"]; let cli = Cli::try_parse_from(argv).unwrap(); let test_blocks = Blocks(vec![ Block::Name, Block::Date, Block::Size, Block::Group, Block::User, Block::Permission, ]); assert_eq!(Blocks::from_cli(&cli), Some(test_blocks)); } #[test] fn test_from_cli_every_second_one() { let argv = ["lsd", "--blocks", "permission,group,date"]; let cli = Cli::try_parse_from(argv).unwrap(); let test_blocks = Blocks(vec![Block::Permission, Block::Group, Block::Date]); assert_eq!(Blocks::from_cli(&cli), Some(test_blocks)); } #[cfg(not(feature = "no-git"))] #[test] fn test_from_cli_implicit_add_git_block() { let argv = vec![ "lsd", "--blocks", "permission,name,group,date", "--git", "--long", ]; let cli = Cli::try_parse_from(argv).unwrap(); let test_blocks = Blocks(vec![ Block::Permission, Block::GitStatus, Block::Name, Block::Group, Block::Date, ]); assert_eq!( Blocks::configure_from(&cli, &Config::with_none()), test_blocks ); } #[test] fn test_from_config_none() { assert_eq!(None, Blocks::from_config(&Config::with_none())); } #[test] fn test_from_config_one() { let mut c = Config::with_none(); c.blocks = Some(vec!["permission".into()]); let blocks = Blocks(vec![Block::Permission]); assert_eq!(Some(blocks), Blocks::from_config(&c)); } #[test] fn test_from_config_reversed_default() { let target = Blocks(vec![ Block::Name, Block::Date, Block::Size, Block::Group, Block::User, Block::Permission, ]); let mut c = Config::with_none(); c.blocks = Some(vec![ "name".into(), "date".into(), "size".into(), "group".into(), "user".into(), "permission".into(), ]); assert_eq!(Some(target), Blocks::from_config(&c)); } #[test] fn test_from_config_every_second_one() { let mut c = Config::with_none(); c.blocks = Some(vec!["permission".into(), "group".into(), "date".into()]); let blocks = Blocks(vec![Block::Permission, Block::Group, Block::Date]); assert_eq!(Some(blocks), Blocks::from_config(&c)); } #[test] fn test_from_config_invalid_is_ignored() { let mut c = Config::with_none(); c.blocks = Some(vec!["permission".into(), "foo".into(), "date".into()]); let blocks = Blocks(vec![Block::Permission, Block::Date]); assert_eq!(Some(blocks), Blocks::from_config(&c)); } #[test] fn test_context_not_present_on_cli() { let argv = ["lsd", "--long"]; let cli = Cli::try_parse_from(argv).unwrap(); let parsed_blocks = Blocks::configure_from(&cli, &Config::with_none()); let it = parsed_blocks.0.iter(); assert_eq!(it.filter(|&x| *x == Block::Context).count(), 0); } #[test] fn test_context_present_if_context_on() { let argv = ["lsd", "--context"]; let cli = Cli::try_parse_from(argv).unwrap(); let parsed_blocks = Blocks::configure_from(&cli, &Config::with_none()); let it = parsed_blocks.0.iter(); assert_eq!(it.filter(|&x| *x == Block::Context).count(), 1); } #[test] fn test_only_one_context_no_other_blocks_affected() { let argv = [ "lsd", "--context", "--blocks", "name,date,size,context,group,user,permission", ]; let cli = Cli::try_parse_from(argv).unwrap(); let test_blocks = Blocks(vec![ Block::Name, Block::Date, Block::Size, Block::Context, Block::Group, Block::User, Block::Permission, ]); let parsed_blocks = Blocks::from_cli(&cli).unwrap(); assert_eq!(test_blocks, parsed_blocks); } } #[cfg(test)] mod test_block { use super::Block; use std::convert::TryFrom; #[test] fn test_err() { assert_eq!( Err(String::from("Not a valid block name: foo")), Block::try_from("foo") ); } #[test] fn test_permission() { assert_eq!(Ok(Block::Permission), Block::try_from("permission")); } #[test] fn test_user() { assert_eq!(Ok(Block::User), Block::try_from("user")); } #[test] fn test_group() { assert_eq!(Ok(Block::Group), Block::try_from("group")); } #[test] fn test_size() { assert_eq!(Ok(Block::Size), Block::try_from("size")); } #[test] fn test_size_value() { assert_eq!(Ok(Block::SizeValue), Block::try_from("size_value")); } #[test] fn test_date() { assert_eq!(Ok(Block::Date), Block::try_from("date")); } #[test] fn test_name() { assert_eq!(Ok(Block::Name), Block::try_from("name")); } #[test] fn test_inode() { assert_eq!(Ok(Block::INode), Block::try_from("inode")); } #[test] fn test_links() { assert_eq!(Ok(Block::Links), Block::try_from("links")); } #[test] fn test_context() { assert_eq!(Ok(Block::Context), Block::try_from("context")); } #[test] fn test_block_headers() { assert_eq!(Block::INode.get_header(), "INode"); assert_eq!(Block::Links.get_header(), "Links"); assert_eq!(Block::Permission.get_header(), "Permissions"); assert_eq!(Block::User.get_header(), "User"); assert_eq!(Block::Group.get_header(), "Group"); assert_eq!(Block::Context.get_header(), "Context"); assert_eq!(Block::Size.get_header(), "Size"); assert_eq!(Block::SizeValue.get_header(), "SizeValue"); assert_eq!(Block::Date.get_header(), "Date Modified"); assert_eq!(Block::Name.get_header(), "Name"); assert_eq!(Block::GitStatus.get_header(), "Git"); } #[test] fn test_git_status() { assert_eq!(Ok(Block::GitStatus), Block::try_from("git")); } } 07070100000022000081A400000000000000000000000166C4C37900002509000000000000000000000000000000000000001D00000000lsd-1.1.5/src/flags/color.rs//! This module defines the [Color]. To set it up from [Cli], a [Config] and its [Default] //! value, use its [configure_from](Configurable::configure_from) method. use super::Configurable; use crate::app::Cli; use crate::config_file::Config; use serde::de::{self, Deserializer, Visitor}; use serde::Deserialize; use std::env; use std::fmt; /// A collection of flags on how to use colors. #[derive(Clone, Debug, Default)] pub struct Color { /// When to use color. pub when: ColorOption, pub theme: ThemeOption, } impl Color { /// Get a `Color` struct from [Cli], a [Config] or the [Default] values. /// /// The [ColorOption] is configured with their respective [Configurable] implementation. pub fn configure_from(cli: &Cli, config: &Config) -> Self { let when = ColorOption::configure_from(cli, config); let theme = ThemeOption::from_config(config); Self { when, theme } } } /// ThemeOption could be one of the following: /// Custom(*.yaml): use the YAML theme file as theme file /// if error happened, use the default theme #[derive(PartialEq, Eq, Debug, Clone, Default)] pub enum ThemeOption { NoColor, #[default] Default, #[allow(dead_code)] NoLscolors, CustomLegacy(String), Custom, } impl ThemeOption { fn from_config(config: &Config) -> ThemeOption { if config.classic == Some(true) { ThemeOption::NoColor } else { config .color .as_ref() .and_then(|c| c.theme.clone()) .unwrap_or_default() } } } impl<'de> de::Deserialize<'de> for ThemeOption { fn deserialize<D>(deserializer: D) -> Result<ThemeOption, D::Error> where D: Deserializer<'de>, { struct ThemeOptionVisitor; impl<'de> Visitor<'de> for ThemeOptionVisitor { type Value = ThemeOption; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("`default` or <theme-file-path>") } fn visit_str<E>(self, value: &str) -> Result<ThemeOption, E> where E: de::Error, { match value { "default" => Ok(ThemeOption::Default), "custom" => Ok(ThemeOption::Custom), str => Ok(ThemeOption::CustomLegacy(str.to_string())), } } } deserializer.deserialize_identifier(ThemeOptionVisitor) } } /// The flag showing when to use colors in the output. #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] #[serde(rename_all = "kebab-case")] pub enum ColorOption { Always, #[default] Auto, Never, } impl ColorOption { fn from_arg_str(value: &str) -> Self { match value { "always" => Self::Always, "auto" => Self::Auto, "never" => Self::Never, // Invalid value should be handled by `clap` when building an `Cli` other => unreachable!("Invalid value '{other}' for 'color'"), } } } impl Configurable<Self> for ColorOption { /// Get a potential `ColorOption` variant from [Cli]. /// /// If the "classic" argument is passed, then this returns the [ColorOption::Never] variant in /// a [Some]. Otherwise if the argument is passed, this returns the variant corresponding to /// its parameter in a [Some]. Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.classic { Some(Self::Never) } else { cli.color.as_deref().map(Self::from_arg_str) } } /// Get a potential `ColorOption` variant from a [Config]. /// /// If the `Config::classic` is `true` then this returns the Some(ColorOption::Never), /// Otherwise if the `Config::color::when` has value and is one of "always", "auto" or "never" /// this returns its corresponding variant in a [Some]. Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { if config.classic == Some(true) { Some(Self::Never) } else { config.color.as_ref().and_then(|c| c.when) } } fn from_environment() -> Option<Self> { if env::var("NO_COLOR").is_ok() { Some(Self::Never) } else { None } } } #[cfg(test)] mod test_color_option { use clap::Parser; use super::ColorOption; use crate::app::Cli; use crate::config_file::{self, Config}; use crate::flags::Configurable; use std::env::set_var; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, ColorOption::from_cli(&cli)); } #[test] fn test_from_cli_always() { let argv = ["lsd", "--color", "always"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(ColorOption::Always), ColorOption::from_cli(&cli)); } #[test] fn test_from_cli_auto() { let argv = ["lsd", "--color", "auto"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(ColorOption::Auto), ColorOption::from_cli(&cli)); } #[test] fn test_from_cli_never() { let argv = ["lsd", "--color", "never"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(ColorOption::Never), ColorOption::from_cli(&cli)); } #[test] fn test_from_env_no_color() { set_var("NO_COLOR", "true"); assert_eq!(Some(ColorOption::Never), ColorOption::from_environment()); } #[test] fn test_from_cli_classic_mode() { let argv = ["lsd", "--color", "always", "--classic"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(ColorOption::Never), ColorOption::from_cli(&cli)); } #[test] fn test_from_cli_color_multiple() { let argv = ["lsd", "--color", "always", "--color", "never"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(ColorOption::Never), ColorOption::from_cli(&cli)); } #[test] fn test_from_config_none() { assert_eq!(None, ColorOption::from_config(&Config::with_none())); } #[test] fn test_from_config_always() { let mut c = Config::with_none(); c.color = Some(config_file::Color { when: Some(ColorOption::Always), theme: None, }); assert_eq!(Some(ColorOption::Always), ColorOption::from_config(&c)); } #[test] fn test_from_config_auto() { let mut c = Config::with_none(); c.color = Some(config_file::Color { when: Some(ColorOption::Auto), theme: None, }); assert_eq!(Some(ColorOption::Auto), ColorOption::from_config(&c)); } #[test] fn test_from_config_never() { let mut c = Config::with_none(); c.color = Some(config_file::Color { when: Some(ColorOption::Never), theme: None, }); assert_eq!(Some(ColorOption::Never), ColorOption::from_config(&c)); } #[test] fn test_from_config_classic_mode() { let mut c = Config::with_none(); c.color = Some(config_file::Color { when: Some(ColorOption::Always), theme: None, }); c.classic = Some(true); assert_eq!(Some(ColorOption::Never), ColorOption::from_config(&c)); } } #[cfg(test)] mod test_theme_option { use super::ThemeOption; use crate::config_file::{self, Config}; #[test] fn test_from_config_none_default() { assert_eq!( ThemeOption::Default, ThemeOption::from_config(&Config::with_none()) ); } #[test] fn test_from_config_default() { let mut c = Config::with_none(); c.color = Some(config_file::Color { when: None, theme: Some(ThemeOption::Default), }); assert_eq!(ThemeOption::Default, ThemeOption::from_config(&c)); } #[test] fn test_from_config_no_color() { let mut c = Config::with_none(); c.color = Some(config_file::Color { when: None, theme: Some(ThemeOption::NoColor), }); assert_eq!(ThemeOption::NoColor, ThemeOption::from_config(&c)); } #[test] fn test_from_config_no_lscolor() { let mut c = Config::with_none(); c.color = Some(config_file::Color { when: None, theme: Some(ThemeOption::NoLscolors), }); assert_eq!(ThemeOption::NoLscolors, ThemeOption::from_config(&c)); } #[test] fn test_from_config_bad_file_flag() { let mut c = Config::with_none(); c.color = Some(config_file::Color { when: None, theme: Some(ThemeOption::CustomLegacy("not-existed".to_string())), }); assert_eq!( ThemeOption::CustomLegacy("not-existed".to_string()), ThemeOption::from_config(&c) ); } #[test] fn test_from_config_classic_mode() { let mut c = Config::with_none(); c.color = Some(config_file::Color { when: None, theme: Some(ThemeOption::Default), }); c.classic = Some(true); assert_eq!(ThemeOption::NoColor, ThemeOption::from_config(&c)); } } 07070100000023000081A400000000000000000000000166C4C379000022E2000000000000000000000000000000000000001C00000000lsd-1.1.5/src/flags/date.rs//! This module defines the [DateFlag]. To set it up from [Cli], a [Config] and its //! [Default] value, use its [configure_from](Configurable::configure_from) method. use super::Configurable; use crate::app::{self, Cli}; use crate::config_file::Config; use crate::print_error; /// The flag showing which kind of time stamps to display. #[derive(Clone, Debug, PartialEq, Eq, Default)] pub enum DateFlag { #[default] Date, Locale, Relative, Iso, Formatted(String), } impl DateFlag { /// Get a value from a date format string fn from_format_string(value: &str) -> Option<Self> { if app::validate_time_format(value).is_ok() { Some(Self::Formatted(value[1..].to_string())) } else { print_error!("Not a valid date format: {}.", value); None } } /// Get a value from a str. fn from_str<S: AsRef<str>>(value: S) -> Option<Self> { let value = value.as_ref(); match value { "date" => Some(Self::Date), "locale" => Some(Self::Locale), "relative" => Some(Self::Relative), _ if value.starts_with('+') => Self::from_format_string(value), _ => { print_error!("Not a valid date value: {}.", value); None } } } } impl Configurable<Self> for DateFlag { /// Get a potential `DateFlag` variant from [Cli]. /// /// If the "classic" argument is passed, then this returns the [DateFlag::Date] variant in a /// [Some]. Otherwise if the argument is passed, this returns the variant corresponding to its /// parameter in a [Some]. Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.classic { Some(Self::Date) } else { cli.date.as_deref().and_then(Self::from_str) } } /// Get a potential `DateFlag` variant from a [Config]. /// /// If the `Config::classic` is `true` then this returns the Some(DateFlag::Date), /// Otherwise if the `Config::date` has value and is one of "date", "locale" or "relative", /// this returns its corresponding variant in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { if config.classic == Some(true) { Some(Self::Date) } else { config.date.as_ref().and_then(Self::from_str) } } /// Get a potential `DateFlag` variant from the environment. fn from_environment() -> Option<Self> { if let Ok(value) = std::env::var("TIME_STYLE") { match value.as_str() { "full-iso" => Some(Self::Formatted("%F %T.%f %z".into())), "long-iso" => Some(Self::Formatted("%F %R".into())), "locale" => Some(Self::Locale), "iso" => Some(Self::Iso), _ if value.starts_with('+') => Self::from_format_string(&value), _ => { print_error!("Not a valid date value: {}.", value); None } } } else { None } } } #[cfg(test)] mod test { use clap::Parser; use super::DateFlag; use crate::app::Cli; use crate::config_file::Config; use crate::flags::Configurable; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, DateFlag::from_cli(&cli)); } #[test] fn test_from_cli_date() { let argv = ["lsd", "--date", "date"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(DateFlag::Date), DateFlag::from_cli(&cli)); } #[test] fn test_from_cli_locale() { let argv = ["lsd", "--date", "locale"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(DateFlag::Locale), DateFlag::from_cli(&cli)); } #[test] fn test_from_cli_relative() { let argv = ["lsd", "--date", "relative"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(DateFlag::Relative), DateFlag::from_cli(&cli)); } #[test] fn test_from_cli_format() { let argv = ["lsd", "--date", "+%F"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!( Some(DateFlag::Formatted("%F".to_string())), DateFlag::from_cli(&cli) ); } #[test] #[should_panic(expected = "invalid format specifier: %J")] fn test_from_cli_format_invalid() { let argv = ["lsd", "--date", "+%J"]; let cli = Cli::try_parse_from(argv).unwrap(); DateFlag::from_cli(&cli); } #[test] fn test_from_cli_classic_mode() { let argv = ["lsd", "--date", "date", "--classic"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(DateFlag::Date), DateFlag::from_cli(&cli)); } #[test] fn test_from_cli_date_multi() { let argv = ["lsd", "--date", "relative", "--date", "date"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(DateFlag::Date), DateFlag::from_cli(&cli)); } #[test] fn test_from_config_none() { assert_eq!(None, DateFlag::from_config(&Config::with_none())); } #[test] fn test_from_config_date() { let mut c = Config::with_none(); c.date = Some("date".into()); assert_eq!(Some(DateFlag::Date), DateFlag::from_config(&c)); } #[test] fn test_from_config_relative() { let mut c = Config::with_none(); c.date = Some("relative".into()); assert_eq!(Some(DateFlag::Relative), DateFlag::from_config(&c)); } #[test] fn test_from_config_format() { let mut c = Config::with_none(); c.date = Some("+%F".into()); assert_eq!( Some(DateFlag::Formatted("%F".to_string())), DateFlag::from_config(&c) ); } #[test] fn test_from_config_format_invalid() { let mut c = Config::with_none(); c.date = Some("+%J".into()); assert_eq!(None, DateFlag::from_config(&c)); } #[test] fn test_from_config_classic_mode() { let mut c = Config::with_none(); c.date = Some("relative".into()); c.classic = Some(true); assert_eq!(Some(DateFlag::Date), DateFlag::from_config(&c)); } #[test] #[serial_test::serial] fn test_from_environment_none() { std::env::set_var("TIME_STYLE", ""); assert_eq!(None, DateFlag::from_environment()); } #[test] #[serial_test::serial] fn test_from_environment_full_iso() { std::env::set_var("TIME_STYLE", "full-iso"); assert_eq!( Some(DateFlag::Formatted("%F %T.%f %z".into())), DateFlag::from_environment() ); } #[test] #[serial_test::serial] fn test_from_environment_long_iso() { std::env::set_var("TIME_STYLE", "long-iso"); assert_eq!( Some(DateFlag::Formatted("%F %R".into())), DateFlag::from_environment() ); } #[test] #[serial_test::serial] fn test_from_environment_iso() { std::env::set_var("TIME_STYLE", "iso"); assert_eq!(Some(DateFlag::Iso), DateFlag::from_environment()); } #[test] #[serial_test::serial] fn test_from_environment_format() { std::env::set_var("TIME_STYLE", "+%F"); assert_eq!( Some(DateFlag::Formatted("%F".into())), DateFlag::from_environment() ); } #[test] #[serial_test::serial] fn test_parsing_order_arg() { std::env::set_var("TIME_STYLE", "+%R"); let argv = ["lsd", "--date", "+%F"]; let cli = Cli::try_parse_from(argv).unwrap(); let mut config = Config::with_none(); config.date = Some("+%c".into()); assert_eq!( DateFlag::Formatted("%F".into()), DateFlag::configure_from(&cli, &config) ); } #[test] #[serial_test::serial] fn test_parsing_order_env() { std::env::set_var("TIME_STYLE", "+%R"); let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); let mut config = Config::with_none(); config.date = Some("+%c".into()); assert_eq!( DateFlag::Formatted("%R".into()), DateFlag::configure_from(&cli, &config) ); } #[test] #[serial_test::serial] fn test_parsing_order_config() { std::env::set_var("TIME_STYLE", ""); let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); let mut config = Config::with_none(); config.date = Some("+%c".into()); assert_eq!( DateFlag::Formatted("%c".into()), DateFlag::configure_from(&cli, &config) ); } } 07070100000024000081A400000000000000000000000166C4C379000008D5000000000000000000000000000000000000002300000000lsd-1.1.5/src/flags/dereference.rs//! This module defines the [Dereference] flag. To set it up from [Cli], a [Config] and its //! [Default] value, use the [configure_from](Configurable::configure_from) method. use super::Configurable; use crate::app::Cli; use crate::config_file::Config; /// The flag showing whether to dereference symbolic links. #[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] pub struct Dereference(pub bool); impl Configurable<Self> for Dereference { /// Get a potential `Dereference` value from [Cli]. /// /// If the "dereference" argument is passed, this returns a `Dereference` with value `true` in /// a [Some]. Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.dereference { Some(Self(true)) } else { None } } /// Get a potential `Dereference` value from a [Config]. /// /// If the `Config::dereference` has value, this returns its value /// as the value of the `Dereference`, in a [Some], Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { config.dereference.map(Self) } } #[cfg(test)] mod test { use clap::Parser; use super::Dereference; use crate::app::Cli; use crate::config_file::Config; use crate::flags::Configurable; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, Dereference::from_cli(&cli)); } #[test] fn test_from_cli_true() { let argv = ["lsd", "--dereference"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(Dereference(true)), Dereference::from_cli(&cli)); } #[test] fn test_from_config_none() { assert_eq!(None, Dereference::from_config(&Config::with_none())); } #[test] fn test_from_config_true() { let mut c = Config::with_none(); c.dereference = Some(true); assert_eq!(Some(Dereference(true)), Dereference::from_config(&c)); } #[test] fn test_from_config_false() { let mut c = Config::with_none(); c.dereference = Some(false); assert_eq!(Some(Dereference(false)), Dereference::from_config(&c)); } } 07070100000025000081A400000000000000000000000166C4C37900001045000000000000000000000000000000000000001F00000000lsd-1.1.5/src/flags/display.rs//! This module defines the [Display] flag. To set it up from [Cli], a [Config] and its //! [Default] value, use its [configure_from](Configurable::configure_from) method. use super::Configurable; use crate::app::Cli; use crate::config_file::Config; use serde::Deserialize; /// The flag showing which file system nodes to display. #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] #[serde(rename_all = "kebab-case")] pub enum Display { /// windows only, used to show files with system protected flag SystemProtected, All, AlmostAll, DirectoryOnly, #[default] VisibleOnly, } impl Configurable<Self> for Display { /// Get a potential `Display` variant from [Cli]. /// /// If any of the "all", "almost-all" or "directory-only" arguments is passed, this returns the /// corresponding `Display` variant in a [Some]. If neither of them is passed, this returns /// [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.directory_only { Some(Self::DirectoryOnly) } else if cli.almost_all { Some(Self::AlmostAll) } else if cli.all { Some(Self::All) } else if cli.system_protected { #[cfg(windows)] return Some(Self::SystemProtected); #[cfg(not(windows))] return Some(Self::All); } else { None } } /// Get a potential `Display` variant from a [Config]. /// /// If the `Config::display` has value and is one of /// "all", "almost-all", "directory-only" or `visible-only`, /// this returns the corresponding `Display` variant in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { config.display } } #[cfg(test)] mod test { use clap::Parser; use super::Display; use crate::app::Cli; use crate::config_file::Config; use crate::flags::Configurable; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, Display::from_cli(&cli)); } #[test] fn test_from_cli_system_protected() { let argv = ["lsd", "--system-protected"]; let cli = Cli::try_parse_from(argv).unwrap(); #[cfg(windows)] assert_eq!(Some(Display::SystemProtected), Display::from_cli(&cli)); #[cfg(not(windows))] assert_eq!(Some(Display::All), Display::from_cli(&cli)); } #[test] fn test_from_cli_all() { let argv = ["lsd", "--all"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(Display::All), Display::from_cli(&cli)); } #[test] fn test_from_cli_almost_all() { let argv = ["lsd", "--almost-all"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(Display::AlmostAll), Display::from_cli(&cli)); } #[test] fn test_from_cli_directory_only() { let argv = ["lsd", "--directory-only"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(Display::DirectoryOnly), Display::from_cli(&cli)); } #[test] fn test_from_config_none() { assert_eq!(None, Display::from_config(&Config::with_none())); } #[test] fn test_from_config_all() { let mut c = Config::with_none(); c.display = Some(Display::All); assert_eq!(Some(Display::All), Display::from_config(&c)); } #[test] fn test_from_config_almost_all() { let mut c = Config::with_none(); c.display = Some(Display::AlmostAll); assert_eq!(Some(Display::AlmostAll), Display::from_config(&c)); } #[test] fn test_from_config_directory_only() { let mut c = Config::with_none(); c.display = Some(Display::DirectoryOnly); assert_eq!(Some(Display::DirectoryOnly), Display::from_config(&c)); } #[test] fn test_from_config_visible_only() { let mut c = Config::with_none(); c.display = Some(Display::VisibleOnly); assert_eq!(Some(Display::VisibleOnly), Display::from_config(&c)); } } 07070100000026000081A400000000000000000000000166C4C3790000085E000000000000000000000000000000000000001E00000000lsd-1.1.5/src/flags/header.rs//! This module defines the [Header] flag. To set it up from [Cli], a [Config] and its //! [Default] value, use the [configure_from](Configurable::configure_from) method. use super::Configurable; use crate::app::Cli; use crate::config_file::Config; /// The flag showing whether to display block headers. #[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] pub struct Header(pub bool); impl Configurable<Self> for Header { /// Get a potential `Header` value from [Cli]. /// /// If the "header" argument is passed, this returns a `Header` with value `true` in a /// [Some]. Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.header { Some(Self(true)) } else { None } } /// Get a potential `Header` value from a [Config]. /// /// If the `Config::header` has value, /// this returns it as the value of the `Header`, in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { config.header.map(Self) } } #[cfg(test)] mod test { use clap::Parser; use super::Header; use crate::app::Cli; use crate::config_file::Config; use crate::flags::Configurable; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, Header::from_cli(&cli)); } #[test] fn test_from_cli_true() { let argv = ["lsd", "--header"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(Header(true)), Header::from_cli(&cli)); } #[test] fn test_from_config_none() { assert_eq!(None, Header::from_config(&Config::with_none())); } #[test] fn test_from_config_true() { let mut c = Config::with_none(); c.header = Some(true); assert_eq!(Some(Header(true)), Header::from_config(&c)); } #[test] fn test_from_config_false() { let mut c = Config::with_none(); c.header = Some(false); assert_eq!(Some(Header(false)), Header::from_config(&c)); } } 07070100000027000081A400000000000000000000000166C4C379000013B9000000000000000000000000000000000000002100000000lsd-1.1.5/src/flags/hyperlink.rs//! This module defines the [HyperlinkOption]. To set it up from [Cli], a [Config] and its //! [Default] value, use its [configure_from](Configurable::configure_from) method. use super::Configurable; use crate::app::Cli; use crate::config_file::Config; use serde::Deserialize; /// The flag showing when to use hyperlink in the output. #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] #[serde(rename_all = "kebab-case")] pub enum HyperlinkOption { Always, Auto, #[default] Never, } impl HyperlinkOption { fn from_arg_str(value: &str) -> Self { match value { "always" => Self::Always, "auto" => Self::Auto, "never" => Self::Never, // Invalid value should be handled by `clap` when building an `Cli` other => unreachable!("Invalid value '{other}' for 'hyperlink'"), } } } impl Configurable<Self> for HyperlinkOption { /// Get a potential `HyperlinkOption` variant from [Cli]. /// /// If the "classic" argument is passed, then this returns the [HyperlinkOption::Never] variant in /// a [Some]. Otherwise if the argument is passed, this returns the variant corresponding to /// its parameter in a [Some]. Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.classic { Some(Self::Never) } else { cli.hyperlink.as_deref().map(Self::from_arg_str) } } /// Get a potential `HyperlinkOption` variant from a [Config]. /// /// If the `Configs::classic` has value and is "true" then this returns Some(HyperlinkOption::Never). /// Otherwise if the `Config::hyperlink::when` has value and is one of "always", "auto" or "never", /// this returns its corresponding variant in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { if config.classic == Some(true) { Some(Self::Never) } else { config.hyperlink } } } #[cfg(test)] mod test_hyperlink_option { use clap::Parser; use super::HyperlinkOption; use crate::app::Cli; use crate::config_file::Config; use crate::flags::Configurable; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, HyperlinkOption::from_cli(&cli)); } #[test] fn test_from_cli_always() { let argv = ["lsd", "--hyperlink", "always"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!( Some(HyperlinkOption::Always), HyperlinkOption::from_cli(&cli) ); } #[test] fn test_from_cli_auto() { let argv = ["lsd", "--hyperlink", "auto"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(HyperlinkOption::Auto), HyperlinkOption::from_cli(&cli)); } #[test] fn test_from_cli_never() { let argv = ["lsd", "--hyperlink", "never"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!( Some(HyperlinkOption::Never), HyperlinkOption::from_cli(&cli) ); } #[test] fn test_from_cli_classic_mode() { let argv = ["lsd", "--hyperlink", "always", "--classic"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!( Some(HyperlinkOption::Never), HyperlinkOption::from_cli(&cli) ); } #[test] fn test_from_cli_hyperlink_when_multi() { let argv = ["lsd", "--hyperlink", "always", "--hyperlink", "never"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!( Some(HyperlinkOption::Never), HyperlinkOption::from_cli(&cli) ); } #[test] fn test_from_config_none() { assert_eq!(None, HyperlinkOption::from_config(&Config::with_none())); } #[test] fn test_from_config_always() { let mut c = Config::with_none(); c.hyperlink = Some(HyperlinkOption::Always); assert_eq!( Some(HyperlinkOption::Always), HyperlinkOption::from_config(&c) ); } #[test] fn test_from_config_auto() { let mut c = Config::with_none(); c.hyperlink = Some(HyperlinkOption::Auto); assert_eq!( Some(HyperlinkOption::Auto), HyperlinkOption::from_config(&c) ); } #[test] fn test_from_config_never() { let mut c = Config::with_none(); c.hyperlink = Some(HyperlinkOption::Never); assert_eq!( Some(HyperlinkOption::Never), HyperlinkOption::from_config(&c) ); } #[test] fn test_from_config_classic_mode() { let mut c = Config::with_none(); c.classic = Some(true); c.hyperlink = Some(HyperlinkOption::Always); assert_eq!( Some(HyperlinkOption::Never), HyperlinkOption::from_config(&c) ); } } 07070100000028000081A400000000000000000000000166C4C37900002A6D000000000000000000000000000000000000001D00000000lsd-1.1.5/src/flags/icons.rs//! This module defines the [IconOption]. To set it up from [Cli], a [Config] and its //! [Default] value, use its [configure_from](Configurable::configure_from) method. use super::Configurable; use crate::app::Cli; use crate::config_file::Config; use serde::Deserialize; /// A collection of flags on how to use icons. #[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct Icons { /// When to use icons. pub when: IconOption, /// Which icon theme to use. pub theme: IconTheme, /// String between icon and name. pub separator: IconSeparator, } impl Icons { /// Get an `Icons` struct from [Cli], a [Config] or the [Default] values. /// /// The [IconOption] and [IconTheme] are configured with their respective [Configurable] /// implementation. pub fn configure_from(cli: &Cli, config: &Config) -> Self { let when = IconOption::configure_from(cli, config); let theme = IconTheme::configure_from(cli, config); let separator = IconSeparator::configure_from(cli, config); Self { when, theme, separator, } } } /// The flag showing when to use icons in the output. #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] #[serde(rename_all = "kebab-case")] pub enum IconOption { Always, #[default] Auto, Never, } impl IconOption { fn from_arg_str(value: &str) -> Self { match value { "always" => Self::Always, "auto" => Self::Auto, "never" => Self::Never, // Invalid value should be handled by `clap` when building an `Cli` other => unreachable!("Invalid value '{other}' for 'icon'"), } } } impl Configurable<Self> for IconOption { /// Get a potential `IconOption` variant from [Cli]. /// /// If the "classic" argument is passed, then this returns the [IconOption::Never] variant in /// a [Some]. Otherwise if the argument is passed, this returns the variant corresponding to /// its parameter in a [Some]. Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.classic { Some(Self::Never) } else { cli.icon.as_deref().map(Self::from_arg_str) } } /// Get a potential `IconOption` variant from a [Config]. /// /// If the `Configs::classic` has value and is "true" then this returns Some(IconOption::Never). /// Otherwise if the `Config::icon::when` has value and is one of "always", "auto" or "never", /// this returns its corresponding variant in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { if config.classic == Some(true) { Some(Self::Never) } else { config.icons.as_ref().and_then(|icon| icon.when) } } } /// The flag showing which icon theme to use. #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum IconTheme { Unicode, #[default] Fancy, } impl IconTheme { fn from_arg_str(value: &str) -> Self { match value { "fancy" => Self::Fancy, "unicode" => Self::Unicode, // Invalid value should be handled by `clap` when building an `Cli` other => unreachable!("Invalid value '{other}' for 'icon-theme'"), } } } impl Configurable<Self> for IconTheme { /// Get a potential `IconTheme` variant from [Cli]. /// /// If the argument is passed, this returns the variant corresponding to its parameter in a /// [Some]. Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { cli.icon_theme.as_deref().map(Self::from_arg_str) } /// Get a potential `IconTheme` variant from a [Config]. /// /// If the `Config::icons::theme` has value and is one of "fancy" or "unicode", /// this returns its corresponding variant in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { config.icons.as_ref().and_then(|icon| icon.theme.clone()) } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct IconSeparator(pub String); impl Configurable<Self> for IconSeparator { /// Get a potential `IconSeparator` variant from [Cli]. /// /// If the argument is passed, this returns the variant corresponding to its parameter in a /// [Some]. Otherwise this returns [None]. fn from_cli(_cli: &Cli) -> Option<Self> { None } /// Get a potential `IconSeparator` variant from a [Config]. /// /// This returns its corresponding variant in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { if let Some(icon) = &config.icons { if let Some(separator) = icon.separator.clone() { return Some(IconSeparator(separator)); } } None } } /// The default value for `IconSeparator` is [" "]. impl Default for IconSeparator { fn default() -> Self { IconSeparator(" ".to_string()) } } #[cfg(test)] mod test_icon_option { use clap::Parser; use super::IconOption; use crate::app::Cli; use crate::config_file::{Config, Icons}; use crate::flags::Configurable; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, IconOption::from_cli(&cli)); } #[test] fn test_from_cli_always() { let argv = ["lsd", "--icon", "always"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(IconOption::Always), IconOption::from_cli(&cli)); } #[test] fn test_from_cli_auto() { let argv = ["lsd", "--icon", "auto"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(IconOption::Auto), IconOption::from_cli(&cli)); } #[test] fn test_from_cli_never() { let argv = ["lsd", "--icon", "never"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(IconOption::Never), IconOption::from_cli(&cli)); } #[test] fn test_from_cli_classic_mode() { let argv = ["lsd", "--icon", "always", "--classic"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(IconOption::Never), IconOption::from_cli(&cli)); } #[test] fn test_from_cli_icon_when_multi() { let argv = ["lsd", "--icon", "always", "--icon", "never"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(IconOption::Never), IconOption::from_cli(&cli)); } #[test] fn test_from_config_none() { assert_eq!(None, IconOption::from_config(&Config::with_none())); } #[test] fn test_from_config_always() { let mut c = Config::with_none(); c.icons = Some(Icons { when: Some(IconOption::Always), theme: None, separator: None, }); assert_eq!(Some(IconOption::Always), IconOption::from_config(&c)); } #[test] fn test_from_config_auto() { let mut c = Config::with_none(); c.icons = Some(Icons { when: Some(IconOption::Auto), theme: None, separator: None, }); assert_eq!(Some(IconOption::Auto), IconOption::from_config(&c)); } #[test] fn test_from_config_never() { let mut c = Config::with_none(); c.icons = Some(Icons { when: Some(IconOption::Never), theme: None, separator: None, }); assert_eq!(Some(IconOption::Never), IconOption::from_config(&c)); } #[test] fn test_from_config_classic_mode() { let mut c = Config::with_none(); c.classic = Some(true); c.icons = Some(Icons { when: Some(IconOption::Always), theme: None, separator: None, }); assert_eq!(Some(IconOption::Never), IconOption::from_config(&c)); } } #[cfg(test)] mod test_icon_theme { use clap::Parser; use super::IconTheme; use crate::app::Cli; use crate::config_file::{Config, Icons}; use crate::flags::Configurable; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, IconTheme::from_cli(&cli)); } #[test] fn test_from_cli_fancy() { let argv = ["lsd", "--icon-theme", "fancy"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(IconTheme::Fancy), IconTheme::from_cli(&cli)); } #[test] fn test_from_cli_unicode() { let argv = ["lsd", "--icon-theme", "unicode"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(IconTheme::Unicode), IconTheme::from_cli(&cli)); } #[test] fn test_from_cli_icon_multi() { let argv = ["lsd", "--icon-theme", "fancy", "--icon-theme", "unicode"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(IconTheme::Unicode), IconTheme::from_cli(&cli)); } #[test] fn test_from_config_none() { assert_eq!(None, IconTheme::from_config(&Config::with_none())); } #[test] fn test_from_config_fancy() { let mut c = Config::with_none(); c.icons = Some(Icons { when: None, theme: Some(IconTheme::Fancy), separator: None, }); assert_eq!(Some(IconTheme::Fancy), IconTheme::from_config(&c)); } #[test] fn test_from_config_unicode() { let mut c = Config::with_none(); c.icons = Some(Icons { when: None, theme: Some(IconTheme::Unicode), separator: None, }); assert_eq!(Some(IconTheme::Unicode), IconTheme::from_config(&c)); } } #[cfg(test)] mod test_icon_separator { use super::IconSeparator; use crate::config_file::{Config, Icons}; use crate::flags::Configurable; #[test] fn test_from_config_default() { let mut c = Config::with_none(); c.icons = Some(Icons { when: None, theme: None, separator: Some(" ".to_string()), }); let expected = Some(IconSeparator(" ".to_string())); assert_eq!(expected, IconSeparator::from_config(&c)); } #[test] fn test_from_config_custom() { let mut c = Config::with_none(); c.icons = Some(Icons { when: None, theme: None, separator: Some(" |".to_string()), }); let expected = Some(IconSeparator(" |".to_string())); assert_eq!(expected, IconSeparator::from_config(&c)); } } 07070100000029000081A400000000000000000000000166C4C37900001559000000000000000000000000000000000000002400000000lsd-1.1.5/src/flags/ignore_globs.rs//! This module defines the [IgnoreGlobs]. To set it up from [Cli], a [Config] and its //! [Default] value, use the [configure_from](IgnoreGlobs::configure_from) method. use crate::app::Cli; use crate::config_file::Config; use clap::error::ErrorKind; use clap::Error; use globset::{Glob, GlobSet, GlobSetBuilder}; /// The struct holding a [GlobSet] and methods to build it. #[derive(Clone, Debug)] pub struct IgnoreGlobs(pub GlobSet); impl IgnoreGlobs { /// Returns a value from either [Cli], a [Config] or a [Default] value. The first value /// that is not [None] is used. The order of precedence for the value used is: /// - [from_cli](IgnoreGlobs::from_cli) /// - [from_config](IgnoreGlobs::from_config) /// - [Default::default] /// /// # Errors /// /// If either of the [Glob::new] or [GlobSetBuilder.build] methods return an [Err]. pub fn configure_from(cli: &Cli, config: &Config) -> Result<Self, Error> { if let Some(value) = Self::from_cli(cli) { return value; } if let Some(value) = Self::from_config(config) { return value; } Ok(Default::default()) } /// Get a potential [IgnoreGlobs] from [Cli]. /// /// If the "ignore-glob" argument has been passed, this returns a [Result] in a [Some] with /// either the built [IgnoreGlobs] or an [Error], if any error was encountered while creating the /// [IgnoreGlobs]. If the argument has not been passed, this returns [None]. fn from_cli(cli: &Cli) -> Option<Result<Self, Error>> { if cli.ignore_glob.is_empty() { return None; } let mut glob_set_builder = GlobSetBuilder::new(); for value in &cli.ignore_glob { match Self::create_glob(value) { Ok(glob) => { glob_set_builder.add(glob); } Err(err) => return Some(Err(err)), } } Some(Self::create_glob_set(&glob_set_builder).map(Self)) } /// Get a potential [IgnoreGlobs] from a [Config]. /// /// If the `Config::ignore-globs` contains an Array of Strings, /// each of its values is used to build the [GlobSet]. If the building /// succeeds, the [IgnoreGlobs] is returned in the [Result] in a [Some]. If any error is /// encountered while building, an [Error] is returned in the Result instead. If the Config does /// not contain such a key, this returns [None]. fn from_config(config: &Config) -> Option<Result<Self, Error>> { let globs = config.ignore_globs.as_ref()?; let mut glob_set_builder = GlobSetBuilder::new(); for glob in globs { match Self::create_glob(glob) { Ok(glob) => { glob_set_builder.add(glob); } Err(err) => return Some(Err(err)), } } Some(Self::create_glob_set(&glob_set_builder).map(Self)) } /// Create a [Glob] from a provided pattern. /// /// This method is mainly a helper to wrap the handling of potential errors. fn create_glob(pattern: &str) -> Result<Glob, Error> { Glob::new(pattern).map_err(|err| Error::raw(ErrorKind::ValueValidation, err)) } /// Create a [GlobSet] from a provided [GlobSetBuilder]. /// /// This method is mainly a helper to wrap the handling of potential errors. fn create_glob_set(builder: &GlobSetBuilder) -> Result<GlobSet, Error> { builder .build() .map_err(|err| Error::raw(ErrorKind::ValueValidation, err)) } } /// The default value of `IgnoreGlobs` is the empty [GlobSet], returned by [GlobSet::empty()]. impl Default for IgnoreGlobs { fn default() -> Self { Self(GlobSet::empty()) } } #[cfg(test)] mod test { use clap::Parser; use super::IgnoreGlobs; use crate::app::Cli; use crate::config_file::Config; // The following tests are implemented using match expressions instead of the assert_eq macro, // because clap::Error does not implement PartialEq. // // Further no tests for actually returned GlobSets are implemented, because GlobSet does not // even implement PartialEq and thus can not be easily compared. #[test] fn test_configuration_from_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert!(matches!( IgnoreGlobs::configure_from(&cli, &Config::with_none()), Ok(..) )); } #[test] fn test_configuration_from_args() { let argv = ["lsd", "--ignore-glob", ".git"]; let cli = Cli::try_parse_from(argv).unwrap(); assert!(matches!( IgnoreGlobs::configure_from(&cli, &Config::with_none()), Ok(..) )); } #[test] fn test_configuration_from_config() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); let mut c = Config::with_none(); c.ignore_globs = Some(vec![".git".into()]); assert!(matches!(IgnoreGlobs::configure_from(&cli, &c), Ok(..))); } #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert!(IgnoreGlobs::from_cli(&cli).is_none()); } #[test] fn test_from_config_none() { assert!(IgnoreGlobs::from_config(&Config::with_none()).is_none()); } } 0707010000002A000081A400000000000000000000000166C4C379000008DA000000000000000000000000000000000000002200000000lsd-1.1.5/src/flags/indicators.rs//! This module defines the [Indicators] flag. To set it up from [Cli], a [Config] and its //! [Default] value, use the [configure_from](Configurable::configure_from) method. use super::Configurable; use crate::app::Cli; use crate::config_file::Config; /// The flag showing whether to print file type indicators. #[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] pub struct Indicators(pub bool); impl Configurable<Self> for Indicators { /// Get a potential `Indicators` value from [Cli]. /// /// If the "indicators" argument is passed, this returns an `Indicators` with value `true` in a /// [Some]. Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.indicators { Some(Self(true)) } else { None } } /// Get a potential `Indicators` value from a [Config]. /// /// If the `Config::indicators` has value, /// this returns its value as the value of the `Indicators`, in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { config.indicators.as_ref().map(|ind| Self(*ind)) } } #[cfg(test)] mod test { use clap::Parser; use super::Indicators; use crate::app::Cli; use crate::config_file::Config; use crate::flags::Configurable; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, Indicators::from_cli(&cli)); } #[test] fn test_from_cli_true() { let argv = ["lsd", "--classify"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(Indicators(true)), Indicators::from_cli(&cli)); } #[test] fn test_from_config_none() { assert_eq!(None, Indicators::from_config(&Config::with_none())); } #[test] fn test_from_config_true() { let mut c = Config::with_none(); c.indicators = Some(true); assert_eq!(Some(Indicators(true)), Indicators::from_config(&c)); } #[test] fn test_from_config_false() { let mut c = Config::with_none(); c.indicators = Some(false); assert_eq!(Some(Indicators(false)), Indicators::from_config(&c)); } } 0707010000002B000081A400000000000000000000000166C4C37900000DF4000000000000000000000000000000000000001E00000000lsd-1.1.5/src/flags/layout.rs//! This module defines the [Layout] flag. To set it up from [Cli], a [Config] and its //! [Default] value, use its [configure_from](Configurable::configure_from) method. use crate::app::Cli; use crate::config_file::Config; use super::Configurable; use serde::Deserialize; /// The flag showing which output layout to print. #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum Layout { #[default] Grid, Tree, OneLine, } impl Configurable<Layout> for Layout { /// Get a potential `Layout` variant from [Cli]. /// /// If any of the "tree", "long" or "oneline" arguments is passed, this returns the /// corresponding `Layout` variant in a [Some]. Otherwise if the number of passed "blocks" /// arguments is greater than 1, this also returns the [OneLine](Layout::OneLine) variant. /// Finally if neither of them is passed, this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.tree { Some(Self::Tree) } else if cli.long || cli.oneline || cli.inode || cli.context || cli.blocks.len() > 1 // TODO: handle this differently { Some(Self::OneLine) } else { None } } /// Get a potential Layout variant from a [Config]. /// /// If the `Config::layout` has value and is one of "tree", "oneline" or "grid", /// this returns the corresponding `Layout` variant in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { config.layout } } #[cfg(test)] mod test { use clap::Parser; use super::Layout; use crate::app::Cli; use crate::config_file::Config; use crate::flags::Configurable; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, Layout::from_cli(&cli)); } #[test] fn test_from_cli_tree() { let argv = ["lsd", "--tree"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(Layout::Tree), Layout::from_cli(&cli)); } #[test] fn test_from_cli_oneline() { let argv = ["lsd", "--oneline"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(Layout::OneLine), Layout::from_cli(&cli)); } #[test] fn test_from_cli_oneline_through_long() { let argv = ["lsd", "--long"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(Layout::OneLine), Layout::from_cli(&cli)); } #[test] fn test_from_cli_oneline_through_blocks() { let argv = ["lsd", "--blocks", "permission,name"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(Layout::OneLine), Layout::from_cli(&cli)); } #[test] fn test_from_config_none() { assert_eq!(None, Layout::from_config(&Config::with_none())); } #[test] fn test_from_config_tree() { let mut c = Config::with_none(); c.layout = Some(Layout::Tree); assert_eq!(Some(Layout::Tree), Layout::from_config(&c)); } #[test] fn test_from_config_oneline() { let mut c = Config::with_none(); c.layout = Some(Layout::OneLine); assert_eq!(Some(Layout::OneLine), Layout::from_config(&c)); } #[test] fn test_from_config_grid() { let mut c = Config::with_none(); c.layout = Some(Layout::Grid); assert_eq!(Some(Layout::Grid), Layout::from_config(&c)); } } 0707010000002C000081A400000000000000000000000166C4C3790000088E000000000000000000000000000000000000001F00000000lsd-1.1.5/src/flags/literal.rs//! This module defines the [Literal]. To set it up from [Cli], a [Config] and its //! [Default] value, use its [configure_from](Configurable::configure_from) method. use super::Configurable; use crate::app::Cli; use crate::config_file::Config; /// The flag to set in order to show literal file names without quotes. #[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] pub struct Literal(pub bool); impl Configurable<Self> for Literal { /// Get a potential `Literal` value from [Cli]. /// /// If the "literal" argument is passed, this returns a `Literal` with value `true` in a /// [Some]. Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.literal { Some(Self(true)) } else { None } } /// Get a potential `Literal` value from a [Config]. /// /// If the `Config::indicators` has value, /// this returns its value as the value of the `Literal`, in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { config.literal.map(Self) } } #[cfg(test)] mod test { use clap::Parser; use super::Literal; use crate::app::Cli; use crate::config_file::Config; use crate::flags::Configurable; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, Literal::from_cli(&cli)); } #[test] fn test_from_cli_literal() { let argv = ["lsd", "--literal"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(Literal(true)), Literal::from_cli(&cli)); } #[test] fn test_from_config_none() { assert_eq!(None, Literal::from_config(&Config::with_none())); } #[test] fn test_from_config_true() { let mut c = Config::with_none(); c.literal = Some(true); assert_eq!(Some(Literal(true)), Literal::from_config(&c)); } #[test] fn test_from_config_false() { let mut c = Config::with_none(); c.literal = Some(false); assert_eq!(Some(Literal(false)), Literal::from_config(&c)); } } 0707010000002D000081A400000000000000000000000166C4C379000015C6000000000000000000000000000000000000002200000000lsd-1.1.5/src/flags/permission.rs//! This module defines the [PermissionFlag]. To set it up from [Cli], a [Config] and its //! [Default] value, use its [configure_from](Configurable::configure_from) method. use super::Configurable; use crate::app::Cli; use crate::config_file::Config; use serde::Deserialize; /// The flag showing which file permissions units to use. #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] #[serde(rename_all = "kebab-case")] pub enum PermissionFlag { /// The variant to show file permissions in rwx format #[cfg_attr(not(target_os = "windows"), default)] Rwx, /// The variant to show file permissions in octal format Octal, /// (windows only): Attributes from powershell's `Get-ChildItem` #[cfg_attr(target_os = "windows", default)] Attributes, /// Disable the display of owner and permissions, may be used to speed up in Windows Disable, } impl PermissionFlag { fn from_arg_str(value: &str) -> Self { match value { "rwx" => Self::Rwx, "octal" => Self::Octal, "attributes" => Self::Attributes, "disable" => Self::Disable, // Invalid value should be handled by `clap` when building an `Cli` other => unreachable!("Invalid value '{other}' for 'permission'"), } } } impl Configurable<Self> for PermissionFlag { /// Get a potential `PermissionFlag` variant from [Cli]. /// /// If any of the "rwx" or "octal" arguments is passed, the corresponding /// `PermissionFlag` variant is returned in a [Some]. If neither of them is passed, /// this returns [None]. /// Sets permissions to rwx if classic flag is enabled. fn from_cli(cli: &Cli) -> Option<Self> { if cli.classic { Some(Self::Rwx) } else { cli.permission.as_deref().map(Self::from_arg_str) } } /// Get a potential `PermissionFlag` variant from a [Config]. /// /// If the `Config::permissions` has value and is one of "rwx" or "octal", /// this returns the corresponding `PermissionFlag` variant in a [Some]. /// Otherwise this returns [None]. /// Sets permissions to rwx if classic flag is enabled. fn from_config(config: &Config) -> Option<Self> { if config.classic == Some(true) { Some(Self::Rwx) } else { config.permission } } } #[cfg(test)] mod test { use clap::Parser; use super::PermissionFlag; use crate::app::Cli; use crate::config_file::Config; use crate::flags::Configurable; #[test] fn test_default() { let expected = if cfg!(target_os = "windows") { PermissionFlag::Attributes } else { PermissionFlag::Rwx }; assert_eq!(expected, PermissionFlag::default()); } #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, PermissionFlag::from_cli(&cli)); } #[test] fn test_from_cli_default() { let argv = ["lsd", "--permission", "rwx"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(PermissionFlag::Rwx), PermissionFlag::from_cli(&cli)); } #[test] fn test_from_cli_short() { let argv = ["lsd", "--permission", "octal"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(PermissionFlag::Octal), PermissionFlag::from_cli(&cli)); } #[test] fn test_from_cli_attributes() { let argv = ["lsd", "--permission", "attributes"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!( Some(PermissionFlag::Attributes), PermissionFlag::from_cli(&cli) ); } #[test] fn test_from_cli_permissions_disable() { let argv = ["lsd", "--permission", "disable"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!( Some(PermissionFlag::Disable), PermissionFlag::from_cli(&cli) ); } #[test] #[should_panic] fn test_from_cli_unknown() { let argv = ["lsd", "--permission", "unknown"]; let _ = Cli::try_parse_from(argv).unwrap(); } #[test] fn test_from_cli_permissions_multi() { let argv = ["lsd", "--permission", "octal", "--permission", "rwx"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(PermissionFlag::Rwx), PermissionFlag::from_cli(&cli)); } #[test] fn test_from_cli_permissions_classic() { let argv = ["lsd", "--permission", "rwx", "--classic"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(PermissionFlag::Rwx), PermissionFlag::from_cli(&cli)); } #[test] fn test_from_config_none() { assert_eq!(None, PermissionFlag::from_config(&Config::with_none())); } #[test] fn test_from_config_rwx() { let mut c = Config::with_none(); c.permission = Some(PermissionFlag::Rwx); assert_eq!(Some(PermissionFlag::Rwx), PermissionFlag::from_config(&c)); } #[test] fn test_from_config_octal() { let mut c = Config::with_none(); c.permission = Some(PermissionFlag::Octal); assert_eq!(Some(PermissionFlag::Octal), PermissionFlag::from_config(&c)); } #[test] fn test_from_config_classic_mode() { let mut c = Config::with_none(); c.classic = Some(true); assert_eq!(Some(PermissionFlag::Rwx), PermissionFlag::from_config(&c)); } } 0707010000002E000081A400000000000000000000000166C4C3790000199D000000000000000000000000000000000000002100000000lsd-1.1.5/src/flags/recursion.rs//! This module defines the [Recursion] options. To set it up from [Cli], a [Config] and its //! [Default] value, use the [configure_from](Recursion::configure_from) method. use crate::app::Cli; use crate::config_file::Config; /// The options relating to recursion. #[derive(Clone, Debug, Copy, PartialEq, Eq)] pub struct Recursion { /// Whether the recursion into directories is enabled. pub enabled: bool, /// The depth for how far to recurse into directories. pub depth: usize, } impl Recursion { /// Get the Recursion from either [Cli], a [Config] or the [Default] value. /// /// The "enabled" value is determined by [enabled_from](Recursion::enabled_from) and the depth /// value is determined by [depth_from](Recursion::depth_from). /// /// # Errors /// /// If [depth_from](Recursion::depth_from) returns an [Error], this returns it. pub fn configure_from(cli: &Cli, config: &Config) -> Self { let enabled = Self::enabled_from(cli, config); let depth = Self::depth_from(cli, config); Self { enabled, depth } } /// Get the "enabled" boolean from [Cli], a [Config] or the [Default] value. The first /// value that is not [None] is used. The order of precedence for the value used is: /// - [enabled_from_cli](Recursion::enabled_from_cli) /// - [Config.recursion.enabled] /// - [Default::default] fn enabled_from(cli: &Cli, config: &Config) -> bool { if let Some(value) = Self::enabled_from_cli(cli) { return value; } if let Some(recursion) = &config.recursion { if let Some(enabled) = recursion.enabled { return enabled; } } Default::default() } /// Get a potential "enabled" boolean from [Cli]. /// /// If the "recursive" argument is passed, this returns `true` in a [Some]. Otherwise this /// returns [None]. fn enabled_from_cli(cli: &Cli) -> Option<bool> { if cli.recursive { Some(true) } else { None } } /// Get the "depth" integer from [Cli], a [Config] or the [Default] value. The first /// value that is not [None] is used. The order of precedence for the value used is: /// - Cli::depth /// - [Config.recursion.depth] /// - [Default::default] /// /// # Note /// /// If both configuration file and Args is error, this will return a Max-Uint value. fn depth_from(cli: &Cli, config: &Config) -> usize { if let Some(value) = cli.depth { return value; } use crate::config_file::Recursion; if let Some(Recursion { depth: Some(value), .. }) = &config.recursion { return *value; } usize::MAX } } /// The default values for `Recursion` are the boolean default and [prim@usize::max_value()]. impl Default for Recursion { fn default() -> Self { Self { depth: usize::MAX, enabled: false, } } } #[cfg(test)] mod test { use clap::error::ErrorKind; use clap::Parser; use super::Recursion; use crate::app::Cli; use crate::config_file::{self, Config}; #[test] fn test_enabled_from_cli_empty() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, Recursion::enabled_from_cli(&cli)); } #[test] fn test_enabled_from_cli_true() { let argv = ["lsd", "--recursive"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(true), Recursion::enabled_from_cli(&cli)); } #[test] fn test_enabled_from_empty_matches_and_config() { let argv = ["lsd"]; assert!(!Recursion::enabled_from( &Cli::try_parse_from(argv).unwrap(), &Config::with_none() )); } #[test] fn test_enabled_from_matches_empty_and_config_true() { let argv = ["lsd"]; let mut c = Config::with_none(); c.recursion = Some(config_file::Recursion { enabled: Some(true), depth: None, }); assert!(Recursion::enabled_from( &Cli::try_parse_from(argv).unwrap(), &c )); } #[test] fn test_enabled_from_matches_empty_and_config_false() { let argv = ["lsd"]; let mut c = Config::with_none(); c.recursion = Some(config_file::Recursion { enabled: Some(false), depth: None, }); assert!(!Recursion::enabled_from( &Cli::try_parse_from(argv).unwrap(), &c )); } // The following depth_from_cli tests are implemented using match expressions instead // of the assert_eq macro, because clap::Error does not implement PartialEq. #[test] fn test_depth_from_cli_empty() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert!(cli.depth.is_none()); } #[test] fn test_depth_from_cli_integer() { let argv = ["lsd", "--depth", "42"]; let cli = Cli::try_parse_from(argv).unwrap(); assert!(matches!(cli.depth, Some(42))); } #[test] fn test_depth_from_cli_depth_multi() { let argv = ["lsd", "--depth", "4", "--depth", "2"]; let cli = Cli::try_parse_from(argv).unwrap(); assert!(matches!(cli.depth, Some(2))); } #[test] fn test_depth_from_cli_neg_int() { let argv = ["lsd", "--depth", "\\-42"]; let cli = Cli::try_parse_from(argv); assert!(matches!(cli, Err(e) if e.kind() == ErrorKind::ValueValidation)); } #[test] fn test_depth_from_cli_non_int() { let argv = ["lsd", "--depth", "foo"]; let cli = Cli::try_parse_from(argv); assert!(matches!(cli, Err(e) if e.kind() == ErrorKind::ValueValidation)); } #[test] fn test_depth_from_config_none_max() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!( usize::MAX, Recursion::depth_from(&cli, &Config::with_none()) ); } #[test] fn test_depth_from_config_pos_integer() { let argv = ["lsd"]; let mut c = Config::with_none(); c.recursion = Some(config_file::Recursion { enabled: None, depth: Some(42), }); assert_eq!( 42, Recursion::depth_from(&Cli::try_parse_from(argv).unwrap(), &c) ); } } 0707010000002F000081A400000000000000000000000166C4C37900001276000000000000000000000000000000000000001C00000000lsd-1.1.5/src/flags/size.rs//! This module defines the [SizeFlag]. To set it up from [Cli], a [Config] and its //! [Default] value, use its [configure_from](Configurable::configure_from) method. use super::Configurable; use crate::app::Cli; use crate::config_file::Config; use serde::Deserialize; /// The flag showing which file size units to use. #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] #[serde(rename_all = "kebab-case")] pub enum SizeFlag { /// The variant to show file size with SI unit prefix and a B for bytes. #[default] Default, /// The variant to show file size with only the SI unit prefix. Short, /// The variant to show file size in bytes. Bytes, } impl SizeFlag { fn from_arg_str(value: &str) -> Self { match value { "default" => Self::Default, "short" => Self::Short, "bytes" => Self::Bytes, // Invalid value should be handled by `clap` when building an `Cli` other => unreachable!("Invalid value '{other}' for 'size'"), } } } impl Configurable<Self> for SizeFlag { /// Get a potential `SizeFlag` variant from [Cli]. /// /// If any of the "default", "short" or "bytes" arguments is passed, the corresponding /// `SizeFlag` variant is returned in a [Some]. If neither of them is passed, this returns /// [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.classic { Some(Self::Bytes) } else { cli.size.as_deref().map(Self::from_arg_str) } } /// Get a potential `SizeFlag` variant from a [Config]. /// /// If the `Config::size` has value and is one of "default", "short" or "bytes", /// this returns the corresponding `SizeFlag` variant in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { if config.classic == Some(true) { Some(Self::Bytes) } else { config.size } } } #[cfg(test)] mod test { use clap::Parser; use super::SizeFlag; use crate::app::Cli; use crate::config_file::Config; use crate::flags::Configurable; #[test] fn test_default() { assert_eq!(SizeFlag::Default, SizeFlag::default()); } #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, SizeFlag::from_cli(&cli)); } #[test] fn test_from_cli_default() { let argv = ["lsd", "--size", "default"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SizeFlag::Default), SizeFlag::from_cli(&cli)); } #[test] fn test_from_cli_short() { let argv = ["lsd", "--size", "short"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SizeFlag::Short), SizeFlag::from_cli(&cli)); } #[test] fn test_from_cli_bytes() { let argv = ["lsd", "--size", "bytes"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SizeFlag::Bytes), SizeFlag::from_cli(&cli)); } #[test] #[should_panic] fn test_from_cli_unknown() { let argv = ["lsd", "--size", "unknown"]; let _ = Cli::try_parse_from(argv).unwrap(); } #[test] fn test_from_cli_size_multi() { let argv = ["lsd", "--size", "bytes", "--size", "short"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SizeFlag::Short), SizeFlag::from_cli(&cli)); } #[test] fn test_from_cli_size_classic() { let argv = ["lsd", "--size", "short", "--classic"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SizeFlag::Bytes), SizeFlag::from_cli(&cli)); } #[test] fn test_from_config_none() { assert_eq!(None, SizeFlag::from_config(&Config::with_none())); } #[test] fn test_from_config_default() { let mut c = Config::with_none(); c.size = Some(SizeFlag::Default); assert_eq!(Some(SizeFlag::Default), SizeFlag::from_config(&c)); } #[test] fn test_from_config_short() { let mut c = Config::with_none(); c.size = Some(SizeFlag::Short); assert_eq!(Some(SizeFlag::Short), SizeFlag::from_config(&c)); } #[test] fn test_from_config_bytes() { let mut c = Config::with_none(); c.size = Some(SizeFlag::Bytes); assert_eq!(Some(SizeFlag::Bytes), SizeFlag::from_config(&c)); } #[test] fn test_from_config_classic_mode() { let mut c = Config::with_none(); c.classic = Some(true); assert_eq!(Some(SizeFlag::Bytes), SizeFlag::from_config(&c)); } } 07070100000030000081A400000000000000000000000166C4C3790000414E000000000000000000000000000000000000001F00000000lsd-1.1.5/src/flags/sorting.rs//! This module defines the [Sorting] options. To set it up from [Cli], a [Config] //! and its [Default] value, use the [configure_from](Sorting::configure_from) method. use super::Configurable; use crate::app::Cli; use crate::config_file::Config; use serde::Deserialize; /// A collection of flags on how to sort the output. #[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] pub struct Sorting { pub column: SortColumn, pub order: SortOrder, pub dir_grouping: DirGrouping, } impl Sorting { /// Get a `Sorting` struct from [Cli], a [Config] or the [Default] values. /// /// The [SortColumn], [SortOrder] and [DirGrouping] are configured with their respective /// [Configurable] implementation. pub fn configure_from(cli: &Cli, config: &Config) -> Self { let column = SortColumn::configure_from(cli, config); let order = SortOrder::configure_from(cli, config); let dir_grouping = DirGrouping::configure_from(cli, config); Self { column, order, dir_grouping, } } } /// The flag showing which column to use for sorting. #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] #[serde(rename_all = "kebab-case")] pub enum SortColumn { None, Extension, #[default] Name, Time, Size, Version, GitStatus, } impl Configurable<Self> for SortColumn { /// Get a potential `SortColumn` variant from [Cli]. /// /// If either the "timesort" or "sizesort" arguments are passed, this returns the corresponding /// `SortColumn` variant in a [Some]. Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { let sort = cli.sort.as_deref(); if cli.timesort || sort == Some("time") { Some(Self::Time) } else if cli.sizesort || sort == Some("size") { Some(Self::Size) } else if cli.extensionsort || sort == Some("extension") { Some(Self::Extension) } else if cli.versionsort || sort == Some("version") { Some(Self::Version) } else if cli.gitsort || sort == Some("git") { Some(Self::GitStatus) } else if cli.no_sort || sort == Some("none") { Some(Self::None) } else { None } } /// Get a potential `SortColumn` variant from a [Config]. /// /// If the `Config::sorting::column` has value and is one of "time", "size" or "name", /// this returns the corresponding variant in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { config.sorting.as_ref().and_then(|s| s.column) } } /// The flag showing which sort order to use. #[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] pub enum SortOrder { #[default] Default, Reverse, } impl Configurable<Self> for SortOrder { /// Get a potential `SortOrder` variant from [Cli]. /// /// If the "reverse" argument is passed, this returns [SortOrder::Reverse] in a [Some]. /// Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.reverse { Some(Self::Reverse) } else { None } } /// Get a potential `SortOrder` variant from a [Config]. /// /// If the `Config::sorting::reverse` has value, /// this returns a mapped variant in a [Some]. /// Otherwise [None] is returned. /// A `true` maps to [SortOrder::Reverse] while `false` maps to [SortOrder::Default]. fn from_config(config: &Config) -> Option<Self> { config.sorting.as_ref().and_then(|s| match s.reverse { Some(true) => Some(Self::Reverse), Some(false) => Some(Self::Default), None => None, }) } } /// The flag showing where to place directories. #[derive(Clone, Debug, Copy, PartialEq, Eq, Deserialize, Default)] #[serde(rename_all = "kebab-case")] pub enum DirGrouping { #[default] None, First, Last, } impl DirGrouping { fn from_arg_str(value: &str) -> Self { match value { "first" => Self::First, "last" => Self::Last, "none" => Self::None, // Invalid value should be handled by `clap` when building an `Cli` other => unreachable!("Invalid value '{other}' for 'group-dirs'"), } } } impl Configurable<Self> for DirGrouping { /// Get a potential `DirGrouping` variant from [Cli]. /// /// If the "classic" argument is passed, then this returns the [DirGrouping::None] variant in a /// [Some]. Otherwise if the argument is passed, this returns the variant corresponding to its /// parameter in a [Some]. Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.classic { return Some(Self::None); } if cli.group_directories_first { return Some(Self::First); } if let Some(mode) = &cli.group_dirs { return Some(Self::from_arg_str(mode)); } None } /// Get a potential `DirGrouping` variant from a [Config]. /// /// If the `Config::classic` has value and is `true`, /// then this returns the the [DirGrouping::None] variant in a [Some]. /// Otherwise if `Config::sorting::dir-grouping` has value and /// is one of "first", "last" or "none", this returns its corresponding variant in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { if config.classic == Some(true) { Some(Self::None) } else { config.sorting.as_ref().and_then(|s| s.dir_grouping) } } } #[cfg(test)] mod test_sort_column { use clap::Parser; use super::SortColumn; use crate::app::Cli; use crate::config_file::{Config, Sorting}; use crate::flags::Configurable; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, SortColumn::from_cli(&cli)); } #[test] fn test_from_cli_extension() { let argv = ["lsd", "--extensionsort"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SortColumn::Extension), SortColumn::from_cli(&cli)); } #[test] fn test_from_cli_time() { let argv = ["lsd", "--timesort"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SortColumn::Time), SortColumn::from_cli(&cli)); } #[test] fn test_from_cli_size() { let argv = ["lsd", "--sizesort"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SortColumn::Size), SortColumn::from_cli(&cli)); } #[test] fn test_from_cli_git() { let argv = ["lsd", "--gitsort"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SortColumn::GitStatus), SortColumn::from_cli(&cli)); } #[test] fn test_from_cli_version() { let argv = ["lsd", "--versionsort"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SortColumn::Version), SortColumn::from_cli(&cli)); } #[test] fn test_from_cli_no_sort() { let argv = ["lsd", "--no-sort"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SortColumn::None), SortColumn::from_cli(&cli)); } #[test] fn test_from_cli_sort() { let argv = ["lsd", "--sort", "time"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SortColumn::Time), SortColumn::from_cli(&cli)); let argv = ["lsd", "--sort", "size"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SortColumn::Size), SortColumn::from_cli(&cli)); let argv = ["lsd", "--sort", "extension"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SortColumn::Extension), SortColumn::from_cli(&cli)); let argv = ["lsd", "--sort", "version"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SortColumn::Version), SortColumn::from_cli(&cli)); let argv = ["lsd", "--sort", "none"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SortColumn::None), SortColumn::from_cli(&cli)); } #[cfg(not(feature = "no-git"))] #[test] fn test_from_arg_cli_sort_git() { let argv = ["lsd", "--sort", "git"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SortColumn::GitStatus), SortColumn::from_cli(&cli)); } #[test] fn test_multi_sort() { let argv = ["lsd", "--sort", "size", "--sort", "time"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SortColumn::Time), SortColumn::from_cli(&cli)); } #[test] fn test_multi_sort_use_last() { let argv = ["lsd", "--sort", "size", "-t", "-S", "-X", "--sort", "time"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SortColumn::Time), SortColumn::from_cli(&cli)); } #[test] fn test_from_config_empty() { assert_eq!(None, SortColumn::from_config(&Config::with_none())); } #[test] fn test_from_config_empty_column() { let mut c = Config::with_none(); c.sorting = Some(Sorting { column: None, reverse: None, dir_grouping: None, }); assert_eq!(None, SortColumn::from_config(&c)); } #[test] fn test_from_config_extension() { let mut c = Config::with_none(); c.sorting = Some(Sorting { column: Some(SortColumn::Extension), reverse: None, dir_grouping: None, }); assert_eq!(Some(SortColumn::Extension), SortColumn::from_config(&c)); } #[test] fn test_from_config_name() { let mut c = Config::with_none(); c.sorting = Some(Sorting { column: Some(SortColumn::Name), reverse: None, dir_grouping: None, }); assert_eq!(Some(SortColumn::Name), SortColumn::from_config(&c)); } #[test] fn test_from_config_time() { let mut c = Config::with_none(); c.sorting = Some(Sorting { column: Some(SortColumn::Time), reverse: None, dir_grouping: None, }); assert_eq!(Some(SortColumn::Time), SortColumn::from_config(&c)); } #[test] fn test_from_config_size() { let mut c = Config::with_none(); c.sorting = Some(Sorting { column: Some(SortColumn::Size), reverse: None, dir_grouping: None, }); assert_eq!(Some(SortColumn::Size), SortColumn::from_config(&c)); } #[test] fn test_from_config_version() { let mut c = Config::with_none(); c.sorting = Some(Sorting { column: Some(SortColumn::Version), reverse: None, dir_grouping: None, }); assert_eq!(Some(SortColumn::Version), SortColumn::from_config(&c)); } #[test] fn test_from_config_git_status() { let mut c = Config::with_none(); c.sorting = Some(Sorting { column: Some(SortColumn::GitStatus), reverse: None, dir_grouping: None, }); assert_eq!(Some(SortColumn::GitStatus), SortColumn::from_config(&c)); } } #[cfg(test)] mod test_sort_order { use clap::Parser; use super::SortOrder; use crate::app::Cli; use crate::config_file::{Config, Sorting}; use crate::flags::Configurable; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, SortOrder::from_cli(&cli)); } #[test] fn test_from_cli_reverse() { let argv = ["lsd", "--reverse"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(SortOrder::Reverse), SortOrder::from_cli(&cli)); } #[test] fn test_from_config_empty() { assert_eq!(None, SortOrder::from_config(&Config::with_none())); } #[test] fn test_from_config_default_config() { assert_eq!( Some(SortOrder::default()), SortOrder::from_config(&Config::builtin()) ); } #[test] fn test_from_config_empty_reverse() { let mut c = Config::with_none(); c.sorting = Some(Sorting { column: None, reverse: None, dir_grouping: None, }); assert_eq!(None, SortOrder::from_config(&c)); } #[test] fn test_from_config_reverse_true() { let mut c = Config::with_none(); c.sorting = Some(Sorting { column: None, reverse: Some(true), dir_grouping: None, }); assert_eq!(Some(SortOrder::Reverse), SortOrder::from_config(&c)); } #[test] fn test_from_config_reverse_false() { let mut c = Config::with_none(); c.sorting = Some(Sorting { column: None, reverse: Some(false), dir_grouping: None, }); assert_eq!(Some(SortOrder::Default), SortOrder::from_config(&c)); } } #[cfg(test)] mod test_dir_grouping { use clap::Parser; use super::DirGrouping; use crate::app::Cli; use crate::config_file::{Config, Sorting}; use crate::flags::Configurable; #[test] #[should_panic] fn test_from_str_bad_value() { DirGrouping::from_arg_str("bad value"); } #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, DirGrouping::from_cli(&cli)); } #[test] fn test_from_cli_first() { let argv = ["lsd", "--group-dirs", "first"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(DirGrouping::First), DirGrouping::from_cli(&cli)); } #[test] fn test_from_cli_last() { let argv = ["lsd", "--group-dirs", "last"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(DirGrouping::Last), DirGrouping::from_cli(&cli)); } #[test] fn test_from_cli_explicit_none() { let argv = ["lsd", "--group-dirs", "none"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(DirGrouping::None), DirGrouping::from_cli(&cli)); } #[test] fn test_from_cli_classic_mode() { let argv = ["lsd", "--group-dirs", "first", "--classic"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(DirGrouping::None), DirGrouping::from_cli(&cli)); } #[test] fn test_from_cli_group_dirs_multi() { let argv = ["lsd", "--group-dirs", "first", "--group-dirs", "last"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(DirGrouping::Last), DirGrouping::from_cli(&cli)); } #[test] fn test_from_cli_group_directories_first() { let argv = ["lsd", "--group-directories-first"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(DirGrouping::First), DirGrouping::from_cli(&cli)); } #[test] fn test_from_config_empty() { assert_eq!(None, DirGrouping::from_config(&Config::with_none())); } #[test] fn test_from_config_first() { let mut c = Config::with_none(); c.sorting = Some(Sorting { column: None, reverse: None, dir_grouping: Some(DirGrouping::First), }); assert_eq!(Some(DirGrouping::First), DirGrouping::from_config(&c)); } #[test] fn test_from_config_last() { let mut c = Config::with_none(); c.sorting = Some(Sorting { column: None, reverse: None, dir_grouping: Some(DirGrouping::Last), }); assert_eq!(Some(DirGrouping::Last), DirGrouping::from_config(&c)); } #[test] fn test_from_config_explicit_empty() { let mut c = Config::with_none(); c.sorting = Some(Sorting { column: None, reverse: None, dir_grouping: None, }); assert_eq!(None, DirGrouping::from_config(&c)); } #[test] fn test_from_config_classic_mode() { let mut c = Config::with_none(); c.sorting = Some(Sorting { column: None, reverse: None, dir_grouping: Some(DirGrouping::Last), }); c.classic = Some(true); assert_eq!(Some(DirGrouping::None), DirGrouping::from_config(&c)); } } 07070100000031000081A400000000000000000000000166C4C3790000086D000000000000000000000000000000000000002500000000lsd-1.1.5/src/flags/symlink_arrow.rsuse super::Configurable; use crate::app::Cli; use crate::config_file::Config; /// The flag showing how to display symbolic arrow. #[derive(Clone, Debug, Eq, PartialEq)] pub struct SymlinkArrow(String); impl Configurable<Self> for SymlinkArrow { /// `SymlinkArrow` can not be configured by [Cli] /// /// Return `None` fn from_cli(_: &Cli) -> Option<Self> { None } /// Get a potential `SymlinkArrow` value from a [Config]. /// /// If the `Config::symlink-arrow` has value, /// returns its value as the value of the `SymlinkArrow`, in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { config .symlink_arrow .as_ref() .map(|arrow| SymlinkArrow(arrow.to_string())) } } /// The default value for the `SymlinkArrow` is `\u{21d2}(⇒)` impl Default for SymlinkArrow { fn default() -> Self { Self(String::from("\u{21d2}")) // ⇒ } } use std::fmt; impl fmt::Display for SymlinkArrow { // This trait requires `fmt` with this exact signature. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) } } #[cfg(test)] mod test { use clap::Parser; use crate::app::Cli; use crate::config_file::Config; use crate::flags::Configurable; use super::SymlinkArrow; #[test] fn test_symlink_arrow_from_config_utf8() { let mut c = Config::with_none(); c.symlink_arrow = Some("↹".into()); assert_eq!( Some(SymlinkArrow(String::from("\u{21B9}"))), SymlinkArrow::from_config(&c) ); } #[test] fn test_symlink_arrow_from_args_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, SymlinkArrow::from_cli(&cli)); } #[test] fn test_symlink_arrow_default() { assert_eq!( SymlinkArrow(String::from("\u{21d2}")), SymlinkArrow::default() ); } #[test] fn test_symlink_display() { assert_eq!("⇒", format!("{}", SymlinkArrow::default())); } } 07070100000032000081A400000000000000000000000166C4C379000008AA000000000000000000000000000000000000002000000000lsd-1.1.5/src/flags/symlinks.rs//! This module defines the [NoSymlink] flag. To set it up from [Cli], a [Config] and its //! [Default] value, use the [configure_from](Configurable::configure_from) method. use super::Configurable; use crate::app::Cli; use crate::config_file::Config; /// The flag showing whether to follow symbolic links. #[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] pub struct NoSymlink(pub bool); impl Configurable<Self> for NoSymlink { /// Get a potential `NoSymlink` value from [Cli]. /// /// If the "no-symlink" argument is passed, this returns a `NoSymlink` with value `true` in a /// [Some]. Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.no_symlink { Some(Self(true)) } else { None } } /// Get a potential `NoSymlink` value from a [Config]. /// /// If the `Config::no-symlink` has value, /// this returns it as the value of the `NoSymlink`, in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { config.no_symlink.map(Self) } } #[cfg(test)] mod test { use clap::Parser; use super::NoSymlink; use crate::app::Cli; use crate::config_file::Config; use crate::flags::Configurable; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, NoSymlink::from_cli(&cli)); } #[test] fn test_from_cli_true() { let argv = ["lsd", "--no-symlink"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(NoSymlink(true)), NoSymlink::from_cli(&cli)); } #[test] fn test_from_config_none() { assert_eq!(None, NoSymlink::from_config(&Config::with_none())); } #[test] fn test_from_config_true() { let mut c = Config::with_none(); c.no_symlink = Some(true); assert_eq!(Some(NoSymlink(true)), NoSymlink::from_config(&c)); } #[test] fn test_from_config_false() { let mut c = Config::with_none(); c.no_symlink = Some(false); assert_eq!(Some(NoSymlink(false)), NoSymlink::from_config(&c)); } } 07070100000033000081A400000000000000000000000166C4C379000008B8000000000000000000000000000000000000002200000000lsd-1.1.5/src/flags/total_size.rs//! This module defines the [TotalSize] flag. To set it up from [Cli], a [Config] and its //! [Default] value, use the [configure_from](Configurable::configure_from) method. use super::Configurable; use crate::app::Cli; use crate::config_file::Config; /// The flag showing whether to show the total size for directories. #[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] pub struct TotalSize(pub bool); impl Configurable<Self> for TotalSize { /// Get a potential `TotalSize` value from [Cli]. /// /// If the "total-size" argument is passed, this returns a `TotalSize` with value `true` in a /// [Some]. Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { if cli.total_size { Some(Self(true)) } else { None } } /// Get a potential `TotalSize` value from a [Config]. /// /// If the `Config::total-size` has value, /// this returns it as the value of the `TotalSize`, in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { config.total_size.map(Self) } } #[cfg(test)] mod test { use clap::Parser; use super::TotalSize; use crate::app::Cli; use crate::config_file::Config; use crate::flags::Configurable; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, TotalSize::from_cli(&cli)); } #[test] fn test_from_cli_true() { let argv = ["lsd", "--total-size"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(Some(TotalSize(true)), TotalSize::from_cli(&cli)); } #[test] fn test_from_config_none() { assert_eq!(None, TotalSize::from_config(&Config::with_none())); } #[test] fn test_from_config_true() { let mut c = Config::with_none(); c.total_size = Some(true); assert_eq!(Some(TotalSize(true)), TotalSize::from_config(&c)); } #[test] fn test_from_config_false() { let mut c = Config::with_none(); c.total_size = Some(false); assert_eq!(Some(TotalSize(false)), TotalSize::from_config(&c)); } } 07070100000034000081A400000000000000000000000166C4C37900000D86000000000000000000000000000000000000002600000000lsd-1.1.5/src/flags/truncate_owner.rs//! This module defines the [TruncateOwner] flag. To set it up from [Cli], a [Config] and its //! [Default] value, use the [configure_from](Configurable::configure_from) method. use super::Configurable; use crate::app::Cli; use crate::config_file::Config; /// The flag showing how to truncate user and group names. #[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct TruncateOwner { pub after: Option<usize>, pub marker: Option<String>, } impl Configurable<Self> for TruncateOwner { /// Get a potential `TruncateOwner` value from [Cli]. /// /// If the "header" argument is passed, this returns a `TruncateOwner` with value `true` in a /// [Some]. Otherwise this returns [None]. fn from_cli(cli: &Cli) -> Option<Self> { match (cli.truncate_owner_after, cli.truncate_owner_marker.clone()) { (None, None) => None, (after, marker) => Some(Self { after, marker }), } } /// Get a potential `TruncateOwner` value from a [Config]. /// /// If the `Config::truncate_owner` has value, /// this returns it as the value of the `TruncateOwner`, in a [Some]. /// Otherwise this returns [None]. fn from_config(config: &Config) -> Option<Self> { config.truncate_owner.as_ref().map(|c| Self { after: c.after, marker: c.marker.clone(), }) } } #[cfg(test)] mod test { use clap::Parser; use super::TruncateOwner; use crate::app::Cli; use crate::config_file::{self, Config}; use crate::flags::Configurable; #[test] fn test_from_cli_none() { let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!(None, TruncateOwner::from_cli(&cli)); } #[test] fn test_from_cli_after_some() { let argv = ["lsd", "--truncate-owner-after", "1"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!( Some(TruncateOwner { after: Some(1), marker: None, }), TruncateOwner::from_cli(&cli) ); } #[test] fn test_from_cli_marker_some() { let argv = ["lsd", "--truncate-owner-marker", "…"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!( Some(TruncateOwner { after: None, marker: Some("…".to_string()), }), TruncateOwner::from_cli(&cli) ); } #[test] fn test_from_config_none() { assert_eq!(None, TruncateOwner::from_config(&Config::with_none())); } #[test] fn test_from_config_all_fields_none() { let mut c = Config::with_none(); c.truncate_owner = Some(config_file::TruncateOwner { after: None, marker: None, }); assert_eq!( Some(TruncateOwner { after: None, marker: None, }), TruncateOwner::from_config(&c) ); } #[test] fn test_from_config_all_fields_some() { let mut c = Config::with_none(); c.truncate_owner = Some(config_file::TruncateOwner { after: Some(1), marker: Some(">".to_string()), }); assert_eq!( Some(TruncateOwner { after: Some(1), marker: Some(">".to_string()), }), TruncateOwner::from_config(&c) ); } } 07070100000035000081A400000000000000000000000166C4C37900003C42000000000000000000000000000000000000001500000000lsd-1.1.5/src/git.rsuse crate::meta::git_file_status::GitFileStatus; use std::path::{Path, PathBuf}; #[allow(dead_code)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] pub enum GitStatus { /// No status info #[default] Default, /// No changes (got from git status) Unmodified, /// Entry is ignored item in workdir Ignored, /// Entry does not exist in old version (now in stage) NewInIndex, /// Entry does not exist in old version (not in stage) NewInWorkdir, /// Type of entry changed between old and new Typechange, /// Entry does not exist in new version Deleted, /// Entry was renamed between old and new Renamed, /// Entry content changed between old and new Modified, /// Entry in the index is conflicted Conflicted, } pub struct GitCache { #[cfg(not(feature = "no-git"))] statuses: Vec<(PathBuf, git2::Status)>, } #[cfg(feature = "no-git")] impl GitCache { pub fn new(_: &Path) -> Self { Self {} } pub fn get(&self, _filepath: &PathBuf, _is_directory: bool) -> Option<GitFileStatus> { None } } #[cfg(not(feature = "no-git"))] impl GitCache { pub fn new(path: &Path) -> GitCache { let repo = match git2::Repository::discover(path) { Ok(r) => r, Err(_e) => { // Unable to retrieve Git info; it doesn't seem to be a git directory return Self::empty(); } }; if let Some(workdir) = repo.workdir().and_then(|x| std::fs::canonicalize(x).ok()) { let mut statuses = Vec::new(); // Retrieving Git statuses for workdir match repo.statuses(None) { Ok(status_list) => { for status_entry in status_list.iter() { // git2-rs provides / separated path even on Windows. We have to rebuild it let str_path = status_entry.path().unwrap(); let path: PathBuf = str_path.split('/').collect::<Vec<_>>().iter().collect(); let path = workdir.join(path); let elem = (path, status_entry.status()); statuses.push(elem); } } Err(err) => { crate::print_error!( "Cannot retrieve Git statuses for directory {:?}: {}", workdir, err ); } } GitCache { statuses } } else { // No workdir Self::empty() } } pub fn empty() -> Self { GitCache { statuses: Vec::new(), } } pub fn get(&self, filepath: &PathBuf, is_directory: bool) -> Option<GitFileStatus> { match std::fs::canonicalize(filepath) { Ok(filename) => Some(self.inner_get(&filename, is_directory)), Err(err) => { if err.kind() != std::io::ErrorKind::NotFound { crate::print_error!("Cannot get git status for {:?}: {}", filepath, err); } None } } } fn inner_get(&self, filepath: &PathBuf, is_directory: bool) -> GitFileStatus { if is_directory { self.statuses .iter() .filter(|&x| x.0.starts_with(filepath)) .map(|x| GitFileStatus::new(x.1)) .fold(GitFileStatus::default(), |acc, x| GitFileStatus { index: std::cmp::max(acc.index, x.index), workdir: std::cmp::max(acc.workdir, x.workdir), }) } else { self.statuses .iter() .find(|&x| filepath == &x.0) .map(|e| GitFileStatus::new(e.1)) .unwrap_or_default() } } } #[cfg(not(feature = "no-git"))] #[cfg(test)] mod tests { use super::*; use assert_fs::prelude::*; use assert_fs::TempDir; use git2::build::CheckoutBuilder; use git2::{CherrypickOptions, Index, Oid, Repository, RepositoryInitOptions}; use std::collections::HashMap; use std::fs::remove_file; #[allow(unused)] use std::process::Command; #[test] fn compare_git_status() { assert!(GitStatus::Unmodified < GitStatus::Conflicted); } macro_rules! t { ($e:expr) => { match $e { Ok(e) => e, Err(e) => panic!("{} failed with {}", stringify!($e), e), } }; } fn repo_init() -> (TempDir, Repository) { let td = t!(TempDir::new()); let mut opts = RepositoryInitOptions::new(); opts.initial_head("master"); let repo = Repository::init_opts(td.path(), &opts).unwrap(); { let mut config = t!(repo.config()); t!(config.set_str("user.name", "name")); t!(config.set_str("user.email", "email")); let mut index = t!(repo.index()); let id = t!(index.write_tree()); let tree = t!(repo.find_tree(id)); let sig = t!(repo.signature()); t!(repo.commit(Some("HEAD"), &sig, &sig, "initial commit", &tree, &[])); } (td, repo) } fn commit(repo: &Repository, index: &mut Index, msg: &str) -> (Oid, Oid) { let tree_id = t!(index.write_tree()); let tree = t!(repo.find_tree(tree_id)); let sig = t!(repo.signature()); let head_id = t!(repo.refname_to_id("HEAD")); let parent = t!(repo.find_commit(head_id)); let commit = t!(repo.commit(Some("HEAD"), &sig, &sig, msg, &tree, &[&parent])); (commit, tree_id) } fn check_cache(root: &Path, statuses: &HashMap<&PathBuf, GitFileStatus>, msg: &str) { let cache = GitCache::new(root); for (&path, status) in statuses.iter() { if let Ok(filename) = std::fs::canonicalize(&root.join(path)) { let is_directory = filename.is_dir(); assert_eq!( &cache.inner_get(&filename, is_directory), status, "Invalid status for file {} at stage {}", filename.to_string_lossy(), msg ); } } } #[test] fn test_git_workflow() { // rename as test_git_workflow let (root, repo) = repo_init(); let mut index = repo.index().unwrap(); let mut expected_statuses = HashMap::new(); // Check now check_cache(root.path(), &expected_statuses, "initialization"); let f0 = PathBuf::from(".gitignore"); root.child(&f0).write_str("*.bak").unwrap(); expected_statuses.insert( &f0, GitFileStatus { index: GitStatus::Unmodified, workdir: GitStatus::NewInWorkdir, }, ); let _success = Command::new("git") .current_dir(root.path()) .arg("status") .status() .expect("Git status failed") .success(); // Check now check_cache(root.path(), &expected_statuses, "new .gitignore"); index.add_path(f0.as_path()).unwrap(); // Check now check_cache(root.path(), &expected_statuses, "unstaged .gitignore"); index.write().unwrap(); *expected_statuses.get_mut(&f0).unwrap() = GitFileStatus { index: GitStatus::NewInIndex, workdir: GitStatus::Unmodified, }; // Check now check_cache(root.path(), &expected_statuses, "staged .gitignore"); commit(&repo, &mut index, "Add gitignore"); *expected_statuses.get_mut(&f0).unwrap() = GitFileStatus { index: GitStatus::Default, workdir: GitStatus::Default, }; // Check now check_cache(root.path(), &expected_statuses, "Committed .gitignore"); let d1 = PathBuf::from("d1"); let f1 = d1.join("f1"); root.child(&f1).touch().unwrap(); let f2 = d1.join("f2.bak"); root.child(&f2).touch().unwrap(); expected_statuses.insert( &d1, GitFileStatus { index: GitStatus::Unmodified, workdir: GitStatus::NewInWorkdir, }, ); expected_statuses.insert( &f1, GitFileStatus { index: GitStatus::Unmodified, workdir: GitStatus::NewInWorkdir, }, ); expected_statuses.insert( &f2, GitFileStatus { index: GitStatus::Unmodified, workdir: GitStatus::Ignored, }, ); // Check now check_cache(root.path(), &expected_statuses, "New files"); index.add_path(f1.as_path()).unwrap(); index.write().unwrap(); *expected_statuses.get_mut(&d1).unwrap() = GitFileStatus { index: GitStatus::NewInIndex, workdir: GitStatus::Ignored, }; *expected_statuses.get_mut(&f1).unwrap() = GitFileStatus { index: GitStatus::NewInIndex, workdir: GitStatus::Unmodified, }; // Check now check_cache(root.path(), &expected_statuses, "Unstaged new files"); index.add_path(f2.as_path()).unwrap(); index.write().unwrap(); *expected_statuses.get_mut(&d1).unwrap() = GitFileStatus { index: GitStatus::NewInIndex, workdir: GitStatus::Unmodified, }; *expected_statuses.get_mut(&f2).unwrap() = GitFileStatus { index: GitStatus::NewInIndex, workdir: GitStatus::Unmodified, }; // Check now check_cache(root.path(), &expected_statuses, "Staged new files"); let (commit1_oid, _) = commit(&repo, &mut index, "Add new files"); *expected_statuses.get_mut(&d1).unwrap() = GitFileStatus { index: GitStatus::Default, workdir: GitStatus::Default, }; *expected_statuses.get_mut(&f1).unwrap() = GitFileStatus { index: GitStatus::Default, workdir: GitStatus::Default, }; *expected_statuses.get_mut(&f2).unwrap() = GitFileStatus { index: GitStatus::Default, workdir: GitStatus::Default, }; // Check now check_cache(root.path(), &expected_statuses, "Committed new files"); remove_file(root.child(&f2).path()).unwrap(); *expected_statuses.get_mut(&d1).unwrap() = GitFileStatus { index: GitStatus::Unmodified, workdir: GitStatus::Deleted, }; *expected_statuses.get_mut(&f2).unwrap() = GitFileStatus { index: GitStatus::Unmodified, workdir: GitStatus::Deleted, }; // Check now check_cache(root.path(), &expected_statuses, "Remove file"); root.child(&f1).write_str("New content").unwrap(); *expected_statuses.get_mut(&d1).unwrap() = GitFileStatus { index: GitStatus::Unmodified, workdir: GitStatus::Modified, }; // more important to see modified vs deleted ? *expected_statuses.get_mut(&f1).unwrap() = GitFileStatus { index: GitStatus::Unmodified, workdir: GitStatus::Modified, }; // Check now check_cache(root.path(), &expected_statuses, "Change file"); index.remove_path(&f2).unwrap(); index.write().unwrap(); *expected_statuses.get_mut(&d1).unwrap() = GitFileStatus { index: GitStatus::Deleted, workdir: GitStatus::Modified, }; *expected_statuses.get_mut(&f2).unwrap() = GitFileStatus { index: GitStatus::Deleted, workdir: GitStatus::Unmodified, }; // Check now check_cache(root.path(), &expected_statuses, "Staged changes"); commit(&repo, &mut index, "Remove backup file"); *expected_statuses.get_mut(&d1).unwrap() = GitFileStatus { index: GitStatus::Unmodified, workdir: GitStatus::Modified, }; *expected_statuses.get_mut(&f2).unwrap() = GitFileStatus { index: GitStatus::Default, workdir: GitStatus::Default, }; // Check now check_cache( root.path(), &expected_statuses, "Committed changes (first part)", ); index.add_path(&f1).unwrap(); index.write().unwrap(); commit(&repo, &mut index, "Save modified file"); *expected_statuses.get_mut(&d1).unwrap() = GitFileStatus { index: GitStatus::Default, workdir: GitStatus::Default, }; *expected_statuses.get_mut(&f1).unwrap() = GitFileStatus { index: GitStatus::Default, workdir: GitStatus::Default, }; // Check now check_cache( root.path(), &expected_statuses, "Committed changes (second part)", ); let branch_commit = repo.find_commit(commit1_oid).unwrap(); let branch = repo .branch("conflict-branch", &branch_commit, true) .unwrap(); repo.set_head(format!("refs/heads/{}", branch.name().unwrap().unwrap()).as_str()) .unwrap(); let mut checkout_opts = CheckoutBuilder::new(); checkout_opts.force(); repo.checkout_head(Some(&mut checkout_opts)).unwrap(); root.child(&f1) .write_str("New conflicting content") .unwrap(); root.child(&f2) .write_str("New conflicting content") .unwrap(); index.add_path(&f1).unwrap(); index.add_path(&f2).unwrap(); index.write().unwrap(); let (commit2_oid, _) = commit(&repo, &mut index, "Save conflicting changes"); // Check now check_cache( root.path(), &expected_statuses, "Committed changes in branch", ); repo.set_head("refs/heads/master").unwrap(); repo.checkout_head(Some(&mut checkout_opts)).unwrap(); let mut cherrypick_opts = CherrypickOptions::new(); let branch_commit = repo.find_commit(commit2_oid).unwrap(); repo.cherrypick(&branch_commit, Some(&mut cherrypick_opts)) .unwrap(); *expected_statuses.get_mut(&d1).unwrap() = GitFileStatus { index: GitStatus::Unmodified, workdir: GitStatus::Conflicted, }; *expected_statuses.get_mut(&f1).unwrap() = GitFileStatus { index: GitStatus::Unmodified, workdir: GitStatus::Conflicted, }; *expected_statuses.get_mut(&f2).unwrap() = GitFileStatus { index: GitStatus::Unmodified, workdir: GitStatus::Conflicted, }; // let _success = Command::new("git") // .current_dir(root.path()) // .arg("status") // .status() // .expect("Git status failed") // .success(); // Check now check_cache( root.path(), &expected_statuses, "Conflict between master and branch", ); } } 07070100000036000081A400000000000000000000000166C4C37900000413000000000000000000000000000000000000001B00000000lsd-1.1.5/src/git_theme.rsuse crate::git::GitStatus; use crate::theme::git::GitThemeSymbols; pub struct GitTheme { symbols: GitThemeSymbols, } impl GitTheme { pub fn new() -> GitTheme { let git_symbols = GitThemeSymbols::default(); Self { symbols: git_symbols, } } pub fn get_symbol(&self, status: &GitStatus) -> String { let symbol = match status { GitStatus::Default => &self.symbols.default, GitStatus::Unmodified => &self.symbols.unmodified, GitStatus::Ignored => &self.symbols.ignored, GitStatus::NewInIndex => &self.symbols.new_in_index, GitStatus::NewInWorkdir => &self.symbols.new_in_workdir, GitStatus::Typechange => &self.symbols.typechange, GitStatus::Deleted => &self.symbols.deleted, GitStatus::Renamed => &self.symbols.renamed, GitStatus::Modified => &self.symbols.modified, GitStatus::Conflicted => &self.symbols.conflicted, }; symbol.to_string() } } 07070100000037000081A400000000000000000000000166C4C3790000239C000000000000000000000000000000000000001600000000lsd-1.1.5/src/icon.rsuse crate::flags::{IconOption, IconTheme as FlagTheme}; use crate::meta::{FileType, Name}; use crate::theme::{icon::IconTheme, Theme}; pub struct Icons { icon_separator: String, theme: Option<IconTheme>, } // In order to add a new icon, write the unicode value like "\ue5fb" then // run the command below in vim: // // s#\\u[0-9a-f]*#\=eval('"'.submatch(0).'"')# impl Icons { pub fn new(tty: bool, when: IconOption, theme: FlagTheme, icon_separator: String) -> Self { let icon_theme = match (tty, when, theme) { (_, IconOption::Never, _) | (false, IconOption::Auto, _) => None, (_, _, FlagTheme::Fancy) => { if let Ok(t) = Theme::from_path::<IconTheme>("icons") { Some(t) } else { Some(IconTheme::default()) } } (_, _, FlagTheme::Unicode) => Some(IconTheme::unicode()), }; Self { icon_separator, theme: icon_theme, } } pub fn get(&self, name: &Name) -> String { match &self.theme { None => String::new(), Some(t) => { // Check file types let file_type: FileType = name.file_type(); let icon = match file_type { FileType::SymLink { is_dir: true } => &t.filetype.symlink_dir, FileType::SymLink { is_dir: false } => &t.filetype.symlink_file, FileType::Socket => &t.filetype.socket, FileType::Pipe => &t.filetype.pipe, FileType::CharDevice => &t.filetype.device_char, FileType::BlockDevice => &t.filetype.device_block, FileType::Special => &t.filetype.special, _ => { if let Some(icon) = t.name.get(name.file_name().to_lowercase().as_str()) { icon } else if let Some(icon) = name .extension() .and_then(|ext| t.extension.get(ext.to_lowercase().as_str())) { icon } else { match file_type { FileType::Directory { .. } => &t.filetype.dir, // If a file has no extension and is executable, show an icon. // Except for Windows, it marks everything as an executable. #[cfg(not(windows))] FileType::File { exec: true, .. } => &t.filetype.executable, _ => &t.filetype.file, } } } }; format!("{}{}", icon, self.icon_separator) } } } } #[cfg(test)] mod test { use super::{IconTheme, Icons}; use crate::flags::{IconOption, IconTheme as FlagTheme, PermissionFlag}; use crate::meta::Meta; use std::fs::File; use tempfile::tempdir; #[test] fn get_no_icon_never_tty() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); let icons = Icons::new(true, IconOption::Never, FlagTheme::Fancy, " ".to_string()); let icon = icons.get(&meta.name); assert_eq!(icon, ""); } #[test] fn get_no_icon_never_not_tty() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); let icons = Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()); let icon = icons.get(&meta.name); assert_eq!(icon, ""); } #[test] fn get_no_icon_auto() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); let icons = Icons::new(false, IconOption::Auto, FlagTheme::Fancy, " ".to_string()); let icon = icons.get(&meta.name); assert_eq!(icon, ""); } #[test] fn get_icon_auto_tty() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); let icons = Icons::new(true, IconOption::Auto, FlagTheme::Fancy, " ".to_string()); let icon = icons.get(&meta.name); assert_eq!(icon, "\u{f15c} "); } #[test] fn get_icon_always_tty_default_file() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path().join("file"); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); let icon = Icons::new(true, IconOption::Always, FlagTheme::Fancy, " ".to_string()); let icon_str = icon.get(&meta.name); assert_eq!(icon_str, "\u{f016} "); // } #[test] fn get_icon_always_not_tty_default_file() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path().join("file"); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); let icon = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); let icon_str = icon.get(&meta.name); assert_eq!(icon_str, "\u{f016} "); // } #[test] fn get_icon_default_file_icon_unicode() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path().join("file"); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); let icon = Icons::new( false, IconOption::Always, FlagTheme::Unicode, " ".to_string(), ); let icon_str = icon.get(&meta.name); assert_eq!(icon_str, format!("{}{}", "\u{1f4c4}", icon.icon_separator)); } #[test] fn get_icon_default_directory() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path(); let meta = Meta::from_path(file_path, false, PermissionFlag::Rwx).unwrap(); let icon = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); let icon_str = icon.get(&meta.name); assert_eq!(icon_str, "\u{f115} "); // } #[test] fn get_icon_default_directory_unicode() { let tmp_dir = tempdir().expect("failed to create temp dir"); let file_path = tmp_dir.path(); let meta = Meta::from_path(file_path, false, PermissionFlag::Rwx).unwrap(); let icon = Icons::new( false, IconOption::Always, FlagTheme::Unicode, " ".to_string(), ); let icon_str = icon.get(&meta.name); assert_eq!(icon_str, format!("{}{}", "\u{1f4c2}", icon.icon_separator)); } #[test] fn get_icon_by_name() { let tmp_dir = tempdir().expect("failed to create temp dir"); for (file_name, file_icon) in &IconTheme::get_default_icons_by_name() { let file_path = tmp_dir.path().join(file_name); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); let icon = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); let icon_str = icon.get(&meta.name); assert_eq!(icon_str, format!("{}{}", file_icon, icon.icon_separator)); } } #[test] fn get_icon_by_extension() { let tmp_dir = tempdir().expect("failed to create temp dir"); for (ext, file_icon) in &IconTheme::get_default_icons_by_extension() { let file_path = tmp_dir.path().join(format!("file.{ext}")); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); let icon = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); let icon_str = icon.get(&meta.name); assert_eq!(icon_str, format!("{}{}", file_icon, icon.icon_separator)); } } } 07070100000038000081A400000000000000000000000166C4C37900000B9F000000000000000000000000000000000000001600000000lsd-1.1.5/src/main.rs#![allow( clippy::cast_precision_loss, clippy::cast_sign_loss, clippy::match_same_arms, clippy::cast_possible_wrap )] extern crate chrono; extern crate chrono_humanize; extern crate clap; extern crate dirs; extern crate libc; extern crate lscolors; #[cfg(test)] extern crate tempfile; extern crate term_grid; extern crate terminal_size; extern crate unicode_width; extern crate url; extern crate wild; extern crate yaml_rust; #[cfg(unix)] extern crate users; #[cfg(windows)] extern crate windows; mod app; mod color; mod config_file; mod core; mod display; mod flags; mod git; mod git_theme; mod icon; mod meta; mod sort; mod theme; use clap::Parser; use crate::app::Cli; use crate::config_file::Config; use crate::core::Core; use crate::flags::Flags; #[derive(PartialEq, Eq, PartialOrd, Copy, Clone)] pub enum ExitCode { OK, MinorIssue, MajorIssue, } impl ExitCode { pub fn set_if_greater(&mut self, code: ExitCode) { let self_i32 = *self as i32; let code_i32 = code as i32; if self_i32 < code_i32 { *self = code; } } } /// Macro used to avoid panicking when the lsd method is used with a pipe and /// stderr close before our program. #[macro_export] macro_rules! print_error { ($($arg:tt)*) => { { use std::io::Write; let stderr = std::io::stderr(); { let mut handle = stderr.lock(); // We can write on stderr, so we simply ignore the error and don't print // and stop with success. let res = handle.write_all(std::format!("lsd: {}\n\n", std::format!($($arg)*)).as_bytes()); if res.is_err() { std::process::exit(0); } } } }; } /// Macro used to avoid panicking when the lsd method is used with a pipe and /// stdout close before our program. #[macro_export] macro_rules! print_output { ($($arg:tt)*) => { use std::io::Write; let stderr = std::io::stdout(); { let mut handle = stderr.lock(); // We can write on stdout, so we simply ignore the error and don't print // and stop with success. let res = handle.write_all(std::format!($($arg)*).as_bytes()); if res.is_err() { std::process::exit(0); } } }; } fn main() { let cli = Cli::parse_from(wild::args_os()); let config = if cli.ignore_config { Config::with_none() } else if let Some(path) = &cli.config_file { Config::from_file(path).expect("Provided file path is invalid") } else { Config::default() }; let flags = Flags::configure_from(&cli, &config).unwrap_or_else(|err| err.exit()); let core = Core::new(flags); let exit_code = core.run(cli.inputs); std::process::exit(exit_code as i32); } 07070100000039000041ED00000000000000000000000266C4C37900000000000000000000000000000000000000000000001300000000lsd-1.1.5/src/meta0707010000003A000081A400000000000000000000000166C4C379000010EA000000000000000000000000000000000000002500000000lsd-1.1.5/src/meta/access_control.rsuse crate::color::{ColoredString, Colors, Elem}; use std::path::Path; #[derive(Clone, Debug)] pub struct AccessControl { has_acl: bool, selinux_context: String, smack_context: String, } impl AccessControl { #[cfg(not(unix))] pub fn for_path(_: &Path) -> Self { Self::from_data(false, &[], &[]) } #[cfg(unix)] pub fn for_path(path: &Path) -> Self { let has_acl = !xattr::get(path, Method::Acl.name()) .unwrap_or_default() .unwrap_or_default() .is_empty(); let selinux_context = xattr::get(path, Method::Selinux.name()) .unwrap_or_default() .unwrap_or_default(); let smack_context = xattr::get(path, Method::Smack.name()) .unwrap_or_default() .unwrap_or_default(); Self::from_data(has_acl, &selinux_context, &smack_context) } fn from_data(has_acl: bool, selinux_context: &[u8], smack_context: &[u8]) -> Self { let selinux_context = String::from_utf8_lossy(selinux_context).to_string(); let smack_context = String::from_utf8_lossy(smack_context).to_string(); Self { has_acl, selinux_context, smack_context, } } pub fn render_method(&self, colors: &Colors) -> ColoredString { if self.has_acl { colors.colorize('+', &Elem::Acl) } else if !self.selinux_context.is_empty() || !self.smack_context.is_empty() { colors.colorize('.', &Elem::Context) } else { colors.colorize("", &Elem::Acl) } } pub fn render_context(&self, colors: &Colors) -> ColoredString { let mut context = self.selinux_context.clone(); if !self.smack_context.is_empty() { if !context.is_empty() { context += "+"; } context += &self.smack_context; } if context.is_empty() { context += "?"; } colors.colorize(context, &Elem::Context) } } #[cfg(unix)] enum Method { Acl, Selinux, Smack, } #[cfg(unix)] impl Method { fn name(&self) -> &'static str { match self { Method::Acl => "system.posix_acl_access", Method::Selinux => "security.selinux", Method::Smack => "security.SMACK64", } } } #[cfg(test)] mod test { use super::AccessControl; use crate::color::{Colors, ThemeOption}; use crossterm::style::{Color, Stylize}; #[test] fn test_acl_only_indicator() { // actual file would collide with proper AC data, no permission to scrub those let access_control = AccessControl::from_data(true, &[], &[]); assert_eq!( String::from("+").with(Color::DarkCyan), access_control.render_method(&Colors::new(ThemeOption::Default)) ); } #[test] fn test_smack_only_indicator() { let access_control = AccessControl::from_data(false, &[], &[b'a']); assert_eq!( String::from(".").with(Color::Cyan), access_control.render_method(&Colors::new(ThemeOption::Default)) ); } #[test] fn test_acl_and_selinux_indicator() { let access_control = AccessControl::from_data(true, &[b'a'], &[]); assert_eq!( String::from("+").with(Color::DarkCyan), access_control.render_method(&Colors::new(ThemeOption::Default)) ); } #[test] fn test_selinux_context() { let access_control = AccessControl::from_data(false, &[b'a'], &[]); assert_eq!( String::from("a").with(Color::Cyan), access_control.render_context(&Colors::new(ThemeOption::Default)) ); } #[test] fn test_selinux_and_smack_context() { let access_control = AccessControl::from_data(false, &[b'a'], &[b'b']); assert_eq!( String::from("a+b").with(Color::Cyan), access_control.render_context(&Colors::new(ThemeOption::Default)) ); } #[test] fn test_no_context() { let access_control = AccessControl::from_data(false, &[], &[]); assert_eq!( String::from("?").with(Color::Cyan), access_control.render_context(&Colors::new(ThemeOption::Default)) ); } } 0707010000003B000081A400000000000000000000000166C4C37900002CB1000000000000000000000000000000000000001B00000000lsd-1.1.5/src/meta/date.rsuse super::locale::current_locale; use crate::color::{ColoredString, Colors, Elem}; use crate::flags::{DateFlag, Flags}; use chrono::{DateTime, Duration, Local}; use chrono_humanize::HumanTime; use std::fs::Metadata; use std::panic; use std::time::SystemTime; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Date { Date(DateTime<Local>), Invalid, } // Note that this is split from the From for Metadata so we can test this one (as we can't mock Metadata) impl From<SystemTime> for Date { fn from(systime: SystemTime) -> Self { // FIXME: This should really involve a result, but there's upstream issues in chrono. See https://github.com/chronotope/chrono/issues/110 let res = panic::catch_unwind(|| systime.into()); res.map_or(Date::Invalid, Date::Date) } } impl From<&Metadata> for Date { fn from(meta: &Metadata) -> Self { meta.modified() .expect("failed to retrieve modified date") .into() } } impl Date { pub fn render(&self, colors: &Colors, flags: &Flags) -> ColoredString { let now = Local::now(); #[allow(deprecated)] let elem = match self { &Date::Date(modified) if modified > now - Duration::hours(1) => Elem::HourOld, &Date::Date(modified) if modified > now - Duration::days(1) => Elem::DayOld, &Date::Date(_) | Date::Invalid => Elem::Older, }; colors.colorize(self.date_string(flags), &elem) } fn date_string(&self, flags: &Flags) -> String { let locale = current_locale(); if let Date::Date(val) = self { #[allow(deprecated)] match &flags.date { DateFlag::Date => val.format("%c").to_string(), DateFlag::Locale => val.format_localized("%c", locale).to_string(), DateFlag::Relative => HumanTime::from(*val - Local::now()).to_string(), DateFlag::Iso => { // 365.2425 * 24 * 60 * 60 = 31556952 seconds per year // 15778476 seconds are 6 months if *val > Local::now() - Duration::seconds(15_778_476) { val.format("%m-%d %R").to_string() } else { val.format("%F").to_string() } } DateFlag::Formatted(format) => val.format_localized(format, locale).to_string(), } } else { String::from('-') } } } #[cfg(test)] mod test { use super::Date; use crate::color::{Colors, ThemeOption}; use crate::flags::{DateFlag, Flags}; use crate::meta::locale::current_locale; use chrono::{DateTime, Duration, Local}; use crossterm::style::{Color, Stylize}; use std::io; use std::path::Path; use std::process::{Command, ExitStatus}; use std::{env, fs}; #[cfg(unix)] fn cross_platform_touch(path: &Path, date: &DateTime<Local>) -> io::Result<ExitStatus> { Command::new("touch") .arg("-t") .arg(date.format("%Y%m%d%H%M.%S").to_string()) .arg(path) .status() } #[cfg(windows)] fn cross_platform_touch(path: &Path, date: &DateTime<Local>) -> io::Result<ExitStatus> { use std::process::Stdio; let copy_success = Command::new("cmd") .arg("/C") .arg("copy") .arg("NUL") .arg(path) .stdout(Stdio::null()) // Windows doesn't have a quiet flag .status()? .success(); assert!(copy_success, "failed to create empty file"); Command::new("powershell") .arg("-Command") .arg(format!( r#"$(Get-Item {}).lastwritetime=$(Get-Date "{}")"#, path.display(), date.to_rfc3339() )) .status() } #[test] fn test_an_hour_old_file_color() { let mut file_path = env::temp_dir(); file_path.push("test_an_hour_old_file_color.tmp"); #[allow(deprecated)] let creation_date = Local::now() - chrono::Duration::seconds(4); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags::default(); assert_eq!( creation_date .format("%c") .to_string() .with(Color::AnsiValue(40)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] fn test_a_day_old_file_color() { let mut file_path = env::temp_dir(); file_path.push("test_a_day_old_file_color.tmp"); #[allow(deprecated)] let creation_date = Local::now() - chrono::Duration::hours(4); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags::default(); assert_eq!( creation_date .format("%c") .to_string() .with(Color::AnsiValue(42)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] fn test_a_several_days_old_file_color() { let mut file_path = env::temp_dir(); file_path.push("test_a_several_days_old_file_color.tmp"); #[allow(deprecated)] let creation_date = Local::now() - chrono::Duration::days(2); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags::default(); assert_eq!( creation_date .format("%c") .to_string() .with(Color::AnsiValue(36)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] fn test_with_relative_date() { let mut file_path = env::temp_dir(); file_path.push("test_with_relative_date.tmp"); #[allow(deprecated)] let creation_date = Local::now() - chrono::Duration::days(2); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags { date: DateFlag::Relative, ..Default::default() }; assert_eq!( "2 days ago".to_string().with(Color::AnsiValue(36)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] fn test_with_relative_date_now() { let mut file_path = env::temp_dir(); file_path.push("test_with_relative_date_now.tmp"); let creation_date = Local::now(); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags { date: DateFlag::Relative, ..Default::default() }; assert_eq!( "now".to_string().with(Color::AnsiValue(40)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] fn test_iso_format_now() { let mut file_path = env::temp_dir(); file_path.push("test_iso_format_now.tmp"); let creation_date = Local::now(); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags { date: DateFlag::Iso, ..Default::default() }; assert_eq!( creation_date .format("%m-%d %R") .to_string() .with(Color::AnsiValue(40)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] fn test_iso_format_year_old() { let mut file_path = env::temp_dir(); file_path.push("test_iso_format_year_old.tmp"); #[allow(deprecated)] let creation_date = Local::now() - Duration::days(400); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags { date: DateFlag::Iso, ..Default::default() }; assert_eq!( creation_date .format("%F") .to_string() .with(Color::AnsiValue(36)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] fn test_locale_format_now() { let mut file_path = env::temp_dir(); file_path.push("test_locale_format_now.tmp"); let creation_date = Local::now(); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags { date: DateFlag::Locale, ..Default::default() }; assert_eq!( creation_date .format_localized("%c", current_locale()) .to_string() .with(Color::AnsiValue(40)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] #[cfg(all(not(windows), target_arch = "x86_64"))] fn test_bad_date() { // 4437052 is the bad year taken from https://github.com/lsd-rs/lsd/issues/529 that we know is both // a) high enough to break chrono // b) not high enough to break SystemTime (as Duration::MAX would) let end_time = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::new(4437052 * 365 * 24 * 60 * 60, 0); let colors = Colors::new(ThemeOption::Default); let date = Date::from(end_time); let flags = Flags { date: DateFlag::Date, ..Default::default() }; assert_eq!( "-".to_string().with(Color::AnsiValue(36)), date.render(&colors, &flags) ); } } 0707010000003C000081A400000000000000000000000166C4C37900002B3C000000000000000000000000000000000000001F00000000lsd-1.1.5/src/meta/filetype.rsuse crate::color::{ColoredString, Colors, Elem}; use std::fs::Metadata; #[derive(Debug, PartialEq, Eq, Copy, Clone)] #[cfg_attr(windows, allow(dead_code))] pub enum FileType { BlockDevice, CharDevice, Directory { uid: bool }, File { uid: bool, exec: bool }, SymLink { is_dir: bool }, Pipe, Socket, Special, } impl FileType { #[cfg(windows)] const EXECUTABLE_EXTENSIONS: &'static [&'static str] = &["exe", "msi", "bat", "ps1"]; #[cfg(unix)] pub fn new( meta: &Metadata, symlink_meta: Option<&Metadata>, permissions: &crate::meta::Permissions, ) -> Self { use std::os::unix::fs::FileTypeExt; let file_type = meta.file_type(); if file_type.is_file() { FileType::File { exec: permissions.is_executable(), uid: permissions.setuid, } } else if file_type.is_dir() { FileType::Directory { uid: permissions.setuid, } } else if file_type.is_fifo() { FileType::Pipe } else if file_type.is_symlink() { FileType::SymLink { // if broken, defaults to false is_dir: symlink_meta.map(|m| m.is_dir()).unwrap_or_default(), } } else if file_type.is_char_device() { FileType::CharDevice } else if file_type.is_block_device() { FileType::BlockDevice } else if file_type.is_socket() { FileType::Socket } else { FileType::Special } } #[cfg(windows)] pub fn new(meta: &Metadata, symlink_meta: Option<&Metadata>, path: &std::path::Path) -> Self { let file_type = meta.file_type(); if file_type.is_file() { let exec = path .extension() .map(|ext| { Self::EXECUTABLE_EXTENSIONS .iter() .map(std::ffi::OsStr::new) .any(|exec_ext| ext == exec_ext) }) .unwrap_or(false); FileType::File { exec, uid: false } } else if file_type.is_dir() { FileType::Directory { uid: false } } else if file_type.is_symlink() { FileType::SymLink { // if broken, defaults to false is_dir: symlink_meta.map(|m| m.is_dir()).unwrap_or_default(), } } else { FileType::Special } } pub fn is_dirlike(self) -> bool { matches!( self, FileType::Directory { .. } | FileType::SymLink { is_dir: true } ) } } impl FileType { pub fn render(self, colors: &Colors) -> ColoredString { match self { FileType::File { exec, .. } => colors.colorize('.', &Elem::File { exec, uid: false }), FileType::Directory { .. } => colors.colorize('d', &Elem::Dir { uid: false }), FileType::Pipe => colors.colorize('|', &Elem::Pipe), FileType::SymLink { .. } => colors.colorize('l', &Elem::SymLink), FileType::BlockDevice => colors.colorize('b', &Elem::BlockDevice), FileType::CharDevice => colors.colorize('c', &Elem::CharDevice), FileType::Socket => colors.colorize('s', &Elem::Socket), FileType::Special => colors.colorize('?', &Elem::Special), } } } #[cfg(test)] mod test { use super::FileType; use crate::color::{Colors, ThemeOption}; #[cfg(unix)] use crate::flags::PermissionFlag; #[cfg(unix)] use crate::meta::permissions_or_attributes::PermissionsOrAttributes; #[cfg(unix)] use crate::meta::Permissions; use crossterm::style::{Color, Stylize}; use std::fs::File; #[cfg(unix)] use std::os::unix::fs::symlink; #[cfg(unix)] use std::os::unix::net::UnixListener; #[cfg(unix)] use std::process::Command; use tempfile::tempdir; #[test] #[cfg(unix)] // Windows uses different default permissions fn test_file_type() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); let meta = file_path.metadata().expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); assert_eq!( ".".to_string().with(Color::AnsiValue(184)), file_type.render(&colors) ); } #[test] fn test_dir_type() { let tmp_dir = tempdir().expect("failed to create temp dir"); #[cfg(not(windows))] let meta = crate::meta::Meta::from_path(tmp_dir.path(), false, PermissionFlag::Rwx) .expect("failed to get tempdir path"); let metadata = tmp_dir.path().metadata().expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); #[cfg(not(windows))] let file_type = match meta.permissions_or_attributes { Some(PermissionsOrAttributes::Permissions(permissions)) => { FileType::new(&metadata, None, &permissions) } _ => panic!("unexpected"), }; #[cfg(windows)] let file_type = FileType::new(&metadata, None, tmp_dir.path()); assert_eq!( "d".to_string().with(Color::AnsiValue(33)), file_type.render(&colors) ); } #[test] #[cfg(unix)] // Symlink support is *hard* on Windows fn test_symlink_type_file() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let file_path = tmp_dir.path().join("file.tmp"); File::create(&file_path).expect("failed to create file"); // Create the symlink let symlink_path = tmp_dir.path().join("target.tmp"); symlink(&file_path, &symlink_path).expect("failed to create symlink"); let meta = symlink_path .symlink_metadata() .expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, Some(&meta), &Permissions::from(&meta)); assert_eq!( "l".to_string().with(Color::AnsiValue(44)), file_type.render(&colors) ); } #[test] #[cfg(unix)] fn test_symlink_type_dir() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create directory let dir_path = tmp_dir.path().join("dir.d"); std::fs::create_dir(&dir_path).expect("failed to create dir"); // Create symlink let symlink_path = tmp_dir.path().join("target.d"); symlink(&dir_path, &symlink_path).expect("failed to create symlink"); let meta = symlink_path .symlink_metadata() .expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, Some(&meta), &Permissions::from(&meta)); assert_eq!( "l".to_string().with(Color::AnsiValue(44)), file_type.render(&colors) ); } #[test] #[cfg(unix)] // Windows pipes aren't like Unix pipes fn test_pipe_type() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the pipe; let pipe_path = tmp_dir.path().join("pipe.tmp"); let success = Command::new("mkfifo") .arg(&pipe_path) .status() .expect("failed to exec mkfifo") .success(); assert!(success, "failed to exec mkfifo"); let meta = pipe_path.metadata().expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); assert_eq!( "|".to_string().with(Color::AnsiValue(44)), file_type.render(&colors) ); } #[test] #[cfg(feature = "sudo")] fn test_char_device_type() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the char device; let char_device_path = tmp_dir.path().join("char-device.tmp"); let success = Command::new("sudo") .arg("mknod") .arg(&char_device_path) .arg("c") .arg("89") .arg("1") .status() .expect("failed to exec mknod") .success(); assert!(success, "failed to exec mknod"); let meta = char_device_path.metadata().expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); assert_eq!( "c".to_string().with(Color::AnsiValue(44)), file_type.render(&colors) ); } #[test] #[cfg(unix)] // Sockets don't work the same way on Windows fn test_socket_type() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the socket; let socket_path = tmp_dir.path().join("socket.tmp"); UnixListener::bind(&socket_path).expect("failed to create the socket"); let meta = socket_path.metadata().expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); assert_eq!( "s".to_string().with(Color::AnsiValue(44)), file_type.render(&colors) ); } #[cfg(windows)] #[test] fn test_file_executable() { let tmp_dir = tempdir().expect("failed to create temp dir"); for ext in FileType::EXECUTABLE_EXTENSIONS { // Create the file; let file_path = tmp_dir.path().join(format!("file.{ext}")); File::create(&file_path).expect("failed to create file"); let meta = file_path.metadata().expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &file_path); assert_eq!( ".".to_string().with(Color::AnsiValue(40)), file_type.render(&colors) ); } } #[cfg(windows)] #[test] fn test_file_not_executable() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); let meta = file_path.metadata().expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &file_path); assert_eq!( ".".to_string().with(Color::AnsiValue(184)), file_type.render(&colors) ); } } 0707010000003D000081A400000000000000000000000166C4C379000009D5000000000000000000000000000000000000002600000000lsd-1.1.5/src/meta/git_file_status.rsuse crate::color::{self, ColoredString, Colors}; use crate::git::GitStatus; use crate::git_theme::GitTheme; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct GitFileStatus { pub index: GitStatus, pub workdir: GitStatus, } impl Default for GitFileStatus { fn default() -> Self { Self { index: GitStatus::Default, workdir: GitStatus::Default, } } } impl GitFileStatus { #[cfg(not(feature = "no-git"))] pub fn new(status: git2::Status) -> Self { Self { index: match status { s if s.contains(git2::Status::INDEX_NEW) => GitStatus::NewInIndex, s if s.contains(git2::Status::INDEX_DELETED) => GitStatus::Deleted, s if s.contains(git2::Status::INDEX_MODIFIED) => GitStatus::Modified, s if s.contains(git2::Status::INDEX_RENAMED) => GitStatus::Renamed, s if s.contains(git2::Status::INDEX_TYPECHANGE) => GitStatus::Typechange, _ => GitStatus::Unmodified, }, workdir: match status { s if s.contains(git2::Status::WT_NEW) => GitStatus::NewInWorkdir, s if s.contains(git2::Status::WT_DELETED) => GitStatus::Deleted, s if s.contains(git2::Status::WT_MODIFIED) => GitStatus::Modified, s if s.contains(git2::Status::WT_RENAMED) => GitStatus::Renamed, s if s.contains(git2::Status::IGNORED) => GitStatus::Ignored, s if s.contains(git2::Status::WT_TYPECHANGE) => GitStatus::Typechange, s if s.contains(git2::Status::CONFLICTED) => GitStatus::Conflicted, _ => GitStatus::Unmodified, }, } } pub fn render(&self, colors: &Colors, git_theme: &GitTheme) -> ColoredString { let res = [ colors.colorize( git_theme.get_symbol(&self.index), &color::Elem::GitStatus { status: self.index }, ), colors.colorize( git_theme.get_symbol(&self.workdir), &color::Elem::GitStatus { status: self.workdir, }, ), ] .into_iter() // From the experiment, the maximum string size is 153 bytes .fold(String::with_capacity(160), |mut acc, x| { acc.push_str(&x.to_string()); acc }); ColoredString::new(Colors::default_style(), res) } } 0707010000003E000081A400000000000000000000000166C4C37900000B11000000000000000000000000000000000000002000000000lsd-1.1.5/src/meta/indicator.rsuse crate::color::{ColoredString, Colors}; use crate::flags::Flags; use crate::meta::FileType; #[derive(Clone, Debug)] pub struct Indicator(&'static str); impl From<FileType> for Indicator { fn from(file_type: FileType) -> Self { let res = match file_type { FileType::Directory { .. } => "/", FileType::File { exec: true, .. } => "*", FileType::Pipe => "|", FileType::Socket => "=", FileType::SymLink { .. } => "@", _ => "", }; Indicator(res) } } impl Indicator { pub fn render(&self, flags: &Flags) -> ColoredString { if flags.display_indicators.0 { ColoredString::new(Colors::default_style(), self.0.to_string()) } else { ColoredString::new(Colors::default_style(), "".into()) } } } #[cfg(test)] mod test { use super::Indicator; use crate::flags::{Flags, Indicators}; use crate::meta::FileType; #[test] fn test_directory_indicator() { let flags = Flags { display_indicators: Indicators(true), ..Default::default() }; let file_type = Indicator::from(FileType::Directory { uid: false }); assert_eq!("/", file_type.render(&flags).to_string()); } #[test] fn test_executable_file_indicator() { let flags = Flags { display_indicators: Indicators(true), ..Default::default() }; let file_type = Indicator::from(FileType::File { uid: false, exec: true, }); assert_eq!("*", file_type.render(&flags).to_string()); } #[test] fn test_socket_indicator() { let flags = Flags { display_indicators: Indicators(true), ..Default::default() }; let file_type = Indicator::from(FileType::Socket); assert_eq!("=", file_type.render(&flags).to_string()); } #[test] fn test_symlink_indicator() { let flags = Flags { display_indicators: Indicators(true), ..Default::default() }; let file_type = Indicator::from(FileType::SymLink { is_dir: false }); assert_eq!("@", file_type.render(&flags).to_string()); let file_type = Indicator::from(FileType::SymLink { is_dir: true }); assert_eq!("@", file_type.render(&flags).to_string()); } #[test] fn test_not_represented_indicator() { let flags = Flags { display_indicators: Indicators(true), ..Default::default() }; // The File type doesn't have any indicator let file_type = Indicator::from(FileType::File { exec: false, uid: false, }); assert_eq!("", file_type.render(&flags).to_string()); } } 0707010000003F000081A400000000000000000000000166C4C379000005CF000000000000000000000000000000000000001C00000000lsd-1.1.5/src/meta/inode.rsuse crate::color::{ColoredString, Colors, Elem}; use std::fs::Metadata; #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub struct INode { index: Option<u64>, } impl From<&Metadata> for INode { #[cfg(unix)] fn from(meta: &Metadata) -> Self { use std::os::unix::fs::MetadataExt; let index = meta.ino(); Self { index: Some(index) } } #[cfg(windows)] fn from(_: &Metadata) -> Self { Self { index: None } } } impl INode { pub fn render(&self, colors: &Colors) -> ColoredString { match self.index { Some(i) => colors.colorize(i.to_string(), &Elem::INode { valid: true }), None => colors.colorize('-', &Elem::INode { valid: false }), } } } #[cfg(test)] #[cfg(unix)] mod tests { use super::INode; use std::env; use std::io; use std::path::Path; use std::process::{Command, ExitStatus}; fn cross_platform_touch(path: &Path) -> io::Result<ExitStatus> { Command::new("touch").arg(path).status() } #[test] fn test_inode_no_zero() { let mut file_path = env::temp_dir(); file_path.push("inode.tmp"); let success = cross_platform_touch(&file_path).unwrap().success(); assert!(success, "failed to exec touch"); let inode = INode::from(&file_path.metadata().unwrap()); #[cfg(unix)] assert!(inode.index.is_some()); #[cfg(windows)] assert!(inode.index.is_none()); } } 07070100000040000081A400000000000000000000000166C4C379000005D5000000000000000000000000000000000000001C00000000lsd-1.1.5/src/meta/links.rsuse crate::color::{ColoredString, Colors, Elem}; use std::fs::Metadata; #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub struct Links { nlink: Option<u64>, } impl From<&Metadata> for Links { #[cfg(unix)] fn from(meta: &Metadata) -> Self { use std::os::unix::fs::MetadataExt; let nlink = meta.nlink(); Self { nlink: Some(nlink) } } #[cfg(windows)] fn from(_: &Metadata) -> Self { Self { nlink: None } } } impl Links { pub fn render(&self, colors: &Colors) -> ColoredString { match self.nlink { Some(i) => colors.colorize(i.to_string(), &Elem::Links { valid: true }), None => colors.colorize('-', &Elem::Links { valid: false }), } } } #[cfg(test)] #[cfg(unix)] mod tests { use super::Links; use std::env; use std::io; use std::path::Path; use std::process::{Command, ExitStatus}; fn cross_platform_touch(path: &Path) -> io::Result<ExitStatus> { Command::new("touch").arg(path).status() } #[test] fn test_hardlinks_no_zero() { let mut file_path = env::temp_dir(); file_path.push("inode.tmp"); let success = cross_platform_touch(&file_path).unwrap().success(); assert!(success, "failed to exec touch"); let links = Links::from(&file_path.metadata().unwrap()); #[cfg(unix)] assert!(links.nlink.is_some()); #[cfg(windows)] assert!(links.nlink.is_none()); } } 07070100000041000081A400000000000000000000000166C4C3790000019D000000000000000000000000000000000000001D00000000lsd-1.1.5/src/meta/locale.rsuse chrono::Locale; use once_cell::sync::OnceCell; use sys_locale::get_locale; fn locale_str() -> String { get_locale().unwrap_or_default().replace('-', "_") } /// Finds current locale pub fn current_locale() -> Locale { const DEFAULT: Locale = Locale::en_US; static CACHE: OnceCell<Locale> = OnceCell::new(); *CACHE.get_or_init(|| Locale::try_from(locale_str().as_str()).unwrap_or(DEFAULT)) } 07070100000042000081A400000000000000000000000166C4C37900003800000000000000000000000000000000000000001A00000000lsd-1.1.5/src/meta/mod.rsmod access_control; mod date; mod filetype; pub mod git_file_status; mod indicator; mod inode; mod links; mod locale; pub mod name; pub mod owner; mod permissions; mod permissions_or_attributes; mod size; mod symlink; #[cfg(windows)] mod windows_attributes; #[cfg(windows)] mod windows_utils; pub use self::access_control::AccessControl; pub use self::date::Date; pub use self::filetype::FileType; pub use self::git_file_status::GitFileStatus; pub use self::indicator::Indicator; pub use self::inode::INode; pub use self::links::Links; pub use self::name::Name; pub use self::owner::{Cache as OwnerCache, Owner}; pub use self::permissions::Permissions; use self::permissions_or_attributes::PermissionsOrAttributes; pub use self::size::Size; pub use self::symlink::SymLink; use crate::flags::{Display, Flags, Layout, PermissionFlag}; use crate::{print_error, ExitCode}; use crate::git::GitCache; use std::io::{self, Error, ErrorKind}; use std::path::{Component, Path, PathBuf}; #[cfg(windows)] use self::windows_attributes::get_attributes; #[derive(Clone, Debug)] pub struct Meta { pub name: Name, pub path: PathBuf, pub permissions_or_attributes: Option<PermissionsOrAttributes>, pub date: Option<Date>, pub owner: Option<Owner>, pub file_type: FileType, pub size: Option<Size>, pub symlink: SymLink, pub indicator: Indicator, pub inode: Option<INode>, pub links: Option<Links>, pub content: Option<Vec<Meta>>, pub access_control: Option<AccessControl>, pub git_status: Option<GitFileStatus>, } impl Meta { pub fn recurse_into( &self, depth: usize, flags: &Flags, cache: Option<&GitCache>, ) -> io::Result<(Option<Vec<Meta>>, ExitCode)> { if depth == 0 { return Ok((None, ExitCode::OK)); } if flags.display == Display::DirectoryOnly && flags.layout != Layout::Tree { return Ok((None, ExitCode::OK)); } match self.file_type { FileType::Directory { .. } => (), FileType::SymLink { is_dir: true } => { if flags.blocks.0.len() > 1 { return Ok((None, ExitCode::OK)); } } _ => return Ok((None, ExitCode::OK)), } let entries = match self.path.read_dir() { Ok(entries) => entries, Err(err) => { print_error!("{}: {}.", self.path.display(), err); return Ok((None, ExitCode::MinorIssue)); } }; let mut content: Vec<Meta> = Vec::new(); if matches!(flags.display, Display::All | Display::SystemProtected) && flags.layout != Layout::Tree { let mut current_meta = self.clone(); ".".clone_into(&mut current_meta.name.name); let mut parent_meta = Self::from_path( &self.path.join(Component::ParentDir), flags.dereference.0, flags.permission, )?; "..".clone_into(&mut parent_meta.name.name); current_meta.git_status = cache.and_then(|cache| cache.get(¤t_meta.path, true)); parent_meta.git_status = cache.and_then(|cache| cache.get(&parent_meta.path, true)); content.push(current_meta); content.push(parent_meta); } let mut exit_code = ExitCode::OK; for entry in entries { let entry = entry?; let path = entry.path(); let name = path .file_name() .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "invalid file name"))?; if flags.ignore_globs.0.is_match(name) { continue; } #[cfg(windows)] let is_hidden = name.to_string_lossy().starts_with('.') || windows_utils::is_path_hidden(&path); #[cfg(not(windows))] let is_hidden = name.to_string_lossy().starts_with('.'); #[cfg(windows)] let is_system = windows_utils::is_path_system(&path); #[cfg(not(windows))] let is_system = false; match flags.display { // show hidden files, but ignore system protected files Display::All | Display::AlmostAll if is_system => continue, // ignore hidden and system protected files Display::VisibleOnly if is_hidden || is_system => continue, _ => {} } let mut entry_meta = match Self::from_path(&path, flags.dereference.0, flags.permission) { Ok(res) => res, Err(err) => { print_error!("{}: {}.", path.display(), err); exit_code.set_if_greater(ExitCode::MinorIssue); continue; } }; // skip files for --tree -d if flags.layout == Layout::Tree && flags.display == Display::DirectoryOnly && !entry.file_type()?.is_dir() { continue; } // check dereferencing if flags.dereference.0 || !matches!(entry_meta.file_type, FileType::SymLink { .. }) { match entry_meta.recurse_into(depth - 1, flags, cache) { Ok((content, rec_exit_code)) => { entry_meta.content = content; exit_code.set_if_greater(rec_exit_code); } Err(err) => { print_error!("{}: {}.", path.display(), err); exit_code.set_if_greater(ExitCode::MinorIssue); continue; } }; } let is_directory = entry.file_type()?.is_dir(); entry_meta.git_status = cache.and_then(|cache| cache.get(&entry_meta.path, is_directory)); content.push(entry_meta); } Ok((Some(content), exit_code)) } pub fn calculate_total_size(&mut self) { if self.size.is_none() { return; } if let FileType::Directory { .. } = self.file_type { if let Some(metas) = &mut self.content { let mut size_accumulated = match &self.size { Some(size) => size.get_bytes(), None => 0, }; for x in &mut metas.iter_mut() { x.calculate_total_size(); size_accumulated += match &x.size { Some(size) => size.get_bytes(), None => 0, }; } self.size = Some(Size::new(size_accumulated)); } else { // possibility that 'depth' limited the recursion in 'recurse_into' self.size = Some(Size::new(Meta::calculate_total_file_size(&self.path))); } } } fn calculate_total_file_size(path: &Path) -> u64 { let metadata = path.symlink_metadata(); let metadata = match metadata { Ok(meta) => meta, Err(err) => { print_error!("{}: {}.", path.display(), err); return 0; } }; let file_type = metadata.file_type(); if file_type.is_file() { metadata.len() } else if file_type.is_dir() { let mut size = metadata.len(); let entries = match path.read_dir() { Ok(entries) => entries, Err(err) => { print_error!("{}: {}.", path.display(), err); return size; } }; for entry in entries { let path = match entry { Ok(entry) => entry.path(), Err(err) => { print_error!("{}: {}.", path.display(), err); continue; } }; size += Meta::calculate_total_file_size(&path); } size } else { 0 } } pub fn from_path( path: &Path, dereference: bool, permission_flag: PermissionFlag, ) -> io::Result<Self> { let mut metadata = path.symlink_metadata()?; let mut symlink_meta = None; let mut broken_link = false; if metadata.file_type().is_symlink() { match path.metadata() { Ok(m) => { if dereference { metadata = m; } else { symlink_meta = Some(m); } } Err(e) => { // This case, it is definitely a symlink or // path.symlink_metadata would have errored out if dereference { broken_link = true; eprintln!("lsd: {}: {}", path.to_str().unwrap_or(""), e); } } } } #[cfg(unix)] let (owner, permissions) = match permission_flag { PermissionFlag::Disable => (None, None), _ => ( Some(Owner::from(&metadata)), Some(Permissions::from(&metadata)), ), }; #[cfg(unix)] let permissions_or_attributes = permissions.map(PermissionsOrAttributes::Permissions); #[cfg(windows)] let (owner, permissions_or_attributes) = match permission_flag { PermissionFlag::Disable => (None, None), PermissionFlag::Attributes => ( None, Some(PermissionsOrAttributes::WindowsAttributes(get_attributes( &metadata, ))), ), _ => match windows_utils::get_file_data(path) { Ok((owner, permissions)) => ( Some(owner), Some(PermissionsOrAttributes::Permissions(permissions)), ), Err(e) => { eprintln!( "lsd: {}: {}(Hint: Consider using `--permission disable`.)", path.to_str().unwrap_or(""), e ); (None, None) } }, }; #[cfg(not(windows))] let file_type = FileType::new( &metadata, symlink_meta.as_ref(), &permissions.unwrap_or_default(), ); #[cfg(windows)] let file_type = FileType::new(&metadata, symlink_meta.as_ref(), path); let name = Name::new(path, file_type); let (inode, links, size, date, owner, permissions_or_attributes, access_control) = match broken_link { true => (None, None, None, None, None, None, None), false => ( Some(INode::from(&metadata)), Some(Links::from(&metadata)), Some(Size::from(&metadata)), Some(Date::from(&metadata)), Some(owner), Some(permissions_or_attributes), Some(AccessControl::for_path(path)), ), }; Ok(Self { inode, links, path: path.to_path_buf(), symlink: SymLink::from(path), size, date, indicator: Indicator::from(file_type), owner: owner.unwrap_or_default(), permissions_or_attributes: permissions_or_attributes.unwrap_or_default(), name, file_type, content: None, access_control, git_status: None, }) } } #[cfg(test)] mod tests { use crate::flags::PermissionFlag; use super::Meta; use std::fs::File; use tempfile::tempdir; #[cfg(unix)] #[test] fn test_from_path_path() { let dir = assert_fs::TempDir::new().unwrap(); let meta = Meta::from_path(dir.path(), false, PermissionFlag::Rwx).unwrap(); assert_eq!(meta.path, dir.path()) } #[test] fn test_from_path_disable_permission() { let dir = assert_fs::TempDir::new().unwrap(); let meta = Meta::from_path(dir.path(), false, PermissionFlag::Disable).unwrap(); assert!(meta.permissions_or_attributes.is_none()); assert!(meta.owner.is_none()); } #[test] fn test_from_path() { let tmp_dir = tempdir().expect("failed to create temp dir"); let path_a = tmp_dir.path().join("aaa.aa"); File::create(&path_a).expect("failed to create file"); let meta_a = Meta::from_path(&path_a, false, PermissionFlag::Rwx).expect("failed to get meta"); let path_b = tmp_dir.path().join("bbb.bb"); let path_c = tmp_dir.path().join("ccc.cc"); #[cfg(unix)] std::os::unix::fs::symlink(path_c, &path_b).expect("failed to create broken symlink"); // this needs to be tested on Windows // likely to fail because of permission issue // see https://doc.rust-lang.org/std/os/windows/fs/fn.symlink_file.html #[cfg(windows)] std::os::windows::fs::symlink_file(path_c, &path_b) .expect("failed to create broken symlink"); let meta_b = Meta::from_path(&path_b, true, PermissionFlag::Rwx).expect("failed to get meta"); assert!( meta_a.inode.is_some() && meta_a.links.is_some() && meta_a.size.is_some() && meta_a.date.is_some() && meta_a.owner.is_some() && meta_a.permissions_or_attributes.is_some() && meta_a.access_control.is_some() ); assert!( meta_b.inode.is_none() && meta_b.links.is_none() && meta_b.size.is_none() && meta_b.date.is_none() && meta_b.owner.is_none() && meta_b.permissions_or_attributes.is_none() && meta_b.access_control.is_none() ); } } 07070100000043000081A400000000000000000000000166C4C37900005ACE000000000000000000000000000000000000001B00000000lsd-1.1.5/src/meta/name.rsuse crate::color::{ColoredString, Colors, Elem}; use crate::flags::HyperlinkOption; use crate::icon::Icons; use crate::meta::filetype::FileType; use crate::print_error; use crate::url::Url; use std::cmp::{Ordering, PartialOrd}; use std::ffi::OsStr; use std::path::{Component, Path, PathBuf}; #[derive(Debug)] pub enum DisplayOption<'a> { FileName, Relative { base_path: &'a Path }, None, } #[derive(Clone, Debug, Eq)] pub struct Name { pub name: String, path: PathBuf, extension: Option<String>, file_type: FileType, } impl Name { pub fn new(path: &Path, file_type: FileType) -> Self { let name = match path.file_name() { Some(name) => name.to_string_lossy().to_string(), None => path.to_string_lossy().to_string(), }; let extension = path .extension() .map(|ext| ext.to_string_lossy().to_string()); Self { name, path: PathBuf::from(path), extension, file_type, } } pub fn file_name(&self) -> &str { self.path .file_name() .and_then(OsStr::to_str) .unwrap_or(&self.name) } fn relative_path<T: AsRef<Path> + Clone>(&self, base_path: T) -> PathBuf { let base_path = base_path.as_ref(); if self.path == base_path { return PathBuf::from(AsRef::<Path>::as_ref(&Component::CurDir)); } let shared_components: PathBuf = self .path .components() .zip(base_path.components()) .take_while(|(target_component, base_component)| target_component == base_component) .map(|tuple| tuple.0) .collect(); base_path .strip_prefix(&shared_components) .unwrap() .components() .map(|_| Component::ParentDir) .chain( self.path .strip_prefix(&shared_components) .unwrap() .components(), ) .collect() } fn escape(&self, string: &str, literal: bool) -> String { let mut name = string.to_string(); if !literal { if name.contains('\\') || name.contains('"') { name = name.replace('\'', "\'\\\'\'"); name = format!("\'{}\'", &name); } else if name.contains('\'') { name = format!("\"{}\"", &name); } else if name.contains(' ') || name.contains('$') { name = format!("\'{}\'", &name); } } let string = name; if string .chars() .all(|c| c >= 0x20 as char && c != 0x7f as char) { string } else { let mut chars = String::new(); for c in string.chars() { // The `escape_default` method on `char` is *almost* what we want here, but // it still escapes non-ASCII UTF-8 characters, which are still printable. if c >= 0x20 as char && c != 0x7f as char { chars.push(c); } else { chars += &c.escape_default().collect::<String>(); } } chars } } fn hyperlink(&self, name: String, hyperlink: HyperlinkOption) -> String { match hyperlink { HyperlinkOption::Always => { // HyperlinkOption::Auto gets converted to None or Always in core.rs based on tty_available match std::fs::canonicalize(&self.path) { Ok(rp) => { if let Ok(url) = Url::from_file_path(rp) { // Crossterm does not support hyperlinks as of now // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda format!("\x1B]8;;{url}\x1B\x5C{name}\x1B]8;;\x1B\x5C") } else { print_error!("{}: unable to form url.", name); name } } Err(err) => { // If the error is NotFound, it just means the file is a broken symlink. // That is not an error, and the user is already warned that the symlink is broken by the colors. if err.kind() != std::io::ErrorKind::NotFound { print_error!("{}: {}", name, err); } name } } } _ => name, } } pub fn render( &self, colors: &Colors, icons: &Icons, display_option: &DisplayOption, hyperlink: HyperlinkOption, literal: bool, ) -> ColoredString { let content = match display_option { DisplayOption::FileName => { format!( "{}{}", icons.get(self), self.hyperlink(self.escape(self.file_name(), literal), hyperlink) ) } DisplayOption::Relative { base_path } => format!( "{}{}", icons.get(self), self.hyperlink( self.escape(&self.relative_path(base_path).to_string_lossy(), literal), hyperlink ) ), DisplayOption::None => format!( "{}{}", icons.get(self), self.hyperlink( self.escape(&self.path.to_string_lossy(), literal), hyperlink ) ), }; let elem = match self.file_type { FileType::CharDevice => Elem::CharDevice, FileType::Directory { uid } => Elem::Dir { uid }, FileType::SymLink { .. } => Elem::SymLink, FileType::File { uid, exec } => Elem::File { uid, exec }, _ => Elem::File { exec: false, uid: false, }, }; colors.colorize_using_path(content, &self.path, &elem) } pub fn extension(&self) -> Option<&str> { self.extension.as_deref() } pub fn file_type(&self) -> FileType { self.file_type } } impl Ord for Name { fn cmp(&self, other: &Self) -> Ordering { self.name.to_lowercase().cmp(&other.name.to_lowercase()) } } impl PartialOrd for Name { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) } } impl PartialEq for Name { fn eq(&self, other: &Self) -> bool { self.name.eq_ignore_ascii_case(&other.name.to_lowercase()) } } #[cfg(test)] mod test { use super::DisplayOption; use super::Name; use crate::color::{self, Colors}; use crate::flags::PermissionFlag; use crate::flags::{HyperlinkOption, IconOption, IconTheme as FlagTheme}; use crate::icon::Icons; use crate::meta::FileType; use crate::meta::Meta; #[cfg(unix)] use crate::meta::Permissions; use crate::url::Url; use crossterm::style::{Color, Stylize}; use std::cmp::Ordering; use std::fs::{self, File}; #[cfg(unix)] use std::os::unix::fs::symlink; use std::path::{Path, PathBuf}; #[cfg(unix)] use std::process::Command; use tempfile::tempdir; #[test] #[cfg(unix)] // Windows uses different default permissions fn test_print_file_name() { let tmp_dir = tempdir().expect("failed to create temp dir"); let icons = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); // Create the file; let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); let meta = file_path.metadata().expect("failed to get metas"); let colors = Colors::new(color::ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); let name = Name::new(&file_path, file_type); assert_eq!( " file.txt".to_string().with(Color::AnsiValue(184)), name.render( &colors, &icons, &DisplayOption::FileName, HyperlinkOption::Never, true, ) ); } #[test] fn test_print_dir_name() { let tmp_dir = tempdir().expect("failed to create temp dir"); let icons = &Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); // Create the directory let dir_path = tmp_dir.path().join("directory"); fs::create_dir(&dir_path).expect("failed to create the dir"); let meta = Meta::from_path(&dir_path, false, PermissionFlag::Rwx).unwrap(); let colors = Colors::new(color::ThemeOption::NoLscolors); assert_eq!( " directory".to_string().with(Color::AnsiValue(33)), meta.name.render( &colors, icons, &DisplayOption::FileName, HyperlinkOption::Never, true ) ); } #[test] #[cfg(unix)] // Symlinks are hard on Windows fn test_print_symlink_name_file() { let tmp_dir = tempdir().expect("failed to create temp dir"); let icons = &Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); // Create the file; let file_path = tmp_dir.path().join("file.tmp"); File::create(&file_path).expect("failed to create file"); // Create the symlink let symlink_path = tmp_dir.path().join("target.tmp"); symlink(&file_path, &symlink_path).expect("failed to create symlink"); let meta = symlink_path .symlink_metadata() .expect("failed to get metas"); let target_meta = symlink_path.metadata().ok(); let colors = Colors::new(color::ThemeOption::NoLscolors); let file_type = FileType::new(&meta, target_meta.as_ref(), &Permissions::from(&meta)); let name = Name::new(&symlink_path, file_type); assert_eq!( " target.tmp".to_string().with(Color::AnsiValue(44)), name.render( &colors, icons, &DisplayOption::FileName, HyperlinkOption::Never, true ) ); } #[test] #[cfg(unix)] // Symlinks are hard on Windows fn test_print_symlink_name_dir() { let tmp_dir = tempdir().expect("failed to create temp dir"); let icons = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); // Create the directory; let dir_path = tmp_dir.path().join("tmp.d"); std::fs::create_dir(&dir_path).expect("failed to create dir"); // Create the symlink let symlink_path = tmp_dir.path().join("target.d"); symlink(&dir_path, &symlink_path).expect("failed to create symlink"); let meta = symlink_path .symlink_metadata() .expect("failed to get metas"); let target_meta = symlink_path.metadata().ok(); let colors = Colors::new(color::ThemeOption::NoLscolors); let file_type = FileType::new(&meta, target_meta.as_ref(), &Permissions::from(&meta)); let name = Name::new(&symlink_path, file_type); assert_eq!( " target.d".to_string().with(Color::AnsiValue(44)), name.render( &colors, &icons, &DisplayOption::FileName, HyperlinkOption::Never, true ) ); } #[test] #[cfg(unix)] fn test_print_other_type_name() { let tmp_dir = tempdir().expect("failed to create temp dir"); let icons = &Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); // Create the pipe; let pipe_path = tmp_dir.path().join("pipe.tmp"); let success = Command::new("mkfifo") .arg(&pipe_path) .status() .expect("failed to exec mkfifo") .success(); assert!(success, "failed to exec mkfifo"); let meta = pipe_path.metadata().expect("failed to get metas"); let colors = Colors::new(color::ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); let name = Name::new(&pipe_path, file_type); assert_eq!( " pipe.tmp".to_string().with(Color::AnsiValue(184)), name.render( &colors, icons, &DisplayOption::FileName, HyperlinkOption::Never, true ) ); } #[test] fn test_print_without_icon_or_color() { let tmp_dir = tempdir().expect("failed to create temp dir"); let icons = Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()); // Create the file; let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); let colors = Colors::new(color::ThemeOption::NoColor); assert_eq!( "file.txt", meta.name .render( &colors, &icons, &DisplayOption::FileName, HyperlinkOption::Never, true ) .to_string() ); } #[test] fn test_print_hyperlink() { let tmp_dir = tempdir().expect("failed to create temp dir"); let icons = Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()); // Create the file; let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); let meta = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap(); let colors = Colors::new(color::ThemeOption::NoColor); let real_path = std::fs::canonicalize(&file_path).expect("canonicalize"); let expected_url = Url::from_file_path(real_path).expect("absolute path"); let expected_text = format!( "\x1B]8;;{}\x1B\x5C{}\x1B]8;;\x1B\x5C", expected_url, "file.txt" ); assert_eq!( expected_text, meta.name .render( &colors, &icons, &DisplayOption::FileName, HyperlinkOption::Always, true ) .to_string() ); } #[test] fn test_extensions_with_valid_file() { let path = Path::new("some-file.txt"); let name = Name::new( path, FileType::File { uid: false, exec: false, }, ); assert_eq!(Some("txt"), name.extension()); } #[test] fn test_extensions_with_file_without_extension() { let path = Path::new(".gitignore"); let name = Name::new( path, FileType::File { uid: false, exec: false, }, ); assert_eq!(None, name.extension()); } #[test] fn test_order_impl_is_case_insensitive() { let path_1 = Path::new("/AAAA"); let name_1 = Name::new( path_1, FileType::File { uid: false, exec: false, }, ); let path_2 = Path::new("/aaaa"); let name_2 = Name::new( path_2, FileType::File { uid: false, exec: false, }, ); assert_eq!(Ordering::Equal, name_1.cmp(&name_2)); } #[test] fn test_partial_order_impl() { let path_a = Path::new("/aaaa"); let name_a = Name::new( path_a, FileType::File { uid: false, exec: false, }, ); let path_z = Path::new("/zzzz"); let name_z = Name::new( path_z, FileType::File { uid: false, exec: false, }, ); assert!(name_a < name_z); } #[test] fn test_partial_order_impl_is_case_insensitive() { let path_a = Path::new("aaaa"); let name_a = Name::new( path_a, FileType::File { uid: false, exec: false, }, ); let path_z = Path::new("ZZZZ"); let name_z = Name::new( path_z, FileType::File { uid: false, exec: false, }, ); assert!(name_a < name_z); } #[test] fn test_partial_eq_impl() { let path_1 = Path::new("aaaa"); let name_1 = Name::new( path_1, FileType::File { uid: false, exec: false, }, ); let path_2 = Path::new("aaaa"); let name_2 = Name::new( path_2, FileType::File { uid: false, exec: false, }, ); assert!(name_1 == name_2); } #[test] fn test_partial_eq_impl_is_case_insensitive() { let path_1 = Path::new("AAAA"); let name_1 = Name::new( path_1, FileType::File { uid: false, exec: false, }, ); let path_2 = Path::new("aaaa"); let name_2 = Name::new( path_2, FileType::File { uid: false, exec: false, }, ); assert!(name_1 == name_2); } #[test] fn test_parent_relative_path() { let name = Name::new( Path::new("/home/parent1/child"), FileType::File { uid: false, exec: false, }, ); let base_path = Path::new("/home/parent2"); assert_eq!( PathBuf::from("../parent1/child"), name.relative_path(base_path), ) } #[test] fn test_current_relative_path() { let name = Name::new( Path::new("/home/parent1/child"), FileType::File { uid: false, exec: false, }, ); let base_path = PathBuf::from("/home/parent1"); assert_eq!(PathBuf::from("child"), name.relative_path(base_path),) } #[test] fn test_grand_parent_relative_path() { let name = Name::new( Path::new("/home/grand-parent1/parent1/child"), FileType::File { uid: false, exec: false, }, ); let base_path = PathBuf::from("/home/grand-parent2/parent1"); assert_eq!( PathBuf::from("../../grand-parent1/parent1/child"), name.relative_path(base_path), ) } #[test] #[cfg(unix)] fn test_special_chars_in_filename() { let tmp_dir = tempdir().expect("failed to create temp dir"); let icons = Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()); // Create the file; let file_path = tmp_dir.path().join("file\ttab.txt"); File::create(&file_path).expect("failed to create file"); let meta = file_path.metadata().expect("failed to get metas"); let colors = Colors::new(color::ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); let name = Name::new(&file_path, file_type); assert_eq!( " file\\ttab.txt".to_string().with(Color::AnsiValue(184)), name.render( &colors, &icons, &DisplayOption::FileName, HyperlinkOption::Never, false, ) ); let file_path = tmp_dir.path().join("a$a.txt"); File::create(&file_path).expect("failed to create file"); let meta = file_path.metadata().expect("failed to get metas"); let colors = Colors::new(color::ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); let name = Name::new(&file_path, file_type); assert_eq!( " \'a$a.txt\'".to_string().with(Color::AnsiValue(184)), name.render( &colors, &icons, &DisplayOption::FileName, HyperlinkOption::Never, false, ) ); let file_path = tmp_dir.path().join(PathBuf::from("\\.txt")); File::create(&file_path).expect("failed to create file"); let meta = file_path.metadata().expect("failed to get metas"); let colors = Colors::new(color::ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); let name = Name::new(&file_path, file_type); assert_eq!( " \'\\.txt\'".to_string().with(Color::AnsiValue(184)), name.render( &colors, &icons, &DisplayOption::FileName, HyperlinkOption::Never, false, ) ); let file_path = tmp_dir.path().join("\"\'.txt"); File::create(&file_path).expect("failed to create file"); let meta = file_path.metadata().expect("failed to get metas"); let colors = Colors::new(color::ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); let name = Name::new(&file_path, file_type); assert_eq!( " \'\"\'\\\'\'.txt\'" .to_string() .with(Color::AnsiValue(184)), name.render( &colors, &icons, &DisplayOption::FileName, HyperlinkOption::Never, false, ) ); let file_path = tmp_dir.path().join("file\nnewline.txt"); File::create(&file_path).expect("failed to create file"); let meta = file_path.metadata().expect("failed to get metas"); let colors = Colors::new(color::ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); let name = Name::new(&file_path, file_type); assert_eq!( " file\\nnewline.txt" .to_string() .with(Color::AnsiValue(184)), name.render( &colors, &icons, &DisplayOption::FileName, HyperlinkOption::Never, false, ) ); } } 07070100000044000081A400000000000000000000000166C4C37900000D01000000000000000000000000000000000000001C00000000lsd-1.1.5/src/meta/owner.rsuse crate::color::{ColoredString, Colors, Elem}; use crate::Flags; #[cfg(unix)] use std::fs::Metadata; #[cfg(unix)] use users::{Groups, Users, UsersCache}; #[derive(Default)] pub struct Cache { #[cfg(unix)] users: UsersCache, #[cfg(unix)] groups: UsersCache, } #[cfg(unix)] #[derive(Clone, Debug, Default)] pub struct Owner { user: u32, group: u32, } #[cfg(windows)] #[derive(Clone, Debug, Default)] pub struct Owner { user: String, group: String, } impl Owner { #[cfg(windows)] pub fn new(user: String, group: String) -> Self { Self { user, group } } } #[cfg(unix)] impl From<&Metadata> for Owner { fn from(meta: &Metadata) -> Self { use std::os::unix::fs::MetadataExt; Self { user: meta.uid(), group: meta.gid(), } } } fn truncate(input: &str, after: Option<usize>, marker: Option<String>) -> String { let mut output = input.to_string(); if let Some(after) = after { if output.len() > after { output.truncate(after); if let Some(marker) = marker { output.push_str(&marker); } } } output } impl Owner { // allow unused variables because cache is used in unix, maybe we can cache for windows in the future #[allow(unused_variables)] pub fn render_user(&self, colors: &Colors, cache: &Cache, flags: &Flags) -> ColoredString { #[cfg(unix)] let user = &match cache.users.get_user_by_uid(self.user) { Some(user) => user.name().to_string_lossy().to_string(), None => self.user.to_string(), }; #[cfg(windows)] let user = &self.user; colors.colorize( truncate( user, flags.truncate_owner.after, flags.truncate_owner.marker.clone(), ), &Elem::User, ) } // allow unused variables because cache is used in unix, maybe we can cache for windows in the future #[allow(unused_variables)] pub fn render_group(&self, colors: &Colors, cache: &Cache, flags: &Flags) -> ColoredString { #[cfg(unix)] let group = &match cache.groups.get_group_by_gid(self.group) { Some(group) => group.name().to_string_lossy().to_string(), None => self.group.to_string(), }; #[cfg(windows)] let group = &self.group; colors.colorize( truncate( group, flags.truncate_owner.after, flags.truncate_owner.marker.clone(), ), &Elem::Group, ) } } #[cfg(test)] mod test_truncate { use crate::meta::owner::truncate; #[test] fn test_none() { assert_eq!("a", truncate("a", None, None)); } #[test] fn test_unchanged_without_marker() { assert_eq!("a", truncate("a", Some(1), None)); } #[test] fn test_unchanged_with_marker() { assert_eq!("a", truncate("a", Some(1), Some("…".to_string()))); } #[test] fn test_truncated_without_marker() { assert_eq!("a", truncate("ab", Some(1), None)); } #[test] fn test_truncated_with_marker() { assert_eq!("a…", truncate("ab", Some(1), Some("…".to_string()))); } } 07070100000045000081A400000000000000000000000166C4C37900002D94000000000000000000000000000000000000002200000000lsd-1.1.5/src/meta/permissions.rsuse crate::color::{ColoredString, Colors, Elem}; use crate::flags::{Flags, PermissionFlag}; use std::fs::Metadata; #[derive(Default, Debug, PartialEq, Eq, Copy, Clone)] pub struct Permissions { pub user_read: bool, pub user_write: bool, pub user_execute: bool, pub group_read: bool, pub group_write: bool, pub group_execute: bool, pub other_read: bool, pub other_write: bool, pub other_execute: bool, pub sticky: bool, pub setgid: bool, pub setuid: bool, } impl From<&Metadata> for Permissions { #[cfg(unix)] fn from(meta: &Metadata) -> Self { use std::os::unix::fs::PermissionsExt; let bits = meta.permissions().mode(); let has_bit = |bit| bits & bit == bit; Self { user_read: has_bit(modes::USER_READ), user_write: has_bit(modes::USER_WRITE), user_execute: has_bit(modes::USER_EXECUTE), group_read: has_bit(modes::GROUP_READ), group_write: has_bit(modes::GROUP_WRITE), group_execute: has_bit(modes::GROUP_EXECUTE), other_read: has_bit(modes::OTHER_READ), other_write: has_bit(modes::OTHER_WRITE), other_execute: has_bit(modes::OTHER_EXECUTE), sticky: has_bit(modes::STICKY), setgid: has_bit(modes::SETGID), setuid: has_bit(modes::SETUID), } } #[cfg(windows)] fn from(_: &Metadata) -> Self { panic!("Cannot get permissions from metadata on Windows") } } impl Permissions { fn bits_to_octal(r: bool, w: bool, x: bool) -> u8 { (r as u8) * 4 + (w as u8) * 2 + (x as u8) } pub fn render(&self, colors: &Colors, flags: &Flags) -> ColoredString { let bit = |bit, chr: &'static str, elem: &Elem| { if bit { colors.colorize(chr, elem) } else { colors.colorize('-', &Elem::NoAccess) } }; let res = match flags.permission { PermissionFlag::Rwx => [ // User permissions bit(self.user_read, "r", &Elem::Read), bit(self.user_write, "w", &Elem::Write), match (self.user_execute, self.setuid) { (false, false) => colors.colorize('-', &Elem::NoAccess), (true, false) => colors.colorize('x', &Elem::Exec), (false, true) => colors.colorize('S', &Elem::ExecSticky), (true, true) => colors.colorize('s', &Elem::ExecSticky), }, // Group permissions bit(self.group_read, "r", &Elem::Read), bit(self.group_write, "w", &Elem::Write), match (self.group_execute, self.setgid) { (false, false) => colors.colorize('-', &Elem::NoAccess), (true, false) => colors.colorize('x', &Elem::Exec), (false, true) => colors.colorize('S', &Elem::ExecSticky), (true, true) => colors.colorize('s', &Elem::ExecSticky), }, // Other permissions bit(self.other_read, "r", &Elem::Read), bit(self.other_write, "w", &Elem::Write), match (self.other_execute, self.sticky) { (false, false) => colors.colorize('-', &Elem::NoAccess), (true, false) => colors.colorize('x', &Elem::Exec), (false, true) => colors.colorize('T', &Elem::ExecSticky), (true, true) => colors.colorize('t', &Elem::ExecSticky), }, ] .into_iter() // From the experiment, the maximum string size is 153 bytes .fold(String::with_capacity(160), |mut acc, x| { acc.push_str(&x.to_string()); acc }), PermissionFlag::Octal => { let octals = [ Self::bits_to_octal(self.setuid, self.setgid, self.sticky), Self::bits_to_octal(self.user_read, self.user_write, self.user_execute), Self::bits_to_octal(self.group_read, self.group_write, self.group_execute), Self::bits_to_octal(self.other_read, self.other_write, self.other_execute), ] .into_iter() .fold(String::with_capacity(4), |mut acc, x| { acc.push( char::from_digit(x as u32, 8) .expect("octal value of permission should not be greater than 7"), ); acc }); colors.colorize(octals, &Elem::Octal).to_string() } // technically this should be an error, hmm PermissionFlag::Attributes => colors.colorize('-', &Elem::NoAccess).to_string(), PermissionFlag::Disable => colors.colorize('-', &Elem::NoAccess).to_string(), }; ColoredString::new(Colors::default_style(), res) } #[cfg(not(windows))] pub fn is_executable(&self) -> bool { self.user_execute || self.group_execute || self.other_execute } } // More readable aliases for the permission bits exposed by libc. #[allow(trivial_numeric_casts)] #[cfg(unix)] mod modes { pub type Mode = u32; // The `libc::mode_t` type’s actual type varies, but the value returned // from `metadata.permissions().mode()` is always `u32`. pub const USER_READ: Mode = libc::S_IRUSR as Mode; pub const USER_WRITE: Mode = libc::S_IWUSR as Mode; pub const USER_EXECUTE: Mode = libc::S_IXUSR as Mode; pub const GROUP_READ: Mode = libc::S_IRGRP as Mode; pub const GROUP_WRITE: Mode = libc::S_IWGRP as Mode; pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode; pub const OTHER_READ: Mode = libc::S_IROTH as Mode; pub const OTHER_WRITE: Mode = libc::S_IWOTH as Mode; pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode; pub const STICKY: Mode = libc::S_ISVTX as Mode; pub const SETGID: Mode = libc::S_ISGID as Mode; pub const SETUID: Mode = libc::S_ISUID as Mode; } #[cfg(unix)] #[cfg(test)] mod test { use super::Flags; use super::{PermissionFlag, Permissions}; use crate::color::{Colors, ThemeOption}; use std::fs; use std::fs::File; use std::os::unix::fs::PermissionsExt; use tempfile::tempdir; #[test] fn permission_rwx() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); fs::set_permissions(&file_path, fs::Permissions::from_mode(0o655)) .expect("unable to set permissions to file"); let meta = file_path.metadata().expect("failed to get meta"); let colors = Colors::new(ThemeOption::NoColor); let perms = Permissions::from(&meta); assert_eq!( "rw-r-xr-x", perms.render(&colors, &Flags::default()).content() ); } #[test] fn permission_rwx2() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); fs::set_permissions(&file_path, fs::Permissions::from_mode(0o777)) .expect("unable to set permissions to file"); let meta = file_path.metadata().expect("failed to get meta"); let colors = Colors::new(ThemeOption::NoColor); let perms = Permissions::from(&meta); assert_eq!( "rwxrwxrwx", perms.render(&colors, &Flags::default()).content() ); } #[test] fn permission_rwx_sticky() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); fs::set_permissions(&file_path, fs::Permissions::from_mode(0o1777)) .expect("unable to set permissions to file"); let meta = file_path.metadata().expect("failed to get meta"); let colors = Colors::new(ThemeOption::NoColor); let flags = Flags { permission: PermissionFlag::Rwx, ..Default::default() }; let perms = Permissions::from(&meta); assert_eq!("rwxrwxrwt", perms.render(&colors, &flags).content()); } #[test] fn permission_octal() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); fs::set_permissions(&file_path, fs::Permissions::from_mode(0o655)) .expect("unable to set permissions to file"); let meta = file_path.metadata().expect("failed to get meta"); let colors = Colors::new(ThemeOption::NoColor); let flags = Flags { permission: PermissionFlag::Octal, ..Default::default() }; let perms = Permissions::from(&meta); assert_eq!("0655", perms.render(&colors, &flags).content()); } #[test] fn permission_octal2() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); fs::set_permissions(&file_path, fs::Permissions::from_mode(0o777)) .expect("unable to set permissions to file"); let meta = file_path.metadata().expect("failed to get meta"); let colors = Colors::new(ThemeOption::NoColor); let flags = Flags { permission: PermissionFlag::Octal, ..Default::default() }; let perms = Permissions::from(&meta); assert_eq!("0777", perms.render(&colors, &flags).content()); } #[test] fn permission_octal_sticky() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); fs::set_permissions(&file_path, fs::Permissions::from_mode(0o1777)) .expect("unable to set permissions to file"); let meta = file_path.metadata().expect("failed to get meta"); let colors = Colors::new(ThemeOption::NoColor); let flags = Flags { permission: PermissionFlag::Octal, ..Default::default() }; let perms = Permissions::from(&meta); assert_eq!("1777", perms.render(&colors, &flags).content()); } #[test] fn permission_disable() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); fs::set_permissions(&file_path, fs::Permissions::from_mode(0o655)) .expect("unable to set permissions to file"); let meta = file_path.metadata().expect("failed to get meta"); let colors = Colors::new(ThemeOption::NoColor); let flags = Flags { permission: PermissionFlag::Disable, ..Default::default() }; let perms = Permissions::from(&meta); assert_eq!("-", perms.render(&colors, &flags).content()); } } 07070100000046000081A400000000000000000000000166C4C379000002D5000000000000000000000000000000000000003000000000lsd-1.1.5/src/meta/permissions_or_attributes.rs#[cfg(windows)] use super::windows_attributes::WindowsAttributes; use crate::{ color::{ColoredString, Colors}, flags::Flags, }; use super::Permissions; #[derive(Clone, Debug)] pub enum PermissionsOrAttributes { Permissions(Permissions), #[cfg(windows)] WindowsAttributes(WindowsAttributes), } impl PermissionsOrAttributes { pub fn render(&self, colors: &Colors, flags: &Flags) -> ColoredString { match self { PermissionsOrAttributes::Permissions(permissions) => permissions.render(colors, flags), #[cfg(windows)] PermissionsOrAttributes::WindowsAttributes(attributes) => { attributes.render(colors, flags) } } } } 07070100000047000081A400000000000000000000000166C4C379000026E6000000000000000000000000000000000000001B00000000lsd-1.1.5/src/meta/size.rsuse crate::color::{ColoredString, Colors, Elem}; use crate::flags::{Flags, SizeFlag}; use std::fs::Metadata; const KB: u64 = 1024; const MB: u64 = 1024_u64.pow(2); const GB: u64 = 1024_u64.pow(3); const TB: u64 = 1024_u64.pow(4); #[derive(Clone, Debug, PartialEq, Eq)] pub enum Unit { Byte, Kilo, Mega, Giga, Tera, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Size { bytes: u64, } impl From<&Metadata> for Size { fn from(meta: &Metadata) -> Self { Self { bytes: meta.len() } } } impl Size { pub fn new(bytes: u64) -> Self { Self { bytes } } pub fn get_bytes(&self) -> u64 { self.bytes } fn format_size(&self, number: f64) -> String { format!("{0:.1$}", number, if number < 10.0 { 1 } else { 0 }) } fn get_unit(&self, flags: &Flags) -> Unit { if flags.size == SizeFlag::Bytes { return Unit::Byte; } match self.bytes { b if b < KB => Unit::Byte, b if b < MB => Unit::Kilo, b if b < GB => Unit::Mega, b if b < TB => Unit::Giga, _ => Unit::Tera, } } pub fn render( &self, colors: &Colors, flags: &Flags, val_alignment: Option<usize>, ) -> ColoredString { let val_content = self.render_value(colors, flags); let unit_content = self.render_unit(colors, flags); let left_pad = if let Some(align) = val_alignment { " ".repeat(align - val_content.content().len()) } else { "".to_string() }; let mut strings: Vec<ColoredString> = vec![ ColoredString::new(Colors::default_style(), left_pad), val_content, ]; if flags.size != SizeFlag::Short { strings.push(ColoredString::new(Colors::default_style(), " ".into())); } strings.push(unit_content); let res = strings .into_iter() .map(|s| s.to_string()) .collect::<Vec<String>>() .join(""); ColoredString::new(Colors::default_style(), res) } fn paint(&self, colors: &Colors, content: String) -> ColoredString { let bytes = self.get_bytes(); let elem = if bytes >= GB { &Elem::FileLarge } else if bytes >= MB { &Elem::FileMedium } else { &Elem::FileSmall }; colors.colorize(content, elem) } pub fn render_value(&self, colors: &Colors, flags: &Flags) -> ColoredString { let content = self.value_string(flags); self.paint(colors, content) } pub fn value_string(&self, flags: &Flags) -> String { let unit = self.get_unit(flags); match unit { Unit::Byte => self.bytes.to_string(), Unit::Kilo => self.format_size(((self.bytes as f64 / KB as f64) * 10.0).round() / 10.0), Unit::Mega => self.format_size(((self.bytes as f64 / MB as f64) * 10.0).round() / 10.0), Unit::Giga => self.format_size(((self.bytes as f64 / GB as f64) * 10.0).round() / 10.0), Unit::Tera => self.format_size(((self.bytes as f64 / TB as f64) * 10.0).round() / 10.0), } } pub fn render_unit(&self, colors: &Colors, flags: &Flags) -> ColoredString { let content = self.unit_string(flags); self.paint(colors, content) } pub fn unit_string(&self, flags: &Flags) -> String { let unit = self.get_unit(flags); match flags.size { SizeFlag::Default => match unit { Unit::Byte => String::from('B'), Unit::Kilo => String::from("KB"), Unit::Mega => String::from("MB"), Unit::Giga => String::from("GB"), Unit::Tera => String::from("TB"), }, SizeFlag::Short => match unit { Unit::Byte => String::from('B'), Unit::Kilo => String::from('K'), Unit::Mega => String::from('M'), Unit::Giga => String::from('G'), Unit::Tera => String::from('T'), }, SizeFlag::Bytes => String::from(""), } } } #[cfg(test)] mod test { use super::{Size, GB, KB, MB, TB}; use crate::color::{Colors, ThemeOption}; use crate::flags::{Flags, SizeFlag}; #[test] fn render_byte() { let size = Size::new(42); // == 42 bytes let mut flags = Flags::default(); assert_eq!(size.value_string(&flags), "42"); assert_eq!(size.unit_string(&flags), "B"); flags.size = SizeFlag::Short; assert_eq!(size.unit_string(&flags), "B"); flags.size = SizeFlag::Bytes; assert_eq!(size.unit_string(&flags), ""); } #[test] fn render_10_minus_kilobyte() { let size = Size::new(4 * KB); // 4 kilobytes let mut flags = Flags::default(); assert_eq!(size.value_string(&flags), "4.0"); assert_eq!(size.unit_string(&flags), "KB"); flags.size = SizeFlag::Short; assert_eq!(size.unit_string(&flags), "K"); } #[test] fn render_kilobyte() { let size = Size::new(42 * KB); // 42 kilobytes let mut flags = Flags::default(); assert_eq!(size.value_string(&flags), "42"); assert_eq!(size.unit_string(&flags), "KB"); flags.size = SizeFlag::Short; assert_eq!(size.unit_string(&flags), "K"); } #[test] fn render_100_plus_kilobyte() { let size = Size::new(420 * KB + 420); // 420.4 kilobytes let mut flags = Flags::default(); assert_eq!(size.value_string(&flags), "420"); assert_eq!(size.unit_string(&flags), "KB"); flags.size = SizeFlag::Short; assert_eq!(size.unit_string(&flags), "K"); } #[test] fn render_10_minus_megabyte() { let size = Size::new(4 * MB); // 4 megabytes let mut flags = Flags::default(); assert_eq!(size.value_string(&flags), "4.0"); assert_eq!(size.unit_string(&flags), "MB"); flags.size = SizeFlag::Short; assert_eq!(size.unit_string(&flags), "M"); } #[test] fn render_megabyte() { let size = Size::new(42 * MB); // 42 megabytes let mut flags = Flags::default(); assert_eq!(size.value_string(&flags), "42"); assert_eq!(size.unit_string(&flags), "MB"); flags.size = SizeFlag::Short; assert_eq!(size.unit_string(&flags), "M"); } #[test] fn render_100_plus_megabyte() { let size = Size::new(420 * MB + 420 * KB); // 420.4 megabytes let mut flags = Flags::default(); assert_eq!(size.value_string(&flags), "420"); assert_eq!(size.unit_string(&flags), "MB"); flags.size = SizeFlag::Short; assert_eq!(size.unit_string(&flags), "M"); } #[test] fn render_10_minus_gigabyte() { let size = Size::new(4 * GB); // 4 gigabytes let mut flags = Flags::default(); assert_eq!(size.value_string(&flags), "4.0"); assert_eq!(size.unit_string(&flags), "GB"); flags.size = SizeFlag::Short; assert_eq!(size.unit_string(&flags), "G"); } #[test] fn render_gigabyte() { let size = Size::new(42 * GB); // 42 gigabytes let mut flags = Flags::default(); assert_eq!(size.value_string(&flags), "42"); assert_eq!(size.unit_string(&flags), "GB"); flags.size = SizeFlag::Short; assert_eq!(size.unit_string(&flags), "G"); } #[test] fn render_100_plus_gigabyte() { let size = Size::new(420 * GB + 420 * MB); // 420.4 gigabytes let mut flags = Flags::default(); assert_eq!(size.value_string(&flags), "420"); assert_eq!(size.unit_string(&flags), "GB"); flags.size = SizeFlag::Short; assert_eq!(size.unit_string(&flags), "G"); } #[test] fn render_10_minus_terabyte() { let size = Size::new(4 * TB); // 4 terabytes let mut flags = Flags::default(); assert_eq!(size.value_string(&flags), "4.0"); assert_eq!(size.unit_string(&flags), "TB"); flags.size = SizeFlag::Short; assert_eq!(size.unit_string(&flags), "T"); } #[test] fn render_terabyte() { let size = Size::new(42 * TB); // 42 terabytes let mut flags = Flags::default(); assert_eq!(size.value_string(&flags), "42"); assert_eq!(size.unit_string(&flags), "TB"); flags.size = SizeFlag::Short; assert_eq!(size.unit_string(&flags), "T"); } #[test] fn render_100_plus_terabyte() { let size = Size::new(420 * TB + 420 * GB); // 420.4 terabytes let mut flags = Flags::default(); assert_eq!(size.value_string(&flags), "420"); assert_eq!(size.unit_string(&flags), "TB"); flags.size = SizeFlag::Short; assert_eq!(size.unit_string(&flags), "T"); } #[test] fn render_with_a_fraction() { let size = Size::new(42 * KB + 103); // 42.1 kilobytes let flags = Flags::default(); assert_eq!(size.value_string(&flags), "42"); assert_eq!(size.unit_string(&flags), "KB"); } #[test] fn render_with_a_truncated_fraction() { let size = Size::new(42 * KB + 1); // 42.001 kilobytes == 42 kilobytes let flags = Flags::default(); assert_eq!(size.value_string(&flags), "42"); assert_eq!(size.unit_string(&flags), "KB"); } #[test] fn render_short_nospaces() { let size = Size::new(42 * KB); // 42 kilobytes let flags = Flags { size: SizeFlag::Short, ..Default::default() }; let colors = Colors::new(ThemeOption::NoColor); assert_eq!(size.render(&colors, &flags, Some(2)).to_string(), "42K"); assert_eq!(size.render(&colors, &flags, Some(3)).to_string(), " 42K"); } } 07070100000048000081A400000000000000000000000166C4C37900000FAD000000000000000000000000000000000000001E00000000lsd-1.1.5/src/meta/symlink.rsuse crate::color::{ColoredString, Colors, Elem}; use crate::flags::Flags; use std::fs::read_link; use std::path::Path; #[derive(Clone, Debug)] pub struct SymLink { target: Option<String>, valid: bool, } impl From<&Path> for SymLink { fn from(path: &Path) -> Self { if let Ok(target) = read_link(path) { if target.is_absolute() || path.parent().is_none() { return Self { valid: target.exists(), target: Some( target .to_str() .expect("failed to convert symlink to str") .to_string(), ), }; } return Self { target: Some( target .to_str() .expect("failed to convert symlink to str") .to_string(), ), valid: path.parent().unwrap().join(target).exists(), }; } Self { target: None, valid: false, } } } impl SymLink { pub fn symlink_string(&self) -> Option<String> { self.target.as_ref().map(|target| target.to_string()) } pub fn render(&self, colors: &Colors, flag: &Flags) -> ColoredString { if let Some(target_string) = self.symlink_string() { let elem = if self.valid { &Elem::SymLink } else { &Elem::MissingSymLinkTarget }; let strings: &[ColoredString] = &[ ColoredString::new(Colors::default_style(), format!(" {} ", flag.symlink_arrow)), // ⇒ \u{21d2} colors.colorize(target_string, elem), ]; let res = strings .iter() .map(|s| s.to_string()) .collect::<Vec<String>>() .join(""); ColoredString::new(Colors::default_style(), res) } else { ColoredString::new(Colors::default_style(), "".into()) } } } #[cfg(test)] mod tests { use clap::Parser; use super::SymLink; use crate::app::Cli; use crate::color::{Colors, ThemeOption}; use crate::config_file::Config; use crate::flags::Flags; #[test] fn test_symlink_render_default_valid_target_nocolor() { let link = SymLink { target: Some("/target".to_string()), valid: true, }; let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!( format!("{}", " ⇒ /target"), link.render( &Colors::new(ThemeOption::NoColor), &Flags::configure_from(&cli, &Config::with_none()).unwrap() ) .to_string() ); } #[test] fn test_symlink_render_default_invalid_target_nocolor() { let link = SymLink { target: Some("/target".to_string()), valid: false, }; let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!( format!("{}", " ⇒ /target"), link.render( &Colors::new(ThemeOption::NoColor), &Flags::configure_from(&cli, &Config::with_none()).unwrap() ) .to_string() ); } #[test] fn test_symlink_render_default_invalid_target_withcolor() { let link = SymLink { target: Some("/target".to_string()), valid: false, }; let argv = ["lsd"]; let cli = Cli::try_parse_from(argv).unwrap(); assert_eq!( format!("{}", " ⇒ \u{1b}[38;5;124m/target\u{1b}[39m"), link.render( &Colors::new(ThemeOption::NoLscolors), &Flags::configure_from(&cli, &Config::with_none()).unwrap() ) .to_string() ); } } 07070100000049000081A400000000000000000000000166C4C37900000EC5000000000000000000000000000000000000002900000000lsd-1.1.5/src/meta/windows_attributes.rsuse crate::{ color::{ColoredString, Colors, Elem}, flags::Flags, }; use std::os::windows::fs::MetadataExt; #[derive(Debug, Clone)] pub struct WindowsAttributes { pub archive: bool, pub readonly: bool, pub hidden: bool, pub system: bool, } pub fn get_attributes(metadata: &std::fs::Metadata) -> WindowsAttributes { use windows::Win32::Storage::FileSystem::{ FILE_ATTRIBUTE_ARCHIVE, FILE_ATTRIBUTE_HIDDEN, FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_SYSTEM, FILE_FLAGS_AND_ATTRIBUTES, }; let bits = metadata.file_attributes(); let has_bit = |bit: FILE_FLAGS_AND_ATTRIBUTES| bits & bit.0 == bit.0; // https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants WindowsAttributes { archive: has_bit(FILE_ATTRIBUTE_ARCHIVE), readonly: has_bit(FILE_ATTRIBUTE_READONLY), hidden: has_bit(FILE_ATTRIBUTE_HIDDEN), system: has_bit(FILE_ATTRIBUTE_SYSTEM), } } impl WindowsAttributes { pub fn render(&self, colors: &Colors, _flags: &Flags) -> ColoredString { let res = [ match self.archive { true => colors.colorize("a", &Elem::Archive), false => colors.colorize('-', &Elem::NoAccess), }, match self.readonly { true => colors.colorize("r", &Elem::AttributeRead), false => colors.colorize('-', &Elem::NoAccess), }, match self.hidden { true => colors.colorize("h", &Elem::Hidden), false => colors.colorize('-', &Elem::NoAccess), }, match self.system { true => colors.colorize("s", &Elem::System), false => colors.colorize('-', &Elem::NoAccess), }, ] .into_iter() .fold(String::with_capacity(4), |mut acc, x| { acc.push_str(&x.to_string()); acc }); ColoredString::new(Colors::default_style(), res) } } #[cfg(test)] mod test { use std::fs; use std::io::Write; use std::process::Command; use crate::{ color::{Colors, ThemeOption}, flags::Flags, }; use super::get_attributes; use tempfile::tempdir; #[test] pub fn archived_file() { let attribute_string = create_and_process_file_with_attributes("archived_file.txt", "+A"); assert_eq!("a---", attribute_string); } #[test] pub fn readonly_file() { let attribute_string = create_and_process_file_with_attributes("readonly_file.txt", "+R"); assert_eq!("ar--", attribute_string); } #[test] pub fn hidden_file() { let attribute_string = create_and_process_file_with_attributes("hidden_file.txt", "+H"); assert_eq!("a-h-", attribute_string); } #[test] pub fn system_file() { let attribute_string = create_and_process_file_with_attributes("system_file.txt", "+S"); assert_eq!("a--s", attribute_string); } fn create_and_process_file_with_attributes(name: &str, attrs: &str) -> String { let tmp_dir = tempdir().expect("failed to create temp dir"); let path = tmp_dir.path().join(name); let mut file = fs::File::create(path.clone()).unwrap(); writeln!(file, "Test content").unwrap(); Command::new("attrib") .arg(attrs) .arg(&path) .output() .expect("able to set attributes"); let metadata = file.metadata().expect("able to get metadata"); let colors = Colors::new(ThemeOption::NoColor); let attributes = get_attributes(&metadata); attributes .render(&colors, &Flags::default()) .content() .to_string() } } 0707010000004A000081A400000000000000000000000166C4C379000034E7000000000000000000000000000000000000002400000000lsd-1.1.5/src/meta/windows_utils.rsuse std::ffi::{OsStr, OsString}; use std::io; use std::mem::MaybeUninit; use std::os::windows::ffi::{OsStrExt, OsStringExt}; use std::path::Path; use windows::Win32::Foundation::PSID; use windows::Win32::Security::{self, Authorization::TRUSTEE_W, ACL}; use super::{Owner, Permissions}; const BUF_SIZE: u32 = 256; pub fn get_file_data(path: &Path) -> Result<(Owner, Permissions), io::Error> { // Overall design: // This function allocates some data with GetNamedSecurityInfoW, // manipulates it only through WinAPI calls (treating the pointers as // opaque) and then frees it at the end with LocalFree. // // For memory safety, the critical things are: // - No pointer is valid before the return value of GetNamedSecurityInfoW // is checked // - LocalFree must be called before returning // - No pointer is valid after the call to LocalFree let windows_path = buf_from_os(path.as_os_str()); // These pointers will be populated by GetNamedSecurityInfoW // sd_ptr points at a new buffer that must be freed // The others point at (opaque) things inside that buffer let mut owner_sid_ptr = MaybeUninit::uninit(); let mut group_sid_ptr = MaybeUninit::uninit(); let mut dacl_ptr = MaybeUninit::uninit(); let mut sd_ptr = MaybeUninit::uninit(); // Assumptions: // - windows_path is a null-terminated WTF-16-encoded string // - The return value is checked against ERROR_SUCCESS before pointers are used // - All pointers are opaque and should only be used with WinAPI calls // - Pointers are only valid if their corresponding X_SECURITY_INFORMATION // flags are set // - sd_ptr must be freed with LocalFree let error_code = unsafe { Security::Authorization::GetNamedSecurityInfoW( windows::core::PCWSTR::from_raw(windows_path.as_ptr()), Security::Authorization::SE_FILE_OBJECT, Security::OWNER_SECURITY_INFORMATION | Security::GROUP_SECURITY_INFORMATION | Security::DACL_SECURITY_INFORMATION, Some(owner_sid_ptr.as_mut_ptr()), Some(group_sid_ptr.as_mut_ptr()), Some(dacl_ptr.as_mut_ptr()), None, sd_ptr.as_mut_ptr(), ) }; if error_code.is_err() { return Err(std::io::Error::from_raw_os_error(error_code.0 as i32)); } // Assumptions: // - owner_sid_ptr is valid // - group_sid_ptr is valid // (both OK because GetNamedSecurityInfoW returned success) let owner_sid_ptr = unsafe { owner_sid_ptr.assume_init() }; let group_sid_ptr = unsafe { group_sid_ptr.assume_init() }; let dacl_ptr = unsafe { dacl_ptr.assume_init() }; let sd_ptr = unsafe { sd_ptr.assume_init() }; let owner = match unsafe { lookup_account_sid(owner_sid_ptr) } { Ok((n, d)) => { let owner_name = os_from_buf(&n); let owner_domain = os_from_buf(&d); format!( "{}\\{}", owner_domain.to_string_lossy(), &owner_name.to_string_lossy() ) } Err(_) => String::from("-"), }; let group = match unsafe { lookup_account_sid(group_sid_ptr) } { Ok((n, d)) => { let group_name = os_from_buf(&n); let group_domain = os_from_buf(&d); format!( "{}\\{}", group_domain.to_string_lossy(), &group_name.to_string_lossy() ) } Err(_) => String::from("-"), }; // This structure will be returned let owner = Owner::new(owner, group); // Get the size and allocate bytes for a 1-sub-authority SID // 1 sub-authority because the Windows World SID is always S-1-1-0, with // only a single sub-authority. // // Assumptions: None // "This function cannot fail" // -- Windows Dev Center docs let mut world_sid_len: u32 = unsafe { Security::GetSidLengthRequired(1) }; let mut world_sid = vec![0u8; world_sid_len as usize]; let world_sid_ptr = PSID(world_sid.as_mut_ptr() as *mut _); // Assumptions: // - world_sid_len is no larger than the number of bytes available at // world_sid // - world_sid is appropriately aligned (if there are strange crashes this // might be why) let result = unsafe { Security::CreateWellKnownSid( Security::WinWorldSid, PSID::default(), world_sid_ptr, &mut world_sid_len, ) }; if result.ok().is_err() { // Failed to create the SID // Assumptions: Same as the other identical calls unsafe { windows::Win32::System::Memory::LocalFree(sd_ptr.0 as _); } // Assumptions: None (GetLastError shouldn't ever fail) return Err(io::Error::from_raw_os_error(unsafe { windows::Win32::Foundation::GetLastError().0 } as i32)); } // Assumptions: // - xxxxx_sid_ptr are valid pointers to SIDs // - xxxxx_trustee is only valid as long as its SID pointer is let owner_trustee = unsafe { trustee_from_sid(owner_sid_ptr) }; let group_trustee = unsafe { trustee_from_sid(group_sid_ptr) }; let world_trustee = unsafe { trustee_from_sid(world_sid_ptr) }; // Assumptions: // - xxxxx_trustee are still valid (including underlying SID) // - dacl_ptr is still valid let owner_access_mask = unsafe { get_acl_access_mask(dacl_ptr, &owner_trustee) }?; let group_access_mask = unsafe { get_acl_access_mask(dacl_ptr, &group_trustee) }?; let world_access_mask = unsafe { get_acl_access_mask(dacl_ptr, &world_trustee) }?; let permissions = { use windows::Win32::Storage::FileSystem::{ FILE_ACCESS_FLAGS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE, }; let has_bit = |field: u32, bit: FILE_ACCESS_FLAGS| field & bit.0 != 0; Permissions { user_read: has_bit(owner_access_mask, FILE_GENERIC_READ), user_write: has_bit(owner_access_mask, FILE_GENERIC_WRITE), user_execute: has_bit(owner_access_mask, FILE_GENERIC_EXECUTE), group_read: has_bit(group_access_mask, FILE_GENERIC_READ), group_write: has_bit(group_access_mask, FILE_GENERIC_WRITE), group_execute: has_bit(group_access_mask, FILE_GENERIC_EXECUTE), other_read: has_bit(world_access_mask, FILE_GENERIC_READ), other_write: has_bit(world_access_mask, FILE_GENERIC_WRITE), other_execute: has_bit(world_access_mask, FILE_GENERIC_EXECUTE), sticky: false, setuid: false, setgid: false, } }; // Assumptions: // - sd_ptr was previously allocated with WinAPI functions // - All pointers into the memory are now invalid // - The free succeeds (currently unchecked -- there's no real recovery // options. It's not much memory, so leaking it on failure is // *probably* fine) unsafe { windows::Win32::System::Memory::LocalFree(sd_ptr.0 as _); } Ok((owner, permissions)) } /// Evaluate an ACL for a particular trustee and get its access rights /// /// Assumptions: /// - acl_ptr points to a valid ACL data structure /// - trustee_ptr points to a valid trustee data structure /// - Both remain valid through the function call (no long-term requirement) unsafe fn get_acl_access_mask( acl_ptr: *const ACL, trustee_ptr: *const TRUSTEE_W, ) -> Result<u32, io::Error> { let mut access_mask = 0; // Assumptions: // - All function assumptions // - Result is not valid until return value is checked let err_code = Security::Authorization::GetEffectiveRightsFromAclW(acl_ptr, trustee_ptr, &mut access_mask); if err_code.is_ok() { Ok(access_mask) } else { Err(io::Error::from_raw_os_error(err_code.0 as i32)) } } /// Get a trustee buffer from a SID /// /// Assumption: sid is valid, and the trustee is only valid as long as the SID /// is /// /// Note: winapi's TRUSTEE_W looks different from the one in the MS docs because /// of some unusual pre-processor macros in the original .h file. The winapi /// version is correct (MS's doc generator messed up) unsafe fn trustee_from_sid<P: Into<PSID>>(sid_ptr: P) -> TRUSTEE_W { let mut trustee = TRUSTEE_W::default(); Security::Authorization::BuildTrusteeWithSidW(&mut trustee, sid_ptr); trustee } /// Get a username and domain name from a SID /// /// Assumption: sid is a valid pointer that remains valid through the entire /// function execution /// /// Returns null-terminated Vec's, one for the name and one for the domain. unsafe fn lookup_account_sid(sid: PSID) -> Result<(Vec<u16>, Vec<u16>), std::io::Error> { let mut name_size: u32 = BUF_SIZE; let mut domain_size: u32 = BUF_SIZE; loop { let mut name: Vec<u16> = vec![0; name_size as usize]; let mut domain: Vec<u16> = vec![0; domain_size as usize]; let old_name_size = name_size; let old_domain_size = domain_size; let mut sid_name_use = MaybeUninit::uninit(); // Assumptions: // - sid is a valid pointer to a SID data structure // - name_size and domain_size accurately reflect the sizes let result = Security::LookupAccountSidW( None, sid, windows::core::PWSTR(name.as_mut_ptr()), &mut name_size, windows::core::PWSTR(domain.as_mut_ptr()), &mut domain_size, sid_name_use.as_mut_ptr(), ); if result.ok().is_ok() { // Success! return Ok((name, domain)); } else if name_size != old_name_size || domain_size != old_domain_size { // Need bigger buffers // name_size and domain_size are already set, just loop continue; } else { // Unknown account and or system domain identification // Possibly foreign item originating from another machine // TODO: Calculate permissions since it has to be possible if Explorer knows. return Err(io::Error::from_raw_os_error( windows::Win32::Foundation::GetLastError().0 as i32, )); } } } /// Create an `OsString` from a NUL-terminated buffer /// /// Decodes the WTF-16 encoded buffer until it hits a NUL (code point 0). /// Everything after and including that code point is not included. fn os_from_buf(buf: &[u16]) -> OsString { OsString::from_wide( &buf.iter() .cloned() .take_while(|&n| n != 0) .collect::<Vec<u16>>(), ) } /// Create a WTF-16-encoded NUL-terminated buffer from an `OsStr`. /// /// Decodes the `OsStr`, then appends a NUL. fn buf_from_os(os: &OsStr) -> Vec<u16> { let mut buf: Vec<u16> = os.encode_wide().collect(); buf.push(0); buf } /// Checks whether the given [`FILE_FLAGS_AND_ATTRIBUTES`] are set for the given /// [`Path`] /// /// [`FILE_FLAGS_AND_ATTRIBUTES`]: windows::Win32::Storage::FileSystem::FILE_FLAGS_AND_ATTRIBUTES #[inline] fn has_path_attribute( path: &Path, flags: windows::Win32::Storage::FileSystem::FILE_FLAGS_AND_ATTRIBUTES, ) -> bool { let windows_path = buf_from_os(path.as_os_str()); let file_attributes = unsafe { windows::Win32::Storage::FileSystem::GetFileAttributesW(windows::core::PCWSTR( windows_path.as_ptr(), )) }; file_attributes & flags.0 > 0 } /// Checks whether the windows [`hidden`] attribute is set for the given /// [`Path`] /// /// [`hidden`]: windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_HIDDEN pub fn is_path_hidden(path: &Path) -> bool { has_path_attribute( path, windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_HIDDEN, ) } /// Checks whether the windows [`system`] attribute is set for the given /// [`Path`] /// /// [`system`]: windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_SYSTEM pub fn is_path_system(path: &Path) -> bool { has_path_attribute( path, windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_SYSTEM, ) } #[cfg(test)] mod test { use super::*; #[test] fn basic_wtf16_behavior() { let basic_os = OsString::from("TeSt"); let basic_buf = vec![0x54, 0x65, 0x53, 0x74, 0x00]; let basic_buf_nuls = vec![0x54, 0x65, 0x53, 0x74, 0x00, 0x00, 0x00, 0x00]; assert_eq!(os_from_buf(&basic_buf), basic_os); assert_eq!(buf_from_os(&basic_os), basic_buf); assert_eq!(os_from_buf(&basic_buf_nuls), basic_os); let unicode_os = OsString::from("💩"); let unicode_buf = vec![0xd83d, 0xdca9, 0x0]; let unicode_buf_nuls = vec![0xd83d, 0xdca9, 0x0, 0x0, 0x0, 0x0, 0x0]; assert_eq!(os_from_buf(&unicode_buf), unicode_os); assert_eq!(buf_from_os(&unicode_os), unicode_buf); assert_eq!(os_from_buf(&unicode_buf_nuls), unicode_os); } #[test] fn every_wtf16_codepair_roundtrip() { for lsb in 0..256u16 { let mut vec: Vec<u16> = Vec::with_capacity(257); for msb in 0..=256u16 { let val = msb << 8 | lsb; if val != 0 { vec.push(val) } } vec.push(0); let os = os_from_buf(&vec); let new_vec = buf_from_os(&os); assert_eq!(&vec, &new_vec); } } } 0707010000004B000081A400000000000000000000000166C4C37900003D11000000000000000000000000000000000000001600000000lsd-1.1.5/src/sort.rsuse crate::flags::{DirGrouping, Flags, SortColumn, SortOrder}; use crate::meta::Meta; use std::cmp::Ordering; use vsort::compare; pub type SortFn = fn(&Meta, &Meta) -> Ordering; pub fn assemble_sorters(flags: &Flags) -> Vec<(SortOrder, SortFn)> { let mut sorters: Vec<(SortOrder, SortFn)> = vec![]; match flags.sorting.dir_grouping { DirGrouping::First => { sorters.push((SortOrder::Default, with_dirs_first)); } DirGrouping::Last => { sorters.push((SortOrder::Reverse, with_dirs_first)); } DirGrouping::None => {} }; match flags.sorting.column { SortColumn::Name => sorters.push((flags.sorting.order, by_name)), SortColumn::Size => sorters.push((flags.sorting.order, by_size)), SortColumn::Time => sorters.push((flags.sorting.order, by_date)), SortColumn::Version => sorters.push((flags.sorting.order, by_version)), SortColumn::Extension => sorters.push((flags.sorting.order, by_extension)), SortColumn::GitStatus => sorters.push((flags.sorting.order, by_git_status)), SortColumn::None => {} } sorters } pub fn by_meta(sorters: &[(SortOrder, SortFn)], a: &Meta, b: &Meta) -> Ordering { for (direction, sorter) in sorters.iter() { match (sorter)(a, b) { Ordering::Equal => continue, ordering => { return match direction { SortOrder::Reverse => ordering.reverse(), SortOrder::Default => ordering, } } } } Ordering::Equal } fn with_dirs_first(a: &Meta, b: &Meta) -> Ordering { b.file_type.is_dirlike().cmp(&a.file_type.is_dirlike()) } fn by_size(a: &Meta, b: &Meta) -> Ordering { match (&a.size, &b.size) { (Some(a_size), Some(b_size)) => b_size.get_bytes().cmp(&a_size.get_bytes()), (Some(_), None) => Ordering::Greater, (None, Some(_)) => Ordering::Less, (None, None) => Ordering::Equal, } } fn by_name(a: &Meta, b: &Meta) -> Ordering { a.name.cmp(&b.name) } fn by_date(a: &Meta, b: &Meta) -> Ordering { b.date.cmp(&a.date).then(a.name.cmp(&b.name)) } fn by_version(a: &Meta, b: &Meta) -> Ordering { compare(&a.name.name, &b.name.name) } fn by_extension(a: &Meta, b: &Meta) -> Ordering { a.name.extension().cmp(&b.name.extension()) } fn by_git_status(a: &Meta, b: &Meta) -> Ordering { a.git_status.cmp(&b.git_status) } #[cfg(test)] mod tests { use super::*; use crate::flags::{Flags, PermissionFlag}; use std::fs::{create_dir, File}; use std::io::prelude::*; use std::process::Command; use tempfile::tempdir; #[test] fn test_sort_assemble_sorters_by_name_with_dirs_first() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let path_a = tmp_dir.path().join("zzz"); File::create(&path_a).expect("failed to create file"); let meta_a = Meta::from_path(&path_a, false, PermissionFlag::Rwx).expect("failed to get meta"); // Create a dir; let path_z = tmp_dir.path().join("aaa"); create_dir(&path_z).expect("failed to create dir"); let meta_z = Meta::from_path(&path_z, false, PermissionFlag::Rwx).expect("failed to get meta"); let mut flags = Flags::default(); flags.sorting.dir_grouping = DirGrouping::First; // Sort with the dirs first let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_z), Ordering::Greater); // Sort with the dirs first (the dirs stay first) flags.sorting.order = SortOrder::Reverse; let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_z), Ordering::Greater); } #[test] fn test_sort_assemble_sorters_by_name_with_files_first() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let path_a = tmp_dir.path().join("zzz"); File::create(&path_a).expect("failed to create file"); let meta_a = Meta::from_path(&path_a, false, PermissionFlag::Rwx).expect("failed to get meta"); // Create a dir; let path_z = tmp_dir.path().join("aaa"); create_dir(&path_z).expect("failed to create dir"); let meta_z = Meta::from_path(&path_z, false, PermissionFlag::Rwx).expect("failed to get meta"); let mut flags = Flags::default(); flags.sorting.dir_grouping = DirGrouping::Last; // Sort with file first let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_z), Ordering::Less); // Sort with file first reversed (this files stay first) let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_z), Ordering::Less); } #[test] fn test_sort_assemble_sorters_by_name_unordered() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let path_a = tmp_dir.path().join("aaa"); File::create(&path_a).expect("failed to create file"); let meta_a = Meta::from_path(&path_a, false, PermissionFlag::Rwx).expect("failed to get meta"); // Create a dir; let path_z = tmp_dir.path().join("zzz"); create_dir(&path_z).expect("failed to create dir"); let meta_z = Meta::from_path(&path_z, false, PermissionFlag::Rwx).expect("failed to get meta"); let mut flags = Flags::default(); flags.sorting.dir_grouping = DirGrouping::None; // Sort by name unordered let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_z), Ordering::Less); // Sort by name unordered flags.sorting.order = SortOrder::Reverse; let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_z), Ordering::Greater); } #[test] fn test_sort_assemble_sorters_by_name_unordered_2() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let path_a = tmp_dir.path().join("zzz"); File::create(&path_a).expect("failed to create file"); let meta_a = Meta::from_path(&path_a, false, PermissionFlag::Rwx).expect("failed to get meta"); // Create a dir; let path_z = tmp_dir.path().join("aaa"); create_dir(&path_z).expect("failed to create dir"); let meta_z = Meta::from_path(&path_z, false, PermissionFlag::Rwx).expect("failed to get meta"); let mut flags = Flags::default(); flags.sorting.dir_grouping = DirGrouping::None; // Sort by name unordered let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_z), Ordering::Greater); // Sort by name unordered reversed flags.sorting.order = SortOrder::Reverse; let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_z), Ordering::Less); } #[test] fn test_sort_assemble_sorters_by_time() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let path_a = tmp_dir.path().join("aaa"); File::create(&path_a).expect("failed to create file"); let meta_a = Meta::from_path(&path_a, false, PermissionFlag::Rwx).expect("failed to get meta"); // Create the file; let path_z = tmp_dir.path().join("zzz"); File::create(&path_z).expect("failed to create file"); #[cfg(unix)] let success = Command::new("touch") .arg("-t") .arg("198511160000") .arg(&path_z) .status() .unwrap() .success(); #[cfg(windows)] let success = Command::new("powershell") .arg("-Command") .arg("$(Get-Item") .arg(&path_z) .arg(").lastwritetime=$(Get-Date \"1985-11-16\")") .status() .unwrap() .success(); assert!(success, "failed to change file timestamp"); let meta_z = Meta::from_path(&path_z, false, PermissionFlag::Rwx).expect("failed to get meta"); let mut flags = Flags::default(); flags.sorting.column = SortColumn::Time; // Sort by time let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_z), Ordering::Less); // Sort by time reversed flags.sorting.order = SortOrder::Reverse; let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_z), Ordering::Greater); } #[test] fn test_sort_assemble_sorters_by_extension() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file with rs extension; let path_a = tmp_dir.path().join("aaa.rs"); File::create(&path_a).expect("failed to create file"); let meta_a = Meta::from_path(&path_a, false, PermissionFlag::Rwx).expect("failed to get meta"); // Create the file with rs extension; let path_z = tmp_dir.path().join("zzz.rs"); File::create(&path_z).expect("failed to create file"); let meta_z = Meta::from_path(&path_z, false, PermissionFlag::Rwx).expect("failed to get meta"); // Create the file with js extension; let path_j = tmp_dir.path().join("zzz.js"); File::create(&path_j).expect("failed to create file"); let meta_j = Meta::from_path(&path_j, false, PermissionFlag::Rwx).expect("failed to get meta"); // Create the file with txt extension; let path_t = tmp_dir.path().join("zzz.txt"); File::create(&path_t).expect("failed to create file"); let meta_t = Meta::from_path(&path_t, false, PermissionFlag::Rwx).expect("failed to get meta"); let mut flags = Flags::default(); flags.sorting.column = SortColumn::Extension; // Sort by extension let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_z), Ordering::Equal); let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_j), Ordering::Greater); let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_t), Ordering::Less); } #[test] fn test_sort_assemble_sorters_by_version() { let tmp_dir = tempdir().expect("failed to create temp dir"); let path_a = tmp_dir.path().join("2"); File::create(&path_a).expect("failed to create file"); let meta_a = Meta::from_path(&path_a, false, PermissionFlag::Rwx).expect("failed to get meta"); let path_b = tmp_dir.path().join("11"); File::create(&path_b).expect("failed to create file"); let meta_b = Meta::from_path(&path_b, false, PermissionFlag::Rwx).expect("failed to get meta"); let path_c = tmp_dir.path().join("12"); File::create(&path_c).expect("failed to create file"); let meta_c = Meta::from_path(&path_c, false, PermissionFlag::Rwx).expect("failed to get meta"); let mut flags = Flags::default(); flags.sorting.column = SortColumn::Version; let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_b, &meta_a), Ordering::Greater); let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_b, &meta_c), Ordering::Less); } #[test] fn test_sort_assemble_sorters_no_sort() { let tmp_dir = tempdir().expect("failed to create temp dir"); let path_a = tmp_dir.path().join("aaa.aa"); File::create(&path_a).expect("failed to create file"); let meta_a = Meta::from_path(&path_a, false, PermissionFlag::Rwx).expect("failed to get meta"); let path_b = tmp_dir.path().join("aaa"); create_dir(&path_b).expect("failed to create dir"); let meta_b = Meta::from_path(&path_b, false, PermissionFlag::Rwx).expect("failed to get meta"); let path_c = tmp_dir.path().join("zzz.zz"); File::create(&path_c).expect("failed to create file"); let meta_c = Meta::from_path(&path_c, false, PermissionFlag::Rwx).expect("failed to get meta"); let path_d = tmp_dir.path().join("zzz"); create_dir(&path_d).expect("failed to create dir"); let meta_d = Meta::from_path(&path_d, false, PermissionFlag::Rwx).expect("failed to get meta"); let mut flags = Flags::default(); flags.sorting.column = SortColumn::None; let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_b), Ordering::Equal); let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_c), Ordering::Equal); let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_a, &meta_d), Ordering::Equal); let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_b, &meta_c), Ordering::Equal); let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_b, &meta_d), Ordering::Equal); let sorter = assemble_sorters(&flags); assert_eq!(by_meta(&sorter, &meta_c, &meta_d), Ordering::Equal); } #[test] fn test_sort_by_size() { let tmp_dir = tempdir().expect("failed to create temp dir"); let path_a = tmp_dir.path().join("aaa.aa"); File::create(&path_a) .expect("failed to create file") .write_all(b"1, 2, 3") .expect("failed to write to file"); let meta_a = Meta::from_path(&path_a, false, PermissionFlag::Rwx).expect("failed to get meta"); let path_b = tmp_dir.path().join("bbb.bb"); File::create(&path_b) .expect("failed to create file") .write_all(b"1, 2, 3, 4, 5, 6, 7, 8, 9, 10") .expect("failed to write file"); let meta_b = Meta::from_path(&path_b, false, PermissionFlag::Rwx).expect("failed to get meta"); let path_c = tmp_dir.path().join("ccc.cc"); let path_d = tmp_dir.path().join("ddd.dd"); #[cfg(unix)] std::os::unix::fs::symlink(path_d, &path_c).expect("failed to create broken symlink"); // this needs to be tested on Windows // likely to fail because of permission issue // see https://doc.rust-lang.org/std/os/windows/fs/fn.symlink_file.html #[cfg(windows)] std::os::windows::fs::symlink_file(path_d, &path_c) .expect("failed to create broken symlink"); let meta_c = Meta::from_path(&path_c, true, PermissionFlag::Rwx).expect("failed to get meta"); assert_eq!(by_size(&meta_a, &meta_a), Ordering::Equal); assert_eq!(by_size(&meta_a, &meta_b), Ordering::Greater); assert_eq!(by_size(&meta_a, &meta_c), Ordering::Greater); assert_eq!(by_size(&meta_b, &meta_a), Ordering::Less); assert_eq!(by_size(&meta_b, &meta_b), Ordering::Equal); assert_eq!(by_size(&meta_b, &meta_c), Ordering::Greater); assert_eq!(by_size(&meta_c, &meta_a), Ordering::Less); assert_eq!(by_size(&meta_c, &meta_b), Ordering::Less); assert_eq!(by_size(&meta_c, &meta_c), Ordering::Equal); } } 0707010000004C000041ED00000000000000000000000266C4C37900000000000000000000000000000000000000000000001400000000lsd-1.1.5/src/theme0707010000004D000081A400000000000000000000000166C4C37900000A36000000000000000000000000000000000000001700000000lsd-1.1.5/src/theme.rspub mod color; pub mod git; pub mod icon; use std::path::Path; use std::{fs, io}; use serde::{de::DeserializeOwned, Deserialize}; use thiserror::Error; use crate::config_file; use crate::print_error; use color::ColorTheme; use git::GitThemeSymbols; use icon::IconTheme; #[derive(Debug, Deserialize, Default, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct Theme { pub color: ColorTheme, pub icon: IconTheme, pub git_theme: GitThemeSymbols, } #[derive(Error, Debug)] pub enum Error { #[error("Can not read the theme file")] ReadFailed(#[from] io::Error), #[error("Theme file format invalid")] InvalidFormat(#[from] serde_yaml::Error), #[error("Theme file path invalid {0}")] InvalidPath(String), } impl Theme { /// Read theme from a file path /// use the file path as-is if it is absolute /// search the config paths folders for it if not pub fn from_path<D>(file: &str) -> Result<D, Error> where D: DeserializeOwned + Default, { let real = if let Some(path) = config_file::expand_home(file) { path } else { print_error!("Not a valid theme file path: {}.", &file); return Err(Error::InvalidPath(file.to_string())); }; let mut paths = if Path::new(&real).is_absolute() { vec![real].into_iter() } else { config_file::Config::config_paths() .map(|p| p.join(real.clone())) .collect::<Vec<_>>() .into_iter() }; let Some(valid) = paths.find_map(|p| { let yaml = p.with_extension("yaml"); let yml = p.with_extension("yml"); if yaml.is_file() { Some(yaml) } else if yml.is_file() { Some(yml) } else { None } }) else { return Err(Error::InvalidPath("No valid theme file found".to_string())); }; match fs::read_to_string(valid) { Ok(yaml) => match Self::with_yaml(&yaml) { Ok(t) => Ok(t), Err(e) => Err(Error::InvalidFormat(e)), }, Err(e) => Err(Error::ReadFailed(e)), } } /// This constructs a Theme struct with a passed [Yaml] str. fn with_yaml<D>(yaml: &str) -> Result<D, serde_yaml::Error> where D: DeserializeOwned + Default, { if yaml.trim() == "" { return Ok(D::default()); } serde_yaml::from_str::<D>(yaml) } } 0707010000004E000081A400000000000000000000000166C4C37900003D98000000000000000000000000000000000000001D00000000lsd-1.1.5/src/theme/color.rs//! This module provides methods to create theme from files and operations related to //! this. use crossterm::style::Color; use serde::{de::IntoDeserializer, Deserialize}; use std::fmt; // Custom color deserialize fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error> where D: serde::de::Deserializer<'de>, { struct ColorVisitor; impl<'de> serde::de::Visitor<'de> for ColorVisitor { type Value = Color; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str( "`black`, `blue`, `dark_blue`, `cyan`, `dark_cyan`, `green`, `dark_green`, `grey`, `dark_grey`, `magenta`, `dark_magenta`, `red`, `dark_red`, `white`, `yellow`, `dark_yellow`, `u8`, or `3 u8 array`", ) } fn visit_str<E>(self, value: &str) -> Result<Color, E> where E: serde::de::Error, { Color::deserialize(value.into_deserializer()) } fn visit_u64<E>(self, value: u64) -> Result<Color, E> where E: serde::de::Error, { if value > 255 { return Err(E::invalid_value( serde::de::Unexpected::Unsigned(value), &self, )); } Ok(Color::AnsiValue(value as u8)) } fn visit_seq<M>(self, mut seq: M) -> Result<Color, M::Error> where M: serde::de::SeqAccess<'de>, { let mut values = Vec::new(); if let Some(size) = seq.size_hint() { if size != 3 { return Err(serde::de::Error::invalid_length( size, &"a list of size 3(RGB)", )); } } loop { match seq.next_element::<u8>() { Ok(Some(x)) => { values.push(x); } Ok(None) => break, Err(e) => { return Err(e); } } } // recheck as size_hint sometimes not working if values.len() != 3 { return Err(serde::de::Error::invalid_length( values.len(), &"a list of size 3(RGB)", )); } Ok(Color::from((values[0], values[1], values[2]))) } } deserializer.deserialize_any(ColorVisitor) } /// A struct holding the theme configuration /// Color table: https://upload.wikimedia.org/wikipedia/commons/1/15/Xterm_256color_chart.svg #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct ColorTheme { #[serde(deserialize_with = "deserialize_color")] pub user: Color, #[serde(deserialize_with = "deserialize_color")] pub group: Color, pub permission: Permission, pub attributes: Attributes, pub date: Date, pub size: Size, pub inode: INode, #[serde(deserialize_with = "deserialize_color")] pub tree_edge: Color, pub links: Links, pub git_status: GitStatus, #[serde(skip)] pub file_type: FileType, } #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct Permission { #[serde(deserialize_with = "deserialize_color")] pub read: Color, #[serde(deserialize_with = "deserialize_color")] pub write: Color, #[serde(deserialize_with = "deserialize_color")] pub exec: Color, #[serde(deserialize_with = "deserialize_color")] pub exec_sticky: Color, #[serde(deserialize_with = "deserialize_color")] pub no_access: Color, #[serde(deserialize_with = "deserialize_color")] pub octal: Color, #[serde(deserialize_with = "deserialize_color")] pub acl: Color, #[serde(deserialize_with = "deserialize_color")] pub context: Color, } #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct Attributes { #[serde(deserialize_with = "deserialize_color")] pub archive: Color, #[serde(deserialize_with = "deserialize_color")] pub read: Color, #[serde(deserialize_with = "deserialize_color")] pub hidden: Color, #[serde(deserialize_with = "deserialize_color")] pub system: Color, } #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct FileType { pub file: File, pub dir: Dir, #[serde(deserialize_with = "deserialize_color")] pub pipe: Color, pub symlink: Symlink, #[serde(deserialize_with = "deserialize_color")] pub block_device: Color, #[serde(deserialize_with = "deserialize_color")] pub char_device: Color, #[serde(deserialize_with = "deserialize_color")] pub socket: Color, #[serde(deserialize_with = "deserialize_color")] pub special: Color, } #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct File { #[serde(deserialize_with = "deserialize_color")] pub exec_uid: Color, #[serde(deserialize_with = "deserialize_color")] pub uid_no_exec: Color, #[serde(deserialize_with = "deserialize_color")] pub exec_no_uid: Color, #[serde(deserialize_with = "deserialize_color")] pub no_exec_no_uid: Color, } #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct Dir { #[serde(deserialize_with = "deserialize_color")] pub uid: Color, #[serde(deserialize_with = "deserialize_color")] pub no_uid: Color, } #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct Symlink { #[serde(deserialize_with = "deserialize_color")] pub default: Color, #[serde(deserialize_with = "deserialize_color")] pub broken: Color, #[serde(deserialize_with = "deserialize_color")] pub missing_target: Color, } #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct Date { #[serde(deserialize_with = "deserialize_color")] pub hour_old: Color, #[serde(deserialize_with = "deserialize_color")] pub day_old: Color, #[serde(deserialize_with = "deserialize_color")] pub older: Color, } #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct Size { #[serde(deserialize_with = "deserialize_color")] pub none: Color, #[serde(deserialize_with = "deserialize_color")] pub small: Color, #[serde(deserialize_with = "deserialize_color")] pub medium: Color, #[serde(deserialize_with = "deserialize_color")] pub large: Color, } #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct INode { #[serde(deserialize_with = "deserialize_color")] pub valid: Color, #[serde(deserialize_with = "deserialize_color")] pub invalid: Color, } #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct Links { #[serde(deserialize_with = "deserialize_color")] pub valid: Color, #[serde(deserialize_with = "deserialize_color")] pub invalid: Color, } #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct GitStatus { #[serde(deserialize_with = "deserialize_color")] pub default: Color, #[serde(deserialize_with = "deserialize_color")] pub unmodified: Color, #[serde(deserialize_with = "deserialize_color")] pub ignored: Color, #[serde(deserialize_with = "deserialize_color")] pub new_in_index: Color, #[serde(deserialize_with = "deserialize_color")] pub new_in_workdir: Color, #[serde(deserialize_with = "deserialize_color")] pub typechange: Color, #[serde(deserialize_with = "deserialize_color")] pub deleted: Color, #[serde(deserialize_with = "deserialize_color")] pub renamed: Color, #[serde(deserialize_with = "deserialize_color")] pub modified: Color, #[serde(deserialize_with = "deserialize_color")] pub conflicted: Color, } impl Default for Permission { fn default() -> Self { Permission { read: Color::DarkGreen, write: Color::DarkYellow, exec: Color::DarkRed, exec_sticky: Color::AnsiValue(5), no_access: Color::AnsiValue(245), // Grey octal: Color::AnsiValue(6), acl: Color::DarkCyan, context: Color::Cyan, } } } impl Default for Attributes { fn default() -> Self { Attributes { archive: Color::DarkGreen, read: Color::DarkYellow, hidden: Color::AnsiValue(13), // Pink, system: Color::AnsiValue(13), // Pink, } } } impl Default for FileType { fn default() -> Self { FileType { file: File::default(), dir: Dir::default(), symlink: Symlink::default(), pipe: Color::AnsiValue(44), // DarkTurquoise block_device: Color::AnsiValue(44), // DarkTurquoise char_device: Color::AnsiValue(172), // Orange3 socket: Color::AnsiValue(44), // DarkTurquoise special: Color::AnsiValue(44), // DarkTurquoise } } } impl Default for File { fn default() -> Self { File { exec_uid: Color::AnsiValue(40), // Green3 uid_no_exec: Color::AnsiValue(184), // Yellow3 exec_no_uid: Color::AnsiValue(40), // Green3 no_exec_no_uid: Color::AnsiValue(184), // Yellow3 } } } impl Default for Dir { fn default() -> Self { Dir { uid: Color::AnsiValue(33), // DodgerBlue1 no_uid: Color::AnsiValue(33), // DodgerBlue1 } } } impl Default for Symlink { fn default() -> Self { Symlink { default: Color::AnsiValue(44), // DarkTurquoise broken: Color::AnsiValue(124), // Red3 missing_target: Color::AnsiValue(124), // Red3 } } } impl Default for Date { fn default() -> Self { Date { hour_old: Color::AnsiValue(40), // Green3 day_old: Color::AnsiValue(42), // SpringGreen2 older: Color::AnsiValue(36), // DarkCyan } } } impl Default for Size { fn default() -> Self { Size { none: Color::AnsiValue(245), // Grey small: Color::AnsiValue(229), // Wheat1 medium: Color::AnsiValue(216), // LightSalmon1 large: Color::AnsiValue(172), // Orange3 } } } impl Default for INode { fn default() -> Self { INode { valid: Color::AnsiValue(13), // Pink invalid: Color::AnsiValue(245), // Grey } } } impl Default for Links { fn default() -> Self { Links { valid: Color::AnsiValue(13), // Pink invalid: Color::AnsiValue(245), // Grey } } } impl Default for GitStatus { fn default() -> Self { GitStatus { default: Color::AnsiValue(245), // Grey unmodified: Color::AnsiValue(245), // Grey ignored: Color::AnsiValue(245), // Grey new_in_index: Color::DarkGreen, new_in_workdir: Color::DarkGreen, typechange: Color::DarkYellow, deleted: Color::DarkRed, renamed: Color::DarkGreen, modified: Color::DarkYellow, conflicted: Color::DarkRed, } } } impl Default for ColorTheme { fn default() -> Self { // TODO(zwpaper): check terminal color and return light or dark Self::default_dark() } } impl ColorTheme { pub fn default_dark() -> Self { ColorTheme { user: Color::AnsiValue(230), // Cornsilk1 group: Color::AnsiValue(187), // LightYellow3 permission: Permission::default(), attributes: Attributes::default(), file_type: FileType::default(), date: Date::default(), size: Size::default(), inode: INode::default(), links: Links::default(), tree_edge: Color::AnsiValue(245), // Grey git_status: Default::default(), } } } #[cfg(test)] mod tests { use super::ColorTheme; use crate::theme::Theme; fn default_yaml() -> &'static str { r#"--- user: 230 group: 187 permission: read: dark_green write: dark_yellow exec: dark_red exec-sticky: 5 no-access: 245 date: hour-old: 40 day-old: 42 older: 36 size: none: 245 small: 229 medium: 216 large: 172 inode: valid: 13 invalid: 245 links: valid: 13 invalid: 245 tree-edge: 245 "# } #[test] fn test_default_theme() { assert_eq!( ColorTheme::default_dark(), Theme::with_yaml(default_yaml()).unwrap() ); } #[test] fn test_default_theme_file() { use std::fs::File; use std::io::Write; let dir = assert_fs::TempDir::new().unwrap(); let theme = dir.path().join("theme.yaml"); let mut file = File::create(&theme).unwrap(); writeln!(file, "{}", default_yaml()).unwrap(); assert_eq!( ColorTheme::default_dark(), Theme::from_path(theme.to_str().unwrap()).unwrap() ); } #[test] fn test_empty_theme_return_default() { // Must contain one field at least // ref https://github.com/dtolnay/serde-yaml/issues/86 let empty_theme: ColorTheme = Theme::with_yaml("user: 230").unwrap(); // 230 is the default value let default_theme = ColorTheme::default_dark(); assert_eq!(empty_theme, default_theme); } #[test] fn test_first_level_theme_return_default_but_changed() { // Must contain one field at least // ref https://github.com/dtolnay/serde-yaml/issues/86 let empty_theme: ColorTheme = Theme::with_yaml("user: 130").unwrap(); let mut theme = ColorTheme::default_dark(); use crossterm::style::Color; theme.user = Color::AnsiValue(130); assert_eq!(empty_theme, theme); } #[test] fn test_hexadecimal_colors() { // Must contain one field at least // ref https://github.com/dtolnay/serde-yaml/issues/86 let empty_theme: ColorTheme = Theme::with_yaml("user: \"#ff007f\"").unwrap(); assert_eq!( empty_theme.user, crossterm::style::Color::Rgb { r: 255, g: 0, b: 127 } ); } #[test] fn test_second_level_theme_return_default_but_changed() { // Must contain one field at least // ref https://github.com/dtolnay/serde-yaml/issues/86 let empty_theme: ColorTheme = Theme::with_yaml( r#"--- permission: read: 130"#, ) .unwrap(); let mut theme = ColorTheme::default_dark(); use crossterm::style::Color; theme.permission.read = Color::AnsiValue(130); assert_eq!(empty_theme, theme); } } 0707010000004F000081A400000000000000000000000166C4C3790000039F000000000000000000000000000000000000001B00000000lsd-1.1.5/src/theme/git.rsuse serde::Deserialize; #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct GitThemeSymbols { pub default: String, pub unmodified: String, pub new_in_index: String, pub new_in_workdir: String, pub deleted: String, pub modified: String, pub renamed: String, pub ignored: String, pub typechange: String, pub conflicted: String, } impl Default for GitThemeSymbols { fn default() -> GitThemeSymbols { GitThemeSymbols { default: "-".into(), unmodified: ".".into(), new_in_index: "N".into(), new_in_workdir: "?".into(), deleted: "D".into(), modified: "M".into(), renamed: "R".into(), ignored: "I".into(), typechange: "T".into(), conflicted: "C".into(), } } } 07070100000050000081A400000000000000000000000166C4C3790000A063000000000000000000000000000000000000001C00000000lsd-1.1.5/src/theme/icon.rsuse serde::Deserialize; use std::collections::HashMap; enum ByFilename { Name, Extension, } fn deserialize_by_filename<'de, D>( deserializer: D, by: ByFilename, ) -> Result<HashMap<String, String>, D::Error> where D: serde::de::Deserializer<'de>, { let default = match by { ByFilename::Name => IconTheme::get_default_icons_by_name(), ByFilename::Extension => IconTheme::get_default_icons_by_extension(), }; HashMap::<_, _>::deserialize(deserializer) .map(|input| default.into_iter().chain(input).collect()) } fn deserialize_by_name<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error> where D: serde::de::Deserializer<'de>, { deserialize_by_filename(deserializer, ByFilename::Name) } fn deserialize_by_extension<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error> where D: serde::de::Deserializer<'de>, { deserialize_by_filename(deserializer, ByFilename::Extension) } #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct IconTheme { #[serde(deserialize_with = "deserialize_by_name")] pub name: HashMap<String, String>, #[serde(deserialize_with = "deserialize_by_extension")] pub extension: HashMap<String, String>, pub filetype: ByType, } #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] #[serde(default)] pub struct ByType { pub dir: String, pub file: String, pub pipe: String, pub socket: String, pub executable: String, pub device_char: String, pub device_block: String, pub special: String, pub symlink_dir: String, pub symlink_file: String, } impl Default for IconTheme { fn default() -> Self { IconTheme { name: Self::get_default_icons_by_name(), extension: Self::get_default_icons_by_extension(), filetype: ByType::default(), } } } impl Default for ByType { fn default() -> ByType { ByType { dir: "\u{f115}".into(), // file: "\u{f016}".into(), // pipe: "\u{f0232}".into(), // socket: "\u{f01a8}".into(), // executable: "\u{f489}".into(), // symlink_dir: "\u{f482}".into(), // symlink_file: "\u{f481}".into(), // device_char: "\u{e601}".into(), // device_block: "\u{f072b}".into(), // special: "\u{f2dc}".into(), // } } } impl ByType { pub fn unicode() -> Self { ByType { dir: "\u{1f4c2}".into(), file: "\u{1f4c4}".into(), pipe: "\u{1f4e9}".into(), socket: "\u{1f4ec}".into(), executable: "\u{1f3d7}".into(), symlink_dir: "\u{1f5c2}".into(), symlink_file: "\u{1f516}".into(), device_char: "\u{1f5a8}".into(), device_block: "\u{1f4bd}".into(), special: "\u{1f4df}".into(), } } } impl IconTheme { pub fn unicode() -> Self { IconTheme { name: HashMap::new(), extension: HashMap::new(), filetype: ByType::unicode(), } } // pub only for testing in icons.rs pub fn get_default_icons_by_name() -> HashMap<String, String> { // Note: filenames must be lower-case [ ("a.out", "\u{f489}"), // "" ("api", "\u{f048d}"), // "" (".asoundrc", "\u{e615}"), // "" (".atom", "\u{e764}"), // "" (".ash", "\u{f489}"), // "" (".ash_history", "\u{f489}"), // "" ("authorized_keys", "\u{e60a}"), // "" ("assets", "\u{f0c7}"), // "" (".android", "\u{f17b}"), // "" (".audacity-data", "\u{e5fc}"), // "" ("backups", "\u{f006f}"), // "" (".bash_history", "\u{f1183}"), // "" (".bash_logout", "\u{f1183}"), // "" (".bash_profile", "\u{f1183}"), // "" (".bashrc", "\u{f1183}"), // "" ("bin", "\u{e5fc}"), // "" (".bpython_history", "\u{e606}"), // "" ("build", "\u{f487}"), // "" ("bspwmrc", "\u{e615}"), // "" ("build.ninja", "\u{f0ad}"), // "" (".cache", "\u{f00e8}"), // "" ("cache", "\u{f00e8}"), // "" ("cargo.lock", "\u{e68b}"), // "" ("cargo.toml", "\u{e68b}"), // "" (".cargo", "\u{e68b}"), // "" (".ccls-cache", "\u{f00e8}"), // "" ("changelog", "\u{e609}"), // "" (".clang-format", "\u{e615}"), // "" ("composer.json", "\u{e608}"), // "" ("composer.lock", "\u{e608}"), // "" ("conf.d", "\u{e5fc}"), // "" ("config.ac", "\u{e615}"), // "" ("config.el", "\u{e632}"), // "" ("config.mk", "\u{e615}"), // "" (".config", "\u{e5fc}"), // "" ("config", "\u{e5fc}"), // "" ("configure", "\u{f0ad}"), // "" ("content", "\u{f0c7}"), // "" ("contributing", "\u{e60a}"), // "" ("copyright", "\u{e60a}"), // "" ("cron.daily", "\u{e5fc}"), // "" ("cron.d", "\u{e5fc}"), // "" ("cron.deny", "\u{e615}"), // "" ("cron.hourly", "\u{e5fc}"), // "" ("cron.monthly", "\u{e5fc}"), // "" ("crontab", "\u{e615}"), // "" ("cron.weekly", "\u{e5fc}"), // "" ("crypttab", "\u{e615}"), // "" (".cshrc", "\u{f1183}"), // "" ("csh.cshrc", "\u{f1183}"), // "" ("csh.login", "\u{f1183}"), // "" ("csh.logout", "\u{f1183}"), // "" ("css", "\u{e749}"), // "" ("custom.el", "\u{e632}"), // "" (".dbus", "\u{f013}"), // "" ("desktop", "\u{f108}"), // "" ("docker-compose.yml", "\u{f308}"), // "" ("dockerfile", "\u{f308}"), // "" ("doc", "\u{f02d}"), // "" ("dist", "\u{f487}"), // "" ("documents", "\u{f02d}"), // "" (".doom.d", "\u{e632}"), // "" ("downloads", "\u{f024d}"), // "" (".ds_store", "\u{f179}"), // "" (".editorconfig", "\u{e615}"), // "" (".electron-gyp", "\u{e5fa}"), // "" (".emacs.d", "\u{e632}"), // "" (".env", "\u{f462}"), // "" ("environment", "\u{f462}"), // "" (".eslintrc.json", "\u{f462}"), // "" (".eslintrc.js", "\u{f462}"), // "" (".eslintrc.yml", "\u{f462}"), // "" ("etc", "\u{e5fc}"), // "" ("favicon.ico", "\u{f005}"), // "" ("favicons", "\u{f005}"), // "" (".fennelrc", "\u{e615}"), // "" ("fstab", "\u{f1c0}"), // "" (".fastboot", "\u{f17b}"), // "" (".gitattributes", "\u{f1d3}"), // "" (".gitconfig", "\u{f1d3}"), // "" (".git-credentials", "\u{e60a}"), // "" (".github", "\u{e5fd}"), // "" ("gitignore_global", "\u{f1d3}"), // "" (".gitignore", "\u{f1d3}"), // "" (".gitlab-ci.yml", "\u{f296}"), // "" (".gitmodules", "\u{f1d3}"), // "" (".git", "\u{e5fb}"), // "" (".gnupg", "\u{f08ac}"), // "" ("go.mod", "\u{e627}"), // "" ("go.sum", "\u{e627}"), // "" ("go.work", "\u{e627}"), // "" ("gradle", "\u{e660}"), // "" ("gradle.properties", "\u{e660}"), // "" ("gradlew", "\u{e660}"), // "" ("gradlew.bat", "\u{e660}"), // "" ("group", "\u{e615}"), // "" ("gruntfile.coffee", "\u{e611}"), // "" ("gruntfile.js", "\u{e611}"), // "" ("gruntfile.ls", "\u{e611}"), // "" ("gshadow", "\u{e615}"), // "" ("gulpfile.coffee", "\u{e610}"), // "" ("gulpfile.js", "\u{e610}"), // "" ("gulpfile.ls", "\u{e610}"), // "" ("heroku.yml", "\u{e77b}"), // "" ("hidden", "\u{f023}"), // "" ("home", "\u{f015}"), // "" ("hostname", "\u{e615}"), // "" ("hosts", "\u{f0002}"), // "" (".htaccess", "\u{e615}"), // "" ("htoprc", "\u{e615}"), // "" (".htpasswd", "\u{e615}"), // "" (".icons", "\u{f005}"), // "" ("icons", "\u{f005}"), // "" ("id_dsa", "\u{f0dd6}"), // "" ("id_ecdsa", "\u{f0dd6}"), // "" ("id_rsa", "\u{f0dd6}"), // "" (".idlerc", "\u{e235}"), // "" ("img", "\u{f1c5}"), // "" ("include", "\u{e5fc}"), // "" ("init.el", "\u{e632}"), // "" (".inputrc", "\u{e615}"), // "" ("inputrc", "\u{e615}"), // "" (".java", "\u{e256}"), // "" ("jenkinsfile", "\u{e66e}"), // "" ("js", "\u{e74e}"), // "" (".jupyter", "\u{e606}"), // "" ("kbuild", "\u{e615}"), // "" ("kconfig", "\u{e615}"), // "" ("kdeglobals", "\u{e615}"), // "" ("kdenliverc", "\u{e615}"), // "" ("known_hosts", "\u{e60a}"), // "" (".kshrc", "\u{f489}"), // "" ("libexec", "\u{f121}"), // "" ("lib32", "\u{f121}"), // "" ("lib64", "\u{f121}"), // "" ("lib", "\u{f121}"), // "" ("license.md", "\u{e60a}"), // "" ("licenses", "\u{e60a}"), // "" ("license.txt", "\u{e60a}"), // "" ("license", "\u{e60a}"), // "" ("localized", "\u{f179}"), // "" ("lsb-release", "\u{e615}"), // "" (".lynxrc", "\u{e615}"), // "" (".mailcap", "\u{f01f0}"), // "" ("mail", "\u{f01f0}"), // "" ("magic", "\u{f0d0}"), // "" ("maintainers", "\u{e60a}"), // "" ("makefile.ac", "\u{e615}"), // "" ("makefile", "\u{e615}"), // "" ("manifest", "\u{f292}"), // "" ("md5sum", "\u{f0565}"), // "" ("meson.build", "\u{f0ad}"), // "" ("metadata", "\u{e5fc}"), // "" ("metadata.xml", "\u{f462}"), // "" ("media", "\u{f40f}"), // "" (".mime.types", "\u{f0645}"), // "" ("mime.types", "\u{f0645}"), // "" ("module.symvers", "\u{f471}"), // "" (".mozilla", "\u{e786}"), // "" ("music", "\u{f1359}"), // "" ("muttrc", "\u{e615}"), // "" (".muttrc", "\u{e615}"), // "" (".mutt", "\u{e615}"), // "" (".mypy_cache", "\u{f00e8}"), // "" ("neomuttrc", "\u{e615}"), // "" (".neomuttrc", "\u{e615}"), // "" ("netlify.toml", "\u{f233}"), // "" (".nix-channels", "\u{f313}"), // "" (".nix-defexpr", "\u{f313}"), // "" (".node-gyp", "\u{e5fa}"), // "" ("node_modules", "\u{e5fa}"), // "" (".node_repl_history", "\u{e718}"), // "" ("npmignore", "\u{e71e}"), // "" (".npm", "\u{e5fa}"), // "" ("nvim", "\u{f36f}"), // "" ("obj", "\u{e624}"), // "" ("os-release", "\u{e615}"), // "" ("package.json", "\u{e718}"), // "" ("package-lock.json", "\u{e718}"), // "" ("packages.el", "\u{e632}"), // "" ("pam.d", "\u{f08ac}"), // "" ("passwd", "\u{f023}"), // "" ("pictures", "\u{f024f}"), // "" ("pkgbuild", "\u{f303}"), // "" (".pki", "\u{f023}"), // "" ("portage", "\u{f30d}"), // "" ("profile", "\u{e615}"), // "" (".profile", "\u{e615}"), // "" ("public", "\u{f415}"), // "" ("__pycache__", "\u{e606}"), // "" ("pyproject.toml", "\u{e606}"), // "" (".python_history", "\u{e606}"), // "" (".pypirc", "\u{e606}"), // "" ("rc.lua", "\u{e615}"), // "" ("readme", "\u{e609}"), // "" (".release.toml", "\u{e68b}"), // "" ("requirements.txt", "\u{f0320}"), // "" ("robots.txt", "\u{f06a9}"), // "" ("root", "\u{f0250}"), // "" ("rubydoc", "\u{e73b}"), // "" ("runtime.txt", "\u{f0320}"), // "" (".rustup", "\u{e68b}"), // "" ("rustfmt.toml", "\u{e68b}"), // "" (".rvm", "\u{e21e}"), // "" ("sass", "\u{e603}"), // "" ("sbin", "\u{e5fc}"), // "" ("scripts", "\u{f489}"), // "" ("scss", "\u{e603}"), // "" ("sha256sum", "\u{f0565}"), // "" ("shadow", "\u{e615}"), // "" ("share", "\u{f064}"), // "" (".shellcheckrc", "\u{e615}"), // "" ("shells", "\u{e615}"), // "" (".spacemacs", "\u{e632}"), // "" (".sqlite_history", "\u{e7c4}"), // "" ("src", "\u{f19fc}"), // "" (".ssh", "\u{f08ac}"), // "" ("static", "\u{f0c7}"), // "" ("std", "\u{f0171}"), // "" ("styles", "\u{e749}"), // "" ("subgid", "\u{e615}"), // "" ("subuid", "\u{e615}"), // "" ("sudoers", "\u{f023}"), // "" ("sxhkdrc", "\u{e615}"), // "" ("template", "\u{f32e}"), // "" ("tests", "\u{f0668}"), // "" ("tigrc", "\u{e615}"), // "" ("timezone", "\u{f43a}"), // "" ("tox.ini", "\u{e615}"), // "" (".trash", "\u{f1f8}"), // "" ("ts", "\u{e628}"), // "" (".tox", "\u{e606}"), // "" ("unlicense", "\u{e60a}"), // "" ("url", "\u{f0ac}"), // "" ("user-dirs.dirs", "\u{e5fc}"), // "" ("vagrantfile", "\u{e615}"), // "" ("vendor", "\u{f0ae6}"), // "" ("venv", "\u{f0320}"), // "" ("videos", "\u{f03d}"), // "" (".viminfo", "\u{e62b}"), // "" (".vimrc", "\u{e62b}"), // "" ("vimrc", "\u{e62b}"), // "" (".vim", "\u{e62b}"), // "" ("vim", "\u{e62b}"), // "" (".vscode", "\u{e70c}"), // "" ("webpack.config.js", "\u{f072b}"), // "" (".wgetrc", "\u{e615}"), // "" ("wgetrc", "\u{e615}"), // "" (".xauthority", "\u{e615}"), // "" (".Xauthority", "\u{e615}"), // "" ("xbps.d", "\u{f32e}"), // "" ("xbps-src", "\u{f32e}"), // "" (".xinitrc", "\u{e615}"), // "" (".xmodmap", "\u{e615}"), // "" (".Xmodmap", "\u{e615}"), // "" ("xmonad.hs", "\u{e615}"), // "" ("xorg.conf.d", "\u{e5fc}"), // "" (".xprofile", "\u{e615}"), // "" (".Xprofile", "\u{e615}"), // "" (".xresources", "\u{e615}"), // "" (".yarnrc", "\u{e6a7}"), // "" ("yarn.lock", "\u{e6a7}"), // "" ("zathurarc", "\u{e615}"), // "" (".zcompdump", "\u{e615}"), // "" (".zlogin", "\u{f1183}"), // "" (".zlogout", "\u{f1183}"), // "" (".zprofile", "\u{f1183}"), // "" (".zsh_history", "\u{f1183}"), // "" (".zshrc", "\u{f1183}"), // "" ] .iter() .map(|&s| (s.0.to_owned(), s.1.to_owned())) .collect::<HashMap<_, _>>() } // pub only for testing in icons.rs pub fn get_default_icons_by_extension() -> HashMap<String, String> { // Note: extensions must be lower-case [ ("1", "\u{f02d}"), // "" ("2", "\u{f02d}"), // "" ("3", "\u{f02d}"), // "" ("4", "\u{f02d}"), // "" ("5", "\u{f02d}"), // "" ("6", "\u{f02d}"), // "" ("7", "\u{f02d}"), // "" ("7z", "\u{f410}"), // "" ("8", "\u{f02d}"), // "" ("890", "\u{f015e}"), // "" ("a", "\u{e624}"), // "" ("ai", "\u{e7b4}"), // "" ("ape", "\u{f001}"), // "" ("apk", "\u{e70e}"), // "" ("apng", "\u{f1c5}"), // "" ("ar", "\u{f410}"), // "" ("asc", "\u{f099d}"), // "" ("asm", "\u{f471}"), // "" ("asp", "\u{f121}"), // "" ("avi", "\u{f008}"), // "" ("avif", "\u{f1c5}"), // "" ("avro", "\u{e60b}"), // "" ("awk", "\u{f489}"), // "" ("bak", "\u{f006f}"), // "" ("bash_history", "\u{f489}"), // "" ("bash_profile", "\u{f489}"), // "" ("bashrc", "\u{f489}"), // "" ("bash", "\u{f489}"), // "" ("bat", "\u{f17a}"), // "" ("bin", "\u{eae8}"), // "" ("bio", "\u{f0411}"), // "" ("blend", "\u{f00ab}"), // "" ("blend1", "\u{f00ab}"), // "" ("bmp", "\u{f1c5}"), // "" ("bz2", "\u{f410}"), // "" ("cc", "\u{e61d}"), // "" ("cfg", "\u{e615}"), // "" ("cip", "\u{f015e}"), // "" ("cjs", "\u{e74e}"), // "" ("class", "\u{e738}"), // "" ("cljs", "\u{e76a}"), // "" ("clj", "\u{e768}"), // "" ("cls", "\u{e600}"), // "" ("cl", "\u{f0172}"), // "" ("coffee", "\u{f0f4}"), // "" ("conf", "\u{e615}"), // "" ("cpp", "\u{e61d}"), // "" ("cp", "\u{e61d}"), // "" ("cshtml", "\u{f1fa}"), // "" ("csh", "\u{f489}"), // "" ("csproj", "\u{f031b}"), // "" ("css", "\u{e749}"), // "" ("cs", "\u{f031b}"), // "" ("csv", "\u{f1c3}"), // "" ("csx", "\u{f031b}"), // "" ("cts", "\u{e628}"), // "" ("c++", "\u{e61d}"), // "" ("c", "\u{e61e}"), // "" ("cue", "\u{f001}"), // "" ("cxx", "\u{e61d}"), // "" ("cypher", "\u{f1c0}"), // "" ("dart", "\u{e798}"), // "" ("dat", "\u{f1c0}"), // "" ("db", "\u{f1c0}"), // "" ("deb", "\u{f187}"), // "" ("desktop", "\u{f108}"), // "" ("diff", "\u{e728}"), // "" ("dll", "\u{f17a}"), // "" ("dockerfile", "\u{f308}"), // "" ("doc", "\u{f1c2}"), // "" ("docx", "\u{f1c2}"), // "" ("download", "\u{f43a}"), // "" ("ds_store", "\u{f179}"), // "" ("dump", "\u{f1c0}"), // "" ("ebook", "\u{e28b}"), // "" ("ebuild", "\u{f30d}"), // "" ("eclass", "\u{f30d}"), // "" ("editorconfig", "\u{e615}"), // "" ("egg-info", "\u{e606}"), // "" ("ejs", "\u{e618}"), // "" ("elc", "\u{f0172}"), // "" ("elf", "\u{f489}"), // "" ("elm", "\u{e62c}"), // "" ("el", "\u{f0172}"), // "" ("env", "\u{f462}"), // "" ("eot", "\u{f031}"), // "" ("epub", "\u{e28a}"), // "" ("erb", "\u{e73b}"), // "" ("erl", "\u{e7b1}"), // "" ("exe", "\u{f17a}"), // "" ("exs", "\u{e62d}"), // "" ("ex", "\u{e62d}"), // "" ("fish", "\u{f489}"), // "" ("flac", "\u{f001}"), // "" ("flv", "\u{f008}"), // "" ("fnl", "\u{e6af}"), // "" ("font", "\u{f031}"), // "" ("fpl", "\u{f0411}"), // "" ("fsi", "\u{e7a7}"), // "" ("fs", "\u{e7a7}"), // "" ("fsx", "\u{e7a7}"), // "" ("gdoc", "\u{f1c2}"), // "" ("gemfile", "\u{e21e}"), // "" ("gemspec", "\u{e21e}"), // "" ("gform", "\u{f298}"), // "" ("gif", "\u{f1c5}"), // "" ("git", "\u{f1d3}"), // "" ("go", "\u{e627}"), // "" ("gradle", "\u{e660}"), // "" ("gsheet", "\u{f1c3}"), // "" ("gslides", "\u{f1c4}"), // "" ("guardfile", "\u{e21e}"), // "" ("gv", "\u{f1049}"), // "" ("gz", "\u{f410}"), // "" ("hbs", "\u{e60f}"), // "" ("heic", "\u{f1c5}"), // "" ("heif", "\u{f1c5}"), // "" ("heix", "\u{f1c5}"), // "" ("hh", "\u{f0fd}"), // "" ("hpp", "\u{f0fd}"), // "" ("hs", "\u{e777}"), // "" ("html", "\u{f13b}"), // "" ("htm", "\u{f13b}"), // "" ("h", "\u{f0fd}"), // "" ("hxx", "\u{f0fd}"), // "" ("ico", "\u{f1c5}"), // "" ("image", "\u{f1c5}"), // "" ("img", "\u{f1c0}"), // "" ("iml", "\u{e7b5}"), // "" ("info", "\u{e795}"), // "" ("in", "\u{f15c}"), // "" ("ini", "\u{e615}"), // "" ("ipynb", "\u{e606}"), // "" ("iso", "\u{f1c0}"), // "" ("j2", "\u{e000}"), // "" ("jar", "\u{e738}"), // "" ("java", "\u{e738}"), // "" ("jinja", "\u{e000}"), // "" ("jl", "\u{e624}"), // "" ("jpeg", "\u{f1c5}"), // "" ("jpg", "\u{f1c5}"), // "" ("jsonc", "\u{e60b}"), // "" ("json", "\u{e60b}"), // "" ("js", "\u{e74e}"), // "" ("jsx", "\u{e7ba}"), // "" ("key", "\u{f0306}"), // "" ("ksh", "\u{f489}"), // "" ("kt", "\u{e634}"), // "" ("kts", "\u{e634}"), // "" ("kusto", "\u{f1c0}"), // "" ("ldb", "\u{f1c0}"), // "" ("ld", "\u{e624}"), // "" ("less", "\u{e758}"), // "" ("lhs", "\u{e777}"), // "" ("license", "\u{e60a}"), // "" ("lisp", "\u{f0172}"), // "" ("list", "\u{f03a}"), // "" ("localized", "\u{f179}"), // "" ("lock", "\u{f023}"), // "" ("log", "\u{f18d}"), // "" ("lss", "\u{e749}"), // "" ("lua", "\u{e620}"), // "" ("lz", "\u{f410}"), // "" ("mgc", "\u{f0d0}"), // "" ("m3u8", "\u{f0411}"), // "" ("m3u", "\u{f0411}"), // "" ("m4a", "\u{f001}"), // "" ("m4v", "\u{f008}"), // "" ("magnet", "\u{f076}"), // "" ("malloy", "\u{f1c0}"), // "" ("man", "\u{f02d}"), // "" ("markdown", "\u{e609}"), // "" ("md", "\u{e609}"), // "" ("mjs", "\u{e74e}"), // "" ("mkd", "\u{e609}"), // "" ("mk", "\u{f085}"), // "" ("mkv", "\u{f008}"), // "" ("ml", "\u{e67a}"), // "" ("mli", "\u{e67a}"), // "" ("mll", "\u{e67a}"), // "" ("mly", "\u{e67a}"), // "" ("mobi", "\u{e28b}"), // "" ("mov", "\u{f008}"), // "" ("mp3", "\u{f001}"), // "" ("mp4", "\u{f008}"), // "" ("msi", "\u{f17a}"), // "" ("mts", "\u{e628}"), // "" ("mustache", "\u{e60f}"), // "" ("nim", "\u{e677}"), // "" ("nimble", "\u{e677}"), // "" ("nix", "\u{f313}"), // "" ("npmignore", "\u{e71e}"), // "" ("odp", "\u{f1c4}"), // "" ("ods", "\u{f1c3}"), // "" ("odt", "\u{f1c2}"), // "" ("ogg", "\u{f001}"), // "" ("ogv", "\u{f008}"), // "" ("old", "\u{f006f}"), // "" ("opus", "\u{f001}"), // "" ("orig", "\u{f006f}"), // "" ("org", "\u{e633}"), // "" ("otf", "\u{f031}"), // "" ("o", "\u{eae8}"), // "" ("part", "\u{f43a}"), // "" ("patch", "\u{e728}"), // "" ("pdb", "\u{f0aaa}"), // "" ("pdf", "\u{f1c1}"), // "" ("pem", "\u{f0306}"), // "" ("phar", "\u{e608}"), // "" ("php", "\u{e608}"), // "" ("pkg", "\u{f187}"), // "" ("pl", "\u{e67e}"), // "" ("plist", "\u{f302}"), // "" ("pls", "\u{f0411}"), // "" ("plx", "\u{e67e}"), // "" ("pm", "\u{e67e}"), // "" ("png", "\u{f1c5}"), // "" ("pod", "\u{e67e}"), // "" ("pp", "\u{e631}"), // "" ("ppt", "\u{f1c4}"), // "" ("pptx", "\u{f1c4}"), // "" ("procfile", "\u{e21e}"), // "" ("properties", "\u{e60b}"), // "" ("prql", "\u{f1c0}"), // "" ("ps1", "\u{f489}"), // "" ("psd", "\u{e7b8}"), // "" ("pub", "\u{f0306}"), // "" ("sbv", "\u{f015e}"), // "" ("scc", "\u{f015e}"), // "" ("slt", "\u{f0221}"), // "" ("smi", "\u{f015e}"), // "" ("pxm", "\u{f1c5}"), // "" ("pyc", "\u{e606}"), // "" ("py", "\u{e606}"), // "" ("rakefile", "\u{e21e}"), // "" ("rar", "\u{f410}"), // "" ("razor", "\u{f1fa}"), // "" ("rb", "\u{e21e}"), // "" ("rdata", "\u{f07d4}"), // "" ("rdb", "\u{e76d}"), // "" ("rdoc", "\u{e609}"), // "" ("rds", "\u{f07d4}"), // "" ("readme", "\u{e609}"), // "" ("rlib", "\u{e68b}"), // "" ("rl", "\u{f11c}"), // "" ("rmd", "\u{e609}"), // "" ("rmeta", "\u{e68b}"), // "" ("rpm", "\u{f187}"), // "" ("rproj", "\u{f05c6}"), // "" ("rq", "\u{f1c0}"), // "" ("rspec_parallel", "\u{e21e}"), // "" ("rspec_status", "\u{e21e}"), // "" ("rspec", "\u{e21e}"), // "" ("rss", "\u{f09e}"), // "" ("rs", "\u{e68b}"), // "" ("rtf", "\u{f15c}"), // "" ("rubydoc", "\u{e73b}"), // "" ("r", "\u{f07d4}"), // "" ("ru", "\u{e21e}"), // "" ("sass", "\u{e603}"), // "" ("scala", "\u{e737}"), // "" ("scpt", "\u{f302}"), // "" ("scss", "\u{e603}"), // "" ("shell", "\u{f489}"), // "" ("sh", "\u{f489}"), // "" ("sig", "\u{e60a}"), // "" ("slim", "\u{e73b}"), // "" ("sln", "\u{e70c}"), // "" ("so", "\u{e624}"), // "" ("sqlite3", "\u{e7c4}"), // "" ("sql", "\u{f1c0}"), // "" ("srt", "\u{f0a16}"), // "" ("styl", "\u{e600}"), // "" ("stylus", "\u{e600}"), // "" ("sublime-menu", "\u{e7aa}"), // "" ("sublime-package", "\u{e7aa}"), // "" ("sublime-project", "\u{e7aa}"), // "" ("sublime-session", "\u{e7aa}"), // "" ("sub", "\u{f0a16}"), // "" ("s", "\u{f471}"), // "" ("svg", "\u{f1c5}"), // "" ("svelte", "\u{e697}"), // "" ("swift", "\u{e755}"), // "" ("swp", "\u{e62b}"), // "" ("sym", "\u{eae8}"), // "" ("tar", "\u{f410}"), // "" ("taz", "\u{f410}"), // "" ("tbz", "\u{f410}"), // "" ("tbz2", "\u{f410}"), // "" ("tex", "\u{e600}"), // "" ("tgz", "\u{f410}"), // "" ("tiff", "\u{f1c5}"), // "" ("timestamp", "\u{f43a}"), // "" ("toml", "\u{e60b}"), // "" ("torrent", "\u{f048d}"), // "" ("trash", "\u{f1f8}"), // "" ("ts", "\u{e628}"), // "" ("tsx", "\u{e7ba}"), // "" ("ttc", "\u{f031}"), // "" ("ttf", "\u{f031}"), // "" ("t", "\u{e769}"), // "" ("twig", "\u{e61c}"), // "" ("txt", "\u{f15c}"), // "" ("unity", "\u{e721}"), // "" ("unity32", "\u{e721}"), // "" ("video", "\u{f008}"), // "" ("vim", "\u{e62b}"), // "" ("vlc", "\u{f0411}"), // "" ("vtt", "\u{f015e}"), // "" ("vue", "\u{f0844}"), // "" ("wav", "\u{f001}"), // "" ("webm", "\u{f008}"), // "" ("webp", "\u{f1c5}"), // "" ("whl", "\u{f487}"), // "" ("windows", "\u{f17a}"), // "" ("wma", "\u{f001}"), // "" ("wmv", "\u{f008}"), // "" ("woff2", "\u{f031}"), // "" ("woff", "\u{f031}"), // "" ("wpl", "\u{f0411}"), // "" ("xbps", "\u{f187}"), // "" ("xcf", "\u{f1c5}"), // "" ("xls", "\u{f1c3}"), // "" ("xlsx", "\u{f1c3}"), // "" ("xml", "\u{f121}"), // "" ("xul", "\u{f269}"), // "" ("xz", "\u{f410}"), // "" ("yaml", "\u{e60b}"), // "" ("yml", "\u{e60b}"), // "" ("zip", "\u{f410}"), // "" ("zig", "\u{e6a9}"), // "" ("zshrc", "\u{f489}"), // "" ("zsh-theme", "\u{f489}"), // "" ("zsh", "\u{f489}"), // "" ("zst", "\u{f410}"), // "" ] .iter() .map(|&s| (s.0.to_owned(), s.1.to_owned())) .collect::<HashMap<_, _>>() } } #[cfg(test)] mod tests { use super::IconTheme; use crate::theme::Theme; fn partial_default_yaml() -> &'static str { r#"--- name: .trash: .cargo: .emacs.d: a.out: extension: go: hs: rs: filetype: dir: file: pipe: socket: executable: symlink-dir: symlink-file: device-char: device-block: special: "# } fn check_partial_yaml(def: &IconTheme, yaml: &IconTheme) { assert_eq!(def.filetype.dir, yaml.filetype.dir,); } #[test] fn test_default_theme() { let def = IconTheme::default(); let yaml = Theme::with_yaml(partial_default_yaml()).unwrap(); check_partial_yaml(&def, &yaml); } #[test] fn test_tmp_partial_default_theme_file() { use std::fs::File; use std::io::Write; let dir = assert_fs::TempDir::new().unwrap(); let theme = dir.path().join("icon.yaml"); let mut file = File::create(&theme).unwrap(); writeln!(file, "{}", partial_default_yaml()).unwrap(); let def = IconTheme::default(); let decoded = Theme::from_path(theme.to_str().unwrap()).unwrap(); check_partial_yaml(&def, &decoded); } #[test] fn test_empty_theme_return_default() { // Must contain one field at least // ref https://github.com/dtolnay/serde-yaml/issues/86 let empty: IconTheme = Theme::with_yaml(" ").unwrap(); let default = IconTheme::default(); check_partial_yaml(&empty, &default); } #[test] fn test_partial_theme_return_default() { // Must contain one field at least // ref https://github.com/dtolnay/serde-yaml/issues/86 let empty: IconTheme = Theme::with_yaml("filetype:\n dir: ").unwrap(); // is the default value let default = IconTheme::default(); check_partial_yaml(&empty, &default); } #[test] fn test_serde_dir_from_yaml() { // Must contain one field at least // ref https://github.com/dtolnay/serde-yaml/issues/86 let empty: IconTheme = Theme::with_yaml("filetype:\n dir: ").unwrap(); assert_eq!(empty.filetype.dir, ""); } #[test] fn test_custom_icon_by_name() { // When a user sets to use 📦-icon for a cargo.toml file, let theme: IconTheme = Theme::with_yaml("name:\n cargo.toml: 📦").unwrap(); // 📦-icon should be used for a cargo.toml file. assert_eq!(theme.name.get("cargo.toml").unwrap(), "📦"); } #[test] fn test_default_icon_by_name_with_custom_entry() { // When a user sets to use 📦-icon for a cargo.toml file, let theme: IconTheme = Theme::with_yaml("name:\n cargo.toml: 📦").unwrap(); // the default icon should be used for a cargo.lock file. assert_eq!(theme.name.get("cargo.lock").unwrap(), "\u{e68b}"); } #[test] fn test_custom_icon_by_extension() { // When a user sets to use 🦀-icon for *.rs files, let theme: IconTheme = Theme::with_yaml("extension:\n rs: 🦀").unwrap(); // 🦀-icon should be used for *.rs files. assert_eq!(theme.extension.get("rs").unwrap(), "🦀"); } #[test] fn test_default_icon_by_extension_with_custom_entry() { // When a user sets to use 🦀-icon for *.rs files, let theme: IconTheme = Theme::with_yaml("extension:\n rs: 🦀").unwrap(); // the default icon should be used for *.go files. assert_eq!(theme.extension.get("go").unwrap(), "\u{e627}"); } } 07070100000051000041ED00000000000000000000000266C4C37900000000000000000000000000000000000000000000001000000000lsd-1.1.5/tests07070100000052000081A400000000000000000000000166C4C37900005184000000000000000000000000000000000000001F00000000lsd-1.1.5/tests/integration.rsextern crate assert_cmd; extern crate predicates; use assert_cmd::prelude::*; use assert_fs::prelude::*; use predicates::prelude::*; use std::process::Command; #[cfg(unix)] use std::os::unix::fs; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; #[test] fn test_runs_okay() { cmd().assert().success(); } #[test] fn test_list_empty_directory() { cmd() .arg("--ignore-config") .arg(tempdir().path()) .assert() .stdout(predicate::eq("")); } #[test] fn test_list_almost_all_empty_directory() { let matched = ""; cmd() .arg("--almost-all") .arg("--ignore-config") .arg(tempdir().path()) .assert() .stdout(predicate::eq(matched)); cmd() .arg("-A") .arg("--ignore-config") .arg(tempdir().path()) .assert() .stdout(predicate::eq(matched)); } #[test] fn test_list_all_empty_directory() { let matched = "\\.\n\\.\\.\n$"; cmd() .arg("--all") .arg("--ignore-config") .arg(tempdir().path()) .assert() .stdout(predicate::str::is_match(matched).unwrap()); cmd() .arg("-a") .arg("--ignore-config") .arg(tempdir().path()) .assert() .stdout(predicate::str::is_match(matched).unwrap()); } #[test] fn test_list_populated_directory() { let dir = tempdir(); dir.child("one").touch().unwrap(); dir.child("two").touch().unwrap(); cmd() .arg("--ignore-config") .arg(dir.path()) .assert() .stdout(predicate::str::is_match("one\ntwo\n$").unwrap()); } #[test] fn test_list_almost_all_populated_directory() { let dir = tempdir(); dir.child("one").touch().unwrap(); dir.child("two").touch().unwrap(); cmd() .arg("--almost-all") .arg("--ignore-config") .arg(dir.path()) .assert() .stdout(predicate::str::is_match("one\ntwo\n$").unwrap()); } #[test] fn test_list_all_populated_directory() { let dir = tempdir(); dir.child("one").touch().unwrap(); dir.child("two").touch().unwrap(); cmd() .arg("--all") .arg("--ignore-config") .arg(dir.path()) .assert() .stdout(predicate::str::is_match("\\.\n\\.\\.\none\ntwo\n$").unwrap()); } #[test] fn test_almost_sort_with_folder() { let tmp = tempdir(); tmp.child("z").create_dir_all().unwrap(); tmp.child("z/a").touch().unwrap(); cmd() .current_dir(tmp.path()) .arg("-a") .arg("--ignore-config") .arg("z") .assert() .stdout(predicate::str::is_match("\\.\n\\.\\.\na\n$").unwrap()); } #[test] fn test_list_inode_populated_directory() { let dir = tempdir(); dir.child("one").touch().unwrap(); dir.child("two").touch().unwrap(); #[cfg(windows)] let matched = "- one\n\\- two\n$"; #[cfg(unix)] let matched = "\\d+ +one\n\\d+ +two\n$"; cmd() .arg("--inode") .arg("--ignore-config") .arg(dir.path()) .assert() .stdout(predicate::str::is_match(matched).unwrap()); cmd() .arg("-i") .arg("--ignore-config") .arg(dir.path()) .assert() .stdout(predicate::str::is_match(matched).unwrap()); } #[test] fn test_list_block_inode_populated_directory_without_long() { let dir = tempdir(); dir.child("one").touch().unwrap(); dir.child("two").touch().unwrap(); #[cfg(windows)] let matched = "- one\n\\- two\n$"; #[cfg(unix)] let matched = "\\d+ +one\n\\d+ +two\n$"; cmd() .arg("--blocks") .arg("inode,name") .arg("--ignore-config") .arg(dir.path()) .assert() .stdout(predicate::str::is_match(matched).unwrap()); } #[test] fn test_list_block_inode_populated_directory_with_long() { let dir = tempdir(); dir.child("one").touch().unwrap(); dir.child("two").touch().unwrap(); #[cfg(windows)] let matched = "- one\n\\- two\n$"; #[cfg(unix)] let matched = "\\d+ +one\n\\d+ +two\n$"; cmd() .arg("--long") .arg("--blocks") .arg("inode,name") .arg("--ignore-config") .arg(dir.path()) .assert() .stdout(predicate::str::is_match(matched).unwrap()); } #[test] fn test_list_inode_with_long_ok() { let dir = tempdir(); cmd() .arg("-i") .arg("-l") .arg("--ignore-config") .arg(dir.path()) .assert() .success(); } #[cfg(unix)] #[test] fn test_list_broken_link_ok() { let dir = tempdir(); let broken_link = dir.path().join("broken-softlink"); let matched = "No such file or directory"; fs::symlink("not-existed-file", &broken_link).unwrap(); cmd() .arg(&broken_link) .arg("--ignore-config") .assert() .stderr(predicate::str::contains(matched).not()); cmd() .arg("-l") .arg("--ignore-config") .arg(broken_link) .assert() .stderr(predicate::str::contains(matched).not()); } // ls link // should show dir content #[cfg(unix)] #[test] fn test_nosymlink_on_non_long() { let dir = tempdir(); dir.child("target").child("inside").touch().unwrap(); let link = dir.path().join("link"); let link_icon = "⇒"; fs::symlink("target", &link).unwrap(); cmd() .arg("--ignore-config") .arg(&link) .assert() .stdout(predicate::str::contains(link_icon).not()); } // ls -l link // should show the link itself #[cfg(unix)] #[test] fn test_symlink_on_long() { let dir = tempdir(); dir.child("target").child("inside").touch().unwrap(); let link = dir.path().join("link"); let link_icon = "⇒"; fs::symlink("target", &link).unwrap(); cmd() .arg("-l") .arg("--ignore-config") .arg(&link) .assert() .stdout(predicate::str::contains(link_icon)); } #[cfg(unix)] #[test] fn test_dereference_link_right_type_and_no_link() { let dir = tempdir(); dir.child("target").touch().unwrap(); let link = dir.path().join("link"); let file_type = ".rw"; let link_icon = "⇒"; fs::symlink("target", &link).unwrap(); cmd() .arg("-l") .arg("--dereference") .arg("--ignore-config") .arg(&link) .assert() .stdout(predicate::str::starts_with(file_type)) .stdout(predicate::str::contains(link_icon).not()); cmd() .arg("-l") .arg("-L") .arg("--ignore-config") .arg(link) .assert() .stdout(predicate::str::starts_with(file_type)) .stdout(predicate::str::contains(link_icon).not()); } #[cfg(unix)] #[test] fn test_dereference_link_broken_link() { let dir = tempdir(); let link = dir.path().join("link"); fs::symlink("target", &link).unwrap(); cmd() .arg("-l") .arg("--dereference") .arg("--ignore-config") .arg(&link) .assert() .stderr(predicate::str::contains("No such file or directory")); cmd() .arg("-l") .arg("-L") .arg("--ignore-config") .arg(link) .assert() .stderr(predicate::str::contains("No such file or directory")); } #[test] fn test_dereference_link_broken_link_output() { let dir = tempdir(); let link = dir.path().join("link"); let target = dir.path().join("target"); #[cfg(unix)] fs::symlink(target, &link).unwrap(); // this needs to be tested on Windows // likely to fail because of permission issue // see https://doc.rust-lang.org/std/os/windows/fs/fn.symlink_file.html #[cfg(windows)] std::os::windows::fs::symlink_file(target, &link).expect("failed to create broken symlink"); cmd() .arg("-l") .arg("--dereference") .arg("--ignore-config") .arg(&link) .assert() .stdout(predicate::str::starts_with("l????????? ? ? ? ?")); cmd() .arg("-l") .arg("-L") .arg("--ignore-config") .arg(link) .assert() .stdout(predicate::str::starts_with("l????????? ? ? ? ?")); } /// should work both tty available and not #[cfg(unix)] #[test] fn test_show_folder_content_of_symlink() { let dir = tempdir(); dir.child("target").child("inside").touch().unwrap(); let link = dir.path().join("link"); fs::symlink("target", &link).unwrap(); cmd() .arg("--ignore-config") .arg(link) .assert() .stdout(predicate::str::starts_with("link").not()) .stdout(predicate::str::starts_with("inside")); } /// ls -l link /// should show the link itself #[cfg(unix)] #[test] fn test_no_show_folder_content_of_symlink_for_long() { let dir = tempdir(); dir.child("target").child("inside").touch().unwrap(); let link = dir.path().join("link"); fs::symlink("target", &link).unwrap(); cmd() .arg("-l") .arg("--ignore-config") .arg(link) .assert() .stdout(predicate::str::starts_with("lrw")) .stdout(predicate::str::contains("⇒")); } /// ls -l link/ /// should show the dir content #[cfg(unix)] #[test] fn test_show_folder_content_of_symlink_for_long_tail_slash() { let dir = tempdir(); dir.child("target").child("inside").touch().unwrap(); let link = dir.path().join("link"); fs::symlink("target", link).unwrap(); cmd() .arg("-l") .arg("--ignore-config") .arg(dir.path().join("link/")) .assert() .stdout(predicate::str::starts_with(".rw")) .stdout(predicate::str::contains("⇒").not()); } #[cfg(unix)] #[test] fn test_show_folder_of_symlink_for_long_multi() { let dir = tempdir(); dir.child("target").child("inside").touch().unwrap(); let link = dir.path().join("link"); fs::symlink("target", link).unwrap(); cmd() .arg("-l") .arg("--ignore-config") .arg(dir.path().join("link/")) .arg(dir.path().join("link")) .assert() .stdout(predicate::str::starts_with("lrw")) .stdout(predicate::str::contains("link:").not()) // do not show dir content when no / .stdout(predicate::str::contains("link/:")); } #[test] fn test_version_sort() { let dir = tempdir(); dir.child("0.3.7").touch().unwrap(); dir.child("0.11.5").touch().unwrap(); dir.child("11a").touch().unwrap(); dir.child("0.2").touch().unwrap(); dir.child("0.11").touch().unwrap(); dir.child("1").touch().unwrap(); dir.child("11").touch().unwrap(); dir.child("2").touch().unwrap(); dir.child("22").touch().unwrap(); cmd() .arg("-v") .arg("--ignore-config") .arg(dir.path()) .assert() .stdout( predicate::str::is_match("0.2\n0.3.7\n0.11\n0.11.5\n1\n2\n11\n11a\n22\n$").unwrap(), ); } #[test] fn test_version_sort_overwrite_by_timesort() { let dir = tempdir(); dir.child("2").touch().unwrap(); dir.child("11").touch().unwrap(); cmd() .arg("-v") .arg("-t") .arg("--ignore-config") .arg(dir.path()) .assert() .stdout(predicate::str::is_match("11\n2\n$").unwrap()); } #[test] fn test_version_sort_overwrite_by_sizesort() { use std::fs::File; use std::io::Write; let dir = tempdir(); dir.child("2").touch().unwrap(); let larger = dir.path().join("11"); let mut larger_file = File::create(larger).unwrap(); writeln!(larger_file, "this is larger").unwrap(); cmd() .arg("-v") .arg("-S") .arg("--ignore-config") .arg(dir.path()) .assert() .stdout(predicate::str::is_match("11\n2\n$").unwrap()); } #[cfg(target_os = "linux")] fn bad_utf8(tmp: &std::path::Path, pre: &str, suf: &str) -> String { let mut fname = format!("{}/{}", tmp.display(), pre).into_bytes(); fname.reserve(2 + suf.len()); fname.push(0xa7); fname.push(0xfd); fname.extend(suf.as_bytes()); unsafe { String::from_utf8_unchecked(fname) } } #[test] #[cfg(target_os = "linux")] fn test_bad_utf_8_extension() { use std::fs::File; let tmp = tempdir(); let fname = bad_utf8(tmp.path(), "bad.extension", ""); File::create(fname).expect("failed to create file"); cmd() .arg(tmp.path()) .arg("--ignore-config") .assert() .stdout(predicate::str::is_match("bad.extension\u{fffd}\u{fffd}\n$").unwrap()); } #[test] #[cfg(target_os = "linux")] fn test_bad_utf_8_name() { use std::fs::File; let tmp = tempdir(); let fname = bad_utf8(tmp.path(), "bad-name", ".ext"); File::create(fname).expect("failed to create file"); cmd() .arg(tmp.path()) .arg("--ignore-config") .assert() .stdout(predicate::str::is_match("bad-name\u{fffd}\u{fffd}.ext\n$").unwrap()); } #[test] fn test_tree() { let tmp = tempdir(); tmp.child("one").touch().unwrap(); tmp.child("one.d").create_dir_all().unwrap(); tmp.child("one.d/two").touch().unwrap(); cmd() .arg(tmp.path()) .arg("--tree") .arg("--ignore-config") .assert() .stdout(predicate::str::is_match("├── one\n└── one.d\n └── two\n$").unwrap()); } #[test] fn test_tree_all_not_show_self() { let tmp = tempdir(); tmp.child("one").touch().unwrap(); tmp.child("one.d").create_dir_all().unwrap(); tmp.child("one.d/two").touch().unwrap(); tmp.child("one.d/.hidden").touch().unwrap(); cmd() .arg(tmp.path()) .arg("--tree") .arg("--all") .arg("--ignore-config") .assert() .stdout( predicate::str::is_match("├── one\n└── one.d\n ├── .hidden\n └── two\n$") .unwrap(), ); } #[test] fn test_tree_show_edge_before_name() { let tmp = tempdir(); tmp.child("one.d").create_dir_all().unwrap(); tmp.child("one.d/two").touch().unwrap(); cmd() .arg(tmp.path()) .arg("--tree") .arg("--long") .arg("--ignore-config") .assert() .stdout(predicate::str::is_match("└── two\n$").unwrap()); } #[test] fn test_tree_d() { let tmp = tempdir(); tmp.child("one").touch().unwrap(); tmp.child("two").touch().unwrap(); tmp.child("one.d").create_dir_all().unwrap(); tmp.child("one.d/one").touch().unwrap(); tmp.child("one.d/one.d").create_dir_all().unwrap(); tmp.child("two.d").create_dir_all().unwrap(); cmd() .arg(tmp.path()) .arg("--tree") .arg("-d") .arg("--ignore-config") .assert() .stdout(predicate::str::is_match("├── one.d\n│ └── one.d\n└── two.d\n$").unwrap()); } #[cfg(unix)] #[test] fn test_tree_no_dereference() { let tmp = tempdir(); tmp.child("one.d").create_dir_all().unwrap(); tmp.child("one.d/samplefile").touch().unwrap(); let link = tmp.path().join("link"); fs::symlink("one.d", link).unwrap(); cmd() .arg("--tree") .arg("--ignore-config") .arg(tmp.path()) .assert() .stdout( predicate::str::is_match("├── link ⇒ one.d\n└── one.d\n └── samplefile\n$").unwrap(), ); } #[cfg(unix)] #[test] fn test_tree_dereference() { let tmp = tempdir(); tmp.child("one.d").create_dir_all().unwrap(); tmp.child("one.d/samplefile").touch().unwrap(); let link = tmp.path().join("link"); fs::symlink("one.d", link).unwrap(); cmd() .arg("--ignore-config") .arg(tmp.path()) .arg("--tree") .arg("-L") .assert() .stdout( predicate::str::is_match( "├── link\n│ └── samplefile\n└── one.d\n └── samplefile\n$", ) .unwrap(), ); } fn cmd() -> Command { Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap() } fn tempdir() -> assert_fs::TempDir { assert_fs::TempDir::new().unwrap() } #[cfg(unix)] #[test] fn test_lower_case_name_icon_match() { let dir = tempdir(); dir.child(".trash").touch().unwrap(); let test_file = dir.path().join(".trash"); cmd() .arg("--icon") .arg("always") .arg("--ignore-config") .arg(test_file) .assert() .stdout(predicate::str::contains("\u{f1f8}")); } #[cfg(unix)] #[test] fn test_upper_case_name_icon_match() { let dir = tempdir(); dir.child(".TRASH").touch().unwrap(); let test_file = dir.path().join(".TRASH"); cmd() .arg("--icon") .arg("always") .arg("--ignore-config") .arg(test_file) .assert() .stdout(predicate::str::contains("\u{f1f8}")); } #[cfg(unix)] #[test] fn test_lower_case_ext_icon_match() { let dir = tempdir(); dir.child("test.7z").touch().unwrap(); let test_file = dir.path().join("test.7z"); cmd() .arg("--icon") .arg("always") .arg("--ignore-config") .arg(test_file) .assert() .stdout(predicate::str::contains("\u{f410}")); } #[cfg(unix)] #[test] fn test_upper_case_ext_icon_match() { let dir = tempdir(); dir.child("test.7Z").touch().unwrap(); let test_file = dir.path().join("test.7Z"); cmd() .arg("--icon") .arg("always") .arg("--ignore-config") .arg(test_file) .assert() .stdout(predicate::str::contains("\u{f410}")); } #[cfg(unix)] #[test] fn test_truncate_owner() { let dir = tempdir(); dir.child("foo").touch().unwrap(); cmd() .arg("-l") .arg("--ignore-config") .arg("--truncate-owner-after") .arg("1") .arg("--truncate-owner-marker") .arg("…") .arg(dir.path()) .assert() .stdout(predicate::str::is_match(" .… .… ").unwrap()); } #[cfg(unix)] #[test] fn test_custom_config_file_parsing() { let dir = tempdir(); dir.child("config.yaml").write_str("layout: tree").unwrap(); dir.child("folder").create_dir_all().unwrap(); dir.child("folder/file").touch().unwrap(); let custom_config = dir.path().join("config.yaml"); cmd() .arg("--config-file") .arg(custom_config) .arg(dir.child("folder").path()) .assert() .stdout(predicate::str::is_match("folder\n└── file").unwrap()); } #[test] fn test_cannot_access_file_exit_status() { let dir = tempdir(); let does_not_exist = dir.path().join("does_not_exist"); let status = cmd() .arg("-l") .arg("--ignore-config") .arg(does_not_exist) .status() .unwrap() .code() .unwrap(); assert_eq!(status, 2) } #[cfg(unix)] #[test] fn test_cannot_access_subdir_exit_status() { let tmp = tempdir(); let readonly = std::fs::Permissions::from_mode(0o400); tmp.child("d/subdir/onemore").create_dir_all().unwrap(); std::fs::set_permissions(tmp.child("d").path().join("subdir"), readonly).unwrap(); let status = cmd() .arg("--tree") .arg("--ignore-config") .arg(tmp.child("d").path()) .status() .unwrap() .code() .unwrap(); assert_eq!(status, 1) } #[test] fn test_date_custom_format_supports_nanos_with_length() { let dir = tempdir(); dir.child("one").touch().unwrap(); dir.child("two").touch().unwrap(); cmd() .arg("-l") .arg("--date") .arg("+testDateFormat%.3f") .arg("--ignore-config") .arg(dir.path()) .assert() .stdout( predicate::str::is_match("testDateFormat\\.[0-9]{3}") .unwrap() .count(2), ); } #[test] fn test_date_custom_format_supports_padding() { let dir = tempdir(); dir.child("one").touch().unwrap(); dir.child("two").touch().unwrap(); cmd() .arg("-l") .arg("--date") .arg("+testDateFormat%_d") .arg("--ignore-config") .arg(dir.path()) .assert() .stdout( predicate::str::is_match("testDateFormat[\\s0-9]{2}") .unwrap() .count(2), ); } #[test] fn test_all_directory() { let dir = tempdir(); dir.child("one").touch().unwrap(); dir.child("two").touch().unwrap(); cmd() .arg("-a") .arg("-d") .arg("--ignore-config") .arg(dir.path()) .assert() .stdout(predicate::str::is_match(".").unwrap()); } #[test] fn test_multiple_files() { let dir = tempdir(); dir.child("one").touch().unwrap(); dir.child("two").touch().unwrap(); cmd() .arg(dir.path().join("one")) .arg(dir.path().join("two")) .assert() .stdout(predicate::str::is_match(".").unwrap()); } 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!1124 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor