From 9bb95ad11dcc8ce8bf1e79bd1b1c95a84d148c64 Mon Sep 17 00:00:00 2001 From: MHS-0 Date: Sat, 31 May 2025 23:13:39 +0330 Subject: [PATCH 01/45] Add an option to specify a custom config file to be used (#422) Co-authored-by: Niklas Mohrin --- docs/src/usage.txt | 1 + src/cache.rs | 4 +- src/cli.rs | 4 ++ src/config.rs | 146 +++++++++++++++++++++++++++------------------ src/main.rs | 30 ++++++---- src/output.rs | 2 +- src/types.rs | 3 + tests/lib.rs | 118 +++++++++++++++++++++++++++++++++++- 8 files changed, 236 insertions(+), 72 deletions(-) diff --git a/docs/src/usage.txt b/docs/src/usage.txt index a4a5fcb..33e6020 100644 --- a/docs/src/usage.txt +++ b/docs/src/usage.txt @@ -18,6 +18,7 @@ Options: -u, --update Update the local cache --no-auto-update If auto update is configured, disable it for this run -c, --clear-cache Clear the local cache + --config-path Override config file location --pager Use a pager to page output -r, --raw Display the raw markdown instead of rendering it -q, --quiet Suppress informational messages diff --git a/src/cache.rs b/src/cache.rs index 76f0abe..c1308f2 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -174,8 +174,8 @@ impl Cache { if let Ok(mtime) = metadata.modified() { let now = SystemTime::now(); return now.duration_since(mtime).ok(); - }; - }; + } + } None } diff --git a/src/cli.rs b/src/cli.rs index 0ba9de9..55fe74f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -74,6 +74,10 @@ pub(crate) struct Cli { #[arg(short = 'c', long = "clear-cache")] pub clear_cache: bool, + /// Override config file location + #[arg(long = "config-path", value_name = "FILE")] + pub config_path: Option, + /// Use a pager to page output #[arg(long = "pager", requires = "command_or_file")] pub pager: bool, diff --git a/src/config.rs b/src/config.rs index 3d82eea..be41dd2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,13 +1,13 @@ use std::{ - env, fmt, fs, - io::{Read, Write}, + env, fmt, + fs::{self, File}, + io::{ErrorKind, Read, Write}, path::{Path, PathBuf}, time::Duration, }; use anyhow::{anyhow, bail, ensure, Context, Result}; use app_dirs::{get_app_root, AppDataType}; -use log::debug; use serde::Serialize as _; use serde_derive::{Deserialize, Serialize}; use yansi::{Color, Style}; @@ -254,6 +254,14 @@ impl RawConfig { fn new() -> Self { Self::default() } + + fn load(mut config: impl Read) -> Result { + let mut content = String::new(); + config + .read_to_string(&mut content) + .context("Failed to read from config file")?; + toml::from_str(&content).context("Failed to parse TOML config file") + } } impl Default for RawConfig { @@ -276,7 +284,7 @@ impl Default for RawConfig { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] pub struct StyleConfig { pub description: Style, pub command_name: Style, @@ -366,6 +374,7 @@ pub struct Config { pub display: DisplayConfig, pub updates: UpdatesConfig, pub directories: DirectoriesConfig, + pub file_path: PathWithSource, } impl Config { @@ -373,10 +382,14 @@ impl Config { /// /// For this, some values need to be converted to other types and some /// defaults need to be set (sometimes based on env variables). - fn from_raw(raw_config: RawConfig, relative_path_root: &Path) -> Result { + fn from_raw(raw_config: RawConfig, config_file_path: PathWithSource) -> Result { let style = raw_config.style.into(); let display = raw_config.display.into(); let updates = raw_config.updates.try_into()?; + let relative_path_root = config_file_path + .path() + .parent() + .context("Failed to get config directory")?; // Determine directories config. For this, we need to take some // additional factory into account, like env variables, or the @@ -438,48 +451,48 @@ impl Config { display, updates, directories, + file_path: config_file_path, }) } - pub fn load(enable_styles: bool) -> Result { - debug!("Loading config"); + /// Load and read the config file from the given path into + /// a [Config] and return it. + /// + /// path: The path to the config file. + pub fn load(path: &Path) -> Result { + let raw_config = RawConfig::load(File::open(path)?)?; + let config = Self::from_raw( + raw_config, + PathWithSource { + path: path.into(), + source: PathSource::Cli, + }, + ) + .context("Could not process raw config")?; + + Ok(config) + } + + /// Load and read the config file from the default path into + /// a [Config] and return it. + pub fn load_default_path() -> Result { // Determine path - let (config_file_path, _) = get_config_path().context("Could not determine config path")?; + let config_file_path = + get_default_config_path().context("Could not determine config path")?; - // Load raw config - let raw_config: RawConfig = if config_file_path.exists() && config_file_path.is_file() { - let mut config_file = fs::File::open(&config_file_path).with_context(|| { - format!("Failed to open config file path at {:?}", &config_file_path) - })?; - let mut contents = String::new(); - config_file.read_to_string(&mut contents).with_context(|| { - format!("Failed to read from config file at {:?}", &config_file_path) - })?; - toml::from_str(&contents).with_context(|| { - format!("Failed to parse TOML config file at {config_file_path:?}") - })? - } else { - RawConfig::new() + let raw_config = match File::open(config_file_path.path()) { + Ok(file) => RawConfig::load(file)?, + Err(e) if e.kind() == ErrorKind::NotFound => RawConfig::default(), + Err(e) => { + return Err(e).context(format!( + "Failed to open config file at {}", + config_file_path.path().display() + )); + } }; - - // Safe to unwrap, it's a file path, so it should have a directory component - let config_file_dir = config_file_path.parent().unwrap(); - - // Convert to config, resolve relative paths from the config file dir - let mut config = - Self::from_raw(raw_config, config_file_dir).context("Could not process raw config")?; - - // Potentially override styles - if !enable_styles { - config.style = StyleConfig { - command_name: Style::default(), - description: Style::default(), - example_text: Style::default(), - example_code: Style::default(), - example_variable: Style::default(), - }; - } + let config = + Self::from_raw(raw_config, config_file_path).context("Could not process raw config")?; Ok(config) } @@ -497,7 +510,7 @@ pub fn get_config_dir() -> Result<(PathBuf, PathSource)> { // $TEALDEER_CONFIG_DIR env variable. if let Ok(value) = env::var("TEALDEER_CONFIG_DIR") { return Ok((PathBuf::from(value), PathSource::EnvVar)); - }; + } // Otherwise, fall back to the user config directory. let dirs = get_app_root(AppDataType::UserConfig, &crate::APP_INFO) @@ -509,29 +522,39 @@ pub fn get_config_dir() -> Result<(PathBuf, PathSource)> { /// /// Note that this function does not verify whether the file at that location /// exists, or is a file. -pub fn get_config_path() -> Result<(PathBuf, PathSource)> { +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((config_file_path, source)) + Ok(PathWithSource { + path: config_file_path, + source, + }) } /// Create default config file. -pub fn make_default_config() -> Result { - let (config_dir, _) = get_config_dir()?; - - // Ensure that config directory exists - if config_dir.exists() { - ensure!( - config_dir.is_dir(), - "Config directory could not be created: {} already exists but is not a directory", - config_dir.to_string_lossy(), - ); +/// path: Can be specified to create the config in that path instead of +/// the default path. +pub fn make_default_config(path: Option<&Path>) -> Result { + let config_file_path = if let Some(p) = path { + p.into() } else { - fs::create_dir_all(&config_dir).context("Could not create config directory")?; - } + let (config_dir, _) = get_config_dir()?; + + // Ensure that config directory exists + if config_dir.exists() { + ensure!( + config_dir.is_dir(), + "Config directory could not be created: {} already exists but is not a directory", + config_dir.to_string_lossy(), + ); + } else { + fs::create_dir_all(&config_dir).context("Could not create config directory")?; + } + + config_dir.join(CONFIG_FILE_NAME) + }; // Ensure that a config file doesn't get overwritten - let config_file_path = config_dir.join(CONFIG_FILE_NAME); ensure!( !config_file_path.is_file(), "A configuration file already exists at {}, no action was taken.", @@ -544,7 +567,7 @@ pub fn make_default_config() -> Result { // Write default config let mut config_file = - fs::File::create(&config_file_path).context("Could not create config file")?; + File::create(&config_file_path).context("Could not create config file")?; let _wc = config_file .write(serialized_config.as_bytes()) .context("Could not write to config file")?; @@ -566,7 +589,14 @@ fn test_relative_path_resolution() { raw_config.directories.cache_dir = Some("../cache".into()); raw_config.directories.custom_pages_dir = Some("../custom_pages".into()); - let config = Config::from_raw(raw_config, Path::new("/path/to/config")).unwrap(); + let config = Config::from_raw( + raw_config, + PathWithSource { + path: PathBuf::from("/path/to/config/config.toml"), + source: PathSource::OsConvention, + }, + ) + .unwrap(); assert_eq!( config.directories.cache_dir.path(), diff --git a/src/main.rs b/src/main.rs index cfd00db..99403b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,8 @@ use std::{ use anyhow::{anyhow, Context, Result}; use app_dirs::AppInfo; use clap::Parser; +use config::StyleConfig; +use log::debug; mod cache; mod cli; @@ -50,7 +52,7 @@ mod utils; use crate::{ cache::{Cache, CacheFreshness, PageLookupResult, TLDR_PAGES_DIR}, cli::Cli, - config::{get_config_dir, get_config_path, make_default_config, Config, PathWithSource}, + config::{get_config_dir, make_default_config, Config, PathWithSource}, extensions::Dedup, output::print_page, types::{ColorOptions, PlatformType}, @@ -153,10 +155,7 @@ fn show_paths(config: &Config) { } }, ); - let config_path = get_config_path().map_or_else( - |e| format!("[Error: {e}]"), - |(path, _)| path.display().to_string(), - ); + let config_path = config.file_path.to_string(); let cache_dir = config.directories.cache_dir.to_string(); let pages_dir = { let mut path = config.directories.cache_dir.path.clone(); @@ -175,8 +174,8 @@ fn show_paths(config: &Config) { println!("Custom pages dir: {custom_pages_dir}"); } -fn create_config() -> Result<()> { - let config_file_path = make_default_config().context("Could not create seed config")?; +fn create_config(path: Option<&Path>) -> Result<()> { + let config_file_path = make_default_config(path).context("Could not create seed config")?; eprintln!( "Successfully created seed config file here: {}", config_file_path.to_str().unwrap() @@ -279,7 +278,18 @@ fn main() -> ExitCode { fn try_main(args: Cli, enable_styles: bool) -> Result { // Look up config file, if none is found fall back to default config. - let config = Config::load(enable_styles).context("Could not load config")?; + debug!("Loading config"); + let mut config = match &args.config_path { + Some(path) if !args.seed_config => { + Config::load(path).context("Could not load config from given path")? + } + _ => Config::load_default_path().context("Could not load config from default path")?, + }; + + // Override styles if needed + if !enable_styles { + config.style = StyleConfig::default(); + } let custom_pages_dir = config .directories @@ -313,7 +323,7 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { // Create a basic config and exit if args.seed_config { - create_config()?; + create_config(args.config_path.as_deref())?; return Ok(ExitCode::SUCCESS); } @@ -345,7 +355,7 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { { // Cache is needed, but missing return Ok(ExitCode::FAILURE); - }; + } // List cached commands and exit if args.list { diff --git a/src/output.rs b/src/output.rs index abef1db..046abe8 100644 --- a/src/output.rs +++ b/src/output.rs @@ -71,7 +71,7 @@ pub fn print_page( !config.display.compact, ) .context("Could not write to stdout")?; - }; + } // We're done outputting data, flush stdout now! handle.flush().context("Could not flush stdout")?; diff --git a/src/types.rs b/src/types.rs index 416d267..69c7599 100644 --- a/src/types.rs +++ b/src/types.rs @@ -205,6 +205,8 @@ pub enum PathSource { EnvVar, /// Config file ConfigFile, + /// CLI argument override + Cli, } impl fmt::Display for PathSource { @@ -216,6 +218,7 @@ impl fmt::Display for PathSource { Self::OsConvention => "OS convention", Self::EnvVar => "env variable", Self::ConfigFile => "config file", + Self::Cli => "command line argument", } ) } diff --git a/tests/lib.rs b/tests/lib.rs index 420eb08..172c1c5 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -70,6 +70,24 @@ impl TestEnv { .expect("Failed to append to config file."); } + fn create_secondary_config(self) -> Self { + self.append_to_secondary_config(format!( + "directories.cache_dir = '{}'\n", + self.cache_dir().to_str().unwrap(), + )); + self + } + + fn append_to_secondary_config(&self, content: impl AsRef) { + File::options() + .create(true) + .append(true) + .open(self.config_dir().join("config-secondary.toml")) + .expect("Failed to open config file") + .write_all(content.as_ref().as_bytes()) + .expect("Failed to append to config file."); + } + fn remove_initial_config(self) -> Self { let _ = fs::remove_file(self.config_dir().join("config.toml")); self @@ -183,6 +201,61 @@ fn test_cannot_build_without_tls_feature() { let _ = TestEnv::new().no_default_features().command(); } +#[test] +fn test_load_the_correct_config() { + let testenv = TestEnv::new() + .install_default_cache() + .create_secondary_config(); + testenv.append_to_secondary_config(include_str!("style-config.toml")); + + let expected_default = include_str!("rendered/inkscape-default.expected"); + let expected_with_config = include_str!("rendered/inkscape-with-config.expected"); + + testenv + .command() + .args(["--color", "always", "inkscape-v2"]) + .assert() + .success() + .stdout(diff(expected_default)); + + testenv + .command() + .args([ + "--color", + "always", + "--config-path", + testenv + .config_dir() + .join("config-secondary.toml") + .to_str() + .unwrap(), + "inkscape-v2", + ]) + .assert() + .success() + .stdout(diff(expected_with_config)); +} + +#[test] +fn test_fail_on_custom_config_path_is_directory() { + let testenv = TestEnv::new(); + let error = if cfg!(windows) { + "Access is denied" + } else { + "Is a directory" + }; + testenv + .command() + .args([ + "--config-path", + testenv.config_dir().to_str().unwrap(), + "sl", + ]) + .assert() + .failure() + .stderr(contains(error)); +} + #[test] fn test_missing_cache() { TestEnv::new() @@ -438,8 +511,9 @@ fn test_setup_seed_config() { .failure() .stderr(contains("A configuration file already exists")); - let testenv = testenv.remove_initial_config(); + assert!(testenv.config_dir().join("config.toml").is_file()); + let testenv = testenv.remove_initial_config(); testenv .command() .args(["--seed-config"]) @@ -448,6 +522,48 @@ fn test_setup_seed_config() { .stderr(contains("Successfully created seed config file here")); assert!(testenv.config_dir().join("config.toml").is_file()); + + // Create parent directories as needed for the default config path. + fs::remove_dir_all(testenv.config_dir()).unwrap(); + testenv + .command() + .args(["--seed-config"]) + .assert() + .success() + .stderr(contains("Successfully created seed config file here")); + + assert!(testenv.config_dir().join("config.toml").is_file()); + + // Write the default config to --config-path if specified by the user + // at the same time. + let custom_config_path = testenv.config_dir().join("config_custom.toml"); + testenv + .command() + .args([ + "--seed-config", + "--config-path", + custom_config_path.to_str().unwrap(), + ]) + .assert() + .success() + .stderr(contains("Successfully created seed config file here")); + + assert!(custom_config_path.is_file()); + + // DON'T create parent directories for a custom config path. + fs::remove_dir_all(testenv.config_dir()).unwrap(); + testenv + .command() + .args([ + "--seed-config", + "--config-path", + custom_config_path.to_str().unwrap(), + ]) + .assert() + .failure() + .stderr(contains("Could not create config file")); + + assert!(!custom_config_path.is_file()); } #[test] From bc820c5f10b1ae54d0c3012575df55d13accfde1 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Fri, 6 Jun 2025 22:52:44 +0200 Subject: [PATCH 02/45] Upload binaries from build step as artifact (#423) --- .github/workflows/ci.yml | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c1cdf3..3553418 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,19 +16,33 @@ jobs: matrix: platform: [ubuntu-latest, macos-latest, windows-latest] toolchain: [stable, 1.75.0] + include: + - platform: windows-latest + exe_suffix: .exe runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} + - run: mkdir artifacts - name: Build with default features - run: cargo build + run: | + cargo build + cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-default${{ matrix.exe_suffix}} - name: Build with logging and Rustls with webpki roots - run: cargo build --features logging,rustls-with-webpki-roots --no-default-features + run: | + cargo build --features logging,rustls-with-webpki-roots --no-default-features + cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-logging-rustls-webpki${{ matrix.exe_suffix}} - name: Build with native TLS backend - # expects runners have the proper Native SSL library - run: cargo build --features native-tls --no-default-features + run: | + # expects runners have the proper Native SSL library + cargo build --features native-tls --no-default-features + cp target/debug/tldr${{ matrix.exe_suffix}} artifacts/tldr-native-tls${{ matrix.exe_suffix}} + - uses: actions/upload-artifact@v4 + with: + name: tldr-debug-build-${{ matrix.platform }}-rust-${{ matrix.toolchain }} + path: artifacts/ - name: Run tests run: cargo test -- --test-threads 1 From 43ab2cb920f500795fcf9a353a9695eb2d896664 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Fri, 6 Jun 2025 23:13:47 +0200 Subject: [PATCH 03/45] Bump MSRV to 1.80 (#426) --- .github/workflows/ci.yml | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3553418..3a99b2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - toolchain: [stable, 1.75.0] + toolchain: [stable, 1.80.1] include: - platform: windows-latest exe_suffix: .exe diff --git a/Cargo.toml b/Cargo.toml index bbd31c7..628c9bb 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.7.2" include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"] -rust-version = "1.75" +rust-version = "1.80" edition = "2021" [[bin]] From d1be7d6bb92bed580257edcf82d665fb86ff2788 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Fri, 6 Jun 2025 23:25:30 +0200 Subject: [PATCH 04/45] Introduce Language struct (#425) * Remove unneeded Clone bound on Dedup * Introduce Language struct to represent language strings * Move language directory name logic into own method on Language type --- src/cache.rs | 26 ++++++++++-------- src/extensions.rs | 4 +-- src/main.rs | 70 ++++++++++++++++++++++++++++++++--------------- 3 files changed, 65 insertions(+), 35 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index c1308f2..fdd77da 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -18,6 +18,19 @@ use crate::{config::TlsBackend, types::PlatformType, utils::print_warning}; pub static TLDR_PAGES_DIR: &str = "tldr-pages"; static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct Language<'a>(pub &'a str); + +impl Language<'_> { + fn directory_name(&self) -> String { + if *self == Language("en") { + String::from("pages") + } else { + format!("pages.{}", self.0) + } + } +} + #[derive(Debug)] pub struct Cache { cache_dir: PathBuf, @@ -227,7 +240,7 @@ impl Cache { pub fn find_page( &self, name: &str, - languages: &[String], + languages: &[Language<'_>], custom_pages_dir: Option<&Path>, platforms: &[PlatformType], ) -> Option { @@ -237,16 +250,7 @@ impl Cache { // Determine directory paths let pages_dir = self.pages_dir(); - let lang_dirs: Vec = languages - .iter() - .map(|lang| { - if lang == "en" { - String::from("pages") - } else { - format!("pages.{lang}") - } - }) - .collect(); + let lang_dirs: Vec = languages.iter().map(Language::directory_name).collect(); // Look up custom page (.page.md). If it exists, return it directly if let Some(config_dir) = custom_pages_dir { diff --git a/src/extensions.rs b/src/extensions.rs index 3aebfa2..e74e9f3 100644 --- a/src/extensions.rs +++ b/src/extensions.rs @@ -1,14 +1,14 @@ use std::mem; /// An extension trait to clear duplicates from a collection. -pub(crate) trait Dedup { +pub(crate) trait Dedup { fn clear_duplicates(&mut self); } /// Clear duplicates from a collection, keep the first one seen. /// /// For small vectors, this will be faster than a `HashSet`. -impl Dedup for Vec { +impl Dedup for Vec { fn clear_duplicates(&mut self) { let orig = mem::replace(self, Vec::with_capacity(self.len())); for item in orig { diff --git a/src/main.rs b/src/main.rs index 99403b9..2113de0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,10 +31,12 @@ use std::{ io::{self, IsTerminal}, path::Path, process::{Command, ExitCode}, + sync::LazyLock, }; use anyhow::{anyhow, Context, Result}; use app_dirs::AppInfo; +use cache::Language; use clap::Parser; use config::StyleConfig; use log::debug; @@ -191,14 +193,16 @@ fn init_log() { #[cfg(not(feature = "logging"))] fn init_log() {} -fn get_languages(env_lang: Option<&str>, env_language: Option<&str>) -> Vec { +fn get_languages<'a>( + env_lang: Option<&'a str>, + env_language: Option<&'a str>, +) -> Vec> { // Language list according to // https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#language - if env_lang.is_none() { - return vec!["en".to_string()]; - } - let env_lang = env_lang.unwrap(); + let Some(env_lang) = env_lang else { + return vec![Language("en")]; + }; // Create an iterator that contains $LANGUAGE (':' separated list) followed by $LANG (single language) let locales = env_language.unwrap_or("").split(':').chain([env_lang]); @@ -207,23 +211,25 @@ fn get_languages(env_lang: Option<&str>, env_language: Option<&str>) -> Vec= 5 && locale.chars().nth(2) == Some('_') { - lang_list.push(&locale[..5]); + lang_list.push(Language(&locale[..5])); } // Language code only (e.g. `en`) if locale.len() >= 2 && locale != "POSIX" { - lang_list.push(&locale[..2]); + lang_list.push(Language(&locale[..2])); } } - lang_list.push("en"); + lang_list.push(Language("en")); lang_list.clear_duplicates(); - lang_list.into_iter().map(str::to_string).collect() + lang_list } -fn get_languages_from_env() -> Vec { +fn get_languages_from_env<'a>() -> Vec> { + static LANG: LazyLock> = LazyLock::new(|| std::env::var("LANG").ok()); + static LANGUAGE: LazyLock> = LazyLock::new(|| std::env::var("LANGUAGE").ok()); get_languages( - std::env::var("LANG").ok().as_deref(), - std::env::var("LANGUAGE").ok().as_deref(), + LANG.as_ref().map(String::as_str), + LANGUAGE.as_ref().map(String::as_str), ) } @@ -372,7 +378,8 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { // Collect languages let languages = args .language - .map_or_else(get_languages_from_env, |lang| vec![lang]); + .as_deref() + .map_or_else(get_languages_from_env, |lang| vec![Language(lang)]); // Search for command in cache let Some(lookup_result) = cache.find_page( @@ -422,7 +429,7 @@ fn compute_platforms(platforms: Option<&Vec>) -> Vec #[cfg(test)] mod test { - use crate::get_languages; + use super::*; mod language { use super::*; @@ -430,41 +437,60 @@ mod test { #[test] fn missing_lang_env() { let lang_list = get_languages(None, Some("de:fr")); - assert_eq!(lang_list, ["en"]); + assert_eq!(lang_list, [Language("en")]); let lang_list = get_languages(None, None); - assert_eq!(lang_list, ["en"]); + assert_eq!(lang_list, [Language("en")]); } #[test] fn missing_language_env() { let lang_list = get_languages(Some("de"), None); - assert_eq!(lang_list, ["de", "en"]); + assert_eq!(lang_list, [Language("de"), Language("en")]); } #[test] fn preference_order() { let lang_list = get_languages(Some("de"), Some("fr:cn")); - assert_eq!(lang_list, ["fr", "cn", "de", "en"]); + assert_eq!( + lang_list, + [ + Language("fr"), + Language("cn"), + Language("de"), + Language("en") + ] + ); } #[test] fn country_code_expansion() { let lang_list = get_languages(Some("pt_BR"), None); - assert_eq!(lang_list, ["pt_BR", "pt", "en"]); + assert_eq!( + lang_list, + [Language("pt_BR"), Language("pt"), Language("en")] + ); } #[test] fn ignore_posix_and_c() { let lang_list = get_languages(Some("POSIX"), None); - assert_eq!(lang_list, ["en"]); + assert_eq!(lang_list, [Language("en")]); let lang_list = get_languages(Some("C"), None); - assert_eq!(lang_list, ["en"]); + assert_eq!(lang_list, [Language("en")]); } #[test] fn no_duplicates() { let lang_list = get_languages(Some("de"), Some("fr:de:cn:de")); - assert_eq!(lang_list, ["fr", "de", "cn", "en"]); + assert_eq!( + lang_list, + [ + Language("fr"), + Language("de"), + Language("cn"), + Language("en") + ] + ); } } } From 1e87db7ab74e7d271849b9bc724e3c5ddd096cb3 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Fri, 1 Aug 2025 16:03:01 +0200 Subject: [PATCH 05/45] Cache rewrite (#416) --- Cargo.lock | 1 - Cargo.toml | 1 - src/cache.rs | 636 +++++++----------- src/main.rs | 220 +++--- .../common/git-checkout.md | 0 .../{pages => pages.en}/common/inkscape-v1.md | 0 .../{pages => pages.en}/common/inkscape-v2.md | 0 .../cache/{pages => pages.en}/common/which.md | 0 tests/lib.rs | 111 ++- 9 files changed, 462 insertions(+), 507 deletions(-) rename tests/cache/{pages => pages.en}/common/git-checkout.md (100%) rename tests/cache/{pages => pages.en}/common/inkscape-v1.md (100%) rename tests/cache/{pages => pages.en}/common/inkscape-v2.md (100%) rename tests/cache/{pages => pages.en}/common/which.md (100%) diff --git a/Cargo.lock b/Cargo.lock index e8db079..4da46ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1068,7 +1068,6 @@ dependencies = [ "tempfile", "toml", "ureq", - "walkdir", "yansi", "zip", ] diff --git a/Cargo.toml b/Cargo.toml index 628c9bb..6594b14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ serde = "1.0.21" serde_derive = "1.0.21" ureq = { version = "3.0.8", default-features = false, features = ["gzip"] } toml = "0.8.19" -walkdir = "2.0.1" yansi = "1" zip = { version = "2.3.0", default-features = false, features = ["deflate"] } diff --git a/src/cache.rs b/src/cache.rs index fdd77da..c18f03f 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,41 +1,38 @@ use std::{ - ffi::OsStr, fs::{self, File}, - io::{BufReader, Cursor, Read}, + io::{BufReader, Cursor, ErrorKind, Read}, path::{Path, PathBuf}, time::{Duration, SystemTime}, }; -use anyhow::{ensure, Context, Result}; +use anyhow::{anyhow, bail, ensure, Context, Result}; use log::debug; -use ureq::tls::{RootCerts, TlsConfig, TlsProvider}; -use ureq::Agent; -use walkdir::{DirEntry, WalkDir}; +use ureq::{ + http::StatusCode, + tls::{RootCerts, TlsConfig, TlsProvider}, + Agent, +}; use zip::ZipArchive; -use crate::{config::TlsBackend, types::PlatformType, utils::print_warning}; +use crate::{config::TlsBackend, types::PlatformType}; pub static TLDR_PAGES_DIR: &str = "tldr-pages"; -static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; +pub static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; #[derive(Debug, PartialEq, Eq, Hash)] pub struct Language<'a>(pub &'a str); -impl Language<'_> { - fn directory_name(&self) -> String { - if *self == Language("en") { - String::from("pages") - } else { - format!("pages.{}", self.0) - } - } +#[derive(Clone)] +pub struct CacheConfig<'a> { + pub pages_directory: &'a Path, + pub custom_pages_directory: Option<&'a Path>, + pub platforms: &'a [PlatformType], + pub languages: &'a [Language<'a>], } -#[derive(Debug)] -pub struct Cache { - cache_dir: PathBuf, - enable_styles: bool, - tls_backend: TlsBackend, +/// The directory backing this cache is checked to be populated at construction. +pub struct Cache<'a> { + config: CacheConfig<'a>, } #[derive(Debug)] @@ -44,6 +41,218 @@ pub struct PageLookupResult { pub patch_path: Option, } +impl<'a> Cache<'a> { + /// Try opening a cache at the location given by `config.pages_directory`. If no directory + /// exists at this location, `Ok(None)` is returned. + pub fn open(config: CacheConfig<'a>) -> Result> { + match config.pages_directory.metadata() { + Ok(md) => { + ensure!( + md.is_dir(), + "Cache directory `{}` exists, but is not a directory.", + config.pages_directory.display(), + ); + Ok(Some(Cache { config })) + } + Err(err) if err.kind() == ErrorKind::NotFound => Ok(None), + Err(err) => Err(anyhow!(err).context(format!( + "Error getting metdata of cache directory {}", + config.pages_directory.display() + ))), + } + } + + /// Open an existing cache at `config.pages_directory` or create one if no cache resides at + /// this location. In case of success, the return value is a tuple with the `Cache` and a + /// boolean indicating whether the cache was newly created. + pub fn open_or_create(config: CacheConfig<'a>) -> Result<(Self, bool)> { + if let Some(cache) = Self::open(config.clone())? { + return Ok((cache, false)); + } + + fs::create_dir_all(config.pages_directory).with_context(|| { + format!( + "Cache directory `{}` cannot be created", + config.pages_directory.display(), + ) + })?; + eprintln!( + "Successfully created cache directory `{}`.", + config.pages_directory.display(), + ); + + Ok((Cache { config }, true)) + } + + pub fn age(&self) -> Result { + let mtime = self.config.pages_directory.metadata()?.modified()?; + SystemTime::now() + .duration_since(mtime) + .context("Error comparing cache mtime with current time") + } + + pub fn find_page(&self, command: &str) -> Option { + let page_filename = format!("{command}.md"); + let patch_filename = format!("{command}.patch.md"); + let custom_filename = format!("{command}.page.md"); + + if let Some(custom_pages_dir) = self.config.custom_pages_directory { + let custom_page = custom_pages_dir.join(custom_filename); + if custom_page.is_file() { + return Some(PageLookupResult::with_page(custom_page)); + } + } + + let patch_path = self + .config + .custom_pages_directory + .map(|dir| dir.join(&patch_filename)) + .filter(|path| path.is_file()); + + for &platform in self.config.platforms { + for language in self.config.languages { + let mut search_path = self.config.pages_directory.to_path_buf(); + search_path.push(language.directory_name()); + search_path.push(platform.directory_name()); + search_path.push(&page_filename); + + if search_path.is_file() { + return Some( + PageLookupResult::with_page(search_path).with_optional_patch(patch_path), + ); + } + } + } + + None + } + + pub fn list_pages(&self) -> Result> { + let mut pages = Vec::new(); + + let mut append_all = |directory: &Path, suffix: &str| -> Result<()> { + let Ok(file_iter) = fs::read_dir(directory) else { + return Ok(()); + }; + + for entry in file_iter { + let entry = entry?; + if entry.file_type()?.is_file() { + let mut page_path = entry + .file_name() + .into_string() + .map_err(|_| anyhow!("Found invalid filename: {:?}", entry.path()))?; + + if page_path.ends_with(suffix) { + page_path.truncate(page_path.len() - suffix.len()); + pages.push(page_path); + } else { + debug!( + "Skipping page entry not ending in \".md\": {:?}", + entry.path(), + ); + } + } + } + + Ok(()) + }; + + let mut search_path = self.config.pages_directory.to_path_buf(); + for language in self.config.languages { + search_path.push(language.directory_name()); + for platform in self.config.platforms { + search_path.push(platform.directory_name()); + append_all(&search_path, ".md")?; + search_path.pop(); + } + search_path.pop(); + } + + if let Some(custom_pages_dir) = self.config.custom_pages_directory { + append_all(custom_pages_dir, ".page.md")?; + } + + pages.sort_unstable(); + pages.dedup(); + Ok(pages) + } + + pub fn old_custom_pages_exist(&self) -> Result { + let Some(directory) = self.config.custom_pages_directory else { + return Ok(false); + }; + let Ok(file_iter) = fs::read_dir(directory) else { + return Ok(false); + }; + + for entry in file_iter { + if let Some(extension) = entry?.path().extension() { + if extension == "page" || extension == "patch" { + return Ok(true); + } + } + } + + Ok(false) + } + + pub fn clear(self) -> Result<()> { + fs::remove_dir_all(self.config.pages_directory).with_context(|| { + format!( + "Could not remove pages directory at {}", + self.config.pages_directory.display(), + ) + }) + } + + pub fn update(&mut self, archive_url: &str, tls_backend: TlsBackend) -> Result<()> { + let client = Self::build_client(tls_backend); + + // Download everything before deleting anything + let archives = self + .config + .languages + .iter() + .map(|lang| { + Ok(( + lang, + Self::download( + &client, + &format!("{archive_url}/tldr-{}.zip", lang.directory_name()), + )? + .map(|bytes| ZipArchive::new(Cursor::new(bytes))) + .transpose()?, + )) + }) + .collect::>>()?; + + // Clear cache directory + // Note: This is not the best solution. Ideally we would download the + // archive to a temporary directory and then swap the two directories. + // But renaming a directory doesn't work across filesystems and Rust + // does not yet offer a recursive directory copying function. So for + // now, we'll use this approach. + fs::remove_dir_all(self.config.pages_directory)?; + fs::create_dir(self.config.pages_directory)?; + + for (lang, archive) in archives { + if let Some(mut archive) = archive { + debug!("Extracting archive for {lang:?}"); + archive.extract(self.config.pages_directory.join(lang.directory_name()))?; + } else { + debug!("No archive found for {lang:?}"); + } + } + + Ok(()) + } + + pub fn config(&self) -> &CacheConfig<'a> { + &self.config + } +} + impl PageLookupResult { pub fn with_page(page_path: PathBuf) -> Self { Self { @@ -90,120 +299,15 @@ impl PageLookupResult { } } -pub enum CacheFreshness { - /// The cache is still fresh (less than `MAX_CACHE_AGE` old) - Fresh, - /// The cache is stale and should be updated - Stale(Duration), - /// The cache is missing - Missing, +impl Language<'_> { + fn directory_name(&self) -> String { + format!("pages.{}", self.0) + } } -impl Cache { - pub fn new

(cache_dir: P, enable_styles: bool, tls_backend: TlsBackend) -> Self - where - P: Into, - { - Self { - cache_dir: cache_dir.into(), - enable_styles, - tls_backend, - } - } - - pub fn cache_dir(&self) -> &Path { - &self.cache_dir - } - - /// Make sure that the cache directory exists and is a directory. - /// If necessary, create the directory. - fn ensure_cache_dir_exists(&self) -> Result<()> { - // Check whether `cache_dir` exists and is a directory - let (cache_dir_exists, cache_dir_is_dir) = self - .cache_dir - .metadata() - .map_or((false, false), |md| (true, md.is_dir())); - ensure!( - !cache_dir_exists || cache_dir_is_dir, - "Cache directory path `{}` is not a directory", - self.cache_dir.display(), - ); - - if !cache_dir_exists { - // If missing, try to create the complete directory path - fs::create_dir_all(&self.cache_dir).with_context(|| { - format!( - "Cache directory path `{}` cannot be created", - self.cache_dir.display(), - ) - })?; - eprintln!( - "Successfully created cache directory path `{}`.", - self.cache_dir.display(), - ); - } - - Ok(()) - } - - fn pages_dir(&self) -> PathBuf { - self.cache_dir.join(TLDR_PAGES_DIR) - } - - /// Update the pages cache from the specified URL. - pub fn update(&self, archive_source: &str) -> Result<()> { - self.ensure_cache_dir_exists()?; - - let archive_url = format!("{archive_source}/tldr.zip"); - - let client = Self::build_client(self.tls_backend)?; - // First, download the compressed data - let bytes: Vec = Self::download(&client, &archive_url)?; - - // Decompress the response body into an `Archive` - let mut archive = ZipArchive::new(Cursor::new(bytes)) - .context("Could not decompress downloaded ZIP archive")?; - - // Clear cache directory - // Note: This is not the best solution. Ideally we would download the - // archive to a temporary directory and then swap the two directories. - // But renaming a directory doesn't work across filesystems and Rust - // does not yet offer a recursive directory copying function. So for - // now, we'll use this approach. - self.clear() - .context("Could not clear the cache directory")?; - - // Extract archive into pages dir - archive - .extract(self.pages_dir()) - .context("Could not unpack compressed data")?; - - Ok(()) - } - - /// Return the duration since the cache directory was last modified. - pub fn last_update(&self) -> Option { - if let Ok(metadata) = fs::metadata(self.pages_dir()) { - if let Ok(mtime) = metadata.modified() { - let now = SystemTime::now(); - return now.duration_since(mtime).ok(); - } - } - None - } - - /// Return the freshness of the cache (fresh, stale or missing). - pub fn freshness(&self) -> CacheFreshness { - match self.last_update() { - Some(ago) if ago > crate::config::MAX_CACHE_AGE => CacheFreshness::Stale(ago), - Some(_) => CacheFreshness::Fresh, - None => CacheFreshness::Missing, - } - } - - /// Return the platform directory. - fn get_platform_dir(platform: PlatformType) -> &'static str { - match platform { +impl PlatformType { + fn directory_name(self) -> &'static str { + match self { PlatformType::Linux => "linux", PlatformType::OsX => "osx", PlatformType::SunOs => "sunos", @@ -215,224 +319,10 @@ impl Cache { PlatformType::Common => "common", } } - - /// Check for pages for a given platform in one of the given languages. - fn find_page_for_platform( - page_name: &str, - pages_dir: &Path, - platform: &str, - language_dirs: &[String], - ) -> Option { - language_dirs - .iter() - .map(|lang_dir| pages_dir.join(lang_dir).join(platform).join(page_name)) - .find(|path| path.exists() && path.is_file()) - } - - /// Look up custom patch (.patch.md). If it exists, store it in a variable. - fn find_patch(patch_name: &str, custom_pages_dir: Option<&Path>) -> Option { - custom_pages_dir - .map(|custom_dir| custom_dir.join(patch_name)) - .filter(|path| path.exists() && path.is_file()) - } - - /// Search for a page and return the path to it. - pub fn find_page( - &self, - name: &str, - languages: &[Language<'_>], - custom_pages_dir: Option<&Path>, - platforms: &[PlatformType], - ) -> Option { - let page_filename = format!("{name}.md"); - let patch_filename = format!("{name}.patch.md"); - let custom_filename = format!("{name}.page.md"); - - // Determine directory paths - let pages_dir = self.pages_dir(); - let lang_dirs: Vec = languages.iter().map(Language::directory_name).collect(); - - // Look up custom page (.page.md). If it exists, return it directly - if let Some(config_dir) = custom_pages_dir { - // TODO: Remove this check 1 year after version 1.7.0 was released - self.check_for_old_custom_pages(config_dir); - - let custom_page = config_dir.join(custom_filename); - if custom_page.exists() && custom_page.is_file() { - return Some(PageLookupResult::with_page(custom_page)); - } - } - - let patch_path = Self::find_patch(&patch_filename, custom_pages_dir); - - // Try to find a platform specific path next, in the order supplied by the user, and append custom patch to it. - for &platform in platforms { - let platform_dir = Cache::get_platform_dir(platform); - if let Some(page) = - Self::find_page_for_platform(&page_filename, &pages_dir, platform_dir, &lang_dirs) - { - return Some(PageLookupResult::with_page(page).with_optional_patch(patch_path)); - } - } - - None - } - - /// Return the available pages. - pub fn list_pages( - &self, - custom_pages_dir: Option<&Path>, - platforms: &[PlatformType], - ) -> Vec { - // Determine platforms directory and platform - let platforms_dir = self.pages_dir().join("pages"); - let platform_dirs: Vec<&'static str> = platforms - .iter() - .map(|&p| Self::get_platform_dir(p)) - .collect(); - - // Closure that allows the WalkDir instance to traverse platform - // relevant page directories, but not others. - let should_walk = |entry: &DirEntry| -> bool { - let file_type = entry.file_type(); - let Some(file_name) = entry.file_name().to_str() else { - return false; - }; - if file_type.is_dir() { - return platform_dirs.contains(&file_name); - } else if file_type.is_file() { - return true; - } - false - }; - - let to_stem = |entry: DirEntry| -> Option { - entry - .path() - .file_stem() - .and_then(OsStr::to_str) - .map(str::to_string) - }; - - let to_stem_custom = |entry: DirEntry| -> Option { - entry - .path() - .file_name() - .and_then(OsStr::to_str) - .and_then(|s| s.strip_suffix(".page.md")) - .map(str::to_string) - }; - - // Recursively walk through platform specific directory - let mut pages = WalkDir::new(platforms_dir) - .min_depth(1) // Skip root directory - .into_iter() - .filter_entry(should_walk) // Filter out pages for other architectures - .filter_map(Result::ok) // Convert results to options, filter out errors - .filter_map(|e| { - let extension = e.path().extension().unwrap_or_default(); - if e.file_type().is_file() && extension == "md" { - to_stem(e) - } else { - None - } - }) - .collect::>(); - - if let Some(custom_pages_dir) = custom_pages_dir { - let is_page = |entry: &DirEntry| -> bool { - entry.file_type().is_file() - && entry - .path() - .file_name() - .and_then(OsStr::to_str) - .is_some_and(|file_name| file_name.ends_with(".page.md")) - }; - - let custom_pages = WalkDir::new(custom_pages_dir) - .min_depth(1) - .max_depth(1) - .into_iter() - .filter_entry(is_page) - .filter_map(Result::ok) - .filter_map(to_stem_custom); - - pages.extend(custom_pages); - } - - pages.sort(); - pages.dedup(); - pages - } - - /// Delete the cache directory - /// - /// Returns true if the cache was deleted and false if the cache dir did - /// not exist. - pub fn clear(&self) -> Result { - if !self.cache_dir.exists() { - return Ok(false); - } - ensure!( - self.cache_dir.is_dir(), - "Cache path ({}) is not a directory.", - self.cache_dir.display(), - ); - - // Delete old tldr-pages cache location as well if present - // TODO: To be removed in the future - for pages_dir_name in [TLDR_PAGES_DIR, TLDR_OLD_PAGES_DIR] { - let pages_dir = self.cache_dir.join(pages_dir_name); - - if pages_dir.exists() { - fs::remove_dir_all(&pages_dir).with_context(|| { - format!( - "Could not remove the cache directory at {}", - pages_dir.display() - ) - })?; - } - } - - Ok(true) - } - - /// Check for old custom pages (without .md suffix) and print a warning. - fn check_for_old_custom_pages(&self, custom_pages_dir: &Path) { - let old_custom_pages_exist = WalkDir::new(custom_pages_dir) - .min_depth(1) - .max_depth(1) - .into_iter() - .filter_entry(|entry| entry.file_type().is_file()) - .any(|entry| { - if let Ok(entry) = entry { - let extension = entry.path().extension(); - if let Some(extension) = extension { - extension == "page" || extension == "patch" - } else { - false - } - } else { - false - } - }); - if old_custom_pages_exist { - print_warning( - self.enable_styles, - &format!( - "Custom pages using the old naming convention were found in {}.\n\ - Please rename them to follow the new convention:\n\ - - `.page` → `.page.md`\n\ - - `.patch` → `.patch.md`", - custom_pages_dir.display() - ), - ); - } - } } -impl Cache { - fn build_client(tls_backend: TlsBackend) -> Result { +impl Cache<'_> { + fn build_client(tls_backend: TlsBackend) -> Agent { let tls_builder = match tls_backend { #[cfg(feature = "native-tls")] TlsBackend::NativeTls => TlsConfig::builder() @@ -448,22 +338,32 @@ impl Cache { .root_certs(RootCerts::PlatformVerifier), }; let config = Agent::config_builder() + .http_status_as_error(false) // because we want to handle them .tls_config(tls_builder.build()) .build(); - Ok(config.into()) + config.into() } /// Download the archive from the specified URL. - fn download(client: &Agent, archive_url: &str) -> Result> { - let response = client - .get(archive_url) - .call() - .with_context(|| format!("Could not download tldr pages from {archive_url}"))?; - let mut buf: Vec = Vec::new(); - response.into_body().into_reader().read_to_end(&mut buf)?; - debug!("{} bytes downloaded", buf.len()); - Ok(buf) + fn download(client: &Agent, archive_url: &str) -> Result>> { + debug!("Downloading archive from {archive_url}"); + let response = client.get(archive_url).call(); + match response { + Ok(response) if response.status().is_success() => { + let mut buf: Vec = Vec::new(); + response.into_body().into_reader().read_to_end(&mut buf)?; + debug!("{} bytes downloaded", buf.len()); + Ok(Some(buf)) + } + Ok(response) if response.status() == StatusCode::NOT_FOUND => Ok(None), + _ => { + bail!( + "Could not download tldr pages from {archive_url}: {:?}", + response, + ) + } + } } } @@ -521,22 +421,4 @@ mod tests { assert_eq!(&buf, b"Hello\n"); } - - #[test] - #[cfg(feature = "native-tls")] - fn test_create_https_client_with_native_tls() { - Cache::build_client(TlsBackend::NativeTls).expect("fails to build a client."); - } - - #[test] - #[cfg(feature = "rustls-with-webpki-roots")] - fn test_create_https_client_with_rustls() { - Cache::build_client(TlsBackend::RustlsWithWebpkiRoots).expect("fails to build a client."); - } - - #[test] - #[cfg(feature = "rustls-with-native-roots")] - fn test_create_https_client_with_rustls_with_native_roots() { - Cache::build_client(TlsBackend::RustlsWithNativeRoots).expect("fails to build a client."); - } } diff --git a/src/main.rs b/src/main.rs index 2113de0..3797f0b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,9 +36,9 @@ use std::{ use anyhow::{anyhow, Context, Result}; use app_dirs::AppInfo; -use cache::Language; +use cache::{CacheConfig, Language, TLDR_OLD_PAGES_DIR}; use clap::Parser; -use config::StyleConfig; +use config::{StyleConfig, TlsBackend}; use log::debug; mod cache; @@ -52,7 +52,7 @@ mod types; mod utils; use crate::{ - cache::{Cache, CacheFreshness, PageLookupResult, TLDR_PAGES_DIR}, + cache::{Cache, PageLookupResult, TLDR_PAGES_DIR}, cli::Cli, config::{get_config_dir, make_default_config, Config, PathWithSource}, extensions::Dedup, @@ -67,77 +67,25 @@ const APP_INFO: AppInfo = AppInfo { author: NAME, }; -/// The cache should be updated if it was explicitly requested, -/// or if an automatic update is due and allowed. -fn should_update_cache(cache: &Cache, args: &Cli, config: &Config) -> bool { - args.update - || (!args.no_auto_update - && config.updates.auto_update - && cache - .last_update() - .map_or(true, |ago| ago >= config.updates.auto_update_interval)) -} - -#[derive(PartialEq)] -enum CheckCacheResult { - CacheFound, - CacheMissing, -} - -/// Check the cache for freshness. If it's stale or missing, show a warning. -fn check_cache(cache: &Cache, args: &Cli, enable_styles: bool) -> CheckCacheResult { - match cache.freshness() { - CacheFreshness::Fresh => CheckCacheResult::CacheFound, - CacheFreshness::Stale(_) if args.quiet => CheckCacheResult::CacheFound, - CacheFreshness::Stale(age) => { - print_warning( - enable_styles, - &format!( - "The cache hasn't been updated for {} days.\n\ - You should probably run `tldr --update` soon.", - age.as_secs() / 24 / 3600 - ), - ); - CheckCacheResult::CacheFound - } - CacheFreshness::Missing => { - print_error( - enable_styles, - &anyhow::anyhow!( - "Page cache not found. Please run `tldr --update` to download the cache." - ), - ); - println!("\nNote: You can optionally enable automatic cache updates by adding the"); - println!("following config to your config file:\n"); - println!(" [updates]"); - println!(" auto_update = true\n"); - println!("The path to your config file can be looked up with `tldr --show-paths`."); - println!("To create an initial config file, use `tldr --seed-config`.\n"); - println!("You can find more tips and tricks in our docs:\n"); - println!(" https://tealdeer-rs.github.io/tealdeer/config_updates.html"); - CheckCacheResult::CacheMissing - } - } -} - /// Clear the cache -fn clear_cache(cache: &Cache, quietly: bool) -> Result<()> { - let cache_dir_found = cache.clear().context("Could not clear cache")?; +fn clear_cache(cache: Cache, quietly: bool) -> Result<()> { + let cache_dir = cache.config().pages_directory.display(); + cache.clear().context("Could not clear cache")?; if !quietly { - let cache_dir = cache.cache_dir().display(); - if cache_dir_found { - eprintln!("Successfully cleared cache at `{cache_dir}`."); - } else { - eprintln!("Cache directory not found at `{cache_dir}`, nothing to do."); - } + eprintln!("Successfully cleared cache at `{cache_dir}`."); } Ok(()) } /// Update the cache -fn update_cache(cache: &Cache, archive_source: &str, quietly: bool) -> Result<()> { +fn update_cache( + cache: &mut Cache, + archive_source: &str, + tls_backend: TlsBackend, + quietly: bool, +) -> Result<()> { cache - .update(archive_source) + .update(archive_source, tls_backend) .context("Could not update cache")?; if !quietly { eprintln!("Successfully updated cache."); @@ -333,8 +281,6 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { return Ok(ExitCode::SUCCESS); } - let platforms = compute_platforms(args.platforms.as_ref()); - // If a local file was passed in, render it and exit if let Some(file) = args.render { let path = PageLookupResult::with_page(file); @@ -342,56 +288,120 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { return Ok(ExitCode::SUCCESS); } - // Instantiate cache. This will not yet create the cache directory! - let cache = Cache::new( - &config.directories.cache_dir.path, - enable_styles, - config.updates.tls_backend, - ); + let platforms = compute_platforms(args.platforms.as_ref()); + let languages = args + .language + .as_deref() + .map_or_else(get_languages_from_env, |lang| vec![Language(lang)]); + + let cache_config = CacheConfig { + pages_directory: &config.directories.cache_dir.path().join(TLDR_PAGES_DIR), + custom_pages_directory: config + .directories + .custom_pages_dir + .as_ref() + .map(PathWithSource::path), + platforms: &platforms, + languages: &languages, + }; + + // TODO: remove in tealdeer 1.9 + let old_config = CacheConfig { + pages_directory: &config.directories.cache_dir.path().join(TLDR_OLD_PAGES_DIR), + ..cache_config + }; + if let Ok(Some(old_cache)) = Cache::open(old_config) { + old_cache.clear()?; + eprintln!("Cleared pages from old cache location."); + } - // Clear cache, pass through if args.clear_cache { - clear_cache(&cache, args.quiet)?; + if let Some(cache) = Cache::open(cache_config)? { + clear_cache(cache, args.quiet)?; + } + return Ok(ExitCode::SUCCESS); } - if should_update_cache(&cache, &args, &config) { - update_cache(&cache, &config.updates.archive_source, args.quiet)?; - } else if (args.list || !args.command.is_empty()) - && check_cache(&cache, &args, enable_styles) == CheckCacheResult::CacheMissing - { - // Cache is needed, but missing - return Ok(ExitCode::FAILURE); - } + 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 { + update_cache( + &mut cache, + &config.updates.archive_source, + config.updates.tls_backend, + args.quiet, + )?; + } + + cache + } else if args.list || !command.is_empty() { + // Cache is needed for these commands to work + let Some(cache) = Cache::open(cache_config)? else { + print_error( + enable_styles, + &anyhow::anyhow!( + "Page cache not found. Please run `tldr --update` to download the cache." + ), + ); + println!("\nNote: You can optionally enable automatic cache updates by adding the"); + println!("following config to your config file:\n"); + println!(" [updates]"); + println!(" auto_update = true\n"); + println!("The path to your config file can be looked up with `tldr --show-paths`."); + println!("To create an initial config file, use `tldr --seed-config`.\n"); + println!("You can find more tips and tricks in our docs:\n"); + println!(" https://tealdeer-rs.github.io/tealdeer/config_updates.html"); + + return Ok(ExitCode::FAILURE); + }; + + 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 + } else { + // There is nothing left to do + return Ok(ExitCode::SUCCESS); + }; - // List cached commands and exit if args.list { - println!( - "{}", - cache.list_pages(custom_pages_dir, &platforms).join("\n") - ); + for page in cache.list_pages()? { + println!("{page}"); + } return Ok(ExitCode::SUCCESS); } // Show command from cache if !command.is_empty() { - // Collect languages - let languages = args - .language - .as_deref() - .map_or_else(get_languages_from_env, |lang| vec![Language(lang)]); + // TODO: Remove this check 1 year after version 1.7.0 was released + if cache.old_custom_pages_exist()? { + print_warning( + enable_styles, + &format!( + "Custom pages using the old naming convention were found in {}.\n\ + Please rename them to follow the new convention:\n\ + - `.page` → `.page.md`\n\ + - `.patch` → `.patch.md`", + cache + .config() + .custom_pages_directory + .expect("Old custom pages can only exist in custom pages directory") + .display(), + ), + ); + } - // Search for command in cache - let Some(lookup_result) = cache.find_page( - &command, - &languages, - config - .directories - .custom_pages_dir - .as_ref() - .map(PathWithSource::path), - &platforms, - ) else { + let Some(lookup_result) = cache.find_page(&command) else { if !args.quiet { print_warning( enable_styles, diff --git a/tests/cache/pages/common/git-checkout.md b/tests/cache/pages.en/common/git-checkout.md similarity index 100% rename from tests/cache/pages/common/git-checkout.md rename to tests/cache/pages.en/common/git-checkout.md diff --git a/tests/cache/pages/common/inkscape-v1.md b/tests/cache/pages.en/common/inkscape-v1.md similarity index 100% rename from tests/cache/pages/common/inkscape-v1.md rename to tests/cache/pages.en/common/inkscape-v1.md diff --git a/tests/cache/pages/common/inkscape-v2.md b/tests/cache/pages.en/common/inkscape-v2.md similarity index 100% rename from tests/cache/pages/common/inkscape-v2.md rename to tests/cache/pages.en/common/inkscape-v2.md diff --git a/tests/cache/pages/common/which.md b/tests/cache/pages.en/common/which.md similarity index 100% rename from tests/cache/pages/common/which.md rename to tests/cache/pages.en/common/which.md diff --git a/tests/lib.rs b/tests/lib.rs index 172c1c5..2a1665c 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -16,6 +16,7 @@ use predicates::{ use tempfile::{Builder as TempfileBuilder, TempDir}; pub static TLDR_PAGES_DIR: &str = "tldr-pages"; +pub static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; struct TestEnv { _test_dir: TempDir, @@ -36,9 +37,9 @@ impl TestEnv { features: vec![], }; - create_dir_all(&this.cache_dir()).unwrap(); - create_dir_all(&this.config_dir()).unwrap(); - create_dir_all(&this.custom_pages_dir()).unwrap(); + create_dir_all(this.cache_dir()).unwrap(); + create_dir_all(this.config_dir()).unwrap(); + create_dir_all(this.custom_pages_dir()).unwrap(); this.append_to_config(format!( "directories.cache_dir = '{}'\n", @@ -100,7 +101,11 @@ impl TestEnv { /// Add entry for that environment to an OS-specific subfolder. fn add_os_entry(&self, os: &str, name: &str, contents: &str) { - let dir = self.cache_dir().join(TLDR_PAGES_DIR).join("pages").join(os); + let dir = self + .cache_dir() + .join(TLDR_PAGES_DIR) + .join("pages.en") + .join(os); create_dir_all(&dir).unwrap(); fs::write(dir.join(format!("{name}.md")), contents.as_bytes()).unwrap(); @@ -355,6 +360,45 @@ fn test_quiet_cache() { .stdout(is_empty()); } +#[test] +fn test_clear_only_pages_directory() { + let testenv = TestEnv::new().install_default_cache(); + testenv + .command() + .args(["--clear-cache"]) + .assert() + .success() + .stderr(contains(format!( + "Successfully cleared cache at `{}`.", + testenv.cache_dir().join(TLDR_PAGES_DIR).to_str().unwrap(), + ))); + + assert!(testenv.cache_dir().is_dir()); + assert!(!testenv.cache_dir().join(TLDR_PAGES_DIR).exists()); +} + +#[test] +fn test_always_delete_old_pages_directory() { + let testenv = TestEnv::new().install_default_cache(); + fs::rename( + testenv.cache_dir().join(TLDR_PAGES_DIR), + testenv.cache_dir().join(TLDR_OLD_PAGES_DIR), + ) + .unwrap(); + + testenv + .command() + .arg("--list") + .assert() + .failure() + .stderr(contains("Cleared pages from old cache location.")) + .stderr(contains("Page cache not found.")); + + assert!(testenv.cache_dir().is_dir()); + assert!(!testenv.cache_dir().join(TLDR_PAGES_DIR).exists()); + assert!(!testenv.cache_dir().join(TLDR_OLD_PAGES_DIR).exists()); +} + #[test] fn test_warn_invalid_tls_backend() { let testenv = TestEnv::new() @@ -429,38 +473,59 @@ fn test_create_cache_directory_path() { .assert() .success() .stderr(contains(format!( - "Successfully created cache directory path `{}`.", - internal_cache_dir.to_str().unwrap() + "Successfully created cache directory `{}`.", + internal_cache_dir.join(TLDR_PAGES_DIR).to_str().unwrap() ))) .stderr(contains("Successfully updated cache.")); assert!(internal_cache_dir.is_dir()); } -#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] #[test] fn test_cache_location_not_a_directory() { - let testenv = TestEnv::new().remove_initial_config(); + let testenv = TestEnv::new(); let cache_dir = &testenv.cache_dir(); - let internal_file = cache_dir.join("internal"); - File::create(&internal_file).unwrap(); - - testenv.append_to_config(format!( - "directories.cache_dir = '{}'\n", - internal_file.to_str().unwrap() - )); + File::create(cache_dir.join(TLDR_PAGES_DIR)).unwrap(); testenv .command() - .arg("--update") + .arg("--list") .assert() .failure() .stderr(contains(format!( - "Cache directory path `{}` is not a directory", - internal_file.display(), + "Cache directory `{}` exists, but is not a directory.", + cache_dir.join(TLDR_PAGES_DIR).display(), ))); } +#[cfg(unix)] +#[test] +fn test_cache_location_permission_denied() { + use std::os::unix::fs::PermissionsExt; + + let testenv = TestEnv::new().install_default_cache(); + + testenv + .command() + .arg("--list") + .assert() + .success() + .stderr(contains("Permission denied").not()); + + // Make cache directory unreadable + let cache_dir = testenv.cache_dir(); + let mut permissions = cache_dir.metadata().unwrap().permissions(); + permissions.set_mode(0); + fs::set_permissions(cache_dir, permissions).unwrap(); + + testenv + .command() + .arg("--list") + .assert() + .failure() + .stderr(contains("Permission denied")); +} + #[test] fn test_cache_location_source() { let testenv = TestEnv::new().remove_initial_config(); @@ -624,7 +689,7 @@ fn test_os_specific_page() { fn test_markdown_rendering() { let testenv = TestEnv::new().install_default_cache(); - let expected = include_str!("cache/pages/common/which.md"); + let expected = include_str!("cache/pages.en/common/which.md"); testenv .command() .args(["--raw", "which"]) @@ -1008,7 +1073,7 @@ fn test_custom_page_overwrites() { // Add .page.md file to custom_pages_dir testenv.add_page_entry( "inkscape-v2", - include_str!("cache/pages/common/inkscape-v2.md"), + include_str!("cache/pages.en/common/inkscape-v2.md"), ); // Load expected output @@ -1051,7 +1116,7 @@ fn test_custom_patch_does_not_append_to_custom() { // In addition to the page in the cache, add the same page as a custom page. testenv.add_page_entry( "inkscape-v2", - include_str!("cache/pages/common/inkscape-v2.md"), + include_str!("cache/pages.en/common/inkscape-v2.md"), ); // Load expected output @@ -1114,7 +1179,7 @@ fn test_raw_render_file() { let path = testenv .cache_dir() .join(TLDR_PAGES_DIR) - .join("pages/common/inkscape-v1.md"); + .join("pages.en/common/inkscape-v1.md"); let mut args = vec!["--color", "never", "-f", &path.to_str().unwrap()]; // Default render @@ -1134,7 +1199,7 @@ fn test_raw_render_file() { .args(&args) .assert() .success() - .stdout(diff(include_str!("cache/pages/common/inkscape-v1.md"))); + .stdout(diff(include_str!("cache/pages.en/common/inkscape-v1.md"))); } fn touch_custom_page(testenv: &TestEnv) { From 630b7f442376232012b01f4fde5fcae6fc236aa2 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Tue, 5 Aug 2025 23:46:49 +0200 Subject: [PATCH 06/45] Allow references in `Config` (#429) For #251, I want to use the `Language<'_>` type inside `Config`. The languages will either refer to values read from the config file, or to static strings from `get_languages_from_env`, so just using `Language<'static>` is not an option. Instead, some input for the `Config` needs to be persisted in the main function for the duration of the program so that the config can reference it. At first I was hoping that this input would be the `contents` string from `RawConfig::load`, but as it turns out you cannot (in general) deserialize strings from toml without having to alter them, for example when they contain escapes like `\n`. Thus, the toml parser seemingly doesn't even try and just throws an error when deserializing into a borrowed string (even if it could in theory just return the correct substring from the input). Given that `RawConfig` should stay static then, the raw config itself is the next best thing to keep alive and have the config reference into. While this change might seem a bit drastic for little benefit, I am actually pretty happy with it because I want to unify the configuration anyways at some point so that the CLI arguments, environment variables, and the config file are merged at the beginning of the program and then only a single config is used for the everything (no more `enable_styles` everywhere!). At this time, the `Config` would have references into `Cli` anyways, and having the `ConfigLoader` as an entity for this merging also seems natural. --- src/config.rs | 146 ++++++++++++++++++++++++++------------------------ src/main.rs | 13 +++-- 2 files changed, 83 insertions(+), 76 deletions(-) diff --git a/src/config.rs b/src/config.rs index be41dd2..2d4d3a8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ use std::{ env, fmt, fs::{self, File}, - io::{ErrorKind, Read, Write}, + io::{ErrorKind, Write}, path::{Path, PathBuf}, time::Duration, }; @@ -138,8 +138,8 @@ struct RawStyleConfig { pub example_variable: RawStyle, } -impl From for StyleConfig { - fn from(raw_style_config: RawStyleConfig) -> Self { +impl From<&RawStyleConfig> for StyleConfig { + fn from(raw_style_config: &RawStyleConfig) -> Self { Self { command_name: raw_style_config.command_name.into(), description: raw_style_config.description.into(), @@ -158,8 +158,8 @@ struct RawDisplayConfig { pub use_pager: bool, } -impl From for DisplayConfig { - fn from(raw_display_config: RawDisplayConfig) -> Self { +impl From<&RawDisplayConfig> for DisplayConfig { + fn from(raw_display_config: &RawDisplayConfig) -> Self { Self { compact: raw_display_config.compact, use_pager: raw_display_config.use_pager, @@ -202,10 +202,10 @@ impl Default for RawUpdatesConfig { } } -impl TryFrom for UpdatesConfig { +impl<'a> TryFrom<&'a RawUpdatesConfig> for UpdatesConfig<'a> { type Error = anyhow::Error; - fn try_from(raw_updates_config: RawUpdatesConfig) -> Result { + fn try_from(raw_updates_config: &'a RawUpdatesConfig) -> Result { let tls_backend = match raw_updates_config.tls_backend { #[cfg(feature = "native-tls")] RawTlsBackend::NativeTls => TlsBackend::NativeTls, @@ -227,7 +227,7 @@ impl TryFrom for UpdatesConfig { auto_update_interval: Duration::from_secs( raw_updates_config.auto_update_interval_hours * 3600, ), - archive_source: raw_updates_config.archive_source, + archive_source: &raw_updates_config.archive_source, tls_backend, }) } @@ -250,20 +250,6 @@ struct RawConfig { directories: RawDirectoriesConfig, } -impl RawConfig { - fn new() -> Self { - Self::default() - } - - fn load(mut config: impl Read) -> Result { - let mut content = String::new(); - config - .read_to_string(&mut content) - .context("Failed to read from config file")?; - toml::from_str(&content).context("Failed to parse TOML config file") - } -} - impl Default for RawConfig { fn default() -> Self { let mut raw_config = RawConfig { @@ -300,10 +286,10 @@ pub struct DisplayConfig { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct UpdatesConfig { +pub struct UpdatesConfig<'a> { pub auto_update: bool, pub auto_update_interval: Duration, - pub archive_source: String, + pub archive_source: &'a str, pub tls_backend: TlsBackend, } @@ -369,23 +355,23 @@ pub enum TlsBackend { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Config { +pub struct Config<'a> { pub style: StyleConfig, pub display: DisplayConfig, - pub updates: UpdatesConfig, + pub updates: UpdatesConfig<'a>, pub directories: DirectoriesConfig, pub file_path: PathWithSource, } -impl Config { +impl<'a> Config<'a> { /// Convert a `RawConfig` to a high-level `Config`. /// /// For this, some values need to be converted to other types and some /// defaults need to be set (sometimes based on env variables). - fn from_raw(raw_config: RawConfig, config_file_path: PathWithSource) -> Result { - let style = raw_config.style.into(); - let display = raw_config.display.into(); - let updates = raw_config.updates.try_into()?; + fn from_raw(raw_config: &'a RawConfig, config_file_path: PathWithSource) -> Result { + let style = (&raw_config.style).into(); + let display = (&raw_config.display).into(); + let updates = (&raw_config.updates).try_into()?; let relative_path_root = config_file_path .path() .parent() @@ -404,7 +390,7 @@ impl Config { path: PathBuf::from(env_var), source: PathSource::EnvVar, } - } else if let Some(config_value) = raw_config.directories.cache_dir { + } else if let Some(config_value) = &raw_config.directories.cache_dir { // If the user explicitly configured a cache directory, use that. PathWithSource { // Resolve possible relative path. It would be nicer to clean up the path, but Rust stdlib @@ -425,6 +411,7 @@ impl Config { let custom_pages_dir = raw_config .directories .custom_pages_dir + .as_ref() .map(|path| PathWithSource { // Resolve possible relative path. path: relative_path_root.join(path), @@ -454,47 +441,64 @@ impl Config { file_path: config_file_path, }) } +} - /// Load and read the config file from the given path into - /// a [Config] and return it. - /// - /// path: The path to the config file. - pub fn load(path: &Path) -> Result { - let raw_config = RawConfig::load(File::open(path)?)?; +/// The [`ConfigLoader`] is used to load a [`Config`] from a file. +/// +/// Since the rich [`Config`] keeps references to [`RawConfig`], the raw config needs to be kept alive outside of the +/// [`Config`]. The [`ConfigLoader`] thus offers the following flow: +/// 1. Read a raw config using [`ConfigLoader::read`] or [`ConfigLoader::read_default_path`]. +/// 2. Validate the contents to a [`Config`] that borrows the [`ConfigLoader`]. +pub struct ConfigLoader { + raw: RawConfig, + path: PathWithSource, +} - let config = Self::from_raw( - raw_config, - PathWithSource { - path: path.into(), - source: PathSource::Cli, - }, - ) - .context("Could not process raw config")?; - - Ok(config) +impl ConfigLoader { + fn read_internal(path: PathWithSource, allow_not_found: bool) -> Result { + match fs::read_to_string(&path.path) { + Ok(content) => Ok(Self { + raw: toml::from_str(&content).with_context(|| { + format!( + "Could not parse config file contents as toml from {}.", + path.path.display() + ) + })?, + path, + }), + Err(e) if allow_not_found && e.kind() == ErrorKind::NotFound => Ok(Self { + raw: RawConfig::default(), + path, + }), + Err(e) => Err(e).context(format!( + "Could not read config file contents from {}.", + path.path().display() + )), + } } - /// Load and read the config file from the default path into - /// a [Config] and return it. - pub fn load_default_path() -> Result { - // Determine path - let config_file_path = - get_default_config_path().context("Could not determine config path")?; + /// Create a loader that uses the config at `path`. + pub fn read(path: PathBuf) -> Result { + Self::read_internal( + PathWithSource { + path, + source: PathSource::Cli, + }, + false, + ) + } - let raw_config = match File::open(config_file_path.path()) { - Ok(file) => RawConfig::load(file)?, - Err(e) if e.kind() == ErrorKind::NotFound => RawConfig::default(), - Err(e) => { - return Err(e).context(format!( - "Failed to open config file at {}", - config_file_path.path().display() - )); - } - }; - let config = - Self::from_raw(raw_config, config_file_path).context("Could not process raw config")?; + /// 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().context("Could not determine default config path.")?; + Self::read_internal(path, true) + } - Ok(config) + /// Parse the read [`RawConfig`] into a [`Config`]. + pub fn load(&self) -> Result> { + Config::from_raw(&self.raw, self.path.clone()) + .context("Could not process raw config into rich config") } } @@ -563,7 +567,7 @@ pub fn make_default_config(path: Option<&Path>) -> Result { // Create default config let serialized_config = - toml::to_string(&RawConfig::new()).context("Failed to serialize default config")?; + toml::to_string(&RawConfig::default()).context("Failed to serialize default config")?; // Write default config let mut config_file = @@ -577,7 +581,7 @@ pub fn make_default_config(path: Option<&Path>) -> Result { #[test] fn test_serialize_deserialize() { - let raw_config = RawConfig::new(); + let raw_config = RawConfig::default(); let serialized = toml::to_string(&raw_config).unwrap(); let deserialized: RawConfig = toml::from_str(&serialized).unwrap(); assert_eq!(raw_config, deserialized); @@ -585,12 +589,12 @@ fn test_serialize_deserialize() { #[test] fn test_relative_path_resolution() { - let mut raw_config = RawConfig::new(); + let mut raw_config = RawConfig::default(); raw_config.directories.cache_dir = Some("../cache".into()); raw_config.directories.custom_pages_dir = Some("../custom_pages".into()); let config = Config::from_raw( - raw_config, + &raw_config, PathWithSource { path: PathBuf::from("/path/to/config/config.toml"), source: PathSource::OsConvention, diff --git a/src/main.rs b/src/main.rs index 3797f0b..6920c63 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,7 +38,7 @@ use anyhow::{anyhow, Context, Result}; use app_dirs::AppInfo; use cache::{CacheConfig, Language, TLDR_OLD_PAGES_DIR}; use clap::Parser; -use config::{StyleConfig, TlsBackend}; +use config::{ConfigLoader, StyleConfig, TlsBackend}; use log::debug; mod cache; @@ -233,12 +233,15 @@ fn main() -> ExitCode { fn try_main(args: Cli, enable_styles: bool) -> Result { // Look up config file, if none is found fall back to default config. debug!("Loading config"); - let mut config = match &args.config_path { + let config_loader = match &args.config_path { Some(path) if !args.seed_config => { - Config::load(path).context("Could not load config from given path")? + ConfigLoader::read(path.clone()).context("Could not read config from given path")? + } + _ => { + ConfigLoader::read_default_path().context("Could not read config from default path")? } - _ => Config::load_default_path().context("Could not load config from default path")?, }; + let mut config = config_loader.load()?; // Override styles if needed if !enable_styles { @@ -327,7 +330,7 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { if was_created || args.update || cache.age()? >= config.updates.auto_update_interval { update_cache( &mut cache, - &config.updates.archive_source, + config.updates.archive_source, config.updates.tls_backend, args.quiet, )?; From 4377366c97770fe12efcb2370fd8a56f3e872317 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 16:52:36 +0200 Subject: [PATCH 07/45] Bump actions/checkout from 4 to 5 (#434) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/gh-pages.yml | 2 +- .github/workflows/release.yml | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a99b2f..f20709e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: exe_suffix: .exe runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} @@ -50,7 +50,7 @@ jobs: name: run clippy lints runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -62,7 +62,7 @@ jobs: name: run rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - 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@v4 + - 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 26fff6e..8746d42 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -8,7 +8,7 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - 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 e5ccdf9..9371ef5 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@v4 + - 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@v4 + - 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@v4 + - uses: actions/checkout@v5 - name: Upload license if: startsWith(github.ref, 'refs/tags/') run: | @@ -66,7 +66,7 @@ jobs: - arch: "arm" libc: "musleabihf" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Pull Docker image run: docker pull messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} - name: Build in Docker @@ -86,7 +86,7 @@ jobs: - arch: "x86_64" - arch: "aarch64" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: @@ -102,7 +102,7 @@ jobs: build-windows: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: @@ -134,7 +134,7 @@ jobs: - macos-aarch64 - windows-x86_64-msvc steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/download-artifact@v4 - name: Upload binary if: startsWith(github.ref, 'refs/tags/') From 5cfb817e999f16681769041379bbdc0bd8ea6251 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 17:17:51 +0200 Subject: [PATCH 08/45] Bump actions/download-artifact from 4 to 5 (#433) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Niklas Mohrin --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9371ef5..b144b0f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -135,7 +135,7 @@ jobs: - windows-x86_64-msvc steps: - uses: actions/checkout@v5 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 - name: Upload binary if: startsWith(github.ref, 'refs/tags/') run: | From 94f9030d36f5c41feae1f1d5ee8cfd1a5243eddf Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Sat, 2 Aug 2025 23:29:18 +0200 Subject: [PATCH 09/45] Move Language related functionality into config module --- src/cache.rs | 8 ++-- src/config.rs | 114 +++++++++++++++++++++++++++++++++++++++++++++++++- src/main.rs | 114 +------------------------------------------------- 3 files changed, 119 insertions(+), 117 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index c18f03f..315f6f0 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -14,14 +14,14 @@ use ureq::{ }; use zip::ZipArchive; -use crate::{config::TlsBackend, types::PlatformType}; +use crate::{ + config::{Language, TlsBackend}, + types::PlatformType, +}; pub static TLDR_PAGES_DIR: &str = "tldr-pages"; pub static TLDR_OLD_PAGES_DIR: &str = "tldr-master"; -#[derive(Debug, PartialEq, Eq, Hash)] -pub struct Language<'a>(pub &'a str); - #[derive(Clone)] pub struct CacheConfig<'a> { pub pages_directory: &'a Path, diff --git a/src/config.rs b/src/config.rs index 2d4d3a8..f9ea88e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,6 +3,7 @@ use std::{ fs::{self, File}, io::{ErrorKind, Write}, path::{Path, PathBuf}, + sync::LazyLock, time::Duration, }; @@ -12,7 +13,7 @@ use serde::Serialize as _; use serde_derive::{Deserialize, Serialize}; use yansi::{Color, Style}; -use crate::types::PathSource; +use crate::{extensions::Dedup as _, types::PathSource}; pub const CONFIG_FILE_NAME: &str = "config.toml"; pub const MAX_CACHE_AGE: Duration = Duration::from_secs(2_592_000); // 30 days @@ -317,6 +318,49 @@ pub struct DirectoriesConfig { pub custom_pages_dir: Option, } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Language<'a>(pub &'a str); + +fn get_languages<'a>( + env_lang: Option<&'a str>, + env_language: Option<&'a str>, +) -> Vec> { + // Language list according to + // https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#language + + let Some(env_lang) = env_lang else { + return vec![Language("en")]; + }; + + // Create an iterator that contains $LANGUAGE (':' separated list) followed by $LANG (single language) + let locales = env_language.unwrap_or("").split(':').chain([env_lang]); + + let mut lang_list = Vec::new(); + for locale in locales { + // Language plus country code (e.g. `en_US`) + if locale.len() >= 5 && locale.chars().nth(2) == Some('_') { + lang_list.push(Language(&locale[..5])); + } + // Language code only (e.g. `en`) + if locale.len() >= 2 && locale != "POSIX" { + lang_list.push(Language(&locale[..2])); + } + } + + lang_list.push(Language("en")); + lang_list.clear_duplicates(); + lang_list +} + +pub fn get_languages_from_env<'a>() -> Vec> { + static LANG: LazyLock> = LazyLock::new(|| std::env::var("LANG").ok()); + static LANGUAGE: LazyLock> = LazyLock::new(|| std::env::var("LANGUAGE").ok()); + get_languages( + LANG.as_ref().map(String::as_str), + LANGUAGE.as_ref().map(String::as_str), + ) +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub enum RawTlsBackend { @@ -611,3 +655,71 @@ fn test_relative_path_resolution() { Path::new("/path/to/config/../custom_pages") ); } + +#[cfg(test)] +mod test { + use super::*; + + mod language { + use super::*; + + #[test] + fn missing_lang_env() { + let lang_list = get_languages(None, Some("de:fr")); + assert_eq!(lang_list, [Language("en")]); + let lang_list = get_languages(None, None); + assert_eq!(lang_list, [Language("en")]); + } + + #[test] + fn missing_language_env() { + let lang_list = get_languages(Some("de"), None); + assert_eq!(lang_list, [Language("de"), Language("en")]); + } + + #[test] + fn preference_order() { + let lang_list = get_languages(Some("de"), Some("fr:cn")); + assert_eq!( + lang_list, + [ + Language("fr"), + Language("cn"), + Language("de"), + Language("en") + ] + ); + } + + #[test] + fn country_code_expansion() { + let lang_list = get_languages(Some("pt_BR"), None); + assert_eq!( + lang_list, + [Language("pt_BR"), Language("pt"), Language("en")] + ); + } + + #[test] + fn ignore_posix_and_c() { + let lang_list = get_languages(Some("POSIX"), None); + assert_eq!(lang_list, [Language("en")]); + let lang_list = get_languages(Some("C"), None); + assert_eq!(lang_list, [Language("en")]); + } + + #[test] + fn no_duplicates() { + let lang_list = get_languages(Some("de"), Some("fr:de:cn:de")); + assert_eq!( + lang_list, + [ + Language("fr"), + Language("de"), + Language("cn"), + Language("en") + ] + ); + } + } +} diff --git a/src/main.rs b/src/main.rs index 6920c63..5ae0a59 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,14 +31,13 @@ use std::{ io::{self, IsTerminal}, path::Path, process::{Command, ExitCode}, - sync::LazyLock, }; use anyhow::{anyhow, Context, Result}; use app_dirs::AppInfo; -use cache::{CacheConfig, Language, TLDR_OLD_PAGES_DIR}; +use cache::{CacheConfig, TLDR_OLD_PAGES_DIR}; use clap::Parser; -use config::{ConfigLoader, StyleConfig, TlsBackend}; +use config::{get_languages_from_env, ConfigLoader, Language, StyleConfig, TlsBackend}; use log::debug; mod cache; @@ -55,7 +54,6 @@ use crate::{ cache::{Cache, PageLookupResult, TLDR_PAGES_DIR}, cli::Cli, config::{get_config_dir, make_default_config, Config, PathWithSource}, - extensions::Dedup, output::print_page, types::{ColorOptions, PlatformType}, utils::{print_error, print_warning}, @@ -141,46 +139,6 @@ fn init_log() { #[cfg(not(feature = "logging"))] fn init_log() {} -fn get_languages<'a>( - env_lang: Option<&'a str>, - env_language: Option<&'a str>, -) -> Vec> { - // Language list according to - // https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#language - - let Some(env_lang) = env_lang else { - return vec![Language("en")]; - }; - - // Create an iterator that contains $LANGUAGE (':' separated list) followed by $LANG (single language) - let locales = env_language.unwrap_or("").split(':').chain([env_lang]); - - let mut lang_list = Vec::new(); - for locale in locales { - // Language plus country code (e.g. `en_US`) - if locale.len() >= 5 && locale.chars().nth(2) == Some('_') { - lang_list.push(Language(&locale[..5])); - } - // Language code only (e.g. `en`) - if locale.len() >= 2 && locale != "POSIX" { - lang_list.push(Language(&locale[..2])); - } - } - - lang_list.push(Language("en")); - lang_list.clear_duplicates(); - lang_list -} - -fn get_languages_from_env<'a>() -> Vec> { - static LANG: LazyLock> = LazyLock::new(|| std::env::var("LANG").ok()); - static LANGUAGE: LazyLock> = LazyLock::new(|| std::env::var("LANGUAGE").ok()); - get_languages( - LANG.as_ref().map(String::as_str), - LANGUAGE.as_ref().map(String::as_str), - ) -} - fn spawn_editor(custom_pages_dir: &Path, file_name: &str) -> Result<()> { create_dir_all(custom_pages_dir).context("Failed to create custom pages directory")?; @@ -439,71 +397,3 @@ fn compute_platforms(platforms: Option<&Vec>) -> Vec None => vec![PlatformType::current(), PlatformType::Common], } } - -#[cfg(test)] -mod test { - use super::*; - - mod language { - use super::*; - - #[test] - fn missing_lang_env() { - let lang_list = get_languages(None, Some("de:fr")); - assert_eq!(lang_list, [Language("en")]); - let lang_list = get_languages(None, None); - assert_eq!(lang_list, [Language("en")]); - } - - #[test] - fn missing_language_env() { - let lang_list = get_languages(Some("de"), None); - assert_eq!(lang_list, [Language("de"), Language("en")]); - } - - #[test] - fn preference_order() { - let lang_list = get_languages(Some("de"), Some("fr:cn")); - assert_eq!( - lang_list, - [ - Language("fr"), - Language("cn"), - Language("de"), - Language("en") - ] - ); - } - - #[test] - fn country_code_expansion() { - let lang_list = get_languages(Some("pt_BR"), None); - assert_eq!( - lang_list, - [Language("pt_BR"), Language("pt"), Language("en")] - ); - } - - #[test] - fn ignore_posix_and_c() { - let lang_list = get_languages(Some("POSIX"), None); - assert_eq!(lang_list, [Language("en")]); - let lang_list = get_languages(Some("C"), None); - assert_eq!(lang_list, [Language("en")]); - } - - #[test] - fn no_duplicates() { - let lang_list = get_languages(Some("de"), Some("fr:de:cn:de")); - assert_eq!( - lang_list, - [ - Language("fr"), - Language("de"), - Language("cn"), - Language("en") - ] - ); - } - } -} From 7e014093cf1151fe4a5d808081c8728fe3faa39e Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Sat, 2 Aug 2025 23:30:30 +0200 Subject: [PATCH 10/45] Move existing tests from config module into test submodule --- src/config.rs | 66 +++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/config.rs b/src/config.rs index f9ea88e..7ae6299 100644 --- a/src/config.rs +++ b/src/config.rs @@ -623,43 +623,43 @@ pub fn make_default_config(path: Option<&Path>) -> Result { Ok(config_file_path) } -#[test] -fn test_serialize_deserialize() { - let raw_config = RawConfig::default(); - let serialized = toml::to_string(&raw_config).unwrap(); - let deserialized: RawConfig = toml::from_str(&serialized).unwrap(); - assert_eq!(raw_config, deserialized); -} - -#[test] -fn test_relative_path_resolution() { - let mut raw_config = RawConfig::default(); - raw_config.directories.cache_dir = Some("../cache".into()); - raw_config.directories.custom_pages_dir = Some("../custom_pages".into()); - - let config = Config::from_raw( - &raw_config, - PathWithSource { - path: PathBuf::from("/path/to/config/config.toml"), - source: PathSource::OsConvention, - }, - ) - .unwrap(); - - assert_eq!( - config.directories.cache_dir.path(), - Path::new("/path/to/config/../cache") - ); - assert_eq!( - config.directories.custom_pages_dir.unwrap().path(), - Path::new("/path/to/config/../custom_pages") - ); -} - #[cfg(test)] mod test { use super::*; + #[test] + fn serialize_deserialize() { + let raw_config = RawConfig::default(); + let serialized = toml::to_string(&raw_config).unwrap(); + let deserialized: RawConfig = toml::from_str(&serialized).unwrap(); + assert_eq!(raw_config, deserialized); + } + + #[test] + fn relative_path_resolution() { + let mut raw_config = RawConfig::default(); + raw_config.directories.cache_dir = Some("../cache".into()); + raw_config.directories.custom_pages_dir = Some("../custom_pages".into()); + + let config = Config::from_raw( + &raw_config, + PathWithSource { + path: PathBuf::from("/path/to/config/config.toml"), + source: PathSource::OsConvention, + }, + ) + .unwrap(); + + assert_eq!( + config.directories.cache_dir.path(), + Path::new("/path/to/config/../cache") + ); + assert_eq!( + config.directories.custom_pages_dir.unwrap().path(), + Path::new("/path/to/config/../custom_pages") + ); + } + mod language { use super::*; From 3fa96a5bb2c73810909635764084edd46432bb70 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Sat, 6 Sep 2025 15:57:17 +0200 Subject: [PATCH 11/45] Add test config::test::language::with_encoding --- src/config.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/config.rs b/src/config.rs index 7ae6299..b4d6910 100644 --- a/src/config.rs +++ b/src/config.rs @@ -700,6 +700,15 @@ mod test { ); } + #[test] + fn with_encoding() { + let lang_list = get_languages(Some("de_DE.UTF-8"), None); + assert_eq!( + lang_list, + [Language("de_DE"), Language("de"), Language("en")] + ); + } + #[test] fn ignore_posix_and_c() { let lang_list = get_languages(Some("POSIX"), None); From a74b7120bd1f6e7a380223ee589649f27195bee1 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Sun, 3 Aug 2025 00:45:12 +0200 Subject: [PATCH 12/45] Add search.languages setting --- docs/src/SUMMARY.md | 1 + docs/src/config.md | 2 +- docs/src/config_search.md | 14 ++++++ src/config.rs | 24 ++++++++++ src/main.rs | 12 ++--- tests/lib.rs | 98 +++++++++++++++++++++++++++++++++++++-- 6 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 docs/src/config_search.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 2d5c716..4649382 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -8,6 +8,7 @@ - [Configuration](./config.md) - [Section: \[display\]](./config_display.md) - [Section: \[style\]](./config_style.md) + - [Section: \[search\]](./config_search.md) - [Section: \[updates\]](./config_updates.md) - [Section: \[directories\]](./config_directories.md) - [Tips and Tricks](./tips_and_tricks.md) diff --git a/docs/src/config.md b/docs/src/config.md index ece2706..6eeede4 100644 --- a/docs/src/config.md +++ b/docs/src/config.md @@ -22,7 +22,7 @@ On Linux, this will usually be `~/.config/tealdeer/config.toml`. Here's an example configuration file. Note that this example does not contain all possible config options. For details on the things that can be configured, please refer to the subsections of this documentation page -([display](config_display.html), [style](config_style.html), +([display](config_display.html), [style](config_style.html), [search](config_search.html), [updates](config_updates.html) or [directories](config_directories.html)). ```toml diff --git a/docs/src/config_search.md b/docs/src/config_search.md new file mode 100644 index 0000000..2d59d65 --- /dev/null +++ b/docs/src/config_search.md @@ -0,0 +1,14 @@ +# Section: \[search\] + +This config section is used to configure the page search in the cache. +The settings apply to `tldr ` and `tldr --list`. + +## `languages` + +The list of languages that should be considered when searching. +If unspecified, the list of languages will be inferred from the `LANG` and `LANGUAGE` environment variables. +Either way, the language used can be overwritten using the `--language` command line flag. + + [search] + # Show pages in German if available, otherwise show in English + languages = ["de", "en"] diff --git a/src/config.rs b/src/config.rs index b4d6910..69de9b5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -242,6 +242,11 @@ struct RawDirectoriesConfig { pub custom_pages_dir: Option, } +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +struct RawSearchConfig { + pub languages: Option>, +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(default)] struct RawConfig { @@ -249,6 +254,7 @@ struct RawConfig { display: RawDisplayConfig, updates: RawUpdatesConfig, directories: RawDirectoriesConfig, + search: RawSearchConfig, } impl Default for RawConfig { @@ -258,6 +264,7 @@ impl Default for RawConfig { display: RawDisplayConfig::default(), updates: RawUpdatesConfig::default(), directories: RawDirectoriesConfig::default(), + search: RawSearchConfig::default(), }; // Set default config @@ -318,6 +325,11 @@ pub struct DirectoriesConfig { pub custom_pages_dir: Option, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SearchConfig<'a> { + pub languages: Vec>, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Language<'a>(pub &'a str); @@ -404,6 +416,7 @@ pub struct Config<'a> { pub display: DisplayConfig, pub updates: UpdatesConfig<'a>, pub directories: DirectoriesConfig, + pub search: SearchConfig<'a>, pub file_path: PathWithSource, } @@ -416,6 +429,16 @@ impl<'a> Config<'a> { let style = (&raw_config.style).into(); let display = (&raw_config.display).into(); let updates = (&raw_config.updates).try_into()?; + let search = SearchConfig { + languages: raw_config + .search + .languages + .as_ref() + .map_or_else(get_languages_from_env, |langs| { + langs.iter().map(|lang| Language(lang)).collect() + }), + }; + let relative_path_root = config_file_path .path() .parent() @@ -482,6 +505,7 @@ impl<'a> Config<'a> { display, updates, directories, + search, file_path: config_file_path, }) } diff --git a/src/main.rs b/src/main.rs index 5ae0a59..5bd0fa0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,7 +37,7 @@ use anyhow::{anyhow, Context, Result}; use app_dirs::AppInfo; use cache::{CacheConfig, TLDR_OLD_PAGES_DIR}; use clap::Parser; -use config::{get_languages_from_env, ConfigLoader, Language, StyleConfig, TlsBackend}; +use config::{ConfigLoader, Language, StyleConfig, TlsBackend}; use log::debug; mod cache; @@ -250,10 +250,10 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { } let platforms = compute_platforms(args.platforms.as_ref()); - let languages = args - .language - .as_deref() - .map_or_else(get_languages_from_env, |lang| vec![Language(lang)]); + let languages = match args.language.as_deref() { + Some(lang) => &[Language(lang)] as &[_], + None => &config.search.languages, + }; let cache_config = CacheConfig { pages_directory: &config.directories.cache_dir.path().join(TLDR_PAGES_DIR), @@ -263,7 +263,7 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { .as_ref() .map(PathWithSource::path), platforms: &platforms, - languages: &languages, + languages, }; // TODO: remove in tealdeer 1.9 diff --git a/tests/lib.rs b/tests/lib.rs index 2a1665c..37f3b30 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -11,6 +11,7 @@ use std::{ use assert_cmd::prelude::*; use predicates::{ boolean::PredicateBooleanExt, + ord::eq, prelude::predicate::str::{contains, diff, is_empty, is_match}, }; use tempfile::{Builder as TempfileBuilder, TempDir}; @@ -41,10 +42,7 @@ impl TestEnv { create_dir_all(this.config_dir()).unwrap(); create_dir_all(this.custom_pages_dir()).unwrap(); - this.append_to_config(format!( - "directories.cache_dir = '{}'\n", - this.cache_dir().to_str().unwrap(), - )); + this.init_config(); this } @@ -70,6 +68,15 @@ impl TestEnv { .write_all(content.as_ref().as_bytes()) .expect("Failed to append to config file."); } + fn delete_config(&self) { + fs::remove_file(self.config_dir().join("config.toml")).unwrap(); + } + fn init_config(&self) { + self.append_to_config(format!( + "directories.cache_dir = '{}'\n", + self.cache_dir().to_str().unwrap(), + )); + } fn create_secondary_config(self) -> Self { self.append_to_secondary_config(format!( @@ -101,10 +108,20 @@ impl TestEnv { /// Add entry for that environment to an OS-specific subfolder. fn add_os_entry(&self, os: &str, name: &str, contents: &str) { + self.add_os_lang_entry(os, "en", name, contents); + } + + /// Add entry for that environment to a language-specific subfolder. + fn add_lang_entry(&self, lang: &str, name: &str, contents: &str) { + self.add_os_lang_entry("common", lang, name, contents); + } + + /// Add entry for that environment to an OS- and language specific subfolder. + fn add_os_lang_entry(&self, os: &str, lang: &str, name: &str, contents: &str) { let dir = self .cache_dir() .join(TLDR_PAGES_DIR) - .join("pages.en") + .join(format!("pages.{lang}")) .join(os); create_dir_all(&dir).unwrap(); @@ -152,6 +169,19 @@ impl TestEnv { } let run = build.run().expect("Failed to build tealdeer for testing"); let mut cmd = run.command(); + + // Avoid inheriting those from the test process. We can't just use .env_clear() because + // this breaks tests on Windows in GitHub Actions. + let relevant_env_variables = [ + "LANG", + "LANGUAGE", + "TEALDEER_CACHE_DIR", + "EDITOR", + "NO_COLOR", + ]; + for variable_name in relevant_env_variables { + cmd.env_remove(variable_name); + } cmd.env("TEALDEER_CONFIG_DIR", self.config_dir().to_str().unwrap()); cmd } @@ -908,6 +938,64 @@ fn test_common_platform_is_used_as_fallback() { .success(); } +#[test] +fn test_search_language_precedence() { + let testenv = TestEnv::new(); + for lang in ["en", "de", "it", "fr", "pl", "nl"] { + testenv.add_lang_entry(lang, lang, ""); + } + + let run = |cases: &[(Vec<(&str, &str)>, Vec<&str>, &str)]| { + for (extra_env, extra_args, expected) in cases { + let mut cmd = testenv.command(); + for (key, value) in extra_env { + cmd.env(key, value); + } + cmd.args(extra_args); + cmd.arg("--list"); + cmd.assert().success().stdout(eq(*expected)); + } + }; + + let env_cases = &[ + (vec![], vec![], "en\n"), + (vec![("LANGUAGE", "de:it")], vec![], "en\n"), + ( + vec![("LANG", "fr"), ("LANGUAGE", "de:it")], + vec![], + "de\nen\nfr\nit\n", + ), + ( + vec![("LANG", "fr"), ("LANGUAGE", "de:it")], + vec!["--language", "pl"], + "pl\n", + ), + ]; + run(env_cases); + + // Environment is only used when config setting is not set + testenv.append_to_config("search.languages = ['nl']\n"); + let config_cases = &[ + (vec![], vec![], "nl\n"), + (vec![("LANGUAGE", "de:it")], vec![], "nl\n"), + (vec![("LANG", "fr"), ("LANGUAGE", "de:it")], vec![], "nl\n"), + ( + vec![("LANG", "fr"), ("LANGUAGE", "de:it")], + vec!["--language", "pl"], + "pl\n", + ), + ]; + run(config_cases); + + // The above update setting does not change anything + testenv.append_to_config("updates.download_languages = ['cz']"); + run(config_cases); + testenv.delete_config(); + testenv.init_config(); + testenv.append_to_config("updates.download_languages = ['cz']"); + run(env_cases); +} + #[test] fn test_list_flag_rendering() { let testenv = TestEnv::new().write_custom_pages_config(); From c741146db592cd7aaccfd8ba3a7f108718a96111 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Sun, 3 Aug 2025 01:00:24 +0200 Subject: [PATCH 13/45] Add updates.download_languages setting --- docs/src/config_updates.md | 20 ++++++++++- src/cache.rs | 39 ++++++++++++-------- src/config.rs | 74 +++++++++++++++++++++----------------- src/main.rs | 21 ++++++++--- tests/lib.rs | 24 +++++++++++++ 5 files changed, 125 insertions(+), 53 deletions(-) diff --git a/docs/src/config_updates.md b/docs/src/config_updates.md index eaac735..cc5868e 100644 --- a/docs/src/config_updates.md +++ b/docs/src/config_updates.md @@ -1,5 +1,7 @@ # Section: \[updates\] +This config section contains settings related to updating the tealdeer cache. + ## Automatic updates Tealdeer can refresh the cache automatically when it is outdated. This @@ -24,7 +26,23 @@ is set to `false`. auto_update = true auto_update_interval_hours = 24 -### archive_source +## Download configuration + +### `download_languages` + +The list of languages which should be downloaded when updating. +If unspecified, the languages listed in the `search.languages` setting are used. +Thus, this setting is the most useful to instruct tealdeer to download pages in additional languages that are not searched by default. +Either way, the language used can be overwritten using the `--language` command line flag. + + [search] + languages = ["de", "en"] + + [updates] + # sometimes I like to read the Italian description + download_languages = ["de", "en", "it"] + +### `archive_source` URL for the location of the tldr pages archive. By default the pages are fetched from the latest `tldr-pages/tldr` GitHub release. diff --git a/src/cache.rs b/src/cache.rs index 315f6f0..23fd668 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -6,7 +6,7 @@ use std::{ }; use anyhow::{anyhow, bail, ensure, Context, Result}; -use log::debug; +use log::{debug, info}; use ureq::{ http::StatusCode, tls::{RootCerts, TlsConfig, TlsProvider}, @@ -27,7 +27,8 @@ pub struct CacheConfig<'a> { pub pages_directory: &'a Path, pub custom_pages_directory: Option<&'a Path>, pub platforms: &'a [PlatformType], - pub languages: &'a [Language<'a>], + pub search_languages: &'a [Language<'a>], + pub download_languages: &'a [Language<'a>], } /// The directory backing this cache is checked to be populated at construction. @@ -110,7 +111,7 @@ impl<'a> Cache<'a> { .filter(|path| path.is_file()); for &platform in self.config.platforms { - for language in self.config.languages { + for language in self.config.search_languages { let mut search_path = self.config.pages_directory.to_path_buf(); search_path.push(language.directory_name()); search_path.push(platform.directory_name()); @@ -159,7 +160,7 @@ impl<'a> Cache<'a> { }; let mut search_path = self.config.pages_directory.to_path_buf(); - for language in self.config.languages { + for language in self.config.search_languages { search_path.push(language.directory_name()); for platform in self.config.platforms { search_path.push(platform.directory_name()); @@ -206,15 +207,23 @@ impl<'a> Cache<'a> { }) } - pub fn update(&mut self, archive_url: &str, tls_backend: TlsBackend) -> Result<()> { + /// Download archives for the languages in `self.config().download_languages` and replace the + /// pages directory with the newly downloaded pages. As not all languages might have pages + /// available (for example, `en_US` instead of `en`), an iterator yielding all languages which + /// were successfully downloaded is returned. + pub fn update( + &mut self, + archive_url: &str, + tls_backend: TlsBackend, + ) -> Result>> { let client = Self::build_client(tls_backend); // Download everything before deleting anything - let archives = self + let mut archives = self .config - .languages + .download_languages .iter() - .map(|lang| { + .map(|&lang| { Ok(( lang, Self::download( @@ -236,16 +245,18 @@ impl<'a> Cache<'a> { fs::remove_dir_all(self.config.pages_directory)?; fs::create_dir(self.config.pages_directory)?; - for (lang, archive) in archives { - if let Some(mut archive) = archive { - debug!("Extracting archive for {lang:?}"); + for (lang, archive) in &mut archives { + if let Some(archive) = archive { + info!("Extracting archive for {lang:?}"); archive.extract(self.config.pages_directory.join(lang.directory_name()))?; } else { - debug!("No archive found for {lang:?}"); + info!("No archive found for {lang:?}"); } } - Ok(()) + Ok(archives + .into_iter() + .filter_map(|(lang, archive)| archive.is_some().then_some(lang))) } pub fn config(&self) -> &CacheConfig<'a> { @@ -347,7 +358,7 @@ impl Cache<'_> { /// Download the archive from the specified URL. fn download(client: &Agent, archive_url: &str) -> Result>> { - debug!("Downloading archive from {archive_url}"); + info!("Downloading archive from {archive_url}"); let response = client.get(archive_url).call(); match response { Ok(response) if response.status().is_success() => { diff --git a/src/config.rs b/src/config.rs index 69de9b5..bac3f74 100644 --- a/src/config.rs +++ b/src/config.rs @@ -190,6 +190,8 @@ struct RawUpdatesConfig { pub archive_source: String, #[serde(default)] pub tls_backend: RawTlsBackend, + #[serde(default)] + pub download_languages: Option>, } impl Default for RawUpdatesConfig { @@ -199,41 +201,11 @@ impl Default for RawUpdatesConfig { auto_update_interval_hours: DEFAULT_UPDATE_INTERVAL_HOURS, archive_source: default_archive_source(), tls_backend: RawTlsBackend::default(), + download_languages: None, } } } -impl<'a> TryFrom<&'a RawUpdatesConfig> for UpdatesConfig<'a> { - type Error = anyhow::Error; - - fn try_from(raw_updates_config: &'a RawUpdatesConfig) -> Result { - let tls_backend = match raw_updates_config.tls_backend { - #[cfg(feature = "native-tls")] - RawTlsBackend::NativeTls => TlsBackend::NativeTls, - #[cfg(feature = "rustls-with-webpki-roots")] - RawTlsBackend::RustlsWithWebpkiRoots => TlsBackend::RustlsWithWebpkiRoots, - #[cfg(feature = "rustls-with-native-roots")] - RawTlsBackend::RustlsWithNativeRoots => TlsBackend::RustlsWithNativeRoots, - // when compiling without all TLS backend features, we want to handle config error. - #[allow(unreachable_patterns)] - _ => return Err(anyhow!( - "Unsupported TLS backend: {}. This tealdeer build has support for the following options: {}", - raw_updates_config.tls_backend, - SUPPORTED_TLS_BACKENDS.iter().map(std::string::ToString::to_string).collect::>().join(", ") - )) - }; - - Ok(Self { - auto_update: raw_updates_config.auto_update, - auto_update_interval: Duration::from_secs( - raw_updates_config.auto_update_interval_hours * 3600, - ), - archive_source: &raw_updates_config.archive_source, - tls_backend, - }) - } -} - #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] struct RawDirectoriesConfig { #[serde(default)] @@ -299,6 +271,7 @@ pub struct UpdatesConfig<'a> { pub auto_update_interval: Duration, pub archive_source: &'a str, pub tls_backend: TlsBackend, + pub download_languages: Vec>, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -330,7 +303,7 @@ pub struct SearchConfig<'a> { pub languages: Vec>, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Language<'a>(pub &'a str); fn get_languages<'a>( @@ -410,6 +383,28 @@ pub enum TlsBackend { RustlsWithNativeRoots, } +impl TryFrom for TlsBackend { + type Error = anyhow::Error; + + fn try_from(raw: RawTlsBackend) -> Result { + match raw { + #[cfg(feature = "native-tls")] + RawTlsBackend::NativeTls => Ok(TlsBackend::NativeTls), + #[cfg(feature = "rustls-with-webpki-roots")] + RawTlsBackend::RustlsWithWebpkiRoots => Ok(TlsBackend::RustlsWithWebpkiRoots), + #[cfg(feature = "rustls-with-native-roots")] + RawTlsBackend::RustlsWithNativeRoots => Ok(TlsBackend::RustlsWithNativeRoots), + // when compiling without all TLS backend features, we want to handle config error. + #[allow(unreachable_patterns)] + _ => Err(anyhow!( + "Unsupported TLS backend: {}. This tealdeer build has support for the following options: {}", + raw, + SUPPORTED_TLS_BACKENDS.iter().map(std::string::ToString::to_string).collect::>().join(", ") + )) + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct Config<'a> { pub style: StyleConfig, @@ -428,7 +423,7 @@ impl<'a> Config<'a> { fn from_raw(raw_config: &'a RawConfig, config_file_path: PathWithSource) -> Result { let style = (&raw_config.style).into(); let display = (&raw_config.display).into(); - let updates = (&raw_config.updates).try_into()?; + let search = SearchConfig { languages: raw_config .search @@ -439,6 +434,19 @@ impl<'a> Config<'a> { }), }; + let updates = UpdatesConfig { + auto_update: raw_config.updates.auto_update, + auto_update_interval: Duration::from_secs( + raw_config.updates.auto_update_interval_hours * 3600, + ), + archive_source: &raw_config.updates.archive_source, + tls_backend: raw_config.updates.tls_backend.try_into()?, + download_languages: raw_config.updates.download_languages.as_ref().map_or_else( + || search.languages.clone(), + |languages| languages.iter().map(|lang| Language(lang)).collect(), + ), + }; + let relative_path_root = config_file_path .path() .parent() diff --git a/src/main.rs b/src/main.rs index 5bd0fa0..2f730a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,11 +82,21 @@ fn update_cache( tls_backend: TlsBackend, quietly: bool, ) -> Result<()> { - cache + let downloaded_languages = cache .update(archive_source, tls_backend) .context("Could not update cache")?; if !quietly { eprintln!("Successfully updated cache."); + eprint!("Pages for the following languages were downloaded: "); + let language_strings: Vec<_> = downloaded_languages + .into_iter() + .map(|lang| lang.0) + .collect(); + if language_strings.is_empty() { + eprintln!("(none)"); + } else { + eprintln!("{}", language_strings.join(", ")); + } } Ok(()) } @@ -250,9 +260,9 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { } let platforms = compute_platforms(args.platforms.as_ref()); - let languages = match args.language.as_deref() { - Some(lang) => &[Language(lang)] as &[_], - None => &config.search.languages, + let (search_languages, download_languages): (&[_], &[_]) = match args.language.as_deref() { + Some(lang) => (&[Language(lang)], &[Language(lang)]), + None => (&config.search.languages, &config.updates.download_languages), }; let cache_config = CacheConfig { @@ -263,7 +273,8 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { .as_ref() .map(PathWithSource::path), platforms: &platforms, - languages, + search_languages, + download_languages, }; // TODO: remove in tealdeer 1.9 diff --git a/tests/lib.rs b/tests/lib.rs index 37f3b30..eceb427 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -996,6 +996,30 @@ fn test_search_language_precedence() { run(env_cases); } +#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] +#[test] +fn test_update_language_arg() { + let testenv = TestEnv::new(); + testenv + .command() + .env("LANG", "it") + .arg("--update") + .assert() + .success() + .stderr(contains("it")) + .stderr(contains("en")); + + testenv + .command() + .env("LANG", "en") + .args(["--language", "it"]) + .arg("--update") + .assert() + .success() + .stderr(contains("it")) + .stderr(contains("en").not()); +} + #[test] fn test_list_flag_rendering() { let testenv = TestEnv::new().write_custom_pages_config(); From abb7e8ac5525f4b65141b996ef00e89f5c42bab4 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Mon, 22 Sep 2025 16:06:36 +0200 Subject: [PATCH 14/45] Remove native-tls from default feature set (#436) --- .github/workflows/release.yml | 2 +- Cargo.toml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b144b0f..360dd94 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,7 +93,7 @@ jobs: toolchain: stable targets: "${{ matrix.arch }}-apple-darwin" - name: Build - run: cargo build --release --target ${{ matrix.arch }}-apple-darwin --no-default-features --features webpki-roots + run: cargo build --release --target ${{ matrix.arch }}-apple-darwin - uses: actions/upload-artifact@v4 with: name: "tealdeer-macos-${{ matrix.arch }}" diff --git a/Cargo.toml b/Cargo.toml index 6594b14..3447307 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,8 @@ tempfile = "3.1.0" filetime = "0.2.10" [features] -default = ["native-tls", "rustls-with-webpki-roots", "rustls-with-native-roots"] +# native-tls is not enabled by default, because it is difficult to build for musl +default = ["rustls-with-webpki-roots", "rustls-with-native-roots"] logging = ["env_logger"] # At least one of variants for `ureq` HTTP client must be selected. From 5b306756af6163eb735eee9e84569ef0be9a0ff1 Mon Sep 17 00:00:00 2001 From: hex1c <84087661+hex1c@users.noreply.github.com> Date: Fri, 3 Oct 2025 23:23:25 +0530 Subject: [PATCH 15/45] Add `display.show_title` option to display command titles in output (#439) --- docs/src/config.md | 1 + docs/src/config_display.md | 10 ++++++ src/config.rs | 4 +++ src/formatter.rs | 14 ++++++-- src/output.rs | 2 ++ tests/lib.rs | 36 +++++++++++++++++++ .../inkscape-with-title-no-color.expected | 34 ++++++++++++++++++ tests/rendered/inkscape-with-title.expected | 34 ++++++++++++++++++ 8 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 tests/rendered/inkscape-with-title-no-color.expected create mode 100644 tests/rendered/inkscape-with-title.expected diff --git a/docs/src/config.md b/docs/src/config.md index 6eeede4..b0bf922 100644 --- a/docs/src/config.md +++ b/docs/src/config.md @@ -29,6 +29,7 @@ please refer to the subsections of this documentation page [display] compact = false use_pager = true +show_title = false [style.command_name] foreground = "red" diff --git a/docs/src/config_display.md b/docs/src/config_display.md index 36d5f23..56b5cd3 100644 --- a/docs/src/config_display.md +++ b/docs/src/config_display.md @@ -21,3 +21,13 @@ Set this to enforce more compact output, where empty lines are stripped out [display] compact = true + +## `show_title` + +Display the command name at the top of the page output (default `false`). + + [display] + show_title = true + +When enabled, the command name will be displayed at the top of the output, +styled with the `command_name` style configuration. \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index bac3f74..446b841 100644 --- a/src/config.rs +++ b/src/config.rs @@ -157,6 +157,8 @@ struct RawDisplayConfig { pub compact: bool, #[serde(default)] pub use_pager: bool, + #[serde(default)] + pub show_title: bool, } impl From<&RawDisplayConfig> for DisplayConfig { @@ -164,6 +166,7 @@ impl From<&RawDisplayConfig> for DisplayConfig { Self { compact: raw_display_config.compact, use_pager: raw_display_config.use_pager, + show_title: raw_display_config.show_title, } } } @@ -263,6 +266,7 @@ pub struct StyleConfig { pub struct DisplayConfig { pub compact: bool, pub use_pager: bool, + pub show_title: bool, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/src/formatter.rs b/src/formatter.rs index 369efce..d86de9b 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -12,6 +12,7 @@ pub enum PageSnippet<'a> { NormalCode(&'a str), Description(&'a str), Text(&'a str), + Title(&'a str), Linebreak, } @@ -20,7 +21,9 @@ impl PageSnippet<'_> { use PageSnippet::*; match self { - CommandName(s) | Variable(s) | NormalCode(s) | Description(s) | Text(s) => s.is_empty(), + CommandName(s) | Variable(s) | NormalCode(s) | Description(s) | Text(s) | Title(s) => { + s.is_empty() + } Linebreak => false, } } @@ -31,6 +34,7 @@ pub fn highlight_lines( lines: L, process_snippet: &mut F, keep_empty_lines: bool, + show_title: bool, ) -> Result<(), E> where L: Iterator, @@ -45,8 +49,12 @@ where } } LineType::Title(title) => { - debug!("Ignoring title"); - + if show_title { + process_snippet(PageSnippet::Linebreak)?; + process_snippet(PageSnippet::Title(&title))?; + } 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; diff --git a/src/output.rs b/src/output.rs index 046abe8..5f1aeae 100644 --- a/src/output.rs +++ b/src/output.rs @@ -69,6 +69,7 @@ pub fn print_page( LineIterator::new(reader), &mut process_snippet, !config.display.compact, + config.display.show_title, ) .context("Could not write to stdout")?; } @@ -92,6 +93,7 @@ fn print_snippet( NormalCode(s) => write!(writer, "{}", s.paint(style.example_code)), 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 eceb427..ef5777d 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -807,6 +807,42 @@ fn test_correct_rendering_with_config() { .stdout(diff(expected)); } +/// An end-to-end integration test for rendering with show_title config option enabled. +#[test] +fn test_show_title_config() { + // Test that default behavior without show_title shows no title + let testenv = TestEnv::new().install_default_cache(); + let expected_no_title = include_str!("rendered/inkscape-default.expected"); + + testenv + .command() + .args(["--color", "always", "inkscape-v2"]) + .assert() + .success() + .stdout(diff(expected_no_title)); + + // Configure to enable show_title + testenv.append_to_config("display.show_title = true\n"); + + let expected_no_color = include_str!("rendered/inkscape-with-title-no-color.expected"); + + testenv + .command() + .args(["inkscape-v2"]) + .assert() + .success() + .stdout(diff(expected_no_color)); + + let expected = include_str!("rendered/inkscape-with-title.expected"); + + testenv + .command() + .args(["--color", "always", "inkscape-v2"]) + .assert() + .success() + .stdout(diff(expected)); +} + #[test] fn test_spaces_find_command() { let testenv = TestEnv::new().install_default_cache(); diff --git a/tests/rendered/inkscape-with-title-no-color.expected b/tests/rendered/inkscape-with-title-no-color.expected new file mode 100644 index 0000000..b6a4572 --- /dev/null +++ b/tests/rendered/inkscape-with-title-no-color.expected @@ -0,0 +1,34 @@ + + inkscape + + An SVG (Scalable Vector Graphics) editing program. + Use -z to not open the GUI and only process files in the console. + + Open an SVG file in the Inkscape GUI: + + inkscape filename.svg + + Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI): + + inkscape filename.svg -e filename.png + + Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur): + + inkscape filename.svg -e filename.png -w 600 -h 400 + + Export a single object, given its ID, into a bitmap: + + inkscape filename.svg -i id -e object.png + + Export an SVG document to PDF, converting all texts to paths: + + inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path + + Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: + + inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit + + Some invalid command just to test the correct highlighting of the command name: + + inkscape --use-inkscape=v3.0 file + diff --git a/tests/rendered/inkscape-with-title.expected b/tests/rendered/inkscape-with-title.expected new file mode 100644 index 0000000..7e7d428 --- /dev/null +++ b/tests/rendered/inkscape-with-title.expected @@ -0,0 +1,34 @@ + + inkscape + + An SVG (Scalable Vector Graphics) editing program. + Use -z to not open the GUI and only process files in the console. + + Open an SVG file in the Inkscape GUI: + + inkscape filename.svg + + Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI): + + inkscape filename.svg -e filename.png + + Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur): + + inkscape filename.svg -e filename.png -w 600 -h 400 + + Export a single object, given its ID, into a bitmap: + + inkscape filename.svg -i id -e object.png + + Export an SVG document to PDF, converting all texts to paths: + + inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path + + Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: + + inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit + + Some invalid command just to test the correct highlighting of the command name: + + inkscape --use-inkscape=v3.0 file + From 911508ce33cf2d405e663de056bea4a483060efe Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Fri, 3 Oct 2025 20:15:34 +0200 Subject: [PATCH 16/45] Add `search.platforms` config option and search all platforms by default (#435) --- docs/src/config_search.md | 15 +++++++++ src/config.rs | 70 +++++++++++++++++++++++++++++++++------ src/main.rs | 27 ++++++--------- tests/lib.rs | 38 +++++++++++++++++++++ 4 files changed, 122 insertions(+), 28 deletions(-) diff --git a/docs/src/config_search.md b/docs/src/config_search.md index 2d59d65..d368159 100644 --- a/docs/src/config_search.md +++ b/docs/src/config_search.md @@ -12,3 +12,18 @@ Either way, the language used can be overwritten using the `--language` command [search] # Show pages in German if available, otherwise show in English languages = ["de", "en"] + +## `platforms` + +The list of platforms that should be considered when searching. +In addition to the platforms listed in the help text of the `--platform` flag, there are two special platforms available: +- `"current"`: equals the platform that tealdeer was compiled for +- `"all"`: adds all remaining platforms to the list + +Tealdeer searches the platforms in order of appearance in this list. +The default list of platforms is `["current", "common", "all"]`. +The list of platforms can be overwritten using the `--platform` command line flag. + + [search] + # Search for linux and common, and then search windows before trying the remaining platforms + platforms = ["linux", "common", "windows", "all"] diff --git a/src/config.rs b/src/config.rs index 446b841..e5e85e8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,11 +9,15 @@ use std::{ use anyhow::{anyhow, bail, ensure, Context, Result}; use app_dirs::{get_app_root, AppDataType}; +use clap::ValueEnum; use serde::Serialize as _; use serde_derive::{Deserialize, Serialize}; use yansi::{Color, Style}; -use crate::{extensions::Dedup as _, types::PathSource}; +use crate::{ + extensions::Dedup as _, + types::{PathSource, PlatformType}, +}; pub const CONFIG_FILE_NAME: &str = "config.toml"; pub const MAX_CACHE_AGE: Duration = Duration::from_secs(2_592_000); // 30 days @@ -217,9 +221,61 @@ struct RawDirectoriesConfig { pub custom_pages_dir: Option, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +enum RawPlatformType { + Current, + All, + MacOs, // alias for Platform(PlatformType::OsX) + #[serde(untagged)] + Platform(PlatformType), +} + +impl RawPlatformType { + pub fn flatten(raw_platforms: impl IntoIterator) -> Vec { + let mut flattened = Vec::new(); + for raw_platform in raw_platforms { + match raw_platform { + RawPlatformType::Current => flattened.push(PlatformType::current()), + RawPlatformType::Platform(platform) => flattened.push(platform), + RawPlatformType::MacOs => flattened.push(PlatformType::OsX), + RawPlatformType::All => flattened.extend(PlatformType::value_variants()), + } + } + flattened.clear_duplicates(); + flattened + } +} + #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] struct RawSearchConfig { pub languages: Option>, + pub platforms: Option>, +} + +impl<'a> From<&'a RawSearchConfig> for SearchConfig<'a> { + fn from(raw_search_config: &'a RawSearchConfig) -> Self { + let languages = raw_search_config + .languages + .as_ref() + .map_or_else(get_languages_from_env, |langs| { + langs.iter().map(|lang| Language(lang)).collect() + }); + let platforms = if let Some(raw_platforms) = raw_search_config.platforms.as_ref() { + RawPlatformType::flatten(raw_platforms.iter().copied()) + } else { + RawPlatformType::flatten([ + RawPlatformType::Current, + RawPlatformType::Platform(PlatformType::Common), + RawPlatformType::All, + ]) + }; + + Self { + languages, + platforms, + } + } } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -305,6 +361,7 @@ pub struct DirectoriesConfig { #[derive(Clone, Debug, PartialEq, Eq)] pub struct SearchConfig<'a> { pub languages: Vec>, + pub platforms: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -427,16 +484,7 @@ impl<'a> Config<'a> { fn from_raw(raw_config: &'a RawConfig, config_file_path: PathWithSource) -> Result { let style = (&raw_config.style).into(); let display = (&raw_config.display).into(); - - let search = SearchConfig { - languages: raw_config - .search - .languages - .as_ref() - .map_or_else(get_languages_from_env, |langs| { - langs.iter().map(|lang| Language(lang)).collect() - }), - }; + let search: SearchConfig<'a> = (&raw_config.search).into(); let updates = UpdatesConfig { auto_update: raw_config.updates.auto_update, diff --git a/src/main.rs b/src/main.rs index 2f730a4..fcf3273 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,6 +39,7 @@ use cache::{CacheConfig, TLDR_OLD_PAGES_DIR}; use clap::Parser; use config::{ConfigLoader, Language, StyleConfig, TlsBackend}; use log::debug; +use types::PlatformType; mod cache; mod cli; @@ -55,7 +56,7 @@ use crate::{ cli::Cli, config::{get_config_dir, make_default_config, Config, PathWithSource}, output::print_page, - types::{ColorOptions, PlatformType}, + types::ColorOptions, utils::{print_error, print_warning}, }; @@ -259,7 +260,13 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { return Ok(ExitCode::SUCCESS); } - let platforms = compute_platforms(args.platforms.as_ref()); + if let Some(platforms) = args.platforms { + config.search.platforms = platforms; + if !config.search.platforms.contains(&PlatformType::Common) { + config.search.platforms.push(PlatformType::Common); + } + } + let (search_languages, download_languages): (&[_], &[_]) = match args.language.as_deref() { Some(lang) => (&[Language(lang)], &[Language(lang)]), None => (&config.search.languages, &config.updates.download_languages), @@ -272,7 +279,7 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { .custom_pages_dir .as_ref() .map(PathWithSource::path), - platforms: &platforms, + platforms: &config.search.platforms, search_languages, download_languages, }; @@ -394,17 +401,3 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { Ok(ExitCode::SUCCESS) } - -/// Returns the passed or default platform types and appends `PlatformType::Common` as fallback. -fn compute_platforms(platforms: Option<&Vec>) -> Vec { - match platforms { - Some(p) => { - let mut result = p.clone(); - if !result.contains(&PlatformType::Common) { - result.push(PlatformType::Common); - } - result - } - None => vec![PlatformType::current(), PlatformType::Common], - } -} diff --git a/tests/lib.rs b/tests/lib.rs index ef5777d..cb987db 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -715,6 +715,36 @@ fn test_os_specific_page() { .success(); } +#[test] +fn test_config_platforms() { + let testenv = TestEnv::new(); + testenv.add_os_entry("sunos", "sunos-command", ""); + + let set_config_platforms = |platforms| { + testenv.delete_config(); + testenv.init_config(); + testenv.append_to_config(format!("search.platforms = {platforms}")); + }; + + // By default all platforms are searched + testenv.command().arg("sunos-command").assert().success(); + + set_config_platforms("[]"); + testenv.command().arg("sunos-command").assert().failure(); + + set_config_platforms("['linux']"); + testenv.command().arg("sunos-command").assert().failure(); + + set_config_platforms("['sunos']"); + testenv.command().arg("sunos-command").assert().success(); + + set_config_platforms("['linux', 'all']"); + testenv.command().arg("sunos-command").assert().success(); + + set_config_platforms("['current', 'all']"); + testenv.command().arg("sunos-command").assert().success(); +} + #[test] fn test_markdown_rendering() { let testenv = TestEnv::new().install_default_cache(); @@ -956,6 +986,14 @@ fn test_macos_is_alias_for_osx() { .args(["--platform", "osx", "--list"]) .assert() .stdout("maconly\n"); + + testenv.append_to_config("search.platforms = ['osx']\n"); + testenv.command().arg("--list").assert().stdout("maconly\n"); + + testenv.delete_config(); + testenv.init_config(); + testenv.append_to_config("search.platforms = ['macos']\n"); + testenv.command().arg("--list").assert().stdout("maconly\n"); } #[test] From 2b127fd67e7c0eef59efdff907dcf63da4dfcbe1 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Fri, 3 Oct 2025 20:59:22 +0200 Subject: [PATCH 17/45] Highlight code examples in user docs (#440) * Highlight code examples in user docs * Add missing quotes to archive_source example --- docs/src/config.md | 8 +++++-- docs/src/config_directories.md | 12 +++++++---- docs/src/config_display.md | 20 +++++++++++------ docs/src/config_search.md | 16 ++++++++------ docs/src/config_style.md | 12 ++++++++--- docs/src/config_updates.md | 39 +++++++++++++++++++++------------- docs/src/installing.md | 16 ++++++++++---- docs/src/usage_custom_pages.md | 16 ++++++++++---- 8 files changed, 94 insertions(+), 45 deletions(-) diff --git a/docs/src/config.md b/docs/src/config.md index b0bf922..a662b57 100644 --- a/docs/src/config.md +++ b/docs/src/config.md @@ -9,11 +9,15 @@ The configuration file path follows OS conventions (e.g. `$XDG_CONFIG_HOME/tealdeer/config.toml` on Linux). The paths can be queried with the following command: - $ tldr --show-paths +```shell +$ tldr --show-paths +``` Creating the config file can be done manually or with the help of `tldr`: - $ tldr --seed-config +```shell +$ tldr --seed-config +``` On Linux, this will usually be `~/.config/tealdeer/config.toml`. diff --git a/docs/src/config_directories.md b/docs/src/config_directories.md index b194dbb..507bd27 100644 --- a/docs/src/config_directories.md +++ b/docs/src/config_directories.md @@ -8,8 +8,10 @@ Override the cache directory. Remember to use an absolute path. Variable expansion will not be performed on the path. If the directory does not yet exist, it will be created. - [directories] - cache_dir = "/home/myuser/.tealdeer-cache/" +```toml +[directories] +cache_dir = "/home/myuser/.tealdeer-cache/" +``` If no `cache_dir` is specified, tealdeer will fall back to a location that follows OS conventions. On Linux, it will usually be at `~/.cache/tealdeer/`. @@ -21,5 +23,7 @@ Set the directory to be used to look up [custom pages](usage_custom_pages.html). Remember to use an absolute path. Variable expansion will not be performed on the path. - [directories] - custom_pages_dir = "/home/myuser/custom-tldr-pages/" +```toml +[directories] +custom_pages_dir = "/home/myuser/custom-tldr-pages/" +``` diff --git a/docs/src/config_display.md b/docs/src/config_display.md index 56b5cd3..78656a4 100644 --- a/docs/src/config_display.md +++ b/docs/src/config_display.md @@ -6,8 +6,10 @@ In the `display` section you can configure the output format. Specifies whether the pager should be used by default or not (default `false`). - [display] - use_pager = true +```toml +[display] +use_pager = true +``` When enabled, `less -R` is used as pager. To override the pager command used, set the `PAGER` environment variable. @@ -19,15 +21,19 @@ NOTE: This feature is not available on Windows. Set this to enforce more compact output, where empty lines are stripped out (default `false`). - [display] - compact = true +```toml +[display] +compact = true +``` ## `show_title` Display the command name at the top of the page output (default `false`). - [display] - show_title = true +```toml +[display] +show_title = true +``` When enabled, the command name will be displayed at the top of the output, -styled with the `command_name` style configuration. \ No newline at end of file +styled with the `command_name` style configuration. diff --git a/docs/src/config_search.md b/docs/src/config_search.md index d368159..28e00d6 100644 --- a/docs/src/config_search.md +++ b/docs/src/config_search.md @@ -9,9 +9,11 @@ The list of languages that should be considered when searching. If unspecified, the list of languages will be inferred from the `LANG` and `LANGUAGE` environment variables. Either way, the language used can be overwritten using the `--language` command line flag. - [search] - # Show pages in German if available, otherwise show in English - languages = ["de", "en"] +```toml +[search] +# Show pages in German if available, otherwise show in English +languages = ["de", "en"] +``` ## `platforms` @@ -24,6 +26,8 @@ Tealdeer searches the platforms in order of appearance in this list. The default list of platforms is `["current", "common", "all"]`. The list of platforms can be overwritten using the `--platform` command line flag. - [search] - # Search for linux and common, and then search windows before trying the remaining platforms - platforms = ["linux", "common", "windows", "all"] +```toml +[search] +# Search for linux and common, and then search windows before trying the remaining platforms +platforms = ["linux", "common", "windows", "all"] +``` diff --git a/docs/src/config_style.md b/docs/src/config_style.md index 190df4c..593a5b5 100644 --- a/docs/src/config_style.md +++ b/docs/src/config_style.md @@ -26,16 +26,22 @@ Colors can be specified in one of three ways: Example: - foreground = "green" + ```toml + foreground = "green" + ``` - 256 color ANSI code (*tealdeer v1.5.0+*) Example: - foreground = { ansi = 4 } + ```toml + foreground = { ansi = 4 } + ``` - 24-bit RGB color (*tealdeer v1.5.0+*) Example: - background = { rgb = { r = 255, g = 255, b = 255 } } + ```toml + background = { rgb = { r = 255, g = 255, b = 255 } } + ``` diff --git a/docs/src/config_updates.md b/docs/src/config_updates.md index cc5868e..9acba55 100644 --- a/docs/src/config_updates.md +++ b/docs/src/config_updates.md @@ -13,8 +13,10 @@ default. Specifies whether the auto-update feature should be enabled (defaults to `false`). - [updates] - auto_update = true +```toml +[updates] +auto_update = true +``` ### `auto_update_interval_hours` @@ -22,9 +24,11 @@ Duration, since the last cache update, after which the cache will be refreshed (defaults to 720 hours). This parameter is ignored if `auto_update` is set to `false`. - [updates] - auto_update = true - auto_update_interval_hours = 24 +```toml +[updates] +auto_update = true +auto_update_interval_hours = 24 +``` ## Download configuration @@ -35,20 +39,24 @@ If unspecified, the languages listed in the `search.languages` setting are used. Thus, this setting is the most useful to instruct tealdeer to download pages in additional languages that are not searched by default. Either way, the language used can be overwritten using the `--language` command line flag. - [search] - languages = ["de", "en"] +```toml +[search] +languages = ["de", "en"] - [updates] - # sometimes I like to read the Italian description - download_languages = ["de", "en", "it"] +[updates] +# sometimes I like to read the Italian description +download_languages = ["de", "en", "it"] +``` ### `archive_source` URL for the location of the tldr pages archive. By default the pages are fetched from the latest `tldr-pages/tldr` GitHub release. - [updates] - archive_source = https://my-company.example.com/tldr/ +```toml +[updates] +archive_source = "https://my-company.example.com/tldr/" +``` ### `tls_backend` @@ -62,9 +70,10 @@ Available options: - Secure Transport on macOS - OpenSSL on other platforms - [updates] - tls_backend = "native-tls" - +```toml +[updates] +tls_backend = "native-tls" +``` [rustls]: https://github.com/rustls/rustls [rustls-webpki]: https://github.com/rustls/webpki diff --git a/docs/src/installing.md b/docs/src/installing.md index 39de050..0bee062 100644 --- a/docs/src/installing.md +++ b/docs/src/installing.md @@ -38,21 +38,29 @@ Simply download the binary for your platform and run it! Build and install the tool via cargo... - $ cargo install tealdeer +```shell +$ cargo install tealdeer +``` ## Build From Source Release build: - $ cargo build --release +```shell +$ cargo build --release +``` Release build with bundled CA roots: - $ cargo build --release --no-default-features --features rustls-with-webpki-roots +```shell +$ cargo build --release --no-default-features --features rustls-with-webpki-roots +``` Debug build with logging support: - $ cargo build --features logging +```shell +$ cargo build --features logging +``` (To enable logging at runtime, export the `RUST_LOG=tldr=debug` env variable.) diff --git a/docs/src/usage_custom_pages.md b/docs/src/usage_custom_pages.md index d5d0f89..c73bf90 100644 --- a/docs/src/usage_custom_pages.md +++ b/docs/src/usage_custom_pages.md @@ -28,11 +28,15 @@ your custom page will be shown instead of the upstream version in the cache. Path: - $CUSTOM_PAGES_DIR/.page.md +```plain +$CUSTOM_PAGES_DIR/.page.md +``` Example: - ~/.local/share/tealdeer/pages/ufw.page.md +```plain +~/.local/share/tealdeer/pages/ufw.page.md +``` ## Custom Patches @@ -43,8 +47,12 @@ pages. Path: - $CUSTOM_PAGES_DIR/.patch.md +```plain +$CUSTOM_PAGES_DIR/.patch.md +``` Example: - ~/.local/share/tealdeer/pages/ufw.patch.md +```plain +~/.local/share/tealdeer/pages/ufw.patch.md +``` From 9a83b58d5110a6134546a9e1be227394c055caa2 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Fri, 3 Oct 2025 22:38:02 +0200 Subject: [PATCH 18/45] Bump MSRV to 1.85 and update dependencies (#441) The latest version of base64ct requires 1.85, and I don't want to think about whether older versions of crypto libraries are safe. --- .github/workflows/ci.yml | 2 +- Cargo.lock | 773 +++++++++++++++++++++++---------------- Cargo.toml | 4 +- 3 files changed, 463 insertions(+), 316 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f20709e..93b83e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - toolchain: [stable, 1.80.1] + toolchain: [stable, 1.85.0] include: - platform: windows-latest exe_suffix: .exe diff --git a/Cargo.lock b/Cargo.lock index 4da46ef..1ac11fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,12 +1,12 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" @@ -19,9 +19,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -34,43 +34,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "app_dirs2" @@ -78,7 +79,7 @@ version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7e7b35733e3a8c1ccb90385088dd5b6eaa61325cb4d1ad56e683b5224ff352e" dependencies = [ - "jni 0.21.1", + "jni", "ndk-context", "winapi", "xdg", @@ -86,18 +87,18 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] [[package]] name = "assert_cmd" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" dependencies = [ "anstyle", "bstr", @@ -111,9 +112,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" @@ -123,21 +124,21 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bstr" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", "regex-automata", @@ -146,22 +147,23 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" -version = "1.7.2" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.6" +version = "1.2.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -173,15 +175,15 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "clap" -version = "4.5.19" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", "clap_derive", @@ -189,9 +191,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstream", "anstyle", @@ -201,9 +203,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck", "proc-macro2", @@ -213,15 +215,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "combine" @@ -243,6 +245,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -251,24 +263,18 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "pem-rfc7468", "zeroize", @@ -276,9 +282,9 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", @@ -291,17 +297,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "doc-comment" version = "0.3.3" @@ -310,9 +305,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "env_filter" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", @@ -320,22 +315,22 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.5" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", - "humantime", + "jiff", "log", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" @@ -350,12 +345,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.9" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] @@ -370,49 +365,55 @@ dependencies = [ [[package]] name = "escargot" -version = "0.5.12" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c000f23e9d459aef148b7267e02b03b94a0aaacf4ec64c65612f67e02f525fb6" +checksum = "11c3aea32bc97b500c9ca6a72b768a26e558264303d101d3409cf6d57a9ed0cf" dependencies = [ "log", - "once_cell", "serde", "serde_json", ] [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] -name = "flate2" -version = "1.0.34" +name = "find-msvc-tools" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] [[package]] name = "float-cmp" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" dependencies = [ "num-traits", ] @@ -440,20 +441,32 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "heck" @@ -463,9 +476,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "1.1.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -474,21 +487,15 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "indexmap" -version = "2.6.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", "hashbrown", @@ -502,22 +509,32 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] -name = "jni" -version = "0.19.0" +name = "jiff" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ - "cesu8", - "combine", - "jni-sys", + "jiff-static", "log", - "thiserror 1.0.64", - "walkdir", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -531,7 +548,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror 1.0.64", + "thiserror", "walkdir", "windows-sys 0.45.0", ] @@ -544,15 +561,15 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags", "libc", @@ -560,43 +577,46 @@ dependencies = [ ] [[package]] -name = "linux-raw-sys" -version = "0.4.14" +name = "libz-rs-sys" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +dependencies = [ + "zlib-rs", +] [[package]] -name = "lockfree-object-pool" -version = "0.1.6" +name = "linux-raw-sys" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "log" -version = "0.4.26" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "native-tls" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", @@ -604,7 +624,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -621,25 +641,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -651,15 +652,21 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "openssl" -version = "0.10.71" +version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ "bitflags", "cfg-if", @@ -689,9 +696,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", @@ -720,21 +727,36 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] [[package]] name = "predicates" -version = "3.1.2" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "difflib", @@ -746,15 +768,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", @@ -762,36 +784,42 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] -name = "redox_syscall" -version = "0.5.7" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.11.0" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", @@ -801,9 +829,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", @@ -812,43 +840,42 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.16", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rustix" -version = "0.38.37" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", - "errno 0.3.9", + "errno 0.3.14", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] name = "rustls" -version = "0.23.23" +version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "log", "once_cell", @@ -861,15 +888,14 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", - "rustls-pemfile", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.5.1", ] [[package]] @@ -883,29 +909,32 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] [[package]] name = "rustls-platform-verifier" -version = "0.3.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbb878bdfdf63a336a5e63561b1835e7a8c91524f51621db870169eac84b490" +checksum = "be59af91596cac372a6942530653ad0c3a246cdd491aaa9dcaee47f88d67d5a0" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", - "jni 0.19.0", + "jni", "log", "once_cell", "rustls", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", - "security-framework", + "security-framework 3.5.1", "security-framework-sys", - "webpki-roots", - "winapi", + "webpki-root-certs", + "windows-sys 0.59.0", ] [[package]] @@ -916,9 +945,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "ring", "rustls-pki-types", @@ -927,9 +956,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -942,11 +971,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -956,18 +985,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", "core-foundation-sys", "libc", - "num-bigint", "security-framework-sys", ] [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -975,18 +1016,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.210" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -995,21 +1046,22 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -1026,12 +1078,6 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "subtle" version = "2.6.1" @@ -1040,9 +1086,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -1074,67 +1120,47 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ - "cfg-if", "fastrand", + "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] name = "terminal_size" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "termtree" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.64", -] - -[[package]] -name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", @@ -1143,9 +1169,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.19" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -1155,31 +1181,38 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] [[package]] -name = "unicode-ident" -version = "1.0.13" +name = "toml_write" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "untrusted" @@ -1189,9 +1222,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.0.8" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06f78313c985f2fba11100dd06d60dd402d0cabb458af4d94791b8e09c025323" +checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" dependencies = [ "base64", "der", @@ -1211,9 +1244,9 @@ dependencies = [ [[package]] name = "ureq-proto" -version = "0.3.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64adb55464bad1ab1aa9229133d0d59d2f679180f4d15f0d9debe616f541f25e" +checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" dependencies = [ "base64", "http", @@ -1241,9 +1274,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "wait-timeout" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] @@ -1260,24 +1293,42 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] [[package]] name = "webpki-root-certs" -version = "0.26.8" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09aed61f5e8d2c18344b3faa33a4c837855fe56642757754775548fee21386c4" +checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "0.26.8" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -1300,11 +1351,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -1313,6 +1364,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-sys" version = "0.45.0" @@ -1340,6 +1397,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.4", +] + +[[package]] +name = "windows-sys" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -1364,13 +1439,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -1383,6 +1475,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -1395,6 +1493,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -1407,12 +1511,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -1425,6 +1541,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -1437,6 +1559,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -1449,6 +1577,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -1462,14 +1596,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "0.6.20" +name = "windows_x86_64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "xdg" version = "2.5.2" @@ -1484,37 +1630,38 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zip" -version = "2.4.1" +version = "5.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938cc23ac49778ac8340e366ddc422b2227ea176edb447e23fc0627608dddadd" +checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532" dependencies = [ "arbitrary", "crc32fast", - "crossbeam-utils", - "displaydoc", "flate2", "indexmap", "memchr", - "thiserror 2.0.12", "zopfli", ] [[package]] -name = "zopfli" -version = "0.8.1" +name = "zlib-rs" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" dependencies = [ "bumpalo", "crc32fast", - "lockfree-object-pool", "log", - "once_cell", "simd-adler32", ] diff --git a/Cargo.toml b/Cargo.toml index 3447307..1123a69 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.7.2" include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"] -rust-version = "1.80" +rust-version = "1.85" edition = "2021" [[bin]] @@ -30,7 +30,7 @@ serde_derive = "1.0.21" ureq = { version = "3.0.8", default-features = false, features = ["gzip"] } toml = "0.8.19" yansi = "1" -zip = { version = "2.3.0", default-features = false, features = ["deflate"] } +zip = { version = "5.1.1", default-features = false, features = ["deflate"] } [target.'cfg(not(windows))'.dependencies] pager = "0.16" From 49626977ffc42b8445dddebde078028357ae3125 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Fri, 3 Oct 2025 22:44:39 +0200 Subject: [PATCH 19/45] Run `cargo +nightly clippy --fix` and formatting (#442) --- src/cache.rs | 5 +---- src/cli.rs | 2 +- src/formatter.rs | 2 +- src/line_iterator.rs | 4 ++-- src/main.rs | 1 + src/types.rs | 8 ++------ 6 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 23fd668..b181310 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -369,10 +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 55fe74f..d461a3e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; -use clap::{arg, builder::ArgAction, command, ArgGroup, Parser}; +use clap::{builder::ArgAction, ArgGroup, Parser}; use crate::types::{ColorOptions, PlatformType}; diff --git a/src/formatter.rs b/src/formatter.rs index d86de9b..5270124 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -68,7 +68,7 @@ where process_snippet(PageSnippet::Linebreak)?; } - LineType::Other(text) => debug!("Unknown line type: {:?}", text), + LineType::Other(text) => debug!("Unknown line type: {text:?}"), } } process_snippet(PageSnippet::Linebreak)?; diff --git a/src/line_iterator.rs b/src/line_iterator.rs index 2e11378..98088c0 100644 --- a/src/line_iterator.rs +++ b/src/line_iterator.rs @@ -53,7 +53,7 @@ impl Iterator for LineIterator { match bytes_read { Ok(0) => None, Err(e) => { - warn!("Could not read line from reader: {:?}", e); + warn!("Could not read line from reader: {e:?}"); None } Ok(_) => { @@ -68,7 +68,7 @@ impl Iterator for LineIterator { .find(|b| matches!(b, Ok(b'\n') | Err(_))) .transpose() { - warn!("Could not read line from reader: {:?}", e); + warn!("Could not read line from reader: {e:?}"); return None; } self.first_line = false; diff --git a/src/main.rs b/src/main.rs index fcf3273..a0cd593 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ #![allow(clippy::similar_names)] #![allow(clippy::struct_excessive_bools)] #![allow(clippy::too_many_lines)] +#![allow(clippy::unnecessary_debug_formatting)] #[cfg(not(any( feature = "native-tls", diff --git a/src/types.rs b/src/types.rs index 69c7599..7ca6e2d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -118,18 +118,14 @@ impl PlatformType { #[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize, clap::ValueEnum)] #[serde(rename_all = "lowercase")] +#[derive(Default)] pub enum ColorOptions { Always, + #[default] Auto, Never, } -impl Default for ColorOptions { - fn default() -> Self { - Self::Auto - } -} - #[derive(Debug, Eq, PartialEq)] pub enum LineType { Empty, From d49c4a9e05eab5018e322c03da01279040def538 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Fri, 3 Oct 2025 22:59:18 +0200 Subject: [PATCH 20/45] Release v1.8.0 --- CHANGELOG.md | 142 ++++++++++++++++++++++++++++++++++++----- Cargo.lock | 2 +- Cargo.toml | 2 +- docs/src/installing.md | 4 +- docs/src/usage.txt | 2 +- 5 files changed, 131 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c8af3a..a1c479f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,86 @@ Possible log types: - `[docs]` for documentation changes. - `[chore]` for maintenance work. +### [v1.8.0][v1.8.0] (2025-10-03) + +One year and one day have passed since tealdeer version 1.7.0 was released, so +it's time for an update! Tealdeer 1.8 comes with a complete rewrite of the page +cache and contains many long awaited improvements around it. + +Firstly, tealdeer now supports language-specific downloads. This means that only +the pages matching the configured languages are downloaded when updating the +cache. The languages used for searching pages can be configured separately to +the ones used for updating, so it is possible to download pages in languages +that are not usually queried. + +Next to configuring which languages are used for searching, it is now also +possible to specify which platforms are used in the config file. Importantly, +the default behavior for page search has changed so that all platforms are +searched if no page is found for the platform that tealdeer is running on. To +restore the behavior of tealdeer 1.7, users should set +```toml +[search] +platforms = ["current", "common"] +``` +in their config file. + +Coming back to updating, the default build configuration of tealdeer now +includes multiple TLS backends. This means that tealdeer does not have to be +rebuilt to try out a different TLS backend. The used backend can be chosen in +the config file. By default, tealdeer comes with support for rustls using webpki +certificates or system certificates. Native TLS is supported, but not enabled by +default to avoid build troubles with OpenSSL and musl. + +For details, please refer to the [user documentation]. + +#### Changes: + +- [added] Resolve paths in config `[directories]` relative to the config directory ([#306]) +- [added] Add `common` platform to CLI ([#401]) +- [added] Add configuration option for `archive_source` ([#337]) +- [added] Allows configuring TLS backend ([#386]) +- [added] Add args: `--edit-page` and `--edit-patch` ([#388]) +- [added] Add an option to specify a custom config file to be used ([#422]) +- [added] Upload binaries from build step as artifact ([#423]) +- [added] Add `search.languages` and `updates.download_languages` settings ([#430]) +- [added] Add `search.platforms` config option and search all platforms by default ([#435]) +- [added] Add `display.show_title` option to display command titles in output ([#439]) +- [chore] Various test improvements ([#399]) +- [chore] Add tests for osx/macos alias ([#407]) +- [chore] Move most of `main` to `try_main` ([#400]) +- [chore] Only create a single temporary directory in integration tests ([#411]) +- [chore] Replace reqwest with ureq ([#417]) +- [chore] Introduce Language struct ([#425]) +- [chore] Cache rewrite ([#416]) +- [chore] Allow references in `Config` ([#429]) +- [docs] Highlight code examples in user docs ([#440]) +- [removed] Remove native-tls from default feature set ([#436]) + +#### Contributors to this version: + +- [Christoph Loy][@beatbrot] +- [Erick Guan][@erickguan] +- [@MHS-0][@MHS-0] +- [Matěj Kafka][@MatejKafka] +- [Nachiket Kanore][@nachiketkanore] +- [Niklas Mohrin][@niklasmohrin] +- [Predrag Minic][@mipedja] +- [@hex1c][@hex1c] +- [lyj][@lengyijun] + +Thanks! + +#### Notes to package maintainers + +1. The MSRV has been bumped to 1.85. +2. Consider whether you want to include the `native-tls` feature in your build + of tealdeer. The feature is disabled for the binaries in the GitHub release + because we target musl, but it might work out of the box for your + distribution. +3. We have added the `ignore-online-tests` feature to automatically mark all + tests that require an internet connection as skipped, so you can use this + feature instead of maintaining a list of these tests yourself. + ### [v1.7.2][v1.7.2] (2025-03-18) This patch release updates the `zip` dependency to mitigate a potential security @@ -34,11 +114,11 @@ This patch release updates the `yansi` dependency to version 1, so that the previous versions of `yansi` can be removed from the package sets of Linux distributions. This change should not impact the behavior of tealdeer. -Changes: +#### Changes: - [chore] Upgrade yansi: 0.5.1 -> 1.0.1 ([#389]) -Contributors to this version: +#### Contributors to this version: - [Blair Noctis][@nc7s] @@ -74,7 +154,7 @@ On a personal note, this will be the last release from me ([Danilo](https://github.com/dbrgn/)) as primary maintainer of tealdeer. For details, see [#376](https://github.com/tealdeer-rs/tealdeer/issues/376). -Changes: +#### Changes: - [added] Allow querying multiple platforms ([#300]) - [added] Add BSD platform support ([#354]) @@ -94,7 +174,7 @@ Changes: - [chore] Update Cargo.toml license field following SPDX 2.1 ([#336]) - [chore] Dependency updates -Contributors to this version: +#### Contributors to this version: - [Adam Henley][@adamazing] - [Andrea Frigido][@frisoft] @@ -118,12 +198,12 @@ Thanks! ### [v1.6.1][v1.6.1] (2022-10-24) -Changes: +#### Changes: - [fixed] Fix path source for custom pages dir ([#297]) - [chore] Update dependendencies ([#299]) -Contributors to this version: +#### Contributors to this version: - [Cyrus Yip][@CyrusYip] - [Danilo Bargen][@dbrgn] @@ -142,7 +222,7 @@ The `TEALDEER_CACHE_DIR` env variable is now deprecated. A note to packagers: Shell completions have been moved to the `completion/` subdirectory! Packaging scripts might need to be updated. -Changes: +#### Changes: - [added] Allow overriding cache directory through config ([#276]) - [added] Add `--no-auto-update` CLI flag ([#257]) @@ -163,7 +243,7 @@ Changes: - [chore] Use anyhow for error handling ([#249]) - [chore] Switch to Rust 2021 edition ([#284]) -Contributors to this version: +#### Contributors to this version: - [@bagohart][@bagohart] - [@cyqsimon][@cyqsimon] @@ -212,7 +292,7 @@ Note that the MSRV (Minimal Supported Rust Version) of the project > When publishing a tealdeer release, the Rust version required to build it > should be stable for at least a month. -Changes: +#### Changes: - [added] Support custom pages and patches ([#142][i142]) - [added] Multi-language support ([#125][i125], [#161][i161]) @@ -243,7 +323,7 @@ Changes: - [chore] All release binaries are now generated in CI. Binaries for macOS and Windows are also provided. ([#240][i240]) - [chore] Update all dependencies -Contributors to this version: +#### Contributors to this version: - [@bl-ue][@bl-ue] - [Cameron Tod][@cam8001] @@ -272,7 +352,7 @@ co-maintainer. Thank you for your help! - [fixed] Syntax error in zsh completion file ([#138][i138]) -Contributors to this version: +#### Contributors to this version: - [Danilo Bargen][@dbrgn] - [Bruno A. Muciño][@mucinoab] @@ -289,7 +369,7 @@ Thanks! - [changed] Make `--list` option comply with official spec ([#112][i112]) - [changed] Move cache age warning to stderr ([#113][i113]) -Contributors to this version: +#### Contributors to this version: - [Atul Bhosale][@Atul9] - [Danilo Bargen][@dbrgn] @@ -315,7 +395,7 @@ Thanks! - [fixed] Fix Fish autocompletion on macOS ([#87][i87]) - [fixed] Fix compilation on Windows by disabling pager ([#99][i99]) -Contributors to this version: +#### Contributors to this version: - [Bruno Heridet][@Delapouite] - [Danilo Bargen][@dbrgn] @@ -341,7 +421,7 @@ Thanks! - [changed] Move to Rust 2018, require Rust 1.32 ([#69][i69] / [#84][i84]) - [fixed] Add (back) support for proxies ([#68][i68]) -Contributors to this version: +#### Contributors to this version: - [Bar Hatsor][@Bassets] - [Danilo Bargen][@dbrgn] @@ -364,7 +444,7 @@ Thanks! - [changed] Require at least Rust 1.28 to build (previous: 1.19) - [fixed] Fix building on systems with openssl 1.1.1 ([#47][i47]) -Contributors to this version: +#### Contributors to this version: - [Danilo Bargen][@dbrgn] - [@equal-l2][@equal-l2] @@ -397,7 +477,7 @@ Thanks! - First crates.io release - +[user documentation]: https://tealdeer-rs.github.io/tealdeer/ [@0ndorio]: https://github.com/0ndorio [@adamazing]: https://github.com/adamazing @@ -460,6 +540,14 @@ Thanks! [@Walker-00]: https://github.com/Walker-00 [@YDX-2147483647]: https://github.com/YDX-2147483647 [@zedseven]: https://github.com/zedseven +[@beatbrot]: https://github.com/beatbrot +[@erickguan]: https://github.com/erickguan +[@MHS-0]: https://github.com/MHS-0 +[@MatejKafka]: https://github.com/MatejKafka +[@nachiketkanore]: https://github.com/nachiketkanore +[@mipedja]: https://github.com/mipedja +[@hex1c]: https://github.com/hex1c +[@lengyijun]: https://github.com/lengyijun [v1.0.0]: https://github.com/tealdeer-rs/tealdeer/compare/v0.4.0...v1.0.0 [v1.1.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.0.0...v1.1.0 @@ -473,6 +561,7 @@ Thanks! [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.8.0]: https://github.com/tealdeer-rs/tealdeer/compare/v1.7.2...v1.8.0 [i34]: https://github.com/tealdeer-rs/tealdeer/issues/34 [i43]: https://github.com/tealdeer-rs/tealdeer/issues/43 @@ -544,6 +633,7 @@ Thanks! [#300]: https://github.com/tealdeer-rs/tealdeer/pull/300 [#303]: https://github.com/tealdeer-rs/tealdeer/pull/303 [#305]: https://github.com/tealdeer-rs/tealdeer/pull/305 +[#306]: https://github.com/tealdeer-rs/tealdeer/pull/306 [#314]: https://github.com/tealdeer-rs/tealdeer/pull/314 [#315]: https://github.com/tealdeer-rs/tealdeer/pull/315 [#322]: https://github.com/tealdeer-rs/tealdeer/pull/322 @@ -552,8 +642,28 @@ Thanks! [#331]: https://github.com/tealdeer-rs/tealdeer/pull/331 [#333]: https://github.com/tealdeer-rs/tealdeer/pull/333 [#336]: https://github.com/tealdeer-rs/tealdeer/pull/336 +[#337]: https://github.com/tealdeer-rs/tealdeer/pull/337 [#342]: https://github.com/tealdeer-rs/tealdeer/pull/342 [#354]: https://github.com/tealdeer-rs/tealdeer/pull/354 [#355]: https://github.com/tealdeer-rs/tealdeer/pull/355 [#362]: https://github.com/tealdeer-rs/tealdeer/pull/362 +[#386]: https://github.com/tealdeer-rs/tealdeer/pull/386 +[#388]: https://github.com/tealdeer-rs/tealdeer/pull/388 [#389]: https://github.com/tealdeer-rs/tealdeer/pull/389 +[#399]: https://github.com/tealdeer-rs/tealdeer/pull/399 +[#400]: https://github.com/tealdeer-rs/tealdeer/pull/400 +[#401]: https://github.com/tealdeer-rs/tealdeer/pull/401 +[#407]: https://github.com/tealdeer-rs/tealdeer/pull/407 +[#411]: https://github.com/tealdeer-rs/tealdeer/pull/411 +[#416]: https://github.com/tealdeer-rs/tealdeer/pull/416 +[#417]: https://github.com/tealdeer-rs/tealdeer/pull/417 +[#422]: https://github.com/tealdeer-rs/tealdeer/pull/422 +[#423]: https://github.com/tealdeer-rs/tealdeer/pull/423 +[#425]: https://github.com/tealdeer-rs/tealdeer/pull/425 +[#426]: https://github.com/tealdeer-rs/tealdeer/pull/426 +[#429]: https://github.com/tealdeer-rs/tealdeer/pull/429 +[#430]: https://github.com/tealdeer-rs/tealdeer/pull/430 +[#435]: https://github.com/tealdeer-rs/tealdeer/pull/435 +[#436]: https://github.com/tealdeer-rs/tealdeer/pull/436 +[#439]: https://github.com/tealdeer-rs/tealdeer/pull/439 +[#440]: https://github.com/tealdeer-rs/tealdeer/pull/440 diff --git a/Cargo.lock b/Cargo.lock index 1ac11fe..661c998 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1097,7 +1097,7 @@ dependencies = [ [[package]] name = "tealdeer" -version = "1.7.2" +version = "1.8.0" dependencies = [ "anyhow", "app_dirs2", diff --git a/Cargo.toml b/Cargo.toml index 1123a69..44c13ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ name = "tealdeer" readme = "README.md" repository = "https://github.com/tealdeer-rs/tealdeer/" documentation = "https://tealdeer-rs.github.io/tealdeer/" -version = "1.7.2" +version = "1.8.0" include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"] rust-version = "1.85" edition = "2021" diff --git a/docs/src/installing.md b/docs/src/installing.md index 0bee062..f0ca823 100644 --- a/docs/src/installing.md +++ b/docs/src/installing.md @@ -50,10 +50,10 @@ Release build: $ cargo build --release ``` -Release build with bundled CA roots: +Release build with native TLS support: ```shell -$ cargo build --release --no-default-features --features rustls-with-webpki-roots +$ cargo build --release --features native-tls ``` Debug build with logging support: diff --git a/docs/src/usage.txt b/docs/src/usage.txt index 33e6020..7618ac4 100644 --- a/docs/src/usage.txt +++ b/docs/src/usage.txt @@ -1,4 +1,4 @@ -tealdeer 1.7.2: A fast TLDR client +tealdeer 1.8.0: A fast TLDR client Danilo Bargen , Niklas Mohrin Usage: tldr [OPTIONS] [COMMAND]... From 6c1d7027696530f30d5ce47f57a8b6ad1e6552d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 00:18:45 +0100 Subject: [PATCH 21/45] Bump actions/download-artifact from 5 to 6 (#448) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 360dd94..ee63de1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -135,7 +135,7 @@ jobs: - windows-x86_64-msvc steps: - uses: actions/checkout@v5 - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 - name: Upload binary if: startsWith(github.ref, 'refs/tags/') run: | From e1213158e45679680540eaec32648ca19d704393 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 00:19:23 +0100 Subject: [PATCH 22/45] Bump actions/upload-artifact from 4 to 5 (#447) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93b83e4..2fc4485 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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@v4 + - uses: actions/upload-artifact@v5 with: name: tldr-debug-build-${{ matrix.platform }}-rust-${{ matrix.toolchain }} path: artifacts/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee63de1..539f2ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,7 +73,7 @@ jobs: 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@v4 + - uses: actions/upload-artifact@v5 with: name: "tealdeer-linux-${{ matrix.arch }}-${{ matrix.libc }}" path: "target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr" @@ -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@v4 + - uses: actions/upload-artifact@v5 with: name: "tealdeer-macos-${{ matrix.arch }}" path: "target/${{ matrix.arch }}-apple-darwin/release/tldr" @@ -109,7 +109,7 @@ jobs: toolchain: stable - name: Build run: cargo build --release --target x86_64-pc-windows-msvc - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: "tealdeer-windows-x86_64-msvc" path: "target/x86_64-pc-windows-msvc/release/tldr.exe" From e769114d8b2dd35096dd00a0bf3e7584a57815fb Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Tue, 11 Nov 2025 17:33:19 +0100 Subject: [PATCH 23/45] Enable ureq's socks-proxy feature (#451) --- Cargo.lock | 18 ++++++++++++++++++ Cargo.toml | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 661c998..dbd9a3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,12 @@ 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" @@ -1078,6 +1084,17 @@ 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" @@ -1236,6 +1253,7 @@ dependencies = [ "rustls-pemfile", "rustls-pki-types", "rustls-platform-verifier", + "socks", "ureq-proto", "utf-8", "webpki-root-certs", diff --git a/Cargo.toml b/Cargo.toml index 44c13ef..e9aff22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ env_logger = { version = "0.11", optional = true } log = "0.4" serde = "1.0.21" serde_derive = "1.0.21" -ureq = { version = "3.0.8", default-features = false, features = ["gzip"] } +ureq = { version = "3.0.8", default-features = false, features = ["gzip", "socks-proxy"] } toml = "0.8.19" yansi = "1" zip = { version = "5.1.1", default-features = false, features = ["deflate"] } From b3cd7b1c216656ea3416a86104363589157477dc Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Tue, 11 Nov 2025 22:27:52 +0100 Subject: [PATCH 24/45] Release v1.8.1 --- CHANGELOG.md | 12 ++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- docs/src/usage.txt | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1c479f..5a80bbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,16 @@ Possible log types: - `[docs]` for documentation changes. - `[chore]` for maintenance work. +### [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 @@ -562,6 +572,7 @@ Thanks! [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.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 @@ -667,3 +678,4 @@ 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 diff --git a/Cargo.lock b/Cargo.lock index dbd9a3f..be9bd24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1114,7 +1114,7 @@ dependencies = [ [[package]] name = "tealdeer" -version = "1.8.0" +version = "1.8.1" dependencies = [ "anyhow", "app_dirs2", diff --git a/Cargo.toml b/Cargo.toml index e9aff22..aa7ae49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ name = "tealdeer" readme = "README.md" repository = "https://github.com/tealdeer-rs/tealdeer/" documentation = "https://tealdeer-rs.github.io/tealdeer/" -version = "1.8.0" +version = "1.8.1" include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"] rust-version = "1.85" edition = "2021" diff --git a/docs/src/usage.txt b/docs/src/usage.txt index 7618ac4..f7fcc46 100644 --- a/docs/src/usage.txt +++ b/docs/src/usage.txt @@ -1,4 +1,4 @@ -tealdeer 1.8.0: A fast TLDR client +tealdeer 1.8.1: A fast TLDR client Danilo Bargen , Niklas Mohrin Usage: tldr [OPTIONS] [COMMAND]... From c5d62e5987b38705814b72354373c50fe165dbb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 20:32:38 +0100 Subject: [PATCH 25/45] Bump actions/checkout from 5 to 6 (#454) Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/gh-pages.yml | 2 +- .github/workflows/release.yml | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2fc4485..b557674 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: exe_suffix: .exe runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} @@ -50,7 +50,7 @@ jobs: name: run clippy lints runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -62,7 +62,7 @@ jobs: name: run rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -74,7 +74,7 @@ jobs: name: build docs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup mdBook uses: peaceiris/actions-mdbook@v2 with: diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 8746d42..f841705 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -8,7 +8,7 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup mdBook uses: peaceiris/actions-mdbook@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 539f2ab..e48fc17 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@v5 + - uses: actions/checkout@v6 - 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@v5 + - uses: actions/checkout@v6 - name: Upload completion if: startsWith(github.ref, 'refs/tags/') run: | @@ -40,7 +40,7 @@ jobs: matrix: target: ["MIT", "APACHE"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Upload license if: startsWith(github.ref, 'refs/tags/') run: | @@ -66,7 +66,7 @@ jobs: - arch: "arm" libc: "musleabihf" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Pull Docker image run: docker pull messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} - name: Build in Docker @@ -86,7 +86,7 @@ jobs: - arch: "x86_64" - arch: "aarch64" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: @@ -102,7 +102,7 @@ jobs: build-windows: runs-on: windows-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: @@ -134,7 +134,7 @@ jobs: - macos-aarch64 - windows-x86_64-msvc steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/download-artifact@v6 - name: Upload binary if: startsWith(github.ref, 'refs/tags/') From 3a6fd99c859a28c1913963d8d25303b43d5fcce0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:55:41 +0100 Subject: [PATCH 26/45] Bump actions/download-artifact from 6 to 7 (#457) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e48fc17..1d978e6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -135,7 +135,7 @@ jobs: - windows-x86_64-msvc steps: - uses: actions/checkout@v6 - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 - name: Upload binary if: startsWith(github.ref, 'refs/tags/') run: | From 5ee1f28021a563ab9d2ec06cc9d07c80254164a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:55:53 +0100 Subject: [PATCH 27/45] Bump actions/upload-artifact from 5 to 6 (#456) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b557674..a0b20ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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@v5 + - uses: actions/upload-artifact@v6 with: name: tldr-debug-build-${{ matrix.platform }}-rust-${{ matrix.toolchain }} path: artifacts/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1d978e6..9813f00 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,7 +73,7 @@ jobs: 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@v5 + - uses: actions/upload-artifact@v6 with: name: "tealdeer-linux-${{ matrix.arch }}-${{ matrix.libc }}" path: "target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr" @@ -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@v5 + - uses: actions/upload-artifact@v6 with: name: "tealdeer-macos-${{ matrix.arch }}" path: "target/${{ matrix.arch }}-apple-darwin/release/tldr" @@ -109,7 +109,7 @@ jobs: toolchain: stable - name: Build run: cargo build --release --target x86_64-pc-windows-msvc - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: "tealdeer-windows-x86_64-msvc" path: "target/x86_64-pc-windows-msvc/release/tldr.exe" From 75e5462312e91cd55831067219560dedb234473f Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Sun, 25 Jan 2026 18:13:31 +0100 Subject: [PATCH 28/45] Update CHANGELOG.md --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a80bbb..644a30a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,46 @@ 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 @@ -566,11 +606,14 @@ 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 From 8b97afe7aa305feaff1a31e6364eae42aa30f439 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Sun, 25 Jan 2026 18:39:52 +0100 Subject: [PATCH 29/45] Add workflow_dispatch trigger for GitHub Pages workflow --- .github/workflows/gh-pages.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index f841705..f8b6f6e 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -3,6 +3,7 @@ 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: From 593e9309b9a78dfbc5a1ad5db9d5982b817151be Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Fri, 20 Feb 2026 23:44:36 +0100 Subject: [PATCH 30/45] Placeholder escaping (#414) Closes #402 This adds special handling for escaped placeholders as required by the client spec. The added tests include examples from current pages that rely on this behavior. The text replacements use `str::replace` which constructs a new allocated `String`. I considered using a custom `replace_inplace` method on `&mut str` (which works if the replacement string is at most as long as the pattern to be replaced), but decided against it because I think that the performance improvement is not significant enough to justify adding `unsafe` code. It is also possible to avoid `unsafe` by re-checking UTF-8 validity after all modifications, but the code still felt a bit out of place for tealdeer. We can always add these optimizations later if we want to. --- src/formatter.rs | 361 ++++++++++++++++++++++++++++++++++++----------- src/output.rs | 4 +- 2 files changed, 278 insertions(+), 87 deletions(-) diff --git a/src/formatter.rs b/src/formatter.rs index 5270124..1b504dd 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -4,19 +4,52 @@ use log::debug; use crate::{extensions::FindFrom, types::LineType}; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Eq)] /// Represents a snippet from a page of a specific highlighting class. -pub enum PageSnippet<'a> { - CommandName(&'a str), - Variable(&'a str), - NormalCode(&'a str), - Description(&'a str), - Text(&'a str), - Title(&'a str), +pub enum PageSnippet { + CommandName(T), + Variable(T), + NormalCode(T), + Description(T), + Text(T), + Title(T), Linebreak, } -impl PageSnippet<'_> { +#[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> { pub fn is_empty(&self) -> bool { use PageSnippet::*; @@ -38,7 +71,7 @@ pub fn highlight_lines( ) -> Result<(), E> where L: Iterator, - F: for<'snip> FnMut(PageSnippet<'snip>) -> Result<(), E>, + F: for<'snip> FnMut(PageSnippet<&'snip str>) -> Result<(), E>, { let mut command = String::new(); for line in lines { @@ -75,29 +108,82 @@ where Ok(()) } -/// 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>, +/// 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>, ) -> Result<(), E> { - 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))?; + // 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..]; } + + 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(); + &s[prefix_start..=marker_index] == forbidden_prefix + }; + 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>) -> Result<(), E>, + process_snippet: &mut impl FnMut(PageSnippet<&'a str>) -> Result<(), E>, ) -> Result<(), E> { if !command_name.is_empty() { let mut search_start = 0; @@ -140,7 +226,6 @@ fn is_freestanding_substring(surrounding: &str, substring: (usize, usize)) -> bo #[cfg(test)] mod tests { use super::*; - use PageSnippet::*; #[test] fn test_is_freestanding_substring() { @@ -167,80 +252,186 @@ 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<'a>| { + let mut process_snippet = |snip: PageSnippet<&str>| { if !snip.is_empty() { - yielded.push(snip); + yielded.push(snip.map(str::to_string)); } Ok::<(), ()>(()) }; - highlight_code_segment(cmd, segment, &mut process_snippet) - .expect("highlight code segment failed"); + highlight_code(cmd, segment, &mut process_snippet).expect("highlight code segment failed"); yielded } - #[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 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_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") - ], - ); - } + mod placeholders { + use super::*; + use PageSnippet::*; - #[test] - fn test_empty_command() { - let segment = "some code"; - let snippets = [NormalCode(segment)]; + #[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:"), + ], + ); - assert_eq!(run("", segment), snippets); - assert_eq!(run(" ", segment), snippets); - assert_eq!(run(" \t ", segment), snippets); + 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}}"), + ], + ); + } } } diff --git a/src/output.rs b/src/output.rs index 5f1aeae..927d20e 100644 --- a/src/output.rs +++ b/src/output.rs @@ -56,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<'_>| { + let mut process_snippet = |snip: PageSnippet<&str>| { if snip.is_empty() { Ok(()) } else { @@ -82,7 +82,7 @@ pub fn print_page( fn print_snippet( writer: &mut impl Write, - snip: PageSnippet<'_>, + snip: PageSnippet<&str>, style: &StyleConfig, ) -> io::Result<()> { use PageSnippet::*; From 47a936e7363ca2afd6a8513862cceccf044d6bf8 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Sat, 21 Feb 2026 00:32:23 +0100 Subject: [PATCH 31/45] Suggest trying different TLS backend when update fails (#465) Closes #453 - Add note about changing tls_backend setting - impl Display for TlsBackend - Remove trailing slash in default archive source to make URL in error look nicer --- src/config.rs | 31 +++++++++++++++++++++++++++++-- src/main.rs | 32 +++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/config.rs b/src/config.rs index e5e85e8..2c39235 100644 --- a/src/config.rs +++ b/src/config.rs @@ -31,6 +31,14 @@ const SUPPORTED_TLS_BACKENDS: &[RawTlsBackend] = &[ RawTlsBackend::RustlsWithNativeRoots, ]; +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 } @@ -184,7 +192,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() + "https://github.com/tldr-pages/tldr/releases/latest/download".to_owned() } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -460,12 +468,31 @@ impl TryFrom for TlsBackend { _ => Err(anyhow!( "Unsupported TLS backend: {}. This tealdeer build has support for the following options: {}", raw, - SUPPORTED_TLS_BACKENDS.iter().map(std::string::ToString::to_string).collect::>().join(", ") + supported_tls_backends_string(), )) } } } +impl TlsBackend { + const fn as_raw(self) -> RawTlsBackend { + match self { + #[cfg(feature = "native-tls")] + Self::NativeTls => RawTlsBackend::NativeTls, + #[cfg(feature = "rustls-with-webpki-roots")] + Self::RustlsWithWebpkiRoots => RawTlsBackend::RustlsWithWebpkiRoots, + #[cfg(feature = "rustls-with-native-roots")] + Self::RustlsWithNativeRoots => RawTlsBackend::RustlsWithNativeRoots, + } + } +} + +impl fmt::Display for TlsBackend { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_raw().fmt(f) + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct Config<'a> { pub style: StyleConfig, diff --git a/src/main.rs b/src/main.rs index a0cd593..1d1b5fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,7 +55,9 @@ mod utils; use crate::{ cache::{Cache, PageLookupResult, TLDR_PAGES_DIR}, cli::Cli, - config::{get_config_dir, make_default_config, Config, PathWithSource}, + config::{ + get_config_dir, make_default_config, supported_tls_backends_string, Config, PathWithSource, + }, output::print_page, types::ColorOptions, utils::{print_error, print_warning}, @@ -305,12 +307,36 @@ 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 { - update_cache( + let result = update_cache( &mut cache, config.updates.archive_source, config.updates.tls_backend, args.quiet, - )?; + ); + + if let Err(e) = result { + print_error(enable_styles, &e); + + eprintln!(); + eprintln!("Note: Update errors are often caused by unexpected or missing TLS certificates."); + eprintln!( + "You are currently using the following TLS backend: {}", + config.updates.tls_backend, + ); + eprintln!( + "Try changing the updates.tls_backend setting in the config file, for example:" + ); + eprintln!(); + eprintln!(" [updates]"); + eprintln!(" tls_backend = \"rustls-with-native-roots\""); + eprintln!(); + eprintln!( + "This build of tealdeer has support for the following options: {}", + supported_tls_backends_string(), + ); + + return Ok(ExitCode::FAILURE); + } } cache From 41739c5bf9599d71f6ee1d35f0dab4a4e8ce580c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:52:53 +0100 Subject: [PATCH 32/45] Bump actions/upload-artifact from 6 to 7 (#468) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.

Release notes

Sourced from actions/upload-artifact's releases.

v7.0.0

v7 What's new

Direct Uploads

Adds support for uploading single files directly (unzipped). Callers can set the new archive parameter to false to skip zipping the file during upload. Right now, we only support single files. The action will fail if the glob passed resolves to multiple files. The name parameter is also ignored with this setting. Instead, the name of the artifact will be the name of the uploaded file.

ESM

To support new versions of the @actions/* packages, we've upgraded the package to ESM.

What's Changed

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v6...v7.0.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=6&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0b20ce..a17ba90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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@v6 + - uses: actions/upload-artifact@v7 with: name: tldr-debug-build-${{ matrix.platform }}-rust-${{ matrix.toolchain }} path: artifacts/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9813f00..316208e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,7 +73,7 @@ jobs: 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@v6 + - uses: actions/upload-artifact@v7 with: name: "tealdeer-linux-${{ matrix.arch }}-${{ matrix.libc }}" path: "target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/tldr" @@ -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@v6 + - uses: actions/upload-artifact@v7 with: name: "tealdeer-macos-${{ matrix.arch }}" path: "target/${{ matrix.arch }}-apple-darwin/release/tldr" @@ -109,7 +109,7 @@ jobs: toolchain: stable - name: Build run: cargo build --release --target x86_64-pc-windows-msvc - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: "tealdeer-windows-x86_64-msvc" path: "target/x86_64-pc-windows-msvc/release/tldr.exe" From 6f91c3a765513c1030723e2ab96a8c6d5ea1dca0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:53:06 +0100 Subject: [PATCH 33/45] Bump actions/download-artifact from 7 to 8 (#469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
Release notes

Sourced from actions/download-artifact's releases.

v8.0.0

v8 - What's new

Direct downloads

To support direct uploads in actions/upload-artifact, the action will no longer attempt to unzip all downloaded files. Instead, the action checks the Content-Type header ahead of unzipping and skips non-zipped files. Callers wishing to download a zipped file as-is can also set the new skip-decompress parameter to false.

Enforced checks (breaking)

A previous release introduced digest checks on the download. If a download hash didn't match the expected hash from the server, the action would log a warning. Callers can now configure the behavior on mismatch with the digest-mismatch parameter. To be secure by default, we are now defaulting the behavior to error which will fail the workflow run.

ESM

To support new versions of the @actions/* packages, we've upgraded the package to ESM.

What's Changed

Full Changelog: https://github.com/actions/download-artifact/compare/v7...v8.0.0

Commits
  • 70fc10c Merge pull request #461 from actions/danwkennedy/digest-mismatch-behavior
  • f258da9 Add change docs
  • ccc058e Fix linting issues
  • bd7976b Add a setting to specify what to do on hash mismatch and default it to error
  • ac21fcf Merge pull request #460 from actions/danwkennedy/download-no-unzip
  • 15999bf Add note about package bumps
  • 974686e Bump the version to v8 and add release notes
  • fbe48b1 Update test names to make it clearer what they do
  • 96bf374 One more test fix
  • b8c4819 Fix skip decompress test
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=7&new-version=8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 316208e..41de665 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -135,7 +135,7 @@ jobs: - windows-x86_64-msvc steps: - uses: actions/checkout@v6 - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 - name: Upload binary if: startsWith(github.ref, 'refs/tags/') run: | From b19517097ae0a5f9015794030b782ae9fd54535c Mon Sep 17 00:00:00 2001 From: Pavel Timofeev Date: Mon, 13 Apr 2026 18:31:31 -0400 Subject: [PATCH 34/45] Add builtin `tldr tealdeer` page (#472) Fixes #218. --- docs/src/usage.txt | 2 ++ pages/tealdeer.md | 42 ++++++++++++++++++++++++++++++++++++++++++ src/cache.rs | 10 +++++----- src/cli.rs | 4 +++- src/main.rs | 29 ++++++++++++++++++++++++----- src/output.rs | 8 +++----- tests/lib.rs | 10 ++++++++++ 7 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 pages/tealdeer.md diff --git a/docs/src/usage.txt b/docs/src/usage.txt index f7fcc46..6a04de7 100644 --- a/docs/src/usage.txt +++ b/docs/src/usage.txt @@ -29,3 +29,5 @@ 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 new file mode 100644 index 0000000..948b278 --- /dev/null +++ b/pages/tealdeer.md @@ -0,0 +1,42 @@ +# 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 b181310..a77036d 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,6 +1,6 @@ use std::{ fs::{self, File}, - io::{BufReader, Cursor, ErrorKind, Read}, + io::{Cursor, ErrorKind, Read}, path::{Path, PathBuf}, time::{Duration, SystemTime}, }; @@ -277,12 +277,12 @@ impl PageLookupResult { self } - /// Create a buffered reader that sequentially reads from the page and the + /// Create a reader that sequentially reads from the page and the /// patch, as if they were concatenated. /// /// 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(BufReader::new(if let Some(patch_file) = patch_file_opt { + Ok(if let Some(patch_file) = patch_file_opt { Box::new(page_file.chain(&b"\n"[..]).chain(patch_file)) as Box } else { Box::new(page_file) as Box - })) + }) } } diff --git a/src/cli.rs b/src/cli.rs index d461a3e..161d69d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -18,7 +18,9 @@ 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/.", + 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.", arg_required_else_help = true, help_expected = true, group = ArgGroup::new("command_or_file").args(&["command", "render"]), diff --git a/src/main.rs b/src/main.rs index 1d1b5fc..5b12613 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,6 +68,8 @@ const APP_INFO: AppInfo = AppInfo { name: NAME, author: NAME, }; +static TEALDEER_PAGE: &str = + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/pages/tealdeer.md")); /// Clear the cache fn clear_cache(cache: Cache, quietly: bool) -> Result<()> { @@ -258,8 +260,20 @@ 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 path = PageLookupResult::with_page(file); - print_page(&path, args.raw, enable_styles, args.pager, &config)?; + 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, + )?; return Ok(ExitCode::SUCCESS); } @@ -407,7 +421,7 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { ); } - let Some(lookup_result) = cache.find_page(&command) else { + let Some(result) = cache.find_page(&command) else { if !args.quiet { print_warning( enable_styles, @@ -419,11 +433,16 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { ), ); } - return Ok(ExitCode::FAILURE); }; - print_page(&lookup_result, args.raw, enable_styles, args.pager, &config)?; + print_page( + result.reader()?, + args.raw, + enable_styles, + args.pager, + &config, + )?; } Ok(ExitCode::SUCCESS) diff --git a/src/output.rs b/src/output.rs index 927d20e..9305c20 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,12 +1,11 @@ //! Functions for printing pages to the terminal -use std::io::{self, BufRead, Write}; +use std::io::{self, BufRead, BufReader, Read, Write}; use anyhow::{Context, Result}; use yansi::Paint; use crate::{ - cache::PageLookupResult, config::{Config, StyleConfig}, formatter::{highlight_lines, PageSnippet}, line_iterator::LineIterator, @@ -30,14 +29,13 @@ fn configure_pager(enable_styles: bool) { /// Print page by path pub fn print_page( - lookup_result: &PageLookupResult, + reader: impl Read, enable_markdown: bool, enable_styles: bool, use_pager: bool, config: &Config, ) -> Result<()> { - // Create reader from file(s) - let reader = lookup_result.reader()?; + let reader = BufReader::new(reader); // Configure pager if applicable if use_pager || config.display.use_pager { diff --git a/tests/lib.rs b/tests/lib.rs index cb987db..40cb7e9 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -301,6 +301,16 @@ 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() { From b8f7c0cc2d96a5d2c6be4c374b4893285bbf6e39 Mon Sep 17 00:00:00 2001 From: Pavel Timofeev Date: Fri, 17 Apr 2026 15:59:34 -0400 Subject: [PATCH 35/45] Add `display.indent` config option (#471) Fixes #394. --- docs/src/config_display.md | 32 ++++++++++++++++ src/config.rs | 38 +++++++++++++++++++ src/formatter.rs | 21 ++++++++-- src/output.rs | 7 ++-- tests/lib.rs | 18 +++++++++ tests/rendered/apt.ja.expected | 16 ++++---- .../inkscape-compact-no-color.expected | 32 ++++++++++++++++ tests/rendered/inkscape-default.expected | 14 +++---- tests/rendered/inkscape-with-config.expected | 14 +++---- tests/rendered/inkscape-with-title.expected | 16 ++++---- 10 files changed, 171 insertions(+), 37 deletions(-) create mode 100644 tests/rendered/inkscape-compact-no-color.expected diff --git a/docs/src/config_display.md b/docs/src/config_display.md index 78656a4..007d64b 100644 --- a/docs/src/config_display.md +++ b/docs/src/config_display.md @@ -37,3 +37,35 @@ 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/src/config.rs b/src/config.rs index 2c39235..b31f422 100644 --- a/src/config.rs +++ b/src/config.rs @@ -43,6 +43,14 @@ fn default_underline() -> bool { false } +const fn default_base_indent() -> usize { + 2 +} + +const fn default_command_indent() -> usize { + 6 +} + fn default_bold() -> bool { false } @@ -171,6 +179,25 @@ 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 { @@ -179,6 +206,10 @@ 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, + }, } } } @@ -331,6 +362,13 @@ 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)] diff --git a/src/formatter.rs b/src/formatter.rs index 1b504dd..1cdc681 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -2,7 +2,7 @@ use log::debug; -use crate::{extensions::FindFrom, types::LineType}; +use crate::{config::Indent, extensions::FindFrom, types::LineType}; #[derive(Debug, Clone, Copy, Eq)] /// Represents a snippet from a page of a specific highlighting class. @@ -68,11 +68,14 @@ 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>, { + let base_indent = " ".repeat(indent.base); + let command_indent = " ".repeat(indent.command); let mut command = String::new(); for line in lines { match line { @@ -84,7 +87,9 @@ 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"); } @@ -93,10 +98,18 @@ where command = title; debug!("Detected command name: {}", &command); } - LineType::Description(text) => process_snippet(PageSnippet::Description(&text))?, - LineType::ExampleText(text) => process_snippet(PageSnippet::Text(&text))?, + 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)?; + } LineType::ExampleCode(text) => { - process_snippet(PageSnippet::NormalCode(" "))?; + process_snippet(PageSnippet::NormalCode(&command_indent))?; highlight_code(&command, &text, process_snippet)?; process_snippet(PageSnippet::Linebreak)?; } diff --git a/src/output.rs b/src/output.rs index 9305c20..117ed1e 100644 --- a/src/output.rs +++ b/src/output.rs @@ -68,6 +68,7 @@ pub fn print_page( &mut process_snippet, !config.display.compact, config.display.show_title, + config.display.indent, ) .context("Could not write to stdout")?; } @@ -89,9 +90,9 @@ fn print_snippet( 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) => writeln!(writer, " {}", s.paint(style.description)), - Text(s) => writeln!(writer, " {}", s.paint(style.example_text)), - Title(s) => writeln!(writer, " {}", s.paint(style.command_name)), + Description(s) => write!(writer, "{}", s.paint(style.description)), + Text(s) => write!(writer, "{}", s.paint(style.example_text)), + Title(s) => write!(writer, "{}", s.paint(style.command_name)), Linebreak => writeln!(writer), } } diff --git a/tests/lib.rs b/tests/lib.rs index 40cb7e9..acdd048 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -821,6 +821,24 @@ fn test_rendering_color_never() { ); } +/// An end-to-end integration test for the indent config option +#[test] +fn test_rendering_with_indentation() { + let testenv = TestEnv::new().install_default_cache(); + let expected_custom_indentation = include_str!("rendered/inkscape-compact-no-color.expected"); + + // Configure to set base and command indents + testenv.append_to_config("display.indent.base = 3\n"); + testenv.append_to_config("display.indent.command = 1\n"); + + testenv + .command() + .args(["--color", "never", "inkscape-v2"]) + .assert() + .success() + .stdout(diff(expected_custom_indentation)); +} + #[test] fn test_rendering_i18n() { _test_correct_rendering( diff --git a/tests/rendered/apt.ja.expected b/tests/rendered/apt.ja.expected index 22424e3..efdd35d 100644 --- a/tests/rendered/apt.ja.expected +++ b/tests/rendered/apt.ja.expected @@ -3,35 +3,35 @@ Ubuntuのバージョンが16.04か、それ以降で対話モードを使う場合`apt-get`の代わりとして使用します。 詳しくはこちら: - 利用可能なパーケージとバージョンのリストの更新(他の`apt`コマンドの前での実行を推奨): + 利用可能なパーケージとバージョンのリストの更新(他の`apt`コマンドの前での実行を推奨):  sudo apt update - 指定されたパッケージの検索: + 指定されたパッケージの検索:  apt search パッケージ - パッケージの情報を出力: + パッケージの情報を出力:  apt show パッケージ - パッケージのインストール、または利用可能な最新バージョンに更新: + パッケージのインストール、または利用可能な最新バージョンに更新:  sudo apt install パッケージ - パッケージの削除(`sudo apt remove --purge`の場合設定ファイルも削除): + パッケージの削除(`sudo apt remove --purge`の場合設定ファイルも削除):  sudo apt remove パッケージ - インストールされている全てのパッケージを最新のバージョンにアップグレード: + インストールされている全てのパッケージを最新のバージョンにアップグレード:  sudo apt upgrade - インストールできるすべてのパッケージを表示: + インストールできるすべてのパッケージを表示:  apt list - インストールされた全てのパッケージを表示(依存関係も表示): + インストールされた全てのパッケージを表示(依存関係も表示):  apt list --installed diff --git a/tests/rendered/inkscape-compact-no-color.expected b/tests/rendered/inkscape-compact-no-color.expected new file mode 100644 index 0000000..473bbe9 --- /dev/null +++ b/tests/rendered/inkscape-compact-no-color.expected @@ -0,0 +1,32 @@ + + An SVG (Scalable Vector Graphics) editing program. + Use -z to not open the GUI and only process files in the console. + + Open an SVG file in the Inkscape GUI: + + inkscape filename.svg + + Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI): + + inkscape filename.svg -e filename.png + + Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur): + + inkscape filename.svg -e filename.png -w 600 -h 400 + + Export a single object, given its ID, into a bitmap: + + inkscape filename.svg -i id -e object.png + + Export an SVG document to PDF, converting all texts to paths: + + inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path + + Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: + + inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit + + Some invalid command just to test the correct highlighting of the command name: + + inkscape --use-inkscape=v3.0 file + diff --git a/tests/rendered/inkscape-default.expected b/tests/rendered/inkscape-default.expected index da909f2..3b37f0e 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. - Open an SVG file in the Inkscape GUI: + 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): + 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): + 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: + 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: + 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: + 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: + 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-with-config.expected b/tests/rendered/inkscape-with-config.expected index e79b219..33540a3 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. - Open an SVG file in the Inkscape GUI: + 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): + 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): + 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: + 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: + 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: + 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: + 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-with-title.expected b/tests/rendered/inkscape-with-title.expected index 7e7d428..ff2de4a 100644 --- a/tests/rendered/inkscape-with-title.expected +++ b/tests/rendered/inkscape-with-title.expected @@ -1,34 +1,34 @@ - inkscape + inkscape An SVG (Scalable Vector Graphics) editing program. Use -z to not open the GUI and only process files in the console. - Open an SVG file in the Inkscape GUI: + 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): + 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): + 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: + 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: + 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: + 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: + Some invalid command just to test the correct highlighting of the command name:  inkscape --use-inkscape=v3.0 file From 6c65c8f71c7e062507abee4fee8d3773c3f6aa68 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Fri, 17 Apr 2026 22:01:45 +0200 Subject: [PATCH 36/45] Fix off-boundary string access in formatter (#474) Closes #473 This regression was introduced in 593e9309b9a78dfbc5a1ad5db9d5982b817151be (#414) and leads to a panic when trying to display pages where characters line up like in the issue or the test. I checked other places and found that a similar panic could occur when parsing language strings, so I added code to ignore them instead. - Add regression test - Fix prefix check - Skip non-ASCII locales --- src/config.rs | 6 ++++++ src/formatter.rs | 13 ++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index b31f422..7c29ed4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,7 @@ use std::{ 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}; @@ -429,6 +430,11 @@ 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])); diff --git a/src/formatter.rs b/src/formatter.rs index 1cdc681..c0d3a91 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -179,7 +179,11 @@ fn find_marker(s: &str, marker: &str, forbidden_prefix: &str) -> Option { let overlaps_with_prefix = (forbidden_prefix.len() <= marker_index + 1) && { let prefix_start = marker_index + 1 - forbidden_prefix.len(); - &s[prefix_start..=marker_index] == forbidden_prefix + // 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); @@ -446,5 +450,12 @@ mod tests { ], ); } + + #[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")],); + } } } From 24e7f383b8ada277c2ab6ac5c954263daab38679 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Fri, 17 Apr 2026 22:08:42 +0200 Subject: [PATCH 37/45] Fix Rust 1.95 clippy lints --- src/cache.rs | 2 +- src/config.rs | 2 +- src/formatter.rs | 6 ++---- src/main.rs | 1 + src/output.rs | 3 +-- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index a77036d..afa3603 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -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/config.rs b/src/config.rs index 7c29ed4..212c1e2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -431,7 +431,7 @@ 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); + info!("Skipping non-ASCII locale string: {locale}"); continue; } diff --git a/src/formatter.rs b/src/formatter.rs index c0d3a91..b2d6b8e 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -230,13 +230,11 @@ fn is_freestanding_substring(surrounding: &str, substring: (usize, usize)) -> bo let char_before_is_okay = surrounding[..start] .chars() .last() - .filter(|prev_char| !prev_char.is_whitespace()) - .is_none(); + .is_none_or(char::is_whitespace); let char_after_is_okay = surrounding[end..] .chars() .next() - .filter(|next_char| !next_char.is_whitespace()) - .is_none(); + .is_none_or(char::is_whitespace); char_before_is_okay && char_after_is_okay } diff --git a/src/main.rs b/src/main.rs index 5b12613..29dad95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ #![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", diff --git a/src/output.rs b/src/output.rs index 117ed1e..6243b44 100644 --- a/src/output.rs +++ b/src/output.rs @@ -87,12 +87,11 @@ fn print_snippet( use PageSnippet::*; match snip { - CommandName(s) => write!(writer, "{}", s.paint(style.command_name)), + CommandName(s) | Title(s) => write!(writer, "{}", s.paint(style.command_name)), Variable(s) => write!(writer, "{}", s.paint(style.example_variable)), 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)), - Title(s) => write!(writer, "{}", s.paint(style.command_name)), Linebreak => writeln!(writer), } } From 1252261d662fcedecce229f08bf23277da8bf51e Mon Sep 17 00:00:00 2001 From: Ellis Clayton Date: Mon, 4 May 2026 05:47:29 +1000 Subject: [PATCH 38/45] Support tilde (`~`) expansion on config paths (#476) Allows directories in the configuration file (cache & custom pages) to be relative to the user's home directory by expanding the common `~` path prefix notation. Works for the current user only (`~diferentUser/` syntax is not supported, and will cause an error if attempted). Works for Linux/Unix (via `HOME` env var) and Windows (via `USERPROFILE` env var). Examples (assuming a user called "foo" on a Linux system): ``` ~/my/custom-pages # /home/foo/custom-pages ~ # /home/foo ~bar/cache # error ``` --- src/config.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 99 insertions(+), 8 deletions(-) diff --git a/src/config.rs b/src/config.rs index 212c1e2..89c5d8f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,9 @@ use std::{ + borrow::Cow, env, fmt, fs::{self, File}, io::{ErrorKind, Write}, - path::{Path, PathBuf}, + path::{Component, Path, PathBuf}, sync::LazyLock, time::Duration, }; @@ -574,6 +575,7 @@ impl<'a> Config<'a> { .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 @@ -589,11 +591,13 @@ impl<'a> Config<'a> { source: PathSource::EnvVar, } } else if let Some(config_value) = &raw_config.directories.cache_dir { - // If the user explicitly configured a cache directory, use that. + // 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); + PathWithSource { - // 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), + path: resolved_path, source: PathSource::ConfigFile, } } else if let Ok(default_dir) = get_app_root(AppDataType::UserCache, &crate::APP_INFO) { @@ -610,11 +614,18 @@ impl<'a> Config<'a> { .directories .custom_pages_dir .as_ref() - .map(|path| PathWithSource { + .map(|path| -> Result { + // Resolve possible ~ prefixed path + let expanded_path = expand_home(path, home_path.as_deref())?; // Resolve possible relative path. - path: relative_path_root.join(path), - source: PathSource::ConfigFile, + let resolved_path = relative_path_root.join(expanded_path); + + Ok(PathWithSource { + path: resolved_path, + source: PathSource::ConfigFile, + }) }) + .transpose()? .or_else(|| { get_app_root(AppDataType::UserData, &crate::APP_INFO) .map(|path| { @@ -642,6 +653,29 @@ 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 @@ -790,6 +824,63 @@ 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(); From 4d33e8a2790b9c53ed4bba91fd5ed1fb4ee103da Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Wed, 6 May 2026 15:36:37 +0100 Subject: [PATCH 39/45] Remove `tldr-c` entry from benchmark results in README.md (#480) `tldr-c-client` is unmaintained. --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 230dfa8..3d22e0a 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,6 @@ 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 | | @@ -113,7 +112,6 @@ 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 From d0108b23e450dddd59f79f4f2d693ce787aed5e4 Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Thu, 7 May 2026 14:07:27 +0100 Subject: [PATCH 40/45] Fix clippy lints on all targets (#481) --- .github/workflows/ci.yml | 2 +- src/formatter.rs | 6 +++--- tests/lib.rs | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a17ba90..78a27ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: toolchain: stable components: clippy - name: run clippy lints - run: cargo clippy --features logging + run: cargo clippy --all-targets --features logging fmt: name: run rustfmt diff --git a/src/formatter.rs b/src/formatter.rs index b2d6b8e..06d575e 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -439,7 +439,7 @@ mod tests { #[test] fn placeholder_inside_escaped() { assert_eq!( - run("test", r#"test \{\{{{var}} normal\}\}"#), + run("test", r"test \{\{{{var}} normal\}\}"), [ CommandName("test"), NormalCode(" {{"), @@ -450,10 +450,10 @@ mod tests { } #[test] - /// Regression test for https://github.com/tealdeer-rs/tealdeer/issues/473 + /// 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("", r"Äxx{{x}}"), [NormalCode("Äxx"), Variable("x")],); } } } diff --git a/tests/lib.rs b/tests/lib.rs index acdd048..30254e9 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -555,7 +555,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(0); + permissions.set_mode(0o0); fs::set_permissions(cache_dir, permissions).unwrap(); testenv @@ -1047,6 +1047,7 @@ 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(); From 51593d27ebc2f76931c4a1ee01cded12620ac717 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 Jul 2026 22:59:46 +0200 Subject: [PATCH 41/45] Bump actions/checkout from 6 to 7 (#493) --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/gh-pages.yml | 2 +- .github/workflows/release.yml | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78a27ca..62eb86d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: exe_suffix: .exe runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} @@ -50,7 +50,7 @@ jobs: name: run clippy lints runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -62,7 +62,7 @@ jobs: name: run rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -74,7 +74,7 @@ jobs: name: build docs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup mdBook uses: peaceiris/actions-mdbook@v2 with: diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index f8b6f6e..ae9ae93 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -9,7 +9,7 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup mdBook uses: peaceiris/actions-mdbook@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41de665..9337d6f 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@v6 + - uses: actions/checkout@v7 - 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@v6 + - uses: actions/checkout@v7 - name: Upload completion if: startsWith(github.ref, 'refs/tags/') run: | @@ -40,7 +40,7 @@ jobs: matrix: target: ["MIT", "APACHE"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Upload license if: startsWith(github.ref, 'refs/tags/') run: | @@ -66,7 +66,7 @@ jobs: - arch: "arm" libc: "musleabihf" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Pull Docker image run: docker pull messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} - name: Build in Docker @@ -86,7 +86,7 @@ jobs: - arch: "x86_64" - arch: "aarch64" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: @@ -102,7 +102,7 @@ jobs: build-windows: runs-on: windows-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup toolchain uses: dtolnay/rust-toolchain@master with: @@ -134,7 +134,7 @@ jobs: - macos-aarch64 - windows-x86_64-msvc steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/download-artifact@v8 - name: Upload binary if: startsWith(github.ref, 'refs/tags/') From df5113ddaa045ce87654ea17482a3778ae58833d Mon Sep 17 00:00:00 2001 From: RedHare <74206389+RedHare-Exe@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:05:15 -0400 Subject: [PATCH 42/45] Added AI Policy to README (#489) Addresses #479, and adds a section for an AI policy based on what is outlined by @niklasmohrin in that issue. The AI Policy is located below the "Development" section. --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 3d22e0a..859d06f 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,17 @@ 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 From f8a2003bc28a67f8f17809699716c0d7b92b479b Mon Sep 17 00:00:00 2001 From: Nikolaos Karaolidis Date: Tue, 7 Jul 2026 13:24:06 +0100 Subject: [PATCH 43/45] Add `updates.warn_cache_age` config option (#492) This is useful when the cache is managed externally, e.g. provisioned from a Nix store path or by a package manager, where the directory's mtime doesn't reflect the cache's real age, causing a spurious warning on every invocation. Today the only way to silence it is `--quiet`, which must be passed every call and hides all other output too. --- docs/src/config_updates.md | 12 ++++++++++++ src/config.rs | 19 +++++++++++++++++++ src/main.rs | 22 ++++++++++++---------- tests/lib.rs | 20 ++++++++++++++++++++ 4 files changed, 63 insertions(+), 10 deletions(-) diff --git a/docs/src/config_updates.md b/docs/src/config_updates.md index 9acba55..a8a10c8 100644 --- a/docs/src/config_updates.md +++ b/docs/src/config_updates.md @@ -30,6 +30,18 @@ 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/src/config.rs b/src/config.rs index 89c5d8f..498b49f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -228,6 +228,17 @@ fn default_archive_source() -> String { "https://github.com/tldr-pages/tldr/releases/latest/download".to_owned() } +/// Controls when a warning about an outdated cache is printed. +/// +/// Currently, the only nameable option is `"never"`. In the future, this may +/// be extended to also accept a duration (e.g. `"60d"`), after which the +/// warning should be shown. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +enum RawWarnCacheAge { + Never, +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] struct RawUpdatesConfig { #[serde(default)] @@ -240,6 +251,8 @@ struct RawUpdatesConfig { pub tls_backend: RawTlsBackend, #[serde(default)] pub download_languages: Option>, + #[serde(default)] + pub warn_cache_age: Option, } impl Default for RawUpdatesConfig { @@ -250,6 +263,7 @@ impl Default for RawUpdatesConfig { archive_source: default_archive_source(), tls_backend: RawTlsBackend::default(), download_languages: None, + warn_cache_age: None, } } } @@ -380,6 +394,7 @@ 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)] @@ -569,6 +584,10 @@ 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 diff --git a/src/main.rs b/src/main.rs index 29dad95..9a7c423 100644 --- a/src/main.rs +++ b/src/main.rs @@ -376,16 +376,18 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { return Ok(ExitCode::FAILURE); }; - 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 - ), - ); + if let Some(max_cache_age) = config.updates.warn_cache_age { + let age = cache.age()?; + if age > max_cache_age && !args.quiet { + print_warning( + enable_styles, + &format!( + "The cache hasn't been updated for {} days.\n\ + You should probably run `tldr --update` soon.", + age.as_secs() / 24 / 3600 + ), + ); + } } cache diff --git a/tests/lib.rs b/tests/lib.rs index 30254e9..d431b5f 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -493,6 +493,26 @@ 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() { From 28ed7850016813a5a6625c2748794cd7045d3055 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Sun, 26 Jul 2026 23:52:43 +0200 Subject: [PATCH 44/45] Fix 1.97 clippy lints --- src/formatter.rs | 2 +- src/main.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/formatter.rs b/src/formatter.rs index 06d575e..082f436 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -96,7 +96,7 @@ where // 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); + debug!("Detected command name: {command}"); } LineType::Description(text) => { process_snippet(PageSnippet::Description(&base_indent))?; diff --git a/src/main.rs b/src/main.rs index 9a7c423..cc03d01 100644 --- a/src/main.rs +++ b/src/main.rs @@ -429,10 +429,9 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { print_warning( enable_styles, &format!( - "Page `{}` not found in cache.\n\ + "Page `{command}` not found in cache.\n\ Try updating with `tldr --update`, or submit a pull request to:\n\ - https://github.com/tldr-pages/tldr", - &command + https://github.com/tldr-pages/tldr" ), ); } From 37b0dee39ffc7eeb95ad8e3dfda3b70991338b0f Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Thu, 6 Aug 2026 00:37:37 +0200 Subject: [PATCH 45/45] Support XDG directory spec on macOS by switching `app_dirs2` to `etcetera` (#467) Closes #311 Similar to how `bat` does it, we now have a global struct which holds all the queried system directories. We prefer the XDG directories, but use the "native" directories as a fallback. This should ensure that when MacOs users upgrade from an older version, their existing config and cache are still used. After deleting the old directories, the new ones should be used by tealdeer automatically. --- .github/workflows/ci.yml | 2 +- Cargo.lock | 36 +++++----------- Cargo.toml | 4 +- src/config.rs | 93 ++++++++++++++++++++++++++-------------- src/main.rs | 23 ++++------ 5 files changed, 83 insertions(+), 75 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 62eb86d..faf3111 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - toolchain: [stable, 1.85.0] + toolchain: [stable, 1.87.0] # MSRV include: - platform: windows-latest exe_suffix: .exe diff --git a/Cargo.lock b/Cargo.lock index be9bd24..4fba42b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,18 +73,6 @@ 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" @@ -380,6 +368,16 @@ 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" @@ -635,12 +633,6 @@ 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" @@ -1117,11 +1109,11 @@ name = "tealdeer" version = "1.8.1" dependencies = [ "anyhow", - "app_dirs2", "assert_cmd", "clap", "env_logger", "escargot", + "etcetera", "filetime", "log", "pager", @@ -1634,12 +1626,6 @@ 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 aa7ae49..1d98992 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.85" +rust-version = "1.87" # MSRV 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/src/config.rs b/src/config.rs index 498b49f..f0feeb4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,8 +8,7 @@ use std::{ time::Duration, }; -use anyhow::{anyhow, bail, ensure, Context, Result}; -use app_dirs::{get_app_root, AppDataType}; +use anyhow::{anyhow, ensure, Context, Result}; use clap::ValueEnum; use log::info; use serde::Serialize as _; @@ -33,6 +32,50 @@ 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() @@ -619,15 +662,11 @@ impl<'a> Config<'a> { path: resolved_path, source: PathSource::ConfigFile, } - } else if let Ok(default_dir) = get_app_root(AppDataType::UserCache, &crate::APP_INFO) { - // Otherwise, fall back to the default user cache directory. + } else { PathWithSource { - path: default_dir, + path: SYSTEM_DIRECTORIES.cache.clone(), source: PathSource::OsConvention, } - } else { - // If everything fails, give up - bail!("Could not determine user cache directory"); }; let custom_pages_dir = raw_config .directories @@ -646,15 +685,11 @@ impl<'a> Config<'a> { }) .transpose()? .or_else(|| { - 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() + // Note: The `join("")` call ensures that there's a trailing slash + Some(PathWithSource { + path: SYSTEM_DIRECTORIES.data.join("pages").join(""), + source: PathSource::OsConvention, + }) }); let directories = DirectoriesConfig { cache_dir, @@ -743,7 +778,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().context("Could not determine default config path.")?; + let path = get_default_config_path(); Self::read_internal(path, true) } @@ -761,30 +796,24 @@ 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() -> Result<(PathBuf, PathSource)> { +pub fn get_config_dir() -> (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 Ok((PathBuf::from(value), PathSource::EnvVar)); + return (PathBuf::from(value), PathSource::EnvVar); } - // 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)) + (SYSTEM_DIRECTORIES.config.clone(), 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() -> 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, - }) +pub fn get_default_config_path() -> PathWithSource { + let (mut path, source) = get_config_dir(); + path.push(CONFIG_FILE_NAME); + PathWithSource { path, source } } /// Create default config file. @@ -794,7 +823,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() { diff --git a/src/main.rs b/src/main.rs index cc03d01..6678ceb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,6 @@ 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}; @@ -65,10 +64,6 @@ use crate::{ }; const NAME: &str = "tealdeer"; -const APP_INFO: AppInfo = AppInfo { - name: NAME, - author: NAME, -}; static TEALDEER_PAGE: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/pages/tealdeer.md")); @@ -110,16 +105,14 @@ fn update_cache( /// Show file paths fn show_paths(config: &Config) { - 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_dir = { + let (mut path, source) = get_config_dir(); + path.push(""); // Trailing path separator + match path.to_str() { + Some(path) => format!("{path} ({source})"), + None => "[Invalid]".to_string(), + } + }; let config_path = config.file_path.to_string(); let cache_dir = config.directories.cache_dir.to_string(); let pages_dir = {