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/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 c18f03f..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}, @@ -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>> { 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 2d4d3a8..bac3f74 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 @@ -189,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 { @@ -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 { - 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)] @@ -241,6 +214,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 { @@ -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>, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -317,6 +298,54 @@ pub struct DirectoriesConfig { pub custom_pages_dir: Option, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SearchConfig<'a> { + pub languages: Vec>, +} + +#[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 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 { @@ -354,12 +383,35 @@ 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, 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 { 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,35 +655,112 @@ 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::*; + + #[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") + ] + ); + } + } } diff --git a/src/main.rs b/src/main.rs index 6920c63..2f730a4 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::{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 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")?; @@ -292,10 +260,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 (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 { .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>) -> 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") - ] - ); - } - } -} diff --git a/tests/lib.rs b/tests/lib.rs index 2a1665c..eceb427 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,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();