Merge pull request #430 from niklasmohrin/language-config

Add `search.languages` and `updates.download_languages` settings
This commit is contained in:
Niklas Mohrin 2025-09-22 00:05:24 +02:00 committed by GitHub
commit 92b6c64c87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 414 additions and 204 deletions

View file

@ -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)

View file

@ -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

14
docs/src/config_search.md Normal file
View file

@ -0,0 +1,14 @@
# Section: \[search\]
This config section is used to configure the page search in the cache.
The settings apply to `tldr <page>` and `tldr --list`.
## `languages`
The list of languages that should be considered when searching.
If unspecified, the list of languages will be inferred from the `LANG` and `LANGUAGE` environment variables.
Either way, the language used can be overwritten using the `--language` command line flag.
[search]
# Show pages in German if available, otherwise show in English
languages = ["de", "en"]

View file

@ -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.

View file

@ -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},
@ -14,20 +14,21 @@ 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,
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<impl IntoIterator<Item = Language<'_>>> {
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<Option<Vec<u8>>> {
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() => {

View file

@ -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
@ -189,6 +190,8 @@ struct RawUpdatesConfig {
pub archive_source: String,
#[serde(default)]
pub tls_backend: RawTlsBackend,
#[serde(default)]
pub download_languages: Option<Vec<String>>,
}
impl Default for RawUpdatesConfig {
@ -198,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<Self> {
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::<Vec<String>>().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)]
@ -241,6 +214,11 @@ struct RawDirectoriesConfig {
pub custom_pages_dir: Option<PathBuf>,
}
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
struct RawSearchConfig {
pub languages: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
struct RawConfig {
@ -248,6 +226,7 @@ struct RawConfig {
display: RawDisplayConfig,
updates: RawUpdatesConfig,
directories: RawDirectoriesConfig,
search: RawSearchConfig,
}
impl Default for RawConfig {
@ -257,6 +236,7 @@ impl Default for RawConfig {
display: RawDisplayConfig::default(),
updates: RawUpdatesConfig::default(),
directories: RawDirectoriesConfig::default(),
search: RawSearchConfig::default(),
};
// Set default config
@ -291,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<Language<'a>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -317,6 +298,54 @@ pub struct DirectoriesConfig {
pub custom_pages_dir: Option<PathWithSource>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SearchConfig<'a> {
pub languages: Vec<Language<'a>>,
}
#[derive(Debug, Clone, Copy, 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<'a>> {
// 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<Language<'a>> {
static LANG: LazyLock<Option<String>> = LazyLock::new(|| std::env::var("LANG").ok());
static LANGUAGE: LazyLock<Option<String>> = 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 {
@ -354,12 +383,35 @@ pub enum TlsBackend {
RustlsWithNativeRoots,
}
impl TryFrom<RawTlsBackend> for TlsBackend {
type Error = anyhow::Error;
fn try_from(raw: RawTlsBackend) -> Result<Self, Self::Error> {
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::<Vec<String>>().join(", ")
))
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Config<'a> {
pub style: StyleConfig,
pub display: DisplayConfig,
pub updates: UpdatesConfig<'a>,
pub directories: DirectoriesConfig,
pub search: SearchConfig<'a>,
pub file_path: PathWithSource,
}
@ -371,7 +423,30 @@ impl<'a> Config<'a> {
fn from_raw(raw_config: &'a RawConfig, config_file_path: PathWithSource) -> Result<Self> {
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 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()
@ -438,6 +513,7 @@ impl<'a> Config<'a> {
display,
updates,
directories,
search,
file_path: config_file_path,
})
}
@ -579,8 +655,12 @@ pub fn make_default_config(path: Option<&Path>) -> Result<PathBuf> {
Ok(config_file_path)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_serialize_deserialize() {
fn serialize_deserialize() {
let raw_config = RawConfig::default();
let serialized = toml::to_string(&raw_config).unwrap();
let deserialized: RawConfig = toml::from_str(&serialized).unwrap();
@ -588,7 +668,7 @@ fn test_serialize_deserialize() {
}
#[test]
fn test_relative_path_resolution() {
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());
@ -611,3 +691,76 @@ fn test_relative_path_resolution() {
Path::new("/path/to/config/../custom_pages")
);
}
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 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);
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")
]
);
}
}
}

View file

@ -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::{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},
@ -84,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(())
}
@ -141,46 +149,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<'a>> {
// 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<Language<'a>> {
static LANG: LazyLock<Option<String>> = LazyLock::new(|| std::env::var("LANG").ok());
static LANGUAGE: LazyLock<Option<String>> = 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")?;
@ -292,10 +260,10 @@ fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> {
}
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 (search_languages, download_languages): (&[_], &[_]) = match args.language.as_deref() {
Some(lang) => (&[Language(lang)], &[Language(lang)]),
None => (&config.search.languages, &config.updates.download_languages),
};
let cache_config = CacheConfig {
pages_directory: &config.directories.cache_dir.path().join(TLDR_PAGES_DIR),
@ -305,7 +273,8 @@ fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> {
.as_ref()
.map(PathWithSource::path),
platforms: &platforms,
languages: &languages,
search_languages,
download_languages,
};
// TODO: remove in tealdeer 1.9
@ -439,71 +408,3 @@ fn compute_platforms(platforms: Option<&Vec<PlatformType>>) -> Vec<PlatformType>
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")
]
);
}
}
}

View file

@ -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,88 @@ 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);
}
#[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();