diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 8ac6b8c..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "monthly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d63075..160d9ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,92 +1,93 @@ -name: CI on: push: branches: - - main - - "v*.x" + - master pull_request: schedule: - cron: '30 3 * * 2' - workflow_dispatch: + +name: CI jobs: + test: name: run tests strategy: matrix: - platform: [ubuntu-latest, macos-latest, windows-latest, windows-11-arm] - toolchain: [stable, 1.88.0] # MSRV - include: - - platform: windows-latest - exe_suffix: .exe - - platform: windows-11-arm - exe_suffix: .exe + platform: [ubuntu-latest, macos-latest, windows-latest] + rust: [1.54, stable] runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v7 - - uses: dtolnay/rust-toolchain@master + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 with: - toolchain: ${{ matrix.toolchain }} - - run: mkdir artifacts + toolchain: ${{ matrix.rust }} + override: true - name: Build with default features - run: | - cargo build --locked - cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-default${{ matrix.exe_suffix}} - - name: Build with logging and Rustls with webpki roots - run: | - cargo build --locked --features logging,rustls-with-webpki-roots --no-default-features - cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-logging-rustls-webpki${{ matrix.exe_suffix}} - - name: Build with native TLS backend - run: | - # expects runners have the proper Native SSL library - cargo build --locked --features native-tls --no-default-features - cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-native-tls${{ matrix.exe_suffix}} - - uses: actions/upload-artifact@v7 + uses: actions-rs/cargo@v1 with: - name: tldr-debug-build-${{ matrix.platform }}-rust-${{ matrix.toolchain }} - path: artifacts/ + command: build + - name: Build with all features + uses: actions-rs/cargo@v1 + with: + command: build + args: --all-features - name: Run tests - run: cargo test --locked -- --test-threads 1 + uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features clippy: name: run clippy lints runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: 1.88.0 # MSRV - components: clippy - - name: run clippy lints - run: cargo clippy --locked --all-targets --features logging + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: clippy + override: true + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features fmt: name: run rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - components: rustfmt - - name: run rustfmt - run: cargo fmt --all -- --check + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.54 + override: true + - run: rustup component add rustfmt + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check docs: name: build docs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 - - run: ./scripts/get-mdbook.sh - - name: Setup toolchain - uses: dtolnay/rust-toolchain@master + - uses: actions/checkout@v2 + - name: Setup mdBook + uses: peaceiris/actions-mdbook@v1 with: - toolchain: stable + mdbook-version: '0.4.4' + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true - name: Build - run: cargo build --locked + uses: actions-rs/cargo@v1 + with: + command: build - name: Ensure that docs can be built - run: ./mdbook build docs + run: cd docs && mdbook build - name: Generate usage string - run: cargo run --locked -- --help > docs/src/usage-actual.txt + run: cargo run -- --help > docs/src/usage-actual.txt - name: Ensure that usage string is up to date run: diff docs/src/usage{,-actual}.txt diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 0000000..35f17b3 --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,25 @@ +name: github pages + +on: + push: + branches: + - master + +jobs: + deploy: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - name: Setup mdBook + uses: peaceiris/actions-mdbook@v1 + with: + mdbook-version: '0.4.4' + + - run: cd docs && mdbook build + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs/book diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index d5def74..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,169 +0,0 @@ -name: Release -on: - push: - tags: - - "v*" # push events to matching v*, i.e. v1.0, v20.15.10 - -jobs: - create-release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - name: Create release for tag - if: startsWith(github.ref, 'refs/tags/') - run: | - source ./scripts/upload-asset.sh - # Create: - create_release ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} "Tealdeer version ${GITHUB_REF#refs/*/v}.\n\nFor the full changelog, see https://github.com/tealdeer-rs/tealdeer/blob/main/CHANGELOG.md.\n\nBinaries were generated automatically in CI, and are therefore unsigned. For a fully trusted release, please build from source." - - upload-completions: - needs: - - create-release - runs-on: ubuntu-latest - strategy: - matrix: - target: ["bash", "fish", "zsh"] - steps: - - uses: actions/checkout@v7 - - name: Upload completion - if: startsWith(github.ref, 'refs/tags/') - run: | - source ./scripts/upload-asset.sh - # Upload: - upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} completion/${{ matrix.target }}_tealdeer completions_${{ matrix.target }} - - upload-license: - needs: - - create-release - runs-on: ubuntu-latest - strategy: - matrix: - target: ["MIT", "APACHE"] - steps: - - uses: actions/checkout@v7 - - name: Upload license - if: startsWith(github.ref, 'refs/tags/') - run: | - source ./scripts/upload-asset.sh - # Upload: - upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} LICENSE-${{ matrix.target }} LICENSE-${{ matrix.target }}.txt - - build-linux: - runs-on: ubuntu-latest - strategy: - matrix: - include: - - arch: "x86_64" - libc: "musl" - - arch: "aarch64" - libc: "musl" - - arch: "i686" - libc: "musl" - - arch: "armv7" - libc: "musleabihf" - - arch: "arm" - libc: "musleabi" - - arch: "arm" - libc: "musleabihf" - steps: - - uses: actions/checkout@v7 - - name: Pull Docker image - run: docker pull messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} - - name: Build in Docker - run: docker run --rm -i -v "$(pwd)":/home/rust/src messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} cargo build --locked --release - - name: Strip binary - run: docker run --rm -i -v "$(pwd)":/home/rust/src messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} musl-strip -s /home/rust/src/target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr - - uses: actions/upload-artifact@v7 - with: - name: "tealdeer-linux-${{ matrix.arch }}-${{ matrix.libc }}" - path: "target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr" - - build-macos: - runs-on: macos-latest - strategy: - matrix: - include: - - arch: "x86_64" - - arch: "aarch64" - steps: - - uses: actions/checkout@v7 - - name: Setup toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - targets: "${{ matrix.arch }}-apple-darwin" - - name: Build - run: cargo build --locked --release --target ${{ matrix.arch }}-apple-darwin - - uses: actions/upload-artifact@v7 - with: - name: "tealdeer-macos-${{ matrix.arch }}" - path: "target/${{ matrix.arch }}-apple-darwin/release/tldr" - - build-windows: - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - arch: "x86_64" - os: windows-latest - - arch: "aarch64" - os: windows-11-arm - steps: - - uses: actions/checkout@v7 - - name: Setup toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - targets: "${{ matrix.arch }}-pc-windows-msvc" - - name: Build - run: cargo build --locked --release --target ${{ matrix.arch }}-pc-windows-msvc - - uses: actions/upload-artifact@v7 - with: - name: "tealdeer-windows-${{ matrix.arch }}-msvc" - path: "target/${{ matrix.arch }}-pc-windows-msvc/release/tldr.exe" - - upload-release: - needs: - - create-release - - build-linux - - build-macos - - build-windows - runs-on: ubuntu-latest - strategy: - matrix: - target: - - linux-x86_64-musl - - linux-aarch64-musl - - linux-i686-musl - - linux-armv7-musleabihf - - linux-arm-musleabi - - linux-arm-musleabihf - - macos-x86_64 - - macos-aarch64 - - windows-x86_64-msvc - - windows-aarch64-msvc - steps: - - uses: actions/checkout@v7 - - uses: actions/download-artifact@v8 - - name: Upload binary - if: startsWith(github.ref, 'refs/tags/') - run: | - source ./scripts/upload-asset.sh - - # Move/rename file - mkdir out && cd out - if [[ "${{ matrix.target }}" == *windows* ]]; then - src="../tealdeer-${{ matrix.target }}/tldr.exe" - filename="tealdeer-${{ matrix.target }}.exe" - else - src="../tealdeer-${{ matrix.target }}/tldr" - filename="tealdeer-${{ matrix.target }}" - fi - cp $src $filename - - # Create checksum - sha256sum "$filename" > "$filename.sha256" - - # Upload: - upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} $filename $filename - upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} $filename.sha256 $filename.sha256 diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index 127a954..0000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,7 +0,0 @@ - version: 2 - - build: - os: ubuntu-26.04 - commands: - - ./scripts/get-mdbook.sh - - ./mdbook build docs --dest-dir $READTHEDOCS_OUTPUT/html diff --git a/CHANGELOG.md b/CHANGELOG.md index 9784245..14718ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,403 +10,16 @@ Possible log types: - `[removed]` for deprecated features removed in this release. - `[fixed]` for any bug fixes. - `[security]` to invite users to upgrade in case of vulnerabilities. -- `[docs]` for documentation changes. -- `[chore]` for maintenance work. - -### [v1.5.1][v1.5.1], [v1.6.2][v1.6.2], [v1.7.3][v1.7.3] (2026-01-25) - -Today I am releasing three patch updates for outdated versions of tealdeer. -They are minimal patches for Linux distributions that ship old versions of -tealdeer which recently broke due to an upstream change. If you can choose -freely which version of tealdeer to use, I recommend using the latest version of -tealdeer, 1.8.1. For more details, see the "Notes to package maintainers" -section below. - -All three updates contain only a single change compared to their respective -previous versions which changes the `ARCHIVE_URL` constant used for updating the -page cache. The reason for this change is that the upstream tldr-pages -repository shut down the domain that clients were previously required to use. - -Note that this issue is already fixed in tealdeer 1.8.0 where we introduced a -config file option for changing the URL used at runtime. The versions 1.8.0 and -1.8.1 also use the new domain of the tldr-pages archive by default, so no action -is needed for users of those versions. - -#### Changes - -- [fixed] Update `ARCHIVE_URL` - -#### Notes to package maintainers - -I have _not_ updated the lockfile for any of these releases, so the locked -dependency versions are still the same as they were for the previous release in -the respective v1.x series. Updating the lockfile for tealdeer 1.5.0 to remove -any `cargo audit` warnings while also maintaining compatibility with Rust 1.54 -also brings larger changes through transitive dependencies, which contradicts my -plan to make this update easy to plug into existing build pipelines. - -If you want to build / distribute tealdeer v1.5.1, v1.6.2, or v1.7.3, please use -an up to date Rust toolchain to permit updates to newer versions of (transitive) -dependencies. Do not use the lockfile, instead update to the newest available -dependency versions. - -For the same reason, there are no artifacts attached to the GitHub releases of -these versions. - -### [v1.8.1][v1.8.1] (2025-11-11) - -This patch release tweaks the enabled features for ureq, the library we use to -perform HTTP requests when updating the cache. In particular, support for socks -proxies is now enabled. - -#### Changes: - -- [added] Enable ureq's socks-proxy feature ([#451]) - -### [v1.8.0][v1.8.0] (2025-10-03) - -One year and one day have passed since tealdeer version 1.7.0 was released, so -it's time for an update! Tealdeer 1.8 comes with a complete rewrite of the page -cache and contains many long awaited improvements around it. - -Firstly, tealdeer now supports language-specific downloads. This means that only -the pages matching the configured languages are downloaded when updating the -cache. The languages used for searching pages can be configured separately to -the ones used for updating, so it is possible to download pages in languages -that are not usually queried. - -Next to configuring which languages are used for searching, it is now also -possible to specify which platforms are used in the config file. Importantly, -the default behavior for page search has changed so that all platforms are -searched if no page is found for the platform that tealdeer is running on. To -restore the behavior of tealdeer 1.7, users should set -```toml -[search] -platforms = ["current", "common"] -``` -in their config file. - -Coming back to updating, the default build configuration of tealdeer now -includes multiple TLS backends. This means that tealdeer does not have to be -rebuilt to try out a different TLS backend. The used backend can be chosen in -the config file. By default, tealdeer comes with support for rustls using webpki -certificates or system certificates. Native TLS is supported, but not enabled by -default to avoid build troubles with OpenSSL and musl. - -For details, please refer to the [user documentation]. - -#### Changes: - -- [added] Resolve paths in config `[directories]` relative to the config directory ([#306]) -- [added] Add `common` platform to CLI ([#401]) -- [added] Add configuration option for `archive_source` ([#337]) -- [added] Allows configuring TLS backend ([#386]) -- [added] Add args: `--edit-page` and `--edit-patch` ([#388]) -- [added] Add an option to specify a custom config file to be used ([#422]) -- [added] Upload binaries from build step as artifact ([#423]) -- [added] Add `search.languages` and `updates.download_languages` settings ([#430]) -- [added] Add `search.platforms` config option and search all platforms by default ([#435]) -- [added] Add `display.show_title` option to display command titles in output ([#439]) -- [chore] Various test improvements ([#399]) -- [chore] Add tests for osx/macos alias ([#407]) -- [chore] Move most of `main` to `try_main` ([#400]) -- [chore] Only create a single temporary directory in integration tests ([#411]) -- [chore] Replace reqwest with ureq ([#417]) -- [chore] Introduce Language struct ([#425]) -- [chore] Cache rewrite ([#416]) -- [chore] Allow references in `Config` ([#429]) -- [docs] Highlight code examples in user docs ([#440]) -- [removed] Remove native-tls from default feature set ([#436]) - -#### Contributors to this version: - -- [Christoph Loy][@beatbrot] -- [Erick Guan][@erickguan] -- [@MHS-0][@MHS-0] -- [Matěj Kafka][@MatejKafka] -- [Nachiket Kanore][@nachiketkanore] -- [Niklas Mohrin][@niklasmohrin] -- [Predrag Minic][@mipedja] -- [@hex1c][@hex1c] -- [lyj][@lengyijun] - -Thanks! - -#### Notes to package maintainers - -1. The MSRV has been bumped to 1.85. -2. Consider whether you want to include the `native-tls` feature in your build - of tealdeer. The feature is disabled for the binaries in the GitHub release - because we target musl, but it might work out of the box for your - distribution. -3. We have added the `ignore-online-tests` feature to automatically mark all - tests that require an internet connection as skipped, so you can use this - feature instead of maintaining a list of these tests yourself. - -### [v1.7.2][v1.7.2] (2025-03-18) - -This patch release updates the `zip` dependency to mitigate a potential security -vulnerability. A successful attack against tealdeer users would require -manipulation of the tldr pages archive downloaded during an update. As the -archive is downloaded from a trusted source (the tldr-pages organization), it -seems very unlikely that running a version of tealdeer prior to 1.7.2 poses a -security risk. Nevertheless, it cannot hurt to rule out any chance of an attack -by updating tealdeer to version 1.7.2. - -For more details, please see https://github.com/advisories/GHSA-94vh-gphv-8pm8. - -- [security] Require `zip >= 2.3.0` -- [chore] Run CI on backport branches and on dispatch - -### [v1.7.1][v1.7.1] (2024-11-14) - -This patch release updates the `yansi` dependency to version 1, so that the -previous versions of `yansi` can be removed from the package sets of Linux -distributions. This change should not impact the behavior of tealdeer. - -#### Changes: - -- [chore] Upgrade yansi: 0.5.1 -> 1.0.1 ([#389]) - -#### Contributors to this version: - -- [Blair Noctis][@nc7s] - -Thanks! - -### [v1.7.0][v1.7.0] (2024-10-02) - -It's been 24 months since the last release, time for tealdeer 1.7.0! Thanks to -16 individual contributors, a few nice changes and features are included in -this release. - -One change is that you can **query multiple platforms at once**. For example: - - tldr --platform openbsd --platform linux df - -This will show the `df` page for OpenBSD (if available), followed by Linux (if -available), with fallback to the current platform on which tealdeer runs. - -What's that `openbsd` thing up there? Yes, there's now **support for the BSD -platforms `freebsd`, `netbsd` and `openbsd`**. - -And since we're already talking about platform support: Our **binary releases -now include builds for ARM64 (aka `aarch64`) on macOS (Apple Silicon, M1/M2/M3) -and Linux**. _(Keep in mind that binary releases are generated in CI and are -unsigned. For a trusted build, please compile from source.)_ - -There's also a breaking change for the folks using [custom pages and -patches](https://tealdeer-rs.github.io/tealdeer/usage_custom_pages.html): These -files now use a `.md` extension. Old files will continue to work, but will -result a deprecation warning being printed when used. - -On a personal note, this will be the last release from me -([Danilo](https://github.com/dbrgn/)) as primary maintainer of tealdeer. For -details, see [#376](https://github.com/tealdeer-rs/tealdeer/issues/376). - -#### Changes: - -- [added] Allow querying multiple platforms ([#300]) -- [added] Add BSD platform support ([#354]) -- [added] Allow building with native-tls in addition to rustls ([#303]) -- [changed] Change custom page files to use a `.md` file extension ([#322]) -- [changed] Update to clap v4 for doing command line parsing ([#298]) -- [changed] Performance optimization in LineIterator ([#314]) -- [changed] Performance optimizations by tweaking Cargo flags ([#355]) -- [changed] Include completions in published crate ([#333]) -- [changed] Minimal supported Rust version is now 1.75 ([#298]) -- [fixed] Fix bash/zsh/fish completions when cache is empty ([#327], [#331]) -- [docs] Publish docs only when tagging a release ([#362]) -- [docs] List Scoop and Debian packages ([#305], [#315]) -- [docs] Add "Tips and Tricks" chapter to user manual ([#342]) -- [docs] Various docs improvements ([#293]) -- [chore] Improvements to CI workflows ([#324]) -- [chore] Update Cargo.toml license field following SPDX 2.1 ([#336]) -- [chore] Dependency updates - -#### Contributors to this version: - -- [Adam Henley][@adamazing] -- [Andrea Frigido][@frisoft] -- [Blair Noctis][@nc7s] -- [Danilo Bargen][@dbrgn] -- [Felix Yan][@felixonmars] -- [Iliia Maleki][@iliya-malecki] -- [JJ Style][@jj-style] -- [K.B.Dharun Krishna][@kbdharun] -- [Linus Walker][@Walker-00] -- [Mohit Raj][@agrmohit] -- [Nicolai Fröhlich][@nifr] -- [Niklas Mohrin][@niklasmohrin] -- [@qknogxxb][@qknogxxb] -- [@tveness][@tveness] -- [Y.D.X.][@YDX-2147483647] -- [Zacchary Dempsey-Plante][@zedseven] - -Thanks! - - -### [v1.6.1][v1.6.1] (2022-10-24) - -#### Changes: - -- [fixed] Fix path source for custom pages dir ([#297]) -- [chore] Update dependendencies ([#299]) - -#### Contributors to this version: - -- [Cyrus Yip][@CyrusYip] -- [Danilo Bargen][@dbrgn] - -Thanks! - - -### [v1.6.0][v1.6.0] (2022-10-02) - -It's been 9 months since the last release already! This is not a huge update -feature-wise, but it still contains a few nice new improvements and a few -bugfixes, contributed by 11 different people. The most important new feature is -probably the option to override the cache directory through the config file. -The `TEALDEER_CACHE_DIR` env variable is now deprecated. - -A note to packagers: Shell completions have been moved to the `completion/` -subdirectory! Packaging scripts might need to be updated. - -#### Changes: - -- [added] Allow overriding cache directory through config ([#276]) -- [added] Add `--no-auto-update` CLI flag ([#257]) -- [added] Show note about auto-updates when cache is missing ([#254]) -- [added] Add support for android platform ([#274]) -- [added] Add custom pages to list output ([#285]) -- [fixed] Cache: Return error if HTTP client cannot be created ([#247]) -- [fixed] Handle cache download errors ([#253]) -- [fixed] Do not page output of `tldr --update` ([#231]) -- [fixed] Create macOS release builds with bundled root certificates ([#272]) -- [fixed] Clean up and fix shell completions ([#262]) -- [deprecated] The `TEALDEER_CACHE_DIR` env variable is now deprecated ([#276]) -- [removed] The `--config-path` command was removed, use `--show-paths` instead ([#290]) -- [removed] The `-o/--os` command was removed, use `-p/--platform` instead ([#290]) -- [removed] The `-m/--markdown` command was removed, use `-r/--raw` instead ([#290]) -- [chore] Move shell completion scripts to their own directory ([#259]) -- [chore] Update dependencies ([#271], [#287], [#291]) -- [chore] Use anyhow for error handling ([#249]) -- [chore] Switch to Rust 2021 edition ([#284]) - -#### Contributors to this version: - -- [@bagohart][@bagohart] -- [@cyqsimon][@cyqsimon] -- [Danilo Bargen][@dbrgn] -- [Danny Mösch][@SimplyDanny] -- [Evan Lloyd New-Schmidt][@newsch] -- [Hans Gaiser][@hgaiser] -- [Kian-Meng Ang][@kianmeng] -- [Marcin Puc][@tranzystorek-io] -- [Niklas Mohrin][@niklasmohrin] -- [Olav de Haas][@Olavhaasie] -- [Simon Perdrisat][@gagarine] - -Thanks! - - -### [v1.5.0][v1.5.0] (2021-12-31) - -This is quite a big release with many new features. In the 15 months since the -last release, 59 pull requests from 16 different contributors were merged! - -The highlights: - -- **Custom pages and patches**: You can now create your own local-only tldr - pages. But not just that, you can also extend existing upstream pages with - your own examples. For more details, see - [the docs](https://tealdeer-rs.github.io/tealdeer/usage_custom_pages.html). -- **Change argument parsing from docopt to clap**: We replaced docopt.rs as - argument parsing library with clap v3, resulting in almost 1 MiB smaller - binaries and a 22% speed increase when rendering a tldr page. -- **Multi-language support**: You can now override the language with `-L/--language`. -- **A new `--show-paths` command**: By running `tldr --show-paths`, you can list - the currently used config dir, cache dir, upstream pages dir and custom pages dir. -- **Compliance with the tldr client spec v1.5**: We renamed `-o/--os` to - `-p/--platform` and implemented transparent lowercasing of the page names. -- **Docs**: The README based documentation has reached its limits. There are - now new mdbook based docs over at - [tealdeer-rs.github.io/tealdeer/](https://tealdeer-rs.github.io/tealdeer/), we hope these - make using tealdeer easier. Of course, documentation improvements are - welcome! Also, if you're confused about how to use a certain feature, feel - free to open an issue, this way we can improve the docs. - -Note that the MSRV (Minimal Supported Rust Version) of the project -[changed][i190]: - -> When publishing a tealdeer release, the Rust version required to build it -> should be stable for at least a month. - -#### Changes: - -- [added] Support custom pages and patches ([#142][i142]) -- [added] Multi-language support ([#125][i125], [#161][i161]) -- [added] Add support for ANSI code and RGB colors ([#148][i148]) -- [added] Implement new `--show-paths` command ([#162][i162]) -- [added] Support for italic text styling ([#197][i197]) -- [added] Allow SunOS platform override ([#176][i176]) -- [added] Automatically lowercase page names before lookup ([#227][i227]) -- [added] Add "macos" alias for "osx" ([#215][i215]) -- [fixed] Consider only standalone command names for styling ([#157][i157]) -- [fixed] Fixed and improved zsh completions ([#168][i168]) -- [fixed] Create cache directory path if it does not exist ([#174][i174]) -- [fixed] Use default style if user-defined style is missing ([#210][i210]) -- [changed] Switch from docopt to clap for argument parsing ([#108][i108]) -- [changed] Switch from OpenSSL to Rustls ([#187][i187]) -- [changed] Performance improvements ([#187][i187]) -- [changed] Send all progress logging messages to stderr ([#171][i171]) -- [changed] Rename `-o/--os` to `-p/--platform` ([#217][i217]) -- [changed] Rename `-m/--markdown` to `-r/--raw` ([#108][i108]) -- [deprecated] The `--config-path` command is deprecated, use `--show-paths` instead ([#162][i162]) -- [deprecated] The `-o/--os` command is deprecated, use `-p/--platform` instead ([#217][i217]) -- [deprecated] The `-m/--markdown` command is deprecated, use `-r/--raw` instead ([#108][i108]) -- [docs] New docs at [tealdeer-rs.github.io/tealdeer/](https://tealdeer-rs.github.io/tealdeer/) -- [docs] Add comparative benchmarks with hyperfine ([#163][i163], [README](https://github.com/tealdeer-rs/tealdeer#goals)) -- [chore] Download tldr pages archive from their website, not from GitHub ([#213][i213]) -- [chore] Bump MSRV to 1.54 and change MSRV policy ([#190][i190]) -- [chore] The `master` branch was renamed to `main` -- [chore] All release binaries are now generated in CI. Binaries for macOS and Windows are also provided. ([#240][i240]) -- [chore] Update all dependencies - -#### Contributors to this version: - -- [@bl-ue][@bl-ue] -- [Cameron Tod][@cam8001] -- [Dalton][@dmaahs2017] -- [Danilo Bargen][@dbrgn] -- [Danny Mösch][@SimplyDanny] -- [Marcin Puc][@tranzystorek-io] -- [Michael Cho][@cho-m] -- [MS_Y][@black7375] -- [Niklas Mohrin][@niklasmohrin] -- [Rithvik Vibhu][@rithvikvibhu] -- [rnd][@0ndorio] -- [Sondre Nilsen][@sondr3] -- [Tomás Farías Santana][@tomasfarias] -- [Tsvetomir Bonev][@invakid404] -- [@tveness][@tveness] -- [ギャラ][@laxect] - -Thanks! - -Last but not least, [Niklas Mohrin][@niklasmohrin] has joined the project as -co-maintainer. Thank you for your help! ### [v1.4.1][v1.4.1] (2020-09-04) - [fixed] Syntax error in zsh completion file ([#138][i138]) -#### Contributors to this version: +Contributors to this version: -- [Danilo Bargen][@dbrgn] -- [Bruno A. Muciño][@mucinoab] - [Francesco][@BachoSeven] +- [Bruno A. Muciño][@mucinoab] Thanks! @@ -419,10 +32,9 @@ Thanks! - [changed] Make `--list` option comply with official spec ([#112][i112]) - [changed] Move cache age warning to stderr ([#113][i113]) -#### Contributors to this version: +Contributors to this version: - [Atul Bhosale][@Atul9] -- [Danilo Bargen][@dbrgn] - [Danny Mösch][@SimplyDanny] - [Ilaï Deutel][@ilai-deutel] - [Kornel][@kornelski] @@ -445,16 +57,15 @@ Thanks! - [fixed] Fix Fish autocompletion on macOS ([#87][i87]) - [fixed] Fix compilation on Windows by disabling pager ([#99][i99]) -#### Contributors to this version: +Contributors to this version: -- [Bruno Heridet][@Delapouite] -- [Danilo Bargen][@dbrgn] -- [Hugo Locurcio][@Calinou] -- [Isak Johansson][@Plommonsorbet] -- [James Doyle][@james2doyle] -- [Jesús Trinidad Díaz Ramírez][@jesdazrez] +- [@Calinou][@Calinou] +- [@Delapouite][@Delapouite] +- [@james2doyle][@james2doyle] +- [@jesdazrez][@jesdazrez] - [@korrat][@korrat] -- [Marc-André Renaud][@ma-renaud] +- [@ma-renaud][@ma-renaud] +- [@Plommonsorbet][@Plommonsorbet] Thanks! @@ -471,17 +82,16 @@ Thanks! - [changed] Move to Rust 2018, require Rust 1.32 ([#69][i69] / [#84][i84]) - [fixed] Add (back) support for proxies ([#68][i68]) -#### Contributors to this version: +Contributors to this version: -- [Bar Hatsor][@Bassets] -- [Danilo Bargen][@dbrgn] -- [Gabriel Martinez][@mystal] -- [Ivan Smirnov][@aldanor] -- [Jan Christian Grünhage][@jcgruenhage] -- [Jonathan Dahan][@jedahan] -- [Juan D. Vega][@jdvr] -- [Natalie Pendragon][@natpen] -- [Raphael Das Gupta][@das-g] +- [@aldanor][@aldanor] +- [@Bassets][@Bassets] +- [@das-g][@das-g] +- [@jcgruenhage][@jcgruenhage] +- [@jdvr][@jdvr] +- [@jedahan][@jedahan] +- [@mystal][@mystal] +- [@natpen][@natpen] Thanks! @@ -494,9 +104,8 @@ Thanks! - [changed] Require at least Rust 1.28 to build (previous: 1.19) - [fixed] Fix building on systems with openssl 1.1.1 ([#47][i47]) -#### Contributors to this version: +Contributors to this version: -- [Danilo Bargen][@dbrgn] - [@equal-l2][@equal-l2] - [Jonathan Dahan][@jedahan] - [Lukas Bergdoll][@Voultapher] @@ -527,198 +136,63 @@ Thanks! - First crates.io release -[user documentation]: https://docs.tealdeer.org -[@0ndorio]: https://github.com/0ndorio -[@adamazing]: https://github.com/adamazing -[@agrmohit]: https://github.com/agrmohit [@aldanor]: https://github.com/aldanor [@Atul9]: https://github.com/Atul9 [@BachoSeven]: https://github.com/BachoSeven -[@bagohart]: https://github.com/bagohart [@Bassets]: https://github.com/Bassets -[@black7375]: https://github.com/black7375 -[@bl-ue]: https://github.com/bl-ue [@Calinou]: https://github.com/Calinou -[@cam8001]: https://github.com/cam8001 -[@cho-m]: https://github.com/cho-m -[@cyqsimon]: https://github.com/cyqsimon -[@CyrusYip]: https://github.com/CyrusYip [@das-g]: https://github.com/das-g -[@dbrgn]: https://github.com/dbrgn [@Delapouite]: https://github.com/Delapouite -[@dmaahs2017]: https://github.com/dmaahs2017 [@equal-l2]: https://github.com/equal-l2 -[@felixonmars]: https://github.com/felixonmars -[@frisoft]: https://github.com/frisoft -[@gagarine]: https://github.com/gagarine -[@hgaiser]: https://github.com/hgaiser [@ilai-deutel]: https://github.com/ilai-deutel -[@iliya-malecki]: https://github.com/iliya-malecki -[@invakid404]: https://github.com/invakid404 [@james2doyle]: https://github.com/james2doyle [@jcgruenhage]: https://github.com/jcgruenhage [@jdvr]: https://github.com/jdvr [@jedahan]: https://github.com/jedahan [@jesdazrez]: https://github.com/jesdazrez -[@jj-style]: https://github.com/jj-style -[@kbdharun]: https://github.com/kbdharun -[@kianmeng]: https://github.com/kianmeng [@kornelski]: https://github.com/kornelski [@korrat]: https://github.com/korrat -[@laxect]: https://github.com/laxect [@LovecraftianHorror]: https://github.com/LovecraftianHorror [@ma-renaud]: https://github.com/ma-renaud [@michaeldel]: https://github.com/michaeldel [@mucinoab]: https://github.com/mucinoab [@mystal]: https://github.com/mystal [@natpen]: https://github.com/natpen -[@nc7s]: https://github.com/nc7s -[@newsch]: https://github.com/newsch -[@nifr]: https://github.com/nifr [@niklasmohrin]: https://github.com/niklasmohrin -[@Olavhaasie]: https://github.com/Olavhaasie [@Plommonsorbet]: https://github.com/Plommonsorbet -[@qknogxxb]: https://github.com/qknogxxb -[@rithvikvibhu]: https://github.com/rithvikvibhu [@SimplyDanny]: https://github.com/SimplyDanny -[@sondr3]: https://github.com/sondr3 -[@tomasfarias]: https://github.com/tomasfarias -[@tranzystorek-io]: https://github.com/tranzystorek-io -[@tveness]: https://github.com/tveness [@Voultapher]: https://github.com/Voultapher -[@Walker-00]: https://github.com/Walker-00 -[@YDX-2147483647]: https://github.com/YDX-2147483647 -[@zedseven]: https://github.com/zedseven -[@beatbrot]: https://github.com/beatbrot -[@erickguan]: https://github.com/erickguan -[@MHS-0]: https://github.com/MHS-0 -[@MatejKafka]: https://github.com/MatejKafka -[@nachiketkanore]: https://github.com/nachiketkanore -[@mipedja]: https://github.com/mipedja -[@hex1c]: https://github.com/hex1c -[@lengyijun]: https://github.com/lengyijun -[v1.0.0]: https://github.com/tealdeer-rs/tealdeer/compare/v0.4.0...v1.0.0 -[v1.1.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.0.0...v1.1.0 -[v1.2.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.1.0...v1.2.0 -[v1.3.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.2.0...v1.3.0 -[v1.4.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.3.0...v1.4.0 -[v1.4.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.4.0...v1.4.1 -[v1.5.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.4.1...v1.5.0 -[v1.5.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.5.0...v1.5.1 -[v1.6.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.5.0...v1.6.0 -[v1.6.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.6.0...v1.6.1 -[v1.6.2]: https://github.com/tealdeer-rs/tealdeer/compare/v1.6.1...v1.6.2 -[v1.7.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.6.1...v1.7.0 -[v1.7.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.0...v1.7.1 -[v1.7.2]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.1...v1.7.2 -[v1.7.3]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.2...v1.7.3 -[v1.8.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.2...v1.8.0 -[v1.8.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.8.0...v1.8.1 +[v1.0.0]: https://github.com/dbrgn/tealdeer/compare/v0.4.0...v1.0.0 +[v1.1.0]: https://github.com/dbrgn/tealdeer/compare/v1.0.0...v1.1.0 +[v1.2.0]: https://github.com/dbrgn/tealdeer/compare/v1.1.0...v1.2.0 +[v1.3.0]: https://github.com/dbrgn/tealdeer/compare/v1.2.0...v1.3.0 +[v1.4.0]: https://github.com/dbrgn/tealdeer/compare/v1.3.0...v1.4.0 +[v1.4.1]: https://github.com/dbrgn/tealdeer/compare/v1.4.0...v1.4.1 -[i34]: https://github.com/tealdeer-rs/tealdeer/issues/34 -[i43]: https://github.com/tealdeer-rs/tealdeer/issues/43 -[i44]: https://github.com/tealdeer-rs/tealdeer/issues/44 -[i47]: https://github.com/tealdeer-rs/tealdeer/issues/47 -[i48]: https://github.com/tealdeer-rs/tealdeer/issues/48 -[i57]: https://github.com/tealdeer-rs/tealdeer/issues/57 -[i58]: https://github.com/tealdeer-rs/tealdeer/issues/58 -[i61]: https://github.com/tealdeer-rs/tealdeer/issues/61 -[i68]: https://github.com/tealdeer-rs/tealdeer/issues/68 -[i69]: https://github.com/tealdeer-rs/tealdeer/issues/69 -[i71]: https://github.com/tealdeer-rs/tealdeer/issues/71 -[i75]: https://github.com/tealdeer-rs/tealdeer/issues/75 -[i77]: https://github.com/tealdeer-rs/tealdeer/issues/77 -[i84]: https://github.com/tealdeer-rs/tealdeer/issues/84 -[i86]: https://github.com/tealdeer-rs/tealdeer/issues/86 -[i87]: https://github.com/tealdeer-rs/tealdeer/issues/87 -[i89]: https://github.com/tealdeer-rs/tealdeer/issues/89 -[i95]: https://github.com/tealdeer-rs/tealdeer/issues/95 -[i97]: https://github.com/tealdeer-rs/tealdeer/issues/97 -[i99]: https://github.com/tealdeer-rs/tealdeer/issues/99 -[i108]: https://github.com/tealdeer-rs/tealdeer/pull/108 -[i111]: https://github.com/tealdeer-rs/tealdeer/issues/111 -[i112]: https://github.com/tealdeer-rs/tealdeer/issues/112 -[i113]: https://github.com/tealdeer-rs/tealdeer/issues/113 -[i115]: https://github.com/tealdeer-rs/tealdeer/issues/115 -[i125]: https://github.com/tealdeer-rs/tealdeer/pull/125 -[i138]: https://github.com/tealdeer-rs/tealdeer/issues/138 -[i142]: https://github.com/tealdeer-rs/tealdeer/pull/142 -[i148]: https://github.com/tealdeer-rs/tealdeer/pull/148 -[i157]: https://github.com/tealdeer-rs/tealdeer/pull/157 -[i161]: https://github.com/tealdeer-rs/tealdeer/pull/161 -[i162]: https://github.com/tealdeer-rs/tealdeer/pull/162 -[i163]: https://github.com/tealdeer-rs/tealdeer/pull/163 -[i168]: https://github.com/tealdeer-rs/tealdeer/pull/168 -[i171]: https://github.com/tealdeer-rs/tealdeer/pull/171 -[i174]: https://github.com/tealdeer-rs/tealdeer/pull/174 -[i176]: https://github.com/tealdeer-rs/tealdeer/pull/176 -[i187]: https://github.com/tealdeer-rs/tealdeer/pull/187 -[i190]: https://github.com/tealdeer-rs/tealdeer/issues/190 -[i197]: https://github.com/tealdeer-rs/tealdeer/pull/197 -[i210]: https://github.com/tealdeer-rs/tealdeer/pull/210 -[i213]: https://github.com/tealdeer-rs/tealdeer/pull/213 -[i215]: https://github.com/tealdeer-rs/tealdeer/pull/215 -[i217]: https://github.com/tealdeer-rs/tealdeer/pull/217 -[i227]: https://github.com/tealdeer-rs/tealdeer/pull/227 -[#231]: https://github.com/tealdeer-rs/tealdeer/pull/231 -[i240]: https://github.com/tealdeer-rs/tealdeer/pull/240 -[#247]: https://github.com/tealdeer-rs/tealdeer/pull/247 -[#249]: https://github.com/tealdeer-rs/tealdeer/pull/249 -[#253]: https://github.com/tealdeer-rs/tealdeer/pull/253 -[#254]: https://github.com/tealdeer-rs/tealdeer/pull/254 -[#257]: https://github.com/tealdeer-rs/tealdeer/pull/257 -[#259]: https://github.com/tealdeer-rs/tealdeer/pull/259 -[#262]: https://github.com/tealdeer-rs/tealdeer/pull/262 -[#271]: https://github.com/tealdeer-rs/tealdeer/pull/271 -[#272]: https://github.com/tealdeer-rs/tealdeer/pull/272 -[#274]: https://github.com/tealdeer-rs/tealdeer/pull/274 -[#276]: https://github.com/tealdeer-rs/tealdeer/pull/276 -[#284]: https://github.com/tealdeer-rs/tealdeer/pull/284 -[#285]: https://github.com/tealdeer-rs/tealdeer/pull/285 -[#287]: https://github.com/tealdeer-rs/tealdeer/pull/287 -[#290]: https://github.com/tealdeer-rs/tealdeer/pull/290 -[#291]: https://github.com/tealdeer-rs/tealdeer/pull/291 -[#293]: https://github.com/tealdeer-rs/tealdeer/pull/293 -[#297]: https://github.com/tealdeer-rs/tealdeer/pull/297 -[#298]: https://github.com/tealdeer-rs/tealdeer/pull/298 -[#299]: https://github.com/tealdeer-rs/tealdeer/pull/299 -[#300]: https://github.com/tealdeer-rs/tealdeer/pull/300 -[#303]: https://github.com/tealdeer-rs/tealdeer/pull/303 -[#305]: https://github.com/tealdeer-rs/tealdeer/pull/305 -[#306]: https://github.com/tealdeer-rs/tealdeer/pull/306 -[#314]: https://github.com/tealdeer-rs/tealdeer/pull/314 -[#315]: https://github.com/tealdeer-rs/tealdeer/pull/315 -[#322]: https://github.com/tealdeer-rs/tealdeer/pull/322 -[#324]: https://github.com/tealdeer-rs/tealdeer/pull/324 -[#327]: https://github.com/tealdeer-rs/tealdeer/pull/327 -[#331]: https://github.com/tealdeer-rs/tealdeer/pull/331 -[#333]: https://github.com/tealdeer-rs/tealdeer/pull/333 -[#336]: https://github.com/tealdeer-rs/tealdeer/pull/336 -[#337]: https://github.com/tealdeer-rs/tealdeer/pull/337 -[#342]: https://github.com/tealdeer-rs/tealdeer/pull/342 -[#354]: https://github.com/tealdeer-rs/tealdeer/pull/354 -[#355]: https://github.com/tealdeer-rs/tealdeer/pull/355 -[#362]: https://github.com/tealdeer-rs/tealdeer/pull/362 -[#386]: https://github.com/tealdeer-rs/tealdeer/pull/386 -[#388]: https://github.com/tealdeer-rs/tealdeer/pull/388 -[#389]: https://github.com/tealdeer-rs/tealdeer/pull/389 -[#399]: https://github.com/tealdeer-rs/tealdeer/pull/399 -[#400]: https://github.com/tealdeer-rs/tealdeer/pull/400 -[#401]: https://github.com/tealdeer-rs/tealdeer/pull/401 -[#407]: https://github.com/tealdeer-rs/tealdeer/pull/407 -[#411]: https://github.com/tealdeer-rs/tealdeer/pull/411 -[#416]: https://github.com/tealdeer-rs/tealdeer/pull/416 -[#417]: https://github.com/tealdeer-rs/tealdeer/pull/417 -[#422]: https://github.com/tealdeer-rs/tealdeer/pull/422 -[#423]: https://github.com/tealdeer-rs/tealdeer/pull/423 -[#425]: https://github.com/tealdeer-rs/tealdeer/pull/425 -[#426]: https://github.com/tealdeer-rs/tealdeer/pull/426 -[#429]: https://github.com/tealdeer-rs/tealdeer/pull/429 -[#430]: https://github.com/tealdeer-rs/tealdeer/pull/430 -[#435]: https://github.com/tealdeer-rs/tealdeer/pull/435 -[#436]: https://github.com/tealdeer-rs/tealdeer/pull/436 -[#439]: https://github.com/tealdeer-rs/tealdeer/pull/439 -[#440]: https://github.com/tealdeer-rs/tealdeer/pull/440 -[#451]: https://github.com/tealdeer-rs/tealdeer/pull/451 +[i34]: https://github.com/dbrgn/tealdeer/issues/34 +[i43]: https://github.com/dbrgn/tealdeer/issues/43 +[i44]: https://github.com/dbrgn/tealdeer/issues/44 +[i47]: https://github.com/dbrgn/tealdeer/issues/47 +[i48]: https://github.com/dbrgn/tealdeer/issues/48 +[i57]: https://github.com/dbrgn/tealdeer/issues/57 +[i58]: https://github.com/dbrgn/tealdeer/issues/58 +[i61]: https://github.com/dbrgn/tealdeer/issues/61 +[i68]: https://github.com/dbrgn/tealdeer/issues/68 +[i69]: https://github.com/dbrgn/tealdeer/issues/69 +[i71]: https://github.com/dbrgn/tealdeer/issues/71 +[i75]: https://github.com/dbrgn/tealdeer/issues/75 +[i77]: https://github.com/dbrgn/tealdeer/issues/77 +[i84]: https://github.com/dbrgn/tealdeer/issues/84 +[i86]: https://github.com/dbrgn/tealdeer/issues/86 +[i87]: https://github.com/dbrgn/tealdeer/issues/87 +[i89]: https://github.com/dbrgn/tealdeer/issues/89 +[i95]: https://github.com/dbrgn/tealdeer/issues/95 +[i97]: https://github.com/dbrgn/tealdeer/issues/97 +[i99]: https://github.com/dbrgn/tealdeer/issues/99 +[i111]: https://github.com/dbrgn/tealdeer/issues/111 +[i112]: https://github.com/dbrgn/tealdeer/issues/112 +[i113]: https://github.com/dbrgn/tealdeer/issues/113 +[i115]: https://github.com/dbrgn/tealdeer/issues/115 +[i138]: https://github.com/dbrgn/tealdeer/issues/138 diff --git a/Cargo.lock b/Cargo.lock index 7ad474f..3585ebf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,96 +1,51 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] -name = "adler2" -version = "2.0.1" +name = "adler" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.1.5" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] [[package]] -name = "anstream" -version = "1.0.0" +name = "ansi_term" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", + "winapi", ] [[package]] -name = "anstyle" -version = "1.0.14" +name = "app_dirs2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +checksum = "2dd95d9b31f552568dcad90bb809b795f63795fba32f733eb7435a4d13f5d28f" dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", + "jni", + "ndk-glue", + "winapi", + "xdg", ] [[package]] name = "assert_cmd" -version = "2.2.2" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" +checksum = "e996dc7940838b7ef1096b882e29ec30a3149a3a443cdc8dba19ed382eca1fe2" dependencies = [ - "anstyle", "bstr", - "libc", + "doc-comment", "predicates", "predicates-core", "predicates-tree", @@ -98,22 +53,27 @@ dependencies = [ ] [[package]] -name = "autocfg" -version = "1.5.1" +name = "atty" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "base64" -version = "0.23.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bitflags" @@ -121,108 +81,86 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - [[package]] name = "bstr" -version = "1.13.1" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" dependencies = [ + "lazy_static", "memchr", "regex-automata", - "serde_core", ] [[package]] name = "bumpalo" -version = "3.20.3" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" [[package]] name = "byteorder" -version = "1.5.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.12.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.4.3" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" -dependencies = [ - "find-msvc-tools", - "shlex", -] +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.6.6" +version = "3.0.0-beta.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +checksum = "feff3878564edb93745d58cf63e17b63f24142506e7a20c87a5521ed7bfb1d63" dependencies = [ - "clap_builder", + "bitflags", "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "terminal_size", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim 0.10.0", + "textwrap", ] [[package]] name = "clap_derive" -version = "4.6.4" +version = "3.0.0-beta.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "8b15c6b4f786ffb6192ffe65a36855bc1fc2444bcd0945ae16748dcd6ed7d0d3" dependencies = [ "heck", + "proc-macro-error", "proc-macro2", "quote", - "syn 3.0.3", + "syn", ] -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - [[package]] name = "combine" -version = "4.6.7" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "b2b2f5d0ee456f3928812dfc8c6d9a1d592b98678f6d56db9b0cd2b7bc6c8db5" dependencies = [ "bytes", "memchr", @@ -230,9 +168,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.10.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" dependencies = [ "core-foundation-sys", "libc", @@ -240,69 +178,63 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.7" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" dependencies = [ "cfg-if", ] [[package]] -name = "defmt" -version = "1.1.1" +name = "darling" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" dependencies = [ - "bitflags 1.3.2", - "defmt-macros", + "darling_core", + "darling_macro", ] [[package]] -name = "defmt-macros" -version = "1.1.1" +name = "darling_core" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" dependencies = [ - "defmt-parser", + "fnv", + "ident_case", "proc-macro2", "quote", - "syn 2.0.119", + "strsim 0.9.3", + "syn", ] [[package]] -name = "defmt-parser" -version = "1.0.0" +name = "darling_macro" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" dependencies = [ - "thiserror", + "darling_core", + "quote", + "syn", ] [[package]] -name = "der" -version = "0.8.1" +name = "derivative" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" -dependencies = [ - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -312,34 +244,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] -name = "env_filter" -version = "2.0.0" +name = "dirs" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" dependencies = [ - "log", - "regex", + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "encoding_rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" +dependencies = [ + "cfg-if", ] [[package]] name = "env_logger" -version = "0.11.11" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", + "atty", + "humantime", "log", + "regex", + "termcolor", ] -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - [[package]] name = "errno" version = "0.2.8" @@ -351,16 +308,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "errno-dragonfly" version = "0.1.2" @@ -373,87 +320,118 @@ dependencies = [ [[package]] name = "escargot" -version = "0.5.15" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c3aea32bc97b500c9ca6a72b768a26e558264303d101d3409cf6d57a9ed0cf" +checksum = "9ead7d8a70259beb627c1ffdd19b0372381f247f88e46a3bd52bb797182690b3" dependencies = [ "log", + "once_cell", "serde", "serde_json", ] -[[package]] -name = "etcetera" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" -dependencies = [ - "cfg-if", - "windows-sys 0.61.2", -] - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" - [[package]] name = "filetime" -version = "0.2.29" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" dependencies = [ "cfg-if", "libc", + "redox_syscall", + "winapi", ] -[[package]] -name = "find-msvc-tools" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" - [[package]] name = "flate2" -version = "1.1.9" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" dependencies = [ + "cfg-if", "crc32fast", + "libc", "miniz_oxide", - "zlib-rs", ] [[package]] name = "float-cmp" -version = "0.10.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" dependencies = [ "num-traits", ] [[package]] -name = "foreign-types" -version = "0.3.2" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ - "foreign-types-shared", + "matches", + "percent-encoding", ] [[package]] -name = "foreign-types-shared" -version = "0.1.1" +name = "futures-channel" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" + +[[package]] +name = "futures-io" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11" + +[[package]] +name = "futures-sink" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af" + +[[package]] +name = "futures-task" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" + +[[package]] +name = "futures-util" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" +dependencies = [ + "futures-core", + "futures-io", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] [[package]] name = "getrandom" -version = "0.2.17" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if", "libc", @@ -461,202 +439,319 @@ dependencies = [ ] [[package]] -name = "getrandom" -version = "0.4.3" +name = "h2" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55" dependencies = [ - "cfg-if", - "libc", - "r-efi", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] name = "hashbrown" -version = "0.17.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "heck" -version = "0.5.0" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] [[package]] name = "http" -version = "1.5.0" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" dependencies = [ "bytes", + "fnv", "itoa", ] [[package]] -name = "httparse" -version = "1.10.1" +name = "http-body" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +dependencies = [ + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] [[package]] name = "indexmap" -version = "2.14.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ - "equivalent", + "autocfg", "hashbrown", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "ipnet" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" + +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] [[package]] name = "itoa" -version = "1.0.18" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jiff" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" -dependencies = [ - "defmt", - "jiff-core", - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" -dependencies = [ - "defmt", -] - -[[package]] -name = "jiff-static" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" -dependencies = [ - "jiff-core", - "proc-macro2", - "quote", - "syn 2.0.119", -] +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "jni" -version = "0.22.4" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" dependencies = [ - "cfg-if", + "cesu8", "combine", - "jni-macros", "jni-sys", "log", - "simd_cesu8", "thiserror", "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn 2.0.119", ] [[package]] name = "jni-sys" -version = "0.4.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" dependencies = [ - "jni-sys-macros", + "wasm-bindgen", ] [[package]] -name = "jni-sys-macros" -version = "0.4.1" +name = "lazy_static" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.119", -] +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "f98a04dce437184842841303488f70d0188c5f51437d2a834dc097eafa909a01" [[package]] name = "log" -version = "0.4.33" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" -version = "2.8.3" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" dependencies = [ - "adler2", - "simd-adler32", + "adler", + "autocfg", ] [[package]] -name = "native-tls" -version = "0.2.18" +name = "mio" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" dependencies = [ "libc", "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", + "miow", + "ntapi", + "winapi", ] +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "ndk" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64d6af06fde0e527b1ba5c7b79a6cc89cfc46325b0b2887dffe8f70197e0c3c" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-glue" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e9e94628f24e7a3cb5b96a2dc5683acd9230bf11991c2a1677b87695138420" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" +dependencies = [ + "darling", + "proc-macro-crate 0.1.5", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ndk-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -664,124 +759,119 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] -name = "num-traits" -version = "0.2.19" +name = "ntapi" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", ] [[package]] -name = "once_cell" -version = "1.21.4" +name = "num_cpus" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "openssl" -version = "0.10.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" dependencies = [ - "bitflags 2.13.1", - "cfg-if", - "foreign-types", + "hermit-abi", "libc", - "openssl-macros", - "openssl-sys", ] [[package]] -name = "openssl-macros" -version = "0.1.1" +name = "num_enum" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f" dependencies = [ + "derivative", + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" +dependencies = [ + "proc-macro-crate 1.1.0", "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + [[package]] name = "openssl-probe" -version = "0.2.1" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] -name = "openssl-sys" -version = "0.9.117" +name = "os_str_bytes" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +checksum = "addaa943333a514159c80c97ff4a93306530d965d27e139188283cd13e06a799" dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", + "memchr", ] [[package]] name = "pager" -version = "0.16.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2599211a5c97fbbb1061d3dc751fa15f404927e4846e07c643287d6d1f462880" +checksum = "05c7d08cf0d0b55c4f0ffedb5e06569ea212e85d622975071370393970491968" dependencies = [ - "errno 0.2.8", + "errno", "libc", ] -[[package]] -name = "pem-rfc7468" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" -version = "2.3.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] -name = "pkg-config" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" - -[[package]] -name = "portable-atomic" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" - -[[package]] -name = "portable-atomic-util" +name = "pin-project-lite" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" -dependencies = [ - "portable-atomic", -] +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" [[package]] name = "predicates" -version = "3.1.4" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +checksum = "95e5a7689e456ab905c22c2b48225bb921aba7c8dfa58440d68ba13f6222a715" dependencies = [ - "anstyle", "difflib", "float-cmp", + "itertools", "normalize-line-endings", "predicates-core", "regex", @@ -789,182 +879,263 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.10" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" +checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451" [[package]] name = "predicates-tree" -version = "1.0.13" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +checksum = "338c7be2905b732ae3984a2f40032b5e94fd8f52505b186c7d4d68d193445df7" dependencies = [ "predicates-core", "termtree", ] [[package]] -name = "proc-macro2" -version = "1.0.107" +name = "proc-macro-crate" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" dependencies = [ - "unicode-ident", + "toml", +] + +[[package]] +name = "proc-macro-crate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +dependencies = [ + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +dependencies = [ + "unicode-xid", ] [[package]] name = "quote" -version = "1.0.47" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" dependencies = [ "proc-macro2", ] [[package]] -name = "r-efi" -version = "6.0.0" +name = "rand" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] [[package]] name = "regex" -version = "1.13.1" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ "aho-corasick", "memchr", - "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.18" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.8.11" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bea77bc708afa10e59905c3d4af7c8fd43c9214251673095ff8b14345fcbc5" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] [[package]] name = "ring" -version = "0.17.14" +version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" dependencies = [ "cc", - "cfg-if", - "getrandom 0.2.17", "libc", + "once_cell", + "spin", "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.13.1", - "errno 0.3.14", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", + "web-sys", + "winapi", ] [[package]] name = "rustls" -version = "0.23.43" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84" dependencies = [ "log", - "once_cell", "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", + "sct", + "webpki 0.22.0", ] [[package]] name = "rustls-native-certs" -version = "0.8.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +checksum = "5ca9ebdfa27d3fc180e42879037b5338ab1c040c06affd00d8338598e7800943" dependencies = [ "openssl-probe", - "rustls-pki-types", + "rustls-pemfile", "schannel", "security-framework", ] [[package]] -name = "rustls-pki-types" -version = "1.15.1" +name = "rustls-pemfile" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" dependencies = [ - "zeroize", + "base64", ] [[package]] -name = "rustls-platform-verifier" -version = "0.7.0" +name = "ryu" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" -dependencies = [ - "core-foundation", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] +checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568" [[package]] name = "same-file" @@ -977,20 +1148,31 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.29" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" dependencies = [ - "windows-sys 0.61.2", + "lazy_static", + "winapi", +] + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", ] [[package]] name = "security-framework" -version = "3.7.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" dependencies = [ - "bitflags 2.13.1", + "bitflags", "core-foundation", "core-foundation-sys", "libc", @@ -999,355 +1181,468 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.17.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" dependencies = [ "core-foundation-sys", "libc", ] -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - [[package]] name = "serde" -version = "1.0.229" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.229" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn", ] [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" dependencies = [ "itoa", - "memchr", + "ryu", "serde", - "serde_core", - "zmij", ] [[package]] -name = "serde_spanned" -version = "1.1.1" +name = "serde_urlencoded" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" dependencies = [ - "serde_core", + "form_urlencoded", + "itoa", + "ryu", + "serde", ] [[package]] -name = "shlex" -version = "2.0.1" +name = "slab" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] -name = "simd-adler32" -version = "0.3.10" +name = "socket2" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" - -[[package]] -name = "simd_cesu8" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "socks" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" -dependencies = [ - "byteorder", "libc", "winapi", ] [[package]] -name = "subtle" -version = "2.6.1" +name = "spin" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[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.119" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" dependencies = [ "proc-macro2", "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "unicode-xid", ] [[package]] name = "tealdeer" -version = "1.8.1" +version = "1.4.1" dependencies = [ - "anyhow", + "ansi_term", + "app_dirs2", "assert_cmd", + "atty", "clap", "env_logger", "escargot", - "etcetera", "filetime", "log", "pager", "predicates", + "reqwest", "serde", "serde_derive", "tempfile", "toml", - "ureq", - "yansi", + "walkdir", "zip", ] [[package]] name = "tempfile" -version = "3.27.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys 0.61.2", + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", ] [[package]] -name = "terminal_size" -version = "0.4.4" +name = "termcolor" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" dependencies = [ - "rustix", - "windows-sys 0.61.2", + "winapi-util", ] [[package]] name = "termtree" -version = "0.5.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +checksum = "13a4ec180a2de59b57434704ccfad967f789b12737738798fa08798cd5824c16" + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" [[package]] name = "thiserror" -version = "2.0.20" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.20" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "pin-project-lite", + "winapi", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4baa378e417d780beff82bf54ceb0d195193ea6a00c14e22359e7f39456b5689" +dependencies = [ + "rustls", + "tokio", + "webpki 0.22.0", +] + +[[package]] +name = "tokio-util" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", ] [[package]] name = "toml" -version = "1.1.4+spec-1.1.0" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", + "serde", ] [[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +name = "tower-service" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" dependencies = [ - "serde_core", + "cfg-if", + "pin-project-lite", + "tracing-core", ] [[package]] -name = "toml_parser" -version = "1.1.3+spec-1.1.0" +name = "tracing-core" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" dependencies = [ - "winnow", + "lazy_static", ] [[package]] -name = "toml_writer" -version = "1.1.2+spec-1.1.0" +name = "try-lock" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "unicode-bidi" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "untrusted" -version = "0.9.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] -name = "ureq" -version = "3.4.0" +name = "url" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" dependencies = [ - "base64", - "der", - "flate2", - "log", - "native-tls", + "form_urlencoded", + "idna", + "matches", "percent-encoding", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "socks", - "ureq-proto", - "utf8-zero", - "webpki-root-certs", - "webpki-roots", ] [[package]] -name = "ureq-proto" -version = "0.6.1" +name = "version_check" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613" -dependencies = [ - "base64", - "http", - "httparse", - "log", -] - -[[package]] -name = "utf8-zero" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" [[package]] name = "wait-timeout" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" dependencies = [ "libc", ] [[package]] name = "walkdir" -version = "2.5.0" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" dependencies = [ "same-file", + "winapi", "winapi-util", ] [[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "want" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] [[package]] -name = "webpki-root-certs" -version = "1.0.9" +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ - "rustls-pki-types", + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "web-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", ] [[package]] name = "webpki-roots" -version = "1.0.9" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" dependencies = [ - "rustls-pki-types", + "webpki 0.21.4", ] [[package]] @@ -1368,11 +1663,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.11" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "windows-sys 0.61.2", + "winapi", ] [[package]] @@ -1382,145 +1677,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-link" -version = "0.2.1" +name = "winreg" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ - "windows-targets", + "winapi", ] [[package]] -name = "windows-sys" -version = "0.61.2" +name = "xdg" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "3a23fe958c70412687039c86f578938b4a0bb50ec788e96bce4d6ab00ddd5803" dependencies = [ - "windows-link", + "dirs", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" - -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "zeroize" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" - [[package]] name = "zip" -version = "5.1.1" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532" +checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" dependencies = [ - "arbitrary", + "byteorder", "crc32fast", "flate2", - "indexmap", - "memchr", - "zopfli", -] - -[[package]] -name = "zlib-rs" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zopfli" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", + "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index 3005213..ba56396 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,33 +4,33 @@ authors = [ "Niklas Mohrin ", ] description = "Fetch and show tldr help pages for many CLI commands. Full featured offline client with caching support." -homepage = "https://github.com/tealdeer-rs/tealdeer/" -license = "MIT OR Apache-2.0" +homepage = "https://github.com/dbrgn/tealdeer/" +license = "MIT/Apache-2.0" name = "tealdeer" readme = "README.md" -repository = "https://github.com/tealdeer-rs/tealdeer/" -documentation = "https://docs.tealdeer.org" -version = "1.8.1" -include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"] -rust-version = "1.88" # MSRV -edition = "2024" +repository = "https://github.com/dbrgn/tealdeer/" +documentation = "https://dbrgn.github.io/tealdeer/" +version = "1.4.1" +include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "/bash_tealdeer", "/fish_tealdeer"] +edition = "2018" [[bin]] name = "tldr" path = "src/main.rs" [dependencies] -anyhow = "1" -clap = { version = "4", features = ["std", "derive", "help", "usage", "cargo", "error-context", "color", "wrap_help"], default-features = false } -env_logger = { version = "0.11", optional = true } -etcetera = "0.11.0" +ansi_term = "0.12.0" +app_dirs = { version = "2", package = "app_dirs2" } +atty = "0.2" +clap = { version = "3.0.0-beta.5", features = ["std", "derive", "suggestions" ], default-features = false } +env_logger = { version = "0.9", optional = true } log = "0.4" +reqwest = { version = "0.11.3", features = ["blocking", "rustls-tls", "rustls-tls-native-roots"], default-features = false } serde = "1.0.21" serde_derive = "1.0.21" -ureq = { version = "3.0.8", default-features = false, features = ["gzip", "socks-proxy"] } -toml = "1" -yansi = "1" -zip = { version = "5.1.1", default-features = false, features = ["deflate"] } +toml = "0.5.1" +walkdir = "2.0.1" +zip = { version = "0.5", default-features = false, features = ["deflate"] } [target.'cfg(not(windows))'.dependencies] pager = "0.16" @@ -38,24 +38,12 @@ pager = "0.16" [dev-dependencies] assert_cmd = "2.0.1" escargot = "0.5" -predicates = "3.1.2" +predicates = "2.0.2" tempfile = "3.1.0" filetime = "0.2.10" [features] -# native-tls is not enabled by default, because it is difficult to build for musl -default = ["rustls-with-webpki-roots", "rustls-with-native-roots"] logging = ["env_logger"] -# At least one of variants for `ureq` HTTP client must be selected. -native-tls = ["ureq/native-tls", "ureq/platform-verifier"] -rustls-with-webpki-roots = ["ureq/rustls"] # ureq uses WebPKI roots by default -rustls-with-native-roots = ["ureq/rustls", "ureq/platform-verifier"] - -ignore-online-tests = [] - [profile.release] -strip = true -opt-level = 3 lto = true -codegen-units = 1 diff --git a/README.md b/README.md index e996600..86c82a6 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,12 @@ Rust: Simplified, example based and community-driven man pages. If you pronounce "tldr" in English, it sounds somewhat like "tealdeer". Hence the project name :) In case you're in a hurry and just want to quickly try tealdeer, you can find static -binaries on the [GitHub releases page](https://github.com/tealdeer-rs/tealdeer/releases/)! +binaries on the [GitHub releases page](https://github.com/dbrgn/tealdeer/releases/)! ## Docs (Installing, Usage, Configuration) -User documentation is available at ! +User documentation is available at ! The docs are generated using [mdbook](https://rust-lang.github.io/mdBook/index.html). They can be edited through the markdown files in the `docs/src/` directory. @@ -31,6 +31,7 @@ High level project goals: - [x] Download and cache pages - [x] Don't require a network connection for anything besides updating the cache +- [x] Command line interface similar or equivalent to the [NodeJS client][node-gh] - [x] Comply with the [tldr client specification][client-spec] - [x] Advanced highlighting and configuration - [x] Be fast @@ -38,6 +39,29 @@ High level project goals: A tool like `tldr` should be as frictionless as possible to use and show the output as fast as possible. +We think that `tealdeer` reaches these goals. We put together a (more or less) +reproducible benchmark that compiles a handful of clients from source and +measures the execution times on a cold disk cache. The benchmarking is run in a +Docker container using sharkdp's [`hyperfine`][hyperfine-gh] +([Dockerfile][benchmark-dockerfile]). + +| Client (50 runs, 17.10.2021) | Programming Language | Mean in ms | Deviation in ms | Comments | +| :---: | :---: | :---: | :---: | :---: | +| [`outfieldr`][outfieldr-gh] | Zig | 9.1 | 0.5 | no user configuration | +| `tealdeer` | Rust | 13.2 | 0.5 | | +| [`fast-tldr`][fast-tldr-gh] | Haskell | 17.0 | 0.6 | no example highlighting | +| [`tldr-hs`][hs-gh] | Haskell | 25.1 | 0.5 | no example highlighting | +| [`tldr-bash`][bash-gh] | Bash | 30.0 | 0.8 | | +| [`tldr-c`][c-gh] | C | 38.4 | 1.0 | | +| [`tldr-python-client`][python-gh] | Python | 87.0 | 2.4 | | +| [`tldr-node-client`][node-gh] | JavaScript / NodeJS | 407.1 | 12.9 | | + +As you can see, `tealdeer` is one of the fastest of the tested clients. +However, we strive for useful features and code quality over raw performance, +even if that means that we don't come out on top in this friendly competition. +That said, we are still optimizing the code, for example when the `outfieldr` +developers [suggested to switch][outfieldr-comment-tls] to a native TLS +implementation instead of the native libraries. ## Development @@ -63,22 +87,10 @@ To run lints: $ cargo clean && cargo clippy -### AI Policy - -Using AI is generally discouraged. However, if it is used as part of a contribution, the contributor MUST: - -1. Clearly mark what parts (if any) of a contribution were created with the help of AI tools. This includes issue and pull request comments. -2. Check all output of AI tools before sharing it with others in the tealdeer project. -3. Not post slop, spam, or low quality contributions. This includes pull request descriptions and comments with excessive text and markdown flair. -4. Leave small or easy tasks to new contributors who want to learn without the use of AI. This is to maintain the presence of the `good-first-issue` tag. -5. Be respectful of everyone's time: *maintainers and other contributors will be reviewing your PRs.* - - ## MSRV (Minimally Supported Rust Version) -When publishing a tealdeer release, the Rust version required to build it -should be stable for at least a month. The current MSRV can always be found in -the `rust-version` field in `Cargo.toml`. +When publishing a Tealdeer release, the Rust version required to build it +should be stable for at least a month. ## License @@ -100,10 +112,21 @@ be dual licensed as above, without any additional terms or conditions. Thanks to @severen for coming up with the name "tealdeer"! +[node-gh]: https://github.com/tldr-pages/tldr-node-client +[c-gh]: https://github.com/tldr-pages/tldr-c-client +[hs-gh]: https://github.com/psibi/tldr-hs +[fast-tldr-gh]: https://github.com/gutjuri/fast-tldr +[bash-gh]: https://4e4.win/tldr +[outfieldr-gh]: https://gitlab.com/ve-nt/outfieldr +[python-gh]: https://github.com/tldr-pages/tldr-python-client + +[benchmark-dockerfile]: https://github.com/dbrgn/tealdeer/blob/master/benchmarks/Dockerfile [client-spec]: https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md +[hyperfine-gh]: https://github.com/sharkdp/hyperfine +[outfieldr-comment-tls]: https://github.com/dbrgn/tealdeer/issues/129#issuecomment-833596765 -[github-actions]: https://github.com/tealdeer-rs/tealdeer/actions?query=branch%3Amain -[github-actions-badge]: https://github.com/tealdeer-rs/tealdeer/actions/workflows/ci.yml/badge.svg?branch=main +[github-actions]: https://github.com/dbrgn/tealdeer/actions?query=branch%3Amaster +[github-actions-badge]: https://github.com/dbrgn/tealdeer/workflows/CI/badge.svg [crates-io]: https://crates.io/crates/tealdeer [crates-io-badge]: https://img.shields.io/crates/v/tealdeer.svg diff --git a/RELEASING.md b/RELEASING.md index f182519..28ccc28 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -7,17 +7,13 @@ Run linting: Set variables: $ export VERSION=X.Y.Z - $ export GPG_KEY=20EE002D778AE197EF7D0D2CB993FF98A90C9AB1 + $ export GPG_KEY=EA456E8BAF0109429583EED83578F667F2F3A5FA Update version numbers: $ vim Cargo.toml $ cargo update -p tealdeer -Update docs: - - $ cargo run -- --help > docs/src/usage.txt - Update changelog: $ vim CHANGELOG.md @@ -32,4 +28,6 @@ Publish: $ cargo publish $ git push && git push --tags -Then publish the release on GitHub. +Create release binaries: + + $ ./release-build.sh diff --git a/completion/bash_tealdeer b/bash_tealdeer similarity index 54% rename from completion/bash_tealdeer rename to bash_tealdeer index d5420b6..2a1429d 100644 --- a/completion/bash_tealdeer +++ b/bash_tealdeer @@ -6,7 +6,7 @@ _tealdeer() _init_completion || return case $prev in - -h|--help|-v|--version|-l|--list|-u|--update|--no-auto-update|-c|--clear-cache|--pager|-r|--raw|--show-paths|--seed-config|-q|--quiet) + -h|--help|-v|--version|-l|--list|-u|--update|-c|--clear-cache|-p|--pager|-r|--raw|--show-paths|--seed-config|-q|--quiet) return ;; -f|--render) @@ -14,7 +14,7 @@ _tealdeer() return ;; -p|--platform) - COMPREPLY=( $(compgen -W 'linux macos sunos windows android freebsd netbsd openbsd' -- "${cur}") ) + COMPREPLY=( $(compgen -W 'linux macos sunos windows' -- "${cur}") ) return ;; --color) @@ -27,9 +27,8 @@ _tealdeer() COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) ) return fi - if tldrlist=$(tldr -l 2>/dev/null); then - COMPREPLY=( $(compgen -W '$( echo "$tldrlist" | tr -d , )' -- "${cur}") ) - fi + + COMPREPLY=( $(compgen -W '$( tldr -l | tr -d , )' -- "${cur}") ) } complete -F _tealdeer tldr diff --git a/benchmarks/Dockerfile b/benchmarks/Dockerfile new file mode 100644 index 0000000..d717eba --- /dev/null +++ b/benchmarks/Dockerfile @@ -0,0 +1,139 @@ +# Benchmark Dockerfile for tealdeer +# +# To run the benchmarks, execute +# +# docker build --pull -t tldr-benchmark . +# docker run --privileged --rm -it tldr-benchmark +# +# as root in the directory of this Dockerfile. This will build the compared +# clients and benchmark them with `hyperfine` at the end. +# +# The `--privileged` flag is needed to drop the disk caches before every run. If +# you want to test with hot caches or don't want to use this flag, you will have +# to remove the `--prepare` line from the `hyperfine` command at the end of this +# file and rebuild the image. + +################################################################################ + +FROM rust AS tealdeer-builder + +WORKDIR /build +RUN git clone https://github.com/dbrgn/tealdeer.git \ + && cd tealdeer \ + && cargo build --release \ + && mkdir /build-outputs \ + && cp target/release/tldr /build-outputs/tealdeer + +################################################################################ + +FROM ubuntu:latest AS tldr-c-builder + +WORKDIR /build +RUN apt-get update && apt-get install -y build-essential git && rm -rf /var/lib/apt/lists/* +RUN git clone https://github.com/tldr-pages/tldr-c-client.git \ + && cd tldr-c-client \ + && DEBIAN_FRONTEND=noninteractive ./deps.sh \ + && make \ + && mkdir /build-outputs /deps \ + && cp tldr /build-outputs/tldr-c \ + && cp deps.sh /deps/tldr-c-deps.sh + +################################################################################ + +FROM haskell AS haskell-builder + +WORKDIR /build + +RUN git clone https://github.com/psibi/tldr-hs.git \ + && cd tldr-hs \ + && stack build --install-ghc + +RUN git clone https://github.com/gutjuri/fast-tldr \ + && cd fast-tldr \ + && stack build --install-ghc + +RUN mkdir /build-outputs \ + && find tldr-hs/.stack-work/dist -type f -iname tldr -exec mv '{}' /build-outputs/tldr-hs \; \ + && find fast-tldr/.stack-work/dist -type f -iname tldr -exec mv '{}' /build-outputs/fast-tldr \; + +################################################################################ + +FROM node:slim AS node-builder + +WORKDIR /build-outputs +RUN npm install tldr \ + && cp $(which node) . \ + && echo './node -- ./node_modules/.bin/tldr "$@"' > tldr-node \ + && chmod +x tldr-node + +################################################################################ + +FROM euantorano/zig:0.8.0 AS zig-builder + +WORKDIR /build +RUN apk add git \ + && git clone https://gitlab.com/ve-nt/outfieldr.git \ + && cd outfieldr \ + && git submodule init \ + && git submodule update \ + && zig build -Drelease-safe \ + && mkdir /build-outputs \ + && cp bin/tldr /build-outputs/outfieldr + +################################################################################ + +FROM ubuntu:latest AS benchmark + +ENV LANG="en_US.UTF-8" + +WORKDIR /deps +RUN apt-get update && apt-get install -y wget unzip python3 python3-venv && rm -rf /var/lib/apt/lists/* +COPY --from=tldr-c-builder /deps/* ./ +RUN for file in *; do DEBIAN_FRONTEND=noninteractive sh $file; done + +WORKDIR /clients +COPY --from=tealdeer-builder /build-outputs/* ./ +COPY --from=tldr-c-builder /build-outputs/* ./ +COPY --from=haskell-builder /build-outputs/* ./ +RUN wget -qO tldr-bash https://4e4.win/tldr && chmod +x tldr-bash +COPY --from=node-builder /build-outputs/node /build-outputs/tldr-node ./ +COPY --from=node-builder /build-outputs/node_modules/ ./node_modules/ +COPY --from=zig-builder /build-outputs/* ./ + +# python is really hard to isolate in a package, using pyinstaller didn't really work either, so for now we just use it like this +RUN python3 -m venv tldr-python \ + && cd tldr-python \ + && bash -c 'source bin/activate; pip install wheel; pip install tldr; deactivate' \ + && cd .. \ + && echo '#!/bin/bash' > tldr-python.bash \ + && echo 'source tldr-python/bin/activate; tldr $@' >> tldr-python.bash \ + && chmod +x tldr-python.bash + +# Update all the individual caches +RUN bash -c 'mkdir -p /caches/{tealdeer,tldr-c,tldr-hs,fast-tldr,tldr-bash,tldr-node,tldr-python,outfieldr/.local/share}' \ + && TEALDEER_CACHE_DIR=/caches/tealdeer ./tealdeer -u \ + && TLDR_CACHE_DIR=/caches/tldr-c ./tldr-c -u \ + && XDG_DATA_HOME=/caches/tldr-hs ./tldr-hs -u \ + && XDG_DATA_HOME=/caches/fast-tldr ./fast-tldr -u \ + && XDG_DATA_HOME=/caches/tldr-bash ./tldr-bash -u \ + && HOME=/caches/tldr-node ./tldr-node -u \ + && HOME=/caches/tldr-python ./tldr-python.bash -u \ + && HOME=/caches/outfieldr ./outfieldr -u + +WORKDIR /tools +RUN wget -q https://github.com/sharkdp/hyperfine/releases/download/v1.11.0/hyperfine_1.11.0_amd64.deb && dpkg -i hyperfine_1.11.0_amd64.deb + +ENV PAGE="tar" +WORKDIR /clients +CMD hyperfine \ + --warmup 10 \ + --runs 50 \ + --prepare 'sync; echo 3 | tee /proc/sys/vm/drop_caches' \ + "TEALDEER_CACHE_DIR=/caches/tealdeer ./tealdeer $PAGE" \ + "TLDR_CACHE_DIR=/caches/tldr-c ./tldr-c $PAGE" \ + "XDG_DATA_HOME=/caches/tldr-hs ./tldr-hs $PAGE" \ + "XDG_DATA_HOME=/caches/fast-tldr ./fast-tldr $PAGE" \ + "XDG_DATA_HOME=/caches/tldr-bash TLDR_LESS=0 ./tldr-bash $PAGE" \ + "HOME=/caches/tldr-python ./tldr-python.bash $PAGE" \ + "HOME=/caches/outfieldr ./outfieldr $PAGE" \ + "HOME=/caches/tldr-node ./tldr-node $PAGE" diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..ece14b8 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +msrv = "1.54" diff --git a/completion/fish_tealdeer b/completion/fish_tealdeer deleted file mode 100644 index 69d313f..0000000 --- a/completion/fish_tealdeer +++ /dev/null @@ -1,34 +0,0 @@ -# -# Completions for the tealdeer implementation of tldr -# https://github.com/tealdeer-rs/tealdeer/ -# - -complete -c tldr -s h -l help -d 'Print the help message' -f -complete -c tldr -s v -l version -d 'Show version information' -f -complete -c tldr -s l -l list -d 'List all commands in the cache' -f -complete -c tldr -l edit-page -d 'Edit custom page with `EDITOR`' -f -complete -c tldr -l edit-patch -d 'Edit custom patch with `EDITOR`' -f -complete -c tldr -s f -l render -d 'Render a specific markdown file' -r -complete -c tldr -s p -l platform -d 'Override the operating system' -xa 'linux macos sunos windows android freebsd netbsd openbsd common' -complete -c tldr -s L -l language -d 'Override the language' -x -complete -c tldr -s u -l update -d 'Update the local cache' -f -complete -c tldr -l no-auto-update -d 'If auto update is configured, disable it for this run' -f -complete -c tldr -s c -l clear-cache -d 'Clear the local cache' -f -complete -c tldr -s L -l config-path -d 'Override config file location' -r -complete -c tldr -s L -l override-config -d 'Override config values after reading config file' -x -complete -c tldr -l pager -d 'Use a pager to page output' -f -complete -c tldr -s r -l raw -d 'Display the raw markdown instead of rendering it' -f -complete -c tldr -s q -l quiet -d 'Suppress informational messages' -f -complete -c tldr -l show-paths -d 'Show file and directory paths used by tealdeer' -f -complete -c tldr -l seed-config -d 'Create a basic config' -f -complete -c tldr -l color -d 'Controls when to use color' -xa 'always auto never' -complete -c tldr -l short-options -d 'Display the short variants of placeholders' -f -complete -c tldr -l long-options -d 'Display the long variants of placeholders' -f - -function __tealdeer_entries - if set entries (tldr --list 2>/dev/null) - string replace -a -i -r "\,\s" "\n" $entries - end -end - -complete -f -c tldr -a '(__tealdeer_entries)' diff --git a/docs/book.toml b/docs/book.toml index 025b3f1..ae63f23 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -1,5 +1,6 @@ [book] -authors = ["Danilo Bargen", "Niklas Mohrin"] +authors = ["Danilo Bargen"] language = "en" +multilingual = false src = "src" title = "Tealdeer User Manual" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 4649382..6201678 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -4,11 +4,7 @@ - [Installing](./installing.md) - [Usage](./usage.md) - - [Custom Pages and Patches](./usage_custom_pages.md) - [Configuration](./config.md) - - [Section: \[display\]](./config_display.md) - - [Section: \[style\]](./config_style.md) - - [Section: \[search\]](./config_search.md) - - [Section: \[updates\]](./config_updates.md) - - [Section: \[directories\]](./config_directories.md) -- [Tips and Tricks](./tips_and_tricks.md) + - [display](./config_display.md) + - [style](./config_style.md) + - [updates](./config_updates.md) diff --git a/docs/src/config.md b/docs/src/config.md index de3460e..79afcdf 100644 --- a/docs/src/config.md +++ b/docs/src/config.md @@ -1,39 +1,37 @@ # Configuration -Tealdeer can be customized with a config file in [TOML -format](https://toml.io/) called `config.toml`. +Tealdeer can be customized with a config file called `config.toml`. Creating +the config file can be done manually or with the help of `tldr`: -## Configfile Path + $ tldr --seed-config -The configuration file path follows OS conventions (e.g. -`$XDG_CONFIG_HOME/tealdeer/config.toml` on Linux). The paths can be queried -with the following command: +The configuration file path follows OS conventions. It can be queried with the +following command: -```shell -$ tldr --show-paths -``` - -Creating the config file can be done manually or with the help of `tldr`: - -```shell -$ tldr --seed-config -``` + $ tldr --show-paths On Linux, this will usually be `~/.config/tealdeer/config.toml`. -## Config Example +## Override Config Directory -Here's an example configuration file. Note that this example does not contain -all possible config options. For details on the things that can be configured, -please refer to the subsections of this documentation page -([display](config_display.html), [style](config_style.html), [search](config_search.html), -[updates](config_updates.html) or [directories](config_directories.html)). +The directory where the configuration file resides may be overwritten by the +environment variable `TEALDEER_CONFIG_DIR`. Remember to use an absolute path. +Variable expansion will not be performed on the path. + +## Override Cache Directory + +Similarly, the cache directory where the pages are downloaded to, also follows +OS conventions. On Linux, it will usually be at `~/.cache/tealdeer/`. The path +can be overwritten using the environment variable `TEALDEER_CACHE_DIR`. +Remember to use an absolute path. Variable expansion will not be performed on +the path. + +## Config Example ```toml [display] compact = false use_pager = true -show_title = false [style.command_name] foreground = "red" @@ -51,22 +49,3 @@ underline = true [updates] auto_update = true ``` - -## Override Config Directory - -The directory where the configuration file resides may be overwritten by the -environment variable `TEALDEER_CONFIG_DIR`. Remember to use an absolute path. -Variable expansion will not be performed on the path. - -## Override Config Values - -Individual config values can be overridden using the `--override-config` command -line argument. The overrides take place after reading the user config file, but -before the raw config is evaluated. - -```shell -$ tldr --override-config "display.compact = true" tealdeer -``` - -Each override is of the form ` = ` where `name` is a config key and -`value` is any TOML value. diff --git a/docs/src/config_directories.md b/docs/src/config_directories.md deleted file mode 100644 index 507bd27..0000000 --- a/docs/src/config_directories.md +++ /dev/null @@ -1,29 +0,0 @@ -# Section: \[directories\] - -This section allows overriding some directory paths. - -## `cache_dir` - -Override the cache directory. Remember to use an absolute path. Variable -expansion will not be performed on the path. If the directory does not yet -exist, it will be created. - -```toml -[directories] -cache_dir = "/home/myuser/.tealdeer-cache/" -``` - -If no `cache_dir` is specified, tealdeer will fall back to a location that -follows OS conventions. On Linux, it will usually be at `~/.cache/tealdeer/`. -Use `tldr --show-paths` to show the path that is being used. - -## `custom_pages_dir` - -Set the directory to be used to look up [custom -pages](usage_custom_pages.html). Remember to use an absolute path. Variable -expansion will not be performed on the path. - -```toml -[directories] -custom_pages_dir = "/home/myuser/custom-tldr-pages/" -``` diff --git a/docs/src/config_display.md b/docs/src/config_display.md index 88dad2d..6d16805 100644 --- a/docs/src/config_display.md +++ b/docs/src/config_display.md @@ -1,4 +1,4 @@ -# Section: \[display\] +# display In the `display` section you can configure the output format. @@ -6,10 +6,8 @@ In the `display` section you can configure the output format. Specifies whether the pager should be used by default or not (default `false`). -```toml -[display] -use_pager = true -``` + [display] + use_pager = true When enabled, `less -R` is used as pager. To override the pager command used, set the `PAGER` environment variable. @@ -21,68 +19,5 @@ NOTE: This feature is not available on Windows. Set this to enforce more compact output, where empty lines are stripped out (default `false`). -```toml -[display] -compact = true -``` - -## `show_title` - -Display the command name at the top of the page output (default `false`). - -```toml -[display] -show_title = true -``` - -When enabled, the command name will be displayed at the top of the output, -styled with the `command_name` style configuration. - -## `indent` - -Controls the indentation of the output via two sub-keys. - -### `indent.base` - -Specifies the number of spaces used to indent descriptions, example text, and titles (default `2`). - -```toml -[display.indent] -base = 2 -``` - -### `indent.command` - -Specifies the number of spaces used to indent example code lines (default `6`). - -```toml -[display.indent] -command = 6 -``` - -You can also configure both subkeys in a single line like this: - -```toml -[display] -indent = { - base = 2, - command = 6, -} -``` - -## `placeholder_format` - -Display the short and/or long variants of placeholders, if available. -Possible values: `"short"`, `"long"`, or `"both"` (default `"long"`). -This behavior can be overridden with the `--short-options` and `--long-options` flags. - -```toml -[display] -# Display only short variants -placeholder_format = "short" -``` - -For example, when displaying the builtin page with `tldr tealdeer`, the `-f` / `--render` flag is displayed as follows: -- `-f`, if `placeholder_format = "short"` -- `--render`, if `placeholder_format = "long"` -- `[-f|--render]`, if `placeholder_format = "both"` + [display] + compact = true diff --git a/docs/src/config_search.md b/docs/src/config_search.md deleted file mode 100644 index 28e00d6..0000000 --- a/docs/src/config_search.md +++ /dev/null @@ -1,33 +0,0 @@ -# Section: \[search\] - -This config section is used to configure the page search in the cache. -The settings apply to `tldr ` and `tldr --list`. - -## `languages` - -The list of languages that should be considered when searching. -If unspecified, the list of languages will be inferred from the `LANG` and `LANGUAGE` environment variables. -Either way, the language used can be overwritten using the `--language` command line flag. - -```toml -[search] -# Show pages in German if available, otherwise show in English -languages = ["de", "en"] -``` - -## `platforms` - -The list of platforms that should be considered when searching. -In addition to the platforms listed in the help text of the `--platform` flag, there are two special platforms available: -- `"current"`: equals the platform that tealdeer was compiled for -- `"all"`: adds all remaining platforms to the list - -Tealdeer searches the platforms in order of appearance in this list. -The default list of platforms is `["current", "common", "all"]`. -The list of platforms can be overwritten using the `--platform` command line flag. - -```toml -[search] -# Search for linux and common, and then search windows before trying the remaining platforms -platforms = ["linux", "common", "windows", "all"] -``` diff --git a/docs/src/config_style.md b/docs/src/config_style.md index bbe3bff..38709ee 100644 --- a/docs/src/config_style.md +++ b/docs/src/config_style.md @@ -1,4 +1,4 @@ -# Section: \[style\] +# style Using the config file, the style (e.g. colors or underlines) can be customized. @@ -10,7 +10,7 @@ Using the config file, the style (e.g. colors or underlines) can be customized. - `command_name`: The command name as part of the example code - `example_text`: The text that describes an example - `example_code`: The example itself (except the `command_name` and `example_variable`) -- `example_variable`: The variables (placeholders) in the example +- `example_variable`: The variables in the example ## Attributes @@ -22,26 +22,20 @@ Using the config file, the style (e.g. colors or underlines) can be customized. Colors can be specified in one of three ways: -- Color string (`black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`): +- Color string (`black`, `red`, `green`, `yellow`, `blue`, `purple`, `cyan`, `white`): Example: - ```toml - foreground = "green" - ``` + foreground = "green" -- 256 color ANSI code (*tealdeer v1.5.0+*) +- 256 color ANSI code (*Tealdeer v1.5.0+*) Example: - ```toml - foreground = { ansi = 4 } - ``` + foreground = { ansi = 4 } -- 24-bit RGB color (*tealdeer v1.5.0+*) +- 24-bit RGB color (*Tealdeer v1.5.0+*) Example: - ```toml - background = { rgb = { r = 255, g = 255, b = 255 } } - ``` + background = { rgb = { r = 255, g = 255, b = 255 } } diff --git a/docs/src/config_updates.md b/docs/src/config_updates.md index a8a10c8..ac9a59b 100644 --- a/docs/src/config_updates.md +++ b/docs/src/config_updates.md @@ -1,10 +1,8 @@ -# Section: \[updates\] - -This config section contains settings related to updating the tealdeer cache. +# updates ## Automatic updates -Tealdeer can refresh the cache automatically when it is outdated. This +tealdeer can refresh the cache automatically when it is outdated. This behavior can be configured in the `updates` section and is disabled by default. @@ -13,10 +11,8 @@ default. Specifies whether the auto-update feature should be enabled (defaults to `false`). -```toml -[updates] -auto_update = true -``` + [updates] + auto_update = true ### `auto_update_interval_hours` @@ -24,68 +20,7 @@ Duration, since the last cache update, after which the cache will be refreshed (defaults to 720 hours). This parameter is ignored if `auto_update` is set to `false`. -```toml -[updates] -auto_update = true -auto_update_interval_hours = 24 -``` + [updates] + auto_update = true + auto_update_interval_hours = 24 -### `warn_cache_age` - -Controls when a warning is printed if the cache has not been updated in a while. -By default, the warning is shown once the cache is older than 30 days. Set this -to `"never"` to silence the warning. This is useful if, for some reason, the -modification time does not reflect its actual age. - -```toml -[updates] -warn_cache_age = "never" -``` - -## Download configuration - -### `download_languages` - -The list of languages which should be downloaded when updating. -If unspecified, the languages listed in the `search.languages` setting are used. -Thus, this setting is the most useful to instruct tealdeer to download pages in additional languages that are not searched by default. -Either way, the language used can be overwritten using the `--language` command line flag. - -```toml -[search] -languages = ["de", "en"] - -[updates] -# sometimes I like to read the Italian description -download_languages = ["de", "en", "it"] -``` - -### `archive_source` - -URL for the location of the tldr pages archive. By default the pages are -fetched from the latest `tldr-pages/tldr` GitHub release. - -```toml -[updates] -archive_source = "https://my-company.example.com/tldr/" -``` - -### `tls_backend` - -Specifies which TLS backend to use. Try changing this setting if you encounter certificate errors. - -Available options: -- `rustls-with-native-roots` - [Rustls][rustls] (a TLS library in Rust) with native roots -- `rustls-with-webpki-roots` - Rustls with [WebPKI][rustls-webpki] roots -- `native-tls` - Native TLS - - SChannel on Windows - - Secure Transport on macOS - - OpenSSL on other platforms - -```toml -[updates] -tls_backend = "native-tls" -``` - -[rustls]: https://github.com/rustls/rustls -[rustls-webpki]: https://github.com/rustls/webpki diff --git a/docs/src/installing.md b/docs/src/installing.md index f0ca823..ed16a33 100644 --- a/docs/src/installing.md +++ b/docs/src/installing.md @@ -1,6 +1,6 @@ # Installing -There are a few different ways to install tealdeer: +There are a few different ways to install Tealdeer: - Through [package managers](#package-managers) - Through [static binaries](#static-binaries-linux) @@ -14,61 +14,49 @@ autocompletions](#autocompletion). Tealdeer has been added to a few package managers: -- Arch Linux: [`tealdeer`](https://archlinux.org/packages/extra/x86_64/tealdeer/) -- Debian: [`tealdeer`](https://tracker.debian.org/tealdeer) +- Arch Linux: [`tealdeer`](https://archlinux.org/packages/community/x86_64/tealdeer/) - Fedora: [`tealdeer`](https://src.fedoraproject.org/rpms/rust-tealdeer) - FreeBSD: [`sysutils/tealdeer`](https://www.freshports.org/sysutils/tealdeer/) - Funtoo: [`app-misc/tealdeer`](https://github.com/funtoo/core-kit/tree/1.4-release/app-misc/tealdeer) - Homebrew: [`tealdeer`](https://formulae.brew.sh/formula/tealdeer) -- MacPorts: [`tealdeer`](https://ports.macports.org/port/tealdeer/) - NetBSD: [`sysutils/tealdeer`](https://pkgsrc.se/sysutils/tealdeer) -- Nix: [`tealdeer`](https://search.nixos.org/packages?query=tealdeer) +- Nix: [`tealdeer`](https://nixos.org/nixos/packages.html#tealdeer) - openSUSE: [`tealdeer`](https://software.opensuse.org/package/tealdeer?search_term=tealdeer) -- Scoop: [`tealdeer`](https://github.com/ScoopInstaller/Main/blob/master/bucket/tealdeer.json) - Solus: [`tealdeer`](https://packages.getsol.us/shannon/t/tealdeer/) - Void Linux: [`tealdeer`](https://github.com/void-linux/void-packages/tree/master/srcpkgs/tealdeer) ## Static Binaries (Linux) Static binary builds (currently for Linux only) are available on the -[GitHub releases page](https://github.com/tealdeer-rs/tealdeer/releases). +[GitHub releases page](https://github.com/dbrgn/tealdeer/releases). Simply download the binary for your platform and run it! ## Through `cargo install` Build and install the tool via cargo... -```shell -$ cargo install tealdeer -``` + $ cargo install tealdeer + +*(Note: You might need to install OpenSSL development headers, otherwise you get +a "failed to run custom build command for openssl-sys" error message. The +package is called `libssl-dev` on Ubuntu.)* ## Build From Source -Release build: +Debug build with logging enabled: -```shell -$ cargo build --release -``` + $ cargo build --features logging -Release build with native TLS support: +Release build without logging: -```shell -$ cargo build --release --features native-tls -``` + $ cargo build --release -Debug build with logging support: +To enable the log output, set the `RUST_LOG` env variable: -```shell -$ cargo build --features logging -``` - -(To enable logging at runtime, export the `RUST_LOG=tldr=debug` env variable.) + $ export RUST_LOG=tldr=debug ## Autocompletion -Shell completion scripts are located in the folder `completion`. -Just copy them to their designated location: - -- *Bash*: `cp completion/bash_tealdeer /usr/share/bash-completion/completions/tldr` -- *Fish*: `cp completion/fish_tealdeer ~/.config/fish/completions/tldr.fish` -- *Zsh*: `cp completion/zsh_tealdeer /usr/share/zsh/site-functions/_tldr` +- *Bash*: copy `bash_tealdeer` to `/usr/share/bash-completion/completions/tldr` +- *Fish*: copy `fish_tealdeer` to `~/.config/fish/completions/tldr.fish` +- *Zsh*: copy `zsh_tealdeer` to `/usr/share/zsh/site-functions/_tldr` diff --git a/docs/src/intro.md b/docs/src/intro.md index a90ecec..71ebce0 100644 --- a/docs/src/intro.md +++ b/docs/src/intro.md @@ -1,14 +1,13 @@ # Tealdeer: Introduction -Tealdeer is a very fast implementation of -[tldr](https://github.com/tldr-pages/tldr) in Rust: Simplified, example based -and community-driven man pages. +Tealdeer very fast implementation of [tldr](https://github.com/tldr-pages/tldr) +in Rust: Simplified, example based and community-driven man pages. ![Screenshot](screenshot-default.png) -This documentation shows how to install, use and configure tealdeer. +This documentation shows how to install, use and configure Tealdeer. ## Links -- [GitHub Project Page](https://github.com/tealdeer-rs/tealdeer) +- [GitHub Project Page](https://github.com/dbrgn/tealdeer) - [TLDR Pages Project](https://tldr.sh/) diff --git a/docs/src/screenshot-custom.png b/docs/src/screenshot-custom.png index 3158219..ff0c78d 100644 Binary files a/docs/src/screenshot-custom.png and b/docs/src/screenshot-custom.png differ diff --git a/docs/src/screenshot-default.png b/docs/src/screenshot-default.png index 011284e..8a73de7 100644 Binary files a/docs/src/screenshot-default.png and b/docs/src/screenshot-default.png differ diff --git a/docs/src/tips_and_tricks.md b/docs/src/tips_and_tricks.md deleted file mode 100644 index 29f7216..0000000 --- a/docs/src/tips_and_tricks.md +++ /dev/null @@ -1,52 +0,0 @@ -# Tips and Tricks - -This page features some example use cases of Tealdeer. - -## Showing a random page on shell start - -To display a randomly selected page, you can invoke `tldr` twice: One time to -select a page and a second time to display this page. To randomly select a page, -we use `shuf` from the GNU coreutils: - -```bash -tldr --quiet $(tldr --quiet --list | shuf -n1) -``` - -You can also add the above command to your `.bashrc` (or similar shell -configuration file) to display a random page every time you start a new shell -session. - -## Displaying all pages with their summary - -If you want to extend the output of `tldr --list` with the first line summary of -each page, you can run the following Python script: - -```python -#!/usr/bin/env python3 - -import subprocess - -commands = subprocess.run( - ["tldr", "--quiet", "--list"], - capture_output=True, - encoding="utf-8", -).stdout.splitlines() - -for command in commands: - output = subprocess.run( - ["tldr", "--quiet", command], - capture_output=True, - encoding="utf-8", - ).stdout - description = output.lstrip().split("\n\n")[0] - description = " ".join(description.split()) - print(f"{command} => {description}") -``` - -Note that there are a lot of pages and the script will run Tealdeer once for -every page, so the script may take a couple of seconds to finish. - -## Extending this chapter - -If you have an interesting setup with Tealdeer, feel free to share your -configuration on [our Github repository](https://github.com/tealdeer-rs/tealdeer). diff --git a/docs/src/usage.txt b/docs/src/usage.txt index fa9931e..66c45c3 100644 --- a/docs/src/usage.txt +++ b/docs/src/usage.txt @@ -1,38 +1,32 @@ -tealdeer 1.8.1: A fast TLDR client +tealdeer 1.4.1 + Danilo Bargen , Niklas Mohrin -Usage: tldr [OPTIONS] [COMMAND]... +A fast TLDR client -Arguments: - [COMMAND]... The command to show (e.g. `tar` or `git log`) +USAGE: + tldr [OPTIONS] [COMMAND]... -Options: - -l, --list List all commands in the cache - --edit-page Edit custom page with `EDITOR` - --edit-patch Edit custom patch with `EDITOR` - -f, --render Render a specific markdown file - -p, --platform Override the operating system, can be specified multiple times - in order of preference [possible values: linux, macos, sunos, - windows, android, freebsd, netbsd, openbsd, common] - -L, --language Override the language - -u, --update Update the local cache - --no-auto-update If auto update is configured, disable it for this run - -c, --clear-cache Clear the local cache - --config-path Override config file location - --override-config Override config values after reading config file (example: - `updates.auto_update = true`) - --pager Use a pager to page output - -r, --raw Display the raw markdown instead of rendering it - -q, --quiet Suppress informational messages - --show-paths Show file and directory paths used by tealdeer - --seed-config Create a basic config - --color Control whether to use color [possible values: always, auto, - never] - --short-options Display the short variants of placeholders - --long-options Display the long variants of placeholders - -v, --version Print the version - -h, --help Print help +ARGS: + ... The command to show (e.g. `tar` or `git log`) -To view the user documentation, please visit https://docs.tealdeer.org. +OPTIONS: + -l, --list List all commands in the cache + -f, --render Render a specific markdown file + -p, --platform Override the operating system [possible values: linux, macos, + windows, sunos, all] + -o, --os Deprecated alias of `platform` + -L, --language Override the language + -u, --update Update the local cache + -c, --clear-cache Clear the local cache + --pager Use a pager to page output + -r, --raw Display the raw markdown instead of rendering it + -q, --quiet Suppress informational messages + --show-paths Show file and directory paths used by tealdeer + --config-path Show config file path + --seed-config Create a basic config + --color Control whether to use color [possible values: always, auto, never] + -v, --version Print the version + -h, --help Print help information -To view usage examples, run tldr tldr or tldr tealdeer. +To view the user documentation, please visit https://dbrgn.github.io/tealdeer/. diff --git a/docs/src/usage_custom_pages.md b/docs/src/usage_custom_pages.md deleted file mode 100644 index c73bf90..0000000 --- a/docs/src/usage_custom_pages.md +++ /dev/null @@ -1,58 +0,0 @@ -# Custom Pages and Patches - -> ⚠️ **Breaking change in version 1.7.0:** The file name extension for custom -> pages and patches was changed: -> -> - `.page` → `.page.md` -> - `.patch` → `.patch.md` -> -> If you have custom pages or patches, you need to rename them. - -Tealdeer allows creating new custom pages, overriding existing pages as well as -extending existing pages. - -The directory, where these custom pages and patches can be placed, follows OS -conventions. On Linux for instance, the default location is -`~/.local/share/tealdeer/pages/`. To print the path used on your system, simply -run `tldr --show-paths`. - -The custom pages directory can be [overridden by the config -file](config_directories.html). - -## Custom Pages - -To document internal command line tools, or if you want to replace an existing -tldr page with one that's better suited for you, place a file with the name -`.page.md` in the custom pages directory. When calling `tldr `, -your custom page will be shown instead of the upstream version in the cache. - -Path: - -```plain -$CUSTOM_PAGES_DIR/.page.md -``` - -Example: - -```plain -~/.local/share/tealdeer/pages/ufw.page.md -``` - -## Custom Patches - -Sometimes you don't want to fully replace an existing upstream page, but just -want to extend it with your own examples that you frequently need. In this -case, use a file called `.patch.md`, it will be appended to existing -pages. - -Path: - -```plain -$CUSTOM_PAGES_DIR/.patch.md -``` - -Example: - -```plain -~/.local/share/tealdeer/pages/ufw.patch.md -``` diff --git a/fish_tealdeer b/fish_tealdeer new file mode 100644 index 0000000..ee68e2a --- /dev/null +++ b/fish_tealdeer @@ -0,0 +1,24 @@ +# +# Completions for the tealdeer implementation of tldr +# https://github.com/dbrgn/tealdeer/ +# + +complete -c tldr -s h -l help -d 'Print the help message.' -f +complete -c tldr -s v -l version -d 'Show version information.' -f +complete -c tldr -s l -l list -d 'List all commands in the cache.' -f +complete -c tldr -s f -l render -d 'Render a specific markdown file.' -r +complete -c tldr -s p -l platform -d 'Override the operating system.' -xa 'linux macos sunos windows' +complete -c tldr -s u -l update -d 'Update the local cache.' -f +complete -c tldr -s c -l clear-cache -d 'Clear the local cache.' -f +complete -c tldr -s p -l pager -d 'Use a pager to page output.' -f +complete -c tldr -s r -l raw -d 'Display the raw markdown instead of rendering it.' -f +complete -c tldr -s q -l quiet -d 'Suppress informational messages.' -f +complete -c tldr -l show-paths -d 'Show file and directory paths used by tealdeer.' -f +complete -c tldr -l seed-config -d 'Create a basic config.' -f +complete -c tldr -l color -d 'Controls when to use color.' -xa 'always auto never' + +function __tealdeer_entries + tldr --list | string replace -a -i -r "\,\s" "\n" +end + +complete -f -c tldr -a '(__tealdeer_entries)' diff --git a/pages/tealdeer.md b/pages/tealdeer.md deleted file mode 100644 index 84eef3d..0000000 --- a/pages/tealdeer.md +++ /dev/null @@ -1,42 +0,0 @@ -# tldr - -> This is a builtin page that shows information for your installed tealdeer version. -> More information: . - -> This page shows tealdeer specific functionality. See tldr tldr for more examples. - -- Render a local markdown file as a tldr page: - -`tldr {{[-f|--render]}} {{path/to/file.md}}` - -- Show the raw markdown source of a page instead of rendering it: - -`tldr {{[-r|--raw]}} {{command}}` - -- Show file and directory paths used by tealdeer: - -`tldr --show-paths` - -- Create an initial config file: - -`tldr --seed-config` - -- Override config file location: - -`tldr --config-path ` - -- Open a custom page for a command in `$EDITOR` (creates it if it doesn't exist): - -`tldr --edit-page {{command}}` - -- Open a custom patch for a command in `$EDITOR` (appended to the existing page): - -`tldr --edit-patch {{command}}` - -- Clear the local cache: - -`tldr {{[-c|--clear-cache]}}` - -- If auto update is configured, disable it for this run: - -`tldr --no-auto-update` diff --git a/release-build.sh b/release-build.sh new file mode 100755 index 0000000..26567a4 --- /dev/null +++ b/release-build.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +set -euo pipefail + +VERSION=$(grep '^version = ' Cargo.toml | sed 's/.*"\([0-9\.]*\)".*/\1/') +GPG_KEY=EA456E8BAF0109429583EED83578F667F2F3A5FA + +declare -a targets=( + "x86_64-musl" + "i686-musl" + "armv7-musleabihf" + "arm-musleabi" + "arm-musleabihf" +) + +declare -a rusttargets=( + "x86_64-unknown-linux-musl" + "i686-unknown-linux-musl" + "armv7-unknown-linux-musleabihf" + "arm-unknown-linux-musleabi" + "arm-unknown-linux-musleabihf" +) + +declare -a completions=( + "bash" + "fish" + "zsh" +) + +function docker-download { + echo "==> Downloading Docker image: messense/rust-musl-cross:$1" + docker pull messense/rust-musl-cross:$1 +} + +function docker-build { + echo "==> Building target: $1" + docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:$1 cargo build --release +} + +echo -e "==> Version $VERSION\n" + +for target in ${targets[@]}; do docker-download $target; done +echo "" +for target in ${targets[@]}; do docker-build $target; done +echo "" + +rm -rf "dist-$VERSION" +mkdir "dist-$VERSION" + +for i in ${!targets[@]}; do + echo "==> Copying ${targets[$i]}" + cp "target/${rusttargets[$i]}/release/tldr" "dist-$VERSION/tldr-linux-${targets[$i]}" +done +echo "" + +for target in ${targets[@]}; do + echo "==> Stripping $target" + docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:$target musl-strip -s /home/rust/src/dist-$VERSION/tldr-linux-$target +done +echo "" + +for target in ${targets[@]}; do + echo "==> Signing $target" + gpg -a --output "dist-$VERSION/tldr-linux-$target.sig" --detach-sig "dist-$VERSION/tldr-linux-$target" +done +echo "" + +for completion in ${completions[@]}; do + echo "==> Copying ${completion} completion" + cp "${completion}_tealdeer" "dist-$VERSION/completions_${completion}" +done +echo "" + +echo "==> Copying licenses" +cp LICENSE-* "dist-$VERSION/" + +echo "Done." diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index f857430..0000000 --- a/rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -# Empty file, use defaults and disregard global settings diff --git a/scripts/get-mdbook.sh b/scripts/get-mdbook.sh deleted file mode 100755 index 1d6c5a8..0000000 --- a/scripts/get-mdbook.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -set -ex - -wget -O mdbook.tar.gz https://github.com/rust-lang/mdBook/releases/download/v0.5.4/mdbook-v0.5.4-x86_64-unknown-linux-musl.tar.gz -echo "5222beabd3e37dc5be0d18ff99b79058469354db5c220153a1b92db5ba12be89 mdbook.tar.gz" > sha256sums -sha256sum --check sha256sums -tar xvf mdbook.tar.gz diff --git a/scripts/upload-asset.sh b/scripts/upload-asset.sh deleted file mode 100644 index 4f9de09..0000000 --- a/scripts/upload-asset.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env bash -# -# Upload artifacts to GitHub Actions. -# -# Based on: https://gist.github.com/schell/2fe896953b6728cc3c5d8d5f9f3a17a3 -# -# Requires curl and jq on PATH - -# Args: -# token: GitHub API user token -# repo: GitHub username/reponame -# tag: Name of the tag for which to create a release -# description: Release description -create_release() { - # Args - token=$1 - repo=$2 - tag=$3 - description=$4 - echo "Creating release:" - echo " repo=$repo" - echo " tag=$tag" - echo "" - - # Create release - http_code=$( - curl -s -o create.json -w '%{http_code}' \ - --header "Accept: application/vnd.github.v3+json" \ - --header "Authorization: Bearer $token" \ - --header "Content-Type:application/json" \ - "https://api.github.com/repos/$repo/releases" \ - -d '{"tag_name":"'"$tag"'","name":"'"${tag/v/Version }"'","draft":true,"body":"'"${description/\"/\\\"}"'"}' - ) - if [ "$http_code" == "201" ]; then - echo "Release for tag $tag created." - else - echo "Asset upload failed with code '$http_code'." - return 1 - fi -} - -# Args: -# token: GitHub API user token -# repo: GitHub username/reponame -# tag: Name of the tag for which to upload the assets -# file: Path to the asset file to upload -# name: Name to use for the uploaded asset -upload_release_file() { - # Args - token=$1 - repo=$2 - tag=$3 - file=$4 - name=$5 - echo "Uploading:" - echo " repo=$repo" - echo " tag=$tag" - echo " file=$file" - echo " name=$name" - echo "" - - # Determine upload URL of latest draft release for the specified tag - upload_url=$( - curl -s \ - --header "Accept: application/vnd.github.v3+json" \ - --header "Authorization: Bearer $token" \ - "https://api.github.com/repos/$repo/releases" \ - | jq -r '[.[] | select(.tag_name == "'"$tag"'" and .draft)][0].upload_url' \ - | cut -d"{" -f'1' - ) - echo "Determined upload URL: $upload_url" - http_code=$( - curl -s -o upload.json -w '%{http_code}' \ - --request POST \ - --header "Accept: application/vnd.github.v3+json" \ - --header "Authorization: Bearer $token" \ - --header "Content-Type: application/octet-stream" \ - --data-binary "@$file" "$upload_url?name=$name" - ) - if [ "$http_code" == "201" ]; then - echo "Asset $name uploaded:" - jq -r .browser_download_url upload.json - else - echo "Asset upload failed with code '$http_code':" - cat upload.json - return 1 - fi -} diff --git a/src/cache.rs b/src/cache.rs index 763045e..1d94d89 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,266 +1,39 @@ use std::{ - fs::{self, File}, - io::{Cursor, ErrorKind, Read}, + env, + ffi::OsStr, + fs, + io::{Cursor, Read, Seek}, + iter, path::{Path, PathBuf}, - time::{Duration, SystemTime}, }; -use anyhow::{Context, Result, anyhow, bail, ensure}; -use log::{debug, info}; -use ureq::{ - Agent, - http::StatusCode, - tls::{RootCerts, TlsConfig, TlsProvider}, -}; +use app_dirs::{get_app_root, AppDataType}; +use log::debug; +use reqwest::{blocking::Client, Proxy}; +use std::time::{Duration, SystemTime}; +use walkdir::{DirEntry, WalkDir}; use zip::ZipArchive; use crate::{ - config::{Language, TlsBackend}, - types::PlatformType, + error::TealdeerError::{self, CacheError, UpdateError}, + types::{PathSource, PlatformStrategy, PlatformType}, }; +static CACHE_DIR_ENV_VAR: &str = "TEALDEER_CACHE_DIR"; + pub static TLDR_PAGES_DIR: &str = "tldr-pages"; +static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; -#[derive(Clone)] -pub struct CacheConfig<'a> { - pub pages_directory: &'a Path, - pub custom_pages_directory: Option<&'a Path>, - pub platforms: &'a [PlatformType], - pub search_languages: &'a [Language<'a>], - pub download_languages: &'a [Language<'a>], -} - -/// The directory backing this cache is checked to be populated at construction. -pub struct Cache<'a> { - config: CacheConfig<'a>, +#[derive(Debug)] +pub struct Cache { + url: String, + platform: PlatformStrategy, } #[derive(Debug)] pub struct PageLookupResult { - pub page_path: PathBuf, - pub patch_path: Option, -} - -impl<'a> Cache<'a> { - /// Try opening a cache at the location given by `config.pages_directory`. If no directory - /// exists at this location, `Ok(None)` is returned. - pub fn open(config: CacheConfig<'a>) -> Result> { - match config.pages_directory.metadata() { - Ok(md) => { - ensure!( - md.is_dir(), - "Cache directory `{}` exists, but is not a directory.", - config.pages_directory.display(), - ); - Ok(Some(Cache { config })) - } - Err(err) if err.kind() == ErrorKind::NotFound => Ok(None), - Err(err) => Err(anyhow!(err).context(format!( - "Error getting metdata of cache directory {}", - config.pages_directory.display() - ))), - } - } - - /// Open an existing cache at `config.pages_directory` or create one if no cache resides at - /// this location. In case of success, the return value is a tuple with the `Cache` and a - /// boolean indicating whether the cache was newly created. - pub fn open_or_create(config: CacheConfig<'a>) -> Result<(Self, bool)> { - if let Some(cache) = Self::open(config.clone())? { - return Ok((cache, false)); - } - - fs::create_dir_all(config.pages_directory).with_context(|| { - format!( - "Cache directory `{}` cannot be created", - config.pages_directory.display(), - ) - })?; - eprintln!( - "Successfully created cache directory `{}`.", - config.pages_directory.display(), - ); - - Ok((Cache { config }, true)) - } - - pub fn age(&self) -> Result { - let mtime = self.config.pages_directory.metadata()?.modified()?; - SystemTime::now() - .duration_since(mtime) - .context("Error comparing cache mtime with current time") - } - - pub fn find_page(&self, command: &str) -> Option { - let page_filename = format!("{command}.md"); - let patch_filename = format!("{command}.patch.md"); - let custom_filename = format!("{command}.page.md"); - - if let Some(custom_pages_dir) = self.config.custom_pages_directory { - let custom_page = custom_pages_dir.join(custom_filename); - if custom_page.is_file() { - return Some(PageLookupResult::with_page(custom_page)); - } - } - - let patch_path = self - .config - .custom_pages_directory - .map(|dir| dir.join(&patch_filename)) - .filter(|path| path.is_file()); - - for &platform in self.config.platforms { - for language in self.config.search_languages { - let mut search_path = self.config.pages_directory.to_path_buf(); - search_path.push(language.directory_name()); - search_path.push(platform.directory_name()); - search_path.push(&page_filename); - - if search_path.is_file() { - return Some( - PageLookupResult::with_page(search_path).with_optional_patch(patch_path), - ); - } - } - } - - None - } - - pub fn list_pages(&self) -> Result + use<>> { - let mut pages = Vec::new(); - - let mut append_all = |directory: &Path, suffix: &str| -> Result<()> { - let Ok(file_iter) = fs::read_dir(directory) else { - return Ok(()); - }; - - for entry in file_iter { - let entry = entry?; - if entry.file_type()?.is_file() { - let mut page_path = entry - .file_name() - .into_string() - .map_err(|_| anyhow!("Found invalid filename: {:?}", entry.path()))?; - - if page_path.ends_with(suffix) { - page_path.truncate(page_path.len() - suffix.len()); - pages.push(page_path); - } else { - debug!( - "Skipping page entry not ending in \".md\": {:?}", - entry.path(), - ); - } - } - } - - Ok(()) - }; - - let mut search_path = self.config.pages_directory.to_path_buf(); - for language in self.config.search_languages { - search_path.push(language.directory_name()); - for platform in self.config.platforms { - search_path.push(platform.directory_name()); - append_all(&search_path, ".md")?; - search_path.pop(); - } - search_path.pop(); - } - - if let Some(custom_pages_dir) = self.config.custom_pages_directory { - append_all(custom_pages_dir, ".page.md")?; - } - - pages.sort_unstable(); - pages.dedup(); - Ok(pages) - } - - pub fn old_custom_pages_exist(&self) -> Result { - let Some(directory) = self.config.custom_pages_directory else { - return Ok(false); - }; - let Ok(file_iter) = fs::read_dir(directory) else { - return Ok(false); - }; - - for entry in file_iter { - if let Some(extension) = entry?.path().extension() - && (extension == "page" || extension == "patch") - { - return Ok(true); - } - } - - Ok(false) - } - - pub fn clear(self) -> Result<()> { - fs::remove_dir_all(self.config.pages_directory).with_context(|| { - format!( - "Could not remove pages directory at {}", - self.config.pages_directory.display(), - ) - }) - } - - /// Download archives for the languages in `self.config().download_languages` and replace the - /// pages directory with the newly downloaded pages. As not all languages might have pages - /// available (for example, `en_US` instead of `en`), an iterator yielding all languages which - /// were successfully downloaded is returned. - pub fn update( - &mut self, - archive_url: &str, - tls_backend: TlsBackend, - ) -> Result> + use<'_>> { - let client = Self::build_client(tls_backend); - - // Download everything before deleting anything - let mut archives = self - .config - .download_languages - .iter() - .map(|&lang| { - Ok(( - lang, - Self::download( - &client, - &format!("{archive_url}/tldr-{}.zip", lang.directory_name()), - )? - .map(|bytes| ZipArchive::new(Cursor::new(bytes))) - .transpose()?, - )) - }) - .collect::>>()?; - - // Clear cache directory - // Note: This is not the best solution. Ideally we would download the - // archive to a temporary directory and then swap the two directories. - // But renaming a directory doesn't work across filesystems and Rust - // does not yet offer a recursive directory copying function. So for - // now, we'll use this approach. - fs::remove_dir_all(self.config.pages_directory)?; - fs::create_dir(self.config.pages_directory)?; - - for (lang, archive) in &mut archives { - if let Some(archive) = archive { - info!("Extracting archive for {lang:?}"); - archive.extract(self.config.pages_directory.join(lang.directory_name()))?; - } else { - info!("No archive found for {lang:?}"); - } - } - - Ok(archives - .into_iter() - .filter_map(|(lang, archive)| archive.is_some().then_some(lang))) - } - - pub fn config(&self) -> &CacheConfig<'a> { - &self.config - } + page_path: PathBuf, + patch_path: Option, } impl PageLookupResult { @@ -276,101 +49,311 @@ impl PageLookupResult { self } - /// Create a reader that sequentially reads from the page and the - /// patch, as if they were concatenated. - /// - /// This will return an error if either the page file or the patch file - /// cannot be opened. - pub fn reader(&self) -> Result> { - // Open page file - let page_file = File::open(&self.page_path) - .with_context(|| format!("Could not open page file at {}", self.page_path.display()))?; + pub fn paths(&self) -> impl Iterator { + iter::once(self.page_path.as_path()).chain(self.patch_path.as_deref()) + } +} - // Open patch file - let patch_file_opt = match &self.patch_path { - Some(path) => Some( - File::open(path) - .with_context(|| format!("Could not open patch file at {}", path.display()))?, - ), - None => None, +pub enum CacheFreshness { + /// The cache is still fresh (less than MAX_CACHE_AGE old) + Fresh, + /// The cache is stale and should be updated + Stale(Duration), + /// The cache is missing + Missing, +} + +impl Cache { + pub fn new(url: S, platform: PlatformStrategy) -> Self + where + S: Into, + { + Self { + url: url.into(), + platform, + } + } + + /// Return the path to the cache directory. + pub fn get_cache_dir() -> Result<(PathBuf, PathSource), TealdeerError> { + // Allow overriding the cache directory by setting the env variable. + if let Ok(value) = env::var(CACHE_DIR_ENV_VAR) { + let path = PathBuf::from(value); + let (path_exists, path_is_dir) = path + .metadata() + .map_or((false, false), |md| (true, md.is_dir())); + if path_exists && !path_is_dir { + return Err(CacheError(format!( + "Path specified by ${} is not a directory.", + CACHE_DIR_ENV_VAR + ))); + } + if !path_exists { + // Try to create the complete directory path. + fs::create_dir_all(&path).map_err(|_| { + CacheError(format!( + "Directory path specified by ${} cannot be created.", + CACHE_DIR_ENV_VAR + )) + })?; + eprintln!( + "Successfully created cache directory path `{}`.", + path.to_str().unwrap() + ); + } + return Ok((path, PathSource::EnvVar)); }; - // Create chained reader from file(s) - // - // Note: It might be worthwhile to create our own struct that accepts - // the page and patch files and that will read them sequentially, - // because it avoids the boxing below. However, the performance impact - // would first need to be shown to be significant using a benchmark. - Ok(if let Some(patch_file) = patch_file_opt { - Box::new(page_file.chain(&b"\n"[..]).chain(patch_file)) as Box + // Otherwise, fall back to user cache directory. + match get_app_root(AppDataType::UserCache, &crate::APP_INFO) { + Ok(dirs) => Ok((dirs, PathSource::OsConvention)), + Err(_) => Err(CacheError( + "Could not determine user cache directory.".into(), + )), + } + } + + /// Download the archive + fn download(&self) -> Result, TealdeerError> { + let mut builder = Client::builder(); + if let Ok(ref host) = env::var("HTTP_PROXY") { + if let Ok(proxy) = Proxy::http(host) { + builder = builder.proxy(proxy); + } + } + if let Ok(ref host) = env::var("HTTPS_PROXY") { + if let Ok(proxy) = Proxy::https(host) { + builder = builder.proxy(proxy); + } + } + let client = builder.build().unwrap_or_else(|_| Client::new()); + let mut resp = client.get(&self.url).send()?; + let mut buf: Vec = vec![]; + let bytes_downloaded = resp.copy_to(&mut buf)?; + debug!("{} bytes downloaded", bytes_downloaded); + Ok(buf) + } + + /// Decompress and open the archive + fn decompress(reader: R) -> ZipArchive { + ZipArchive::new(reader).unwrap() + } + + /// Update the pages cache. + pub fn update(&self) -> Result<(), TealdeerError> { + // First, download the compressed data + let bytes: Vec = self.download()?; + + // Decompress the response body into an `Archive` + let mut archive = Self::decompress(Cursor::new(bytes)); + + // Determine paths + let (cache_dir, _) = Self::get_cache_dir()?; + let pages_dir = cache_dir.join(TLDR_PAGES_DIR); + + // Make sure that cache directory exists + debug!("Ensure cache directory {:?} exists", &cache_dir); + fs::create_dir_all(&cache_dir) + .map_err(|e| UpdateError(format!("Could not create cache directory: {}", e)))?; + + // Clear cache directory + // Note: This is not the best solution. Ideally we would download the + // archive to a temporary directory and then swap the two directories. + // But renaming a directory doesn't work across filesystems and Rust + // does not yet offer a recursive directory copying function. So for + // now, we'll use this approach. + Self::clear()?; + + // Extract archive + archive + .extract(&pages_dir) + .map_err(|e| UpdateError(format!("Could not unpack compressed data: {}", e)))?; + + Ok(()) + } + + /// Return the duration since the cache directory was last modified. + pub fn last_update() -> Option { + if let Ok((cache_dir, _)) = Self::get_cache_dir() { + if let Ok(metadata) = fs::metadata(cache_dir.join(TLDR_PAGES_DIR)) { + if let Ok(mtime) = metadata.modified() { + let now = SystemTime::now(); + return now.duration_since(mtime).ok(); + }; + }; + }; + None + } + + /// Return the freshness of the cache (fresh, stale or missing). + pub fn freshness() -> CacheFreshness { + match Cache::last_update() { + Some(ago) if ago > crate::config::MAX_CACHE_AGE => CacheFreshness::Stale(ago), + Some(_) => CacheFreshness::Fresh, + None => CacheFreshness::Missing, + } + } + + /// Return the platform directory. + fn get_platform_dir(&self) -> &'static str { + match self.platform.platform_type { + PlatformType::Linux { .. } => "linux", + PlatformType::OsX { .. } => "osx", + PlatformType::SunOs { .. } => "sunos", + PlatformType::Windows { .. } => "windows", + } + } + + /// Check for pages for a given platform in one of the given languages. + fn find_page_for_platform( + page_name: &str, + cache_dir: &Path, + platform: &str, + language_dirs: &[String], + ) -> Option { + language_dirs + .iter() + .map(|lang_dir| cache_dir.join(lang_dir).join(platform).join(page_name)) + .find(|path| path.exists() && path.is_file()) + } + + /// Look up custom patch (.patch). If it exists, store it in a variable. + fn find_patch(patch_name: &str, custom_pages_dir: Option<&Path>) -> Option { + custom_pages_dir + .map(|custom_dir| custom_dir.join(patch_name)) + .filter(|path| path.exists() && path.is_file()) + } + + /// Search for a page and return the path to it. + pub fn find_page( + &self, + name: &str, + languages: &[String], + custom_pages_dir: Option<&Path>, + ) -> Option { + let page_filename = format!("{}.md", name); + let patch_filename = format!("{}.patch", name); + let custom_filename = format!("{}.page", name); + + // Get cache dir + let cache_dir = match Self::get_cache_dir() { + Ok((cache_dir, _)) => cache_dir.join(TLDR_PAGES_DIR), + Err(e) => { + log::error!("Could not get cache directory: {}", e); + return None; + } + }; + + let lang_dirs: Vec = languages + .iter() + .map(|lang| { + if lang == "en" { + String::from("pages") + } else { + format!("pages.{}", lang) + } + }) + .collect(); + + // Look up custom page (.page). If it exists, return it directly + if let Some(config_dir) = custom_pages_dir { + let custom_page = config_dir.join(custom_filename); + if custom_page.exists() && custom_page.is_file() { + return Some(PageLookupResult::with_page(custom_page)); + } + } + + let patch_path = Self::find_patch(&patch_filename, custom_pages_dir); + + // Try to find a platform specific path next, append custom patch to it. + let platform_dir = self.get_platform_dir(); + if let Some(page) = + Self::find_page_for_platform(&page_filename, &cache_dir, platform_dir, &lang_dirs) + { + return Some(PageLookupResult::with_page(page).with_optional_patch(patch_path)); + } + + // Did not find platform specific results, fall back to "common" + Self::find_page_for_platform(&page_filename, &cache_dir, "common", &lang_dirs) + .map(|page| PageLookupResult::with_page(page).with_optional_patch(patch_path)) + } + + /// Return the available pages. + pub fn list_pages(&self) -> Result, TealdeerError> { + // Determine platforms directory and platform + let (cache_dir, _) = Self::get_cache_dir()?; + let platforms_dir = cache_dir.join(TLDR_PAGES_DIR).join("pages"); + let platform_dir = self.get_platform_dir(); + + // Closure that allows the WalkDir instance to traverse platform + // specific and common page directories, but not others. + let should_walk = |entry: &DirEntry| -> bool { + let file_type = entry.file_type(); + let file_name = match entry.file_name().to_str() { + Some(name) => name, + None => return false, + }; + if file_type.is_dir() { + return file_name == "common" || file_name == platform_dir; + } else if file_type.is_file() { + return true; + } + false + }; + + // Recursively walk through common and (if applicable) platform specific directory + let mut pages = WalkDir::new(platforms_dir) + .min_depth(1) // Skip root directory + .into_iter() + .filter_entry(|e| self.platform.list_all || should_walk(e)) // Filter out pages for other architectures + .filter_map(Result::ok) // Convert results to options, filter out errors + .filter_map(|e| { + let path = e.path(); + let extension = &path.extension().and_then(OsStr::to_str).unwrap_or(""); + if e.file_type().is_file() && extension == &"md" { + path.file_stem() + .and_then(|stem| stem.to_str().map(Into::into)) + } else { + None + } + }) + .collect::>(); + pages.sort(); + pages.dedup(); + Ok(pages) + } + + /// Delete the cache directory. + pub fn clear() -> Result<(), TealdeerError> { + let (path, _) = Self::get_cache_dir()?; + if path.exists() && path.is_dir() { + // Delete old tldr-pages cache location as well if present + // TODO: To be removed in the future + for pages_dir_name in [TLDR_PAGES_DIR, TLDR_OLD_PAGES_DIR] { + let pages_dir = path.join(pages_dir_name); + + if pages_dir.exists() { + fs::remove_dir_all(&pages_dir).map_err(|e| { + CacheError(format!( + "Could not remove cache directory ({}): {}", + pages_dir.display(), + e + )) + })?; + } + } + } else if path.exists() { + return Err(CacheError(format!( + "Cache path ({}) is not a directory.", + path.display() + ))); } else { - Box::new(page_file) as Box - }) - } -} - -impl Language<'_> { - fn directory_name(&self) -> String { - format!("pages.{}", self.0) - } -} - -impl PlatformType { - fn directory_name(self) -> &'static str { - match self { - PlatformType::Linux => "linux", - PlatformType::OsX => "osx", - PlatformType::SunOs => "sunos", - PlatformType::Windows => "windows", - PlatformType::Android => "android", - PlatformType::FreeBsd => "freebsd", - PlatformType::NetBsd => "netbsd", - PlatformType::OpenBsd => "openbsd", - PlatformType::Common => "common", - } - } -} - -impl Cache<'_> { - fn build_client(tls_backend: TlsBackend) -> Agent { - let tls_builder = match tls_backend { - #[cfg(feature = "native-tls")] - TlsBackend::NativeTls => TlsConfig::builder() - .provider(TlsProvider::NativeTls) - .root_certs(RootCerts::PlatformVerifier), - #[cfg(feature = "rustls-with-webpki-roots")] - TlsBackend::RustlsWithWebpkiRoots => TlsConfig::builder() - .provider(TlsProvider::Rustls) - .root_certs(RootCerts::WebPki), - #[cfg(feature = "rustls-with-native-roots")] - TlsBackend::RustlsWithNativeRoots => TlsConfig::builder() - .provider(TlsProvider::Rustls) - .root_certs(RootCerts::PlatformVerifier), + return Err(CacheError(format!( + "Cache path ({}) does not exist.", + path.display() + ))); }; - let config = Agent::config_builder() - .http_status_as_error(false) // because we want to handle them - .tls_config(tls_builder.build()) - .build(); - - config.into() - } - - /// Download the archive from the specified URL. - fn download(client: &Agent, archive_url: &str) -> Result>> { - info!("Downloading archive from {archive_url}"); - let response = client.get(archive_url).call(); - match response { - Ok(response) if response.status().is_success() => { - let mut buf: Vec = Vec::new(); - response.into_body().into_reader().read_to_end(&mut buf)?; - debug!("{} bytes downloaded", buf.len()); - Ok(Some(buf)) - } - Ok(response) if response.status() == StatusCode::NOT_FOUND => Ok(None), - _ => { - bail!("Could not download tldr pages from {archive_url}: {response:?}") - } - } + Ok(()) } } @@ -379,53 +362,21 @@ impl Cache<'_> { mod tests { use super::*; - use std::{ - fs::File, - io::{Read, Write}, - }; - #[test] - fn test_reader_with_patch() { - // Write test files - let dir = tempfile::tempdir().unwrap(); - let page_path = dir.path().join("test.page.md"); - let patch_path = dir.path().join("test.patch.md"); - { - let mut f1 = File::create(&page_path).unwrap(); - f1.write_all(b"Hello\n").unwrap(); - let mut f2 = File::create(&patch_path).unwrap(); - f2.write_all(b"World").unwrap(); - } - - // Create chained reader from lookup result - let lr = PageLookupResult::with_page(page_path).with_optional_patch(Some(patch_path)); - let mut reader = lr.reader().unwrap(); - - // Read into a Vec - let mut buf = Vec::new(); - reader.read_to_end(&mut buf).unwrap(); - - assert_eq!(&buf, b"Hello\n\nWorld"); + fn test_page_lookup_result_iter_with_patch() { + let lookup = PageLookupResult::with_page(PathBuf::from("test.page")) + .with_optional_patch(Some(PathBuf::from("test.patch"))); + let mut iter = lookup.paths(); + assert_eq!(iter.next(), Some(Path::new("test.page"))); + assert_eq!(iter.next(), Some(Path::new("test.patch"))); + assert_eq!(iter.next(), None); } #[test] - fn test_reader_without_patch() { - // Write test file - let dir = tempfile::tempdir().unwrap(); - let page_path = dir.path().join("test.page.md"); - { - let mut f = File::create(&page_path).unwrap(); - f.write_all(b"Hello\n").unwrap(); - } - - // Create chained reader from lookup result - let lr = PageLookupResult::with_page(page_path); - let mut reader = lr.reader().unwrap(); - - // Read into a Vec - let mut buf = Vec::new(); - reader.read_to_end(&mut buf).unwrap(); - - assert_eq!(&buf, b"Hello\n"); + fn test_page_lookup_result_iter_no_patch() { + let lookup = PageLookupResult::with_page(PathBuf::from("test.page")); + let mut iter = lookup.paths(); + assert_eq!(iter.next(), Some(Path::new("test.page"))); + assert_eq!(iter.next(), None); } } diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index 8270a8f..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,124 +0,0 @@ -//! Definition of the CLI arguments and options. - -use std::path::PathBuf; - -use clap::{ArgGroup, Parser, builder::ArgAction}; - -use crate::types::{ColorOptions, PlatformType}; - -// Note: flag names are specified explicitly in clap attributes -// to improve readability and allow contributors to grep names like "clear-cache" -#[derive(Parser, Debug)] -#[command( - about = "A fast TLDR client", - version, - disable_version_flag = true, - author, - help_template = "{before-help}{name} {version}: {about-with-newline}{author-with-newline} -{usage-heading} {usage} - -{all-args}{after-help}", - after_help = "To view the user documentation, please visit https://docs.tealdeer.org. - -To view usage examples, run tldr tldr or tldr tealdeer.", - arg_required_else_help = true, - help_expected = true, - group = ArgGroup::new("command_or_file").args(&["command", "render"]), -)] -pub(crate) struct Cli { - /// The command to show (e.g. `tar` or `git log`) - #[arg(num_args(1..))] - pub command: Vec, - - /// List all commands in the cache - #[arg(short = 'l', long = "list")] - pub list: bool, - - /// Edit custom page with `EDITOR` - #[arg(long, requires = "command")] - pub edit_page: bool, - - /// Edit custom patch with `EDITOR` - #[arg(long, requires = "command", conflicts_with = "edit_page")] - pub edit_patch: bool, - - /// Render a specific markdown file - #[arg( - short = 'f', - long = "render", - value_name = "FILE", - conflicts_with = "command" - )] - pub render: Option, - - /// Override the operating system, can be specified multiple times in order of preference - #[arg( - short = 'p', - long = "platform", - value_name = "PLATFORM", - action = ArgAction::Append, - )] - pub platforms: Option>, - - /// Override the language - #[arg(short = 'L', long = "language")] - pub language: Option, - - /// Update the local cache - #[arg(short = 'u', long = "update")] - pub update: bool, - - /// If auto update is configured, disable it for this run - #[arg(long = "no-auto-update")] - pub no_auto_update: bool, - - /// Clear the local cache - #[arg(short = 'c', long = "clear-cache")] - pub clear_cache: bool, - - /// Override config file location - #[arg(long = "config-path", value_name = "FILE")] - pub config_path: Option, - - /// Override config values after reading config file (example: `updates.auto_update = true`) - #[arg(long, action = ArgAction::Append, value_name = "OVERRIDE")] - pub override_config: Vec, - - /// Use a pager to page output - #[arg(long = "pager", requires = "command_or_file")] - pub pager: bool, - - /// Display the raw markdown instead of rendering it - #[arg(short = 'r', long = "raw", requires = "command_or_file")] - pub raw: bool, - - /// Suppress informational messages - #[arg(short = 'q', long = "quiet")] - pub quiet: bool, - - /// Show file and directory paths used by tealdeer - #[arg(long = "show-paths")] - pub show_paths: bool, - - /// Create a basic config - #[arg(long = "seed-config")] - pub seed_config: bool, - - /// Control whether to use color - #[arg(long = "color", value_name = "WHEN")] - pub color: Option, - - /// Display the short variants of placeholders - #[arg(long)] - pub short_options: bool, - - /// Display the long variants of placeholders - #[arg(long)] - pub long_options: bool, - - /// Print the version - // Note: We override the version flag because clap uses `-V` by default, - // while TLDR specification requires `-v` to be used. - #[arg(short = 'v', long = "version", action = ArgAction::Version)] - pub version: (), -} diff --git a/src/config.rs b/src/config.rs index ec84984..d1b040b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,103 +1,28 @@ use std::{ - borrow::Cow, - env, fmt, - fs::{self, File}, - io::{ErrorKind, Write}, - path::{Component, Path, PathBuf}, - str::FromStr, - sync::LazyLock, + env, fs, + io::{Error as IoError, Read, Write}, + path::PathBuf, time::Duration, }; -use anyhow::{Context, Result, anyhow, bail, ensure}; -use clap::ValueEnum; -use log::info; -use serde::Serialize as _; +use ansi_term::{Color, Style}; +use app_dirs::{get_app_root, AppDataType}; +use log::debug; use serde_derive::{Deserialize, Serialize}; -use toml::map::Map; -use yansi::{Color, Style}; use crate::{ - extensions::Dedup as _, - types::{PathSource, PlatformType}, + error::TealdeerError::{self, ConfigError}, + types::PathSource, }; pub const CONFIG_FILE_NAME: &str = "config.toml"; pub const MAX_CACHE_AGE: Duration = Duration::from_secs(2_592_000); // 30 days const DEFAULT_UPDATE_INTERVAL_HOURS: u64 = MAX_CACHE_AGE.as_secs() / 3600; // 30 days -const SUPPORTED_TLS_BACKENDS: &[RawTlsBackend] = &[ - #[cfg(feature = "native-tls")] - RawTlsBackend::NativeTls, - #[cfg(feature = "rustls-with-webpki-roots")] - RawTlsBackend::RustlsWithWebpkiRoots, - #[cfg(feature = "rustls-with-native-roots")] - RawTlsBackend::RustlsWithNativeRoots, -]; - -struct SystemDirectories { - config: PathBuf, - cache: PathBuf, - data: PathBuf, -} - -impl SystemDirectories { - fn discover() -> Result { - use etcetera::{ - AppStrategy, AppStrategyArgs, app_strategy::choose_native_strategy, choose_app_strategy, - }; - - let args = AppStrategyArgs { - top_level_domain: String::new(), - author: String::new(), - app_name: crate::NAME.to_string(), - }; - - // The app strategy prefers XDG on MacOs, whereas the native strategy returns paths which - // are used by installed applications. On Linux and Windows, the strategies are the same. - let app_dirs = choose_app_strategy(args.clone())?; - let native_dirs = choose_native_strategy(args)?; - - // We prefer the XDG paths, but before tealdeer 1.9, we used only the native paths on MacOs. - // So if we find files in these locations, we keep using them. - let fallback = |app_dir: PathBuf, native_dir: PathBuf| { - if !app_dir.exists() && native_dir.exists() { - native_dir - } else { - app_dir - } - }; - - Ok(Self { - config: fallback(app_dirs.config_dir(), native_dirs.config_dir()), - cache: fallback(app_dirs.cache_dir(), native_dirs.cache_dir()), - data: fallback(app_dirs.data_dir(), native_dirs.data_dir()), - }) - } -} -static SYSTEM_DIRECTORIES: LazyLock = LazyLock::new(|| { - SystemDirectories::discover().expect("Failed to initialize system directories.") -}); - -pub(crate) fn supported_tls_backends_string() -> String { - SUPPORTED_TLS_BACKENDS - .iter() - .map(std::string::ToString::to_string) - .collect::>() - .join(", ") -} fn default_underline() -> bool { false } -const fn default_base_indent() -> usize { - 2 -} - -const fn default_command_indent() -> usize { - 6 -} - fn default_bold() -> bool { false } @@ -114,8 +39,7 @@ pub enum RawColor { Green, Yellow, Blue, - Magenta, - Purple, // Backwards compatibility with ansi_term (until tealdeer 1.5.0) + Purple, Cyan, White, Ansi(u8), @@ -130,11 +54,11 @@ impl From for Color { RawColor::Green => Self::Green, RawColor::Yellow => Self::Yellow, RawColor::Blue => Self::Blue, - RawColor::Magenta | RawColor::Purple => Self::Magenta, + RawColor::Purple => Self::Purple, RawColor::Cyan => Self::Cyan, RawColor::White => Self::White, RawColor::Ansi(num) => Self::Fixed(num), - RawColor::Rgb { r, g, b } => Self::Rgb(r, g, b), + RawColor::Rgb { r, g, b } => Self::RGB(r, g, b), } } } @@ -173,7 +97,7 @@ impl From for Style { } if let Some(background) = raw_style.background { - style = style.bg(Color::from(background)); + style = style.on(Color::from(background)); } if raw_style.underline { @@ -206,77 +130,12 @@ struct RawStyleConfig { pub example_variable: RawStyle, } -impl From<&RawStyleConfig> for StyleConfig { - fn from(raw_style_config: &RawStyleConfig) -> Self { - Self { - command_name: raw_style_config.command_name.into(), - description: raw_style_config.description.into(), - example_text: raw_style_config.example_text.into(), - example_code: raw_style_config.example_code.into(), - example_variable: raw_style_config.example_variable.into(), - } - } -} - #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] struct RawDisplayConfig { #[serde(default)] pub compact: bool, #[serde(default)] pub use_pager: bool, - #[serde(default)] - pub show_title: bool, - #[serde(default)] - pub indent: RawIndent, - #[serde(default)] - pub placeholder_format: PlaceholderFormat, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -struct RawIndent { - #[serde(default = "default_base_indent")] - base: usize, - #[serde(default = "default_command_indent")] - command: usize, -} - -impl Default for RawIndent { - fn default() -> Self { - Self { - base: 2, - command: 6, - } - } -} - -impl From for Indent { - fn from(raw_indent: RawIndent) -> Self { - Self { - base: raw_indent.base, - command: raw_indent.command, - } - } -} - -#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "lowercase")] -pub enum PlaceholderFormat { - Short, - #[default] - Long, - Both, -} - -impl From<&RawDisplayConfig> for DisplayConfig { - fn from(raw_display_config: &RawDisplayConfig) -> Self { - Self { - compact: raw_display_config.compact, - use_pager: raw_display_config.use_pager, - show_title: raw_display_config.show_title, - indent: raw_display_config.indent.into(), - placeholder_format: raw_display_config.placeholder_format, - } - } } /// Serde doesn't support default values yet (tracking issue: @@ -287,35 +146,12 @@ const fn default_auto_update_interval_hours() -> u64 { DEFAULT_UPDATE_INTERVAL_HOURS } -fn default_archive_source() -> String { - "https://github.com/tldr-pages/tldr/releases/latest/download".to_owned() -} - -/// Controls when a warning about an outdated cache is printed. -/// -/// Currently, the only nameable option is `"never"`. In the future, this may -/// be extended to also accept a duration (e.g. `"60d"`), after which the -/// warning should be shown. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -enum RawWarnCacheAge { - Never, -} - #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] struct RawUpdatesConfig { #[serde(default)] pub auto_update: bool, #[serde(default = "default_auto_update_interval_hours")] pub auto_update_interval_hours: u64, - #[serde(default = "default_archive_source")] - pub archive_source: String, - #[serde(default)] - pub tls_backend: RawTlsBackend, - #[serde(default)] - pub download_languages: Option>, - #[serde(default)] - pub warn_cache_age: Option, } impl Default for RawUpdatesConfig { @@ -323,75 +159,22 @@ impl Default for RawUpdatesConfig { Self { auto_update: false, auto_update_interval_hours: DEFAULT_UPDATE_INTERVAL_HOURS, - archive_source: default_archive_source(), - tls_backend: RawTlsBackend::default(), - download_languages: None, - warn_cache_age: None, } } } -#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] struct RawDirectoriesConfig { - #[serde(default)] - pub cache_dir: Option, #[serde(default)] pub custom_pages_dir: Option, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -enum RawPlatformType { - Current, - All, - MacOs, // alias for Platform(PlatformType::OsX) - #[serde(untagged)] - Platform(PlatformType), -} - -impl RawPlatformType { - pub fn flatten(raw_platforms: impl IntoIterator) -> Vec { - let mut flattened = Vec::new(); - for raw_platform in raw_platforms { - match raw_platform { - RawPlatformType::Current => flattened.push(PlatformType::current()), - RawPlatformType::Platform(platform) => flattened.push(platform), - RawPlatformType::MacOs => flattened.push(PlatformType::OsX), - RawPlatformType::All => flattened.extend(PlatformType::value_variants()), - } - } - flattened.clear_duplicates(); - flattened - } -} - -#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] -struct RawSearchConfig { - pub languages: Option>, - pub platforms: Option>, -} - -impl<'a> From<&'a RawSearchConfig> for SearchConfig<'a> { - fn from(raw_search_config: &'a RawSearchConfig) -> Self { - let languages = raw_search_config - .languages - .as_ref() - .map_or_else(get_languages_from_env, |langs| { - langs.iter().map(|lang| Language(lang)).collect() - }); - let platforms = if let Some(raw_platforms) = raw_search_config.platforms.as_ref() { - RawPlatformType::flatten(raw_platforms.iter().copied()) - } else { - RawPlatformType::flatten([ - RawPlatformType::Current, - RawPlatformType::Platform(PlatformType::Common), - RawPlatformType::All, - ]) - }; - +impl Default for RawDirectoriesConfig { + fn default() -> Self { Self { - languages, - platforms, + custom_pages_dir: get_app_root(AppDataType::UserData, &crate::APP_INFO) + .map(|path| path.join("pages")) + .ok(), } } } @@ -403,7 +186,12 @@ struct RawConfig { display: RawDisplayConfig, updates: RawUpdatesConfig, directories: RawDirectoriesConfig, - search: RawSearchConfig, +} + +impl RawConfig { + fn new() -> Self { + Self::default() + } } impl Default for RawConfig { @@ -413,7 +201,6 @@ impl Default for RawConfig { display: RawDisplayConfig::default(), updates: RawUpdatesConfig::default(), directories: RawDirectoriesConfig::default(), - search: RawSearchConfig::default(), }; // Set default config @@ -427,7 +214,7 @@ impl Default for RawConfig { } } -#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq)] pub struct StyleConfig { pub description: Style, pub command_name: Style, @@ -436,424 +223,104 @@ pub struct StyleConfig { pub example_variable: Style, } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq)] pub struct DisplayConfig { pub compact: bool, pub use_pager: bool, - pub show_title: bool, - pub indent: Indent, - pub placeholder_format: PlaceholderFormat, } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct Indent { - pub base: usize, - pub command: usize, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct UpdatesConfig<'a> { +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct UpdatesConfig { pub auto_update: bool, pub auto_update_interval: Duration, - pub archive_source: &'a str, - pub tls_backend: TlsBackend, - pub download_languages: Vec>, - pub warn_cache_age: Option, } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PathWithSource { - pub path: PathBuf, - pub source: PathSource, -} - -impl PathWithSource { - pub fn path(&self) -> &Path { - &self.path - } -} - -impl fmt::Display for PathWithSource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} ({})", self.path.display(), self.source) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq)] pub struct DirectoriesConfig { - pub cache_dir: PathWithSource, - pub custom_pages_dir: Option, + pub custom_pages_dir: Option, } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SearchConfig<'a> { - pub languages: Vec>, - pub platforms: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Language<'a>(pub &'a str); - -fn get_languages<'a>( - env_lang: Option<&'a str>, - env_language: Option<&'a str>, -) -> Vec> { - // Language list according to - // https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#language - - let Some(env_lang) = env_lang else { - return vec![Language("en")]; - }; - - // Create an iterator that contains $LANGUAGE (':' separated list) followed by $LANG (single language) - let locales = env_language.unwrap_or("").split(':').chain([env_lang]); - - let mut lang_list = Vec::new(); - for locale in locales { - if !locale.is_ascii() { - info!("Skipping non-ASCII locale string: {locale}"); - continue; - } - - // Language plus country code (e.g. `en_US`) - if locale.len() >= 5 && locale.chars().nth(2) == Some('_') { - lang_list.push(Language(&locale[..5])); - } - // Language code only (e.g. `en`) - if locale.len() >= 2 && locale != "POSIX" { - lang_list.push(Language(&locale[..2])); - } - } - - lang_list.push(Language("en")); - lang_list.clear_duplicates(); - lang_list -} - -pub fn get_languages_from_env<'a>() -> Vec> { - static LANG: LazyLock> = LazyLock::new(|| std::env::var("LANG").ok()); - static LANGUAGE: LazyLock> = LazyLock::new(|| std::env::var("LANGUAGE").ok()); - get_languages( - LANG.as_ref().map(String::as_str), - LANGUAGE.as_ref().map(String::as_str), - ) -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub enum RawTlsBackend { - /// Native TLS (`SChannel` on Windows, Secure Transport on macOS and OpenSSL otherwise) - NativeTls, - /// Rustls with `WebPKI` roots. - RustlsWithWebpkiRoots, - /// Rustls with native roots. - RustlsWithNativeRoots, -} - -impl Default for RawTlsBackend { - fn default() -> Self { - *SUPPORTED_TLS_BACKENDS.first().unwrap() - } -} - -impl std::fmt::Display for RawTlsBackend { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - self.serialize(f) - } -} - -/// Allows choosing a `reqwest`'s TLS backend. Available TLS backends: -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum TlsBackend { - /// Native TLS (`SChannel` on Windows, Secure Transport on macOS and OpenSSL otherwise) - #[cfg(feature = "native-tls")] - NativeTls, - /// Rustls with `WebPKI` roots. - #[cfg(feature = "rustls-with-webpki-roots")] - RustlsWithWebpkiRoots, - /// Rustls with native roots. - #[cfg(feature = "rustls-with-native-roots")] - RustlsWithNativeRoots, -} - -impl TryFrom for TlsBackend { - type Error = anyhow::Error; - - fn try_from(raw: RawTlsBackend) -> Result { - match raw { - #[cfg(feature = "native-tls")] - RawTlsBackend::NativeTls => Ok(TlsBackend::NativeTls), - #[cfg(feature = "rustls-with-webpki-roots")] - RawTlsBackend::RustlsWithWebpkiRoots => Ok(TlsBackend::RustlsWithWebpkiRoots), - #[cfg(feature = "rustls-with-native-roots")] - RawTlsBackend::RustlsWithNativeRoots => Ok(TlsBackend::RustlsWithNativeRoots), - // when compiling without all TLS backend features, we want to handle config error. - #[allow(unreachable_patterns)] - _ => Err(anyhow!( - "Unsupported TLS backend: {}. This tealdeer build has support for the following options: {}", - raw, - supported_tls_backends_string(), - )), - } - } -} - -impl TlsBackend { - const fn as_raw(self) -> RawTlsBackend { - match self { - #[cfg(feature = "native-tls")] - Self::NativeTls => RawTlsBackend::NativeTls, - #[cfg(feature = "rustls-with-webpki-roots")] - Self::RustlsWithWebpkiRoots => RawTlsBackend::RustlsWithWebpkiRoots, - #[cfg(feature = "rustls-with-native-roots")] - Self::RustlsWithNativeRoots => RawTlsBackend::RustlsWithNativeRoots, - } - } -} - -impl fmt::Display for TlsBackend { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.as_raw().fmt(f) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Config<'a> { +#[derive(Clone, Debug, PartialEq)] +pub struct Config { pub style: StyleConfig, pub display: DisplayConfig, - pub updates: UpdatesConfig<'a>, + pub updates: UpdatesConfig, pub directories: DirectoriesConfig, - pub search: SearchConfig<'a>, - pub file_path: PathWithSource, } -impl<'a> Config<'a> { - /// Convert a `RawConfig` to a high-level `Config`. - /// - /// For this, some values need to be converted to other types and some - /// defaults need to be set (sometimes based on env variables). - fn from_raw(raw_config: &'a RawConfig, config_file_path: PathWithSource) -> Result { - let style = (&raw_config.style).into(); - let display = (&raw_config.display).into(); - let search: SearchConfig<'a> = (&raw_config.search).into(); - - let updates = UpdatesConfig { - auto_update: raw_config.updates.auto_update, - auto_update_interval: Duration::from_secs( - raw_config.updates.auto_update_interval_hours * 3600, - ), - archive_source: &raw_config.updates.archive_source, - tls_backend: raw_config.updates.tls_backend.try_into()?, - download_languages: raw_config.updates.download_languages.as_ref().map_or_else( - || search.languages.clone(), - |languages| languages.iter().map(|lang| Language(lang)).collect(), - ), - warn_cache_age: match raw_config.updates.warn_cache_age { - None => Some(MAX_CACHE_AGE), - Some(RawWarnCacheAge::Never) => None, +impl From for Config { + fn from(raw_config: RawConfig) -> Self { + Self { + style: StyleConfig { + command_name: raw_config.style.command_name.into(), + description: raw_config.style.description.into(), + example_text: raw_config.style.example_text.into(), + example_code: raw_config.style.example_code.into(), + example_variable: raw_config.style.example_variable.into(), }, - }; + display: DisplayConfig { + compact: raw_config.display.compact, + use_pager: raw_config.display.use_pager, + }, + updates: UpdatesConfig { + auto_update: raw_config.updates.auto_update, + auto_update_interval: Duration::from_secs( + raw_config.updates.auto_update_interval_hours * 3600, + ), + }, + directories: DirectoriesConfig { + custom_pages_dir: raw_config.directories.custom_pages_dir, + }, + } + } +} - let relative_path_root = config_file_path - .path() - .parent() - .context("Failed to get config directory")?; - let home_path = env::home_dir(); +#[allow(clippy::needless_pass_by_value)] +fn map_io_err_to_config_err(e: IoError) -> TealdeerError { + ConfigError(format!("Io Error: {}", e)) +} - // Determine directories config. For this, we need to take some - // additional factory into account, like env variables, or the - // user config. - let cache_dir_env_var = "TEALDEER_CACHE_DIR"; - let cache_dir = if let Ok(env_var) = env::var(cache_dir_env_var) { - // For backwards compatibility reasons, the cache directory can be - // overridden using an env variable. This is deprecated and will be - // phased out in the future. - eprintln!( - "Warning: The ${cache_dir_env_var} env variable is deprecated, use the `cache_dir` option in the config file instead." - ); - PathWithSource { - path: PathBuf::from(env_var), - source: PathSource::EnvVar, - } - } else if let Some(config_value) = &raw_config.directories.cache_dir { - // Resolve possible ~ prefixed path - let expanded_path = expand_home(config_value, home_path.as_deref())?; - // Resolve possible relative path. - let resolved_path = relative_path_root.join(expanded_path); +impl Config { + pub fn load(enable_styles: bool) -> Result { + debug!("Loading config"); - PathWithSource { - path: resolved_path, - source: PathSource::ConfigFile, - } + // Determine path + let (config_file_path, _) = get_config_path() + .map_err(|e| ConfigError(format!("Could not determine config path: {}", e)))?; + + // Load raw config + let raw_config: RawConfig = if config_file_path.exists() && config_file_path.is_file() { + let mut config_file = + fs::File::open(&config_file_path).map_err(map_io_err_to_config_err)?; + let mut contents = String::new(); + let _ = config_file + .read_to_string(&mut contents) + .map_err(map_io_err_to_config_err)?; + toml::from_str(&contents).map_err(|err| { + ConfigError(format!( + "Failed to parse config file at {:?}:\n{}", + config_file_path, err + )) + })? } else { - PathWithSource { - path: SYSTEM_DIRECTORIES.cache.clone(), - source: PathSource::OsConvention, - } - }; - let custom_pages_dir = raw_config - .directories - .custom_pages_dir - .as_ref() - .map(|path| -> Result { - // Resolve possible ~ prefixed path - let expanded_path = expand_home(path, home_path.as_deref())?; - // Resolve possible relative path. - let resolved_path = relative_path_root.join(expanded_path); - - Ok(PathWithSource { - path: resolved_path, - source: PathSource::ConfigFile, - }) - }) - .transpose()? - .or_else(|| { - // Note: The `join("")` call ensures that there's a trailing slash - Some(PathWithSource { - path: SYSTEM_DIRECTORIES.data.join("pages").join(""), - source: PathSource::OsConvention, - }) - }); - let directories = DirectoriesConfig { - cache_dir, - custom_pages_dir, + RawConfig::new() }; - Ok(Self { - style, - display, - updates, - directories, - search, - file_path: config_file_path, - }) - } -} + // Convert to config + let mut config = Self::from(raw_config); -/// Expands tilde (~) prefixed directories into its absolute version -fn expand_home<'a>(input_path: &'a Path, home_path: Option<&Path>) -> Result> { - let mut components = input_path.components(); - - if let Some(Component::Normal(first_component_raw)) = components.next() { - let first_component = first_component_raw - .to_str() - .ok_or(anyhow!("Path contains invalid UTF-8"))?; - - if first_component == "~" { - let home_path = home_path.ok_or(anyhow!("Unable to find user home directory"))?; - let rest: PathBuf = components.collect(); - let expanded = home_path.join(rest); - - return Ok(Cow::Owned(expanded)); - } else if first_component.starts_with('~') { - return Err(anyhow!("Tilde expansion with a login name not supported")); - } - } - - Ok(Cow::Borrowed(input_path)) -} - -/// The [`ConfigLoader`] is used to load a [`Config`] from a file. -/// -/// Since the rich [`Config`] keeps references to [`RawConfig`], the raw config needs to be kept alive outside of the -/// [`Config`]. The [`ConfigLoader`] thus offers the following flow: -/// 1. Read a raw config using [`ConfigLoader::read`] or [`ConfigLoader::read_default_path`]. -/// 2. Validate the contents to a [`Config`] that borrows the [`ConfigLoader`]. -pub struct ConfigLoader { - raw: RawConfig, - path: PathWithSource, -} - -impl ConfigLoader { - fn read_internal( - path: PathWithSource, - allow_not_found: bool, - overrides: &[String], - ) -> Result { - let read_raw_config = match fs::read_to_string(&path.path) { - Ok(content) => toml::from_str(&content).with_context(|| { - format!( - "Could not parse config file contents as toml from {}.", - path.path.display() - ) - })?, - Err(e) if allow_not_found && e.kind() == ErrorKind::NotFound => RawConfig::default(), - Err(e) => { - return Err(e).context(format!( - "Could not read config file contents from {}.", - path.path().display() - )); - } - }; - - let read_config_table = toml::Table::try_from(read_raw_config)?; - let used_config_table = Self::override_config_with(read_config_table, overrides) - .context("Failed to apply config overrides")?; - let raw = used_config_table.try_into()?; - - Ok(Self { raw, path }) - } - - fn override_config_with( - config_table: toml::Table, - overrides: &[String], - ) -> Result { - let mut config_table = toml::Value::Table(config_table); - for override_str in overrides { - let (name, value) = override_str - .split_once('=') - .ok_or(anyhow!("Invalid override-string: {override_str} (correct example: \"display.compact = true\")"))?; - - let name = name.trim(); - let value = toml::Value::from_str(value.trim())?; - - let mut entry = &mut config_table; - for subkey in name.split('.') { - let toml::Value::Table(entry_table) = entry else { - bail!( - "\"{name}\" is not a valid identifier since \"{subkey}\" already refers to a value which is not a toml-Table." - ); - }; - - entry = entry_table - .entry(subkey) - .or_insert(toml::Value::Table(Map::default())); - } - - *entry = value; + // Potentially override styles + if !enable_styles { + config.style = StyleConfig { + command_name: Style::default(), + description: Style::default(), + example_text: Style::default(), + example_code: Style::default(), + example_variable: Style::default(), + }; } - match config_table { - toml::Value::Table(config_table) => Ok(config_table), - _ => unreachable!("root table is never modified"), - } - } - - /// Create a loader that uses the config at `path`. - /// `overrides`: If set, overrides the default values of the config - pub fn read(path: PathBuf, overrides: &[String]) -> Result { - Self::read_internal( - PathWithSource { - path, - source: PathSource::Cli, - }, - false, - overrides, - ) - } - - /// Create a loader that uses the default config file location. If no file is present at the default location, the - /// default configuration is used. - /// `overrides`: If set, overrides the default values of the config - pub fn read_default_path(overrides: &[String]) -> Result { - let path = get_default_config_path(); - Self::read_internal(path, true, overrides) - } - - /// Parse the read [`RawConfig`] into a [`Config`]. - pub fn load(&self) -> Result> { - Config::from_raw(&self.raw, self.path.clone()) - .context("Could not process raw config into rich config") + Ok(config) } } @@ -864,323 +331,77 @@ impl ConfigLoader { /// /// Note that this function does not verify whether the directory at that /// location exists, or is a directory. -pub fn get_config_dir() -> (PathBuf, PathSource) { +pub fn get_config_dir() -> Result<(PathBuf, PathSource), TealdeerError> { // Allow overriding the config directory by setting the // $TEALDEER_CONFIG_DIR env variable. if let Ok(value) = env::var("TEALDEER_CONFIG_DIR") { - return (PathBuf::from(value), PathSource::EnvVar); - } + return Ok((PathBuf::from(value), PathSource::EnvVar)); + }; - (SYSTEM_DIRECTORIES.config.clone(), PathSource::OsConvention) + // Otherwise, fall back to the user config directory. + match get_app_root(AppDataType::UserConfig, &crate::APP_INFO) { + Ok(dirs) => Ok((dirs, PathSource::OsConvention)), + Err(_) => Err(ConfigError( + "Could not determine the user config directory.".into(), + )), + } } /// Return the path to the config file. /// /// Note that this function does not verify whether the file at that location /// exists, or is a file. -pub fn get_default_config_path() -> PathWithSource { - let (mut path, source) = get_config_dir(); - path.push(CONFIG_FILE_NAME); - PathWithSource { path, source } +pub fn get_config_path() -> Result<(PathBuf, PathSource), TealdeerError> { + let (config_dir, source) = get_config_dir()?; + let config_file_path = config_dir.join(CONFIG_FILE_NAME); + Ok((config_file_path, source)) } /// Create default config file. -/// path: Can be specified to create the config in that path instead of -/// the default path. -pub fn make_default_config(path: Option<&Path>) -> Result { - let config_file_path = if let Some(p) = path { - p.into() - } else { - let (config_dir, _) = get_config_dir(); +pub fn make_default_config() -> Result { + let (config_dir, _) = get_config_dir()?; - // Ensure that config directory exists - if config_dir.exists() { - ensure!( - config_dir.is_dir(), - "Config directory could not be created: {} already exists but is not a directory", - config_dir.to_string_lossy(), - ); - } else { - fs::create_dir_all(&config_dir).context("Could not create config directory")?; + // Ensure that config directory exists + if !config_dir.exists() { + if let Err(e) = fs::create_dir_all(&config_dir) { + return Err(ConfigError(format!( + "Could not create config directory: {}", + e + ))); } - - config_dir.join(CONFIG_FILE_NAME) - }; + } else if !config_dir.is_dir() { + return Err(ConfigError(format!( + "Config directory could not be created: {} already exists but is not a directory", + config_dir.to_string_lossy(), + ))); + } // Ensure that a config file doesn't get overwritten - ensure!( - !config_file_path.is_file(), - "A configuration file already exists at {}, no action was taken.", - config_file_path.to_str().unwrap() - ); + let config_file_path = config_dir.join(CONFIG_FILE_NAME); + if config_file_path.is_file() { + return Err(ConfigError(format!( + "A configuration file already exists at {}, no action was taken.", + config_file_path.to_str().unwrap() + ))); + } // Create default config - let serialized_config = - toml::to_string(&RawConfig::default()).context("Failed to serialize default config")?; + let serialized_config = toml::to_string(&RawConfig::new()) + .map_err(|err| ConfigError(format!("Failed to serialize default config: {}", err)))?; // Write default config - let mut config_file = - File::create(&config_file_path).context("Could not create config file")?; + let mut config_file = fs::File::create(&config_file_path).map_err(map_io_err_to_config_err)?; let _wc = config_file .write(serialized_config.as_bytes()) - .context("Could not write to config file")?; + .map_err(map_io_err_to_config_err)?; Ok(config_file_path) } -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn serialize_deserialize() { - let raw_config = RawConfig::default(); - let serialized = toml::to_string(&raw_config).unwrap(); - let deserialized: RawConfig = toml::from_str(&serialized).unwrap(); - assert_eq!(raw_config, deserialized); - } - - #[test] - fn expand_path_with_valid_home() { - let home = Some(PathBuf::from("/foo/bar")); - let path_to_expand = PathBuf::from("~/baz"); - - assert_eq!( - *expand_home(&path_to_expand, home.as_deref()).unwrap(), - PathBuf::from("/foo/bar/baz") - ); - } - - #[test] - fn expand_path_with_absolute_path() { - let home = Some(PathBuf::from("/foo/bar")); - let dir_to_expand = PathBuf::from("/one/two"); - - assert_eq!( - *expand_home(&dir_to_expand, home.as_deref()).unwrap(), - dir_to_expand - ); - } - - #[test] - fn error_with_tilde_username() { - let home = Some(PathBuf::from("/foo/bar")); - let dir_to_expand = PathBuf::from("~baz/foo"); - - assert!(expand_home(&dir_to_expand, home.as_deref()).is_err()); - } - - #[test] - fn expand_tilde_in_config_file() { - let mut raw_config = RawConfig::default(); - raw_config.directories.cache_dir = Some("~/my/custom_cache".into()); - raw_config.directories.custom_pages_dir = Some("~/custom_pages".into()); - - let config = Config::from_raw( - &raw_config, - PathWithSource { - path: PathBuf::from("/path/to/config/config.toml"), - source: PathSource::OsConvention, - }, - ) - .unwrap(); - - let home_dir = env::home_dir().unwrap(); - - assert_eq!( - config.directories.cache_dir.path(), - home_dir.join("my/custom_cache") - ); - assert_eq!( - config.directories.custom_pages_dir.unwrap().path(), - home_dir.join("custom_pages") - ); - } - - #[test] - fn relative_path_resolution() { - let mut raw_config = RawConfig::default(); - raw_config.directories.cache_dir = Some("../cache".into()); - raw_config.directories.custom_pages_dir = Some("../custom_pages".into()); - - let config = Config::from_raw( - &raw_config, - PathWithSource { - path: PathBuf::from("/path/to/config/config.toml"), - source: PathSource::OsConvention, - }, - ) - .unwrap(); - - assert_eq!( - config.directories.cache_dir.path(), - Path::new("/path/to/config/../cache") - ); - assert_eq!( - config.directories.custom_pages_dir.unwrap().path(), - Path::new("/path/to/config/../custom_pages") - ); - } - - mod override_config { - use super::*; - use toml::Value; - - fn base_config() -> toml::Table { - toml::Table::from_str( - " - global_value = false - - [some] - value = 0 - - [some.inner] - value1 = 1 - value2 = \"a string\" - - [some.other] - value1 = 3 - value2 = [ 1, \"text\", true ] - ", - ) - .unwrap() - } - - #[test] - fn basic() { - let original_config = base_config(); - let overrides = &["some.inner.value1 = 'some text'".to_string()]; - - let new_config = ConfigLoader::override_config_with(original_config, overrides) - .expect("config should be successfully overwritten"); - - assert_eq!(new_config["some"]["value"], Value::Integer(0)); - assert_eq!( - new_config["some"]["inner"]["value1"], - Value::String("some text".to_string()), - ); - assert_eq!( - new_config["some"]["inner"]["value2"], - Value::String("a string".to_string()), - ); - assert_eq!(new_config["some"]["other"]["value1"], Value::Integer(3)); - assert_eq!(new_config["global_value"], Value::Boolean(false)); - } - - macro_rules! style_config_with { - ($config:ident, $overrides:expr) => { - let loader = ConfigLoader::read( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/style-config.toml"), - $overrides, - ) - .unwrap(); - let $config = loader.load().unwrap(); - }; - } - - #[test] - fn dependent_config() { - style_config_with!(config, &["search.languages = ['de', 'it']".to_string()]); - assert_eq!(config.search.languages, [Language("de"), Language("it")]); - // Value is copied after override is applied - assert_eq!( - config.updates.download_languages, - [Language("de"), Language("it")] - ); - } - - #[test] - fn order() { - style_config_with!( - config, - &[ - "display.compact = false".to_string(), - "display.compact = true".to_string(), - ] - ); - assert!(config.display.compact); - } - - #[test] - fn override_with_table() { - style_config_with!(config, &["display = {'compact' = true}".to_string()]); - assert!(config.display.compact); - assert_eq!( - config.display.indent, - RawConfig::default().display.indent.into() - ); - } - } - - mod language { - use super::*; - - #[test] - fn missing_lang_env() { - let lang_list = get_languages(None, Some("de:fr")); - assert_eq!(lang_list, [Language("en")]); - let lang_list = get_languages(None, None); - assert_eq!(lang_list, [Language("en")]); - } - - #[test] - fn missing_language_env() { - let lang_list = get_languages(Some("de"), None); - assert_eq!(lang_list, [Language("de"), Language("en")]); - } - - #[test] - fn preference_order() { - let lang_list = get_languages(Some("de"), Some("fr:cn")); - assert_eq!( - lang_list, - [ - Language("fr"), - Language("cn"), - Language("de"), - Language("en") - ] - ); - } - - #[test] - fn country_code_expansion() { - let lang_list = get_languages(Some("pt_BR"), None); - assert_eq!( - lang_list, - [Language("pt_BR"), Language("pt"), Language("en")] - ); - } - - #[test] - fn with_encoding() { - let lang_list = get_languages(Some("de_DE.UTF-8"), None); - assert_eq!( - lang_list, - [Language("de_DE"), Language("de"), Language("en")] - ); - } - - #[test] - fn ignore_posix_and_c() { - let lang_list = get_languages(Some("POSIX"), None); - assert_eq!(lang_list, [Language("en")]); - let lang_list = get_languages(Some("C"), None); - assert_eq!(lang_list, [Language("en")]); - } - - #[test] - fn no_duplicates() { - let lang_list = get_languages(Some("de"), Some("fr:de:cn:de")); - assert_eq!( - lang_list, - [ - Language("fr"), - Language("de"), - Language("cn"), - Language("en") - ] - ); - } - } +#[test] +fn test_serialize_deserialize() { + let raw_config = RawConfig::new(); + let serialized = toml::to_string(&raw_config).unwrap(); + let deserialized: RawConfig = toml::from_str(&serialized).unwrap(); + assert_eq!(raw_config, deserialized); } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..548c39f --- /dev/null +++ b/src/error.rs @@ -0,0 +1,40 @@ +use std::fmt; + +use reqwest::Error as ReqwestError; + +#[derive(Debug)] +#[allow(clippy::enum_variant_names)] +pub enum TealdeerError { + CacheError(String), + ConfigError(String), + UpdateError(String), + WriteError(String), +} + +impl TealdeerError { + pub fn message(&self) -> &str { + match self { + Self::CacheError(msg) + | Self::ConfigError(msg) + | Self::UpdateError(msg) + | Self::WriteError(msg) => msg, + } + } +} + +impl From for TealdeerError { + fn from(err: ReqwestError) -> Self { + Self::UpdateError(format!("HTTP error: {}", err.to_string())) + } +} + +impl fmt::Display for TealdeerError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::CacheError(e) => write!(f, "CacheError: {}", e), + Self::ConfigError(e) => write!(f, "ConfigError: {}", e), + Self::UpdateError(e) => write!(f, "UpdateError: {}", e), + Self::WriteError(e) => write!(f, "WriteError: {}", e), + } + } +} diff --git a/src/extensions.rs b/src/extensions.rs index e74e9f3..3aebfa2 100644 --- a/src/extensions.rs +++ b/src/extensions.rs @@ -1,14 +1,14 @@ use std::mem; /// An extension trait to clear duplicates from a collection. -pub(crate) trait Dedup { +pub(crate) trait Dedup { fn clear_duplicates(&mut self); } /// Clear duplicates from a collection, keep the first one seen. /// /// For small vectors, this will be faster than a `HashSet`. -impl Dedup for Vec { +impl Dedup for Vec { fn clear_duplicates(&mut self) { let orig = mem::replace(self, Vec::with_capacity(self.len())); for item in orig { diff --git a/src/formatter.rs b/src/formatter.rs index f06c098..9789975 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -1,81 +1,26 @@ //! Functions related to formatting and printing lines from a `Tokenizer`. +use crate::{extensions::FindFrom, types::LineType}; + use log::debug; -use crate::{config::Indent, extensions::FindFrom, types::LineType}; - -#[derive(Debug, Clone, Copy, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] /// Represents a snippet from a page of a specific highlighting class. -pub enum PageSnippet { - CommandName(T), - Placeholder(T), - PlaceholderVariants { short: T, long: T }, - NormalCode(T), - Description(T), - Text(T), - Title(T), - Indent(usize), +pub enum PageSnippet<'a> { + CommandName(&'a str), + Variable(&'a str), + NormalCode(&'a str), + Description(&'a str), + Text(&'a str), Linebreak, } -#[cfg_attr(not(test), allow(dead_code))] -impl PageSnippet { - pub fn map(self, f: F) -> PageSnippet - where - F: Fn(T) -> U, - { - match self { - PageSnippet::CommandName(s) => PageSnippet::CommandName(f(s)), - PageSnippet::Placeholder(s) => PageSnippet::Placeholder(f(s)), - PageSnippet::PlaceholderVariants { short, long } => PageSnippet::PlaceholderVariants { - short: f(short), - long: f(long), - }, - PageSnippet::NormalCode(s) => PageSnippet::NormalCode(f(s)), - PageSnippet::Description(s) => PageSnippet::Description(f(s)), - PageSnippet::Text(s) => PageSnippet::Text(f(s)), - PageSnippet::Title(s) => PageSnippet::Title(f(s)), - PageSnippet::Indent(n) => PageSnippet::Indent(n), - PageSnippet::Linebreak => PageSnippet::Linebreak, - } - } -} - -impl, U> PartialEq> for PageSnippet { - fn eq(&self, other: &PageSnippet) -> bool { - match (self, other) { - (PageSnippet::CommandName(s), PageSnippet::CommandName(t)) - | (PageSnippet::Placeholder(s), PageSnippet::Placeholder(t)) - | (PageSnippet::NormalCode(s), PageSnippet::NormalCode(t)) - | (PageSnippet::Description(s), PageSnippet::Description(t)) - | (PageSnippet::Text(s), PageSnippet::Text(t)) - | (PageSnippet::Title(s), PageSnippet::Title(t)) => s == t, - ( - PageSnippet::PlaceholderVariants { - short: left_short, - long: left_long, - }, - PageSnippet::PlaceholderVariants { - short: right_short, - long: right_long, - }, - ) => left_short == right_short && left_long == right_long, - (PageSnippet::Indent(n), PageSnippet::Indent(m)) => n == m, - (PageSnippet::Linebreak, PageSnippet::Linebreak) => true, - _ => false, - } - } -} - -impl PageSnippet<&str> { +impl<'a> PageSnippet<'a> { pub fn is_empty(&self) -> bool { use PageSnippet::*; match self { - CommandName(s) | Placeholder(s) | NormalCode(s) | Description(s) | Text(s) - | Title(s) => s.is_empty(), - PageSnippet::PlaceholderVariants { short, long } => short.is_empty() && long.is_empty(), - Indent(n) => *n == 0, + CommandName(s) | Variable(s) | NormalCode(s) | Description(s) | Text(s) => s.is_empty(), Linebreak => false, } } @@ -86,12 +31,10 @@ pub fn highlight_lines( lines: L, process_snippet: &mut F, keep_empty_lines: bool, - show_title: bool, - indent: Indent, ) -> Result<(), E> where L: Iterator, - F: for<'snip> FnMut(PageSnippet<&'snip str>) -> Result<(), E>, + F: for<'snip> FnMut(PageSnippet<'snip>) -> Result<(), E>, { let mut command = String::new(); for line in lines { @@ -102,131 +45,51 @@ where } } LineType::Title(title) => { - if show_title { - process_snippet(PageSnippet::Linebreak)?; - process_snippet(PageSnippet::Indent(indent.base))?; - process_snippet(PageSnippet::Title(&title))?; - process_snippet(PageSnippet::Linebreak)?; - } else { - debug!("Ignoring title"); - } + debug!("Ignoring title"); + // This is safe as long as the parsed title is only the command, // and the iterator yields values in order of appearance. command = title; - debug!("Detected command name: {command}"); - } - LineType::Description(text) => { - process_snippet(PageSnippet::Indent(indent.base))?; - process_snippet(PageSnippet::Description(&text))?; - process_snippet(PageSnippet::Linebreak)?; - } - LineType::ExampleText(text) => { - process_snippet(PageSnippet::Indent(indent.base))?; - process_snippet(PageSnippet::Text(&text))?; - process_snippet(PageSnippet::Linebreak)?; + debug!("Detected command name: {}", &command); } + LineType::Description(text) => process_snippet(PageSnippet::Description(&text))?, + LineType::ExampleText(text) => process_snippet(PageSnippet::Text(&text))?, LineType::ExampleCode(text) => { - process_snippet(PageSnippet::Indent(indent.command))?; + process_snippet(PageSnippet::NormalCode(" "))?; highlight_code(&command, &text, process_snippet)?; process_snippet(PageSnippet::Linebreak)?; } - LineType::Other(text) => debug!("Unknown line type: {text:?}"), + LineType::Other(text) => debug!("Unknown line type: {:?}", text), } } process_snippet(PageSnippet::Linebreak)?; Ok(()) } -/// Highlight code examples. -/// - parse placeholders (`{{ curly braces }}`) -/// - replace escaped placeholder markers (`\{\{` and `\}\}`) -fn highlight_code( - command: &str, - mut text: &str, - process_snippet: &mut impl FnMut(PageSnippet<&str>) -> Result<(), E>, +/// Highlight code examples including user variables in {{ curly braces }}. +fn highlight_code<'a, E>( + command: &'a str, + text: &'a str, + process_snippet: &mut impl FnMut(PageSnippet<'a>) -> Result<(), E>, ) -> Result<(), E> { - // We replace escaped placeholder markers at the end so that our replacing does not interfere - // with finding the actual markers. - // NOTE: This is not optimal, as it allocates one String for each `replace` - let replace_escaped = |s: &str| s.replace(r"\{\{", "{{").replace(r"\}\}", "}}"); - - loop { - // Find placeholder markers and split into code and placeholder accordingly - - let Some(start_marker) = find_marker(text, "{{", r"\{\{") else { - break; - }; - let Some(mut end_marker) = find_marker(&text[start_marker + 2..], "}}", r"\}\}") else { - break; - }; - end_marker += start_marker + 2; - - // Greedily extend matched range - while end_marker + 2 < text.len() && text.as_bytes()[end_marker + 2] == b'}' { - end_marker += 1; - } - - let placeholder_content = &text[start_marker + 2..end_marker]; - - if start_marker > 0 { - highlight_code_segment( - command, - &replace_escaped(&text[..start_marker]), - process_snippet, - )?; - } - - let placeholder_content = replace_escaped(placeholder_content); - if let Some(s) = placeholder_content.strip_prefix('[') - && let Some(s) = s.strip_suffix(']') - && let Some((short, long)) = s.split_once('|') - { - process_snippet(PageSnippet::PlaceholderVariants { short, long })?; - } else { - process_snippet(PageSnippet::Placeholder(&placeholder_content))?; - } - - text = &text[end_marker + 2..]; + let variable_splits = text + .split("}}") + .map(|s| s.split_once("{{").unwrap_or((s, ""))); + for (code_segment, variable) in variable_splits { + highlight_code_segment(command, code_segment, process_snippet)?; + process_snippet(PageSnippet::Variable(variable))?; } - - if !text.is_empty() { - highlight_code_segment(command, &replace_escaped(text), process_snippet)?; - } - Ok(()) } -/// Find a "{{" (or "}}") substring that does not overlap with a preceding "\{\{" (or "\}\}"). -fn find_marker(s: &str, marker: &str, forbidden_prefix: &str) -> Option { - let mut search_start = 0; - loop { - let marker_index = s.find_from(marker, search_start)?; - - let overlaps_with_prefix = (forbidden_prefix.len() <= marker_index + 1) && { - let prefix_start = marker_index + 1 - forbidden_prefix.len(); - // NOTE: The indices might not be valid character offsets, so we should do this - // comparison on raw bytes. If prefix_start is indeed not a character offset than the - // comparison is guaranteed to return false because forbidden_prefix[0] definitely _is_ - // the start of a (single byte, ASCII) character. - &s.as_bytes()[prefix_start..=marker_index] == forbidden_prefix.as_bytes() - }; - if !overlaps_with_prefix { - return Some(marker_index); - } - - // The next valid marker cannot include the first character of the current match - search_start = marker_index + 1; - } -} - -/// Yields `NormalCode` and `CommandName` in alternating order according to the occurrences of -/// `command_name` in `segment`. Placeholders are not detected here, see `highlight_code` +/// Yields `NormalCode` and `CommandName` in alternating order according to the occurences of +/// `command_name` in `segment`. Variables are not detected here, see `highlight_code` /// instead. fn highlight_code_segment<'a, E>( command_name: &'a str, mut segment: &'a str, - process_snippet: &mut impl FnMut(PageSnippet<&'a str>) -> Result<(), E>, + process_snippet: &mut impl FnMut(PageSnippet<'a>) -> Result<(), E>, ) -> Result<(), E> { if !command_name.is_empty() { let mut search_start = 0; @@ -250,23 +113,26 @@ fn highlight_code_segment<'a, E>( } /// Checks whether the characters right before and after the substring (given by half-open index interval) are whitespace (if they exist). -fn is_freestanding_substring(surrounding: &str, substring: (usize, usize)) -> bool { +fn is_freestanding_substring(surrouding: &str, substring: (usize, usize)) -> bool { let (start, end) = substring; // "okay" meaning or - let char_before_is_okay = surrounding[..start] + let char_before_is_okay = surrouding[..start] .chars() .last() - .is_none_or(char::is_whitespace); - let char_after_is_okay = surrounding[end..] + .filter(|prev_char| !prev_char.is_whitespace()) + .is_none(); + let char_after_is_okay = surrouding[end..] .chars() .next() - .is_none_or(char::is_whitespace); + .filter(|next_char| !next_char.is_whitespace()) + .is_none(); char_before_is_okay && char_after_is_okay } #[cfg(test)] mod tests { use super::*; + use PageSnippet::*; #[test] fn test_is_freestanding_substring() { @@ -293,251 +159,80 @@ mod tests { )); } - fn run<'a>(cmd: &'a str, segment: &'a str) -> Vec> { + fn run<'a>(cmd: &'a str, segment: &'a str) -> Vec> { let mut yielded = Vec::new(); - let mut process_snippet = |snip: PageSnippet<&str>| { + let mut process_snippet = |snip: PageSnippet<'a>| { if !snip.is_empty() { - yielded.push(snip.map(str::to_string)); + yielded.push(snip); } Ok::<(), ()>(()) }; - highlight_code(cmd, segment, &mut process_snippet).expect("highlight code segment failed"); + highlight_code_segment(cmd, segment, &mut process_snippet) + .expect("highlight code segment failed"); yielded } - mod highlight_code_segment { - use super::*; - use PageSnippet::*; - - #[test] - fn test_highlight_code_segment() { - assert!(run("make", "").is_empty()); - assert_eq!( - &run("make", "make all CC=clang -q"), - &[CommandName("make"), NormalCode(" all CC=clang -q")] - ); - assert_eq!( - &run("make", " make money --always-make"), - &[ - NormalCode(" "), - CommandName("make"), - NormalCode(" money --always-make") - ] - ); - assert_eq!( - &run("git commit", "git commit -m 'git commit'"), - &[CommandName("git commit"), NormalCode(" -m 'git commit'"),] - ); - } - - #[test] - fn test_i18n() { - assert_eq!( - &run("mäke", "mäke höhlenrätselbücher"), - &[CommandName("mäke"), NormalCode(" höhlenrätselbücher")] - ); - assert_eq!( - &run( - "Müll", - "1000 Gründe warum Müll heute größer ist als Müll früher, ärgerlich" - ), - &[ - NormalCode("1000 Gründe warum "), - CommandName("Müll"), - NormalCode(" heute größer ist als "), - CommandName("Müll"), - NormalCode(" früher, ärgerlich") - ] - ); - assert_eq!( - &run( - "übergang", - "die Zustandsübergangsfunktion übergang Änderungen", - ), - &[ - NormalCode("die Zustandsübergangsfunktion "), - CommandName("übergang"), - NormalCode(" Änderungen") - ], - ); - } - - #[test] - fn test_empty_command() { - let segment = "some code"; - let snippets = [NormalCode(segment)]; - - assert_eq!(run("", segment), snippets); - assert_eq!(run(" ", segment), snippets); - assert_eq!(run(" \t ", segment), snippets); - } + #[test] + fn test_highlight_code_segment() { + assert!(run("make", "").is_empty()); + assert_eq!( + &run("make", "make all CC=clang -q"), + &[CommandName("make"), NormalCode(" all CC=clang -q")] + ); + assert_eq!( + &run("make", " make money --always-make"), + &[ + NormalCode(" "), + CommandName("make"), + NormalCode(" money --always-make") + ] + ); + assert_eq!( + &run("git commit", "git commit -m 'git commit'"), + &[CommandName("git commit"), NormalCode(" -m 'git commit'"),] + ); } - mod placeholders { - use super::*; - use PageSnippet::*; - - #[test] - fn placeholder_vs_escaped() { - assert_eq!( - run("ping", "ping {{example.com}}"), - [ - CommandName("ping"), - NormalCode(" "), - Placeholder("example.com"), - ], - ); - assert_eq!( - run( - "docker inspect", - r"docker inspect --format '\{\{range.NetworkSettings.Networks\}\}\{\{.IPAddress\}\}\{\{end\}\}' {{container}}" - ), - [ - CommandName("docker inspect"), - NormalCode( - " --format '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' " - ), - Placeholder("container"), - ], - ); - assert_eq!( - run("mount", r"mount \\{{computer_name}}\{{share_name}} Z:"), - [ - CommandName("mount"), - NormalCode(r" \\"), - Placeholder("computer_name"), - NormalCode(r"\"), - Placeholder("share_name"), - NormalCode(" Z:"), - ], - ); - - assert_eq!(run("", r"\{"), [NormalCode(r"\{")]); - assert_eq!(run("", r"\{{a"), [NormalCode(r"\{{a")]); - assert_eq!(run("", r"\{{a}}"), [NormalCode(r"\"), Placeholder("a")]); - - // Placeholder has begin marker, but no end marker - assert_eq!(run("", r"{{\}\}}"), [NormalCode("{{}}}")]); - } - - #[test] - fn outer_precedence() { - assert_eq!( - run("git stash", "git stash show --patch {{stash@{0}}}"), - [ - CommandName("git stash"), - NormalCode(" show --patch "), - Placeholder("stash@{0}"), - ], - ); - - // The following is not listed in the specification, but this is the highlighting I would expect. - assert_eq!( - run("rg", "rg {{}}}"), - [CommandName("rg"), NormalCode(" "), Placeholder("}")] - ); - - // And these are just to document the current behavior - assert_eq!(run("", "{{{}}}"), [Placeholder("{}")]); - assert_eq!(run("", "{{{{}}}"), [Placeholder("{{}")]); - assert_eq!(run("", "{{{}}}}"), [Placeholder("{}}")]); - } - - #[test] - fn escaped_inside_placeholder() { - assert_eq!( - run( - "playerctl", - r#"playerctl metadata {{[-f|--format]}} "{{Now playing: \{\{artist\}\} - \{\{album\}\} - \{\{title\}\}}}""# - ), - [ - CommandName("playerctl"), - NormalCode(" metadata "), - PlaceholderVariants { - short: "-f", - long: "--format" - }, - NormalCode(" \""), - Placeholder("Now playing: {{artist}} - {{album}} - {{title}}"), - NormalCode("\""), - ], - ); - } - - #[test] - fn placeholder_inside_escaped() { - assert_eq!( - run("test", r"test \{\{{{var}} normal\}\}"), - [ - CommandName("test"), - NormalCode(" {{"), - Placeholder("var"), - NormalCode(" normal}}"), - ], - ); - } - - #[test] - /// Regression test for - fn prefix_check_character_boundary() { - assert_eq!("Ä".len(), 2); - assert_eq!(run("", r"Äxx{{x}}"), [NormalCode("Äxx"), Placeholder("x")],); - } + #[test] + fn test_i18n() { + assert_eq!( + &run("mäke", "mäke höhlenrätselbücher"), + &[CommandName("mäke"), NormalCode(" höhlenrätselbücher")] + ); + assert_eq!( + &run( + "Müll", + "1000 Gründe warum Müll heute größer ist als Müll früher, ärgerlich" + ), + &[ + NormalCode("1000 Gründe warum "), + CommandName("Müll"), + NormalCode(" heute größer ist als "), + CommandName("Müll"), + NormalCode(" früher, ärgerlich") + ] + ); + assert_eq!( + &run( + "übergang", + "die Zustandsübergangsfunktion übergang Änderungen", + ), + &[ + NormalCode("die Zustandsübergangsfunktion "), + CommandName("übergang"), + NormalCode(" Änderungen") + ], + ); } - mod placeholder_variants { - use super::*; - use PageSnippet::*; + #[test] + fn test_empty_command() { + let segment = "some code"; + let snippets = [NormalCode(segment)]; - #[test] - fn missing_marker() { - assert_eq!( - run("foo", "{{[short|long]}}"), - [PlaceholderVariants { - short: "short", - long: "long" - }] - ); - - assert_eq!(run("foo", "{{short|long]}}"), [Placeholder("short|long]")]); - assert_eq!(run("foo", "{{[short|long}}"), [Placeholder("[short|long")]); - assert_eq!(run("foo", "{{[shortlong]}}"), [Placeholder("[shortlong]")]); - } - - /// The character `[` is a valid command name - #[test] - fn command_name_interaction() { - for name in ["[", "]", "|"] { - assert_eq!( - run(name, "{{[short|long]}}"), - [PlaceholderVariants { - short: "short", - long: "long" - }] - ); - } - } - - #[test] - fn empty_variant() { - for name in ["[", "]", "|"] { - assert_eq!( - run(name, "{{[|long]}}"), - [PlaceholderVariants { - short: "", - long: "long" - }] - ); - assert_eq!( - run(name, "{{[short|]}}"), - [PlaceholderVariants { - short: "short", - long: "" - }] - ); - assert_eq!(run(name, "{{[|]}}"), [] as [PageSnippet::; 0]); - } - } + assert_eq!(run("", segment), snippets); + assert_eq!(run(" ", segment), snippets); + assert_eq!(run(" \t ", segment), snippets); } } diff --git a/src/line_iterator.rs b/src/line_iterator.rs index 98088c0..4d0eab6 100644 --- a/src/line_iterator.rs +++ b/src/line_iterator.rs @@ -1,6 +1,6 @@ //! Code to split a `BufRead` instance into an iterator of `LineType`s. -use std::io::{BufRead, Read}; +use std::io::BufRead; use log::warn; @@ -12,7 +12,7 @@ pub enum TldrFormat { Undecided, /// The original format V1, - /// The new format (see ) + /// The new format (see https://github.com/tldr-pages/tldr/pull/958) V2, } @@ -53,7 +53,7 @@ impl Iterator for LineIterator { match bytes_read { Ok(0) => None, Err(e) => { - warn!("Could not read line from reader: {e:?}"); + warn!("Could not read line from reader: {:?}", e); None } Ok(_) => { @@ -64,11 +64,10 @@ impl Iterator for LineIterator { self.format = TldrFormat::V1; } else { // It's the new format! Drop next line. - if let Err(e) = Read::bytes(&mut self.reader) - .find(|b| matches!(b, Ok(b'\n') | Err(_))) - .transpose() - { - warn!("Could not read line from reader: {e:?}"); + // (Hmm, is there a way to do this without an allocation?) + let mut devnull = String::new(); + if let Err(e) = self.reader.read_line(&mut devnull) { + warn!("Could not read line from reader: {:?}", e); return None; } self.first_line = false; @@ -96,27 +95,21 @@ mod test { #[test] fn test_first_line_old_format() { - let input = "# The Title\n> Description\n"; + let input = "# The Title\n\n"; let mut lines = LineIterator::new(input.as_bytes()); let title = lines.next().unwrap(); assert_eq!(title, LineType::Title("The Title".to_string())); - let description = lines.next().unwrap(); - assert_eq!( - description, - LineType::Description("Description".to_string()) - ); + let empty = lines.next().unwrap(); + assert_eq!(empty, LineType::Empty); } #[test] fn test_first_line_new_format() { - let input = "The Title\n=========\n> Description\n"; + let input = "The Title\n=========\n\n"; let mut lines = LineIterator::new(input.as_bytes()); let title = lines.next().unwrap(); assert_eq!(title, LineType::Title("The Title".to_string())); - let description = lines.next().unwrap(); - assert_eq!( - description, - LineType::Description("Description".to_string()) - ); + let empty = lines.next().unwrap(); + assert_eq!(empty, LineType::Empty); } } diff --git a/src/main.rs b/src/main.rs index ca9faf3..4c9e676 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,36 +15,18 @@ #![allow(clippy::similar_names)] #![allow(clippy::struct_excessive_bools)] #![allow(clippy::too_many_lines)] -#![allow(clippy::unnecessary_debug_formatting)] -#![allow(clippy::while_let_loop)] -#[cfg(not(any( - feature = "native-tls", - feature = "rustls-with-webpki-roots", - feature = "rustls-with-native-roots", -)))] -compile_error!( - "at least one of the features \"native-tls\", \"rustls-with-webpki-roots\" or \"rustls-with-native-roots\" must be enabled" -); +use std::{env, path::PathBuf, process}; -use std::{ - env, - fs::create_dir_all, - io::{self, IsTerminal}, - path::Path, - process::{Command, ExitCode}, -}; - -use anyhow::{Context, Result, anyhow}; -use cache::CacheConfig; -use clap::Parser; -use config::{ConfigLoader, Language, StyleConfig, TlsBackend}; -use log::debug; -use types::PlatformType; +use app_dirs::AppInfo; +use atty::Stream; +use clap::{AppSettings, ArgGroup, Parser}; +#[cfg(not(target_os = "windows"))] +use pager::Pager; mod cache; -mod cli; mod config; +mod error; pub mod extensions; mod formatter; mod line_iterator; @@ -53,93 +35,297 @@ mod types; mod utils; use crate::{ - cache::{Cache, PageLookupResult, TLDR_PAGES_DIR}, - cli::Cli, - config::{ - Config, PathWithSource, PlaceholderFormat, get_config_dir, make_default_config, - supported_tls_backends_string, - }, + cache::{Cache, CacheFreshness, PageLookupResult, TLDR_PAGES_DIR}, + config::{get_config_dir, get_config_path, make_default_config, Config}, + error::TealdeerError::ConfigError, + extensions::Dedup, output::print_page, - types::ColorOptions, + types::{ColorOptions, PlatformStrategy, PlatformType}, utils::{print_error, print_warning}, }; const NAME: &str = "tealdeer"; -static TEALDEER_PAGE: &str = - include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/pages/tealdeer.md")); +const APP_INFO: AppInfo = AppInfo { + name: NAME, + author: NAME, +}; +const ARCHIVE_URL: &str = "https://tldr.sh/assets/tldr.zip"; + +// Note: flag names are specified explicitly in clap attributes +// to improve readability and allow contributors to grep names like "clear-cache" +#[derive(Parser, Debug)] +#[clap(about = "A fast TLDR client", author, version)] +#[clap(setting = AppSettings::ArgRequiredElseHelp)] +#[clap(setting = AppSettings::HelpRequired)] +#[clap(setting = AppSettings::DeriveDisplayOrder)] +#[clap( + after_help = "To view the user documentation, please visit https://dbrgn.github.io/tealdeer/." +)] +#[clap(group = ArgGroup::new("command_or_file").args(&["command", "render"]))] +struct Args { + /// The command to show (e.g. `tar` or `git log`) + #[clap(min_values = 1)] + command: Vec, + + /// List all commands in the cache + #[clap(short = 'l', long = "list")] + list: bool, + + /// Render a specific markdown file + #[clap( + short = 'f', + long = "render", + value_name = "FILE", + conflicts_with = "command" + )] + render: Option, + + /// Override the operating system [possible values: linux, macos, windows, sunos, all] + #[clap( + short = 'p', + long = "platform", + possible_values = ["linux", "macos", "windows", "sunos", "osx", "current", "all"], + default_value = "current", + hide_possible_values = true, + hide_default_value = true, + )] + platform: PlatformStrategy, + + /// Deprecated alias of `platform` + #[clap( + short = 'o', + long = "os", + conflicts_with = "platform", + possible_values = ["linux", "macos", "windows", "sunos", "osx", "current", "all"], + default_value = "current", + hide_possible_values = true, + hide_default_value = true, + )] + os: PlatformStrategy, + + /// Override the language + #[clap(short = 'L', long = "language")] + language: Option, + + /// Update the local cache + #[clap(short = 'u', long = "update")] + update: bool, + + /// Clear the local cache + #[clap(short = 'c', long = "clear-cache")] + clear_cache: bool, + + /// Use a pager to page output + #[clap(long = "pager", requires = "command_or_file")] + pager: bool, + + /// Display the raw markdown instead of rendering it + #[clap(short = 'r', long = "--raw", requires = "command_or_file")] + raw: bool, + + /// Deprecated alias of `raw` + #[clap( + long = "markdown", + short = 'm', + requires = "command_or_file", + hidden = true + )] + markdown: bool, + + /// Suppress informational messages + #[clap(short = 'q', long = "quiet")] + quiet: bool, + + /// Show file and directory paths used by tealdeer + #[clap(long = "show-paths")] + show_paths: bool, + + /// Show config file path + #[clap(long = "config-path")] + config_path: bool, + + /// Create a basic config + #[clap(long = "seed-config")] + seed_config: bool, + + /// Control whether to use color + #[clap( + long = "color", + value_name = "WHEN", + possible_values = ["always", "auto", "never"] + )] + color: Option, + + /// Print the version + // Note: We override the version flag because clap uses `-V` by default, + // while TLDR specification requires `-v` to be used. + #[clap(short = 'v', long = "version")] + version: bool, +} + +/// Set up display pager +#[cfg(not(target_os = "windows"))] +fn configure_pager(_: bool) { + Pager::with_default_pager("less -R").setup(); +} + +#[cfg(target_os = "windows")] +fn configure_pager(enable_styles: bool) { + print_warning(enable_styles, "--pager flag not available on Windows!"); +} + +/// The cache should get updated if this was requested by the user, or if auto +/// updates are enabled and the cache age is longer than the auto update interval. +fn should_update_cache(args: &Args, config: &Config) -> bool { + args.update + || (config.updates.auto_update + && Cache::last_update().map_or(true, |ago| ago >= config.updates.auto_update_interval)) +} + +#[derive(PartialEq)] +enum CheckCacheResult { + CacheFound, + CacheMissing, +} + +/// Check the cache for freshness. If it's stale or missing, show a warning. +fn check_cache(args: &Args, enable_styles: bool) -> CheckCacheResult { + match Cache::freshness() { + CacheFreshness::Fresh => CheckCacheResult::CacheFound, + CacheFreshness::Stale(_) if args.quiet => CheckCacheResult::CacheFound, + CacheFreshness::Stale(age) => { + print_warning( + enable_styles, + &format!( + "The cache hasn't been updated for {} days.\n\ + You should probably run `tldr --update` soon.", + age.as_secs() / 24 / 3600 + ), + ); + CheckCacheResult::CacheFound + } + CacheFreshness::Missing => { + print_warning( + enable_styles, + "Cache not found. Please run `tldr --update`.", + ); + CheckCacheResult::CacheMissing + } + } +} /// Clear the cache -fn clear_cache(cache: Cache, quietly: bool) -> Result<()> { - let cache_dir = cache.config().pages_directory.display(); - cache.clear().context("Could not clear cache")?; +fn clear_cache(quietly: bool, enable_styles: bool) { + Cache::clear().unwrap_or_else(|e| { + print_error( + enable_styles, + &format!("Could not delete cache: {}", e.message()), + ); + process::exit(1); + }); if !quietly { - eprintln!("Successfully cleared cache at `{cache_dir}`."); + eprintln!("Successfully deleted cache."); } - Ok(()) } /// Update the cache -fn update_cache( - cache: &mut Cache, - archive_source: &str, - tls_backend: TlsBackend, - quietly: bool, -) -> Result<()> { - let downloaded_languages = cache - .update(archive_source, tls_backend) - .context("Could not update cache")?; +fn update_cache(cache: &Cache, quietly: bool, enable_styles: bool) { + cache.update().unwrap_or_else(|e| { + print_error( + enable_styles, + &format!("Could not update cache: {}", e.message()), + ); + process::exit(1); + }); if !quietly { eprintln!("Successfully updated cache."); - eprint!("Pages for the following languages were downloaded: "); - let language_strings: Vec<_> = downloaded_languages - .into_iter() - .map(|lang| lang.0) - .collect(); - if language_strings.is_empty() { - eprintln!("(none)"); - } else { - eprintln!("{}", language_strings.join(", ")); + } +} + +/// Show the config path (DEPRECATED) +fn show_config_path(enable_styles: bool) { + match get_config_path() { + Ok((config_file_path, _)) => { + println!("Config path is: {}", config_file_path.to_str().unwrap()); + } + Err(ConfigError(msg)) => { + print_error( + enable_styles, + &format!("Could not look up config_path: {}", msg), + ); + process::exit(1); + } + Err(_) => { + print_error(enable_styles, "Unknown error"); + process::exit(1); } } - Ok(()) } /// Show file paths -fn show_paths(config: &Config) { - let config_dir = { - let (mut path, source) = get_config_dir(); - path.push(""); // Trailing path separator - match path.to_str() { - Some(path) => format!("{path} ({source})"), - None => "[Invalid]".to_string(), - } - }; - let config_path = config.file_path.to_string(); - let cache_dir = config.directories.cache_dir.to_string(); - let pages_dir = { - let mut path = config.directories.cache_dir.path.clone(); - path.push(TLDR_PAGES_DIR); - path.push(""); // Trailing path separator - path.display().to_string() - }; - let custom_pages_dir = match config.directories.custom_pages_dir { - Some(ref path_with_source) => path_with_source.to_string(), - None => "[None]".to_string(), - }; - println!("Config dir: {config_dir}"); - println!("Config path: {config_path}"); - println!("Cache dir: {cache_dir}"); - println!("Pages dir: {pages_dir}"); - println!("Custom pages dir: {custom_pages_dir}"); +fn show_paths() { + let config_dir = get_config_dir().map_or_else( + |e| format!("[Error: {}]", e), + |(mut path, source)| { + path.push(""); // Trailing path separator + match path.to_str() { + Some(path) => format!("{} ({})", path, source), + None => "[Invalid]".to_string(), + } + }, + ); + let config_path = get_config_path().map_or_else( + |e| format!("[Error: {}]", e), + |(path, _)| path.to_str().unwrap_or("[Invalid]").to_string(), + ); + let cache_dir = Cache::get_cache_dir().map_or_else( + |e| format!("[Error: {}]", e), + |(mut path, source)| { + path.push(""); // Trailing path separator + match path.to_str() { + Some(path) => format!("{} ({})", path, source), + None => "[Invalid]".to_string(), + } + }, + ); + let pages_dir = Cache::get_cache_dir().map_or_else( + |e| format!("[Error: {}]", e), + |(mut path, _)| { + path.push(TLDR_PAGES_DIR); + path.push(""); // Trailing path separator + path.into_os_string() + .into_string() + .unwrap_or_else(|_| String::from("[Invalid]")) + }, + ); + println!("Config dir: {}", config_dir); + println!("Config path: {}", config_path); + println!("Cache dir: {}", cache_dir); + println!("Pages dir: {}", pages_dir); } -fn create_config(path: Option<&Path>) -> Result<()> { - let config_file_path = make_default_config(path).context("Could not create seed config")?; - eprintln!( - "Successfully created seed config file here: {}", - config_file_path.to_str().unwrap() - ); - Ok(()) +/// Create seed config file and exit +fn create_config_and_exit(enable_styles: bool) { + match make_default_config() { + Ok(config_file_path) => { + eprintln!( + "Successfully created seed config file here: {}", + config_file_path.to_str().unwrap() + ); + process::exit(0); + } + Err(ConfigError(msg)) => { + print_error( + enable_styles, + &format!("Could not create seed config: {}", msg), + ); + process::exit(1); + } + Err(_) => { + print_error(enable_styles, "Unknown error"); + process::exit(1); + } + } } #[cfg(feature = "logging")] @@ -150,295 +336,254 @@ fn init_log() { #[cfg(not(feature = "logging"))] fn init_log() {} -fn spawn_editor(custom_pages_dir: &Path, file_name: &str) -> Result<()> { - create_dir_all(custom_pages_dir).context("Failed to create custom pages directory")?; +fn get_languages(env_lang: Option<&str>, env_language: Option<&str>) -> Vec { + // Language list according to + // https://github.com/tldr-pages/tldr/blob/master/CLIENT-SPECIFICATION.md#language - let custom_page_path = custom_pages_dir.join(file_name); - let Some(custom_page_path) = custom_page_path.to_str() else { - return Err(anyhow!("`custom_page_path.to_str()` failed")); - }; - let Ok(editor) = env::var("EDITOR") else { - return Err(anyhow!( - "To edit a custom page, please set the `EDITOR` environment variable." - )); - }; - println!("Editing {custom_page_path:?}"); - - let status = Command::new(&editor).arg(custom_page_path).status()?; - if !status.success() { - return Err(anyhow!("{editor} exit with code {:?}", status.code())); + if env_lang.is_none() { + return vec!["en".to_string()]; } - Ok(()) + let env_lang = env_lang.unwrap(); + + // Create an iterator that contains $LANGUAGE (':' separated list) followed by $LANG (single language) + let locales = env_language.unwrap_or("").split(':').chain([env_lang]); + + let mut lang_list = Vec::new(); + for locale in locales { + // Language plus country code (e.g. `en_US`) + if locale.len() >= 5 && locale.chars().nth(2) == Some('_') { + lang_list.push(&locale[..5]); + } + // Language code only (e.g. `en`) + if locale.len() >= 2 && locale != "POSIX" { + lang_list.push(&locale[..2]); + } + } + + lang_list.push("en"); + lang_list.clear_duplicates(); + lang_list.into_iter().map(str::to_string).collect() } -fn main() -> ExitCode { +fn get_languages_from_env() -> Vec { + get_languages( + std::env::var("LANG").ok().as_deref(), + std::env::var("LANGUAGE").ok().as_deref(), + ) +} + +fn main() { // Initialize logger init_log(); // Parse arguments - let args = Cli::parse(); + let mut args = Args::parse(); // Determine the usage of styles + #[cfg(target_os = "windows")] + let ansi_support = ansi_term::enable_ansi_support().is_ok(); + #[cfg(not(target_os = "windows"))] + let ansi_support = true; let enable_styles = match args.color.unwrap_or_default() { // Attempt to use styling if instructed - ColorOptions::Always => { - yansi::enable(); // disable yansi's automatic detection for ANSI support on Windows - true - } + ColorOptions::Always => true, // Enable styling if: + // * There is `ansi_support` // * NO_COLOR env var isn't set: https://no-color.org/ // * The output stream is stdout (not being piped) - ColorOptions::Auto => env::var_os("NO_COLOR").is_none() && io::stdout().is_terminal(), + ColorOptions::Auto => { + ansi_support && env::var_os("NO_COLOR").is_none() && atty::is(Stream::Stdout) + } // Disable styling ColorOptions::Never => false, }; - try_main(args, enable_styles).unwrap_or_else(|error| { - print_error(enable_styles, &error); - ExitCode::FAILURE - }) -} - -fn try_main(args: Cli, enable_styles: bool) -> Result { - // Look up config file, if none is found fall back to default config. - debug!("Loading config"); - let config_loader = match &args.config_path { - Some(path) if !args.seed_config => ConfigLoader::read(path.clone(), &args.override_config) - .context("Could not read config from given path")?, - _ => ConfigLoader::read_default_path(&args.override_config) - .context("Could not read config from default path")?, - }; - let mut config = config_loader.load()?; - - // Override styles if needed - if !enable_styles { - config.style = StyleConfig::default(); + // Handle renamed arguments + if args.markdown { + args.raw = true; + print_warning( + enable_styles, + "The -m / --markdown flag is deprecated, use -r / --raw instead", + ); + } + let default_platform = PlatformType::current(); + if args.os.platform_type != default_platform || args.os.list_all { + print_warning( + enable_styles, + "The -o / --os flag is deprecated, use -p / --platform instead", + ); + args.platform = args.os; } - config.display.placeholder_format = match (args.short_options, args.long_options) { - (false, false) => config.display.placeholder_format, // keep old value - (true, false) => PlaceholderFormat::Short, - (false, true) => PlaceholderFormat::Long, - (true, true) => PlaceholderFormat::Both, - }; - - let custom_pages_dir = config - .directories - .custom_pages_dir - .as_ref() - .map(PathWithSource::path); - - // Note: According to the TLDR client spec, page names must be transparently - // lowercased before lookup: - // https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#page-names - let command = args.command.join("-").to_lowercase(); - - if args.edit_patch || args.edit_page { - let file_name = if args.edit_patch { - format!("{command}.patch.md") - } else { - format!("{command}.page.md") - }; - - custom_pages_dir - .context("To edit custom pages/patches, please specify a custom pages directory.") - .and_then(|custom_pages_dir| spawn_editor(custom_pages_dir, &file_name))?; - - return Ok(ExitCode::SUCCESS); + // Show config file and path, pass through + if args.config_path { + print_warning( + enable_styles, + "The --config-path flag is deprecated, use --show-paths instead", + ); + show_config_path(enable_styles); } - - // Show various paths if args.show_paths { - show_paths(&config); + show_paths(); } // Create a basic config and exit if args.seed_config { - create_config(args.config_path.as_deref())?; - return Ok(ExitCode::SUCCESS); + create_config_and_exit(enable_styles); + } + + // Look up config file, if none is found fall back to default config. + let config = match Config::load(enable_styles) { + Ok(config) => config, + Err(ConfigError(msg)) => { + print_error(enable_styles, &format!("Could not load config: {}", msg)); + process::exit(1); + } + Err(e) => { + print_error(enable_styles, &format!("Could not load config: {}", e)); + process::exit(1); + } + }; + + if args.pager || config.display.use_pager { + configure_pager(enable_styles); } // If a local file was passed in, render it and exit if let Some(file) = args.render { - let reader = PageLookupResult::with_page(file).reader()?; - print_page(reader, args.raw, enable_styles, args.pager, &config)?; - return Ok(ExitCode::SUCCESS); - } - - // The tealdeer page is embedded in the binary, no cache needed - if command == "tealdeer" { - print_page( - TEALDEER_PAGE.as_bytes(), - args.raw, - enable_styles, - args.pager, - &config, - )?; - return Ok(ExitCode::SUCCESS); - } - - if let Some(platforms) = args.platforms { - config.search.platforms = platforms; - if !config.search.platforms.contains(&PlatformType::Common) { - config.search.platforms.push(PlatformType::Common); - } - } - - let (search_languages, download_languages): (&[_], &[_]) = match args.language.as_deref() { - Some(lang) => (&[Language(lang)], &[Language(lang)]), - None => (&config.search.languages, &config.updates.download_languages), - }; - - let cache_config = CacheConfig { - pages_directory: &config.directories.cache_dir.path().join(TLDR_PAGES_DIR), - custom_pages_directory: config - .directories - .custom_pages_dir - .as_ref() - .map(PathWithSource::path), - platforms: &config.search.platforms, - search_languages, - download_languages, - }; - - if args.clear_cache { - if let Some(cache) = Cache::open(cache_config)? { - clear_cache(cache, args.quiet)?; - } - return Ok(ExitCode::SUCCESS); - } - - let cache = if args.update || config.updates.auto_update && !args.no_auto_update { - let (mut cache, was_created) = Cache::open_or_create(cache_config)?; - if was_created || args.update || cache.age()? >= config.updates.auto_update_interval { - let result = update_cache( - &mut cache, - config.updates.archive_source, - config.updates.tls_backend, - args.quiet, - ); - - if let Err(e) = result { - print_error(enable_styles, &e); - - eprintln!(); - eprintln!( - "Note: Update errors are often caused by unexpected or missing TLS certificates." - ); - eprintln!( - "You are currently using the following TLS backend: {}", - config.updates.tls_backend, - ); - eprintln!( - "Try changing the updates.tls_backend setting in the config file, for example:" - ); - eprintln!(); - eprintln!(" [updates]"); - eprintln!(" tls_backend = \"rustls-with-native-roots\""); - eprintln!(); - eprintln!( - "This build of tealdeer has support for the following options: {}", - supported_tls_backends_string(), - ); - - return Ok(ExitCode::FAILURE); - } - } - - cache - } else if args.list || !command.is_empty() { - // Cache is needed for these commands to work - let Some(cache) = Cache::open(cache_config)? else { - if !args.quiet { - print_error( - enable_styles, - &anyhow::anyhow!( - "Page cache not found. Please run `tldr --update` to download the cache." - ), - ); - println!("\nNote: You can optionally enable automatic cache updates by adding the"); - println!("following config to your config file:\n"); - println!(" [updates]"); - println!(" auto_update = true\n"); - println!("The path to your config file can be looked up with `tldr --show-paths`."); - println!("To create an initial config file, use `tldr --seed-config`.\n"); - println!("You can find more tips and tricks in our docs:\n"); - println!(" https://docs.tealdeer.org"); - } - - return Ok(ExitCode::FAILURE); + let path = PageLookupResult::with_page(file); + if let Err(msg) = print_page(&path, args.raw, &config) { + print_error(enable_styles, &msg); + process::exit(1); + } else { + process::exit(0); }; + } - if let Some(max_cache_age) = config.updates.warn_cache_age { - let age = cache.age()?; - if age > max_cache_age && !args.quiet { - print_warning( - enable_styles, - &format!( - "The cache hasn't been updated for {} days.\n\ - You should probably run `tldr --update` soon.", - age.as_secs() / 24 / 3600 - ), - ); - } - } + // Initialize cache + let cache = Cache::new(ARCHIVE_URL, args.platform); - cache + // Clear cache, pass through + if args.clear_cache { + clear_cache(args.quiet, enable_styles); + } + + // Cache update, pass through + let cache_updated = if should_update_cache(&args, &config) { + update_cache(&cache, args.quiet, enable_styles); + true } else { - // There is nothing left to do - return Ok(ExitCode::SUCCESS); + false }; - if args.list { - for page in cache.list_pages()? { - println!("{page}"); - } + // Check cache presence and freshness + if !cache_updated + && (args.list || !args.command.is_empty()) + && check_cache(&args, enable_styles) == CheckCacheResult::CacheMissing + { + process::exit(1); + } - return Ok(ExitCode::SUCCESS); + // List cached commands and exit + if args.list { + // Get list of pages + let pages = cache.list_pages().unwrap_or_else(|e| { + print_error( + enable_styles, + &format!("Could not get list of pages: {}", e.message()), + ); + process::exit(1); + }); + + // Print pages + println!("{}", pages.join("\n")); + process::exit(0); } // Show command from cache - if !command.is_empty() { - // TODO: Remove this check 1 year after version 1.7.0 was released - if cache.old_custom_pages_exist()? { - print_warning( - enable_styles, - &format!( - "Custom pages using the old naming convention were found in {}.\n\ - Please rename them to follow the new convention:\n\ - - `.page` → `.page.md`\n\ - - `.patch` → `.patch.md`", - cache - .config() - .custom_pages_directory - .expect("Old custom pages can only exist in custom pages directory") - .display(), - ), - ); - } + if !args.command.is_empty() { + // Note: According to the TLDR client spec, page names must be transparently + // lowercased before lookup: + // https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#page-names + let command = args.command.join("-").to_lowercase(); - let Some(result) = cache.find_page(&command) else { + let languages = args + .language + .map_or_else(get_languages_from_env, |lang| vec![lang]); + + // Search for command in cache + if let Some(page) = cache.find_page( + &command, + &languages, + config.directories.custom_pages_dir.as_deref(), + ) { + if let Err(msg) = print_page(&page, args.raw, &config) { + print_error(enable_styles, &msg); + process::exit(1); + } + process::exit(0); + } else { if !args.quiet { print_warning( enable_styles, &format!( - "Page `{command}` not found in cache.\n\ + "Page `{}` not found in cache.\n\ Try updating with `tldr --update`, or submit a pull request to:\n\ - https://github.com/tldr-pages/tldr" + https://github.com/tldr-pages/tldr", + &command ), ); } - return Ok(ExitCode::FAILURE); - }; - - print_page( - result.reader()?, - args.raw, - enable_styles, - args.pager, - &config, - )?; + process::exit(1); + } + } +} + +#[cfg(test)] +mod test { + use crate::get_languages; + + mod language { + use super::*; + + #[test] + fn missing_lang_env() { + let lang_list = get_languages(None, Some("de:fr")); + assert_eq!(lang_list, ["en"]); + let lang_list = get_languages(None, None); + assert_eq!(lang_list, ["en"]); + } + + #[test] + fn missing_language_env() { + let lang_list = get_languages(Some("de"), None); + assert_eq!(lang_list, ["de", "en"]); + } + + #[test] + fn preference_order() { + let lang_list = get_languages(Some("de"), Some("fr:cn")); + assert_eq!(lang_list, ["fr", "cn", "de", "en"]); + } + + #[test] + fn country_code_expansion() { + let lang_list = get_languages(Some("pt_BR"), None); + assert_eq!(lang_list, ["pt_BR", "pt", "en"]); + } + + #[test] + fn ignore_posix_and_c() { + let lang_list = get_languages(Some("POSIX"), None); + assert_eq!(lang_list, ["en"]); + let lang_list = get_languages(Some("C"), None); + assert_eq!(lang_list, ["en"]); + } + + #[test] + fn no_duplicates() { + let lang_list = get_languages(Some("de"), Some("fr:de:cn:de")); + assert_eq!(lang_list, ["fr", "de", "cn", "en"]); + } } - - Ok(ExitCode::SUCCESS) } diff --git a/src/output.rs b/src/output.rs index 41c1119..70cd138 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,116 +1,75 @@ //! Functions for printing pages to the terminal -use std::io::{self, BufRead, BufReader, Read, Write}; - -use anyhow::{Context, Result}; -use yansi::Paint; +use std::{ + fs::File, + io::{self, BufRead, BufReader, Write}, +}; use crate::{ - config::{Config, PlaceholderFormat, StyleConfig}, - formatter::{PageSnippet, highlight_lines}, + cache::PageLookupResult, + config::{Config, StyleConfig}, + error::TealdeerError::WriteError, + formatter::{highlight_lines, PageSnippet}, line_iterator::LineIterator, }; -/// Set up display pager -/// -/// SAFETY: this function may be called multiple times -#[cfg(not(target_os = "windows"))] -fn configure_pager(_: bool) { - use std::sync::Once; - static INIT: Once = Once::new(); - INIT.call_once(|| pager::Pager::with_default_pager("less -R").setup()); -} - -#[cfg(target_os = "windows")] -fn configure_pager(enable_styles: bool) { - use crate::utils::print_warning; - print_warning(enable_styles, "--pager flag not available on Windows!"); -} - /// Print page by path pub fn print_page( - reader: impl Read, + page: &PageLookupResult, enable_markdown: bool, - enable_styles: bool, - use_pager: bool, config: &Config, -) -> Result<()> { - let reader = BufReader::new(reader); - - // Configure pager if applicable - if use_pager || config.display.use_pager { - configure_pager(enable_styles); - } - - // Lock stdout only once, this improves performance considerably +) -> Result<(), String> { let stdout = io::stdout(); let mut handle = stdout.lock(); - if enable_markdown { - // Print the raw markdown of the file. - for line in reader.lines() { - let line = line.context("Error while reading from a page")?; - writeln!(handle, "{line}").context("Could not write to stdout")?; - } - } else { - // Closure that processes a page snippet and writes it to stdout - let mut process_snippet = |snip: PageSnippet<&str>| { - if snip.is_empty() { - Ok(()) - } else { - print_snippet( - &mut handle, - snip, - &config.style, - config.display.placeholder_format, - ) - .context("Failed to print snippet") - } - }; + for path in page.paths() { + let file = File::open(path).map_err(|msg| format!("Could not open file: {}", msg))?; + let reader = BufReader::new(file); - // Print highlighted lines - highlight_lines( - LineIterator::new(reader), - &mut process_snippet, - !config.display.compact, - config.display.show_title, - config.display.indent, - ) - .context("Could not write to stdout")?; + if enable_markdown { + // Print the raw markdown of the file. + for line in reader.lines() { + writeln!(handle, "{}", line.unwrap()) + .map_err(|_| "Could not write to stdout".to_string())?; + } + } else { + let mut process_snippet = |snip: PageSnippet<'_>| { + if snip.is_empty() { + Ok(()) + } else { + print_snippet(&mut handle, snip, &config.style) + .map_err(|e| WriteError(e.to_string())) + } + }; + highlight_lines( + LineIterator::new(reader), + &mut process_snippet, + !config.display.compact, + ) + .map_err(|e| format!("Could not write to stdout: {}", e.message()))?; + }; } - // We're done outputting data, flush stdout now! - handle.flush().context("Could not flush stdout")?; + handle + .flush() + .map_err(|_| "Could not flush stdout".to_string())?; Ok(()) } fn print_snippet( writer: &mut impl Write, - snip: PageSnippet<&str>, + snip: PageSnippet<'_>, style: &StyleConfig, - placeholder_format: PlaceholderFormat, -) -> io::Result<()> { +) -> Result<(), io::Error> { use PageSnippet::*; match snip { - CommandName(s) | Title(s) => write!(writer, "{}", s.paint(style.command_name)), - Placeholder(s) => write!(writer, "{}", s.paint(style.example_variable)), - PlaceholderVariants { short, long } => match placeholder_format { - PlaceholderFormat::Short => write!(writer, "{}", short.paint(style.example_code)), - PlaceholderFormat::Long => write!(writer, "{}", long.paint(style.example_code)), - PlaceholderFormat::Both => { - write!( - writer, - "{}", - format!("[{short}|{long}]").paint(style.example_code) - ) - } - }, - NormalCode(s) => write!(writer, "{}", s.paint(style.example_code)), - Description(s) => write!(writer, "{}", s.paint(style.description)), - Text(s) => write!(writer, "{}", s.paint(style.example_text)), - Indent(n) => write!(writer, "{:n$}", ' '), + CommandName(s) => write!(writer, "{}", style.command_name.paint(s)), + Variable(s) => write!(writer, "{}", style.example_variable.paint(s)), + NormalCode(s) => write!(writer, "{}", style.example_code.paint(s)), + Description(s) => writeln!(writer, " {}", style.description.paint(s)), + Text(s) => writeln!(writer, " {}", style.example_text.paint(s)), Linebreak => writeln!(writer), } } diff --git a/src/types.rs b/src/types.rs index 7ca6e2d..72d5b92 100644 --- a/src/types.rs +++ b/src/types.rs @@ -2,21 +2,16 @@ use std::{fmt, str}; -use serde_derive::{Deserialize, Serialize}; +use serde::Deserialize; -#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] +/// The platform types supported by tldr. +#[derive(Debug, Eq, PartialEq, Copy, Clone)] #[allow(dead_code)] pub enum PlatformType { Linux, OsX, - Windows, SunOs, - Android, - FreeBsd, - NetBsd, - OpenBsd, - Common, + Windows, } impl fmt::Display for PlatformType { @@ -24,43 +19,66 @@ impl fmt::Display for PlatformType { match self { Self::Linux => write!(f, "Linux"), Self::OsX => write!(f, "macOS / BSD"), - Self::Windows => write!(f, "Windows"), Self::SunOs => write!(f, "SunOS"), - Self::Android => write!(f, "Android"), - Self::FreeBsd => write!(f, "FreeBSD"), - Self::NetBsd => write!(f, "NetBSD"), - Self::OpenBsd => write!(f, "OpenBSD"), - Self::Common => write!(f, "Common"), + Self::Windows => write!(f, "Windows"), } } } -impl clap::ValueEnum for PlatformType { - fn value_variants<'a>() -> &'a [Self] { - &[ - Self::Linux, - Self::OsX, - Self::SunOs, - Self::Windows, - Self::Android, - Self::FreeBsd, - Self::NetBsd, - Self::OpenBsd, - Self::Common, - ] +/// The platform lookup strategy. +/// +/// Includes both the platform type, as well as +#[derive(Debug, Copy, Clone)] +pub struct PlatformStrategy { + /// The platform type that should be looked up. + pub platform_type: PlatformType, + /// Flag indicating whether all pages should be listed or not. This is only + /// used when the special platform type `all` is specified by the user. + pub list_all: bool, +} + +impl PlatformStrategy { + pub fn new(platform_type: PlatformType) -> Self { + Self { + platform_type, + list_all: false, + } } - fn to_possible_value<'a>(&self) -> Option { - match self { - Self::Linux => Some(clap::builder::PossibleValue::new("linux")), - Self::OsX => Some(clap::builder::PossibleValue::new("macos").alias("osx")), - Self::Windows => Some(clap::builder::PossibleValue::new("windows")), - Self::SunOs => Some(clap::builder::PossibleValue::new("sunos")), - Self::Android => Some(clap::builder::PossibleValue::new("android")), - Self::FreeBsd => Some(clap::builder::PossibleValue::new("freebsd")), - Self::NetBsd => Some(clap::builder::PossibleValue::new("netbsd")), - Self::OpenBsd => Some(clap::builder::PossibleValue::new("openbsd")), - Self::Common => Some(clap::builder::PossibleValue::new("common")), + /// Return a `PlatformStrategy` containing the current platform as the + /// target platform type. + pub fn current() -> Self { + Self { + platform_type: PlatformType::current(), + list_all: false, + } + } + + /// Like `current()`, but when listing the pages, return the pages for all + /// platforms, not just for the current platform. + pub fn all() -> Self { + Self { + platform_type: PlatformType::current(), + list_all: true, + } + } +} + +impl str::FromStr for PlatformStrategy { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "linux" => Ok(PlatformStrategy::new(PlatformType::Linux)), + "osx" | "macos" => Ok(PlatformStrategy::new(PlatformType::OsX)), + "windows" => Ok(PlatformStrategy::new(PlatformType::Windows)), + "sunos" => Ok(PlatformStrategy::new(PlatformType::SunOs)), + "current" => Ok(PlatformStrategy::current()), + "all" => Ok(PlatformStrategy::all()), + other => Err(format!( + "Unknown platform: {}. Possible values: linux, macos, osx, windows, sunos, current, all", + other + )), } } } @@ -71,7 +89,13 @@ impl PlatformType { Self::Linux } - #[cfg(any(target_os = "macos", target_os = "dragonfly"))] + #[cfg(any( + target_os = "macos", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "dragonfly" + ))] pub fn current() -> Self { Self::OsX } @@ -81,26 +105,6 @@ impl PlatformType { Self::Windows } - #[cfg(target_os = "android")] - pub fn current() -> Self { - Self::Android - } - - #[cfg(target_os = "freebsd")] - pub fn current() -> Self { - Self::FreeBsd - } - - #[cfg(target_os = "netbsd")] - pub fn current() -> Self { - Self::NetBsd - } - - #[cfg(target_os = "openbsd")] - pub fn current() -> Self { - Self::OpenBsd - } - #[cfg(not(any( target_os = "linux", target_os = "macos", @@ -108,24 +112,43 @@ impl PlatformType { target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly", - target_os = "windows", - target_os = "android", + target_os = "windows" )))] pub fn current() -> Self { Self::Other } } -#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize, clap::ValueEnum)] +#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)] #[serde(rename_all = "lowercase")] -#[derive(Default)] pub enum ColorOptions { Always, - #[default] Auto, Never, } +impl str::FromStr for ColorOptions { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "always" => Ok(Self::Always), + "auto" => Ok(Self::Auto), + "never" => Ok(Self::Never), + other => Err(format!( + "Unknown color option: {}. Possible values: always, auto, never", + other + )), + } + } +} + +impl Default for ColorOptions { + fn default() -> Self { + Self::Auto + } +} + #[derive(Debug, Eq, PartialEq)] pub enum LineType { Empty, @@ -193,16 +216,16 @@ impl LineType { } /// The reason why a certain path (e.g. config path or cache dir) was chosen. -#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[derive(Debug, PartialEq)] pub enum PathSource { /// OS convention (e.g. XDG on Linux) OsConvention, /// Env variable (TEALDEER_*) EnvVar, - /// Config file - ConfigFile, - /// CLI argument override - Cli, + + #[allow(dead_code)] // Waiting for Pull Request #141 + /// Config file variable + ConfigVar, } impl fmt::Display for PathSource { @@ -213,8 +236,7 @@ impl fmt::Display for PathSource { match self { Self::OsConvention => "OS convention", Self::EnvVar => "env variable", - Self::ConfigFile => "config file", - Self::Cli => "command line argument", + Self::ConfigVar => "config file variable", } ) } diff --git a/src/utils.rs b/src/utils.rs index f4d825a..e6f65f5 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -use yansi::{Color, Paint}; +use ansi_term::{Color, Style}; /// Print a warning to stderr. If `enable_styles` is true, then a yellow /// message will be printed. @@ -6,16 +6,17 @@ pub fn print_warning(enable_styles: bool, message: &str) { print_msg(enable_styles, message, "Warning: ", Color::Yellow); } -/// Print an anyhow error to stderr. If `enable_styles` is true, then a red -/// message will be printed. -pub fn print_error(enable_styles: bool, error: &anyhow::Error) { - print_msg(enable_styles, &format!("{error:?}"), "Error: ", Color::Red); +/// Print an error to stderr. If `enable_styles` is true, then a red message +/// will be printed. +pub fn print_error(enable_styles: bool, message: &str) { + print_msg(enable_styles, message, "Error: ", Color::Red); } fn print_msg(enable_styles: bool, message: &str, prefix: &'static str, color: Color) { if enable_styles { - eprintln!("{}{}", prefix.paint(color), message.paint(color)); + let style = Style::new().fg(color); + eprintln!("{}{}", style.paint(prefix), style.paint(message)); } else { - eprintln!("{message}"); + eprintln!("{}", message); } } diff --git a/tests/cache/pages.en/common/git-checkout.md b/tests/cache/pages.en/common/git-checkout.md deleted file mode 100644 index ca1bacc..0000000 --- a/tests/cache/pages.en/common/git-checkout.md +++ /dev/null @@ -1,36 +0,0 @@ -# git checkout - -> Checkout a branch or paths to the working tree. -> More information: . - -- Create and switch to a new branch: - -`git checkout -b {{branch_name}}` - -- Create and switch to a new branch based on a specific reference (branch, remote/branch, tag are examples of valid references): - -`git checkout -b {{branch_name}} {{reference}}` - -- Switch to an existing local branch: - -`git checkout {{branch_name}}` - -- Switch to the previously checked out branch: - -`git checkout -` - -- Switch to an existing remote branch: - -`git checkout --track {{remote_name}}/{{branch_name}}` - -- Discard all unstaged changes in the current directory (see `git reset` for more undo-like commands): - -`git checkout .` - -- Discard unstaged changes to a given file: - -`git checkout {{path/to/file}}` - -- Replace a file in the current directory with the version of it committed in a given branch: - -`git checkout {{branch_name}} -- {{path/to/file}}` diff --git a/tests/cache/pages.en/common/playerctl.md b/tests/cache/pages.en/common/playerctl.md deleted file mode 100644 index 8b54f12..0000000 --- a/tests/cache/pages.en/common/playerctl.md +++ /dev/null @@ -1,32 +0,0 @@ -# playerctl - -> Control media players via MPRIS. -> More information: . - -- Toggle play: - -`playerctl play-pause` - -- Skip to the next track: - -`playerctl next` - -- Go back to the previous track: - -`playerctl previous` - -- List all players: - -`playerctl {{[-l|--list-all]}}` - -- Send a command to a specific player: - -`playerctl {{[-p|--player]}} {{player_name}} {{play-pause|next|previous|...}}` - -- Send a command to all players: - -`playerctl {{[-a|--all-players]}} {{play-pause|next|previous|...}}` - -- Display metadata about the current track: - -`playerctl metadata {{[-f|--format]}} "{{Now playing: \{\{artist\}\} - \{\{album\}\} - \{\{title\}\}}}"` diff --git a/tests/cache/pages.ja/common/apt.md b/tests/cache/pages.ja/common/apt.md deleted file mode 100644 index fe16a3d..0000000 --- a/tests/cache/pages.ja/common/apt.md +++ /dev/null @@ -1,37 +0,0 @@ -# apt - -> Debian系ディストリビューションで使われるパッケージ管理システムです。 -> Ubuntuのバージョンが16.04か、それ以降で対話モードを使う場合`apt-get`の代わりとして使用します。 -> 詳しくはこちら: - -- 利用可能なパーケージとバージョンのリストの更新(他の`apt`コマンドの前での実行を推奨): - -`sudo apt update` - -- 指定されたパッケージの検索: - -`apt search {{パッケージ}}` - -- パッケージの情報を出力: - -`apt show {{パッケージ}}` - -- パッケージのインストール、または利用可能な最新バージョンに更新: - -`sudo apt install {{パッケージ}}` - -- パッケージの削除(`sudo apt remove --purge`の場合設定ファイルも削除): - -`sudo apt remove {{パッケージ}}` - -- インストールされている全てのパッケージを最新のバージョンにアップグレード: - -`sudo apt upgrade` - -- インストールできるすべてのパッケージを表示: - -`apt list` - -- インストールされた全てのパッケージを表示(依存関係も表示): - -`apt list --installed` diff --git a/tests/chmod.ru.expected b/tests/chmod.ru.expected new file mode 100644 index 0000000..c7f92c2 --- /dev/null +++ b/tests/chmod.ru.expected @@ -0,0 +1,32 @@ + + Изменить права доступа файлу или папке. + Больше информации: . + + Дать [u]пользователю, который владеет файлом, права на его [x]исполнение: + + chmod u+x файл + + Дать права [u]пользователю права [r]чтения и [w]записи в файл/папку: + + chmod u+rw файл_или_папка + + Убрать права на [x]исполнение у [g]группы: + + chmod g-x файл + + Дать [a]всем пользователям права на [r]чтение и [x]исполенеие: + + chmod a+rx файл + + Дать [o]другим (не из группы владельцев файлом) такие же права как и у [g]группы: + + chmod o=g файл + + Убрать все права у [o]других: + + chmod o= файл + + Изменить права рекурсивно, дав [g]группе и [o]другим возможность [w]записи в папку: + + chmod -R g+w,o+w папка + diff --git a/tests/chmod.ru.md b/tests/chmod.ru.md new file mode 100644 index 0000000..4b92329 --- /dev/null +++ b/tests/chmod.ru.md @@ -0,0 +1,32 @@ +# chmod + +> Изменить права доступа файлу или папке. +> Больше информации: . + +- Дать [u]пользователю, который владеет файлом, права на его [x]исполнение: + +`chmod u+x {{файл}}` + +- Дать права [u]пользователю права [r]чтения и [w]записи в файл/папку: + +`chmod u+rw {{файл_или_папка}}` + +- Убрать права на [x]исполнение у [g]группы: + +`chmod g-x {{файл}}` + +- Дать [a]всем пользователям права на [r]чтение и [x]исполенеие: + +`chmod a+rx {{файл}}` + +- Дать [o]другим (не из группы владельцев файлом) такие же права как и у [g]группы: + +`chmod o=g {{файл}}` + +- Убрать все права у [o]других: + +`chmod o= {{файл}}` + +- Изменить права рекурсивно, дав [g]группе и [o]другим возможность [w]записи в папку: + +`chmod -R g+w,o+w {{папка}}` diff --git a/tests/style-config.toml b/tests/config.toml similarity index 73% rename from tests/style-config.toml rename to tests/config.toml index c68f2cc..3b90d90 100644 --- a/tests/style-config.toml +++ b/tests/config.toml @@ -19,3 +19,11 @@ underline = false underline = true bold = false italic = true + +[display] +use_pager = false +compact = false + +[updates] +auto_update = false +auto_update_interval_hours = 720 diff --git a/tests/custom-pages/inkscape-v2.patch.md b/tests/custom-pages/inkscape-v2.patch.md deleted file mode 100644 index 5cc5d22..0000000 --- a/tests/custom-pages/inkscape-v2.patch.md +++ /dev/null @@ -1,3 +0,0 @@ -Custom inkscape entry - - My Inkscape example diff --git a/tests/rendered/inkscape-default-no-color.expected b/tests/inkscape-default-no-color.expected similarity index 100% rename from tests/rendered/inkscape-default-no-color.expected rename to tests/inkscape-default-no-color.expected diff --git a/tests/inkscape-default.expected b/tests/inkscape-default.expected new file mode 100644 index 0000000..da909f2 --- /dev/null +++ b/tests/inkscape-default.expected @@ -0,0 +1,32 @@ + + An SVG (Scalable Vector Graphics) editing program. + Use -z to not open the GUI and only process files in the console. + + Open an SVG file in the Inkscape GUI: + + inkscape filename.svg + + Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI): + + inkscape filename.svg -e filename.png + + Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur): + + inkscape filename.svg -e filename.png -w 600 -h 400 + + Export a single object, given its ID, into a bitmap: + + inkscape filename.svg -i id -e object.png + + Export an SVG document to PDF, converting all texts to paths: + + inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path + + Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: + + inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit + + Some invalid command just to test the correct highlighting of the command name: + + inkscape --use-inkscape=v3.0 file + diff --git a/tests/rendered/inkscape-patched-no-color.expected b/tests/inkscape-patched-no-color.expected similarity index 100% rename from tests/rendered/inkscape-patched-no-color.expected rename to tests/inkscape-patched-no-color.expected diff --git a/tests/cache/pages.en/common/inkscape-v1.md b/tests/inkscape-v1.md similarity index 100% rename from tests/cache/pages.en/common/inkscape-v1.md rename to tests/inkscape-v1.md diff --git a/tests/cache/pages.en/common/inkscape-v2.md b/tests/inkscape-v2.md similarity index 100% rename from tests/cache/pages.en/common/inkscape-v2.md rename to tests/inkscape-v2.md diff --git a/tests/inkscape-v2.patch b/tests/inkscape-v2.patch new file mode 100644 index 0000000..ffca34c --- /dev/null +++ b/tests/inkscape-v2.patch @@ -0,0 +1,5 @@ +This header shouldn't be required +================================= +Custom inkscape entry + + My Inkscape example diff --git a/tests/rendered/inkscape-with-config.expected b/tests/inkscape-with-config.expected similarity index 100% rename from tests/rendered/inkscape-with-config.expected rename to tests/inkscape-with-config.expected diff --git a/tests/lib.rs b/tests/lib.rs index 53c15ef..e79d9aa 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,9 +1,8 @@ //! Integration tests. use std::{ - fs::{self, File, create_dir_all}, - io::{self, Write}, - path::{Path, PathBuf}, + fs::{create_dir_all, File}, + io::Write, process::Command, time::{Duration, SystemTime}, }; @@ -11,93 +10,46 @@ use std::{ use assert_cmd::prelude::*; use predicates::{ boolean::PredicateBooleanExt, - ord::eq, - prelude::predicate::str::{contains, diff, is_empty, is_match}, + prelude::predicate::str::{contains, diff, is_empty}, }; -use tempfile::{Builder as TempfileBuilder, TempDir}; +use tempfile::{Builder, TempDir}; + +// TODO: Should be 'cache::CACHE_DIR_ENV_VAR'. This requires to have a library crate for the logic. +static CACHE_DIR_ENV_VAR: &str = "TEALDEER_CACHE_DIR"; pub static TLDR_PAGES_DIR: &str = "tldr-pages"; struct TestEnv { - _test_dir: TempDir, + pub cache_dir: TempDir, + pub custom_pages_dir: TempDir, + pub config_dir: TempDir, + pub input_dir: TempDir, pub default_features: bool, pub features: Vec, } impl TestEnv { fn new() -> Self { - let test_dir: TempDir = TempfileBuilder::new() - .prefix(".tldr.test") - .tempdir() - .unwrap(); - - let this = TestEnv { - _test_dir: test_dir, + TestEnv { + cache_dir: Builder::new().prefix(".tldr.test.cache").tempdir().unwrap(), + config_dir: Builder::new().prefix(".tldr.test.conf").tempdir().unwrap(), + custom_pages_dir: Builder::new() + .prefix(".tldr.test.custom-pages") + .tempdir() + .unwrap(), + input_dir: Builder::new().prefix(".tldr.test.input").tempdir().unwrap(), default_features: true, features: vec![], - }; - - create_dir_all(this.cache_dir()).unwrap(); - create_dir_all(this.config_dir()).unwrap(); - create_dir_all(this.custom_pages_dir()).unwrap(); - - this.init_config(); - - this + } } - fn cache_dir(&self) -> PathBuf { - self._test_dir.path().join(".cache") - } + /// Write `content` to "config.toml" in the `config_dir` directory + fn write_config(&self, content: impl AsRef) { + let config_file_name = self.config_dir.path().join("config.toml"); + println!("Config path: {:?}", config_file_name); - fn config_dir(&self) -> PathBuf { - self._test_dir.path().join(".config") - } - - fn custom_pages_dir(&self) -> PathBuf { - self._test_dir.path().join(".custom_pages") - } - - fn append_to_config(&self, content: impl AsRef) { - File::options() - .create(true) - .append(true) - .open(self.config_dir().join("config.toml")) - .expect("Failed to open config file") - .write_all(content.as_ref().as_bytes()) - .expect("Failed to append to config file."); - } - fn delete_config(&self) { - fs::remove_file(self.config_dir().join("config.toml")).unwrap(); - } - fn init_config(&self) { - self.append_to_config(format!( - "directories.cache_dir = '{}'\n", - self.cache_dir().to_str().unwrap(), - )); - } - - fn create_secondary_config(self) -> Self { - self.append_to_secondary_config(format!( - "directories.cache_dir = '{}'\n", - self.cache_dir().to_str().unwrap(), - )); - self - } - - fn append_to_secondary_config(&self, content: impl AsRef) { - File::options() - .create(true) - .append(true) - .open(self.config_dir().join("config-secondary.toml")) - .expect("Failed to open config file") - .write_all(content.as_ref().as_bytes()) - .expect("Failed to append to config file."); - } - - fn remove_initial_config(self) -> Self { - let _ = fs::remove_file(self.config_dir().join("config.toml")); - self + let mut config_file = File::create(&config_file_name).unwrap(); + config_file.write_all(content.as_ref().as_bytes()).unwrap(); } /// Add entry for that environment to the "common" pages. @@ -107,47 +59,43 @@ impl TestEnv { /// Add entry for that environment to an OS-specific subfolder. fn add_os_entry(&self, os: &str, name: &str, contents: &str) { - self.add_os_lang_entry(os, "en", name, contents); - } - - /// Add entry for that environment to a language-specific subfolder. - fn add_lang_entry(&self, lang: &str, name: &str, contents: &str) { - self.add_os_lang_entry("common", lang, name, contents); - } - - /// Add entry for that environment to an OS- and language specific subfolder. - fn add_os_lang_entry(&self, os: &str, lang: &str, name: &str, contents: &str) { let dir = self - .cache_dir() + .cache_dir + .path() .join(TLDR_PAGES_DIR) - .join(format!("pages.{lang}")) + .join("pages") .join(os); create_dir_all(&dir).unwrap(); - fs::write(dir.join(format!("{name}.md")), contents.as_bytes()).unwrap(); + let mut file = File::create(&dir.join(format!("{}.md", name))).unwrap(); + file.write_all(contents.as_bytes()).unwrap(); } /// Add custom patch entry to the custom_pages_dir fn add_page_entry(&self, name: &str, contents: &str) { - let dir = &self.custom_pages_dir(); + let dir = self.custom_pages_dir.path(); create_dir_all(dir).unwrap(); - fs::write(dir.join(format!("{name}.page.md")), contents.as_bytes()).unwrap(); + let mut file = File::create(&dir.join(format!("{}.page", name))).unwrap(); + file.write_all(contents.as_bytes()).unwrap(); } /// Add custom patch entry to the custom_pages_dir fn add_patch_entry(&self, name: &str, contents: &str) { - let dir = &self.custom_pages_dir(); + let dir = self.custom_pages_dir.path(); create_dir_all(dir).unwrap(); - fs::write(dir.join(format!("{name}.patch.md")), contents.as_bytes()).unwrap(); + let mut file = File::create(&dir.join(format!("{}.patch", name))).unwrap(); + file.write_all(contents.as_bytes()).unwrap(); } /// Disable default features. + #[allow(dead_code)] // Might be useful in the future fn no_default_features(mut self) -> Self { self.default_features = false; self } /// Add the specified feature. + #[allow(dead_code)] // Might be useful in the future fn with_feature>(mut self, feature: S) -> Self { self.features.push(feature.into()); self @@ -157,137 +105,23 @@ impl TestEnv { fn command(&self) -> Command { let mut build = escargot::CargoBuild::new() .bin("tldr") - .arg("--color=never") .current_release() .current_target(); if !self.default_features { - build = build.no_default_features(); + build = build.arg("--no-default-features"); } if !self.features.is_empty() { - build = build.features(self.features.join(" ")) + build = build.arg(&format!("--feature {}", self.features.join(","))); } - let run = build.run().expect("Failed to build tealdeer for testing"); + let run = build.run().unwrap(); let mut cmd = run.command(); - - // Avoid inheriting those from the test process. We can't just use .env_clear() because - // this breaks tests on Windows in GitHub Actions. - let relevant_env_variables = [ - "LANG", - "LANGUAGE", - "TEALDEER_CACHE_DIR", - "EDITOR", - "NO_COLOR", - ]; - for variable_name in relevant_env_variables { - cmd.env_remove(variable_name); - } - cmd.env("TEALDEER_CONFIG_DIR", self.config_dir().to_str().unwrap()); + cmd.env(CACHE_DIR_ENV_VAR, self.cache_dir.path().to_str().unwrap()); + cmd.env( + "TEALDEER_CONFIG_DIR", + self.config_dir.path().to_str().unwrap(), + ); cmd } - - fn install_default_cache(self) -> Self { - copy_recursively( - &PathBuf::from_iter([env!("CARGO_MANIFEST_DIR"), "tests", "cache"]), - &self.cache_dir().join(TLDR_PAGES_DIR), - ) - .expect("Failed to copy the cache to the test environment"); - - self - } - - fn install_default_custom_pages(self) -> Self { - copy_recursively( - &PathBuf::from_iter([env!("CARGO_MANIFEST_DIR"), "tests", "custom-pages"]), - self.custom_pages_dir().as_path(), - ) - .expect("Failed to copy the custom pages to the test environment"); - - self.write_custom_pages_config() - } - - fn write_custom_pages_config(self) -> Self { - self.append_to_config(format!( - "directories.custom_pages_dir = '{}'\n", - self.custom_pages_dir().to_str().unwrap() - )); - - self - } -} - -fn copy_recursively(source: &Path, destination: &Path) -> io::Result<()> { - if source.is_dir() { - fs::create_dir_all(destination)?; - for entry in fs::read_dir(source)? { - let entry = entry?; - copy_recursively(&entry.path(), &destination.join(entry.file_name()))?; - } - } else { - fs::copy(source, destination)?; - } - - Ok(()) -} - -#[test] -#[should_panic] -fn test_cannot_build_without_tls_feature() { - let _ = TestEnv::new().no_default_features().command(); -} - -#[test] -fn test_load_the_correct_config() { - let testenv = TestEnv::new() - .install_default_cache() - .create_secondary_config(); - testenv.append_to_secondary_config(include_str!("style-config.toml")); - - let expected_default = include_str!("rendered/inkscape-default.expected"); - let expected_with_config = include_str!("rendered/inkscape-with-config.expected"); - - testenv - .command() - .args(["--color", "always", "inkscape-v2"]) - .assert() - .success() - .stdout(diff(expected_default)); - - testenv - .command() - .args([ - "--color", - "always", - "--config-path", - testenv - .config_dir() - .join("config-secondary.toml") - .to_str() - .unwrap(), - "inkscape-v2", - ]) - .assert() - .success() - .stdout(diff(expected_with_config)); -} - -#[test] -fn test_fail_on_custom_config_path_is_directory() { - let testenv = TestEnv::new(); - let error = if cfg!(windows) { - "Access is denied" - } else { - "Is a directory" - }; - testenv - .command() - .args([ - "--config-path", - testenv.config_dir().to_str().unwrap(), - "sl", - ]) - .assert() - .failure() - .stderr(contains(error)); } #[test] @@ -297,22 +131,11 @@ fn test_missing_cache() { .args(["sl"]) .assert() .failure() - .stderr(contains("Page cache not found. Please run `tldr --update`")); + .stderr(contains("Cache not found. Please run `tldr --update`.")); } #[test] -fn test_tealdeer_page_works_without_cache() { - TestEnv::new() - .command() - .args(["tealdeer"]) - .assert() - .success() - .stdout(contains("for your installed tealdeer version")); -} - -#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] -#[test] -fn test_update_cache_default_features() { +fn test_update_cache() { let testenv = TestEnv::new(); testenv @@ -320,7 +143,7 @@ fn test_update_cache_default_features() { .args(["sl"]) .assert() .failure() - .stderr(contains("Page cache not found. Please run `tldr --update`")); + .stderr(contains("Cache not found. Please run `tldr --update`.")); testenv .command() @@ -332,55 +155,6 @@ fn test_update_cache_default_features() { testenv.command().args(["sl"]).assert().success(); } -#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] -#[test] -fn test_update_cache_rustls_webpki() { - let testenv = TestEnv::new() - .no_default_features() - .with_feature("rustls-with-webpki-roots"); - - testenv - .command() - .args(["sl"]) - .assert() - .failure() - .stderr(contains("Page cache not found. Please run `tldr --update`")); - - testenv - .command() - .args(["--update"]) - .assert() - .success() - .stderr(contains("Successfully updated cache.")); - - testenv.command().args(["sl"]).assert().success(); -} - -#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] -#[test] -fn test_update_cache_native_tls() { - let testenv = TestEnv::new() - .no_default_features() - .with_feature("rustls-with-native-roots"); - - testenv - .command() - .args(["sl"]) - .assert() - .failure() - .stderr(contains("Page cache not found. Please run `tldr --update`")); - - testenv - .command() - .args(["--update"]) - .assert() - .success() - .stderr(contains("Successfully updated cache.")); - - testenv.command().args(["sl"]).assert().success(); -} - -#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] #[test] fn test_quiet_cache() { let testenv = TestEnv::new(); @@ -400,42 +174,15 @@ fn test_quiet_cache() { } #[test] -fn test_clear_only_pages_directory() { - let testenv = TestEnv::new().install_default_cache(); +fn test_quiet_failures() { + let testenv = TestEnv::new(); + testenv .command() - .args(["--clear-cache"]) + .args(["--update", "-q"]) .assert() .success() - .stderr(contains(format!( - "Successfully cleared cache at `{}`.", - testenv.cache_dir().join(TLDR_PAGES_DIR).to_str().unwrap(), - ))); - - assert!(testenv.cache_dir().is_dir()); - assert!(!testenv.cache_dir().join(TLDR_PAGES_DIR).exists()); -} - -#[test] -fn test_warn_invalid_tls_backend() { - let testenv = TestEnv::new() - .no_default_features() - .with_feature("rustls-with-webpki-roots") - .remove_initial_config(); - - testenv.append_to_config("updates.tls_backend = 'invalid-tls-backend'\n"); - - testenv - .command() - .args(["sl"]) - .assert() - .failure() - .stderr(contains("unknown variant `invalid-tls-backend`, expected one of `native-tls`, `rustls-with-webpki-roots`, `rustls-with-native-roots`")); -} - -#[test] -fn test_quiet_failures() { - let testenv = TestEnv::new().install_default_cache(); + .stdout(is_empty()); testenv .command() @@ -446,87 +193,55 @@ fn test_quiet_failures() { } #[test] -fn test_quiet_missing_cache() { +fn test_quiet_old_cache() { let testenv = TestEnv::new(); - for args in [["--list", "--quiet"], ["sl", "--quiet"]] { - testenv - .command() - .args(args) - .assert() - .failure() - .stdout(is_empty()) - .stderr(is_empty()); - } -} - -#[test] -fn test_quiet_old_cache() { - let testenv = TestEnv::new().install_default_cache(); + testenv + .command() + .args(["--update", "-q"]) + .assert() + .success() + .stdout(is_empty()); filetime::set_file_mtime( - testenv.cache_dir().join(TLDR_PAGES_DIR), + testenv.cache_dir.path().join(TLDR_PAGES_DIR), filetime::FileTime::from_unix_time(1, 0), ) .unwrap(); testenv .command() - .args(["which"]) + .args(["tldr"]) .assert() .success() .stderr(contains("The cache hasn't been updated for ")); testenv .command() - .args(["which", "--quiet"]) + .args(["tldr", "--quiet"]) .assert() .success() .stderr(contains("The cache hasn't been updated for ").not()); } -#[test] -fn test_warn_cache_age_never() { - let testenv = TestEnv::new().install_default_cache(); - - filetime::set_file_mtime( - testenv.cache_dir().join(TLDR_PAGES_DIR), - filetime::FileTime::from_unix_time(1, 0), - ) - .unwrap(); - - testenv.append_to_config("[updates]\nwarn_cache_age = \"never\"\n"); - - testenv - .command() - .args(["which"]) - .assert() - .success() - .stderr(contains("The cache hasn't been updated for ").not()); -} - -#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] #[test] fn test_create_cache_directory_path() { - let testenv = TestEnv::new().remove_initial_config(); - let cache_dir = &testenv.cache_dir(); + let testenv = TestEnv::new(); + let cache_dir = testenv.cache_dir.path(); let internal_cache_dir = cache_dir.join("internal"); - testenv.append_to_config(format!( - "directories.cache_dir = '{}'\n", - internal_cache_dir.to_str().unwrap() - )); let mut command = testenv.command(); + command.env(CACHE_DIR_ENV_VAR, internal_cache_dir.to_str().unwrap()); assert!(!internal_cache_dir.exists()); command - .arg("--update") + .arg("-u") .assert() .success() .stderr(contains(format!( - "Successfully created cache directory `{}`.", - internal_cache_dir.join(TLDR_PAGES_DIR).to_str().unwrap() + "Successfully created cache directory path `{}`.", + internal_cache_dir.to_str().unwrap() ))) .stderr(contains("Successfully updated cache.")); @@ -536,191 +251,69 @@ fn test_create_cache_directory_path() { #[test] fn test_cache_location_not_a_directory() { let testenv = TestEnv::new(); - let cache_dir = &testenv.cache_dir(); - File::create(cache_dir.join(TLDR_PAGES_DIR)).unwrap(); + let cache_dir = testenv.cache_dir.path(); + let internal_file = cache_dir.join("internal"); + File::create(&internal_file).unwrap(); - testenv - .command() - .arg("--list") + let mut command = testenv.command(); + command.env(CACHE_DIR_ENV_VAR, internal_file.to_str().unwrap()); + + command + .arg("-u") .assert() .failure() .stderr(contains(format!( - "Cache directory `{}` exists, but is not a directory.", - cache_dir.join(TLDR_PAGES_DIR).display(), + "Path specified by ${} is not a directory.", + CACHE_DIR_ENV_VAR ))); } -#[cfg(unix)] -#[test] -fn test_cache_location_permission_denied() { - use std::os::unix::fs::PermissionsExt; - - let testenv = TestEnv::new().install_default_cache(); - - testenv - .command() - .arg("--list") - .assert() - .success() - .stderr(contains("Permission denied").not()); - - // Make cache directory unreadable - let cache_dir = testenv.cache_dir(); - let mut permissions = cache_dir.metadata().unwrap().permissions(); - permissions.set_mode(0o0); - fs::set_permissions(cache_dir, permissions).unwrap(); - - testenv - .command() - .arg("--list") - .assert() - .failure() - .stderr(contains("Permission denied")); -} - -#[test] -fn test_cache_location_source() { - let testenv = TestEnv::new().remove_initial_config(); - let default_cache_dir = &testenv.cache_dir(); - let tmp_cache_dir = TempfileBuilder::new() - .prefix(".tldr.test.cache_dir") - .tempdir() - .unwrap(); - - // Source: Default (OS convention) - let mut command = testenv.command(); - command - .arg("--show-paths") - .assert() - .success() - .stdout(is_match("\nCache dir: [^(]* \\(OS convention\\)\n").unwrap()); - - // Source: Config variable - let mut command = testenv.command(); - testenv.append_to_config(format!( - "directories.cache_dir = '{}'\n", - tmp_cache_dir.path().to_str().unwrap(), - )); - command - .arg("--show-paths") - .assert() - .success() - .stdout(is_match("\nCache dir: [^(]* \\(config file\\)\n").unwrap()); - - // Source: Env var - let mut command = testenv.command(); - command.env("TEALDEER_CACHE_DIR", default_cache_dir.to_str().unwrap()); - command - .arg("--show-paths") - .assert() - .success() - .stdout(is_match("\nCache dir: [^(]* \\(env variable\\)\n").unwrap()); -} - #[test] fn test_setup_seed_config() { let testenv = TestEnv::new(); - testenv - .command() - .args(["--seed-config"]) - .assert() - .failure() - .stderr(contains("A configuration file already exists")); - - assert!(testenv.config_dir().join("config.toml").is_file()); - - let testenv = testenv.remove_initial_config(); testenv .command() .args(["--seed-config"]) .assert() .success() .stderr(contains("Successfully created seed config file here")); - - assert!(testenv.config_dir().join("config.toml").is_file()); - - // Create parent directories as needed for the default config path. - fs::remove_dir_all(testenv.config_dir()).unwrap(); - testenv - .command() - .args(["--seed-config"]) - .assert() - .success() - .stderr(contains("Successfully created seed config file here")); - - assert!(testenv.config_dir().join("config.toml").is_file()); - - // Write the default config to --config-path if specified by the user - // at the same time. - let custom_config_path = testenv.config_dir().join("config_custom.toml"); - testenv - .command() - .args([ - "--seed-config", - "--config-path", - custom_config_path.to_str().unwrap(), - ]) - .assert() - .success() - .stderr(contains("Successfully created seed config file here")); - - assert!(custom_config_path.is_file()); - - // DON'T create parent directories for a custom config path. - fs::remove_dir_all(testenv.config_dir()).unwrap(); - testenv - .command() - .args([ - "--seed-config", - "--config-path", - custom_config_path.to_str().unwrap(), - ]) - .assert() - .failure() - .stderr(contains("Could not create config file")); - - assert!(!custom_config_path.is_file()); } #[test] fn test_show_paths() { let testenv = TestEnv::new(); - // Show general commands testenv .command() .args(["--show-paths"]) .assert() .success() .stdout(contains(format!( - "Config dir: {}", - testenv.config_dir().to_str().unwrap(), + "Config dir: {}", + testenv.config_dir.path().to_str().unwrap(), ))) .stdout(contains(format!( - "Config path: {}", - testenv.config_dir().join("config.toml").to_str().unwrap(), + "Config path: {}", + testenv + .config_dir + .path() + .join("config.toml") + .to_str() + .unwrap(), ))) .stdout(contains(format!( - "Cache dir: {}", - testenv.cache_dir().to_str().unwrap(), + "Cache dir: {}", + testenv.cache_dir.path().to_str().unwrap(), ))) .stdout(contains(format!( - "Pages dir: {}", - testenv.cache_dir().join(TLDR_PAGES_DIR).to_str().unwrap(), - ))); - - let testenv = testenv.write_custom_pages_config(); - - // Now ensure that this path is contained in the output - testenv - .command() - .args(["--show-paths"]) - .assert() - .success() - .stdout(contains(format!( - "Custom pages dir: {}", - testenv.custom_pages_dir().to_str().unwrap(), + "Pages dir: {}", + testenv + .cache_dir + .path() + .join(TLDR_PAGES_DIR) + .to_str() + .unwrap(), ))); } @@ -737,41 +330,13 @@ fn test_os_specific_page() { .success(); } -#[test] -fn test_config_platforms() { - let testenv = TestEnv::new(); - testenv.add_os_entry("sunos", "sunos-command", ""); - - let set_config_platforms = |platforms| { - testenv.delete_config(); - testenv.init_config(); - testenv.append_to_config(format!("search.platforms = {platforms}")); - }; - - // By default all platforms are searched - testenv.command().arg("sunos-command").assert().success(); - - set_config_platforms("[]"); - testenv.command().arg("sunos-command").assert().failure(); - - set_config_platforms("['linux']"); - testenv.command().arg("sunos-command").assert().failure(); - - set_config_platforms("['sunos']"); - testenv.command().arg("sunos-command").assert().success(); - - set_config_platforms("['linux', 'all']"); - testenv.command().arg("sunos-command").assert().success(); - - set_config_platforms("['current', 'all']"); - testenv.command().arg("sunos-command").assert().success(); -} - #[test] fn test_markdown_rendering() { - let testenv = TestEnv::new().install_default_cache(); + let testenv = TestEnv::new(); - let expected = include_str!("cache/pages.en/common/which.md"); + testenv.add_entry("which", include_str!("which-markdown.expected")); + + let expected = include_str!("which-markdown.expected"); testenv .command() .args(["--raw", "which"]) @@ -780,13 +345,23 @@ fn test_markdown_rendering() { .stdout(diff(expected)); } -fn _test_correct_rendering(page: &str, expected: &'static str, additional_args: &[&str]) { - let testenv = TestEnv::new().install_default_cache(); +fn _test_correct_rendering( + input_file: &str, + filename: &str, + expected: &'static str, + color_option: &str, +) { + let testenv = TestEnv::new(); + + // Create input file + let file_path = testenv.input_dir.path().join(filename); + println!("Testfile path: {:?}", file_path); + let mut file = File::create(&file_path).unwrap(); + file.write_all(input_file.as_bytes()).unwrap(); testenv .command() - .args(additional_args) - .arg(page) + .args(["--color", color_option, "-f", file_path.to_str().unwrap()]) .assert() .success() .stdout(diff(expected)); @@ -796,9 +371,10 @@ fn _test_correct_rendering(page: &str, expected: &'static str, additional_args: #[test] fn test_correct_rendering_v1() { _test_correct_rendering( - "inkscape-v1", - include_str!("rendered/inkscape-default.expected"), - &["--color", "always"], + include_str!("inkscape-v1.md"), + "inkscape-v1.md", + include_str!("inkscape-default.expected"), + "always", ); } @@ -806,9 +382,10 @@ fn test_correct_rendering_v1() { #[test] fn test_correct_rendering_v2() { _test_correct_rendering( - "inkscape-v2", - include_str!("rendered/inkscape-default.expected"), - &["--color", "always"], + include_str!("inkscape-v2.md"), + "inkscape-v2.md", + include_str!("inkscape-default.expected"), + "always", ); } @@ -817,9 +394,10 @@ fn test_correct_rendering_v2() { /// will not use styling since output is not stdout. fn test_rendering_color_auto() { _test_correct_rendering( - "inkscape-v2", - include_str!("rendered/inkscape-default-no-color.expected"), - &["--color", "auto"], + include_str!("inkscape-v2.md"), + "inkscape-v2.md", + include_str!("inkscape-default-no-color.expected"), + "auto", ); } @@ -827,87 +405,51 @@ fn test_rendering_color_auto() { /// An end-to-end integration test for direct file rendering with the `--color never` option. fn test_rendering_color_never() { _test_correct_rendering( - "inkscape-v2", - include_str!("rendered/inkscape-default-no-color.expected"), - &["--color", "never"], + include_str!("inkscape-v2.md"), + "inkscape-v2.md", + include_str!("inkscape-default-no-color.expected"), + "never", ); } -/// An end-to-end integration test for the indent config option -#[test] -fn test_rendering_with_indentation() { - let testenv = TestEnv::new().install_default_cache(); - let expected_custom_indentation = include_str!("rendered/inkscape-compact-no-color.expected"); - - // Configure to set base and command indents - testenv.append_to_config("display.indent.base = 3\n"); - testenv.append_to_config("display.indent.command = 1\n"); - - testenv - .command() - .args(["--color", "never", "inkscape-v2"]) - .assert() - .success() - .stdout(diff(expected_custom_indentation)); -} - #[test] fn test_rendering_i18n() { _test_correct_rendering( - "apt", - include_str!("rendered/apt.ja.expected"), - &["--color", "always", "--language", "ja"], + include_str!("chmod.ru.md"), + "chmod.ru.md", + include_str!("chmod.ru.expected"), + "always", ); } /// An end-to-end integration test for rendering with custom syntax config. #[test] fn test_correct_rendering_with_config() { - let testenv = TestEnv::new().install_default_cache(); + let testenv = TestEnv::new(); - testenv.append_to_config(include_str!("style-config.toml")); + // Setup config file + // TODO should be config::CONFIG_FILE_NAME + let config_file_path = testenv.config_dir.path().join("config.toml"); + println!("Config path: {:?}", config_file_path); - let expected = include_str!("rendered/inkscape-with-config.expected"); + let mut config_file = File::create(&config_file_path).unwrap(); + config_file + .write_all(include_bytes!("config.toml")) + .unwrap(); + + // Create input file + let file_path = testenv.input_dir.path().join("inkscape-v2.md"); + println!("Testfile path: {:?}", file_path); + + let mut file = File::create(&file_path).unwrap(); + file.write_all(include_bytes!("inkscape-v2.md")).unwrap(); + + // Load expected output + let expected = include_str!("inkscape-with-config.expected"); testenv .command() - .args(["--color", "always", "inkscape-v2"]) - .assert() - .success() - .stdout(diff(expected)); -} - -/// An end-to-end integration test for rendering with show_title config option enabled. -#[test] -fn test_show_title_config() { - // Test that default behavior without show_title shows no title - let testenv = TestEnv::new().install_default_cache(); - let expected_no_title = include_str!("rendered/inkscape-default.expected"); - - testenv - .command() - .args(["--color", "always", "inkscape-v2"]) - .assert() - .success() - .stdout(diff(expected_no_title)); - - // Configure to enable show_title - testenv.append_to_config("display.show_title = true\n"); - - let expected_no_color = include_str!("rendered/inkscape-with-title-no-color.expected"); - - testenv - .command() - .args(["inkscape-v2"]) - .assert() - .success() - .stdout(diff(expected_no_color)); - - let expected = include_str!("rendered/inkscape-with-title.expected"); - - testenv - .command() - .args(["--color", "always", "inkscape-v2"]) + .args(["--color", "always", "-f", file_path.to_str().unwrap()]) .assert() .success() .stdout(diff(expected)); @@ -915,7 +457,14 @@ fn test_show_title_config() { #[test] fn test_spaces_find_command() { - let testenv = TestEnv::new().install_default_cache(); + let testenv = TestEnv::new(); + + testenv + .command() + .args(["--update"]) + .assert() + .success() + .stderr(contains("Successfully updated cache.")); testenv .command() @@ -926,7 +475,14 @@ fn test_spaces_find_command() { #[test] fn test_pager_flag_enable() { - let testenv = TestEnv::new().install_default_cache(); + let testenv = TestEnv::new(); + + testenv + .command() + .args(["--update"]) + .assert() + .success() + .stderr(contains("Successfully updated cache.")); testenv .command() @@ -935,216 +491,16 @@ fn test_pager_flag_enable() { .success(); } -#[test] -fn test_multiple_platform_command_search() { - let testenv = TestEnv::new(); - testenv.add_os_entry("linux", "linux-only", "this command only exists for linux"); - testenv.add_os_entry( - "linux", - "windows-and-linux", - "# windows-and-linux \n\n > linux version", - ); - testenv.add_os_entry( - "windows", - "windows-and-linux", - "# windows-and-linux \n\n > windows version", - ); - - testenv - .command() - .args(["--platform", "windows", "--platform", "linux", "linux-only"]) - .assert() - .success(); - - // test order of platforms supplied if preserved - testenv - .command() - .args([ - "--platform", - "windows", - "--platform", - "linux", - "windows-and-linux", - ]) - .assert() - .success() - .stdout(contains("windows version")); - - testenv - .command() - .args([ - "--platform", - "linux", - "--platform", - "windows", - "windows-and-linux", - ]) - .assert() - .success() - .stdout(contains("linux version")); -} - -#[test] -fn test_multiple_platform_command_search_not_found() { - let testenv = TestEnv::new(); - testenv.add_os_entry( - "windows", - "windows-only", - "this command only exists for Windows", - ); - - testenv - .command() - .args(["--platform", "macos", "--platform", "linux", "windows-only"]) - .assert() - .stderr(contains("Page `windows-only` not found in cache.")); -} - -#[test] -fn test_macos_is_alias_for_osx() { - let testenv = TestEnv::new(); - testenv.add_os_entry("osx", "maconly", "this command only exists on mac"); - - testenv - .command() - .args(["--platform", "macos", "maconly"]) - .assert() - .success(); - testenv - .command() - .args(["--platform", "osx", "maconly"]) - .assert() - .success(); - - testenv - .command() - .args(["--platform", "macos", "--list"]) - .assert() - .stdout("maconly\n"); - testenv - .command() - .args(["--platform", "osx", "--list"]) - .assert() - .stdout("maconly\n"); - - testenv.append_to_config("search.platforms = ['osx']\n"); - testenv.command().arg("--list").assert().stdout("maconly\n"); - - testenv.delete_config(); - testenv.init_config(); - testenv.append_to_config("search.platforms = ['macos']\n"); - testenv.command().arg("--list").assert().stdout("maconly\n"); -} - -#[test] -fn test_common_platform_is_used_as_fallback() { - let testenv = TestEnv::new(); - testenv.add_entry("in-common", "this command comes from common"); - - // No platform specified - testenv.command().args(["in-common"]).assert().success(); - - // Platform specified - testenv - .command() - .args(["--platform", "linux", "in-common"]) - .assert() - .success(); -} - -#[test] -fn test_search_language_precedence() { - let testenv = TestEnv::new(); - for lang in ["en", "de", "it", "fr", "pl", "nl"] { - testenv.add_lang_entry(lang, lang, ""); - } - - #[expect(clippy::type_complexity)] - let run = |cases: &[(Vec<(&str, &str)>, Vec<&str>, &str)]| { - for (extra_env, extra_args, expected) in cases { - let mut cmd = testenv.command(); - for (key, value) in extra_env { - cmd.env(key, value); - } - cmd.args(extra_args); - cmd.arg("--list"); - cmd.assert().success().stdout(eq(*expected)); - } - }; - - let env_cases = &[ - (vec![], vec![], "en\n"), - (vec![("LANGUAGE", "de:it")], vec![], "en\n"), - ( - vec![("LANG", "fr"), ("LANGUAGE", "de:it")], - vec![], - "de\nen\nfr\nit\n", - ), - ( - vec![("LANG", "fr"), ("LANGUAGE", "de:it")], - vec!["--language", "pl"], - "pl\n", - ), - ]; - run(env_cases); - - // Environment is only used when config setting is not set - testenv.append_to_config("search.languages = ['nl']\n"); - let config_cases = &[ - (vec![], vec![], "nl\n"), - (vec![("LANGUAGE", "de:it")], vec![], "nl\n"), - (vec![("LANG", "fr"), ("LANGUAGE", "de:it")], vec![], "nl\n"), - ( - vec![("LANG", "fr"), ("LANGUAGE", "de:it")], - vec!["--language", "pl"], - "pl\n", - ), - ]; - run(config_cases); - - // The above update setting does not change anything - testenv.append_to_config("updates.download_languages = ['cz']"); - run(config_cases); - testenv.delete_config(); - testenv.init_config(); - testenv.append_to_config("updates.download_languages = ['cz']"); - run(env_cases); -} - -#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] -#[test] -fn test_update_language_arg() { - let testenv = TestEnv::new(); - testenv - .command() - .env("LANG", "it") - .arg("--update") - .assert() - .success() - .stderr(contains("it")) - .stderr(contains("en")); - - testenv - .command() - .env("LANG", "en") - .args(["--language", "it"]) - .arg("--update") - .assert() - .success() - .stderr(contains("it")) - .stderr(contains("en").not()); -} - #[test] fn test_list_flag_rendering() { - let testenv = TestEnv::new().write_custom_pages_config(); + let testenv = TestEnv::new(); testenv .command() .args(["--list"]) .assert() .failure() - .stderr(contains("Page cache not found. Please run `tldr --update`")); + .stderr(contains("Cache not found. Please run `tldr --update`.")); testenv.add_entry("foo", ""); @@ -1158,87 +514,49 @@ fn test_list_flag_rendering() { testenv.add_entry("bar", ""); testenv.add_entry("baz", ""); testenv.add_entry("qux", ""); - testenv.add_page_entry("faz", ""); - testenv.add_page_entry("bar", ""); - testenv.add_page_entry("fiz", ""); - testenv.add_patch_entry("buz", ""); testenv .command() .args(["--list"]) .assert() .success() - .stdout("bar\nbaz\nfaz\nfiz\nfoo\nqux\n"); + .stdout("bar\nbaz\nfoo\nqux\n"); } #[test] -fn test_multi_platform_list_flag_rendering() { - let testenv = TestEnv::new().write_custom_pages_config(); +fn test_list_platform_filtering() { + let testenv = TestEnv::new(); - testenv.add_entry("common", ""); + testenv.add_os_entry("common", "a-common", ""); + testenv.add_os_entry("windows", "a-windows", ""); + testenv.add_os_entry("linux", "a-linux", ""); + testenv.add_os_entry("linux", "b-linux", ""); + // Filter: linux testenv .command() - .args(["--list"]) + .args(["--list", "--platform", "linux"]) .assert() .success() - .stdout("common\n"); + .stdout("a-common\na-linux\nb-linux\n"); + // Filter: windows testenv .command() - .args(["--platform", "linux", "--list"]) + .args(["--list", "--platform", "windows"]) .assert() .success() - .stdout("common\n"); + .stdout("a-common\na-windows\n"); + // Filter: all testenv .command() - .args(["--platform", "windows", "--list"]) + .args(["--list", "--platform", "all"]) .assert() .success() - .stdout("common\n"); - - testenv.add_os_entry("linux", "rm", ""); - testenv.add_os_entry("linux", "ls", ""); - testenv.add_os_entry("windows", "del", ""); - testenv.add_os_entry("windows", "dir", ""); - testenv.add_os_entry("linux", "winux", ""); - testenv.add_os_entry("windows", "winux", ""); - - // test `--list` for `--platform linux` by itself - testenv - .command() - .args(["--platform", "linux", "--list"]) - .assert() - .success() - .stdout("common\nls\nrm\nwinux\n"); - - // test `--list` for `--platform windows` by itself - testenv - .command() - .args(["--platform", "windows", "--list"]) - .assert() - .success() - .stdout("common\ndel\ndir\nwinux\n"); - - // test `--list` for `--platform linux --platform windows` - testenv - .command() - .args(["--platform", "linux", "--platform", "windows", "--list"]) - .assert() - .success() - .stdout("common\ndel\ndir\nls\nrm\nwinux\n"); - - // test `--list` for `--platform windows --platform linux` - testenv - .command() - .args(["--platform", "linux", "--platform", "windows", "--list"]) - .assert() - .success() - .stdout("common\ndel\ndir\nls\nrm\nwinux\n"); + .stdout("a-common\na-linux\na-windows\nb-linux\n"); } -#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] #[test] fn test_autoupdate_cache() { let testenv = TestEnv::new(); @@ -1249,12 +567,17 @@ fn test_autoupdate_cache() { .args(["--list"]) .assert() .failure() - .stderr(contains("Page cache not found. Please run `tldr --update`")); + .stderr(contains("Cache not found. Please run `tldr --update`.")); - let cache_file_path = testenv.cache_dir().join(TLDR_PAGES_DIR); + let config_file_path = testenv.config_dir.path().join("config.toml"); + let cache_file_path = testenv.cache_dir.path().join(TLDR_PAGES_DIR); - testenv - .append_to_config("updates.auto_update = true\nupdates.auto_update_interval_hours = 24\n"); + // Activate automatic updates, set the auto-update interval to 24 hours + let mut config_file = File::create(&config_file_path).unwrap(); + config_file + .write_all(b"[updates]\nauto_update = true\nauto_update_interval_hours = 24") + .unwrap(); + config_file.flush().unwrap(); // Helper function that runs `tldr --list` and asserts that the cache is automatically updated // or not, depending on the value of `expected`. @@ -1290,34 +613,24 @@ fn test_autoupdate_cache() { check_cache_updated(false); } -/// Regression test: `--no-auto-update` should be usable together with `--list`, -/// since the auto-update gate in `main.rs` also applies to `--list`. -#[test] -fn test_no_auto_update_with_list() { - let testenv = TestEnv::new().install_default_cache(); - - testenv - .command() - .args(["--list", "--no-auto-update"]) - .assert() - .success(); -} - -/// End-end test to ensure .page.md files overwrite pages in cache_dir +/// End-end test to ensure .page files overwrite pages in cache_dir #[test] fn test_custom_page_overwrites() { - let testenv = TestEnv::new().write_custom_pages_config(); + let testenv = TestEnv::new(); + + // set custom pages directory + testenv.write_config(format!( + "[directories]\ncustom_pages_dir = '{}'", + testenv.custom_pages_dir.path().to_str().unwrap() + )); // Add file that should be ignored to the cache dir testenv.add_entry("inkscape-v2", ""); - // Add .page.md file to custom_pages_dir - testenv.add_page_entry( - "inkscape-v2", - include_str!("cache/pages.en/common/inkscape-v2.md"), - ); + // Add .page file to custome_pages_dir + testenv.add_page_entry("inkscape-v2", include_str!("inkscape-v2.md")); // Load expected output - let expected = include_str!("rendered/inkscape-default-no-color.expected"); + let expected = include_str!("inkscape-default-no-color.expected"); testenv .command() @@ -1327,15 +640,24 @@ fn test_custom_page_overwrites() { .stdout(diff(expected)); } -/// End-End test to ensure that .patch.md files are appended to pages in the cache_dir +/// End-End test to ensure that .patch files are appened to pages in the cache_dir #[test] fn test_custom_patch_appends_to_common() { - let testenv = TestEnv::new() - .install_default_cache() - .install_default_custom_pages(); + let testenv = TestEnv::new(); + + // set custom pages directory + testenv.write_config(format!( + "[directories]\ncustom_pages_dir = '{}'", + testenv.custom_pages_dir.path().to_str().unwrap() + )); + + // Add page to the cache dir + testenv.add_entry("inkscape-v2", include_str!("inkscape-v2.md")); + // Add .page file to custome_pages_dir + testenv.add_patch_entry("inkscape-v2", include_str!("inkscape-v2.patch")); // Load expected output - let expected = include_str!("rendered/inkscape-patched-no-color.expected"); + let expected = include_str!("inkscape-patched-no-color.expected"); testenv .command() @@ -1345,22 +667,27 @@ fn test_custom_patch_appends_to_common() { .stdout(diff(expected)); } -/// End-End test to ensure that .patch.md files are not appended to .page.md files in the custom_pages_dir +/// End-End test to ensure that .patch files are not appended to .page files in the custom_pages_dir /// Maybe this interaction should change but I put this test here for the coverage #[test] fn test_custom_patch_does_not_append_to_custom() { - let testenv = TestEnv::new() - .install_default_cache() - .install_default_custom_pages(); + let testenv = TestEnv::new(); - // In addition to the page in the cache, add the same page as a custom page. - testenv.add_page_entry( - "inkscape-v2", - include_str!("cache/pages.en/common/inkscape-v2.md"), - ); + // set custom pages directory + testenv.write_config(format!( + "[directories]\ncustom_pages_dir = '{}'", + testenv.custom_pages_dir.path().to_str().unwrap() + )); + + testenv.add_entry("test", ""); + + // Add page to the cache dir + testenv.add_page_entry("inkscape-v2", include_str!("inkscape-v2.md")); + // Add .page file to custome_pages_dir + testenv.add_patch_entry("inkscape-v2", include_str!("inkscape-v2.patch")); // Load expected output - let expected = include_str!("rendered/inkscape-default-no-color.expected"); + let expected = include_str!("inkscape-default-no-color.expected"); testenv .command() @@ -1373,7 +700,13 @@ fn test_custom_patch_does_not_append_to_custom() { #[test] #[cfg(target_os = "windows")] fn test_pager_warning() { - let testenv = TestEnv::new().install_default_cache(); + let testenv = TestEnv::new(); + testenv + .command() + .args(["--update"]) + .assert() + .success() + .stderr(contains("Successfully updated cache.")); // Regular call should not show a "pager flag not available on windows" warning testenv @@ -1414,13 +747,15 @@ fn test_lowercased_page_lookup() { /// Regression test for #219: It should be possible to combine `--raw` and `-f`. #[test] fn test_raw_render_file() { - let testenv = TestEnv::new().install_default_cache(); + let testenv = TestEnv::new(); - let path = testenv - .cache_dir() - .join(TLDR_PAGES_DIR) - .join("pages.en/common/inkscape-v1.md"); - let mut args = vec!["--color", "never", "-f", &path.to_str().unwrap()]; + // Create input file + let file_path = testenv.input_dir.path().join("inkscape.md"); + let mut file = File::create(&file_path).unwrap(); + file.write_all(include_bytes!("inkscape-v1.md")).unwrap(); + + // Base args + let mut args = vec!["--color", "never", "-f", file_path.to_str().unwrap()]; // Default render testenv @@ -1428,9 +763,7 @@ fn test_raw_render_file() { .args(&args) .assert() .success() - .stdout(diff(include_str!( - "rendered/inkscape-default-no-color.expected" - ))); + .stdout(diff(include_str!("inkscape-default-no-color.expected"))); // Raw render args.push("--raw"); @@ -1439,130 +772,5 @@ fn test_raw_render_file() { .args(&args) .assert() .success() - .stdout(diff(include_str!("cache/pages.en/common/inkscape-v1.md"))); -} - -fn touch_custom_page(testenv: &TestEnv) { - let args = vec!["--edit-page", "foo"]; - - testenv - .command() - .args(&args) - .env("EDITOR", "touch") - .assert() - .success(); - assert!(testenv.custom_pages_dir().join("foo.page.md").exists()); -} - -fn touch_custom_patch(testenv: &TestEnv) { - let args = vec!["--edit-patch", "foo"]; - - testenv - .command() - .args(&args) - .env("EDITOR", "touch") - .assert() - .success(); - assert!(testenv.custom_pages_dir().join("foo.patch.md").exists()); -} - -#[test] -fn test_edit_page() { - let testenv = TestEnv::new().write_custom_pages_config(); - touch_custom_page(&testenv); -} - -#[test] -fn test_edit_patch() { - let testenv = TestEnv::new().write_custom_pages_config(); - touch_custom_patch(&testenv); -} - -#[test] -fn test_recreate_dir() { - let testenv = TestEnv::new().write_custom_pages_config(); - touch_custom_patch(&testenv); - touch_custom_page(&testenv); -} - -#[test] -fn test_custom_pages_dir_is_not_dir() { - let testenv = TestEnv::new().write_custom_pages_config(); - let _ = std::fs::remove_dir_all(testenv.custom_pages_dir()); - let _ = File::create(testenv.custom_pages_dir()).unwrap(); - assert!(testenv.custom_pages_dir().is_file()); - - let args = vec!["--edit-patch", "foo"]; - - testenv - .command() - .args(&args) - .env("EDITOR", "touch") - .assert() - .failure(); -} - -mod placeholder_format { - use super::*; - - #[test] - fn default_long() { - let testenv = TestEnv::new().install_default_cache(); - testenv - .command() - .args(["--color=always", "playerctl"]) - .assert() - .success() - .stdout(eq(include_str!("rendered/playerctl-long.expected"))); - } - - #[test] - fn config() { - let cases = [ - ("short", include_str!("rendered/playerctl-short.expected")), - ("long", include_str!("rendered/playerctl-long.expected")), - ("both", include_str!("rendered/playerctl-both.expected")), - ]; - for (setting, expected) in cases { - let testenv = TestEnv::new().install_default_cache(); - testenv.append_to_config(format!("display.placeholder_format = \"{setting}\"\n")); - testenv - .command() - .args(["--color=always", "playerctl"]) - .assert() - .success() - .stdout(eq(expected)); - } - } - - #[test] - fn cli() { - let testenv = TestEnv::new().install_default_cache(); - testenv.append_to_config("display.placeholder_format = \"both\"\n"); - testenv - .command() - .args(["--color=always", "--short-options", "playerctl"]) - .assert() - .success() - .stdout(eq(include_str!("rendered/playerctl-short.expected"))); - - testenv - .command() - .args(["--color=always", "--long-options", "playerctl"]) - .assert() - .success() - .stdout(eq(include_str!("rendered/playerctl-long.expected"))); - - testenv - .command() - .args([ - "--color=always", - "--short-options", - "--long-options", - "playerctl", - ]) - .assert() - .success() - .stdout(eq(include_str!("rendered/playerctl-both.expected"))); - } + .stdout(diff(include_str!("inkscape-v1.md"))); } diff --git a/tests/rendered/apt.ja.expected b/tests/rendered/apt.ja.expected deleted file mode 100644 index e79bf7e..0000000 --- a/tests/rendered/apt.ja.expected +++ /dev/null @@ -1,37 +0,0 @@ - - Debian系ディストリビューションで使われるパッケージ管理システムです。 - Ubuntuのバージョンが16.04か、それ以降で対話モードを使う場合`apt-get`の代わりとして使用します。 - 詳しくはこちら: - - 利用可能なパーケージとバージョンのリストの更新(他の`apt`コマンドの前での実行を推奨): - - sudo apt update - - 指定されたパッケージの検索: - - apt search パッケージ - - パッケージの情報を出力: - - apt show パッケージ - - パッケージのインストール、または利用可能な最新バージョンに更新: - - sudo apt install パッケージ - - パッケージの削除(`sudo apt remove --purge`の場合設定ファイルも削除): - - sudo apt remove パッケージ - - インストールされている全てのパッケージを最新のバージョンにアップグレード: - - sudo apt upgrade - - インストールできるすべてのパッケージを表示: - - apt list - - インストールされた全てのパッケージを表示(依存関係も表示): - - apt list --installed - diff --git a/tests/rendered/inkscape-compact-no-color.expected b/tests/rendered/inkscape-compact-no-color.expected deleted file mode 100644 index 473bbe9..0000000 --- a/tests/rendered/inkscape-compact-no-color.expected +++ /dev/null @@ -1,32 +0,0 @@ - - An SVG (Scalable Vector Graphics) editing program. - Use -z to not open the GUI and only process files in the console. - - Open an SVG file in the Inkscape GUI: - - inkscape filename.svg - - Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI): - - inkscape filename.svg -e filename.png - - Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur): - - inkscape filename.svg -e filename.png -w 600 -h 400 - - Export a single object, given its ID, into a bitmap: - - inkscape filename.svg -i id -e object.png - - Export an SVG document to PDF, converting all texts to paths: - - inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path - - Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: - - inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit - - Some invalid command just to test the correct highlighting of the command name: - - inkscape --use-inkscape=v3.0 file - diff --git a/tests/rendered/inkscape-default.expected b/tests/rendered/inkscape-default.expected deleted file mode 100644 index 5b91247..0000000 --- a/tests/rendered/inkscape-default.expected +++ /dev/null @@ -1,32 +0,0 @@ - - An SVG (Scalable Vector Graphics) editing program. - Use -z to not open the GUI and only process files in the console. - - Open an SVG file in the Inkscape GUI: - - inkscape filename.svg - - Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI): - - inkscape filename.svg -e filename.png - - Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur): - - inkscape filename.svg -e filename.png -w 600 -h 400 - - Export a single object, given its ID, into a bitmap: - - inkscape filename.svg -i id -e object.png - - Export an SVG document to PDF, converting all texts to paths: - - inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path - - Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: - - inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit - - Some invalid command just to test the correct highlighting of the command name: - - inkscape --use-inkscape=v3.0 file - diff --git a/tests/rendered/inkscape-with-title-no-color.expected b/tests/rendered/inkscape-with-title-no-color.expected deleted file mode 100644 index b6a4572..0000000 --- a/tests/rendered/inkscape-with-title-no-color.expected +++ /dev/null @@ -1,34 +0,0 @@ - - inkscape - - An SVG (Scalable Vector Graphics) editing program. - Use -z to not open the GUI and only process files in the console. - - Open an SVG file in the Inkscape GUI: - - inkscape filename.svg - - Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI): - - inkscape filename.svg -e filename.png - - Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur): - - inkscape filename.svg -e filename.png -w 600 -h 400 - - Export a single object, given its ID, into a bitmap: - - inkscape filename.svg -i id -e object.png - - Export an SVG document to PDF, converting all texts to paths: - - inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path - - Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: - - inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit - - Some invalid command just to test the correct highlighting of the command name: - - inkscape --use-inkscape=v3.0 file - diff --git a/tests/rendered/inkscape-with-title.expected b/tests/rendered/inkscape-with-title.expected deleted file mode 100644 index 7e81d41..0000000 --- a/tests/rendered/inkscape-with-title.expected +++ /dev/null @@ -1,34 +0,0 @@ - - inkscape - - An SVG (Scalable Vector Graphics) editing program. - Use -z to not open the GUI and only process files in the console. - - Open an SVG file in the Inkscape GUI: - - inkscape filename.svg - - Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI): - - inkscape filename.svg -e filename.png - - Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur): - - inkscape filename.svg -e filename.png -w 600 -h 400 - - Export a single object, given its ID, into a bitmap: - - inkscape filename.svg -i id -e object.png - - Export an SVG document to PDF, converting all texts to paths: - - inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path - - Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: - - inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit - - Some invalid command just to test the correct highlighting of the command name: - - inkscape --use-inkscape=v3.0 file - diff --git a/tests/rendered/playerctl-both.expected b/tests/rendered/playerctl-both.expected deleted file mode 100644 index 1f49457..0000000 --- a/tests/rendered/playerctl-both.expected +++ /dev/null @@ -1,32 +0,0 @@ - - Control media players via MPRIS. - More information: . - - Toggle play: - - playerctl play-pause - - Skip to the next track: - - playerctl next - - Go back to the previous track: - - playerctl previous - - List all players: - - playerctl [-l|--list-all] - - Send a command to a specific player: - - playerctl [-p|--player] player_name play-pause|next|previous|... - - Send a command to all players: - - playerctl [-a|--all-players] play-pause|next|previous|... - - Display metadata about the current track: - - playerctl metadata [-f|--format] "Now playing: {{artist}} - {{album}} - {{title}}" - diff --git a/tests/rendered/playerctl-long.expected b/tests/rendered/playerctl-long.expected deleted file mode 100644 index 4d0ad10..0000000 --- a/tests/rendered/playerctl-long.expected +++ /dev/null @@ -1,32 +0,0 @@ - - Control media players via MPRIS. - More information: . - - Toggle play: - - playerctl play-pause - - Skip to the next track: - - playerctl next - - Go back to the previous track: - - playerctl previous - - List all players: - - playerctl --list-all - - Send a command to a specific player: - - playerctl --player player_name play-pause|next|previous|... - - Send a command to all players: - - playerctl --all-players play-pause|next|previous|... - - Display metadata about the current track: - - playerctl metadata --format "Now playing: {{artist}} - {{album}} - {{title}}" - diff --git a/tests/rendered/playerctl-short.expected b/tests/rendered/playerctl-short.expected deleted file mode 100644 index f2f5511..0000000 --- a/tests/rendered/playerctl-short.expected +++ /dev/null @@ -1,32 +0,0 @@ - - Control media players via MPRIS. - More information: . - - Toggle play: - - playerctl play-pause - - Skip to the next track: - - playerctl next - - Go back to the previous track: - - playerctl previous - - List all players: - - playerctl -l - - Send a command to a specific player: - - playerctl -p player_name play-pause|next|previous|... - - Send a command to all players: - - playerctl -a play-pause|next|previous|... - - Display metadata about the current track: - - playerctl metadata -f "Now playing: {{artist}} - {{album}} - {{title}}" - diff --git a/tests/cache/pages.en/common/which.md b/tests/which-markdown.expected similarity index 100% rename from tests/cache/pages.en/common/which.md rename to tests/which-markdown.expected diff --git a/completion/zsh_tealdeer b/zsh_tealdeer similarity index 65% rename from completion/zsh_tealdeer rename to zsh_tealdeer index 6ce4b12..28cd34b 100644 --- a/completion/zsh_tealdeer +++ b/zsh_tealdeer @@ -2,9 +2,8 @@ _applications() { local -a commands - if commands=(${(uonzf)"$(tldr --list 2>/dev/null)"//:/\\:}); then - _describe -t commands 'command' commands - fi + commands=(${(uonzf)"$(tldr --list 2>/dev/null)"//:/\\:}) + _describe -t commands 'command' commands } _tealdeer() { @@ -14,26 +13,16 @@ _tealdeer() { args+=( "($I -l --list)"{-l,--list}"[List all commands in the cache]" - "($I)--edit-page[Edit custom page with EDITOR]" - "($I)--edit-patch[Edit custom patch with EDITOR]" "($I -f --render)"{-f,--render}"[Render a specific markdown file]:file:_files" "($I -p --platform)"{-p,--platform}'[Override the operating system]:platform:(( linux macos sunos windows - android - freebsd - netbsd - openbsd - common ))' "($I -L --language)"{-L,--language}"[Override the language settings]:lang" "($I -u --update)"{-u,--update}"[Update the local cache]" - "($I)--no-auto-update[If auto update is configured, disable it for this run]" "($I -c --clear-cache)"{-c,--clear-cache}"[Clear the local cache]" - "($I)--config-path[Override config file location]" - "($I)--override-config[Override config values after reading config file]" "($I)--pager[Use a pager to page output]" "($I -r --raw)"{-r,--raw}"[Display the raw markdown instead of rendering it]" "($I -q --quiet)"{-q,--quiet}"[Suppress informational messages]" @@ -44,8 +33,6 @@ _tealdeer() { auto never ))" - "($I)--short-options[Display the short variants of placeholders]" - "($I)--long-options[Display the long variants of placeholders]" '(- *)'{-h,--help}'[Display help]' '(- *)'{-v,--version}'[Show version information]' '1: :_applications'