Add updates.download_languages setting

This commit is contained in:
Niklas Mohrin 2025-08-03 01:00:24 +02:00
commit c741146db5
No known key found for this signature in database
GPG key ID: 0ACD89A5C1DEB3EB
5 changed files with 125 additions and 53 deletions

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

@ -190,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 {
@ -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<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)]
@ -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<Language<'a>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -330,7 +303,7 @@ pub struct SearchConfig<'a> {
pub languages: Vec<Language<'a>>,
}
#[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<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,
@ -428,7 +423,7 @@ 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
@ -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()

View file

@ -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<ExitCode> {
}
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<ExitCode> {
.as_ref()
.map(PathWithSource::path),
platforms: &platforms,
languages,
search_languages,
download_languages,
};
// TODO: remove in tealdeer 1.9

View file

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