mirror of
https://github.com/tealdeer-rs/tealdeer.git
synced 2026-08-09 17:59:11 +02:00
Compare commits
No commits in common. "main" and "v1.8.0" have entirely different histories.
23 changed files with 273 additions and 974 deletions
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
|
@ -15,13 +15,13 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
toolchain: [stable, 1.87.0] # MSRV
|
||||
toolchain: [stable, 1.85.0]
|
||||
include:
|
||||
- platform: windows-latest
|
||||
exe_suffix: .exe
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
|
|
@ -39,7 +39,7 @@ jobs:
|
|||
# expects runners have the proper Native SSL library
|
||||
cargo build --features native-tls --no-default-features
|
||||
cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-native-tls${{ matrix.exe_suffix}}
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tldr-debug-build-${{ matrix.platform }}-rust-${{ matrix.toolchain }}
|
||||
path: artifacts/
|
||||
|
|
@ -50,19 +50,19 @@ jobs:
|
|||
name: run clippy lints
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
- name: run clippy lints
|
||||
run: cargo clippy --all-targets --features logging
|
||||
run: cargo clippy --features logging
|
||||
|
||||
fmt:
|
||||
name: run rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
|
|
@ -74,7 +74,7 @@ jobs:
|
|||
name: build docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v2
|
||||
with:
|
||||
|
|
|
|||
3
.github/workflows/gh-pages.yml
vendored
3
.github/workflows/gh-pages.yml
vendored
|
|
@ -3,13 +3,12 @@ on:
|
|||
push:
|
||||
tags:
|
||||
- "v[1-9]*" # push events matching `v` followed by anything larger than 0, e.g. v1.0, v20.15.10
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v2
|
||||
|
|
|
|||
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
|
|
@ -8,7 +8,7 @@ jobs:
|
|||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v5
|
||||
- name: Create release for tag
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
matrix:
|
||||
target: ["bash", "fish", "zsh"]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v5
|
||||
- name: Upload completion
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
matrix:
|
||||
target: ["MIT", "APACHE"]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v5
|
||||
- name: Upload license
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
|
|
@ -66,14 +66,14 @@ jobs:
|
|||
- arch: "arm"
|
||||
libc: "musleabihf"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v5
|
||||
- name: Pull Docker image
|
||||
run: docker pull messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }}
|
||||
- name: Build in Docker
|
||||
run: docker run --rm -i -v "$(pwd)":/home/rust/src messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} cargo build --release
|
||||
- name: Strip binary
|
||||
run: docker run --rm -i -v "$(pwd)":/home/rust/src messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} musl-strip -s /home/rust/src/target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "tealdeer-linux-${{ matrix.arch }}-${{ matrix.libc }}"
|
||||
path: "target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr"
|
||||
|
|
@ -86,7 +86,7 @@ jobs:
|
|||
- arch: "x86_64"
|
||||
- arch: "aarch64"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
|
|
@ -94,7 +94,7 @@ jobs:
|
|||
targets: "${{ matrix.arch }}-apple-darwin"
|
||||
- name: Build
|
||||
run: cargo build --release --target ${{ matrix.arch }}-apple-darwin
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "tealdeer-macos-${{ matrix.arch }}"
|
||||
path: "target/${{ matrix.arch }}-apple-darwin/release/tldr"
|
||||
|
|
@ -102,14 +102,14 @@ jobs:
|
|||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Build
|
||||
run: cargo build --release --target x86_64-pc-windows-msvc
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "tealdeer-windows-x86_64-msvc"
|
||||
path: "target/x86_64-pc-windows-msvc/release/tldr.exe"
|
||||
|
|
@ -134,8 +134,8 @@ jobs:
|
|||
- macos-aarch64
|
||||
- windows-x86_64-msvc
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/download-artifact@v5
|
||||
- name: Upload binary
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
|
|
|
|||
55
CHANGELOG.md
55
CHANGELOG.md
|
|
@ -13,56 +13,6 @@ Possible log types:
|
|||
- `[docs]` for documentation changes.
|
||||
- `[chore]` for maintenance work.
|
||||
|
||||
### [v1.5.1][v1.5.1], [v1.6.2][v1.6.2], [v1.7.3][v1.7.3] (2026-01-25)
|
||||
|
||||
Today I am releasing three patch updates for outdated versions of tealdeer.
|
||||
They are minimal patches for Linux distributions that ship old versions of
|
||||
tealdeer which recently broke due to an upstream change. If you can choose
|
||||
freely which version of tealdeer to use, I recommend using the latest version of
|
||||
tealdeer, 1.8.1. For more details, see the "Notes to package maintainers"
|
||||
section below.
|
||||
|
||||
All three updates contain only a single change compared to their respective
|
||||
previous versions which changes the `ARCHIVE_URL` constant used for updating the
|
||||
page cache. The reason for this change is that the upstream tldr-pages
|
||||
repository shut down the domain that clients were previously required to use.
|
||||
|
||||
Note that this issue is already fixed in tealdeer 1.8.0 where we introduced a
|
||||
config file option for changing the URL used at runtime. The versions 1.8.0 and
|
||||
1.8.1 also use the new domain of the tldr-pages archive by default, so no action
|
||||
is needed for users of those versions.
|
||||
|
||||
#### Changes
|
||||
|
||||
- [fixed] Update `ARCHIVE_URL`
|
||||
|
||||
#### Notes to package maintainers
|
||||
|
||||
I have _not_ updated the lockfile for any of these releases, so the locked
|
||||
dependency versions are still the same as they were for the previous release in
|
||||
the respective v1.x series. Updating the lockfile for tealdeer 1.5.0 to remove
|
||||
any `cargo audit` warnings while also maintaining compatibility with Rust 1.54
|
||||
also brings larger changes through transitive dependencies, which contradicts my
|
||||
plan to make this update easy to plug into existing build pipelines.
|
||||
|
||||
If you want to build / distribute tealdeer v1.5.1, v1.6.2, or v1.7.3, please use
|
||||
an up to date Rust toolchain to permit updates to newer versions of (transitive)
|
||||
dependencies. Do not use the lockfile, instead update to the newest available
|
||||
dependency versions.
|
||||
|
||||
For the same reason, there are no artifacts attached to the GitHub releases of
|
||||
these versions.
|
||||
|
||||
### [v1.8.1][v1.8.1] (2025-11-11)
|
||||
|
||||
This patch release tweaks the enabled features for ureq, the library we use to
|
||||
perform HTTP requests when updating the cache. In particular, support for socks
|
||||
proxies is now enabled.
|
||||
|
||||
#### Changes:
|
||||
|
||||
- [added] Enable ureq's socks-proxy feature ([#451])
|
||||
|
||||
### [v1.8.0][v1.8.0] (2025-10-03)
|
||||
|
||||
One year and one day have passed since tealdeer version 1.7.0 was released, so
|
||||
|
|
@ -606,16 +556,12 @@ Thanks!
|
|||
[v1.4.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.3.0...v1.4.0
|
||||
[v1.4.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.4.0...v1.4.1
|
||||
[v1.5.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.4.1...v1.5.0
|
||||
[v1.5.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.5.0...v1.5.1
|
||||
[v1.6.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.5.0...v1.6.0
|
||||
[v1.6.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.6.0...v1.6.1
|
||||
[v1.6.2]: https://github.com/tealdeer-rs/tealdeer/compare/v1.6.1...v1.6.2
|
||||
[v1.7.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.6.1...v1.7.0
|
||||
[v1.7.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.0...v1.7.1
|
||||
[v1.7.2]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.1...v1.7.2
|
||||
[v1.7.3]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.2...v1.7.3
|
||||
[v1.8.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.2...v1.8.0
|
||||
[v1.8.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.8.0...v1.8.1
|
||||
|
||||
[i34]: https://github.com/tealdeer-rs/tealdeer/issues/34
|
||||
[i43]: https://github.com/tealdeer-rs/tealdeer/issues/43
|
||||
|
|
@ -721,4 +667,3 @@ Thanks!
|
|||
[#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
|
||||
|
|
|
|||
56
Cargo.lock
generated
56
Cargo.lock
generated
|
|
@ -73,6 +73,18 @@ version = "1.0.100"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "app_dirs2"
|
||||
version = "2.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7e7b35733e3a8c1ccb90385088dd5b6eaa61325cb4d1ad56e683b5224ff352e"
|
||||
dependencies = [
|
||||
"jni",
|
||||
"ndk-context",
|
||||
"winapi",
|
||||
"xdg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
|
|
@ -139,12 +151,6 @@ version = "3.19.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
|
|
@ -368,16 +374,6 @@ dependencies = [
|
|||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etcetera"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
|
|
@ -633,6 +629,12 @@ dependencies = [
|
|||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-context"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||
|
||||
[[package]]
|
||||
name = "normalize-line-endings"
|
||||
version = "0.3.0"
|
||||
|
|
@ -1076,17 +1078,6 @@ version = "0.3.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "socks"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
|
@ -1106,14 +1097,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tealdeer"
|
||||
version = "1.8.1"
|
||||
version = "1.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"app_dirs2",
|
||||
"assert_cmd",
|
||||
"clap",
|
||||
"env_logger",
|
||||
"escargot",
|
||||
"etcetera",
|
||||
"filetime",
|
||||
"log",
|
||||
"pager",
|
||||
|
|
@ -1245,7 +1236,6 @@ dependencies = [
|
|||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"socks",
|
||||
"ureq-proto",
|
||||
"utf-8",
|
||||
"webpki-root-certs",
|
||||
|
|
@ -1626,6 +1616,12 @@ version = "0.46.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
|
||||
[[package]]
|
||||
name = "xdg"
|
||||
version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546"
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ name = "tealdeer"
|
|||
readme = "README.md"
|
||||
repository = "https://github.com/tealdeer-rs/tealdeer/"
|
||||
documentation = "https://tealdeer-rs.github.io/tealdeer/"
|
||||
version = "1.8.1"
|
||||
version = "1.8.0"
|
||||
include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"]
|
||||
rust-version = "1.87" # MSRV
|
||||
rust-version = "1.85"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
|
|
@ -21,13 +21,13 @@ path = "src/main.rs"
|
|||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
app_dirs = { version = "2", package = "app_dirs2" }
|
||||
clap = { version = "4", features = ["std", "derive", "help", "usage", "cargo", "error-context", "color", "wrap_help"], default-features = false }
|
||||
env_logger = { version = "0.11", optional = true }
|
||||
etcetera = "0.11.0"
|
||||
log = "0.4"
|
||||
serde = "1.0.21"
|
||||
serde_derive = "1.0.21"
|
||||
ureq = { version = "3.0.8", default-features = false, features = ["gzip", "socks-proxy"] }
|
||||
ureq = { version = "3.0.8", default-features = false, features = ["gzip"] }
|
||||
toml = "0.8.19"
|
||||
yansi = "1"
|
||||
zip = { version = "5.1.1", default-features = false, features = ["deflate"] }
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -52,6 +52,7 @@ Docker container using sharkdp's [`hyperfine`][hyperfine-gh]
|
|||
| [`fast-tldr`][fast-tldr-gh] | Haskell | 17.0 | 0.6 | no example highlighting |
|
||||
| [`tldr-hs`][hs-gh] | Haskell | 25.1 | 0.5 | no example highlighting |
|
||||
| [`tldr-bash`][bash-gh] | Bash | 30.0 | 0.8 | |
|
||||
| [`tldr-c`][c-gh] | C | 38.4 | 1.0 | |
|
||||
| [`tldr-python-client`][python-gh] | Python | 87.0 | 2.4 | |
|
||||
| [`tldr-node-client`][node-gh] | JavaScript / NodeJS | 407.1 | 12.9 | |
|
||||
|
||||
|
|
@ -86,17 +87,6 @@ To run lints:
|
|||
$ cargo clean && cargo clippy
|
||||
|
||||
|
||||
### AI Policy
|
||||
|
||||
Using AI is generally discouraged. However, if it is used as part of a contribution, the contributor MUST:
|
||||
|
||||
1. Clearly mark what parts (if any) of a contribution were created with the help of AI tools. This includes issue and pull request comments.
|
||||
2. Check all output of AI tools before sharing it with others in the tealdeer project.
|
||||
3. Not post slop, spam, or low quality contributions. This includes pull request descriptions and comments with excessive text and markdown flair.
|
||||
4. Leave small or easy tasks to new contributors who want to learn without the use of AI. This is to maintain the presence of the `good-first-issue` tag.
|
||||
5. Be respectful of everyone's time: *maintainers and other contributors will be reviewing your PRs.*
|
||||
|
||||
|
||||
## MSRV (Minimally Supported Rust Version)
|
||||
|
||||
When publishing a tealdeer release, the Rust version required to build it
|
||||
|
|
@ -123,6 +113,7 @@ Thanks to @severen for coming up with the name "tealdeer"!
|
|||
|
||||
|
||||
[node-gh]: https://github.com/tldr-pages/tldr-node-client
|
||||
[c-gh]: https://github.com/tldr-pages/tldr-c-client
|
||||
[hs-gh]: https://github.com/psibi/tldr-hs
|
||||
[fast-tldr-gh]: https://github.com/gutjuri/fast-tldr
|
||||
[bash-gh]: https://4e4.win/tldr
|
||||
|
|
|
|||
|
|
@ -37,35 +37,3 @@ 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,
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -30,18 +30,6 @@ auto_update = true
|
|||
auto_update_interval_hours = 24
|
||||
```
|
||||
|
||||
### `warn_cache_age`
|
||||
|
||||
Controls when a warning is printed if the cache has not been updated in a while.
|
||||
By default, the warning is shown once the cache is older than 30 days. Set this
|
||||
to `"never"` to silence the warning. This is useful if, for some reason, the
|
||||
modification time does not reflect its actual age.
|
||||
|
||||
```toml
|
||||
[updates]
|
||||
warn_cache_age = "never"
|
||||
```
|
||||
|
||||
## Download configuration
|
||||
|
||||
### `download_languages`
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
tealdeer 1.8.1: A fast TLDR client
|
||||
tealdeer 1.8.0: A fast TLDR client
|
||||
Danilo Bargen <mail@dbrgn.ch>, Niklas Mohrin <dev@niklasmohrin.de>
|
||||
|
||||
Usage: tldr [OPTIONS] [COMMAND]...
|
||||
|
|
@ -29,5 +29,3 @@ Options:
|
|||
-h, --help Print help
|
||||
|
||||
To view the user documentation, please visit https://tealdeer-rs.github.io/tealdeer/.
|
||||
|
||||
To view usage examples, run tldr tldr or tldr tealdeer.
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
# tldr
|
||||
|
||||
> This is a builtin page that shows information for your installed tealdeer version.
|
||||
> More information: <https://tealdeer-rs.github.io/tealdeer/>.
|
||||
|
||||
> This page shows tealdeer specific functionality. See tldr tldr for more examples.
|
||||
|
||||
- Render a local markdown file as a tldr page:
|
||||
|
||||
`tldr --render {{path/to/file.md}}`
|
||||
|
||||
- Show the raw markdown source of a page instead of rendering it:
|
||||
|
||||
`tldr --raw {{command}}`
|
||||
|
||||
- Show file and directory paths used by tealdeer:
|
||||
|
||||
`tldr --show-paths`
|
||||
|
||||
- Create an initial config file:
|
||||
|
||||
`tldr --seed-config`
|
||||
|
||||
- Override config file location:
|
||||
|
||||
`tldr --config-path <FILE>`
|
||||
|
||||
- Open a custom page for a command in `$EDITOR` (creates it if it doesn't exist):
|
||||
|
||||
`tldr --edit-page {{command}}`
|
||||
|
||||
- Open a custom patch for a command in `$EDITOR` (appended to the existing page):
|
||||
|
||||
`tldr --edit-patch {{command}}`
|
||||
|
||||
- Clear the local cache:
|
||||
|
||||
`tldr --clear-cache`
|
||||
|
||||
- If auto update is configured, disable it for this run:
|
||||
|
||||
`tldr --no-auto-update`
|
||||
12
src/cache.rs
12
src/cache.rs
|
|
@ -1,6 +1,6 @@
|
|||
use std::{
|
||||
fs::{self, File},
|
||||
io::{Cursor, ErrorKind, Read},
|
||||
io::{BufReader, Cursor, ErrorKind, Read},
|
||||
path::{Path, PathBuf},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
|
@ -277,12 +277,12 @@ impl PageLookupResult {
|
|||
self
|
||||
}
|
||||
|
||||
/// Create a reader that sequentially reads from the page and the
|
||||
/// Create a buffered reader that sequentially reads from the page and the
|
||||
/// patch, as if they were concatenated.
|
||||
///
|
||||
/// This will return an error if either the page file or the patch file
|
||||
/// cannot be opened.
|
||||
pub fn reader(&self) -> Result<Box<dyn Read>> {
|
||||
pub fn reader(&self) -> Result<BufReader<Box<dyn Read>>> {
|
||||
// Open page file
|
||||
let page_file = File::open(&self.page_path)
|
||||
.with_context(|| format!("Could not open page file at {}", self.page_path.display()))?;
|
||||
|
|
@ -302,11 +302,11 @@ impl PageLookupResult {
|
|||
// the page and patch files and that will read them sequentially,
|
||||
// because it avoids the boxing below. However, the performance impact
|
||||
// would first need to be shown to be significant using a benchmark.
|
||||
Ok(if let Some(patch_file) = patch_file_opt {
|
||||
Ok(BufReader::new(if let Some(patch_file) = patch_file_opt {
|
||||
Box::new(page_file.chain(&b"\n"[..]).chain(patch_file)) as Box<dyn Read>
|
||||
} else {
|
||||
Box::new(page_file) as Box<dyn Read>
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -369,7 +369,7 @@ impl Cache<'_> {
|
|||
}
|
||||
Ok(response) if response.status() == StatusCode::NOT_FOUND => Ok(None),
|
||||
_ => {
|
||||
bail!("Could not download tldr pages from {archive_url}: {response:?}")
|
||||
bail!("Could not download tldr pages from {archive_url}: {response:?}",)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@ use crate::types::{ColorOptions, PlatformType};
|
|||
{usage-heading} {usage}
|
||||
|
||||
{all-args}{after-help}",
|
||||
after_help = "To view the user documentation, please visit https://tealdeer-rs.github.io/tealdeer/.
|
||||
|
||||
To view usage examples, run tldr tldr or tldr tealdeer.",
|
||||
after_help = "To view the user documentation, please visit https://tealdeer-rs.github.io/tealdeer/.",
|
||||
arg_required_else_help = true,
|
||||
help_expected = true,
|
||||
group = ArgGroup::new("command_or_file").args(&["command", "render"]),
|
||||
|
|
|
|||
294
src/config.rs
294
src/config.rs
|
|
@ -1,16 +1,15 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
env, fmt,
|
||||
fs::{self, File},
|
||||
io::{ErrorKind, Write},
|
||||
path::{Component, Path, PathBuf},
|
||||
path::{Path, PathBuf},
|
||||
sync::LazyLock,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, ensure, Context, Result};
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
use app_dirs::{get_app_root, AppDataType};
|
||||
use clap::ValueEnum;
|
||||
use log::info;
|
||||
use serde::Serialize as _;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use yansi::{Color, Style};
|
||||
|
|
@ -32,70 +31,10 @@ const SUPPORTED_TLS_BACKENDS: &[RawTlsBackend] = &[
|
|||
RawTlsBackend::RustlsWithNativeRoots,
|
||||
];
|
||||
|
||||
struct SystemDirectories {
|
||||
config: PathBuf,
|
||||
cache: PathBuf,
|
||||
data: PathBuf,
|
||||
}
|
||||
|
||||
impl SystemDirectories {
|
||||
fn discover() -> Result<Self> {
|
||||
use etcetera::{
|
||||
app_strategy::choose_native_strategy, choose_app_strategy, AppStrategy, AppStrategyArgs,
|
||||
};
|
||||
|
||||
let args = AppStrategyArgs {
|
||||
top_level_domain: String::new(),
|
||||
author: String::new(),
|
||||
app_name: crate::NAME.to_string(),
|
||||
};
|
||||
|
||||
// The app strategy prefers XDG on MacOs, whereas the native strategy returns paths which
|
||||
// are used by installed applications. On Linux and Windows, the strategies are the same.
|
||||
let app_dirs = choose_app_strategy(args.clone())?;
|
||||
let native_dirs = choose_native_strategy(args)?;
|
||||
|
||||
// We prefer the XDG paths, but before tealdeer 1.9, we used only the native paths on MacOs.
|
||||
// So if we find files in these locations, we keep using them.
|
||||
let fallback = |app_dir: PathBuf, native_dir: PathBuf| {
|
||||
if !app_dir.exists() && native_dir.exists() {
|
||||
native_dir
|
||||
} else {
|
||||
app_dir
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
config: fallback(app_dirs.config_dir(), native_dirs.config_dir()),
|
||||
cache: fallback(app_dirs.cache_dir(), native_dirs.cache_dir()),
|
||||
data: fallback(app_dirs.data_dir(), native_dirs.data_dir()),
|
||||
})
|
||||
}
|
||||
}
|
||||
static SYSTEM_DIRECTORIES: LazyLock<SystemDirectories> = LazyLock::new(|| {
|
||||
SystemDirectories::discover().expect("Failed to initialize system directories.")
|
||||
});
|
||||
|
||||
pub(crate) fn supported_tls_backends_string() -> String {
|
||||
SUPPORTED_TLS_BACKENDS
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
fn default_underline() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
const fn default_base_indent() -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
const fn default_command_indent() -> usize {
|
||||
6
|
||||
}
|
||||
|
||||
fn default_bold() -> bool {
|
||||
false
|
||||
}
|
||||
|
|
@ -224,25 +163,6 @@ struct RawDisplayConfig {
|
|||
pub use_pager: bool,
|
||||
#[serde(default)]
|
||||
pub show_title: bool,
|
||||
#[serde(default)]
|
||||
pub indent: RawIndent,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
struct RawIndent {
|
||||
#[serde(default = "default_base_indent")]
|
||||
base: usize,
|
||||
#[serde(default = "default_command_indent")]
|
||||
command: usize,
|
||||
}
|
||||
|
||||
impl Default for RawIndent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base: 2,
|
||||
command: 6,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RawDisplayConfig> for DisplayConfig {
|
||||
|
|
@ -251,10 +171,6 @@ impl From<&RawDisplayConfig> for DisplayConfig {
|
|||
compact: raw_display_config.compact,
|
||||
use_pager: raw_display_config.use_pager,
|
||||
show_title: raw_display_config.show_title,
|
||||
indent: Indent {
|
||||
base: raw_display_config.indent.base,
|
||||
command: raw_display_config.indent.command,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -268,18 +184,7 @@ const fn default_auto_update_interval_hours() -> u64 {
|
|||
}
|
||||
|
||||
fn default_archive_source() -> String {
|
||||
"https://github.com/tldr-pages/tldr/releases/latest/download".to_owned()
|
||||
}
|
||||
|
||||
/// Controls when a warning about an outdated cache is printed.
|
||||
///
|
||||
/// Currently, the only nameable option is `"never"`. In the future, this may
|
||||
/// be extended to also accept a duration (e.g. `"60d"`), after which the
|
||||
/// warning should be shown.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum RawWarnCacheAge {
|
||||
Never,
|
||||
"https://github.com/tldr-pages/tldr/releases/latest/download/".to_owned()
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
|
|
@ -294,8 +199,6 @@ struct RawUpdatesConfig {
|
|||
pub tls_backend: RawTlsBackend,
|
||||
#[serde(default)]
|
||||
pub download_languages: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub warn_cache_age: Option<RawWarnCacheAge>,
|
||||
}
|
||||
|
||||
impl Default for RawUpdatesConfig {
|
||||
|
|
@ -306,7 +209,6 @@ impl Default for RawUpdatesConfig {
|
|||
archive_source: default_archive_source(),
|
||||
tls_backend: RawTlsBackend::default(),
|
||||
download_languages: None,
|
||||
warn_cache_age: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -421,13 +323,6 @@ pub struct DisplayConfig {
|
|||
pub compact: bool,
|
||||
pub use_pager: bool,
|
||||
pub show_title: bool,
|
||||
pub indent: Indent,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Indent {
|
||||
pub base: usize,
|
||||
pub command: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
@ -437,7 +332,6 @@ pub struct UpdatesConfig<'a> {
|
|||
pub archive_source: &'a str,
|
||||
pub tls_backend: TlsBackend,
|
||||
pub download_languages: Vec<Language<'a>>,
|
||||
pub warn_cache_age: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
@ -489,11 +383,6 @@ fn get_languages<'a>(
|
|||
|
||||
let mut lang_list = Vec::new();
|
||||
for locale in locales {
|
||||
if !locale.is_ascii() {
|
||||
info!("Skipping non-ASCII locale string: {locale}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Language plus country code (e.g. `en_US`)
|
||||
if locale.len() >= 5 && locale.chars().nth(2) == Some('_') {
|
||||
lang_list.push(Language(&locale[..5]));
|
||||
|
|
@ -571,31 +460,12 @@ impl TryFrom<RawTlsBackend> for TlsBackend {
|
|||
_ => Err(anyhow!(
|
||||
"Unsupported TLS backend: {}. This tealdeer build has support for the following options: {}",
|
||||
raw,
|
||||
supported_tls_backends_string(),
|
||||
SUPPORTED_TLS_BACKENDS.iter().map(std::string::ToString::to_string).collect::<Vec<String>>().join(", ")
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TlsBackend {
|
||||
const fn as_raw(self) -> RawTlsBackend {
|
||||
match self {
|
||||
#[cfg(feature = "native-tls")]
|
||||
Self::NativeTls => RawTlsBackend::NativeTls,
|
||||
#[cfg(feature = "rustls-with-webpki-roots")]
|
||||
Self::RustlsWithWebpkiRoots => RawTlsBackend::RustlsWithWebpkiRoots,
|
||||
#[cfg(feature = "rustls-with-native-roots")]
|
||||
Self::RustlsWithNativeRoots => RawTlsBackend::RustlsWithNativeRoots,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TlsBackend {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.as_raw().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Config<'a> {
|
||||
pub style: StyleConfig,
|
||||
|
|
@ -627,17 +497,12 @@ impl<'a> Config<'a> {
|
|||
|| search.languages.clone(),
|
||||
|languages| languages.iter().map(|lang| Language(lang)).collect(),
|
||||
),
|
||||
warn_cache_age: match raw_config.updates.warn_cache_age {
|
||||
None => Some(MAX_CACHE_AGE),
|
||||
Some(RawWarnCacheAge::Never) => None,
|
||||
},
|
||||
};
|
||||
|
||||
let relative_path_root = config_file_path
|
||||
.path()
|
||||
.parent()
|
||||
.context("Failed to get config directory")?;
|
||||
let home_path = env::home_dir();
|
||||
|
||||
// Determine directories config. For this, we need to take some
|
||||
// additional factory into account, like env variables, or the
|
||||
|
|
@ -653,43 +518,42 @@ impl<'a> Config<'a> {
|
|||
source: PathSource::EnvVar,
|
||||
}
|
||||
} else if let Some(config_value) = &raw_config.directories.cache_dir {
|
||||
// Resolve possible ~ prefixed path
|
||||
let expanded_path = expand_home(config_value, home_path.as_deref())?;
|
||||
// Resolve possible relative path.
|
||||
let resolved_path = relative_path_root.join(expanded_path);
|
||||
|
||||
// If the user explicitly configured a cache directory, use that.
|
||||
PathWithSource {
|
||||
path: resolved_path,
|
||||
// Resolve possible relative path. It would be nicer to clean up the path, but Rust stdlib
|
||||
// does not give any method for that that does not need the paths to exist.
|
||||
path: relative_path_root.join(config_value),
|
||||
source: PathSource::ConfigFile,
|
||||
}
|
||||
} else {
|
||||
} else if let Ok(default_dir) = get_app_root(AppDataType::UserCache, &crate::APP_INFO) {
|
||||
// Otherwise, fall back to the default user cache directory.
|
||||
PathWithSource {
|
||||
path: SYSTEM_DIRECTORIES.cache.clone(),
|
||||
path: default_dir,
|
||||
source: PathSource::OsConvention,
|
||||
}
|
||||
} else {
|
||||
// If everything fails, give up
|
||||
bail!("Could not determine user cache directory");
|
||||
};
|
||||
let custom_pages_dir = raw_config
|
||||
.directories
|
||||
.custom_pages_dir
|
||||
.as_ref()
|
||||
.map(|path| -> Result<PathWithSource> {
|
||||
// Resolve possible ~ prefixed path
|
||||
let expanded_path = expand_home(path, home_path.as_deref())?;
|
||||
.map(|path| PathWithSource {
|
||||
// Resolve possible relative path.
|
||||
let resolved_path = relative_path_root.join(expanded_path);
|
||||
|
||||
Ok(PathWithSource {
|
||||
path: resolved_path,
|
||||
source: PathSource::ConfigFile,
|
||||
})
|
||||
path: relative_path_root.join(path),
|
||||
source: PathSource::ConfigFile,
|
||||
})
|
||||
.transpose()?
|
||||
.or_else(|| {
|
||||
// Note: The `join("")` call ensures that there's a trailing slash
|
||||
Some(PathWithSource {
|
||||
path: SYSTEM_DIRECTORIES.data.join("pages").join(""),
|
||||
source: PathSource::OsConvention,
|
||||
})
|
||||
get_app_root(AppDataType::UserData, &crate::APP_INFO)
|
||||
.map(|path| {
|
||||
// Note: The `join("")` call ensures that there's a trailing slash
|
||||
PathWithSource {
|
||||
path: path.join("pages").join(""),
|
||||
source: PathSource::OsConvention,
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
let directories = DirectoriesConfig {
|
||||
cache_dir,
|
||||
|
|
@ -707,29 +571,6 @@ impl<'a> Config<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Expands tilde (~) prefixed directories into its absolute version
|
||||
fn expand_home<'a>(input_path: &'a Path, home_path: Option<&Path>) -> Result<Cow<'a, Path>> {
|
||||
let mut components = input_path.components();
|
||||
|
||||
if let Some(Component::Normal(first_component_raw)) = components.next() {
|
||||
let first_component = first_component_raw
|
||||
.to_str()
|
||||
.ok_or(anyhow!("Path contains invalid UTF-8"))?;
|
||||
|
||||
if first_component == "~" {
|
||||
let home_path = home_path.ok_or(anyhow!("Unable to find user home directory"))?;
|
||||
let rest: PathBuf = components.collect();
|
||||
let expanded = home_path.join(rest);
|
||||
|
||||
return Ok(Cow::Owned(expanded));
|
||||
} else if first_component.starts_with('~') {
|
||||
return Err(anyhow!("Tilde expansion with a login name not supported"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Cow::Borrowed(input_path))
|
||||
}
|
||||
|
||||
/// The [`ConfigLoader`] is used to load a [`Config`] from a file.
|
||||
///
|
||||
/// Since the rich [`Config`] keeps references to [`RawConfig`], the raw config needs to be kept alive outside of the
|
||||
|
|
@ -778,7 +619,7 @@ impl ConfigLoader {
|
|||
/// Create a loader that uses the default config file location. If no file is present at the default location, the
|
||||
/// default configuration is used.
|
||||
pub fn read_default_path() -> Result<Self> {
|
||||
let path = get_default_config_path();
|
||||
let path = get_default_config_path().context("Could not determine default config path.")?;
|
||||
Self::read_internal(path, true)
|
||||
}
|
||||
|
||||
|
|
@ -796,24 +637,30 @@ impl ConfigLoader {
|
|||
///
|
||||
/// Note that this function does not verify whether the directory at that
|
||||
/// location exists, or is a directory.
|
||||
pub fn get_config_dir() -> (PathBuf, PathSource) {
|
||||
pub fn get_config_dir() -> Result<(PathBuf, PathSource)> {
|
||||
// Allow overriding the config directory by setting the
|
||||
// $TEALDEER_CONFIG_DIR env variable.
|
||||
if let Ok(value) = env::var("TEALDEER_CONFIG_DIR") {
|
||||
return (PathBuf::from(value), PathSource::EnvVar);
|
||||
return Ok((PathBuf::from(value), PathSource::EnvVar));
|
||||
}
|
||||
|
||||
(SYSTEM_DIRECTORIES.config.clone(), PathSource::OsConvention)
|
||||
// Otherwise, fall back to the user config directory.
|
||||
let dirs = get_app_root(AppDataType::UserConfig, &crate::APP_INFO)
|
||||
.context("Failed to determine the user config directory")?;
|
||||
Ok((dirs, PathSource::OsConvention))
|
||||
}
|
||||
|
||||
/// Return the path to the config file.
|
||||
///
|
||||
/// Note that this function does not verify whether the file at that location
|
||||
/// exists, or is a file.
|
||||
pub fn get_default_config_path() -> PathWithSource {
|
||||
let (mut path, source) = get_config_dir();
|
||||
path.push(CONFIG_FILE_NAME);
|
||||
PathWithSource { path, source }
|
||||
pub fn get_default_config_path() -> Result<PathWithSource> {
|
||||
let (config_dir, source) = get_config_dir()?;
|
||||
let config_file_path = config_dir.join(CONFIG_FILE_NAME);
|
||||
Ok(PathWithSource {
|
||||
path: config_file_path,
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create default config file.
|
||||
|
|
@ -823,7 +670,7 @@ pub fn make_default_config(path: Option<&Path>) -> Result<PathBuf> {
|
|||
let config_file_path = if let Some(p) = path {
|
||||
p.into()
|
||||
} else {
|
||||
let (config_dir, _) = get_config_dir();
|
||||
let (config_dir, _) = get_config_dir()?;
|
||||
|
||||
// Ensure that config directory exists
|
||||
if config_dir.exists() {
|
||||
|
|
@ -872,63 +719,6 @@ mod test {
|
|||
assert_eq!(raw_config, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_with_valid_home() {
|
||||
let home = Some(PathBuf::from("/foo/bar"));
|
||||
let path_to_expand = PathBuf::from("~/baz");
|
||||
|
||||
assert_eq!(
|
||||
*expand_home(&path_to_expand, home.as_deref()).unwrap(),
|
||||
PathBuf::from("/foo/bar/baz")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_path_with_absolute_path() {
|
||||
let home = Some(PathBuf::from("/foo/bar"));
|
||||
let dir_to_expand = PathBuf::from("/one/two");
|
||||
|
||||
assert_eq!(
|
||||
*expand_home(&dir_to_expand, home.as_deref()).unwrap(),
|
||||
dir_to_expand
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_tilde_username() {
|
||||
let home = Some(PathBuf::from("/foo/bar"));
|
||||
let dir_to_expand = PathBuf::from("~baz/foo");
|
||||
|
||||
assert!(expand_home(&dir_to_expand, home.as_deref()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_tilde_in_config_file() {
|
||||
let mut raw_config = RawConfig::default();
|
||||
raw_config.directories.cache_dir = Some("~/my/custom_cache".into());
|
||||
raw_config.directories.custom_pages_dir = Some("~/custom_pages".into());
|
||||
|
||||
let config = Config::from_raw(
|
||||
&raw_config,
|
||||
PathWithSource {
|
||||
path: PathBuf::from("/path/to/config/config.toml"),
|
||||
source: PathSource::OsConvention,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let home_dir = env::home_dir().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config.directories.cache_dir.path(),
|
||||
home_dir.join("my/custom_cache")
|
||||
);
|
||||
assert_eq!(
|
||||
config.directories.custom_pages_dir.unwrap().path(),
|
||||
home_dir.join("custom_pages")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_path_resolution() {
|
||||
let mut raw_config = RawConfig::default();
|
||||
|
|
|
|||
401
src/formatter.rs
401
src/formatter.rs
|
|
@ -2,54 +2,21 @@
|
|||
|
||||
use log::debug;
|
||||
|
||||
use crate::{config::Indent, extensions::FindFrom, types::LineType};
|
||||
use crate::{extensions::FindFrom, types::LineType};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// Represents a snippet from a page of a specific highlighting class.
|
||||
pub enum PageSnippet<T> {
|
||||
CommandName(T),
|
||||
Variable(T),
|
||||
NormalCode(T),
|
||||
Description(T),
|
||||
Text(T),
|
||||
Title(T),
|
||||
pub enum PageSnippet<'a> {
|
||||
CommandName(&'a str),
|
||||
Variable(&'a str),
|
||||
NormalCode(&'a str),
|
||||
Description(&'a str),
|
||||
Text(&'a str),
|
||||
Title(&'a str),
|
||||
Linebreak,
|
||||
}
|
||||
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
impl<T> PageSnippet<T> {
|
||||
pub fn map<F, U>(self, f: F) -> PageSnippet<U>
|
||||
where
|
||||
F: FnOnce(T) -> U,
|
||||
{
|
||||
match self {
|
||||
PageSnippet::CommandName(s) => PageSnippet::CommandName(f(s)),
|
||||
PageSnippet::Variable(s) => PageSnippet::Variable(f(s)),
|
||||
PageSnippet::NormalCode(s) => PageSnippet::NormalCode(f(s)),
|
||||
PageSnippet::Description(s) => PageSnippet::Description(f(s)),
|
||||
PageSnippet::Text(s) => PageSnippet::Text(f(s)),
|
||||
PageSnippet::Title(s) => PageSnippet::Title(f(s)),
|
||||
PageSnippet::Linebreak => PageSnippet::Linebreak,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq<U>, U> PartialEq<PageSnippet<U>> for PageSnippet<T> {
|
||||
fn eq(&self, other: &PageSnippet<U>) -> bool {
|
||||
match (self, other) {
|
||||
(PageSnippet::CommandName(s), PageSnippet::CommandName(t))
|
||||
| (PageSnippet::Variable(s), PageSnippet::Variable(t))
|
||||
| (PageSnippet::NormalCode(s), PageSnippet::NormalCode(t))
|
||||
| (PageSnippet::Description(s), PageSnippet::Description(t))
|
||||
| (PageSnippet::Text(s), PageSnippet::Text(t))
|
||||
| (PageSnippet::Title(s), PageSnippet::Title(t)) => s == t,
|
||||
(PageSnippet::Linebreak, PageSnippet::Linebreak) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PageSnippet<&str> {
|
||||
impl PageSnippet<'_> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
use PageSnippet::*;
|
||||
|
||||
|
|
@ -68,14 +35,11 @@ pub fn highlight_lines<L, F, E>(
|
|||
process_snippet: &mut F,
|
||||
keep_empty_lines: bool,
|
||||
show_title: bool,
|
||||
indent: Indent,
|
||||
) -> Result<(), E>
|
||||
where
|
||||
L: Iterator<Item = LineType>,
|
||||
F: for<'snip> FnMut(PageSnippet<&'snip str>) -> Result<(), E>,
|
||||
F: for<'snip> FnMut(PageSnippet<'snip>) -> Result<(), E>,
|
||||
{
|
||||
let base_indent = " ".repeat(indent.base);
|
||||
let command_indent = " ".repeat(indent.command);
|
||||
let mut command = String::new();
|
||||
for line in lines {
|
||||
match line {
|
||||
|
|
@ -87,29 +51,19 @@ where
|
|||
LineType::Title(title) => {
|
||||
if show_title {
|
||||
process_snippet(PageSnippet::Linebreak)?;
|
||||
process_snippet(PageSnippet::Title(&base_indent))?;
|
||||
process_snippet(PageSnippet::Title(&title))?;
|
||||
process_snippet(PageSnippet::Linebreak)?;
|
||||
} else {
|
||||
debug!("Ignoring title");
|
||||
}
|
||||
// This is safe as long as the parsed title is only the command,
|
||||
// and the iterator yields values in order of appearance.
|
||||
command = title;
|
||||
debug!("Detected command name: {command}");
|
||||
}
|
||||
LineType::Description(text) => {
|
||||
process_snippet(PageSnippet::Description(&base_indent))?;
|
||||
process_snippet(PageSnippet::Description(&text))?;
|
||||
process_snippet(PageSnippet::Linebreak)?;
|
||||
}
|
||||
LineType::ExampleText(text) => {
|
||||
process_snippet(PageSnippet::Text(&base_indent))?;
|
||||
process_snippet(PageSnippet::Text(&text))?;
|
||||
process_snippet(PageSnippet::Linebreak)?;
|
||||
debug!("Detected command name: {}", &command);
|
||||
}
|
||||
LineType::Description(text) => process_snippet(PageSnippet::Description(&text))?,
|
||||
LineType::ExampleText(text) => process_snippet(PageSnippet::Text(&text))?,
|
||||
LineType::ExampleCode(text) => {
|
||||
process_snippet(PageSnippet::NormalCode(&command_indent))?;
|
||||
process_snippet(PageSnippet::NormalCode(" "))?;
|
||||
highlight_code(&command, &text, process_snippet)?;
|
||||
process_snippet(PageSnippet::Linebreak)?;
|
||||
}
|
||||
|
|
@ -121,86 +75,29 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Highlight code examples.
|
||||
/// - parse placeholders (`{{ curly braces }}`)
|
||||
/// - replace escaped placeholder markers (`\{\{` and `\}\}`)
|
||||
fn highlight_code<E>(
|
||||
command: &str,
|
||||
mut text: &str,
|
||||
process_snippet: &mut impl FnMut(PageSnippet<&str>) -> Result<(), E>,
|
||||
/// Highlight code examples including user variables in {{ curly braces }}.
|
||||
fn highlight_code<'a, E>(
|
||||
command: &'a str,
|
||||
text: &'a str,
|
||||
process_snippet: &mut impl FnMut(PageSnippet<'a>) -> Result<(), E>,
|
||||
) -> Result<(), E> {
|
||||
// We replace escaped placeholder markers at the end so that our replacing does not interfere
|
||||
// with finding the actual markers.
|
||||
// NOTE: This is not optimal, as it allocates one String for each `replace`
|
||||
let replace_escaped = |s: &str| s.replace(r"\{\{", "{{").replace(r"\}\}", "}}");
|
||||
|
||||
loop {
|
||||
// Find placeholder markers and split into code and placeholder accordingly
|
||||
|
||||
let Some(start_marker) = find_marker(text, "{{", r"\{\{") else {
|
||||
break;
|
||||
};
|
||||
let Some(mut end_marker) = find_marker(&text[start_marker + 2..], "}}", r"\}\}") else {
|
||||
break;
|
||||
};
|
||||
end_marker += start_marker + 2;
|
||||
|
||||
// Greedily extend matched range
|
||||
while end_marker + 2 < text.len() && text.as_bytes()[end_marker + 2] == b'}' {
|
||||
end_marker += 1;
|
||||
}
|
||||
|
||||
let placeholder_content = &text[start_marker + 2..end_marker];
|
||||
|
||||
if start_marker > 0 {
|
||||
highlight_code_segment(
|
||||
command,
|
||||
&replace_escaped(&text[..start_marker]),
|
||||
process_snippet,
|
||||
)?;
|
||||
}
|
||||
process_snippet(PageSnippet::Variable(&replace_escaped(placeholder_content)))?;
|
||||
|
||||
text = &text[end_marker + 2..];
|
||||
let variable_splits = text
|
||||
.split("}}")
|
||||
.map(|s| s.split_once("{{").unwrap_or((s, "")));
|
||||
for (code_segment, variable) in variable_splits {
|
||||
highlight_code_segment(command, code_segment, process_snippet)?;
|
||||
process_snippet(PageSnippet::Variable(variable))?;
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
highlight_code_segment(command, &replace_escaped(text), process_snippet)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find a "{{" (or "}}") substring that does not overlap with a preceding "\{\{" (or "\}\}").
|
||||
fn find_marker(s: &str, marker: &str, forbidden_prefix: &str) -> Option<usize> {
|
||||
let mut search_start = 0;
|
||||
loop {
|
||||
let marker_index = s.find_from(marker, search_start)?;
|
||||
|
||||
let overlaps_with_prefix = (forbidden_prefix.len() <= marker_index + 1) && {
|
||||
let prefix_start = marker_index + 1 - forbidden_prefix.len();
|
||||
// NOTE: The indices might not be valid character offsets, so we should do this
|
||||
// comparison on raw bytes. If prefix_start is indeed not a character offset than the
|
||||
// comparison is guaranteed to return false because forbidden_prefix[0] definitely _is_
|
||||
// the start of a (single byte, ASCII) character.
|
||||
&s.as_bytes()[prefix_start..=marker_index] == forbidden_prefix.as_bytes()
|
||||
};
|
||||
if !overlaps_with_prefix {
|
||||
return Some(marker_index);
|
||||
}
|
||||
|
||||
// The next valid marker cannot include the first character of the current match
|
||||
search_start = marker_index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Yields `NormalCode` and `CommandName` in alternating order according to the occurrences of
|
||||
/// `command_name` in `segment`. Variables are not detected here, see `highlight_code`
|
||||
/// instead.
|
||||
fn highlight_code_segment<'a, E>(
|
||||
command_name: &'a str,
|
||||
mut segment: &'a str,
|
||||
process_snippet: &mut impl FnMut(PageSnippet<&'a str>) -> Result<(), E>,
|
||||
process_snippet: &mut impl FnMut(PageSnippet<'a>) -> Result<(), E>,
|
||||
) -> Result<(), E> {
|
||||
if !command_name.is_empty() {
|
||||
let mut search_start = 0;
|
||||
|
|
@ -230,17 +127,20 @@ fn is_freestanding_substring(surrounding: &str, substring: (usize, usize)) -> bo
|
|||
let char_before_is_okay = surrounding[..start]
|
||||
.chars()
|
||||
.last()
|
||||
.is_none_or(char::is_whitespace);
|
||||
.filter(|prev_char| !prev_char.is_whitespace())
|
||||
.is_none();
|
||||
let char_after_is_okay = surrounding[end..]
|
||||
.chars()
|
||||
.next()
|
||||
.is_none_or(char::is_whitespace);
|
||||
.filter(|next_char| !next_char.is_whitespace())
|
||||
.is_none();
|
||||
char_before_is_okay && char_after_is_okay
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use PageSnippet::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_freestanding_substring() {
|
||||
|
|
@ -267,193 +167,80 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
fn run<'a>(cmd: &'a str, segment: &'a str) -> Vec<PageSnippet<String>> {
|
||||
fn run<'a>(cmd: &'a str, segment: &'a str) -> Vec<PageSnippet<'a>> {
|
||||
let mut yielded = Vec::new();
|
||||
let mut process_snippet = |snip: PageSnippet<&str>| {
|
||||
let mut process_snippet = |snip: PageSnippet<'a>| {
|
||||
if !snip.is_empty() {
|
||||
yielded.push(snip.map(str::to_string));
|
||||
yielded.push(snip);
|
||||
}
|
||||
Ok::<(), ()>(())
|
||||
};
|
||||
|
||||
highlight_code(cmd, segment, &mut process_snippet).expect("highlight code segment failed");
|
||||
highlight_code_segment(cmd, segment, &mut process_snippet)
|
||||
.expect("highlight code segment failed");
|
||||
yielded
|
||||
}
|
||||
|
||||
mod highlight_code_segment {
|
||||
use super::*;
|
||||
use PageSnippet::*;
|
||||
|
||||
#[test]
|
||||
fn test_highlight_code_segment() {
|
||||
assert!(run("make", "").is_empty());
|
||||
assert_eq!(
|
||||
&run("make", "make all CC=clang -q"),
|
||||
&[CommandName("make"), NormalCode(" all CC=clang -q")]
|
||||
);
|
||||
assert_eq!(
|
||||
&run("make", " make money --always-make"),
|
||||
&[
|
||||
NormalCode(" "),
|
||||
CommandName("make"),
|
||||
NormalCode(" money --always-make")
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
&run("git commit", "git commit -m 'git commit'"),
|
||||
&[CommandName("git commit"), NormalCode(" -m 'git commit'"),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_i18n() {
|
||||
assert_eq!(
|
||||
&run("mäke", "mäke höhlenrätselbücher"),
|
||||
&[CommandName("mäke"), NormalCode(" höhlenrätselbücher")]
|
||||
);
|
||||
assert_eq!(
|
||||
&run(
|
||||
"Müll",
|
||||
"1000 Gründe warum Müll heute größer ist als Müll früher, ärgerlich"
|
||||
),
|
||||
&[
|
||||
NormalCode("1000 Gründe warum "),
|
||||
CommandName("Müll"),
|
||||
NormalCode(" heute größer ist als "),
|
||||
CommandName("Müll"),
|
||||
NormalCode(" früher, ärgerlich")
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
&run(
|
||||
"übergang",
|
||||
"die Zustandsübergangsfunktion übergang Änderungen",
|
||||
),
|
||||
&[
|
||||
NormalCode("die Zustandsübergangsfunktion "),
|
||||
CommandName("übergang"),
|
||||
NormalCode(" Änderungen")
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_command() {
|
||||
let segment = "some code";
|
||||
let snippets = [NormalCode(segment)];
|
||||
|
||||
assert_eq!(run("", segment), snippets);
|
||||
assert_eq!(run(" ", segment), snippets);
|
||||
assert_eq!(run(" \t ", segment), snippets);
|
||||
}
|
||||
#[test]
|
||||
fn test_highlight_code_segment() {
|
||||
assert!(run("make", "").is_empty());
|
||||
assert_eq!(
|
||||
&run("make", "make all CC=clang -q"),
|
||||
&[CommandName("make"), NormalCode(" all CC=clang -q")]
|
||||
);
|
||||
assert_eq!(
|
||||
&run("make", " make money --always-make"),
|
||||
&[
|
||||
NormalCode(" "),
|
||||
CommandName("make"),
|
||||
NormalCode(" money --always-make")
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
&run("git commit", "git commit -m 'git commit'"),
|
||||
&[CommandName("git commit"), NormalCode(" -m 'git commit'"),]
|
||||
);
|
||||
}
|
||||
|
||||
mod placeholders {
|
||||
use super::*;
|
||||
use PageSnippet::*;
|
||||
#[test]
|
||||
fn test_i18n() {
|
||||
assert_eq!(
|
||||
&run("mäke", "mäke höhlenrätselbücher"),
|
||||
&[CommandName("mäke"), NormalCode(" höhlenrätselbücher")]
|
||||
);
|
||||
assert_eq!(
|
||||
&run(
|
||||
"Müll",
|
||||
"1000 Gründe warum Müll heute größer ist als Müll früher, ärgerlich"
|
||||
),
|
||||
&[
|
||||
NormalCode("1000 Gründe warum "),
|
||||
CommandName("Müll"),
|
||||
NormalCode(" heute größer ist als "),
|
||||
CommandName("Müll"),
|
||||
NormalCode(" früher, ärgerlich")
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
&run(
|
||||
"übergang",
|
||||
"die Zustandsübergangsfunktion übergang Änderungen",
|
||||
),
|
||||
&[
|
||||
NormalCode("die Zustandsübergangsfunktion "),
|
||||
CommandName("übergang"),
|
||||
NormalCode(" Änderungen")
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_vs_escaped() {
|
||||
assert_eq!(
|
||||
run("ping", "ping {{example.com}}"),
|
||||
[
|
||||
CommandName("ping"),
|
||||
NormalCode(" "),
|
||||
Variable("example.com"),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
run(
|
||||
"docker inspect",
|
||||
r"docker inspect --format '\{\{range.NetworkSettings.Networks\}\}\{\{.IPAddress\}\}\{\{end\}\}' {{container}}"
|
||||
),
|
||||
[
|
||||
CommandName("docker inspect"),
|
||||
NormalCode(
|
||||
" --format '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "
|
||||
),
|
||||
Variable("container"),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
run("mount", r"mount \\{{computer_name}}\{{share_name}} Z:"),
|
||||
[
|
||||
CommandName("mount"),
|
||||
NormalCode(r" \\"),
|
||||
Variable("computer_name"),
|
||||
NormalCode(r"\"),
|
||||
Variable("share_name"),
|
||||
NormalCode(" Z:"),
|
||||
],
|
||||
);
|
||||
#[test]
|
||||
fn test_empty_command() {
|
||||
let segment = "some code";
|
||||
let snippets = [NormalCode(segment)];
|
||||
|
||||
assert_eq!(run("", r"\{"), [NormalCode(r"\{")]);
|
||||
assert_eq!(run("", r"\{{a"), [NormalCode(r"\{{a")]);
|
||||
assert_eq!(run("", r"\{{a}}"), [NormalCode(r"\"), Variable("a")]);
|
||||
|
||||
// Placeholder has begin marker, but no end marker
|
||||
assert_eq!(run("", r"{{\}\}}"), [NormalCode("{{}}}")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outer_precedence() {
|
||||
assert_eq!(
|
||||
run("git stash", "git stash show --patch {{stash@{0}}}"),
|
||||
[
|
||||
CommandName("git stash"),
|
||||
NormalCode(" show --patch "),
|
||||
Variable("stash@{0}"),
|
||||
],
|
||||
);
|
||||
|
||||
// The following is not listed in the specification, but this is the highlighting I would expect.
|
||||
assert_eq!(
|
||||
run("rg", "rg {{}}}"),
|
||||
[CommandName("rg"), NormalCode(" "), Variable("}")]
|
||||
);
|
||||
|
||||
// And these are just to document the current behavior
|
||||
assert_eq!(run("", "{{{}}}"), [Variable("{}")]);
|
||||
assert_eq!(run("", "{{{{}}}"), [Variable("{{}")]);
|
||||
assert_eq!(run("", "{{{}}}}"), [Variable("{}}")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaped_inside_placeholder() {
|
||||
assert_eq!(
|
||||
run(
|
||||
"playerctl",
|
||||
r#"playerctl metadata {{[-f|--format]}} "{{Now playing: \{\{artist\}\} - \{\{album\}\} - \{\{title\}\}}}""#
|
||||
),
|
||||
[
|
||||
CommandName("playerctl"),
|
||||
NormalCode(" metadata "),
|
||||
Variable("[-f|--format]"),
|
||||
NormalCode(" \""),
|
||||
Variable("Now playing: {{artist}} - {{album}} - {{title}}"),
|
||||
NormalCode("\""),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_inside_escaped() {
|
||||
assert_eq!(
|
||||
run("test", r"test \{\{{{var}} normal\}\}"),
|
||||
[
|
||||
CommandName("test"),
|
||||
NormalCode(" {{"),
|
||||
Variable("var"),
|
||||
NormalCode(" normal}}"),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Regression test for <https://github.com/tealdeer-rs/tealdeer/issues/473>
|
||||
fn prefix_check_character_boundary() {
|
||||
assert_eq!("Ä".len(), 2);
|
||||
assert_eq!(run("", r"Äxx{{x}}"), [NormalCode("Äxx"), Variable("x")],);
|
||||
}
|
||||
assert_eq!(run("", segment), snippets);
|
||||
assert_eq!(run(" ", segment), snippets);
|
||||
assert_eq!(run(" \t ", segment), snippets);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
112
src/main.rs
112
src/main.rs
|
|
@ -16,7 +16,6 @@
|
|||
#![allow(clippy::struct_excessive_bools)]
|
||||
#![allow(clippy::too_many_lines)]
|
||||
#![allow(clippy::unnecessary_debug_formatting)]
|
||||
#![allow(clippy::while_let_loop)]
|
||||
|
||||
#[cfg(not(any(
|
||||
feature = "native-tls",
|
||||
|
|
@ -36,6 +35,7 @@ use std::{
|
|||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use app_dirs::AppInfo;
|
||||
use cache::{CacheConfig, TLDR_OLD_PAGES_DIR};
|
||||
use clap::Parser;
|
||||
use config::{ConfigLoader, Language, StyleConfig, TlsBackend};
|
||||
|
|
@ -55,17 +55,17 @@ mod utils;
|
|||
use crate::{
|
||||
cache::{Cache, PageLookupResult, TLDR_PAGES_DIR},
|
||||
cli::Cli,
|
||||
config::{
|
||||
get_config_dir, make_default_config, supported_tls_backends_string, Config, PathWithSource,
|
||||
},
|
||||
config::{get_config_dir, make_default_config, Config, PathWithSource},
|
||||
output::print_page,
|
||||
types::ColorOptions,
|
||||
utils::{print_error, print_warning},
|
||||
};
|
||||
|
||||
const NAME: &str = "tealdeer";
|
||||
static TEALDEER_PAGE: &str =
|
||||
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/pages/tealdeer.md"));
|
||||
const APP_INFO: AppInfo = AppInfo {
|
||||
name: NAME,
|
||||
author: NAME,
|
||||
};
|
||||
|
||||
/// Clear the cache
|
||||
fn clear_cache(cache: Cache, quietly: bool) -> Result<()> {
|
||||
|
|
@ -105,14 +105,16 @@ fn update_cache(
|
|||
|
||||
/// Show file paths
|
||||
fn show_paths(config: &Config) {
|
||||
let config_dir = {
|
||||
let (mut path, source) = get_config_dir();
|
||||
path.push(""); // Trailing path separator
|
||||
match path.to_str() {
|
||||
Some(path) => format!("{path} ({source})"),
|
||||
None => "[Invalid]".to_string(),
|
||||
}
|
||||
};
|
||||
let config_dir = get_config_dir().map_or_else(
|
||||
|e| format!("[Error: {e}]"),
|
||||
|(mut path, source)| {
|
||||
path.push(""); // Trailing path separator
|
||||
match path.to_str() {
|
||||
Some(path) => format!("{path} ({source})"),
|
||||
None => "[Invalid]".to_string(),
|
||||
}
|
||||
},
|
||||
);
|
||||
let config_path = config.file_path.to_string();
|
||||
let cache_dir = config.directories.cache_dir.to_string();
|
||||
let pages_dir = {
|
||||
|
|
@ -254,20 +256,8 @@ fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> {
|
|||
|
||||
// If a local file was passed in, render it and exit
|
||||
if let Some(file) = args.render {
|
||||
let reader = PageLookupResult::with_page(file).reader()?;
|
||||
print_page(reader, args.raw, enable_styles, args.pager, &config)?;
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
// The tealdeer page is embedded in the binary, no cache needed
|
||||
if command == "tealdeer" {
|
||||
print_page(
|
||||
TEALDEER_PAGE.as_bytes(),
|
||||
args.raw,
|
||||
enable_styles,
|
||||
args.pager,
|
||||
&config,
|
||||
)?;
|
||||
let path = PageLookupResult::with_page(file);
|
||||
print_page(&path, args.raw, enable_styles, args.pager, &config)?;
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
|
|
@ -315,36 +305,12 @@ fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> {
|
|||
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(
|
||||
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
|
||||
|
|
@ -369,18 +335,16 @@ fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> {
|
|||
return Ok(ExitCode::FAILURE);
|
||||
};
|
||||
|
||||
if let Some(max_cache_age) = config.updates.warn_cache_age {
|
||||
let age = cache.age()?;
|
||||
if age > max_cache_age && !args.quiet {
|
||||
print_warning(
|
||||
enable_styles,
|
||||
&format!(
|
||||
"The cache hasn't been updated for {} days.\n\
|
||||
You should probably run `tldr --update` soon.",
|
||||
age.as_secs() / 24 / 3600
|
||||
),
|
||||
);
|
||||
}
|
||||
let age = cache.age()?;
|
||||
if age > config::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
|
||||
|
|
@ -417,27 +381,23 @@ fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> {
|
|||
);
|
||||
}
|
||||
|
||||
let Some(result) = cache.find_page(&command) else {
|
||||
let Some(lookup_result) = cache.find_page(&command) else {
|
||||
if !args.quiet {
|
||||
print_warning(
|
||||
enable_styles,
|
||||
&format!(
|
||||
"Page `{command}` not found in cache.\n\
|
||||
"Page `{}` not found in cache.\n\
|
||||
Try updating with `tldr --update`, or submit a pull request to:\n\
|
||||
https://github.com/tldr-pages/tldr"
|
||||
https://github.com/tldr-pages/tldr",
|
||||
&command
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Ok(ExitCode::FAILURE);
|
||||
};
|
||||
|
||||
print_page(
|
||||
result.reader()?,
|
||||
args.raw,
|
||||
enable_styles,
|
||||
args.pager,
|
||||
&config,
|
||||
)?;
|
||||
print_page(&lookup_result, args.raw, enable_styles, args.pager, &config)?;
|
||||
}
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
//! Functions for printing pages to the terminal
|
||||
|
||||
use std::io::{self, BufRead, BufReader, Read, Write};
|
||||
use std::io::{self, BufRead, Write};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use yansi::Paint;
|
||||
|
||||
use crate::{
|
||||
cache::PageLookupResult,
|
||||
config::{Config, StyleConfig},
|
||||
formatter::{highlight_lines, PageSnippet},
|
||||
line_iterator::LineIterator,
|
||||
|
|
@ -29,13 +30,14 @@ fn configure_pager(enable_styles: bool) {
|
|||
|
||||
/// Print page by path
|
||||
pub fn print_page(
|
||||
reader: impl Read,
|
||||
lookup_result: &PageLookupResult,
|
||||
enable_markdown: bool,
|
||||
enable_styles: bool,
|
||||
use_pager: bool,
|
||||
config: &Config,
|
||||
) -> Result<()> {
|
||||
let reader = BufReader::new(reader);
|
||||
// Create reader from file(s)
|
||||
let reader = lookup_result.reader()?;
|
||||
|
||||
// Configure pager if applicable
|
||||
if use_pager || config.display.use_pager {
|
||||
|
|
@ -54,7 +56,7 @@ pub fn print_page(
|
|||
}
|
||||
} else {
|
||||
// Closure that processes a page snippet and writes it to stdout
|
||||
let mut process_snippet = |snip: PageSnippet<&str>| {
|
||||
let mut process_snippet = |snip: PageSnippet<'_>| {
|
||||
if snip.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
|
|
@ -68,7 +70,6 @@ pub fn print_page(
|
|||
&mut process_snippet,
|
||||
!config.display.compact,
|
||||
config.display.show_title,
|
||||
config.display.indent,
|
||||
)
|
||||
.context("Could not write to stdout")?;
|
||||
}
|
||||
|
|
@ -81,17 +82,18 @@ pub fn print_page(
|
|||
|
||||
fn print_snippet(
|
||||
writer: &mut impl Write,
|
||||
snip: PageSnippet<&str>,
|
||||
snip: PageSnippet<'_>,
|
||||
style: &StyleConfig,
|
||||
) -> io::Result<()> {
|
||||
use PageSnippet::*;
|
||||
|
||||
match snip {
|
||||
CommandName(s) | Title(s) => write!(writer, "{}", s.paint(style.command_name)),
|
||||
CommandName(s) => write!(writer, "{}", s.paint(style.command_name)),
|
||||
Variable(s) => write!(writer, "{}", s.paint(style.example_variable)),
|
||||
NormalCode(s) => write!(writer, "{}", s.paint(style.example_code)),
|
||||
Description(s) => write!(writer, "{}", s.paint(style.description)),
|
||||
Text(s) => write!(writer, "{}", s.paint(style.example_text)),
|
||||
Description(s) => writeln!(writer, " {}", s.paint(style.description)),
|
||||
Text(s) => writeln!(writer, " {}", s.paint(style.example_text)),
|
||||
Title(s) => writeln!(writer, " {}", s.paint(style.command_name)),
|
||||
Linebreak => writeln!(writer),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
51
tests/lib.rs
51
tests/lib.rs
|
|
@ -301,16 +301,6 @@ fn test_missing_cache() {
|
|||
.stderr(contains("Page cache not found. Please run `tldr --update`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tealdeer_page_works_without_cache() {
|
||||
TestEnv::new()
|
||||
.command()
|
||||
.args(["tealdeer"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("for your installed tealdeer version"));
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")]
|
||||
#[test]
|
||||
fn test_update_cache_default_features() {
|
||||
|
|
@ -493,26 +483,6 @@ fn test_quiet_old_cache() {
|
|||
.stderr(contains("The cache hasn't been updated for ").not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_warn_cache_age_never() {
|
||||
let testenv = TestEnv::new().install_default_cache();
|
||||
|
||||
filetime::set_file_mtime(
|
||||
testenv.cache_dir().join(TLDR_PAGES_DIR),
|
||||
filetime::FileTime::from_unix_time(1, 0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
testenv.append_to_config("[updates]\nwarn_cache_age = \"never\"\n");
|
||||
|
||||
testenv
|
||||
.command()
|
||||
.args(["which"])
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(contains("The cache hasn't been updated for ").not());
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")]
|
||||
#[test]
|
||||
fn test_create_cache_directory_path() {
|
||||
|
|
@ -575,7 +545,7 @@ fn test_cache_location_permission_denied() {
|
|||
// Make cache directory unreadable
|
||||
let cache_dir = testenv.cache_dir();
|
||||
let mut permissions = cache_dir.metadata().unwrap().permissions();
|
||||
permissions.set_mode(0o0);
|
||||
permissions.set_mode(0);
|
||||
fs::set_permissions(cache_dir, permissions).unwrap();
|
||||
|
||||
testenv
|
||||
|
|
@ -841,24 +811,6 @@ fn test_rendering_color_never() {
|
|||
);
|
||||
}
|
||||
|
||||
/// An end-to-end integration test for the indent config option
|
||||
#[test]
|
||||
fn test_rendering_with_indentation() {
|
||||
let testenv = TestEnv::new().install_default_cache();
|
||||
let expected_custom_indentation = include_str!("rendered/inkscape-compact-no-color.expected");
|
||||
|
||||
// Configure to set base and command indents
|
||||
testenv.append_to_config("display.indent.base = 3\n");
|
||||
testenv.append_to_config("display.indent.command = 1\n");
|
||||
|
||||
testenv
|
||||
.command()
|
||||
.args(["--color", "never", "inkscape-v2"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(diff(expected_custom_indentation));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rendering_i18n() {
|
||||
_test_correct_rendering(
|
||||
|
|
@ -1067,7 +1019,6 @@ fn test_search_language_precedence() {
|
|||
testenv.add_lang_entry(lang, lang, "");
|
||||
}
|
||||
|
||||
#[expect(clippy::type_complexity)]
|
||||
let run = |cases: &[(Vec<(&str, &str)>, Vec<&str>, &str)]| {
|
||||
for (extra_env, extra_args, expected) in cases {
|
||||
let mut cmd = testenv.command();
|
||||
|
|
|
|||
|
|
@ -3,35 +3,35 @@
|
|||
Ubuntuのバージョンが16.04か、それ以降で対話モードを使う場合`apt-get`の代わりとして使用します。
|
||||
詳しくはこちら: <https://manned.org/apt.8>
|
||||
|
||||
[32m [0m[32m利用可能なパーケージとバージョンのリストの更新(他の`apt`コマンドの前での実行を推奨):[0m
|
||||
[32m利用可能なパーケージとバージョンのリストの更新(他の`apt`コマンドの前での実行を推奨):[0m
|
||||
|
||||
[36m [0m[36msudo [0m[36mapt[0m[36m update[0m
|
||||
|
||||
[32m [0m[32m指定されたパッケージの検索:[0m
|
||||
[32m指定されたパッケージの検索:[0m
|
||||
|
||||
[36m [0m[36mapt[0m[36m search [0m[4;36mパッケージ[0m
|
||||
|
||||
[32m [0m[32mパッケージの情報を出力:[0m
|
||||
[32mパッケージの情報を出力:[0m
|
||||
|
||||
[36m [0m[36mapt[0m[36m show [0m[4;36mパッケージ[0m
|
||||
|
||||
[32m [0m[32mパッケージのインストール、または利用可能な最新バージョンに更新:[0m
|
||||
[32mパッケージのインストール、または利用可能な最新バージョンに更新:[0m
|
||||
|
||||
[36m [0m[36msudo [0m[36mapt[0m[36m install [0m[4;36mパッケージ[0m
|
||||
|
||||
[32m [0m[32mパッケージの削除(`sudo apt remove --purge`の場合設定ファイルも削除):[0m
|
||||
[32mパッケージの削除(`sudo apt remove --purge`の場合設定ファイルも削除):[0m
|
||||
|
||||
[36m [0m[36msudo [0m[36mapt[0m[36m remove [0m[4;36mパッケージ[0m
|
||||
|
||||
[32m [0m[32mインストールされている全てのパッケージを最新のバージョンにアップグレード:[0m
|
||||
[32mインストールされている全てのパッケージを最新のバージョンにアップグレード:[0m
|
||||
|
||||
[36m [0m[36msudo [0m[36mapt[0m[36m upgrade[0m
|
||||
|
||||
[32m [0m[32mインストールできるすべてのパッケージを表示:[0m
|
||||
[32mインストールできるすべてのパッケージを表示:[0m
|
||||
|
||||
[36m [0m[36mapt[0m[36m list[0m
|
||||
|
||||
[32m [0m[32mインストールされた全てのパッケージを表示(依存関係も表示):[0m
|
||||
[32mインストールされた全てのパッケージを表示(依存関係も表示):[0m
|
||||
|
||||
[36m [0m[36mapt[0m[36m list --installed[0m
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
|
||||
An SVG (Scalable Vector Graphics) editing program.
|
||||
Use -z to not open the GUI and only process files in the console.
|
||||
|
||||
Open an SVG file in the Inkscape GUI:
|
||||
|
||||
inkscape filename.svg
|
||||
|
||||
Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):
|
||||
|
||||
inkscape filename.svg -e filename.png
|
||||
|
||||
Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):
|
||||
|
||||
inkscape filename.svg -e filename.png -w 600 -h 400
|
||||
|
||||
Export a single object, given its ID, into a bitmap:
|
||||
|
||||
inkscape filename.svg -i id -e object.png
|
||||
|
||||
Export an SVG document to PDF, converting all texts to paths:
|
||||
|
||||
inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path
|
||||
|
||||
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
|
||||
|
||||
inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
||||
|
||||
Some invalid command just to test the correct highlighting of the command name:
|
||||
|
||||
inkscape --use-inkscape=v3.0 file
|
||||
|
||||
|
|
@ -2,31 +2,31 @@
|
|||
An SVG (Scalable Vector Graphics) editing program.
|
||||
Use -z to not open the GUI and only process files in the console.
|
||||
|
||||
[32m [0m[32mOpen an SVG file in the Inkscape GUI:[0m
|
||||
[32mOpen an SVG file in the Inkscape GUI:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m
|
||||
|
||||
[32m [0m[32mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||
[32mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -e [0m[4;36mfilename.png[0m
|
||||
|
||||
[32m [0m[32mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
||||
[32mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -e [0m[4;36mfilename.png[0m[36m -w [0m[4;36m600[0m[36m -h [0m[4;36m400[0m
|
||||
|
||||
[32m [0m[32mExport a single object, given its ID, into a bitmap:[0m
|
||||
[32mExport a single object, given its ID, into a bitmap:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -i [0m[4;36mid[0m[36m -e [0m[4;36mobject.png[0m
|
||||
|
||||
[32m [0m[32mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||
[32mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m | [0m[36minkscape[0m[36m | [0m[36minkscape[0m[36m --export-pdf=[0m[4;36minkscape.pdf[0m[36m | [0m[36minkscape[0m[36m | [0m[36minkscape[0m[36m --export-text-to-path[0m
|
||||
|
||||
[32m [0m[32mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||
[32mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit[0m
|
||||
|
||||
[32m [0m[32mSome invalid command just to test the correct highlighting of the command name:[0m
|
||||
[32mSome invalid command just to test the correct highlighting of the command name:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m --use-inkscape=v3.0 file[0m
|
||||
|
||||
|
|
|
|||
|
|
@ -2,31 +2,31 @@
|
|||
An SVG (Scalable Vector Graphics) editing program.
|
||||
Use -z to not open the GUI and only process files in the console.
|
||||
|
||||
[44;30m [0m[44;30mOpen an SVG file in the Inkscape GUI:[0m
|
||||
[44;30mOpen an SVG file in the Inkscape GUI:[0m
|
||||
|
||||
[1minkscape[0m [3;4mfilename.svg[0m
|
||||
|
||||
[44;30m [0m[44;30mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||
[44;30mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||
|
||||
[1minkscape[0m [3;4mfilename.svg[0m -e [3;4mfilename.png[0m
|
||||
|
||||
[44;30m [0m[44;30mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
||||
[44;30mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
||||
|
||||
[1minkscape[0m [3;4mfilename.svg[0m -e [3;4mfilename.png[0m -w [3;4m600[0m -h [3;4m400[0m
|
||||
|
||||
[44;30m [0m[44;30mExport a single object, given its ID, into a bitmap:[0m
|
||||
[44;30mExport a single object, given its ID, into a bitmap:[0m
|
||||
|
||||
[1minkscape[0m [3;4mfilename.svg[0m -i [3;4mid[0m -e [3;4mobject.png[0m
|
||||
|
||||
[44;30m [0m[44;30mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||
[44;30mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||
|
||||
[1minkscape[0m [3;4mfilename.svg[0m | [1minkscape[0m | [1minkscape[0m --export-pdf=[3;4minkscape.pdf[0m | [1minkscape[0m | [1minkscape[0m --export-text-to-path
|
||||
|
||||
[44;30m [0m[44;30mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||
[44;30mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||
|
||||
[1minkscape[0m [3;4mfilename.svg[0m --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
||||
|
||||
[44;30m [0m[44;30mSome invalid command just to test the correct highlighting of the command name:[0m
|
||||
[44;30mSome invalid command just to test the correct highlighting of the command name:[0m
|
||||
|
||||
[1minkscape[0m --use-inkscape=v3.0 file
|
||||
|
||||
|
|
|
|||
|
|
@ -1,34 +1,34 @@
|
|||
|
||||
[36m [0m[36minkscape[0m
|
||||
[36minkscape[0m
|
||||
|
||||
An SVG (Scalable Vector Graphics) editing program.
|
||||
Use -z to not open the GUI and only process files in the console.
|
||||
|
||||
[32m [0m[32mOpen an SVG file in the Inkscape GUI:[0m
|
||||
[32mOpen an SVG file in the Inkscape GUI:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m
|
||||
|
||||
[32m [0m[32mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||
[32mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -e [0m[4;36mfilename.png[0m
|
||||
|
||||
[32m [0m[32mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
||||
[32mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -e [0m[4;36mfilename.png[0m[36m -w [0m[4;36m600[0m[36m -h [0m[4;36m400[0m
|
||||
|
||||
[32m [0m[32mExport a single object, given its ID, into a bitmap:[0m
|
||||
[32mExport a single object, given its ID, into a bitmap:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m -i [0m[4;36mid[0m[36m -e [0m[4;36mobject.png[0m
|
||||
|
||||
[32m [0m[32mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||
[32mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m | [0m[36minkscape[0m[36m | [0m[36minkscape[0m[36m --export-pdf=[0m[4;36minkscape.pdf[0m[36m | [0m[36minkscape[0m[36m | [0m[36minkscape[0m[36m --export-text-to-path[0m
|
||||
|
||||
[32m [0m[32mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||
[32mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m [0m[4;36mfilename.svg[0m[36m --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit[0m
|
||||
|
||||
[32m [0m[32mSome invalid command just to test the correct highlighting of the command name:[0m
|
||||
[32mSome invalid command just to test the correct highlighting of the command name:[0m
|
||||
|
||||
[36m [0m[36minkscape[0m[36m --use-inkscape=v3.0 file[0m
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue