diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19f5198..faf3111 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,16 +13,15 @@ jobs: test: name: run tests strategy: - fail-fast: false matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - toolchain: [stable, 1.54] + toolchain: [stable, 1.87.0] # MSRV include: - platform: windows-latest exe_suffix: .exe runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} @@ -31,11 +30,16 @@ jobs: run: | cargo build cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-default${{ matrix.exe_suffix}} - - name: Build with logging + - name: Build with logging and Rustls with webpki roots run: | - cargo build --features logging --no-default-features - cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-logging${{ matrix.exe_suffix}} - - uses: actions/upload-artifact@v5 + 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 with: name: tldr-debug-build-${{ matrix.platform }}-rust-${{ matrix.toolchain }} path: artifacts/ @@ -46,19 +50,19 @@ jobs: name: run clippy lints runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@master with: toolchain: stable components: clippy - name: run clippy lints - run: cargo clippy --features logging + run: cargo clippy --all-targets --features logging fmt: name: run rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -70,7 +74,7 @@ jobs: name: build docs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Setup mdBook uses: peaceiris/actions-mdbook@v2 with: diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 8746d42..ae9ae93 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -3,12 +3,13 @@ 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: jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Setup mdBook uses: peaceiris/actions-mdbook@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 539f2ab..9337d6f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ jobs: create-release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Create release for tag if: startsWith(github.ref, 'refs/tags/') run: | @@ -24,7 +24,7 @@ jobs: matrix: target: ["bash", "fish", "zsh"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Upload completion if: startsWith(github.ref, 'refs/tags/') run: | @@ -40,7 +40,7 @@ jobs: matrix: target: ["MIT", "APACHE"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Upload license if: startsWith(github.ref, 'refs/tags/') run: | @@ -66,14 +66,14 @@ jobs: - arch: "arm" libc: "musleabihf" steps: - - uses: actions/checkout@v5 + - 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@v5 + - uses: actions/upload-artifact@v7 with: name: "tealdeer-linux-${{ matrix.arch }}-${{ matrix.libc }}" path: "target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr" @@ -86,7 +86,7 @@ jobs: - arch: "x86_64" - arch: "aarch64" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: @@ -94,7 +94,7 @@ jobs: targets: "${{ matrix.arch }}-apple-darwin" - name: Build run: cargo build --release --target ${{ matrix.arch }}-apple-darwin - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v7 with: name: "tealdeer-macos-${{ matrix.arch }}" path: "target/${{ matrix.arch }}-apple-darwin/release/tldr" @@ -102,14 +102,14 @@ jobs: build-windows: runs-on: windows-latest steps: - - uses: actions/checkout@v5 + - 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@v5 + - uses: actions/upload-artifact@v7 with: name: "tealdeer-windows-x86_64-msvc" path: "target/x86_64-pc-windows-msvc/release/tldr.exe" @@ -134,8 +134,8 @@ jobs: - macos-aarch64 - windows-x86_64-msvc steps: - - uses: actions/checkout@v5 - - uses: actions/download-artifact@v6 + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 - name: Upload binary if: startsWith(github.ref, 'refs/tags/') run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index cdf6e5e..644a30a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,302 @@ Possible log types: - `[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) @@ -24,7 +320,7 @@ 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://dbrgn.github.io/tealdeer/usage_custom_pages.html). + [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. @@ -35,7 +331,7 @@ The highlights: `-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 - [dbrgn.github.io/tealdeer/](https://dbrgn.github.io/tealdeer/), we hope these + [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. @@ -43,10 +339,10 @@ The highlights: 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 +> When publishing a tealdeer release, the Rust version required to build it > should be stable for at least a month. -Changes: +#### Changes: - [added] Support custom pages and patches ([#142][i142]) - [added] Multi-language support ([#125][i125], [#161][i161]) @@ -61,6 +357,7 @@ Changes: - [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]) @@ -68,15 +365,15 @@ Changes: - [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 [dbrgn.github.io/tealdeer/](https://dbrgn.github.io/tealdeer/) -- [docs] Add comparative benchmarks with hyperfine ([#163][i163], [README](https://github.com/dbrgn/tealdeer#goals)) +- [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: +#### Contributors to this version: - [@bl-ue][@bl-ue] - [Cameron Tod][@cam8001] @@ -105,7 +402,7 @@ co-maintainer. Thank you for your help! - [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] @@ -122,7 +419,7 @@ 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] @@ -148,7 +445,7 @@ 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] @@ -174,7 +471,7 @@ 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] @@ -197,7 +494,7 @@ 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] @@ -230,29 +527,43 @@ 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 @@ -262,8 +573,13 @@ Thanks! [@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 @@ -271,58 +587,138 @@ Thanks! [@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/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 -[v1.5.0]: https://github.com/dbrgn/tealdeer/compare/v1.4.1...v1.5.0 +[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 -[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 -[i108]: https://github.com/dbrgn/tealdeer/pull/108 -[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 -[i125]: https://github.com/dbrgn/tealdeer/pull/125 -[i138]: https://github.com/dbrgn/tealdeer/issues/138 -[i142]: https://github.com/dbrgn/tealdeer/pull/142 -[i148]: https://github.com/dbrgn/tealdeer/pull/148 -[i157]: https://github.com/dbrgn/tealdeer/pull/157 -[i161]: https://github.com/dbrgn/tealdeer/pull/161 -[i162]: https://github.com/dbrgn/tealdeer/pull/162 -[i163]: https://github.com/dbrgn/tealdeer/pull/163 -[i168]: https://github.com/dbrgn/tealdeer/pull/168 -[i171]: https://github.com/dbrgn/tealdeer/pull/171 -[i174]: https://github.com/dbrgn/tealdeer/pull/174 -[i176]: https://github.com/dbrgn/tealdeer/pull/176 -[i187]: https://github.com/dbrgn/tealdeer/pull/187 -[i190]: https://github.com/dbrgn/tealdeer/issues/190 -[i197]: https://github.com/dbrgn/tealdeer/pull/197 -[i210]: https://github.com/dbrgn/tealdeer/pull/210 -[i213]: https://github.com/dbrgn/tealdeer/pull/213 -[i215]: https://github.com/dbrgn/tealdeer/pull/215 -[i217]: https://github.com/dbrgn/tealdeer/pull/217 -[i227]: https://github.com/dbrgn/tealdeer/pull/227 -[i240]: https://github.com/dbrgn/tealdeer/pull/240 +[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 diff --git a/Cargo.lock b/Cargo.lock index 10ec150..4fba42b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,120 +1,165 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] -name = "ansi_term" -version = "0.12.1" +name = "anstream" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ - "winapi", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", ] [[package]] -name = "app_dirs2" -version = "2.3.3" +name = "anstyle" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dd95d9b31f552568dcad90bb809b795f63795fba32f733eb7435a4d13f5d28f" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ - "jni", - "ndk-glue", - "winapi", - "xdg", + "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", ] [[package]] name = "assert_cmd" -version = "2.0.2" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e996dc7940838b7ef1096b882e29ec30a3149a3a443cdc8dba19ed382eca1fe2" +checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" dependencies = [ + "anstyle", "bstr", "doc-comment", + "libc", "predicates", "predicates-core", "predicates-tree", "wait-timeout", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - [[package]] name = "autocfg" -version = "1.0.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" -version = "0.13.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bitflags" -version = "1.3.2" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bstr" -version = "0.2.17" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ - "lazy_static", "memchr", "regex-automata", + "serde", ] [[package]] name = "bumpalo" -version = "3.8.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.1.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.0.72" +version = "1.2.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +dependencies = [ + "find-msvc-tools", + "shlex", +] [[package]] name = "cesu8" @@ -124,45 +169,61 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "clap" -version = "3.0.0-rc.11" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6f243c7279f09ffed852a0a564c72091331651484cdbb32b7287f16df8611a7" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ - "atty", - "bitflags", + "clap_builder", "clap_derive", - "indexmap", - "lazy_static", - "os_str_bytes", - "strsim 0.10.0", - "termcolor", - "textwrap", +] + +[[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", ] [[package]] name = "clap_derive" -version = "3.0.0-rc.11" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd9992739777a4a23535089a8d235eac43044ba8b431d9f54fe334dfa779930" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck", - "proc-macro-error", "proc-macro2", "quote", "syn", ] [[package]] -name = "combine" -version = "4.6.2" +name = "clap_lex" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b2f5d0ee456f3928812dfc8c6d9a1d592b98678f6d56db9b0cd2b7bc6c8db5" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", @@ -170,9 +231,19 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +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" dependencies = [ "core-foundation-sys", "libc", @@ -180,51 +251,37 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" -version = "1.3.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] -name = "darling" -version = "0.10.2" +name = "der" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "darling_core", - "darling_macro", + "pem-rfc7468", + "zeroize", ] [[package]] -name = "darling_core" -version = "0.10.2" +name = "derive_arbitrary" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" 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", ] @@ -234,26 +291,6 @@ 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" @@ -261,33 +298,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] -name = "either" -version = "1.6.1" +name = "env_filter" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" - -[[package]] -name = "encoding_rs" -version = "0.8.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ - "cfg-if", + "log", + "regex", ] [[package]] name = "env_logger" -version = "0.9.0" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ - "atty", - "humantime", + "anstream", + "anstyle", + "env_filter", + "jiff", "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" @@ -299,6 +337,16 @@ 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" @@ -311,45 +359,65 @@ dependencies = [ [[package]] name = "escargot" -version = "0.5.7" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5584ba17d7ab26a8a7284f13e5bd196294dd2f2d79773cff29b9e9edef601a6" +checksum = "11c3aea32bc97b500c9ca6a72b768a26e558264303d101d3409cf6d57a9ed0cf" dependencies = [ "log", - "once_cell", "serde", "serde_json", ] [[package]] -name = "filetime" -version = "0.2.15" +name = "etcetera" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" dependencies = [ "cfg-if", - "libc", - "redox_syscall", - "winapi", + "windows-sys 0.61.1", ] [[package]] -name = "flate2" -version = "1.0.22" +name = "fastrand" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", - "crc32fast", "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "libz-rs-sys", "miniz_oxide", ] [[package]] name = "float-cmp" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" dependencies = [ "num-traits", ] @@ -361,260 +429,132 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "form_urlencoded" -version = "1.0.1" +name = "foreign-types" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "matches", - "percent-encoding", + "foreign-types-shared", ] [[package]] -name = "futures-channel" -version = "0.3.19" +name = "foreign-types-shared" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" - -[[package]] -name = "futures-io" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" - -[[package]] -name = "futures-sink" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" - -[[package]] -name = "futures-task" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" - -[[package]] -name = "futures-util" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" -dependencies = [ - "futures-core", - "futures-io", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.2.3" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "wasi", -] - -[[package]] -name = "h2" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f072413d126e57991455e0a922b31e4c8ba7c2ffbebf6b78b4f8521397d65cd" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "heck" -version = "0.3.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -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", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "0.2.6" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", - "itoa 1.0.1", -] - -[[package]] -name = "http-body" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" -dependencies = [ - "bytes", - "http", - "pin-project-lite", + "itoa", ] [[package]] name = "httparse" -version = "1.5.1" +version = "1.10.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.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa 0.4.8", - "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", -] +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "indexmap" -version = "1.7.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ - "autocfg", + "equivalent", "hashbrown", ] [[package]] -name = "ipnet" -version = "2.3.1" +name = "is_terminal_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] -name = "itertools" -version = "0.10.3" +name = "itoa" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ - "either", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", ] [[package]] -name = "itoa" -version = "0.4.8" +name = "jiff-static" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - -[[package]] -name = "itoa" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "jni" -version = "0.19.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", + "cfg-if", "combine", "jni-sys", "log", "thiserror", "walkdir", + "windows-sys 0.45.0", ] [[package]] @@ -624,250 +564,201 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] -name = "js-sys" -version = "0.3.55" +name = "libc" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "wasm-bindgen", + "bitflags", + "libc", + "redox_syscall", ] [[package]] -name = "lazy_static" -version = "1.4.0" +name = "libz-rs-sys" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +dependencies = [ + "zlib-rs", +] [[package]] -name = "libc" -version = "0.2.112" +name = "linux-raw-sys" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "log" -version = "0.4.14" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" -version = "2.4.1" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" - -[[package]] -name = "mime" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "miniz_oxide" -version = "0.4.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "adler", - "autocfg", + "adler2", ] [[package]] -name = "mio" -version = "0.7.14" +name = "native-tls" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", - "miow", - "ntapi", - "winapi", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", ] -[[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" -[[package]] -name = "ntapi" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" -dependencies = [ - "winapi", -] - [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] -name = "num_cpus" -version = "1.13.1" +name = "once_cell" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +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" dependencies = [ - "hermit-abi", + "bitflags", + "cfg-if", + "foreign-types", "libc", + "once_cell", + "openssl-macros", + "openssl-sys", ] [[package]] -name = "num_enum" -version = "0.5.6" +name = "openssl-macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "720d3ea1055e4e4574c0c0b0f8c3fd4f24c4cdaf465948206dea090b57b526ad" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "num_enum_derive", -] - -[[package]] -name = "num_enum_derive" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d992b768490d7fe0d8586d9b5745f6c49f557da6d81dc982b1d167ad4edbb21" -dependencies = [ - "proc-macro-crate 1.1.0", "proc-macro2", "quote", "syn", ] -[[package]] -name = "once_cell" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" - [[package]] name = "openssl-probe" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] -name = "os_str_bytes" -version = "6.0.0" +name = "openssl-sys" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ - "memchr", + "cc", + "libc", + "pkg-config", + "vcpkg", ] [[package]] name = "pager" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c7d08cf0d0b55c4f0ffedb5e06569ea212e85d622975071370393970491968" +checksum = "2599211a5c97fbbb1061d3dc751fa15f404927e4846e07c643287d6d1f462880" dependencies = [ - "errno", + "errno 0.2.8", "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.1.0" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "pin-project-lite" -version = "0.2.8" +name = "pkg-config" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "pin-utils" -version = "0.1.0" +name = "portable-atomic" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] -name = "ppv-lite86" -version = "0.2.16" +name = "portable-atomic-util" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] [[package]] name = "predicates" -version = "2.1.0" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95e5a7689e456ab905c22c2b48225bb921aba7c8dfa58440d68ba13f6222a715" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ + "anstyle", "difflib", "float-cmp", - "itertools", "normalize-line-endings", "predicates-core", "regex", @@ -875,263 +766,197 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.2" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" -version = "1.0.4" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338c7be2905b732ae3984a2f40032b5e94fd8f52505b186c7d4d68d193445df7" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", ] -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "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.36" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.14" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] -name = "rand" -version = "0.8.4" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -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", -] +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] [[package]] -name = "redox_users" -version = "0.4.0" +name = "regex" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ - "getrandom", - "redox_syscall", + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] [[package]] -name = "regex" -version = "1.5.4" +name = "regex-automata" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" - [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - -[[package]] -name = "reqwest" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c4e0a76dc12a116108933f6301b95e83634e0c47b0afbed6abbaa0601e99258" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "hyper-rustls", - "ipnet", - "js-sys", - "lazy_static", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "rustls", - "rustls-native-certs", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-rustls", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", - "winreg", -] +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "ring" -version = "0.16.20" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", + "cfg-if", + "getrandom 0.2.16", "libc", - "once_cell", - "spin", "untrusted", - "web-sys", - "winapi", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno 0.3.14", + "libc", + "linux-raw-sys", + "windows-sys 0.61.1", ] [[package]] name = "rustls" -version = "0.20.2" +version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "log", + "once_cell", "ring", - "sct", - "webpki", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.6.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca9ebdfa27d3fc180e42879037b5338ab1c040c06affd00d8338598e7800943" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.5.1", ] [[package]] name = "rustls-pemfile" -version = "0.2.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64", + "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", ] [[package]] name = "ryu" -version = "1.0.9" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -1144,32 +969,34 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.19" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "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", + "windows-sys 0.61.1", ] [[package]] name = "security-framework" -version = "2.4.2" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", - "core-foundation", + "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-sys", "libc", "security-framework-sys", @@ -1177,9 +1004,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.4.2" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -1187,18 +1014,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.132" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008" +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" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.132" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1207,427 +1044,303 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.73" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "itoa 1.0.1", + "itoa", + "memchr", "ryu", "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", ] [[package]] -name = "serde_urlencoded" -version = "0.7.0" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" -dependencies = [ - "form_urlencoded", - "itoa 0.4.8", - "ryu", - "serde", -] +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "slab" -version = "0.4.5" +name = "simd-adler32" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] -name = "socket2" -version = "0.4.2" +name = "socks" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" dependencies = [ + "byteorder", "libc", "winapi", ] [[package]] -name = "spin" -version = "0.5.2" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -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" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "1.0.84" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] name = "tealdeer" -version = "1.5.1" +version = "1.8.1" dependencies = [ - "ansi_term", - "app_dirs2", + "anyhow", "assert_cmd", - "atty", "clap", "env_logger", "escargot", + "etcetera", "filetime", "log", "pager", "predicates", - "reqwest", "serde", "serde_derive", "tempfile", "toml", - "walkdir", + "ureq", + "yansi", "zip", ] [[package]] name = "tempfile" -version = "3.2.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ - "cfg-if", - "libc", - "rand", - "redox_syscall", - "remove_dir_all", - "winapi", + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.61.1", ] [[package]] -name = "termcolor" -version = "1.1.2" +name = "terminal_size" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "winapi-util", + "rustix", + "windows-sys 0.60.2", ] [[package]] name = "termtree" -version = "0.2.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13a4ec180a2de59b57434704ccfad967f789b12737738798fa08798cd5824c16" - -[[package]] -name = "textwrap" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 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.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" -dependencies = [ - "bytes", - "libc", - "memchr", - "mio", - "num_cpus", - "pin-project-lite", - "winapi", -] - -[[package]] -name = "tokio-rustls" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b" -dependencies = [ - "rustls", - "tokio", - "webpki", -] - -[[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.5.8" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +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" dependencies = [ "serde", ] [[package]] -name = "tower-service" -version = "0.3.1" +name = "toml_edit" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" - -[[package]] -name = "tracing" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "cfg-if", - "pin-project-lite", - "tracing-core", + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", ] [[package]] -name = "tracing-core" -version = "0.1.21" +name = "toml_write" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" -dependencies = [ - "lazy_static", -] +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] -name = "try-lock" -version = "0.2.3" +name = "unicode-ident" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -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" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] -name = "url" -version = "2.2.2" +name = "ureq" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" dependencies = [ - "form_urlencoded", - "idna", - "matches", + "base64", + "der", + "flate2", + "log", + "native-tls", "percent-encoding", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "rustls-platform-verifier", + "socks", + "ureq-proto", + "utf-8", + "webpki-root-certs", + "webpki-roots", ] [[package]] -name = "version_check" -version = "0.9.4" +name = "ureq-proto" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +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" [[package]] name = "wait-timeout" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] [[package]] name = "walkdir" -version = "2.3.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", - "winapi", "winapi-util", ] [[package]] -name = "want" -version = "0.3.0" +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" -dependencies = [ - "log", - "try-lock", -] +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - -[[package]] -name = "wasm-bindgen" -version = "0.2.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ - "cfg-if", - "wasm-bindgen-macro", + "wasip2", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.78" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "bumpalo", - "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-futures" -version = "0.4.28" +name = "webpki-root-certs" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" +checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" 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.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", + "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "0.22.2" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552ceb903e957524388c4d3475725ff2c8b7960922063af6ce53c9a43da07449" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ - "webpki", + "rustls-pki-types", ] [[package]] @@ -1648,11 +1361,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "winapi", + "windows-sys 0.61.1", ] [[package]] @@ -1662,31 +1375,297 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "winreg" -version = "0.7.0" +name = "windows-link" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "winapi", + "windows-targets 0.42.2", ] [[package]] -name = "xdg" -version = "2.4.0" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a23fe958c70412687039c86f578938b4a0bb50ec788e96bce4d6ab00ddd5803" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "dirs", + "windows-targets 0.52.6", ] +[[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 = "0.5.13" +version = "5.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" +checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532" dependencies = [ - "byteorder", + "arbitrary", "crc32fast", "flate2", - "thiserror", + "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", ] diff --git a/Cargo.toml b/Cargo.toml index 40cbf47..1d98992 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/dbrgn/tealdeer/" -license = "MIT/Apache-2.0" +homepage = "https://github.com/tealdeer-rs/tealdeer/" +license = "MIT OR Apache-2.0" name = "tealdeer" readme = "README.md" -repository = "https://github.com/dbrgn/tealdeer/" -documentation = "https://dbrgn.github.io/tealdeer/" -version = "1.5.1" -include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "/bash_tealdeer", "/fish_tealdeer"] -edition = "2018" +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" [[bin]] name = "tldr" path = "src/main.rs" [dependencies] -ansi_term = "0.12.0" -app_dirs = { version = "2", package = "app_dirs2" } -atty = "0.2" -clap = { version = "=3.0.0-rc.11", features = ["std", "derive", "suggestions", "color"], default-features = false } -env_logger = { version = "0.9", optional = true } +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" 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" -toml = "0.5.1" -walkdir = "2.0.1" -zip = { version = "0.5", default-features = false, features = ["deflate"] } +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"] } [target.'cfg(not(windows))'.dependencies] pager = "0.16" @@ -38,12 +38,24 @@ pager = "0.16" [dev-dependencies] assert_cmd = "2.0.1" escargot = "0.5" -predicates = "2.0.2" +predicates = "3.1.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 1746860..859d06f 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/dbrgn/tealdeer/releases/)! +binaries on the [GitHub releases page](https://github.com/tealdeer-rs/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,7 +52,6 @@ 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 | | @@ -87,9 +86,20 @@ 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. @@ -113,20 +123,19 @@ Thanks to @severen for coming up with the name "tealdeer"! [node-gh]: https://github.com/tldr-pages/tldr-node-client -[c-gh]: https://github.com/tldr-pages/tldr-c-client [hs-gh]: https://github.com/psibi/tldr-hs [fast-tldr-gh]: https://github.com/gutjuri/fast-tldr [bash-gh]: https://4e4.win/tldr [outfieldr-gh]: https://gitlab.com/ve-nt/outfieldr [python-gh]: https://github.com/tldr-pages/tldr-python-client -[benchmark-dockerfile]: https://github.com/dbrgn/tealdeer/blob/main/benchmarks/Dockerfile +[benchmark-dockerfile]: https://github.com/tealdeer-rs/tealdeer/blob/main/benchmarks/Dockerfile [client-spec]: https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md [hyperfine-gh]: https://github.com/sharkdp/hyperfine -[outfieldr-comment-tls]: https://github.com/dbrgn/tealdeer/issues/129#issuecomment-833596765 +[outfieldr-comment-tls]: https://github.com/tealdeer-rs/tealdeer/issues/129#issuecomment-833596765 -[github-actions]: https://github.com/dbrgn/tealdeer/actions?query=branch%3Amain -[github-actions-badge]: https://github.com/dbrgn/tealdeer/workflows/CI/badge.svg +[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 [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 6eb868c..f182519 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -7,7 +7,7 @@ Run linting: Set variables: $ export VERSION=X.Y.Z - $ export GPG_KEY=EA456E8BAF0109429583EED83578F667F2F3A5FA + $ export GPG_KEY=20EE002D778AE197EF7D0D2CB993FF98A90C9AB1 Update version numbers: diff --git a/benchmarks/Dockerfile b/benchmarks/Dockerfile index d717eba..2a36fe0 100644 --- a/benchmarks/Dockerfile +++ b/benchmarks/Dockerfile @@ -18,7 +18,7 @@ FROM rust AS tealdeer-builder WORKDIR /build -RUN git clone https://github.com/dbrgn/tealdeer.git \ +RUN git clone https://github.com/tealdeer-rs/tealdeer.git \ && cd tealdeer \ && cargo build --release \ && mkdir /build-outputs \ diff --git a/clippy.toml b/clippy.toml deleted file mode 100644 index ece14b8..0000000 --- a/clippy.toml +++ /dev/null @@ -1 +0,0 @@ -msrv = "1.54" diff --git a/bash_tealdeer b/completion/bash_tealdeer similarity index 54% rename from bash_tealdeer rename to completion/bash_tealdeer index 2a1429d..d5420b6 100644 --- a/bash_tealdeer +++ b/completion/bash_tealdeer @@ -6,7 +6,7 @@ _tealdeer() _init_completion || return case $prev in - -h|--help|-v|--version|-l|--list|-u|--update|-c|--clear-cache|-p|--pager|-r|--raw|--show-paths|--seed-config|-q|--quiet) + -h|--help|-v|--version|-l|--list|-u|--update|--no-auto-update|-c|--clear-cache|--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' -- "${cur}") ) + COMPREPLY=( $(compgen -W 'linux macos sunos windows android freebsd netbsd openbsd' -- "${cur}") ) return ;; --color) @@ -27,8 +27,9 @@ _tealdeer() COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) ) return fi - - COMPREPLY=( $(compgen -W '$( tldr -l | tr -d , )' -- "${cur}") ) + if tldrlist=$(tldr -l 2>/dev/null); then + COMPREPLY=( $(compgen -W '$( echo "$tldrlist" | tr -d , )' -- "${cur}") ) + fi } complete -F _tealdeer tldr diff --git a/completion/fish_tealdeer b/completion/fish_tealdeer new file mode 100644 index 0000000..528be44 --- /dev/null +++ b/completion/fish_tealdeer @@ -0,0 +1,28 @@ +# +# 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/zsh_tealdeer b/completion/zsh_tealdeer similarity index 82% rename from zsh_tealdeer rename to completion/zsh_tealdeer index 28cd34b..fbab749 100644 --- a/zsh_tealdeer +++ b/completion/zsh_tealdeer @@ -2,8 +2,9 @@ _applications() { local -a commands - commands=(${(uonzf)"$(tldr --list 2>/dev/null)"//:/\\:}) - _describe -t commands 'command' commands + if commands=(${(uonzf)"$(tldr --list 2>/dev/null)"//:/\\:}); then + _describe -t commands 'command' commands + fi } _tealdeer() { @@ -19,9 +20,14 @@ _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]" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 8d1a634..4649382 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -8,5 +8,7 @@ - [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) diff --git a/docs/src/config.md b/docs/src/config.md index 020687e..a662b57 100644 --- a/docs/src/config.md +++ b/docs/src/config.md @@ -9,11 +9,15 @@ 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: - $ tldr --show-paths +```shell +$ tldr --show-paths +``` Creating the config file can be done manually or with the help of `tldr`: - $ tldr --seed-config +```shell +$ tldr --seed-config +``` On Linux, this will usually be `~/.config/tealdeer/config.toml`. @@ -22,13 +26,14 @@ On Linux, this will usually be `~/.config/tealdeer/config.toml`. 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), +([display](config_display.html), [style](config_style.html), [search](config_search.html), [updates](config_updates.html) or [directories](config_directories.html)). ```toml [display] compact = false use_pager = true +show_title = false [style.command_name] foreground = "red" @@ -52,11 +57,3 @@ auto_update = true 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. diff --git a/docs/src/config_directories.md b/docs/src/config_directories.md index d894e90..507bd27 100644 --- a/docs/src/config_directories.md +++ b/docs/src/config_directories.md @@ -2,11 +2,28 @@ 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. - [directories] - custom_pages_dir = "/home/myuser/custom-tldr-pages/" +```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 36d5f23..007d64b 100644 --- a/docs/src/config_display.md +++ b/docs/src/config_display.md @@ -6,8 +6,10 @@ In the `display` section you can configure the output format. Specifies whether the pager should be used by default or not (default `false`). - [display] - use_pager = true +```toml +[display] +use_pager = true +``` When enabled, `less -R` is used as pager. To override the pager command used, set the `PAGER` environment variable. @@ -19,5 +21,51 @@ NOTE: This feature is not available on Windows. Set this to enforce more compact output, where empty lines are stripped out (default `false`). - [display] - compact = true +```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, +} +``` diff --git a/docs/src/config_search.md b/docs/src/config_search.md new file mode 100644 index 0000000..28e00d6 --- /dev/null +++ b/docs/src/config_search.md @@ -0,0 +1,33 @@ +# 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 be2a4cc..593a5b5 100644 --- a/docs/src/config_style.md +++ b/docs/src/config_style.md @@ -22,20 +22,26 @@ 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`, `purple`, `cyan`, `white`): +- Color string (`black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`): Example: - foreground = "green" + ```toml + foreground = "green" + ``` -- 256 color ANSI code (*Tealdeer v1.5.0+*) +- 256 color ANSI code (*tealdeer v1.5.0+*) Example: - foreground = { ansi = 4 } + ```toml + foreground = { ansi = 4 } + ``` -- 24-bit RGB color (*Tealdeer v1.5.0+*) +- 24-bit RGB color (*tealdeer v1.5.0+*) Example: - background = { rgb = { r = 255, g = 255, b = 255 } } + ```toml + background = { rgb = { r = 255, g = 255, b = 255 } } + ``` diff --git a/docs/src/config_updates.md b/docs/src/config_updates.md index 5eb8f95..a8a10c8 100644 --- a/docs/src/config_updates.md +++ b/docs/src/config_updates.md @@ -1,8 +1,10 @@ # Section: \[updates\] +This config section contains settings related to updating the tealdeer cache. + ## 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. @@ -11,8 +13,10 @@ default. Specifies whether the auto-update feature should be enabled (defaults to `false`). - [updates] - auto_update = true +```toml +[updates] +auto_update = true +``` ### `auto_update_interval_hours` @@ -20,7 +24,68 @@ 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`. - [updates] - auto_update = true - auto_update_interval_hours = 24 +```toml +[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 ed16a33..f0ca823 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,49 +14,61 @@ autocompletions](#autocompletion). Tealdeer has been added to a few package managers: -- Arch Linux: [`tealdeer`](https://archlinux.org/packages/community/x86_64/tealdeer/) +- Arch Linux: [`tealdeer`](https://archlinux.org/packages/extra/x86_64/tealdeer/) +- Debian: [`tealdeer`](https://tracker.debian.org/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://nixos.org/nixos/packages.html#tealdeer) +- Nix: [`tealdeer`](https://search.nixos.org/packages?query=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/dbrgn/tealdeer/releases). +[GitHub releases page](https://github.com/tealdeer-rs/tealdeer/releases). Simply download the binary for your platform and run it! ## Through `cargo install` Build and install the tool via cargo... - $ 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.)* +```shell +$ cargo install tealdeer +``` ## Build From Source -Debug build with logging enabled: +Release build: - $ cargo build --features logging +```shell +$ cargo build --release +``` -Release build without logging: +Release build with native TLS support: - $ cargo build --release +```shell +$ cargo build --release --features native-tls +``` -To enable the log output, set the `RUST_LOG` env variable: +Debug build with logging support: - $ export RUST_LOG=tldr=debug +```shell +$ cargo build --features logging +``` + +(To enable logging at runtime, export the `RUST_LOG=tldr=debug` env variable.) ## Autocompletion -- *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` +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` diff --git a/docs/src/intro.md b/docs/src/intro.md index 71ebce0..a90ecec 100644 --- a/docs/src/intro.md +++ b/docs/src/intro.md @@ -1,13 +1,14 @@ # Tealdeer: Introduction -Tealdeer very fast implementation of [tldr](https://github.com/tldr-pages/tldr) -in Rust: Simplified, example based and community-driven man pages. +Tealdeer is a 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/dbrgn/tealdeer) +- [GitHub Project Page](https://github.com/tealdeer-rs/tealdeer) - [TLDR Pages Project](https://tldr.sh/) diff --git a/docs/src/tips_and_tricks.md b/docs/src/tips_and_tricks.md new file mode 100644 index 0000000..29f7216 --- /dev/null +++ b/docs/src/tips_and_tricks.md @@ -0,0 +1,52 @@ +# 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 68ba0f4..6a04de7 100644 --- a/docs/src/usage.txt +++ b/docs/src/usage.txt @@ -1,29 +1,33 @@ -tealdeer 1.5.1 +tealdeer 1.8.1: A fast TLDR client Danilo Bargen , Niklas Mohrin -A fast TLDR client -USAGE: - tldr [OPTIONS] [COMMAND]... +Usage: tldr [OPTIONS] [COMMAND]... -ARGS: - ... The command to show (e.g. `tar` or `git log`) +Arguments: + [COMMAND]... The command to show (e.g. `tar` or `git log`) -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, osx] - -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 +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 -To view the user documentation, please visit https://dbrgn.github.io/tealdeer/. +To view the user documentation, please visit https://tealdeer-rs.github.io/tealdeer/. + +To view usage examples, run tldr tldr or tldr tealdeer. diff --git a/docs/src/usage_custom_pages.md b/docs/src/usage_custom_pages.md index bdae7a9..c73bf90 100644 --- a/docs/src/usage_custom_pages.md +++ b/docs/src/usage_custom_pages.md @@ -1,5 +1,13 @@ # 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. @@ -15,31 +23,36 @@ file](config_directories.html). 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` in the custom pages directory. When calling `tldr `, +`.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: - $CUSTOM_PAGES_DIR/.page +```plain +$CUSTOM_PAGES_DIR/.page.md +``` Example: - ~/.local/share/tealdeer/pages/ufw.page +```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`, it will be appended to existing +case, use a file called `.patch.md`, it will be appended to existing pages. -> **Note:** Because the patch file will be concatenated directly to the page -> file, it needs to start with an empty line. - Path: - $CUSTOM_PAGES_DIR/.patch +```plain +$CUSTOM_PAGES_DIR/.patch.md +``` Example: - ~/.local/share/tealdeer/pages/ufw.patch +```plain +~/.local/share/tealdeer/pages/ufw.patch.md +``` diff --git a/fish_tealdeer b/fish_tealdeer deleted file mode 100644 index ee68e2a..0000000 --- a/fish_tealdeer +++ /dev/null @@ -1,24 +0,0 @@ -# -# 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 new file mode 100644 index 0000000..948b278 --- /dev/null +++ b/pages/tealdeer.md @@ -0,0 +1,42 @@ +# 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/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..f857430 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +# Empty file, use defaults and disregard global settings diff --git a/scripts/upload-asset.sh b/scripts/upload-asset.sh index 7f0a9c3..4f9de09 100644 --- a/scripts/upload-asset.sh +++ b/scripts/upload-asset.sh @@ -29,7 +29,7 @@ create_release() { --header "Authorization: Bearer $token" \ --header "Content-Type:application/json" \ "https://api.github.com/repos/$repo/releases" \ - -d '{"tag_name":"'"$tag"'","name":"'"${tag/v/Release }"'","draft":true,"body":"'"${description/\"/\\\"}"'"}' + -d '{"tag_name":"'"$tag"'","name":"'"${tag/v/Version }"'","draft":true,"body":"'"${description/\"/\\\"}"'"}' ) if [ "$http_code" == "201" ]; then echo "Release for tag $tag created." diff --git a/src/cache.rs b/src/cache.rs index 87db30c..afa3603 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,32 +1,39 @@ use std::{ - env, - ffi::OsStr, fs::{self, File}, - io::{BufReader, Cursor, Read, Seek}, + io::{Cursor, ErrorKind, Read}, path::{Path, PathBuf}, + time::{Duration, SystemTime}, }; -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 anyhow::{anyhow, bail, ensure, Context, Result}; +use log::{debug, info}; +use ureq::{ + http::StatusCode, + tls::{RootCerts, TlsConfig, TlsProvider}, + Agent, +}; use zip::ZipArchive; use crate::{ - error::TealdeerError::{self, CacheError, UpdateError}, - types::{PathSource, PlatformType}, + config::{Language, TlsBackend}, + types::PlatformType, }; -static CACHE_DIR_ENV_VAR: &str = "TEALDEER_CACHE_DIR"; - pub static TLDR_PAGES_DIR: &str = "tldr-pages"; -static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; +pub static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; -#[derive(Debug)] -pub struct Cache { - url: String, - platform: PlatformType, +#[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)] @@ -35,6 +42,228 @@ pub struct PageLookupResult { 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 + } +} + impl PageLookupResult { pub fn with_page(page_path: PathBuf) -> Self { Self { @@ -48,21 +277,21 @@ impl PageLookupResult { self } - /// Create a buffered reader that sequentially reads from the page and the + /// 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>, String> { + pub fn reader(&self) -> Result> { // Open page file let page_file = File::open(&self.page_path) - .map_err(|msg| format!("Could not open page file at {:?}: {}", self.page_path, msg))?; + .with_context(|| format!("Could not open page file at {}", self.page_path.display()))?; // Open patch file let patch_file_opt = match &self.patch_path { Some(path) => Some( File::open(path) - .map_err(|msg| format!("Could not open patch file at {:?}: {}", path, msg))?, + .with_context(|| format!("Could not open patch file at {}", path.display()))?, ), None => None, }; @@ -73,314 +302,76 @@ impl PageLookupResult { // 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(BufReader::new(if let Some(patch_file) = patch_file_opt { + Ok(if let Some(patch_file) = patch_file_opt { Box::new(page_file.chain(&b"\n"[..]).chain(patch_file)) as Box } else { Box::new(page_file) as Box - })) + }) } } -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 Language<'_> { + fn directory_name(&self) -> String { + format!("pages.{}", self.0) + } } -impl Cache { - pub fn new(url: S, platform: PlatformType) -> 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)); - }; - - // 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 { +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", } } +} - /// 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; - } +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), }; + let config = Agent::config_builder() + .http_status_as_error(false) // because we want to handle them + .tls_config(tls_builder.build()) + .build(); - let lang_dirs: Vec = languages - .iter() - .map(|lang| { - if lang == "en" { - String::from("pages") - } else { - format!("pages.{}", lang) - } - }) - .collect(); + config.into() + } - // 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)); + /// 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:?}") } } - - 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| 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 { - return Err(CacheError(format!( - "Cache path ({}) does not exist.", - path.display() - ))); - }; - Ok(()) } } @@ -398,8 +389,8 @@ mod tests { fn test_reader_with_patch() { // Write test files let dir = tempfile::tempdir().unwrap(); - let page_path = dir.path().join("test.page"); - let patch_path = dir.path().join("test.patch"); + 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(); @@ -422,7 +413,7 @@ mod tests { fn test_reader_without_patch() { // Write test file let dir = tempfile::tempdir().unwrap(); - let page_path = dir.path().join("test.page"); + let page_path = dir.path().join("test.page.md"); { let mut f = File::create(&page_path).unwrap(); f.write_all(b"Hello\n").unwrap(); diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..161d69d --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,112 @@ +//! 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 32e902e..f0feeb4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,28 +1,101 @@ use std::{ - env, fs, - io::{Error as IoError, Read, Write}, - path::PathBuf, + borrow::Cow, + env, fmt, + fs::{self, File}, + io::{ErrorKind, Write}, + path::{Component, Path, PathBuf}, + sync::LazyLock, time::Duration, }; -use ansi_term::{Color, Style}; -use app_dirs::{get_app_root, AppDataType}; -use log::debug; +use anyhow::{anyhow, ensure, Context, Result}; +use clap::ValueEnum; +use log::info; +use serde::Serialize as _; use serde_derive::{Deserialize, Serialize}; +use yansi::{Color, Style}; use crate::{ - error::TealdeerError::{self, ConfigError}, - types::PathSource, + extensions::Dedup as _, + types::{PathSource, PlatformType}, }; 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 } @@ -39,7 +112,8 @@ pub enum RawColor { Green, Yellow, Blue, - Purple, + Magenta, + Purple, // Backwards compatibility with ansi_term (until tealdeer 1.5.0) Cyan, White, Ansi(u8), @@ -54,11 +128,11 @@ impl From for Color { RawColor::Green => Self::Green, RawColor::Yellow => Self::Yellow, RawColor::Blue => Self::Blue, - RawColor::Purple => Self::Purple, + RawColor::Magenta | RawColor::Purple => Self::Magenta, 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), } } } @@ -97,7 +171,7 @@ impl From for Style { } if let Some(background) = raw_style.background { - style = style.on(Color::from(background)); + style = style.bg(Color::from(background)); } if raw_style.underline { @@ -130,12 +204,59 @@ 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: @@ -146,12 +267,35 @@ 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 { @@ -159,25 +303,75 @@ 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, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] struct RawDirectoriesConfig { + #[serde(default)] + pub cache_dir: Option, #[serde(default)] pub custom_pages_dir: Option, } -impl Default for RawDirectoriesConfig { - fn default() -> Self { +#[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, + ]) + }; + Self { - custom_pages_dir: get_app_root(AppDataType::UserData, &crate::APP_INFO) - .map(|path| { - // Note: The `join("")` call ensures that there's a trailing slash - path.join("pages").join("") - }) - .ok(), + languages, + platforms, } } } @@ -189,12 +383,7 @@ struct RawConfig { display: RawDisplayConfig, updates: RawUpdatesConfig, directories: RawDirectoriesConfig, -} - -impl RawConfig { - fn new() -> Self { - Self::default() - } + search: RawSearchConfig, } impl Default for RawConfig { @@ -204,6 +393,7 @@ impl Default for RawConfig { display: RawDisplayConfig::default(), updates: RawUpdatesConfig::default(), directories: RawDirectoriesConfig::default(), + search: RawSearchConfig::default(), }; // Set default config @@ -217,7 +407,7 @@ impl Default for RawConfig { } } -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] pub struct StyleConfig { pub description: Style, pub command_name: Style, @@ -226,104 +416,376 @@ pub struct StyleConfig { pub example_variable: Style, } -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct DisplayConfig { pub compact: bool, pub use_pager: bool, + pub show_title: bool, + pub indent: Indent, } -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct UpdatesConfig { +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Indent { + pub base: usize, + pub command: usize, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UpdatesConfig<'a> { 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)] +#[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)] pub struct DirectoriesConfig { - pub custom_pages_dir: Option, + pub cache_dir: PathWithSource, + pub custom_pages_dir: Option, } -#[derive(Clone, Debug, PartialEq)] -pub struct Config { - pub style: StyleConfig, - pub display: DisplayConfig, - pub updates: UpdatesConfig, - pub directories: DirectoriesConfig, +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SearchConfig<'a> { + pub languages: Vec>, + pub platforms: Vec, } -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, - }, +#[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(), + )) } } } -#[allow(clippy::needless_pass_by_value)] -fn map_io_err_to_config_err(e: IoError) -> TealdeerError { - ConfigError(format!("Io Error: {}", e)) +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 Config { - pub fn load(enable_styles: bool) -> Result { - debug!("Loading config"); +impl fmt::Display for TlsBackend { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_raw().fmt(f) + } +} - // Determine path - let (config_file_path, _) = get_config_path() - .map_err(|e| ConfigError(format!("Could not determine config path: {}", e)))?; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Config<'a> { + pub style: StyleConfig, + pub display: DisplayConfig, + pub updates: UpdatesConfig<'a>, + pub directories: DirectoriesConfig, + pub search: SearchConfig<'a>, + pub file_path: PathWithSource, +} - // 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 { - RawConfig::new() +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, + }, }; - // Convert to config - let mut config = Self::from(raw_config); + let relative_path_root = config_file_path + .path() + .parent() + .context("Failed to get config directory")?; + let home_path = env::home_dir(); - // 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(), - }; + // 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); + + PathWithSource { + path: resolved_path, + source: PathSource::ConfigFile, + } + } 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, + }; + + Ok(Self { + style, + display, + updates, + directories, + search, + file_path: config_file_path, + }) + } +} + +/// Expands tilde (~) prefixed directories into its absolute version +fn expand_home<'a>(input_path: &'a Path, home_path: Option<&Path>) -> Result> { + let mut components = input_path.components(); + + if let Some(Component::Normal(first_component_raw)) = components.next() { + let first_component = first_component_raw + .to_str() + .ok_or(anyhow!("Path contains invalid UTF-8"))?; + + if first_component == "~" { + let home_path = home_path.ok_or(anyhow!("Unable to find user home directory"))?; + let rest: PathBuf = components.collect(); + let expanded = home_path.join(rest); + + return Ok(Cow::Owned(expanded)); + } else if first_component.starts_with('~') { + return Err(anyhow!("Tilde expansion with a login name not supported")); } + } - Ok(config) + 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") } } @@ -334,77 +796,233 @@ impl Config { /// /// Note that this function does not verify whether the directory at that /// location exists, or is a directory. -pub fn get_config_dir() -> Result<(PathBuf, PathSource), TealdeerError> { +pub fn get_config_dir() -> (PathBuf, PathSource) { // Allow overriding the config directory by setting the // $TEALDEER_CONFIG_DIR env variable. if let Ok(value) = env::var("TEALDEER_CONFIG_DIR") { - return Ok((PathBuf::from(value), PathSource::EnvVar)); - }; - - // 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 (PathBuf::from(value), PathSource::EnvVar); } + + (SYSTEM_DIRECTORIES.config.clone(), PathSource::OsConvention) } /// 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_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)) +pub fn get_default_config_path() -> PathWithSource { + let (mut path, source) = get_config_dir(); + path.push(CONFIG_FILE_NAME); + PathWithSource { path, source } } /// Create default config file. -pub fn make_default_config() -> Result { - let (config_dir, _) = get_config_dir()?; +/// 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(); - // 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 - ))); + // 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")?; } - } 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(), - ))); - } + + config_dir.join(CONFIG_FILE_NAME) + }; // Ensure that a config file doesn't get overwritten - 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() - ))); - } + ensure!( + !config_file_path.is_file(), + "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::new()) - .map_err(|err| ConfigError(format!("Failed to serialize default config: {}", err)))?; + let serialized_config = + toml::to_string(&RawConfig::default()).context("Failed to serialize default config")?; // Write default config - let mut config_file = fs::File::create(&config_file_path).map_err(map_io_err_to_config_err)?; + let mut config_file = + File::create(&config_file_path).context("Could not create config file")?; let _wc = config_file .write(serialized_config.as_bytes()) - .map_err(map_io_err_to_config_err)?; + .context("Could not write to config file")?; Ok(config_file_path) } -#[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); +#[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") + ] + ); + } + } } diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 548c39f..0000000 --- a/src/error.rs +++ /dev/null @@ -1,40 +0,0 @@ -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 3aebfa2..e74e9f3 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 9789975..082f436 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -1,26 +1,62 @@ //! Functions related to formatting and printing lines from a `Tokenizer`. -use crate::{extensions::FindFrom, types::LineType}; - use log::debug; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +use crate::{config::Indent, extensions::FindFrom, types::LineType}; + +#[derive(Debug, Clone, Copy, Eq)] /// Represents a snippet from a page of a specific highlighting class. -pub enum PageSnippet<'a> { - CommandName(&'a str), - Variable(&'a str), - NormalCode(&'a str), - Description(&'a str), - Text(&'a str), +pub enum PageSnippet { + CommandName(T), + Variable(T), + NormalCode(T), + Description(T), + Text(T), + Title(T), Linebreak, } -impl<'a> PageSnippet<'a> { +#[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> { pub fn is_empty(&self) -> bool { use PageSnippet::*; match self { - CommandName(s) | Variable(s) | NormalCode(s) | Description(s) | Text(s) => s.is_empty(), + CommandName(s) | Variable(s) | NormalCode(s) | Description(s) | Text(s) | Title(s) => { + s.is_empty() + } Linebreak => false, } } @@ -31,11 +67,15 @@ 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>) -> Result<(), E>, + F: for<'snip> FnMut(PageSnippet<&'snip str>) -> 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 { @@ -45,51 +85,122 @@ where } } LineType::Title(title) => { - debug!("Ignoring 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"); + } // 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); + 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)?; } - LineType::Description(text) => process_snippet(PageSnippet::Description(&text))?, - LineType::ExampleText(text) => process_snippet(PageSnippet::Text(&text))?, LineType::ExampleCode(text) => { - process_snippet(PageSnippet::NormalCode(" "))?; + process_snippet(PageSnippet::NormalCode(&command_indent))?; 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 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>, +/// 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>, ) -> Result<(), E> { - 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))?; + // 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..]; } + + if !text.is_empty() { + highlight_code_segment(command, &replace_escaped(text), process_snippet)?; + } + Ok(()) } -/// Yields `NormalCode` and `CommandName` in alternating order according to the occurences of +/// Find a "{{" (or "}}") substring that does not overlap with a preceding "\{\{" (or "\}\}"). +fn find_marker(s: &str, marker: &str, forbidden_prefix: &str) -> Option { + let mut search_start = 0; + loop { + let marker_index = s.find_from(marker, search_start)?; + + let overlaps_with_prefix = (forbidden_prefix.len() <= marker_index + 1) && { + let prefix_start = marker_index + 1 - forbidden_prefix.len(); + // NOTE: The indices might not be valid character offsets, so we should do this + // comparison on raw bytes. If prefix_start is indeed not a character offset than the + // comparison is guaranteed to return false because forbidden_prefix[0] definitely _is_ + // the start of a (single byte, ASCII) character. + &s.as_bytes()[prefix_start..=marker_index] == forbidden_prefix.as_bytes() + }; + if !overlaps_with_prefix { + return Some(marker_index); + } + + // The next valid marker cannot include the first character of the current match + search_start = marker_index + 1; + } +} + +/// Yields `NormalCode` and `CommandName` in alternating order according to the occurrences of /// `command_name` in `segment`. 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>) -> Result<(), E>, + process_snippet: &mut impl FnMut(PageSnippet<&'a str>) -> Result<(), E>, ) -> Result<(), E> { if !command_name.is_empty() { let mut search_start = 0; @@ -113,26 +224,23 @@ 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(surrouding: &str, substring: (usize, usize)) -> bool { +fn is_freestanding_substring(surrounding: &str, substring: (usize, usize)) -> bool { let (start, end) = substring; // "okay" meaning or - let char_before_is_okay = surrouding[..start] + let char_before_is_okay = surrounding[..start] .chars() .last() - .filter(|prev_char| !prev_char.is_whitespace()) - .is_none(); - let char_after_is_okay = surrouding[end..] + .is_none_or(char::is_whitespace); + let char_after_is_okay = surrounding[end..] .chars() .next() - .filter(|next_char| !next_char.is_whitespace()) - .is_none(); + .is_none_or(char::is_whitespace); char_before_is_okay && char_after_is_okay } #[cfg(test)] mod tests { use super::*; - use PageSnippet::*; #[test] fn test_is_freestanding_substring() { @@ -159,80 +267,193 @@ 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<'a>| { + let mut process_snippet = |snip: PageSnippet<&str>| { if !snip.is_empty() { - yielded.push(snip); + yielded.push(snip.map(str::to_string)); } Ok::<(), ()>(()) }; - highlight_code_segment(cmd, segment, &mut process_snippet) - .expect("highlight code segment failed"); + highlight_code(cmd, segment, &mut process_snippet).expect("highlight code segment failed"); yielded } - #[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 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_i18n() { - assert_eq!( - &run("mäke", "mäke höhlenrätselbücher"), - &[CommandName("mäke"), NormalCode(" höhlenrätselbücher")] - ); - assert_eq!( - &run( - "Müll", - "1000 Gründe warum Müll heute größer ist als Müll früher, ärgerlich" - ), - &[ - NormalCode("1000 Gründe warum "), - CommandName("Müll"), - NormalCode(" heute größer ist als "), - CommandName("Müll"), - NormalCode(" früher, ärgerlich") - ] - ); - assert_eq!( - &run( - "übergang", - "die Zustandsübergangsfunktion übergang Änderungen", - ), - &[ - NormalCode("die Zustandsübergangsfunktion "), - CommandName("übergang"), - NormalCode(" Änderungen") - ], - ); - } + mod placeholders { + use super::*; + use PageSnippet::*; - #[test] - fn test_empty_command() { - let segment = "some code"; - let snippets = [NormalCode(segment)]; + #[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:"), + ], + ); - assert_eq!(run("", segment), snippets); - assert_eq!(run(" ", segment), snippets); - assert_eq!(run(" \t ", segment), snippets); + 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")],); + } } } diff --git a/src/line_iterator.rs b/src/line_iterator.rs index 4d0eab6..98088c0 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; +use std::io::{BufRead, Read}; use log::warn; @@ -12,7 +12,7 @@ pub enum TldrFormat { Undecided, /// The original format V1, - /// The new format (see https://github.com/tldr-pages/tldr/pull/958) + /// The new format (see ) 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,10 +64,11 @@ impl Iterator for LineIterator { self.format = TldrFormat::V1; } else { // It's the new format! Drop next line. - // (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); + 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:?}"); return None; } self.first_line = false; @@ -95,21 +96,27 @@ mod test { #[test] fn test_first_line_old_format() { - let input = "# The Title\n\n"; + let input = "# The Title\n> Description\n"; let mut lines = LineIterator::new(input.as_bytes()); let title = lines.next().unwrap(); assert_eq!(title, LineType::Title("The Title".to_string())); - let empty = lines.next().unwrap(); - assert_eq!(empty, LineType::Empty); + let description = lines.next().unwrap(); + assert_eq!( + description, + LineType::Description("Description".to_string()) + ); } #[test] fn test_first_line_new_format() { - let input = "The Title\n=========\n\n"; + let input = "The Title\n=========\n> Description\n"; let mut lines = LineIterator::new(input.as_bytes()); let title = lines.next().unwrap(); assert_eq!(title, LineType::Title("The Title".to_string())); - let empty = lines.next().unwrap(); - assert_eq!(empty, LineType::Empty); + let description = lines.next().unwrap(); + assert_eq!( + description, + LineType::Description("Description".to_string()) + ); } } diff --git a/src/main.rs b/src/main.rs index b959f6e..6678ceb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,18 +15,36 @@ #![allow(clippy::similar_names)] #![allow(clippy::struct_excessive_bools)] #![allow(clippy::too_many_lines)] +#![allow(clippy::unnecessary_debug_formatting)] +#![allow(clippy::while_let_loop)] -use std::{env, path::PathBuf, process}; +#[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 app_dirs::AppInfo; -use atty::Stream; -use clap::{AppSettings, ArgGroup, Parser}; -#[cfg(not(target_os = "windows"))] -use pager::Pager; +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; mod cache; +mod cli; mod config; -mod error; pub mod extensions; mod formatter; mod line_iterator; @@ -35,299 +53,92 @@ mod types; mod utils; use crate::{ - cache::{Cache, CacheFreshness, PageLookupResult, TLDR_PAGES_DIR}, - config::{get_config_dir, get_config_path, make_default_config, Config}, - error::TealdeerError::ConfigError, - extensions::Dedup, + cache::{Cache, PageLookupResult, TLDR_PAGES_DIR}, + cli::Cli, + config::{ + get_config_dir, make_default_config, supported_tls_backends_string, Config, PathWithSource, + }, output::print_page, - types::{ColorOptions, PlatformType}, + types::ColorOptions, utils::{print_error, print_warning}, }; const NAME: &str = "tealdeer"; -const APP_INFO: AppInfo = AppInfo { - name: NAME, - author: NAME, -}; -const ARCHIVE_URL: &str = "https://github.com/tldr-pages/tldr/releases/latest/download/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::DeriveDisplayOrder)] -#[clap(setting = AppSettings::DisableColoredHelp)] -#[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 - #[clap( - short = 'p', - long = "platform", - possible_values = ["linux", "macos", "windows", "sunos", "osx"], - )] - platform: Option, - - /// Deprecated alias of `platform` - #[clap( - short = 'o', - long = "os", - possible_values = ["linux", "macos", "windows", "sunos", "osx"], - hide = true - )] - os: Option, - - /// 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", - hide = 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 - } - } -} +static TEALDEER_PAGE: &str = + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/pages/tealdeer.md")); /// Clear the 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); - }); +fn clear_cache(cache: Cache, quietly: bool) -> Result<()> { + let cache_dir = cache.config().pages_directory.display(); + cache.clear().context("Could not clear cache")?; if !quietly { - eprintln!("Successfully deleted cache."); + eprintln!("Successfully cleared cache at `{cache_dir}`."); } + Ok(()) } /// Update the 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); - }); +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")?; if !quietly { eprintln!("Successfully updated cache."); - } -} - -/// 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); + 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(", ")); } } + Ok(()) } /// Show file paths fn show_paths(config: &Config) { - 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(|_| "[Invalid]".to_string()) - }, - ); - let custom_pages_dir = config.directories.custom_pages_dir.as_deref().map_or_else( - || "[None]".to_string(), - |path| { - path.to_str() - .map_or_else(|| "[Invalid]".to_string(), ToString::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); + 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}"); } -/// 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); - } - } +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(()) } #[cfg(feature = "logging")] @@ -338,109 +149,96 @@ fn init_log() { #[cfg(not(feature = "logging"))] fn init_log() {} -fn get_languages(env_lang: Option<&str>, env_language: Option<&str>) -> Vec { - // Language list according to - // https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#language +fn spawn_editor(custom_pages_dir: &Path, file_name: &str) -> Result<()> { + create_dir_all(custom_pages_dir).context("Failed to create custom pages directory")?; - if env_lang.is_none() { - return vec!["en".to_string()]; + 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())); } - 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() + Ok(()) } -fn get_languages_from_env() -> Vec { - get_languages( - std::env::var("LANG").ok().as_deref(), - std::env::var("LANGUAGE").ok().as_deref(), - ) -} - -fn main() { +fn main() -> ExitCode { // Initialize logger init_log(); // Parse arguments - let mut args = Args::parse(); + let args = Cli::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 => true, + ColorOptions::Always => { + yansi::enable(); // disable yansi's automatic detection for ANSI support on Windows + 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 => { - ansi_support && env::var_os("NO_COLOR").is_none() && atty::is(Stream::Stdout) - } + ColorOptions::Auto => env::var_os("NO_COLOR").is_none() && io::stdout().is_terminal(), // Disable styling ColorOptions::Never => false, }; - // Handle renamed arguments - if args.markdown { - args.raw = true; - print_warning( - enable_styles, - "The -m / --markdown flag is deprecated, use -r / --raw instead", - ); - } - if args.os.is_some() { - print_warning( - enable_styles, - "The -o / --os flag is deprecated, use -p / --platform instead", - ); - } - args.platform = args.platform.or(args.os); - - // 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); - } + 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. - 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); + 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")? } - Err(e) => { - print_error(enable_styles, &format!("Could not load config: {}", e)); - process::exit(1); + _ => { + ConfigLoader::read_default_path().context("Could not read config from default path")? } }; + let mut config = config_loader.load()?; - // Set up pager - if args.pager || config.display.use_pager { - configure_pager(enable_styles); + // Override styles if needed + if !enable_styles { + config.style = StyleConfig::default(); + } + + 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 various paths @@ -450,148 +248,197 @@ fn main() { // Create a basic config and exit if args.seed_config { - create_config_and_exit(enable_styles); + create_config(args.config_path.as_deref())?; + return Ok(ExitCode::SUCCESS); } - // Specify target OS - let platform: PlatformType = args.platform.unwrap_or_else(PlatformType::current); - // If a local file was passed in, render it and exit if let Some(file) = args.render { - 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); - }; + let reader = PageLookupResult::with_page(file).reader()?; + print_page(reader, args.raw, enable_styles, args.pager, &config)?; + return Ok(ExitCode::SUCCESS); } - // Initialize cache - let cache = Cache::new(ARCHIVE_URL, platform); - - // Clear cache, pass through - if args.clear_cache { - clear_cache(args.quiet, enable_styles); + // 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); } - // 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 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), }; - // Check cache presence and freshness - if !cache_updated - && (args.list || !args.command.is_empty()) - && check_cache(&args, enable_styles) == CheckCacheResult::CacheMissing - { - process::exit(1); + 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."); } - // List cached commands and exit - if args.list { - // Get list of pages - let pages = cache.list_pages().unwrap_or_else(|e| { + if args.clear_cache { + if let Some(cache) = Cache::open(cache_config)? { + clear_cache(cache, args.quiet)?; + } + return Ok(ExitCode::SUCCESS); + } + + let cache = if args.update || config.updates.auto_update && !args.no_auto_update { + let (mut cache, was_created) = Cache::open_or_create(cache_config)?; + if was_created || args.update || cache.age()? >= config.updates.auto_update_interval { + let result = update_cache( + &mut cache, + config.updates.archive_source, + config.updates.tls_backend, + args.quiet, + ); + + if let Err(e) = result { + print_error(enable_styles, &e); + + eprintln!(); + eprintln!("Note: Update errors are often caused by unexpected or missing TLS certificates."); + eprintln!( + "You are currently using the following TLS backend: {}", + config.updates.tls_backend, + ); + eprintln!( + "Try changing the updates.tls_backend setting in the config file, for example:" + ); + eprintln!(); + eprintln!(" [updates]"); + eprintln!(" tls_backend = \"rustls-with-native-roots\""); + eprintln!(); + eprintln!( + "This build of tealdeer has support for the following options: {}", + supported_tls_backends_string(), + ); + + return Ok(ExitCode::FAILURE); + } + } + + cache + } else if args.list || !command.is_empty() { + // Cache is needed for these commands to work + let Some(cache) = Cache::open(cache_config)? else { print_error( enable_styles, - &format!("Could not get list of pages: {}", e.message()), + &anyhow::anyhow!( + "Page cache not found. Please run `tldr --update` to download the cache." + ), ); - process::exit(1); - }); + 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"); - // Print pages - println!("{}", pages.join("\n")); - process::exit(0); + 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); } // Show command from cache - 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(); + 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(), + ), + ); + } - // Collect languages - let languages = args - .language - .map_or_else(get_languages_from_env, |lang| vec![lang]); - - // Search for command in cache - if let Some(lookup_result) = cache.find_page( - &command, - &languages, - config.directories.custom_pages_dir.as_deref(), - ) { - if let Err(msg) = print_page(&lookup_result, args.raw, &config) { - print_error(enable_styles, &msg); - process::exit(1); - } - process::exit(0); - } else { + let Some(result) = cache.find_page(&command) else { if !args.quiet { print_warning( enable_styles, &format!( - "Page `{}` not found in cache.\n\ + "Page `{command}` not found in cache.\n\ Try updating with `tldr --update`, or submit a pull request to:\n\ - https://github.com/tldr-pages/tldr", - &command + https://github.com/tldr-pages/tldr" ), ); } - 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"]); - } + return Ok(ExitCode::FAILURE); + }; + + print_page( + result.reader()?, + args.raw, + enable_styles, + args.pager, + &config, + )?; } + + Ok(ExitCode::SUCCESS) } diff --git a/src/output.rs b/src/output.rs index cad0690..6243b44 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,23 +1,46 @@ //! Functions for printing pages to the terminal -use std::io::{self, BufRead, Write}; +use std::io::{self, BufRead, BufReader, Read, Write}; + +use anyhow::{Context, Result}; +use yansi::Paint; 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( - lookup_result: &PageLookupResult, + reader: impl Read, enable_markdown: bool, + enable_styles: bool, + use_pager: bool, config: &Config, -) -> Result<(), String> { - // Create reader from file(s) - let reader = lookup_result.reader()?; +) -> 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 let stdout = io::stdout(); @@ -26,17 +49,16 @@ pub fn print_page( 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())?; + 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<'_>| { + let mut process_snippet = |snip: PageSnippet<&str>| { if snip.is_empty() { Ok(()) } else { - print_snippet(&mut handle, snip, &config.style) - .map_err(|e| WriteError(e.to_string())) + print_snippet(&mut handle, snip, &config.style).context("Failed to print snippet") } }; @@ -45,31 +67,31 @@ pub fn print_page( LineIterator::new(reader), &mut process_snippet, !config.display.compact, + config.display.show_title, + config.display.indent, ) - .map_err(|e| format!("Could not write to stdout: {}", e.message()))?; - }; + .context("Could not write to stdout")?; + } // We're done outputting data, flush stdout now! - handle - .flush() - .map_err(|_| "Could not flush stdout".to_string())?; + handle.flush().context("Could not flush stdout")?; Ok(()) } fn print_snippet( writer: &mut impl Write, - snip: PageSnippet<'_>, + snip: PageSnippet<&str>, style: &StyleConfig, -) -> Result<(), io::Error> { +) -> io::Result<()> { use PageSnippet::*; match snip { - 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)), + 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)), Linebreak => writeln!(writer), } } diff --git a/src/types.rs b/src/types.rs index 3981776..7ca6e2d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -10,8 +10,13 @@ use serde_derive::{Deserialize, Serialize}; pub enum PlatformType { Linux, OsX, - SunOs, Windows, + SunOs, + Android, + FreeBsd, + NetBsd, + OpenBsd, + Common, } impl fmt::Display for PlatformType { @@ -19,25 +24,43 @@ impl fmt::Display for PlatformType { match self { Self::Linux => write!(f, "Linux"), Self::OsX => write!(f, "macOS / BSD"), - Self::SunOs => write!(f, "SunOS"), 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"), } } } -impl str::FromStr for PlatformType { - type Err = String; +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, + ] + } - fn from_str(s: &str) -> Result { - match s { - "linux" => Ok(Self::Linux), - "osx" | "macos" => Ok(Self::OsX), - "sunos" => Ok(Self::SunOs), - "windows" => Ok(Self::Windows), - other => Err(format!( - "Unknown OS: {}. Possible values: linux, macos, osx, sunos, windows", - other - )), + 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")), } } } @@ -48,13 +71,7 @@ impl PlatformType { Self::Linux } - #[cfg(any( - target_os = "macos", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", - target_os = "dragonfly" - ))] + #[cfg(any(target_os = "macos", target_os = "dragonfly"))] pub fn current() -> Self { Self::OsX } @@ -64,6 +81,26 @@ 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", @@ -71,43 +108,24 @@ impl PlatformType { target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly", - target_os = "windows" + target_os = "windows", + target_os = "android", )))] pub fn current() -> Self { Self::Other } } -#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)] +#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize, clap::ValueEnum)] #[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, @@ -175,16 +193,16 @@ impl LineType { } /// The reason why a certain path (e.g. config path or cache dir) was chosen. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum PathSource { /// OS convention (e.g. XDG on Linux) OsConvention, /// Env variable (TEALDEER_*) EnvVar, - - #[allow(dead_code)] // Waiting for Pull Request #141 - /// Config file variable - ConfigVar, + /// Config file + ConfigFile, + /// CLI argument override + Cli, } impl fmt::Display for PathSource { @@ -195,7 +213,8 @@ impl fmt::Display for PathSource { match self { Self::OsConvention => "OS convention", Self::EnvVar => "env variable", - Self::ConfigVar => "config file variable", + Self::ConfigFile => "config file", + Self::Cli => "command line argument", } ) } diff --git a/src/utils.rs b/src/utils.rs index e6f65f5..f4d825a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -use ansi_term::{Color, Style}; +use yansi::{Color, Paint}; /// Print a warning to stderr. If `enable_styles` is true, then a yellow /// message will be printed. @@ -6,17 +6,16 @@ pub fn print_warning(enable_styles: bool, message: &str) { print_msg(enable_styles, message, "Warning: ", Color::Yellow); } -/// 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); +/// 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); } fn print_msg(enable_styles: bool, message: &str, prefix: &'static str, color: Color) { if enable_styles { - let style = Style::new().fg(color); - eprintln!("{}{}", style.paint(prefix), style.paint(message)); + eprintln!("{}{}", prefix.paint(color), message.paint(color)); } 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 new file mode 100644 index 0000000..ca1bacc --- /dev/null +++ b/tests/cache/pages.en/common/git-checkout.md @@ -0,0 +1,36 @@ +# 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/inkscape-v1.md b/tests/cache/pages.en/common/inkscape-v1.md similarity index 100% rename from tests/inkscape-v1.md rename to tests/cache/pages.en/common/inkscape-v1.md diff --git a/tests/inkscape-v2.md b/tests/cache/pages.en/common/inkscape-v2.md similarity index 100% rename from tests/inkscape-v2.md rename to tests/cache/pages.en/common/inkscape-v2.md diff --git a/tests/which-markdown.expected b/tests/cache/pages.en/common/which.md similarity index 100% rename from tests/which-markdown.expected rename to tests/cache/pages.en/common/which.md diff --git a/tests/cache/pages.ja/common/apt.md b/tests/cache/pages.ja/common/apt.md new file mode 100644 index 0000000..fe16a3d --- /dev/null +++ b/tests/cache/pages.ja/common/apt.md @@ -0,0 +1,37 @@ +# 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 deleted file mode 100644 index c7f92c2..0000000 --- a/tests/chmod.ru.expected +++ /dev/null @@ -1,32 +0,0 @@ - - Изменить права доступа файлу или папке. - Больше информации: . - - Дать [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 deleted file mode 100644 index 4b92329..0000000 --- a/tests/chmod.ru.md +++ /dev/null @@ -1,32 +0,0 @@ -# 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/inkscape-v2.patch b/tests/custom-pages/inkscape-v2.patch.md similarity index 100% rename from tests/inkscape-v2.patch rename to tests/custom-pages/inkscape-v2.patch.md diff --git a/tests/lib.rs b/tests/lib.rs index dd03135..d431b5f 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,8 +1,9 @@ //! Integration tests. use std::{ - fs::{create_dir_all, File}, - io::Write, + fs::{self, create_dir_all, File}, + io::{self, Write}, + path::{Path, PathBuf}, process::Command, time::{Duration, SystemTime}, }; @@ -10,46 +11,94 @@ use std::{ use assert_cmd::prelude::*; use predicates::{ boolean::PredicateBooleanExt, - prelude::predicate::str::{contains, diff, is_empty}, + ord::eq, + prelude::predicate::str::{contains, diff, is_empty, is_match}, }; -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"; +use tempfile::{Builder as TempfileBuilder, TempDir}; pub static TLDR_PAGES_DIR: &str = "tldr-pages"; +pub static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; struct TestEnv { - pub cache_dir: TempDir, - pub custom_pages_dir: TempDir, - pub config_dir: TempDir, - pub input_dir: TempDir, + _test_dir: TempDir, pub default_features: bool, pub features: Vec, } impl TestEnv { fn new() -> Self { - 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(), + let test_dir: TempDir = TempfileBuilder::new() + .prefix(".tldr.test") + .tempdir() + .unwrap(); + + let this = TestEnv { + _test_dir: test_dir, 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 } - /// 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 cache_dir(&self) -> PathBuf { + self._test_dir.path().join(".cache") + } - let mut config_file = File::create(&config_file_name).unwrap(); - config_file.write_all(content.as_ref().as_bytes()).unwrap(); + 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 } /// Add entry for that environment to the "common" pages. @@ -59,43 +108,47 @@ 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 - .path() + .cache_dir() .join(TLDR_PAGES_DIR) - .join("pages") + .join(format!("pages.{lang}")) .join(os); create_dir_all(&dir).unwrap(); - let mut file = File::create(&dir.join(format!("{}.md", name))).unwrap(); - file.write_all(contents.as_bytes()).unwrap(); + fs::write(dir.join(format!("{name}.md")), 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.path(); + let dir = &self.custom_pages_dir(); create_dir_all(dir).unwrap(); - let mut file = File::create(&dir.join(format!("{}.page", name))).unwrap(); - file.write_all(contents.as_bytes()).unwrap(); + fs::write(dir.join(format!("{name}.page.md")), 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.path(); + let dir = &self.custom_pages_dir(); create_dir_all(dir).unwrap(); - let mut file = File::create(&dir.join(format!("{}.patch", name))).unwrap(); - file.write_all(contents.as_bytes()).unwrap(); + fs::write(dir.join(format!("{name}.patch.md")), 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 @@ -105,23 +158,137 @@ 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.arg("--no-default-features"); + build = build.no_default_features(); } if !self.features.is_empty() { - build = build.arg(&format!("--feature {}", self.features.join(","))); + build = build.features(self.features.join(" ")) } - let run = build.run().unwrap(); + let run = build.run().expect("Failed to build tealdeer for testing"); let mut cmd = run.command(); - 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(), - ); + + // 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 } + + 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] @@ -131,11 +298,22 @@ fn test_missing_cache() { .args(["sl"]) .assert() .failure() - .stderr(contains("Cache not found. Please run `tldr --update`.")); + .stderr(contains("Page cache not found. Please run `tldr --update`")); } #[test] -fn test_update_cache() { +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() { let testenv = TestEnv::new(); testenv @@ -143,7 +321,7 @@ fn test_update_cache() { .args(["sl"]) .assert() .failure() - .stderr(contains("Cache not found. Please run `tldr --update`.")); + .stderr(contains("Page cache not found. Please run `tldr --update`")); testenv .command() @@ -155,6 +333,55 @@ fn test_update_cache() { 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(); @@ -174,15 +401,64 @@ fn test_quiet_cache() { } #[test] -fn test_quiet_failures() { - let testenv = TestEnv::new(); +fn test_clear_only_pages_directory() { + let testenv = TestEnv::new().install_default_cache(); + testenv + .command() + .args(["--clear-cache"]) + .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() - .args(["--update", "-q"]) + .arg("--list") .assert() - .success() - .stdout(is_empty()); + .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(); testenv .command() @@ -194,54 +470,71 @@ fn test_quiet_failures() { #[test] fn test_quiet_old_cache() { - let testenv = TestEnv::new(); - - testenv - .command() - .args(["--update", "-q"]) - .assert() - .success() - .stdout(is_empty()); + let testenv = TestEnv::new().install_default_cache(); filetime::set_file_mtime( - testenv.cache_dir.path().join(TLDR_PAGES_DIR), + testenv.cache_dir().join(TLDR_PAGES_DIR), filetime::FileTime::from_unix_time(1, 0), ) .unwrap(); testenv .command() - .args(["tldr"]) + .args(["which"]) .assert() .success() .stderr(contains("The cache hasn't been updated for ")); testenv .command() - .args(["tldr", "--quiet"]) + .args(["which", "--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(); - let cache_dir = testenv.cache_dir.path(); + let testenv = TestEnv::new().remove_initial_config(); + let cache_dir = &testenv.cache_dir(); 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("-u") + .arg("--update") .assert() .success() .stderr(contains(format!( - "Successfully created cache directory path `{}`.", - internal_cache_dir.to_str().unwrap() + "Successfully created cache directory `{}`.", + internal_cache_dir.join(TLDR_PAGES_DIR).to_str().unwrap() ))) .stderr(contains("Successfully updated cache.")); @@ -251,33 +544,151 @@ fn test_create_cache_directory_path() { #[test] fn test_cache_location_not_a_directory() { let testenv = TestEnv::new(); - let cache_dir = testenv.cache_dir.path(); - let internal_file = cache_dir.join("internal"); - File::create(&internal_file).unwrap(); + let cache_dir = &testenv.cache_dir(); + File::create(cache_dir.join(TLDR_PAGES_DIR)).unwrap(); - let mut command = testenv.command(); - command.env(CACHE_DIR_ENV_VAR, internal_file.to_str().unwrap()); - - command - .arg("-u") + testenv + .command() + .arg("--list") .assert() .failure() .stderr(contains(format!( - "Path specified by ${} is not a directory.", - CACHE_DIR_ENV_VAR + "Cache directory `{}` exists, but is not a directory.", + cache_dir.join(TLDR_PAGES_DIR).display(), ))); } +#[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] @@ -292,36 +703,22 @@ fn test_show_paths() { .success() .stdout(contains(format!( "Config dir: {}", - testenv.config_dir.path().to_str().unwrap(), + testenv.config_dir().to_str().unwrap(), ))) .stdout(contains(format!( "Config path: {}", - testenv - .config_dir - .path() - .join("config.toml") - .to_str() - .unwrap(), + testenv.config_dir().join("config.toml").to_str().unwrap(), ))) .stdout(contains(format!( "Cache dir: {}", - testenv.cache_dir.path().to_str().unwrap(), + testenv.cache_dir().to_str().unwrap(), ))) .stdout(contains(format!( "Pages dir: {}", - testenv - .cache_dir - .path() - .join(TLDR_PAGES_DIR) - .to_str() - .unwrap(), + testenv.cache_dir().join(TLDR_PAGES_DIR).to_str().unwrap(), ))); - // Set custom pages directory - testenv.write_config(format!( - "[directories]\ncustom_pages_dir = '{}'", - testenv.custom_pages_dir.path().to_str().unwrap() - )); + let testenv = testenv.write_custom_pages_config(); // Now ensure that this path is contained in the output testenv @@ -331,7 +728,7 @@ fn test_show_paths() { .success() .stdout(contains(format!( "Custom pages dir: {}", - testenv.custom_pages_dir.path().to_str().unwrap(), + testenv.custom_pages_dir().to_str().unwrap(), ))); } @@ -349,12 +746,40 @@ fn test_os_specific_page() { } #[test] -fn test_markdown_rendering() { +fn test_config_platforms() { let testenv = TestEnv::new(); + testenv.add_os_entry("sunos", "sunos-command", ""); - testenv.add_entry("which", include_str!("which-markdown.expected")); + let set_config_platforms = |platforms| { + testenv.delete_config(); + testenv.init_config(); + testenv.append_to_config(format!("search.platforms = {platforms}")); + }; - let expected = include_str!("which-markdown.expected"); + // 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 expected = include_str!("cache/pages.en/common/which.md"); testenv .command() .args(["--raw", "which"]) @@ -363,23 +788,13 @@ fn test_markdown_rendering() { .stdout(diff(expected)); } -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(); +fn _test_correct_rendering(page: &str, expected: &'static str, additional_args: &[&str]) { + let testenv = TestEnv::new().install_default_cache(); testenv .command() - .args(["--color", color_option, "-f", file_path.to_str().unwrap()]) + .args(additional_args) + .arg(page) .assert() .success() .stdout(diff(expected)); @@ -389,10 +804,9 @@ fn _test_correct_rendering( #[test] fn test_correct_rendering_v1() { _test_correct_rendering( - include_str!("inkscape-v1.md"), - "inkscape-v1.md", - include_str!("inkscape-default.expected"), - "always", + "inkscape-v1", + include_str!("rendered/inkscape-default.expected"), + &["--color", "always"], ); } @@ -400,10 +814,9 @@ fn test_correct_rendering_v1() { #[test] fn test_correct_rendering_v2() { _test_correct_rendering( - include_str!("inkscape-v2.md"), - "inkscape-v2.md", - include_str!("inkscape-default.expected"), - "always", + "inkscape-v2", + include_str!("rendered/inkscape-default.expected"), + &["--color", "always"], ); } @@ -412,10 +825,9 @@ fn test_correct_rendering_v2() { /// will not use styling since output is not stdout. fn test_rendering_color_auto() { _test_correct_rendering( - include_str!("inkscape-v2.md"), - "inkscape-v2.md", - include_str!("inkscape-default-no-color.expected"), - "auto", + "inkscape-v2", + include_str!("rendered/inkscape-default-no-color.expected"), + &["--color", "auto"], ); } @@ -423,51 +835,87 @@ 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( - include_str!("inkscape-v2.md"), - "inkscape-v2.md", - include_str!("inkscape-default-no-color.expected"), - "never", + "inkscape-v2", + include_str!("rendered/inkscape-default-no-color.expected"), + &["--color", "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( - include_str!("chmod.ru.md"), - "chmod.ru.md", - include_str!("chmod.ru.expected"), - "always", + "apt", + include_str!("rendered/apt.ja.expected"), + &["--color", "always", "--language", "ja"], ); } /// An end-to-end integration test for rendering with custom syntax config. #[test] fn test_correct_rendering_with_config() { - let testenv = TestEnv::new(); + let testenv = TestEnv::new().install_default_cache(); - // 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); + testenv.append_to_config(include_str!("style-config.toml")); - 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"); + let expected = include_str!("rendered/inkscape-with-config.expected"); testenv .command() - .args(["--color", "always", "-f", file_path.to_str().unwrap()]) + .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"]) .assert() .success() .stdout(diff(expected)); @@ -475,14 +923,7 @@ fn test_correct_rendering_with_config() { #[test] fn test_spaces_find_command() { - let testenv = TestEnv::new(); - - testenv - .command() - .args(["--update"]) - .assert() - .success() - .stderr(contains("Successfully updated cache.")); + let testenv = TestEnv::new().install_default_cache(); testenv .command() @@ -493,14 +934,7 @@ fn test_spaces_find_command() { #[test] fn test_pager_flag_enable() { - let testenv = TestEnv::new(); - - testenv - .command() - .args(["--update"]) - .assert() - .success() - .stderr(contains("Successfully updated cache.")); + let testenv = TestEnv::new().install_default_cache(); testenv .command() @@ -510,15 +944,215 @@ fn test_pager_flag_enable() { } #[test] -fn test_list_flag_rendering() { +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(); testenv .command() .args(["--list"]) .assert() .failure() - .stderr(contains("Cache not found. Please run `tldr --update`.")); + .stderr(contains("Page cache not found. Please run `tldr --update`")); testenv.add_entry("foo", ""); @@ -532,15 +1166,87 @@ 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\nfoo\nqux\n"); + .stdout("bar\nbaz\nfaz\nfiz\nfoo\nqux\n"); } +#[test] +fn test_multi_platform_list_flag_rendering() { + let testenv = TestEnv::new().write_custom_pages_config(); + + testenv.add_entry("common", ""); + + testenv + .command() + .args(["--list"]) + .assert() + .success() + .stdout("common\n"); + + testenv + .command() + .args(["--platform", "linux", "--list"]) + .assert() + .success() + .stdout("common\n"); + + testenv + .command() + .args(["--platform", "windows", "--list"]) + .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"); +} + +#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] #[test] fn test_autoupdate_cache() { let testenv = TestEnv::new(); @@ -551,17 +1257,12 @@ fn test_autoupdate_cache() { .args(["--list"]) .assert() .failure() - .stderr(contains("Cache not found. Please run `tldr --update`.")); + .stderr(contains("Page cache not found. Please run `tldr --update`")); - let config_file_path = testenv.config_dir.path().join("config.toml"); - let cache_file_path = testenv.cache_dir.path().join(TLDR_PAGES_DIR); + let cache_file_path = testenv.cache_dir().join(TLDR_PAGES_DIR); - // 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(); + testenv + .append_to_config("updates.auto_update = true\nupdates.auto_update_interval_hours = 24\n"); // Helper function that runs `tldr --list` and asserts that the cache is automatically updated // or not, depending on the value of `expected`. @@ -597,24 +1298,21 @@ fn test_autoupdate_cache() { check_cache_updated(false); } -/// End-end test to ensure .page files overwrite pages in cache_dir +/// End-end test to ensure .page.md files overwrite pages in cache_dir #[test] fn test_custom_page_overwrites() { - let testenv = TestEnv::new(); - - // set custom pages directory - testenv.write_config(format!( - "[directories]\ncustom_pages_dir = '{}'", - testenv.custom_pages_dir.path().to_str().unwrap() - )); + let testenv = TestEnv::new().write_custom_pages_config(); // Add file that should be ignored to the cache dir testenv.add_entry("inkscape-v2", ""); - // Add .page file to custome_pages_dir - testenv.add_page_entry("inkscape-v2", include_str!("inkscape-v2.md")); + // Add .page.md file to custom_pages_dir + testenv.add_page_entry( + "inkscape-v2", + include_str!("cache/pages.en/common/inkscape-v2.md"), + ); // Load expected output - let expected = include_str!("inkscape-default-no-color.expected"); + let expected = include_str!("rendered/inkscape-default-no-color.expected"); testenv .command() @@ -624,24 +1322,15 @@ fn test_custom_page_overwrites() { .stdout(diff(expected)); } -/// End-End test to ensure that .patch files are appened to pages in the cache_dir +/// End-End test to ensure that .patch.md files are appended to pages in the cache_dir #[test] fn test_custom_patch_appends_to_common() { - 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")); + let testenv = TestEnv::new() + .install_default_cache() + .install_default_custom_pages(); // Load expected output - let expected = include_str!("inkscape-patched-no-color.expected"); + let expected = include_str!("rendered/inkscape-patched-no-color.expected"); testenv .command() @@ -651,27 +1340,22 @@ fn test_custom_patch_appends_to_common() { .stdout(diff(expected)); } -/// End-End test to ensure that .patch files are not appended to .page files in the custom_pages_dir +/// End-End test to ensure that .patch.md files are not appended to .page.md 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(); + let testenv = TestEnv::new() + .install_default_cache() + .install_default_custom_pages(); - // 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")); + // 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"), + ); // Load expected output - let expected = include_str!("inkscape-default-no-color.expected"); + let expected = include_str!("rendered/inkscape-default-no-color.expected"); testenv .command() @@ -684,13 +1368,7 @@ fn test_custom_patch_does_not_append_to_custom() { #[test] #[cfg(target_os = "windows")] fn test_pager_warning() { - let testenv = TestEnv::new(); - testenv - .command() - .args(["--update"]) - .assert() - .success() - .stderr(contains("Successfully updated cache.")); + let testenv = TestEnv::new().install_default_cache(); // Regular call should not show a "pager flag not available on windows" warning testenv @@ -731,15 +1409,13 @@ 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(); + let testenv = TestEnv::new().install_default_cache(); - // 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()]; + 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()]; // Default render testenv @@ -747,7 +1423,9 @@ fn test_raw_render_file() { .args(&args) .assert() .success() - .stdout(diff(include_str!("inkscape-default-no-color.expected"))); + .stdout(diff(include_str!( + "rendered/inkscape-default-no-color.expected" + ))); // Raw render args.push("--raw"); @@ -756,5 +1434,65 @@ fn test_raw_render_file() { .args(&args) .assert() .success() - .stdout(diff(include_str!("inkscape-v1.md"))); + .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(); } diff --git a/tests/rendered/apt.ja.expected b/tests/rendered/apt.ja.expected new file mode 100644 index 0000000..efdd35d --- /dev/null +++ b/tests/rendered/apt.ja.expected @@ -0,0 +1,37 @@ + + 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 new file mode 100644 index 0000000..473bbe9 --- /dev/null +++ b/tests/rendered/inkscape-compact-no-color.expected @@ -0,0 +1,32 @@ + + An SVG (Scalable Vector Graphics) editing program. + Use -z to not open the GUI and only process files in the console. + + Open an SVG file in the Inkscape GUI: + + inkscape filename.svg + + Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI): + + inkscape filename.svg -e filename.png + + Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur): + + inkscape filename.svg -e filename.png -w 600 -h 400 + + Export a single object, given its ID, into a bitmap: + + inkscape filename.svg -i id -e object.png + + Export an SVG document to PDF, converting all texts to paths: + + inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path + + Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: + + inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit + + Some invalid command just to test the correct highlighting of the command name: + + inkscape --use-inkscape=v3.0 file + diff --git a/tests/inkscape-default-no-color.expected b/tests/rendered/inkscape-default-no-color.expected similarity index 100% rename from tests/inkscape-default-no-color.expected rename to tests/rendered/inkscape-default-no-color.expected diff --git a/tests/inkscape-default.expected b/tests/rendered/inkscape-default.expected similarity index 61% rename from tests/inkscape-default.expected rename to tests/rendered/inkscape-default.expected index da909f2..3b37f0e 100644 --- a/tests/inkscape-default.expected +++ b/tests/rendered/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/inkscape-patched-no-color.expected b/tests/rendered/inkscape-patched-no-color.expected similarity index 100% rename from tests/inkscape-patched-no-color.expected rename to tests/rendered/inkscape-patched-no-color.expected diff --git a/tests/inkscape-with-config.expected b/tests/rendered/inkscape-with-config.expected similarity index 53% rename from tests/inkscape-with-config.expected rename to tests/rendered/inkscape-with-config.expected index e79b219..33540a3 100644 --- a/tests/inkscape-with-config.expected +++ b/tests/rendered/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/rendered/inkscape-with-title-no-color.expected b/tests/rendered/inkscape-with-title-no-color.expected new file mode 100644 index 0000000..b6a4572 --- /dev/null +++ b/tests/rendered/inkscape-with-title-no-color.expected @@ -0,0 +1,34 @@ + + 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 new file mode 100644 index 0000000..ff2de4a --- /dev/null +++ b/tests/rendered/inkscape-with-title.expected @@ -0,0 +1,34 @@ + + 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/config.toml b/tests/style-config.toml similarity index 73% rename from tests/config.toml rename to tests/style-config.toml index 3b90d90..c68f2cc 100644 --- a/tests/config.toml +++ b/tests/style-config.toml @@ -19,11 +19,3 @@ underline = false underline = true bold = false italic = true - -[display] -use_pager = false -compact = false - -[updates] -auto_update = false -auto_update_interval_hours = 720