diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index faf3111..2fc4485 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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@v5
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:
diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml
index ae9ae93..8746d42 100644
--- a/.github/workflows/gh-pages.yml
+++ b/.github/workflows/gh-pages.yml
@@ -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
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9337d6f..539f2ab 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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@v5
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@v5
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@v5
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@v6
- name: Upload binary
if: startsWith(github.ref, 'refs/tags/')
run: |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 644a30a..5a80bbb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,46 +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
@@ -606,14 +566,11 @@ Thanks!
[v1.4.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.3.0...v1.4.0
[v1.4.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.4.0...v1.4.1
[v1.5.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.4.1...v1.5.0
-[v1.5.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.5.0...v1.5.1
[v1.6.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.5.0...v1.6.0
[v1.6.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.6.0...v1.6.1
-[v1.6.2]: https://github.com/tealdeer-rs/tealdeer/compare/v1.6.1...v1.6.2
[v1.7.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.6.1...v1.7.0
[v1.7.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.0...v1.7.1
[v1.7.2]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.1...v1.7.2
-[v1.7.3]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.2...v1.7.3
[v1.8.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.2...v1.8.0
[v1.8.1]: https://github.com/tealdeer-rs/tealdeer/compare/v1.8.0...v1.8.1
diff --git a/Cargo.lock b/Cargo.lock
index 4fba42b..be9bd24 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
@@ -368,16 +380,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 +635,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"
@@ -1109,11 +1117,11 @@ name = "tealdeer"
version = "1.8.1"
dependencies = [
"anyhow",
+ "app_dirs2",
"assert_cmd",
"clap",
"env_logger",
"escargot",
- "etcetera",
"filetime",
"log",
"pager",
@@ -1626,6 +1634,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"
diff --git a/Cargo.toml b/Cargo.toml
index 1d98992..aa7ae49 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,7 +12,7 @@ repository = "https://github.com/tealdeer-rs/tealdeer/"
documentation = "https://tealdeer-rs.github.io/tealdeer/"
version = "1.8.1"
include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"]
-rust-version = "1.87" # MSRV
+rust-version = "1.85"
edition = "2021"
[[bin]]
@@ -21,9 +21,9 @@ 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"
diff --git a/README.md b/README.md
index 859d06f..230dfa8 100644
--- a/README.md
+++ b/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
diff --git a/docs/src/config_display.md b/docs/src/config_display.md
index 007d64b..78656a4 100644
--- a/docs/src/config_display.md
+++ b/docs/src/config_display.md
@@ -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,
-}
-```
diff --git a/docs/src/config_updates.md b/docs/src/config_updates.md
index a8a10c8..9acba55 100644
--- a/docs/src/config_updates.md
+++ b/docs/src/config_updates.md
@@ -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`
diff --git a/docs/src/usage.txt b/docs/src/usage.txt
index 6a04de7..f7fcc46 100644
--- a/docs/src/usage.txt
+++ b/docs/src/usage.txt
@@ -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.
diff --git a/pages/tealdeer.md b/pages/tealdeer.md
deleted file mode 100644
index 948b278..0000000
--- a/pages/tealdeer.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# tldr
-
-> This is a builtin page that shows information for your installed tealdeer version.
-> More information: .
-
-> This page shows tealdeer specific functionality. See tldr tldr for more examples.
-
-- Render a local markdown file as a tldr page:
-
-`tldr --render {{path/to/file.md}}`
-
-- Show the raw markdown source of a page instead of rendering it:
-
-`tldr --raw {{command}}`
-
-- Show file and directory paths used by tealdeer:
-
-`tldr --show-paths`
-
-- Create an initial config file:
-
-`tldr --seed-config`
-
-- Override config file location:
-
-`tldr --config-path `
-
-- Open a custom page for a command in `$EDITOR` (creates it if it doesn't exist):
-
-`tldr --edit-page {{command}}`
-
-- Open a custom patch for a command in `$EDITOR` (appended to the existing page):
-
-`tldr --edit-patch {{command}}`
-
-- Clear the local cache:
-
-`tldr --clear-cache`
-
-- If auto update is configured, disable it for this run:
-
-`tldr --no-auto-update`
diff --git a/src/cache.rs b/src/cache.rs
index afa3603..b181310 100644
--- a/src/cache.rs
+++ b/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> {
+ pub fn reader(&self) -> Result>> {
// Open page file
let page_file = File::open(&self.page_path)
.with_context(|| format!("Could not open page file at {}", self.page_path.display()))?;
@@ -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
} else {
Box::new(page_file) as Box
- })
+ }))
}
}
@@ -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:?}",)
}
}
}
diff --git a/src/cli.rs b/src/cli.rs
index 161d69d..d461a3e 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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"]),
diff --git a/src/config.rs b/src/config.rs
index f0feeb4..e5e85e8 100644
--- a/src/config.rs
+++ b/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 {
- use etcetera::{
- app_strategy::choose_native_strategy, choose_app_strategy, AppStrategy, AppStrategyArgs,
- };
-
- let args = AppStrategyArgs {
- top_level_domain: String::new(),
- author: String::new(),
- app_name: crate::NAME.to_string(),
- };
-
- // The app strategy prefers XDG on MacOs, whereas the native strategy returns paths which
- // are used by installed applications. On Linux and Windows, the strategies are the same.
- let app_dirs = choose_app_strategy(args.clone())?;
- let native_dirs = choose_native_strategy(args)?;
-
- // We prefer the XDG paths, but before tealdeer 1.9, we used only the native paths on MacOs.
- // So if we find files in these locations, we keep using them.
- let fallback = |app_dir: PathBuf, native_dir: PathBuf| {
- if !app_dir.exists() && native_dir.exists() {
- native_dir
- } else {
- app_dir
- }
- };
-
- Ok(Self {
- config: fallback(app_dirs.config_dir(), native_dirs.config_dir()),
- cache: fallback(app_dirs.cache_dir(), native_dirs.cache_dir()),
- data: fallback(app_dirs.data_dir(), native_dirs.data_dir()),
- })
- }
-}
-static SYSTEM_DIRECTORIES: LazyLock = LazyLock::new(|| {
- SystemDirectories::discover().expect("Failed to initialize system directories.")
-});
-
-pub(crate) fn supported_tls_backends_string() -> String {
- SUPPORTED_TLS_BACKENDS
- .iter()
- .map(std::string::ToString::to_string)
- .collect::>()
- .join(", ")
-}
-
fn default_underline() -> bool {
false
}
-const fn default_base_indent() -> usize {
- 2
-}
-
-const fn default_command_indent() -> usize {
- 6
-}
-
fn default_bold() -> bool {
false
}
@@ -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>,
- #[serde(default)]
- pub warn_cache_age: Option,
}
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>,
- pub warn_cache_age: Option,
}
#[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 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::>().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 {
- // 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> {
- 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 {
- 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 {
+ 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 {
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();
diff --git a/src/formatter.rs b/src/formatter.rs
index 082f436..5270124 100644
--- a/src/formatter.rs
+++ b/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 {
- 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 PageSnippet {
- pub fn map(self, f: F) -> PageSnippet
- where
- F: FnOnce(T) -> U,
- {
- match self {
- PageSnippet::CommandName(s) => PageSnippet::CommandName(f(s)),
- PageSnippet::Variable(s) => PageSnippet::Variable(f(s)),
- PageSnippet::NormalCode(s) => PageSnippet::NormalCode(f(s)),
- PageSnippet::Description(s) => PageSnippet::Description(f(s)),
- PageSnippet::Text(s) => PageSnippet::Text(f(s)),
- PageSnippet::Title(s) => PageSnippet::Title(f(s)),
- PageSnippet::Linebreak => PageSnippet::Linebreak,
- }
- }
-}
-
-impl, U> PartialEq> for PageSnippet {
- fn eq(&self, other: &PageSnippet) -> bool {
- match (self, other) {
- (PageSnippet::CommandName(s), PageSnippet::CommandName(t))
- | (PageSnippet::Variable(s), PageSnippet::Variable(t))
- | (PageSnippet::NormalCode(s), PageSnippet::NormalCode(t))
- | (PageSnippet::Description(s), PageSnippet::Description(t))
- | (PageSnippet::Text(s), PageSnippet::Text(t))
- | (PageSnippet::Title(s), PageSnippet::Title(t)) => s == t,
- (PageSnippet::Linebreak, PageSnippet::Linebreak) => true,
- _ => false,
- }
- }
-}
-
-impl PageSnippet<&str> {
+impl PageSnippet<'_> {
pub fn is_empty(&self) -> bool {
use PageSnippet::*;
@@ -68,14 +35,11 @@ pub fn highlight_lines(
process_snippet: &mut F,
keep_empty_lines: bool,
show_title: bool,
- indent: Indent,
) -> Result<(), E>
where
L: Iterator- ,
- 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(
- 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 {
- 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> {
+ fn run<'a>(cmd: &'a str, segment: &'a str) -> Vec> {
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
- 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);
}
}
diff --git a/src/main.rs b/src/main.rs
index 6678ceb..a0cd593 100644
--- a/src/main.rs
+++ b/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 {
// 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 {
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 {
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 {
);
}
- 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)
diff --git a/src/output.rs b/src/output.rs
index 6243b44..5f1aeae 100644
--- a/src/output.rs
+++ b/src/output.rs
@@ -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),
}
}
diff --git a/tests/lib.rs b/tests/lib.rs
index d431b5f..cb987db 100644
--- a/tests/lib.rs
+++ b/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();
diff --git a/tests/rendered/apt.ja.expected b/tests/rendered/apt.ja.expected
index efdd35d..22424e3 100644
--- a/tests/rendered/apt.ja.expected
+++ b/tests/rendered/apt.ja.expected
@@ -3,35 +3,35 @@
Ubuntuのバージョンが16.04か、それ以降で対話モードを使う場合`apt-get`の代わりとして使用します。
詳しくはこちら:
-[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
diff --git a/tests/rendered/inkscape-compact-no-color.expected b/tests/rendered/inkscape-compact-no-color.expected
deleted file mode 100644
index 473bbe9..0000000
--- a/tests/rendered/inkscape-compact-no-color.expected
+++ /dev/null
@@ -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
-
diff --git a/tests/rendered/inkscape-default.expected b/tests/rendered/inkscape-default.expected
index 3b37f0e..da909f2 100644
--- a/tests/rendered/inkscape-default.expected
+++ b/tests/rendered/inkscape-default.expected
@@ -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
diff --git a/tests/rendered/inkscape-with-config.expected b/tests/rendered/inkscape-with-config.expected
index 33540a3..e79b219 100644
--- a/tests/rendered/inkscape-with-config.expected
+++ b/tests/rendered/inkscape-with-config.expected
@@ -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
diff --git a/tests/rendered/inkscape-with-title.expected b/tests/rendered/inkscape-with-title.expected
index ff2de4a..7e7d428 100644
--- a/tests/rendered/inkscape-with-title.expected
+++ b/tests/rendered/inkscape-with-title.expected
@@ -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