mirror of
https://github.com/tealdeer-rs/tealdeer.git
synced 2026-08-09 09:49:10 +02:00
Merge pull request #430 from niklasmohrin/language-config
Add `search.languages` and `updates.download_languages` settings
This commit is contained in:
commit
92b6c64c87
8 changed files with 414 additions and 204 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
14
docs/src/config_search.md
Normal 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"]
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
47
src/cache.rs
47
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<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() => {
|
||||
|
|
|
|||
281
src/config.rs
281
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<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,35 +655,112 @@ pub fn make_default_config(path: Option<&Path>) -> Result<PathBuf> {
|
|||
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")
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
137
src/main.rs
137
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<'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")
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
122
tests/lib.rs
122
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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue