mirror of
https://github.com/tealdeer-rs/tealdeer.git
synced 2026-08-09 17:59:11 +02:00
Compare commits
No commits in common. "main" and "v1.7.2" have entirely different histories.
45 changed files with 2264 additions and 3654 deletions
37
.github/workflows/ci.yml
vendored
37
.github/workflows/ci.yml
vendored
|
|
@ -15,54 +15,37 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
toolchain: [stable, 1.87.0] # MSRV
|
||||
include:
|
||||
- platform: windows-latest
|
||||
exe_suffix: .exe
|
||||
toolchain: [stable, 1.75.0]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- 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/
|
||||
run: cargo build
|
||||
- name: Build with logging and webpki roots
|
||||
run: cargo build --features logging,webpki-roots --no-default-features
|
||||
- name: Run tests
|
||||
run: cargo test -- --test-threads 1
|
||||
run: cargo test
|
||||
|
||||
clippy:
|
||||
name: run clippy lints
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
- name: run clippy lints
|
||||
run: cargo clippy --all-targets --features logging
|
||||
run: cargo clippy --features logging
|
||||
|
||||
fmt:
|
||||
name: run rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
|
|
@ -74,7 +57,7 @@ jobs:
|
|||
name: build docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v2
|
||||
with:
|
||||
|
|
|
|||
3
.github/workflows/gh-pages.yml
vendored
3
.github/workflows/gh-pages.yml
vendored
|
|
@ -3,13 +3,12 @@ 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
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v2
|
||||
|
|
|
|||
24
.github/workflows/release.yml
vendored
24
.github/workflows/release.yml
vendored
|
|
@ -8,7 +8,7 @@ jobs:
|
|||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create release for tag
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
matrix:
|
||||
target: ["bash", "fish", "zsh"]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- name: Upload completion
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
matrix:
|
||||
target: ["MIT", "APACHE"]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- name: Upload license
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
|
|
@ -66,14 +66,14 @@ jobs:
|
|||
- arch: "arm"
|
||||
libc: "musleabihf"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "tealdeer-linux-${{ matrix.arch }}-${{ matrix.libc }}"
|
||||
path: "target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr"
|
||||
|
|
@ -86,15 +86,15 @@ jobs:
|
|||
- arch: "x86_64"
|
||||
- arch: "aarch64"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
run: cargo build --release --target ${{ matrix.arch }}-apple-darwin --no-default-features --features webpki-roots
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "tealdeer-macos-${{ matrix.arch }}"
|
||||
path: "target/${{ matrix.arch }}-apple-darwin/release/tldr"
|
||||
|
|
@ -102,14 +102,14 @@ jobs:
|
|||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "tealdeer-windows-x86_64-msvc"
|
||||
path: "target/x86_64-pc-windows-msvc/release/tldr.exe"
|
||||
|
|
@ -134,8 +134,8 @@ jobs:
|
|||
- macos-aarch64
|
||||
- windows-x86_64-msvc
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
- name: Upload binary
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
|
|
|
|||
197
CHANGELOG.md
197
CHANGELOG.md
|
|
@ -13,136 +13,6 @@ Possible log types:
|
|||
- `[docs]` for documentation changes.
|
||||
- `[chore]` for maintenance work.
|
||||
|
||||
### [v1.5.1][v1.5.1], [v1.6.2][v1.6.2], [v1.7.3][v1.7.3] (2026-01-25)
|
||||
|
||||
Today I am releasing three patch updates for outdated versions of tealdeer.
|
||||
They are minimal patches for Linux distributions that ship old versions of
|
||||
tealdeer which recently broke due to an upstream change. If you can choose
|
||||
freely which version of tealdeer to use, I recommend using the latest version of
|
||||
tealdeer, 1.8.1. For more details, see the "Notes to package maintainers"
|
||||
section below.
|
||||
|
||||
All three updates contain only a single change compared to their respective
|
||||
previous versions which changes the `ARCHIVE_URL` constant used for updating the
|
||||
page cache. The reason for this change is that the upstream tldr-pages
|
||||
repository shut down the domain that clients were previously required to use.
|
||||
|
||||
Note that this issue is already fixed in tealdeer 1.8.0 where we introduced a
|
||||
config file option for changing the URL used at runtime. The versions 1.8.0 and
|
||||
1.8.1 also use the new domain of the tldr-pages archive by default, so no action
|
||||
is needed for users of those versions.
|
||||
|
||||
#### Changes
|
||||
|
||||
- [fixed] Update `ARCHIVE_URL`
|
||||
|
||||
#### Notes to package maintainers
|
||||
|
||||
I have _not_ updated the lockfile for any of these releases, so the locked
|
||||
dependency versions are still the same as they were for the previous release in
|
||||
the respective v1.x series. Updating the lockfile for tealdeer 1.5.0 to remove
|
||||
any `cargo audit` warnings while also maintaining compatibility with Rust 1.54
|
||||
also brings larger changes through transitive dependencies, which contradicts my
|
||||
plan to make this update easy to plug into existing build pipelines.
|
||||
|
||||
If you want to build / distribute tealdeer v1.5.1, v1.6.2, or v1.7.3, please use
|
||||
an up to date Rust toolchain to permit updates to newer versions of (transitive)
|
||||
dependencies. Do not use the lockfile, instead update to the newest available
|
||||
dependency versions.
|
||||
|
||||
For the same reason, there are no artifacts attached to the GitHub releases of
|
||||
these versions.
|
||||
|
||||
### [v1.8.1][v1.8.1] (2025-11-11)
|
||||
|
||||
This patch release tweaks the enabled features for ureq, the library we use to
|
||||
perform HTTP requests when updating the cache. In particular, support for socks
|
||||
proxies is now enabled.
|
||||
|
||||
#### Changes:
|
||||
|
||||
- [added] Enable ureq's socks-proxy feature ([#451])
|
||||
|
||||
### [v1.8.0][v1.8.0] (2025-10-03)
|
||||
|
||||
One year and one day have passed since tealdeer version 1.7.0 was released, so
|
||||
it's time for an update! Tealdeer 1.8 comes with a complete rewrite of the page
|
||||
cache and contains many long awaited improvements around it.
|
||||
|
||||
Firstly, tealdeer now supports language-specific downloads. This means that only
|
||||
the pages matching the configured languages are downloaded when updating the
|
||||
cache. The languages used for searching pages can be configured separately to
|
||||
the ones used for updating, so it is possible to download pages in languages
|
||||
that are not usually queried.
|
||||
|
||||
Next to configuring which languages are used for searching, it is now also
|
||||
possible to specify which platforms are used in the config file. Importantly,
|
||||
the default behavior for page search has changed so that all platforms are
|
||||
searched if no page is found for the platform that tealdeer is running on. To
|
||||
restore the behavior of tealdeer 1.7, users should set
|
||||
```toml
|
||||
[search]
|
||||
platforms = ["current", "common"]
|
||||
```
|
||||
in their config file.
|
||||
|
||||
Coming back to updating, the default build configuration of tealdeer now
|
||||
includes multiple TLS backends. This means that tealdeer does not have to be
|
||||
rebuilt to try out a different TLS backend. The used backend can be chosen in
|
||||
the config file. By default, tealdeer comes with support for rustls using webpki
|
||||
certificates or system certificates. Native TLS is supported, but not enabled by
|
||||
default to avoid build troubles with OpenSSL and musl.
|
||||
|
||||
For details, please refer to the [user documentation].
|
||||
|
||||
#### Changes:
|
||||
|
||||
- [added] Resolve paths in config `[directories]` relative to the config directory ([#306])
|
||||
- [added] Add `common` platform to CLI ([#401])
|
||||
- [added] Add configuration option for `archive_source` ([#337])
|
||||
- [added] Allows configuring TLS backend ([#386])
|
||||
- [added] Add args: `--edit-page` and `--edit-patch` ([#388])
|
||||
- [added] Add an option to specify a custom config file to be used ([#422])
|
||||
- [added] Upload binaries from build step as artifact ([#423])
|
||||
- [added] Add `search.languages` and `updates.download_languages` settings ([#430])
|
||||
- [added] Add `search.platforms` config option and search all platforms by default ([#435])
|
||||
- [added] Add `display.show_title` option to display command titles in output ([#439])
|
||||
- [chore] Various test improvements ([#399])
|
||||
- [chore] Add tests for osx/macos alias ([#407])
|
||||
- [chore] Move most of `main` to `try_main` ([#400])
|
||||
- [chore] Only create a single temporary directory in integration tests ([#411])
|
||||
- [chore] Replace reqwest with ureq ([#417])
|
||||
- [chore] Introduce Language struct ([#425])
|
||||
- [chore] Cache rewrite ([#416])
|
||||
- [chore] Allow references in `Config` ([#429])
|
||||
- [docs] Highlight code examples in user docs ([#440])
|
||||
- [removed] Remove native-tls from default feature set ([#436])
|
||||
|
||||
#### Contributors to this version:
|
||||
|
||||
- [Christoph Loy][@beatbrot]
|
||||
- [Erick Guan][@erickguan]
|
||||
- [@MHS-0][@MHS-0]
|
||||
- [Matěj Kafka][@MatejKafka]
|
||||
- [Nachiket Kanore][@nachiketkanore]
|
||||
- [Niklas Mohrin][@niklasmohrin]
|
||||
- [Predrag Minic][@mipedja]
|
||||
- [@hex1c][@hex1c]
|
||||
- [lyj][@lengyijun]
|
||||
|
||||
Thanks!
|
||||
|
||||
#### Notes to package maintainers
|
||||
|
||||
1. The MSRV has been bumped to 1.85.
|
||||
2. Consider whether you want to include the `native-tls` feature in your build
|
||||
of tealdeer. The feature is disabled for the binaries in the GitHub release
|
||||
because we target musl, but it might work out of the box for your
|
||||
distribution.
|
||||
3. We have added the `ignore-online-tests` feature to automatically mark all
|
||||
tests that require an internet connection as skipped, so you can use this
|
||||
feature instead of maintaining a list of these tests yourself.
|
||||
|
||||
### [v1.7.2][v1.7.2] (2025-03-18)
|
||||
|
||||
This patch release updates the `zip` dependency to mitigate a potential security
|
||||
|
|
@ -164,11 +34,11 @@ 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:
|
||||
Changes:
|
||||
|
||||
- [chore] Upgrade yansi: 0.5.1 -> 1.0.1 ([#389])
|
||||
|
||||
#### Contributors to this version:
|
||||
Contributors to this version:
|
||||
|
||||
- [Blair Noctis][@nc7s]
|
||||
|
||||
|
|
@ -204,7 +74,7 @@ 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:
|
||||
Changes:
|
||||
|
||||
- [added] Allow querying multiple platforms ([#300])
|
||||
- [added] Add BSD platform support ([#354])
|
||||
|
|
@ -224,7 +94,7 @@ details, see [#376](https://github.com/tealdeer-rs/tealdeer/issues/376).
|
|||
- [chore] Update Cargo.toml license field following SPDX 2.1 ([#336])
|
||||
- [chore] Dependency updates
|
||||
|
||||
#### Contributors to this version:
|
||||
Contributors to this version:
|
||||
|
||||
- [Adam Henley][@adamazing]
|
||||
- [Andrea Frigido][@frisoft]
|
||||
|
|
@ -248,12 +118,12 @@ Thanks!
|
|||
|
||||
### [v1.6.1][v1.6.1] (2022-10-24)
|
||||
|
||||
#### Changes:
|
||||
Changes:
|
||||
|
||||
- [fixed] Fix path source for custom pages dir ([#297])
|
||||
- [chore] Update dependendencies ([#299])
|
||||
|
||||
#### Contributors to this version:
|
||||
Contributors to this version:
|
||||
|
||||
- [Cyrus Yip][@CyrusYip]
|
||||
- [Danilo Bargen][@dbrgn]
|
||||
|
|
@ -272,7 +142,7 @@ 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:
|
||||
Changes:
|
||||
|
||||
- [added] Allow overriding cache directory through config ([#276])
|
||||
- [added] Add `--no-auto-update` CLI flag ([#257])
|
||||
|
|
@ -293,7 +163,7 @@ subdirectory! Packaging scripts might need to be updated.
|
|||
- [chore] Use anyhow for error handling ([#249])
|
||||
- [chore] Switch to Rust 2021 edition ([#284])
|
||||
|
||||
#### Contributors to this version:
|
||||
Contributors to this version:
|
||||
|
||||
- [@bagohart][@bagohart]
|
||||
- [@cyqsimon][@cyqsimon]
|
||||
|
|
@ -342,7 +212,7 @@ Note that the MSRV (Minimal Supported Rust Version) of the project
|
|||
> When publishing a tealdeer release, the Rust version required to build it
|
||||
> should be stable for at least a month.
|
||||
|
||||
#### Changes:
|
||||
Changes:
|
||||
|
||||
- [added] Support custom pages and patches ([#142][i142])
|
||||
- [added] Multi-language support ([#125][i125], [#161][i161])
|
||||
|
|
@ -373,7 +243,7 @@ Note that the MSRV (Minimal Supported Rust Version) of the project
|
|||
- [chore] All release binaries are now generated in CI. Binaries for macOS and Windows are also provided. ([#240][i240])
|
||||
- [chore] Update all dependencies
|
||||
|
||||
#### Contributors to this version:
|
||||
Contributors to this version:
|
||||
|
||||
- [@bl-ue][@bl-ue]
|
||||
- [Cameron Tod][@cam8001]
|
||||
|
|
@ -402,7 +272,7 @@ co-maintainer. Thank you for your help!
|
|||
|
||||
- [fixed] Syntax error in zsh completion file ([#138][i138])
|
||||
|
||||
#### Contributors to this version:
|
||||
Contributors to this version:
|
||||
|
||||
- [Danilo Bargen][@dbrgn]
|
||||
- [Bruno A. Muciño][@mucinoab]
|
||||
|
|
@ -419,7 +289,7 @@ Thanks!
|
|||
- [changed] Make `--list` option comply with official spec ([#112][i112])
|
||||
- [changed] Move cache age warning to stderr ([#113][i113])
|
||||
|
||||
#### Contributors to this version:
|
||||
Contributors to this version:
|
||||
|
||||
- [Atul Bhosale][@Atul9]
|
||||
- [Danilo Bargen][@dbrgn]
|
||||
|
|
@ -445,7 +315,7 @@ Thanks!
|
|||
- [fixed] Fix Fish autocompletion on macOS ([#87][i87])
|
||||
- [fixed] Fix compilation on Windows by disabling pager ([#99][i99])
|
||||
|
||||
#### Contributors to this version:
|
||||
Contributors to this version:
|
||||
|
||||
- [Bruno Heridet][@Delapouite]
|
||||
- [Danilo Bargen][@dbrgn]
|
||||
|
|
@ -471,7 +341,7 @@ Thanks!
|
|||
- [changed] Move to Rust 2018, require Rust 1.32 ([#69][i69] / [#84][i84])
|
||||
- [fixed] Add (back) support for proxies ([#68][i68])
|
||||
|
||||
#### Contributors to this version:
|
||||
Contributors to this version:
|
||||
|
||||
- [Bar Hatsor][@Bassets]
|
||||
- [Danilo Bargen][@dbrgn]
|
||||
|
|
@ -494,7 +364,7 @@ Thanks!
|
|||
- [changed] Require at least Rust 1.28 to build (previous: 1.19)
|
||||
- [fixed] Fix building on systems with openssl 1.1.1 ([#47][i47])
|
||||
|
||||
#### Contributors to this version:
|
||||
Contributors to this version:
|
||||
|
||||
- [Danilo Bargen][@dbrgn]
|
||||
- [@equal-l2][@equal-l2]
|
||||
|
|
@ -527,7 +397,7 @@ Thanks!
|
|||
|
||||
- First crates.io release
|
||||
|
||||
[user documentation]: https://tealdeer-rs.github.io/tealdeer/
|
||||
|
||||
|
||||
[@0ndorio]: https://github.com/0ndorio
|
||||
[@adamazing]: https://github.com/adamazing
|
||||
|
|
@ -590,14 +460,6 @@ Thanks!
|
|||
[@Walker-00]: https://github.com/Walker-00
|
||||
[@YDX-2147483647]: https://github.com/YDX-2147483647
|
||||
[@zedseven]: https://github.com/zedseven
|
||||
[@beatbrot]: https://github.com/beatbrot
|
||||
[@erickguan]: https://github.com/erickguan
|
||||
[@MHS-0]: https://github.com/MHS-0
|
||||
[@MatejKafka]: https://github.com/MatejKafka
|
||||
[@nachiketkanore]: https://github.com/nachiketkanore
|
||||
[@mipedja]: https://github.com/mipedja
|
||||
[@hex1c]: https://github.com/hex1c
|
||||
[@lengyijun]: https://github.com/lengyijun
|
||||
|
||||
[v1.0.0]: https://github.com/tealdeer-rs/tealdeer/compare/v0.4.0...v1.0.0
|
||||
[v1.1.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.0.0...v1.1.0
|
||||
|
|
@ -606,16 +468,11 @@ Thanks!
|
|||
[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/tealdeer-rs/tealdeer/issues/34
|
||||
[i43]: https://github.com/tealdeer-rs/tealdeer/issues/43
|
||||
|
|
@ -687,7 +544,6 @@ Thanks!
|
|||
[#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
|
||||
|
|
@ -696,29 +552,8 @@ Thanks!
|
|||
[#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
|
||||
|
|
|
|||
1469
Cargo.lock
generated
1469
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
32
Cargo.toml
32
Cargo.toml
|
|
@ -10,9 +10,9 @@ name = "tealdeer"
|
|||
readme = "README.md"
|
||||
repository = "https://github.com/tealdeer-rs/tealdeer/"
|
||||
documentation = "https://tealdeer-rs.github.io/tealdeer/"
|
||||
version = "1.8.1"
|
||||
version = "1.7.2"
|
||||
include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"]
|
||||
rust-version = "1.87" # MSRV
|
||||
rust-version = "1.75"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
|
|
@ -21,16 +21,17 @@ path = "src/main.rs"
|
|||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
app_dirs = { version = "2", package = "app_dirs2" }
|
||||
clap = { version = "4", features = ["std", "derive", "help", "usage", "cargo", "error-context", "color", "wrap_help"], default-features = false }
|
||||
env_logger = { version = "0.11", optional = true }
|
||||
etcetera = "0.11.0"
|
||||
log = "0.4"
|
||||
reqwest = { version = "0.12.5", features = ["blocking"], default-features = false }
|
||||
serde = "1.0.21"
|
||||
serde_derive = "1.0.21"
|
||||
ureq = { version = "3.0.8", default-features = false, features = ["gzip", "socks-proxy"] }
|
||||
toml = "0.8.19"
|
||||
walkdir = "2.0.1"
|
||||
yansi = "1"
|
||||
zip = { version = "5.1.1", default-features = false, features = ["deflate"] }
|
||||
zip = { version = "2.3.0", default-features = false, features = ["deflate"] }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
pager = "0.16"
|
||||
|
|
@ -43,16 +44,21 @@ 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"]
|
||||
default = ["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 = []
|
||||
# Reqwest (the HTTP client library) can handle TLS connections in three
|
||||
# different modes:
|
||||
#
|
||||
# - Rustls with native roots
|
||||
# - Rustls with WebPK roots
|
||||
# - Native TLS (SChannel on Windows, Secure Transport on macOS and OpenSSL otherwise)
|
||||
#
|
||||
# Exactly one of the three variants must be selected. By default, Rustls with
|
||||
# native roots is enabled.
|
||||
native-roots = ["reqwest/rustls-tls-native-roots"]
|
||||
webpki-roots = ["reqwest/rustls-tls-webpki-roots"]
|
||||
native-tls = ["reqwest/native-tls"]
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -52,6 +52,7 @@ Docker container using sharkdp's [`hyperfine`][hyperfine-gh]
|
|||
| [`fast-tldr`][fast-tldr-gh] | Haskell | 17.0 | 0.6 | no example highlighting |
|
||||
| [`tldr-hs`][hs-gh] | Haskell | 25.1 | 0.5 | no example highlighting |
|
||||
| [`tldr-bash`][bash-gh] | Bash | 30.0 | 0.8 | |
|
||||
| [`tldr-c`][c-gh] | C | 38.4 | 1.0 | |
|
||||
| [`tldr-python-client`][python-gh] | Python | 87.0 | 2.4 | |
|
||||
| [`tldr-node-client`][node-gh] | JavaScript / NodeJS | 407.1 | 12.9 | |
|
||||
|
||||
|
|
@ -86,17 +87,6 @@ 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
|
||||
|
|
@ -123,6 +113,7 @@ Thanks to @severen for coming up with the name "tealdeer"!
|
|||
|
||||
|
||||
[node-gh]: https://github.com/tldr-pages/tldr-node-client
|
||||
[c-gh]: https://github.com/tldr-pages/tldr-c-client
|
||||
[hs-gh]: https://github.com/psibi/tldr-hs
|
||||
[fast-tldr-gh]: https://github.com/gutjuri/fast-tldr
|
||||
[bash-gh]: https://4e4.win/tldr
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
- [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)
|
||||
|
|
|
|||
|
|
@ -9,15 +9,11 @@ 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
|
||||
```
|
||||
$ tldr --show-paths
|
||||
|
||||
Creating the config file can be done manually or with the help of `tldr`:
|
||||
|
||||
```shell
|
||||
$ tldr --seed-config
|
||||
```
|
||||
$ tldr --seed-config
|
||||
|
||||
On Linux, this will usually be `~/.config/tealdeer/config.toml`.
|
||||
|
||||
|
|
@ -26,14 +22,13 @@ On Linux, this will usually be `~/.config/tealdeer/config.toml`.
|
|||
Here's an example configuration file. Note that this example does not contain
|
||||
all possible config options. For details on the things that can be configured,
|
||||
please refer to the subsections of this documentation page
|
||||
([display](config_display.html), [style](config_style.html), [search](config_search.html),
|
||||
([display](config_display.html), [style](config_style.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"
|
||||
|
|
|
|||
|
|
@ -8,10 +8,8 @@ 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/"
|
||||
```
|
||||
[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/`.
|
||||
|
|
@ -23,7 +21,5 @@ 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/"
|
||||
```
|
||||
[directories]
|
||||
custom_pages_dir = "/home/myuser/custom-tldr-pages/"
|
||||
|
|
|
|||
|
|
@ -6,10 +6,8 @@ In the `display` section you can configure the output format.
|
|||
|
||||
Specifies whether the pager should be used by default or not (default `false`).
|
||||
|
||||
```toml
|
||||
[display]
|
||||
use_pager = true
|
||||
```
|
||||
[display]
|
||||
use_pager = true
|
||||
|
||||
When enabled, `less -R` is used as pager. To override the pager command used,
|
||||
set the `PAGER` environment variable.
|
||||
|
|
@ -21,51 +19,5 @@ NOTE: This feature is not available on Windows.
|
|||
Set this to enforce more compact output, where empty lines are stripped out
|
||||
(default `false`).
|
||||
|
||||
```toml
|
||||
[display]
|
||||
compact = true
|
||||
```
|
||||
|
||||
## `show_title`
|
||||
|
||||
Display the command name at the top of the page output (default `false`).
|
||||
|
||||
```toml
|
||||
[display]
|
||||
show_title = true
|
||||
```
|
||||
|
||||
When enabled, the command name will be displayed at the top of the output,
|
||||
styled with the `command_name` style configuration.
|
||||
|
||||
## `indent`
|
||||
|
||||
Controls the indentation of the output via two sub-keys.
|
||||
|
||||
### `indent.base`
|
||||
|
||||
Specifies the number of spaces used to indent descriptions, example text, and titles (default `2`).
|
||||
|
||||
```toml
|
||||
[display.indent]
|
||||
base = 2
|
||||
```
|
||||
|
||||
### `indent.command`
|
||||
|
||||
Specifies the number of spaces used to indent example code lines (default `6`).
|
||||
|
||||
```toml
|
||||
[display.indent]
|
||||
command = 6
|
||||
```
|
||||
|
||||
You can also configure both subkeys in a single line like this:
|
||||
|
||||
```toml
|
||||
[display]
|
||||
indent = {
|
||||
base = 2,
|
||||
command = 6,
|
||||
}
|
||||
```
|
||||
[display]
|
||||
compact = true
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
# Section: \[search\]
|
||||
|
||||
This config section is used to configure the page search in the cache.
|
||||
The settings apply to `tldr <page>` 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"]
|
||||
```
|
||||
|
|
@ -26,22 +26,16 @@ Colors can be specified in one of three ways:
|
|||
|
||||
Example:
|
||||
|
||||
```toml
|
||||
foreground = "green"
|
||||
```
|
||||
foreground = "green"
|
||||
|
||||
- 256 color ANSI code (*tealdeer v1.5.0+*)
|
||||
|
||||
Example:
|
||||
|
||||
```toml
|
||||
foreground = { ansi = 4 }
|
||||
```
|
||||
foreground = { ansi = 4 }
|
||||
|
||||
- 24-bit RGB color (*tealdeer v1.5.0+*)
|
||||
|
||||
Example:
|
||||
|
||||
```toml
|
||||
background = { rgb = { r = 255, g = 255, b = 255 } }
|
||||
```
|
||||
background = { rgb = { r = 255, g = 255, b = 255 } }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
# 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
|
||||
|
|
@ -13,10 +11,8 @@ default.
|
|||
Specifies whether the auto-update feature should be enabled (defaults to
|
||||
`false`).
|
||||
|
||||
```toml
|
||||
[updates]
|
||||
auto_update = true
|
||||
```
|
||||
[updates]
|
||||
auto_update = true
|
||||
|
||||
### `auto_update_interval_hours`
|
||||
|
||||
|
|
@ -24,68 +20,7 @@ Duration, since the last cache update, after which the cache will be
|
|||
refreshed (defaults to 720 hours). This parameter is ignored if `auto_update`
|
||||
is set to `false`.
|
||||
|
||||
```toml
|
||||
[updates]
|
||||
auto_update = true
|
||||
auto_update_interval_hours = 24
|
||||
```
|
||||
[updates]
|
||||
auto_update = true
|
||||
auto_update_interval_hours = 24
|
||||
|
||||
### `warn_cache_age`
|
||||
|
||||
Controls when a warning is printed if the cache has not been updated in a while.
|
||||
By default, the warning is shown once the cache is older than 30 days. Set this
|
||||
to `"never"` to silence the warning. This is useful if, for some reason, the
|
||||
modification time does not reflect its actual age.
|
||||
|
||||
```toml
|
||||
[updates]
|
||||
warn_cache_age = "never"
|
||||
```
|
||||
|
||||
## Download configuration
|
||||
|
||||
### `download_languages`
|
||||
|
||||
The list of languages which should be downloaded when updating.
|
||||
If unspecified, the languages listed in the `search.languages` setting are used.
|
||||
Thus, this setting is the most useful to instruct tealdeer to download pages in additional languages that are not searched by default.
|
||||
Either way, the language used can be overwritten using the `--language` command line flag.
|
||||
|
||||
```toml
|
||||
[search]
|
||||
languages = ["de", "en"]
|
||||
|
||||
[updates]
|
||||
# sometimes I like to read the Italian description
|
||||
download_languages = ["de", "en", "it"]
|
||||
```
|
||||
|
||||
### `archive_source`
|
||||
|
||||
URL for the location of the tldr pages archive. By default the pages are
|
||||
fetched from the latest `tldr-pages/tldr` GitHub release.
|
||||
|
||||
```toml
|
||||
[updates]
|
||||
archive_source = "https://my-company.example.com/tldr/"
|
||||
```
|
||||
|
||||
### `tls_backend`
|
||||
|
||||
Specifies which TLS backend to use. Try changing this setting if you encounter certificate errors.
|
||||
|
||||
Available options:
|
||||
- `rustls-with-native-roots` - [Rustls][rustls] (a TLS library in Rust) with native roots
|
||||
- `rustls-with-webpki-roots` - Rustls with [WebPKI][rustls-webpki] roots
|
||||
- `native-tls` - Native TLS
|
||||
- SChannel on Windows
|
||||
- Secure Transport on macOS
|
||||
- OpenSSL on other platforms
|
||||
|
||||
```toml
|
||||
[updates]
|
||||
tls_backend = "native-tls"
|
||||
```
|
||||
|
||||
[rustls]: https://github.com/rustls/rustls
|
||||
[rustls-webpki]: https://github.com/rustls/webpki
|
||||
|
|
|
|||
|
|
@ -38,29 +38,21 @@ Simply download the binary for your platform and run it!
|
|||
|
||||
Build and install the tool via cargo...
|
||||
|
||||
```shell
|
||||
$ cargo install tealdeer
|
||||
```
|
||||
$ cargo install tealdeer
|
||||
|
||||
## Build From Source
|
||||
|
||||
Release build:
|
||||
|
||||
```shell
|
||||
$ cargo build --release
|
||||
```
|
||||
$ cargo build --release
|
||||
|
||||
Release build with native TLS support:
|
||||
Release build with bundled CA roots:
|
||||
|
||||
```shell
|
||||
$ cargo build --release --features native-tls
|
||||
```
|
||||
$ cargo build --release --no-default-features --features webpki-roots
|
||||
|
||||
Debug build with logging support:
|
||||
|
||||
```shell
|
||||
$ cargo build --features logging
|
||||
```
|
||||
$ cargo build --features logging
|
||||
|
||||
(To enable logging at runtime, export the `RUST_LOG=tldr=debug` env variable.)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
tealdeer 1.8.1: A fast TLDR client
|
||||
tealdeer 1.7.2: A fast TLDR client
|
||||
Danilo Bargen <mail@dbrgn.ch>, Niklas Mohrin <dev@niklasmohrin.de>
|
||||
|
||||
Usage: tldr [OPTIONS] [COMMAND]...
|
||||
|
|
@ -8,17 +8,14 @@ Arguments:
|
|||
|
||||
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 <FILE> Render a specific markdown file
|
||||
-p, --platform <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]
|
||||
freebsd, netbsd, openbsd]
|
||||
-L, --language <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 <FILE> 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
|
||||
|
|
@ -29,5 +26,3 @@ Options:
|
|||
-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.
|
||||
|
|
|
|||
|
|
@ -28,15 +28,11 @@ your custom page will be shown instead of the upstream version in the cache.
|
|||
|
||||
Path:
|
||||
|
||||
```plain
|
||||
$CUSTOM_PAGES_DIR/<command>.page.md
|
||||
```
|
||||
$CUSTOM_PAGES_DIR/<command>.page.md
|
||||
|
||||
Example:
|
||||
|
||||
```plain
|
||||
~/.local/share/tealdeer/pages/ufw.page.md
|
||||
```
|
||||
~/.local/share/tealdeer/pages/ufw.page.md
|
||||
|
||||
## Custom Patches
|
||||
|
||||
|
|
@ -47,12 +43,8 @@ pages.
|
|||
|
||||
Path:
|
||||
|
||||
```plain
|
||||
$CUSTOM_PAGES_DIR/<command>.patch.md
|
||||
```
|
||||
$CUSTOM_PAGES_DIR/<command>.patch.md
|
||||
|
||||
Example:
|
||||
|
||||
```plain
|
||||
~/.local/share/tealdeer/pages/ufw.patch.md
|
||||
```
|
||||
~/.local/share/tealdeer/pages/ufw.patch.md
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
# tldr
|
||||
|
||||
> This is a builtin page that shows information for your installed tealdeer version.
|
||||
> More information: <https://tealdeer-rs.github.io/tealdeer/>.
|
||||
|
||||
> 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 <FILE>`
|
||||
|
||||
- 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`
|
||||
665
src/cache.rs
665
src/cache.rs
|
|
@ -1,39 +1,27 @@
|
|||
use std::{
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fs::{self, File},
|
||||
io::{Cursor, ErrorKind, Read},
|
||||
io::{BufReader, Cursor, Read},
|
||||
path::{Path, PathBuf},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
use log::{debug, info};
|
||||
use ureq::{
|
||||
http::StatusCode,
|
||||
tls::{RootCerts, TlsConfig, TlsProvider},
|
||||
Agent,
|
||||
};
|
||||
use anyhow::{ensure, Context, Result};
|
||||
use log::debug;
|
||||
use reqwest::{blocking::Client, Proxy};
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::{
|
||||
config::{Language, TlsBackend},
|
||||
types::PlatformType,
|
||||
};
|
||||
use crate::{types::PlatformType, utils::print_warning};
|
||||
|
||||
pub static TLDR_PAGES_DIR: &str = "tldr-pages";
|
||||
pub static TLDR_OLD_PAGES_DIR: &str = "tldr-master";
|
||||
static TLDR_OLD_PAGES_DIR: &str = "tldr-master";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CacheConfig<'a> {
|
||||
pub pages_directory: &'a Path,
|
||||
pub custom_pages_directory: Option<&'a Path>,
|
||||
pub platforms: &'a [PlatformType],
|
||||
pub search_languages: &'a [Language<'a>],
|
||||
pub download_languages: &'a [Language<'a>],
|
||||
}
|
||||
|
||||
/// The directory backing this cache is checked to be populated at construction.
|
||||
pub struct Cache<'a> {
|
||||
config: CacheConfig<'a>,
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
cache_dir: PathBuf,
|
||||
enable_styles: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -42,228 +30,6 @@ pub struct PageLookupResult {
|
|||
pub patch_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
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<Option<Self>> {
|
||||
match config.pages_directory.metadata() {
|
||||
Ok(md) => {
|
||||
ensure!(
|
||||
md.is_dir(),
|
||||
"Cache directory `{}` exists, but is not a directory.",
|
||||
config.pages_directory.display(),
|
||||
);
|
||||
Ok(Some(Cache { config }))
|
||||
}
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(anyhow!(err).context(format!(
|
||||
"Error getting metdata of cache directory {}",
|
||||
config.pages_directory.display()
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Open an existing cache at `config.pages_directory` or create one if no cache resides at
|
||||
/// this location. In case of success, the return value is a tuple with the `Cache` and a
|
||||
/// boolean indicating whether the cache was newly created.
|
||||
pub fn open_or_create(config: CacheConfig<'a>) -> Result<(Self, bool)> {
|
||||
if let Some(cache) = Self::open(config.clone())? {
|
||||
return Ok((cache, false));
|
||||
}
|
||||
|
||||
fs::create_dir_all(config.pages_directory).with_context(|| {
|
||||
format!(
|
||||
"Cache directory `{}` cannot be created",
|
||||
config.pages_directory.display(),
|
||||
)
|
||||
})?;
|
||||
eprintln!(
|
||||
"Successfully created cache directory `{}`.",
|
||||
config.pages_directory.display(),
|
||||
);
|
||||
|
||||
Ok((Cache { config }, true))
|
||||
}
|
||||
|
||||
pub fn age(&self) -> Result<Duration> {
|
||||
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<PageLookupResult> {
|
||||
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<impl IntoIterator<Item = String>> {
|
||||
let mut pages = Vec::new();
|
||||
|
||||
let mut append_all = |directory: &Path, suffix: &str| -> Result<()> {
|
||||
let Ok(file_iter) = fs::read_dir(directory) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
for entry in file_iter {
|
||||
let entry = entry?;
|
||||
if entry.file_type()?.is_file() {
|
||||
let mut page_path = entry
|
||||
.file_name()
|
||||
.into_string()
|
||||
.map_err(|_| anyhow!("Found invalid filename: {:?}", entry.path()))?;
|
||||
|
||||
if page_path.ends_with(suffix) {
|
||||
page_path.truncate(page_path.len() - suffix.len());
|
||||
pages.push(page_path);
|
||||
} else {
|
||||
debug!(
|
||||
"Skipping page entry not ending in \".md\": {:?}",
|
||||
entry.path(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let mut search_path = self.config.pages_directory.to_path_buf();
|
||||
for language in self.config.search_languages {
|
||||
search_path.push(language.directory_name());
|
||||
for platform in self.config.platforms {
|
||||
search_path.push(platform.directory_name());
|
||||
append_all(&search_path, ".md")?;
|
||||
search_path.pop();
|
||||
}
|
||||
search_path.pop();
|
||||
}
|
||||
|
||||
if let Some(custom_pages_dir) = self.config.custom_pages_directory {
|
||||
append_all(custom_pages_dir, ".page.md")?;
|
||||
}
|
||||
|
||||
pages.sort_unstable();
|
||||
pages.dedup();
|
||||
Ok(pages)
|
||||
}
|
||||
|
||||
pub fn old_custom_pages_exist(&self) -> Result<bool> {
|
||||
let Some(directory) = self.config.custom_pages_directory else {
|
||||
return Ok(false);
|
||||
};
|
||||
let Ok(file_iter) = fs::read_dir(directory) else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
for entry in file_iter {
|
||||
if let Some(extension) = entry?.path().extension() {
|
||||
if extension == "page" || extension == "patch" {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub fn clear(self) -> Result<()> {
|
||||
fs::remove_dir_all(self.config.pages_directory).with_context(|| {
|
||||
format!(
|
||||
"Could not remove pages directory at {}",
|
||||
self.config.pages_directory.display(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Download archives for the languages in `self.config().download_languages` and replace the
|
||||
/// pages directory with the newly downloaded pages. As not all languages might have pages
|
||||
/// available (for example, `en_US` instead of `en`), an iterator yielding all languages which
|
||||
/// were successfully downloaded is returned.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
archive_url: &str,
|
||||
tls_backend: TlsBackend,
|
||||
) -> Result<impl IntoIterator<Item = Language<'_>>> {
|
||||
let client = Self::build_client(tls_backend);
|
||||
|
||||
// Download everything before deleting anything
|
||||
let mut archives = self
|
||||
.config
|
||||
.download_languages
|
||||
.iter()
|
||||
.map(|&lang| {
|
||||
Ok((
|
||||
lang,
|
||||
Self::download(
|
||||
&client,
|
||||
&format!("{archive_url}/tldr-{}.zip", lang.directory_name()),
|
||||
)?
|
||||
.map(|bytes| ZipArchive::new(Cursor::new(bytes)))
|
||||
.transpose()?,
|
||||
))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
// Clear cache directory
|
||||
// Note: This is not the best solution. Ideally we would download the
|
||||
// archive to a temporary directory and then swap the two directories.
|
||||
// But renaming a directory doesn't work across filesystems and Rust
|
||||
// does not yet offer a recursive directory copying function. So for
|
||||
// now, we'll use this approach.
|
||||
fs::remove_dir_all(self.config.pages_directory)?;
|
||||
fs::create_dir(self.config.pages_directory)?;
|
||||
|
||||
for (lang, archive) in &mut archives {
|
||||
if let Some(archive) = archive {
|
||||
info!("Extracting archive for {lang:?}");
|
||||
archive.extract(self.config.pages_directory.join(lang.directory_name()))?;
|
||||
} else {
|
||||
info!("No archive found for {lang:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(archives
|
||||
.into_iter()
|
||||
.filter_map(|(lang, archive)| archive.is_some().then_some(lang)))
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &CacheConfig<'a> {
|
||||
&self.config
|
||||
}
|
||||
}
|
||||
|
||||
impl PageLookupResult {
|
||||
pub fn with_page(page_path: PathBuf) -> Self {
|
||||
Self {
|
||||
|
|
@ -277,12 +43,12 @@ impl PageLookupResult {
|
|||
self
|
||||
}
|
||||
|
||||
/// Create a reader that sequentially reads from the page and the
|
||||
/// Create a buffered 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<Box<dyn Read>> {
|
||||
pub fn reader(&self) -> Result<BufReader<Box<dyn Read>>> {
|
||||
// Open page file
|
||||
let page_file = File::open(&self.page_path)
|
||||
.with_context(|| format!("Could not open page file at {}", self.page_path.display()))?;
|
||||
|
|
@ -302,23 +68,151 @@ impl PageLookupResult {
|
|||
// the page and patch files and that will read them sequentially,
|
||||
// because it avoids the boxing below. However, the performance impact
|
||||
// would first need to be shown to be significant using a benchmark.
|
||||
Ok(if let Some(patch_file) = patch_file_opt {
|
||||
Ok(BufReader::new(if let Some(patch_file) = patch_file_opt {
|
||||
Box::new(page_file.chain(&b"\n"[..]).chain(patch_file)) as Box<dyn Read>
|
||||
} else {
|
||||
Box::new(page_file) as Box<dyn Read>
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Language<'_> {
|
||||
fn directory_name(&self) -> String {
|
||||
format!("pages.{}", self.0)
|
||||
}
|
||||
pub enum CacheFreshness {
|
||||
/// The cache is still fresh (less than `MAX_CACHE_AGE` old)
|
||||
Fresh,
|
||||
/// The cache is stale and should be updated
|
||||
Stale(Duration),
|
||||
/// The cache is missing
|
||||
Missing,
|
||||
}
|
||||
|
||||
impl PlatformType {
|
||||
fn directory_name(self) -> &'static str {
|
||||
match self {
|
||||
impl Cache {
|
||||
pub fn new<P>(cache_dir: P, enable_styles: bool) -> Self
|
||||
where
|
||||
P: Into<PathBuf>,
|
||||
{
|
||||
Self {
|
||||
cache_dir: cache_dir.into(),
|
||||
enable_styles,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache_dir(&self) -> &Path {
|
||||
&self.cache_dir
|
||||
}
|
||||
|
||||
/// Make sure that the cache directory exists and is a directory.
|
||||
/// If necessary, create the directory.
|
||||
fn ensure_cache_dir_exists(&self) -> Result<()> {
|
||||
// Check whether `cache_dir` exists and is a directory
|
||||
let (cache_dir_exists, cache_dir_is_dir) = self
|
||||
.cache_dir
|
||||
.metadata()
|
||||
.map_or((false, false), |md| (true, md.is_dir()));
|
||||
ensure!(
|
||||
!cache_dir_exists || cache_dir_is_dir,
|
||||
"Cache directory path `{}` is not a directory",
|
||||
self.cache_dir.display(),
|
||||
);
|
||||
|
||||
if !cache_dir_exists {
|
||||
// If missing, try to create the complete directory path
|
||||
fs::create_dir_all(&self.cache_dir).with_context(|| {
|
||||
format!(
|
||||
"Cache directory path `{}` cannot be created",
|
||||
self.cache_dir.display(),
|
||||
)
|
||||
})?;
|
||||
eprintln!(
|
||||
"Successfully created cache directory path `{}`.",
|
||||
self.cache_dir.display(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pages_dir(&self) -> PathBuf {
|
||||
self.cache_dir.join(TLDR_PAGES_DIR)
|
||||
}
|
||||
|
||||
/// Download the archive from the specified URL.
|
||||
fn download(archive_url: &str) -> Result<Vec<u8>> {
|
||||
let mut builder = Client::builder();
|
||||
if let Ok(ref host) = env::var("HTTP_PROXY") {
|
||||
if let Ok(proxy) = Proxy::http(host) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
}
|
||||
if let Ok(ref host) = env::var("HTTPS_PROXY") {
|
||||
if let Ok(proxy) = Proxy::https(host) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
}
|
||||
let client = builder
|
||||
.build()
|
||||
.context("Could not instantiate HTTP client")?;
|
||||
let mut resp = client
|
||||
.get(archive_url)
|
||||
.send()?
|
||||
.error_for_status()
|
||||
.with_context(|| format!("Could not download tldr pages from {archive_url}"))?;
|
||||
let mut buf: Vec<u8> = vec![];
|
||||
let bytes_downloaded = resp.copy_to(&mut buf)?;
|
||||
debug!("{} bytes downloaded", bytes_downloaded);
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Update the pages cache from the specified URL.
|
||||
pub fn update(&self, archive_url: &str) -> Result<()> {
|
||||
self.ensure_cache_dir_exists()?;
|
||||
|
||||
// First, download the compressed data
|
||||
let bytes: Vec<u8> = Self::download(archive_url)?;
|
||||
|
||||
// Decompress the response body into an `Archive`
|
||||
let mut archive = ZipArchive::new(Cursor::new(bytes))
|
||||
.context("Could not decompress downloaded ZIP archive")?;
|
||||
|
||||
// Clear cache directory
|
||||
// Note: This is not the best solution. Ideally we would download the
|
||||
// archive to a temporary directory and then swap the two directories.
|
||||
// But renaming a directory doesn't work across filesystems and Rust
|
||||
// does not yet offer a recursive directory copying function. So for
|
||||
// now, we'll use this approach.
|
||||
self.clear()
|
||||
.context("Could not clear the cache directory")?;
|
||||
|
||||
// Extract archive into pages dir
|
||||
archive
|
||||
.extract(self.pages_dir())
|
||||
.context("Could not unpack compressed data")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the duration since the cache directory was last modified.
|
||||
pub fn last_update(&self) -> Option<Duration> {
|
||||
if let Ok(metadata) = fs::metadata(self.pages_dir()) {
|
||||
if let Ok(mtime) = metadata.modified() {
|
||||
let now = SystemTime::now();
|
||||
return now.duration_since(mtime).ok();
|
||||
};
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
/// Return the freshness of the cache (fresh, stale or missing).
|
||||
pub fn freshness(&self) -> CacheFreshness {
|
||||
match self.last_update() {
|
||||
Some(ago) if ago > crate::config::MAX_CACHE_AGE => CacheFreshness::Stale(ago),
|
||||
Some(_) => CacheFreshness::Fresh,
|
||||
None => CacheFreshness::Missing,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the platform directory.
|
||||
fn get_platform_dir(platform: PlatformType) -> &'static str {
|
||||
match platform {
|
||||
PlatformType::Linux => "linux",
|
||||
PlatformType::OsX => "osx",
|
||||
PlatformType::SunOs => "sunos",
|
||||
|
|
@ -327,51 +221,232 @@ impl PlatformType {
|
|||
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()
|
||||
/// Check for pages for a given platform in one of the given languages.
|
||||
fn find_page_for_platform(
|
||||
page_name: &str,
|
||||
pages_dir: &Path,
|
||||
platform: &str,
|
||||
language_dirs: &[String],
|
||||
) -> Option<PathBuf> {
|
||||
language_dirs
|
||||
.iter()
|
||||
.map(|lang_dir| pages_dir.join(lang_dir).join(platform).join(page_name))
|
||||
.find(|path| path.exists() && path.is_file())
|
||||
}
|
||||
|
||||
/// Download the archive from the specified URL.
|
||||
fn download(client: &Agent, archive_url: &str) -> Result<Option<Vec<u8>>> {
|
||||
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<u8> = Vec::new();
|
||||
response.into_body().into_reader().read_to_end(&mut buf)?;
|
||||
debug!("{} bytes downloaded", buf.len());
|
||||
Ok(Some(buf))
|
||||
/// Look up custom patch (<name>.patch.md). If it exists, store it in a variable.
|
||||
fn find_patch(patch_name: &str, custom_pages_dir: Option<&Path>) -> Option<PathBuf> {
|
||||
custom_pages_dir
|
||||
.map(|custom_dir| custom_dir.join(patch_name))
|
||||
.filter(|path| path.exists() && path.is_file())
|
||||
}
|
||||
|
||||
/// Search for a page and return the path to it.
|
||||
pub fn find_page(
|
||||
&self,
|
||||
name: &str,
|
||||
languages: &[String],
|
||||
custom_pages_dir: Option<&Path>,
|
||||
platforms: &[PlatformType],
|
||||
) -> Option<PageLookupResult> {
|
||||
let page_filename = format!("{name}.md");
|
||||
let patch_filename = format!("{name}.patch.md");
|
||||
let custom_filename = format!("{name}.page.md");
|
||||
|
||||
// Determine directory paths
|
||||
let pages_dir = self.pages_dir();
|
||||
let lang_dirs: Vec<String> = languages
|
||||
.iter()
|
||||
.map(|lang| {
|
||||
if lang == "en" {
|
||||
String::from("pages")
|
||||
} else {
|
||||
format!("pages.{lang}")
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Look up custom page (<name>.page.md). If it exists, return it directly
|
||||
if let Some(config_dir) = custom_pages_dir {
|
||||
// TODO: Remove this check 1 year after version 1.7.0 was released
|
||||
self.check_for_old_custom_pages(config_dir);
|
||||
|
||||
let custom_page = config_dir.join(custom_filename);
|
||||
if custom_page.exists() && custom_page.is_file() {
|
||||
return Some(PageLookupResult::with_page(custom_page));
|
||||
}
|
||||
Ok(response) if response.status() == StatusCode::NOT_FOUND => Ok(None),
|
||||
_ => {
|
||||
bail!("Could not download tldr pages from {archive_url}: {response:?}")
|
||||
}
|
||||
|
||||
let patch_path = Self::find_patch(&patch_filename, custom_pages_dir);
|
||||
|
||||
// Try to find a platform specific path next, in the order supplied by the user, and append custom patch to it.
|
||||
for &platform in platforms {
|
||||
let platform_dir = Cache::get_platform_dir(platform);
|
||||
if let Some(page) =
|
||||
Self::find_page_for_platform(&page_filename, &pages_dir, platform_dir, &lang_dirs)
|
||||
{
|
||||
return Some(PageLookupResult::with_page(page).with_optional_patch(patch_path));
|
||||
}
|
||||
}
|
||||
|
||||
// Did not find platform specific results, fall back to "common"
|
||||
Self::find_page_for_platform(&page_filename, &pages_dir, "common", &lang_dirs)
|
||||
.map(|page| PageLookupResult::with_page(page).with_optional_patch(patch_path))
|
||||
}
|
||||
|
||||
/// Return the available pages.
|
||||
pub fn list_pages(
|
||||
&self,
|
||||
custom_pages_dir: Option<&Path>,
|
||||
platforms: &[PlatformType],
|
||||
) -> Vec<String> {
|
||||
// Determine platforms directory and platform
|
||||
let platforms_dir = self.pages_dir().join("pages");
|
||||
let platform_dirs: Vec<&'static str> = platforms
|
||||
.iter()
|
||||
.map(|&p| Self::get_platform_dir(p))
|
||||
.collect();
|
||||
|
||||
// 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 Some(file_name) = entry.file_name().to_str() else {
|
||||
return false;
|
||||
};
|
||||
if file_type.is_dir() {
|
||||
return file_name == "common" || platform_dirs.contains(&file_name);
|
||||
} else if file_type.is_file() {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
};
|
||||
|
||||
let to_stem = |entry: DirEntry| -> Option<String> {
|
||||
entry
|
||||
.path()
|
||||
.file_stem()
|
||||
.and_then(OsStr::to_str)
|
||||
.map(str::to_string)
|
||||
};
|
||||
|
||||
let to_stem_custom = |entry: DirEntry| -> Option<String> {
|
||||
entry
|
||||
.path()
|
||||
.file_name()
|
||||
.and_then(OsStr::to_str)
|
||||
.and_then(|s| s.strip_suffix(".page.md"))
|
||||
.map(str::to_string)
|
||||
};
|
||||
|
||||
// Recursively walk through common and (if applicable) platform specific directory
|
||||
let mut pages = WalkDir::new(platforms_dir)
|
||||
.min_depth(1) // Skip root directory
|
||||
.into_iter()
|
||||
.filter_entry(should_walk) // Filter out pages for other architectures
|
||||
.filter_map(Result::ok) // Convert results to options, filter out errors
|
||||
.filter_map(|e| {
|
||||
let extension = e.path().extension().unwrap_or_default();
|
||||
if e.file_type().is_file() && extension == "md" {
|
||||
to_stem(e)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
if let Some(custom_pages_dir) = custom_pages_dir {
|
||||
let is_page = |entry: &DirEntry| -> bool {
|
||||
entry.file_type().is_file()
|
||||
&& entry
|
||||
.path()
|
||||
.file_name()
|
||||
.and_then(OsStr::to_str)
|
||||
.map_or(false, |file_name| file_name.ends_with(".page.md"))
|
||||
};
|
||||
|
||||
let custom_pages = WalkDir::new(custom_pages_dir)
|
||||
.min_depth(1)
|
||||
.max_depth(1)
|
||||
.into_iter()
|
||||
.filter_entry(is_page)
|
||||
.filter_map(Result::ok)
|
||||
.filter_map(to_stem_custom);
|
||||
|
||||
pages.extend(custom_pages);
|
||||
}
|
||||
|
||||
pages.sort();
|
||||
pages.dedup();
|
||||
pages
|
||||
}
|
||||
|
||||
/// Delete the cache directory
|
||||
///
|
||||
/// Returns true if the cache was deleted and false if the cache dir did
|
||||
/// not exist.
|
||||
pub fn clear(&self) -> Result<bool> {
|
||||
if !self.cache_dir.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
ensure!(
|
||||
self.cache_dir.is_dir(),
|
||||
"Cache path ({}) is not a directory.",
|
||||
self.cache_dir.display(),
|
||||
);
|
||||
|
||||
// Delete old tldr-pages cache location as well if present
|
||||
// TODO: To be removed in the future
|
||||
for pages_dir_name in [TLDR_PAGES_DIR, TLDR_OLD_PAGES_DIR] {
|
||||
let pages_dir = self.cache_dir.join(pages_dir_name);
|
||||
|
||||
if pages_dir.exists() {
|
||||
fs::remove_dir_all(&pages_dir).with_context(|| {
|
||||
format!(
|
||||
"Could not remove the cache directory at {}",
|
||||
pages_dir.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Check for old custom pages (without .md suffix) and print a warning.
|
||||
fn check_for_old_custom_pages(&self, custom_pages_dir: &Path) {
|
||||
let old_custom_pages_exist = WalkDir::new(custom_pages_dir)
|
||||
.min_depth(1)
|
||||
.max_depth(1)
|
||||
.into_iter()
|
||||
.filter_entry(|entry| entry.file_type().is_file())
|
||||
.any(|entry| {
|
||||
if let Ok(entry) = entry {
|
||||
let extension = entry.path().extension();
|
||||
if let Some(extension) = extension {
|
||||
extension == "page" || extension == "patch"
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if old_custom_pages_exist {
|
||||
print_warning(
|
||||
self.enable_styles,
|
||||
&format!(
|
||||
"Custom pages using the old naming convention were found in {}.\n\
|
||||
Please rename them to follow the new convention:\n\
|
||||
- `<name>.page` → `<name>.page.md`\n\
|
||||
- `<name>.patch` → `<name>.patch.md`",
|
||||
custom_pages_dir.display()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
18
src/cli.rs
18
src/cli.rs
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{builder::ArgAction, ArgGroup, Parser};
|
||||
use clap::{arg, builder::ArgAction, command, ArgGroup, Parser};
|
||||
|
||||
use crate::types::{ColorOptions, PlatformType};
|
||||
|
||||
|
|
@ -18,9 +18,7 @@ use crate::types::{ColorOptions, PlatformType};
|
|||
{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.",
|
||||
after_help = "To view the user documentation, please visit https://tealdeer-rs.github.io/tealdeer/.",
|
||||
arg_required_else_help = true,
|
||||
help_expected = true,
|
||||
group = ArgGroup::new("command_or_file").args(&["command", "render"]),
|
||||
|
|
@ -34,14 +32,6 @@ pub(crate) struct Cli {
|
|||
#[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',
|
||||
|
|
@ -76,10 +66,6 @@ pub(crate) struct Cli {
|
|||
#[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<PathBuf>,
|
||||
|
||||
/// Use a pager to page output
|
||||
#[arg(long = "pager", requires = "command_or_file")]
|
||||
pub pager: bool,
|
||||
|
|
|
|||
796
src/config.rs
796
src/config.rs
|
|
@ -1,101 +1,26 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
env, fmt,
|
||||
fs::{self, File},
|
||||
io::{ErrorKind, Write},
|
||||
path::{Component, Path, PathBuf},
|
||||
sync::LazyLock,
|
||||
env, fmt, fs,
|
||||
io::{Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, ensure, Context, Result};
|
||||
use clap::ValueEnum;
|
||||
use log::info;
|
||||
use serde::Serialize as _;
|
||||
use anyhow::{bail, ensure, Context, Result};
|
||||
use app_dirs::{get_app_root, AppDataType};
|
||||
use log::debug;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use yansi::{Color, Style};
|
||||
|
||||
use crate::{
|
||||
extensions::Dedup as _,
|
||||
types::{PathSource, PlatformType},
|
||||
};
|
||||
use crate::types::PathSource;
|
||||
|
||||
pub const CONFIG_FILE_NAME: &str = "config.toml";
|
||||
pub const MAX_CACHE_AGE: Duration = Duration::from_secs(2_592_000); // 30 days
|
||||
const DEFAULT_UPDATE_INTERVAL_HOURS: u64 = MAX_CACHE_AGE.as_secs() / 3600; // 30 days
|
||||
const SUPPORTED_TLS_BACKENDS: &[RawTlsBackend] = &[
|
||||
#[cfg(feature = "native-tls")]
|
||||
RawTlsBackend::NativeTls,
|
||||
#[cfg(feature = "rustls-with-webpki-roots")]
|
||||
RawTlsBackend::RustlsWithWebpkiRoots,
|
||||
#[cfg(feature = "rustls-with-native-roots")]
|
||||
RawTlsBackend::RustlsWithNativeRoots,
|
||||
];
|
||||
|
||||
struct SystemDirectories {
|
||||
config: PathBuf,
|
||||
cache: PathBuf,
|
||||
data: PathBuf,
|
||||
}
|
||||
|
||||
impl SystemDirectories {
|
||||
fn discover() -> Result<Self> {
|
||||
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<SystemDirectories> = 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::<Vec<String>>()
|
||||
.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
|
||||
}
|
||||
|
|
@ -204,8 +129,8 @@ struct RawStyleConfig {
|
|||
pub example_variable: RawStyle,
|
||||
}
|
||||
|
||||
impl From<&RawStyleConfig> for StyleConfig {
|
||||
fn from(raw_style_config: &RawStyleConfig) -> Self {
|
||||
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(),
|
||||
|
|
@ -222,39 +147,13 @@ struct RawDisplayConfig {
|
|||
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 {
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -267,35 +166,12 @@ const fn default_auto_update_interval_hours() -> u64 {
|
|||
DEFAULT_UPDATE_INTERVAL_HOURS
|
||||
}
|
||||
|
||||
fn default_archive_source() -> String {
|
||||
"https://github.com/tldr-pages/tldr/releases/latest/download".to_owned()
|
||||
}
|
||||
|
||||
/// Controls when a warning about an outdated cache is printed.
|
||||
///
|
||||
/// Currently, the only nameable option is `"never"`. In the future, this may
|
||||
/// be extended to also accept a duration (e.g. `"60d"`), after which the
|
||||
/// warning should be shown.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum RawWarnCacheAge {
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
struct RawUpdatesConfig {
|
||||
#[serde(default)]
|
||||
pub auto_update: bool,
|
||||
#[serde(default = "default_auto_update_interval_hours")]
|
||||
pub auto_update_interval_hours: u64,
|
||||
#[serde(default = "default_archive_source")]
|
||||
pub archive_source: String,
|
||||
#[serde(default)]
|
||||
pub tls_backend: RawTlsBackend,
|
||||
#[serde(default)]
|
||||
pub download_languages: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub warn_cache_age: Option<RawWarnCacheAge>,
|
||||
}
|
||||
|
||||
impl Default for RawUpdatesConfig {
|
||||
|
|
@ -303,10 +179,17 @@ impl Default for RawUpdatesConfig {
|
|||
Self {
|
||||
auto_update: false,
|
||||
auto_update_interval_hours: DEFAULT_UPDATE_INTERVAL_HOURS,
|
||||
archive_source: default_archive_source(),
|
||||
tls_backend: RawTlsBackend::default(),
|
||||
download_languages: None,
|
||||
warn_cache_age: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RawUpdatesConfig> for UpdatesConfig {
|
||||
fn from(raw_updates_config: RawUpdatesConfig) -> Self {
|
||||
Self {
|
||||
auto_update: raw_updates_config.auto_update,
|
||||
auto_update_interval: Duration::from_secs(
|
||||
raw_updates_config.auto_update_interval_hours * 3600,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -319,63 +202,6 @@ struct RawDirectoriesConfig {
|
|||
pub custom_pages_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[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<Item = Self>) -> Vec<PlatformType> {
|
||||
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<Vec<String>>,
|
||||
pub platforms: Option<Vec<RawPlatformType>>,
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -383,7 +209,12 @@ struct RawConfig {
|
|||
display: RawDisplayConfig,
|
||||
updates: RawUpdatesConfig,
|
||||
directories: RawDirectoriesConfig,
|
||||
search: RawSearchConfig,
|
||||
}
|
||||
|
||||
impl RawConfig {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RawConfig {
|
||||
|
|
@ -393,7 +224,6 @@ impl Default for RawConfig {
|
|||
display: RawDisplayConfig::default(),
|
||||
updates: RawUpdatesConfig::default(),
|
||||
directories: RawDirectoriesConfig::default(),
|
||||
search: RawSearchConfig::default(),
|
||||
};
|
||||
|
||||
// Set default config
|
||||
|
|
@ -407,7 +237,7 @@ impl Default for RawConfig {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StyleConfig {
|
||||
pub description: Style,
|
||||
pub command_name: Style,
|
||||
|
|
@ -420,24 +250,12 @@ pub struct StyleConfig {
|
|||
pub struct DisplayConfig {
|
||||
pub compact: bool,
|
||||
pub use_pager: bool,
|
||||
pub show_title: bool,
|
||||
pub indent: Indent,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Indent {
|
||||
pub base: usize,
|
||||
pub command: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct UpdatesConfig<'a> {
|
||||
pub struct UpdatesConfig {
|
||||
pub auto_update: bool,
|
||||
pub auto_update_interval: Duration,
|
||||
pub archive_source: &'a str,
|
||||
pub tls_backend: TlsBackend,
|
||||
pub download_languages: Vec<Language<'a>>,
|
||||
pub warn_cache_age: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
@ -465,179 +283,22 @@ pub struct DirectoriesConfig {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SearchConfig<'a> {
|
||||
pub languages: Vec<Language<'a>>,
|
||||
pub platforms: Vec<PlatformType>,
|
||||
}
|
||||
|
||||
#[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<'a>> {
|
||||
// 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<Language<'a>> {
|
||||
static LANG: LazyLock<Option<String>> = LazyLock::new(|| std::env::var("LANG").ok());
|
||||
static LANGUAGE: LazyLock<Option<String>> = 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<RawTlsBackend> for TlsBackend {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(raw: RawTlsBackend) -> Result<Self, Self::Error> {
|
||||
match raw {
|
||||
#[cfg(feature = "native-tls")]
|
||||
RawTlsBackend::NativeTls => Ok(TlsBackend::NativeTls),
|
||||
#[cfg(feature = "rustls-with-webpki-roots")]
|
||||
RawTlsBackend::RustlsWithWebpkiRoots => Ok(TlsBackend::RustlsWithWebpkiRoots),
|
||||
#[cfg(feature = "rustls-with-native-roots")]
|
||||
RawTlsBackend::RustlsWithNativeRoots => Ok(TlsBackend::RustlsWithNativeRoots),
|
||||
// when compiling without all TLS backend features, we want to handle config error.
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(anyhow!(
|
||||
"Unsupported TLS backend: {}. This tealdeer build has support for the following options: {}",
|
||||
raw,
|
||||
supported_tls_backends_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TlsBackend {
|
||||
const fn as_raw(self) -> RawTlsBackend {
|
||||
match self {
|
||||
#[cfg(feature = "native-tls")]
|
||||
Self::NativeTls => RawTlsBackend::NativeTls,
|
||||
#[cfg(feature = "rustls-with-webpki-roots")]
|
||||
Self::RustlsWithWebpkiRoots => RawTlsBackend::RustlsWithWebpkiRoots,
|
||||
#[cfg(feature = "rustls-with-native-roots")]
|
||||
Self::RustlsWithNativeRoots => RawTlsBackend::RustlsWithNativeRoots,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TlsBackend {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.as_raw().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Config<'a> {
|
||||
pub struct Config {
|
||||
pub style: StyleConfig,
|
||||
pub display: DisplayConfig,
|
||||
pub updates: UpdatesConfig<'a>,
|
||||
pub updates: UpdatesConfig,
|
||||
pub directories: DirectoriesConfig,
|
||||
pub search: SearchConfig<'a>,
|
||||
pub file_path: PathWithSource,
|
||||
}
|
||||
|
||||
impl<'a> Config<'a> {
|
||||
impl Config {
|
||||
/// 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<Self> {
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
let relative_path_root = config_file_path
|
||||
.path()
|
||||
.parent()
|
||||
.context("Failed to get config directory")?;
|
||||
let home_path = env::home_dir();
|
||||
fn from_raw(raw_config: RawConfig) -> Result<Self> {
|
||||
let style = raw_config.style.into();
|
||||
let display = raw_config.display.into();
|
||||
let updates = raw_config.updates.into();
|
||||
|
||||
// Determine directories config. For this, we need to take some
|
||||
// additional factory into account, like env variables, or the
|
||||
|
|
@ -652,44 +313,39 @@ impl<'a> Config<'a> {
|
|||
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);
|
||||
|
||||
} else if let Some(config_value) = raw_config.directories.cache_dir {
|
||||
// If the user explicitly configured a cache directory, use that.
|
||||
PathWithSource {
|
||||
path: resolved_path,
|
||||
path: config_value,
|
||||
source: PathSource::ConfigFile,
|
||||
}
|
||||
} else {
|
||||
} else if let Ok(default_dir) = get_app_root(AppDataType::UserCache, &crate::APP_INFO) {
|
||||
// Otherwise, fall back to the default user cache directory.
|
||||
PathWithSource {
|
||||
path: SYSTEM_DIRECTORIES.cache.clone(),
|
||||
path: default_dir,
|
||||
source: PathSource::OsConvention,
|
||||
}
|
||||
} else {
|
||||
// If everything fails, give up
|
||||
bail!("Could not determine user cache directory");
|
||||
};
|
||||
let custom_pages_dir = raw_config
|
||||
.directories
|
||||
.custom_pages_dir
|
||||
.as_ref()
|
||||
.map(|path| -> Result<PathWithSource> {
|
||||
// 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,
|
||||
})
|
||||
.map(|path| PathWithSource {
|
||||
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,
|
||||
})
|
||||
get_app_root(AppDataType::UserData, &crate::APP_INFO)
|
||||
.map(|path| {
|
||||
// Note: The `join("")` call ensures that there's a trailing slash
|
||||
PathWithSource {
|
||||
path: path.join("pages").join(""),
|
||||
source: PathSource::OsConvention,
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
let directories = DirectoriesConfig {
|
||||
cache_dir,
|
||||
|
|
@ -701,91 +357,46 @@ impl<'a> Config<'a> {
|
|||
display,
|
||||
updates,
|
||||
directories,
|
||||
search,
|
||||
file_path: config_file_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands tilde (~) prefixed directories into its absolute version
|
||||
fn expand_home<'a>(input_path: &'a Path, home_path: Option<&Path>) -> Result<Cow<'a, Path>> {
|
||||
let mut components = input_path.components();
|
||||
pub fn load(enable_styles: bool) -> Result<Self> {
|
||||
debug!("Loading config");
|
||||
|
||||
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"))?;
|
||||
// Determine path
|
||||
let (config_file_path, _) = get_config_path().context("Could not determine config path")?;
|
||||
|
||||
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);
|
||||
// Load raw config
|
||||
let raw_config: RawConfig = if config_file_path.exists() && config_file_path.is_file() {
|
||||
let mut config_file = fs::File::open(&config_file_path).with_context(|| {
|
||||
format!("Failed to open config file path at {:?}", &config_file_path)
|
||||
})?;
|
||||
let mut contents = String::new();
|
||||
config_file.read_to_string(&mut contents).with_context(|| {
|
||||
format!("Failed to read from config file at {:?}", &config_file_path)
|
||||
})?;
|
||||
toml::from_str(&contents).with_context(|| {
|
||||
format!("Failed to parse TOML config file at {config_file_path:?}")
|
||||
})?
|
||||
} else {
|
||||
RawConfig::new()
|
||||
};
|
||||
|
||||
return Ok(Cow::Owned(expanded));
|
||||
} else if first_component.starts_with('~') {
|
||||
return Err(anyhow!("Tilde expansion with a login name not supported"));
|
||||
// Convert to config
|
||||
let mut config = Self::from_raw(raw_config).context("Could not process raw config")?;
|
||||
|
||||
// Potentially override styles
|
||||
if !enable_styles {
|
||||
config.style = StyleConfig {
|
||||
command_name: Style::default(),
|
||||
description: Style::default(),
|
||||
example_text: Style::default(),
|
||||
example_code: Style::default(),
|
||||
example_variable: Style::default(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Cow::Borrowed(input_path))
|
||||
}
|
||||
|
||||
/// The [`ConfigLoader`] is used to load a [`Config`] from a file.
|
||||
///
|
||||
/// Since the rich [`Config`] keeps references to [`RawConfig`], the raw config needs to be kept alive outside of the
|
||||
/// [`Config`]. The [`ConfigLoader`] thus offers the following flow:
|
||||
/// 1. Read a raw config using [`ConfigLoader::read`] or [`ConfigLoader::read_default_path`].
|
||||
/// 2. Validate the contents to a [`Config`] that borrows the [`ConfigLoader`].
|
||||
pub struct ConfigLoader {
|
||||
raw: RawConfig,
|
||||
path: PathWithSource,
|
||||
}
|
||||
|
||||
impl ConfigLoader {
|
||||
fn read_internal(path: PathWithSource, allow_not_found: bool) -> Result<Self> {
|
||||
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> {
|
||||
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<Self> {
|
||||
let path = get_default_config_path();
|
||||
Self::read_internal(path, true)
|
||||
}
|
||||
|
||||
/// Parse the read [`RawConfig`] into a [`Config`].
|
||||
pub fn load(&self) -> Result<Config<'_>> {
|
||||
Config::from_raw(&self.raw, self.path.clone())
|
||||
.context("Could not process raw config into rich config")
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -796,50 +407,46 @@ impl ConfigLoader {
|
|||
///
|
||||
/// Note that this function does not verify whether the directory at that
|
||||
/// location exists, or is a directory.
|
||||
pub fn get_config_dir() -> (PathBuf, PathSource) {
|
||||
pub fn get_config_dir() -> Result<(PathBuf, PathSource)> {
|
||||
// Allow overriding the config directory by setting the
|
||||
// $TEALDEER_CONFIG_DIR env variable.
|
||||
if let Ok(value) = env::var("TEALDEER_CONFIG_DIR") {
|
||||
return (PathBuf::from(value), PathSource::EnvVar);
|
||||
}
|
||||
return Ok((PathBuf::from(value), PathSource::EnvVar));
|
||||
};
|
||||
|
||||
(SYSTEM_DIRECTORIES.config.clone(), PathSource::OsConvention)
|
||||
// Otherwise, fall back to the user config directory.
|
||||
let dirs = get_app_root(AppDataType::UserConfig, &crate::APP_INFO)
|
||||
.context("Failed to determine the user config directory")?;
|
||||
Ok((dirs, 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_default_config_path() -> PathWithSource {
|
||||
let (mut path, source) = get_config_dir();
|
||||
path.push(CONFIG_FILE_NAME);
|
||||
PathWithSource { path, source }
|
||||
pub fn get_config_path() -> Result<(PathBuf, PathSource)> {
|
||||
let (config_dir, source) = get_config_dir()?;
|
||||
let config_file_path = config_dir.join(CONFIG_FILE_NAME);
|
||||
Ok((config_file_path, source))
|
||||
}
|
||||
|
||||
/// Create default config file.
|
||||
/// path: Can be specified to create the config in that path instead of
|
||||
/// the default path.
|
||||
pub fn make_default_config(path: Option<&Path>) -> Result<PathBuf> {
|
||||
let config_file_path = if let Some(p) = path {
|
||||
p.into()
|
||||
pub fn make_default_config() -> Result<PathBuf> {
|
||||
let (config_dir, _) = get_config_dir()?;
|
||||
|
||||
// Ensure that config directory exists
|
||||
if config_dir.exists() {
|
||||
ensure!(
|
||||
config_dir.is_dir(),
|
||||
"Config directory could not be created: {} already exists but is not a directory",
|
||||
config_dir.to_string_lossy(),
|
||||
);
|
||||
} else {
|
||||
let (config_dir, _) = get_config_dir();
|
||||
|
||||
// Ensure that config directory exists
|
||||
if config_dir.exists() {
|
||||
ensure!(
|
||||
config_dir.is_dir(),
|
||||
"Config directory could not be created: {} already exists but is not a directory",
|
||||
config_dir.to_string_lossy(),
|
||||
);
|
||||
} else {
|
||||
fs::create_dir_all(&config_dir).context("Could not create config directory")?;
|
||||
}
|
||||
|
||||
config_dir.join(CONFIG_FILE_NAME)
|
||||
};
|
||||
fs::create_dir_all(&config_dir).context("Could not create config directory")?;
|
||||
}
|
||||
|
||||
// Ensure that a config file doesn't get overwritten
|
||||
let config_file_path = config_dir.join(CONFIG_FILE_NAME);
|
||||
ensure!(
|
||||
!config_file_path.is_file(),
|
||||
"A configuration file already exists at {}, no action was taken.",
|
||||
|
|
@ -848,11 +455,11 @@ pub fn make_default_config(path: Option<&Path>) -> Result<PathBuf> {
|
|||
|
||||
// Create default config
|
||||
let serialized_config =
|
||||
toml::to_string(&RawConfig::default()).context("Failed to serialize default config")?;
|
||||
toml::to_string(&RawConfig::new()).context("Failed to serialize default config")?;
|
||||
|
||||
// Write default config
|
||||
let mut config_file =
|
||||
File::create(&config_file_path).context("Could not create config file")?;
|
||||
fs::File::create(&config_file_path).context("Could not create config file")?;
|
||||
let _wc = config_file
|
||||
.write(serialized_config.as_bytes())
|
||||
.context("Could not write to config file")?;
|
||||
|
|
@ -860,169 +467,10 @@ pub fn make_default_config(path: Option<&Path>) -> Result<PathBuf> {
|
|||
Ok(config_file_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn serialize_deserialize() {
|
||||
let raw_config = RawConfig::default();
|
||||
let serialized = toml::to_string(&raw_config).unwrap();
|
||||
let deserialized: RawConfig = toml::from_str(&serialized).unwrap();
|
||||
assert_eq!(raw_config, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_with_valid_home() {
|
||||
let home = Some(PathBuf::from("/foo/bar"));
|
||||
let path_to_expand = PathBuf::from("~/baz");
|
||||
|
||||
assert_eq!(
|
||||
*expand_home(&path_to_expand, home.as_deref()).unwrap(),
|
||||
PathBuf::from("/foo/bar/baz")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_with_absolute_path() {
|
||||
let home = Some(PathBuf::from("/foo/bar"));
|
||||
let dir_to_expand = PathBuf::from("/one/two");
|
||||
|
||||
assert_eq!(
|
||||
*expand_home(&dir_to_expand, home.as_deref()).unwrap(),
|
||||
dir_to_expand
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_tilde_username() {
|
||||
let home = Some(PathBuf::from("/foo/bar"));
|
||||
let dir_to_expand = PathBuf::from("~baz/foo");
|
||||
|
||||
assert!(expand_home(&dir_to_expand, home.as_deref()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_tilde_in_config_file() {
|
||||
let mut raw_config = RawConfig::default();
|
||||
raw_config.directories.cache_dir = Some("~/my/custom_cache".into());
|
||||
raw_config.directories.custom_pages_dir = Some("~/custom_pages".into());
|
||||
|
||||
let config = Config::from_raw(
|
||||
&raw_config,
|
||||
PathWithSource {
|
||||
path: PathBuf::from("/path/to/config/config.toml"),
|
||||
source: PathSource::OsConvention,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let home_dir = env::home_dir().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config.directories.cache_dir.path(),
|
||||
home_dir.join("my/custom_cache")
|
||||
);
|
||||
assert_eq!(
|
||||
config.directories.custom_pages_dir.unwrap().path(),
|
||||
home_dir.join("custom_pages")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_path_resolution() {
|
||||
let mut raw_config = RawConfig::default();
|
||||
raw_config.directories.cache_dir = Some("../cache".into());
|
||||
raw_config.directories.custom_pages_dir = Some("../custom_pages".into());
|
||||
|
||||
let config = Config::from_raw(
|
||||
&raw_config,
|
||||
PathWithSource {
|
||||
path: PathBuf::from("/path/to/config/config.toml"),
|
||||
source: PathSource::OsConvention,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config.directories.cache_dir.path(),
|
||||
Path::new("/path/to/config/../cache")
|
||||
);
|
||||
assert_eq!(
|
||||
config.directories.custom_pages_dir.unwrap().path(),
|
||||
Path::new("/path/to/config/../custom_pages")
|
||||
);
|
||||
}
|
||||
|
||||
mod language {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn missing_lang_env() {
|
||||
let lang_list = get_languages(None, Some("de:fr"));
|
||||
assert_eq!(lang_list, [Language("en")]);
|
||||
let lang_list = get_languages(None, None);
|
||||
assert_eq!(lang_list, [Language("en")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_language_env() {
|
||||
let lang_list = get_languages(Some("de"), None);
|
||||
assert_eq!(lang_list, [Language("de"), Language("en")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preference_order() {
|
||||
let lang_list = get_languages(Some("de"), Some("fr:cn"));
|
||||
assert_eq!(
|
||||
lang_list,
|
||||
[
|
||||
Language("fr"),
|
||||
Language("cn"),
|
||||
Language("de"),
|
||||
Language("en")
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn country_code_expansion() {
|
||||
let lang_list = get_languages(Some("pt_BR"), None);
|
||||
assert_eq!(
|
||||
lang_list,
|
||||
[Language("pt_BR"), Language("pt"), Language("en")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_encoding() {
|
||||
let lang_list = get_languages(Some("de_DE.UTF-8"), None);
|
||||
assert_eq!(
|
||||
lang_list,
|
||||
[Language("de_DE"), Language("de"), Language("en")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignore_posix_and_c() {
|
||||
let lang_list = get_languages(Some("POSIX"), None);
|
||||
assert_eq!(lang_list, [Language("en")]);
|
||||
let lang_list = get_languages(Some("C"), None);
|
||||
assert_eq!(lang_list, [Language("en")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicates() {
|
||||
let lang_list = get_languages(Some("de"), Some("fr:de:cn:de"));
|
||||
assert_eq!(
|
||||
lang_list,
|
||||
[
|
||||
Language("fr"),
|
||||
Language("de"),
|
||||
Language("cn"),
|
||||
Language("en")
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_serialize_deserialize() {
|
||||
let raw_config = RawConfig::new();
|
||||
let serialized = toml::to_string(&raw_config).unwrap();
|
||||
let deserialized: RawConfig = toml::from_str(&serialized).unwrap();
|
||||
assert_eq!(raw_config, deserialized);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
use std::mem;
|
||||
|
||||
/// An extension trait to clear duplicates from a collection.
|
||||
pub(crate) trait Dedup<T: PartialEq> {
|
||||
pub(crate) trait Dedup<T: PartialEq + Clone> {
|
||||
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<T: PartialEq> Dedup<T> for Vec<T> {
|
||||
impl<T: PartialEq + Clone> Dedup<T> for Vec<T> {
|
||||
fn clear_duplicates(&mut self) {
|
||||
let orig = mem::replace(self, Vec::with_capacity(self.len()));
|
||||
for item in orig {
|
||||
|
|
|
|||
415
src/formatter.rs
415
src/formatter.rs
|
|
@ -2,61 +2,25 @@
|
|||
|
||||
use log::debug;
|
||||
|
||||
use crate::{config::Indent, extensions::FindFrom, types::LineType};
|
||||
use crate::{extensions::FindFrom, types::LineType};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// Represents a snippet from a page of a specific highlighting class.
|
||||
pub enum PageSnippet<T> {
|
||||
CommandName(T),
|
||||
Variable(T),
|
||||
NormalCode(T),
|
||||
Description(T),
|
||||
Text(T),
|
||||
Title(T),
|
||||
pub enum PageSnippet<'a> {
|
||||
CommandName(&'a str),
|
||||
Variable(&'a str),
|
||||
NormalCode(&'a str),
|
||||
Description(&'a str),
|
||||
Text(&'a str),
|
||||
Linebreak,
|
||||
}
|
||||
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
impl<T> PageSnippet<T> {
|
||||
pub fn map<F, U>(self, f: F) -> PageSnippet<U>
|
||||
where
|
||||
F: FnOnce(T) -> U,
|
||||
{
|
||||
match self {
|
||||
PageSnippet::CommandName(s) => PageSnippet::CommandName(f(s)),
|
||||
PageSnippet::Variable(s) => PageSnippet::Variable(f(s)),
|
||||
PageSnippet::NormalCode(s) => PageSnippet::NormalCode(f(s)),
|
||||
PageSnippet::Description(s) => PageSnippet::Description(f(s)),
|
||||
PageSnippet::Text(s) => PageSnippet::Text(f(s)),
|
||||
PageSnippet::Title(s) => PageSnippet::Title(f(s)),
|
||||
PageSnippet::Linebreak => PageSnippet::Linebreak,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq<U>, U> PartialEq<PageSnippet<U>> for PageSnippet<T> {
|
||||
fn eq(&self, other: &PageSnippet<U>) -> bool {
|
||||
match (self, other) {
|
||||
(PageSnippet::CommandName(s), PageSnippet::CommandName(t))
|
||||
| (PageSnippet::Variable(s), PageSnippet::Variable(t))
|
||||
| (PageSnippet::NormalCode(s), PageSnippet::NormalCode(t))
|
||||
| (PageSnippet::Description(s), PageSnippet::Description(t))
|
||||
| (PageSnippet::Text(s), PageSnippet::Text(t))
|
||||
| (PageSnippet::Title(s), PageSnippet::Title(t)) => s == t,
|
||||
(PageSnippet::Linebreak, PageSnippet::Linebreak) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PageSnippet<&str> {
|
||||
impl<'a> PageSnippet<'a> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
use PageSnippet::*;
|
||||
|
||||
match self {
|
||||
CommandName(s) | Variable(s) | NormalCode(s) | Description(s) | Text(s) | Title(s) => {
|
||||
s.is_empty()
|
||||
}
|
||||
CommandName(s) | Variable(s) | NormalCode(s) | Description(s) | Text(s) => s.is_empty(),
|
||||
Linebreak => false,
|
||||
}
|
||||
}
|
||||
|
|
@ -67,15 +31,11 @@ pub fn highlight_lines<L, F, E>(
|
|||
lines: L,
|
||||
process_snippet: &mut F,
|
||||
keep_empty_lines: bool,
|
||||
show_title: bool,
|
||||
indent: Indent,
|
||||
) -> Result<(), E>
|
||||
where
|
||||
L: Iterator<Item = LineType>,
|
||||
F: for<'snip> FnMut(PageSnippet<&'snip str>) -> Result<(), E>,
|
||||
F: for<'snip> FnMut(PageSnippet<'snip>) -> Result<(), E>,
|
||||
{
|
||||
let base_indent = " ".repeat(indent.base);
|
||||
let command_indent = " ".repeat(indent.command);
|
||||
let mut command = String::new();
|
||||
for line in lines {
|
||||
match line {
|
||||
|
|
@ -85,122 +45,51 @@ where
|
|||
}
|
||||
}
|
||||
LineType::Title(title) => {
|
||||
if show_title {
|
||||
process_snippet(PageSnippet::Linebreak)?;
|
||||
process_snippet(PageSnippet::Title(&base_indent))?;
|
||||
process_snippet(PageSnippet::Title(&title))?;
|
||||
process_snippet(PageSnippet::Linebreak)?;
|
||||
} else {
|
||||
debug!("Ignoring title");
|
||||
}
|
||||
debug!("Ignoring title");
|
||||
|
||||
// This is safe as long as the parsed title is only the command,
|
||||
// and the iterator yields values in order of appearance.
|
||||
command = title;
|
||||
debug!("Detected command name: {command}");
|
||||
}
|
||||
LineType::Description(text) => {
|
||||
process_snippet(PageSnippet::Description(&base_indent))?;
|
||||
process_snippet(PageSnippet::Description(&text))?;
|
||||
process_snippet(PageSnippet::Linebreak)?;
|
||||
}
|
||||
LineType::ExampleText(text) => {
|
||||
process_snippet(PageSnippet::Text(&base_indent))?;
|
||||
process_snippet(PageSnippet::Text(&text))?;
|
||||
process_snippet(PageSnippet::Linebreak)?;
|
||||
debug!("Detected command name: {}", &command);
|
||||
}
|
||||
LineType::Description(text) => process_snippet(PageSnippet::Description(&text))?,
|
||||
LineType::ExampleText(text) => process_snippet(PageSnippet::Text(&text))?,
|
||||
LineType::ExampleCode(text) => {
|
||||
process_snippet(PageSnippet::NormalCode(&command_indent))?;
|
||||
process_snippet(PageSnippet::NormalCode(" "))?;
|
||||
highlight_code(&command, &text, process_snippet)?;
|
||||
process_snippet(PageSnippet::Linebreak)?;
|
||||
}
|
||||
|
||||
LineType::Other(text) => debug!("Unknown line type: {text:?}"),
|
||||
LineType::Other(text) => debug!("Unknown line type: {:?}", text),
|
||||
}
|
||||
}
|
||||
process_snippet(PageSnippet::Linebreak)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Highlight code examples.
|
||||
/// - parse placeholders (`{{ curly braces }}`)
|
||||
/// - replace escaped placeholder markers (`\{\{` and `\}\}`)
|
||||
fn highlight_code<E>(
|
||||
command: &str,
|
||||
mut text: &str,
|
||||
process_snippet: &mut impl FnMut(PageSnippet<&str>) -> Result<(), E>,
|
||||
/// Highlight code examples including user variables in {{ curly braces }}.
|
||||
fn highlight_code<'a, E>(
|
||||
command: &'a str,
|
||||
text: &'a str,
|
||||
process_snippet: &mut impl FnMut(PageSnippet<'a>) -> Result<(), E>,
|
||||
) -> Result<(), E> {
|
||||
// We replace escaped placeholder markers at the end so that our replacing does not interfere
|
||||
// with finding the actual markers.
|
||||
// NOTE: This is not optimal, as it allocates one String for each `replace`
|
||||
let replace_escaped = |s: &str| s.replace(r"\{\{", "{{").replace(r"\}\}", "}}");
|
||||
|
||||
loop {
|
||||
// Find placeholder markers and split into code and placeholder accordingly
|
||||
|
||||
let Some(start_marker) = find_marker(text, "{{", r"\{\{") else {
|
||||
break;
|
||||
};
|
||||
let Some(mut end_marker) = find_marker(&text[start_marker + 2..], "}}", r"\}\}") else {
|
||||
break;
|
||||
};
|
||||
end_marker += start_marker + 2;
|
||||
|
||||
// Greedily extend matched range
|
||||
while end_marker + 2 < text.len() && text.as_bytes()[end_marker + 2] == b'}' {
|
||||
end_marker += 1;
|
||||
}
|
||||
|
||||
let placeholder_content = &text[start_marker + 2..end_marker];
|
||||
|
||||
if start_marker > 0 {
|
||||
highlight_code_segment(
|
||||
command,
|
||||
&replace_escaped(&text[..start_marker]),
|
||||
process_snippet,
|
||||
)?;
|
||||
}
|
||||
process_snippet(PageSnippet::Variable(&replace_escaped(placeholder_content)))?;
|
||||
|
||||
text = &text[end_marker + 2..];
|
||||
let variable_splits = text
|
||||
.split("}}")
|
||||
.map(|s| s.split_once("{{").unwrap_or((s, "")));
|
||||
for (code_segment, variable) in variable_splits {
|
||||
highlight_code_segment(command, code_segment, process_snippet)?;
|
||||
process_snippet(PageSnippet::Variable(variable))?;
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
highlight_code_segment(command, &replace_escaped(text), process_snippet)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find a "{{" (or "}}") substring that does not overlap with a preceding "\{\{" (or "\}\}").
|
||||
fn find_marker(s: &str, marker: &str, forbidden_prefix: &str) -> Option<usize> {
|
||||
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>,
|
||||
process_snippet: &mut impl FnMut(PageSnippet<'a>) -> Result<(), E>,
|
||||
) -> Result<(), E> {
|
||||
if !command_name.is_empty() {
|
||||
let mut search_start = 0;
|
||||
|
|
@ -230,17 +119,20 @@ fn is_freestanding_substring(surrounding: &str, substring: (usize, usize)) -> bo
|
|||
let char_before_is_okay = surrounding[..start]
|
||||
.chars()
|
||||
.last()
|
||||
.is_none_or(char::is_whitespace);
|
||||
.filter(|prev_char| !prev_char.is_whitespace())
|
||||
.is_none();
|
||||
let char_after_is_okay = surrounding[end..]
|
||||
.chars()
|
||||
.next()
|
||||
.is_none_or(char::is_whitespace);
|
||||
.filter(|next_char| !next_char.is_whitespace())
|
||||
.is_none();
|
||||
char_before_is_okay && char_after_is_okay
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use PageSnippet::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_freestanding_substring() {
|
||||
|
|
@ -267,193 +159,80 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
fn run<'a>(cmd: &'a str, segment: &'a str) -> Vec<PageSnippet<String>> {
|
||||
fn run<'a>(cmd: &'a str, segment: &'a str) -> Vec<PageSnippet<'a>> {
|
||||
let mut yielded = Vec::new();
|
||||
let mut process_snippet = |snip: PageSnippet<&str>| {
|
||||
let mut process_snippet = |snip: PageSnippet<'a>| {
|
||||
if !snip.is_empty() {
|
||||
yielded.push(snip.map(str::to_string));
|
||||
yielded.push(snip);
|
||||
}
|
||||
Ok::<(), ()>(())
|
||||
};
|
||||
|
||||
highlight_code(cmd, segment, &mut process_snippet).expect("highlight code segment failed");
|
||||
highlight_code_segment(cmd, segment, &mut process_snippet)
|
||||
.expect("highlight code segment failed");
|
||||
yielded
|
||||
}
|
||||
|
||||
mod highlight_code_segment {
|
||||
use super::*;
|
||||
use PageSnippet::*;
|
||||
|
||||
#[test]
|
||||
fn test_highlight_code_segment() {
|
||||
assert!(run("make", "").is_empty());
|
||||
assert_eq!(
|
||||
&run("make", "make all CC=clang -q"),
|
||||
&[CommandName("make"), NormalCode(" all CC=clang -q")]
|
||||
);
|
||||
assert_eq!(
|
||||
&run("make", " make money --always-make"),
|
||||
&[
|
||||
NormalCode(" "),
|
||||
CommandName("make"),
|
||||
NormalCode(" money --always-make")
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
&run("git commit", "git commit -m 'git commit'"),
|
||||
&[CommandName("git commit"), NormalCode(" -m 'git commit'"),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_i18n() {
|
||||
assert_eq!(
|
||||
&run("mäke", "mäke höhlenrätselbücher"),
|
||||
&[CommandName("mäke"), NormalCode(" höhlenrätselbücher")]
|
||||
);
|
||||
assert_eq!(
|
||||
&run(
|
||||
"Müll",
|
||||
"1000 Gründe warum Müll heute größer ist als Müll früher, ärgerlich"
|
||||
),
|
||||
&[
|
||||
NormalCode("1000 Gründe warum "),
|
||||
CommandName("Müll"),
|
||||
NormalCode(" heute größer ist als "),
|
||||
CommandName("Müll"),
|
||||
NormalCode(" früher, ärgerlich")
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
&run(
|
||||
"übergang",
|
||||
"die Zustandsübergangsfunktion übergang Änderungen",
|
||||
),
|
||||
&[
|
||||
NormalCode("die Zustandsübergangsfunktion "),
|
||||
CommandName("übergang"),
|
||||
NormalCode(" Änderungen")
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_command() {
|
||||
let segment = "some code";
|
||||
let snippets = [NormalCode(segment)];
|
||||
|
||||
assert_eq!(run("", segment), snippets);
|
||||
assert_eq!(run(" ", segment), snippets);
|
||||
assert_eq!(run(" \t ", segment), snippets);
|
||||
}
|
||||
#[test]
|
||||
fn test_highlight_code_segment() {
|
||||
assert!(run("make", "").is_empty());
|
||||
assert_eq!(
|
||||
&run("make", "make all CC=clang -q"),
|
||||
&[CommandName("make"), NormalCode(" all CC=clang -q")]
|
||||
);
|
||||
assert_eq!(
|
||||
&run("make", " make money --always-make"),
|
||||
&[
|
||||
NormalCode(" "),
|
||||
CommandName("make"),
|
||||
NormalCode(" money --always-make")
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
&run("git commit", "git commit -m 'git commit'"),
|
||||
&[CommandName("git commit"), NormalCode(" -m 'git commit'"),]
|
||||
);
|
||||
}
|
||||
|
||||
mod placeholders {
|
||||
use super::*;
|
||||
use PageSnippet::*;
|
||||
#[test]
|
||||
fn test_i18n() {
|
||||
assert_eq!(
|
||||
&run("mäke", "mäke höhlenrätselbücher"),
|
||||
&[CommandName("mäke"), NormalCode(" höhlenrätselbücher")]
|
||||
);
|
||||
assert_eq!(
|
||||
&run(
|
||||
"Müll",
|
||||
"1000 Gründe warum Müll heute größer ist als Müll früher, ärgerlich"
|
||||
),
|
||||
&[
|
||||
NormalCode("1000 Gründe warum "),
|
||||
CommandName("Müll"),
|
||||
NormalCode(" heute größer ist als "),
|
||||
CommandName("Müll"),
|
||||
NormalCode(" früher, ärgerlich")
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
&run(
|
||||
"übergang",
|
||||
"die Zustandsübergangsfunktion übergang Änderungen",
|
||||
),
|
||||
&[
|
||||
NormalCode("die Zustandsübergangsfunktion "),
|
||||
CommandName("übergang"),
|
||||
NormalCode(" Änderungen")
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_vs_escaped() {
|
||||
assert_eq!(
|
||||
run("ping", "ping {{example.com}}"),
|
||||
[
|
||||
CommandName("ping"),
|
||||
NormalCode(" "),
|
||||
Variable("example.com"),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
run(
|
||||
"docker inspect",
|
||||
r"docker inspect --format '\{\{range.NetworkSettings.Networks\}\}\{\{.IPAddress\}\}\{\{end\}\}' {{container}}"
|
||||
),
|
||||
[
|
||||
CommandName("docker inspect"),
|
||||
NormalCode(
|
||||
" --format '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "
|
||||
),
|
||||
Variable("container"),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
run("mount", r"mount \\{{computer_name}}\{{share_name}} Z:"),
|
||||
[
|
||||
CommandName("mount"),
|
||||
NormalCode(r" \\"),
|
||||
Variable("computer_name"),
|
||||
NormalCode(r"\"),
|
||||
Variable("share_name"),
|
||||
NormalCode(" Z:"),
|
||||
],
|
||||
);
|
||||
#[test]
|
||||
fn test_empty_command() {
|
||||
let segment = "some code";
|
||||
let snippets = [NormalCode(segment)];
|
||||
|
||||
assert_eq!(run("", r"\{"), [NormalCode(r"\{")]);
|
||||
assert_eq!(run("", r"\{{a"), [NormalCode(r"\{{a")]);
|
||||
assert_eq!(run("", r"\{{a}}"), [NormalCode(r"\"), Variable("a")]);
|
||||
|
||||
// Placeholder has begin marker, but no end marker
|
||||
assert_eq!(run("", r"{{\}\}}"), [NormalCode("{{}}}")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outer_precedence() {
|
||||
assert_eq!(
|
||||
run("git stash", "git stash show --patch {{stash@{0}}}"),
|
||||
[
|
||||
CommandName("git stash"),
|
||||
NormalCode(" show --patch "),
|
||||
Variable("stash@{0}"),
|
||||
],
|
||||
);
|
||||
|
||||
// The following is not listed in the specification, but this is the highlighting I would expect.
|
||||
assert_eq!(
|
||||
run("rg", "rg {{}}}"),
|
||||
[CommandName("rg"), NormalCode(" "), Variable("}")]
|
||||
);
|
||||
|
||||
// And these are just to document the current behavior
|
||||
assert_eq!(run("", "{{{}}}"), [Variable("{}")]);
|
||||
assert_eq!(run("", "{{{{}}}"), [Variable("{{}")]);
|
||||
assert_eq!(run("", "{{{}}}}"), [Variable("{}}")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaped_inside_placeholder() {
|
||||
assert_eq!(
|
||||
run(
|
||||
"playerctl",
|
||||
r#"playerctl metadata {{[-f|--format]}} "{{Now playing: \{\{artist\}\} - \{\{album\}\} - \{\{title\}\}}}""#
|
||||
),
|
||||
[
|
||||
CommandName("playerctl"),
|
||||
NormalCode(" metadata "),
|
||||
Variable("[-f|--format]"),
|
||||
NormalCode(" \""),
|
||||
Variable("Now playing: {{artist}} - {{album}} - {{title}}"),
|
||||
NormalCode("\""),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_inside_escaped() {
|
||||
assert_eq!(
|
||||
run("test", r"test \{\{{{var}} normal\}\}"),
|
||||
[
|
||||
CommandName("test"),
|
||||
NormalCode(" {{"),
|
||||
Variable("var"),
|
||||
NormalCode(" normal}}"),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Regression test for <https://github.com/tealdeer-rs/tealdeer/issues/473>
|
||||
fn prefix_check_character_boundary() {
|
||||
assert_eq!("Ä".len(), 2);
|
||||
assert_eq!(run("", r"Äxx{{x}}"), [NormalCode("Äxx"), Variable("x")],);
|
||||
}
|
||||
assert_eq!(run("", segment), snippets);
|
||||
assert_eq!(run(" ", segment), snippets);
|
||||
assert_eq!(run(" \t ", segment), snippets);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ impl<R: BufRead> Iterator for LineIterator<R> {
|
|||
match bytes_read {
|
||||
Ok(0) => None,
|
||||
Err(e) => {
|
||||
warn!("Could not read line from reader: {e:?}");
|
||||
warn!("Could not read line from reader: {:?}", e);
|
||||
None
|
||||
}
|
||||
Ok(_) => {
|
||||
|
|
@ -68,7 +68,7 @@ impl<R: BufRead> Iterator for LineIterator<R> {
|
|||
.find(|b| matches!(b, Ok(b'\n') | Err(_)))
|
||||
.transpose()
|
||||
{
|
||||
warn!("Could not read line from reader: {e:?}");
|
||||
warn!("Could not read line from reader: {:?}", e);
|
||||
return None;
|
||||
}
|
||||
self.first_line = false;
|
||||
|
|
|
|||
578
src/main.rs
578
src/main.rs
|
|
@ -15,32 +15,29 @@
|
|||
#![allow(clippy::similar_names)]
|
||||
#![allow(clippy::struct_excessive_bools)]
|
||||
#![allow(clippy::too_many_lines)]
|
||||
#![allow(clippy::unnecessary_debug_formatting)]
|
||||
#![allow(clippy::while_let_loop)]
|
||||
|
||||
#[cfg(not(any(
|
||||
feature = "native-tls",
|
||||
feature = "rustls-with-webpki-roots",
|
||||
feature = "rustls-with-native-roots",
|
||||
)))]
|
||||
#[cfg(any(
|
||||
all(feature = "native-roots", feature = "webpki-roots"),
|
||||
all(feature = "native-roots", feature = "native-tls"),
|
||||
all(feature = "webpki-roots", feature = "native-tls"),
|
||||
not(any(
|
||||
feature = "native-roots",
|
||||
feature = "webpki-roots",
|
||||
feature = "native-tls"
|
||||
)),
|
||||
))]
|
||||
compile_error!(
|
||||
"at least one of the features \"native-tls\", \"rustls-with-webpki-roots\" or \"rustls-with-native-roots\" must be enabled"
|
||||
"exactly one of the features \"native-roots\", \"webpki-roots\" or \"native-tls\" must be enabled"
|
||||
);
|
||||
|
||||
use std::{
|
||||
env,
|
||||
fs::create_dir_all,
|
||||
io::{self, IsTerminal},
|
||||
path::Path,
|
||||
process::{Command, ExitCode},
|
||||
process,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use cache::{CacheConfig, TLDR_OLD_PAGES_DIR};
|
||||
use app_dirs::AppInfo;
|
||||
use clap::Parser;
|
||||
use config::{ConfigLoader, Language, StyleConfig, TlsBackend};
|
||||
use log::debug;
|
||||
use types::PlatformType;
|
||||
|
||||
mod cache;
|
||||
mod cli;
|
||||
|
|
@ -53,67 +50,118 @@ mod types;
|
|||
mod utils;
|
||||
|
||||
use crate::{
|
||||
cache::{Cache, PageLookupResult, TLDR_PAGES_DIR},
|
||||
cache::{Cache, CacheFreshness, PageLookupResult, TLDR_PAGES_DIR},
|
||||
cli::Cli,
|
||||
config::{
|
||||
get_config_dir, make_default_config, supported_tls_backends_string, Config, PathWithSource,
|
||||
},
|
||||
config::{get_config_dir, get_config_path, make_default_config, Config, PathWithSource},
|
||||
extensions::Dedup,
|
||||
output::print_page,
|
||||
types::ColorOptions,
|
||||
types::{ColorOptions, PlatformType},
|
||||
utils::{print_error, print_warning},
|
||||
};
|
||||
|
||||
const NAME: &str = "tealdeer";
|
||||
static TEALDEER_PAGE: &str =
|
||||
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/pages/tealdeer.md"));
|
||||
const APP_INFO: AppInfo = AppInfo {
|
||||
name: NAME,
|
||||
author: NAME,
|
||||
};
|
||||
const ARCHIVE_URL: &str = "https://tldr.sh/assets/tldr.zip";
|
||||
|
||||
/// The cache should be updated if it was explicitly requested,
|
||||
/// or if an automatic update is due and allowed.
|
||||
fn should_update_cache(cache: &Cache, args: &Cli, config: &Config) -> bool {
|
||||
args.update
|
||||
|| (!args.no_auto_update
|
||||
&& config.updates.auto_update
|
||||
&& cache
|
||||
.last_update()
|
||||
.map_or(true, |ago| ago >= config.updates.auto_update_interval))
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum CheckCacheResult {
|
||||
CacheFound,
|
||||
CacheMissing,
|
||||
}
|
||||
|
||||
/// Check the cache for freshness. If it's stale or missing, show a warning.
|
||||
fn check_cache(cache: &Cache, args: &Cli, enable_styles: bool) -> CheckCacheResult {
|
||||
match cache.freshness() {
|
||||
CacheFreshness::Fresh => CheckCacheResult::CacheFound,
|
||||
CacheFreshness::Stale(_) if args.quiet => CheckCacheResult::CacheFound,
|
||||
CacheFreshness::Stale(age) => {
|
||||
print_warning(
|
||||
enable_styles,
|
||||
&format!(
|
||||
"The cache hasn't been updated for {} days.\n\
|
||||
You should probably run `tldr --update` soon.",
|
||||
age.as_secs() / 24 / 3600
|
||||
),
|
||||
);
|
||||
CheckCacheResult::CacheFound
|
||||
}
|
||||
CacheFreshness::Missing => {
|
||||
print_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");
|
||||
CheckCacheResult::CacheMissing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the cache
|
||||
fn clear_cache(cache: Cache, quietly: bool) -> Result<()> {
|
||||
let cache_dir = cache.config().pages_directory.display();
|
||||
cache.clear().context("Could not clear cache")?;
|
||||
fn clear_cache(cache: &Cache, quietly: bool, enable_styles: bool) {
|
||||
let cache_dir_found = cache.clear().unwrap_or_else(|e| {
|
||||
print_error(enable_styles, &e.context("Could not clear cache"));
|
||||
process::exit(1);
|
||||
});
|
||||
if !quietly {
|
||||
eprintln!("Successfully cleared cache at `{cache_dir}`.");
|
||||
let cache_dir = cache.cache_dir().display();
|
||||
if cache_dir_found {
|
||||
eprintln!("Successfully cleared cache at `{cache_dir}`.");
|
||||
} else {
|
||||
eprintln!("Cache directory not found at `{cache_dir}`, nothing to do.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the cache
|
||||
fn update_cache(
|
||||
cache: &mut Cache,
|
||||
archive_source: &str,
|
||||
tls_backend: TlsBackend,
|
||||
quietly: bool,
|
||||
) -> Result<()> {
|
||||
let downloaded_languages = cache
|
||||
.update(archive_source, tls_backend)
|
||||
.context("Could not update cache")?;
|
||||
fn update_cache(cache: &Cache, quietly: bool, enable_styles: bool) {
|
||||
cache.update(ARCHIVE_URL).unwrap_or_else(|e| {
|
||||
print_error(enable_styles, &e.context("Could not update cache"));
|
||||
process::exit(1);
|
||||
});
|
||||
if !quietly {
|
||||
eprintln!("Successfully updated cache.");
|
||||
eprint!("Pages for the following languages were downloaded: ");
|
||||
let language_strings: Vec<_> = downloaded_languages
|
||||
.into_iter()
|
||||
.map(|lang| lang.0)
|
||||
.collect();
|
||||
if language_strings.is_empty() {
|
||||
eprintln!("(none)");
|
||||
} else {
|
||||
eprintln!("{}", language_strings.join(", "));
|
||||
}
|
||||
}
|
||||
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 config_dir = get_config_dir().map_or_else(
|
||||
|e| format!("[Error: {e}]"),
|
||||
|(mut path, source)| {
|
||||
path.push(""); // Trailing path separator
|
||||
match path.to_str() {
|
||||
Some(path) => format!("{path} ({source})"),
|
||||
None => "[Invalid]".to_string(),
|
||||
}
|
||||
},
|
||||
);
|
||||
let config_path = get_config_path().map_or_else(
|
||||
|e| format!("[Error: {e}]"),
|
||||
|(path, _)| path.display().to_string(),
|
||||
);
|
||||
let cache_dir = config.directories.cache_dir.to_string();
|
||||
let pages_dir = {
|
||||
let mut path = config.directories.cache_dir.path.clone();
|
||||
|
|
@ -132,13 +180,21 @@ fn show_paths(config: &Config) {
|
|||
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(())
|
||||
/// Create seed config file and exit
|
||||
fn create_config_and_exit(enable_styles: bool) {
|
||||
match make_default_config() {
|
||||
Ok(config_file_path) => {
|
||||
eprintln!(
|
||||
"Successfully created seed config file here: {}",
|
||||
config_file_path.to_str().unwrap()
|
||||
);
|
||||
process::exit(0);
|
||||
}
|
||||
Err(e) => {
|
||||
print_error(enable_styles, &e.context("Could not create seed config"));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "logging")]
|
||||
|
|
@ -149,28 +205,43 @@ fn init_log() {
|
|||
#[cfg(not(feature = "logging"))]
|
||||
fn init_log() {}
|
||||
|
||||
fn spawn_editor(custom_pages_dir: &Path, file_name: &str) -> Result<()> {
|
||||
create_dir_all(custom_pages_dir).context("Failed to create custom pages directory")?;
|
||||
fn get_languages(env_lang: Option<&str>, env_language: Option<&str>) -> Vec<String> {
|
||||
// Language list according to
|
||||
// https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#language
|
||||
|
||||
let custom_page_path = custom_pages_dir.join(file_name);
|
||||
let Some(custom_page_path) = custom_page_path.to_str() else {
|
||||
return Err(anyhow!("`custom_page_path.to_str()` failed"));
|
||||
};
|
||||
let Ok(editor) = env::var("EDITOR") else {
|
||||
return Err(anyhow!(
|
||||
"To edit a custom page, please set the `EDITOR` environment variable."
|
||||
));
|
||||
};
|
||||
println!("Editing {custom_page_path:?}");
|
||||
|
||||
let status = Command::new(&editor).arg(custom_page_path).status()?;
|
||||
if !status.success() {
|
||||
return Err(anyhow!("{editor} exit with code {:?}", status.code()));
|
||||
if env_lang.is_none() {
|
||||
return vec!["en".to_string()];
|
||||
}
|
||||
Ok(())
|
||||
let env_lang = env_lang.unwrap();
|
||||
|
||||
// Create an iterator that contains $LANGUAGE (':' separated list) followed by $LANG (single language)
|
||||
let locales = env_language.unwrap_or("").split(':').chain([env_lang]);
|
||||
|
||||
let mut lang_list = Vec::new();
|
||||
for locale in locales {
|
||||
// Language plus country code (e.g. `en_US`)
|
||||
if locale.len() >= 5 && locale.chars().nth(2) == Some('_') {
|
||||
lang_list.push(&locale[..5]);
|
||||
}
|
||||
// Language code only (e.g. `en`)
|
||||
if locale.len() >= 2 && locale != "POSIX" {
|
||||
lang_list.push(&locale[..2]);
|
||||
}
|
||||
}
|
||||
|
||||
lang_list.push("en");
|
||||
lang_list.clear_duplicates();
|
||||
lang_list.into_iter().map(str::to_string).collect()
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
fn get_languages_from_env() -> Vec<String> {
|
||||
get_languages(
|
||||
std::env::var("LANG").ok().as_deref(),
|
||||
std::env::var("LANGUAGE").ok().as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Initialize logger
|
||||
init_log();
|
||||
|
||||
|
|
@ -192,54 +263,14 @@ fn main() -> ExitCode {
|
|||
ColorOptions::Never => false,
|
||||
};
|
||||
|
||||
try_main(args, enable_styles).unwrap_or_else(|error| {
|
||||
print_error(enable_styles, &error);
|
||||
ExitCode::FAILURE
|
||||
})
|
||||
}
|
||||
|
||||
fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> {
|
||||
// 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 config = match Config::load(enable_styles) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
print_error(enable_styles, &e.context("Could not load config"));
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
let mut config = config_loader.load()?;
|
||||
|
||||
// Override styles if needed
|
||||
if !enable_styles {
|
||||
config.style = StyleConfig::default();
|
||||
}
|
||||
|
||||
let custom_pages_dir = config
|
||||
.directories
|
||||
.custom_pages_dir
|
||||
.as_ref()
|
||||
.map(PathWithSource::path);
|
||||
|
||||
// Note: According to the TLDR client spec, page names must be transparently
|
||||
// lowercased before lookup:
|
||||
// https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#page-names
|
||||
let command = args.command.join("-").to_lowercase();
|
||||
|
||||
if args.edit_patch || args.edit_page {
|
||||
let file_name = if args.edit_patch {
|
||||
format!("{command}.patch.md")
|
||||
} else {
|
||||
format!("{command}.page.md")
|
||||
};
|
||||
|
||||
custom_pages_dir
|
||||
.context("To edit custom pages/patches, please specify a custom pages directory.")
|
||||
.and_then(|custom_pages_dir| spawn_editor(custom_pages_dir, &file_name))?;
|
||||
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
// Show various paths
|
||||
if args.show_paths {
|
||||
|
|
@ -248,197 +279,156 @@ fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> {
|
|||
|
||||
// Create a basic config and exit
|
||||
if args.seed_config {
|
||||
create_config(args.config_path.as_deref())?;
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
create_config_and_exit(enable_styles);
|
||||
}
|
||||
|
||||
let fallback_platforms: &[PlatformType] = &[PlatformType::current()];
|
||||
let platforms = args
|
||||
.platforms
|
||||
.as_ref()
|
||||
.map_or(fallback_platforms, Vec::as_slice);
|
||||
|
||||
// If a local file was passed in, render it and exit
|
||||
if let Some(file) = args.render {
|
||||
let reader = PageLookupResult::with_page(file).reader()?;
|
||||
print_page(reader, args.raw, enable_styles, args.pager, &config)?;
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
let path = PageLookupResult::with_page(file);
|
||||
if let Err(ref e) = print_page(&path, args.raw, enable_styles, args.pager, &config) {
|
||||
print_error(enable_styles, e);
|
||||
process::exit(1);
|
||||
} else {
|
||||
process::exit(0);
|
||||
};
|
||||
}
|
||||
|
||||
// The tealdeer page is embedded in the binary, no cache needed
|
||||
if command == "tealdeer" {
|
||||
print_page(
|
||||
TEALDEER_PAGE.as_bytes(),
|
||||
args.raw,
|
||||
enable_styles,
|
||||
args.pager,
|
||||
&config,
|
||||
)?;
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
// Instantiate cache. This will not yet create the cache directory!
|
||||
let cache = Cache::new(&config.directories.cache_dir.path, enable_styles);
|
||||
|
||||
// Clear cache, pass through
|
||||
if args.clear_cache {
|
||||
clear_cache(&cache, args.quiet, enable_styles);
|
||||
}
|
||||
|
||||
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),
|
||||
// Cache update, pass through
|
||||
let cache_updated = if should_update_cache(&cache, &args, &config) {
|
||||
update_cache(&cache, args.quiet, enable_styles);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let cache_config = CacheConfig {
|
||||
pages_directory: &config.directories.cache_dir.path().join(TLDR_PAGES_DIR),
|
||||
custom_pages_directory: config
|
||||
// Check cache presence and freshness
|
||||
if !cache_updated
|
||||
&& (args.list || !args.command.is_empty())
|
||||
&& check_cache(&cache, &args, enable_styles) == CheckCacheResult::CacheMissing
|
||||
{
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
// List cached commands and exit
|
||||
if args.list {
|
||||
let custom_pages_dir = 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.");
|
||||
}
|
||||
|
||||
if args.clear_cache {
|
||||
if let Some(cache) = Cache::open(cache_config)? {
|
||||
clear_cache(cache, args.quiet)?;
|
||||
}
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
let cache = if args.update || config.updates.auto_update && !args.no_auto_update {
|
||||
let (mut cache, was_created) = Cache::open_or_create(cache_config)?;
|
||||
if was_created || args.update || cache.age()? >= config.updates.auto_update_interval {
|
||||
let result = update_cache(
|
||||
&mut cache,
|
||||
config.updates.archive_source,
|
||||
config.updates.tls_backend,
|
||||
args.quiet,
|
||||
);
|
||||
|
||||
if let Err(e) = result {
|
||||
print_error(enable_styles, &e);
|
||||
|
||||
eprintln!();
|
||||
eprintln!("Note: Update errors are often caused by unexpected or missing TLS certificates.");
|
||||
eprintln!(
|
||||
"You are currently using the following TLS backend: {}",
|
||||
config.updates.tls_backend,
|
||||
);
|
||||
eprintln!(
|
||||
"Try changing the updates.tls_backend setting in the config file, for example:"
|
||||
);
|
||||
eprintln!();
|
||||
eprintln!(" [updates]");
|
||||
eprintln!(" tls_backend = \"rustls-with-native-roots\"");
|
||||
eprintln!();
|
||||
eprintln!(
|
||||
"This build of tealdeer has support for the following options: {}",
|
||||
supported_tls_backends_string(),
|
||||
);
|
||||
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
cache
|
||||
} else if args.list || !command.is_empty() {
|
||||
// Cache is needed for these commands to work
|
||||
let Some(cache) = Cache::open(cache_config)? else {
|
||||
print_error(
|
||||
enable_styles,
|
||||
&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);
|
||||
.map(PathWithSource::path);
|
||||
println!(
|
||||
"{}",
|
||||
cache.list_pages(custom_pages_dir, platforms).join("\n")
|
||||
);
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
// Show command from cache
|
||||
if !command.is_empty() {
|
||||
// TODO: Remove this check 1 year after version 1.7.0 was released
|
||||
if cache.old_custom_pages_exist()? {
|
||||
print_warning(
|
||||
enable_styles,
|
||||
&format!(
|
||||
"Custom pages using the old naming convention were found in {}.\n\
|
||||
Please rename them to follow the new convention:\n\
|
||||
- `<name>.page` → `<name>.page.md`\n\
|
||||
- `<name>.patch` → `<name>.patch.md`",
|
||||
cache
|
||||
.config()
|
||||
.custom_pages_directory
|
||||
.expect("Old custom pages can only exist in custom pages directory")
|
||||
.display(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if !args.command.is_empty() {
|
||||
// Note: According to the TLDR client spec, page names must be transparently
|
||||
// lowercased before lookup:
|
||||
// https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#page-names
|
||||
let command = args.command.join("-").to_lowercase();
|
||||
|
||||
let Some(result) = cache.find_page(&command) else {
|
||||
// Collect languages
|
||||
let languages = args
|
||||
.language
|
||||
.map_or_else(get_languages_from_env, |lang| vec![lang]);
|
||||
|
||||
// Search for command in cache
|
||||
if let Some(lookup_result) = cache.find_page(
|
||||
&command,
|
||||
&languages,
|
||||
config
|
||||
.directories
|
||||
.custom_pages_dir
|
||||
.as_ref()
|
||||
.map(PathWithSource::path),
|
||||
platforms,
|
||||
) {
|
||||
if let Err(ref e) =
|
||||
print_page(&lookup_result, args.raw, enable_styles, args.pager, &config)
|
||||
{
|
||||
print_error(enable_styles, e);
|
||||
process::exit(1);
|
||||
}
|
||||
process::exit(0);
|
||||
} else {
|
||||
if !args.quiet {
|
||||
print_warning(
|
||||
enable_styles,
|
||||
&format!(
|
||||
"Page `{command}` not found in cache.\n\
|
||||
"Page `{}` not found in cache.\n\
|
||||
Try updating with `tldr --update`, or submit a pull request to:\n\
|
||||
https://github.com/tldr-pages/tldr"
|
||||
https://github.com/tldr-pages/tldr",
|
||||
&command
|
||||
),
|
||||
);
|
||||
}
|
||||
return Ok(ExitCode::FAILURE);
|
||||
};
|
||||
|
||||
print_page(
|
||||
result.reader()?,
|
||||
args.raw,
|
||||
enable_styles,
|
||||
args.pager,
|
||||
&config,
|
||||
)?;
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::get_languages;
|
||||
|
||||
mod language {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn missing_lang_env() {
|
||||
let lang_list = get_languages(None, Some("de:fr"));
|
||||
assert_eq!(lang_list, ["en"]);
|
||||
let lang_list = get_languages(None, None);
|
||||
assert_eq!(lang_list, ["en"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_language_env() {
|
||||
let lang_list = get_languages(Some("de"), None);
|
||||
assert_eq!(lang_list, ["de", "en"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preference_order() {
|
||||
let lang_list = get_languages(Some("de"), Some("fr:cn"));
|
||||
assert_eq!(lang_list, ["fr", "cn", "de", "en"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn country_code_expansion() {
|
||||
let lang_list = get_languages(Some("pt_BR"), None);
|
||||
assert_eq!(lang_list, ["pt_BR", "pt", "en"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignore_posix_and_c() {
|
||||
let lang_list = get_languages(Some("POSIX"), None);
|
||||
assert_eq!(lang_list, ["en"]);
|
||||
let lang_list = get_languages(Some("C"), None);
|
||||
assert_eq!(lang_list, ["en"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicates() {
|
||||
let lang_list = get_languages(Some("de"), Some("fr:de:cn:de"));
|
||||
assert_eq!(lang_list, ["fr", "de", "cn", "en"]);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
//! Functions for printing pages to the terminal
|
||||
|
||||
use std::io::{self, BufRead, BufReader, Read, Write};
|
||||
use std::io::{self, BufRead, Write};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use yansi::Paint;
|
||||
|
||||
use crate::{
|
||||
cache::PageLookupResult,
|
||||
config::{Config, StyleConfig},
|
||||
formatter::{highlight_lines, PageSnippet},
|
||||
line_iterator::LineIterator,
|
||||
|
|
@ -29,13 +30,14 @@ fn configure_pager(enable_styles: bool) {
|
|||
|
||||
/// Print page by path
|
||||
pub fn print_page(
|
||||
reader: impl Read,
|
||||
lookup_result: &PageLookupResult,
|
||||
enable_markdown: bool,
|
||||
enable_styles: bool,
|
||||
use_pager: bool,
|
||||
config: &Config,
|
||||
) -> Result<()> {
|
||||
let reader = BufReader::new(reader);
|
||||
// Create reader from file(s)
|
||||
let reader = lookup_result.reader()?;
|
||||
|
||||
// Configure pager if applicable
|
||||
if use_pager || config.display.use_pager {
|
||||
|
|
@ -54,7 +56,7 @@ pub fn print_page(
|
|||
}
|
||||
} else {
|
||||
// Closure that processes a page snippet and writes it to stdout
|
||||
let mut process_snippet = |snip: PageSnippet<&str>| {
|
||||
let mut process_snippet = |snip: PageSnippet<'_>| {
|
||||
if snip.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
|
|
@ -67,11 +69,9 @@ pub fn print_page(
|
|||
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")?;
|
||||
|
|
@ -81,17 +81,17 @@ pub fn print_page(
|
|||
|
||||
fn print_snippet(
|
||||
writer: &mut impl Write,
|
||||
snip: PageSnippet<&str>,
|
||||
snip: PageSnippet<'_>,
|
||||
style: &StyleConfig,
|
||||
) -> io::Result<()> {
|
||||
use PageSnippet::*;
|
||||
|
||||
match snip {
|
||||
CommandName(s) | Title(s) => write!(writer, "{}", s.paint(style.command_name)),
|
||||
CommandName(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)),
|
||||
Description(s) => writeln!(writer, " {}", s.paint(style.description)),
|
||||
Text(s) => writeln!(writer, " {}", s.paint(style.example_text)),
|
||||
Linebreak => writeln!(writer),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
src/types.rs
15
src/types.rs
|
|
@ -16,7 +16,6 @@ pub enum PlatformType {
|
|||
FreeBsd,
|
||||
NetBsd,
|
||||
OpenBsd,
|
||||
Common,
|
||||
}
|
||||
|
||||
impl fmt::Display for PlatformType {
|
||||
|
|
@ -30,7 +29,6 @@ impl fmt::Display for PlatformType {
|
|||
Self::FreeBsd => write!(f, "FreeBSD"),
|
||||
Self::NetBsd => write!(f, "NetBSD"),
|
||||
Self::OpenBsd => write!(f, "OpenBSD"),
|
||||
Self::Common => write!(f, "Common"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +44,6 @@ impl clap::ValueEnum for PlatformType {
|
|||
Self::FreeBsd,
|
||||
Self::NetBsd,
|
||||
Self::OpenBsd,
|
||||
Self::Common,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +57,6 @@ impl clap::ValueEnum for PlatformType {
|
|||
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")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -118,14 +114,18 @@ impl PlatformType {
|
|||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize, clap::ValueEnum)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[derive(Default)]
|
||||
pub enum ColorOptions {
|
||||
Always,
|
||||
#[default]
|
||||
Auto,
|
||||
Never,
|
||||
}
|
||||
|
||||
impl Default for ColorOptions {
|
||||
fn default() -> Self {
|
||||
Self::Auto
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum LineType {
|
||||
Empty,
|
||||
|
|
@ -201,8 +201,6 @@ pub enum PathSource {
|
|||
EnvVar,
|
||||
/// Config file
|
||||
ConfigFile,
|
||||
/// CLI argument override
|
||||
Cli,
|
||||
}
|
||||
|
||||
impl fmt::Display for PathSource {
|
||||
|
|
@ -214,7 +212,6 @@ impl fmt::Display for PathSource {
|
|||
Self::OsConvention => "OS convention",
|
||||
Self::EnvVar => "env variable",
|
||||
Self::ConfigFile => "config file",
|
||||
Self::Cli => "command line argument",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
36
tests/cache/pages.en/common/git-checkout.md
vendored
36
tests/cache/pages.en/common/git-checkout.md
vendored
|
|
@ -1,36 +0,0 @@
|
|||
# git checkout
|
||||
|
||||
> Checkout a branch or paths to the working tree.
|
||||
> More information: <https://git-scm.com/docs/git-checkout>.
|
||||
|
||||
- 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}}`
|
||||
37
tests/cache/pages.ja/common/apt.md
vendored
37
tests/cache/pages.ja/common/apt.md
vendored
|
|
@ -1,37 +0,0 @@
|
|||
# apt
|
||||
|
||||
> Debian系ディストリビューションで使われるパッケージ管理システムです。
|
||||
> Ubuntuのバージョンが16.04か、それ以降で対話モードを使う場合`apt-get`の代わりとして使用します。
|
||||
> 詳しくはこちら: <https://manned.org/apt.8>
|
||||
|
||||
- 利用可能なパーケージとバージョンのリストの更新(他の`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`
|
||||
32
tests/chmod.ru.expected
Normal file
32
tests/chmod.ru.expected
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
Изменить права доступа файлу или папке.
|
||||
Больше информации: <https://www.gnu.org/software/coreutils/chmod>.
|
||||
|
||||
[32mДать [u]пользователю, который владеет файлом, права на его [x]исполнение:[0m
|
||||
|
||||
[36m [0m[36mchmod[0m[36m u+x [0m[4;36mфайл[0m
|
||||
|
||||
[32mДать права [u]пользователю права [r]чтения и [w]записи в файл/папку:[0m
|
||||
|
||||
[36m [0m[36mchmod[0m[36m u+rw [0m[4;36mфайл_или_папка[0m
|
||||
|
||||
[32mУбрать права на [x]исполнение у [g]группы:[0m
|
||||
|
||||
[36m [0m[36mchmod[0m[36m g-x [0m[4;36mфайл[0m
|
||||
|
||||
[32mДать [a]всем пользователям права на [r]чтение и [x]исполенеие:[0m
|
||||
|
||||
[36m [0m[36mchmod[0m[36m a+rx [0m[4;36mфайл[0m
|
||||
|
||||
[32mДать [o]другим (не из группы владельцев файлом) такие же права как и у [g]группы:[0m
|
||||
|
||||
[36m [0m[36mchmod[0m[36m o=g [0m[4;36mфайл[0m
|
||||
|
||||
[32mУбрать все права у [o]других:[0m
|
||||
|
||||
[36m [0m[36mchmod[0m[36m o= [0m[4;36mфайл[0m
|
||||
|
||||
[32mИзменить права рекурсивно, дав [g]группе и [o]другим возможность [w]записи в папку:[0m
|
||||
|
||||
[36m [0m[36mchmod[0m[36m -R g+w,o+w [0m[4;36mпапка[0m
|
||||
|
||||
32
tests/chmod.ru.md
Normal file
32
tests/chmod.ru.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# chmod
|
||||
|
||||
> Изменить права доступа файлу или папке.
|
||||
> Больше информации: <https://www.gnu.org/software/coreutils/chmod>.
|
||||
|
||||
- Дать [u]пользователю, который владеет файлом, права на его [x]исполнение:
|
||||
|
||||
`chmod u+x {{файл}}`
|
||||
|
||||
- Дать права [u]пользователю права [r]чтения и [w]записи в файл/папку:
|
||||
|
||||
`chmod u+rw {{файл_или_папка}}`
|
||||
|
||||
- Убрать права на [x]исполнение у [g]группы:
|
||||
|
||||
`chmod g-x {{файл}}`
|
||||
|
||||
- Дать [a]всем пользователям права на [r]чтение и [x]исполенеие:
|
||||
|
||||
`chmod a+rx {{файл}}`
|
||||
|
||||
- Дать [o]другим (не из группы владельцев файлом) такие же права как и у [g]группы:
|
||||
|
||||
`chmod o=g {{файл}}`
|
||||
|
||||
- Убрать все права у [o]других:
|
||||
|
||||
`chmod o= {{файл}}`
|
||||
|
||||
- Изменить права рекурсивно, дав [g]группе и [o]другим возможность [w]записи в папку:
|
||||
|
||||
`chmod -R g+w,o+w {{папка}}`
|
||||
|
|
@ -19,3 +19,11 @@ underline = false
|
|||
underline = true
|
||||
bold = false
|
||||
italic = true
|
||||
|
||||
[display]
|
||||
use_pager = false
|
||||
compact = false
|
||||
|
||||
[updates]
|
||||
auto_update = false
|
||||
auto_update_interval_hours = 720
|
||||
|
|
@ -2,31 +2,31 @@
|
|||
An SVG (Scalable Vector Graphics) editing program.
|
||||
Use -z to not open the GUI and only process files in the console.
|
||||
|
||||
[32m [0m[32mOpen an SVG file in the Inkscape GUI:[0m
|
||||
[32mOpen an SVG file in the Inkscape GUI:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m
|
||||
|
||||
[32m [0m[32mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||
[32mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -e [0m[4;36mfilename.png[0m
|
||||
|
||||
[32m [0m[32mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
||||
[32mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -e [0m[4;36mfilename.png[0m[36m -w [0m[4;36m600[0m[36m -h [0m[4;36m400[0m
|
||||
|
||||
[32m [0m[32mExport a single object, given its ID, into a bitmap:[0m
|
||||
[32mExport a single object, given its ID, into a bitmap:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -i [0m[4;36mid[0m[36m -e [0m[4;36mobject.png[0m
|
||||
|
||||
[32m [0m[32mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||
[32mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m | [0m[36minkscape[0m[36m | [0m[36minkscape[0m[36m --export-pdf=[0m[4;36minkscape.pdf[0m[36m | [0m[36minkscape[0m[36m | [0m[36minkscape[0m[36m --export-text-to-path[0m
|
||||
|
||||
[32m [0m[32mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||
[32mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit[0m
|
||||
|
||||
[32m [0m[32mSome invalid command just to test the correct highlighting of the command name:[0m
|
||||
[32mSome invalid command just to test the correct highlighting of the command name:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m --use-inkscape=v3.0 file[0m
|
||||
|
||||
|
|
@ -2,31 +2,31 @@
|
|||
An SVG (Scalable Vector Graphics) editing program.
|
||||
Use -z to not open the GUI and only process files in the console.
|
||||
|
||||
[44;30m [0m[44;30mOpen an SVG file in the Inkscape GUI:[0m
|
||||
[44;30mOpen an SVG file in the Inkscape GUI:[0m
|
||||
|
||||
[1minkscape[0m [3;4mfilename.svg[0m
|
||||
|
||||
[44;30m [0m[44;30mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||
[44;30mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||
|
||||
[1minkscape[0m [3;4mfilename.svg[0m -e [3;4mfilename.png[0m
|
||||
|
||||
[44;30m [0m[44;30mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
||||
[44;30mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
||||
|
||||
[1minkscape[0m [3;4mfilename.svg[0m -e [3;4mfilename.png[0m -w [3;4m600[0m -h [3;4m400[0m
|
||||
|
||||
[44;30m [0m[44;30mExport a single object, given its ID, into a bitmap:[0m
|
||||
[44;30mExport a single object, given its ID, into a bitmap:[0m
|
||||
|
||||
[1minkscape[0m [3;4mfilename.svg[0m -i [3;4mid[0m -e [3;4mobject.png[0m
|
||||
|
||||
[44;30m [0m[44;30mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||
[44;30mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||
|
||||
[1minkscape[0m [3;4mfilename.svg[0m | [1minkscape[0m | [1minkscape[0m --export-pdf=[3;4minkscape.pdf[0m | [1minkscape[0m | [1minkscape[0m --export-text-to-path
|
||||
|
||||
[44;30m [0m[44;30mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||
[44;30mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||
|
||||
[1minkscape[0m [3;4mfilename.svg[0m --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
||||
|
||||
[44;30m [0m[44;30mSome invalid command just to test the correct highlighting of the command name:[0m
|
||||
[44;30mSome invalid command just to test the correct highlighting of the command name:[0m
|
||||
|
||||
[1minkscape[0m --use-inkscape=v3.0 file
|
||||
|
||||
1071
tests/lib.rs
1071
tests/lib.rs
File diff suppressed because it is too large
Load diff
|
|
@ -1,37 +0,0 @@
|
|||
|
||||
Debian系ディストリビューションで使われるパッケージ管理システムです。
|
||||
Ubuntuのバージョンが16.04か、それ以降で対話モードを使う場合`apt-get`の代わりとして使用します。
|
||||
詳しくはこちら: <https://manned.org/apt.8>
|
||||
|
||||
[32m [0m[32m利用可能なパーケージとバージョンのリストの更新(他の`apt`コマンドの前での実行を推奨):[0m
|
||||
|
||||
[36m [0m[36msudo [0m[36mapt[0m[36m update[0m
|
||||
|
||||
[32m [0m[32m指定されたパッケージの検索:[0m
|
||||
|
||||
[36m [0m[36mapt[0m[36m search [0m[4;36mパッケージ[0m
|
||||
|
||||
[32m [0m[32mパッケージの情報を出力:[0m
|
||||
|
||||
[36m [0m[36mapt[0m[36m show [0m[4;36mパッケージ[0m
|
||||
|
||||
[32m [0m[32mパッケージのインストール、または利用可能な最新バージョンに更新:[0m
|
||||
|
||||
[36m [0m[36msudo [0m[36mapt[0m[36m install [0m[4;36mパッケージ[0m
|
||||
|
||||
[32m [0m[32mパッケージの削除(`sudo apt remove --purge`の場合設定ファイルも削除):[0m
|
||||
|
||||
[36m [0m[36msudo [0m[36mapt[0m[36m remove [0m[4;36mパッケージ[0m
|
||||
|
||||
[32m [0m[32mインストールされている全てのパッケージを最新のバージョンにアップグレード:[0m
|
||||
|
||||
[36m [0m[36msudo [0m[36mapt[0m[36m upgrade[0m
|
||||
|
||||
[32m [0m[32mインストールできるすべてのパッケージを表示:[0m
|
||||
|
||||
[36m [0m[36mapt[0m[36m list[0m
|
||||
|
||||
[32m [0m[32mインストールされた全てのパッケージを表示(依存関係も表示):[0m
|
||||
|
||||
[36m [0m[36mapt[0m[36m list --installed[0m
|
||||
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
|
||||
An SVG (Scalable Vector Graphics) editing program.
|
||||
Use -z to not open the GUI and only process files in the console.
|
||||
|
||||
Open an SVG file in the Inkscape GUI:
|
||||
|
||||
inkscape filename.svg
|
||||
|
||||
Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):
|
||||
|
||||
inkscape filename.svg -e filename.png
|
||||
|
||||
Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):
|
||||
|
||||
inkscape filename.svg -e filename.png -w 600 -h 400
|
||||
|
||||
Export a single object, given its ID, into a bitmap:
|
||||
|
||||
inkscape filename.svg -i id -e object.png
|
||||
|
||||
Export an SVG document to PDF, converting all texts to paths:
|
||||
|
||||
inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path
|
||||
|
||||
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
|
||||
|
||||
inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
||||
|
||||
Some invalid command just to test the correct highlighting of the command name:
|
||||
|
||||
inkscape --use-inkscape=v3.0 file
|
||||
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
|
||||
inkscape
|
||||
|
||||
An SVG (Scalable Vector Graphics) editing program.
|
||||
Use -z to not open the GUI and only process files in the console.
|
||||
|
||||
Open an SVG file in the Inkscape GUI:
|
||||
|
||||
inkscape filename.svg
|
||||
|
||||
Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):
|
||||
|
||||
inkscape filename.svg -e filename.png
|
||||
|
||||
Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):
|
||||
|
||||
inkscape filename.svg -e filename.png -w 600 -h 400
|
||||
|
||||
Export a single object, given its ID, into a bitmap:
|
||||
|
||||
inkscape filename.svg -i id -e object.png
|
||||
|
||||
Export an SVG document to PDF, converting all texts to paths:
|
||||
|
||||
inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path
|
||||
|
||||
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
|
||||
|
||||
inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
||||
|
||||
Some invalid command just to test the correct highlighting of the command name:
|
||||
|
||||
inkscape --use-inkscape=v3.0 file
|
||||
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
|
||||
[36m [0m[36minkscape[0m
|
||||
|
||||
An SVG (Scalable Vector Graphics) editing program.
|
||||
Use -z to not open the GUI and only process files in the console.
|
||||
|
||||
[32m [0m[32mOpen an SVG file in the Inkscape GUI:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m
|
||||
|
||||
[32m [0m[32mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -e [0m[4;36mfilename.png[0m
|
||||
|
||||
[32m [0m[32mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -e [0m[4;36mfilename.png[0m[36m -w [0m[4;36m600[0m[36m -h [0m[4;36m400[0m
|
||||
|
||||
[32m [0m[32mExport a single object, given its ID, into a bitmap:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -i [0m[4;36mid[0m[36m -e [0m[4;36mobject.png[0m
|
||||
|
||||
[32m [0m[32mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m | [0m[36minkscape[0m[36m | [0m[36minkscape[0m[36m --export-pdf=[0m[4;36minkscape.pdf[0m[36m | [0m[36minkscape[0m[36m | [0m[36minkscape[0m[36m --export-text-to-path[0m
|
||||
|
||||
[32m [0m[32mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit[0m
|
||||
|
||||
[32m [0m[32mSome invalid command just to test the correct highlighting of the command name:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m --use-inkscape=v3.0 file[0m
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue