diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8ac6b8c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 160d9ae..faf3111 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,90 +1,90 @@ +name: CI on: push: branches: - - master + - main + - "v*.x" pull_request: schedule: - cron: '30 3 * * 2' - -name: CI + workflow_dispatch: jobs: - test: name: run tests strategy: matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - rust: [1.54, stable] + toolchain: [stable, 1.87.0] # MSRV + include: + - platform: windows-latest + exe_suffix: .exe runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@master with: - toolchain: ${{ matrix.rust }} - override: true + toolchain: ${{ matrix.toolchain }} + - run: mkdir artifacts - name: Build with default features - uses: actions-rs/cargo@v1 + run: | + cargo build + cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-default${{ matrix.exe_suffix}} + - name: Build with logging and Rustls with webpki roots + run: | + cargo build --features logging,rustls-with-webpki-roots --no-default-features + cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-logging-rustls-webpki${{ matrix.exe_suffix}} + - name: Build with native TLS backend + run: | + # expects runners have the proper Native SSL library + cargo build --features native-tls --no-default-features + cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-native-tls${{ matrix.exe_suffix}} + - uses: actions/upload-artifact@v7 with: - command: build - - name: Build with all features - uses: actions-rs/cargo@v1 - with: - command: build - args: --all-features + name: tldr-debug-build-${{ matrix.platform }}-rust-${{ matrix.toolchain }} + path: artifacts/ - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-features + run: cargo test -- --test-threads 1 clippy: name: run clippy lints runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - components: clippy - override: true - - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: clippy + - name: run clippy lints + run: cargo clippy --all-targets --features logging fmt: name: run rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: 1.54 - override: true - - run: rustup component add rustfmt - - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: rustfmt + - name: run rustfmt + run: cargo fmt --all -- --check docs: name: build docs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v7 - name: Setup mdBook - uses: peaceiris/actions-mdbook@v1 + uses: peaceiris/actions-mdbook@v2 with: mdbook-version: '0.4.4' - - uses: actions-rs/toolchain@v1 + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master with: - toolchain: stable - override: true + toolchain: stable - name: Build - uses: actions-rs/cargo@v1 - with: - command: build + run: cargo build - name: Ensure that docs can be built run: cd docs && mdbook build - name: Generate usage string diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 35f17b3..ae9ae93 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -1,25 +1,25 @@ -name: github pages - +name: GitHub Pages on: push: - branches: - - master + 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-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v7 - name: Setup mdBook - uses: peaceiris/actions-mdbook@v1 + uses: peaceiris/actions-mdbook@v2 with: mdbook-version: '0.4.4' - run: cd docs && mdbook build - name: Deploy - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs/book diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9337d6f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,160 @@ +name: Release +on: + push: + tags: + - "v*" # push events to matching v*, i.e. v1.0, v20.15.10 + +jobs: + create-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Create release for tag + if: startsWith(github.ref, 'refs/tags/') + run: | + source ./scripts/upload-asset.sh + # Create: + create_release ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} "Tealdeer version ${GITHUB_REF#refs/*/v}.\n\nFor the full changelog, see https://github.com/tealdeer-rs/tealdeer/blob/main/CHANGELOG.md.\n\nBinaries were generated automatically in CI, and are therefore unsigned. For a fully trusted release, please build from source." + + upload-completions: + needs: + - create-release + runs-on: ubuntu-latest + strategy: + matrix: + target: ["bash", "fish", "zsh"] + steps: + - uses: actions/checkout@v7 + - name: Upload completion + if: startsWith(github.ref, 'refs/tags/') + run: | + source ./scripts/upload-asset.sh + # Upload: + upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} completion/${{ matrix.target }}_tealdeer completions_${{ matrix.target }} + + upload-license: + needs: + - create-release + runs-on: ubuntu-latest + strategy: + matrix: + target: ["MIT", "APACHE"] + steps: + - uses: actions/checkout@v7 + - name: Upload license + if: startsWith(github.ref, 'refs/tags/') + run: | + source ./scripts/upload-asset.sh + # Upload: + upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} LICENSE-${{ matrix.target }} LICENSE-${{ matrix.target }}.txt + + build-linux: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - arch: "x86_64" + libc: "musl" + - arch: "aarch64" + libc: "musl" + - arch: "i686" + libc: "musl" + - arch: "armv7" + libc: "musleabihf" + - arch: "arm" + libc: "musleabi" + - arch: "arm" + libc: "musleabihf" + steps: + - uses: actions/checkout@v7 + - name: Pull Docker image + run: docker pull messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} + - name: Build in Docker + run: docker run --rm -i -v "$(pwd)":/home/rust/src messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} cargo build --release + - name: Strip binary + run: docker run --rm -i -v "$(pwd)":/home/rust/src messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} musl-strip -s /home/rust/src/target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr + - uses: actions/upload-artifact@v7 + with: + name: "tealdeer-linux-${{ matrix.arch }}-${{ matrix.libc }}" + path: "target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr" + + build-macos: + runs-on: macos-latest + strategy: + matrix: + include: + - arch: "x86_64" + - arch: "aarch64" + steps: + - uses: actions/checkout@v7 + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + targets: "${{ matrix.arch }}-apple-darwin" + - name: Build + run: cargo build --release --target ${{ matrix.arch }}-apple-darwin + - uses: actions/upload-artifact@v7 + with: + name: "tealdeer-macos-${{ matrix.arch }}" + path: "target/${{ matrix.arch }}-apple-darwin/release/tldr" + + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v7 + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + - name: Build + run: cargo build --release --target x86_64-pc-windows-msvc + - uses: actions/upload-artifact@v7 + with: + name: "tealdeer-windows-x86_64-msvc" + path: "target/x86_64-pc-windows-msvc/release/tldr.exe" + + upload-release: + needs: + - create-release + - build-linux + - build-macos + - build-windows + runs-on: ubuntu-latest + strategy: + matrix: + target: + - linux-x86_64-musl + - linux-aarch64-musl + - linux-i686-musl + - linux-armv7-musleabihf + - linux-arm-musleabi + - linux-arm-musleabihf + - macos-x86_64 + - macos-aarch64 + - windows-x86_64-msvc + steps: + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 + - name: Upload binary + if: startsWith(github.ref, 'refs/tags/') + run: | + source ./scripts/upload-asset.sh + + # Move/rename file + mkdir out && cd out + if [[ "${{ matrix.target }}" == *windows* ]]; then + src="../tealdeer-${{ matrix.target }}/tldr.exe" + filename="tealdeer-${{ matrix.target }}.exe" + else + src="../tealdeer-${{ matrix.target }}/tldr" + filename="tealdeer-${{ matrix.target }}" + fi + cp $src $filename + + # Create checksum + sha256sum "$filename" > "$filename.sha256" + + # Upload: + upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} $filename $filename + upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} $filename.sha256 $filename.sha256 diff --git a/CHANGELOG.md b/CHANGELOG.md index 14718ee..644a30a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,16 +10,403 @@ Possible log types: - `[removed]` for deprecated features removed in this release. - `[fixed]` for any bug fixes. - `[security]` to invite users to upgrade in case of vulnerabilities. +- `[docs]` for documentation changes. +- `[chore]` for maintenance work. + +### [v1.5.1][v1.5.1], [v1.6.2][v1.6.2], [v1.7.3][v1.7.3] (2026-01-25) + +Today I am releasing three patch updates for outdated versions of tealdeer. +They are minimal patches for Linux distributions that ship old versions of +tealdeer which recently broke due to an upstream change. If you can choose +freely which version of tealdeer to use, I recommend using the latest version of +tealdeer, 1.8.1. For more details, see the "Notes to package maintainers" +section below. + +All three updates contain only a single change compared to their respective +previous versions which changes the `ARCHIVE_URL` constant used for updating the +page cache. The reason for this change is that the upstream tldr-pages +repository shut down the domain that clients were previously required to use. + +Note that this issue is already fixed in tealdeer 1.8.0 where we introduced a +config file option for changing the URL used at runtime. The versions 1.8.0 and +1.8.1 also use the new domain of the tldr-pages archive by default, so no action +is needed for users of those versions. + +#### Changes + +- [fixed] Update `ARCHIVE_URL` + +#### Notes to package maintainers + +I have _not_ updated the lockfile for any of these releases, so the locked +dependency versions are still the same as they were for the previous release in +the respective v1.x series. Updating the lockfile for tealdeer 1.5.0 to remove +any `cargo audit` warnings while also maintaining compatibility with Rust 1.54 +also brings larger changes through transitive dependencies, which contradicts my +plan to make this update easy to plug into existing build pipelines. + +If you want to build / distribute tealdeer v1.5.1, v1.6.2, or v1.7.3, please use +an up to date Rust toolchain to permit updates to newer versions of (transitive) +dependencies. Do not use the lockfile, instead update to the newest available +dependency versions. + +For the same reason, there are no artifacts attached to the GitHub releases of +these versions. + +### [v1.8.1][v1.8.1] (2025-11-11) + +This patch release tweaks the enabled features for ureq, the library we use to +perform HTTP requests when updating the cache. In particular, support for socks +proxies is now enabled. + +#### Changes: + +- [added] Enable ureq's socks-proxy feature ([#451]) + +### [v1.8.0][v1.8.0] (2025-10-03) + +One year and one day have passed since tealdeer version 1.7.0 was released, so +it's time for an update! Tealdeer 1.8 comes with a complete rewrite of the page +cache and contains many long awaited improvements around it. + +Firstly, tealdeer now supports language-specific downloads. This means that only +the pages matching the configured languages are downloaded when updating the +cache. The languages used for searching pages can be configured separately to +the ones used for updating, so it is possible to download pages in languages +that are not usually queried. + +Next to configuring which languages are used for searching, it is now also +possible to specify which platforms are used in the config file. Importantly, +the default behavior for page search has changed so that all platforms are +searched if no page is found for the platform that tealdeer is running on. To +restore the behavior of tealdeer 1.7, users should set +```toml +[search] +platforms = ["current", "common"] +``` +in their config file. + +Coming back to updating, the default build configuration of tealdeer now +includes multiple TLS backends. This means that tealdeer does not have to be +rebuilt to try out a different TLS backend. The used backend can be chosen in +the config file. By default, tealdeer comes with support for rustls using webpki +certificates or system certificates. Native TLS is supported, but not enabled by +default to avoid build troubles with OpenSSL and musl. + +For details, please refer to the [user documentation]. + +#### Changes: + +- [added] Resolve paths in config `[directories]` relative to the config directory ([#306]) +- [added] Add `common` platform to CLI ([#401]) +- [added] Add configuration option for `archive_source` ([#337]) +- [added] Allows configuring TLS backend ([#386]) +- [added] Add args: `--edit-page` and `--edit-patch` ([#388]) +- [added] Add an option to specify a custom config file to be used ([#422]) +- [added] Upload binaries from build step as artifact ([#423]) +- [added] Add `search.languages` and `updates.download_languages` settings ([#430]) +- [added] Add `search.platforms` config option and search all platforms by default ([#435]) +- [added] Add `display.show_title` option to display command titles in output ([#439]) +- [chore] Various test improvements ([#399]) +- [chore] Add tests for osx/macos alias ([#407]) +- [chore] Move most of `main` to `try_main` ([#400]) +- [chore] Only create a single temporary directory in integration tests ([#411]) +- [chore] Replace reqwest with ureq ([#417]) +- [chore] Introduce Language struct ([#425]) +- [chore] Cache rewrite ([#416]) +- [chore] Allow references in `Config` ([#429]) +- [docs] Highlight code examples in user docs ([#440]) +- [removed] Remove native-tls from default feature set ([#436]) + +#### Contributors to this version: + +- [Christoph Loy][@beatbrot] +- [Erick Guan][@erickguan] +- [@MHS-0][@MHS-0] +- [Matěj Kafka][@MatejKafka] +- [Nachiket Kanore][@nachiketkanore] +- [Niklas Mohrin][@niklasmohrin] +- [Predrag Minic][@mipedja] +- [@hex1c][@hex1c] +- [lyj][@lengyijun] + +Thanks! + +#### Notes to package maintainers + +1. The MSRV has been bumped to 1.85. +2. Consider whether you want to include the `native-tls` feature in your build + of tealdeer. The feature is disabled for the binaries in the GitHub release + because we target musl, but it might work out of the box for your + distribution. +3. We have added the `ignore-online-tests` feature to automatically mark all + tests that require an internet connection as skipped, so you can use this + feature instead of maintaining a list of these tests yourself. + +### [v1.7.2][v1.7.2] (2025-03-18) + +This patch release updates the `zip` dependency to mitigate a potential security +vulnerability. A successful attack against tealdeer users would require +manipulation of the tldr pages archive downloaded during an update. As the +archive is downloaded from a trusted source (the tldr-pages organization), it +seems very unlikely that running a version of tealdeer prior to 1.7.2 poses a +security risk. Nevertheless, it cannot hurt to rule out any chance of an attack +by updating tealdeer to version 1.7.2. + +For more details, please see https://github.com/advisories/GHSA-94vh-gphv-8pm8. + +- [security] Require `zip >= 2.3.0` +- [chore] Run CI on backport branches and on dispatch + +### [v1.7.1][v1.7.1] (2024-11-14) + +This patch release updates the `yansi` dependency to version 1, so that the +previous versions of `yansi` can be removed from the package sets of Linux +distributions. This change should not impact the behavior of tealdeer. + +#### Changes: + +- [chore] Upgrade yansi: 0.5.1 -> 1.0.1 ([#389]) + +#### Contributors to this version: + +- [Blair Noctis][@nc7s] + +Thanks! + +### [v1.7.0][v1.7.0] (2024-10-02) + +It's been 24 months since the last release, time for tealdeer 1.7.0! Thanks to +16 individual contributors, a few nice changes and features are included in +this release. + +One change is that you can **query multiple platforms at once**. For example: + + tldr --platform openbsd --platform linux df + +This will show the `df` page for OpenBSD (if available), followed by Linux (if +available), with fallback to the current platform on which tealdeer runs. + +What's that `openbsd` thing up there? Yes, there's now **support for the BSD +platforms `freebsd`, `netbsd` and `openbsd`**. + +And since we're already talking about platform support: Our **binary releases +now include builds for ARM64 (aka `aarch64`) on macOS (Apple Silicon, M1/M2/M3) +and Linux**. _(Keep in mind that binary releases are generated in CI and are +unsigned. For a trusted build, please compile from source.)_ + +There's also a breaking change for the folks using [custom pages and +patches](https://tealdeer-rs.github.io/tealdeer/usage_custom_pages.html): These +files now use a `.md` extension. Old files will continue to work, but will +result a deprecation warning being printed when used. + +On a personal note, this will be the last release from me +([Danilo](https://github.com/dbrgn/)) as primary maintainer of tealdeer. For +details, see [#376](https://github.com/tealdeer-rs/tealdeer/issues/376). + +#### Changes: + +- [added] Allow querying multiple platforms ([#300]) +- [added] Add BSD platform support ([#354]) +- [added] Allow building with native-tls in addition to rustls ([#303]) +- [changed] Change custom page files to use a `.md` file extension ([#322]) +- [changed] Update to clap v4 for doing command line parsing ([#298]) +- [changed] Performance optimization in LineIterator ([#314]) +- [changed] Performance optimizations by tweaking Cargo flags ([#355]) +- [changed] Include completions in published crate ([#333]) +- [changed] Minimal supported Rust version is now 1.75 ([#298]) +- [fixed] Fix bash/zsh/fish completions when cache is empty ([#327], [#331]) +- [docs] Publish docs only when tagging a release ([#362]) +- [docs] List Scoop and Debian packages ([#305], [#315]) +- [docs] Add "Tips and Tricks" chapter to user manual ([#342]) +- [docs] Various docs improvements ([#293]) +- [chore] Improvements to CI workflows ([#324]) +- [chore] Update Cargo.toml license field following SPDX 2.1 ([#336]) +- [chore] Dependency updates + +#### Contributors to this version: + +- [Adam Henley][@adamazing] +- [Andrea Frigido][@frisoft] +- [Blair Noctis][@nc7s] +- [Danilo Bargen][@dbrgn] +- [Felix Yan][@felixonmars] +- [Iliia Maleki][@iliya-malecki] +- [JJ Style][@jj-style] +- [K.B.Dharun Krishna][@kbdharun] +- [Linus Walker][@Walker-00] +- [Mohit Raj][@agrmohit] +- [Nicolai Fröhlich][@nifr] +- [Niklas Mohrin][@niklasmohrin] +- [@qknogxxb][@qknogxxb] +- [@tveness][@tveness] +- [Y.D.X.][@YDX-2147483647] +- [Zacchary Dempsey-Plante][@zedseven] + +Thanks! + + +### [v1.6.1][v1.6.1] (2022-10-24) + +#### Changes: + +- [fixed] Fix path source for custom pages dir ([#297]) +- [chore] Update dependendencies ([#299]) + +#### Contributors to this version: + +- [Cyrus Yip][@CyrusYip] +- [Danilo Bargen][@dbrgn] + +Thanks! + + +### [v1.6.0][v1.6.0] (2022-10-02) + +It's been 9 months since the last release already! This is not a huge update +feature-wise, but it still contains a few nice new improvements and a few +bugfixes, contributed by 11 different people. The most important new feature is +probably the option to override the cache directory through the config file. +The `TEALDEER_CACHE_DIR` env variable is now deprecated. + +A note to packagers: Shell completions have been moved to the `completion/` +subdirectory! Packaging scripts might need to be updated. + +#### Changes: + +- [added] Allow overriding cache directory through config ([#276]) +- [added] Add `--no-auto-update` CLI flag ([#257]) +- [added] Show note about auto-updates when cache is missing ([#254]) +- [added] Add support for android platform ([#274]) +- [added] Add custom pages to list output ([#285]) +- [fixed] Cache: Return error if HTTP client cannot be created ([#247]) +- [fixed] Handle cache download errors ([#253]) +- [fixed] Do not page output of `tldr --update` ([#231]) +- [fixed] Create macOS release builds with bundled root certificates ([#272]) +- [fixed] Clean up and fix shell completions ([#262]) +- [deprecated] The `TEALDEER_CACHE_DIR` env variable is now deprecated ([#276]) +- [removed] The `--config-path` command was removed, use `--show-paths` instead ([#290]) +- [removed] The `-o/--os` command was removed, use `-p/--platform` instead ([#290]) +- [removed] The `-m/--markdown` command was removed, use `-r/--raw` instead ([#290]) +- [chore] Move shell completion scripts to their own directory ([#259]) +- [chore] Update dependencies ([#271], [#287], [#291]) +- [chore] Use anyhow for error handling ([#249]) +- [chore] Switch to Rust 2021 edition ([#284]) + +#### Contributors to this version: + +- [@bagohart][@bagohart] +- [@cyqsimon][@cyqsimon] +- [Danilo Bargen][@dbrgn] +- [Danny Mösch][@SimplyDanny] +- [Evan Lloyd New-Schmidt][@newsch] +- [Hans Gaiser][@hgaiser] +- [Kian-Meng Ang][@kianmeng] +- [Marcin Puc][@tranzystorek-io] +- [Niklas Mohrin][@niklasmohrin] +- [Olav de Haas][@Olavhaasie] +- [Simon Perdrisat][@gagarine] + +Thanks! + + +### [v1.5.0][v1.5.0] (2021-12-31) + +This is quite a big release with many new features. In the 15 months since the +last release, 59 pull requests from 16 different contributors were merged! + +The highlights: + +- **Custom pages and patches**: You can now create your own local-only tldr + pages. But not just that, you can also extend existing upstream pages with + your own examples. For more details, see + [the docs](https://tealdeer-rs.github.io/tealdeer/usage_custom_pages.html). +- **Change argument parsing from docopt to clap**: We replaced docopt.rs as + argument parsing library with clap v3, resulting in almost 1 MiB smaller + binaries and a 22% speed increase when rendering a tldr page. +- **Multi-language support**: You can now override the language with `-L/--language`. +- **A new `--show-paths` command**: By running `tldr --show-paths`, you can list + the currently used config dir, cache dir, upstream pages dir and custom pages dir. +- **Compliance with the tldr client spec v1.5**: We renamed `-o/--os` to + `-p/--platform` and implemented transparent lowercasing of the page names. +- **Docs**: The README based documentation has reached its limits. There are + now new mdbook based docs over at + [tealdeer-rs.github.io/tealdeer/](https://tealdeer-rs.github.io/tealdeer/), we hope these + make using tealdeer easier. Of course, documentation improvements are + welcome! Also, if you're confused about how to use a certain feature, feel + free to open an issue, this way we can improve the docs. + +Note that the MSRV (Minimal Supported Rust Version) of the project +[changed][i190]: + +> When publishing a tealdeer release, the Rust version required to build it +> should be stable for at least a month. + +#### Changes: + +- [added] Support custom pages and patches ([#142][i142]) +- [added] Multi-language support ([#125][i125], [#161][i161]) +- [added] Add support for ANSI code and RGB colors ([#148][i148]) +- [added] Implement new `--show-paths` command ([#162][i162]) +- [added] Support for italic text styling ([#197][i197]) +- [added] Allow SunOS platform override ([#176][i176]) +- [added] Automatically lowercase page names before lookup ([#227][i227]) +- [added] Add "macos" alias for "osx" ([#215][i215]) +- [fixed] Consider only standalone command names for styling ([#157][i157]) +- [fixed] Fixed and improved zsh completions ([#168][i168]) +- [fixed] Create cache directory path if it does not exist ([#174][i174]) +- [fixed] Use default style if user-defined style is missing ([#210][i210]) +- [changed] Switch from docopt to clap for argument parsing ([#108][i108]) +- [changed] Switch from OpenSSL to Rustls ([#187][i187]) +- [changed] Performance improvements ([#187][i187]) +- [changed] Send all progress logging messages to stderr ([#171][i171]) +- [changed] Rename `-o/--os` to `-p/--platform` ([#217][i217]) +- [changed] Rename `-m/--markdown` to `-r/--raw` ([#108][i108]) +- [deprecated] The `--config-path` command is deprecated, use `--show-paths` instead ([#162][i162]) +- [deprecated] The `-o/--os` command is deprecated, use `-p/--platform` instead ([#217][i217]) +- [deprecated] The `-m/--markdown` command is deprecated, use `-r/--raw` instead ([#108][i108]) +- [docs] New docs at [tealdeer-rs.github.io/tealdeer/](https://tealdeer-rs.github.io/tealdeer/) +- [docs] Add comparative benchmarks with hyperfine ([#163][i163], [README](https://github.com/tealdeer-rs/tealdeer#goals)) +- [chore] Download tldr pages archive from their website, not from GitHub ([#213][i213]) +- [chore] Bump MSRV to 1.54 and change MSRV policy ([#190][i190]) +- [chore] The `master` branch was renamed to `main` +- [chore] All release binaries are now generated in CI. Binaries for macOS and Windows are also provided. ([#240][i240]) +- [chore] Update all dependencies + +#### Contributors to this version: + +- [@bl-ue][@bl-ue] +- [Cameron Tod][@cam8001] +- [Dalton][@dmaahs2017] +- [Danilo Bargen][@dbrgn] +- [Danny Mösch][@SimplyDanny] +- [Marcin Puc][@tranzystorek-io] +- [Michael Cho][@cho-m] +- [MS_Y][@black7375] +- [Niklas Mohrin][@niklasmohrin] +- [Rithvik Vibhu][@rithvikvibhu] +- [rnd][@0ndorio] +- [Sondre Nilsen][@sondr3] +- [Tomás Farías Santana][@tomasfarias] +- [Tsvetomir Bonev][@invakid404] +- [@tveness][@tveness] +- [ギャラ][@laxect] + +Thanks! + +Last but not least, [Niklas Mohrin][@niklasmohrin] has joined the project as +co-maintainer. Thank you for your help! ### [v1.4.1][v1.4.1] (2020-09-04) - [fixed] Syntax error in zsh completion file ([#138][i138]) -Contributors to this version: +#### Contributors to this version: -- [Francesco][@BachoSeven] +- [Danilo Bargen][@dbrgn] - [Bruno A. Muciño][@mucinoab] +- [Francesco][@BachoSeven] Thanks! @@ -32,9 +419,10 @@ Thanks! - [changed] Make `--list` option comply with official spec ([#112][i112]) - [changed] Move cache age warning to stderr ([#113][i113]) -Contributors to this version: +#### Contributors to this version: - [Atul Bhosale][@Atul9] +- [Danilo Bargen][@dbrgn] - [Danny Mösch][@SimplyDanny] - [Ilaï Deutel][@ilai-deutel] - [Kornel][@kornelski] @@ -57,15 +445,16 @@ 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: -- [@Calinou][@Calinou] -- [@Delapouite][@Delapouite] -- [@james2doyle][@james2doyle] -- [@jesdazrez][@jesdazrez] +- [Bruno Heridet][@Delapouite] +- [Danilo Bargen][@dbrgn] +- [Hugo Locurcio][@Calinou] +- [Isak Johansson][@Plommonsorbet] +- [James Doyle][@james2doyle] +- [Jesús Trinidad Díaz Ramírez][@jesdazrez] - [@korrat][@korrat] -- [@ma-renaud][@ma-renaud] -- [@Plommonsorbet][@Plommonsorbet] +- [Marc-André Renaud][@ma-renaud] Thanks! @@ -82,16 +471,17 @@ 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: -- [@aldanor][@aldanor] -- [@Bassets][@Bassets] -- [@das-g][@das-g] -- [@jcgruenhage][@jcgruenhage] -- [@jdvr][@jdvr] -- [@jedahan][@jedahan] -- [@mystal][@mystal] -- [@natpen][@natpen] +- [Bar Hatsor][@Bassets] +- [Danilo Bargen][@dbrgn] +- [Gabriel Martinez][@mystal] +- [Ivan Smirnov][@aldanor] +- [Jan Christian Grünhage][@jcgruenhage] +- [Jonathan Dahan][@jedahan] +- [Juan D. Vega][@jdvr] +- [Natalie Pendragon][@natpen] +- [Raphael Das Gupta][@das-g] Thanks! @@ -104,8 +494,9 @@ Thanks! - [changed] Require at least Rust 1.28 to build (previous: 1.19) - [fixed] Fix building on systems with openssl 1.1.1 ([#47][i47]) -Contributors to this version: +#### Contributors to this version: +- [Danilo Bargen][@dbrgn] - [@equal-l2][@equal-l2] - [Jonathan Dahan][@jedahan] - [Lukas Bergdoll][@Voultapher] @@ -136,63 +527,198 @@ Thanks! - First crates.io release +[user documentation]: https://tealdeer-rs.github.io/tealdeer/ +[@0ndorio]: https://github.com/0ndorio +[@adamazing]: https://github.com/adamazing +[@agrmohit]: https://github.com/agrmohit [@aldanor]: https://github.com/aldanor [@Atul9]: https://github.com/Atul9 [@BachoSeven]: https://github.com/BachoSeven +[@bagohart]: https://github.com/bagohart [@Bassets]: https://github.com/Bassets +[@black7375]: https://github.com/black7375 +[@bl-ue]: https://github.com/bl-ue [@Calinou]: https://github.com/Calinou +[@cam8001]: https://github.com/cam8001 +[@cho-m]: https://github.com/cho-m +[@cyqsimon]: https://github.com/cyqsimon +[@CyrusYip]: https://github.com/CyrusYip [@das-g]: https://github.com/das-g +[@dbrgn]: https://github.com/dbrgn [@Delapouite]: https://github.com/Delapouite +[@dmaahs2017]: https://github.com/dmaahs2017 [@equal-l2]: https://github.com/equal-l2 +[@felixonmars]: https://github.com/felixonmars +[@frisoft]: https://github.com/frisoft +[@gagarine]: https://github.com/gagarine +[@hgaiser]: https://github.com/hgaiser [@ilai-deutel]: https://github.com/ilai-deutel +[@iliya-malecki]: https://github.com/iliya-malecki +[@invakid404]: https://github.com/invakid404 [@james2doyle]: https://github.com/james2doyle [@jcgruenhage]: https://github.com/jcgruenhage [@jdvr]: https://github.com/jdvr [@jedahan]: https://github.com/jedahan [@jesdazrez]: https://github.com/jesdazrez +[@jj-style]: https://github.com/jj-style +[@kbdharun]: https://github.com/kbdharun +[@kianmeng]: https://github.com/kianmeng [@kornelski]: https://github.com/kornelski [@korrat]: https://github.com/korrat +[@laxect]: https://github.com/laxect [@LovecraftianHorror]: https://github.com/LovecraftianHorror [@ma-renaud]: https://github.com/ma-renaud [@michaeldel]: https://github.com/michaeldel [@mucinoab]: https://github.com/mucinoab [@mystal]: https://github.com/mystal [@natpen]: https://github.com/natpen +[@nc7s]: https://github.com/nc7s +[@newsch]: https://github.com/newsch +[@nifr]: https://github.com/nifr [@niklasmohrin]: https://github.com/niklasmohrin +[@Olavhaasie]: https://github.com/Olavhaasie [@Plommonsorbet]: https://github.com/Plommonsorbet +[@qknogxxb]: https://github.com/qknogxxb +[@rithvikvibhu]: https://github.com/rithvikvibhu [@SimplyDanny]: https://github.com/SimplyDanny +[@sondr3]: https://github.com/sondr3 +[@tomasfarias]: https://github.com/tomasfarias +[@tranzystorek-io]: https://github.com/tranzystorek-io +[@tveness]: https://github.com/tveness [@Voultapher]: https://github.com/Voultapher +[@Walker-00]: https://github.com/Walker-00 +[@YDX-2147483647]: https://github.com/YDX-2147483647 +[@zedseven]: https://github.com/zedseven +[@beatbrot]: https://github.com/beatbrot +[@erickguan]: https://github.com/erickguan +[@MHS-0]: https://github.com/MHS-0 +[@MatejKafka]: https://github.com/MatejKafka +[@nachiketkanore]: https://github.com/nachiketkanore +[@mipedja]: https://github.com/mipedja +[@hex1c]: https://github.com/hex1c +[@lengyijun]: https://github.com/lengyijun -[v1.0.0]: https://github.com/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.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 -[i111]: https://github.com/dbrgn/tealdeer/issues/111 -[i112]: https://github.com/dbrgn/tealdeer/issues/112 -[i113]: https://github.com/dbrgn/tealdeer/issues/113 -[i115]: https://github.com/dbrgn/tealdeer/issues/115 -[i138]: https://github.com/dbrgn/tealdeer/issues/138 +[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 3585ebf..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,43 +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-beta.5" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feff3878564edb93745d58cf63e17b63f24142506e7a20c87a5521ed7bfb1d63" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ - "bitflags", + "clap_builder", "clap_derive", - "indexmap", - "lazy_static", - "os_str_bytes", - "strsim 0.10.0", - "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-beta.5" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b15c6b4f786ffb6192ffe65a36855bc1fc2444bcd0945ae16748dcd6ed7d0d3" +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", @@ -168,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", @@ -178,59 +251,34 @@ 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" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.9.3", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", @@ -243,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" @@ -270,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.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" +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" @@ -308,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" @@ -320,45 +359,65 @@ dependencies = [ [[package]] name = "escargot" -version = "0.5.6" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ead7d8a70259beb627c1ffdd19b0372381f247f88e46a3bd52bb797182690b3" +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", ] @@ -370,254 +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.18" +name = "foreign-types-shared" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27" +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.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" - -[[package]] -name = "futures-io" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11" - -[[package]] -name = "futures-sink" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af" - -[[package]] -name = "futures-task" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" - -[[package]] -name = "futures-util" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" -dependencies = [ - "futures-core", - "futures-io", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", + "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.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", + "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.5" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", "itoa", ] -[[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", -] - [[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.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" -dependencies = [ - "http", - "hyper", - "rustls", - "tokio", - "tokio-rustls", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] +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" - -[[package]] -name = "itertools" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" -dependencies = [ - "either", -] +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[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]] @@ -627,251 +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.109" +name = "linux-raw-sys" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98a04dce437184842841303488f70d0188c5f51437d2a834dc097eafa909a01" +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.0" +name = "once_cell" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +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.4" +name = "openssl-macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "derivative", - "num_enum_derive", -] - -[[package]] -name = "num_enum_derive" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" -dependencies = [ - "proc-macro-crate 1.1.0", "proc-macro2", "quote", "syn", ] -[[package]] -name = "once_cell" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" - [[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 = "4.2.0" +name = "openssl-sys" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addaa943333a514159c80c97ff4a93306530d965d27e139188283cd13e06a799" +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.7" +name = "pkg-config" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +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.15" +name = "portable-atomic-util" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" +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", @@ -879,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.32" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.10" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +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.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bea77bc708afa10e59905c3d4af7c8fd43c9214251673095ff8b14345fcbc5" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "hyper-rustls", - "ipnet", - "js-sys", - "lazy_static", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "rustls", - "rustls-native-certs", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-rustls", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", - "winreg", -] +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 0.22.0", + "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.6" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -1148,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", @@ -1181,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", @@ -1191,18 +1014,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.130" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +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.130" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1211,438 +1044,303 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.72" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "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", - "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.82" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] name = "tealdeer" -version = "1.4.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.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" -dependencies = [ - "autocfg", - "bytes", - "libc", - "memchr", - "mio", - "num_cpus", - "pin-project-lite", - "winapi", -] - -[[package]] -name = "tokio-rustls" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4baa378e417d780beff82bf54ceb0d195193ea6a00c14e22359e7f39456b5689" -dependencies = [ - "rustls", - "tokio", - "webpki 0.22.0", -] - -[[package]] -name = "tokio-util" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "log", - "pin-project-lite", - "tokio", -] - [[package]] name = "toml" -version = "0.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.3" +name = "ureq-proto" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +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.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", + "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "0.21.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ - "webpki 0.21.4", + "rustls-pki-types", ] [[package]] @@ -1663,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]] @@ -1677,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 ba56396..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.4.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-beta.5", features = ["std", "derive", "suggestions" ], 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 86c82a6..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/master/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%3Amaster -[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 28ccc28..f182519 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -7,13 +7,17 @@ Run linting: Set variables: $ export VERSION=X.Y.Z - $ export GPG_KEY=EA456E8BAF0109429583EED83578F667F2F3A5FA + $ export GPG_KEY=20EE002D778AE197EF7D0D2CB993FF98A90C9AB1 Update version numbers: $ vim Cargo.toml $ cargo update -p tealdeer +Update docs: + + $ cargo run -- --help > docs/src/usage.txt + Update changelog: $ vim CHANGELOG.md @@ -28,6 +32,4 @@ Publish: $ cargo publish $ git push && git push --tags -Create release binaries: - - $ ./release-build.sh +Then publish the release on GitHub. 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 6201678..4649382 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -4,7 +4,11 @@ - [Installing](./installing.md) - [Usage](./usage.md) + - [Custom Pages and Patches](./usage_custom_pages.md) - [Configuration](./config.md) - - [display](./config_display.md) - - [style](./config_style.md) - - [updates](./config_updates.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 79afcdf..a662b57 100644 --- a/docs/src/config.md +++ b/docs/src/config.md @@ -1,37 +1,39 @@ # Configuration -Tealdeer can be customized with a config file called `config.toml`. Creating -the config file can be done manually or with the help of `tldr`: +Tealdeer can be customized with a config file in [TOML +format](https://toml.io/) called `config.toml`. - $ tldr --seed-config +## Configfile Path -The configuration file path follows OS conventions. It can be queried with the -following command: +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`: + +```shell +$ tldr --seed-config +``` On Linux, this will usually be `~/.config/tealdeer/config.toml`. -## Override Config Directory - -The directory where the configuration file resides may be overwritten by the -environment variable `TEALDEER_CONFIG_DIR`. Remember to use an absolute path. -Variable expansion will not be performed on the path. - -## Override Cache Directory - -Similarly, the cache directory where the pages are downloaded to, also follows -OS conventions. On Linux, it will usually be at `~/.cache/tealdeer/`. The path -can be overwritten using the environment variable `TEALDEER_CACHE_DIR`. -Remember to use an absolute path. Variable expansion will not be performed on -the path. - ## Config Example +Here's an example configuration file. Note that this example does not contain +all possible config options. For details on the things that can be configured, +please refer to the subsections of this documentation page +([display](config_display.html), [style](config_style.html), [search](config_search.html), +[updates](config_updates.html) or [directories](config_directories.html)). + ```toml [display] compact = false use_pager = true +show_title = false [style.command_name] foreground = "red" @@ -49,3 +51,9 @@ underline = true [updates] auto_update = true ``` + +## Override Config Directory + +The directory where the configuration file resides may be overwritten by the +environment variable `TEALDEER_CONFIG_DIR`. Remember to use an absolute path. +Variable expansion will not be performed on the path. diff --git a/docs/src/config_directories.md b/docs/src/config_directories.md new file mode 100644 index 0000000..507bd27 --- /dev/null +++ b/docs/src/config_directories.md @@ -0,0 +1,29 @@ +# Section: \[directories\] + +This section allows overriding some directory paths. + +## `cache_dir` + +Override the cache directory. Remember to use an absolute path. Variable +expansion will not be performed on the path. If the directory does not yet +exist, it will be created. + +```toml +[directories] +cache_dir = "/home/myuser/.tealdeer-cache/" +``` + +If no `cache_dir` is specified, tealdeer will fall back to a location that +follows OS conventions. On Linux, it will usually be at `~/.cache/tealdeer/`. +Use `tldr --show-paths` to show the path that is being used. + +## `custom_pages_dir` + +Set the directory to be used to look up [custom +pages](usage_custom_pages.html). Remember to use an absolute path. Variable +expansion will not be performed on the path. + +```toml +[directories] +custom_pages_dir = "/home/myuser/custom-tldr-pages/" +``` diff --git a/docs/src/config_display.md b/docs/src/config_display.md index 6d16805..007d64b 100644 --- a/docs/src/config_display.md +++ b/docs/src/config_display.md @@ -1,4 +1,4 @@ -# display +# Section: \[display\] In the `display` section you can configure the output format. @@ -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 38709ee..593a5b5 100644 --- a/docs/src/config_style.md +++ b/docs/src/config_style.md @@ -1,4 +1,4 @@ -# style +# Section: \[style\] Using the config file, the style (e.g. colors or underlines) can be customized. @@ -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 ac9a59b..a8a10c8 100644 --- a/docs/src/config_updates.md +++ b/docs/src/config_updates.md @@ -1,8 +1,10 @@ -# updates +# 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 66c45c3..6a04de7 100644 --- a/docs/src/usage.txt +++ b/docs/src/usage.txt @@ -1,32 +1,33 @@ -tealdeer 1.4.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]... +Arguments: + [COMMAND]... The command to show (e.g. `tar` or `git log`) -ARGS: - ... The command to show (e.g. `tar` or `git log`) +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 -OPTIONS: - -l, --list List all commands in the cache - -f, --render Render a specific markdown file - -p, --platform Override the operating system [possible values: linux, macos, - windows, sunos, all] - -o, --os Deprecated alias of `platform` - -L, --language Override the language - -u, --update Update the local cache - -c, --clear-cache Clear the local cache - --pager Use a pager to page output - -r, --raw Display the raw markdown instead of rendering it - -q, --quiet Suppress informational messages - --show-paths Show file and directory paths used by tealdeer - --config-path Show config file path - --seed-config Create a basic config - --color Control whether to use color [possible values: always, auto, never] - -v, --version Print the version - -h, --help Print help information +To view the user documentation, please visit https://tealdeer-rs.github.io/tealdeer/. -To view the user documentation, please visit https://dbrgn.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 new file mode 100644 index 0000000..c73bf90 --- /dev/null +++ b/docs/src/usage_custom_pages.md @@ -0,0 +1,58 @@ +# Custom Pages and Patches + +> ⚠️ **Breaking change in version 1.7.0:** The file name extension for custom +> pages and patches was changed: +> +> - `.page` → `.page.md` +> - `.patch` → `.patch.md` +> +> If you have custom pages or patches, you need to rename them. + +Tealdeer allows creating new custom pages, overriding existing pages as well as +extending existing pages. + +The directory, where these custom pages and patches can be placed, follows OS +conventions. On Linux for instance, the default location is +`~/.local/share/tealdeer/pages/`. To print the path used on your system, simply +run `tldr --show-paths`. + +The custom pages directory can be [overridden by the config +file](config_directories.html). + +## Custom Pages + +To document internal command line tools, or if you want to replace an existing +tldr page with one that's better suited for you, place a file with the name +`.page.md` in the custom pages directory. When calling `tldr `, +your custom page will be shown instead of the upstream version in the cache. + +Path: + +```plain +$CUSTOM_PAGES_DIR/.page.md +``` + +Example: + +```plain +~/.local/share/tealdeer/pages/ufw.page.md +``` + +## Custom Patches + +Sometimes you don't want to fully replace an existing upstream page, but just +want to extend it with your own examples that you frequently need. In this +case, use a file called `.patch.md`, it will be appended to existing +pages. + +Path: + +```plain +$CUSTOM_PAGES_DIR/.patch.md +``` + +Example: + +```plain +~/.local/share/tealdeer/pages/ufw.patch.md +``` diff --git a/fish_tealdeer b/fish_tealdeer 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/release-build.sh b/release-build.sh deleted file mode 100755 index 26567a4..0000000 --- a/release-build.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -VERSION=$(grep '^version = ' Cargo.toml | sed 's/.*"\([0-9\.]*\)".*/\1/') -GPG_KEY=EA456E8BAF0109429583EED83578F667F2F3A5FA - -declare -a targets=( - "x86_64-musl" - "i686-musl" - "armv7-musleabihf" - "arm-musleabi" - "arm-musleabihf" -) - -declare -a rusttargets=( - "x86_64-unknown-linux-musl" - "i686-unknown-linux-musl" - "armv7-unknown-linux-musleabihf" - "arm-unknown-linux-musleabi" - "arm-unknown-linux-musleabihf" -) - -declare -a completions=( - "bash" - "fish" - "zsh" -) - -function docker-download { - echo "==> Downloading Docker image: messense/rust-musl-cross:$1" - docker pull messense/rust-musl-cross:$1 -} - -function docker-build { - echo "==> Building target: $1" - docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:$1 cargo build --release -} - -echo -e "==> Version $VERSION\n" - -for target in ${targets[@]}; do docker-download $target; done -echo "" -for target in ${targets[@]}; do docker-build $target; done -echo "" - -rm -rf "dist-$VERSION" -mkdir "dist-$VERSION" - -for i in ${!targets[@]}; do - echo "==> Copying ${targets[$i]}" - cp "target/${rusttargets[$i]}/release/tldr" "dist-$VERSION/tldr-linux-${targets[$i]}" -done -echo "" - -for target in ${targets[@]}; do - echo "==> Stripping $target" - docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:$target musl-strip -s /home/rust/src/dist-$VERSION/tldr-linux-$target -done -echo "" - -for target in ${targets[@]}; do - echo "==> Signing $target" - gpg -a --output "dist-$VERSION/tldr-linux-$target.sig" --detach-sig "dist-$VERSION/tldr-linux-$target" -done -echo "" - -for completion in ${completions[@]}; do - echo "==> Copying ${completion} completion" - cp "${completion}_tealdeer" "dist-$VERSION/completions_${completion}" -done -echo "" - -echo "==> Copying licenses" -cp LICENSE-* "dist-$VERSION/" - -echo "Done." diff --git a/rustfmt.toml b/rustfmt.toml 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 new file mode 100644 index 0000000..4f9de09 --- /dev/null +++ b/scripts/upload-asset.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# +# Upload artifacts to GitHub Actions. +# +# Based on: https://gist.github.com/schell/2fe896953b6728cc3c5d8d5f9f3a17a3 +# +# Requires curl and jq on PATH + +# Args: +# token: GitHub API user token +# repo: GitHub username/reponame +# tag: Name of the tag for which to create a release +# description: Release description +create_release() { + # Args + token=$1 + repo=$2 + tag=$3 + description=$4 + echo "Creating release:" + echo " repo=$repo" + echo " tag=$tag" + echo "" + + # Create release + http_code=$( + curl -s -o create.json -w '%{http_code}' \ + --header "Accept: application/vnd.github.v3+json" \ + --header "Authorization: Bearer $token" \ + --header "Content-Type:application/json" \ + "https://api.github.com/repos/$repo/releases" \ + -d '{"tag_name":"'"$tag"'","name":"'"${tag/v/Version }"'","draft":true,"body":"'"${description/\"/\\\"}"'"}' + ) + if [ "$http_code" == "201" ]; then + echo "Release for tag $tag created." + else + echo "Asset upload failed with code '$http_code'." + return 1 + fi +} + +# Args: +# token: GitHub API user token +# repo: GitHub username/reponame +# tag: Name of the tag for which to upload the assets +# file: Path to the asset file to upload +# name: Name to use for the uploaded asset +upload_release_file() { + # Args + token=$1 + repo=$2 + tag=$3 + file=$4 + name=$5 + echo "Uploading:" + echo " repo=$repo" + echo " tag=$tag" + echo " file=$file" + echo " name=$name" + echo "" + + # Determine upload URL of latest draft release for the specified tag + upload_url=$( + curl -s \ + --header "Accept: application/vnd.github.v3+json" \ + --header "Authorization: Bearer $token" \ + "https://api.github.com/repos/$repo/releases" \ + | jq -r '[.[] | select(.tag_name == "'"$tag"'" and .draft)][0].upload_url' \ + | cut -d"{" -f'1' + ) + echo "Determined upload URL: $upload_url" + http_code=$( + curl -s -o upload.json -w '%{http_code}' \ + --request POST \ + --header "Accept: application/vnd.github.v3+json" \ + --header "Authorization: Bearer $token" \ + --header "Content-Type: application/octet-stream" \ + --data-binary "@$file" "$upload_url?name=$name" + ) + if [ "$http_code" == "201" ]; then + echo "Asset $name uploaded:" + jq -r .browser_download_url upload.json + else + echo "Asset upload failed with code '$http_code':" + cat upload.json + return 1 + fi +} diff --git a/src/cache.rs b/src/cache.rs index 1d94d89..afa3603 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,39 +1,267 @@ use std::{ - env, - ffi::OsStr, - fs, - io::{Cursor, Read, Seek}, - iter, + fs::{self, File}, + 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, PlatformStrategy, 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: PlatformStrategy, +#[derive(Clone)] +pub struct CacheConfig<'a> { + pub pages_directory: &'a Path, + pub custom_pages_directory: Option<&'a Path>, + pub platforms: &'a [PlatformType], + pub search_languages: &'a [Language<'a>], + pub download_languages: &'a [Language<'a>], +} + +/// The directory backing this cache is checked to be populated at construction. +pub struct Cache<'a> { + config: CacheConfig<'a>, } #[derive(Debug)] pub struct PageLookupResult { - page_path: PathBuf, - patch_path: Option, + pub page_path: PathBuf, + pub patch_path: Option, +} + +impl<'a> Cache<'a> { + /// Try opening a cache at the location given by `config.pages_directory`. If no directory + /// exists at this location, `Ok(None)` is returned. + pub fn open(config: CacheConfig<'a>) -> Result> { + match config.pages_directory.metadata() { + Ok(md) => { + ensure!( + md.is_dir(), + "Cache directory `{}` exists, but is not a directory.", + config.pages_directory.display(), + ); + Ok(Some(Cache { config })) + } + Err(err) if err.kind() == ErrorKind::NotFound => Ok(None), + Err(err) => Err(anyhow!(err).context(format!( + "Error getting metdata of cache directory {}", + config.pages_directory.display() + ))), + } + } + + /// Open an existing cache at `config.pages_directory` or create one if no cache resides at + /// this location. In case of success, the return value is a tuple with the `Cache` and a + /// boolean indicating whether the cache was newly created. + pub fn open_or_create(config: CacheConfig<'a>) -> Result<(Self, bool)> { + if let Some(cache) = Self::open(config.clone())? { + return Ok((cache, false)); + } + + fs::create_dir_all(config.pages_directory).with_context(|| { + format!( + "Cache directory `{}` cannot be created", + config.pages_directory.display(), + ) + })?; + eprintln!( + "Successfully created cache directory `{}`.", + config.pages_directory.display(), + ); + + Ok((Cache { config }, true)) + } + + pub fn age(&self) -> Result { + let mtime = self.config.pages_directory.metadata()?.modified()?; + SystemTime::now() + .duration_since(mtime) + .context("Error comparing cache mtime with current time") + } + + pub fn find_page(&self, command: &str) -> Option { + let page_filename = format!("{command}.md"); + let patch_filename = format!("{command}.patch.md"); + let custom_filename = format!("{command}.page.md"); + + if let Some(custom_pages_dir) = self.config.custom_pages_directory { + let custom_page = custom_pages_dir.join(custom_filename); + if custom_page.is_file() { + return Some(PageLookupResult::with_page(custom_page)); + } + } + + let patch_path = self + .config + .custom_pages_directory + .map(|dir| dir.join(&patch_filename)) + .filter(|path| path.is_file()); + + for &platform in self.config.platforms { + for language in self.config.search_languages { + let mut search_path = self.config.pages_directory.to_path_buf(); + search_path.push(language.directory_name()); + search_path.push(platform.directory_name()); + search_path.push(&page_filename); + + if search_path.is_file() { + return Some( + PageLookupResult::with_page(search_path).with_optional_patch(patch_path), + ); + } + } + } + + None + } + + pub fn list_pages(&self) -> Result> { + let mut pages = Vec::new(); + + let mut append_all = |directory: &Path, suffix: &str| -> Result<()> { + let Ok(file_iter) = fs::read_dir(directory) else { + return Ok(()); + }; + + for entry in file_iter { + let entry = entry?; + if entry.file_type()?.is_file() { + let mut page_path = entry + .file_name() + .into_string() + .map_err(|_| anyhow!("Found invalid filename: {:?}", entry.path()))?; + + if page_path.ends_with(suffix) { + page_path.truncate(page_path.len() - suffix.len()); + pages.push(page_path); + } else { + debug!( + "Skipping page entry not ending in \".md\": {:?}", + entry.path(), + ); + } + } + } + + Ok(()) + }; + + let mut search_path = self.config.pages_directory.to_path_buf(); + for language in self.config.search_languages { + search_path.push(language.directory_name()); + for platform in self.config.platforms { + search_path.push(platform.directory_name()); + append_all(&search_path, ".md")?; + search_path.pop(); + } + search_path.pop(); + } + + if let Some(custom_pages_dir) = self.config.custom_pages_directory { + append_all(custom_pages_dir, ".page.md")?; + } + + pages.sort_unstable(); + pages.dedup(); + Ok(pages) + } + + pub fn old_custom_pages_exist(&self) -> Result { + let Some(directory) = self.config.custom_pages_directory else { + return Ok(false); + }; + let Ok(file_iter) = fs::read_dir(directory) else { + return Ok(false); + }; + + for entry in file_iter { + if let Some(extension) = entry?.path().extension() { + if extension == "page" || extension == "patch" { + return Ok(true); + } + } + } + + Ok(false) + } + + pub fn clear(self) -> Result<()> { + fs::remove_dir_all(self.config.pages_directory).with_context(|| { + format!( + "Could not remove pages directory at {}", + self.config.pages_directory.display(), + ) + }) + } + + /// Download archives for the languages in `self.config().download_languages` and replace the + /// pages directory with the newly downloaded pages. As not all languages might have pages + /// available (for example, `en_US` instead of `en`), an iterator yielding all languages which + /// were successfully downloaded is returned. + pub fn update( + &mut self, + archive_url: &str, + tls_backend: TlsBackend, + ) -> Result>> { + let client = Self::build_client(tls_backend); + + // Download everything before deleting anything + let mut archives = self + .config + .download_languages + .iter() + .map(|&lang| { + Ok(( + lang, + Self::download( + &client, + &format!("{archive_url}/tldr-{}.zip", lang.directory_name()), + )? + .map(|bytes| ZipArchive::new(Cursor::new(bytes))) + .transpose()?, + )) + }) + .collect::>>()?; + + // Clear cache directory + // Note: This is not the best solution. Ideally we would download the + // archive to a temporary directory and then swap the two directories. + // But renaming a directory doesn't work across filesystems and Rust + // does not yet offer a recursive directory copying function. So for + // now, we'll use this approach. + fs::remove_dir_all(self.config.pages_directory)?; + fs::create_dir(self.config.pages_directory)?; + + for (lang, archive) in &mut archives { + if let Some(archive) = archive { + info!("Extracting archive for {lang:?}"); + archive.extract(self.config.pages_directory.join(lang.directory_name()))?; + } else { + info!("No archive found for {lang:?}"); + } + } + + Ok(archives + .into_iter() + .filter_map(|(lang, archive)| archive.is_some().then_some(lang))) + } + + pub fn config(&self) -> &CacheConfig<'a> { + &self.config + } } impl PageLookupResult { @@ -49,311 +277,101 @@ impl PageLookupResult { self } - pub fn paths(&self) -> impl Iterator { - iter::once(self.page_path.as_path()).chain(self.patch_path.as_deref()) - } -} + /// Create a reader that sequentially reads from the page and the + /// patch, as if they were concatenated. + /// + /// This will return an error if either the page file or the patch file + /// cannot be opened. + pub fn reader(&self) -> Result> { + // Open page file + let page_file = File::open(&self.page_path) + .with_context(|| format!("Could not open page file at {}", self.page_path.display()))?; -pub enum CacheFreshness { - /// The cache is still fresh (less than MAX_CACHE_AGE old) - Fresh, - /// The cache is stale and should be updated - Stale(Duration), - /// The cache is missing - Missing, -} - -impl Cache { - pub fn new(url: S, platform: PlatformStrategy) -> Self - where - S: Into, - { - Self { - url: url.into(), - platform, - } - } - - /// Return the path to the cache directory. - pub fn get_cache_dir() -> Result<(PathBuf, PathSource), TealdeerError> { - // Allow overriding the cache directory by setting the env variable. - if let Ok(value) = env::var(CACHE_DIR_ENV_VAR) { - let path = PathBuf::from(value); - let (path_exists, path_is_dir) = path - .metadata() - .map_or((false, false), |md| (true, md.is_dir())); - if path_exists && !path_is_dir { - return Err(CacheError(format!( - "Path specified by ${} is not a directory.", - CACHE_DIR_ENV_VAR - ))); - } - if !path_exists { - // Try to create the complete directory path. - fs::create_dir_all(&path).map_err(|_| { - CacheError(format!( - "Directory path specified by ${} cannot be created.", - CACHE_DIR_ENV_VAR - )) - })?; - eprintln!( - "Successfully created cache directory path `{}`.", - path.to_str().unwrap() - ); - } - return Ok((path, PathSource::EnvVar)); + // Open patch file + let patch_file_opt = match &self.patch_path { + Some(path) => Some( + File::open(path) + .with_context(|| format!("Could not open patch file at {}", path.display()))?, + ), + None => None, }; - // Otherwise, fall back to user cache directory. - match get_app_root(AppDataType::UserCache, &crate::APP_INFO) { - Ok(dirs) => Ok((dirs, PathSource::OsConvention)), - Err(_) => Err(CacheError( - "Could not determine user cache directory.".into(), - )), - } - } - - /// Download the archive - fn download(&self) -> Result, TealdeerError> { - let mut builder = Client::builder(); - if let Ok(ref host) = env::var("HTTP_PROXY") { - if let Ok(proxy) = Proxy::http(host) { - builder = builder.proxy(proxy); - } - } - if let Ok(ref host) = env::var("HTTPS_PROXY") { - if let Ok(proxy) = Proxy::https(host) { - builder = builder.proxy(proxy); - } - } - let client = builder.build().unwrap_or_else(|_| Client::new()); - let mut resp = client.get(&self.url).send()?; - let mut buf: Vec = vec![]; - let bytes_downloaded = resp.copy_to(&mut buf)?; - debug!("{} bytes downloaded", bytes_downloaded); - Ok(buf) - } - - /// Decompress and open the archive - fn decompress(reader: R) -> ZipArchive { - ZipArchive::new(reader).unwrap() - } - - /// Update the pages cache. - pub fn update(&self) -> Result<(), TealdeerError> { - // First, download the compressed data - let bytes: Vec = self.download()?; - - // Decompress the response body into an `Archive` - let mut archive = Self::decompress(Cursor::new(bytes)); - - // Determine paths - let (cache_dir, _) = Self::get_cache_dir()?; - let pages_dir = cache_dir.join(TLDR_PAGES_DIR); - - // Make sure that cache directory exists - debug!("Ensure cache directory {:?} exists", &cache_dir); - fs::create_dir_all(&cache_dir) - .map_err(|e| UpdateError(format!("Could not create cache directory: {}", e)))?; - - // Clear cache directory - // Note: This is not the best solution. Ideally we would download the - // archive to a temporary directory and then swap the two directories. - // But renaming a directory doesn't work across filesystems and Rust - // does not yet offer a recursive directory copying function. So for - // now, we'll use this approach. - Self::clear()?; - - // Extract archive - archive - .extract(&pages_dir) - .map_err(|e| UpdateError(format!("Could not unpack compressed data: {}", e)))?; - - Ok(()) - } - - /// Return the duration since the cache directory was last modified. - pub fn last_update() -> Option { - if let Ok((cache_dir, _)) = Self::get_cache_dir() { - if let Ok(metadata) = fs::metadata(cache_dir.join(TLDR_PAGES_DIR)) { - if let Ok(mtime) = metadata.modified() { - let now = SystemTime::now(); - return now.duration_since(mtime).ok(); - }; - }; - }; - None - } - - /// Return the freshness of the cache (fresh, stale or missing). - pub fn freshness() -> CacheFreshness { - match Cache::last_update() { - Some(ago) if ago > crate::config::MAX_CACHE_AGE => CacheFreshness::Stale(ago), - Some(_) => CacheFreshness::Fresh, - None => CacheFreshness::Missing, - } - } - - /// Return the platform directory. - fn get_platform_dir(&self) -> &'static str { - match self.platform.platform_type { - PlatformType::Linux { .. } => "linux", - PlatformType::OsX { .. } => "osx", - PlatformType::SunOs { .. } => "sunos", - PlatformType::Windows { .. } => "windows", - } - } - - /// Check for pages for a given platform in one of the given languages. - fn find_page_for_platform( - page_name: &str, - cache_dir: &Path, - platform: &str, - language_dirs: &[String], - ) -> Option { - language_dirs - .iter() - .map(|lang_dir| cache_dir.join(lang_dir).join(platform).join(page_name)) - .find(|path| path.exists() && path.is_file()) - } - - /// Look up custom patch (.patch). If it exists, store it in a variable. - fn find_patch(patch_name: &str, custom_pages_dir: Option<&Path>) -> Option { - custom_pages_dir - .map(|custom_dir| custom_dir.join(patch_name)) - .filter(|path| path.exists() && path.is_file()) - } - - /// Search for a page and return the path to it. - pub fn find_page( - &self, - name: &str, - languages: &[String], - custom_pages_dir: Option<&Path>, - ) -> Option { - let page_filename = format!("{}.md", name); - let patch_filename = format!("{}.patch", name); - let custom_filename = format!("{}.page", name); - - // Get cache dir - let cache_dir = match Self::get_cache_dir() { - Ok((cache_dir, _)) => cache_dir.join(TLDR_PAGES_DIR), - Err(e) => { - log::error!("Could not get cache directory: {}", e); - return None; - } - }; - - let lang_dirs: Vec = languages - .iter() - .map(|lang| { - if lang == "en" { - String::from("pages") - } else { - format!("pages.{}", lang) - } - }) - .collect(); - - // Look up custom page (.page). If it exists, return it directly - if let Some(config_dir) = custom_pages_dir { - let custom_page = config_dir.join(custom_filename); - if custom_page.exists() && custom_page.is_file() { - return Some(PageLookupResult::with_page(custom_page)); - } - } - - let patch_path = Self::find_patch(&patch_filename, custom_pages_dir); - - // Try to find a platform specific path next, append custom patch to it. - let platform_dir = self.get_platform_dir(); - if let Some(page) = - Self::find_page_for_platform(&page_filename, &cache_dir, platform_dir, &lang_dirs) - { - return Some(PageLookupResult::with_page(page).with_optional_patch(patch_path)); - } - - // Did not find platform specific results, fall back to "common" - Self::find_page_for_platform(&page_filename, &cache_dir, "common", &lang_dirs) - .map(|page| PageLookupResult::with_page(page).with_optional_patch(patch_path)) - } - - /// Return the available pages. - pub fn list_pages(&self) -> Result, TealdeerError> { - // Determine platforms directory and platform - let (cache_dir, _) = Self::get_cache_dir()?; - let platforms_dir = cache_dir.join(TLDR_PAGES_DIR).join("pages"); - let platform_dir = self.get_platform_dir(); - - // Closure that allows the WalkDir instance to traverse platform - // specific and common page directories, but not others. - let should_walk = |entry: &DirEntry| -> bool { - let file_type = entry.file_type(); - let file_name = match entry.file_name().to_str() { - Some(name) => name, - None => return false, - }; - if file_type.is_dir() { - return file_name == "common" || file_name == platform_dir; - } else if file_type.is_file() { - return true; - } - false - }; - - // Recursively walk through common and (if applicable) platform specific directory - let mut pages = WalkDir::new(platforms_dir) - .min_depth(1) // Skip root directory - .into_iter() - .filter_entry(|e| self.platform.list_all || should_walk(e)) // Filter out pages for other architectures - .filter_map(Result::ok) // Convert results to options, filter out errors - .filter_map(|e| { - let path = e.path(); - let extension = &path.extension().and_then(OsStr::to_str).unwrap_or(""); - if e.file_type().is_file() && extension == &"md" { - path.file_stem() - .and_then(|stem| stem.to_str().map(Into::into)) - } else { - None - } - }) - .collect::>(); - pages.sort(); - pages.dedup(); - Ok(pages) - } - - /// Delete the cache directory. - pub fn clear() -> Result<(), TealdeerError> { - let (path, _) = Self::get_cache_dir()?; - if path.exists() && path.is_dir() { - // Delete old tldr-pages cache location as well if present - // TODO: To be removed in the future - for pages_dir_name in [TLDR_PAGES_DIR, TLDR_OLD_PAGES_DIR] { - let pages_dir = path.join(pages_dir_name); - - if pages_dir.exists() { - fs::remove_dir_all(&pages_dir).map_err(|e| { - CacheError(format!( - "Could not remove cache directory ({}): {}", - pages_dir.display(), - e - )) - })?; - } - } - } else if path.exists() { - return Err(CacheError(format!( - "Cache path ({}) is not a directory.", - path.display() - ))); + // Create chained reader from file(s) + // + // Note: It might be worthwhile to create our own struct that accepts + // the page and patch files and that will read them sequentially, + // because it avoids the boxing below. However, the performance impact + // would first need to be shown to be significant using a benchmark. + Ok(if let Some(patch_file) = patch_file_opt { + Box::new(page_file.chain(&b"\n"[..]).chain(patch_file)) as Box } else { - return Err(CacheError(format!( - "Cache path ({}) does not exist.", - path.display() - ))); + Box::new(page_file) as Box + }) + } +} + +impl Language<'_> { + fn directory_name(&self) -> String { + format!("pages.{}", self.0) + } +} + +impl PlatformType { + fn directory_name(self) -> &'static str { + match self { + PlatformType::Linux => "linux", + PlatformType::OsX => "osx", + PlatformType::SunOs => "sunos", + PlatformType::Windows => "windows", + PlatformType::Android => "android", + PlatformType::FreeBsd => "freebsd", + PlatformType::NetBsd => "netbsd", + PlatformType::OpenBsd => "openbsd", + PlatformType::Common => "common", + } + } +} + +impl Cache<'_> { + fn build_client(tls_backend: TlsBackend) -> Agent { + let tls_builder = match tls_backend { + #[cfg(feature = "native-tls")] + TlsBackend::NativeTls => TlsConfig::builder() + .provider(TlsProvider::NativeTls) + .root_certs(RootCerts::PlatformVerifier), + #[cfg(feature = "rustls-with-webpki-roots")] + TlsBackend::RustlsWithWebpkiRoots => TlsConfig::builder() + .provider(TlsProvider::Rustls) + .root_certs(RootCerts::WebPki), + #[cfg(feature = "rustls-with-native-roots")] + TlsBackend::RustlsWithNativeRoots => TlsConfig::builder() + .provider(TlsProvider::Rustls) + .root_certs(RootCerts::PlatformVerifier), }; - Ok(()) + let config = Agent::config_builder() + .http_status_as_error(false) // because we want to handle them + .tls_config(tls_builder.build()) + .build(); + + config.into() + } + + /// Download the archive from the specified URL. + fn download(client: &Agent, archive_url: &str) -> Result>> { + info!("Downloading archive from {archive_url}"); + let response = client.get(archive_url).call(); + match response { + Ok(response) if response.status().is_success() => { + let mut buf: Vec = Vec::new(); + response.into_body().into_reader().read_to_end(&mut buf)?; + debug!("{} bytes downloaded", buf.len()); + Ok(Some(buf)) + } + Ok(response) if response.status() == StatusCode::NOT_FOUND => Ok(None), + _ => { + bail!("Could not download tldr pages from {archive_url}: {response:?}") + } + } } } @@ -362,21 +380,53 @@ impl Cache { mod tests { use super::*; + use std::{ + fs::File, + io::{Read, Write}, + }; + #[test] - fn test_page_lookup_result_iter_with_patch() { - let lookup = PageLookupResult::with_page(PathBuf::from("test.page")) - .with_optional_patch(Some(PathBuf::from("test.patch"))); - let mut iter = lookup.paths(); - assert_eq!(iter.next(), Some(Path::new("test.page"))); - assert_eq!(iter.next(), Some(Path::new("test.patch"))); - assert_eq!(iter.next(), None); + fn test_reader_with_patch() { + // Write test files + let dir = tempfile::tempdir().unwrap(); + let page_path = dir.path().join("test.page.md"); + let patch_path = dir.path().join("test.patch.md"); + { + let mut f1 = File::create(&page_path).unwrap(); + f1.write_all(b"Hello\n").unwrap(); + let mut f2 = File::create(&patch_path).unwrap(); + f2.write_all(b"World").unwrap(); + } + + // Create chained reader from lookup result + let lr = PageLookupResult::with_page(page_path).with_optional_patch(Some(patch_path)); + let mut reader = lr.reader().unwrap(); + + // Read into a Vec + let mut buf = Vec::new(); + reader.read_to_end(&mut buf).unwrap(); + + assert_eq!(&buf, b"Hello\n\nWorld"); } #[test] - fn test_page_lookup_result_iter_no_patch() { - let lookup = PageLookupResult::with_page(PathBuf::from("test.page")); - let mut iter = lookup.paths(); - assert_eq!(iter.next(), Some(Path::new("test.page"))); - assert_eq!(iter.next(), None); + fn test_reader_without_patch() { + // Write test file + let dir = tempfile::tempdir().unwrap(); + let page_path = dir.path().join("test.page.md"); + { + let mut f = File::create(&page_path).unwrap(); + f.write_all(b"Hello\n").unwrap(); + } + + // Create chained reader from lookup result + let lr = PageLookupResult::with_page(page_path); + let mut reader = lr.reader().unwrap(); + + // Read into a Vec + let mut buf = Vec::new(); + reader.read_to_end(&mut buf).unwrap(); + + assert_eq!(&buf, b"Hello\n"); } } 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 d1b040b..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,22 +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| path.join("pages")) - .ok(), + languages, + platforms, } } } @@ -186,12 +383,7 @@ struct RawConfig { display: RawDisplayConfig, updates: RawUpdatesConfig, directories: RawDirectoriesConfig, -} - -impl RawConfig { - fn new() -> Self { - Self::default() - } + search: RawSearchConfig, } impl Default for RawConfig { @@ -201,6 +393,7 @@ impl Default for RawConfig { display: RawDisplayConfig::default(), updates: RawUpdatesConfig::default(), directories: RawDirectoriesConfig::default(), + search: RawSearchConfig::default(), }; // Set default config @@ -214,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, @@ -223,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") } } @@ -331,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 4c9e676..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,297 +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, PlatformStrategy, 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://tldr.sh/assets/tldr.zip"; - -// Note: flag names are specified explicitly in clap attributes -// to improve readability and allow contributors to grep names like "clear-cache" -#[derive(Parser, Debug)] -#[clap(about = "A fast TLDR client", author, version)] -#[clap(setting = AppSettings::ArgRequiredElseHelp)] -#[clap(setting = AppSettings::HelpRequired)] -#[clap(setting = AppSettings::DeriveDisplayOrder)] -#[clap( - after_help = "To view the user documentation, please visit https://dbrgn.github.io/tealdeer/." -)] -#[clap(group = ArgGroup::new("command_or_file").args(&["command", "render"]))] -struct Args { - /// The command to show (e.g. `tar` or `git log`) - #[clap(min_values = 1)] - command: Vec, - - /// List all commands in the cache - #[clap(short = 'l', long = "list")] - list: bool, - - /// Render a specific markdown file - #[clap( - short = 'f', - long = "render", - value_name = "FILE", - conflicts_with = "command" - )] - render: Option, - - /// Override the operating system [possible values: linux, macos, windows, sunos, all] - #[clap( - short = 'p', - long = "platform", - possible_values = ["linux", "macos", "windows", "sunos", "osx", "current", "all"], - default_value = "current", - hide_possible_values = true, - hide_default_value = true, - )] - platform: PlatformStrategy, - - /// Deprecated alias of `platform` - #[clap( - short = 'o', - long = "os", - conflicts_with = "platform", - possible_values = ["linux", "macos", "windows", "sunos", "osx", "current", "all"], - default_value = "current", - hide_possible_values = true, - hide_default_value = true, - )] - os: PlatformStrategy, - - /// Override the language - #[clap(short = 'L', long = "language")] - language: Option, - - /// Update the local cache - #[clap(short = 'u', long = "update")] - update: bool, - - /// Clear the local cache - #[clap(short = 'c', long = "clear-cache")] - clear_cache: bool, - - /// Use a pager to page output - #[clap(long = "pager", requires = "command_or_file")] - pager: bool, - - /// Display the raw markdown instead of rendering it - #[clap(short = 'r', long = "--raw", requires = "command_or_file")] - raw: bool, - - /// Deprecated alias of `raw` - #[clap( - long = "markdown", - short = 'm', - requires = "command_or_file", - hidden = true - )] - markdown: bool, - - /// Suppress informational messages - #[clap(short = 'q', long = "quiet")] - quiet: bool, - - /// Show file and directory paths used by tealdeer - #[clap(long = "show-paths")] - show_paths: bool, - - /// Show config file path - #[clap(long = "config-path")] - config_path: bool, - - /// Create a basic config - #[clap(long = "seed-config")] - seed_config: bool, - - /// Control whether to use color - #[clap( - long = "color", - value_name = "WHEN", - possible_values = ["always", "auto", "never"] - )] - color: Option, - - /// Print the version - // Note: We override the version flag because clap uses `-V` by default, - // while TLDR specification requires `-v` to be used. - #[clap(short = 'v', long = "version")] - version: bool, -} - -/// Set up display pager -#[cfg(not(target_os = "windows"))] -fn configure_pager(_: bool) { - Pager::with_default_pager("less -R").setup(); -} - -#[cfg(target_os = "windows")] -fn configure_pager(enable_styles: bool) { - print_warning(enable_styles, "--pager flag not available on Windows!"); -} - -/// The cache should get updated if this was requested by the user, or if auto -/// updates are enabled and the cache age is longer than the auto update interval. -fn should_update_cache(args: &Args, config: &Config) -> bool { - args.update - || (config.updates.auto_update - && Cache::last_update().map_or(true, |ago| ago >= config.updates.auto_update_interval)) -} - -#[derive(PartialEq)] -enum CheckCacheResult { - CacheFound, - CacheMissing, -} - -/// Check the cache for freshness. If it's stale or missing, show a warning. -fn check_cache(args: &Args, enable_styles: bool) -> CheckCacheResult { - match Cache::freshness() { - CacheFreshness::Fresh => CheckCacheResult::CacheFound, - CacheFreshness::Stale(_) if args.quiet => CheckCacheResult::CacheFound, - CacheFreshness::Stale(age) => { - print_warning( - enable_styles, - &format!( - "The cache hasn't been updated for {} days.\n\ - You should probably run `tldr --update` soon.", - age.as_secs() / 24 / 3600 - ), - ); - CheckCacheResult::CacheFound - } - CacheFreshness::Missing => { - print_warning( - enable_styles, - "Cache not found. Please run `tldr --update`.", - ); - CheckCacheResult::CacheMissing - } - } -} +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() { - let config_dir = get_config_dir().map_or_else( - |e| format!("[Error: {}]", e), - |(mut path, source)| { - path.push(""); // Trailing path separator - match path.to_str() { - Some(path) => format!("{} ({})", path, source), - None => "[Invalid]".to_string(), - } - }, - ); - let config_path = get_config_path().map_or_else( - |e| format!("[Error: {}]", e), - |(path, _)| path.to_str().unwrap_or("[Invalid]").to_string(), - ); - let cache_dir = Cache::get_cache_dir().map_or_else( - |e| format!("[Error: {}]", e), - |(mut path, source)| { - path.push(""); // Trailing path separator - match path.to_str() { - Some(path) => format!("{} ({})", path, source), - None => "[Invalid]".to_string(), - } - }, - ); - let pages_dir = Cache::get_cache_dir().map_or_else( - |e| format!("[Error: {}]", e), - |(mut path, _)| { - path.push(TLDR_PAGES_DIR); - path.push(""); // Trailing path separator - path.into_os_string() - .into_string() - .unwrap_or_else(|_| String::from("[Invalid]")) - }, - ); - println!("Config dir: {}", config_dir); - println!("Config path: {}", config_path); - println!("Cache dir: {}", cache_dir); - println!("Pages dir: {}", pages_dir); +fn show_paths(config: &Config) { + let config_dir = { + let (mut path, source) = get_config_dir(); + path.push(""); // Trailing path separator + match path.to_str() { + Some(path) => format!("{path} ({source})"), + None => "[Invalid]".to_string(), + } + }; + let config_path = config.file_path.to_string(); + let cache_dir = config.directories.cache_dir.to_string(); + let pages_dir = { + let mut path = config.directories.cache_dir.path.clone(); + path.push(TLDR_PAGES_DIR); + path.push(""); // Trailing path separator + path.display().to_string() + }; + let custom_pages_dir = match config.directories.custom_pages_dir { + Some(ref path_with_source) => path_with_source.to_string(), + None => "[None]".to_string(), + }; + println!("Config dir: {config_dir}"); + println!("Config path: {config_path}"); + println!("Cache dir: {cache_dir}"); + println!("Pages dir: {pages_dir}"); + println!("Custom pages dir: {custom_pages_dir}"); } -/// 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")] @@ -336,254 +149,296 @@ 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/master/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", - ); - } - let default_platform = PlatformType::current(); - if args.os.platform_type != default_platform || args.os.list_all { - print_warning( - enable_styles, - "The -o / --os flag is deprecated, use -p / --platform instead", - ); - args.platform = args.os; + try_main(args, enable_styles).unwrap_or_else(|error| { + print_error(enable_styles, &error); + ExitCode::FAILURE + }) +} + +fn try_main(args: Cli, enable_styles: bool) -> Result { + // Look up config file, if none is found fall back to default config. + debug!("Loading config"); + let config_loader = match &args.config_path { + Some(path) if !args.seed_config => { + ConfigLoader::read(path.clone()).context("Could not read config from given path")? + } + _ => { + ConfigLoader::read_default_path().context("Could not read config from default path")? + } + }; + let mut config = config_loader.load()?; + + // Override styles if needed + if !enable_styles { + config.style = StyleConfig::default(); } - // 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); + 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 if args.show_paths { - show_paths(); + show_paths(&config); } // Create a basic config and exit if args.seed_config { - create_config_and_exit(enable_styles); - } - - // Look up config file, if none is found fall back to default config. - let config = match Config::load(enable_styles) { - Ok(config) => config, - Err(ConfigError(msg)) => { - print_error(enable_styles, &format!("Could not load config: {}", msg)); - process::exit(1); - } - Err(e) => { - print_error(enable_styles, &format!("Could not load config: {}", e)); - process::exit(1); - } - }; - - if args.pager || config.display.use_pager { - configure_pager(enable_styles); + create_config(args.config_path.as_deref())?; + return Ok(ExitCode::SUCCESS); } // 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, args.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(), + ), + ); + } - let languages = args - .language - .map_or_else(get_languages_from_env, |lang| vec![lang]); - - // Search for command in cache - if let Some(page) = cache.find_page( - &command, - &languages, - config.directories.custom_pages_dir.as_deref(), - ) { - if let Err(msg) = print_page(&page, args.raw, &config) { - print_error(enable_styles, &msg); - process::exit(1); - } - process::exit(0); - } else { + 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 70cd138..6243b44 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,75 +1,97 @@ //! Functions for printing pages to the terminal -use std::{ - fs::File, - io::{self, BufRead, BufReader, 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( - page: &PageLookupResult, + reader: impl Read, enable_markdown: bool, + enable_styles: bool, + use_pager: bool, config: &Config, -) -> Result<(), String> { +) -> 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(); let mut handle = stdout.lock(); - for path in page.paths() { - let file = File::open(path).map_err(|msg| format!("Could not open file: {}", msg))?; - let reader = BufReader::new(file); - - 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())?; + if enable_markdown { + // Print the raw markdown of the file. + for line in reader.lines() { + let line = line.context("Error while reading from a page")?; + writeln!(handle, "{line}").context("Could not write to stdout")?; + } + } else { + // Closure that processes a page snippet and writes it to stdout + let mut process_snippet = |snip: PageSnippet<&str>| { + if snip.is_empty() { + Ok(()) + } else { + print_snippet(&mut handle, snip, &config.style).context("Failed to print snippet") } - } else { - let mut process_snippet = |snip: PageSnippet<'_>| { - if snip.is_empty() { - Ok(()) - } else { - print_snippet(&mut handle, snip, &config.style) - .map_err(|e| WriteError(e.to_string())) - } - }; - highlight_lines( - LineIterator::new(reader), - &mut process_snippet, - !config.display.compact, - ) - .map_err(|e| format!("Could not write to stdout: {}", e.message()))?; }; + + // Print highlighted lines + highlight_lines( + LineIterator::new(reader), + &mut process_snippet, + !config.display.compact, + config.display.show_title, + config.display.indent, + ) + .context("Could not write to stdout")?; } - handle - .flush() - .map_err(|_| "Could not flush stdout".to_string())?; + // We're done outputting data, flush stdout now! + 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 72d5b92..7ca6e2d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -2,16 +2,21 @@ use std::{fmt, str}; -use serde::Deserialize; +use serde_derive::{Deserialize, Serialize}; -/// The platform types supported by tldr. -#[derive(Debug, Eq, PartialEq, Copy, Clone)] +#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] #[allow(dead_code)] pub enum PlatformType { Linux, OsX, - SunOs, Windows, + SunOs, + Android, + FreeBsd, + NetBsd, + OpenBsd, + Common, } impl fmt::Display for PlatformType { @@ -19,66 +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"), } } } -/// The platform lookup strategy. -/// -/// Includes both the platform type, as well as -#[derive(Debug, Copy, Clone)] -pub struct PlatformStrategy { - /// The platform type that should be looked up. - pub platform_type: PlatformType, - /// Flag indicating whether all pages should be listed or not. This is only - /// used when the special platform type `all` is specified by the user. - pub list_all: bool, -} - -impl PlatformStrategy { - pub fn new(platform_type: PlatformType) -> Self { - Self { - platform_type, - list_all: false, - } +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, + ] } - /// Return a `PlatformStrategy` containing the current platform as the - /// target platform type. - pub fn current() -> Self { - Self { - platform_type: PlatformType::current(), - list_all: false, - } - } - - /// Like `current()`, but when listing the pages, return the pages for all - /// platforms, not just for the current platform. - pub fn all() -> Self { - Self { - platform_type: PlatformType::current(), - list_all: true, - } - } -} - -impl str::FromStr for PlatformStrategy { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "linux" => Ok(PlatformStrategy::new(PlatformType::Linux)), - "osx" | "macos" => Ok(PlatformStrategy::new(PlatformType::OsX)), - "windows" => Ok(PlatformStrategy::new(PlatformType::Windows)), - "sunos" => Ok(PlatformStrategy::new(PlatformType::SunOs)), - "current" => Ok(PlatformStrategy::current()), - "all" => Ok(PlatformStrategy::all()), - other => Err(format!( - "Unknown platform: {}. Possible values: linux, macos, osx, windows, sunos, current, all", - other - )), + 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")), } } } @@ -89,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 } @@ -105,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", @@ -112,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, @@ -216,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 { @@ -236,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/custom-pages/inkscape-v2.patch.md b/tests/custom-pages/inkscape-v2.patch.md new file mode 100644 index 0000000..5cc5d22 --- /dev/null +++ b/tests/custom-pages/inkscape-v2.patch.md @@ -0,0 +1,3 @@ +Custom inkscape entry + + My Inkscape example diff --git a/tests/inkscape-v2.patch b/tests/inkscape-v2.patch deleted file mode 100644 index ffca34c..0000000 --- a/tests/inkscape-v2.patch +++ /dev/null @@ -1,5 +0,0 @@ -This header shouldn't be required -================================= -Custom inkscape entry - - My Inkscape example diff --git a/tests/lib.rs b/tests/lib.rs index e79d9aa..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,69 +544,191 @@ 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] fn test_show_paths() { let testenv = TestEnv::new(); + // Show general commands testenv .command() .args(["--show-paths"]) .assert() .success() .stdout(contains(format!( - "Config dir: {}", - testenv.config_dir.path().to_str().unwrap(), + "Config dir: {}", + testenv.config_dir().to_str().unwrap(), ))) .stdout(contains(format!( - "Config path: {}", - testenv - .config_dir - .path() - .join("config.toml") - .to_str() - .unwrap(), + "Config path: {}", + testenv.config_dir().join("config.toml").to_str().unwrap(), ))) .stdout(contains(format!( - "Cache dir: {}", - testenv.cache_dir.path().to_str().unwrap(), + "Cache dir: {}", + testenv.cache_dir().to_str().unwrap(), ))) .stdout(contains(format!( - "Pages dir: {}", - testenv - .cache_dir - .path() - .join(TLDR_PAGES_DIR) - .to_str() - .unwrap(), + "Pages dir: {}", + testenv.cache_dir().join(TLDR_PAGES_DIR).to_str().unwrap(), + ))); + + let testenv = testenv.write_custom_pages_config(); + + // Now ensure that this path is contained in the output + testenv + .command() + .args(["--show-paths"]) + .assert() + .success() + .stdout(contains(format!( + "Custom pages dir: {}", + testenv.custom_pages_dir().to_str().unwrap(), ))); } @@ -331,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"]) @@ -345,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)); @@ -371,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"], ); } @@ -382,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"], ); } @@ -394,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"], ); } @@ -405,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)); @@ -457,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() @@ -475,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() @@ -492,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", ""); @@ -514,49 +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_list_platform_filtering() { - let testenv = TestEnv::new(); +fn test_multi_platform_list_flag_rendering() { + let testenv = TestEnv::new().write_custom_pages_config(); - testenv.add_os_entry("common", "a-common", ""); - testenv.add_os_entry("windows", "a-windows", ""); - testenv.add_os_entry("linux", "a-linux", ""); - testenv.add_os_entry("linux", "b-linux", ""); + testenv.add_entry("common", ""); - // Filter: linux testenv .command() - .args(["--list", "--platform", "linux"]) + .args(["--list"]) .assert() .success() - .stdout("a-common\na-linux\nb-linux\n"); + .stdout("common\n"); - // Filter: windows testenv .command() - .args(["--list", "--platform", "windows"]) + .args(["--platform", "linux", "--list"]) .assert() .success() - .stdout("a-common\na-windows\n"); + .stdout("common\n"); - // Filter: all testenv .command() - .args(["--list", "--platform", "all"]) + .args(["--platform", "windows", "--list"]) .assert() .success() - .stdout("a-common\na-linux\na-windows\nb-linux\n"); + .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(); @@ -567,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`. @@ -613,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() @@ -640,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() @@ -667,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() @@ -700,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 @@ -747,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 @@ -763,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"); @@ -772,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