mirror of
https://github.com/tealdeer-rs/tealdeer.git
synced 2026-08-22 08:04:19 +02:00
Compare commits
68 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d07fb02e86 |
||
|
|
f9fab32812 |
||
|
|
fbc7520f8c |
||
|
|
911277cd21 |
||
|
|
43157e78c7 |
||
|
|
39094354b6 |
||
|
|
1414194988 |
||
|
|
8b00800a69 |
||
|
|
5f837f4dd6 |
||
|
|
82b9c88f39 |
||
|
|
b7da9a34f3 |
||
|
|
144ea1d727 |
||
|
|
5d202fca81 |
||
|
|
fabb378368 |
||
|
|
e86ca1aa86 |
||
|
|
65ec680ea3 |
||
|
|
d15718f672 |
||
|
|
c80a935b89 |
||
|
|
21ab081dae |
||
|
|
64db5accfa |
||
|
|
8bd3a0d4aa |
||
|
|
1c68f99c2a |
||
|
|
37b0dee39f |
||
|
|
28ed785001 |
||
|
|
f8a2003bc2 |
||
|
|
df5113ddaa |
||
|
|
51593d27eb |
||
|
|
d0108b23e4 |
||
|
|
4d33e8a279 |
||
|
|
1252261d66 |
||
|
|
24e7f383b8 |
||
|
|
6c65c8f71c |
||
|
|
b8f7c0cc2d |
||
|
|
b19517097a |
||
|
|
6f91c3a765 |
||
|
|
41739c5bf9 |
||
|
|
47a936e736 |
||
|
|
593e9309b9 |
||
|
|
8b97afe7aa |
||
|
|
75e5462312 |
||
|
|
5ee1f28021 |
||
|
|
3a6fd99c85 |
||
|
|
c5d62e5987 |
||
|
|
b3cd7b1c21 |
||
|
|
e769114d8b |
||
|
|
e1213158e4 |
||
|
|
6c1d702769 |
||
|
|
d49c4a9e05 |
||
|
|
49626977ff |
||
|
|
9a83b58d51 |
||
|
|
2b127fd67e |
||
|
|
911508ce33 |
||
|
|
5b306756af |
||
|
|
abb7e8ac55 |
||
|
|
92b6c64c87 |
||
|
|
c741146db5 |
||
|
|
a74b7120bd |
||
|
|
3fa96a5bb2 |
||
|
|
7e014093cf |
||
|
|
94f9030d36 |
||
|
|
5cfb817e99 |
||
|
|
4377366c97 |
||
|
|
630b7f4423 |
||
|
|
1e87db7ab7 |
||
|
|
d1be7d6bb9 |
||
|
|
43ab2cb920 |
||
|
|
bc820c5f10 |
||
|
|
9bb95ad11d |
49 changed files with 3581 additions and 1777 deletions
53
.github/workflows/ci.yml
vendored
53
.github/workflows/ci.yml
vendored
|
|
@ -14,41 +14,57 @@ jobs:
|
||||||
name: run tests
|
name: run tests
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
platform: [ubuntu-latest, macos-latest, windows-latest, windows-11-arm]
|
||||||
toolchain: [stable, 1.75.0]
|
toolchain: [stable, 1.88.0] # MSRV
|
||||||
|
include:
|
||||||
|
- platform: windows-latest
|
||||||
|
exe_suffix: .exe
|
||||||
|
- platform: windows-11-arm
|
||||||
|
exe_suffix: .exe
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ matrix.toolchain }}
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
- run: mkdir artifacts
|
||||||
- name: Build with default features
|
- name: Build with default features
|
||||||
run: cargo build
|
run: |
|
||||||
|
cargo build --locked
|
||||||
|
cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-default${{ matrix.exe_suffix}}
|
||||||
- name: Build with logging and Rustls with webpki roots
|
- name: Build with logging and Rustls with webpki roots
|
||||||
run: cargo build --features logging,rustls-with-webpki-roots --no-default-features
|
run: |
|
||||||
|
cargo build --locked --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
|
- name: Build with native TLS backend
|
||||||
# expects runners have the proper Native SSL library
|
run: |
|
||||||
run: cargo build --features native-tls --no-default-features
|
# expects runners have the proper Native SSL library
|
||||||
|
cargo build --locked --features native-tls --no-default-features
|
||||||
|
cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-native-tls${{ matrix.exe_suffix}}
|
||||||
|
- uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: tldr-debug-build-${{ matrix.platform }}-rust-${{ matrix.toolchain }}
|
||||||
|
path: artifacts/
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test -- --test-threads 1
|
run: cargo test --locked -- --test-threads 1
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
name: run clippy lints
|
name: run clippy lints
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: 1.88.0 # MSRV
|
||||||
components: clippy
|
components: clippy
|
||||||
- name: run clippy lints
|
- name: run clippy lints
|
||||||
run: cargo clippy --features logging
|
run: cargo clippy --locked --all-targets --features logging
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
name: run rustfmt
|
name: run rustfmt
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
|
|
@ -60,20 +76,17 @@ jobs:
|
||||||
name: build docs
|
name: build docs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Setup mdBook
|
- run: ./scripts/get-mdbook.sh
|
||||||
uses: peaceiris/actions-mdbook@v2
|
|
||||||
with:
|
|
||||||
mdbook-version: '0.4.4'
|
|
||||||
- name: Setup toolchain
|
- name: Setup toolchain
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build
|
run: cargo build --locked
|
||||||
- name: Ensure that docs can be built
|
- name: Ensure that docs can be built
|
||||||
run: cd docs && mdbook build
|
run: ./mdbook build docs
|
||||||
- name: Generate usage string
|
- name: Generate usage string
|
||||||
run: cargo run -- --help > docs/src/usage-actual.txt
|
run: cargo run --locked -- --help > docs/src/usage-actual.txt
|
||||||
- name: Ensure that usage string is up to date
|
- name: Ensure that usage string is up to date
|
||||||
run: diff docs/src/usage{,-actual}.txt
|
run: diff docs/src/usage{,-actual}.txt
|
||||||
|
|
|
||||||
24
.github/workflows/gh-pages.yml
vendored
24
.github/workflows/gh-pages.yml
vendored
|
|
@ -1,24 +0,0 @@
|
||||||
name: GitHub Pages
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v[1-9]*" # push events matching `v` followed by anything larger than 0, e.g. v1.0, v20.15.10
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup mdBook
|
|
||||||
uses: peaceiris/actions-mdbook@v2
|
|
||||||
with:
|
|
||||||
mdbook-version: '0.4.4'
|
|
||||||
|
|
||||||
- run: cd docs && mdbook build
|
|
||||||
|
|
||||||
- name: Deploy
|
|
||||||
uses: peaceiris/actions-gh-pages@v4
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
publish_dir: ./docs/book
|
|
||||||
43
.github/workflows/release.yml
vendored
43
.github/workflows/release.yml
vendored
|
|
@ -8,7 +8,7 @@ jobs:
|
||||||
create-release:
|
create-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Create release for tag
|
- name: Create release for tag
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -24,7 +24,7 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
target: ["bash", "fish", "zsh"]
|
target: ["bash", "fish", "zsh"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Upload completion
|
- name: Upload completion
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -40,7 +40,7 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
target: ["MIT", "APACHE"]
|
target: ["MIT", "APACHE"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Upload license
|
- name: Upload license
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -66,14 +66,14 @@ jobs:
|
||||||
- arch: "arm"
|
- arch: "arm"
|
||||||
libc: "musleabihf"
|
libc: "musleabihf"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Pull Docker image
|
- name: Pull Docker image
|
||||||
run: docker pull messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }}
|
run: docker pull messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }}
|
||||||
- name: Build in Docker
|
- 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
|
run: docker run --rm -i -v "$(pwd)":/home/rust/src messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} cargo build --locked --release
|
||||||
- name: Strip binary
|
- 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
|
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@v4
|
- uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: "tealdeer-linux-${{ matrix.arch }}-${{ matrix.libc }}"
|
name: "tealdeer-linux-${{ matrix.arch }}-${{ matrix.libc }}"
|
||||||
path: "target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr"
|
path: "target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr"
|
||||||
|
|
@ -86,33 +86,41 @@ jobs:
|
||||||
- arch: "x86_64"
|
- arch: "x86_64"
|
||||||
- arch: "aarch64"
|
- arch: "aarch64"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Setup toolchain
|
- name: Setup toolchain
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
targets: "${{ matrix.arch }}-apple-darwin"
|
targets: "${{ matrix.arch }}-apple-darwin"
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --release --target ${{ matrix.arch }}-apple-darwin --no-default-features --features webpki-roots
|
run: cargo build --locked --release --target ${{ matrix.arch }}-apple-darwin
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: "tealdeer-macos-${{ matrix.arch }}"
|
name: "tealdeer-macos-${{ matrix.arch }}"
|
||||||
path: "target/${{ matrix.arch }}-apple-darwin/release/tldr"
|
path: "target/${{ matrix.arch }}-apple-darwin/release/tldr"
|
||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
runs-on: windows-latest
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- arch: "x86_64"
|
||||||
|
os: windows-latest
|
||||||
|
- arch: "aarch64"
|
||||||
|
os: windows-11-arm
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Setup toolchain
|
- name: Setup toolchain
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
|
targets: "${{ matrix.arch }}-pc-windows-msvc"
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --release --target x86_64-pc-windows-msvc
|
run: cargo build --locked --release --target ${{ matrix.arch }}-pc-windows-msvc
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: "tealdeer-windows-x86_64-msvc"
|
name: "tealdeer-windows-${{ matrix.arch }}-msvc"
|
||||||
path: "target/x86_64-pc-windows-msvc/release/tldr.exe"
|
path: "target/${{ matrix.arch }}-pc-windows-msvc/release/tldr.exe"
|
||||||
|
|
||||||
upload-release:
|
upload-release:
|
||||||
needs:
|
needs:
|
||||||
|
|
@ -133,9 +141,10 @@ jobs:
|
||||||
- macos-x86_64
|
- macos-x86_64
|
||||||
- macos-aarch64
|
- macos-aarch64
|
||||||
- windows-x86_64-msvc
|
- windows-x86_64-msvc
|
||||||
|
- windows-aarch64-msvc
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v8
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
7
.readthedocs.yaml
Normal file
7
.readthedocs.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
build:
|
||||||
|
os: ubuntu-26.04
|
||||||
|
commands:
|
||||||
|
- ./scripts/get-mdbook.sh
|
||||||
|
- ./mdbook build docs --dest-dir $READTHEDOCS_OUTPUT/html
|
||||||
197
CHANGELOG.md
197
CHANGELOG.md
|
|
@ -13,6 +13,136 @@ Possible log types:
|
||||||
- `[docs]` for documentation changes.
|
- `[docs]` for documentation changes.
|
||||||
- `[chore]` for maintenance work.
|
- `[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)
|
### [v1.7.2][v1.7.2] (2025-03-18)
|
||||||
|
|
||||||
This patch release updates the `zip` dependency to mitigate a potential security
|
This patch release updates the `zip` dependency to mitigate a potential security
|
||||||
|
|
@ -34,11 +164,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
|
previous versions of `yansi` can be removed from the package sets of Linux
|
||||||
distributions. This change should not impact the behavior of tealdeer.
|
distributions. This change should not impact the behavior of tealdeer.
|
||||||
|
|
||||||
Changes:
|
#### Changes:
|
||||||
|
|
||||||
- [chore] Upgrade yansi: 0.5.1 -> 1.0.1 ([#389])
|
- [chore] Upgrade yansi: 0.5.1 -> 1.0.1 ([#389])
|
||||||
|
|
||||||
Contributors to this version:
|
#### Contributors to this version:
|
||||||
|
|
||||||
- [Blair Noctis][@nc7s]
|
- [Blair Noctis][@nc7s]
|
||||||
|
|
||||||
|
|
@ -74,7 +204,7 @@ On a personal note, this will be the last release from me
|
||||||
([Danilo](https://github.com/dbrgn/)) as primary maintainer of tealdeer. For
|
([Danilo](https://github.com/dbrgn/)) as primary maintainer of tealdeer. For
|
||||||
details, see [#376](https://github.com/tealdeer-rs/tealdeer/issues/376).
|
details, see [#376](https://github.com/tealdeer-rs/tealdeer/issues/376).
|
||||||
|
|
||||||
Changes:
|
#### Changes:
|
||||||
|
|
||||||
- [added] Allow querying multiple platforms ([#300])
|
- [added] Allow querying multiple platforms ([#300])
|
||||||
- [added] Add BSD platform support ([#354])
|
- [added] Add BSD platform support ([#354])
|
||||||
|
|
@ -94,7 +224,7 @@ Changes:
|
||||||
- [chore] Update Cargo.toml license field following SPDX 2.1 ([#336])
|
- [chore] Update Cargo.toml license field following SPDX 2.1 ([#336])
|
||||||
- [chore] Dependency updates
|
- [chore] Dependency updates
|
||||||
|
|
||||||
Contributors to this version:
|
#### Contributors to this version:
|
||||||
|
|
||||||
- [Adam Henley][@adamazing]
|
- [Adam Henley][@adamazing]
|
||||||
- [Andrea Frigido][@frisoft]
|
- [Andrea Frigido][@frisoft]
|
||||||
|
|
@ -118,12 +248,12 @@ Thanks!
|
||||||
|
|
||||||
### [v1.6.1][v1.6.1] (2022-10-24)
|
### [v1.6.1][v1.6.1] (2022-10-24)
|
||||||
|
|
||||||
Changes:
|
#### Changes:
|
||||||
|
|
||||||
- [fixed] Fix path source for custom pages dir ([#297])
|
- [fixed] Fix path source for custom pages dir ([#297])
|
||||||
- [chore] Update dependendencies ([#299])
|
- [chore] Update dependendencies ([#299])
|
||||||
|
|
||||||
Contributors to this version:
|
#### Contributors to this version:
|
||||||
|
|
||||||
- [Cyrus Yip][@CyrusYip]
|
- [Cyrus Yip][@CyrusYip]
|
||||||
- [Danilo Bargen][@dbrgn]
|
- [Danilo Bargen][@dbrgn]
|
||||||
|
|
@ -142,7 +272,7 @@ The `TEALDEER_CACHE_DIR` env variable is now deprecated.
|
||||||
A note to packagers: Shell completions have been moved to the `completion/`
|
A note to packagers: Shell completions have been moved to the `completion/`
|
||||||
subdirectory! Packaging scripts might need to be updated.
|
subdirectory! Packaging scripts might need to be updated.
|
||||||
|
|
||||||
Changes:
|
#### Changes:
|
||||||
|
|
||||||
- [added] Allow overriding cache directory through config ([#276])
|
- [added] Allow overriding cache directory through config ([#276])
|
||||||
- [added] Add `--no-auto-update` CLI flag ([#257])
|
- [added] Add `--no-auto-update` CLI flag ([#257])
|
||||||
|
|
@ -163,7 +293,7 @@ Changes:
|
||||||
- [chore] Use anyhow for error handling ([#249])
|
- [chore] Use anyhow for error handling ([#249])
|
||||||
- [chore] Switch to Rust 2021 edition ([#284])
|
- [chore] Switch to Rust 2021 edition ([#284])
|
||||||
|
|
||||||
Contributors to this version:
|
#### Contributors to this version:
|
||||||
|
|
||||||
- [@bagohart][@bagohart]
|
- [@bagohart][@bagohart]
|
||||||
- [@cyqsimon][@cyqsimon]
|
- [@cyqsimon][@cyqsimon]
|
||||||
|
|
@ -212,7 +342,7 @@ Note that the MSRV (Minimal Supported Rust Version) of the project
|
||||||
> When publishing a tealdeer release, the Rust version required to build it
|
> When publishing a tealdeer release, the Rust version required to build it
|
||||||
> should be stable for at least a month.
|
> should be stable for at least a month.
|
||||||
|
|
||||||
Changes:
|
#### Changes:
|
||||||
|
|
||||||
- [added] Support custom pages and patches ([#142][i142])
|
- [added] Support custom pages and patches ([#142][i142])
|
||||||
- [added] Multi-language support ([#125][i125], [#161][i161])
|
- [added] Multi-language support ([#125][i125], [#161][i161])
|
||||||
|
|
@ -243,7 +373,7 @@ Changes:
|
||||||
- [chore] All release binaries are now generated in CI. Binaries for macOS and Windows are also provided. ([#240][i240])
|
- [chore] All release binaries are now generated in CI. Binaries for macOS and Windows are also provided. ([#240][i240])
|
||||||
- [chore] Update all dependencies
|
- [chore] Update all dependencies
|
||||||
|
|
||||||
Contributors to this version:
|
#### Contributors to this version:
|
||||||
|
|
||||||
- [@bl-ue][@bl-ue]
|
- [@bl-ue][@bl-ue]
|
||||||
- [Cameron Tod][@cam8001]
|
- [Cameron Tod][@cam8001]
|
||||||
|
|
@ -272,7 +402,7 @@ co-maintainer. Thank you for your help!
|
||||||
|
|
||||||
- [fixed] Syntax error in zsh completion file ([#138][i138])
|
- [fixed] Syntax error in zsh completion file ([#138][i138])
|
||||||
|
|
||||||
Contributors to this version:
|
#### Contributors to this version:
|
||||||
|
|
||||||
- [Danilo Bargen][@dbrgn]
|
- [Danilo Bargen][@dbrgn]
|
||||||
- [Bruno A. Muciño][@mucinoab]
|
- [Bruno A. Muciño][@mucinoab]
|
||||||
|
|
@ -289,7 +419,7 @@ Thanks!
|
||||||
- [changed] Make `--list` option comply with official spec ([#112][i112])
|
- [changed] Make `--list` option comply with official spec ([#112][i112])
|
||||||
- [changed] Move cache age warning to stderr ([#113][i113])
|
- [changed] Move cache age warning to stderr ([#113][i113])
|
||||||
|
|
||||||
Contributors to this version:
|
#### Contributors to this version:
|
||||||
|
|
||||||
- [Atul Bhosale][@Atul9]
|
- [Atul Bhosale][@Atul9]
|
||||||
- [Danilo Bargen][@dbrgn]
|
- [Danilo Bargen][@dbrgn]
|
||||||
|
|
@ -315,7 +445,7 @@ Thanks!
|
||||||
- [fixed] Fix Fish autocompletion on macOS ([#87][i87])
|
- [fixed] Fix Fish autocompletion on macOS ([#87][i87])
|
||||||
- [fixed] Fix compilation on Windows by disabling pager ([#99][i99])
|
- [fixed] Fix compilation on Windows by disabling pager ([#99][i99])
|
||||||
|
|
||||||
Contributors to this version:
|
#### Contributors to this version:
|
||||||
|
|
||||||
- [Bruno Heridet][@Delapouite]
|
- [Bruno Heridet][@Delapouite]
|
||||||
- [Danilo Bargen][@dbrgn]
|
- [Danilo Bargen][@dbrgn]
|
||||||
|
|
@ -341,7 +471,7 @@ Thanks!
|
||||||
- [changed] Move to Rust 2018, require Rust 1.32 ([#69][i69] / [#84][i84])
|
- [changed] Move to Rust 2018, require Rust 1.32 ([#69][i69] / [#84][i84])
|
||||||
- [fixed] Add (back) support for proxies ([#68][i68])
|
- [fixed] Add (back) support for proxies ([#68][i68])
|
||||||
|
|
||||||
Contributors to this version:
|
#### Contributors to this version:
|
||||||
|
|
||||||
- [Bar Hatsor][@Bassets]
|
- [Bar Hatsor][@Bassets]
|
||||||
- [Danilo Bargen][@dbrgn]
|
- [Danilo Bargen][@dbrgn]
|
||||||
|
|
@ -364,7 +494,7 @@ Thanks!
|
||||||
- [changed] Require at least Rust 1.28 to build (previous: 1.19)
|
- [changed] Require at least Rust 1.28 to build (previous: 1.19)
|
||||||
- [fixed] Fix building on systems with openssl 1.1.1 ([#47][i47])
|
- [fixed] Fix building on systems with openssl 1.1.1 ([#47][i47])
|
||||||
|
|
||||||
Contributors to this version:
|
#### Contributors to this version:
|
||||||
|
|
||||||
- [Danilo Bargen][@dbrgn]
|
- [Danilo Bargen][@dbrgn]
|
||||||
- [@equal-l2][@equal-l2]
|
- [@equal-l2][@equal-l2]
|
||||||
|
|
@ -397,7 +527,7 @@ Thanks!
|
||||||
|
|
||||||
- First crates.io release
|
- First crates.io release
|
||||||
|
|
||||||
|
[user documentation]: https://docs.tealdeer.org
|
||||||
|
|
||||||
[@0ndorio]: https://github.com/0ndorio
|
[@0ndorio]: https://github.com/0ndorio
|
||||||
[@adamazing]: https://github.com/adamazing
|
[@adamazing]: https://github.com/adamazing
|
||||||
|
|
@ -460,6 +590,14 @@ Thanks!
|
||||||
[@Walker-00]: https://github.com/Walker-00
|
[@Walker-00]: https://github.com/Walker-00
|
||||||
[@YDX-2147483647]: https://github.com/YDX-2147483647
|
[@YDX-2147483647]: https://github.com/YDX-2147483647
|
||||||
[@zedseven]: https://github.com/zedseven
|
[@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.0.0]: https://github.com/tealdeer-rs/tealdeer/compare/v0.4.0...v1.0.0
|
||||||
[v1.1.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.0.0...v1.1.0
|
[v1.1.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.0.0...v1.1.0
|
||||||
|
|
@ -468,11 +606,16 @@ Thanks!
|
||||||
[v1.4.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.3.0...v1.4.0
|
[v1.4.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.3.0...v1.4.0
|
||||||
[v1.4.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.4.0...v1.4.1
|
[v1.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.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.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.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.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.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.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
|
[i34]: https://github.com/tealdeer-rs/tealdeer/issues/34
|
||||||
[i43]: https://github.com/tealdeer-rs/tealdeer/issues/43
|
[i43]: https://github.com/tealdeer-rs/tealdeer/issues/43
|
||||||
|
|
@ -544,6 +687,7 @@ Thanks!
|
||||||
[#300]: https://github.com/tealdeer-rs/tealdeer/pull/300
|
[#300]: https://github.com/tealdeer-rs/tealdeer/pull/300
|
||||||
[#303]: https://github.com/tealdeer-rs/tealdeer/pull/303
|
[#303]: https://github.com/tealdeer-rs/tealdeer/pull/303
|
||||||
[#305]: https://github.com/tealdeer-rs/tealdeer/pull/305
|
[#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
|
[#314]: https://github.com/tealdeer-rs/tealdeer/pull/314
|
||||||
[#315]: https://github.com/tealdeer-rs/tealdeer/pull/315
|
[#315]: https://github.com/tealdeer-rs/tealdeer/pull/315
|
||||||
[#322]: https://github.com/tealdeer-rs/tealdeer/pull/322
|
[#322]: https://github.com/tealdeer-rs/tealdeer/pull/322
|
||||||
|
|
@ -552,8 +696,29 @@ Thanks!
|
||||||
[#331]: https://github.com/tealdeer-rs/tealdeer/pull/331
|
[#331]: https://github.com/tealdeer-rs/tealdeer/pull/331
|
||||||
[#333]: https://github.com/tealdeer-rs/tealdeer/pull/333
|
[#333]: https://github.com/tealdeer-rs/tealdeer/pull/333
|
||||||
[#336]: https://github.com/tealdeer-rs/tealdeer/pull/336
|
[#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
|
[#342]: https://github.com/tealdeer-rs/tealdeer/pull/342
|
||||||
[#354]: https://github.com/tealdeer-rs/tealdeer/pull/354
|
[#354]: https://github.com/tealdeer-rs/tealdeer/pull/354
|
||||||
[#355]: https://github.com/tealdeer-rs/tealdeer/pull/355
|
[#355]: https://github.com/tealdeer-rs/tealdeer/pull/355
|
||||||
[#362]: https://github.com/tealdeer-rs/tealdeer/pull/362
|
[#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
|
[#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
|
||||||
|
|
|
||||||
1039
Cargo.lock
generated
1039
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
20
Cargo.toml
20
Cargo.toml
|
|
@ -9,11 +9,11 @@ license = "MIT OR Apache-2.0"
|
||||||
name = "tealdeer"
|
name = "tealdeer"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/tealdeer-rs/tealdeer/"
|
repository = "https://github.com/tealdeer-rs/tealdeer/"
|
||||||
documentation = "https://tealdeer-rs.github.io/tealdeer/"
|
documentation = "https://docs.tealdeer.org"
|
||||||
version = "1.7.2"
|
version = "1.8.1"
|
||||||
include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"]
|
include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"]
|
||||||
rust-version = "1.75"
|
rust-version = "1.88" # MSRV
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "tldr"
|
name = "tldr"
|
||||||
|
|
@ -21,17 +21,16 @@ path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
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 }
|
clap = { version = "4", features = ["std", "derive", "help", "usage", "cargo", "error-context", "color", "wrap_help"], default-features = false }
|
||||||
env_logger = { version = "0.11", optional = true }
|
env_logger = { version = "0.11", optional = true }
|
||||||
|
etcetera = "0.11.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
serde = "1.0.21"
|
serde = "1.0.21"
|
||||||
serde_derive = "1.0.21"
|
serde_derive = "1.0.21"
|
||||||
ureq = { version = "3.0.8", default-features = false, features = ["gzip"] }
|
ureq = { version = "3.0.8", default-features = false, features = ["gzip", "socks-proxy"] }
|
||||||
toml = "0.8.19"
|
toml = "1"
|
||||||
walkdir = "2.0.1"
|
|
||||||
yansi = "1"
|
yansi = "1"
|
||||||
zip = { version = "2.3.0", default-features = false, features = ["deflate"] }
|
zip = { version = "5.1.1", default-features = false, features = ["deflate"] }
|
||||||
|
|
||||||
[target.'cfg(not(windows))'.dependencies]
|
[target.'cfg(not(windows))'.dependencies]
|
||||||
pager = "0.16"
|
pager = "0.16"
|
||||||
|
|
@ -44,7 +43,8 @@ tempfile = "3.1.0"
|
||||||
filetime = "0.2.10"
|
filetime = "0.2.10"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["native-tls", "rustls-with-webpki-roots", "rustls-with-native-roots"]
|
# native-tls is not enabled by default, because it is difficult to build for musl
|
||||||
|
default = ["rustls-with-webpki-roots", "rustls-with-native-roots"]
|
||||||
logging = ["env_logger"]
|
logging = ["env_logger"]
|
||||||
|
|
||||||
# At least one of variants for `ureq` HTTP client must be selected.
|
# At least one of variants for `ureq` HTTP client must be selected.
|
||||||
|
|
|
||||||
51
README.md
51
README.md
|
|
@ -19,7 +19,7 @@ binaries on the [GitHub releases page](https://github.com/tealdeer-rs/tealdeer/r
|
||||||
|
|
||||||
## Docs (Installing, Usage, Configuration)
|
## Docs (Installing, Usage, Configuration)
|
||||||
|
|
||||||
User documentation is available at <https://tealdeer-rs.github.io/tealdeer/>!
|
User documentation is available at <https://docs.tealdeer.org>!
|
||||||
|
|
||||||
The docs are generated using [mdbook](https://rust-lang.github.io/mdBook/index.html).
|
The docs are generated using [mdbook](https://rust-lang.github.io/mdBook/index.html).
|
||||||
They can be edited through the markdown files in the `docs/src/` directory.
|
They can be edited through the markdown files in the `docs/src/` directory.
|
||||||
|
|
@ -31,7 +31,6 @@ High level project goals:
|
||||||
|
|
||||||
- [x] Download and cache pages
|
- [x] Download and cache pages
|
||||||
- [x] Don't require a network connection for anything besides updating the cache
|
- [x] Don't require a network connection for anything besides updating the cache
|
||||||
- [x] Command line interface similar or equivalent to the [NodeJS client][node-gh]
|
|
||||||
- [x] Comply with the [tldr client specification][client-spec]
|
- [x] Comply with the [tldr client specification][client-spec]
|
||||||
- [x] Advanced highlighting and configuration
|
- [x] Advanced highlighting and configuration
|
||||||
- [x] Be fast
|
- [x] Be fast
|
||||||
|
|
@ -39,29 +38,6 @@ High level project goals:
|
||||||
A tool like `tldr` should be as frictionless as possible to use and show the
|
A tool like `tldr` should be as frictionless as possible to use and show the
|
||||||
output as fast as possible.
|
output as fast as possible.
|
||||||
|
|
||||||
We think that `tealdeer` reaches these goals. We put together a (more or less)
|
|
||||||
reproducible benchmark that compiles a handful of clients from source and
|
|
||||||
measures the execution times on a cold disk cache. The benchmarking is run in a
|
|
||||||
Docker container using sharkdp's [`hyperfine`][hyperfine-gh]
|
|
||||||
([Dockerfile][benchmark-dockerfile]).
|
|
||||||
|
|
||||||
| Client (50 runs, 17.10.2021) | Programming Language | Mean in ms | Deviation in ms | Comments |
|
|
||||||
| :---: | :---: | :---: | :---: | :---: |
|
|
||||||
| [`outfieldr`][outfieldr-gh] | Zig | 9.1 | 0.5 | no user configuration |
|
|
||||||
| `tealdeer` | Rust | 13.2 | 0.5 | |
|
|
||||||
| [`fast-tldr`][fast-tldr-gh] | Haskell | 17.0 | 0.6 | no example highlighting |
|
|
||||||
| [`tldr-hs`][hs-gh] | Haskell | 25.1 | 0.5 | no example highlighting |
|
|
||||||
| [`tldr-bash`][bash-gh] | Bash | 30.0 | 0.8 | |
|
|
||||||
| [`tldr-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 | |
|
|
||||||
|
|
||||||
As you can see, `tealdeer` is one of the fastest of the tested clients.
|
|
||||||
However, we strive for useful features and code quality over raw performance,
|
|
||||||
even if that means that we don't come out on top in this friendly competition.
|
|
||||||
That said, we are still optimizing the code, for example when the `outfieldr`
|
|
||||||
developers [suggested to switch][outfieldr-comment-tls] to a native TLS
|
|
||||||
implementation instead of the native libraries.
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|
@ -87,10 +63,22 @@ To run lints:
|
||||||
$ cargo clean && cargo clippy
|
$ 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)
|
## MSRV (Minimally Supported Rust Version)
|
||||||
|
|
||||||
When publishing a tealdeer release, the Rust version required to build it
|
When publishing a tealdeer release, the Rust version required to build it
|
||||||
should be stable for at least a month.
|
should be stable for at least a month. The current MSRV can always be found in
|
||||||
|
the `rust-version` field in `Cargo.toml`.
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
@ -112,18 +100,7 @@ be dual licensed as above, without any additional terms or conditions.
|
||||||
Thanks to @severen for coming up with the name "tealdeer"!
|
Thanks to @severen for coming up with the name "tealdeer"!
|
||||||
|
|
||||||
|
|
||||||
[node-gh]: https://github.com/tldr-pages/tldr-node-client
|
|
||||||
[c-gh]: https://github.com/tldr-pages/tldr-c-client
|
|
||||||
[hs-gh]: https://github.com/psibi/tldr-hs
|
|
||||||
[fast-tldr-gh]: https://github.com/gutjuri/fast-tldr
|
|
||||||
[bash-gh]: https://4e4.win/tldr
|
|
||||||
[outfieldr-gh]: https://gitlab.com/ve-nt/outfieldr
|
|
||||||
[python-gh]: https://github.com/tldr-pages/tldr-python-client
|
|
||||||
|
|
||||||
[benchmark-dockerfile]: https://github.com/tealdeer-rs/tealdeer/blob/main/benchmarks/Dockerfile
|
|
||||||
[client-spec]: https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md
|
[client-spec]: https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md
|
||||||
[hyperfine-gh]: https://github.com/sharkdp/hyperfine
|
|
||||||
[outfieldr-comment-tls]: https://github.com/tealdeer-rs/tealdeer/issues/129#issuecomment-833596765
|
|
||||||
|
|
||||||
<!-- Badges -->
|
<!-- Badges -->
|
||||||
[github-actions]: https://github.com/tealdeer-rs/tealdeer/actions?query=branch%3Amain
|
[github-actions]: https://github.com/tealdeer-rs/tealdeer/actions?query=branch%3Amain
|
||||||
|
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
# Benchmark Dockerfile for tealdeer
|
|
||||||
#
|
|
||||||
# To run the benchmarks, execute
|
|
||||||
#
|
|
||||||
# docker build --pull -t tldr-benchmark .
|
|
||||||
# docker run --privileged --rm -it tldr-benchmark
|
|
||||||
#
|
|
||||||
# as root in the directory of this Dockerfile. This will build the compared
|
|
||||||
# clients and benchmark them with `hyperfine` at the end.
|
|
||||||
#
|
|
||||||
# The `--privileged` flag is needed to drop the disk caches before every run. If
|
|
||||||
# you want to test with hot caches or don't want to use this flag, you will have
|
|
||||||
# to remove the `--prepare` line from the `hyperfine` command at the end of this
|
|
||||||
# file and rebuild the image.
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
FROM rust AS tealdeer-builder
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
RUN git clone https://github.com/tealdeer-rs/tealdeer.git \
|
|
||||||
&& cd tealdeer \
|
|
||||||
&& cargo build --release \
|
|
||||||
&& mkdir /build-outputs \
|
|
||||||
&& cp target/release/tldr /build-outputs/tealdeer
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
FROM ubuntu:latest AS tldr-c-builder
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
RUN apt-get update && apt-get install -y build-essential git && rm -rf /var/lib/apt/lists/*
|
|
||||||
RUN git clone https://github.com/tldr-pages/tldr-c-client.git \
|
|
||||||
&& cd tldr-c-client \
|
|
||||||
&& DEBIAN_FRONTEND=noninteractive ./deps.sh \
|
|
||||||
&& make \
|
|
||||||
&& mkdir /build-outputs /deps \
|
|
||||||
&& cp tldr /build-outputs/tldr-c \
|
|
||||||
&& cp deps.sh /deps/tldr-c-deps.sh
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
FROM haskell AS haskell-builder
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
RUN git clone https://github.com/psibi/tldr-hs.git \
|
|
||||||
&& cd tldr-hs \
|
|
||||||
&& stack build --install-ghc
|
|
||||||
|
|
||||||
RUN git clone https://github.com/gutjuri/fast-tldr \
|
|
||||||
&& cd fast-tldr \
|
|
||||||
&& stack build --install-ghc
|
|
||||||
|
|
||||||
RUN mkdir /build-outputs \
|
|
||||||
&& find tldr-hs/.stack-work/dist -type f -iname tldr -exec mv '{}' /build-outputs/tldr-hs \; \
|
|
||||||
&& find fast-tldr/.stack-work/dist -type f -iname tldr -exec mv '{}' /build-outputs/fast-tldr \;
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
FROM node:slim AS node-builder
|
|
||||||
|
|
||||||
WORKDIR /build-outputs
|
|
||||||
RUN npm install tldr \
|
|
||||||
&& cp $(which node) . \
|
|
||||||
&& echo './node -- ./node_modules/.bin/tldr "$@"' > tldr-node \
|
|
||||||
&& chmod +x tldr-node
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
FROM euantorano/zig:0.8.0 AS zig-builder
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
RUN apk add git \
|
|
||||||
&& git clone https://gitlab.com/ve-nt/outfieldr.git \
|
|
||||||
&& cd outfieldr \
|
|
||||||
&& git submodule init \
|
|
||||||
&& git submodule update \
|
|
||||||
&& zig build -Drelease-safe \
|
|
||||||
&& mkdir /build-outputs \
|
|
||||||
&& cp bin/tldr /build-outputs/outfieldr
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
FROM ubuntu:latest AS benchmark
|
|
||||||
|
|
||||||
ENV LANG="en_US.UTF-8"
|
|
||||||
|
|
||||||
WORKDIR /deps
|
|
||||||
RUN apt-get update && apt-get install -y wget unzip python3 python3-venv && rm -rf /var/lib/apt/lists/*
|
|
||||||
COPY --from=tldr-c-builder /deps/* ./
|
|
||||||
RUN for file in *; do DEBIAN_FRONTEND=noninteractive sh $file; done
|
|
||||||
|
|
||||||
WORKDIR /clients
|
|
||||||
COPY --from=tealdeer-builder /build-outputs/* ./
|
|
||||||
COPY --from=tldr-c-builder /build-outputs/* ./
|
|
||||||
COPY --from=haskell-builder /build-outputs/* ./
|
|
||||||
RUN wget -qO tldr-bash https://4e4.win/tldr && chmod +x tldr-bash
|
|
||||||
COPY --from=node-builder /build-outputs/node /build-outputs/tldr-node ./
|
|
||||||
COPY --from=node-builder /build-outputs/node_modules/ ./node_modules/
|
|
||||||
COPY --from=zig-builder /build-outputs/* ./
|
|
||||||
|
|
||||||
# python is really hard to isolate in a package, using pyinstaller didn't really work either, so for now we just use it like this
|
|
||||||
RUN python3 -m venv tldr-python \
|
|
||||||
&& cd tldr-python \
|
|
||||||
&& bash -c 'source bin/activate; pip install wheel; pip install tldr; deactivate' \
|
|
||||||
&& cd .. \
|
|
||||||
&& echo '#!/bin/bash' > tldr-python.bash \
|
|
||||||
&& echo 'source tldr-python/bin/activate; tldr $@' >> tldr-python.bash \
|
|
||||||
&& chmod +x tldr-python.bash
|
|
||||||
|
|
||||||
# Update all the individual caches
|
|
||||||
RUN bash -c 'mkdir -p /caches/{tealdeer,tldr-c,tldr-hs,fast-tldr,tldr-bash,tldr-node,tldr-python,outfieldr/.local/share}' \
|
|
||||||
&& TEALDEER_CACHE_DIR=/caches/tealdeer ./tealdeer -u \
|
|
||||||
&& TLDR_CACHE_DIR=/caches/tldr-c ./tldr-c -u \
|
|
||||||
&& XDG_DATA_HOME=/caches/tldr-hs ./tldr-hs -u \
|
|
||||||
&& XDG_DATA_HOME=/caches/fast-tldr ./fast-tldr -u \
|
|
||||||
&& XDG_DATA_HOME=/caches/tldr-bash ./tldr-bash -u \
|
|
||||||
&& HOME=/caches/tldr-node ./tldr-node -u \
|
|
||||||
&& HOME=/caches/tldr-python ./tldr-python.bash -u \
|
|
||||||
&& HOME=/caches/outfieldr ./outfieldr -u
|
|
||||||
|
|
||||||
WORKDIR /tools
|
|
||||||
RUN wget -q https://github.com/sharkdp/hyperfine/releases/download/v1.11.0/hyperfine_1.11.0_amd64.deb && dpkg -i hyperfine_1.11.0_amd64.deb
|
|
||||||
|
|
||||||
ENV PAGE="tar"
|
|
||||||
WORKDIR /clients
|
|
||||||
CMD hyperfine \
|
|
||||||
--warmup 10 \
|
|
||||||
--runs 50 \
|
|
||||||
--prepare 'sync; echo 3 | tee /proc/sys/vm/drop_caches' \
|
|
||||||
"TEALDEER_CACHE_DIR=/caches/tealdeer ./tealdeer $PAGE" \
|
|
||||||
"TLDR_CACHE_DIR=/caches/tldr-c ./tldr-c $PAGE" \
|
|
||||||
"XDG_DATA_HOME=/caches/tldr-hs ./tldr-hs $PAGE" \
|
|
||||||
"XDG_DATA_HOME=/caches/fast-tldr ./fast-tldr $PAGE" \
|
|
||||||
"XDG_DATA_HOME=/caches/tldr-bash TLDR_LESS=0 ./tldr-bash $PAGE" \
|
|
||||||
"HOME=/caches/tldr-python ./tldr-python.bash $PAGE" \
|
|
||||||
"HOME=/caches/outfieldr ./outfieldr $PAGE" \
|
|
||||||
"HOME=/caches/tldr-node ./tldr-node $PAGE"
|
|
||||||
|
|
@ -3,21 +3,27 @@
|
||||||
# https://github.com/tealdeer-rs/tealdeer/
|
# https://github.com/tealdeer-rs/tealdeer/
|
||||||
#
|
#
|
||||||
|
|
||||||
complete -c tldr -s h -l help -d 'Print the help message.' -f
|
complete -c tldr -s h -l help -d 'Print the help message' -f
|
||||||
complete -c tldr -s v -l version -d 'Show version information.' -f
|
complete -c tldr -s v -l version -d 'Show version information' -f
|
||||||
complete -c tldr -s l -l list -d 'List all commands in the cache.' -f
|
complete -c tldr -s l -l list -d 'List all commands in the cache' -f
|
||||||
complete -c tldr -s f -l render -d 'Render a specific markdown file.' -r
|
complete -c tldr -l edit-page -d 'Edit custom page with `EDITOR`' -f
|
||||||
complete -c tldr -s p -l platform -d 'Override the operating system.' -xa 'linux macos sunos windows android freebsd netbsd openbsd'
|
complete -c tldr -l edit-patch -d 'Edit custom patch with `EDITOR`' -f
|
||||||
complete -c tldr -s L -l language -d 'Override the language' -x
|
complete -c tldr -s f -l render -d 'Render a specific markdown file' -r
|
||||||
complete -c tldr -s u -l update -d 'Update the local cache.' -f
|
complete -c tldr -s p -l platform -d 'Override the operating system' -xa 'linux macos sunos windows android freebsd netbsd openbsd common'
|
||||||
complete -c tldr -l no-auto-update -d 'If auto update is configured, disable it for this run.' -f
|
complete -c tldr -s L -l language -d 'Override the language' -x
|
||||||
complete -c tldr -s c -l clear-cache -d 'Clear the local cache.' -f
|
complete -c tldr -s u -l update -d 'Update the local cache' -f
|
||||||
complete -c tldr -l pager -d 'Use a pager to page output.' -f
|
complete -c tldr -l no-auto-update -d 'If auto update is configured, disable it for this run' -f
|
||||||
complete -c tldr -s r -l raw -d 'Display the raw markdown instead of rendering it.' -f
|
complete -c tldr -s c -l clear-cache -d 'Clear the local cache' -f
|
||||||
complete -c tldr -s q -l quiet -d 'Suppress informational messages.' -f
|
complete -c tldr -s L -l config-path -d 'Override config file location' -r
|
||||||
complete -c tldr -l show-paths -d 'Show file and directory paths used by tealdeer.' -f
|
complete -c tldr -s L -l override-config -d 'Override config values after reading config file' -x
|
||||||
complete -c tldr -l seed-config -d 'Create a basic config.' -f
|
complete -c tldr -l pager -d 'Use a pager to page output' -f
|
||||||
complete -c tldr -l color -d 'Controls when to use color.' -xa 'always auto never'
|
complete -c tldr -s r -l raw -d 'Display the raw markdown instead of rendering it' -f
|
||||||
|
complete -c tldr -s q -l quiet -d 'Suppress informational messages' -f
|
||||||
|
complete -c tldr -l show-paths -d 'Show file and directory paths used by tealdeer' -f
|
||||||
|
complete -c tldr -l seed-config -d 'Create a basic config' -f
|
||||||
|
complete -c tldr -l color -d 'Controls when to use color' -xa 'always auto never'
|
||||||
|
complete -c tldr -l short-options -d 'Display the short variants of placeholders' -f
|
||||||
|
complete -c tldr -l long-options -d 'Display the long variants of placeholders' -f
|
||||||
|
|
||||||
function __tealdeer_entries
|
function __tealdeer_entries
|
||||||
if set entries (tldr --list 2>/dev/null)
|
if set entries (tldr --list 2>/dev/null)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ _tealdeer() {
|
||||||
|
|
||||||
args+=(
|
args+=(
|
||||||
"($I -l --list)"{-l,--list}"[List all commands in the cache]"
|
"($I -l --list)"{-l,--list}"[List all commands in the cache]"
|
||||||
|
"($I)--edit-page[Edit custom page with EDITOR]"
|
||||||
|
"($I)--edit-patch[Edit custom patch with EDITOR]"
|
||||||
"($I -f --render)"{-f,--render}"[Render a specific markdown file]:file:_files"
|
"($I -f --render)"{-f,--render}"[Render a specific markdown file]:file:_files"
|
||||||
"($I -p --platform)"{-p,--platform}'[Override the operating system]:platform:((
|
"($I -p --platform)"{-p,--platform}'[Override the operating system]:platform:((
|
||||||
linux
|
linux
|
||||||
|
|
@ -24,11 +26,14 @@ _tealdeer() {
|
||||||
freebsd
|
freebsd
|
||||||
netbsd
|
netbsd
|
||||||
openbsd
|
openbsd
|
||||||
|
common
|
||||||
))'
|
))'
|
||||||
"($I -L --language)"{-L,--language}"[Override the language settings]:lang"
|
"($I -L --language)"{-L,--language}"[Override the language settings]:lang"
|
||||||
"($I -u --update)"{-u,--update}"[Update the local cache]"
|
"($I -u --update)"{-u,--update}"[Update the local cache]"
|
||||||
"($I)--no-auto-update[If auto update is configured, disable it for this run]"
|
"($I)--no-auto-update[If auto update is configured, disable it for this run]"
|
||||||
"($I -c --clear-cache)"{-c,--clear-cache}"[Clear the local cache]"
|
"($I -c --clear-cache)"{-c,--clear-cache}"[Clear the local cache]"
|
||||||
|
"($I)--config-path[Override config file location]"
|
||||||
|
"($I)--override-config[Override config values after reading config file]"
|
||||||
"($I)--pager[Use a pager to page output]"
|
"($I)--pager[Use a pager to page output]"
|
||||||
"($I -r --raw)"{-r,--raw}"[Display the raw markdown instead of rendering it]"
|
"($I -r --raw)"{-r,--raw}"[Display the raw markdown instead of rendering it]"
|
||||||
"($I -q --quiet)"{-q,--quiet}"[Suppress informational messages]"
|
"($I -q --quiet)"{-q,--quiet}"[Suppress informational messages]"
|
||||||
|
|
@ -39,6 +44,8 @@ _tealdeer() {
|
||||||
auto
|
auto
|
||||||
never
|
never
|
||||||
))"
|
))"
|
||||||
|
"($I)--short-options[Display the short variants of placeholders]"
|
||||||
|
"($I)--long-options[Display the long variants of placeholders]"
|
||||||
'(- *)'{-h,--help}'[Display help]'
|
'(- *)'{-h,--help}'[Display help]'
|
||||||
'(- *)'{-v,--version}'[Show version information]'
|
'(- *)'{-v,--version}'[Show version information]'
|
||||||
'1: :_applications'
|
'1: :_applications'
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
[book]
|
[book]
|
||||||
authors = ["Danilo Bargen"]
|
authors = ["Danilo Bargen", "Niklas Mohrin"]
|
||||||
language = "en"
|
language = "en"
|
||||||
multilingual = false
|
|
||||||
src = "src"
|
src = "src"
|
||||||
title = "Tealdeer User Manual"
|
title = "Tealdeer User Manual"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
- [Configuration](./config.md)
|
- [Configuration](./config.md)
|
||||||
- [Section: \[display\]](./config_display.md)
|
- [Section: \[display\]](./config_display.md)
|
||||||
- [Section: \[style\]](./config_style.md)
|
- [Section: \[style\]](./config_style.md)
|
||||||
|
- [Section: \[search\]](./config_search.md)
|
||||||
- [Section: \[updates\]](./config_updates.md)
|
- [Section: \[updates\]](./config_updates.md)
|
||||||
- [Section: \[directories\]](./config_directories.md)
|
- [Section: \[directories\]](./config_directories.md)
|
||||||
- [Tips and Tricks](./tips_and_tricks.md)
|
- [Tips and Tricks](./tips_and_tricks.md)
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,15 @@ The configuration file path follows OS conventions (e.g.
|
||||||
`$XDG_CONFIG_HOME/tealdeer/config.toml` on Linux). The paths can be queried
|
`$XDG_CONFIG_HOME/tealdeer/config.toml` on Linux). The paths can be queried
|
||||||
with the following command:
|
with the following command:
|
||||||
|
|
||||||
$ tldr --show-paths
|
```shell
|
||||||
|
$ tldr --show-paths
|
||||||
|
```
|
||||||
|
|
||||||
Creating the config file can be done manually or with the help of `tldr`:
|
Creating the config file can be done manually or with the help of `tldr`:
|
||||||
|
|
||||||
$ tldr --seed-config
|
```shell
|
||||||
|
$ tldr --seed-config
|
||||||
|
```
|
||||||
|
|
||||||
On Linux, this will usually be `~/.config/tealdeer/config.toml`.
|
On Linux, this will usually be `~/.config/tealdeer/config.toml`.
|
||||||
|
|
||||||
|
|
@ -22,13 +26,14 @@ On Linux, this will usually be `~/.config/tealdeer/config.toml`.
|
||||||
Here's an example configuration file. Note that this example does not contain
|
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,
|
all possible config options. For details on the things that can be configured,
|
||||||
please refer to the subsections of this documentation page
|
please refer to the subsections of this documentation page
|
||||||
([display](config_display.html), [style](config_style.html),
|
([display](config_display.html), [style](config_style.html), [search](config_search.html),
|
||||||
[updates](config_updates.html) or [directories](config_directories.html)).
|
[updates](config_updates.html) or [directories](config_directories.html)).
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[display]
|
[display]
|
||||||
compact = false
|
compact = false
|
||||||
use_pager = true
|
use_pager = true
|
||||||
|
show_title = false
|
||||||
|
|
||||||
[style.command_name]
|
[style.command_name]
|
||||||
foreground = "red"
|
foreground = "red"
|
||||||
|
|
@ -52,3 +57,16 @@ auto_update = true
|
||||||
The directory where the configuration file resides may be overwritten by the
|
The directory where the configuration file resides may be overwritten by the
|
||||||
environment variable `TEALDEER_CONFIG_DIR`. Remember to use an absolute path.
|
environment variable `TEALDEER_CONFIG_DIR`. Remember to use an absolute path.
|
||||||
Variable expansion will not be performed on the path.
|
Variable expansion will not be performed on the path.
|
||||||
|
|
||||||
|
## Override Config Values
|
||||||
|
|
||||||
|
Individual config values can be overridden using the `--override-config` command
|
||||||
|
line argument. The overrides take place after reading the user config file, but
|
||||||
|
before the raw config is evaluated.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ tldr --override-config "display.compact = true" tealdeer
|
||||||
|
```
|
||||||
|
|
||||||
|
Each override is of the form `<name> = <value>` where `name` is a config key and
|
||||||
|
`value` is any TOML value.
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ 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
|
expansion will not be performed on the path. If the directory does not yet
|
||||||
exist, it will be created.
|
exist, it will be created.
|
||||||
|
|
||||||
[directories]
|
```toml
|
||||||
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
|
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/`.
|
follows OS conventions. On Linux, it will usually be at `~/.cache/tealdeer/`.
|
||||||
|
|
@ -21,5 +23,7 @@ Set the directory to be used to look up [custom
|
||||||
pages](usage_custom_pages.html). Remember to use an absolute path. Variable
|
pages](usage_custom_pages.html). Remember to use an absolute path. Variable
|
||||||
expansion will not be performed on the path.
|
expansion will not be performed on the path.
|
||||||
|
|
||||||
[directories]
|
```toml
|
||||||
custom_pages_dir = "/home/myuser/custom-tldr-pages/"
|
[directories]
|
||||||
|
custom_pages_dir = "/home/myuser/custom-tldr-pages/"
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ In the `display` section you can configure the output format.
|
||||||
|
|
||||||
Specifies whether the pager should be used by default or not (default `false`).
|
Specifies whether the pager should be used by default or not (default `false`).
|
||||||
|
|
||||||
[display]
|
```toml
|
||||||
use_pager = true
|
[display]
|
||||||
|
use_pager = true
|
||||||
|
```
|
||||||
|
|
||||||
When enabled, `less -R` is used as pager. To override the pager command used,
|
When enabled, `less -R` is used as pager. To override the pager command used,
|
||||||
set the `PAGER` environment variable.
|
set the `PAGER` environment variable.
|
||||||
|
|
@ -19,5 +21,68 @@ NOTE: This feature is not available on Windows.
|
||||||
Set this to enforce more compact output, where empty lines are stripped out
|
Set this to enforce more compact output, where empty lines are stripped out
|
||||||
(default `false`).
|
(default `false`).
|
||||||
|
|
||||||
[display]
|
```toml
|
||||||
compact = true
|
[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,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `placeholder_format`
|
||||||
|
|
||||||
|
Display the short and/or long variants of placeholders, if available.
|
||||||
|
Possible values: `"short"`, `"long"`, or `"both"` (default `"long"`).
|
||||||
|
This behavior can be overridden with the `--short-options` and `--long-options` flags.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[display]
|
||||||
|
# Display only short variants
|
||||||
|
placeholder_format = "short"
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, when displaying the builtin page with `tldr tealdeer`, the `-f` / `--render` flag is displayed as follows:
|
||||||
|
- `-f`, if `placeholder_format = "short"`
|
||||||
|
- `--render`, if `placeholder_format = "long"`
|
||||||
|
- `[-f|--render]`, if `placeholder_format = "both"`
|
||||||
|
|
|
||||||
33
docs/src/config_search.md
Normal file
33
docs/src/config_search.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# 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"]
|
||||||
|
```
|
||||||
|
|
@ -10,7 +10,7 @@ Using the config file, the style (e.g. colors or underlines) can be customized.
|
||||||
- `command_name`: The command name as part of the example code
|
- `command_name`: The command name as part of the example code
|
||||||
- `example_text`: The text that describes an example
|
- `example_text`: The text that describes an example
|
||||||
- `example_code`: The example itself (except the `command_name` and `example_variable`)
|
- `example_code`: The example itself (except the `command_name` and `example_variable`)
|
||||||
- `example_variable`: The variables in the example
|
- `example_variable`: The variables (placeholders) in the example
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
|
|
@ -26,16 +26,22 @@ Colors can be specified in one of three ways:
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
foreground = "green"
|
```toml
|
||||||
|
foreground = "green"
|
||||||
|
```
|
||||||
|
|
||||||
- 256 color ANSI code (*tealdeer v1.5.0+*)
|
- 256 color ANSI code (*tealdeer v1.5.0+*)
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
foreground = { ansi = 4 }
|
```toml
|
||||||
|
foreground = { ansi = 4 }
|
||||||
|
```
|
||||||
|
|
||||||
- 24-bit RGB color (*tealdeer v1.5.0+*)
|
- 24-bit RGB color (*tealdeer v1.5.0+*)
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
background = { rgb = { r = 255, g = 255, b = 255 } }
|
```toml
|
||||||
|
background = { rgb = { r = 255, g = 255, b = 255 } }
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
# Section: \[updates\]
|
# Section: \[updates\]
|
||||||
|
|
||||||
|
This config section contains settings related to updating the tealdeer cache.
|
||||||
|
|
||||||
## Automatic updates
|
## Automatic updates
|
||||||
|
|
||||||
Tealdeer can refresh the cache automatically when it is outdated. This
|
Tealdeer can refresh the cache automatically when it is outdated. This
|
||||||
|
|
@ -11,8 +13,10 @@ default.
|
||||||
Specifies whether the auto-update feature should be enabled (defaults to
|
Specifies whether the auto-update feature should be enabled (defaults to
|
||||||
`false`).
|
`false`).
|
||||||
|
|
||||||
[updates]
|
```toml
|
||||||
auto_update = true
|
[updates]
|
||||||
|
auto_update = true
|
||||||
|
```
|
||||||
|
|
||||||
### `auto_update_interval_hours`
|
### `auto_update_interval_hours`
|
||||||
|
|
||||||
|
|
@ -20,17 +24,51 @@ Duration, since the last cache update, after which the cache will be
|
||||||
refreshed (defaults to 720 hours). This parameter is ignored if `auto_update`
|
refreshed (defaults to 720 hours). This parameter is ignored if `auto_update`
|
||||||
is set to `false`.
|
is set to `false`.
|
||||||
|
|
||||||
[updates]
|
```toml
|
||||||
auto_update = true
|
[updates]
|
||||||
auto_update_interval_hours = 24
|
auto_update = true
|
||||||
|
auto_update_interval_hours = 24
|
||||||
|
```
|
||||||
|
|
||||||
### archive_source
|
### `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
|
URL for the location of the tldr pages archive. By default the pages are
|
||||||
fetched from the latest `tldr-pages/tldr` GitHub release.
|
fetched from the latest `tldr-pages/tldr` GitHub release.
|
||||||
|
|
||||||
[updates]
|
```toml
|
||||||
archive_source = https://my-company.example.com/tldr/
|
[updates]
|
||||||
|
archive_source = "https://my-company.example.com/tldr/"
|
||||||
|
```
|
||||||
|
|
||||||
### `tls_backend`
|
### `tls_backend`
|
||||||
|
|
||||||
|
|
@ -44,9 +82,10 @@ Available options:
|
||||||
- Secure Transport on macOS
|
- Secure Transport on macOS
|
||||||
- OpenSSL on other platforms
|
- OpenSSL on other platforms
|
||||||
|
|
||||||
[updates]
|
```toml
|
||||||
tls_backend = "native-tls"
|
[updates]
|
||||||
|
tls_backend = "native-tls"
|
||||||
|
```
|
||||||
|
|
||||||
[rustls]: https://github.com/rustls/rustls
|
[rustls]: https://github.com/rustls/rustls
|
||||||
[rustls-webpki]: https://github.com/rustls/webpki
|
[rustls-webpki]: https://github.com/rustls/webpki
|
||||||
|
|
|
||||||
|
|
@ -38,21 +38,29 @@ Simply download the binary for your platform and run it!
|
||||||
|
|
||||||
Build and install the tool via cargo...
|
Build and install the tool via cargo...
|
||||||
|
|
||||||
$ cargo install tealdeer
|
```shell
|
||||||
|
$ cargo install tealdeer
|
||||||
|
```
|
||||||
|
|
||||||
## Build From Source
|
## Build From Source
|
||||||
|
|
||||||
Release build:
|
Release build:
|
||||||
|
|
||||||
$ cargo build --release
|
```shell
|
||||||
|
$ cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
Release build with bundled CA roots:
|
Release build with native TLS support:
|
||||||
|
|
||||||
$ cargo build --release --no-default-features --features rustls-with-webpki-roots
|
```shell
|
||||||
|
$ cargo build --release --features native-tls
|
||||||
|
```
|
||||||
|
|
||||||
Debug build with logging support:
|
Debug build with logging support:
|
||||||
|
|
||||||
$ cargo build --features logging
|
```shell
|
||||||
|
$ cargo build --features logging
|
||||||
|
```
|
||||||
|
|
||||||
(To enable logging at runtime, export the `RUST_LOG=tldr=debug` env variable.)
|
(To enable logging at runtime, export the `RUST_LOG=tldr=debug` env variable.)
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 165 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 153 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
tealdeer 1.7.2: A fast TLDR client
|
tealdeer 1.8.1: A fast TLDR client
|
||||||
Danilo Bargen <mail@dbrgn.ch>, Niklas Mohrin <dev@niklasmohrin.de>
|
Danilo Bargen <mail@dbrgn.ch>, Niklas Mohrin <dev@niklasmohrin.de>
|
||||||
|
|
||||||
Usage: tldr [OPTIONS] [COMMAND]...
|
Usage: tldr [OPTIONS] [COMMAND]...
|
||||||
|
|
@ -7,24 +7,32 @@ Arguments:
|
||||||
[COMMAND]... The command to show (e.g. `tar` or `git log`)
|
[COMMAND]... The command to show (e.g. `tar` or `git log`)
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-l, --list List all commands in the cache
|
-l, --list List all commands in the cache
|
||||||
--edit-page Edit custom page with `EDITOR`
|
--edit-page Edit custom page with `EDITOR`
|
||||||
--edit-patch Edit custom patch with `EDITOR`
|
--edit-patch Edit custom patch with `EDITOR`
|
||||||
-f, --render <FILE> Render a specific markdown file
|
-f, --render <FILE> Render a specific markdown file
|
||||||
-p, --platform <PLATFORM> Override the operating system, can be specified multiple times in order
|
-p, --platform <PLATFORM> Override the operating system, can be specified multiple times
|
||||||
of preference [possible values: linux, macos, sunos, windows, android,
|
in order of preference [possible values: linux, macos, sunos,
|
||||||
freebsd, netbsd, openbsd, common]
|
windows, android, freebsd, netbsd, openbsd, common]
|
||||||
-L, --language <LANGUAGE> Override the language
|
-L, --language <LANGUAGE> Override the language
|
||||||
-u, --update Update the local cache
|
-u, --update Update the local cache
|
||||||
--no-auto-update If auto update is configured, disable it for this run
|
--no-auto-update If auto update is configured, disable it for this run
|
||||||
-c, --clear-cache Clear the local cache
|
-c, --clear-cache Clear the local cache
|
||||||
--pager Use a pager to page output
|
--config-path <FILE> Override config file location
|
||||||
-r, --raw Display the raw markdown instead of rendering it
|
--override-config <OVERRIDE> Override config values after reading config file (example:
|
||||||
-q, --quiet Suppress informational messages
|
`updates.auto_update = true`)
|
||||||
--show-paths Show file and directory paths used by tealdeer
|
--pager Use a pager to page output
|
||||||
--seed-config Create a basic config
|
-r, --raw Display the raw markdown instead of rendering it
|
||||||
--color <WHEN> Control whether to use color [possible values: always, auto, never]
|
-q, --quiet Suppress informational messages
|
||||||
-v, --version Print the version
|
--show-paths Show file and directory paths used by tealdeer
|
||||||
-h, --help Print help
|
--seed-config Create a basic config
|
||||||
|
--color <WHEN> Control whether to use color [possible values: always, auto,
|
||||||
|
never]
|
||||||
|
--short-options Display the short variants of placeholders
|
||||||
|
--long-options Display the long variants of placeholders
|
||||||
|
-v, --version Print the version
|
||||||
|
-h, --help Print help
|
||||||
|
|
||||||
To view the user documentation, please visit https://tealdeer-rs.github.io/tealdeer/.
|
To view the user documentation, please visit https://docs.tealdeer.org.
|
||||||
|
|
||||||
|
To view usage examples, run tldr tldr or tldr tealdeer.
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,15 @@ your custom page will be shown instead of the upstream version in the cache.
|
||||||
|
|
||||||
Path:
|
Path:
|
||||||
|
|
||||||
$CUSTOM_PAGES_DIR/<command>.page.md
|
```plain
|
||||||
|
$CUSTOM_PAGES_DIR/<command>.page.md
|
||||||
|
```
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
~/.local/share/tealdeer/pages/ufw.page.md
|
```plain
|
||||||
|
~/.local/share/tealdeer/pages/ufw.page.md
|
||||||
|
```
|
||||||
|
|
||||||
## Custom Patches
|
## Custom Patches
|
||||||
|
|
||||||
|
|
@ -43,8 +47,12 @@ pages.
|
||||||
|
|
||||||
Path:
|
Path:
|
||||||
|
|
||||||
$CUSTOM_PAGES_DIR/<command>.patch.md
|
```plain
|
||||||
|
$CUSTOM_PAGES_DIR/<command>.patch.md
|
||||||
|
```
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
~/.local/share/tealdeer/pages/ufw.patch.md
|
```plain
|
||||||
|
~/.local/share/tealdeer/pages/ufw.patch.md
|
||||||
|
```
|
||||||
|
|
|
||||||
42
pages/tealdeer.md
Normal file
42
pages/tealdeer.md
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# tldr
|
||||||
|
|
||||||
|
> This is a builtin page that shows information for your installed tealdeer version.
|
||||||
|
> More information: <https://docs.tealdeer.org>.
|
||||||
|
|
||||||
|
> This page shows tealdeer specific functionality. See tldr tldr for more examples.
|
||||||
|
|
||||||
|
- Render a local markdown file as a tldr page:
|
||||||
|
|
||||||
|
`tldr {{[-f|--render]}} {{path/to/file.md}}`
|
||||||
|
|
||||||
|
- Show the raw markdown source of a page instead of rendering it:
|
||||||
|
|
||||||
|
`tldr {{[-r|--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 {{[-c|--clear-cache]}}`
|
||||||
|
|
||||||
|
- If auto update is configured, disable it for this run:
|
||||||
|
|
||||||
|
`tldr --no-auto-update`
|
||||||
8
scripts/get-mdbook.sh
Executable file
8
scripts/get-mdbook.sh
Executable file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
wget -O mdbook.tar.gz https://github.com/rust-lang/mdBook/releases/download/v0.5.4/mdbook-v0.5.4-x86_64-unknown-linux-musl.tar.gz
|
||||||
|
echo "5222beabd3e37dc5be0d18ff99b79058469354db5c220153a1b92db5ba12be89 mdbook.tar.gz" > sha256sums
|
||||||
|
sha256sum --check sha256sums
|
||||||
|
tar xvf mdbook.tar.gz
|
||||||
659
src/cache.rs
659
src/cache.rs
|
|
@ -1,28 +1,38 @@
|
||||||
use std::{
|
use std::{
|
||||||
ffi::OsStr,
|
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::{BufReader, Cursor, Read},
|
io::{Cursor, ErrorKind, Read},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{ensure, Context, Result};
|
use anyhow::{Context, Result, anyhow, bail, ensure};
|
||||||
use log::debug;
|
use log::{debug, info};
|
||||||
use ureq::tls::{RootCerts, TlsConfig, TlsProvider};
|
use ureq::{
|
||||||
use ureq::Agent;
|
Agent,
|
||||||
use walkdir::{DirEntry, WalkDir};
|
http::StatusCode,
|
||||||
|
tls::{RootCerts, TlsConfig, TlsProvider},
|
||||||
|
};
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
|
||||||
use crate::{config::TlsBackend, types::PlatformType, utils::print_warning};
|
use crate::{
|
||||||
|
config::{Language, TlsBackend},
|
||||||
|
types::PlatformType,
|
||||||
|
};
|
||||||
|
|
||||||
pub static TLDR_PAGES_DIR: &str = "tldr-pages";
|
pub static TLDR_PAGES_DIR: &str = "tldr-pages";
|
||||||
static TLDR_OLD_PAGES_DIR: &str = "tldr-master";
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone)]
|
||||||
pub struct Cache {
|
pub struct CacheConfig<'a> {
|
||||||
cache_dir: PathBuf,
|
pub pages_directory: &'a Path,
|
||||||
enable_styles: bool,
|
pub custom_pages_directory: Option<&'a Path>,
|
||||||
tls_backend: TlsBackend,
|
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)]
|
#[derive(Debug)]
|
||||||
|
|
@ -31,6 +41,228 @@ pub struct PageLookupResult {
|
||||||
pub patch_path: Option<PathBuf>,
|
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> + use<>> {
|
||||||
|
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()
|
||||||
|
&& (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<'_>> + use<'_>> {
|
||||||
|
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 {
|
impl PageLookupResult {
|
||||||
pub fn with_page(page_path: PathBuf) -> Self {
|
pub fn with_page(page_path: PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -44,12 +276,12 @@ impl PageLookupResult {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a buffered reader that sequentially reads from the page and the
|
/// Create a reader that sequentially reads from the page and the
|
||||||
/// patch, as if they were concatenated.
|
/// patch, as if they were concatenated.
|
||||||
///
|
///
|
||||||
/// This will return an error if either the page file or the patch file
|
/// This will return an error if either the page file or the patch file
|
||||||
/// cannot be opened.
|
/// cannot be opened.
|
||||||
pub fn reader(&self) -> Result<BufReader<Box<dyn Read>>> {
|
pub fn reader(&self) -> Result<Box<dyn Read>> {
|
||||||
// Open page file
|
// Open page file
|
||||||
let page_file = File::open(&self.page_path)
|
let page_file = File::open(&self.page_path)
|
||||||
.with_context(|| format!("Could not open page file at {}", self.page_path.display()))?;
|
.with_context(|| format!("Could not open page file at {}", self.page_path.display()))?;
|
||||||
|
|
@ -69,128 +301,23 @@ impl PageLookupResult {
|
||||||
// the page and patch files and that will read them sequentially,
|
// the page and patch files and that will read them sequentially,
|
||||||
// because it avoids the boxing below. However, the performance impact
|
// because it avoids the boxing below. However, the performance impact
|
||||||
// would first need to be shown to be significant using a benchmark.
|
// would first need to be shown to be significant using a benchmark.
|
||||||
Ok(BufReader::new(if let Some(patch_file) = patch_file_opt {
|
Ok(if let Some(patch_file) = patch_file_opt {
|
||||||
Box::new(page_file.chain(&b"\n"[..]).chain(patch_file)) as Box<dyn Read>
|
Box::new(page_file.chain(&b"\n"[..]).chain(patch_file)) as Box<dyn Read>
|
||||||
} else {
|
} else {
|
||||||
Box::new(page_file) as Box<dyn Read>
|
Box::new(page_file) as Box<dyn Read>
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum CacheFreshness {
|
impl Language<'_> {
|
||||||
/// The cache is still fresh (less than `MAX_CACHE_AGE` old)
|
fn directory_name(&self) -> String {
|
||||||
Fresh,
|
format!("pages.{}", self.0)
|
||||||
/// The cache is stale and should be updated
|
}
|
||||||
Stale(Duration),
|
|
||||||
/// The cache is missing
|
|
||||||
Missing,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cache {
|
impl PlatformType {
|
||||||
pub fn new<P>(cache_dir: P, enable_styles: bool, tls_backend: TlsBackend) -> Self
|
fn directory_name(self) -> &'static str {
|
||||||
where
|
match self {
|
||||||
P: Into<PathBuf>,
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
cache_dir: cache_dir.into(),
|
|
||||||
enable_styles,
|
|
||||||
tls_backend,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the pages cache from the specified URL.
|
|
||||||
pub fn update(&self, archive_source: &str) -> Result<()> {
|
|
||||||
self.ensure_cache_dir_exists()?;
|
|
||||||
|
|
||||||
let archive_url = format!("{archive_source}/tldr.zip");
|
|
||||||
|
|
||||||
let client = Self::build_client(self.tls_backend)?;
|
|
||||||
// First, download the compressed data
|
|
||||||
let bytes: Vec<u8> = Self::download(&client, &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::Linux => "linux",
|
||||||
PlatformType::OsX => "osx",
|
PlatformType::OsX => "osx",
|
||||||
PlatformType::SunOs => "sunos",
|
PlatformType::SunOs => "sunos",
|
||||||
|
|
@ -202,233 +329,10 @@ impl Cache {
|
||||||
PlatformType::Common => "common",
|
PlatformType::Common => "common",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
// relevant 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 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 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)
|
|
||||||
.is_some_and(|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()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cache {
|
impl Cache<'_> {
|
||||||
fn build_client(tls_backend: TlsBackend) -> Result<Agent> {
|
fn build_client(tls_backend: TlsBackend) -> Agent {
|
||||||
let tls_builder = match tls_backend {
|
let tls_builder = match tls_backend {
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
TlsBackend::NativeTls => TlsConfig::builder()
|
TlsBackend::NativeTls => TlsConfig::builder()
|
||||||
|
|
@ -444,22 +348,29 @@ impl Cache {
|
||||||
.root_certs(RootCerts::PlatformVerifier),
|
.root_certs(RootCerts::PlatformVerifier),
|
||||||
};
|
};
|
||||||
let config = Agent::config_builder()
|
let config = Agent::config_builder()
|
||||||
|
.http_status_as_error(false) // because we want to handle them
|
||||||
.tls_config(tls_builder.build())
|
.tls_config(tls_builder.build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
Ok(config.into())
|
config.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download the archive from the specified URL.
|
/// Download the archive from the specified URL.
|
||||||
fn download(client: &Agent, archive_url: &str) -> Result<Vec<u8>> {
|
fn download(client: &Agent, archive_url: &str) -> Result<Option<Vec<u8>>> {
|
||||||
let response = client
|
info!("Downloading archive from {archive_url}");
|
||||||
.get(archive_url)
|
let response = client.get(archive_url).call();
|
||||||
.call()
|
match response {
|
||||||
.with_context(|| format!("Could not download tldr pages from {archive_url}"))?;
|
Ok(response) if response.status().is_success() => {
|
||||||
let mut buf: Vec<u8> = Vec::new();
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
response.into_body().into_reader().read_to_end(&mut buf)?;
|
response.into_body().into_reader().read_to_end(&mut buf)?;
|
||||||
debug!("{} bytes downloaded", buf.len());
|
debug!("{} bytes downloaded", buf.len());
|
||||||
Ok(buf)
|
Ok(Some(buf))
|
||||||
|
}
|
||||||
|
Ok(response) if response.status() == StatusCode::NOT_FOUND => Ok(None),
|
||||||
|
_ => {
|
||||||
|
bail!("Could not download tldr pages from {archive_url}: {response:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -517,22 +428,4 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(&buf, b"Hello\n");
|
assert_eq!(&buf, b"Hello\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(feature = "native-tls")]
|
|
||||||
fn test_create_https_client_with_native_tls() {
|
|
||||||
Cache::build_client(TlsBackend::NativeTls).expect("fails to build a client.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(feature = "rustls-with-webpki-roots")]
|
|
||||||
fn test_create_https_client_with_rustls() {
|
|
||||||
Cache::build_client(TlsBackend::RustlsWithWebpkiRoots).expect("fails to build a client.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(feature = "rustls-with-native-roots")]
|
|
||||||
fn test_create_https_client_with_rustls_with_native_roots() {
|
|
||||||
Cache::build_client(TlsBackend::RustlsWithNativeRoots).expect("fails to build a client.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
src/cli.rs
24
src/cli.rs
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::{arg, builder::ArgAction, command, ArgGroup, Parser};
|
use clap::{ArgGroup, Parser, builder::ArgAction};
|
||||||
|
|
||||||
use crate::types::{ColorOptions, PlatformType};
|
use crate::types::{ColorOptions, PlatformType};
|
||||||
|
|
||||||
|
|
@ -18,7 +18,9 @@ use crate::types::{ColorOptions, PlatformType};
|
||||||
{usage-heading} {usage}
|
{usage-heading} {usage}
|
||||||
|
|
||||||
{all-args}{after-help}",
|
{all-args}{after-help}",
|
||||||
after_help = "To view the user documentation, please visit https://tealdeer-rs.github.io/tealdeer/.",
|
after_help = "To view the user documentation, please visit https://docs.tealdeer.org.
|
||||||
|
|
||||||
|
To view usage examples, run tldr tldr or tldr tealdeer.",
|
||||||
arg_required_else_help = true,
|
arg_required_else_help = true,
|
||||||
help_expected = true,
|
help_expected = true,
|
||||||
group = ArgGroup::new("command_or_file").args(&["command", "render"]),
|
group = ArgGroup::new("command_or_file").args(&["command", "render"]),
|
||||||
|
|
@ -67,13 +69,21 @@ pub(crate) struct Cli {
|
||||||
pub update: bool,
|
pub update: bool,
|
||||||
|
|
||||||
/// If auto update is configured, disable it for this run
|
/// If auto update is configured, disable it for this run
|
||||||
#[arg(long = "no-auto-update", requires = "command_or_file")]
|
#[arg(long = "no-auto-update")]
|
||||||
pub no_auto_update: bool,
|
pub no_auto_update: bool,
|
||||||
|
|
||||||
/// Clear the local cache
|
/// Clear the local cache
|
||||||
#[arg(short = 'c', long = "clear-cache")]
|
#[arg(short = 'c', long = "clear-cache")]
|
||||||
pub clear_cache: bool,
|
pub clear_cache: bool,
|
||||||
|
|
||||||
|
/// Override config file location
|
||||||
|
#[arg(long = "config-path", value_name = "FILE")]
|
||||||
|
pub config_path: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Override config values after reading config file (example: `updates.auto_update = true`)
|
||||||
|
#[arg(long, action = ArgAction::Append, value_name = "OVERRIDE")]
|
||||||
|
pub override_config: Vec<String>,
|
||||||
|
|
||||||
/// Use a pager to page output
|
/// Use a pager to page output
|
||||||
#[arg(long = "pager", requires = "command_or_file")]
|
#[arg(long = "pager", requires = "command_or_file")]
|
||||||
pub pager: bool,
|
pub pager: bool,
|
||||||
|
|
@ -98,6 +108,14 @@ pub(crate) struct Cli {
|
||||||
#[arg(long = "color", value_name = "WHEN")]
|
#[arg(long = "color", value_name = "WHEN")]
|
||||||
pub color: Option<ColorOptions>,
|
pub color: Option<ColorOptions>,
|
||||||
|
|
||||||
|
/// Display the short variants of placeholders
|
||||||
|
#[arg(long)]
|
||||||
|
pub short_options: bool,
|
||||||
|
|
||||||
|
/// Display the long variants of placeholders
|
||||||
|
#[arg(long)]
|
||||||
|
pub long_options: bool,
|
||||||
|
|
||||||
/// Print the version
|
/// Print the version
|
||||||
// Note: We override the version flag because clap uses `-V` by default,
|
// Note: We override the version flag because clap uses `-V` by default,
|
||||||
// while TLDR specification requires `-v` to be used.
|
// while TLDR specification requires `-v` to be used.
|
||||||
|
|
|
||||||
947
src/config.rs
947
src/config.rs
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +1,14 @@
|
||||||
use std::mem;
|
use std::mem;
|
||||||
|
|
||||||
/// An extension trait to clear duplicates from a collection.
|
/// An extension trait to clear duplicates from a collection.
|
||||||
pub(crate) trait Dedup<T: PartialEq + Clone> {
|
pub(crate) trait Dedup<T: PartialEq> {
|
||||||
fn clear_duplicates(&mut self);
|
fn clear_duplicates(&mut self);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear duplicates from a collection, keep the first one seen.
|
/// Clear duplicates from a collection, keep the first one seen.
|
||||||
///
|
///
|
||||||
/// For small vectors, this will be faster than a `HashSet`.
|
/// For small vectors, this will be faster than a `HashSet`.
|
||||||
impl<T: PartialEq + Clone> Dedup<T> for Vec<T> {
|
impl<T: PartialEq> Dedup<T> for Vec<T> {
|
||||||
fn clear_duplicates(&mut self) {
|
fn clear_duplicates(&mut self) {
|
||||||
let orig = mem::replace(self, Vec::with_capacity(self.len()));
|
let orig = mem::replace(self, Vec::with_capacity(self.len()));
|
||||||
for item in orig {
|
for item in orig {
|
||||||
|
|
|
||||||
499
src/formatter.rs
499
src/formatter.rs
|
|
@ -2,25 +2,80 @@
|
||||||
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
use crate::{extensions::FindFrom, types::LineType};
|
use crate::{config::Indent, extensions::FindFrom, types::LineType};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Eq)]
|
||||||
/// Represents a snippet from a page of a specific highlighting class.
|
/// Represents a snippet from a page of a specific highlighting class.
|
||||||
pub enum PageSnippet<'a> {
|
pub enum PageSnippet<T> {
|
||||||
CommandName(&'a str),
|
CommandName(T),
|
||||||
Variable(&'a str),
|
Placeholder(T),
|
||||||
NormalCode(&'a str),
|
PlaceholderVariants { short: T, long: T },
|
||||||
Description(&'a str),
|
NormalCode(T),
|
||||||
Text(&'a str),
|
Description(T),
|
||||||
|
Text(T),
|
||||||
|
Title(T),
|
||||||
|
Indent(usize),
|
||||||
Linebreak,
|
Linebreak,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PageSnippet<'_> {
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
impl<T> PageSnippet<T> {
|
||||||
|
pub fn map<F, U>(self, f: F) -> PageSnippet<U>
|
||||||
|
where
|
||||||
|
F: Fn(T) -> U,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
PageSnippet::CommandName(s) => PageSnippet::CommandName(f(s)),
|
||||||
|
PageSnippet::Placeholder(s) => PageSnippet::Placeholder(f(s)),
|
||||||
|
PageSnippet::PlaceholderVariants { short, long } => PageSnippet::PlaceholderVariants {
|
||||||
|
short: f(short),
|
||||||
|
long: f(long),
|
||||||
|
},
|
||||||
|
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::Indent(n) => PageSnippet::Indent(n),
|
||||||
|
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::Placeholder(s), PageSnippet::Placeholder(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::PlaceholderVariants {
|
||||||
|
short: left_short,
|
||||||
|
long: left_long,
|
||||||
|
},
|
||||||
|
PageSnippet::PlaceholderVariants {
|
||||||
|
short: right_short,
|
||||||
|
long: right_long,
|
||||||
|
},
|
||||||
|
) => left_short == right_short && left_long == right_long,
|
||||||
|
(PageSnippet::Indent(n), PageSnippet::Indent(m)) => n == m,
|
||||||
|
(PageSnippet::Linebreak, PageSnippet::Linebreak) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PageSnippet<&str> {
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
use PageSnippet::*;
|
use PageSnippet::*;
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
CommandName(s) | Variable(s) | NormalCode(s) | Description(s) | Text(s) => s.is_empty(),
|
CommandName(s) | Placeholder(s) | NormalCode(s) | Description(s) | Text(s)
|
||||||
|
| Title(s) => s.is_empty(),
|
||||||
|
PageSnippet::PlaceholderVariants { short, long } => short.is_empty() && long.is_empty(),
|
||||||
|
Indent(n) => *n == 0,
|
||||||
Linebreak => false,
|
Linebreak => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -31,10 +86,12 @@ pub fn highlight_lines<L, F, E>(
|
||||||
lines: L,
|
lines: L,
|
||||||
process_snippet: &mut F,
|
process_snippet: &mut F,
|
||||||
keep_empty_lines: bool,
|
keep_empty_lines: bool,
|
||||||
|
show_title: bool,
|
||||||
|
indent: Indent,
|
||||||
) -> Result<(), E>
|
) -> Result<(), E>
|
||||||
where
|
where
|
||||||
L: Iterator<Item = LineType>,
|
L: Iterator<Item = LineType>,
|
||||||
F: for<'snip> FnMut(PageSnippet<'snip>) -> Result<(), E>,
|
F: for<'snip> FnMut(PageSnippet<&'snip str>) -> Result<(), E>,
|
||||||
{
|
{
|
||||||
let mut command = String::new();
|
let mut command = String::new();
|
||||||
for line in lines {
|
for line in lines {
|
||||||
|
|
@ -45,51 +102,131 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LineType::Title(title) => {
|
LineType::Title(title) => {
|
||||||
debug!("Ignoring title");
|
if show_title {
|
||||||
|
process_snippet(PageSnippet::Linebreak)?;
|
||||||
|
process_snippet(PageSnippet::Indent(indent.base))?;
|
||||||
|
process_snippet(PageSnippet::Title(&title))?;
|
||||||
|
process_snippet(PageSnippet::Linebreak)?;
|
||||||
|
} else {
|
||||||
|
debug!("Ignoring title");
|
||||||
|
}
|
||||||
// This is safe as long as the parsed title is only the command,
|
// This is safe as long as the parsed title is only the command,
|
||||||
// and the iterator yields values in order of appearance.
|
// and the iterator yields values in order of appearance.
|
||||||
command = title;
|
command = title;
|
||||||
debug!("Detected command name: {}", &command);
|
debug!("Detected command name: {command}");
|
||||||
|
}
|
||||||
|
LineType::Description(text) => {
|
||||||
|
process_snippet(PageSnippet::Indent(indent.base))?;
|
||||||
|
process_snippet(PageSnippet::Description(&text))?;
|
||||||
|
process_snippet(PageSnippet::Linebreak)?;
|
||||||
|
}
|
||||||
|
LineType::ExampleText(text) => {
|
||||||
|
process_snippet(PageSnippet::Indent(indent.base))?;
|
||||||
|
process_snippet(PageSnippet::Text(&text))?;
|
||||||
|
process_snippet(PageSnippet::Linebreak)?;
|
||||||
}
|
}
|
||||||
LineType::Description(text) => process_snippet(PageSnippet::Description(&text))?,
|
|
||||||
LineType::ExampleText(text) => process_snippet(PageSnippet::Text(&text))?,
|
|
||||||
LineType::ExampleCode(text) => {
|
LineType::ExampleCode(text) => {
|
||||||
process_snippet(PageSnippet::NormalCode(" "))?;
|
process_snippet(PageSnippet::Indent(indent.command))?;
|
||||||
highlight_code(&command, &text, process_snippet)?;
|
highlight_code(&command, &text, process_snippet)?;
|
||||||
process_snippet(PageSnippet::Linebreak)?;
|
process_snippet(PageSnippet::Linebreak)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
LineType::Other(text) => debug!("Unknown line type: {:?}", text),
|
LineType::Other(text) => debug!("Unknown line type: {text:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
process_snippet(PageSnippet::Linebreak)?;
|
process_snippet(PageSnippet::Linebreak)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Highlight code examples including user variables in {{ curly braces }}.
|
/// Highlight code examples.
|
||||||
fn highlight_code<'a, E>(
|
/// - parse placeholders (`{{ curly braces }}`)
|
||||||
command: &'a str,
|
/// - replace escaped placeholder markers (`\{\{` and `\}\}`)
|
||||||
text: &'a str,
|
fn highlight_code<E>(
|
||||||
process_snippet: &mut impl FnMut(PageSnippet<'a>) -> Result<(), E>,
|
command: &str,
|
||||||
|
mut text: &str,
|
||||||
|
process_snippet: &mut impl FnMut(PageSnippet<&str>) -> Result<(), E>,
|
||||||
) -> Result<(), E> {
|
) -> Result<(), E> {
|
||||||
let variable_splits = text
|
// We replace escaped placeholder markers at the end so that our replacing does not interfere
|
||||||
.split("}}")
|
// with finding the actual markers.
|
||||||
.map(|s| s.split_once("{{").unwrap_or((s, "")));
|
// NOTE: This is not optimal, as it allocates one String for each `replace`
|
||||||
for (code_segment, variable) in variable_splits {
|
let replace_escaped = |s: &str| s.replace(r"\{\{", "{{").replace(r"\}\}", "}}");
|
||||||
highlight_code_segment(command, code_segment, process_snippet)?;
|
|
||||||
process_snippet(PageSnippet::Variable(variable))?;
|
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,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let placeholder_content = replace_escaped(placeholder_content);
|
||||||
|
if let Some(s) = placeholder_content.strip_prefix('[')
|
||||||
|
&& let Some(s) = s.strip_suffix(']')
|
||||||
|
&& let Some((short, long)) = s.split_once('|')
|
||||||
|
{
|
||||||
|
process_snippet(PageSnippet::PlaceholderVariants { short, long })?;
|
||||||
|
} else {
|
||||||
|
process_snippet(PageSnippet::Placeholder(&placeholder_content))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
text = &text[end_marker + 2..];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !text.is_empty() {
|
||||||
|
highlight_code_segment(command, &replace_escaped(text), process_snippet)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
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
|
/// Yields `NormalCode` and `CommandName` in alternating order according to the occurrences of
|
||||||
/// `command_name` in `segment`. Variables are not detected here, see `highlight_code`
|
/// `command_name` in `segment`. Placeholders are not detected here, see `highlight_code`
|
||||||
/// instead.
|
/// instead.
|
||||||
fn highlight_code_segment<'a, E>(
|
fn highlight_code_segment<'a, E>(
|
||||||
command_name: &'a str,
|
command_name: &'a str,
|
||||||
mut segment: &'a str,
|
mut segment: &'a str,
|
||||||
process_snippet: &mut impl FnMut(PageSnippet<'a>) -> Result<(), E>,
|
process_snippet: &mut impl FnMut(PageSnippet<&'a str>) -> Result<(), E>,
|
||||||
) -> Result<(), E> {
|
) -> Result<(), E> {
|
||||||
if !command_name.is_empty() {
|
if !command_name.is_empty() {
|
||||||
let mut search_start = 0;
|
let mut search_start = 0;
|
||||||
|
|
@ -119,20 +256,17 @@ fn is_freestanding_substring(surrounding: &str, substring: (usize, usize)) -> bo
|
||||||
let char_before_is_okay = surrounding[..start]
|
let char_before_is_okay = surrounding[..start]
|
||||||
.chars()
|
.chars()
|
||||||
.last()
|
.last()
|
||||||
.filter(|prev_char| !prev_char.is_whitespace())
|
.is_none_or(char::is_whitespace);
|
||||||
.is_none();
|
|
||||||
let char_after_is_okay = surrounding[end..]
|
let char_after_is_okay = surrounding[end..]
|
||||||
.chars()
|
.chars()
|
||||||
.next()
|
.next()
|
||||||
.filter(|next_char| !next_char.is_whitespace())
|
.is_none_or(char::is_whitespace);
|
||||||
.is_none();
|
|
||||||
char_before_is_okay && char_after_is_okay
|
char_before_is_okay && char_after_is_okay
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use PageSnippet::*;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_is_freestanding_substring() {
|
fn test_is_freestanding_substring() {
|
||||||
|
|
@ -159,80 +293,251 @@ mod tests {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run<'a>(cmd: &'a str, segment: &'a str) -> Vec<PageSnippet<'a>> {
|
fn run<'a>(cmd: &'a str, segment: &'a str) -> Vec<PageSnippet<String>> {
|
||||||
let mut yielded = Vec::new();
|
let mut yielded = Vec::new();
|
||||||
let mut process_snippet = |snip: PageSnippet<'a>| {
|
let mut process_snippet = |snip: PageSnippet<&str>| {
|
||||||
if !snip.is_empty() {
|
if !snip.is_empty() {
|
||||||
yielded.push(snip);
|
yielded.push(snip.map(str::to_string));
|
||||||
}
|
}
|
||||||
Ok::<(), ()>(())
|
Ok::<(), ()>(())
|
||||||
};
|
};
|
||||||
|
|
||||||
highlight_code_segment(cmd, segment, &mut process_snippet)
|
highlight_code(cmd, segment, &mut process_snippet).expect("highlight code segment failed");
|
||||||
.expect("highlight code segment failed");
|
|
||||||
yielded
|
yielded
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
mod highlight_code_segment {
|
||||||
fn test_highlight_code_segment() {
|
use super::*;
|
||||||
assert!(run("make", "").is_empty());
|
use PageSnippet::*;
|
||||||
assert_eq!(
|
|
||||||
&run("make", "make all CC=clang -q"),
|
#[test]
|
||||||
&[CommandName("make"), NormalCode(" all CC=clang -q")]
|
fn test_highlight_code_segment() {
|
||||||
);
|
assert!(run("make", "").is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&run("make", " make money --always-make"),
|
&run("make", "make all CC=clang -q"),
|
||||||
&[
|
&[CommandName("make"), NormalCode(" all CC=clang -q")]
|
||||||
NormalCode(" "),
|
);
|
||||||
CommandName("make"),
|
assert_eq!(
|
||||||
NormalCode(" money --always-make")
|
&run("make", " make money --always-make"),
|
||||||
]
|
&[
|
||||||
);
|
NormalCode(" "),
|
||||||
assert_eq!(
|
CommandName("make"),
|
||||||
&run("git commit", "git commit -m 'git commit'"),
|
NormalCode(" money --always-make")
|
||||||
&[CommandName("git commit"), NormalCode(" -m 'git commit'"),]
|
]
|
||||||
);
|
);
|
||||||
|
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]
|
mod placeholders {
|
||||||
fn test_i18n() {
|
use super::*;
|
||||||
assert_eq!(
|
use PageSnippet::*;
|
||||||
&run("mäke", "mäke höhlenrätselbücher"),
|
|
||||||
&[CommandName("mäke"), NormalCode(" höhlenrätselbücher")]
|
#[test]
|
||||||
);
|
fn placeholder_vs_escaped() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&run(
|
run("ping", "ping {{example.com}}"),
|
||||||
"Müll",
|
[
|
||||||
"1000 Gründe warum Müll heute größer ist als Müll früher, ärgerlich"
|
CommandName("ping"),
|
||||||
),
|
NormalCode(" "),
|
||||||
&[
|
Placeholder("example.com"),
|
||||||
NormalCode("1000 Gründe warum "),
|
],
|
||||||
CommandName("Müll"),
|
);
|
||||||
NormalCode(" heute größer ist als "),
|
assert_eq!(
|
||||||
CommandName("Müll"),
|
run(
|
||||||
NormalCode(" früher, ärgerlich")
|
"docker inspect",
|
||||||
]
|
r"docker inspect --format '\{\{range.NetworkSettings.Networks\}\}\{\{.IPAddress\}\}\{\{end\}\}' {{container}}"
|
||||||
);
|
),
|
||||||
assert_eq!(
|
[
|
||||||
&run(
|
CommandName("docker inspect"),
|
||||||
"übergang",
|
NormalCode(
|
||||||
"die Zustandsübergangsfunktion übergang Änderungen",
|
" --format '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "
|
||||||
),
|
),
|
||||||
&[
|
Placeholder("container"),
|
||||||
NormalCode("die Zustandsübergangsfunktion "),
|
],
|
||||||
CommandName("übergang"),
|
);
|
||||||
NormalCode(" Änderungen")
|
assert_eq!(
|
||||||
],
|
run("mount", r"mount \\{{computer_name}}\{{share_name}} Z:"),
|
||||||
);
|
[
|
||||||
|
CommandName("mount"),
|
||||||
|
NormalCode(r" \\"),
|
||||||
|
Placeholder("computer_name"),
|
||||||
|
NormalCode(r"\"),
|
||||||
|
Placeholder("share_name"),
|
||||||
|
NormalCode(" Z:"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(run("", r"\{"), [NormalCode(r"\{")]);
|
||||||
|
assert_eq!(run("", r"\{{a"), [NormalCode(r"\{{a")]);
|
||||||
|
assert_eq!(run("", r"\{{a}}"), [NormalCode(r"\"), Placeholder("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 "),
|
||||||
|
Placeholder("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(" "), Placeholder("}")]
|
||||||
|
);
|
||||||
|
|
||||||
|
// And these are just to document the current behavior
|
||||||
|
assert_eq!(run("", "{{{}}}"), [Placeholder("{}")]);
|
||||||
|
assert_eq!(run("", "{{{{}}}"), [Placeholder("{{}")]);
|
||||||
|
assert_eq!(run("", "{{{}}}}"), [Placeholder("{}}")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escaped_inside_placeholder() {
|
||||||
|
assert_eq!(
|
||||||
|
run(
|
||||||
|
"playerctl",
|
||||||
|
r#"playerctl metadata {{[-f|--format]}} "{{Now playing: \{\{artist\}\} - \{\{album\}\} - \{\{title\}\}}}""#
|
||||||
|
),
|
||||||
|
[
|
||||||
|
CommandName("playerctl"),
|
||||||
|
NormalCode(" metadata "),
|
||||||
|
PlaceholderVariants {
|
||||||
|
short: "-f",
|
||||||
|
long: "--format"
|
||||||
|
},
|
||||||
|
NormalCode(" \""),
|
||||||
|
Placeholder("Now playing: {{artist}} - {{album}} - {{title}}"),
|
||||||
|
NormalCode("\""),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_inside_escaped() {
|
||||||
|
assert_eq!(
|
||||||
|
run("test", r"test \{\{{{var}} normal\}\}"),
|
||||||
|
[
|
||||||
|
CommandName("test"),
|
||||||
|
NormalCode(" {{"),
|
||||||
|
Placeholder("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"), Placeholder("x")],);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
mod placeholder_variants {
|
||||||
fn test_empty_command() {
|
use super::*;
|
||||||
let segment = "some code";
|
use PageSnippet::*;
|
||||||
let snippets = [NormalCode(segment)];
|
|
||||||
|
|
||||||
assert_eq!(run("", segment), snippets);
|
#[test]
|
||||||
assert_eq!(run(" ", segment), snippets);
|
fn missing_marker() {
|
||||||
assert_eq!(run(" \t ", segment), snippets);
|
assert_eq!(
|
||||||
|
run("foo", "{{[short|long]}}"),
|
||||||
|
[PlaceholderVariants {
|
||||||
|
short: "short",
|
||||||
|
long: "long"
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(run("foo", "{{short|long]}}"), [Placeholder("short|long]")]);
|
||||||
|
assert_eq!(run("foo", "{{[short|long}}"), [Placeholder("[short|long")]);
|
||||||
|
assert_eq!(run("foo", "{{[shortlong]}}"), [Placeholder("[shortlong]")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The character `[` is a valid command name
|
||||||
|
#[test]
|
||||||
|
fn command_name_interaction() {
|
||||||
|
for name in ["[", "]", "|"] {
|
||||||
|
assert_eq!(
|
||||||
|
run(name, "{{[short|long]}}"),
|
||||||
|
[PlaceholderVariants {
|
||||||
|
short: "short",
|
||||||
|
long: "long"
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_variant() {
|
||||||
|
for name in ["[", "]", "|"] {
|
||||||
|
assert_eq!(
|
||||||
|
run(name, "{{[|long]}}"),
|
||||||
|
[PlaceholderVariants {
|
||||||
|
short: "",
|
||||||
|
long: "long"
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
run(name, "{{[short|]}}"),
|
||||||
|
[PlaceholderVariants {
|
||||||
|
short: "short",
|
||||||
|
long: ""
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
assert_eq!(run(name, "{{[|]}}"), [] as [PageSnippet::<String>; 0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ impl<R: BufRead> Iterator for LineIterator<R> {
|
||||||
match bytes_read {
|
match bytes_read {
|
||||||
Ok(0) => None,
|
Ok(0) => None,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Could not read line from reader: {:?}", e);
|
warn!("Could not read line from reader: {e:?}");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
|
@ -68,7 +68,7 @@ impl<R: BufRead> Iterator for LineIterator<R> {
|
||||||
.find(|b| matches!(b, Ok(b'\n') | Err(_)))
|
.find(|b| matches!(b, Ok(b'\n') | Err(_)))
|
||||||
.transpose()
|
.transpose()
|
||||||
{
|
{
|
||||||
warn!("Could not read line from reader: {:?}", e);
|
warn!("Could not read line from reader: {e:?}");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
self.first_line = false;
|
self.first_line = false;
|
||||||
|
|
|
||||||
458
src/main.rs
458
src/main.rs
|
|
@ -15,6 +15,8 @@
|
||||||
#![allow(clippy::similar_names)]
|
#![allow(clippy::similar_names)]
|
||||||
#![allow(clippy::struct_excessive_bools)]
|
#![allow(clippy::struct_excessive_bools)]
|
||||||
#![allow(clippy::too_many_lines)]
|
#![allow(clippy::too_many_lines)]
|
||||||
|
#![allow(clippy::unnecessary_debug_formatting)]
|
||||||
|
#![allow(clippy::while_let_loop)]
|
||||||
|
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
feature = "native-tls",
|
feature = "native-tls",
|
||||||
|
|
@ -33,9 +35,12 @@ use std::{
|
||||||
process::{Command, ExitCode},
|
process::{Command, ExitCode},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use app_dirs::AppInfo;
|
use cache::CacheConfig;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use config::{ConfigLoader, Language, StyleConfig, TlsBackend};
|
||||||
|
use log::debug;
|
||||||
|
use types::PlatformType;
|
||||||
|
|
||||||
mod cache;
|
mod cache;
|
||||||
mod cli;
|
mod cli;
|
||||||
|
|
@ -48,115 +53,68 @@ mod types;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cache::{Cache, CacheFreshness, PageLookupResult, TLDR_PAGES_DIR},
|
cache::{Cache, PageLookupResult, TLDR_PAGES_DIR},
|
||||||
cli::Cli,
|
cli::Cli,
|
||||||
config::{get_config_dir, get_config_path, make_default_config, Config, PathWithSource},
|
config::{
|
||||||
extensions::Dedup,
|
Config, PathWithSource, PlaceholderFormat, get_config_dir, make_default_config,
|
||||||
|
supported_tls_backends_string,
|
||||||
|
},
|
||||||
output::print_page,
|
output::print_page,
|
||||||
types::{ColorOptions, PlatformType},
|
types::ColorOptions,
|
||||||
utils::{print_error, print_warning},
|
utils::{print_error, print_warning},
|
||||||
};
|
};
|
||||||
|
|
||||||
const NAME: &str = "tealdeer";
|
const NAME: &str = "tealdeer";
|
||||||
const APP_INFO: AppInfo = AppInfo {
|
static TEALDEER_PAGE: &str =
|
||||||
name: NAME,
|
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/pages/tealdeer.md"));
|
||||||
author: NAME,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// 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
|
/// Clear the cache
|
||||||
fn clear_cache(cache: &Cache, quietly: bool) -> Result<()> {
|
fn clear_cache(cache: Cache, quietly: bool) -> Result<()> {
|
||||||
let cache_dir_found = cache.clear().context("Could not clear cache")?;
|
let cache_dir = cache.config().pages_directory.display();
|
||||||
|
cache.clear().context("Could not clear cache")?;
|
||||||
if !quietly {
|
if !quietly {
|
||||||
let cache_dir = cache.cache_dir().display();
|
eprintln!("Successfully cleared cache at `{cache_dir}`.");
|
||||||
if cache_dir_found {
|
|
||||||
eprintln!("Successfully cleared cache at `{cache_dir}`.");
|
|
||||||
} else {
|
|
||||||
eprintln!("Cache directory not found at `{cache_dir}`, nothing to do.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the cache
|
/// Update the cache
|
||||||
fn update_cache(cache: &Cache, archive_source: &str, quietly: bool) -> Result<()> {
|
fn update_cache(
|
||||||
cache
|
cache: &mut Cache,
|
||||||
.update(archive_source)
|
archive_source: &str,
|
||||||
|
tls_backend: TlsBackend,
|
||||||
|
quietly: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let downloaded_languages = cache
|
||||||
|
.update(archive_source, tls_backend)
|
||||||
.context("Could not update cache")?;
|
.context("Could not update cache")?;
|
||||||
if !quietly {
|
if !quietly {
|
||||||
eprintln!("Successfully updated cache.");
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show file paths
|
/// Show file paths
|
||||||
fn show_paths(config: &Config) {
|
fn show_paths(config: &Config) {
|
||||||
let config_dir = get_config_dir().map_or_else(
|
let config_dir = {
|
||||||
|e| format!("[Error: {e}]"),
|
let (mut path, source) = get_config_dir();
|
||||||
|(mut path, source)| {
|
path.push(""); // Trailing path separator
|
||||||
path.push(""); // Trailing path separator
|
match path.to_str() {
|
||||||
match path.to_str() {
|
Some(path) => format!("{path} ({source})"),
|
||||||
Some(path) => format!("{path} ({source})"),
|
None => "[Invalid]".to_string(),
|
||||||
None => "[Invalid]".to_string(),
|
}
|
||||||
}
|
};
|
||||||
},
|
let config_path = config.file_path.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 cache_dir = config.directories.cache_dir.to_string();
|
||||||
let pages_dir = {
|
let pages_dir = {
|
||||||
let mut path = config.directories.cache_dir.path.clone();
|
let mut path = config.directories.cache_dir.path.clone();
|
||||||
|
|
@ -175,8 +133,8 @@ fn show_paths(config: &Config) {
|
||||||
println!("Custom pages dir: {custom_pages_dir}");
|
println!("Custom pages dir: {custom_pages_dir}");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_config() -> Result<()> {
|
fn create_config(path: Option<&Path>) -> Result<()> {
|
||||||
let config_file_path = make_default_config().context("Could not create seed config")?;
|
let config_file_path = make_default_config(path).context("Could not create seed config")?;
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Successfully created seed config file here: {}",
|
"Successfully created seed config file here: {}",
|
||||||
config_file_path.to_str().unwrap()
|
config_file_path.to_str().unwrap()
|
||||||
|
|
@ -192,42 +150,6 @@ fn init_log() {
|
||||||
#[cfg(not(feature = "logging"))]
|
#[cfg(not(feature = "logging"))]
|
||||||
fn init_log() {}
|
fn init_log() {}
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
if env_lang.is_none() {
|
|
||||||
return vec!["en".to_string()];
|
|
||||||
}
|
|
||||||
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 get_languages_from_env() -> Vec<String> {
|
|
||||||
get_languages(
|
|
||||||
std::env::var("LANG").ok().as_deref(),
|
|
||||||
std::env::var("LANGUAGE").ok().as_deref(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawn_editor(custom_pages_dir: &Path, file_name: &str) -> Result<()> {
|
fn spawn_editor(custom_pages_dir: &Path, file_name: &str) -> Result<()> {
|
||||||
create_dir_all(custom_pages_dir).context("Failed to create custom pages directory")?;
|
create_dir_all(custom_pages_dir).context("Failed to create custom pages directory")?;
|
||||||
|
|
||||||
|
|
@ -279,7 +201,26 @@ fn main() -> ExitCode {
|
||||||
|
|
||||||
fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> {
|
fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> {
|
||||||
// Look up config file, if none is found fall back to default config.
|
// Look up config file, if none is found fall back to default config.
|
||||||
let config = Config::load(enable_styles).context("Could not load config")?;
|
debug!("Loading config");
|
||||||
|
let config_loader = match &args.config_path {
|
||||||
|
Some(path) if !args.seed_config => ConfigLoader::read(path.clone(), &args.override_config)
|
||||||
|
.context("Could not read config from given path")?,
|
||||||
|
_ => ConfigLoader::read_default_path(&args.override_config)
|
||||||
|
.context("Could not read config from default path")?,
|
||||||
|
};
|
||||||
|
let mut config = config_loader.load()?;
|
||||||
|
|
||||||
|
// Override styles if needed
|
||||||
|
if !enable_styles {
|
||||||
|
config.style = StyleConfig::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
config.display.placeholder_format = match (args.short_options, args.long_options) {
|
||||||
|
(false, false) => config.display.placeholder_format, // keep old value
|
||||||
|
(true, false) => PlaceholderFormat::Short,
|
||||||
|
(false, true) => PlaceholderFormat::Long,
|
||||||
|
(true, true) => PlaceholderFormat::Both,
|
||||||
|
};
|
||||||
|
|
||||||
let custom_pages_dir = config
|
let custom_pages_dir = config
|
||||||
.directories
|
.directories
|
||||||
|
|
@ -313,148 +254,191 @@ fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> {
|
||||||
|
|
||||||
// Create a basic config and exit
|
// Create a basic config and exit
|
||||||
if args.seed_config {
|
if args.seed_config {
|
||||||
create_config()?;
|
create_config(args.config_path.as_deref())?;
|
||||||
return Ok(ExitCode::SUCCESS);
|
return Ok(ExitCode::SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
let platforms = compute_platforms(args.platforms.as_ref());
|
|
||||||
|
|
||||||
// If a local file was passed in, render it and exit
|
// If a local file was passed in, render it and exit
|
||||||
if let Some(file) = args.render {
|
if let Some(file) = args.render {
|
||||||
let path = PageLookupResult::with_page(file);
|
let reader = PageLookupResult::with_page(file).reader()?;
|
||||||
print_page(&path, args.raw, enable_styles, args.pager, &config)?;
|
print_page(reader, args.raw, enable_styles, args.pager, &config)?;
|
||||||
return Ok(ExitCode::SUCCESS);
|
return Ok(ExitCode::SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instantiate cache. This will not yet create the cache directory!
|
// The tealdeer page is embedded in the binary, no cache needed
|
||||||
let cache = Cache::new(
|
if command == "tealdeer" {
|
||||||
&config.directories.cache_dir.path,
|
print_page(
|
||||||
enable_styles,
|
TEALDEER_PAGE.as_bytes(),
|
||||||
config.updates.tls_backend,
|
args.raw,
|
||||||
);
|
enable_styles,
|
||||||
|
args.pager,
|
||||||
// Clear cache, pass through
|
&config,
|
||||||
if args.clear_cache {
|
)?;
|
||||||
clear_cache(&cache, args.quiet)?;
|
return Ok(ExitCode::SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
if should_update_cache(&cache, &args, &config) {
|
if let Some(platforms) = args.platforms {
|
||||||
update_cache(&cache, &config.updates.archive_source, args.quiet)?;
|
config.search.platforms = platforms;
|
||||||
} else if (args.list || !args.command.is_empty())
|
if !config.search.platforms.contains(&PlatformType::Common) {
|
||||||
&& check_cache(&cache, &args, enable_styles) == CheckCacheResult::CacheMissing
|
config.search.platforms.push(PlatformType::Common);
|
||||||
{
|
}
|
||||||
// Cache is needed, but missing
|
}
|
||||||
return Ok(ExitCode::FAILURE);
|
|
||||||
|
let (search_languages, download_languages): (&[_], &[_]) = match args.language.as_deref() {
|
||||||
|
Some(lang) => (&[Language(lang)], &[Language(lang)]),
|
||||||
|
None => (&config.search.languages, &config.updates.download_languages),
|
||||||
|
};
|
||||||
|
|
||||||
|
let cache_config = CacheConfig {
|
||||||
|
pages_directory: &config.directories.cache_dir.path().join(TLDR_PAGES_DIR),
|
||||||
|
custom_pages_directory: config
|
||||||
|
.directories
|
||||||
|
.custom_pages_dir
|
||||||
|
.as_ref()
|
||||||
|
.map(PathWithSource::path),
|
||||||
|
platforms: &config.search.platforms,
|
||||||
|
search_languages,
|
||||||
|
download_languages,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if !args.quiet {
|
||||||
|
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://docs.tealdeer.org");
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
// List cached commands and exit
|
|
||||||
if args.list {
|
if args.list {
|
||||||
println!(
|
for page in cache.list_pages()? {
|
||||||
"{}",
|
println!("{page}");
|
||||||
cache.list_pages(custom_pages_dir, &platforms).join("\n")
|
}
|
||||||
);
|
|
||||||
|
|
||||||
return Ok(ExitCode::SUCCESS);
|
return Ok(ExitCode::SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show command from cache
|
// Show command from cache
|
||||||
if !command.is_empty() {
|
if !command.is_empty() {
|
||||||
// Collect languages
|
// TODO: Remove this check 1 year after version 1.7.0 was released
|
||||||
let languages = args
|
if cache.old_custom_pages_exist()? {
|
||||||
.language
|
print_warning(
|
||||||
.map_or_else(get_languages_from_env, |lang| vec![lang]);
|
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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Search for command in cache
|
let Some(result) = cache.find_page(&command) else {
|
||||||
let Some(lookup_result) = cache.find_page(
|
|
||||||
&command,
|
|
||||||
&languages,
|
|
||||||
config
|
|
||||||
.directories
|
|
||||||
.custom_pages_dir
|
|
||||||
.as_ref()
|
|
||||||
.map(PathWithSource::path),
|
|
||||||
&platforms,
|
|
||||||
) else {
|
|
||||||
if !args.quiet {
|
if !args.quiet {
|
||||||
print_warning(
|
print_warning(
|
||||||
enable_styles,
|
enable_styles,
|
||||||
&format!(
|
&format!(
|
||||||
"Page `{}` not found in cache.\n\
|
"Page `{command}` not found in cache.\n\
|
||||||
Try updating with `tldr --update`, or submit a pull request to:\n\
|
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);
|
return Ok(ExitCode::FAILURE);
|
||||||
};
|
};
|
||||||
|
|
||||||
print_page(&lookup_result, args.raw, enable_styles, args.pager, &config)?;
|
print_page(
|
||||||
|
result.reader()?,
|
||||||
|
args.raw,
|
||||||
|
enable_styles,
|
||||||
|
args.pager,
|
||||||
|
&config,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ExitCode::SUCCESS)
|
Ok(ExitCode::SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the passed or default platform types and appends `PlatformType::Common` as fallback.
|
|
||||||
fn compute_platforms(platforms: Option<&Vec<PlatformType>>) -> Vec<PlatformType> {
|
|
||||||
match platforms {
|
|
||||||
Some(p) => {
|
|
||||||
let mut result = p.clone();
|
|
||||||
if !result.contains(&PlatformType::Common) {
|
|
||||||
result.push(PlatformType::Common);
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
None => vec![PlatformType::current(), PlatformType::Common],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
//! Functions for printing pages to the terminal
|
//! Functions for printing pages to the terminal
|
||||||
|
|
||||||
use std::io::{self, BufRead, Write};
|
use std::io::{self, BufRead, BufReader, Read, Write};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use yansi::Paint;
|
use yansi::Paint;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cache::PageLookupResult,
|
config::{Config, PlaceholderFormat, StyleConfig},
|
||||||
config::{Config, StyleConfig},
|
formatter::{PageSnippet, highlight_lines},
|
||||||
formatter::{highlight_lines, PageSnippet},
|
|
||||||
line_iterator::LineIterator,
|
line_iterator::LineIterator,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -30,14 +29,13 @@ fn configure_pager(enable_styles: bool) {
|
||||||
|
|
||||||
/// Print page by path
|
/// Print page by path
|
||||||
pub fn print_page(
|
pub fn print_page(
|
||||||
lookup_result: &PageLookupResult,
|
reader: impl Read,
|
||||||
enable_markdown: bool,
|
enable_markdown: bool,
|
||||||
enable_styles: bool,
|
enable_styles: bool,
|
||||||
use_pager: bool,
|
use_pager: bool,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Create reader from file(s)
|
let reader = BufReader::new(reader);
|
||||||
let reader = lookup_result.reader()?;
|
|
||||||
|
|
||||||
// Configure pager if applicable
|
// Configure pager if applicable
|
||||||
if use_pager || config.display.use_pager {
|
if use_pager || config.display.use_pager {
|
||||||
|
|
@ -56,11 +54,17 @@ pub fn print_page(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Closure that processes a page snippet and writes it to stdout
|
// Closure that processes a page snippet and writes it to stdout
|
||||||
let mut process_snippet = |snip: PageSnippet<'_>| {
|
let mut process_snippet = |snip: PageSnippet<&str>| {
|
||||||
if snip.is_empty() {
|
if snip.is_empty() {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
print_snippet(&mut handle, snip, &config.style).context("Failed to print snippet")
|
print_snippet(
|
||||||
|
&mut handle,
|
||||||
|
snip,
|
||||||
|
&config.style,
|
||||||
|
config.display.placeholder_format,
|
||||||
|
)
|
||||||
|
.context("Failed to print snippet")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -69,9 +73,11 @@ pub fn print_page(
|
||||||
LineIterator::new(reader),
|
LineIterator::new(reader),
|
||||||
&mut process_snippet,
|
&mut process_snippet,
|
||||||
!config.display.compact,
|
!config.display.compact,
|
||||||
|
config.display.show_title,
|
||||||
|
config.display.indent,
|
||||||
)
|
)
|
||||||
.context("Could not write to stdout")?;
|
.context("Could not write to stdout")?;
|
||||||
};
|
}
|
||||||
|
|
||||||
// We're done outputting data, flush stdout now!
|
// We're done outputting data, flush stdout now!
|
||||||
handle.flush().context("Could not flush stdout")?;
|
handle.flush().context("Could not flush stdout")?;
|
||||||
|
|
@ -81,17 +87,30 @@ pub fn print_page(
|
||||||
|
|
||||||
fn print_snippet(
|
fn print_snippet(
|
||||||
writer: &mut impl Write,
|
writer: &mut impl Write,
|
||||||
snip: PageSnippet<'_>,
|
snip: PageSnippet<&str>,
|
||||||
style: &StyleConfig,
|
style: &StyleConfig,
|
||||||
|
placeholder_format: PlaceholderFormat,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
use PageSnippet::*;
|
use PageSnippet::*;
|
||||||
|
|
||||||
match snip {
|
match snip {
|
||||||
CommandName(s) => write!(writer, "{}", s.paint(style.command_name)),
|
CommandName(s) | Title(s) => write!(writer, "{}", s.paint(style.command_name)),
|
||||||
Variable(s) => write!(writer, "{}", s.paint(style.example_variable)),
|
Placeholder(s) => write!(writer, "{}", s.paint(style.example_variable)),
|
||||||
|
PlaceholderVariants { short, long } => match placeholder_format {
|
||||||
|
PlaceholderFormat::Short => write!(writer, "{}", short.paint(style.example_code)),
|
||||||
|
PlaceholderFormat::Long => write!(writer, "{}", long.paint(style.example_code)),
|
||||||
|
PlaceholderFormat::Both => {
|
||||||
|
write!(
|
||||||
|
writer,
|
||||||
|
"{}",
|
||||||
|
format!("[{short}|{long}]").paint(style.example_code)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
NormalCode(s) => write!(writer, "{}", s.paint(style.example_code)),
|
NormalCode(s) => write!(writer, "{}", s.paint(style.example_code)),
|
||||||
Description(s) => writeln!(writer, " {}", s.paint(style.description)),
|
Description(s) => write!(writer, "{}", s.paint(style.description)),
|
||||||
Text(s) => writeln!(writer, " {}", s.paint(style.example_text)),
|
Text(s) => write!(writer, "{}", s.paint(style.example_text)),
|
||||||
|
Indent(n) => write!(writer, "{:n$}", ' '),
|
||||||
Linebreak => writeln!(writer),
|
Linebreak => writeln!(writer),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
src/types.rs
11
src/types.rs
|
|
@ -118,18 +118,14 @@ impl PlatformType {
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize, clap::ValueEnum)]
|
#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize, clap::ValueEnum)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
|
#[derive(Default)]
|
||||||
pub enum ColorOptions {
|
pub enum ColorOptions {
|
||||||
Always,
|
Always,
|
||||||
|
#[default]
|
||||||
Auto,
|
Auto,
|
||||||
Never,
|
Never,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ColorOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Auto
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub enum LineType {
|
pub enum LineType {
|
||||||
Empty,
|
Empty,
|
||||||
|
|
@ -205,6 +201,8 @@ pub enum PathSource {
|
||||||
EnvVar,
|
EnvVar,
|
||||||
/// Config file
|
/// Config file
|
||||||
ConfigFile,
|
ConfigFile,
|
||||||
|
/// CLI argument override
|
||||||
|
Cli,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for PathSource {
|
impl fmt::Display for PathSource {
|
||||||
|
|
@ -216,6 +214,7 @@ impl fmt::Display for PathSource {
|
||||||
Self::OsConvention => "OS convention",
|
Self::OsConvention => "OS convention",
|
||||||
Self::EnvVar => "env variable",
|
Self::EnvVar => "env variable",
|
||||||
Self::ConfigFile => "config file",
|
Self::ConfigFile => "config file",
|
||||||
|
Self::Cli => "command line argument",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
tests/cache/pages.en/common/playerctl.md
vendored
Normal file
32
tests/cache/pages.en/common/playerctl.md
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# playerctl
|
||||||
|
|
||||||
|
> Control media players via MPRIS.
|
||||||
|
> More information: <https://github.com/altdesktop/playerctl#using-the-cli>.
|
||||||
|
|
||||||
|
- Toggle play:
|
||||||
|
|
||||||
|
`playerctl play-pause`
|
||||||
|
|
||||||
|
- Skip to the next track:
|
||||||
|
|
||||||
|
`playerctl next`
|
||||||
|
|
||||||
|
- Go back to the previous track:
|
||||||
|
|
||||||
|
`playerctl previous`
|
||||||
|
|
||||||
|
- List all players:
|
||||||
|
|
||||||
|
`playerctl {{[-l|--list-all]}}`
|
||||||
|
|
||||||
|
- Send a command to a specific player:
|
||||||
|
|
||||||
|
`playerctl {{[-p|--player]}} {{player_name}} {{play-pause|next|previous|...}}`
|
||||||
|
|
||||||
|
- Send a command to all players:
|
||||||
|
|
||||||
|
`playerctl {{[-a|--all-players]}} {{play-pause|next|previous|...}}`
|
||||||
|
|
||||||
|
- Display metadata about the current track:
|
||||||
|
|
||||||
|
`playerctl metadata {{[-f|--format]}} "{{Now playing: \{\{artist\}\} - \{\{album\}\} - \{\{title\}\}}}"`
|
||||||
544
tests/lib.rs
544
tests/lib.rs
|
|
@ -1,7 +1,7 @@
|
||||||
//! Integration tests.
|
//! Integration tests.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fs::{self, create_dir_all, File},
|
fs::{self, File, create_dir_all},
|
||||||
io::{self, Write},
|
io::{self, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Command,
|
process::Command,
|
||||||
|
|
@ -11,6 +11,7 @@ use std::{
|
||||||
use assert_cmd::prelude::*;
|
use assert_cmd::prelude::*;
|
||||||
use predicates::{
|
use predicates::{
|
||||||
boolean::PredicateBooleanExt,
|
boolean::PredicateBooleanExt,
|
||||||
|
ord::eq,
|
||||||
prelude::predicate::str::{contains, diff, is_empty, is_match},
|
prelude::predicate::str::{contains, diff, is_empty, is_match},
|
||||||
};
|
};
|
||||||
use tempfile::{Builder as TempfileBuilder, TempDir};
|
use tempfile::{Builder as TempfileBuilder, TempDir};
|
||||||
|
|
@ -36,14 +37,11 @@ impl TestEnv {
|
||||||
features: vec![],
|
features: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
create_dir_all(&this.cache_dir()).unwrap();
|
create_dir_all(this.cache_dir()).unwrap();
|
||||||
create_dir_all(&this.config_dir()).unwrap();
|
create_dir_all(this.config_dir()).unwrap();
|
||||||
create_dir_all(&this.custom_pages_dir()).unwrap();
|
create_dir_all(this.custom_pages_dir()).unwrap();
|
||||||
|
|
||||||
this.append_to_config(format!(
|
this.init_config();
|
||||||
"directories.cache_dir = '{}'\n",
|
|
||||||
this.cache_dir().to_str().unwrap(),
|
|
||||||
));
|
|
||||||
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
@ -69,6 +67,33 @@ impl TestEnv {
|
||||||
.write_all(content.as_ref().as_bytes())
|
.write_all(content.as_ref().as_bytes())
|
||||||
.expect("Failed to append to config file.");
|
.expect("Failed to append to config file.");
|
||||||
}
|
}
|
||||||
|
fn delete_config(&self) {
|
||||||
|
fs::remove_file(self.config_dir().join("config.toml")).unwrap();
|
||||||
|
}
|
||||||
|
fn init_config(&self) {
|
||||||
|
self.append_to_config(format!(
|
||||||
|
"directories.cache_dir = '{}'\n",
|
||||||
|
self.cache_dir().to_str().unwrap(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_secondary_config(self) -> Self {
|
||||||
|
self.append_to_secondary_config(format!(
|
||||||
|
"directories.cache_dir = '{}'\n",
|
||||||
|
self.cache_dir().to_str().unwrap(),
|
||||||
|
));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_to_secondary_config(&self, content: impl AsRef<str>) {
|
||||||
|
File::options()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(self.config_dir().join("config-secondary.toml"))
|
||||||
|
.expect("Failed to open config file")
|
||||||
|
.write_all(content.as_ref().as_bytes())
|
||||||
|
.expect("Failed to append to config file.");
|
||||||
|
}
|
||||||
|
|
||||||
fn remove_initial_config(self) -> Self {
|
fn remove_initial_config(self) -> Self {
|
||||||
let _ = fs::remove_file(self.config_dir().join("config.toml"));
|
let _ = fs::remove_file(self.config_dir().join("config.toml"));
|
||||||
|
|
@ -82,7 +107,21 @@ impl TestEnv {
|
||||||
|
|
||||||
/// Add entry for that environment to an OS-specific subfolder.
|
/// Add entry for that environment to an OS-specific subfolder.
|
||||||
fn add_os_entry(&self, os: &str, name: &str, contents: &str) {
|
fn add_os_entry(&self, os: &str, name: &str, contents: &str) {
|
||||||
let dir = self.cache_dir().join(TLDR_PAGES_DIR).join("pages").join(os);
|
self.add_os_lang_entry(os, "en", name, contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add entry for that environment to a language-specific subfolder.
|
||||||
|
fn add_lang_entry(&self, lang: &str, name: &str, contents: &str) {
|
||||||
|
self.add_os_lang_entry("common", lang, name, contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add entry for that environment to an OS- and language specific subfolder.
|
||||||
|
fn add_os_lang_entry(&self, os: &str, lang: &str, name: &str, contents: &str) {
|
||||||
|
let dir = self
|
||||||
|
.cache_dir()
|
||||||
|
.join(TLDR_PAGES_DIR)
|
||||||
|
.join(format!("pages.{lang}"))
|
||||||
|
.join(os);
|
||||||
create_dir_all(&dir).unwrap();
|
create_dir_all(&dir).unwrap();
|
||||||
|
|
||||||
fs::write(dir.join(format!("{name}.md")), contents.as_bytes()).unwrap();
|
fs::write(dir.join(format!("{name}.md")), contents.as_bytes()).unwrap();
|
||||||
|
|
@ -129,6 +168,19 @@ impl TestEnv {
|
||||||
}
|
}
|
||||||
let run = build.run().expect("Failed to build tealdeer for testing");
|
let run = build.run().expect("Failed to build tealdeer for testing");
|
||||||
let mut cmd = run.command();
|
let mut cmd = run.command();
|
||||||
|
|
||||||
|
// Avoid inheriting those from the test process. We can't just use .env_clear() because
|
||||||
|
// this breaks tests on Windows in GitHub Actions.
|
||||||
|
let relevant_env_variables = [
|
||||||
|
"LANG",
|
||||||
|
"LANGUAGE",
|
||||||
|
"TEALDEER_CACHE_DIR",
|
||||||
|
"EDITOR",
|
||||||
|
"NO_COLOR",
|
||||||
|
];
|
||||||
|
for variable_name in relevant_env_variables {
|
||||||
|
cmd.env_remove(variable_name);
|
||||||
|
}
|
||||||
cmd.env("TEALDEER_CONFIG_DIR", self.config_dir().to_str().unwrap());
|
cmd.env("TEALDEER_CONFIG_DIR", self.config_dir().to_str().unwrap());
|
||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
|
|
@ -183,6 +235,61 @@ fn test_cannot_build_without_tls_feature() {
|
||||||
let _ = TestEnv::new().no_default_features().command();
|
let _ = TestEnv::new().no_default_features().command();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_the_correct_config() {
|
||||||
|
let testenv = TestEnv::new()
|
||||||
|
.install_default_cache()
|
||||||
|
.create_secondary_config();
|
||||||
|
testenv.append_to_secondary_config(include_str!("style-config.toml"));
|
||||||
|
|
||||||
|
let expected_default = include_str!("rendered/inkscape-default.expected");
|
||||||
|
let expected_with_config = include_str!("rendered/inkscape-with-config.expected");
|
||||||
|
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args(["--color", "always", "inkscape-v2"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(diff(expected_default));
|
||||||
|
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args([
|
||||||
|
"--color",
|
||||||
|
"always",
|
||||||
|
"--config-path",
|
||||||
|
testenv
|
||||||
|
.config_dir()
|
||||||
|
.join("config-secondary.toml")
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"inkscape-v2",
|
||||||
|
])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(diff(expected_with_config));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fail_on_custom_config_path_is_directory() {
|
||||||
|
let testenv = TestEnv::new();
|
||||||
|
let error = if cfg!(windows) {
|
||||||
|
"Access is denied"
|
||||||
|
} else {
|
||||||
|
"Is a directory"
|
||||||
|
};
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args([
|
||||||
|
"--config-path",
|
||||||
|
testenv.config_dir().to_str().unwrap(),
|
||||||
|
"sl",
|
||||||
|
])
|
||||||
|
.assert()
|
||||||
|
.failure()
|
||||||
|
.stderr(contains(error));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_missing_cache() {
|
fn test_missing_cache() {
|
||||||
TestEnv::new()
|
TestEnv::new()
|
||||||
|
|
@ -193,6 +300,16 @@ fn test_missing_cache() {
|
||||||
.stderr(contains("Page cache not found. Please run `tldr --update`"));
|
.stderr(contains("Page cache not found. Please run `tldr --update`"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tealdeer_page_works_without_cache() {
|
||||||
|
TestEnv::new()
|
||||||
|
.command()
|
||||||
|
.args(["tealdeer"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(contains("for your installed tealdeer version"));
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")]
|
#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_update_cache_default_features() {
|
fn test_update_cache_default_features() {
|
||||||
|
|
@ -282,6 +399,23 @@ fn test_quiet_cache() {
|
||||||
.stdout(is_empty());
|
.stdout(is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clear_only_pages_directory() {
|
||||||
|
let testenv = TestEnv::new().install_default_cache();
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args(["--clear-cache"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stderr(contains(format!(
|
||||||
|
"Successfully cleared cache at `{}`.",
|
||||||
|
testenv.cache_dir().join(TLDR_PAGES_DIR).to_str().unwrap(),
|
||||||
|
)));
|
||||||
|
|
||||||
|
assert!(testenv.cache_dir().is_dir());
|
||||||
|
assert!(!testenv.cache_dir().join(TLDR_PAGES_DIR).exists());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_warn_invalid_tls_backend() {
|
fn test_warn_invalid_tls_backend() {
|
||||||
let testenv = TestEnv::new()
|
let testenv = TestEnv::new()
|
||||||
|
|
@ -311,6 +445,21 @@ fn test_quiet_failures() {
|
||||||
.stdout(is_empty());
|
.stdout(is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quiet_missing_cache() {
|
||||||
|
let testenv = TestEnv::new();
|
||||||
|
|
||||||
|
for args in [["--list", "--quiet"], ["sl", "--quiet"]] {
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args(args)
|
||||||
|
.assert()
|
||||||
|
.failure()
|
||||||
|
.stdout(is_empty())
|
||||||
|
.stderr(is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_quiet_old_cache() {
|
fn test_quiet_old_cache() {
|
||||||
let testenv = TestEnv::new().install_default_cache();
|
let testenv = TestEnv::new().install_default_cache();
|
||||||
|
|
@ -336,6 +485,26 @@ fn test_quiet_old_cache() {
|
||||||
.stderr(contains("The cache hasn't been updated for ").not());
|
.stderr(contains("The cache hasn't been updated for ").not());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_warn_cache_age_never() {
|
||||||
|
let testenv = TestEnv::new().install_default_cache();
|
||||||
|
|
||||||
|
filetime::set_file_mtime(
|
||||||
|
testenv.cache_dir().join(TLDR_PAGES_DIR),
|
||||||
|
filetime::FileTime::from_unix_time(1, 0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
testenv.append_to_config("[updates]\nwarn_cache_age = \"never\"\n");
|
||||||
|
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args(["which"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stderr(contains("The cache hasn't been updated for ").not());
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")]
|
#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_cache_directory_path() {
|
fn test_create_cache_directory_path() {
|
||||||
|
|
@ -356,38 +525,59 @@ fn test_create_cache_directory_path() {
|
||||||
.assert()
|
.assert()
|
||||||
.success()
|
.success()
|
||||||
.stderr(contains(format!(
|
.stderr(contains(format!(
|
||||||
"Successfully created cache directory path `{}`.",
|
"Successfully created cache directory `{}`.",
|
||||||
internal_cache_dir.to_str().unwrap()
|
internal_cache_dir.join(TLDR_PAGES_DIR).to_str().unwrap()
|
||||||
)))
|
)))
|
||||||
.stderr(contains("Successfully updated cache."));
|
.stderr(contains("Successfully updated cache."));
|
||||||
|
|
||||||
assert!(internal_cache_dir.is_dir());
|
assert!(internal_cache_dir.is_dir());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cache_location_not_a_directory() {
|
fn test_cache_location_not_a_directory() {
|
||||||
let testenv = TestEnv::new().remove_initial_config();
|
let testenv = TestEnv::new();
|
||||||
let cache_dir = &testenv.cache_dir();
|
let cache_dir = &testenv.cache_dir();
|
||||||
let internal_file = cache_dir.join("internal");
|
File::create(cache_dir.join(TLDR_PAGES_DIR)).unwrap();
|
||||||
File::create(&internal_file).unwrap();
|
|
||||||
|
|
||||||
testenv.append_to_config(format!(
|
|
||||||
"directories.cache_dir = '{}'\n",
|
|
||||||
internal_file.to_str().unwrap()
|
|
||||||
));
|
|
||||||
|
|
||||||
testenv
|
testenv
|
||||||
.command()
|
.command()
|
||||||
.arg("--update")
|
.arg("--list")
|
||||||
.assert()
|
.assert()
|
||||||
.failure()
|
.failure()
|
||||||
.stderr(contains(format!(
|
.stderr(contains(format!(
|
||||||
"Cache directory path `{}` is not a directory",
|
"Cache directory `{}` exists, but is not a directory.",
|
||||||
internal_file.display(),
|
cache_dir.join(TLDR_PAGES_DIR).display(),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn test_cache_location_permission_denied() {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
let testenv = TestEnv::new().install_default_cache();
|
||||||
|
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.arg("--list")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stderr(contains("Permission denied").not());
|
||||||
|
|
||||||
|
// Make cache directory unreadable
|
||||||
|
let cache_dir = testenv.cache_dir();
|
||||||
|
let mut permissions = cache_dir.metadata().unwrap().permissions();
|
||||||
|
permissions.set_mode(0o0);
|
||||||
|
fs::set_permissions(cache_dir, permissions).unwrap();
|
||||||
|
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.arg("--list")
|
||||||
|
.assert()
|
||||||
|
.failure()
|
||||||
|
.stderr(contains("Permission denied"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cache_location_source() {
|
fn test_cache_location_source() {
|
||||||
let testenv = TestEnv::new().remove_initial_config();
|
let testenv = TestEnv::new().remove_initial_config();
|
||||||
|
|
@ -438,8 +628,9 @@ fn test_setup_seed_config() {
|
||||||
.failure()
|
.failure()
|
||||||
.stderr(contains("A configuration file already exists"));
|
.stderr(contains("A configuration file already exists"));
|
||||||
|
|
||||||
let testenv = testenv.remove_initial_config();
|
assert!(testenv.config_dir().join("config.toml").is_file());
|
||||||
|
|
||||||
|
let testenv = testenv.remove_initial_config();
|
||||||
testenv
|
testenv
|
||||||
.command()
|
.command()
|
||||||
.args(["--seed-config"])
|
.args(["--seed-config"])
|
||||||
|
|
@ -448,6 +639,48 @@ fn test_setup_seed_config() {
|
||||||
.stderr(contains("Successfully created seed config file here"));
|
.stderr(contains("Successfully created seed config file here"));
|
||||||
|
|
||||||
assert!(testenv.config_dir().join("config.toml").is_file());
|
assert!(testenv.config_dir().join("config.toml").is_file());
|
||||||
|
|
||||||
|
// Create parent directories as needed for the default config path.
|
||||||
|
fs::remove_dir_all(testenv.config_dir()).unwrap();
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args(["--seed-config"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stderr(contains("Successfully created seed config file here"));
|
||||||
|
|
||||||
|
assert!(testenv.config_dir().join("config.toml").is_file());
|
||||||
|
|
||||||
|
// Write the default config to --config-path if specified by the user
|
||||||
|
// at the same time.
|
||||||
|
let custom_config_path = testenv.config_dir().join("config_custom.toml");
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args([
|
||||||
|
"--seed-config",
|
||||||
|
"--config-path",
|
||||||
|
custom_config_path.to_str().unwrap(),
|
||||||
|
])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stderr(contains("Successfully created seed config file here"));
|
||||||
|
|
||||||
|
assert!(custom_config_path.is_file());
|
||||||
|
|
||||||
|
// DON'T create parent directories for a custom config path.
|
||||||
|
fs::remove_dir_all(testenv.config_dir()).unwrap();
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args([
|
||||||
|
"--seed-config",
|
||||||
|
"--config-path",
|
||||||
|
custom_config_path.to_str().unwrap(),
|
||||||
|
])
|
||||||
|
.assert()
|
||||||
|
.failure()
|
||||||
|
.stderr(contains("Could not create config file"));
|
||||||
|
|
||||||
|
assert!(!custom_config_path.is_file());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -504,11 +737,41 @@ fn test_os_specific_page() {
|
||||||
.success();
|
.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_platforms() {
|
||||||
|
let testenv = TestEnv::new();
|
||||||
|
testenv.add_os_entry("sunos", "sunos-command", "");
|
||||||
|
|
||||||
|
let set_config_platforms = |platforms| {
|
||||||
|
testenv.delete_config();
|
||||||
|
testenv.init_config();
|
||||||
|
testenv.append_to_config(format!("search.platforms = {platforms}"));
|
||||||
|
};
|
||||||
|
|
||||||
|
// By default all platforms are searched
|
||||||
|
testenv.command().arg("sunos-command").assert().success();
|
||||||
|
|
||||||
|
set_config_platforms("[]");
|
||||||
|
testenv.command().arg("sunos-command").assert().failure();
|
||||||
|
|
||||||
|
set_config_platforms("['linux']");
|
||||||
|
testenv.command().arg("sunos-command").assert().failure();
|
||||||
|
|
||||||
|
set_config_platforms("['sunos']");
|
||||||
|
testenv.command().arg("sunos-command").assert().success();
|
||||||
|
|
||||||
|
set_config_platforms("['linux', 'all']");
|
||||||
|
testenv.command().arg("sunos-command").assert().success();
|
||||||
|
|
||||||
|
set_config_platforms("['current', 'all']");
|
||||||
|
testenv.command().arg("sunos-command").assert().success();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_markdown_rendering() {
|
fn test_markdown_rendering() {
|
||||||
let testenv = TestEnv::new().install_default_cache();
|
let testenv = TestEnv::new().install_default_cache();
|
||||||
|
|
||||||
let expected = include_str!("cache/pages/common/which.md");
|
let expected = include_str!("cache/pages.en/common/which.md");
|
||||||
testenv
|
testenv
|
||||||
.command()
|
.command()
|
||||||
.args(["--raw", "which"])
|
.args(["--raw", "which"])
|
||||||
|
|
@ -570,6 +833,24 @@ fn test_rendering_color_never() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An end-to-end integration test for the indent config option
|
||||||
|
#[test]
|
||||||
|
fn test_rendering_with_indentation() {
|
||||||
|
let testenv = TestEnv::new().install_default_cache();
|
||||||
|
let expected_custom_indentation = include_str!("rendered/inkscape-compact-no-color.expected");
|
||||||
|
|
||||||
|
// Configure to set base and command indents
|
||||||
|
testenv.append_to_config("display.indent.base = 3\n");
|
||||||
|
testenv.append_to_config("display.indent.command = 1\n");
|
||||||
|
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args(["--color", "never", "inkscape-v2"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(diff(expected_custom_indentation));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_rendering_i18n() {
|
fn test_rendering_i18n() {
|
||||||
_test_correct_rendering(
|
_test_correct_rendering(
|
||||||
|
|
@ -596,6 +877,42 @@ fn test_correct_rendering_with_config() {
|
||||||
.stdout(diff(expected));
|
.stdout(diff(expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An end-to-end integration test for rendering with show_title config option enabled.
|
||||||
|
#[test]
|
||||||
|
fn test_show_title_config() {
|
||||||
|
// Test that default behavior without show_title shows no title
|
||||||
|
let testenv = TestEnv::new().install_default_cache();
|
||||||
|
let expected_no_title = include_str!("rendered/inkscape-default.expected");
|
||||||
|
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args(["--color", "always", "inkscape-v2"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(diff(expected_no_title));
|
||||||
|
|
||||||
|
// Configure to enable show_title
|
||||||
|
testenv.append_to_config("display.show_title = true\n");
|
||||||
|
|
||||||
|
let expected_no_color = include_str!("rendered/inkscape-with-title-no-color.expected");
|
||||||
|
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args(["inkscape-v2"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(diff(expected_no_color));
|
||||||
|
|
||||||
|
let expected = include_str!("rendered/inkscape-with-title.expected");
|
||||||
|
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args(["--color", "always", "inkscape-v2"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(diff(expected));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_spaces_find_command() {
|
fn test_spaces_find_command() {
|
||||||
let testenv = TestEnv::new().install_default_cache();
|
let testenv = TestEnv::new().install_default_cache();
|
||||||
|
|
@ -709,6 +1026,14 @@ fn test_macos_is_alias_for_osx() {
|
||||||
.args(["--platform", "osx", "--list"])
|
.args(["--platform", "osx", "--list"])
|
||||||
.assert()
|
.assert()
|
||||||
.stdout("maconly\n");
|
.stdout("maconly\n");
|
||||||
|
|
||||||
|
testenv.append_to_config("search.platforms = ['osx']\n");
|
||||||
|
testenv.command().arg("--list").assert().stdout("maconly\n");
|
||||||
|
|
||||||
|
testenv.delete_config();
|
||||||
|
testenv.init_config();
|
||||||
|
testenv.append_to_config("search.platforms = ['macos']\n");
|
||||||
|
testenv.command().arg("--list").assert().stdout("maconly\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -727,6 +1052,89 @@ fn test_common_platform_is_used_as_fallback() {
|
||||||
.success();
|
.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_search_language_precedence() {
|
||||||
|
let testenv = TestEnv::new();
|
||||||
|
for lang in ["en", "de", "it", "fr", "pl", "nl"] {
|
||||||
|
testenv.add_lang_entry(lang, lang, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::type_complexity)]
|
||||||
|
let run = |cases: &[(Vec<(&str, &str)>, Vec<&str>, &str)]| {
|
||||||
|
for (extra_env, extra_args, expected) in cases {
|
||||||
|
let mut cmd = testenv.command();
|
||||||
|
for (key, value) in extra_env {
|
||||||
|
cmd.env(key, value);
|
||||||
|
}
|
||||||
|
cmd.args(extra_args);
|
||||||
|
cmd.arg("--list");
|
||||||
|
cmd.assert().success().stdout(eq(*expected));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let env_cases = &[
|
||||||
|
(vec![], vec![], "en\n"),
|
||||||
|
(vec![("LANGUAGE", "de:it")], vec![], "en\n"),
|
||||||
|
(
|
||||||
|
vec![("LANG", "fr"), ("LANGUAGE", "de:it")],
|
||||||
|
vec![],
|
||||||
|
"de\nen\nfr\nit\n",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
vec![("LANG", "fr"), ("LANGUAGE", "de:it")],
|
||||||
|
vec!["--language", "pl"],
|
||||||
|
"pl\n",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
run(env_cases);
|
||||||
|
|
||||||
|
// Environment is only used when config setting is not set
|
||||||
|
testenv.append_to_config("search.languages = ['nl']\n");
|
||||||
|
let config_cases = &[
|
||||||
|
(vec![], vec![], "nl\n"),
|
||||||
|
(vec![("LANGUAGE", "de:it")], vec![], "nl\n"),
|
||||||
|
(vec![("LANG", "fr"), ("LANGUAGE", "de:it")], vec![], "nl\n"),
|
||||||
|
(
|
||||||
|
vec![("LANG", "fr"), ("LANGUAGE", "de:it")],
|
||||||
|
vec!["--language", "pl"],
|
||||||
|
"pl\n",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
run(config_cases);
|
||||||
|
|
||||||
|
// The above update setting does not change anything
|
||||||
|
testenv.append_to_config("updates.download_languages = ['cz']");
|
||||||
|
run(config_cases);
|
||||||
|
testenv.delete_config();
|
||||||
|
testenv.init_config();
|
||||||
|
testenv.append_to_config("updates.download_languages = ['cz']");
|
||||||
|
run(env_cases);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")]
|
||||||
|
#[test]
|
||||||
|
fn test_update_language_arg() {
|
||||||
|
let testenv = TestEnv::new();
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.env("LANG", "it")
|
||||||
|
.arg("--update")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stderr(contains("it"))
|
||||||
|
.stderr(contains("en"));
|
||||||
|
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.env("LANG", "en")
|
||||||
|
.args(["--language", "it"])
|
||||||
|
.arg("--update")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stderr(contains("it"))
|
||||||
|
.stderr(contains("en").not());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_list_flag_rendering() {
|
fn test_list_flag_rendering() {
|
||||||
let testenv = TestEnv::new().write_custom_pages_config();
|
let testenv = TestEnv::new().write_custom_pages_config();
|
||||||
|
|
@ -882,6 +1290,19 @@ fn test_autoupdate_cache() {
|
||||||
check_cache_updated(false);
|
check_cache_updated(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Regression test: `--no-auto-update` should be usable together with `--list`,
|
||||||
|
/// since the auto-update gate in `main.rs` also applies to `--list`.
|
||||||
|
#[test]
|
||||||
|
fn test_no_auto_update_with_list() {
|
||||||
|
let testenv = TestEnv::new().install_default_cache();
|
||||||
|
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args(["--list", "--no-auto-update"])
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
}
|
||||||
|
|
||||||
/// End-end test to ensure .page.md files overwrite pages in cache_dir
|
/// End-end test to ensure .page.md files overwrite pages in cache_dir
|
||||||
#[test]
|
#[test]
|
||||||
fn test_custom_page_overwrites() {
|
fn test_custom_page_overwrites() {
|
||||||
|
|
@ -892,7 +1313,7 @@ fn test_custom_page_overwrites() {
|
||||||
// Add .page.md file to custom_pages_dir
|
// Add .page.md file to custom_pages_dir
|
||||||
testenv.add_page_entry(
|
testenv.add_page_entry(
|
||||||
"inkscape-v2",
|
"inkscape-v2",
|
||||||
include_str!("cache/pages/common/inkscape-v2.md"),
|
include_str!("cache/pages.en/common/inkscape-v2.md"),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load expected output
|
// Load expected output
|
||||||
|
|
@ -935,7 +1356,7 @@ fn test_custom_patch_does_not_append_to_custom() {
|
||||||
// In addition to the page in the cache, add the same page as a custom page.
|
// In addition to the page in the cache, add the same page as a custom page.
|
||||||
testenv.add_page_entry(
|
testenv.add_page_entry(
|
||||||
"inkscape-v2",
|
"inkscape-v2",
|
||||||
include_str!("cache/pages/common/inkscape-v2.md"),
|
include_str!("cache/pages.en/common/inkscape-v2.md"),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load expected output
|
// Load expected output
|
||||||
|
|
@ -998,7 +1419,7 @@ fn test_raw_render_file() {
|
||||||
let path = testenv
|
let path = testenv
|
||||||
.cache_dir()
|
.cache_dir()
|
||||||
.join(TLDR_PAGES_DIR)
|
.join(TLDR_PAGES_DIR)
|
||||||
.join("pages/common/inkscape-v1.md");
|
.join("pages.en/common/inkscape-v1.md");
|
||||||
let mut args = vec!["--color", "never", "-f", &path.to_str().unwrap()];
|
let mut args = vec!["--color", "never", "-f", &path.to_str().unwrap()];
|
||||||
|
|
||||||
// Default render
|
// Default render
|
||||||
|
|
@ -1018,7 +1439,7 @@ fn test_raw_render_file() {
|
||||||
.args(&args)
|
.args(&args)
|
||||||
.assert()
|
.assert()
|
||||||
.success()
|
.success()
|
||||||
.stdout(diff(include_str!("cache/pages/common/inkscape-v1.md")));
|
.stdout(diff(include_str!("cache/pages.en/common/inkscape-v1.md")));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn touch_custom_page(testenv: &TestEnv) {
|
fn touch_custom_page(testenv: &TestEnv) {
|
||||||
|
|
@ -1080,3 +1501,68 @@ fn test_custom_pages_dir_is_not_dir() {
|
||||||
.assert()
|
.assert()
|
||||||
.failure();
|
.failure();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod placeholder_format {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_long() {
|
||||||
|
let testenv = TestEnv::new().install_default_cache();
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args(["--color=always", "playerctl"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(eq(include_str!("rendered/playerctl-long.expected")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config() {
|
||||||
|
let cases = [
|
||||||
|
("short", include_str!("rendered/playerctl-short.expected")),
|
||||||
|
("long", include_str!("rendered/playerctl-long.expected")),
|
||||||
|
("both", include_str!("rendered/playerctl-both.expected")),
|
||||||
|
];
|
||||||
|
for (setting, expected) in cases {
|
||||||
|
let testenv = TestEnv::new().install_default_cache();
|
||||||
|
testenv.append_to_config(format!("display.placeholder_format = \"{setting}\"\n"));
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args(["--color=always", "playerctl"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(eq(expected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli() {
|
||||||
|
let testenv = TestEnv::new().install_default_cache();
|
||||||
|
testenv.append_to_config("display.placeholder_format = \"both\"\n");
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args(["--color=always", "--short-options", "playerctl"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(eq(include_str!("rendered/playerctl-short.expected")));
|
||||||
|
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args(["--color=always", "--long-options", "playerctl"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(eq(include_str!("rendered/playerctl-long.expected")));
|
||||||
|
|
||||||
|
testenv
|
||||||
|
.command()
|
||||||
|
.args([
|
||||||
|
"--color=always",
|
||||||
|
"--short-options",
|
||||||
|
"--long-options",
|
||||||
|
"playerctl",
|
||||||
|
])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(eq(include_str!("rendered/playerctl-both.expected")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,33 +5,33 @@
|
||||||
|
|
||||||
[32m利用可能なパーケージとバージョンのリストの更新(他の`apt`コマンドの前での実行を推奨):[0m
|
[32m利用可能なパーケージとバージョンのリストの更新(他の`apt`コマンドの前での実行を推奨):[0m
|
||||||
|
|
||||||
[36m [0m[36msudo [0m[36mapt[0m[36m update[0m
|
[36msudo [0m[36mapt[0m[36m update[0m
|
||||||
|
|
||||||
[32m指定されたパッケージの検索:[0m
|
[32m指定されたパッケージの検索:[0m
|
||||||
|
|
||||||
[36m [0m[36mapt[0m[36m search [0m[4;36mパッケージ[0m
|
[36mapt[0m[36m search [0m[4;36mパッケージ[0m
|
||||||
|
|
||||||
[32mパッケージの情報を出力:[0m
|
[32mパッケージの情報を出力:[0m
|
||||||
|
|
||||||
[36m [0m[36mapt[0m[36m show [0m[4;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
|
[36msudo [0m[36mapt[0m[36m install [0m[4;36mパッケージ[0m
|
||||||
|
|
||||||
[32mパッケージの削除(`sudo apt remove --purge`の場合設定ファイルも削除):[0m
|
[32mパッケージの削除(`sudo apt remove --purge`の場合設定ファイルも削除):[0m
|
||||||
|
|
||||||
[36m [0m[36msudo [0m[36mapt[0m[36m remove [0m[4;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
|
[36msudo [0m[36mapt[0m[36m upgrade[0m
|
||||||
|
|
||||||
[32mインストールできるすべてのパッケージを表示:[0m
|
[32mインストールできるすべてのパッケージを表示:[0m
|
||||||
|
|
||||||
[36m [0m[36mapt[0m[36m list[0m
|
[36mapt[0m[36m list[0m
|
||||||
|
|
||||||
[32mインストールされた全てのパッケージを表示(依存関係も表示):[0m
|
[32mインストールされた全てのパッケージを表示(依存関係も表示):[0m
|
||||||
|
|
||||||
[36m [0m[36mapt[0m[36m list --installed[0m
|
[36mapt[0m[36m list --installed[0m
|
||||||
|
|
||||||
|
|
|
||||||
32
tests/rendered/inkscape-compact-no-color.expected
Normal file
32
tests/rendered/inkscape-compact-no-color.expected
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
An SVG (Scalable Vector Graphics) editing program.
|
||||||
|
Use -z to not open the GUI and only process files in the console.
|
||||||
|
|
||||||
|
Open an SVG file in the Inkscape GUI:
|
||||||
|
|
||||||
|
inkscape filename.svg
|
||||||
|
|
||||||
|
Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):
|
||||||
|
|
||||||
|
inkscape filename.svg -e filename.png
|
||||||
|
|
||||||
|
Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):
|
||||||
|
|
||||||
|
inkscape filename.svg -e filename.png -w 600 -h 400
|
||||||
|
|
||||||
|
Export a single object, given its ID, into a bitmap:
|
||||||
|
|
||||||
|
inkscape filename.svg -i id -e object.png
|
||||||
|
|
||||||
|
Export an SVG document to PDF, converting all texts to paths:
|
||||||
|
|
||||||
|
inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path
|
||||||
|
|
||||||
|
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
|
||||||
|
|
||||||
|
inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
||||||
|
|
||||||
|
Some invalid command just to test the correct highlighting of the command name:
|
||||||
|
|
||||||
|
inkscape --use-inkscape=v3.0 file
|
||||||
|
|
||||||
|
|
@ -4,29 +4,29 @@
|
||||||
|
|
||||||
[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
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[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
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -e [0m[4;36mfilename.png[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
|
[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
|
||||||
|
|
||||||
[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
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -i [0m[4;36mid[0m[36m -e [0m[4;36mobject.png[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
|
[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
|
||||||
|
|
||||||
[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
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit[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
|
[36minkscape[0m[36m --use-inkscape=v3.0 file[0m
|
||||||
|
|
||||||
|
|
|
||||||
34
tests/rendered/inkscape-with-title-no-color.expected
Normal file
34
tests/rendered/inkscape-with-title-no-color.expected
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
|
||||||
|
inkscape
|
||||||
|
|
||||||
|
An SVG (Scalable Vector Graphics) editing program.
|
||||||
|
Use -z to not open the GUI and only process files in the console.
|
||||||
|
|
||||||
|
Open an SVG file in the Inkscape GUI:
|
||||||
|
|
||||||
|
inkscape filename.svg
|
||||||
|
|
||||||
|
Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):
|
||||||
|
|
||||||
|
inkscape filename.svg -e filename.png
|
||||||
|
|
||||||
|
Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):
|
||||||
|
|
||||||
|
inkscape filename.svg -e filename.png -w 600 -h 400
|
||||||
|
|
||||||
|
Export a single object, given its ID, into a bitmap:
|
||||||
|
|
||||||
|
inkscape filename.svg -i id -e object.png
|
||||||
|
|
||||||
|
Export an SVG document to PDF, converting all texts to paths:
|
||||||
|
|
||||||
|
inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path
|
||||||
|
|
||||||
|
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
|
||||||
|
|
||||||
|
inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
||||||
|
|
||||||
|
Some invalid command just to test the correct highlighting of the command name:
|
||||||
|
|
||||||
|
inkscape --use-inkscape=v3.0 file
|
||||||
|
|
||||||
34
tests/rendered/inkscape-with-title.expected
Normal file
34
tests/rendered/inkscape-with-title.expected
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
|
||||||
|
[36minkscape[0m
|
||||||
|
|
||||||
|
An SVG (Scalable Vector Graphics) editing program.
|
||||||
|
Use -z to not open the GUI and only process files in the console.
|
||||||
|
|
||||||
|
[32mOpen an SVG file in the Inkscape GUI:[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m
|
||||||
|
|
||||||
|
[32mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -e [0m[4;36mfilename.png[0m
|
||||||
|
|
||||||
|
[32mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[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
|
||||||
|
|
||||||
|
[32mExport a single object, given its ID, into a bitmap:[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -i [0m[4;36mid[0m[36m -e [0m[4;36mobject.png[0m
|
||||||
|
|
||||||
|
[32mExport an SVG document to PDF, converting all texts to paths:[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
|
||||||
|
|
||||||
|
[32mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit[0m
|
||||||
|
|
||||||
|
[32mSome invalid command just to test the correct highlighting of the command name:[0m
|
||||||
|
|
||||||
|
[36minkscape[0m[36m --use-inkscape=v3.0 file[0m
|
||||||
|
|
||||||
32
tests/rendered/playerctl-both.expected
Normal file
32
tests/rendered/playerctl-both.expected
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
Control media players via MPRIS.
|
||||||
|
More information: <https://github.com/altdesktop/playerctl#using-the-cli>.
|
||||||
|
|
||||||
|
[32mToggle play:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m play-pause[0m
|
||||||
|
|
||||||
|
[32mSkip to the next track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m next[0m
|
||||||
|
|
||||||
|
[32mGo back to the previous track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m previous[0m
|
||||||
|
|
||||||
|
[32mList all players:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m[-l|--list-all][0m
|
||||||
|
|
||||||
|
[32mSend a command to a specific player:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m[-p|--player][0m[36m [0m[4;36mplayer_name[0m[36m [0m[4;36mplay-pause|next|previous|...[0m
|
||||||
|
|
||||||
|
[32mSend a command to all players:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m[-a|--all-players][0m[36m [0m[4;36mplay-pause|next|previous|...[0m
|
||||||
|
|
||||||
|
[32mDisplay metadata about the current track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m metadata [0m[36m[-f|--format][0m[36m "[0m[4;36mNow playing: {{artist}} - {{album}} - {{title}}[0m[36m"[0m
|
||||||
|
|
||||||
32
tests/rendered/playerctl-long.expected
Normal file
32
tests/rendered/playerctl-long.expected
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
Control media players via MPRIS.
|
||||||
|
More information: <https://github.com/altdesktop/playerctl#using-the-cli>.
|
||||||
|
|
||||||
|
[32mToggle play:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m play-pause[0m
|
||||||
|
|
||||||
|
[32mSkip to the next track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m next[0m
|
||||||
|
|
||||||
|
[32mGo back to the previous track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m previous[0m
|
||||||
|
|
||||||
|
[32mList all players:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m--list-all[0m
|
||||||
|
|
||||||
|
[32mSend a command to a specific player:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m--player[0m[36m [0m[4;36mplayer_name[0m[36m [0m[4;36mplay-pause|next|previous|...[0m
|
||||||
|
|
||||||
|
[32mSend a command to all players:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m--all-players[0m[36m [0m[4;36mplay-pause|next|previous|...[0m
|
||||||
|
|
||||||
|
[32mDisplay metadata about the current track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m metadata [0m[36m--format[0m[36m "[0m[4;36mNow playing: {{artist}} - {{album}} - {{title}}[0m[36m"[0m
|
||||||
|
|
||||||
32
tests/rendered/playerctl-short.expected
Normal file
32
tests/rendered/playerctl-short.expected
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
Control media players via MPRIS.
|
||||||
|
More information: <https://github.com/altdesktop/playerctl#using-the-cli>.
|
||||||
|
|
||||||
|
[32mToggle play:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m play-pause[0m
|
||||||
|
|
||||||
|
[32mSkip to the next track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m next[0m
|
||||||
|
|
||||||
|
[32mGo back to the previous track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m previous[0m
|
||||||
|
|
||||||
|
[32mList all players:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m-l[0m
|
||||||
|
|
||||||
|
[32mSend a command to a specific player:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m-p[0m[36m [0m[4;36mplayer_name[0m[36m [0m[4;36mplay-pause|next|previous|...[0m
|
||||||
|
|
||||||
|
[32mSend a command to all players:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m [0m[36m-a[0m[36m [0m[4;36mplay-pause|next|previous|...[0m
|
||||||
|
|
||||||
|
[32mDisplay metadata about the current track:[0m
|
||||||
|
|
||||||
|
[36mplayerctl[0m[36m metadata [0m[36m-f[0m[36m "[0m[4;36mNow playing: {{artist}} - {{album}} - {{title}}[0m[36m"[0m
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue