diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index f432d19..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,34 +0,0 @@ -version: 2 -jobs: - build: - docker: - - image: rust:1.31 - steps: - - checkout - # Load cargo target from cache if possible. - # Multiple caches are used to increase the chance of a cache hit. - - restore_cache: - keys: - - v1-cargo-cache-{{ arch }}-{{ .Branch }} - - v1-cargo-cache-{{ arch }} - - # Show versions - - run: rustc --version && cargo --version - - # Build - - run: cargo build - - run: cargo build --features logging - - # Run tests - - run: cargo test - - - save_cache: - key: v1-cargo-cache-{{ arch }}-{{ .Branch }} - paths: - - target - - /usr/local/cargo - - save_cache: - key: v1-cargo-cache-{{ arch }} - paths: - - target - - /usr/local/cargo diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..953567c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto + +*.md eol=lf +*.expected eol=lf 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 new file mode 100644 index 0000000..faf3111 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,93 @@ +name: CI +on: + push: + branches: + - main + - "v*.x" + pull_request: + schedule: + - cron: '30 3 * * 2' + workflow_dispatch: + +jobs: + test: + name: run tests + strategy: + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + toolchain: [stable, 1.87.0] # MSRV + include: + - platform: windows-latest + exe_suffix: .exe + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.toolchain }} + - run: mkdir artifacts + - name: Build with default features + run: | + cargo build + cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-default${{ matrix.exe_suffix}} + - name: Build with logging and Rustls with webpki roots + run: | + cargo build --features logging,rustls-with-webpki-roots --no-default-features + cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-logging-rustls-webpki${{ matrix.exe_suffix}} + - name: Build with native TLS backend + run: | + # expects runners have the proper Native SSL library + cargo build --features native-tls --no-default-features + cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-native-tls${{ matrix.exe_suffix}} + - uses: actions/upload-artifact@v7 + with: + name: tldr-debug-build-${{ matrix.platform }}-rust-${{ matrix.toolchain }} + path: artifacts/ + - name: Run tests + run: cargo test -- --test-threads 1 + + clippy: + name: run clippy lints + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: clippy + - name: run clippy lints + run: cargo clippy --all-targets --features logging + + fmt: + name: run rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: rustfmt + - name: run rustfmt + run: cargo fmt --all -- --check + + docs: + name: build docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Setup mdBook + uses: peaceiris/actions-mdbook@v2 + with: + mdbook-version: '0.4.4' + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + - name: Build + run: cargo build + - name: Ensure that docs can be built + run: cd docs && mdbook build + - name: Generate usage string + run: cargo run -- --help > docs/src/usage-actual.txt + - name: Ensure that usage string is up to date + run: diff docs/src/usage{,-actual}.txt diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 0000000..ae9ae93 --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,25 @@ +name: GitHub Pages +on: + push: + tags: + - "v[1-9]*" # push events matching `v` followed by anything larger than 0, e.g. v1.0, v20.15.10 + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Setup mdBook + uses: peaceiris/actions-mdbook@v2 + with: + mdbook-version: '0.4.4' + + - run: cd docs && mdbook build + + - name: Deploy + 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/.travis.yml b/.travis.yml deleted file mode 100644 index 3bf39a9..0000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: rust -os: osx -rust: - - 1.31.0 - - stable -cache: cargo -script: cargo test diff --git a/CHANGELOG.md b/CHANGELOG.md index ed03204..644a30a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,480 @@ 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: + +- [Danilo Bargen][@dbrgn] +- [Bruno A. Muciño][@mucinoab] +- [Francesco][@BachoSeven] + +Thanks! + + +### [v1.4.0][v1.4.0] (2020-09-03) + +- [added] Configurable automatic cache updates ([#115][i115]) +- [added] Improved color detection and support for `--color` argument and + `NO_COLOR` env variable ([#111][i111]) +- [changed] Make `--list` option comply with official spec ([#112][i112]) +- [changed] Move cache age warning to stderr ([#113][i113]) + +#### Contributors to this version: + +- [Atul Bhosale][@Atul9] +- [Danilo Bargen][@dbrgn] +- [Danny Mösch][@SimplyDanny] +- [Ilaï Deutel][@ilai-deutel] +- [Kornel][@kornelski] +- [@LovecraftianHorror][@LovecraftianHorror] +- [@michaeldel][@michaeldel] +- [Niklas Mohrin][@niklasmohrin] + +Thanks! + + +### [v1.3.0][v1.3.0] (2020-02-28) + +- [added] New config option for compact output mode ([#89][i89]) +- [added] New -m/--markdown parameter for raw rendering ([#95][i95]) +- [added] Provide zsh autocompletion ([#86][i86]) +- [changed] Require at least Rust 1.39 to build (previous: 1.32) +- [changed] Switch to GitHub actions, CI testing now covers Windows as well ([#99][i99]) +- [changed] Tweak the "outdated cache" warning message ([#97][i97]) +- [changed] General maintenance: Upgrade dependencies, fix linter warnings +- [fixed] Fix Fish autocompletion on macOS ([#87][i87]) +- [fixed] Fix compilation on Windows by disabling pager ([#99][i99]) + +#### Contributors to this version: + +- [Bruno Heridet][@Delapouite] +- [Danilo Bargen][@dbrgn] +- [Hugo Locurcio][@Calinou] +- [Isak Johansson][@Plommonsorbet] +- [James Doyle][@james2doyle] +- [Jesús Trinidad Díaz Ramírez][@jesdazrez] +- [@korrat][@korrat] +- [Marc-André Renaud][@ma-renaud] + +Thanks! + + +### [v1.2.0][v1.2.0] (2019-08-10) + +- [added] Add Windows support ([#77][i77]) +- [added] Add support for spaces in commands ([#75][i75]) +- [added] Add support for Fish-based autocompletion ([#71][i71]) +- [added] Add pager support ([#44][i44]) +- [added] Print detected OS with `-v` / `--version` ([#57][i57]) +- [changed] OS detection: Treat BSDs as "osx" ([#58][i58]) +- [changed] Move from curl to reqwest ([#61][i61]) +- [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: + +- [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! ### [v1.1.0][v1.1.0] (2018-10-22) @@ -20,8 +494,9 @@ Possible log types: - [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] @@ -52,15 +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.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 -[i47]: https://github.com/dbrgn/tealdeer/issues/47 -[i48]: https://github.com/dbrgn/tealdeer/issues/48 +[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 db7c19b..4fba42b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,1768 +1,1671 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + [[package]] -name = "adler32" -version = "1.0.3" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "0.6.8" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ - "memchr 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr", ] [[package]] -name = "ansi_term" -version = "0.10.2" +name = "anstream" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] [[package]] -name = "arrayvec" -version = "0.4.7" +name = "anstyle" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ - "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", + "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 = "0.10.2" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" dependencies = [ - "escargot 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "predicates 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "predicates-core 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "predicates-tree 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anstyle", + "bstr", + "doc-comment", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", ] [[package]] -name = "atty" -version = "0.2.11" +name = "autocfg" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" -version = "0.9.3" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ - "byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)", - "safemem 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr", + "regex-automata", + "serde", ] [[package]] -name = "bitflags" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "bitflags" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "build_const" -version = "0.2.1" +name = "bumpalo" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" -version = "1.2.7" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "0.4.11" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "cargo_metadata" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.32 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.0.25" +version = "1.2.40" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "0.1.6" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] -name = "clippy" -version = "0.0.174" +name = "clap" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ - "cargo_metadata 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "clippy_lints 0.0.174 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "clap_builder", + "clap_derive", ] [[package]] -name = "clippy_lints" -version = "0.0.174" +name = "clap_builder" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ - "if_chain 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "itertools 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "pulldown-cmark 0.0.15 (registry+https://github.com/rust-lang/crates.io-index)", - "quine-mc_cluskey 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "semver 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", - "toml 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "anstream", + "anstyle", + "clap_lex", + "terminal_size", ] [[package]] -name = "cloudabi" -version = "0.0.3" +name = "clap_derive" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ - "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", ] [[package]] name = "core-foundation" -version = "0.5.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "core-foundation-sys 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", + "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", ] [[package]] name = "core-foundation-sys" -version = "0.5.1" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "crc" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-deque" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-epoch 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-utils" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "crossbeam-utils" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "difference" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "docopt" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", - "strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "dtoa" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "either" +name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] [[package]] -name = "encoding_rs" -version = "0.8.12" +name = "der" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", ] [[package]] name = "env_logger" -version = "0.5.13" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ - "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", ] [[package]] name = "escargot" -version = "0.3.1" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11c3aea32bc97b500c9ca6a72b768a26e558264303d101d3409cf6d57a9ed0cf" dependencies = [ - "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.32 (registry+https://github.com/rust-lang/crates.io-index)", + "log", + "serde", + "serde_json", ] +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.1", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "filetime" -version = "0.2.1" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ - "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", ] [[package]] -name = "flate2" -version = "1.0.4" +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 = [ - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "miniz-sys 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", - "miniz_oxide_c_api 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "crc32fast", + "libz-rs-sys", + "miniz_oxide", ] [[package]] name = "float-cmp" -version = "0.4.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" dependencies = [ - "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits", ] [[package]] name = "fnv" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] -name = "fuchsia-zircon" +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ - "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" +name = "hashbrown" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] -name = "futures" -version = "0.1.25" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "futures-cpupool" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "getopts" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "h2" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", - "indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "string 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "0.1.13" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ - "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes", + "fnv", + "itoa", ] [[package]] name = "httparse" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "humantime" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "hyper" -version = "0.12.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "h2 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", - "httparse 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-reactor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-tcp 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-threadpool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-timer 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", - "want 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "hyper-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper 0.12.14 (registry+https://github.com/rust-lang/crates.io-index)", - "native-tls 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "idna" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "if_chain" -version = "0.1.3" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "indexmap" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "iovec" -version = "0.1.2" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "equivalent", + "hashbrown", ] [[package]] -name = "itertools" -version = "0.6.5" +name = "is_terminal_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "0.4.3" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] -name = "kernel32-sys" -version = "0.2.2" +name = "jiff" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", ] [[package]] -name = "lazy_static" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "lazy_static" -version = "1.1.0" +name = "jiff-static" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ - "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "lazycell" -version = "1.2.0" +name = "jni" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "libc" -version = "0.2.43" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] -name = "libflate" -version = "0.1.18" +name = "libredox" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)", - "crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "libc", + "redox_syscall", ] [[package]] -name = "lock_api" -version = "0.1.4" +name = "libz-rs-sys" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" dependencies = [ - "owning_ref 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "zlib-rs", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "log" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "matches" -version = "0.1.8" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" -version = "2.1.0" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "memoffset" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "mime" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "unicase 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "mime_guess" -version = "2.0.0-alpha.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "mime 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", - "phf 0.7.23 (registry+https://github.com/rust-lang/crates.io-index)", - "phf_codegen 0.7.23 (registry+https://github.com/rust-lang/crates.io-index)", - "unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "miniz-sys" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "miniz_oxide" -version = "0.2.0" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "miniz_oxide_c_api" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", - "crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "miniz_oxide 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "mio" -version = "0.6.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazycell 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "mio-uds" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "miow" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "adler2", ] [[package]] name = "native-tls" -version = "0.2.2" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ - "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl 0.10.15 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.9.39 (registry+https://github.com/rust-lang/crates.io-index)", - "schannel 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tempfile 3.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", ] -[[package]] -name = "net2" -version = "0.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "nodrop" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "normalize-line-endings" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-traits" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "num_cpus" -version = "1.8.0" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", ] [[package]] -name = "openssl" -version = "0.10.15" +name = "once_cell" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.9.39 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "openssl-probe" -version = "0.1.2" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.39" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ - "cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", - "vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "cc", + "libc", + "pkg-config", + "vcpkg", ] [[package]] -name = "owning_ref" -version = "0.3.3" +name = "pager" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2599211a5c97fbbb1061d3dc751fa15f404927e4846e07c643287d6d1f462880" dependencies = [ - "stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "errno 0.2.8", + "libc", ] [[package]] -name = "parking_lot" -version = "0.6.4" +name = "pem-rfc7468" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ - "lock_api 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "parking_lot_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "parking_lot_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "smallvec 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "base64ct", ] [[package]] name = "percent-encoding" -version = "1.0.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "phf" -version = "0.7.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "phf_shared 0.7.23 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "phf_codegen" -version = "0.7.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "phf_generator 0.7.23 (registry+https://github.com/rust-lang/crates.io-index)", - "phf_shared 0.7.23 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "phf_generator" -version = "0.7.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "phf_shared 0.7.23 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "phf_shared" -version = "0.7.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pkg-config" -version = "0.3.14" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] [[package]] name = "predicates" -version = "1.0.0" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ - "difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "float-cmp 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "normalize-line-endings 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "predicates-core 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", ] [[package]] name = "predicates-core" -version = "1.0.0" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" -version = "1.0.0" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ - "predicates-core 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "treeline 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "predicates-core", + "termtree", ] [[package]] name = "proc-macro2" -version = "0.4.20" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ - "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-ident", ] -[[package]] -name = "pulldown-cmark" -version = "0.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", - "getopts 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "quick-error" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "quine-mc_cluskey" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "quote" -version = "0.6.8" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ - "proc-macro2 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", ] [[package]] -name = "rand" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand_core 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand_core" -version = "0.3.0" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "redox_syscall" -version = "0.1.40" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] [[package]] -name = "redox_termios" +name = "regex" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "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.23.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be59af91596cac372a6942530653ad0c3a246cdd491aaa9dcaee47f88d67d5a0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] -name = "regex" -version = "0.2.11" +name = "rustls-webpki" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ - "aho-corasick 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "utf8-ranges 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "regex" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "aho-corasick 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "utf8-ranges 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "regex-syntax" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "regex-syntax" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "regex-syntax" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "remove_dir_all" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "reqwest" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "encoding_rs 0.8.12 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper 0.12.14 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper-tls 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libflate 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "mime 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", - "mime_guess 2.0.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)", - "native-tls 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.32 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_urlencoded 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", - "uuid 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] name = "ryu" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "safemem" -version = "0.3.0" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" -version = "1.0.3" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "winapi-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-util", ] [[package]] name = "schannel" -version = "0.1.14" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "windows-sys 0.61.1", ] [[package]] -name = "scopeguard" -version = "0.3.3" +name = "security-framework" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] [[package]] name = "security-framework" -version = "0.2.1" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "core-foundation 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "core-foundation-sys 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", ] [[package]] name = "security-framework-sys" -version = "0.2.1" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ - "core-foundation-sys 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys", + "libc", ] -[[package]] -name = "semver" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "serde" -version = "1.0.80" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ - "serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", + "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.80" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ - "proc-macro2 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.15.13 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "serde_json" -version = "1.0.32" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "ryu 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", ] [[package]] -name = "serde_urlencoded" -version = "0.5.3" +name = "serde_spanned" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ - "dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde", ] [[package]] -name = "siphasher" -version = "0.2.3" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "slab" -version = "0.4.1" +name = "simd-adler32" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] -name = "smallvec" -version = "0.6.6" +name = "socks" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" dependencies = [ - "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder", + "libc", + "winapi", ] [[package]] -name = "stable_deref_trait" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "string" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "strsim" -version = "0.6.0" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "0.15.13" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ - "proc-macro2 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tar" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "filetime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", - "xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] name = "tealdeer" -version = "1.1.0" +version = "1.8.1" dependencies = [ - "ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", - "assert_cmd 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", - "clippy 0.0.174 (registry+https://github.com/rust-lang/crates.io-index)", - "docopt 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", - "env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)", - "escargot 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "flate2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "predicates 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "reqwest 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", - "tar 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)", - "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", - "toml 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "utime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "walkdir 2.2.5 (registry+https://github.com/rust-lang/crates.io-index)", - "xdg 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tempdir" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow", + "assert_cmd", + "clap", + "env_logger", + "escargot", + "etcetera", + "filetime", + "log", + "pager", + "predicates", + "serde", + "serde_derive", + "tempfile", + "toml", + "ureq", + "yansi", + "zip", ] [[package]] name = "tempfile" -version = "3.0.4" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ - "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", - "remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.61.1", ] [[package]] -name = "termcolor" -version = "1.0.4" +name = "terminal_size" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rustix", + "windows-sys 0.60.2", ] [[package]] -name = "termion" -version = "1.5.1" +name = "termtree" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror-impl", ] [[package]] -name = "thread_local" -version = "0.3.6" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "time" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-current-thread 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-fs 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-reactor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-tcp 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-threadpool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-timer 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-udp 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-uds 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-codec" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-current-thread" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-executor" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-fs" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-threadpool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-io" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-reactor" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-utils 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parking_lot 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-tcp" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-reactor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-threadpool" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-deque 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-timer" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-utils 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-udp" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-reactor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "tokio-uds" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", - "mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-reactor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "toml" -version = "0.4.8" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ - "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", ] [[package]] -name = "treeline" -version = "0.1.0" +name = "toml_datetime" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] [[package]] -name = "try-lock" +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" +dependencies = [ + "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 = "ureq-proto" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "ucd-util" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "unicase" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "unicase" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "unicode-normalization" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "unicode-width" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "unicode-xid" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "unreachable" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "url" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "utf8-ranges" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "utime" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "uuid" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vcpkg" -version = "0.2.6" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] -name = "version_check" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "void" -version = "1.0.2" +name = "wait-timeout" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] [[package]] name = "walkdir" -version = "2.2.5" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ - "same-file 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "same-file", + "winapi-util", ] [[package]] -name = "want" -version = "0.0.6" +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ - "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", ] [[package]] name = "winapi" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "winapi" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.1" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "windows-sys 0.61.1", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "wincolor" +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.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 = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532" dependencies = [ - "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "memchr", + "zopfli", ] [[package]] -name = "ws2_32-sys" -version = "0.2.1" +name = "zlib-rs" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" [[package]] -name = "xattr" -version = "0.2.2" +name = "zopfli" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" dependencies = [ - "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", + "bumpalo", + "crc32fast", + "log", + "simd-adler32", ] - -[[package]] -name = "xdg" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[metadata] -"checksum adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c" -"checksum aho-corasick 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "68f56c7353e5a9547cbd76ed90f7bb5ffc3ba09d4ea9bd1d8c06c8b1142eeb5a" -"checksum ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b3568b48b7cefa6b8ce125f9bb4989e52fbcc29ebea88df04cc7c5f12f70455" -"checksum arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a1e964f9e24d588183fcb43503abda40d288c8657dfc27311516ce2f05675aef" -"checksum assert_cmd 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b7ac5c260f75e4e4ba87b7342be6edcecbcb3eb6741a0507fda7ad115845cc65" -"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" -"checksum base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" -"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" -"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" -"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" -"checksum byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "94f88df23a25417badc922ab0f5716cc1330e87f71ddd9203b3a3ccd9cedf75d" -"checksum bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "40ade3d27603c2cb345eb0912aec461a6dec7e06a4ae48589904e808335c7afa" -"checksum cargo_metadata 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "be1057b8462184f634c3a208ee35b0f935cfd94b694b26deadccd98732088d7b" -"checksum cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "f159dfd43363c4d08055a07703eb7a3406b0dac4d0584d96965a3262db3c9d16" -"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4" -"checksum clippy 0.0.174 (registry+https://github.com/rust-lang/crates.io-index)" = "1b03ded6eba74b16dbeb598be58e874bc72f6cf12f7ccc577b328091e1d4392a" -"checksum clippy_lints 0.0.174 (registry+https://github.com/rust-lang/crates.io-index)" = "9b8e508648d6d41040e0061f45fc5562b3af5c8a7d67853f15841fb531c8e984" -"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -"checksum core-foundation 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "286e0b41c3a20da26536c6000a280585d519fd07b3956b43aed8a79e9edce980" -"checksum core-foundation-sys 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "716c271e8613ace48344f723b60b900a93150271e5be206212d052bbc0883efa" -"checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" -"checksum crossbeam-deque 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4fe1b6f945f824c7a25afe44f62e25d714c0cc523f8e99d8db5cd1026e1269d3" -"checksum crossbeam-epoch 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2449aaa4ec7ef96e5fb24db16024b935df718e9ae1cec0a1e68feeca2efca7b8" -"checksum crossbeam-utils 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "677d453a17e8bd2b913fa38e8b9cf04bcdbb5be790aa294f2389661d72036015" -"checksum crossbeam-utils 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c55913cc2799171a550e307918c0a360e8c16004820291bf3b638969b4a01816" -"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" -"checksum docopt 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d8acd393692c503b168471874953a2531df0e9ab77d0b6bbc582395743300a4a" -"checksum dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d301140eb411af13d3115f9a562c85cc6b541ade9dfa314132244aaee7489dd" -"checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0" -"checksum encoding_rs 0.8.12 (registry+https://github.com/rust-lang/crates.io-index)" = "ca20350a7cb5aab5b9034731123d6d412caf3e92d4985e739e411ba0955fd0eb" -"checksum env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)" = "15b0a4d2e39f8420210be8b27eeda28029729e2fd4291019455016c348240c38" -"checksum escargot 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "19db1f7e74438642a5018cdf263bb1325b2e792f02dd0a3ca6d6c0f0d7b1d5a5" -"checksum filetime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "da4b9849e77b13195302c174324b5ba73eec9b236b24c221a61000daefb95c5f" -"checksum flate2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3b0c7353385f92079524de3b7116cf99d73947c08a7472774e9b3b04bff3b901" -"checksum float-cmp 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "134a8fa843d80a51a5b77d36d42bc2def9edcb0262c914861d08129fd1926600" -"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" -"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" -"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" -"checksum futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "49e7653e374fe0d0c12de4250f0bdb60680b8c80eed558c5c7538eec9c89e21b" -"checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" -"checksum getopts 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "0a7292d30132fb5424b354f5dc02512a86e4c516fe544bb7a25e7f266951b797" -"checksum h2 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "7dd33bafe2e6370e6c8eb0cf1b8c5f93390b90acde7e9b03723f166b28b648ed" -"checksum http 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "24f58e8c2d8e886055c3ead7b28793e1455270b5fb39650984c224bc538ba581" -"checksum httparse 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e8734b0cfd3bc3e101ec59100e101c2eecd19282202e87808b3037b442777a83" -"checksum humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0484fda3e7007f2a4a0d9c3a703ca38c71c54c55602ce4660c419fd32e188c9e" -"checksum hyper 0.12.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2f60ae467ef4fc5eba9a34d31648c9c8ed902faf45a217f6734ce9ea64779ac7" -"checksum hyper-tls 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "32cd73f14ad370d3b4d4b7dce08f69b81536c82e39fcc89731930fe5788cd661" -"checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" -"checksum if_chain 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4bac95d9aa0624e7b78187d6fb8ab012b41d9f6f54b1bcb61e61c4845f8357ec" -"checksum indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7e81a7c05f79578dbc15793d8b619db9ba32b4577003ef3af1a91c416798c58d" -"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" -"checksum itertools 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d3f2be4da1690a039e9ae5fd575f706a63ad5a2120f161b1d653c9da3930dd21" -"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" -"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -"checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" -"checksum lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca488b89a5657b0a2ecd45b95609b3e848cf1755da332a0da46e2b2b1cb371a7" -"checksum lazycell 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ddba4c30a78328befecec92fc94970e53b3ae385827d28620f0f5bb2493081e0" -"checksum libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)" = "76e3a3ef172f1a0b9a9ff0dd1491ae5e6c948b94479a3021819ba7d860c8645d" -"checksum libflate 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "21138fc6669f438ed7ae3559d5789a5f0ba32f28c1f0608d1e452b0bb06ee936" -"checksum lock_api 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "775751a3e69bde4df9b38dd00a1b5d6ac13791e4223d4a0506577f0dd27cfb7a" -"checksum log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fcce5fa49cc693c312001daf1d13411c4a5283796bac1084299ea3e567113f" -"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" -"checksum memchr 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4b3629fe9fdbff6daa6c33b90f7c08355c1aca05a3d01fa8063b822fcf185f3b" -"checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3" -"checksum mime 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)" = "0a907b83e7b9e987032439a387e187119cddafc92d5c2aaeb1d92580a793f630" -"checksum mime_guess 2.0.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)" = "30de2e4613efcba1ec63d8133f344076952090c122992a903359be5a4f99c3ed" -"checksum miniz-sys 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "0300eafb20369952951699b68243ab4334f4b10a88f411c221d444b36c40e649" -"checksum miniz_oxide 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5ad30a47319c16cde58d0314f5d98202a80c9083b5f61178457403dfb14e509c" -"checksum miniz_oxide_c_api 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "28edaef377517fd9fe3e085c37d892ce7acd1fbeab9239c5a36eec352d8a8b7e" -"checksum mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)" = "71646331f2619b1026cc302f87a2b8b648d5c6dd6937846a16cc8ce0f347f432" -"checksum mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125" -"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" -"checksum native-tls 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ff8e08de0070bbf4c31f452ea2a70db092f36f6f2e4d897adf5674477d488fb2" -"checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" -"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" -"checksum normalize-line-endings 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2e0a1a39eab95caf4f5556da9289b9e68f0aafac901b2ce80daaf020d3b733a8" -"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" -"checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30" -"checksum openssl 0.10.15 (registry+https://github.com/rust-lang/crates.io-index)" = "5e1309181cdcbdb51bc3b6bedb33dfac2a83b3d585033d3f6d9e22e8c1928613" -"checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" -"checksum openssl-sys 0.9.39 (registry+https://github.com/rust-lang/crates.io-index)" = "278c1ad40a89aa1e741a1eed089a2f60b18fab8089c3139b542140fc7d674106" -"checksum owning_ref 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cdf84f41639e037b484f93433aa3897863b561ed65c6e59c7073d7c561710f37" -"checksum parking_lot 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f0802bff09003b291ba756dc7e79313e51cc31667e94afbe847def490424cde5" -"checksum parking_lot_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ad7f7e6ebdc79edff6fdcb87a55b620174f7a989e3eb31b65231f4af57f00b8c" -"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" -"checksum phf 0.7.23 (registry+https://github.com/rust-lang/crates.io-index)" = "cec29da322b242f4c3098852c77a0ca261c9c01b806cae85a5572a1eb94db9a6" -"checksum phf_codegen 0.7.23 (registry+https://github.com/rust-lang/crates.io-index)" = "7d187f00cd98d5afbcd8898f6cf181743a449162aeb329dcd2f3849009e605ad" -"checksum phf_generator 0.7.23 (registry+https://github.com/rust-lang/crates.io-index)" = "03dc191feb9b08b0dc1330d6549b795b9d81aec19efe6b4a45aec8d4caee0c4b" -"checksum phf_shared 0.7.23 (registry+https://github.com/rust-lang/crates.io-index)" = "b539898d22d4273ded07f64a05737649dc69095d92cb87c7097ec68e3f150b93" -"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" -"checksum predicates 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fa984b7cd021a0bf5315bcce4c4ae61d2a535db2a8d288fc7578638690a7b7c3" -"checksum predicates-core 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "06075c3a3e92559ff8929e7a280684489ea27fe44805174c3ebd9328dcb37178" -"checksum predicates-tree 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8e63c4859013b38a76eca2414c64911fba30def9e3202ac461a2d22831220124" -"checksum proc-macro2 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)" = "3d7b7eaaa90b4a90a932a9ea6666c95a389e424eff347f0f793979289429feee" -"checksum pulldown-cmark 0.0.15 (registry+https://github.com/rust-lang/crates.io-index)" = "378e941dbd392c101f2cb88097fa4d7167bc421d4b88de3ff7dbee503bc3233b" -"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" -"checksum quine-mc_cluskey 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "07589615d719a60c8dd8a4622e7946465dfef20d1a428f969e3443e7386d5f45" -"checksum quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "dd636425967c33af890042c483632d33fa7a18f19ad1d7ea72e8998c6ef8dea5" -"checksum rand 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8356f47b32624fef5b3301c1be97e5944ecdd595409cc5da11d05f211db6cfbd" -"checksum rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e464cd887e869cddcae8792a4ee31d23c7edd516700695608f5b98c67ee0131c" -"checksum rand_core 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1961a422c4d189dfb50ffa9320bf1f2a9bd54ecb92792fb9477f99a1045f3372" -"checksum rand_core 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0905b6b7079ec73b314d4c748701f6931eb79fd97c668caa3f1899b22b32c6db" -"checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" -"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" -"checksum regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" -"checksum regex 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2069749032ea3ec200ca51e4a31df41759190a88edca0d2d86ee8bedf7073341" -"checksum regex-syntax 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8e931c58b93d86f080c734bfd2bce7dd0079ae2331235818133c8be7f422e20e" -"checksum regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" -"checksum regex-syntax 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "747ba3b235651f6e2f67dfa8bcdcd073ddb7c243cb21c442fc12395dfcac212d" -"checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" -"checksum reqwest 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ab52e462d1e15891441aeefadff68bdea005174328ce3da0a314f2ad313ec837" -"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -"checksum ryu 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7153dd96dade874ab973e098cb62fcdbb89a03682e46b144fd09550998d4a4a7" -"checksum safemem 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dca453248a96cb0749e36ccdfe2b0b4e54a61bfef89fb97ec621eb8e0a93dd9" -"checksum same-file 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "10f7794e2fda7f594866840e95f5c5962e886e228e68b6505885811a94dd728c" -"checksum schannel 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "0e1a231dc10abf6749cfa5d7767f25888d484201accbd919b66ab5413c502d56" -"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" -"checksum security-framework 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "697d3f3c23a618272ead9e1fb259c1411102b31c6af8b93f1d64cca9c3b0e8e0" -"checksum security-framework-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ab01dfbe5756785b5b4d46e0289e5a18071dfa9a7c2b24213ea00b9ef9b665bf" -"checksum semver 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a3186ec9e65071a2095434b1f5bb24838d4e8e130f584c790f6033c79943537" -"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -"checksum serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)" = "15c141fc7027dd265a47c090bf864cf62b42c4d228bbcf4e51a0c9e2b0d3f7ef" -"checksum serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)" = "225de307c6302bec3898c51ca302fc94a7a1697ef0845fcee6448f33c032249c" -"checksum serde_json 1.0.32 (registry+https://github.com/rust-lang/crates.io-index)" = "43344e7ce05d0d8280c5940cabb4964bea626aa58b1ec0e8c73fa2a8512a38ce" -"checksum serde_urlencoded 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "aaed41d9fb1e2f587201b863356590c90c1157495d811430a0c0325fe8169650" -"checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" -"checksum slab 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5f9776d6b986f77b35c6cf846c11ad986ff128fe0b2b63a3628e3755e8d3102d" -"checksum smallvec 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "622df2d454c29a4d89b30dc3b27b42d7d90d6b9e587dbf8f67652eb7514da484" -"checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" -"checksum string 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00caf261d6f90f588f8450b8e1230fa0d5be49ee6140fdfbcb55335aff350970" -"checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694" -"checksum syn 0.15.13 (registry+https://github.com/rust-lang/crates.io-index)" = "7b4439ee8325b4e4b57e59309c3724c9a4478eaeb4eb094b6f3fac180a3b2876" -"checksum tar 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)" = "83b0d14b53dbfd62681933fadd651e815f99e6084b649e049ab99296e05ab3de" -"checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" -"checksum tempfile 3.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "55c1195ef8513f3273d55ff59fe5da6940287a0d7a98331254397f464833675b" -"checksum termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4096add70612622289f2fdcdbd5086dc81c1e2675e6ae58d6c4f62a16c6d7f2f" -"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" -"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" -"checksum time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" -"checksum tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "6e93c78d23cc61aa245a8acd2c4a79c4d7fa7fb5c3ca90d5737029f043a84895" -"checksum tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5c501eceaf96f0e1793cf26beb63da3d11c738c4a943fdf3746d81d64684c39f" -"checksum tokio-current-thread 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f90fcd90952f0a496d438a976afba8e5c205fb12123f813d8ab3aa1c8436638c" -"checksum tokio-executor 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "c117b6cf86bb730aab4834f10df96e4dd586eff2c3c27d3781348da49e255bde" -"checksum tokio-fs 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "60ae25f6b17d25116d2cba342083abe5255d3c2c79cb21ea11aa049c53bf7c75" -"checksum tokio-io 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "7392fe0a70d5ce0c882c4778116c519bd5dbaa8a7c3ae3d04578b3afafdcda21" -"checksum tokio-reactor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "4b26fd37f1125738b2170c80b551f69ff6fecb277e6e5ca885e53eec2b005018" -"checksum tokio-tcp 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7ad235e9dadd126b2d47f6736f65aa1fdcd6420e66ca63f44177bc78df89f912" -"checksum tokio-threadpool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3929aee321c9220ed838ed6c3928be7f9b69986b0e3c22c972a66dbf8a298c68" -"checksum tokio-timer 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3a52f00c97fedb6d535d27f65cccb7181c8dd4c6edc3eda9ea93f6d45d05168e" -"checksum tokio-udp 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "da941144b816d0dcda4db3a1ba87596e4df5e860a72b70783fe435891f80601c" -"checksum tokio-uds 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "df195376b43508f01570bacc73e13a1de0854dc59e79d1ec09913e8db6dd2a70" -"checksum toml 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "4a2ecc31b0351ea18b3fe11274b8db6e4d82bce861bbb22e6dbed40417902c65" -"checksum treeline 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" -"checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" -"checksum ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd2be2d6639d0f8fe6cdda291ad456e23629558d466e2789d2c3e9892bda285d" -"checksum unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" -"checksum unicase 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9d3218ea14b4edcaccfa0df0a64a3792a2c32cc706f1b336e48867f9d3147f90" -"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" -"checksum unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "6a0180bc61fc5a987082bfa111f4cc95c4caff7f9799f3e46df09163a937aa25" -"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" -"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" -"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" -"checksum url 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2a321979c09843d272956e73700d12c4e7d3d92b2ee112b31548aef0d4efc5a6" -"checksum utf8-ranges 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd70f467df6810094968e2fce0ee1bd0e87157aceb026a8c083bcf5e25b9efe4" -"checksum utime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "055058552ca15c566082fc61da433ae678f78986a6f16957e33162d1b218792a" -"checksum uuid 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dab5c5526c5caa3d106653401a267fed923e7046f35895ffcb5ca42db64942e6" -"checksum vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "def296d3eb3b12371b2c7d0e83bfe1403e4db2d7a0bba324a12b21c4ee13143d" -"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" -"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" -"checksum walkdir 2.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "af464bc7be7b785c7ac72e266a6b67c4c9070155606f51655a650a6686204e35" -"checksum want 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "797464475f30ddb8830cc529aaaae648d581f99e2036a928877dfde027ddf6b3" -"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" -"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" -"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" -"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -"checksum winapi-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "afc5508759c5bf4285e61feb862b6083c8480aec864fa17a81fdec6f69b461ab" -"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -"checksum wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "561ed901ae465d6185fa7864d63fbd5720d0ef718366c9a4dc83cf6170d7e9ba" -"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" -"checksum xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" -"checksum xdg 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a66b7c2281ebde13cf4391d70d4c7e5946c3c25e72a7b859ca8f677dcd0b0c61" diff --git a/Cargo.toml b/Cargo.toml index d4deb05..1d98992 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,44 +1,61 @@ [package] -authors = ["Danilo Bargen "] +authors = [ + "Danilo Bargen ", + "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/" -version = "1.1.0" -include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "/bash_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.10.2" -clippy = { version = "0.0.174", optional = true } -docopt = "0.8.1" -env_logger = { version = "0.5", optional = true } -flate2 = "1.0" +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" serde = "1.0.21" serde_derive = "1.0.21" -tar = "0.4.14" -time = "0.1.38" -toml = "0.4.6" -walkdir = "2.0.1" -xdg = "2.1.0" -reqwest = "0.9.5" +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" [dev-dependencies] -assert_cmd = "0.10" -escargot = "0.3" -predicates = "1.0" -tempdir = "^0.3" -utime = "0.2.0" +assert_cmd = "2.0.1" +escargot = "0.5" +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/LICENSE-MIT b/LICENSE-MIT index 2249a66..1c952f1 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,4 +1,4 @@ -Copyright (C) 2015-2018 Danilo Bargen and contributors +Copyright (C) 2015-2021 Danilo Bargen and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index 6c4fa13..859d06f 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,28 @@ # tealdeer -![teal deer](deer.png) +![teal deer](docs/src/deer.png) -|Crate|Linux|macOS| -|:---:|:---:|:---:| -|[![Crates.io][crates-io-badge]][crates-io]|[![Circle CI][circle-ci-badge]][circle-ci]|[![Travis CI][travis-ci-badge]][travis-ci]| +|Crate|CI (Linux/macOS/Windows)| +|:---:|:---:| +|[![Crates.io][crates-io-badge]][crates-io]|[![GitHub CI][github-actions-badge]][github-actions]| A very fast implementation of [tldr](https://github.com/tldr-pages/tldr) in Rust: Simplified, example based and community-driven man pages. -Screenshot of tldr command +Screenshot of tldr command 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 ! + +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. ## Goals @@ -23,103 +31,40 @@ High level project goals: - [x] Download and cache pages - [x] Don't require a network connection for anything besides updating the cache -- [x] Command line interface similar or equivalent to the [NodeJS client][tldr-node-client] +- [x] Command line interface similar or equivalent to the [NodeJS client][node-gh] +- [x] Comply with the [tldr client specification][client-spec] +- [x] Advanced highlighting and configuration - [x] Be fast -A tool like `tldr` should be as frictionless as possible to use. It should be -easy to invoke (just `tldr tar`, not using another subcommand like `tldr find -tar`) and it should show the output as fast as possible. +A tool like `tldr` should be as frictionless as possible to use and show the +output as fast as possible. -tealdeer reaches these goals. During a (highly non-scientific) test (see -[#38](https://github.com/dbrgn/tealdeer/issues/38) for details), I tested the -invocation speed of `tldr ` for a few of the existing clients: +We think that `tealdeer` reaches these goals. We put together a (more or less) +reproducible benchmark that compiles a handful of clients from source and +measures the execution times on a cold disk cache. The benchmarking is run in a +Docker container using sharkdp's [`hyperfine`][hyperfine-gh] +([Dockerfile][benchmark-dockerfile]). -| Client | Times (ms) | Avg of 5 (ms) | -| --- | --- | --- | -| [Tealdeer](https://github.com/dbrgn/tealdeer/) | `15/11/5/5/11` | `9.4` (100%) | -| [C client](https://github.com/tldr-pages/tldr-cpp-client) | `11/5/12/11/15` | `10.8` (115%) | -| [Bash client](https://github.com/pepa65/tldr-bash-client) | `15/19/22/25/24` | `21.0` (223%) | -| [Go client by k3mist](https://github.com/k3mist/tldr/) | `98/96/100/95/101` | `98.8` (1'051%) | -| [Python client](https://github.com/lord63/tldr.py) | `152/148/151/158/140` | `149.8` (1'594%) | -| [NodeJS client](https://github.com/tldr-pages/tldr-node-client) | `169/171/170/170/170` | `170.0` (1'809%) | +| Client (50 runs, 17.10.2021) | Programming Language | Mean in ms | Deviation in ms | Comments | +| :---: | :---: | :---: | :---: | :---: | +| [`outfieldr`][outfieldr-gh] | Zig | 9.1 | 0.5 | no user configuration | +| `tealdeer` | Rust | 13.2 | 0.5 | | +| [`fast-tldr`][fast-tldr-gh] | Haskell | 17.0 | 0.6 | no example highlighting | +| [`tldr-hs`][hs-gh] | Haskell | 25.1 | 0.5 | no example highlighting | +| [`tldr-bash`][bash-gh] | Bash | 30.0 | 0.8 | | +| [`tldr-python-client`][python-gh] | Python | 87.0 | 2.4 | | +| [`tldr-node-client`][node-gh] | JavaScript / NodeJS | 407.1 | 12.9 | | -tealdeer was the winner here, although the C client and the Bash client are in -the same speed class. Interpreted languages are clearly much slower to invoke, -a delay of 170 milliseconds is definitely noticeable and increases friction for -the user. +As you can see, `tealdeer` is one of the fastest of the tested clients. +However, we strive for useful features and code quality over raw performance, +even if that means that we don't come out on top in this friendly competition. +That said, we are still optimizing the code, for example when the `outfieldr` +developers [suggested to switch][outfieldr-comment-tls] to a native TLS +implementation instead of the native libraries. -These are the clients I tried but failed to compile or run: -[Haskell client](https://github.com/psibi/tldr-hs), -[Ruby client](https://github.com/YellowApple/tldrb), -[Perl client](https://github.com/skaji/perl-tldr), -[Go client by anoopengineer](https://github.com/anoopengineer/tldr/), -[PHP client](https://github.com/BrainMaestro/tldr-php). +## Development - -## Usage - - tldr [options] - tldr [options] - - Options: - - -h --help Show this screen - -v --version Show version information - -l --list List all commands in the cache - -f --render Render a specific markdown file - -o --os Override the operating system [linux, osx, sunos] - -u --update Update the local cache - -c --clear-cache Clear the local cache - -q --quiet Suppress informational messages - --config-path Show config file path - --seed-config Create a basic config - - Examples: - - $ tldr tar - $ tldr --list - - To control the cache: - - $ tldr --update - $ tldr --clear-cache - - To render a local file (for testing): - - $ tldr --render /path/to/file.md - - -## Installing - -### Static Binaries (Linux) - -Static binary builds (currently for Linux only) are available on the -[GitHub releases page](https://github.com/dbrgn/tealdeer/releases). -Simply download the binary for your platform and run it! - -Builds for other platforms are planned. - -### Cargo Install (any platform) - -Build and install the tool via cargo... - - $ cargo install tealdeer - -### From Package Manager - -tealdeer has been added to a few package managers: - -- Arch Linux AUR: [`tealdeer`](https://aur.archlinux.org/packages/tealdeer/) - or [`tealdeer-git`](https://aur.archlinux.org/packages/tealdeer-git/) -- macOS Homebrew: [`tealdeer`](https://formulae.brew.sh/formula/tealdeer) -- Nix: [`tealdeer`](https://nixos.org/nixos/packages.html#tealdeer) -- Void Linux XBPS: [`tealdeer`](https://github.com/void-linux/void-packages/tree/master/srcpkgs/tealdeer) - -### From Source (any platform) - -tealdeer requires at least Rust 1.31. - -Debug build with logging enabled: +Creating a debug build with logging enabled: $ cargo build --features logging @@ -131,60 +76,6 @@ To enable the log output, set the `RUST_LOG` env variable: $ export RUST_LOG=tldr=debug - -## Configuration - -The tldr command can be customized with a config file called `config.toml`. -Creating the config file can be done manually or with the help of tldr: - - $ tldr --seed-config - -The configuration file path follows OS conventions. It can be queried with the following command: - - $ tldr --config-path - -### Style - -Using the config file, the style (e.g. colors or underlines) can be customized. - -Possible styles: - -- `description`: The initial description text -- `command_name`: The command name as part of the example code -- `example_text`: The text that describes an example -- `example_code`: The example itself, except the `command_name` and `example_variable` -- `example_variable`: The variables in the example - -Currently supported attributes: - -- `foreground` (color string, see below) -- `background` (color string, see below) -- `underline` (`true` or `false`) -- `bold` (`true` or `false`) - -The currently supported colors are: - -- `black` -- `red` -- `green` -- `yellow` -- `blue` -- `purple` -- `cyan` -- `white` - -Example customization: - -Screenshot of customized version - - -## Autocompletion - -- *Bash*: copy `bash_tealdeer` to `/usr/share/bash-completion/completions/tldr` -- *Fish*: copy `fish_tealdeer` to `~/.config/fish/completions/tldr.fish` - -## Development - To run tests: $ cargo test @@ -195,6 +86,23 @@ To run lints: $ cargo clean && cargo clippy +### AI Policy + +Using AI is generally discouraged. However, if it is used as part of a contribution, the contributor MUST: + +1. Clearly mark what parts (if any) of a contribution were created with the help of AI tools. This includes issue and pull request comments. +2. Check all output of AI tools before sharing it with others in the tealdeer project. +3. Not post slop, spam, or low quality contributions. This includes pull request descriptions and comments with excessive text and markdown flair. +4. Leave small or easy tasks to new contributors who want to learn without the use of AI. This is to maintain the presence of the `good-first-issue` tag. +5. Be respectful of everyone's time: *maintainers and other contributors will be reviewing your PRs.* + + +## MSRV (Minimally Supported Rust Version) + +When publishing a tealdeer release, the Rust version required to build it +should be stable for at least a month. + + ## License Licensed under either of @@ -211,15 +119,23 @@ Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. -Thanks to @SShrike for coming up with the name "tealdeer"! +Thanks to @severen for coming up with the name "tealdeer"! -[tldr-node-client]: https://github.com/tldr-pages/tldr-node-client +[node-gh]: https://github.com/tldr-pages/tldr-node-client +[hs-gh]: https://github.com/psibi/tldr-hs +[fast-tldr-gh]: https://github.com/gutjuri/fast-tldr +[bash-gh]: https://4e4.win/tldr +[outfieldr-gh]: https://gitlab.com/ve-nt/outfieldr +[python-gh]: https://github.com/tldr-pages/tldr-python-client + +[benchmark-dockerfile]: https://github.com/tealdeer-rs/tealdeer/blob/main/benchmarks/Dockerfile +[client-spec]: https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md +[hyperfine-gh]: https://github.com/sharkdp/hyperfine +[outfieldr-comment-tls]: https://github.com/tealdeer-rs/tealdeer/issues/129#issuecomment-833596765 -[circle-ci]: https://circleci.com/gh/dbrgn/tealdeer/tree/master -[circle-ci-badge]: https://circleci.com/gh/dbrgn/tealdeer/tree/master.svg?style=shield -[travis-ci]: https://travis-ci.org/dbrgn/tealdeer -[travis-ci-badge]: https://travis-ci.org/dbrgn/tealdeer.svg?branch=master +[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 aed6a9b..f182519 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -7,12 +7,16 @@ 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 + $ cargo update -p tealdeer + +Update docs: + + $ cargo run -- --help > docs/src/usage.txt Update changelog: @@ -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/bash_tealdeer b/bash_tealdeer deleted file mode 100644 index 64efb08..0000000 --- a/bash_tealdeer +++ /dev/null @@ -1,30 +0,0 @@ -# tealdeer bash completion - -_tealdeer() -{ - local cur prev words cword - _init_completion || return - - case $prev in - -h|--help|-v|--version|-l|--list|-u|--update|-c|--clear-cache|--config-path|--seed-config|-q|--quiet) - return - ;; - -f|--render) - _filedir - return - ;; - -o|--os) - COMPREPLY=( $(compgen -W 'linux osx sunos' -- "${cur}") ) - return - ;; - esac - - if [[ $cur == -* ]]; then - COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) ) - return - fi - - COMPREPLY=( $(compgen -W '$( tldr -l | tr -d , )' -- "${cur}") ) -} - -complete -F _tealdeer tldr diff --git a/benchmarks/Dockerfile b/benchmarks/Dockerfile new file mode 100644 index 0000000..2a36fe0 --- /dev/null +++ b/benchmarks/Dockerfile @@ -0,0 +1,139 @@ +# Benchmark Dockerfile for tealdeer +# +# To run the benchmarks, execute +# +# docker build --pull -t tldr-benchmark . +# docker run --privileged --rm -it tldr-benchmark +# +# as root in the directory of this Dockerfile. This will build the compared +# clients and benchmark them with `hyperfine` at the end. +# +# The `--privileged` flag is needed to drop the disk caches before every run. If +# you want to test with hot caches or don't want to use this flag, you will have +# to remove the `--prepare` line from the `hyperfine` command at the end of this +# file and rebuild the image. + +################################################################################ + +FROM rust AS tealdeer-builder + +WORKDIR /build +RUN git clone https://github.com/tealdeer-rs/tealdeer.git \ + && cd tealdeer \ + && cargo build --release \ + && mkdir /build-outputs \ + && cp target/release/tldr /build-outputs/tealdeer + +################################################################################ + +FROM ubuntu:latest AS tldr-c-builder + +WORKDIR /build +RUN apt-get update && apt-get install -y build-essential git && rm -rf /var/lib/apt/lists/* +RUN git clone https://github.com/tldr-pages/tldr-c-client.git \ + && cd tldr-c-client \ + && DEBIAN_FRONTEND=noninteractive ./deps.sh \ + && make \ + && mkdir /build-outputs /deps \ + && cp tldr /build-outputs/tldr-c \ + && cp deps.sh /deps/tldr-c-deps.sh + +################################################################################ + +FROM haskell AS haskell-builder + +WORKDIR /build + +RUN git clone https://github.com/psibi/tldr-hs.git \ + && cd tldr-hs \ + && stack build --install-ghc + +RUN git clone https://github.com/gutjuri/fast-tldr \ + && cd fast-tldr \ + && stack build --install-ghc + +RUN mkdir /build-outputs \ + && find tldr-hs/.stack-work/dist -type f -iname tldr -exec mv '{}' /build-outputs/tldr-hs \; \ + && find fast-tldr/.stack-work/dist -type f -iname tldr -exec mv '{}' /build-outputs/fast-tldr \; + +################################################################################ + +FROM node:slim AS node-builder + +WORKDIR /build-outputs +RUN npm install tldr \ + && cp $(which node) . \ + && echo './node -- ./node_modules/.bin/tldr "$@"' > tldr-node \ + && chmod +x tldr-node + +################################################################################ + +FROM euantorano/zig:0.8.0 AS zig-builder + +WORKDIR /build +RUN apk add git \ + && git clone https://gitlab.com/ve-nt/outfieldr.git \ + && cd outfieldr \ + && git submodule init \ + && git submodule update \ + && zig build -Drelease-safe \ + && mkdir /build-outputs \ + && cp bin/tldr /build-outputs/outfieldr + +################################################################################ + +FROM ubuntu:latest AS benchmark + +ENV LANG="en_US.UTF-8" + +WORKDIR /deps +RUN apt-get update && apt-get install -y wget unzip python3 python3-venv && rm -rf /var/lib/apt/lists/* +COPY --from=tldr-c-builder /deps/* ./ +RUN for file in *; do DEBIAN_FRONTEND=noninteractive sh $file; done + +WORKDIR /clients +COPY --from=tealdeer-builder /build-outputs/* ./ +COPY --from=tldr-c-builder /build-outputs/* ./ +COPY --from=haskell-builder /build-outputs/* ./ +RUN wget -qO tldr-bash https://4e4.win/tldr && chmod +x tldr-bash +COPY --from=node-builder /build-outputs/node /build-outputs/tldr-node ./ +COPY --from=node-builder /build-outputs/node_modules/ ./node_modules/ +COPY --from=zig-builder /build-outputs/* ./ + +# python is really hard to isolate in a package, using pyinstaller didn't really work either, so for now we just use it like this +RUN python3 -m venv tldr-python \ + && cd tldr-python \ + && bash -c 'source bin/activate; pip install wheel; pip install tldr; deactivate' \ + && cd .. \ + && echo '#!/bin/bash' > tldr-python.bash \ + && echo 'source tldr-python/bin/activate; tldr $@' >> tldr-python.bash \ + && chmod +x tldr-python.bash + +# Update all the individual caches +RUN bash -c 'mkdir -p /caches/{tealdeer,tldr-c,tldr-hs,fast-tldr,tldr-bash,tldr-node,tldr-python,outfieldr/.local/share}' \ + && TEALDEER_CACHE_DIR=/caches/tealdeer ./tealdeer -u \ + && TLDR_CACHE_DIR=/caches/tldr-c ./tldr-c -u \ + && XDG_DATA_HOME=/caches/tldr-hs ./tldr-hs -u \ + && XDG_DATA_HOME=/caches/fast-tldr ./fast-tldr -u \ + && XDG_DATA_HOME=/caches/tldr-bash ./tldr-bash -u \ + && HOME=/caches/tldr-node ./tldr-node -u \ + && HOME=/caches/tldr-python ./tldr-python.bash -u \ + && HOME=/caches/outfieldr ./outfieldr -u + +WORKDIR /tools +RUN wget -q https://github.com/sharkdp/hyperfine/releases/download/v1.11.0/hyperfine_1.11.0_amd64.deb && dpkg -i hyperfine_1.11.0_amd64.deb + +ENV PAGE="tar" +WORKDIR /clients +CMD hyperfine \ + --warmup 10 \ + --runs 50 \ + --prepare 'sync; echo 3 | tee /proc/sys/vm/drop_caches' \ + "TEALDEER_CACHE_DIR=/caches/tealdeer ./tealdeer $PAGE" \ + "TLDR_CACHE_DIR=/caches/tldr-c ./tldr-c $PAGE" \ + "XDG_DATA_HOME=/caches/tldr-hs ./tldr-hs $PAGE" \ + "XDG_DATA_HOME=/caches/fast-tldr ./fast-tldr $PAGE" \ + "XDG_DATA_HOME=/caches/tldr-bash TLDR_LESS=0 ./tldr-bash $PAGE" \ + "HOME=/caches/tldr-python ./tldr-python.bash $PAGE" \ + "HOME=/caches/outfieldr ./outfieldr $PAGE" \ + "HOME=/caches/tldr-node ./tldr-node $PAGE" diff --git a/completion/bash_tealdeer b/completion/bash_tealdeer new file mode 100644 index 0000000..d5420b6 --- /dev/null +++ b/completion/bash_tealdeer @@ -0,0 +1,35 @@ +# tealdeer bash completion + +_tealdeer() +{ + local cur prev words cword + _init_completion || return + + case $prev in + -h|--help|-v|--version|-l|--list|-u|--update|--no-auto-update|-c|--clear-cache|--pager|-r|--raw|--show-paths|--seed-config|-q|--quiet) + return + ;; + -f|--render) + _filedir + return + ;; + -p|--platform) + COMPREPLY=( $(compgen -W 'linux macos sunos windows android freebsd netbsd openbsd' -- "${cur}") ) + return + ;; + --color) + COMPREPLY=( $(compgen -W 'always auto never' -- "${cur}") ) + return + ;; + esac + + if [[ $cur == -* ]]; then + COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) ) + return + fi + if tldrlist=$(tldr -l 2>/dev/null); then + COMPREPLY=( $(compgen -W '$( echo "$tldrlist" | tr -d , )' -- "${cur}") ) + fi +} + +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/completion/zsh_tealdeer b/completion/zsh_tealdeer new file mode 100644 index 0000000..fbab749 --- /dev/null +++ b/completion/zsh_tealdeer @@ -0,0 +1,51 @@ +#compdef tldr + +_applications() { + local -a commands + if commands=(${(uonzf)"$(tldr --list 2>/dev/null)"//:/\\:}); then + _describe -t commands 'command' commands + fi +} + +_tealdeer() { + local I="-h --help -v --version" + integer ret=1 + local -a args + + args+=( + "($I -l --list)"{-l,--list}"[List all commands in the cache]" + "($I -f --render)"{-f,--render}"[Render a specific markdown file]:file:_files" + "($I -p --platform)"{-p,--platform}'[Override the operating system]:platform:(( + linux + macos + sunos + windows + android + freebsd + netbsd + openbsd + ))' + "($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]" + "($I -q --quiet)"{-q,--quiet}"[Suppress informational messages]" + "($I)--show-paths[Show file and directory paths used by tealdeer]" + "($I)--seed-config[Create a basic config]" + "($I)--color[Controls when to use color]:when:(( + always + auto + never + ))" + '(- *)'{-h,--help}'[Display help]' + '(- *)'{-v,--version}'[Show version information]' + '1: :_applications' + ) + + _arguments $args[@] && ret=0 + return ret +} + +_tealdeer diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..7585238 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +book diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..7b63a06 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,8 @@ +# Tealdeer Docs + +To build the docs, install [mdbook](https://github.com/rust-lang/mdBook). + +You can build the HTML with `mdbook build`. + +To serve the docs on `localhost:3000` and watch for changes, use `mdbook +serve`. diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 0000000..ae63f23 --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Danilo Bargen"] +language = "en" +multilingual = false +src = "src" +title = "Tealdeer User Manual" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md new file mode 100644 index 0000000..4649382 --- /dev/null +++ b/docs/src/SUMMARY.md @@ -0,0 +1,14 @@ +# Summary + +[Introduction](./intro.md) + +- [Installing](./installing.md) +- [Usage](./usage.md) + - [Custom Pages and Patches](./usage_custom_pages.md) +- [Configuration](./config.md) + - [Section: \[display\]](./config_display.md) + - [Section: \[style\]](./config_style.md) + - [Section: \[search\]](./config_search.md) + - [Section: \[updates\]](./config_updates.md) + - [Section: \[directories\]](./config_directories.md) +- [Tips and Tricks](./tips_and_tricks.md) diff --git a/docs/src/config.md b/docs/src/config.md new file mode 100644 index 0000000..a662b57 --- /dev/null +++ b/docs/src/config.md @@ -0,0 +1,59 @@ +# Configuration + +Tealdeer can be customized with a config file in [TOML +format](https://toml.io/) called `config.toml`. + +## Configfile Path + +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: + +```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`. + +## 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" + +[style.example_text] +foreground = "green" + +[style.example_code] +foreground = "blue" + +[style.example_variable] +foreground = "blue" +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 new file mode 100644 index 0000000..007d64b --- /dev/null +++ b/docs/src/config_display.md @@ -0,0 +1,71 @@ +# Section: \[display\] + +In the `display` section you can configure the output format. + +## `use_pager` + +Specifies whether the pager should be used by default or not (default `false`). + +```toml +[display] +use_pager = true +``` + +When enabled, `less -R` is used as pager. To override the pager command used, +set the `PAGER` environment variable. + +NOTE: This feature is not available on Windows. + +## `compact` + +Set this to enforce more compact output, where empty lines are stripped out +(default `false`). + +```toml +[display] +compact = true +``` + +## `show_title` + +Display the command name at the top of the page output (default `false`). + +```toml +[display] +show_title = true +``` + +When enabled, the command name will be displayed at the top of the output, +styled with the `command_name` style configuration. + +## `indent` + +Controls the indentation of the output via two sub-keys. + +### `indent.base` + +Specifies the number of spaces used to indent descriptions, example text, and titles (default `2`). + +```toml +[display.indent] +base = 2 +``` + +### `indent.command` + +Specifies the number of spaces used to indent example code lines (default `6`). + +```toml +[display.indent] +command = 6 +``` + +You can also configure both subkeys in a single line like this: + +```toml +[display] +indent = { + base = 2, + command = 6, +} +``` 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 new file mode 100644 index 0000000..593a5b5 --- /dev/null +++ b/docs/src/config_style.md @@ -0,0 +1,47 @@ +# Section: \[style\] + +Using the config file, the style (e.g. colors or underlines) can be customized. + +Screenshot of customized version + +## Style Targets + +- `description`: The initial description text +- `command_name`: The command name as part of the example code +- `example_text`: The text that describes an example +- `example_code`: The example itself (except the `command_name` and `example_variable`) +- `example_variable`: The variables in the example + +## Attributes + +- `foreground` (color string, ANSI code, or RGB, see below) +- `background` (color string, ANSI code, or RGB, see below) +- `underline` (`true` or `false`) +- `bold` (`true` or `false`) +- `italic` (`true` or `false`) + +Colors can be specified in one of three ways: + +- Color string (`black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`): + + Example: + + ```toml + foreground = "green" + ``` + +- 256 color ANSI code (*tealdeer v1.5.0+*) + + Example: + + ```toml + foreground = { ansi = 4 } + ``` + +- 24-bit RGB color (*tealdeer v1.5.0+*) + + Example: + + ```toml + background = { rgb = { r = 255, g = 255, b = 255 } } + ``` diff --git a/docs/src/config_updates.md b/docs/src/config_updates.md new file mode 100644 index 0000000..a8a10c8 --- /dev/null +++ b/docs/src/config_updates.md @@ -0,0 +1,91 @@ +# 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 +behavior can be configured in the `updates` section and is disabled by +default. + +### `auto_update` + +Specifies whether the auto-update feature should be enabled (defaults to +`false`). + +```toml +[updates] +auto_update = true +``` + +### `auto_update_interval_hours` + +Duration, since the last cache update, after which the cache will be +refreshed (defaults to 720 hours). This parameter is ignored if `auto_update` +is set to `false`. + +```toml +[updates] +auto_update = true +auto_update_interval_hours = 24 +``` + +### `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/deer.png b/docs/src/deer.png new file mode 100644 index 0000000..ebc9c9d Binary files /dev/null and b/docs/src/deer.png differ diff --git a/docs/src/deer.svg b/docs/src/deer.svg new file mode 100644 index 0000000..b686b4b --- /dev/null +++ b/docs/src/deer.svg @@ -0,0 +1,52 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/docs/src/installing.md b/docs/src/installing.md new file mode 100644 index 0000000..f0ca823 --- /dev/null +++ b/docs/src/installing.md @@ -0,0 +1,74 @@ +# Installing + +There are a few different ways to install tealdeer: + +- Through [package managers](#package-managers) +- Through [static binaries](#static-binaries-linux) +- Through [cargo install](#through-cargo-install) +- By [building from source](#build-from-source) + +Additionally, when not using system packages, you can [manually install +autocompletions](#autocompletion). + +## Package Managers + +Tealdeer has been added to a few package managers: + +- Arch Linux: [`tealdeer`](https://archlinux.org/packages/extra/x86_64/tealdeer/) +- Debian: [`tealdeer`](https://tracker.debian.org/tealdeer) +- Fedora: [`tealdeer`](https://src.fedoraproject.org/rpms/rust-tealdeer) +- FreeBSD: [`sysutils/tealdeer`](https://www.freshports.org/sysutils/tealdeer/) +- Funtoo: [`app-misc/tealdeer`](https://github.com/funtoo/core-kit/tree/1.4-release/app-misc/tealdeer) +- Homebrew: [`tealdeer`](https://formulae.brew.sh/formula/tealdeer) +- MacPorts: [`tealdeer`](https://ports.macports.org/port/tealdeer/) +- NetBSD: [`sysutils/tealdeer`](https://pkgsrc.se/sysutils/tealdeer) +- Nix: [`tealdeer`](https://search.nixos.org/packages?query=tealdeer) +- openSUSE: [`tealdeer`](https://software.opensuse.org/package/tealdeer?search_term=tealdeer) +- Scoop: [`tealdeer`](https://github.com/ScoopInstaller/Main/blob/master/bucket/tealdeer.json) +- Solus: [`tealdeer`](https://packages.getsol.us/shannon/t/tealdeer/) +- Void Linux: [`tealdeer`](https://github.com/void-linux/void-packages/tree/master/srcpkgs/tealdeer) + +## Static Binaries (Linux) + +Static binary builds (currently for Linux only) are available on the +[GitHub releases page](https://github.com/tealdeer-rs/tealdeer/releases). +Simply download the binary for your platform and run it! + +## Through `cargo install` + +Build and install the tool via cargo... + +```shell +$ cargo install tealdeer +``` + +## Build From Source + +Release build: + +```shell +$ cargo build --release +``` + +Release build with native TLS support: + +```shell +$ cargo build --release --features native-tls +``` + +Debug build with logging support: + +```shell +$ cargo build --features logging +``` + +(To enable logging at runtime, export the `RUST_LOG=tldr=debug` env variable.) + +## Autocompletion + +Shell completion scripts are located in the folder `completion`. +Just copy them to their designated location: + +- *Bash*: `cp completion/bash_tealdeer /usr/share/bash-completion/completions/tldr` +- *Fish*: `cp completion/fish_tealdeer ~/.config/fish/completions/tldr.fish` +- *Zsh*: `cp completion/zsh_tealdeer /usr/share/zsh/site-functions/_tldr` diff --git a/docs/src/intro.md b/docs/src/intro.md new file mode 100644 index 0000000..a90ecec --- /dev/null +++ b/docs/src/intro.md @@ -0,0 +1,14 @@ +# Tealdeer: Introduction + +Tealdeer is a very fast implementation of +[tldr](https://github.com/tldr-pages/tldr) in Rust: Simplified, example based +and community-driven man pages. + +![Screenshot](screenshot-default.png) + +This documentation shows how to install, use and configure tealdeer. + +## Links + +- [GitHub Project Page](https://github.com/tealdeer-rs/tealdeer) +- [TLDR Pages Project](https://tldr.sh/) diff --git a/screenshot-custom.png b/docs/src/screenshot-custom.png similarity index 100% rename from screenshot-custom.png rename to docs/src/screenshot-custom.png diff --git a/screenshot-default.png b/docs/src/screenshot-default.png similarity index 100% rename from screenshot-default.png rename to docs/src/screenshot-default.png 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.md b/docs/src/usage.md new file mode 100644 index 0000000..6fba933 --- /dev/null +++ b/docs/src/usage.md @@ -0,0 +1,10 @@ +# Usage + +Tealdeer is straightforward to use, through the binary named `tldr`. + +You can view the available options using `tldr --help`: + + +``` +{{#include usage.txt}} +``` diff --git a/docs/src/usage.txt b/docs/src/usage.txt new file mode 100644 index 0000000..6a04de7 --- /dev/null +++ b/docs/src/usage.txt @@ -0,0 +1,33 @@ +tealdeer 1.8.1: A fast TLDR client +Danilo Bargen , Niklas Mohrin + +Usage: tldr [OPTIONS] [COMMAND]... + +Arguments: + [COMMAND]... 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 + +To view the user documentation, please visit https://tealdeer-rs.github.io/tealdeer/. + +To view usage examples, run tldr tldr or tldr tealdeer. diff --git a/docs/src/usage_custom_pages.md b/docs/src/usage_custom_pages.md 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 9276e9a..0000000 --- a/fish_tealdeer +++ /dev/null @@ -1,21 +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 o -l os -d 'Override the operating system.' -xa 'linux osx sunos other' -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 q -l quiet -d 'Suppress informational messages.' -f -complete -c tldr -l config-path -d 'Show config file path.' -f -complete -c tldr -l seed-config -d 'Create a basic config.' -f - -function __tealdeer_entries - tldr --list | sed -e 's/, /\n/g' -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 42e2f6d..0000000 --- a/release-build.sh +++ /dev/null @@ -1,62 +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" -) - -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-${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-$target -done -echo "" - -for target in ${targets[@]}; do - echo "==> Signing $target" - gpg -a --output "dist-$VERSION/tldr-$target.sig" --detach-sig "dist-$VERSION/tldr-$target" -done -echo "" - -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 d08615f..afa3603 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,106 +1,240 @@ -use std::env; -use std::fs; -use std::io::Read; -use std::path::PathBuf; +use std::{ + fs::{self, File}, + io::{Cursor, ErrorKind, Read}, + path::{Path, PathBuf}, + time::{Duration, SystemTime}, +}; -#[cfg(unix)] -use std::os::unix::fs::MetadataExt; +use anyhow::{anyhow, bail, ensure, Context, Result}; +use log::{debug, info}; +use ureq::{ + http::StatusCode, + tls::{RootCerts, TlsConfig, TlsProvider}, + Agent, +}; +use zip::ZipArchive; -use reqwest::{Client, Proxy}; -use flate2::read::GzDecoder; -use log::debug; -use tar::Archive; -use time; -use walkdir::{DirEntry, WalkDir}; -use xdg::BaseDirectories; +use crate::{ + config::{Language, TlsBackend}, + types::PlatformType, +}; -use crate::error::TealdeerError::{self, CacheError, UpdateError}; -use crate::types::OsType; +pub static TLDR_PAGES_DIR: &str = "tldr-pages"; +pub static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; -#[derive(Debug)] -pub struct Cache { - url: String, - os: OsType, +#[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>], } -impl Cache { - pub fn new(url: S, os: OsType) -> Self - where - S: Into, - { - Self { - url: url.into(), - os, +/// The directory backing this cache is checked to be populated at construction. +pub struct Cache<'a> { + config: CacheConfig<'a>, +} + +#[derive(Debug)] +pub struct PageLookupResult { + pub page_path: PathBuf, + pub patch_path: Option, +} + +impl<'a> Cache<'a> { + /// Try opening a cache at the location given by `config.pages_directory`. If no directory + /// exists at this location, `Ok(None)` is returned. + pub fn open(config: CacheConfig<'a>) -> Result> { + match config.pages_directory.metadata() { + Ok(md) => { + ensure!( + md.is_dir(), + "Cache directory `{}` exists, but is not a directory.", + config.pages_directory.display(), + ); + Ok(Some(Cache { config })) + } + Err(err) if err.kind() == ErrorKind::NotFound => Ok(None), + Err(err) => Err(anyhow!(err).context(format!( + "Error getting metdata of cache directory {}", + config.pages_directory.display() + ))), } } - /// Return the path to the cache directory. - fn get_cache_dir(&self) -> Result { - // Allow overriding the cache directory by setting the - // $TEALDEER_CACHE_DIR env variable. - if let Ok(value) = env::var("TEALDEER_CACHE_DIR") { - let path = PathBuf::from(value); + /// 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)); + } - if path.exists() && path.is_dir() { - return Ok(path); - } else { - return Err(CacheError( - "Path specified by $TEALDEER_CACHE_DIR \ - does not exist or is not a directory." - .into(), - )); + 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(()) }; - // Otherwise, fall back to $XDG_CACHE_HOME/tealdeer. - let xdg_dirs = match BaseDirectories::with_prefix(crate::NAME) { - Ok(dirs) => dirs, - Err(_) => return Err(CacheError("Could not determine XDG base directory.".into())), + 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); }; - Ok(xdg_dirs.get_cache_home()) - } - /// 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); + for entry in file_iter { + if let Some(extension) = entry?.path().extension() { + if extension == "page" || extension == "patch" { + return Ok(true); + } } } - 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) + + Ok(false) } - /// Decompress and open the archive - fn decompress(&self, reader: R) -> Archive> { - Archive::new(GzDecoder::new(reader)) + 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(), + ) + }) } - /// Update the pages cache. - pub fn update(&self) -> Result<(), TealdeerError> { - // First, download the compressed data - let bytes: Vec = self.download()?; + /// 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); - // Decompress the response body into an `Archive` - let mut archive = self.decompress(&bytes[..]); - - // Determine paths - let cache_dir = self.get_cache_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)))?; + // 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 @@ -108,143 +242,191 @@ impl Cache { // 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()?; + fs::remove_dir_all(self.config.pages_directory)?; + fs::create_dir(self.config.pages_directory)?; - // Extract archive - archive - .unpack(&cache_dir) - .map_err(|e| UpdateError(format!("Could not unpack compressed data: {}", e)))?; - - Ok(()) - } - - #[cfg(unix)] - /// Return the number of seconds since the cache directory was last modified. - pub fn last_update(&self) -> Option { - if let Ok(cache_dir) = self.get_cache_dir() { - if let Ok(metadata) = fs::metadata(cache_dir.join("tldr-master")) { - let mtime = metadata.mtime(); - let now = time::now_utc().to_timespec(); - return Some(now.sec - mtime); - }; - }; - None - } - - /// Return the platform directory. - #[allow(clippy::match_same_arms)] - fn get_platform_dir(&self) -> Option<&'static str> { - match self.os { - OsType::Linux => Some("linux"), - OsType::OsX => Some("osx"), - OsType::SunOs => None, // TODO: Does Rust support SunOS? - OsType::Other => None, - } - } - - /// Search for a page and return the path to it. - pub fn find_page(&self, name: &str) -> Option { - // Build page file name - let page_filename = format!("{}.md", name); - - // Get platform dir - let platforms_dir = match self.get_cache_dir() { - Ok(cache_dir) => cache_dir.join("tldr-master").join("pages"), - _ => return None, - }; - - // Determine platform - let platform = self.get_platform_dir(); - - // Search for the page in the platform specific directory - if let Some(pf) = platform { - let path = platforms_dir.join(&pf).join(&page_filename); - if path.exists() && path.is_file() { - return Some(path); + 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:?}"); } } - // If platform is not supported or if platform specific page does not exist, - // look up the page in the "common" directory. - let path = platforms_dir.join("common").join(&page_filename); - - // Return it if it exists, otherwise give up and return `None` - if path.exists() && path.is_file() { - Some(path) - } else { - None - } - } - - /// 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-master").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() { - if file_name == "common" { - return true; - } - if let Some(platform) = platform_dir { - return file_name == platform; - } - } 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 + Ok(archives .into_iter() - .filter_entry(|e| should_walk(e)) // Filter out pages for other architectures - .filter_map(|e| e.ok()) // Convert results to options, filter out errors - .filter_map(|e| { - let path = e.path(); - let extension = &path.extension().and_then(|s| s.to_str()).unwrap_or(""); - if e.file_type().is_file() && extension == &"md" { - path.file_stem() - .and_then(|stem| stem.to_str().map(|s| s.into())) - } else { - None - } - }) - .collect::>(); - pages.sort(); - pages.dedup(); - Ok(pages) + .filter_map(|(lang, archive)| archive.is_some().then_some(lang))) } - /// Delete the cache directory. - pub fn clear(&self) -> Result<(), TealdeerError> { - let path = self.get_cache_dir()?; - if path.exists() && path.is_dir() { - fs::remove_dir_all(&path).map_err(|_| CacheError(format!( - "Could not remove cache directory ({}).", - path.display() - )))?; - } else if path.exists() { - return Err(CacheError(format!( - "Cache path ({}) is not a directory.", - path.display() - ))); - } else { - return Err(CacheError(format!( - "Cache path ({}) does not exist.", - path.display() - ))); - }; - Ok(()) + pub fn config(&self) -> &CacheConfig<'a> { + &self.config + } +} + +impl PageLookupResult { + pub fn with_page(page_path: PathBuf) -> Self { + Self { + page_path, + patch_path: None, + } + } + + pub fn with_optional_patch(mut self, patch_path: Option) -> Self { + self.patch_path = patch_path; + self + } + + /// Create a reader that sequentially reads from the page and the + /// patch, as if they were concatenated. + /// + /// This will return an error if either the page file or the patch file + /// cannot be opened. + pub fn reader(&self) -> Result> { + // Open page file + let page_file = File::open(&self.page_path) + .with_context(|| format!("Could not open page file at {}", self.page_path.display()))?; + + // 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, + }; + + // 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 { + 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), + }; + 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:?}") + } + } + } +} + +/// Unit Tests for cache module +#[cfg(test)] +mod tests { + use super::*; + + use std::{ + fs::File, + io::{Read, Write}, + }; + + #[test] + fn test_reader_with_patch() { + // Write test files + let dir = tempfile::tempdir().unwrap(); + let page_path = dir.path().join("test.page.md"); + let patch_path = dir.path().join("test.patch.md"); + { + let mut f1 = File::create(&page_path).unwrap(); + f1.write_all(b"Hello\n").unwrap(); + let mut f2 = File::create(&patch_path).unwrap(); + f2.write_all(b"World").unwrap(); + } + + // Create chained reader from lookup result + let lr = PageLookupResult::with_page(page_path).with_optional_patch(Some(patch_path)); + let mut reader = lr.reader().unwrap(); + + // Read into a Vec + let mut buf = Vec::new(); + reader.read_to_end(&mut buf).unwrap(); + + assert_eq!(&buf, b"Hello\n\nWorld"); + } + + #[test] + fn test_reader_without_patch() { + // Write test file + let dir = tempfile::tempdir().unwrap(); + let page_path = dir.path().join("test.page.md"); + { + let mut f = File::create(&page_path).unwrap(); + f.write_all(b"Hello\n").unwrap(); + } + + // Create chained reader from lookup result + let lr = PageLookupResult::with_page(page_path); + let mut reader = lr.reader().unwrap(); + + // Read into a Vec + let mut buf = Vec::new(); + reader.read_to_end(&mut buf).unwrap(); + + assert_eq!(&buf, b"Hello\n"); } } 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 787de94..f0feeb4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,50 +1,138 @@ -use std::env; -use std::fs; -use std::io::{Error as IoError, Read, Write}; -use std::path::PathBuf; +use std::{ + borrow::Cow, + env, fmt, + fs::{self, File}, + io::{ErrorKind, Write}, + path::{Component, Path, PathBuf}, + sync::LazyLock, + time::Duration, +}; -use ansi_term::{Color, Style}; -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 toml; -use xdg::BaseDirectories; +use yansi::{Color, Style}; -use crate::error::TealdeerError::{self, ConfigError}; +use crate::{ + 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 } -#[serde(rename_all = "lowercase")] +fn default_italic() -> bool { + false +} + #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "lowercase")] pub enum RawColor { Black, Red, Green, Yellow, Blue, - Purple, + Magenta, + Purple, // Backwards compatibility with ansi_term (until tealdeer 1.5.0) Cyan, White, + Ansi(u8), + Rgb { r: u8, g: u8, b: u8 }, } impl From for Color { fn from(raw_color: RawColor) -> Self { match raw_color { - RawColor::Black => Color::Black, - RawColor::Red => Color::Red, - RawColor::Green => Color::Green, - RawColor::Yellow => Color::Yellow, - RawColor::Blue => Color::Blue, - RawColor::Purple => Color::Purple, - RawColor::Cyan => Color::Cyan, - RawColor::White => Color::White, + RawColor::Black => Self::Black, + RawColor::Red => Self::Red, + RawColor::Green => Self::Green, + RawColor::Yellow => Self::Yellow, + RawColor::Blue => Self::Blue, + 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), } } } @@ -57,8 +145,11 @@ struct RawStyle { pub underline: bool, #[serde(default = "default_bold")] pub bold: bool, + #[serde(default = "default_italic")] + pub italic: bool, } +#[allow(clippy::derivable_impls)] // Explicitly control defaults impl Default for RawStyle { fn default() -> Self { Self { @@ -66,9 +157,10 @@ impl Default for RawStyle { background: None, underline: false, bold: false, + italic: false, } } -} // impl RawStyle +} impl From for Style { fn from(raw_style: RawStyle) -> Self { @@ -79,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 { @@ -90,6 +182,10 @@ impl From for Style { style = style.bold(); } + if raw_style.italic { + style = style.italic(); + } + style } } @@ -108,14 +204,197 @@ struct RawStyleConfig { pub example_variable: RawStyle, } -#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] -struct RawConfig { - style: RawStyleConfig, +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(), + } + } } -impl RawConfig { - fn new() -> Self { - let mut raw_config = Self::default(); +#[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: +/// ), so we need to wrap +/// `DEFAULT_UPDATE_INTERVAL_HOURS` in a function to be able to use +/// `#[serde(default = ...)]` +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 { + fn default() -> Self { + Self { + auto_update: false, + auto_update_interval_hours: DEFAULT_UPDATE_INTERVAL_HOURS, + archive_source: default_archive_source(), + tls_backend: RawTlsBackend::default(), + download_languages: None, + warn_cache_age: None, + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +struct RawDirectoriesConfig { + #[serde(default)] + pub cache_dir: Option, + #[serde(default)] + pub custom_pages_dir: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +enum RawPlatformType { + Current, + All, + MacOs, // alias for Platform(PlatformType::OsX) + #[serde(untagged)] + Platform(PlatformType), +} + +impl RawPlatformType { + pub fn flatten(raw_platforms: impl IntoIterator) -> Vec { + let mut flattened = Vec::new(); + for raw_platform in raw_platforms { + match raw_platform { + RawPlatformType::Current => flattened.push(PlatformType::current()), + RawPlatformType::Platform(platform) => flattened.push(platform), + RawPlatformType::MacOs => flattened.push(PlatformType::OsX), + RawPlatformType::All => flattened.extend(PlatformType::value_variants()), + } + } + flattened.clear_duplicates(); + flattened + } +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +struct RawSearchConfig { + pub languages: Option>, + pub platforms: Option>, +} + +impl<'a> From<&'a RawSearchConfig> for SearchConfig<'a> { + fn from(raw_search_config: &'a RawSearchConfig) -> Self { + let languages = raw_search_config + .languages + .as_ref() + .map_or_else(get_languages_from_env, |langs| { + langs.iter().map(|lang| Language(lang)).collect() + }); + let platforms = if let Some(raw_platforms) = raw_search_config.platforms.as_ref() { + RawPlatformType::flatten(raw_platforms.iter().copied()) + } else { + RawPlatformType::flatten([ + RawPlatformType::Current, + RawPlatformType::Platform(PlatformType::Common), + RawPlatformType::All, + ]) + }; + + Self { + languages, + platforms, + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(default)] +struct RawConfig { + style: RawStyleConfig, + display: RawDisplayConfig, + updates: RawUpdatesConfig, + directories: RawDirectoriesConfig, + search: RawSearchConfig, +} + +impl Default for RawConfig { + fn default() -> Self { + let mut raw_config = RawConfig { + style: RawStyleConfig::default(), + display: RawDisplayConfig::default(), + updates: RawUpdatesConfig::default(), + directories: RawDirectoriesConfig::default(), + search: RawSearchConfig::default(), + }; // Set default config raw_config.style.example_text.foreground = Some(RawColor::Green); @@ -126,9 +405,9 @@ impl RawConfig { raw_config } -} // impl RawConfig +} -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] pub struct StyleConfig { pub description: Style, pub command_name: Style, @@ -137,135 +416,613 @@ pub struct StyleConfig { pub example_variable: Style, } -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct Config { - pub style: StyleConfig, +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct DisplayConfig { + pub compact: bool, + pub use_pager: bool, + pub show_title: bool, + pub indent: Indent, } -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(), - }, +#[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, 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 cache_dir: PathWithSource, + pub custom_pages_dir: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SearchConfig<'a> { + pub languages: Vec>, + pub platforms: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Language<'a>(pub &'a str); + +fn get_languages<'a>( + env_lang: Option<&'a str>, + env_language: Option<&'a str>, +) -> Vec> { + // Language list according to + // https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#language + + let Some(env_lang) = env_lang else { + return vec![Language("en")]; + }; + + // Create an iterator that contains $LANGUAGE (':' separated list) followed by $LANG (single language) + let locales = env_language.unwrap_or("").split(':').chain([env_lang]); + + let mut lang_list = Vec::new(); + for locale in locales { + if !locale.is_ascii() { + info!("Skipping non-ASCII locale string: {locale}"); + continue; + } + + // Language plus country code (e.g. `en_US`) + if locale.len() >= 5 && locale.chars().nth(2) == Some('_') { + lang_list.push(Language(&locale[..5])); + } + // Language code only (e.g. `en`) + if locale.len() >= 2 && locale != "POSIX" { + lang_list.push(Language(&locale[..2])); + } + } + + lang_list.push(Language("en")); + lang_list.clear_duplicates(); + lang_list +} + +pub fn get_languages_from_env<'a>() -> Vec> { + static LANG: LazyLock> = LazyLock::new(|| std::env::var("LANG").ok()); + static LANGUAGE: LazyLock> = LazyLock::new(|| std::env::var("LANGUAGE").ok()); + get_languages( + LANG.as_ref().map(String::as_str), + LANGUAGE.as_ref().map(String::as_str), + ) +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum RawTlsBackend { + /// Native TLS (`SChannel` on Windows, Secure Transport on macOS and OpenSSL otherwise) + NativeTls, + /// Rustls with `WebPKI` roots. + RustlsWithWebpkiRoots, + /// Rustls with native roots. + RustlsWithNativeRoots, +} + +impl Default for RawTlsBackend { + fn default() -> Self { + *SUPPORTED_TLS_BACKENDS.first().unwrap() + } +} + +impl std::fmt::Display for RawTlsBackend { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + self.serialize(f) + } +} + +/// Allows choosing a `reqwest`'s TLS backend. Available TLS backends: +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum TlsBackend { + /// Native TLS (`SChannel` on Windows, Secure Transport on macOS and OpenSSL otherwise) + #[cfg(feature = "native-tls")] + NativeTls, + /// Rustls with `WebPKI` roots. + #[cfg(feature = "rustls-with-webpki-roots")] + RustlsWithWebpkiRoots, + /// Rustls with native roots. + #[cfg(feature = "rustls-with-native-roots")] + RustlsWithNativeRoots, +} + +impl TryFrom for TlsBackend { + type Error = anyhow::Error; + + fn try_from(raw: RawTlsBackend) -> Result { + match raw { + #[cfg(feature = "native-tls")] + RawTlsBackend::NativeTls => Ok(TlsBackend::NativeTls), + #[cfg(feature = "rustls-with-webpki-roots")] + RawTlsBackend::RustlsWithWebpkiRoots => Ok(TlsBackend::RustlsWithWebpkiRoots), + #[cfg(feature = "rustls-with-native-roots")] + RawTlsBackend::RustlsWithNativeRoots => Ok(TlsBackend::RustlsWithNativeRoots), + // when compiling without all TLS backend features, we want to handle config error. + #[allow(unreachable_patterns)] + _ => Err(anyhow!( + "Unsupported TLS backend: {}. This tealdeer build has support for the following options: {}", + raw, + supported_tls_backends_string(), + )) } } } -#[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() -> 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 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: {}", 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, + }, }; - Ok(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(); + + // 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, + }) } -} // impl Config +} + +/// Expands tilde (~) prefixed directories into its absolute version +fn expand_home<'a>(input_path: &'a Path, home_path: Option<&Path>) -> Result> { + let mut components = input_path.components(); + + if let Some(Component::Normal(first_component_raw)) = components.next() { + let first_component = first_component_raw + .to_str() + .ok_or(anyhow!("Path contains invalid UTF-8"))?; + + if first_component == "~" { + let home_path = home_path.ok_or(anyhow!("Unable to find user home directory"))?; + let rest: PathBuf = components.collect(); + let expanded = home_path.join(rest); + + return Ok(Cow::Owned(expanded)); + } else if first_component.starts_with('~') { + return Err(anyhow!("Tilde expansion with a login name not supported")); + } + } + + Ok(Cow::Borrowed(input_path)) +} + +/// The [`ConfigLoader`] is used to load a [`Config`] from a file. +/// +/// Since the rich [`Config`] keeps references to [`RawConfig`], the raw config needs to be kept alive outside of the +/// [`Config`]. The [`ConfigLoader`] thus offers the following flow: +/// 1. Read a raw config using [`ConfigLoader::read`] or [`ConfigLoader::read_default_path`]. +/// 2. Validate the contents to a [`Config`] that borrows the [`ConfigLoader`]. +pub struct ConfigLoader { + raw: RawConfig, + path: PathWithSource, +} + +impl ConfigLoader { + fn read_internal(path: PathWithSource, allow_not_found: bool) -> 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") + } +} /// Return the path to the config directory. /// /// The config dir path can be overridden using the `TEALDEER_CONFIG_DIR` env -/// variable. Otherwise, `$XDG_CONFIG_hOME/tealdeer` is returned. +/// variable. Otherwise, the user config directory is returned. /// /// Note that this function does not verify whether the directory at that -/// loation exists, or is a directory. -pub fn get_config_dir() -> Result { +/// location exists, or is a directory. +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)); - }; + return (PathBuf::from(value), PathSource::EnvVar); + } - // Otherwise, fall back to $XDG_CONFIG_HOME/tealdeer. - let xdg_dirs = match BaseDirectories::with_prefix(crate::NAME) { - Ok(dirs) => dirs, - Err(_) => { - return Err(ConfigError("Could not determine XDG base directory.".into())) - } - }; - Ok(xdg_dirs.get_config_home()) + (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 { - let config_dir = get_config_dir()?; - let config_file_path = config_dir.join(CONFIG_FILE_NAME); - Ok(config_file_path) +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 60e0756..0000000 --- a/src/error.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::fmt; -use reqwest::Error as ReqwestError; - -#[derive(Debug)] -#[allow(clippy::pub_enum_variant_names)] -pub enum TealdeerError { - CacheError(String), - ConfigError(String), - UpdateError(String), -} - -impl From for TealdeerError { - fn from(err: ReqwestError) -> Self { - TealdeerError::UpdateError(format!("HTTP error: {}", err.to_string())) - } -} - -impl fmt::Display for TealdeerError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - TealdeerError::CacheError(e) => write!(f, "CacheError: {}", e), - TealdeerError::ConfigError(e) => write!(f, "ConfigError: {}", e), - TealdeerError::UpdateError(e) => write!(f, "UpdateError: {}", e), - } - } -} diff --git a/src/extensions.rs b/src/extensions.rs new file mode 100644 index 0000000..e74e9f3 --- /dev/null +++ b/src/extensions.rs @@ -0,0 +1,33 @@ +use std::mem; + +/// An extension trait to clear duplicates from a collection. +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 { + fn clear_duplicates(&mut self) { + let orig = mem::replace(self, Vec::with_capacity(self.len())); + for item in orig { + if !self.contains(&item) { + self.push(item); + } + } + } +} + +/// Like `str::find`, but starts searching at `start`. +pub(crate) trait FindFrom { + fn find_from(&self, needle: &Self, start: usize) -> Option; +} + +impl FindFrom for str { + fn find_from(&self, needle: &Self, start: usize) -> Option { + self.get(start..) + .and_then(|s| s.find(needle)) + .map(|i| i + start) + } +} diff --git a/src/formatter.rs b/src/formatter.rs index 488f8ed..082f436 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -1,78 +1,459 @@ //! Functions related to formatting and printing lines from a `Tokenizer`. -use std::io::BufRead; - -use ansi_term::{ANSIString, ANSIStrings}; use log::debug; -use crate::config::Config; -use crate::tokenizer::Tokenizer; -use crate::types::LineType; +use crate::{config::Indent, extensions::FindFrom, types::LineType}; -fn highlight_command<'a>( - command: &'a str, - example_code: &'a str, - config: &Config, - parts: &mut Vec>, -) { - let mut code_part_end_pos = 0; - while let Some(command_start) = example_code[code_part_end_pos..].find(&command) { - let code_part = &example_code[code_part_end_pos..code_part_end_pos + command_start]; - parts.push(config.style.example_code.paint(code_part)); - parts.push(config.style.command_name.paint(command)); - - code_part_end_pos += command_start + command.len(); - } - parts.push( - config - .style - .example_code - .paint(&example_code[code_part_end_pos..]), - ); +#[derive(Debug, Clone, Copy, Eq)] +/// Represents a snippet from a page of a specific highlighting class. +pub enum PageSnippet { + CommandName(T), + Variable(T), + NormalCode(T), + Description(T), + Text(T), + Title(T), + Linebreak, } -/// Format and highlight code examples including variables in {{ curly braces }}. -fn format_code(command: &str, text: &str, config: &Config) -> String { - let mut parts = Vec::new(); - for between_variables in text.split("}}") { - if let Some(variable_start) = between_variables.find("{{") { - let example_code = &between_variables[..variable_start]; - let example_variable = &between_variables[variable_start + 2..]; - - highlight_command(&command, &example_code, &config, &mut parts); - parts.push(config.style.example_variable.paint(example_variable)); - } else { - highlight_command(&command, &between_variables, &config, &mut parts); +#[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, } } - - ANSIStrings(&parts).to_string() } -/// Print a token stream to an ANSI terminal. -pub fn print_lines(tokenizer: &mut Tokenizer, config: &Config) +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) | Title(s) => { + s.is_empty() + } + Linebreak => false, + } + } +} + +/// Parse the content of each line yielded by `lines` and yield `HighLightingSnippet`s accordingly. +pub fn highlight_lines( + lines: L, + process_snippet: &mut F, + keep_empty_lines: bool, + show_title: bool, + indent: Indent, +) -> Result<(), E> where - R: BufRead, + L: Iterator, + 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(); - while let Some(token) = tokenizer.next_token() { - match token { - LineType::Empty => println!(), + for line in lines { + match line { + LineType::Empty => { + if keep_empty_lines { + process_snippet(PageSnippet::Linebreak)?; + } + } 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 tokenizer yields values in order of appearance. + // 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) => println!(" {}", config.style.description.paint(text)), - LineType::ExampleText(text) => println!(" {}", config.style.example_text.paint(text)), LineType::ExampleCode(text) => { - println!(" {}", &format_code(&command, &text, &config)) + 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. +/// - 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> { + // 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(()) +} + +/// 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 str>) -> Result<(), E>, +) -> Result<(), E> { + if !command_name.is_empty() { + let mut search_start = 0; + while let Some(match_start) = segment.find_from(command_name, search_start) { + let match_end = match_start + command_name.len(); + if is_freestanding_substring(segment, (match_start, match_end)) { + process_snippet(PageSnippet::NormalCode(&segment[..match_start]))?; + process_snippet(PageSnippet::CommandName(command_name))?; + segment = &segment[match_end..]; + search_start = 0; + } else { + search_start = segment[match_start..] + .char_indices() + .nth(1) + .map_or(segment.len(), |(i, _)| match_start + i); + } + } + } + process_snippet(PageSnippet::NormalCode(segment))?; + Ok(()) +} + +/// Checks whether the characters right before and after the substring (given by half-open index interval) are whitespace (if they exist). +fn is_freestanding_substring(surrounding: &str, substring: (usize, usize)) -> bool { + let (start, end) = substring; + // "okay" meaning or + let char_before_is_okay = surrounding[..start] + .chars() + .last() + .is_none_or(char::is_whitespace); + let char_after_is_okay = surrounding[end..] + .chars() + .next() + .is_none_or(char::is_whitespace); + char_before_is_okay && char_after_is_okay +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_freestanding_substring() { + assert!(is_freestanding_substring("I love tldr", (0, 1))); + assert!(is_freestanding_substring("I love tldr", (2, 6))); + assert!(is_freestanding_substring("I love tldr", (7, 11))); + + assert!(is_freestanding_substring("tldr", (0, 4))); + assert!(is_freestanding_substring("tldr ", (0, 4))); + assert!(is_freestanding_substring(" tldr", (1, 5))); + assert!(is_freestanding_substring(" tldr ", (1, 5))); + + assert!(!is_freestanding_substring("tldr", (1, 3))); + assert!(!is_freestanding_substring("tldr ", (1, 4))); + assert!(!is_freestanding_substring(" tldr", (1, 4))); + + assert!(is_freestanding_substring( + " épicé ", + (1, " épicé".len()) // note the missing trailing space + )); + assert!(!is_freestanding_substring( + " épicé ", + (1, " épic".len()) // note the missing trailing space and character + )); + } + + fn run<'a>(cmd: &'a str, segment: &'a str) -> Vec> { + let mut yielded = Vec::new(); + let mut process_snippet = |snip: PageSnippet<&str>| { + if !snip.is_empty() { + yielded.push(snip.map(str::to_string)); + } + Ok::<(), ()>(()) + }; + + highlight_code(cmd, segment, &mut process_snippet).expect("highlight code segment failed"); + yielded + } + + mod highlight_code_segment { + use super::*; + use PageSnippet::*; + + #[test] + fn test_highlight_code_segment() { + assert!(run("make", "").is_empty()); + assert_eq!( + &run("make", "make all CC=clang -q"), + &[CommandName("make"), NormalCode(" all CC=clang -q")] + ); + assert_eq!( + &run("make", " make money --always-make"), + &[ + NormalCode(" "), + CommandName("make"), + NormalCode(" money --always-make") + ] + ); + assert_eq!( + &run("git commit", "git commit -m 'git commit'"), + &[CommandName("git commit"), NormalCode(" -m 'git commit'"),] + ); + } + + #[test] + fn test_i18n() { + assert_eq!( + &run("mäke", "mäke höhlenrätselbücher"), + &[CommandName("mäke"), NormalCode(" höhlenrätselbücher")] + ); + assert_eq!( + &run( + "Müll", + "1000 Gründe warum Müll heute größer ist als Müll früher, ärgerlich" + ), + &[ + NormalCode("1000 Gründe warum "), + CommandName("Müll"), + NormalCode(" heute größer ist als "), + CommandName("Müll"), + NormalCode(" früher, ärgerlich") + ] + ); + assert_eq!( + &run( + "übergang", + "die Zustandsübergangsfunktion übergang Änderungen", + ), + &[ + NormalCode("die Zustandsübergangsfunktion "), + CommandName("übergang"), + NormalCode(" Änderungen") + ], + ); + } + + #[test] + fn test_empty_command() { + let segment = "some code"; + let snippets = [NormalCode(segment)]; + + assert_eq!(run("", segment), snippets); + assert_eq!(run(" ", segment), snippets); + assert_eq!(run(" \t ", segment), snippets); + } + } + + mod placeholders { + use super::*; + use PageSnippet::*; + + #[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("", 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")],); } } - println!(); } diff --git a/src/line_iterator.rs b/src/line_iterator.rs new file mode 100644 index 0000000..98088c0 --- /dev/null +++ b/src/line_iterator.rs @@ -0,0 +1,122 @@ +//! Code to split a `BufRead` instance into an iterator of `LineType`s. + +use std::io::{BufRead, Read}; + +use log::warn; + +use crate::types::LineType; + +#[derive(Debug, PartialEq, Eq)] +pub enum TldrFormat { + /// Not yet clear + Undecided, + /// The original format + V1, + /// The new format (see ) + V2, +} + +/// A `LineIterator` is initialized with a `BufReader` instance that contains the +/// entire Tldr page. It then implements `Iterator`. +#[derive(Debug)] +pub struct LineIterator { + /// An instance of `R: BufRead`. + reader: R, + /// Whether the first line has already been processed or not. + first_line: bool, + /// Buffer for the current line. Used internally. + current_line: String, + /// The tldr page format. + format: TldrFormat, +} + +impl LineIterator +where + R: BufRead, +{ + pub fn new(reader: R) -> Self { + Self { + reader, + first_line: true, + current_line: String::new(), + format: TldrFormat::Undecided, + } + } +} + +impl Iterator for LineIterator { + type Item = LineType; + + fn next(&mut self) -> Option { + self.current_line.clear(); + let bytes_read = self.reader.read_line(&mut self.current_line); + match bytes_read { + Ok(0) => None, + Err(e) => { + warn!("Could not read line from reader: {e:?}"); + None + } + Ok(_) => { + // Handle new titles + if self.first_line { + if self.current_line.starts_with('#') { + // It's the old format. + self.format = TldrFormat::V1; + } else { + // It's the new format! Drop next line. + if let Err(e) = Read::bytes(&mut self.reader) + .find(|b| matches!(b, Ok(b'\n') | Err(_))) + .transpose() + { + warn!("Could not read line from reader: {e:?}"); + return None; + } + self.first_line = false; + self.format = TldrFormat::V2; + return Some(LineType::Title(self.current_line.trim_end().to_string())); + } + } + self.first_line = false; + + // Convert line to a `LineType` instance + match self.format { + TldrFormat::V1 => Some(LineType::from_v1(&self.current_line[..])), + TldrFormat::V2 => Some(LineType::from(&self.current_line[..])), + TldrFormat::Undecided => panic!("Could not determine page format version"), + } + } + } + } +} + +#[cfg(test)] +mod test { + use super::LineIterator; + use crate::types::LineType; + + #[test] + fn test_first_line_old_format() { + 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 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> 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 description = lines.next().unwrap(); + assert_eq!( + description, + LineType::Description("Description".to_string()) + ); + } +} diff --git a/src/main.rs b/src/main.rs index 06c3f72..6678ceb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ //! An implementation of [tldr](https://github.com/tldr-pages/tldr) in Rust. // -// Copyright (c) 2015-2018 tealdeer developers +// Copyright (c) 2015-2021 tealdeer developers // // Licensed under the Apache License, Version 2.0 or the MIT license @@ -10,138 +10,135 @@ #![deny(clippy::all)] #![warn(clippy::pedantic)] +#![allow(clippy::enum_glob_use)] +#![allow(clippy::module_name_repetitions)] #![allow(clippy::similar_names)] -#![allow(clippy::stutter)] +#![allow(clippy::struct_excessive_bools)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::unnecessary_debug_formatting)] +#![allow(clippy::while_let_loop)] -#[cfg(feature = "logging")] -extern crate env_logger; +#[cfg(not(any( + feature = "native-tls", + feature = "rustls-with-webpki-roots", + feature = "rustls-with-native-roots", +)))] +compile_error!( + "at least one of the features \"native-tls\", \"rustls-with-webpki-roots\" or \"rustls-with-native-roots\" must be enabled" +); -use std::fs::File; -use std::io::BufReader; -use std::path::{Path, PathBuf}; -use std::process; +use std::{ + env, + fs::create_dir_all, + io::{self, IsTerminal}, + path::Path, + process::{Command, ExitCode}, +}; -use ansi_term::Color; -use docopt::Docopt; -use serde_derive::Deserialize; +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 tokenizer; +mod line_iterator; +mod output; mod types; +mod utils; -use crate::cache::Cache; -use crate::config::{get_config_path, make_default_config, Config}; -use crate::error::TealdeerError::{CacheError, ConfigError, UpdateError}; -use crate::formatter::print_lines; -use crate::tokenizer::Tokenizer; -use crate::types::OsType; +use crate::{ + 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, + utils::{print_error, print_warning}, +}; const NAME: &str = "tealdeer"; -const VERSION: &str = env!("CARGO_PKG_VERSION"); -const USAGE: &str = " -Usage: - - tldr [options] - tldr [options] - -Options: - - -h --help Show this screen - -v --version Show version information - -l --list List all commands in the cache - -f --render Render a specific markdown file - -o --os Override the operating system [linux, osx, sunos] - -u --update Update the local cache - -c --clear-cache Clear the local cache - -q --quiet Suppress informational messages - --config-path Show config file path - --seed-config Create a basic config - -Examples: - - $ tldr tar - $ tldr --list - -To control the cache: - - $ tldr --update - $ tldr --clear-cache - -To render a local file (for testing): - - $ tldr --render /path/to/file.md -"; -const ARCHIVE_URL: &str = "https://github.com/tldr-pages/tldr/archive/master.tar.gz"; -const MAX_CACHE_AGE: i64 = 2_592_000; // 30 days - -#[derive(Debug, Deserialize)] -struct Args { - arg_command: Option, - flag_help: bool, - flag_version: bool, - flag_list: bool, - flag_render: Option, - flag_os: Option, - flag_update: bool, - flag_clear_cache: bool, - flag_quiet: bool, - flag_config_path: bool, - flag_seed_config: bool, -} - -/// Print page by path -fn print_page(path: &Path) -> Result<(), String> { - // Open file - let file = File::open(path).map_err(|msg| format!("Could not open file: {}", msg))?; - let reader = BufReader::new(file); - - // Look up config file, if none is found fall back to default config. - let config = match Config::load() { - Ok(config) => config, - Err(ConfigError(msg)) => { - eprintln!("Could not load config: {}", msg); - process::exit(1); - } - Err(e) => { - eprintln!("Could not load config: {}", e); - process::exit(1); - } - }; - - // Create tokenizer and print output - let mut tokenizer = Tokenizer::new(reader); - print_lines(&mut tokenizer, &config); +static TEALDEER_PAGE: &str = + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/pages/tealdeer.md")); +/// Clear the cache +fn clear_cache(cache: Cache, quietly: bool) -> Result<()> { + let cache_dir = cache.config().pages_directory.display(); + cache.clear().context("Could not clear cache")?; + if !quietly { + eprintln!("Successfully cleared cache at `{cache_dir}`."); + } Ok(()) } -/// Check the cache for freshness -fn check_cache(args: &Args, cache: &Cache) { - if !args.flag_update { - match cache.last_update() { - Some(ago) if ago > MAX_CACHE_AGE => { - if args.flag_quiet { - return; - } - println!( - "{}", - Color::Red.paint(format!( - "Cache wasn't updated for more than {} days.\n\ - You should probably run `tldr --update` soon.", - MAX_CACHE_AGE / 24 / 3600 - )) - ); - } - None => { - eprintln!("Cache not found. Please run `tldr --update`."); - process::exit(1); - } - _ => {} +/// Update the cache +fn update_cache( + cache: &mut Cache, + archive_source: &str, + tls_backend: TlsBackend, + quietly: bool, +) -> Result<()> { + let downloaded_languages = cache + .update(archive_source, tls_backend) + .context("Could not update cache")?; + if !quietly { + eprintln!("Successfully updated cache."); + eprint!("Pages for the following languages were downloaded: "); + let language_strings: Vec<_> = downloaded_languages + .into_iter() + .map(|lang| lang.0) + .collect(); + if language_strings.is_empty() { + eprintln!("(none)"); + } else { + eprintln!("{}", language_strings.join(", ")); + } + } + Ok(()) +} + +/// Show file paths +fn show_paths(config: &Config) { + let config_dir = { + let (mut path, source) = get_config_dir(); + path.push(""); // Trailing path separator + match path.to_str() { + Some(path) => format!("{path} ({source})"), + None => "[Invalid]".to_string(), } }; + let config_path = config.file_path.to_string(); + let cache_dir = config.directories.cache_dir.to_string(); + let pages_dir = { + let mut path = config.directories.cache_dir.path.clone(); + path.push(TLDR_PAGES_DIR); + path.push(""); // Trailing path separator + path.display().to_string() + }; + let custom_pages_dir = match config.directories.custom_pages_dir { + Some(ref path_with_source) => path_with_source.to_string(), + None => "[None]".to_string(), + }; + println!("Config dir: {config_dir}"); + println!("Config path: {config_path}"); + println!("Cache dir: {cache_dir}"); + println!("Pages dir: {pages_dir}"); + println!("Custom pages dir: {custom_pages_dir}"); +} + +fn 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")] @@ -152,204 +149,296 @@ fn init_log() { #[cfg(not(feature = "logging"))] fn init_log() {} -#[cfg(target_os = "linux")] -fn get_os() -> OsType { - OsType::Linux +fn spawn_editor(custom_pages_dir: &Path, file_name: &str) -> Result<()> { + create_dir_all(custom_pages_dir).context("Failed to create custom pages directory")?; + + 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())); + } + Ok(()) } -#[cfg(any(target_os = "macos", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", - target_os = "dragonfly"))] -fn get_os() -> OsType { - OsType::OsX -} - -#[cfg(not(any(target_os = "linux", - target_os = "macos", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd", - target_os = "dragonfly")))] -fn get_os() -> OsType { - OsType::Other -} - -fn main() { +fn main() -> ExitCode { // Initialize logger init_log(); // Parse arguments - let args: Args = Docopt::new(USAGE) - .and_then(|d| d.deserialize()) - .unwrap_or_else(|e| e.exit()); + let args = Cli::parse(); - // Show version and exit - if args.flag_version { - let os = get_os(); - println!("{} v{} ({})", NAME, VERSION, os); - process::exit(0); - } - - // Specify target OS - let os: OsType = match args.flag_os { - Some(os) => os, - None => get_os(), + // Determine the usage of styles + let enable_styles = match args.color.unwrap_or_default() { + // Attempt to use styling if instructed + ColorOptions::Always => { + yansi::enable(); // disable yansi's automatic detection for ANSI support on Windows + true + } + // Enable styling if: + // * NO_COLOR env var isn't set: https://no-color.org/ + // * The output stream is stdout (not being piped) + ColorOptions::Auto => env::var_os("NO_COLOR").is_none() && io::stdout().is_terminal(), + // Disable styling + ColorOptions::Never => false, }; - // Initialize cache - let cache = Cache::new(ARCHIVE_URL, os); + try_main(args, enable_styles).unwrap_or_else(|error| { + print_error(enable_styles, &error); + ExitCode::FAILURE + }) +} - // Clear cache, pass through - if args.flag_clear_cache { - cache.clear().unwrap_or_else(|e| { - match e { - CacheError(msg) | ConfigError(msg) | UpdateError(msg) => { - eprintln!("Could not delete cache: {}", msg) - } - }; - process::exit(1); - }); - if !args.flag_quiet { - println!("Successfully deleted cache."); +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(); } - // Update cache, pass through - if args.flag_update { - cache.update().unwrap_or_else(|e| { - match e { - CacheError(msg) | ConfigError(msg) | UpdateError(msg) => { - eprintln!("Could not update cache: {}", msg) - } - }; - process::exit(1); - }); - if !args.flag_quiet { - println!("Successfully updated cache."); - } + let custom_pages_dir = config + .directories + .custom_pages_dir + .as_ref() + .map(PathWithSource::path); + + // Note: According to the TLDR client spec, page names must be transparently + // lowercased before lookup: + // https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#page-names + let command = args.command.join("-").to_lowercase(); + + if args.edit_patch || args.edit_page { + let file_name = if args.edit_patch { + format!("{command}.patch.md") + } else { + format!("{command}.page.md") + }; + + custom_pages_dir + .context("To edit custom pages/patches, please specify a custom pages directory.") + .and_then(|custom_pages_dir| spawn_editor(custom_pages_dir, &file_name))?; + + return Ok(ExitCode::SUCCESS); } - // Show config file and path, pass through - if args.flag_config_path { - match get_config_path() { - Ok(config_file_path) => { - println!("Config path is: {}", config_file_path.to_str().unwrap()); - } - Err(ConfigError(msg)) => { - eprintln!("Could not look up config_path: {}", msg); - process::exit(1); - } - Err(_) => { - eprintln!("Unknown error"); - process::exit(1); - } - } + // Show various paths + if args.show_paths { + show_paths(&config); } // Create a basic config and exit - if args.flag_seed_config { - match make_default_config() { - Ok(config_file_path) => { - println!( - "Successfully created seed config file here: {}", - config_file_path.to_str().unwrap() - ); - process::exit(0); - } - Err(ConfigError(msg)) => { - eprintln!("Could not create seed config: {}", msg); - process::exit(1); - } - Err(_) => { - eprintln!("Unkown error"); - process::exit(1); - } + if args.seed_config { + 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 reader = PageLookupResult::with_page(file).reader()?; + print_page(reader, args.raw, enable_styles, args.pager, &config)?; + return Ok(ExitCode::SUCCESS); + } + + // The tealdeer page is embedded in the binary, no cache needed + if command == "tealdeer" { + print_page( + TEALDEER_PAGE.as_bytes(), + args.raw, + enable_styles, + args.pager, + &config, + )?; + return Ok(ExitCode::SUCCESS); + } + + if let Some(platforms) = args.platforms { + config.search.platforms = platforms; + if !config.search.platforms.contains(&PlatformType::Common) { + config.search.platforms.push(PlatformType::Common); } } + let (search_languages, download_languages): (&[_], &[_]) = match args.language.as_deref() { + Some(lang) => (&[Language(lang)], &[Language(lang)]), + None => (&config.search.languages, &config.updates.download_languages), + }; - // Render local file and exit - if let Some(ref file) = args.flag_render { - let path = PathBuf::from(file); - if let Err(msg) = print_page(&path) { - eprintln!("{}", msg); - process::exit(1); - } else { - process::exit(0); - }; + 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.flag_list { - // Check cache for freshness - check_cache(&args, &cache); + if args.clear_cache { + if let Some(cache) = Cache::open(cache_config)? { + clear_cache(cache, args.quiet)?; + } + return Ok(ExitCode::SUCCESS); + } - // Get list of pages - let pages = cache.list_pages().unwrap_or_else(|e| { - match e { - CacheError(msg) | ConfigError(msg) | UpdateError(msg) => { - eprintln!("Could not get list of pages: {}", msg) - } + 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); } - process::exit(1); - }); + } - // Print pages - println!("{}", pages.join(", ")); - process::exit(0); + 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, + &anyhow::anyhow!( + "Page cache not found. Please run `tldr --update` to download the cache." + ), + ); + println!("\nNote: You can optionally enable automatic cache updates by adding the"); + println!("following config to your config file:\n"); + println!(" [updates]"); + println!(" auto_update = true\n"); + println!("The path to your config file can be looked up with `tldr --show-paths`."); + println!("To create an initial config file, use `tldr --seed-config`.\n"); + println!("You can find more tips and tricks in our docs:\n"); + println!(" https://tealdeer-rs.github.io/tealdeer/config_updates.html"); + + 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 let Some(ref command) = args.arg_command { - // Check cache for freshness - check_cache(&args, &cache); - - // Search for command in cache - if let Some(path) = cache.find_page(&command) { - if let Err(msg) = print_page(&path) { - eprintln!("{}", msg); - process::exit(1); - } else { - process::exit(0); - } - } else { - if !args.flag_quiet { - println!("Page {} not found in cache", &command); - println!("Try updating with `tldr --update`, or submit a pull request to:"); - println!("https://github.com/tldr-pages/tldr"); - } - process::exit(1); + 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 Some(result) = cache.find_page(&command) else { + if !args.quiet { + print_warning( + enable_styles, + &format!( + "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" + ), + ); + } + return Ok(ExitCode::FAILURE); + }; + + print_page( + result.reader()?, + args.raw, + enable_styles, + args.pager, + &config, + )?; } - // Some flags can be run without a command. - if !(args.flag_update || args.flag_clear_cache || args.flag_config_path) { - eprintln!("{}", USAGE); - process::exit(1); - } -} - -#[cfg(test)] -mod test { - use docopt::{Docopt, Error}; - use crate::{Args, OsType, USAGE}; - - fn test_helper(argv: &[&str]) -> Result { - Docopt::new(USAGE).and_then(|d| d.argv(argv.iter()).deserialize()) - } - - #[test] - fn test_docopt_os_case_insensitive() { - let argv = vec!["cp", "--os", "LiNuX"]; - let os = test_helper(&argv).unwrap().flag_os.unwrap(); - assert_eq!(OsType::Linux, os); - } - - #[test] - fn test_docopt_expect_error() { - let argv = vec!["cp", "--os", "lindows"]; - assert!(!test_helper(&argv).is_ok()); - } + Ok(ExitCode::SUCCESS) } diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..6243b44 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,97 @@ +//! Functions for printing pages to the terminal + +use std::io::{self, BufRead, BufReader, Read, Write}; + +use anyhow::{Context, Result}; +use yansi::Paint; + +use crate::{ + config::{Config, StyleConfig}, + formatter::{highlight_lines, PageSnippet}, + line_iterator::LineIterator, +}; + +/// Set up display pager +/// +/// SAFETY: this function may be called multiple times +#[cfg(not(target_os = "windows"))] +fn configure_pager(_: bool) { + use std::sync::Once; + static INIT: Once = Once::new(); + INIT.call_once(|| pager::Pager::with_default_pager("less -R").setup()); +} + +#[cfg(target_os = "windows")] +fn configure_pager(enable_styles: bool) { + use crate::utils::print_warning; + print_warning(enable_styles, "--pager flag not available on Windows!"); +} + +/// Print page by path +pub fn print_page( + reader: impl Read, + enable_markdown: bool, + enable_styles: bool, + use_pager: bool, + config: &Config, +) -> Result<()> { + let reader = BufReader::new(reader); + + // Configure pager if applicable + if use_pager || config.display.use_pager { + configure_pager(enable_styles); + } + + // Lock stdout only once, this improves performance considerably + let stdout = io::stdout(); + let mut handle = stdout.lock(); + + if enable_markdown { + // Print the raw markdown of the file. + for line in reader.lines() { + let line = line.context("Error while reading from a page")?; + writeln!(handle, "{line}").context("Could not write to stdout")?; + } + } else { + // Closure that processes a page snippet and writes it to stdout + let mut process_snippet = |snip: PageSnippet<&str>| { + if snip.is_empty() { + Ok(()) + } else { + print_snippet(&mut handle, snip, &config.style).context("Failed to print snippet") + } + }; + + // 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")?; + } + + // 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<&str>, + style: &StyleConfig, +) -> io::Result<()> { + use PageSnippet::*; + + match snip { + CommandName(s) | Title(s) => write!(writer, "{}", s.paint(style.command_name)), + Variable(s) => write!(writer, "{}", s.paint(style.example_variable)), + NormalCode(s) => write!(writer, "{}", s.paint(style.example_code)), + Description(s) => write!(writer, "{}", s.paint(style.description)), + Text(s) => write!(writer, "{}", s.paint(style.example_text)), + Linebreak => writeln!(writer), + } +} diff --git a/src/tokenizer.rs b/src/tokenizer.rs deleted file mode 100644 index 7c33740..0000000 --- a/src/tokenizer.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Code to tokenize a `BufRead` instance into an iterator of `LineType`s. - -use std::io::BufRead; - -use log::warn; -use crate::types::LineType; - -#[derive(Debug, PartialEq, Eq)] -pub enum TldrFormat { - /// Not yet clear - Undecided, - /// The original format - V1, - /// The new format (see https://github.com/tldr-pages/tldr/pull/958) - V2, -} - -/// A tokenizer is initialized with a `BufReader` instance that contains the -/// entire Tldr page. It then returns tokens as `Option`. -#[derive(Debug)] -pub struct Tokenizer { - /// An instance of `R: BufRead`. - reader: R, - /// Whether the first line has already been tokenized or not. - first_line: bool, - /// Buffer for the current line. Used internally. - current_line: String, - /// The tldr page format. - format: TldrFormat, -} - -impl Tokenizer -where - R: BufRead, -{ - pub fn new(reader: R) -> Self { - Self { - reader, - first_line: true, - current_line: String::new(), - format: TldrFormat::Undecided, - } - } - - pub fn next_token(&mut self) -> Option { - self.current_line.clear(); - let bytes_read = self.reader.read_line(&mut self.current_line); - match bytes_read { - Ok(0) => None, - Err(e) => { - warn!("Could not read line from token reader: {:?}", e); - None - } - Ok(_) => { - // Handle new titles - if self.first_line && !self.current_line.starts_with('#') { - // 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 token reader: {:?}", e); - return None; - } - self.first_line = false; - self.format = TldrFormat::V2; - return Some(LineType::Title(self.current_line.trim_right().to_string())); - } - - if self.first_line { - // Clear `first_line` flag - self.first_line = false; - - // It's the old format. - self.format = TldrFormat::V1; - } - - // Convert line to a `LineType` instance - match self.format { - TldrFormat::V1 => Some(LineType::from_v1(&self.current_line[..])), - TldrFormat::V2 => Some(LineType::from(&self.current_line[..])), - TldrFormat::Undecided => panic!("Could not determine page format version"), - } - } - } - } -} - -#[cfg(test)] -mod test { - use super::Tokenizer; - use crate::types::LineType; - - #[test] - fn test_first_line_old_format() { - let input = "# The Title\n\n"; - let mut tokenizer = Tokenizer::new(input.as_bytes()); - let title = tokenizer.next_token().unwrap(); - assert_eq!(title, LineType::Title("The Title".to_string())); - let empty = tokenizer.next_token().unwrap(); - assert_eq!(empty, LineType::Empty); - } - - #[test] - fn test_first_line_new_format() { - let input = "The Title\n=========\n\n"; - let mut tokenizer = Tokenizer::new(input.as_bytes()); - let title = tokenizer.next_token().unwrap(); - assert_eq!(title, LineType::Title("The Title".to_string())); - let empty = tokenizer.next_token().unwrap(); - assert_eq!(empty, LineType::Empty); - } -} diff --git a/src/types.rs b/src/types.rs index 2e2c3b9..7ca6e2d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,30 +1,131 @@ -//! Types used in the client. +//! Shared types used in tealdeer. -use std::fmt; +use std::{fmt, str}; use serde_derive::{Deserialize, Serialize}; #[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] #[allow(dead_code)] -pub enum OsType { +pub enum PlatformType { Linux, OsX, + Windows, SunOs, - Other, + Android, + FreeBsd, + NetBsd, + OpenBsd, + Common, } -impl fmt::Display for OsType { +impl fmt::Display for PlatformType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - OsType::Linux => write!(f, "Linux"), - OsType::OsX => write!(f, "macOS / BSD"), - OsType::SunOs => write!(f, "SunOS"), - OsType::Other => write!(f, "Unknown OS"), + Self::Linux => write!(f, "Linux"), + Self::OsX => write!(f, "macOS / BSD"), + Self::Windows => write!(f, "Windows"), + Self::SunOs => write!(f, "SunOS"), + Self::Android => write!(f, "Android"), + Self::FreeBsd => write!(f, "FreeBSD"), + Self::NetBsd => write!(f, "NetBSD"), + Self::OpenBsd => write!(f, "OpenBSD"), + Self::Common => write!(f, "Common"), } } } +impl clap::ValueEnum for PlatformType { + fn value_variants<'a>() -> &'a [Self] { + &[ + Self::Linux, + Self::OsX, + Self::SunOs, + Self::Windows, + Self::Android, + Self::FreeBsd, + Self::NetBsd, + Self::OpenBsd, + Self::Common, + ] + } + + fn 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")), + } + } +} + +impl PlatformType { + #[cfg(target_os = "linux")] + pub fn current() -> Self { + Self::Linux + } + + #[cfg(any(target_os = "macos", target_os = "dragonfly"))] + pub fn current() -> Self { + Self::OsX + } + + #[cfg(target_os = "windows")] + pub fn current() -> Self { + 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", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "dragonfly", + target_os = "windows", + target_os = "android", + )))] + pub fn current() -> Self { + Self::Other + } +} + +#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize, clap::ValueEnum)] +#[serde(rename_all = "lowercase")] +#[derive(Default)] +pub enum ColorOptions { + Always, + #[default] + Auto, + Never, +} + #[derive(Debug, Eq, PartialEq)] pub enum LineType { Empty, @@ -36,28 +137,24 @@ pub enum LineType { } impl<'a> From<&'a str> for LineType { - /// Convert a string slice to a LineType. Newlines and trailing whitespace are trimmed. + /// Convert a string slice to a `LineType`. Newlines and trailing whitespace are trimmed. fn from(line: &'a str) -> Self { - let trimmed: &str = line.trim_right(); + let trimmed: &str = line.trim_end(); let mut chars = trimmed.chars(); match chars.next() { - None => LineType::Empty, - Some('#') => LineType::Title( + None => Self::Empty, + Some('#') => Self::Title( trimmed - .trim_left_matches(|chr: char| chr == '#' || chr.is_whitespace()) + .trim_start_matches(|chr: char| chr == '#' || chr.is_whitespace()) .into(), ), - Some('>') => LineType::Description( + Some('>') => Self::Description( trimmed - .trim_left_matches(|chr: char| chr == '>' || chr.is_whitespace()) + .trim_start_matches(|chr: char| chr == '>' || chr.is_whitespace()) .into(), ), - Some(' ') => LineType::ExampleCode( - trimmed - .trim_left_matches(|chr: char| chr.is_whitespace()) - .into(), - ), - _ => LineType::ExampleText(trimmed.into()), + Some(' ') => Self::ExampleCode(trimmed.trim_start_matches(char::is_whitespace).into()), + Some(_) => Self::ExampleText(trimmed.into()), } } } @@ -69,32 +166,60 @@ impl LineType { let trimmed = line.trim(); let mut chars = trimmed.chars(); match chars.next() { - None => LineType::Empty, - Some('#') => LineType::Title( + None => Self::Empty, + Some('#') => Self::Title( trimmed - .trim_left_matches(|chr: char| chr == '#' || chr.is_whitespace()) + .trim_start_matches(|chr: char| chr == '#' || chr.is_whitespace()) .into(), ), - Some('>') => LineType::Description( + Some('>') => Self::Description( trimmed - .trim_left_matches(|chr: char| chr == '>' || chr.is_whitespace()) + .trim_start_matches(|chr: char| chr == '>' || chr.is_whitespace()) .into(), ), - Some('-') => LineType::ExampleText( + Some('-') => Self::ExampleText( trimmed - .trim_left_matches(|chr: char| chr == '-' || chr.is_whitespace()) + .trim_start_matches(|chr: char| chr == '-' || chr.is_whitespace()) .into(), ), - Some('`') if chars.last() == Some('`') => LineType::ExampleCode( + Some('`') if chars.last() == Some('`') => Self::ExampleCode( trimmed .trim_matches(|chr: char| chr == '`' || chr.is_whitespace()) .into(), ), - _ => LineType::Other(trimmed.into()), + Some(_) => Self::Other(trimmed.into()), } } } +/// The reason why a certain path (e.g. config path or cache dir) was chosen. +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum PathSource { + /// OS convention (e.g. XDG on Linux) + OsConvention, + /// Env variable (TEALDEER_*) + EnvVar, + /// Config file + ConfigFile, + /// CLI argument override + Cli, +} + +impl fmt::Display for PathSource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::OsConvention => "OS convention", + Self::EnvVar => "env variable", + Self::ConfigFile => "config file", + Self::Cli => "command line argument", + } + ) + } +} + #[cfg(test)] mod test { use super::LineType; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..f4d825a --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,21 @@ +use yansi::{Color, Paint}; + +/// Print a warning to stderr. If `enable_styles` is true, then a yellow +/// message will be printed. +pub fn print_warning(enable_styles: bool, message: &str) { + print_msg(enable_styles, message, "Warning: ", Color::Yellow); +} + +/// Print an anyhow error to stderr. If `enable_styles` is true, then a red +/// message will be printed. +pub fn print_error(enable_styles: bool, error: &anyhow::Error) { + print_msg(enable_styles, &format!("{error:?}"), "Error: ", Color::Red); +} + +fn print_msg(enable_styles: bool, message: &str, prefix: &'static str, color: Color) { + if enable_styles { + eprintln!("{}{}", prefix.paint(color), message.paint(color)); + } else { + 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 89% rename from tests/inkscape-v1.md rename to tests/cache/pages.en/common/inkscape-v1.md index 4071196..fa63b2b 100644 --- a/tests/inkscape-v1.md +++ b/tests/cache/pages.en/common/inkscape-v1.md @@ -26,3 +26,7 @@ - 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-v2.md b/tests/cache/pages.en/common/inkscape-v2.md similarity index 89% rename from tests/inkscape-v2.md rename to tests/cache/pages.en/common/inkscape-v2.md index 3051867..0a1b530 100644 --- a/tests/inkscape-v2.md +++ b/tests/cache/pages.en/common/inkscape-v2.md @@ -27,3 +27,7 @@ Export an SVG document to PDF, converting all texts to paths: Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: inkscape {{filename.svg}} --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit + +Some invalid command just to test the correct highlighting of the command name: + + inkscape --use-inkscape=v3.0 file diff --git a/tests/cache/pages.en/common/which.md b/tests/cache/pages.en/common/which.md new file mode 100644 index 0000000..36d3941 --- /dev/null +++ b/tests/cache/pages.en/common/which.md @@ -0,0 +1,11 @@ +# which + +> Locate a program in the user's path. + +- Search the PATH environment variable and display the location of any matching executables: + +`which {{executable}}` + +- If there are multiple executables which match, display all: + +`which -a {{executable}}` 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/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-default.expected b/tests/inkscape-default.expected deleted file mode 100644 index c7061b3..0000000 --- a/tests/inkscape-default.expected +++ /dev/null @@ -1,28 +0,0 @@ - - An SVG (Scalable Vector Graphics) editing program. - Use -z to not open the GUI and only process files in the console. - - Open an SVG file in the Inkscape GUI: - - inkscape filename.svg - - Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI): - - inkscape filename.svg -e filename.png - - Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur): - - inkscape filename.svg -e filename.png -w 600 -h 400 - - Export a single object, given its ID, into a bitmap: - - inkscape filename.svg -i id -e object.png - - Export an SVG document to PDF, converting all texts to paths: - - inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path - - Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: - - inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit - diff --git a/tests/inkscape-with-config.expected b/tests/inkscape-with-config.expected deleted file mode 100644 index 313e140..0000000 --- a/tests/inkscape-with-config.expected +++ /dev/null @@ -1,28 +0,0 @@ - - An SVG (Scalable Vector Graphics) editing program. - Use -z to not open the GUI and only process files in the console. - - Open an SVG file in the Inkscape GUI: - - inkscape filename.svg - - Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI): - - inkscape filename.svg -e filename.png - - Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur): - - inkscape filename.svg -e filename.png -w 600 -h 400 - - Export a single object, given its ID, into a bitmap: - - inkscape filename.svg -i id -e object.png - - Export an SVG document to PDF, converting all texts to paths: - - inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path - - Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: - - inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit - diff --git a/tests/lib.rs b/tests/lib.rs index 204c3fb..d431b5f 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,37 +1,145 @@ //! Integration tests. -extern crate assert_cmd; -extern crate escargot; -extern crate predicates; -extern crate tempdir; -extern crate utime; - -use std::fs::File; -use std::io::Write; -use std::process::Command; +use std::{ + fs::{self, create_dir_all, File}, + io::{self, Write}, + path::{Path, PathBuf}, + process::Command, + time::{Duration, SystemTime}, +}; use assert_cmd::prelude::*; -use tempdir::TempDir; -use predicates::boolean::PredicateBooleanExt; -use predicates::prelude::predicate::str::{contains, is_empty, similar}; +use predicates::{ + boolean::PredicateBooleanExt, + ord::eq, + prelude::predicate::str::{contains, diff, is_empty, is_match}, +}; +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 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: TempDir::new(".tldr.test.cache").unwrap(), - config_dir: TempDir::new(".tldr.test.config").unwrap(), - input_dir: TempDir::new(".tldr.test.input").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 + } + + fn cache_dir(&self) -> PathBuf { + self._test_dir.path().join(".cache") + } + + 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. + fn add_entry(&self, name: &str, contents: &str) { + self.add_os_entry("common", name, contents); + } + + /// 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() + .join(TLDR_PAGES_DIR) + .join(format!("pages.{lang}")) + .join(os); + create_dir_all(&dir).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(); + create_dir_all(dir).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(); + create_dir_all(dir).unwrap(); + fs::write(dir.join(format!("{name}.patch.md")), contents.as_bytes()).unwrap(); } /// Disable default features. @@ -50,89 +158,311 @@ 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("TEALDEER_CACHE_DIR", 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] fn test_missing_cache() { TestEnv::new() .command() - .args(&["sl"]) + .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 .command() - .args(&["sl"]) + .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() - .args(&["--update"]) + .args(["--update"]) .assert() .success() - .stdout(contains("Successfully updated cache.")); + .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_rustls_webpki() { + let testenv = TestEnv::new() + .no_default_features() + .with_feature("rustls-with-webpki-roots"); testenv .command() - .args(&["sl"]) + .args(["sl"]) .assert() - .success(); + .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(); testenv .command() - .args(&["--update", "--quiet"]) + .args(["--update", "--quiet"]) .assert() .success() .stdout(is_empty()); testenv .command() - .args(&["--clear-cache", "--quiet"]) + .args(["--clear-cache", "--quiet"]) .assert() .success() .stdout(is_empty()); } #[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(&["--update", "-q"]) + .args(["--clear-cache"]) .assert() .success() - .stdout(is_empty()); + .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(&["fakeprogram", "-q"]) + .arg("--list") + .assert() + .failure() + .stderr(contains("Cleared pages from old cache location.")) + .stderr(contains("Page cache not found.")); + + assert!(testenv.cache_dir().is_dir()); + assert!(!testenv.cache_dir().join(TLDR_PAGES_DIR).exists()); + assert!(!testenv.cache_dir().join(TLDR_OLD_PAGES_DIR).exists()); +} + +#[test] +fn test_warn_invalid_tls_backend() { + let testenv = TestEnv::new() + .no_default_features() + .with_feature("rustls-with-webpki-roots") + .remove_initial_config(); + + testenv.append_to_config("updates.tls_backend = 'invalid-tls-backend'\n"); + + testenv + .command() + .args(["sl"]) + .assert() + .failure() + .stderr(contains("unknown variant `invalid-tls-backend`, expected one of `native-tls`, `rustls-with-webpki-roots`, `rustls-with-native-roots`")); +} + +#[test] +fn test_quiet_failures() { + let testenv = TestEnv::new().install_default_cache(); + + testenv + .command() + .args(["fakeprogram", "-q"]) .assert() .failure() .stdout(is_empty()); @@ -140,30 +470,159 @@ fn test_quiet_failures() { #[test] fn test_quiet_old_cache() { + 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 + .command() + .args(["which"]) + .assert() + .success() + .stderr(contains("The cache hasn't been updated for ")); + + testenv + .command() + .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().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(); + + assert!(!internal_cache_dir.exists()); + + command + .arg("--update") + .assert() + .success() + .stderr(contains(format!( + "Successfully created cache directory `{}`.", + internal_cache_dir.join(TLDR_PAGES_DIR).to_str().unwrap() + ))) + .stderr(contains("Successfully updated cache.")); + + assert!(internal_cache_dir.is_dir()); +} + +#[test] +fn test_cache_location_not_a_directory() { let testenv = TestEnv::new(); + let cache_dir = &testenv.cache_dir(); + File::create(cache_dir.join(TLDR_PAGES_DIR)).unwrap(); testenv .command() - .args(&["--update", "-q"]) + .arg("--list") .assert() - .success() - .stdout(is_empty()); + .failure() + .stderr(contains(format!( + "Cache directory `{}` exists, but is not a directory.", + cache_dir.join(TLDR_PAGES_DIR).display(), + ))); +} - let _ = utime::set_file_times(testenv.cache_dir.path().join("tldr-master"), 1, 1).unwrap(); +#[cfg(unix)] +#[test] +fn test_cache_location_permission_denied() { + use std::os::unix::fs::PermissionsExt; + + let testenv = TestEnv::new().install_default_cache(); testenv .command() - .args(&["tldr"]) + .arg("--list") .assert() .success() - .stdout(contains("Cache wasn't updated for more than ")); + .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() - .args(&["tldr", "--quiet"]) + .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(contains("Cache wasn't updated for more than ").not()); + .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] @@ -172,88 +631,868 @@ fn test_setup_seed_config() { testenv .command() - .args(&["--seed-config"]) + .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() - .stdout(contains("Successfully created seed config file")); + .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_config_path() { +fn test_show_paths() { let testenv = TestEnv::new(); + // Show general commands testenv .command() - .args(&["--config-path"]) + .args(["--show-paths"]) .assert() .success() .stdout(contains(format!( - "Config path is: {}/config.toml", - testenv.config_dir.path().to_str().unwrap(), + "Config dir: {}", + testenv.config_dir().to_str().unwrap(), + ))) + .stdout(contains(format!( + "Config path: {}", + testenv.config_dir().join("config.toml").to_str().unwrap(), + ))) + .stdout(contains(format!( + "Cache dir: {}", + testenv.cache_dir().to_str().unwrap(), + ))) + .stdout(contains(format!( + "Pages dir: {}", + testenv.cache_dir().join(TLDR_PAGES_DIR).to_str().unwrap(), + ))); + + let testenv = testenv.write_custom_pages_config(); + + // Now ensure that this path is contained in the output + testenv + .command() + .args(["--show-paths"]) + .assert() + .success() + .stdout(contains(format!( + "Custom pages dir: {}", + testenv.custom_pages_dir().to_str().unwrap(), ))); } -fn _test_correct_rendering(input_file: &str, filename: &str) { +#[test] +fn test_os_specific_page() { 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(); - - // Load expected output - let expected = include_str!("inkscape-default.expected"); + testenv.add_os_entry("sunos", "truss", "contents"); testenv .command() - .args(&["-f", &file_path.to_str().unwrap()]) + .args(["--platform", "sunos", "truss"]) + .assert() + .success(); +} + +#[test] +fn test_config_platforms() { + let testenv = TestEnv::new(); + testenv.add_os_entry("sunos", "sunos-command", ""); + + let set_config_platforms = |platforms| { + testenv.delete_config(); + testenv.init_config(); + testenv.append_to_config(format!("search.platforms = {platforms}")); + }; + + // By default all platforms are searched + testenv.command().arg("sunos-command").assert().success(); + + set_config_platforms("[]"); + testenv.command().arg("sunos-command").assert().failure(); + + set_config_platforms("['linux']"); + testenv.command().arg("sunos-command").assert().failure(); + + set_config_platforms("['sunos']"); + testenv.command().arg("sunos-command").assert().success(); + + set_config_platforms("['linux', 'all']"); + testenv.command().arg("sunos-command").assert().success(); + + set_config_platforms("['current', 'all']"); + testenv.command().arg("sunos-command").assert().success(); +} + +#[test] +fn test_markdown_rendering() { + let testenv = TestEnv::new().install_default_cache(); + + let expected = include_str!("cache/pages.en/common/which.md"); + testenv + .command() + .args(["--raw", "which"]) .assert() .success() - .stdout(similar(expected)); + .stdout(diff(expected)); +} + +fn _test_correct_rendering(page: &str, expected: &'static str, additional_args: &[&str]) { + let testenv = TestEnv::new().install_default_cache(); + + testenv + .command() + .args(additional_args) + .arg(page) + .assert() + .success() + .stdout(diff(expected)); } /// An end-to-end integration test for direct file rendering (v1 syntax). #[test] fn test_correct_rendering_v1() { - _test_correct_rendering(include_str!("inkscape-v1.md"), "inkscape-v1.md"); + _test_correct_rendering( + "inkscape-v1", + include_str!("rendered/inkscape-default.expected"), + &["--color", "always"], + ); } /// An end-to-end integration test for direct file rendering (v2 syntax). #[test] fn test_correct_rendering_v2() { - _test_correct_rendering(include_str!("inkscape-v2.md"), "inkscape-v2.md"); + _test_correct_rendering( + "inkscape-v2", + include_str!("rendered/inkscape-default.expected"), + &["--color", "always"], + ); } -/// An end-to-end integration test for rendering with constom syntax config. #[test] -fn test_correct_rendering_with_config() { - let testenv = TestEnv::new(); +/// An end-to-end integration test for direct file rendering with the `--color auto` option. This +/// will not use styling since output is not stdout. +fn test_rendering_color_auto() { + _test_correct_rendering( + "inkscape-v2", + include_str!("rendered/inkscape-default-no-color.expected"), + &["--color", "auto"], + ); +} - // 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); +#[test] +/// An end-to-end integration test for direct file rendering with the `--color never` option. +fn test_rendering_color_never() { + _test_correct_rendering( + "inkscape-v2", + include_str!("rendered/inkscape-default-no-color.expected"), + &["--color", "never"], + ); +} - let mut config_file = File::create(&config_file_path).unwrap(); - config_file - .write(include_str!("config.toml").as_bytes()) - .unwrap(); +/// 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"); - // 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_str!("inkscape-v2.md").as_bytes()).unwrap(); - - // Load expected output - let expected = include_str!("inkscape-with-config.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(&["-f", &file_path.to_str().unwrap()]) + .args(["--color", "never", "inkscape-v2"]) .assert() .success() - .stdout(similar(expected)); + .stdout(diff(expected_custom_indentation)); +} + +#[test] +fn test_rendering_i18n() { + _test_correct_rendering( + "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().install_default_cache(); + + testenv.append_to_config(include_str!("style-config.toml")); + + let expected = include_str!("rendered/inkscape-with-config.expected"); + + testenv + .command() + .args(["--color", "always", "inkscape-v2"]) + .assert() + .success() + .stdout(diff(expected)); +} + +/// An end-to-end integration test for rendering with show_title config option enabled. +#[test] +fn test_show_title_config() { + // Test that default behavior without show_title shows no title + let testenv = TestEnv::new().install_default_cache(); + let expected_no_title = include_str!("rendered/inkscape-default.expected"); + + testenv + .command() + .args(["--color", "always", "inkscape-v2"]) + .assert() + .success() + .stdout(diff(expected_no_title)); + + // Configure to enable show_title + testenv.append_to_config("display.show_title = true\n"); + + let expected_no_color = include_str!("rendered/inkscape-with-title-no-color.expected"); + + testenv + .command() + .args(["inkscape-v2"]) + .assert() + .success() + .stdout(diff(expected_no_color)); + + let expected = include_str!("rendered/inkscape-with-title.expected"); + + testenv + .command() + .args(["--color", "always", "inkscape-v2"]) + .assert() + .success() + .stdout(diff(expected)); +} + +#[test] +fn test_spaces_find_command() { + let testenv = TestEnv::new().install_default_cache(); + + testenv + .command() + .args(["git", "checkout"]) + .assert() + .success(); +} + +#[test] +fn test_pager_flag_enable() { + let testenv = TestEnv::new().install_default_cache(); + + testenv + .command() + .args(["--pager", "which"]) + .assert() + .success(); +} + +#[test] +fn test_multiple_platform_command_search() { + let testenv = TestEnv::new(); + testenv.add_os_entry("linux", "linux-only", "this command only exists for linux"); + testenv.add_os_entry( + "linux", + "windows-and-linux", + "# windows-and-linux \n\n > linux version", + ); + testenv.add_os_entry( + "windows", + "windows-and-linux", + "# windows-and-linux \n\n > windows version", + ); + + testenv + .command() + .args(["--platform", "windows", "--platform", "linux", "linux-only"]) + .assert() + .success(); + + // test order of platforms supplied if preserved + testenv + .command() + .args([ + "--platform", + "windows", + "--platform", + "linux", + "windows-and-linux", + ]) + .assert() + .success() + .stdout(contains("windows version")); + + testenv + .command() + .args([ + "--platform", + "linux", + "--platform", + "windows", + "windows-and-linux", + ]) + .assert() + .success() + .stdout(contains("linux version")); +} + +#[test] +fn test_multiple_platform_command_search_not_found() { + let testenv = TestEnv::new(); + testenv.add_os_entry( + "windows", + "windows-only", + "this command only exists for Windows", + ); + + testenv + .command() + .args(["--platform", "macos", "--platform", "linux", "windows-only"]) + .assert() + .stderr(contains("Page `windows-only` not found in cache.")); +} + +#[test] +fn test_macos_is_alias_for_osx() { + let testenv = TestEnv::new(); + testenv.add_os_entry("osx", "maconly", "this command only exists on mac"); + + testenv + .command() + .args(["--platform", "macos", "maconly"]) + .assert() + .success(); + testenv + .command() + .args(["--platform", "osx", "maconly"]) + .assert() + .success(); + + testenv + .command() + .args(["--platform", "macos", "--list"]) + .assert() + .stdout("maconly\n"); + testenv + .command() + .args(["--platform", "osx", "--list"]) + .assert() + .stdout("maconly\n"); + + testenv.append_to_config("search.platforms = ['osx']\n"); + testenv.command().arg("--list").assert().stdout("maconly\n"); + + testenv.delete_config(); + testenv.init_config(); + testenv.append_to_config("search.platforms = ['macos']\n"); + testenv.command().arg("--list").assert().stdout("maconly\n"); +} + +#[test] +fn test_common_platform_is_used_as_fallback() { + let testenv = TestEnv::new(); + testenv.add_entry("in-common", "this command comes from common"); + + // No platform specified + testenv.command().args(["in-common"]).assert().success(); + + // Platform specified + testenv + .command() + .args(["--platform", "linux", "in-common"]) + .assert() + .success(); +} + +#[test] +fn test_search_language_precedence() { + let testenv = TestEnv::new(); + for lang in ["en", "de", "it", "fr", "pl", "nl"] { + testenv.add_lang_entry(lang, lang, ""); + } + + #[expect(clippy::type_complexity)] + let run = |cases: &[(Vec<(&str, &str)>, Vec<&str>, &str)]| { + for (extra_env, extra_args, expected) in cases { + let mut cmd = testenv.command(); + for (key, value) in extra_env { + cmd.env(key, value); + } + cmd.args(extra_args); + cmd.arg("--list"); + cmd.assert().success().stdout(eq(*expected)); + } + }; + + let env_cases = &[ + (vec![], vec![], "en\n"), + (vec![("LANGUAGE", "de:it")], vec![], "en\n"), + ( + vec![("LANG", "fr"), ("LANGUAGE", "de:it")], + vec![], + "de\nen\nfr\nit\n", + ), + ( + vec![("LANG", "fr"), ("LANGUAGE", "de:it")], + vec!["--language", "pl"], + "pl\n", + ), + ]; + run(env_cases); + + // Environment is only used when config setting is not set + testenv.append_to_config("search.languages = ['nl']\n"); + let config_cases = &[ + (vec![], vec![], "nl\n"), + (vec![("LANGUAGE", "de:it")], vec![], "nl\n"), + (vec![("LANG", "fr"), ("LANGUAGE", "de:it")], vec![], "nl\n"), + ( + vec![("LANG", "fr"), ("LANGUAGE", "de:it")], + vec!["--language", "pl"], + "pl\n", + ), + ]; + run(config_cases); + + // The above update setting does not change anything + testenv.append_to_config("updates.download_languages = ['cz']"); + run(config_cases); + testenv.delete_config(); + testenv.init_config(); + testenv.append_to_config("updates.download_languages = ['cz']"); + run(env_cases); +} + +#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] +#[test] +fn test_update_language_arg() { + let testenv = TestEnv::new(); + testenv + .command() + .env("LANG", "it") + .arg("--update") + .assert() + .success() + .stderr(contains("it")) + .stderr(contains("en")); + + testenv + .command() + .env("LANG", "en") + .args(["--language", "it"]) + .arg("--update") + .assert() + .success() + .stderr(contains("it")) + .stderr(contains("en").not()); +} + +#[test] +fn test_list_flag_rendering() { + let testenv = TestEnv::new().write_custom_pages_config(); + + testenv + .command() + .args(["--list"]) + .assert() + .failure() + .stderr(contains("Page cache not found. Please run `tldr --update`")); + + testenv.add_entry("foo", ""); + + testenv + .command() + .args(["--list"]) + .assert() + .success() + .stdout("foo\n"); + + testenv.add_entry("bar", ""); + testenv.add_entry("baz", ""); + testenv.add_entry("qux", ""); + testenv.add_page_entry("faz", ""); + testenv.add_page_entry("bar", ""); + testenv.add_page_entry("fiz", ""); + testenv.add_patch_entry("buz", ""); + + testenv + .command() + .args(["--list"]) + .assert() + .success() + .stdout("bar\nbaz\nfaz\nfiz\nfoo\nqux\n"); +} + +#[test] +fn test_multi_platform_list_flag_rendering() { + let testenv = TestEnv::new().write_custom_pages_config(); + + testenv.add_entry("common", ""); + + testenv + .command() + .args(["--list"]) + .assert() + .success() + .stdout("common\n"); + + testenv + .command() + .args(["--platform", "linux", "--list"]) + .assert() + .success() + .stdout("common\n"); + + testenv + .command() + .args(["--platform", "windows", "--list"]) + .assert() + .success() + .stdout("common\n"); + + testenv.add_os_entry("linux", "rm", ""); + testenv.add_os_entry("linux", "ls", ""); + testenv.add_os_entry("windows", "del", ""); + testenv.add_os_entry("windows", "dir", ""); + testenv.add_os_entry("linux", "winux", ""); + testenv.add_os_entry("windows", "winux", ""); + + // test `--list` for `--platform linux` by itself + testenv + .command() + .args(["--platform", "linux", "--list"]) + .assert() + .success() + .stdout("common\nls\nrm\nwinux\n"); + + // test `--list` for `--platform windows` by itself + testenv + .command() + .args(["--platform", "windows", "--list"]) + .assert() + .success() + .stdout("common\ndel\ndir\nwinux\n"); + + // test `--list` for `--platform linux --platform windows` + testenv + .command() + .args(["--platform", "linux", "--platform", "windows", "--list"]) + .assert() + .success() + .stdout("common\ndel\ndir\nls\nrm\nwinux\n"); + + // test `--list` for `--platform windows --platform linux` + testenv + .command() + .args(["--platform", "linux", "--platform", "windows", "--list"]) + .assert() + .success() + .stdout("common\ndel\ndir\nls\nrm\nwinux\n"); +} + +#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] +#[test] +fn test_autoupdate_cache() { + let testenv = TestEnv::new(); + + // The first time, if automatic updates are disabled, the cache should not be found + testenv + .command() + .args(["--list"]) + .assert() + .failure() + .stderr(contains("Page cache not found. Please run `tldr --update`")); + + let cache_file_path = testenv.cache_dir().join(TLDR_PAGES_DIR); + + 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`. + let check_cache_updated = |expected| { + let assert = testenv.command().args(["--list"]).assert().success(); + let pred = contains("Successfully updated cache"); + if expected { + assert.stderr(pred) + } else { + assert.stderr(pred.not()) + }; + }; + + // The cache is updated the first time we run `tldr --list` + check_cache_updated(true); + + // The cache is not updated with a subsequent call + check_cache_updated(false); + + // We update the modification and access times such that they are about 23 hours from now. + // auto-update interval is 24 hours, the cache should not be updated + let new_mtime = SystemTime::now() - Duration::from_secs(82_800); + filetime::set_file_mtime(&cache_file_path, new_mtime.into()).unwrap(); + check_cache_updated(false); + + // We update the modification and access times such that they are about 25 hours from now. + // auto-update interval is 24 hours, the cache should be updated + let new_mtime = SystemTime::now() - Duration::from_secs(90_000); + filetime::set_file_mtime(&cache_file_path, new_mtime.into()).unwrap(); + check_cache_updated(true); + + // The cache is not updated with a subsequent call + check_cache_updated(false); +} + +/// End-end test to ensure .page.md files overwrite pages in cache_dir +#[test] +fn test_custom_page_overwrites() { + 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.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!("rendered/inkscape-default-no-color.expected"); + + testenv + .command() + .args(["inkscape-v2", "--color", "never"]) + .assert() + .success() + .stdout(diff(expected)); +} + +/// 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() + .install_default_cache() + .install_default_custom_pages(); + + // Load expected output + let expected = include_str!("rendered/inkscape-patched-no-color.expected"); + + testenv + .command() + .args(["inkscape-v2", "--color", "never"]) + .assert() + .success() + .stdout(diff(expected)); +} + +/// 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() + .install_default_cache() + .install_default_custom_pages(); + + // 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!("rendered/inkscape-default-no-color.expected"); + + testenv + .command() + .args(["inkscape-v2", "--color", "never"]) + .assert() + .success() + .stdout(diff(expected)); +} + +#[test] +#[cfg(target_os = "windows")] +fn test_pager_warning() { + let testenv = TestEnv::new().install_default_cache(); + + // Regular call should not show a "pager flag not available on windows" warning + testenv + .command() + .args(["which"]) + .assert() + .success() + .stderr(contains("pager flag not available on Windows").not()); + + // But it should be shown if the pager flag is true + testenv + .command() + .args(["--pager", "which"]) + .assert() + .success() + .stderr(contains("pager flag not available on Windows")); +} + +/// Ensure that page lookup is case insensitive, so a page lookup for `eyed3` +/// and `eyeD3` should return the same page. +#[test] +fn test_lowercased_page_lookup() { + let testenv = TestEnv::new(); + + // Lookup `eyed3`, initially fails + testenv.command().args(["eyed3"]).assert().failure(); + + // Add entry + testenv.add_entry("eyed3", "contents"); + + // Lookup `eyed3` again + testenv.command().args(["eyed3"]).assert().success(); + + // Lookup `eyeD3`, should succeed as well + testenv.command().args(["eyeD3"]).assert().success(); +} + +/// Regression test for #219: It should be possible to combine `--raw` and `-f`. +#[test] +fn test_raw_render_file() { + let testenv = TestEnv::new().install_default_cache(); + + let 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 + .command() + .args(&args) + .assert() + .success() + .stdout(diff(include_str!( + "rendered/inkscape-default-no-color.expected" + ))); + + // Raw render + args.push("--raw"); + testenv + .command() + .args(&args) + .assert() + .success() + .stdout(diff(include_str!("cache/pages.en/common/inkscape-v1.md"))); +} + +fn touch_custom_page(testenv: &TestEnv) { + let args = vec!["--edit-page", "foo"]; + + testenv + .command() + .args(&args) + .env("EDITOR", "touch") + .assert() + .success(); + assert!(testenv.custom_pages_dir().join("foo.page.md").exists()); +} + +fn touch_custom_patch(testenv: &TestEnv) { + let args = vec!["--edit-patch", "foo"]; + + testenv + .command() + .args(&args) + .env("EDITOR", "touch") + .assert() + .success(); + assert!(testenv.custom_pages_dir().join("foo.patch.md").exists()); +} + +#[test] +fn test_edit_page() { + let testenv = TestEnv::new().write_custom_pages_config(); + touch_custom_page(&testenv); +} + +#[test] +fn test_edit_patch() { + let testenv = TestEnv::new().write_custom_pages_config(); + touch_custom_patch(&testenv); +} + +#[test] +fn test_recreate_dir() { + let testenv = TestEnv::new().write_custom_pages_config(); + touch_custom_patch(&testenv); + touch_custom_page(&testenv); +} + +#[test] +fn test_custom_pages_dir_is_not_dir() { + let testenv = TestEnv::new().write_custom_pages_config(); + let _ = std::fs::remove_dir_all(testenv.custom_pages_dir()); + let _ = File::create(testenv.custom_pages_dir()).unwrap(); + assert!(testenv.custom_pages_dir().is_file()); + + let args = vec!["--edit-patch", "foo"]; + + testenv + .command() + .args(&args) + .env("EDITOR", "touch") + .assert() + .failure(); } 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/rendered/inkscape-default-no-color.expected b/tests/rendered/inkscape-default-no-color.expected new file mode 100644 index 0000000..f34c1ce --- /dev/null +++ b/tests/rendered/inkscape-default-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/rendered/inkscape-default.expected b/tests/rendered/inkscape-default.expected new file mode 100644 index 0000000..3b37f0e --- /dev/null +++ b/tests/rendered/inkscape-default.expected @@ -0,0 +1,32 @@ + + An SVG (Scalable Vector Graphics) editing program. + Use -z to not open the GUI and only process files in the console. + + Open an SVG file in the Inkscape GUI: + + inkscape filename.svg + + Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI): + + inkscape filename.svg -e filename.png + + Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur): + + inkscape filename.svg -e filename.png -w 600 -h 400 + + Export a single object, given its ID, into a bitmap: + + inkscape filename.svg -i id -e object.png + + Export an SVG document to PDF, converting all texts to paths: + + inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path + + Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: + + inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit + + Some invalid command just to test the correct highlighting of the command name: + + inkscape --use-inkscape=v3.0 file + diff --git a/tests/rendered/inkscape-patched-no-color.expected b/tests/rendered/inkscape-patched-no-color.expected new file mode 100644 index 0000000..de1db15 --- /dev/null +++ b/tests/rendered/inkscape-patched-no-color.expected @@ -0,0 +1,36 @@ + + 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 + + Custom inkscape entry + + My Inkscape example + diff --git a/tests/rendered/inkscape-with-config.expected b/tests/rendered/inkscape-with-config.expected new file mode 100644 index 0000000..33540a3 --- /dev/null +++ b/tests/rendered/inkscape-with-config.expected @@ -0,0 +1,32 @@ + + An SVG (Scalable Vector Graphics) editing program. + Use -z to not open the GUI and only process files in the console. + + Open an SVG file in the Inkscape GUI: + + inkscape filename.svg + + Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI): + + inkscape filename.svg -e filename.png + + Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur): + + inkscape filename.svg -e filename.png -w 600 -h 400 + + Export a single object, given its ID, into a bitmap: + + inkscape filename.svg -i id -e object.png + + Export an SVG document to PDF, converting all texts to paths: + + inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path + + Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: + + inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit + + Some invalid command just to test the correct highlighting of the command name: + + inkscape --use-inkscape=v3.0 file + diff --git a/tests/rendered/inkscape-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 84% rename from tests/config.toml rename to tests/style-config.toml index 7440a82..c68f2cc 100644 --- a/tests/config.toml +++ b/tests/style-config.toml @@ -3,6 +3,9 @@ foreground = "green" underline = false bold = false +[style.command_name] +bold = true + [style.description] underline = false bold = false @@ -15,3 +18,4 @@ underline = false [style.example_variable] underline = true bold = false +italic = true