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 faf3111..160d9ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,90 +1,90 @@ -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] - toolchain: [stable, 1.87.0] # MSRV - include: - - platform: windows-latest - exe_suffix: .exe + 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 - 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 --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 --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 -- --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: stable - components: clippy - - name: run clippy lints - run: cargo clippy --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 + - uses: actions/checkout@v2 - name: Setup mdBook - uses: peaceiris/actions-mdbook@v2 + uses: peaceiris/actions-mdbook@v1 with: mdbook-version: '0.4.4' - - name: Setup toolchain - uses: dtolnay/rust-toolchain@master + - uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: stable + override: true - name: Build - run: cargo build + uses: actions-rs/cargo@v1 + with: + command: build - name: Ensure that docs can be built run: cd docs && mdbook build - name: Generate usage string diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index ae9ae93..35f17b3 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -1,25 +1,25 @@ -name: GitHub Pages +name: github pages + on: push: - tags: - - "v[1-9]*" # push events matching `v` followed by anything larger than 0, e.g. v1.0, v20.15.10 - workflow_dispatch: + branches: + - master jobs: deploy: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v2 - name: Setup mdBook - uses: peaceiris/actions-mdbook@v2 + uses: peaceiris/actions-mdbook@v1 with: mdbook-version: '0.4.4' - run: cd docs && mdbook build - name: Deploy - uses: peaceiris/actions-gh-pages@v4 + 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 9337d6f..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,160 +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 --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 --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: windows-latest - steps: - - uses: actions/checkout@v7 - - name: Setup toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - - name: Build - run: cargo build --release --target x86_64-pc-windows-msvc - - uses: actions/upload-artifact@v7 - with: - name: "tealdeer-windows-x86_64-msvc" - path: "target/x86_64-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 - 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/CHANGELOG.md b/CHANGELOG.md index 644a30a..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://tealdeer-rs.github.io/tealdeer/ -[@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 4fba42b..3585ebf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,97 +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.3" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] [[package]] -name = "anstream" -version = "0.6.21" +name = "ansi_term" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", + "winapi", ] [[package]] -name = "anstyle" -version = "1.0.13" +name = "app_dirs2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "2dd95d9b31f552568dcad90bb809b795f63795fba32f733eb7435a4d13f5d28f" dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" -dependencies = [ - "windows-sys 0.60.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.60.2", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[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.0.17" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" +checksum = "e996dc7940838b7ef1096b882e29ec30a3149a3a443cdc8dba19ed382eca1fe2" dependencies = [ - "anstyle", "bstr", "doc-comment", - "libc", "predicates", "predicates-core", "predicates-tree", @@ -99,67 +53,68 @@ dependencies = [ ] [[package]] -name = "autocfg" -version = "1.5.0" +name = "atty" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +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.22.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bitflags" -version = "2.9.4" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr" -version = "1.12.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" dependencies = [ + "lazy_static", "memchr", "regex-automata", - "serde", ] [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +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.10.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.2.40" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" -dependencies = [ - "find-msvc-tools", - "shlex", -] +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" [[package]] name = "cesu8" @@ -169,61 +124,43 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.48" +version = "3.0.0-beta.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "feff3878564edb93745d58cf63e17b63f24142506e7a20c87a5521ed7bfb1d63" dependencies = [ - "clap_builder", + "bitflags", "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "terminal_size", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim 0.10.0", + "textwrap", ] [[package]] name = "clap_derive" -version = "4.5.47" +version = "3.0.0-beta.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "8b15c6b4f786ffb6192ffe65a36855bc1fc2444bcd0945ae16748dcd6ed7d0d3" dependencies = [ "heck", + "proc-macro-error", "proc-macro2", "quote", "syn", ] -[[package]] -name = "clap_lex" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - [[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", @@ -231,19 +168,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" dependencies = [ "core-foundation-sys", "libc", @@ -251,34 +178,59 @@ 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 = "der" -version = "0.7.10" +name = "darling" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" dependencies = [ - "pem-rfc7468", - "zeroize", + "darling_core", + "darling_macro", ] [[package]] -name = "derive_arbitrary" -version = "1.4.2" +name = "darling_core" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.9.3", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", @@ -291,6 +243,26 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "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" @@ -298,34 +270,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] -name = "env_filter" -version = "0.1.3" +name = "either" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "encoding_rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" dependencies = [ - "log", - "regex", + "cfg-if", ] [[package]] name = "env_logger" -version = "0.11.8" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +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" @@ -337,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.1", -] - [[package]] name = "errno-dragonfly" version = "0.1.2" @@ -359,65 +320,45 @@ 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.1", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" dependencies = [ "cfg-if", "libc", - "libredox", - "windows-sys 0.60.2", + "redox_syscall", + "winapi", ] -[[package]] -name = "find-msvc-tools" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" - [[package]] name = "flate2" -version = "1.1.2" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" dependencies = [ + "cfg-if", "crc32fast", - "libz-rs-sys", + "libc", "miniz_oxide", ] [[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", ] @@ -429,60 +370,122 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "foreign-types" -version = "0.3.2" +name = "form_urlencoded" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +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 = "getrandom" -version = "0.2.16" +name = "futures-core" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +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 = [ - "cfg-if", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "futures-core", + "futures-io", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if", "libc", - "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasi", +] + +[[package]] +name = "h2" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +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.3.1" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" dependencies = [ "bytes", "fnv", @@ -490,71 +493,131 @@ dependencies = [ ] [[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.11.4" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ - "equivalent", + "autocfg", "hashbrown", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.1" +name = "ipnet" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +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.15" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jiff" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde", -] - -[[package]] -name = "jiff-static" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "jni" -version = "0.21.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" dependencies = [ "cesu8", - "cfg-if", "combine", "jni-sys", "log", "thiserror", "walkdir", - "windows-sys 0.45.0", ] [[package]] @@ -563,76 +626,132 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" -version = "0.2.176" +version = "0.2.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" - -[[package]] -name = "libredox" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" -dependencies = [ - "bitflags", - "libc", - "redox_syscall", -] - -[[package]] -name = "libz-rs-sys" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" -dependencies = [ - "zlib-rs", -] - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "f98a04dce437184842841303488f70d0188c5f51437d2a834dc097eafa909a01" [[package]] name = "log" -version = "0.4.28" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +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.7.6" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +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", + "adler", + "autocfg", ] [[package]] -name = "native-tls" -version = "0.2.14" +name = "mio" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" dependencies = [ "libc", "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "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" @@ -640,125 +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.3" +name = "num_cpus" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - -[[package]] -name = "openssl" -version = "0.10.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", + "hermit-abi", "libc", - "once_cell", - "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", ] [[package]] -name = "openssl-probe" -version = "0.1.6" +name = "once_cell" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" [[package]] -name = "openssl-sys" -version = "0.9.109" +name = "openssl-probe" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + +[[package]] +name = "os_str_bytes" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -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.32" +name = "pin-project-lite" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" [[package]] -name = "portable-atomic" -version = "1.11.1" +name = "pin-utils" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "portable-atomic-util" -version = "0.2.4" +name = "ppv-lite86" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" [[package]] name = "predicates" -version = "3.1.3" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "95e5a7689e456ab905c22c2b48225bb921aba7c8dfa58440d68ba13f6222a715" dependencies = [ - "anstyle", "difflib", "float-cmp", + "itertools", "normalize-line-endings", "predicates-core", "regex", @@ -766,197 +879,263 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "338c7be2905b732ae3984a2f40032b5e94fd8f52505b186c7d4d68d193445df7" dependencies = [ "predicates-core", "termtree", ] [[package]] -name = "proc-macro2" -version = "1.0.101" +name = "proc-macro-crate" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +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.41" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" dependencies = [ "proc-macro2", ] [[package]] -name = "r-efi" -version = "5.3.0" +name = "rand" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +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.5.17" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags", ] [[package]] -name = "regex" -version = "1.11.3" +name = "redox_users" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ "aho-corasick", "memchr", - "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] -name = "ring" -version = "0.17.14" +name = "remove_dir_all" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", + "winapi", ] [[package]] -name = "rustix" -version = "1.1.2" +name = "reqwest" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "07bea77bc708afa10e59905c3d4af7c8fd43c9214251673095ff8b14345fcbc5" dependencies = [ - "bitflags", - "errno 0.3.14", + "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.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", "libc", - "linux-raw-sys", - "windows-sys 0.61.1", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", ] [[package]] name = "rustls" -version = "0.23.32" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +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.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "5ca9ebdfa27d3fc180e42879037b5338ab1c040c06affd00d8338598e7800943" dependencies = [ "openssl-probe", - "rustls-pki-types", + "rustls-pemfile", "schannel", - "security-framework 3.5.1", + "security-framework", ] [[package]] name = "rustls-pemfile" -version = "2.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be59af91596cac372a6942530653ad0c3a246cdd491aaa9dcaee47f88d67d5a0" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework 3.5.1", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.59.0", -] - -[[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.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", + "base64", ] [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568" [[package]] name = "same-file" @@ -969,34 +1148,32 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" dependencies = [ - "windows-sys 0.61.1", + "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 = "2.11.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" dependencies = [ "bitflags", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags", - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -1004,9 +1181,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" dependencies = [ "core-foundation-sys", "libc", @@ -1014,28 +1191,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", @@ -1044,303 +1211,438 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" dependencies = [ "itoa", - "memchr", "ryu", "serde", - "serde_core", ] [[package]] -name = "serde_spanned" -version = "0.6.9" +name = "serde_urlencoded" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" dependencies = [ + "form_urlencoded", + "itoa", + "ryu", "serde", ] [[package]] -name = "shlex" -version = "1.3.0" +name = "slab" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] -name = "simd-adler32" -version = "0.3.7" +name = "socket2" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "socks" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" 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.106" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" 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.23.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ - "fastrand", - "getrandom 0.3.3", - "once_cell", - "rustix", - "windows-sys 0.61.1", + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", ] [[package]] -name = "terminal_size" -version = "0.4.3" +name = "termcolor" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" dependencies = [ - "rustix", - "windows-sys 0.60.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 = "1.0.69" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.69" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", "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 = "0.8.23" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" dependencies = [ "serde", ] [[package]] -name = "toml_edit" -version = "0.22.27" +name = "tower-service" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", + "cfg-if", + "pin-project-lite", + "tracing-core", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "tracing-core" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +dependencies = [ + "lazy_static", +] [[package]] -name = "unicode-ident" -version = "1.0.19" +name = "try-lock" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +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.1.2" +name = "url" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" dependencies = [ - "base64", - "der", - "flate2", - "log", - "native-tls", + "form_urlencoded", + "idna", + "matches", "percent-encoding", - "rustls", - "rustls-pemfile", - "rustls-pki-types", - "rustls-platform-verifier", - "socks", - "ureq-proto", - "utf-8", - "webpki-root-certs", - "webpki-roots", ] [[package]] -name = "ureq-proto" -version = "0.5.2" +name = "version_check" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" -dependencies = [ - "base64", - "http", - "httparse", - "log", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[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" - -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" dependencies = [ - "wasip2", + "log", + "try-lock", ] [[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ - "wit-bindgen", + "cfg-if", + "wasm-bindgen-macro", ] [[package]] -name = "webpki-root-certs" -version = "1.0.2" +name = "wasm-bindgen-backend" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" dependencies = [ - "rustls-pki-types", + "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.2" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" dependencies = [ - "rustls-pki-types", + "webpki 0.21.4", ] [[package]] @@ -1361,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.1", + "winapi", ] [[package]] @@ -1375,297 +1677,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-link" -version = "0.2.0" +name = "winreg" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ - "windows-targets 0.42.2", + "winapi", ] [[package]] -name = "windows-sys" -version = "0.52.0" +name = "xdg" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "3a23fe958c70412687039c86f578938b4a0bb50ec788e96bce4d6ab00ddd5803" dependencies = [ - "windows-targets 0.52.6", + "dirs", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.4", -] - -[[package]] -name = "windows-sys" -version = "0.61.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[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_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "winnow" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - [[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.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" - -[[package]] -name = "zopfli" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", + "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index 1d98992..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://tealdeer-rs.github.io/tealdeer/" -version = "1.8.1" -include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"] -rust-version = "1.87" # MSRV -edition = "2021" +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 = "0.8.19" -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 859d06f..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. @@ -52,6 +52,7 @@ Docker container using sharkdp's [`hyperfine`][hyperfine-gh] | [`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 | | @@ -86,20 +87,9 @@ 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 +When publishing a Tealdeer release, the Rust version required to build it should be stable for at least a month. @@ -123,19 +113,20 @@ 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/tealdeer-rs/tealdeer/blob/main/benchmarks/Dockerfile +[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/tealdeer-rs/tealdeer/issues/129#issuecomment-833596765 +[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 index 2a36fe0..d717eba 100644 --- a/benchmarks/Dockerfile +++ b/benchmarks/Dockerfile @@ -18,7 +18,7 @@ FROM rust AS tealdeer-builder WORKDIR /build -RUN git clone https://github.com/tealdeer-rs/tealdeer.git \ +RUN git clone https://github.com/dbrgn/tealdeer.git \ && cd tealdeer \ && cargo build --release \ && mkdir /build-outputs \ 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 528be44..0000000 --- a/completion/fish_tealdeer +++ /dev/null @@ -1,28 +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 -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' -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 -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 - 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/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 a662b57..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,9 +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. 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 007d64b..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,51 +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, -} -``` + [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 593a5b5..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. @@ -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/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 6a04de7..66c45c3 100644 --- a/docs/src/usage.txt +++ b/docs/src/usage.txt @@ -1,33 +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 - --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] - -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://tealdeer-rs.github.io/tealdeer/. +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 948b278..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 --render {{path/to/file.md}}` - -- Show the raw markdown source of a page instead of rendering it: - -`tldr --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 --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/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 afa3603..1d94d89 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,267 +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::{anyhow, bail, ensure, Context, Result}; -use log::{debug, info}; -use ureq::{ - http::StatusCode, - tls::{RootCerts, TlsConfig, TlsProvider}, - Agent, -}; +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"; -pub static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; +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> { - 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() { - if 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>> { - 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 { @@ -277,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(()) } } @@ -380,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 161d69d..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Definition of the CLI arguments and options. - -use std::path::PathBuf; - -use clap::{builder::ArgAction, ArgGroup, Parser}; - -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://tealdeer-rs.github.io/tealdeer/. - -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", requires = "command_or_file")] - 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, - - /// 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, - - /// 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 f0feeb4..d1b040b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,101 +1,28 @@ use std::{ - borrow::Cow, - env, fmt, - fs::{self, File}, - io::{ErrorKind, Write}, - path::{Component, Path, PathBuf}, - sync::LazyLock, + env, fs, + io::{Error as IoError, Read, Write}, + path::PathBuf, time::Duration, }; -use anyhow::{anyhow, ensure, Context, Result}; -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 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::{ - app_strategy::choose_native_strategy, choose_app_strategy, AppStrategy, AppStrategyArgs, - }; - - 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 } @@ -112,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), @@ -128,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), } } } @@ -171,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 { @@ -204,59 +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, -} - -#[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<&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: Indent { - base: raw_display_config.indent.base, - command: raw_display_config.indent.command, - }, - } - } } /// Serde doesn't support default values yet (tracking issue: @@ -267,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 { @@ -303,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(), } } } @@ -383,7 +186,12 @@ struct RawConfig { display: RawDisplayConfig, updates: RawUpdatesConfig, directories: RawDirectoriesConfig, - search: RawSearchConfig, +} + +impl RawConfig { + fn new() -> Self { + Self::default() + } } impl Default for RawConfig { @@ -393,7 +201,6 @@ impl Default for RawConfig { display: RawDisplayConfig::default(), updates: RawUpdatesConfig::default(), directories: RawDirectoriesConfig::default(), - search: RawSearchConfig::default(), }; // Set default config @@ -407,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, @@ -416,376 +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, } -#[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")); + // 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(), + }; } - } - 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) -> Result { - match fs::read_to_string(&path.path) { - Ok(content) => Ok(Self { - raw: toml::from_str(&content).with_context(|| { - format!( - "Could not parse config file contents as toml from {}.", - path.path.display() - ) - })?, - path, - }), - Err(e) if allow_not_found && e.kind() == ErrorKind::NotFound => Ok(Self { - raw: RawConfig::default(), - path, - }), - Err(e) => Err(e).context(format!( - "Could not read config file contents from {}.", - path.path().display() - )), - } - } - - /// Create a loader that uses the config at `path`. - pub fn read(path: PathBuf) -> Result { - Self::read_internal( - PathWithSource { - path, - source: PathSource::Cli, - }, - false, - ) - } - - /// Create a loader that uses the default config file location. If no file is present at the default location, the - /// default configuration is used. - pub fn read_default_path() -> Result { - let path = get_default_config_path(); - Self::read_internal(path, true) - } - - /// 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) } } @@ -796,233 +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 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 082f436..9789975 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -1,62 +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), - Variable(T), - NormalCode(T), - Description(T), - Text(T), - Title(T), +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: FnOnce(T) -> U, - { - match self { - PageSnippet::CommandName(s) => PageSnippet::CommandName(f(s)), - PageSnippet::Variable(s) => PageSnippet::Variable(f(s)), - 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::Linebreak => PageSnippet::Linebreak, - } - } -} - -impl, U> PartialEq> for PageSnippet { - fn eq(&self, other: &PageSnippet) -> bool { - match (self, other) { - (PageSnippet::CommandName(s), PageSnippet::CommandName(t)) - | (PageSnippet::Variable(s), PageSnippet::Variable(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::Linebreak, PageSnippet::Linebreak) => true, - _ => false, - } - } -} - -impl PageSnippet<&str> { +impl<'a> PageSnippet<'a> { pub fn is_empty(&self) -> bool { use PageSnippet::*; match self { - CommandName(s) | Variable(s) | NormalCode(s) | Description(s) | Text(s) | Title(s) => { - s.is_empty() - } + CommandName(s) | Variable(s) | NormalCode(s) | Description(s) | Text(s) => s.is_empty(), Linebreak => false, } } @@ -67,15 +31,11 @@ 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 base_indent = " ".repeat(indent.base); - let command_indent = " ".repeat(indent.command); let mut command = String::new(); for line in lines { match line { @@ -85,122 +45,51 @@ where } } LineType::Title(title) => { - if show_title { - process_snippet(PageSnippet::Linebreak)?; - process_snippet(PageSnippet::Title(&base_indent))?; - 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::Description(&base_indent))?; - process_snippet(PageSnippet::Description(&text))?; - process_snippet(PageSnippet::Linebreak)?; - } - LineType::ExampleText(text) => { - process_snippet(PageSnippet::Text(&base_indent))?; - 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::NormalCode(&command_indent))?; + 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, - )?; - } - process_snippet(PageSnippet::Variable(&replace_escaped(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 +/// 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; @@ -224,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() { @@ -267,193 +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 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 variable_vs_escaped() { - assert_eq!( - run("ping", "ping {{example.com}}"), - [ - CommandName("ping"), - NormalCode(" "), - Variable("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}}' " - ), - Variable("container"), - ], - ); - assert_eq!( - run("mount", r"mount \\{{computer_name}}\{{share_name}} Z:"), - [ - CommandName("mount"), - NormalCode(r" \\"), - Variable("computer_name"), - NormalCode(r"\"), - Variable("share_name"), - NormalCode(" Z:"), - ], - ); + #[test] + fn test_empty_command() { + let segment = "some code"; + let snippets = [NormalCode(segment)]; - assert_eq!(run("", r"\{"), [NormalCode(r"\{")]); - assert_eq!(run("", r"\{{a"), [NormalCode(r"\{{a")]); - assert_eq!(run("", r"\{{a}}"), [NormalCode(r"\"), Variable("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 "), - Variable("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(" "), Variable("}")] - ); - - // And these are just to document the current behavior - assert_eq!(run("", "{{{}}}"), [Variable("{}")]); - assert_eq!(run("", "{{{{}}}"), [Variable("{{}")]); - assert_eq!(run("", "{{{}}}}"), [Variable("{}}")]); - } - - #[test] - fn escaped_inside_placeholder() { - assert_eq!( - run( - "playerctl", - r#"playerctl metadata {{[-f|--format]}} "{{Now playing: \{\{artist\}\} - \{\{album\}\} - \{\{title\}\}}}""# - ), - [ - CommandName("playerctl"), - NormalCode(" metadata "), - Variable("[-f|--format]"), - NormalCode(" \""), - Variable("Now playing: {{artist}} - {{album}} - {{title}}"), - NormalCode("\""), - ], - ); - } - - #[test] - fn placeholder_inside_escaped() { - assert_eq!( - run("test", r"test \{\{{{var}} normal\}\}"), - [ - CommandName("test"), - NormalCode(" {{"), - Variable("var"), - NormalCode(" normal}}"), - ], - ); - } - - #[test] - /// Regression test for - fn prefix_check_character_boundary() { - assert_eq!("Ä".len(), 2); - assert_eq!(run("", r"Äxx{{x}}"), [NormalCode("Äxx"), Variable("x")],); - } + 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 6678ceb..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::{anyhow, Context, Result}; -use cache::{CacheConfig, TLDR_OLD_PAGES_DIR}; -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,92 +35,297 @@ mod types; mod utils; use crate::{ - cache::{Cache, PageLookupResult, TLDR_PAGES_DIR}, - cli::Cli, - config::{ - get_config_dir, make_default_config, supported_tls_backends_string, Config, PathWithSource, - }, + 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")] @@ -149,296 +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()).context("Could not read config from given path")? - } - _ => { - ConfigLoader::read_default_path().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; } - 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); + 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); + }; } - // 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, - }; - - // TODO: remove in tealdeer 1.9 - let old_config = CacheConfig { - pages_directory: &config.directories.cache_dir.path().join(TLDR_OLD_PAGES_DIR), - ..cache_config - }; - if let Ok(Some(old_cache)) = Cache::open(old_config) { - old_cache.clear()?; - eprintln!("Cleared pages from old cache location."); - } + // Initialize cache + let cache = Cache::new(ARCHIVE_URL, args.platform); + // Clear cache, pass through if args.clear_cache { - if let Some(cache) = Cache::open(cache_config)? { - clear_cache(cache, args.quiet)?; - } - return Ok(ExitCode::SUCCESS); + clear_cache(args.quiet, enable_styles); } - 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, - ); + // Cache update, pass through + let cache_updated = if should_update_cache(&args, &config) { + update_cache(&cache, args.quiet, enable_styles); + true + } else { + false + }; - if let Err(e) = result { - print_error(enable_styles, &e); + // Check cache presence and freshness + if !cache_updated + && (args.list || !args.command.is_empty()) + && check_cache(&args, enable_styles) == CheckCacheResult::CacheMissing + { + process::exit(1); + } - 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 { + // 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, - &anyhow::anyhow!( - "Page cache not found. Please run `tldr --update` to download the cache." - ), + &format!("Could not get list of pages: {}", e.message()), ); - 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://tealdeer-rs.github.io/tealdeer/config_updates.html"); + process::exit(1); + }); - return Ok(ExitCode::FAILURE); - }; - - 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 - ), - ); - } - } - - cache - } else { - // There is nothing left to do - return Ok(ExitCode::SUCCESS); - }; - - if args.list { - for page in cache.list_pages()? { - println!("{page}"); - } - - return Ok(ExitCode::SUCCESS); + // 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 6243b44..70cd138 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,97 +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::{ + 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).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, -) -> io::Result<()> { +) -> Result<(), io::Error> { use PageSnippet::*; match snip { - CommandName(s) | Title(s) => write!(writer, "{}", s.paint(style.command_name)), - Variable(s) => write!(writer, "{}", s.paint(style.example_variable)), - 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)), + 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.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/rendered/inkscape-default.expected b/tests/inkscape-default.expected similarity index 61% rename from tests/rendered/inkscape-default.expected rename to tests/inkscape-default.expected index 3b37f0e..da909f2 100644 --- a/tests/rendered/inkscape-default.expected +++ b/tests/inkscape-default.expected @@ -2,31 +2,31 @@ 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: + 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): + 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): + 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: + 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: + 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: + 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: + 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 53% rename from tests/rendered/inkscape-with-config.expected rename to tests/inkscape-with-config.expected index 33540a3..e79b219 100644 --- a/tests/rendered/inkscape-with-config.expected +++ b/tests/inkscape-with-config.expected @@ -2,31 +2,31 @@ 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: + 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): + 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): + 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: + 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: + 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: + 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: + Some invalid command just to test the correct highlighting of the command name: inkscape --use-inkscape=v3.0 file diff --git a/tests/lib.rs b/tests/lib.rs index d431b5f..e79d9aa 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,9 +1,8 @@ //! Integration tests. use std::{ - fs::{self, create_dir_all, File}, - io::{self, Write}, - path::{Path, PathBuf}, + fs::{create_dir_all, File}, + io::Write, process::Command, time::{Duration, SystemTime}, }; @@ -11,94 +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"; -pub static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; 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. @@ -108,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 @@ -158,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] @@ -298,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 @@ -321,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() @@ -333,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(); @@ -401,64 +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_always_delete_old_pages_directory() { - let testenv = TestEnv::new().install_default_cache(); - fs::rename( - testenv.cache_dir().join(TLDR_PAGES_DIR), - testenv.cache_dir().join(TLDR_OLD_PAGES_DIR), - ) - .unwrap(); - - testenv - .command() - .arg("--list") - .assert() - .failure() - .stderr(contains("Cleared pages from old cache location.")) - .stderr(contains("Page cache not found.")); - - assert!(testenv.cache_dir().is_dir()); - assert!(!testenv.cache_dir().join(TLDR_PAGES_DIR).exists()); - assert!(!testenv.cache_dir().join(TLDR_OLD_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() @@ -470,71 +194,54 @@ fn test_quiet_failures() { #[test] fn test_quiet_old_cache() { - let testenv = TestEnv::new().install_default_cache(); + let testenv = TestEnv::new(); + + 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.")); @@ -544,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(), ))); } @@ -745,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"]) @@ -788,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)); @@ -804,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", ); } @@ -814,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", ); } @@ -825,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", ); } @@ -835,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)); @@ -923,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() @@ -934,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() @@ -943,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", ""); @@ -1166,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(); @@ -1257,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`. @@ -1298,21 +613,24 @@ fn test_autoupdate_cache() { check_cache_updated(false); } -/// 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() @@ -1322,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() @@ -1340,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() @@ -1368,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 @@ -1409,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 @@ -1423,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"); @@ -1434,65 +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(); + .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 efdd35d..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-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 ff2de4a..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/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 82% rename from completion/zsh_tealdeer rename to zsh_tealdeer index fbab749..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() { @@ -20,14 +19,9 @@ _tealdeer() { macos sunos windows - android - freebsd - netbsd - openbsd ))' "($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)--pager[Use a pager to page output]" "($I -r --raw)"{-r,--raw}"[Display the raw markdown instead of rendering it]"