Compare commits

..

3 commits

Author SHA1 Message Date
Danilo Bargen
4a92bed585 Improve API by introducing a PlatformType struct 2021-12-06 00:35:18 +01:00
Danilo Bargen
3b69359757 Support all platform when listing pages 2021-12-06 00:35:18 +01:00
Danilo Bargen
1c05333de6 Allow setting platform to all
The goal is supporting the special `all` platform that results in pages
for all platforms being listed when calling `--list`. It's part of the
tldr client specification.

However, `All` should not be a variant of the `PlatformType` enum,
because `Current` isn't a `PlatformType` either. Thus, we accept the
string `all` but convert it into the current platform when parsing.

For consistency, the same is done when no platform is specified, by
introducing yet another possible value `current` which is used by
default. This way, we get rid of the `Option`.

To simplify handling of os / platform arguments, a conflict between
`--platform` and `--os` was introduced.
2021-12-06 00:35:14 +01:00
71 changed files with 3044 additions and 5986 deletions

View file

@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"

View file

@ -1,92 +1,93 @@
name: CI
on: on:
push: push:
branches: branches:
- main - master
- "v*.x"
pull_request: pull_request:
schedule: schedule:
- cron: '30 3 * * 2' - cron: '30 3 * * 2'
workflow_dispatch:
name: CI
jobs: jobs:
test: test:
name: run tests name: run tests
strategy: strategy:
matrix: matrix:
platform: [ubuntu-latest, macos-latest, windows-latest, windows-11-arm] platform: [ubuntu-latest, macos-latest, windows-latest]
toolchain: [stable, 1.88.0] # MSRV rust: [1.54, stable]
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@v7 - uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@master - uses: actions-rs/toolchain@v1
with: with:
toolchain: ${{ matrix.toolchain }} toolchain: ${{ matrix.rust }}
- run: mkdir artifacts override: true
- name: Build with default features - name: Build with default features
run: | uses: actions-rs/cargo@v1
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
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
run: |
# 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: with:
name: tldr-debug-build-${{ matrix.platform }}-rust-${{ matrix.toolchain }} command: build
path: artifacts/ - name: Build with all features
uses: actions-rs/cargo@v1
with:
command: build
args: --all-features
- name: Run tests - name: Run tests
run: cargo test --locked -- --test-threads 1 uses: actions-rs/cargo@v1
with:
command: test
args: --all-features
clippy: clippy:
name: run clippy lints name: run clippy lints
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@master - uses: actions-rs/toolchain@v1
with: with:
toolchain: 1.88.0 # MSRV toolchain: stable
components: clippy components: clippy
- name: run clippy lints override: true
run: cargo clippy --locked --all-targets --features logging - uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features
fmt: fmt:
name: run rustfmt name: run rustfmt
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@master - uses: actions-rs/toolchain@v1
with: with:
toolchain: stable toolchain: 1.54
components: rustfmt override: true
- name: run rustfmt - run: rustup component add rustfmt
run: cargo fmt --all -- --check - uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
docs: docs:
name: build docs name: build docs
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v2
- run: ./scripts/get-mdbook.sh - name: Setup mdBook
- name: Setup toolchain uses: peaceiris/actions-mdbook@v1
uses: dtolnay/rust-toolchain@master
with: with:
toolchain: stable mdbook-version: '0.4.4'
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Build - name: Build
run: cargo build --locked uses: actions-rs/cargo@v1
with:
command: build
- name: Ensure that docs can be built - name: Ensure that docs can be built
run: ./mdbook build docs run: cd docs && mdbook build
- name: Generate usage string - name: Generate usage string
run: cargo run --locked -- --help > docs/src/usage-actual.txt run: cargo run -- --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

25
.github/workflows/gh-pages.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: github pages
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v1
with:
mdbook-version: '0.4.4'
- run: cd docs && mdbook build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/book

View file

@ -1,169 +0,0 @@
name: Release
on:
push:
tags:
- "v*" # push events to matching v*, i.e. v1.0, v20.15.10
jobs:
create-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Create release for tag
if: startsWith(github.ref, 'refs/tags/')
run: |
source ./scripts/upload-asset.sh
# Create: <token> <repo> <tag>
create_release ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} "Tealdeer version ${GITHUB_REF#refs/*/v}.\n\nFor the full changelog, see https://github.com/tealdeer-rs/tealdeer/blob/main/CHANGELOG.md.\n\nBinaries were generated automatically in CI, and are therefore unsigned. For a fully trusted release, please build from source."
upload-completions:
needs:
- create-release
runs-on: ubuntu-latest
strategy:
matrix:
target: ["bash", "fish", "zsh"]
steps:
- uses: actions/checkout@v7
- name: Upload completion
if: startsWith(github.ref, 'refs/tags/')
run: |
source ./scripts/upload-asset.sh
# Upload: <token> <repo> <tag> <file> <name>
upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} completion/${{ matrix.target }}_tealdeer completions_${{ matrix.target }}
upload-license:
needs:
- create-release
runs-on: ubuntu-latest
strategy:
matrix:
target: ["MIT", "APACHE"]
steps:
- uses: actions/checkout@v7
- name: Upload license
if: startsWith(github.ref, 'refs/tags/')
run: |
source ./scripts/upload-asset.sh
# Upload: <token> <repo> <tag> <file> <name>
upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} LICENSE-${{ matrix.target }} LICENSE-${{ matrix.target }}.txt
build-linux:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- arch: "x86_64"
libc: "musl"
- arch: "aarch64"
libc: "musl"
- arch: "i686"
libc: "musl"
- arch: "armv7"
libc: "musleabihf"
- arch: "arm"
libc: "musleabi"
- arch: "arm"
libc: "musleabihf"
steps:
- uses: actions/checkout@v7
- name: Pull Docker image
run: docker pull messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }}
- name: Build in Docker
run: docker run --rm -i -v "$(pwd)":/home/rust/src messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} cargo build --locked --release
- name: Strip binary
run: docker run --rm -i -v "$(pwd)":/home/rust/src messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} musl-strip -s /home/rust/src/target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr
- uses: actions/upload-artifact@v7
with:
name: "tealdeer-linux-${{ matrix.arch }}-${{ matrix.libc }}"
path: "target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr"
build-macos:
runs-on: macos-latest
strategy:
matrix:
include:
- arch: "x86_64"
- arch: "aarch64"
steps:
- uses: actions/checkout@v7
- name: Setup toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
targets: "${{ matrix.arch }}-apple-darwin"
- name: Build
run: cargo build --locked --release --target ${{ matrix.arch }}-apple-darwin
- uses: actions/upload-artifact@v7
with:
name: "tealdeer-macos-${{ matrix.arch }}"
path: "target/${{ matrix.arch }}-apple-darwin/release/tldr"
build-windows:
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- arch: "x86_64"
os: windows-latest
- arch: "aarch64"
os: windows-11-arm
steps:
- uses: actions/checkout@v7
- name: Setup toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
targets: "${{ matrix.arch }}-pc-windows-msvc"
- name: Build
run: cargo build --locked --release --target ${{ matrix.arch }}-pc-windows-msvc
- uses: actions/upload-artifact@v7
with:
name: "tealdeer-windows-${{ matrix.arch }}-msvc"
path: "target/${{ matrix.arch }}-pc-windows-msvc/release/tldr.exe"
upload-release:
needs:
- create-release
- build-linux
- build-macos
- build-windows
runs-on: ubuntu-latest
strategy:
matrix:
target:
- linux-x86_64-musl
- linux-aarch64-musl
- linux-i686-musl
- linux-armv7-musleabihf
- linux-arm-musleabi
- linux-arm-musleabihf
- macos-x86_64
- macos-aarch64
- windows-x86_64-msvc
- windows-aarch64-msvc
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
- name: Upload binary
if: startsWith(github.ref, 'refs/tags/')
run: |
source ./scripts/upload-asset.sh
# Move/rename file
mkdir out && cd out
if [[ "${{ matrix.target }}" == *windows* ]]; then
src="../tealdeer-${{ matrix.target }}/tldr.exe"
filename="tealdeer-${{ matrix.target }}.exe"
else
src="../tealdeer-${{ matrix.target }}/tldr"
filename="tealdeer-${{ matrix.target }}"
fi
cp $src $filename
# Create checksum
sha256sum "$filename" > "$filename.sha256"
# Upload: <token> <repo> <tag> <file> <name>
upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} $filename $filename
upload_release_file ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }} ${GITHUB_REF#refs/*/} $filename.sha256 $filename.sha256

View file

@ -1,7 +0,0 @@
version: 2
build:
os: ubuntu-26.04
commands:
- ./scripts/get-mdbook.sh
- ./mdbook build docs --dest-dir $READTHEDOCS_OUTPUT/html

View file

@ -10,403 +10,16 @@ Possible log types:
- `[removed]` for deprecated features removed in this release. - `[removed]` for deprecated features removed in this release.
- `[fixed]` for any bug fixes. - `[fixed]` for any bug fixes.
- `[security]` to invite users to upgrade in case of vulnerabilities. - `[security]` to invite users to upgrade in case of vulnerabilities.
- `[docs]` for documentation changes.
- `[chore]` for maintenance work.
### [v1.5.1][v1.5.1], [v1.6.2][v1.6.2], [v1.7.3][v1.7.3] (2026-01-25)
Today I am releasing three patch updates for outdated versions of tealdeer.
They are minimal patches for Linux distributions that ship old versions of
tealdeer which recently broke due to an upstream change. If you can choose
freely which version of tealdeer to use, I recommend using the latest version of
tealdeer, 1.8.1. For more details, see the "Notes to package maintainers"
section below.
All three updates contain only a single change compared to their respective
previous versions which changes the `ARCHIVE_URL` constant used for updating the
page cache. The reason for this change is that the upstream tldr-pages
repository shut down the domain that clients were previously required to use.
Note that this issue is already fixed in tealdeer 1.8.0 where we introduced a
config file option for changing the URL used at runtime. The versions 1.8.0 and
1.8.1 also use the new domain of the tldr-pages archive by default, so no action
is needed for users of those versions.
#### Changes
- [fixed] Update `ARCHIVE_URL`
#### Notes to package maintainers
I have _not_ updated the lockfile for any of these releases, so the locked
dependency versions are still the same as they were for the previous release in
the respective v1.x series. Updating the lockfile for tealdeer 1.5.0 to remove
any `cargo audit` warnings while also maintaining compatibility with Rust 1.54
also brings larger changes through transitive dependencies, which contradicts my
plan to make this update easy to plug into existing build pipelines.
If you want to build / distribute tealdeer v1.5.1, v1.6.2, or v1.7.3, please use
an up to date Rust toolchain to permit updates to newer versions of (transitive)
dependencies. Do not use the lockfile, instead update to the newest available
dependency versions.
For the same reason, there are no artifacts attached to the GitHub releases of
these versions.
### [v1.8.1][v1.8.1] (2025-11-11)
This patch release tweaks the enabled features for ureq, the library we use to
perform HTTP requests when updating the cache. In particular, support for socks
proxies is now enabled.
#### Changes:
- [added] Enable ureq's socks-proxy feature ([#451])
### [v1.8.0][v1.8.0] (2025-10-03)
One year and one day have passed since tealdeer version 1.7.0 was released, so
it's time for an update! Tealdeer 1.8 comes with a complete rewrite of the page
cache and contains many long awaited improvements around it.
Firstly, tealdeer now supports language-specific downloads. This means that only
the pages matching the configured languages are downloaded when updating the
cache. The languages used for searching pages can be configured separately to
the ones used for updating, so it is possible to download pages in languages
that are not usually queried.
Next to configuring which languages are used for searching, it is now also
possible to specify which platforms are used in the config file. Importantly,
the default behavior for page search has changed so that all platforms are
searched if no page is found for the platform that tealdeer is running on. To
restore the behavior of tealdeer 1.7, users should set
```toml
[search]
platforms = ["current", "common"]
```
in their config file.
Coming back to updating, the default build configuration of tealdeer now
includes multiple TLS backends. This means that tealdeer does not have to be
rebuilt to try out a different TLS backend. The used backend can be chosen in
the config file. By default, tealdeer comes with support for rustls using webpki
certificates or system certificates. Native TLS is supported, but not enabled by
default to avoid build troubles with OpenSSL and musl.
For details, please refer to the [user documentation].
#### Changes:
- [added] Resolve paths in config `[directories]` relative to the config directory ([#306])
- [added] Add `common` platform to CLI ([#401])
- [added] Add configuration option for `archive_source` ([#337])
- [added] Allows configuring TLS backend ([#386])
- [added] Add args: `--edit-page` and `--edit-patch` ([#388])
- [added] Add an option to specify a custom config file to be used ([#422])
- [added] Upload binaries from build step as artifact ([#423])
- [added] Add `search.languages` and `updates.download_languages` settings ([#430])
- [added] Add `search.platforms` config option and search all platforms by default ([#435])
- [added] Add `display.show_title` option to display command titles in output ([#439])
- [chore] Various test improvements ([#399])
- [chore] Add tests for osx/macos alias ([#407])
- [chore] Move most of `main` to `try_main` ([#400])
- [chore] Only create a single temporary directory in integration tests ([#411])
- [chore] Replace reqwest with ureq ([#417])
- [chore] Introduce Language struct ([#425])
- [chore] Cache rewrite ([#416])
- [chore] Allow references in `Config` ([#429])
- [docs] Highlight code examples in user docs ([#440])
- [removed] Remove native-tls from default feature set ([#436])
#### Contributors to this version:
- [Christoph Loy][@beatbrot]
- [Erick Guan][@erickguan]
- [@MHS-0][@MHS-0]
- [Matěj Kafka][@MatejKafka]
- [Nachiket Kanore][@nachiketkanore]
- [Niklas Mohrin][@niklasmohrin]
- [Predrag Minic][@mipedja]
- [@hex1c][@hex1c]
- [lyj][@lengyijun]
Thanks!
#### Notes to package maintainers
1. The MSRV has been bumped to 1.85.
2. Consider whether you want to include the `native-tls` feature in your build
of tealdeer. The feature is disabled for the binaries in the GitHub release
because we target musl, but it might work out of the box for your
distribution.
3. We have added the `ignore-online-tests` feature to automatically mark all
tests that require an internet connection as skipped, so you can use this
feature instead of maintaining a list of these tests yourself.
### [v1.7.2][v1.7.2] (2025-03-18)
This patch release updates the `zip` dependency to mitigate a potential security
vulnerability. A successful attack against tealdeer users would require
manipulation of the tldr pages archive downloaded during an update. As the
archive is downloaded from a trusted source (the tldr-pages organization), it
seems very unlikely that running a version of tealdeer prior to 1.7.2 poses a
security risk. Nevertheless, it cannot hurt to rule out any chance of an attack
by updating tealdeer to version 1.7.2.
For more details, please see https://github.com/advisories/GHSA-94vh-gphv-8pm8.
- [security] Require `zip >= 2.3.0`
- [chore] Run CI on backport branches and on dispatch
### [v1.7.1][v1.7.1] (2024-11-14)
This patch release updates the `yansi` dependency to version 1, so that the
previous versions of `yansi` can be removed from the package sets of Linux
distributions. This change should not impact the behavior of tealdeer.
#### Changes:
- [chore] Upgrade yansi: 0.5.1 -> 1.0.1 ([#389])
#### Contributors to this version:
- [Blair Noctis][@nc7s]
Thanks!
### [v1.7.0][v1.7.0] (2024-10-02)
It's been 24 months since the last release, time for tealdeer 1.7.0! Thanks to
16 individual contributors, a few nice changes and features are included in
this release.
One change is that you can **query multiple platforms at once**. For example:
tldr --platform openbsd --platform linux df
This will show the `df` page for OpenBSD (if available), followed by Linux (if
available), with fallback to the current platform on which tealdeer runs.
What's that `openbsd` thing up there? Yes, there's now **support for the BSD
platforms `freebsd`, `netbsd` and `openbsd`**.
And since we're already talking about platform support: Our **binary releases
now include builds for ARM64 (aka `aarch64`) on macOS (Apple Silicon, M1/M2/M3)
and Linux**. _(Keep in mind that binary releases are generated in CI and are
unsigned. For a trusted build, please compile from source.)_
There's also a breaking change for the folks using [custom pages and
patches](https://tealdeer-rs.github.io/tealdeer/usage_custom_pages.html): These
files now use a `.md` extension. Old files will continue to work, but will
result a deprecation warning being printed when used.
On a personal note, this will be the last release from me
([Danilo](https://github.com/dbrgn/)) as primary maintainer of tealdeer. For
details, see [#376](https://github.com/tealdeer-rs/tealdeer/issues/376).
#### Changes:
- [added] Allow querying multiple platforms ([#300])
- [added] Add BSD platform support ([#354])
- [added] Allow building with native-tls in addition to rustls ([#303])
- [changed] Change custom page files to use a `.md` file extension ([#322])
- [changed] Update to clap v4 for doing command line parsing ([#298])
- [changed] Performance optimization in LineIterator ([#314])
- [changed] Performance optimizations by tweaking Cargo flags ([#355])
- [changed] Include completions in published crate ([#333])
- [changed] Minimal supported Rust version is now 1.75 ([#298])
- [fixed] Fix bash/zsh/fish completions when cache is empty ([#327], [#331])
- [docs] Publish docs only when tagging a release ([#362])
- [docs] List Scoop and Debian packages ([#305], [#315])
- [docs] Add "Tips and Tricks" chapter to user manual ([#342])
- [docs] Various docs improvements ([#293])
- [chore] Improvements to CI workflows ([#324])
- [chore] Update Cargo.toml license field following SPDX 2.1 ([#336])
- [chore] Dependency updates
#### Contributors to this version:
- [Adam Henley][@adamazing]
- [Andrea Frigido][@frisoft]
- [Blair Noctis][@nc7s]
- [Danilo Bargen][@dbrgn]
- [Felix Yan][@felixonmars]
- [Iliia Maleki][@iliya-malecki]
- [JJ Style][@jj-style]
- [K.B.Dharun Krishna][@kbdharun]
- [Linus Walker][@Walker-00]
- [Mohit Raj][@agrmohit]
- [Nicolai Fröhlich][@nifr]
- [Niklas Mohrin][@niklasmohrin]
- [@qknogxxb][@qknogxxb]
- [@tveness][@tveness]
- [Y.D.X.][@YDX-2147483647]
- [Zacchary Dempsey-Plante][@zedseven]
Thanks!
### [v1.6.1][v1.6.1] (2022-10-24)
#### Changes:
- [fixed] Fix path source for custom pages dir ([#297])
- [chore] Update dependendencies ([#299])
#### Contributors to this version:
- [Cyrus Yip][@CyrusYip]
- [Danilo Bargen][@dbrgn]
Thanks!
### [v1.6.0][v1.6.0] (2022-10-02)
It's been 9 months since the last release already! This is not a huge update
feature-wise, but it still contains a few nice new improvements and a few
bugfixes, contributed by 11 different people. The most important new feature is
probably the option to override the cache directory through the config file.
The `TEALDEER_CACHE_DIR` env variable is now deprecated.
A note to packagers: Shell completions have been moved to the `completion/`
subdirectory! Packaging scripts might need to be updated.
#### Changes:
- [added] Allow overriding cache directory through config ([#276])
- [added] Add `--no-auto-update` CLI flag ([#257])
- [added] Show note about auto-updates when cache is missing ([#254])
- [added] Add support for android platform ([#274])
- [added] Add custom pages to list output ([#285])
- [fixed] Cache: Return error if HTTP client cannot be created ([#247])
- [fixed] Handle cache download errors ([#253])
- [fixed] Do not page output of `tldr --update` ([#231])
- [fixed] Create macOS release builds with bundled root certificates ([#272])
- [fixed] Clean up and fix shell completions ([#262])
- [deprecated] The `TEALDEER_CACHE_DIR` env variable is now deprecated ([#276])
- [removed] The `--config-path` command was removed, use `--show-paths` instead ([#290])
- [removed] The `-o/--os` command was removed, use `-p/--platform` instead ([#290])
- [removed] The `-m/--markdown` command was removed, use `-r/--raw` instead ([#290])
- [chore] Move shell completion scripts to their own directory ([#259])
- [chore] Update dependencies ([#271], [#287], [#291])
- [chore] Use anyhow for error handling ([#249])
- [chore] Switch to Rust 2021 edition ([#284])
#### Contributors to this version:
- [@bagohart][@bagohart]
- [@cyqsimon][@cyqsimon]
- [Danilo Bargen][@dbrgn]
- [Danny Mösch][@SimplyDanny]
- [Evan Lloyd New-Schmidt][@newsch]
- [Hans Gaiser][@hgaiser]
- [Kian-Meng Ang][@kianmeng]
- [Marcin Puc][@tranzystorek-io]
- [Niklas Mohrin][@niklasmohrin]
- [Olav de Haas][@Olavhaasie]
- [Simon Perdrisat][@gagarine]
Thanks!
### [v1.5.0][v1.5.0] (2021-12-31)
This is quite a big release with many new features. In the 15 months since the
last release, 59 pull requests from 16 different contributors were merged!
The highlights:
- **Custom pages and patches**: You can now create your own local-only tldr
pages. But not just that, you can also extend existing upstream pages with
your own examples. For more details, see
[the docs](https://tealdeer-rs.github.io/tealdeer/usage_custom_pages.html).
- **Change argument parsing from docopt to clap**: We replaced docopt.rs as
argument parsing library with clap v3, resulting in almost 1 MiB smaller
binaries and a 22% speed increase when rendering a tldr page.
- **Multi-language support**: You can now override the language with `-L/--language`.
- **A new `--show-paths` command**: By running `tldr --show-paths`, you can list
the currently used config dir, cache dir, upstream pages dir and custom pages dir.
- **Compliance with the tldr client spec v1.5**: We renamed `-o/--os` to
`-p/--platform` and implemented transparent lowercasing of the page names.
- **Docs**: The README based documentation has reached its limits. There are
now new mdbook based docs over at
[tealdeer-rs.github.io/tealdeer/](https://tealdeer-rs.github.io/tealdeer/), we hope these
make using tealdeer easier. Of course, documentation improvements are
welcome! Also, if you're confused about how to use a certain feature, feel
free to open an issue, this way we can improve the docs.
Note that the MSRV (Minimal Supported Rust Version) of the project
[changed][i190]:
> When publishing a tealdeer release, the Rust version required to build it
> should be stable for at least a month.
#### Changes:
- [added] Support custom pages and patches ([#142][i142])
- [added] Multi-language support ([#125][i125], [#161][i161])
- [added] Add support for ANSI code and RGB colors ([#148][i148])
- [added] Implement new `--show-paths` command ([#162][i162])
- [added] Support for italic text styling ([#197][i197])
- [added] Allow SunOS platform override ([#176][i176])
- [added] Automatically lowercase page names before lookup ([#227][i227])
- [added] Add "macos" alias for "osx" ([#215][i215])
- [fixed] Consider only standalone command names for styling ([#157][i157])
- [fixed] Fixed and improved zsh completions ([#168][i168])
- [fixed] Create cache directory path if it does not exist ([#174][i174])
- [fixed] Use default style if user-defined style is missing ([#210][i210])
- [changed] Switch from docopt to clap for argument parsing ([#108][i108])
- [changed] Switch from OpenSSL to Rustls ([#187][i187])
- [changed] Performance improvements ([#187][i187])
- [changed] Send all progress logging messages to stderr ([#171][i171])
- [changed] Rename `-o/--os` to `-p/--platform` ([#217][i217])
- [changed] Rename `-m/--markdown` to `-r/--raw` ([#108][i108])
- [deprecated] The `--config-path` command is deprecated, use `--show-paths` instead ([#162][i162])
- [deprecated] The `-o/--os` command is deprecated, use `-p/--platform` instead ([#217][i217])
- [deprecated] The `-m/--markdown` command is deprecated, use `-r/--raw` instead ([#108][i108])
- [docs] New docs at [tealdeer-rs.github.io/tealdeer/](https://tealdeer-rs.github.io/tealdeer/)
- [docs] Add comparative benchmarks with hyperfine ([#163][i163], [README](https://github.com/tealdeer-rs/tealdeer#goals))
- [chore] Download tldr pages archive from their website, not from GitHub ([#213][i213])
- [chore] Bump MSRV to 1.54 and change MSRV policy ([#190][i190])
- [chore] The `master` branch was renamed to `main`
- [chore] All release binaries are now generated in CI. Binaries for macOS and Windows are also provided. ([#240][i240])
- [chore] Update all dependencies
#### Contributors to this version:
- [@bl-ue][@bl-ue]
- [Cameron Tod][@cam8001]
- [Dalton][@dmaahs2017]
- [Danilo Bargen][@dbrgn]
- [Danny Mösch][@SimplyDanny]
- [Marcin Puc][@tranzystorek-io]
- [Michael Cho][@cho-m]
- [MS_Y][@black7375]
- [Niklas Mohrin][@niklasmohrin]
- [Rithvik Vibhu][@rithvikvibhu]
- [rnd][@0ndorio]
- [Sondre Nilsen][@sondr3]
- [Tomás Farías Santana][@tomasfarias]
- [Tsvetomir Bonev][@invakid404]
- [@tveness][@tveness]
- [ギャラ][@laxect]
Thanks!
Last but not least, [Niklas Mohrin][@niklasmohrin] has joined the project as
co-maintainer. Thank you for your help!
### [v1.4.1][v1.4.1] (2020-09-04) ### [v1.4.1][v1.4.1] (2020-09-04)
- [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]
- [Bruno A. Muciño][@mucinoab]
- [Francesco][@BachoSeven] - [Francesco][@BachoSeven]
- [Bruno A. Muciño][@mucinoab]
Thanks! Thanks!
@ -419,10 +32,9 @@ 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]
- [Danny Mösch][@SimplyDanny] - [Danny Mösch][@SimplyDanny]
- [Ilaï Deutel][@ilai-deutel] - [Ilaï Deutel][@ilai-deutel]
- [Kornel][@kornelski] - [Kornel][@kornelski]
@ -445,16 +57,15 @@ 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] - [@Calinou][@Calinou]
- [Danilo Bargen][@dbrgn] - [@Delapouite][@Delapouite]
- [Hugo Locurcio][@Calinou] - [@james2doyle][@james2doyle]
- [Isak Johansson][@Plommonsorbet] - [@jesdazrez][@jesdazrez]
- [James Doyle][@james2doyle]
- [Jesús Trinidad Díaz Ramírez][@jesdazrez]
- [@korrat][@korrat] - [@korrat][@korrat]
- [Marc-André Renaud][@ma-renaud] - [@ma-renaud][@ma-renaud]
- [@Plommonsorbet][@Plommonsorbet]
Thanks! Thanks!
@ -471,17 +82,16 @@ 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] - [@aldanor][@aldanor]
- [Danilo Bargen][@dbrgn] - [@Bassets][@Bassets]
- [Gabriel Martinez][@mystal] - [@das-g][@das-g]
- [Ivan Smirnov][@aldanor] - [@jcgruenhage][@jcgruenhage]
- [Jan Christian Grünhage][@jcgruenhage] - [@jdvr][@jdvr]
- [Jonathan Dahan][@jedahan] - [@jedahan][@jedahan]
- [Juan D. Vega][@jdvr] - [@mystal][@mystal]
- [Natalie Pendragon][@natpen] - [@natpen][@natpen]
- [Raphael Das Gupta][@das-g]
Thanks! Thanks!
@ -494,9 +104,8 @@ 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]
- [@equal-l2][@equal-l2] - [@equal-l2][@equal-l2]
- [Jonathan Dahan][@jedahan] - [Jonathan Dahan][@jedahan]
- [Lukas Bergdoll][@Voultapher] - [Lukas Bergdoll][@Voultapher]
@ -527,198 +136,63 @@ Thanks!
- First crates.io release - First crates.io release
[user documentation]: https://docs.tealdeer.org
[@0ndorio]: https://github.com/0ndorio
[@adamazing]: https://github.com/adamazing
[@agrmohit]: https://github.com/agrmohit
[@aldanor]: https://github.com/aldanor [@aldanor]: https://github.com/aldanor
[@Atul9]: https://github.com/Atul9 [@Atul9]: https://github.com/Atul9
[@BachoSeven]: https://github.com/BachoSeven [@BachoSeven]: https://github.com/BachoSeven
[@bagohart]: https://github.com/bagohart
[@Bassets]: https://github.com/Bassets [@Bassets]: https://github.com/Bassets
[@black7375]: https://github.com/black7375
[@bl-ue]: https://github.com/bl-ue
[@Calinou]: https://github.com/Calinou [@Calinou]: https://github.com/Calinou
[@cam8001]: https://github.com/cam8001
[@cho-m]: https://github.com/cho-m
[@cyqsimon]: https://github.com/cyqsimon
[@CyrusYip]: https://github.com/CyrusYip
[@das-g]: https://github.com/das-g [@das-g]: https://github.com/das-g
[@dbrgn]: https://github.com/dbrgn
[@Delapouite]: https://github.com/Delapouite [@Delapouite]: https://github.com/Delapouite
[@dmaahs2017]: https://github.com/dmaahs2017
[@equal-l2]: https://github.com/equal-l2 [@equal-l2]: https://github.com/equal-l2
[@felixonmars]: https://github.com/felixonmars
[@frisoft]: https://github.com/frisoft
[@gagarine]: https://github.com/gagarine
[@hgaiser]: https://github.com/hgaiser
[@ilai-deutel]: https://github.com/ilai-deutel [@ilai-deutel]: https://github.com/ilai-deutel
[@iliya-malecki]: https://github.com/iliya-malecki
[@invakid404]: https://github.com/invakid404
[@james2doyle]: https://github.com/james2doyle [@james2doyle]: https://github.com/james2doyle
[@jcgruenhage]: https://github.com/jcgruenhage [@jcgruenhage]: https://github.com/jcgruenhage
[@jdvr]: https://github.com/jdvr [@jdvr]: https://github.com/jdvr
[@jedahan]: https://github.com/jedahan [@jedahan]: https://github.com/jedahan
[@jesdazrez]: https://github.com/jesdazrez [@jesdazrez]: https://github.com/jesdazrez
[@jj-style]: https://github.com/jj-style
[@kbdharun]: https://github.com/kbdharun
[@kianmeng]: https://github.com/kianmeng
[@kornelski]: https://github.com/kornelski [@kornelski]: https://github.com/kornelski
[@korrat]: https://github.com/korrat [@korrat]: https://github.com/korrat
[@laxect]: https://github.com/laxect
[@LovecraftianHorror]: https://github.com/LovecraftianHorror [@LovecraftianHorror]: https://github.com/LovecraftianHorror
[@ma-renaud]: https://github.com/ma-renaud [@ma-renaud]: https://github.com/ma-renaud
[@michaeldel]: https://github.com/michaeldel [@michaeldel]: https://github.com/michaeldel
[@mucinoab]: https://github.com/mucinoab [@mucinoab]: https://github.com/mucinoab
[@mystal]: https://github.com/mystal [@mystal]: https://github.com/mystal
[@natpen]: https://github.com/natpen [@natpen]: https://github.com/natpen
[@nc7s]: https://github.com/nc7s
[@newsch]: https://github.com/newsch
[@nifr]: https://github.com/nifr
[@niklasmohrin]: https://github.com/niklasmohrin [@niklasmohrin]: https://github.com/niklasmohrin
[@Olavhaasie]: https://github.com/Olavhaasie
[@Plommonsorbet]: https://github.com/Plommonsorbet [@Plommonsorbet]: https://github.com/Plommonsorbet
[@qknogxxb]: https://github.com/qknogxxb
[@rithvikvibhu]: https://github.com/rithvikvibhu
[@SimplyDanny]: https://github.com/SimplyDanny [@SimplyDanny]: https://github.com/SimplyDanny
[@sondr3]: https://github.com/sondr3
[@tomasfarias]: https://github.com/tomasfarias
[@tranzystorek-io]: https://github.com/tranzystorek-io
[@tveness]: https://github.com/tveness
[@Voultapher]: https://github.com/Voultapher [@Voultapher]: https://github.com/Voultapher
[@Walker-00]: https://github.com/Walker-00
[@YDX-2147483647]: https://github.com/YDX-2147483647
[@zedseven]: https://github.com/zedseven
[@beatbrot]: https://github.com/beatbrot
[@erickguan]: https://github.com/erickguan
[@MHS-0]: https://github.com/MHS-0
[@MatejKafka]: https://github.com/MatejKafka
[@nachiketkanore]: https://github.com/nachiketkanore
[@mipedja]: https://github.com/mipedja
[@hex1c]: https://github.com/hex1c
[@lengyijun]: https://github.com/lengyijun
[v1.0.0]: https://github.com/tealdeer-rs/tealdeer/compare/v0.4.0...v1.0.0 [v1.0.0]: https://github.com/dbrgn/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/dbrgn/tealdeer/compare/v1.0.0...v1.1.0
[v1.2.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.1.0...v1.2.0 [v1.2.0]: https://github.com/dbrgn/tealdeer/compare/v1.1.0...v1.2.0
[v1.3.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.2.0...v1.3.0 [v1.3.0]: https://github.com/dbrgn/tealdeer/compare/v1.2.0...v1.3.0
[v1.4.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.3.0...v1.4.0 [v1.4.0]: https://github.com/dbrgn/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/dbrgn/tealdeer/compare/v1.4.0...v1.4.1
[v1.5.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.4.1...v1.5.0
[v1.5.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.5.0...v1.5.1
[v1.6.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.5.0...v1.6.0
[v1.6.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.6.0...v1.6.1
[v1.6.2]: https://github.com/tealdeer-rs/tealdeer/compare/v1.6.1...v1.6.2
[v1.7.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.6.1...v1.7.0
[v1.7.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.0...v1.7.1
[v1.7.2]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.1...v1.7.2
[v1.7.3]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.2...v1.7.3
[v1.8.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.2...v1.8.0
[v1.8.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.8.0...v1.8.1
[i34]: https://github.com/tealdeer-rs/tealdeer/issues/34 [i34]: https://github.com/dbrgn/tealdeer/issues/34
[i43]: https://github.com/tealdeer-rs/tealdeer/issues/43 [i43]: https://github.com/dbrgn/tealdeer/issues/43
[i44]: https://github.com/tealdeer-rs/tealdeer/issues/44 [i44]: https://github.com/dbrgn/tealdeer/issues/44
[i47]: https://github.com/tealdeer-rs/tealdeer/issues/47 [i47]: https://github.com/dbrgn/tealdeer/issues/47
[i48]: https://github.com/tealdeer-rs/tealdeer/issues/48 [i48]: https://github.com/dbrgn/tealdeer/issues/48
[i57]: https://github.com/tealdeer-rs/tealdeer/issues/57 [i57]: https://github.com/dbrgn/tealdeer/issues/57
[i58]: https://github.com/tealdeer-rs/tealdeer/issues/58 [i58]: https://github.com/dbrgn/tealdeer/issues/58
[i61]: https://github.com/tealdeer-rs/tealdeer/issues/61 [i61]: https://github.com/dbrgn/tealdeer/issues/61
[i68]: https://github.com/tealdeer-rs/tealdeer/issues/68 [i68]: https://github.com/dbrgn/tealdeer/issues/68
[i69]: https://github.com/tealdeer-rs/tealdeer/issues/69 [i69]: https://github.com/dbrgn/tealdeer/issues/69
[i71]: https://github.com/tealdeer-rs/tealdeer/issues/71 [i71]: https://github.com/dbrgn/tealdeer/issues/71
[i75]: https://github.com/tealdeer-rs/tealdeer/issues/75 [i75]: https://github.com/dbrgn/tealdeer/issues/75
[i77]: https://github.com/tealdeer-rs/tealdeer/issues/77 [i77]: https://github.com/dbrgn/tealdeer/issues/77
[i84]: https://github.com/tealdeer-rs/tealdeer/issues/84 [i84]: https://github.com/dbrgn/tealdeer/issues/84
[i86]: https://github.com/tealdeer-rs/tealdeer/issues/86 [i86]: https://github.com/dbrgn/tealdeer/issues/86
[i87]: https://github.com/tealdeer-rs/tealdeer/issues/87 [i87]: https://github.com/dbrgn/tealdeer/issues/87
[i89]: https://github.com/tealdeer-rs/tealdeer/issues/89 [i89]: https://github.com/dbrgn/tealdeer/issues/89
[i95]: https://github.com/tealdeer-rs/tealdeer/issues/95 [i95]: https://github.com/dbrgn/tealdeer/issues/95
[i97]: https://github.com/tealdeer-rs/tealdeer/issues/97 [i97]: https://github.com/dbrgn/tealdeer/issues/97
[i99]: https://github.com/tealdeer-rs/tealdeer/issues/99 [i99]: https://github.com/dbrgn/tealdeer/issues/99
[i108]: https://github.com/tealdeer-rs/tealdeer/pull/108 [i111]: https://github.com/dbrgn/tealdeer/issues/111
[i111]: https://github.com/tealdeer-rs/tealdeer/issues/111 [i112]: https://github.com/dbrgn/tealdeer/issues/112
[i112]: https://github.com/tealdeer-rs/tealdeer/issues/112 [i113]: https://github.com/dbrgn/tealdeer/issues/113
[i113]: https://github.com/tealdeer-rs/tealdeer/issues/113 [i115]: https://github.com/dbrgn/tealdeer/issues/115
[i115]: https://github.com/tealdeer-rs/tealdeer/issues/115 [i138]: https://github.com/dbrgn/tealdeer/issues/138
[i125]: https://github.com/tealdeer-rs/tealdeer/pull/125
[i138]: https://github.com/tealdeer-rs/tealdeer/issues/138
[i142]: https://github.com/tealdeer-rs/tealdeer/pull/142
[i148]: https://github.com/tealdeer-rs/tealdeer/pull/148
[i157]: https://github.com/tealdeer-rs/tealdeer/pull/157
[i161]: https://github.com/tealdeer-rs/tealdeer/pull/161
[i162]: https://github.com/tealdeer-rs/tealdeer/pull/162
[i163]: https://github.com/tealdeer-rs/tealdeer/pull/163
[i168]: https://github.com/tealdeer-rs/tealdeer/pull/168
[i171]: https://github.com/tealdeer-rs/tealdeer/pull/171
[i174]: https://github.com/tealdeer-rs/tealdeer/pull/174
[i176]: https://github.com/tealdeer-rs/tealdeer/pull/176
[i187]: https://github.com/tealdeer-rs/tealdeer/pull/187
[i190]: https://github.com/tealdeer-rs/tealdeer/issues/190
[i197]: https://github.com/tealdeer-rs/tealdeer/pull/197
[i210]: https://github.com/tealdeer-rs/tealdeer/pull/210
[i213]: https://github.com/tealdeer-rs/tealdeer/pull/213
[i215]: https://github.com/tealdeer-rs/tealdeer/pull/215
[i217]: https://github.com/tealdeer-rs/tealdeer/pull/217
[i227]: https://github.com/tealdeer-rs/tealdeer/pull/227
[#231]: https://github.com/tealdeer-rs/tealdeer/pull/231
[i240]: https://github.com/tealdeer-rs/tealdeer/pull/240
[#247]: https://github.com/tealdeer-rs/tealdeer/pull/247
[#249]: https://github.com/tealdeer-rs/tealdeer/pull/249
[#253]: https://github.com/tealdeer-rs/tealdeer/pull/253
[#254]: https://github.com/tealdeer-rs/tealdeer/pull/254
[#257]: https://github.com/tealdeer-rs/tealdeer/pull/257
[#259]: https://github.com/tealdeer-rs/tealdeer/pull/259
[#262]: https://github.com/tealdeer-rs/tealdeer/pull/262
[#271]: https://github.com/tealdeer-rs/tealdeer/pull/271
[#272]: https://github.com/tealdeer-rs/tealdeer/pull/272
[#274]: https://github.com/tealdeer-rs/tealdeer/pull/274
[#276]: https://github.com/tealdeer-rs/tealdeer/pull/276
[#284]: https://github.com/tealdeer-rs/tealdeer/pull/284
[#285]: https://github.com/tealdeer-rs/tealdeer/pull/285
[#287]: https://github.com/tealdeer-rs/tealdeer/pull/287
[#290]: https://github.com/tealdeer-rs/tealdeer/pull/290
[#291]: https://github.com/tealdeer-rs/tealdeer/pull/291
[#293]: https://github.com/tealdeer-rs/tealdeer/pull/293
[#297]: https://github.com/tealdeer-rs/tealdeer/pull/297
[#298]: https://github.com/tealdeer-rs/tealdeer/pull/298
[#299]: https://github.com/tealdeer-rs/tealdeer/pull/299
[#300]: https://github.com/tealdeer-rs/tealdeer/pull/300
[#303]: https://github.com/tealdeer-rs/tealdeer/pull/303
[#305]: https://github.com/tealdeer-rs/tealdeer/pull/305
[#306]: https://github.com/tealdeer-rs/tealdeer/pull/306
[#314]: https://github.com/tealdeer-rs/tealdeer/pull/314
[#315]: https://github.com/tealdeer-rs/tealdeer/pull/315
[#322]: https://github.com/tealdeer-rs/tealdeer/pull/322
[#324]: https://github.com/tealdeer-rs/tealdeer/pull/324
[#327]: https://github.com/tealdeer-rs/tealdeer/pull/327
[#331]: https://github.com/tealdeer-rs/tealdeer/pull/331
[#333]: https://github.com/tealdeer-rs/tealdeer/pull/333
[#336]: https://github.com/tealdeer-rs/tealdeer/pull/336
[#337]: https://github.com/tealdeer-rs/tealdeer/pull/337
[#342]: https://github.com/tealdeer-rs/tealdeer/pull/342
[#354]: https://github.com/tealdeer-rs/tealdeer/pull/354
[#355]: https://github.com/tealdeer-rs/tealdeer/pull/355
[#362]: https://github.com/tealdeer-rs/tealdeer/pull/362
[#386]: https://github.com/tealdeer-rs/tealdeer/pull/386
[#388]: https://github.com/tealdeer-rs/tealdeer/pull/388
[#389]: https://github.com/tealdeer-rs/tealdeer/pull/389
[#399]: https://github.com/tealdeer-rs/tealdeer/pull/399
[#400]: https://github.com/tealdeer-rs/tealdeer/pull/400
[#401]: https://github.com/tealdeer-rs/tealdeer/pull/401
[#407]: https://github.com/tealdeer-rs/tealdeer/pull/407
[#411]: https://github.com/tealdeer-rs/tealdeer/pull/411
[#416]: https://github.com/tealdeer-rs/tealdeer/pull/416
[#417]: https://github.com/tealdeer-rs/tealdeer/pull/417
[#422]: https://github.com/tealdeer-rs/tealdeer/pull/422
[#423]: https://github.com/tealdeer-rs/tealdeer/pull/423
[#425]: https://github.com/tealdeer-rs/tealdeer/pull/425
[#426]: https://github.com/tealdeer-rs/tealdeer/pull/426
[#429]: https://github.com/tealdeer-rs/tealdeer/pull/429
[#430]: https://github.com/tealdeer-rs/tealdeer/pull/430
[#435]: https://github.com/tealdeer-rs/tealdeer/pull/435
[#436]: https://github.com/tealdeer-rs/tealdeer/pull/436
[#439]: https://github.com/tealdeer-rs/tealdeer/pull/439
[#440]: https://github.com/tealdeer-rs/tealdeer/pull/440
[#451]: https://github.com/tealdeer-rs/tealdeer/pull/451

1853
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,33 +4,33 @@ authors = [
"Niklas Mohrin <dev@niklasmohrin.de>", "Niklas Mohrin <dev@niklasmohrin.de>",
] ]
description = "Fetch and show tldr help pages for many CLI commands. Full featured offline client with caching support." description = "Fetch and show tldr help pages for many CLI commands. Full featured offline client with caching support."
homepage = "https://github.com/tealdeer-rs/tealdeer/" homepage = "https://github.com/dbrgn/tealdeer/"
license = "MIT OR Apache-2.0" license = "MIT/Apache-2.0"
name = "tealdeer" name = "tealdeer"
readme = "README.md" readme = "README.md"
repository = "https://github.com/tealdeer-rs/tealdeer/" repository = "https://github.com/dbrgn/tealdeer/"
documentation = "https://docs.tealdeer.org" documentation = "https://dbrgn.github.io/tealdeer/"
version = "1.8.1" version = "1.4.1"
include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"] include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "/bash_tealdeer", "/fish_tealdeer"]
rust-version = "1.88" # MSRV edition = "2018"
edition = "2024"
[[bin]] [[bin]]
name = "tldr" name = "tldr"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
anyhow = "1" ansi_term = "0.12.0"
clap = { version = "4", features = ["std", "derive", "help", "usage", "cargo", "error-context", "color", "wrap_help"], default-features = false } app_dirs = { version = "2", package = "app_dirs2" }
env_logger = { version = "0.11", optional = true } atty = "0.2"
etcetera = "0.11.0" clap = { version = "3.0.0-beta.5", features = ["std", "derive", "suggestions" ], default-features = false }
env_logger = { version = "0.9", optional = true }
log = "0.4" log = "0.4"
reqwest = { version = "0.11.3", features = ["blocking", "rustls-tls", "rustls-tls-native-roots"], default-features = false }
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", "socks-proxy"] } toml = "0.5.1"
toml = "1" walkdir = "2.0.1"
yansi = "1" zip = { version = "0.5", 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"
@ -38,24 +38,12 @@ pager = "0.16"
[dev-dependencies] [dev-dependencies]
assert_cmd = "2.0.1" assert_cmd = "2.0.1"
escargot = "0.5" escargot = "0.5"
predicates = "3.1.2" predicates = "2.0.2"
tempfile = "3.1.0" tempfile = "3.1.0"
filetime = "0.2.10" filetime = "0.2.10"
[features] [features]
# native-tls is not enabled by default, because it is difficult to build for musl
default = ["rustls-with-webpki-roots", "rustls-with-native-roots"]
logging = ["env_logger"] logging = ["env_logger"]
# At least one of variants for `ureq` HTTP client must be selected.
native-tls = ["ureq/native-tls", "ureq/platform-verifier"]
rustls-with-webpki-roots = ["ureq/rustls"] # ureq uses WebPKI roots by default
rustls-with-native-roots = ["ureq/rustls", "ureq/platform-verifier"]
ignore-online-tests = []
[profile.release] [profile.release]
strip = true
opt-level = 3
lto = true lto = true
codegen-units = 1

View file

@ -14,12 +14,12 @@ Rust: Simplified, example based and community-driven man pages.
If you pronounce "tldr" in English, it sounds somewhat like "tealdeer". Hence the project name :) If you pronounce "tldr" in English, it sounds somewhat like "tealdeer". Hence the project name :)
In case you're in a hurry and just want to quickly try tealdeer, you can find static In case you're in a hurry and just want to quickly try tealdeer, you can find static
binaries on the [GitHub releases page](https://github.com/tealdeer-rs/tealdeer/releases/)! binaries on the [GitHub releases page](https://github.com/dbrgn/tealdeer/releases/)!
## Docs (Installing, Usage, Configuration) ## Docs (Installing, Usage, Configuration)
User documentation is available at <https://docs.tealdeer.org>! User documentation is available at <https://dbrgn.github.io/tealdeer/>!
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,6 +31,7 @@ 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
@ -38,6 +39,29 @@ 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
@ -63,22 +87,10 @@ 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. The current MSRV can always be found in should be stable for at least a month.
the `rust-version` field in `Cargo.toml`.
## License ## License
@ -100,10 +112,21 @@ 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/dbrgn/tealdeer/blob/master/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/dbrgn/tealdeer/issues/129#issuecomment-833596765
<!-- Badges --> <!-- Badges -->
[github-actions]: https://github.com/tealdeer-rs/tealdeer/actions?query=branch%3Amain [github-actions]: https://github.com/dbrgn/tealdeer/actions?query=branch%3Amaster
[github-actions-badge]: https://github.com/tealdeer-rs/tealdeer/actions/workflows/ci.yml/badge.svg?branch=main [github-actions-badge]: https://github.com/dbrgn/tealdeer/workflows/CI/badge.svg
[crates-io]: https://crates.io/crates/tealdeer [crates-io]: https://crates.io/crates/tealdeer
[crates-io-badge]: https://img.shields.io/crates/v/tealdeer.svg [crates-io-badge]: https://img.shields.io/crates/v/tealdeer.svg

View file

@ -7,17 +7,13 @@ Run linting:
Set variables: Set variables:
$ export VERSION=X.Y.Z $ export VERSION=X.Y.Z
$ export GPG_KEY=20EE002D778AE197EF7D0D2CB993FF98A90C9AB1 $ export GPG_KEY=EA456E8BAF0109429583EED83578F667F2F3A5FA
Update version numbers: Update version numbers:
$ vim Cargo.toml $ vim Cargo.toml
$ cargo update -p tealdeer $ cargo update -p tealdeer
Update docs:
$ cargo run -- --help > docs/src/usage.txt
Update changelog: Update changelog:
$ vim CHANGELOG.md $ vim CHANGELOG.md
@ -32,4 +28,6 @@ Publish:
$ cargo publish $ cargo publish
$ git push && git push --tags $ git push && git push --tags
Then publish the release on GitHub. Create release binaries:
$ ./release-build.sh

View file

@ -6,7 +6,7 @@ _tealdeer()
_init_completion || return _init_completion || return
case $prev in case $prev in
-h|--help|-v|--version|-l|--list|-u|--update|--no-auto-update|-c|--clear-cache|--pager|-r|--raw|--show-paths|--seed-config|-q|--quiet) -h|--help|-v|--version|-l|--list|-u|--update|-c|--clear-cache|-p|--pager|-r|--raw|--show-paths|--seed-config|-q|--quiet)
return return
;; ;;
-f|--render) -f|--render)
@ -14,7 +14,7 @@ _tealdeer()
return return
;; ;;
-p|--platform) -p|--platform)
COMPREPLY=( $(compgen -W 'linux macos sunos windows android freebsd netbsd openbsd' -- "${cur}") ) COMPREPLY=( $(compgen -W 'linux macos sunos windows' -- "${cur}") )
return return
;; ;;
--color) --color)
@ -27,9 +27,8 @@ _tealdeer()
COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) ) COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) )
return return
fi fi
if tldrlist=$(tldr -l 2>/dev/null); then
COMPREPLY=( $(compgen -W '$( echo "$tldrlist" | tr -d , )' -- "${cur}") ) COMPREPLY=( $(compgen -W '$( tldr -l | tr -d , )' -- "${cur}") )
fi
} }
complete -F _tealdeer tldr complete -F _tealdeer tldr

139
benchmarks/Dockerfile Normal file
View file

@ -0,0 +1,139 @@
# Benchmark Dockerfile for tealdeer
#
# To run the benchmarks, execute
#
# docker build --pull -t tldr-benchmark .
# docker run --privileged --rm -it tldr-benchmark
#
# as root in the directory of this Dockerfile. This will build the compared
# clients and benchmark them with `hyperfine` at the end.
#
# The `--privileged` flag is needed to drop the disk caches before every run. If
# you want to test with hot caches or don't want to use this flag, you will have
# to remove the `--prepare` line from the `hyperfine` command at the end of this
# file and rebuild the image.
################################################################################
FROM rust AS tealdeer-builder
WORKDIR /build
RUN git clone https://github.com/dbrgn/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"

1
clippy.toml Normal file
View file

@ -0,0 +1 @@
msrv = "1.54"

View file

@ -1,34 +0,0 @@
#
# Completions for the tealdeer implementation of tldr
# https://github.com/tealdeer-rs/tealdeer/
#
complete -c tldr -s h -l help -d 'Print the help message' -f
complete -c tldr -s v -l version -d 'Show version information' -f
complete -c tldr -s l -l list -d 'List all commands in the cache' -f
complete -c tldr -l edit-page -d 'Edit custom page with `EDITOR`' -f
complete -c tldr -l edit-patch -d 'Edit custom patch with `EDITOR`' -f
complete -c tldr -s f -l render -d 'Render a specific markdown file' -r
complete -c tldr -s p -l platform -d 'Override the operating system' -xa 'linux macos sunos windows android freebsd netbsd openbsd common'
complete -c tldr -s L -l language -d 'Override the language' -x
complete -c tldr -s u -l update -d 'Update the local cache' -f
complete -c tldr -l no-auto-update -d 'If auto update is configured, disable it for this run' -f
complete -c tldr -s c -l clear-cache -d 'Clear the local cache' -f
complete -c tldr -s L -l config-path -d 'Override config file location' -r
complete -c tldr -s L -l override-config -d 'Override config values after reading config file' -x
complete -c tldr -l pager -d 'Use a pager to page output' -f
complete -c tldr -s r -l raw -d 'Display the raw markdown instead of rendering it' -f
complete -c tldr -s q -l quiet -d 'Suppress informational messages' -f
complete -c tldr -l show-paths -d 'Show file and directory paths used by tealdeer' -f
complete -c tldr -l seed-config -d 'Create a basic config' -f
complete -c tldr -l color -d 'Controls when to use color' -xa 'always auto never'
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
if set entries (tldr --list 2>/dev/null)
string replace -a -i -r "\,\s" "\n" $entries
end
end
complete -f -c tldr -a '(__tealdeer_entries)'

View file

@ -1,5 +1,6 @@
[book] [book]
authors = ["Danilo Bargen", "Niklas Mohrin"] authors = ["Danilo Bargen"]
language = "en" language = "en"
multilingual = false
src = "src" src = "src"
title = "Tealdeer User Manual" title = "Tealdeer User Manual"

View file

@ -4,11 +4,7 @@
- [Installing](./installing.md) - [Installing](./installing.md)
- [Usage](./usage.md) - [Usage](./usage.md)
- [Custom Pages and Patches](./usage_custom_pages.md)
- [Configuration](./config.md) - [Configuration](./config.md)
- [Section: \[display\]](./config_display.md) - [display](./config_display.md)
- [Section: \[style\]](./config_style.md) - [style](./config_style.md)
- [Section: \[search\]](./config_search.md) - [updates](./config_updates.md)
- [Section: \[updates\]](./config_updates.md)
- [Section: \[directories\]](./config_directories.md)
- [Tips and Tricks](./tips_and_tricks.md)

View file

@ -1,39 +1,37 @@
# Configuration # Configuration
Tealdeer can be customized with a config file in [TOML Tealdeer can be customized with a config file called `config.toml`. Creating
format](https://toml.io/) called `config.toml`. the config file can be done manually or with the help of `tldr`:
## Configfile Path $ tldr --seed-config
The configuration file path follows OS conventions (e.g. The configuration file path follows OS conventions. It can be queried with the
`$XDG_CONFIG_HOME/tealdeer/config.toml` on Linux). The paths can be queried following command:
with the following command:
```shell $ tldr --show-paths
$ tldr --show-paths
```
Creating the config file can be done manually or with the help of `tldr`:
```shell
$ tldr --seed-config
```
On Linux, this will usually be `~/.config/tealdeer/config.toml`. On Linux, this will usually be `~/.config/tealdeer/config.toml`.
## Config Example ## Override Config Directory
Here's an example configuration file. Note that this example does not contain The directory where the configuration file resides may be overwritten by the
all possible config options. For details on the things that can be configured, environment variable `TEALDEER_CONFIG_DIR`. Remember to use an absolute path.
please refer to the subsections of this documentation page Variable expansion will not be performed on the path.
([display](config_display.html), [style](config_style.html), [search](config_search.html),
[updates](config_updates.html) or [directories](config_directories.html)). ## Override Cache Directory
Similarly, the cache directory where the pages are downloaded to, also follows
OS conventions. On Linux, it will usually be at `~/.cache/tealdeer/`. The path
can be overwritten using the environment variable `TEALDEER_CACHE_DIR`.
Remember to use an absolute path. Variable expansion will not be performed on
the path.
## Config Example
```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"
@ -51,22 +49,3 @@ underline = true
[updates] [updates]
auto_update = true auto_update = true
``` ```
## Override Config Directory
The directory where the configuration file resides may be overwritten by the
environment variable `TEALDEER_CONFIG_DIR`. Remember to use an absolute path.
Variable expansion will not be performed on the path.
## 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.

View file

@ -1,29 +0,0 @@
# Section: \[directories\]
This section allows overriding some directory paths.
## `cache_dir`
Override the cache directory. Remember to use an absolute path. Variable
expansion will not be performed on the path. If the directory does not yet
exist, it will be created.
```toml
[directories]
cache_dir = "/home/myuser/.tealdeer-cache/"
```
If no `cache_dir` is specified, tealdeer will fall back to a location that
follows OS conventions. On Linux, it will usually be at `~/.cache/tealdeer/`.
Use `tldr --show-paths` to show the path that is being used.
## `custom_pages_dir`
Set the directory to be used to look up [custom
pages](usage_custom_pages.html). Remember to use an absolute path. Variable
expansion will not be performed on the path.
```toml
[directories]
custom_pages_dir = "/home/myuser/custom-tldr-pages/"
```

View file

@ -1,4 +1,4 @@
# Section: \[display\] # display
In the `display` section you can configure the output format. In the `display` section you can configure the output format.
@ -6,10 +6,8 @@ In the `display` section you can configure the output format.
Specifies whether the pager should be used by default or not (default `false`). Specifies whether the pager should be used by default or not (default `false`).
```toml [display]
[display] use_pager = true
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.
@ -21,68 +19,5 @@ 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`).
```toml [display]
[display] compact = true
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"`

View file

@ -1,33 +0,0 @@
# Section: \[search\]
This config section is used to configure the page search in the cache.
The settings apply to `tldr <page>` and `tldr --list`.
## `languages`
The list of languages that should be considered when searching.
If unspecified, the list of languages will be inferred from the `LANG` and `LANGUAGE` environment variables.
Either way, the language used can be overwritten using the `--language` command line flag.
```toml
[search]
# Show pages in German if available, otherwise show in English
languages = ["de", "en"]
```
## `platforms`
The list of platforms that should be considered when searching.
In addition to the platforms listed in the help text of the `--platform` flag, there are two special platforms available:
- `"current"`: equals the platform that tealdeer was compiled for
- `"all"`: adds all remaining platforms to the list
Tealdeer searches the platforms in order of appearance in this list.
The default list of platforms is `["current", "common", "all"]`.
The list of platforms can be overwritten using the `--platform` command line flag.
```toml
[search]
# Search for linux and common, and then search windows before trying the remaining platforms
platforms = ["linux", "common", "windows", "all"]
```

View file

@ -1,4 +1,4 @@
# Section: \[style\] # style
Using the config file, the style (e.g. colors or underlines) can be customized. Using the config file, the style (e.g. colors or underlines) can be customized.
@ -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 (placeholders) in the example - `example_variable`: The variables in the example
## Attributes ## Attributes
@ -22,26 +22,20 @@ Using the config file, the style (e.g. colors or underlines) can be customized.
Colors can be specified in one of three ways: Colors can be specified in one of three ways:
- Color string (`black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`): - Color string (`black`, `red`, `green`, `yellow`, `blue`, `purple`, `cyan`, `white`):
Example: Example:
```toml foreground = "green"
foreground = "green"
```
- 256 color ANSI code (*tealdeer v1.5.0+*) - 256 color ANSI code (*Tealdeer v1.5.0+*)
Example: Example:
```toml foreground = { ansi = 4 }
foreground = { ansi = 4 }
```
- 24-bit RGB color (*tealdeer v1.5.0+*) - 24-bit RGB color (*Tealdeer v1.5.0+*)
Example: Example:
```toml background = { rgb = { r = 255, g = 255, b = 255 } }
background = { rgb = { r = 255, g = 255, b = 255 } }
```

View file

@ -1,10 +1,8 @@
# Section: \[updates\] # 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
behavior can be configured in the `updates` section and is disabled by behavior can be configured in the `updates` section and is disabled by
default. default.
@ -13,10 +11,8 @@ 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`).
```toml [updates]
[updates] auto_update = true
auto_update = true
```
### `auto_update_interval_hours` ### `auto_update_interval_hours`
@ -24,68 +20,7 @@ Duration, since the last cache update, after which the cache will be
refreshed (defaults to 720 hours). This parameter is ignored if `auto_update` refreshed (defaults to 720 hours). This parameter is ignored if `auto_update`
is set to `false`. is set to `false`.
```toml [updates]
[updates] auto_update = true
auto_update = true auto_update_interval_hours = 24
auto_update_interval_hours = 24
```
### `warn_cache_age`
Controls when a warning is printed if the cache has not been updated in a while.
By default, the warning is shown once the cache is older than 30 days. Set this
to `"never"` to silence the warning. This is useful if, for some reason, the
modification time does not reflect its actual age.
```toml
[updates]
warn_cache_age = "never"
```
## Download configuration
### `download_languages`
The list of languages which should be downloaded when updating.
If unspecified, the languages listed in the `search.languages` setting are used.
Thus, this setting is the most useful to instruct tealdeer to download pages in additional languages that are not searched by default.
Either way, the language used can be overwritten using the `--language` command line flag.
```toml
[search]
languages = ["de", "en"]
[updates]
# sometimes I like to read the Italian description
download_languages = ["de", "en", "it"]
```
### `archive_source`
URL for the location of the tldr pages archive. By default the pages are
fetched from the latest `tldr-pages/tldr` GitHub release.
```toml
[updates]
archive_source = "https://my-company.example.com/tldr/"
```
### `tls_backend`
Specifies which TLS backend to use. Try changing this setting if you encounter certificate errors.
Available options:
- `rustls-with-native-roots` - [Rustls][rustls] (a TLS library in Rust) with native roots
- `rustls-with-webpki-roots` - Rustls with [WebPKI][rustls-webpki] roots
- `native-tls` - Native TLS
- SChannel on Windows
- Secure Transport on macOS
- OpenSSL on other platforms
```toml
[updates]
tls_backend = "native-tls"
```
[rustls]: https://github.com/rustls/rustls
[rustls-webpki]: https://github.com/rustls/webpki

View file

@ -1,6 +1,6 @@
# Installing # Installing
There are a few different ways to install tealdeer: There are a few different ways to install Tealdeer:
- Through [package managers](#package-managers) - Through [package managers](#package-managers)
- Through [static binaries](#static-binaries-linux) - Through [static binaries](#static-binaries-linux)
@ -14,61 +14,49 @@ autocompletions](#autocompletion).
Tealdeer has been added to a few package managers: Tealdeer has been added to a few package managers:
- Arch Linux: [`tealdeer`](https://archlinux.org/packages/extra/x86_64/tealdeer/) - Arch Linux: [`tealdeer`](https://archlinux.org/packages/community/x86_64/tealdeer/)
- Debian: [`tealdeer`](https://tracker.debian.org/tealdeer)
- Fedora: [`tealdeer`](https://src.fedoraproject.org/rpms/rust-tealdeer) - Fedora: [`tealdeer`](https://src.fedoraproject.org/rpms/rust-tealdeer)
- FreeBSD: [`sysutils/tealdeer`](https://www.freshports.org/sysutils/tealdeer/) - FreeBSD: [`sysutils/tealdeer`](https://www.freshports.org/sysutils/tealdeer/)
- Funtoo: [`app-misc/tealdeer`](https://github.com/funtoo/core-kit/tree/1.4-release/app-misc/tealdeer) - Funtoo: [`app-misc/tealdeer`](https://github.com/funtoo/core-kit/tree/1.4-release/app-misc/tealdeer)
- Homebrew: [`tealdeer`](https://formulae.brew.sh/formula/tealdeer) - Homebrew: [`tealdeer`](https://formulae.brew.sh/formula/tealdeer)
- MacPorts: [`tealdeer`](https://ports.macports.org/port/tealdeer/)
- NetBSD: [`sysutils/tealdeer`](https://pkgsrc.se/sysutils/tealdeer) - NetBSD: [`sysutils/tealdeer`](https://pkgsrc.se/sysutils/tealdeer)
- Nix: [`tealdeer`](https://search.nixos.org/packages?query=tealdeer) - Nix: [`tealdeer`](https://nixos.org/nixos/packages.html#tealdeer)
- openSUSE: [`tealdeer`](https://software.opensuse.org/package/tealdeer?search_term=tealdeer) - openSUSE: [`tealdeer`](https://software.opensuse.org/package/tealdeer?search_term=tealdeer)
- Scoop: [`tealdeer`](https://github.com/ScoopInstaller/Main/blob/master/bucket/tealdeer.json)
- Solus: [`tealdeer`](https://packages.getsol.us/shannon/t/tealdeer/) - Solus: [`tealdeer`](https://packages.getsol.us/shannon/t/tealdeer/)
- Void Linux: [`tealdeer`](https://github.com/void-linux/void-packages/tree/master/srcpkgs/tealdeer) - Void Linux: [`tealdeer`](https://github.com/void-linux/void-packages/tree/master/srcpkgs/tealdeer)
## Static Binaries (Linux) ## Static Binaries (Linux)
Static binary builds (currently for Linux only) are available on the Static binary builds (currently for Linux only) are available on the
[GitHub releases page](https://github.com/tealdeer-rs/tealdeer/releases). [GitHub releases page](https://github.com/dbrgn/tealdeer/releases).
Simply download the binary for your platform and run it! Simply download the binary for your platform and run it!
## Through `cargo install` ## Through `cargo install`
Build and install the tool via cargo... Build and install the tool via cargo...
```shell $ cargo install tealdeer
$ cargo install tealdeer
``` *(Note: You might need to install OpenSSL development headers, otherwise you get
a "failed to run custom build command for openssl-sys" error message. The
package is called `libssl-dev` on Ubuntu.)*
## Build From Source ## Build From Source
Release build: Debug build with logging enabled:
```shell $ cargo build --features logging
$ cargo build --release
```
Release build with native TLS support: Release build without logging:
```shell $ cargo build --release
$ cargo build --release --features native-tls
```
Debug build with logging support: To enable the log output, set the `RUST_LOG` env variable:
```shell $ export RUST_LOG=tldr=debug
$ cargo build --features logging
```
(To enable logging at runtime, export the `RUST_LOG=tldr=debug` env variable.)
## Autocompletion ## Autocompletion
Shell completion scripts are located in the folder `completion`. - *Bash*: copy `bash_tealdeer` to `/usr/share/bash-completion/completions/tldr`
Just copy them to their designated location: - *Fish*: copy `fish_tealdeer` to `~/.config/fish/completions/tldr.fish`
- *Zsh*: copy `zsh_tealdeer` to `/usr/share/zsh/site-functions/_tldr`
- *Bash*: `cp completion/bash_tealdeer /usr/share/bash-completion/completions/tldr`
- *Fish*: `cp completion/fish_tealdeer ~/.config/fish/completions/tldr.fish`
- *Zsh*: `cp completion/zsh_tealdeer /usr/share/zsh/site-functions/_tldr`

View file

@ -1,14 +1,13 @@
# Tealdeer: Introduction # Tealdeer: Introduction
Tealdeer is a very fast implementation of Tealdeer very fast implementation of [tldr](https://github.com/tldr-pages/tldr)
[tldr](https://github.com/tldr-pages/tldr) in Rust: Simplified, example based in Rust: Simplified, example based and community-driven man pages.
and community-driven man pages.
![Screenshot](screenshot-default.png) ![Screenshot](screenshot-default.png)
This documentation shows how to install, use and configure tealdeer. This documentation shows how to install, use and configure Tealdeer.
## Links ## Links
- [GitHub Project Page](https://github.com/tealdeer-rs/tealdeer) - [GitHub Project Page](https://github.com/dbrgn/tealdeer)
- [TLDR Pages Project](https://tldr.sh/) - [TLDR Pages Project](https://tldr.sh/)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Before After
Before After

View file

@ -1,52 +0,0 @@
# Tips and Tricks
This page features some example use cases of Tealdeer.
## Showing a random page on shell start
To display a randomly selected page, you can invoke `tldr` twice: One time to
select a page and a second time to display this page. To randomly select a page,
we use `shuf` from the GNU coreutils:
```bash
tldr --quiet $(tldr --quiet --list | shuf -n1)
```
You can also add the above command to your `.bashrc` (or similar shell
configuration file) to display a random page every time you start a new shell
session.
## Displaying all pages with their summary
If you want to extend the output of `tldr --list` with the first line summary of
each page, you can run the following Python script:
```python
#!/usr/bin/env python3
import subprocess
commands = subprocess.run(
["tldr", "--quiet", "--list"],
capture_output=True,
encoding="utf-8",
).stdout.splitlines()
for command in commands:
output = subprocess.run(
["tldr", "--quiet", command],
capture_output=True,
encoding="utf-8",
).stdout
description = output.lstrip().split("\n\n")[0]
description = " ".join(description.split())
print(f"{command} => {description}")
```
Note that there are a lot of pages and the script will run Tealdeer once for
every page, so the script may take a couple of seconds to finish.
## Extending this chapter
If you have an interesting setup with Tealdeer, feel free to share your
configuration on [our Github repository](https://github.com/tealdeer-rs/tealdeer).

View file

@ -1,38 +1,32 @@
tealdeer 1.8.1: A fast TLDR client tealdeer 1.4.1
Danilo Bargen <mail@dbrgn.ch>, Niklas Mohrin <dev@niklasmohrin.de> Danilo Bargen <mail@dbrgn.ch>, Niklas Mohrin <dev@niklasmohrin.de>
Usage: tldr [OPTIONS] [COMMAND]... A fast TLDR client
Arguments: USAGE:
[COMMAND]... The command to show (e.g. `tar` or `git log`) tldr [OPTIONS] [COMMAND]...
Options: ARGS:
-l, --list List all commands in the cache <COMMAND>... The command to show (e.g. `tar` or `git log`)
--edit-page Edit custom page with `EDITOR`
--edit-patch Edit custom patch with `EDITOR`
-f, --render <FILE> Render a specific markdown file
-p, --platform <PLATFORM> Override the operating system, can be specified multiple times
in order of preference [possible values: linux, macos, sunos,
windows, android, freebsd, netbsd, openbsd, common]
-L, --language <LANGUAGE> Override the language
-u, --update Update the local cache
--no-auto-update If auto update is configured, disable it for this run
-c, --clear-cache Clear the local cache
--config-path <FILE> Override config file location
--override-config <OVERRIDE> Override config values after reading config file (example:
`updates.auto_update = true`)
--pager Use a pager to page output
-r, --raw Display the raw markdown instead of rendering it
-q, --quiet Suppress informational messages
--show-paths Show file and directory paths used by tealdeer
--seed-config Create a basic config
--color <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://docs.tealdeer.org. OPTIONS:
-l, --list List all commands in the cache
-f, --render <FILE> Render a specific markdown file
-p, --platform <PLATFORM> Override the operating system [possible values: linux, macos,
windows, sunos, all]
-o, --os <OS> Deprecated alias of `platform`
-L, --language <LANGUAGE> Override the language
-u, --update Update the local cache
-c, --clear-cache Clear the local cache
--pager Use a pager to page output
-r, --raw Display the raw markdown instead of rendering it
-q, --quiet Suppress informational messages
--show-paths Show file and directory paths used by tealdeer
--config-path Show config file path
--seed-config Create a basic config
--color <WHEN> Control whether to use color [possible values: always, auto, never]
-v, --version Print the version
-h, --help Print help information
To view usage examples, run tldr tldr or tldr tealdeer. To view the user documentation, please visit https://dbrgn.github.io/tealdeer/.

View file

@ -1,58 +0,0 @@
# Custom Pages and Patches
> ⚠️ **Breaking change in version 1.7.0:** The file name extension for custom
> pages and patches was changed:
>
> - `<name>.page``<name>.page.md`
> - `<name>.patch``<name>.patch.md`
>
> If you have custom pages or patches, you need to rename them.
Tealdeer allows creating new custom pages, overriding existing pages as well as
extending existing pages.
The directory, where these custom pages and patches can be placed, follows OS
conventions. On Linux for instance, the default location is
`~/.local/share/tealdeer/pages/`. To print the path used on your system, simply
run `tldr --show-paths`.
The custom pages directory can be [overridden by the config
file](config_directories.html).
## Custom Pages
To document internal command line tools, or if you want to replace an existing
tldr page with one that's better suited for you, place a file with the name
`<command>.page.md` in the custom pages directory. When calling `tldr <command>`,
your custom page will be shown instead of the upstream version in the cache.
Path:
```plain
$CUSTOM_PAGES_DIR/<command>.page.md
```
Example:
```plain
~/.local/share/tealdeer/pages/ufw.page.md
```
## Custom Patches
Sometimes you don't want to fully replace an existing upstream page, but just
want to extend it with your own examples that you frequently need. In this
case, use a file called `<command>.patch.md`, it will be appended to existing
pages.
Path:
```plain
$CUSTOM_PAGES_DIR/<command>.patch.md
```
Example:
```plain
~/.local/share/tealdeer/pages/ufw.patch.md
```

24
fish_tealdeer Normal file
View file

@ -0,0 +1,24 @@
#
# Completions for the tealdeer implementation of tldr
# https://github.com/dbrgn/tealdeer/
#
complete -c tldr -s h -l help -d 'Print the help message.' -f
complete -c tldr -s v -l version -d 'Show version information.' -f
complete -c tldr -s l -l list -d 'List all commands in the cache.' -f
complete -c tldr -s f -l render -d 'Render a specific markdown file.' -r
complete -c tldr -s p -l platform -d 'Override the operating system.' -xa 'linux macos sunos windows'
complete -c tldr -s u -l update -d 'Update the local cache.' -f
complete -c tldr -s c -l clear-cache -d 'Clear the local cache.' -f
complete -c tldr -s p -l pager -d 'Use a pager to page output.' -f
complete -c tldr -s r -l raw -d 'Display the raw markdown instead of rendering it.' -f
complete -c tldr -s q -l quiet -d 'Suppress informational messages.' -f
complete -c tldr -l show-paths -d 'Show file and directory paths used by tealdeer.' -f
complete -c tldr -l seed-config -d 'Create a basic config.' -f
complete -c tldr -l color -d 'Controls when to use color.' -xa 'always auto never'
function __tealdeer_entries
tldr --list | string replace -a -i -r "\,\s" "\n"
end
complete -f -c tldr -a '(__tealdeer_entries)'

View file

@ -1,42 +0,0 @@
# 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`

77
release-build.sh Executable file
View file

@ -0,0 +1,77 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION=$(grep '^version = ' Cargo.toml | sed 's/.*"\([0-9\.]*\)".*/\1/')
GPG_KEY=EA456E8BAF0109429583EED83578F667F2F3A5FA
declare -a targets=(
"x86_64-musl"
"i686-musl"
"armv7-musleabihf"
"arm-musleabi"
"arm-musleabihf"
)
declare -a rusttargets=(
"x86_64-unknown-linux-musl"
"i686-unknown-linux-musl"
"armv7-unknown-linux-musleabihf"
"arm-unknown-linux-musleabi"
"arm-unknown-linux-musleabihf"
)
declare -a completions=(
"bash"
"fish"
"zsh"
)
function docker-download {
echo "==> Downloading Docker image: messense/rust-musl-cross:$1"
docker pull messense/rust-musl-cross:$1
}
function docker-build {
echo "==> Building target: $1"
docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:$1 cargo build --release
}
echo -e "==> Version $VERSION\n"
for target in ${targets[@]}; do docker-download $target; done
echo ""
for target in ${targets[@]}; do docker-build $target; done
echo ""
rm -rf "dist-$VERSION"
mkdir "dist-$VERSION"
for i in ${!targets[@]}; do
echo "==> Copying ${targets[$i]}"
cp "target/${rusttargets[$i]}/release/tldr" "dist-$VERSION/tldr-linux-${targets[$i]}"
done
echo ""
for target in ${targets[@]}; do
echo "==> Stripping $target"
docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:$target musl-strip -s /home/rust/src/dist-$VERSION/tldr-linux-$target
done
echo ""
for target in ${targets[@]}; do
echo "==> Signing $target"
gpg -a --output "dist-$VERSION/tldr-linux-$target.sig" --detach-sig "dist-$VERSION/tldr-linux-$target"
done
echo ""
for completion in ${completions[@]}; do
echo "==> Copying ${completion} completion"
cp "${completion}_tealdeer" "dist-$VERSION/completions_${completion}"
done
echo ""
echo "==> Copying licenses"
cp LICENSE-* "dist-$VERSION/"
echo "Done."

View file

@ -1 +0,0 @@
# Empty file, use defaults and disregard global settings

View file

@ -1,8 +0,0 @@
#!/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

View file

@ -1,88 +0,0 @@
#!/usr/bin/env bash
#
# Upload artifacts to GitHub Actions.
#
# Based on: https://gist.github.com/schell/2fe896953b6728cc3c5d8d5f9f3a17a3
#
# Requires curl and jq on PATH
# Args:
# token: GitHub API user token
# repo: GitHub username/reponame
# tag: Name of the tag for which to create a release
# description: Release description
create_release() {
# Args
token=$1
repo=$2
tag=$3
description=$4
echo "Creating release:"
echo " repo=$repo"
echo " tag=$tag"
echo ""
# Create release
http_code=$(
curl -s -o create.json -w '%{http_code}' \
--header "Accept: application/vnd.github.v3+json" \
--header "Authorization: Bearer $token" \
--header "Content-Type:application/json" \
"https://api.github.com/repos/$repo/releases" \
-d '{"tag_name":"'"$tag"'","name":"'"${tag/v/Version }"'","draft":true,"body":"'"${description/\"/\\\"}"'"}'
)
if [ "$http_code" == "201" ]; then
echo "Release for tag $tag created."
else
echo "Asset upload failed with code '$http_code'."
return 1
fi
}
# Args:
# token: GitHub API user token
# repo: GitHub username/reponame
# tag: Name of the tag for which to upload the assets
# file: Path to the asset file to upload
# name: Name to use for the uploaded asset
upload_release_file() {
# Args
token=$1
repo=$2
tag=$3
file=$4
name=$5
echo "Uploading:"
echo " repo=$repo"
echo " tag=$tag"
echo " file=$file"
echo " name=$name"
echo ""
# Determine upload URL of latest draft release for the specified tag
upload_url=$(
curl -s \
--header "Accept: application/vnd.github.v3+json" \
--header "Authorization: Bearer $token" \
"https://api.github.com/repos/$repo/releases" \
| jq -r '[.[] | select(.tag_name == "'"$tag"'" and .draft)][0].upload_url' \
| cut -d"{" -f'1'
)
echo "Determined upload URL: $upload_url"
http_code=$(
curl -s -o upload.json -w '%{http_code}' \
--request POST \
--header "Accept: application/vnd.github.v3+json" \
--header "Authorization: Bearer $token" \
--header "Content-Type: application/octet-stream" \
--data-binary "@$file" "$upload_url?name=$name"
)
if [ "$http_code" == "201" ]; then
echo "Asset $name uploaded:"
jq -r .browser_download_url upload.json
else
echo "Asset upload failed with code '$http_code':"
cat upload.json
return 1
fi
}

View file

@ -1,266 +1,39 @@
use std::{ use std::{
fs::{self, File}, env,
io::{Cursor, ErrorKind, Read}, ffi::OsStr,
fs,
io::{Cursor, Read, Seek},
iter,
path::{Path, PathBuf}, path::{Path, PathBuf},
time::{Duration, SystemTime},
}; };
use anyhow::{Context, Result, anyhow, bail, ensure}; use app_dirs::{get_app_root, AppDataType};
use log::{debug, info}; use log::debug;
use ureq::{ use reqwest::{blocking::Client, Proxy};
Agent, use std::time::{Duration, SystemTime};
http::StatusCode, use walkdir::{DirEntry, WalkDir};
tls::{RootCerts, TlsConfig, TlsProvider},
};
use zip::ZipArchive; use zip::ZipArchive;
use crate::{ use crate::{
config::{Language, TlsBackend}, error::TealdeerError::{self, CacheError, UpdateError},
types::PlatformType, types::{PathSource, PlatformStrategy, PlatformType},
}; };
static CACHE_DIR_ENV_VAR: &str = "TEALDEER_CACHE_DIR";
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(Clone)] #[derive(Debug)]
pub struct CacheConfig<'a> { pub struct Cache {
pub pages_directory: &'a Path, url: String,
pub custom_pages_directory: Option<&'a Path>, platform: PlatformStrategy,
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)]
pub struct PageLookupResult { pub struct PageLookupResult {
pub page_path: PathBuf, page_path: PathBuf,
pub patch_path: Option<PathBuf>, 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 {
@ -276,101 +49,311 @@ impl PageLookupResult {
self self
} }
/// Create a reader that sequentially reads from the page and the pub fn paths(&self) -> impl Iterator<Item = &Path> {
/// patch, as if they were concatenated. iter::once(self.page_path.as_path()).chain(self.patch_path.as_deref())
/// }
/// This will return an error if either the page file or the patch file }
/// cannot be opened.
pub fn reader(&self) -> Result<Box<dyn Read>> {
// Open page file
let page_file = File::open(&self.page_path)
.with_context(|| format!("Could not open page file at {}", self.page_path.display()))?;
// Open patch file pub enum CacheFreshness {
let patch_file_opt = match &self.patch_path { /// The cache is still fresh (less than MAX_CACHE_AGE old)
Some(path) => Some( Fresh,
File::open(path) /// The cache is stale and should be updated
.with_context(|| format!("Could not open patch file at {}", path.display()))?, Stale(Duration),
), /// The cache is missing
None => None, Missing,
}
impl Cache {
pub fn new<S>(url: S, platform: PlatformStrategy) -> Self
where
S: Into<String>,
{
Self {
url: url.into(),
platform,
}
}
/// Return the path to the cache directory.
pub fn get_cache_dir() -> Result<(PathBuf, PathSource), TealdeerError> {
// Allow overriding the cache directory by setting the env variable.
if let Ok(value) = env::var(CACHE_DIR_ENV_VAR) {
let path = PathBuf::from(value);
let (path_exists, path_is_dir) = path
.metadata()
.map_or((false, false), |md| (true, md.is_dir()));
if path_exists && !path_is_dir {
return Err(CacheError(format!(
"Path specified by ${} is not a directory.",
CACHE_DIR_ENV_VAR
)));
}
if !path_exists {
// Try to create the complete directory path.
fs::create_dir_all(&path).map_err(|_| {
CacheError(format!(
"Directory path specified by ${} cannot be created.",
CACHE_DIR_ENV_VAR
))
})?;
eprintln!(
"Successfully created cache directory path `{}`.",
path.to_str().unwrap()
);
}
return Ok((path, PathSource::EnvVar));
}; };
// Create chained reader from file(s) // Otherwise, fall back to user cache directory.
// match get_app_root(AppDataType::UserCache, &crate::APP_INFO) {
// Note: It might be worthwhile to create our own struct that accepts Ok(dirs) => Ok((dirs, PathSource::OsConvention)),
// the page and patch files and that will read them sequentially, Err(_) => Err(CacheError(
// because it avoids the boxing below. However, the performance impact "Could not determine user cache directory.".into(),
// would first need to be shown to be significant using a benchmark. )),
Ok(if let Some(patch_file) = patch_file_opt { }
Box::new(page_file.chain(&b"\n"[..]).chain(patch_file)) as Box<dyn Read> }
/// Download the archive
fn download(&self) -> Result<Vec<u8>, TealdeerError> {
let mut builder = Client::builder();
if let Ok(ref host) = env::var("HTTP_PROXY") {
if let Ok(proxy) = Proxy::http(host) {
builder = builder.proxy(proxy);
}
}
if let Ok(ref host) = env::var("HTTPS_PROXY") {
if let Ok(proxy) = Proxy::https(host) {
builder = builder.proxy(proxy);
}
}
let client = builder.build().unwrap_or_else(|_| Client::new());
let mut resp = client.get(&self.url).send()?;
let mut buf: Vec<u8> = vec![];
let bytes_downloaded = resp.copy_to(&mut buf)?;
debug!("{} bytes downloaded", bytes_downloaded);
Ok(buf)
}
/// Decompress and open the archive
fn decompress<R: Read + Seek>(reader: R) -> ZipArchive<R> {
ZipArchive::new(reader).unwrap()
}
/// Update the pages cache.
pub fn update(&self) -> Result<(), TealdeerError> {
// First, download the compressed data
let bytes: Vec<u8> = self.download()?;
// Decompress the response body into an `Archive`
let mut archive = Self::decompress(Cursor::new(bytes));
// Determine paths
let (cache_dir, _) = Self::get_cache_dir()?;
let pages_dir = cache_dir.join(TLDR_PAGES_DIR);
// Make sure that cache directory exists
debug!("Ensure cache directory {:?} exists", &cache_dir);
fs::create_dir_all(&cache_dir)
.map_err(|e| UpdateError(format!("Could not create cache directory: {}", e)))?;
// Clear cache directory
// Note: This is not the best solution. Ideally we would download the
// archive to a temporary directory and then swap the two directories.
// But renaming a directory doesn't work across filesystems and Rust
// does not yet offer a recursive directory copying function. So for
// now, we'll use this approach.
Self::clear()?;
// Extract archive
archive
.extract(&pages_dir)
.map_err(|e| UpdateError(format!("Could not unpack compressed data: {}", e)))?;
Ok(())
}
/// Return the duration since the cache directory was last modified.
pub fn last_update() -> Option<Duration> {
if let Ok((cache_dir, _)) = Self::get_cache_dir() {
if let Ok(metadata) = fs::metadata(cache_dir.join(TLDR_PAGES_DIR)) {
if let Ok(mtime) = metadata.modified() {
let now = SystemTime::now();
return now.duration_since(mtime).ok();
};
};
};
None
}
/// Return the freshness of the cache (fresh, stale or missing).
pub fn freshness() -> CacheFreshness {
match Cache::last_update() {
Some(ago) if ago > crate::config::MAX_CACHE_AGE => CacheFreshness::Stale(ago),
Some(_) => CacheFreshness::Fresh,
None => CacheFreshness::Missing,
}
}
/// Return the platform directory.
fn get_platform_dir(&self) -> &'static str {
match self.platform.platform_type {
PlatformType::Linux { .. } => "linux",
PlatformType::OsX { .. } => "osx",
PlatformType::SunOs { .. } => "sunos",
PlatformType::Windows { .. } => "windows",
}
}
/// Check for pages for a given platform in one of the given languages.
fn find_page_for_platform(
page_name: &str,
cache_dir: &Path,
platform: &str,
language_dirs: &[String],
) -> Option<PathBuf> {
language_dirs
.iter()
.map(|lang_dir| cache_dir.join(lang_dir).join(platform).join(page_name))
.find(|path| path.exists() && path.is_file())
}
/// Look up custom patch (<name>.patch). 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>,
) -> Option<PageLookupResult> {
let page_filename = format!("{}.md", name);
let patch_filename = format!("{}.patch", name);
let custom_filename = format!("{}.page", name);
// Get cache dir
let cache_dir = match Self::get_cache_dir() {
Ok((cache_dir, _)) => cache_dir.join(TLDR_PAGES_DIR),
Err(e) => {
log::error!("Could not get cache directory: {}", e);
return None;
}
};
let lang_dirs: Vec<String> = languages
.iter()
.map(|lang| {
if lang == "en" {
String::from("pages")
} else {
format!("pages.{}", lang)
}
})
.collect();
// Look up custom page (<name>.page). If it exists, return it directly
if let Some(config_dir) = custom_pages_dir {
let custom_page = config_dir.join(custom_filename);
if custom_page.exists() && custom_page.is_file() {
return Some(PageLookupResult::with_page(custom_page));
}
}
let patch_path = Self::find_patch(&patch_filename, custom_pages_dir);
// Try to find a platform specific path next, append custom patch to it.
let platform_dir = self.get_platform_dir();
if let Some(page) =
Self::find_page_for_platform(&page_filename, &cache_dir, platform_dir, &lang_dirs)
{
return Some(PageLookupResult::with_page(page).with_optional_patch(patch_path));
}
// Did not find platform specific results, fall back to "common"
Self::find_page_for_platform(&page_filename, &cache_dir, "common", &lang_dirs)
.map(|page| PageLookupResult::with_page(page).with_optional_patch(patch_path))
}
/// Return the available pages.
pub fn list_pages(&self) -> Result<Vec<String>, TealdeerError> {
// Determine platforms directory and platform
let (cache_dir, _) = Self::get_cache_dir()?;
let platforms_dir = cache_dir.join(TLDR_PAGES_DIR).join("pages");
let platform_dir = self.get_platform_dir();
// Closure that allows the WalkDir instance to traverse platform
// specific and common page directories, but not others.
let should_walk = |entry: &DirEntry| -> bool {
let file_type = entry.file_type();
let file_name = match entry.file_name().to_str() {
Some(name) => name,
None => return false,
};
if file_type.is_dir() {
return file_name == "common" || file_name == platform_dir;
} else if file_type.is_file() {
return true;
}
false
};
// Recursively walk through common and (if applicable) platform specific directory
let mut pages = WalkDir::new(platforms_dir)
.min_depth(1) // Skip root directory
.into_iter()
.filter_entry(|e| self.platform.list_all || should_walk(e)) // Filter out pages for other architectures
.filter_map(Result::ok) // Convert results to options, filter out errors
.filter_map(|e| {
let path = e.path();
let extension = &path.extension().and_then(OsStr::to_str).unwrap_or("");
if e.file_type().is_file() && extension == &"md" {
path.file_stem()
.and_then(|stem| stem.to_str().map(Into::into))
} else {
None
}
})
.collect::<Vec<String>>();
pages.sort();
pages.dedup();
Ok(pages)
}
/// Delete the cache directory.
pub fn clear() -> Result<(), TealdeerError> {
let (path, _) = Self::get_cache_dir()?;
if path.exists() && path.is_dir() {
// Delete old tldr-pages cache location as well if present
// TODO: To be removed in the future
for pages_dir_name in [TLDR_PAGES_DIR, TLDR_OLD_PAGES_DIR] {
let pages_dir = path.join(pages_dir_name);
if pages_dir.exists() {
fs::remove_dir_all(&pages_dir).map_err(|e| {
CacheError(format!(
"Could not remove cache directory ({}): {}",
pages_dir.display(),
e
))
})?;
}
}
} else if path.exists() {
return Err(CacheError(format!(
"Cache path ({}) is not a directory.",
path.display()
)));
} else { } else {
Box::new(page_file) as Box<dyn Read> return Err(CacheError(format!(
}) "Cache path ({}) does not exist.",
} path.display()
} )));
impl Language<'_> {
fn directory_name(&self) -> String {
format!("pages.{}", self.0)
}
}
impl PlatformType {
fn directory_name(self) -> &'static str {
match self {
PlatformType::Linux => "linux",
PlatformType::OsX => "osx",
PlatformType::SunOs => "sunos",
PlatformType::Windows => "windows",
PlatformType::Android => "android",
PlatformType::FreeBsd => "freebsd",
PlatformType::NetBsd => "netbsd",
PlatformType::OpenBsd => "openbsd",
PlatformType::Common => "common",
}
}
}
impl Cache<'_> {
fn build_client(tls_backend: TlsBackend) -> Agent {
let tls_builder = match tls_backend {
#[cfg(feature = "native-tls")]
TlsBackend::NativeTls => TlsConfig::builder()
.provider(TlsProvider::NativeTls)
.root_certs(RootCerts::PlatformVerifier),
#[cfg(feature = "rustls-with-webpki-roots")]
TlsBackend::RustlsWithWebpkiRoots => TlsConfig::builder()
.provider(TlsProvider::Rustls)
.root_certs(RootCerts::WebPki),
#[cfg(feature = "rustls-with-native-roots")]
TlsBackend::RustlsWithNativeRoots => TlsConfig::builder()
.provider(TlsProvider::Rustls)
.root_certs(RootCerts::PlatformVerifier),
}; };
let config = Agent::config_builder() Ok(())
.http_status_as_error(false) // because we want to handle them
.tls_config(tls_builder.build())
.build();
config.into()
}
/// Download the archive from the specified URL.
fn download(client: &Agent, archive_url: &str) -> Result<Option<Vec<u8>>> {
info!("Downloading archive from {archive_url}");
let response = client.get(archive_url).call();
match response {
Ok(response) if response.status().is_success() => {
let mut buf: Vec<u8> = Vec::new();
response.into_body().into_reader().read_to_end(&mut buf)?;
debug!("{} bytes downloaded", buf.len());
Ok(Some(buf))
}
Ok(response) if response.status() == StatusCode::NOT_FOUND => Ok(None),
_ => {
bail!("Could not download tldr pages from {archive_url}: {response:?}")
}
}
} }
} }
@ -379,53 +362,21 @@ impl Cache<'_> {
mod tests { mod tests {
use super::*; use super::*;
use std::{
fs::File,
io::{Read, Write},
};
#[test] #[test]
fn test_reader_with_patch() { fn test_page_lookup_result_iter_with_patch() {
// Write test files let lookup = PageLookupResult::with_page(PathBuf::from("test.page"))
let dir = tempfile::tempdir().unwrap(); .with_optional_patch(Some(PathBuf::from("test.patch")));
let page_path = dir.path().join("test.page.md"); let mut iter = lookup.paths();
let patch_path = dir.path().join("test.patch.md"); assert_eq!(iter.next(), Some(Path::new("test.page")));
{ assert_eq!(iter.next(), Some(Path::new("test.patch")));
let mut f1 = File::create(&page_path).unwrap(); assert_eq!(iter.next(), None);
f1.write_all(b"Hello\n").unwrap();
let mut f2 = File::create(&patch_path).unwrap();
f2.write_all(b"World").unwrap();
}
// Create chained reader from lookup result
let lr = PageLookupResult::with_page(page_path).with_optional_patch(Some(patch_path));
let mut reader = lr.reader().unwrap();
// Read into a Vec
let mut buf = Vec::new();
reader.read_to_end(&mut buf).unwrap();
assert_eq!(&buf, b"Hello\n\nWorld");
} }
#[test] #[test]
fn test_reader_without_patch() { fn test_page_lookup_result_iter_no_patch() {
// Write test file let lookup = PageLookupResult::with_page(PathBuf::from("test.page"));
let dir = tempfile::tempdir().unwrap(); let mut iter = lookup.paths();
let page_path = dir.path().join("test.page.md"); assert_eq!(iter.next(), Some(Path::new("test.page")));
{ assert_eq!(iter.next(), None);
let mut f = File::create(&page_path).unwrap();
f.write_all(b"Hello\n").unwrap();
}
// Create chained reader from lookup result
let lr = PageLookupResult::with_page(page_path);
let mut reader = lr.reader().unwrap();
// Read into a Vec
let mut buf = Vec::new();
reader.read_to_end(&mut buf).unwrap();
assert_eq!(&buf, b"Hello\n");
} }
} }

View file

@ -1,124 +0,0 @@
//! Definition of the CLI arguments and options.
use std::path::PathBuf;
use clap::{ArgGroup, Parser, builder::ArgAction};
use crate::types::{ColorOptions, PlatformType};
// Note: flag names are specified explicitly in clap attributes
// to improve readability and allow contributors to grep names like "clear-cache"
#[derive(Parser, Debug)]
#[command(
about = "A fast TLDR client",
version,
disable_version_flag = true,
author,
help_template = "{before-help}{name} {version}: {about-with-newline}{author-with-newline}
{usage-heading} {usage}
{all-args}{after-help}",
after_help = "To view the user documentation, please visit https://docs.tealdeer.org.
To view usage examples, run tldr tldr or tldr tealdeer.",
arg_required_else_help = true,
help_expected = true,
group = ArgGroup::new("command_or_file").args(&["command", "render"]),
)]
pub(crate) struct Cli {
/// The command to show (e.g. `tar` or `git log`)
#[arg(num_args(1..))]
pub command: Vec<String>,
/// List all commands in the cache
#[arg(short = 'l', long = "list")]
pub list: bool,
/// Edit custom page with `EDITOR`
#[arg(long, requires = "command")]
pub edit_page: bool,
/// Edit custom patch with `EDITOR`
#[arg(long, requires = "command", conflicts_with = "edit_page")]
pub edit_patch: bool,
/// Render a specific markdown file
#[arg(
short = 'f',
long = "render",
value_name = "FILE",
conflicts_with = "command"
)]
pub render: Option<PathBuf>,
/// Override the operating system, can be specified multiple times in order of preference
#[arg(
short = 'p',
long = "platform",
value_name = "PLATFORM",
action = ArgAction::Append,
)]
pub platforms: Option<Vec<PlatformType>>,
/// Override the language
#[arg(short = 'L', long = "language")]
pub language: Option<String>,
/// Update the local cache
#[arg(short = 'u', long = "update")]
pub update: bool,
/// If auto update is configured, disable it for this run
#[arg(long = "no-auto-update")]
pub no_auto_update: bool,
/// Clear the local cache
#[arg(short = 'c', long = "clear-cache")]
pub clear_cache: bool,
/// Override config file location
#[arg(long = "config-path", value_name = "FILE")]
pub config_path: Option<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
#[arg(long = "pager", requires = "command_or_file")]
pub pager: bool,
/// Display the raw markdown instead of rendering it
#[arg(short = 'r', long = "raw", requires = "command_or_file")]
pub raw: bool,
/// Suppress informational messages
#[arg(short = 'q', long = "quiet")]
pub quiet: bool,
/// Show file and directory paths used by tealdeer
#[arg(long = "show-paths")]
pub show_paths: bool,
/// Create a basic config
#[arg(long = "seed-config")]
pub seed_config: bool,
/// Control whether to use color
#[arg(long = "color", value_name = "WHEN")]
pub color: Option<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
// Note: We override the version flag because clap uses `-V` by default,
// while TLDR specification requires `-v` to be used.
#[arg(short = 'v', long = "version", action = ArgAction::Version)]
pub version: (),
}

File diff suppressed because it is too large Load diff

40
src/error.rs Normal file
View file

@ -0,0 +1,40 @@
use std::fmt;
use reqwest::Error as ReqwestError;
#[derive(Debug)]
#[allow(clippy::enum_variant_names)]
pub enum TealdeerError {
CacheError(String),
ConfigError(String),
UpdateError(String),
WriteError(String),
}
impl TealdeerError {
pub fn message(&self) -> &str {
match self {
Self::CacheError(msg)
| Self::ConfigError(msg)
| Self::UpdateError(msg)
| Self::WriteError(msg) => msg,
}
}
}
impl From<ReqwestError> for TealdeerError {
fn from(err: ReqwestError) -> Self {
Self::UpdateError(format!("HTTP error: {}", err.to_string()))
}
}
impl fmt::Display for TealdeerError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::CacheError(e) => write!(f, "CacheError: {}", e),
Self::ConfigError(e) => write!(f, "ConfigError: {}", e),
Self::UpdateError(e) => write!(f, "UpdateError: {}", e),
Self::WriteError(e) => write!(f, "WriteError: {}", e),
}
}
}

View file

@ -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> { pub(crate) trait Dedup<T: PartialEq + Clone> {
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> Dedup<T> for Vec<T> { impl<T: PartialEq + Clone> 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 {

View file

@ -1,81 +1,26 @@
//! Functions related to formatting and printing lines from a `Tokenizer`. //! Functions related to formatting and printing lines from a `Tokenizer`.
use crate::{extensions::FindFrom, types::LineType};
use log::debug; use log::debug;
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<T> { pub enum PageSnippet<'a> {
CommandName(T), CommandName(&'a str),
Placeholder(T), Variable(&'a str),
PlaceholderVariants { short: T, long: T }, NormalCode(&'a str),
NormalCode(T), Description(&'a str),
Description(T), Text(&'a str),
Text(T),
Title(T),
Indent(usize),
Linebreak, Linebreak,
} }
#[cfg_attr(not(test), allow(dead_code))] impl<'a> PageSnippet<'a> {
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) | Placeholder(s) | NormalCode(s) | Description(s) | Text(s) CommandName(s) | Variable(s) | NormalCode(s) | Description(s) | Text(s) => s.is_empty(),
| Title(s) => s.is_empty(),
PageSnippet::PlaceholderVariants { short, long } => short.is_empty() && long.is_empty(),
Indent(n) => *n == 0,
Linebreak => false, Linebreak => false,
} }
} }
@ -86,12 +31,10 @@ 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 str>) -> Result<(), E>, F: for<'snip> FnMut(PageSnippet<'snip>) -> Result<(), E>,
{ {
let mut command = String::new(); let mut command = String::new();
for line in lines { for line in lines {
@ -102,131 +45,51 @@ where
} }
} }
LineType::Title(title) => { LineType::Title(title) => {
if show_title { debug!("Ignoring 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::Indent(indent.command))?; process_snippet(PageSnippet::NormalCode(" "))?;
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. /// Highlight code examples including user variables in {{ curly braces }}.
/// - parse placeholders (`{{ curly braces }}`) fn highlight_code<'a, E>(
/// - replace escaped placeholder markers (`\{\{` and `\}\}`) command: &'a str,
fn highlight_code<E>( text: &'a str,
command: &str, process_snippet: &mut impl FnMut(PageSnippet<'a>) -> Result<(), E>,
mut text: &str,
process_snippet: &mut impl FnMut(PageSnippet<&str>) -> Result<(), E>,
) -> Result<(), E> { ) -> Result<(), E> {
// We replace escaped placeholder markers at the end so that our replacing does not interfere let variable_splits = text
// with finding the actual markers. .split("}}")
// NOTE: This is not optimal, as it allocates one String for each `replace` .map(|s| s.split_once("{{").unwrap_or((s, "")));
let replace_escaped = |s: &str| s.replace(r"\{\{", "{{").replace(r"\}\}", "}}"); for (code_segment, variable) in variable_splits {
highlight_code_segment(command, code_segment, process_snippet)?;
loop { process_snippet(PageSnippet::Variable(variable))?;
// 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 "\}\}"). /// Yields `NormalCode` and `CommandName` in alternating order according to the occurences of
fn find_marker(s: &str, marker: &str, forbidden_prefix: &str) -> Option<usize> { /// `command_name` in `segment`. Variables are not detected here, see `highlight_code`
let mut search_start = 0;
loop {
let marker_index = s.find_from(marker, search_start)?;
let overlaps_with_prefix = (forbidden_prefix.len() <= marker_index + 1) && {
let prefix_start = marker_index + 1 - forbidden_prefix.len();
// NOTE: The indices might not be valid character offsets, so we should do this
// comparison on raw bytes. If prefix_start is indeed not a character offset than the
// comparison is guaranteed to return false because forbidden_prefix[0] definitely _is_
// the start of a (single byte, ASCII) character.
&s.as_bytes()[prefix_start..=marker_index] == forbidden_prefix.as_bytes()
};
if !overlaps_with_prefix {
return Some(marker_index);
}
// The next valid marker cannot include the first character of the current match
search_start = marker_index + 1;
}
}
/// Yields `NormalCode` and `CommandName` in alternating order according to the occurrences of
/// `command_name` in `segment`. 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 str>) -> Result<(), E>, process_snippet: &mut impl FnMut(PageSnippet<'a>) -> 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;
@ -250,23 +113,26 @@ fn highlight_code_segment<'a, E>(
} }
/// Checks whether the characters right before and after the substring (given by half-open index interval) are whitespace (if they exist). /// Checks whether the characters right before and after the substring (given by half-open index interval) are whitespace (if they exist).
fn is_freestanding_substring(surrounding: &str, substring: (usize, usize)) -> bool { fn is_freestanding_substring(surrouding: &str, substring: (usize, usize)) -> bool {
let (start, end) = substring; let (start, end) = substring;
// "okay" meaning <exists and is whitespace> or <doesn't exist> // "okay" meaning <exists and is whitespace> or <doesn't exist>
let char_before_is_okay = surrounding[..start] let char_before_is_okay = surrouding[..start]
.chars() .chars()
.last() .last()
.is_none_or(char::is_whitespace); .filter(|prev_char| !prev_char.is_whitespace())
let char_after_is_okay = surrounding[end..] .is_none();
let char_after_is_okay = surrouding[end..]
.chars() .chars()
.next() .next()
.is_none_or(char::is_whitespace); .filter(|next_char| !next_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() {
@ -293,251 +159,80 @@ mod tests {
)); ));
} }
fn run<'a>(cmd: &'a str, segment: &'a str) -> Vec<PageSnippet<String>> { fn run<'a>(cmd: &'a str, segment: &'a str) -> Vec<PageSnippet<'a>> {
let mut yielded = Vec::new(); let mut yielded = Vec::new();
let mut process_snippet = |snip: PageSnippet<&str>| { let mut process_snippet = |snip: PageSnippet<'a>| {
if !snip.is_empty() { if !snip.is_empty() {
yielded.push(snip.map(str::to_string)); yielded.push(snip);
} }
Ok::<(), ()>(()) Ok::<(), ()>(())
}; };
highlight_code(cmd, segment, &mut process_snippet).expect("highlight code segment failed"); highlight_code_segment(cmd, segment, &mut process_snippet)
.expect("highlight code segment failed");
yielded yielded
} }
mod highlight_code_segment { #[test]
use super::*; fn test_highlight_code_segment() {
use PageSnippet::*; assert!(run("make", "").is_empty());
assert_eq!(
#[test] &run("make", "make all CC=clang -q"),
fn test_highlight_code_segment() { &[CommandName("make"), NormalCode(" all CC=clang -q")]
assert!(run("make", "").is_empty()); );
assert_eq!( assert_eq!(
&run("make", "make all CC=clang -q"), &run("make", " make money --always-make"),
&[CommandName("make"), NormalCode(" all CC=clang -q")] &[
); NormalCode(" "),
assert_eq!( CommandName("make"),
&run("make", " make money --always-make"), NormalCode(" money --always-make")
&[ ]
NormalCode(" "), );
CommandName("make"), assert_eq!(
NormalCode(" money --always-make") &run("git commit", "git commit -m 'git commit'"),
] &[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);
}
} }
mod placeholders { #[test]
use super::*; fn test_i18n() {
use PageSnippet::*; assert_eq!(
&run("mäke", "mäke höhlenrätselbücher"),
#[test] &[CommandName("mäke"), NormalCode(" höhlenrätselbücher")]
fn placeholder_vs_escaped() { );
assert_eq!( assert_eq!(
run("ping", "ping {{example.com}}"), &run(
[ "Müll",
CommandName("ping"), "1000 Gründe warum Müll heute größer ist als Müll früher, ärgerlich"
NormalCode(" "), ),
Placeholder("example.com"), &[
], NormalCode("1000 Gründe warum "),
); CommandName("Müll"),
assert_eq!( NormalCode(" heute größer ist als "),
run( CommandName("Müll"),
"docker inspect", NormalCode(" früher, ärgerlich")
r"docker inspect --format '\{\{range.NetworkSettings.Networks\}\}\{\{.IPAddress\}\}\{\{end\}\}' {{container}}" ]
), );
[ assert_eq!(
CommandName("docker inspect"), &run(
NormalCode( "übergang",
" --format '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' " "die Zustandsübergangsfunktion übergang Änderungen",
), ),
Placeholder("container"), &[
], NormalCode("die Zustandsübergangsfunktion "),
); CommandName("übergang"),
assert_eq!( NormalCode(" Änderungen")
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")],);
}
} }
mod placeholder_variants { #[test]
use super::*; fn test_empty_command() {
use PageSnippet::*; let segment = "some code";
let snippets = [NormalCode(segment)];
#[test] assert_eq!(run("", segment), snippets);
fn missing_marker() { assert_eq!(run(" ", segment), snippets);
assert_eq!( assert_eq!(run(" \t ", segment), snippets);
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]);
}
}
} }
} }

View file

@ -1,6 +1,6 @@
//! Code to split a `BufRead` instance into an iterator of `LineType`s. //! Code to split a `BufRead` instance into an iterator of `LineType`s.
use std::io::{BufRead, Read}; use std::io::BufRead;
use log::warn; use log::warn;
@ -12,7 +12,7 @@ pub enum TldrFormat {
Undecided, Undecided,
/// The original format /// The original format
V1, V1,
/// The new format (see <https://github.com/tldr-pages/tldr/pull/958>) /// The new format (see https://github.com/tldr-pages/tldr/pull/958)
V2, V2,
} }
@ -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(_) => {
@ -64,11 +64,10 @@ impl<R: BufRead> Iterator for LineIterator<R> {
self.format = TldrFormat::V1; self.format = TldrFormat::V1;
} else { } else {
// It's the new format! Drop next line. // It's the new format! Drop next line.
if let Err(e) = Read::bytes(&mut self.reader) // (Hmm, is there a way to do this without an allocation?)
.find(|b| matches!(b, Ok(b'\n') | Err(_))) let mut devnull = String::new();
.transpose() if let Err(e) = self.reader.read_line(&mut devnull) {
{ 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;
@ -96,27 +95,21 @@ mod test {
#[test] #[test]
fn test_first_line_old_format() { fn test_first_line_old_format() {
let input = "# The Title\n> Description\n"; let input = "# The Title\n\n";
let mut lines = LineIterator::new(input.as_bytes()); let mut lines = LineIterator::new(input.as_bytes());
let title = lines.next().unwrap(); let title = lines.next().unwrap();
assert_eq!(title, LineType::Title("The Title".to_string())); assert_eq!(title, LineType::Title("The Title".to_string()));
let description = lines.next().unwrap(); let empty = lines.next().unwrap();
assert_eq!( assert_eq!(empty, LineType::Empty);
description,
LineType::Description("Description".to_string())
);
} }
#[test] #[test]
fn test_first_line_new_format() { fn test_first_line_new_format() {
let input = "The Title\n=========\n> Description\n"; let input = "The Title\n=========\n\n";
let mut lines = LineIterator::new(input.as_bytes()); let mut lines = LineIterator::new(input.as_bytes());
let title = lines.next().unwrap(); let title = lines.next().unwrap();
assert_eq!(title, LineType::Title("The Title".to_string())); assert_eq!(title, LineType::Title("The Title".to_string()));
let description = lines.next().unwrap(); let empty = lines.next().unwrap();
assert_eq!( assert_eq!(empty, LineType::Empty);
description,
LineType::Description("Description".to_string())
);
} }
} }

View file

@ -15,36 +15,18 @@
#![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( use std::{env, path::PathBuf, process};
feature = "native-tls",
feature = "rustls-with-webpki-roots",
feature = "rustls-with-native-roots",
)))]
compile_error!(
"at least one of the features \"native-tls\", \"rustls-with-webpki-roots\" or \"rustls-with-native-roots\" must be enabled"
);
use std::{ use app_dirs::AppInfo;
env, use atty::Stream;
fs::create_dir_all, use clap::{AppSettings, ArgGroup, Parser};
io::{self, IsTerminal}, #[cfg(not(target_os = "windows"))]
path::Path, use pager::Pager;
process::{Command, ExitCode},
};
use anyhow::{Context, Result, anyhow};
use cache::CacheConfig;
use clap::Parser;
use config::{ConfigLoader, Language, StyleConfig, TlsBackend};
use log::debug;
use types::PlatformType;
mod cache; mod cache;
mod cli;
mod config; mod config;
mod error;
pub mod extensions; pub mod extensions;
mod formatter; mod formatter;
mod line_iterator; mod line_iterator;
@ -53,93 +35,297 @@ mod types;
mod utils; mod utils;
use crate::{ use crate::{
cache::{Cache, PageLookupResult, TLDR_PAGES_DIR}, cache::{Cache, CacheFreshness, PageLookupResult, TLDR_PAGES_DIR},
cli::Cli, config::{get_config_dir, get_config_path, make_default_config, Config},
config::{ error::TealdeerError::ConfigError,
Config, PathWithSource, PlaceholderFormat, get_config_dir, make_default_config, extensions::Dedup,
supported_tls_backends_string,
},
output::print_page, output::print_page,
types::ColorOptions, types::{ColorOptions, PlatformStrategy, PlatformType},
utils::{print_error, print_warning}, utils::{print_error, print_warning},
}; };
const NAME: &str = "tealdeer"; const NAME: &str = "tealdeer";
static TEALDEER_PAGE: &str = const APP_INFO: AppInfo = AppInfo {
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/pages/tealdeer.md")); name: NAME,
author: NAME,
};
const ARCHIVE_URL: &str = "https://tldr.sh/assets/tldr.zip";
// Note: flag names are specified explicitly in clap attributes
// to improve readability and allow contributors to grep names like "clear-cache"
#[derive(Parser, Debug)]
#[clap(about = "A fast TLDR client", author, version)]
#[clap(setting = AppSettings::ArgRequiredElseHelp)]
#[clap(setting = AppSettings::HelpRequired)]
#[clap(setting = AppSettings::DeriveDisplayOrder)]
#[clap(
after_help = "To view the user documentation, please visit https://dbrgn.github.io/tealdeer/."
)]
#[clap(group = ArgGroup::new("command_or_file").args(&["command", "render"]))]
struct Args {
/// The command to show (e.g. `tar` or `git log`)
#[clap(min_values = 1)]
command: Vec<String>,
/// List all commands in the cache
#[clap(short = 'l', long = "list")]
list: bool,
/// Render a specific markdown file
#[clap(
short = 'f',
long = "render",
value_name = "FILE",
conflicts_with = "command"
)]
render: Option<PathBuf>,
/// Override the operating system [possible values: linux, macos, windows, sunos, all]
#[clap(
short = 'p',
long = "platform",
possible_values = ["linux", "macos", "windows", "sunos", "osx", "current", "all"],
default_value = "current",
hide_possible_values = true,
hide_default_value = true,
)]
platform: PlatformStrategy,
/// Deprecated alias of `platform`
#[clap(
short = 'o',
long = "os",
conflicts_with = "platform",
possible_values = ["linux", "macos", "windows", "sunos", "osx", "current", "all"],
default_value = "current",
hide_possible_values = true,
hide_default_value = true,
)]
os: PlatformStrategy,
/// Override the language
#[clap(short = 'L', long = "language")]
language: Option<String>,
/// Update the local cache
#[clap(short = 'u', long = "update")]
update: bool,
/// Clear the local cache
#[clap(short = 'c', long = "clear-cache")]
clear_cache: bool,
/// Use a pager to page output
#[clap(long = "pager", requires = "command_or_file")]
pager: bool,
/// Display the raw markdown instead of rendering it
#[clap(short = 'r', long = "--raw", requires = "command_or_file")]
raw: bool,
/// Deprecated alias of `raw`
#[clap(
long = "markdown",
short = 'm',
requires = "command_or_file",
hidden = true
)]
markdown: bool,
/// Suppress informational messages
#[clap(short = 'q', long = "quiet")]
quiet: bool,
/// Show file and directory paths used by tealdeer
#[clap(long = "show-paths")]
show_paths: bool,
/// Show config file path
#[clap(long = "config-path")]
config_path: bool,
/// Create a basic config
#[clap(long = "seed-config")]
seed_config: bool,
/// Control whether to use color
#[clap(
long = "color",
value_name = "WHEN",
possible_values = ["always", "auto", "never"]
)]
color: Option<ColorOptions>,
/// Print the version
// Note: We override the version flag because clap uses `-V` by default,
// while TLDR specification requires `-v` to be used.
#[clap(short = 'v', long = "version")]
version: bool,
}
/// Set up display pager
#[cfg(not(target_os = "windows"))]
fn configure_pager(_: bool) {
Pager::with_default_pager("less -R").setup();
}
#[cfg(target_os = "windows")]
fn configure_pager(enable_styles: bool) {
print_warning(enable_styles, "--pager flag not available on Windows!");
}
/// The cache should get updated if this was requested by the user, or if auto
/// updates are enabled and the cache age is longer than the auto update interval.
fn should_update_cache(args: &Args, config: &Config) -> bool {
args.update
|| (config.updates.auto_update
&& Cache::last_update().map_or(true, |ago| ago >= config.updates.auto_update_interval))
}
#[derive(PartialEq)]
enum CheckCacheResult {
CacheFound,
CacheMissing,
}
/// Check the cache for freshness. If it's stale or missing, show a warning.
fn check_cache(args: &Args, enable_styles: bool) -> CheckCacheResult {
match Cache::freshness() {
CacheFreshness::Fresh => CheckCacheResult::CacheFound,
CacheFreshness::Stale(_) if args.quiet => CheckCacheResult::CacheFound,
CacheFreshness::Stale(age) => {
print_warning(
enable_styles,
&format!(
"The cache hasn't been updated for {} days.\n\
You should probably run `tldr --update` soon.",
age.as_secs() / 24 / 3600
),
);
CheckCacheResult::CacheFound
}
CacheFreshness::Missing => {
print_warning(
enable_styles,
"Cache not found. Please run `tldr --update`.",
);
CheckCacheResult::CacheMissing
}
}
}
/// Clear the cache /// Clear the cache
fn clear_cache(cache: Cache, quietly: bool) -> Result<()> { fn clear_cache(quietly: bool, enable_styles: bool) {
let cache_dir = cache.config().pages_directory.display(); Cache::clear().unwrap_or_else(|e| {
cache.clear().context("Could not clear cache")?; print_error(
enable_styles,
&format!("Could not delete cache: {}", e.message()),
);
process::exit(1);
});
if !quietly { if !quietly {
eprintln!("Successfully cleared cache at `{cache_dir}`."); eprintln!("Successfully deleted cache.");
} }
Ok(())
} }
/// Update the cache /// Update the cache
fn update_cache( fn update_cache(cache: &Cache, quietly: bool, enable_styles: bool) {
cache: &mut Cache, cache.update().unwrap_or_else(|e| {
archive_source: &str, print_error(
tls_backend: TlsBackend, enable_styles,
quietly: bool, &format!("Could not update cache: {}", e.message()),
) -> Result<()> { );
let downloaded_languages = cache process::exit(1);
.update(archive_source, tls_backend) });
.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) /// Show the config path (DEPRECATED)
.collect(); fn show_config_path(enable_styles: bool) {
if language_strings.is_empty() { match get_config_path() {
eprintln!("(none)"); Ok((config_file_path, _)) => {
} else { println!("Config path is: {}", config_file_path.to_str().unwrap());
eprintln!("{}", language_strings.join(", ")); }
Err(ConfigError(msg)) => {
print_error(
enable_styles,
&format!("Could not look up config_path: {}", msg),
);
process::exit(1);
}
Err(_) => {
print_error(enable_styles, "Unknown error");
process::exit(1);
} }
} }
Ok(())
} }
/// Show file paths /// Show file paths
fn show_paths(config: &Config) { fn show_paths() {
let config_dir = { let config_dir = get_config_dir().map_or_else(
let (mut path, source) = get_config_dir(); |e| format!("[Error: {}]", e),
path.push(""); // Trailing path separator |(mut path, source)| {
match path.to_str() { path.push(""); // Trailing path separator
Some(path) => format!("{path} ({source})"), match path.to_str() {
None => "[Invalid]".to_string(), Some(path) => format!("{} ({})", path, source),
} None => "[Invalid]".to_string(),
}; }
let config_path = config.file_path.to_string(); },
let cache_dir = config.directories.cache_dir.to_string(); );
let pages_dir = { let config_path = get_config_path().map_or_else(
let mut path = config.directories.cache_dir.path.clone(); |e| format!("[Error: {}]", e),
path.push(TLDR_PAGES_DIR); |(path, _)| path.to_str().unwrap_or("[Invalid]").to_string(),
path.push(""); // Trailing path separator );
path.display().to_string() let cache_dir = Cache::get_cache_dir().map_or_else(
}; |e| format!("[Error: {}]", e),
let custom_pages_dir = match config.directories.custom_pages_dir { |(mut path, source)| {
Some(ref path_with_source) => path_with_source.to_string(), path.push(""); // Trailing path separator
None => "[None]".to_string(), match path.to_str() {
}; Some(path) => format!("{} ({})", path, source),
println!("Config dir: {config_dir}"); None => "[Invalid]".to_string(),
println!("Config path: {config_path}"); }
println!("Cache dir: {cache_dir}"); },
println!("Pages dir: {pages_dir}"); );
println!("Custom pages dir: {custom_pages_dir}"); let pages_dir = Cache::get_cache_dir().map_or_else(
|e| format!("[Error: {}]", e),
|(mut path, _)| {
path.push(TLDR_PAGES_DIR);
path.push(""); // Trailing path separator
path.into_os_string()
.into_string()
.unwrap_or_else(|_| String::from("[Invalid]"))
},
);
println!("Config dir: {}", config_dir);
println!("Config path: {}", config_path);
println!("Cache dir: {}", cache_dir);
println!("Pages dir: {}", pages_dir);
} }
fn create_config(path: Option<&Path>) -> Result<()> { /// Create seed config file and exit
let config_file_path = make_default_config(path).context("Could not create seed config")?; fn create_config_and_exit(enable_styles: bool) {
eprintln!( match make_default_config() {
"Successfully created seed config file here: {}", Ok(config_file_path) => {
config_file_path.to_str().unwrap() eprintln!(
); "Successfully created seed config file here: {}",
Ok(()) config_file_path.to_str().unwrap()
);
process::exit(0);
}
Err(ConfigError(msg)) => {
print_error(
enable_styles,
&format!("Could not create seed config: {}", msg),
);
process::exit(1);
}
Err(_) => {
print_error(enable_styles, "Unknown error");
process::exit(1);
}
}
} }
#[cfg(feature = "logging")] #[cfg(feature = "logging")]
@ -150,295 +336,254 @@ fn init_log() {
#[cfg(not(feature = "logging"))] #[cfg(not(feature = "logging"))]
fn init_log() {} fn init_log() {}
fn spawn_editor(custom_pages_dir: &Path, file_name: &str) -> Result<()> { fn get_languages(env_lang: Option<&str>, env_language: Option<&str>) -> Vec<String> {
create_dir_all(custom_pages_dir).context("Failed to create custom pages directory")?; // Language list according to
// https://github.com/tldr-pages/tldr/blob/master/CLIENT-SPECIFICATION.md#language
let custom_page_path = custom_pages_dir.join(file_name); if env_lang.is_none() {
let Some(custom_page_path) = custom_page_path.to_str() else { return vec!["en".to_string()];
return Err(anyhow!("`custom_page_path.to_str()` failed"));
};
let Ok(editor) = env::var("EDITOR") else {
return Err(anyhow!(
"To edit a custom page, please set the `EDITOR` environment variable."
));
};
println!("Editing {custom_page_path:?}");
let status = Command::new(&editor).arg(custom_page_path).status()?;
if !status.success() {
return Err(anyhow!("{editor} exit with code {:?}", status.code()));
} }
Ok(()) let env_lang = env_lang.unwrap();
// Create an iterator that contains $LANGUAGE (':' separated list) followed by $LANG (single language)
let locales = env_language.unwrap_or("").split(':').chain([env_lang]);
let mut lang_list = Vec::new();
for locale in locales {
// Language plus country code (e.g. `en_US`)
if locale.len() >= 5 && locale.chars().nth(2) == Some('_') {
lang_list.push(&locale[..5]);
}
// Language code only (e.g. `en`)
if locale.len() >= 2 && locale != "POSIX" {
lang_list.push(&locale[..2]);
}
}
lang_list.push("en");
lang_list.clear_duplicates();
lang_list.into_iter().map(str::to_string).collect()
} }
fn main() -> ExitCode { fn get_languages_from_env() -> Vec<String> {
get_languages(
std::env::var("LANG").ok().as_deref(),
std::env::var("LANGUAGE").ok().as_deref(),
)
}
fn main() {
// Initialize logger // Initialize logger
init_log(); init_log();
// Parse arguments // Parse arguments
let args = Cli::parse(); let mut args = Args::parse();
// Determine the usage of styles // Determine the usage of styles
#[cfg(target_os = "windows")]
let ansi_support = ansi_term::enable_ansi_support().is_ok();
#[cfg(not(target_os = "windows"))]
let ansi_support = true;
let enable_styles = match args.color.unwrap_or_default() { let enable_styles = match args.color.unwrap_or_default() {
// Attempt to use styling if instructed // Attempt to use styling if instructed
ColorOptions::Always => { ColorOptions::Always => true,
yansi::enable(); // disable yansi's automatic detection for ANSI support on Windows
true
}
// Enable styling if: // Enable styling if:
// * There is `ansi_support`
// * NO_COLOR env var isn't set: https://no-color.org/ // * NO_COLOR env var isn't set: https://no-color.org/
// * The output stream is stdout (not being piped) // * The output stream is stdout (not being piped)
ColorOptions::Auto => env::var_os("NO_COLOR").is_none() && io::stdout().is_terminal(), ColorOptions::Auto => {
ansi_support && env::var_os("NO_COLOR").is_none() && atty::is(Stream::Stdout)
}
// Disable styling // Disable styling
ColorOptions::Never => false, ColorOptions::Never => false,
}; };
try_main(args, enable_styles).unwrap_or_else(|error| { // Handle renamed arguments
print_error(enable_styles, &error); if args.markdown {
ExitCode::FAILURE args.raw = true;
}) print_warning(
} enable_styles,
"The -m / --markdown flag is deprecated, use -r / --raw instead",
fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> { );
// Look up config file, if none is found fall back to default config. }
debug!("Loading config"); let default_platform = PlatformType::current();
let config_loader = match &args.config_path { if args.os.platform_type != default_platform || args.os.list_all {
Some(path) if !args.seed_config => ConfigLoader::read(path.clone(), &args.override_config) print_warning(
.context("Could not read config from given path")?, enable_styles,
_ => ConfigLoader::read_default_path(&args.override_config) "The -o / --os flag is deprecated, use -p / --platform instead",
.context("Could not read config from default path")?, );
}; args.platform = args.os;
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) { // Show config file and path, pass through
(false, false) => config.display.placeholder_format, // keep old value if args.config_path {
(true, false) => PlaceholderFormat::Short, print_warning(
(false, true) => PlaceholderFormat::Long, enable_styles,
(true, true) => PlaceholderFormat::Both, "The --config-path flag is deprecated, use --show-paths instead",
}; );
show_config_path(enable_styles);
let custom_pages_dir = config
.directories
.custom_pages_dir
.as_ref()
.map(PathWithSource::path);
// Note: According to the TLDR client spec, page names must be transparently
// lowercased before lookup:
// https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#page-names
let command = args.command.join("-").to_lowercase();
if args.edit_patch || args.edit_page {
let file_name = if args.edit_patch {
format!("{command}.patch.md")
} else {
format!("{command}.page.md")
};
custom_pages_dir
.context("To edit custom pages/patches, please specify a custom pages directory.")
.and_then(|custom_pages_dir| spawn_editor(custom_pages_dir, &file_name))?;
return Ok(ExitCode::SUCCESS);
} }
// Show various paths
if args.show_paths { if args.show_paths {
show_paths(&config); show_paths();
} }
// Create a basic config and exit // Create a basic config and exit
if args.seed_config { if args.seed_config {
create_config(args.config_path.as_deref())?; create_config_and_exit(enable_styles);
return Ok(ExitCode::SUCCESS); }
// Look up config file, if none is found fall back to default config.
let config = match Config::load(enable_styles) {
Ok(config) => config,
Err(ConfigError(msg)) => {
print_error(enable_styles, &format!("Could not load config: {}", msg));
process::exit(1);
}
Err(e) => {
print_error(enable_styles, &format!("Could not load config: {}", e));
process::exit(1);
}
};
if args.pager || config.display.use_pager {
configure_pager(enable_styles);
} }
// 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 reader = PageLookupResult::with_page(file).reader()?; let path = PageLookupResult::with_page(file);
print_page(reader, args.raw, enable_styles, args.pager, &config)?; if let Err(msg) = print_page(&path, args.raw, &config) {
return Ok(ExitCode::SUCCESS); print_error(enable_styles, &msg);
} process::exit(1);
} else {
// The tealdeer page is embedded in the binary, no cache needed process::exit(0);
if command == "tealdeer" {
print_page(
TEALDEER_PAGE.as_bytes(),
args.raw,
enable_styles,
args.pager,
&config,
)?;
return Ok(ExitCode::SUCCESS);
}
if let Some(platforms) = args.platforms {
config.search.platforms = platforms;
if !config.search.platforms.contains(&PlatformType::Common) {
config.search.platforms.push(PlatformType::Common);
}
}
let (search_languages, download_languages): (&[_], &[_]) = match args.language.as_deref() {
Some(lang) => (&[Language(lang)], &[Language(lang)]),
None => (&config.search.languages, &config.updates.download_languages),
};
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 { // Initialize cache
let age = cache.age()?; let cache = Cache::new(ARCHIVE_URL, args.platform);
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 // Clear cache, pass through
if args.clear_cache {
clear_cache(args.quiet, enable_styles);
}
// Cache update, pass through
let cache_updated = if should_update_cache(&args, &config) {
update_cache(&cache, args.quiet, enable_styles);
true
} else { } else {
// There is nothing left to do false
return Ok(ExitCode::SUCCESS);
}; };
if args.list { // Check cache presence and freshness
for page in cache.list_pages()? { if !cache_updated
println!("{page}"); && (args.list || !args.command.is_empty())
} && check_cache(&args, enable_styles) == CheckCacheResult::CacheMissing
{
process::exit(1);
}
return Ok(ExitCode::SUCCESS); // List cached commands and exit
if args.list {
// Get list of pages
let pages = cache.list_pages().unwrap_or_else(|e| {
print_error(
enable_styles,
&format!("Could not get list of pages: {}", e.message()),
);
process::exit(1);
});
// Print pages
println!("{}", pages.join("\n"));
process::exit(0);
} }
// Show command from cache // Show command from cache
if !command.is_empty() { if !args.command.is_empty() {
// TODO: Remove this check 1 year after version 1.7.0 was released // Note: According to the TLDR client spec, page names must be transparently
if cache.old_custom_pages_exist()? { // lowercased before lookup:
print_warning( // https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#page-names
enable_styles, let command = args.command.join("-").to_lowercase();
&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(),
),
);
}
let Some(result) = cache.find_page(&command) else { let languages = args
.language
.map_or_else(get_languages_from_env, |lang| vec![lang]);
// Search for command in cache
if let Some(page) = cache.find_page(
&command,
&languages,
config.directories.custom_pages_dir.as_deref(),
) {
if let Err(msg) = print_page(&page, args.raw, &config) {
print_error(enable_styles, &msg);
process::exit(1);
}
process::exit(0);
} else {
if !args.quiet { if !args.quiet {
print_warning( print_warning(
enable_styles, enable_styles,
&format!( &format!(
"Page `{command}` not found in cache.\n\ "Page `{}` not found in cache.\n\
Try updating with `tldr --update`, or submit a pull request to:\n\ 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); process::exit(1);
}; }
}
print_page( }
result.reader()?,
args.raw, #[cfg(test)]
enable_styles, mod test {
args.pager, use crate::get_languages;
&config,
)?; mod language {
use super::*;
#[test]
fn missing_lang_env() {
let lang_list = get_languages(None, Some("de:fr"));
assert_eq!(lang_list, ["en"]);
let lang_list = get_languages(None, None);
assert_eq!(lang_list, ["en"]);
}
#[test]
fn missing_language_env() {
let lang_list = get_languages(Some("de"), None);
assert_eq!(lang_list, ["de", "en"]);
}
#[test]
fn preference_order() {
let lang_list = get_languages(Some("de"), Some("fr:cn"));
assert_eq!(lang_list, ["fr", "cn", "de", "en"]);
}
#[test]
fn country_code_expansion() {
let lang_list = get_languages(Some("pt_BR"), None);
assert_eq!(lang_list, ["pt_BR", "pt", "en"]);
}
#[test]
fn ignore_posix_and_c() {
let lang_list = get_languages(Some("POSIX"), None);
assert_eq!(lang_list, ["en"]);
let lang_list = get_languages(Some("C"), None);
assert_eq!(lang_list, ["en"]);
}
#[test]
fn no_duplicates() {
let lang_list = get_languages(Some("de"), Some("fr:de:cn:de"));
assert_eq!(lang_list, ["fr", "de", "cn", "en"]);
}
} }
Ok(ExitCode::SUCCESS)
} }

View file

@ -1,116 +1,75 @@
//! Functions for printing pages to the terminal //! Functions for printing pages to the terminal
use std::io::{self, BufRead, BufReader, Read, Write}; use std::{
fs::File,
use anyhow::{Context, Result}; io::{self, BufRead, BufReader, Write},
use yansi::Paint; };
use crate::{ use crate::{
config::{Config, PlaceholderFormat, StyleConfig}, cache::PageLookupResult,
formatter::{PageSnippet, highlight_lines}, config::{Config, StyleConfig},
error::TealdeerError::WriteError,
formatter::{highlight_lines, PageSnippet},
line_iterator::LineIterator, line_iterator::LineIterator,
}; };
/// Set up display pager
///
/// SAFETY: this function may be called multiple times
#[cfg(not(target_os = "windows"))]
fn configure_pager(_: bool) {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| pager::Pager::with_default_pager("less -R").setup());
}
#[cfg(target_os = "windows")]
fn configure_pager(enable_styles: bool) {
use crate::utils::print_warning;
print_warning(enable_styles, "--pager flag not available on Windows!");
}
/// Print page by path /// Print page by path
pub fn print_page( pub fn print_page(
reader: impl Read, page: &PageLookupResult,
enable_markdown: bool, enable_markdown: bool,
enable_styles: bool,
use_pager: bool,
config: &Config, config: &Config,
) -> Result<()> { ) -> Result<(), String> {
let reader = BufReader::new(reader);
// Configure pager if applicable
if use_pager || config.display.use_pager {
configure_pager(enable_styles);
}
// Lock stdout only once, this improves performance considerably
let stdout = io::stdout(); let stdout = io::stdout();
let mut handle = stdout.lock(); let mut handle = stdout.lock();
if enable_markdown { for path in page.paths() {
// Print the raw markdown of the file. let file = File::open(path).map_err(|msg| format!("Could not open file: {}", msg))?;
for line in reader.lines() { let reader = BufReader::new(file);
let line = line.context("Error while reading from a page")?;
writeln!(handle, "{line}").context("Could not write to stdout")?;
}
} else {
// Closure that processes a page snippet and writes it to stdout
let mut process_snippet = |snip: PageSnippet<&str>| {
if snip.is_empty() {
Ok(())
} else {
print_snippet(
&mut handle,
snip,
&config.style,
config.display.placeholder_format,
)
.context("Failed to print snippet")
}
};
// Print highlighted lines if enable_markdown {
highlight_lines( // Print the raw markdown of the file.
LineIterator::new(reader), for line in reader.lines() {
&mut process_snippet, writeln!(handle, "{}", line.unwrap())
!config.display.compact, .map_err(|_| "Could not write to stdout".to_string())?;
config.display.show_title, }
config.display.indent, } else {
) let mut process_snippet = |snip: PageSnippet<'_>| {
.context("Could not write to stdout")?; if snip.is_empty() {
Ok(())
} else {
print_snippet(&mut handle, snip, &config.style)
.map_err(|e| WriteError(e.to_string()))
}
};
highlight_lines(
LineIterator::new(reader),
&mut process_snippet,
!config.display.compact,
)
.map_err(|e| format!("Could not write to stdout: {}", e.message()))?;
};
} }
// We're done outputting data, flush stdout now! handle
handle.flush().context("Could not flush stdout")?; .flush()
.map_err(|_| "Could not flush stdout".to_string())?;
Ok(()) Ok(())
} }
fn print_snippet( fn print_snippet(
writer: &mut impl Write, writer: &mut impl Write,
snip: PageSnippet<&str>, snip: PageSnippet<'_>,
style: &StyleConfig, style: &StyleConfig,
placeholder_format: PlaceholderFormat, ) -> Result<(), io::Error> {
) -> io::Result<()> {
use PageSnippet::*; use PageSnippet::*;
match snip { match snip {
CommandName(s) | Title(s) => write!(writer, "{}", s.paint(style.command_name)), CommandName(s) => write!(writer, "{}", style.command_name.paint(s)),
Placeholder(s) => write!(writer, "{}", s.paint(style.example_variable)), Variable(s) => write!(writer, "{}", style.example_variable.paint(s)),
PlaceholderVariants { short, long } => match placeholder_format { NormalCode(s) => write!(writer, "{}", style.example_code.paint(s)),
PlaceholderFormat::Short => write!(writer, "{}", short.paint(style.example_code)), Description(s) => writeln!(writer, " {}", style.description.paint(s)),
PlaceholderFormat::Long => write!(writer, "{}", long.paint(style.example_code)), Text(s) => writeln!(writer, " {}", style.example_text.paint(s)),
PlaceholderFormat::Both => {
write!(
writer,
"{}",
format!("[{short}|{long}]").paint(style.example_code)
)
}
},
NormalCode(s) => write!(writer, "{}", s.paint(style.example_code)),
Description(s) => write!(writer, "{}", s.paint(style.description)),
Text(s) => write!(writer, "{}", s.paint(style.example_text)),
Indent(n) => write!(writer, "{:n$}", ' '),
Linebreak => writeln!(writer), Linebreak => writeln!(writer),
} }
} }

View file

@ -2,21 +2,16 @@
use std::{fmt, str}; use std::{fmt, str};
use serde_derive::{Deserialize, Serialize}; use serde::Deserialize;
#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] /// The platform types supported by tldr.
#[serde(rename_all = "lowercase")] #[derive(Debug, Eq, PartialEq, Copy, Clone)]
#[allow(dead_code)] #[allow(dead_code)]
pub enum PlatformType { pub enum PlatformType {
Linux, Linux,
OsX, OsX,
Windows,
SunOs, SunOs,
Android, Windows,
FreeBsd,
NetBsd,
OpenBsd,
Common,
} }
impl fmt::Display for PlatformType { impl fmt::Display for PlatformType {
@ -24,43 +19,66 @@ impl fmt::Display for PlatformType {
match self { match self {
Self::Linux => write!(f, "Linux"), Self::Linux => write!(f, "Linux"),
Self::OsX => write!(f, "macOS / BSD"), Self::OsX => write!(f, "macOS / BSD"),
Self::Windows => write!(f, "Windows"),
Self::SunOs => write!(f, "SunOS"), Self::SunOs => write!(f, "SunOS"),
Self::Android => write!(f, "Android"), Self::Windows => write!(f, "Windows"),
Self::FreeBsd => write!(f, "FreeBSD"),
Self::NetBsd => write!(f, "NetBSD"),
Self::OpenBsd => write!(f, "OpenBSD"),
Self::Common => write!(f, "Common"),
} }
} }
} }
impl clap::ValueEnum for PlatformType { /// The platform lookup strategy.
fn value_variants<'a>() -> &'a [Self] { ///
&[ /// Includes both the platform type, as well as
Self::Linux, #[derive(Debug, Copy, Clone)]
Self::OsX, pub struct PlatformStrategy {
Self::SunOs, /// The platform type that should be looked up.
Self::Windows, pub platform_type: PlatformType,
Self::Android, /// Flag indicating whether all pages should be listed or not. This is only
Self::FreeBsd, /// used when the special platform type `all` is specified by the user.
Self::NetBsd, pub list_all: bool,
Self::OpenBsd, }
Self::Common,
] impl PlatformStrategy {
pub fn new(platform_type: PlatformType) -> Self {
Self {
platform_type,
list_all: false,
}
} }
fn to_possible_value<'a>(&self) -> Option<clap::builder::PossibleValue> { /// Return a `PlatformStrategy` containing the current platform as the
match self { /// target platform type.
Self::Linux => Some(clap::builder::PossibleValue::new("linux")), pub fn current() -> Self {
Self::OsX => Some(clap::builder::PossibleValue::new("macos").alias("osx")), Self {
Self::Windows => Some(clap::builder::PossibleValue::new("windows")), platform_type: PlatformType::current(),
Self::SunOs => Some(clap::builder::PossibleValue::new("sunos")), list_all: false,
Self::Android => Some(clap::builder::PossibleValue::new("android")), }
Self::FreeBsd => Some(clap::builder::PossibleValue::new("freebsd")), }
Self::NetBsd => Some(clap::builder::PossibleValue::new("netbsd")),
Self::OpenBsd => Some(clap::builder::PossibleValue::new("openbsd")), /// Like `current()`, but when listing the pages, return the pages for all
Self::Common => Some(clap::builder::PossibleValue::new("common")), /// platforms, not just for the current platform.
pub fn all() -> Self {
Self {
platform_type: PlatformType::current(),
list_all: true,
}
}
}
impl str::FromStr for PlatformStrategy {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"linux" => Ok(PlatformStrategy::new(PlatformType::Linux)),
"osx" | "macos" => Ok(PlatformStrategy::new(PlatformType::OsX)),
"windows" => Ok(PlatformStrategy::new(PlatformType::Windows)),
"sunos" => Ok(PlatformStrategy::new(PlatformType::SunOs)),
"current" => Ok(PlatformStrategy::current()),
"all" => Ok(PlatformStrategy::all()),
other => Err(format!(
"Unknown platform: {}. Possible values: linux, macos, osx, windows, sunos, current, all",
other
)),
} }
} }
} }
@ -71,7 +89,13 @@ impl PlatformType {
Self::Linux Self::Linux
} }
#[cfg(any(target_os = "macos", target_os = "dragonfly"))] #[cfg(any(
target_os = "macos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
))]
pub fn current() -> Self { pub fn current() -> Self {
Self::OsX Self::OsX
} }
@ -81,26 +105,6 @@ impl PlatformType {
Self::Windows Self::Windows
} }
#[cfg(target_os = "android")]
pub fn current() -> Self {
Self::Android
}
#[cfg(target_os = "freebsd")]
pub fn current() -> Self {
Self::FreeBsd
}
#[cfg(target_os = "netbsd")]
pub fn current() -> Self {
Self::NetBsd
}
#[cfg(target_os = "openbsd")]
pub fn current() -> Self {
Self::OpenBsd
}
#[cfg(not(any( #[cfg(not(any(
target_os = "linux", target_os = "linux",
target_os = "macos", target_os = "macos",
@ -108,24 +112,43 @@ impl PlatformType {
target_os = "netbsd", target_os = "netbsd",
target_os = "openbsd", target_os = "openbsd",
target_os = "dragonfly", target_os = "dragonfly",
target_os = "windows", target_os = "windows"
target_os = "android",
)))] )))]
pub fn current() -> Self { pub fn current() -> Self {
Self::Other Self::Other
} }
} }
#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize, clap::ValueEnum)] #[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum ColorOptions { pub enum ColorOptions {
Always, Always,
#[default]
Auto, Auto,
Never, Never,
} }
impl str::FromStr for ColorOptions {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"always" => Ok(Self::Always),
"auto" => Ok(Self::Auto),
"never" => Ok(Self::Never),
other => Err(format!(
"Unknown color option: {}. Possible values: always, auto, never",
other
)),
}
}
}
impl Default for ColorOptions {
fn default() -> Self {
Self::Auto
}
}
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
pub enum LineType { pub enum LineType {
Empty, Empty,
@ -193,16 +216,16 @@ impl LineType {
} }
/// The reason why a certain path (e.g. config path or cache dir) was chosen. /// The reason why a certain path (e.g. config path or cache dir) was chosen.
#[derive(Debug, PartialEq, Eq, Copy, Clone)] #[derive(Debug, PartialEq)]
pub enum PathSource { pub enum PathSource {
/// OS convention (e.g. XDG on Linux) /// OS convention (e.g. XDG on Linux)
OsConvention, OsConvention,
/// Env variable (TEALDEER_*) /// Env variable (TEALDEER_*)
EnvVar, EnvVar,
/// Config file
ConfigFile, #[allow(dead_code)] // Waiting for Pull Request #141
/// CLI argument override /// Config file variable
Cli, ConfigVar,
} }
impl fmt::Display for PathSource { impl fmt::Display for PathSource {
@ -213,8 +236,7 @@ impl fmt::Display for PathSource {
match self { match self {
Self::OsConvention => "OS convention", Self::OsConvention => "OS convention",
Self::EnvVar => "env variable", Self::EnvVar => "env variable",
Self::ConfigFile => "config file", Self::ConfigVar => "config file variable",
Self::Cli => "command line argument",
} }
) )
} }

View file

@ -1,4 +1,4 @@
use yansi::{Color, Paint}; use ansi_term::{Color, Style};
/// Print a warning to stderr. If `enable_styles` is true, then a yellow /// Print a warning to stderr. If `enable_styles` is true, then a yellow
/// message will be printed. /// message will be printed.
@ -6,16 +6,17 @@ pub fn print_warning(enable_styles: bool, message: &str) {
print_msg(enable_styles, message, "Warning: ", Color::Yellow); print_msg(enable_styles, message, "Warning: ", Color::Yellow);
} }
/// Print an anyhow error to stderr. If `enable_styles` is true, then a red /// Print an error to stderr. If `enable_styles` is true, then a red message
/// message will be printed. /// will be printed.
pub fn print_error(enable_styles: bool, error: &anyhow::Error) { pub fn print_error(enable_styles: bool, message: &str) {
print_msg(enable_styles, &format!("{error:?}"), "Error: ", Color::Red); print_msg(enable_styles, message, "Error: ", Color::Red);
} }
fn print_msg(enable_styles: bool, message: &str, prefix: &'static str, color: Color) { fn print_msg(enable_styles: bool, message: &str, prefix: &'static str, color: Color) {
if enable_styles { if enable_styles {
eprintln!("{}{}", prefix.paint(color), message.paint(color)); let style = Style::new().fg(color);
eprintln!("{}{}", style.paint(prefix), style.paint(message));
} else { } else {
eprintln!("{message}"); eprintln!("{}", message);
} }
} }

View file

@ -1,36 +0,0 @@
# git checkout
> Checkout a branch or paths to the working tree.
> More information: <https://git-scm.com/docs/git-checkout>.
- Create and switch to a new branch:
`git checkout -b {{branch_name}}`
- Create and switch to a new branch based on a specific reference (branch, remote/branch, tag are examples of valid references):
`git checkout -b {{branch_name}} {{reference}}`
- Switch to an existing local branch:
`git checkout {{branch_name}}`
- Switch to the previously checked out branch:
`git checkout -`
- Switch to an existing remote branch:
`git checkout --track {{remote_name}}/{{branch_name}}`
- Discard all unstaged changes in the current directory (see `git reset` for more undo-like commands):
`git checkout .`
- Discard unstaged changes to a given file:
`git checkout {{path/to/file}}`
- Replace a file in the current directory with the version of it committed in a given branch:
`git checkout {{branch_name}} -- {{path/to/file}}`

View file

@ -1,32 +0,0 @@
# 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\}\}}}"`

View file

@ -1,37 +0,0 @@
# apt
> Debian系ディストリビューションで使われるパッケージ管理システムです。
> Ubuntuのバージョンが16.04か、それ以降で対話モードを使う場合`apt-get`の代わりとして使用します。
> 詳しくはこちら: <https://manned.org/apt.8>
- 利用可能なパーケージとバージョンのリストの更新(他の`apt`コマンドの前での実行を推奨):
`sudo apt update`
- 指定されたパッケージの検索:
`apt search {{パッケージ}}`
- パッケージの情報を出力:
`apt show {{パッケージ}}`
- パッケージのインストール、または利用可能な最新バージョンに更新:
`sudo apt install {{パッケージ}}`
- パッケージの削除(`sudo apt remove --purge`の場合設定ファイルも削除):
`sudo apt remove {{パッケージ}}`
- インストールされている全てのパッケージを最新のバージョンにアップグレード:
`sudo apt upgrade`
- インストールできるすべてのパッケージを表示:
`apt list`
- インストールされた全てのパッケージを表示(依存関係も表示):
`apt list --installed`

32
tests/chmod.ru.expected Normal file
View file

@ -0,0 +1,32 @@
Изменить права доступа файлу или папке.
Больше информации: <https://www.gnu.org/software/coreutils/chmod>.
Дать [u]пользователю, который владеет файлом, права на его [x]исполнение:
 chmod u+x файл
Дать права [u]пользователю права [r]чтения и [w]записи в файл/папку:
 chmod u+rw файл_или_папка
Убрать права на [x]исполнение у [g]группы:
 chmod g-x файл
Дать [a]всем пользователям права на [r]чтение и [x]исполенеие:
 chmod a+rx файл
Дать [o]другим (не из группы владельцев файлом) такие же права как и у [g]группы:
 chmod o=g файл
Убрать все права у [o]других:
 chmod o= файл
Изменить права рекурсивно, дав [g]группе и [o]другим возможность [w]записи в папку:
 chmod -R g+w,o+w папка

32
tests/chmod.ru.md Normal file
View file

@ -0,0 +1,32 @@
# chmod
> Изменить права доступа файлу или папке.
> Больше информации: <https://www.gnu.org/software/coreutils/chmod>.
- Дать [u]пользователю, который владеет файлом, права на его [x]исполнение:
`chmod u+x {{файл}}`
- Дать права [u]пользователю права [r]чтения и [w]записи в файл/папку:
`chmod u+rw {{файл_или_папка}}`
- Убрать права на [x]исполнение у [g]группы:
`chmod g-x {{файл}}`
- Дать [a]всем пользователям права на [r]чтение и [x]исполенеие:
`chmod a+rx {{файл}}`
- Дать [o]другим (не из группы владельцев файлом) такие же права как и у [g]группы:
`chmod o=g {{файл}}`
- Убрать все права у [o]других:
`chmod o= {{файл}}`
- Изменить права рекурсивно, дав [g]группе и [o]другим возможность [w]записи в папку:
`chmod -R g+w,o+w {{папка}}`

View file

@ -19,3 +19,11 @@ underline = false
underline = true underline = true
bold = false bold = false
italic = true italic = true
[display]
use_pager = false
compact = false
[updates]
auto_update = false
auto_update_interval_hours = 720

View file

@ -1,3 +0,0 @@
Custom inkscape entry
My Inkscape example

View 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

5
tests/inkscape-v2.patch Normal file
View file

@ -0,0 +1,5 @@
This header shouldn't be required
=================================
Custom inkscape entry
My Inkscape example

File diff suppressed because it is too large Load diff

View file

@ -1,37 +0,0 @@
Debian系ディストリビューションで使われるパッケージ管理システムです。
Ubuntuのバージョンが16.04か、それ以降で対話モードを使う場合`apt-get`の代わりとして使用します。
詳しくはこちら: <https://manned.org/apt.8>
利用可能なパーケージとバージョンのリストの更新他の`apt`コマンドの前での実行を推奨):
sudo apt update
指定されたパッケージの検索:
apt search パッケージ
パッケージの情報を出力:
apt show パッケージ
パッケージのインストール、または利用可能な最新バージョンに更新:
sudo apt install パッケージ
パッケージの削除`sudo apt remove --purge`の場合設定ファイルも削除):
sudo apt remove パッケージ
インストールされている全てのパッケージを最新のバージョンにアップグレード:
sudo apt upgrade
インストールできるすべてのパッケージを表示:
apt list
インストールされた全てのパッケージを表示依存関係も表示:
apt list --installed

View file

@ -1,32 +0,0 @@
An SVG (Scalable Vector Graphics) editing program.
Use -z to not open the GUI and only process files in the console.
Open an SVG file in the Inkscape GUI:
inkscape filename.svg
Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):
inkscape filename.svg -e filename.png
Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):
inkscape filename.svg -e filename.png -w 600 -h 400
Export a single object, given its ID, into a bitmap:
inkscape filename.svg -i id -e object.png
Export an SVG document to PDF, converting all texts to paths:
inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
Some invalid command just to test the correct highlighting of the command name:
inkscape --use-inkscape=v3.0 file

View file

@ -1,32 +0,0 @@
An SVG (Scalable Vector Graphics) editing program.
Use -z to not open the GUI and only process files in the console.
Open an SVG file in the Inkscape GUI:
inkscape filename.svg
Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):
inkscape filename.svg -e filename.png
Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):
inkscape filename.svg -e filename.png -w 600 -h 400
Export a single object, given its ID, into a bitmap:
inkscape filename.svg -i id -e object.png
Export an SVG document to PDF, converting all texts to paths:
inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
Some invalid command just to test the correct highlighting of the command name:
inkscape --use-inkscape=v3.0 file

View file

@ -1,34 +0,0 @@
inkscape
An SVG (Scalable Vector Graphics) editing program.
Use -z to not open the GUI and only process files in the console.
Open an SVG file in the Inkscape GUI:
inkscape filename.svg
Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):
inkscape filename.svg -e filename.png
Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):
inkscape filename.svg -e filename.png -w 600 -h 400
Export a single object, given its ID, into a bitmap:
inkscape filename.svg -i id -e object.png
Export an SVG document to PDF, converting all texts to paths:
inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
Some invalid command just to test the correct highlighting of the command name:
inkscape --use-inkscape=v3.0 file

View file

@ -1,34 +0,0 @@
inkscape
An SVG (Scalable Vector Graphics) editing program.
Use -z to not open the GUI and only process files in the console.
Open an SVG file in the Inkscape GUI:
inkscape filename.svg
Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):
inkscape filename.svg -e filename.png
Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):
inkscape filename.svg -e filename.png -w 600 -h 400
Export a single object, given its ID, into a bitmap:
inkscape filename.svg -i id -e object.png
Export an SVG document to PDF, converting all texts to paths:
inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
Some invalid command just to test the correct highlighting of the command name:
inkscape --use-inkscape=v3.0 file

View file

@ -1,32 +0,0 @@
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}}"

View file

@ -1,32 +0,0 @@
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 --list-all
Send a command to a specific player:
playerctl --player player_name play-pause|next|previous|...
Send a command to all players:
playerctl --all-players play-pause|next|previous|...
Display metadata about the current track:
playerctl metadata --format "Now playing: {{artist}} - {{album}} - {{title}}"

View file

@ -1,32 +0,0 @@
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
Send a command to a specific player:
playerctl -p player_name play-pause|next|previous|...
Send a command to all players:
playerctl -a play-pause|next|previous|...
Display metadata about the current track:
playerctl metadata -f "Now playing: {{artist}} - {{album}} - {{title}}"

View file

@ -2,9 +2,8 @@
_applications() { _applications() {
local -a commands local -a commands
if commands=(${(uonzf)"$(tldr --list 2>/dev/null)"//:/\\:}); then commands=(${(uonzf)"$(tldr --list 2>/dev/null)"//:/\\:})
_describe -t commands 'command' commands _describe -t commands 'command' commands
fi
} }
_tealdeer() { _tealdeer() {
@ -14,26 +13,16 @@ _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
macos macos
sunos sunos
windows windows
android
freebsd
netbsd
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 -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]"
@ -44,8 +33,6 @@ _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'