diff --git a/docs/src/config_updates.md b/docs/src/config_updates.md index eaac735..cc5868e 100644 --- a/docs/src/config_updates.md +++ b/docs/src/config_updates.md @@ -1,5 +1,7 @@ # Section: \[updates\] +This config section contains settings related to updating the tealdeer cache. + ## Automatic updates Tealdeer can refresh the cache automatically when it is outdated. This @@ -24,7 +26,23 @@ is set to `false`. auto_update = true auto_update_interval_hours = 24 -### archive_source +## Download configuration + +### `download_languages` + +The list of languages which should be downloaded when updating. +If unspecified, the languages listed in the `search.languages` setting are used. +Thus, this setting is the most useful to instruct tealdeer to download pages in additional languages that are not searched by default. +Either way, the language used can be overwritten using the `--language` command line flag. + + [search] + languages = ["de", "en"] + + [updates] + # sometimes I like to read the Italian description + download_languages = ["de", "en", "it"] + +### `archive_source` URL for the location of the tldr pages archive. By default the pages are fetched from the latest `tldr-pages/tldr` GitHub release. diff --git a/src/cache.rs b/src/cache.rs index 315f6f0..23fd668 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -6,7 +6,7 @@ use std::{ }; use anyhow::{anyhow, bail, ensure, Context, Result}; -use log::debug; +use log::{debug, info}; use ureq::{ http::StatusCode, tls::{RootCerts, TlsConfig, TlsProvider}, @@ -27,7 +27,8 @@ pub struct CacheConfig<'a> { pub pages_directory: &'a Path, pub custom_pages_directory: Option<&'a Path>, pub platforms: &'a [PlatformType], - pub languages: &'a [Language<'a>], + pub search_languages: &'a [Language<'a>], + pub download_languages: &'a [Language<'a>], } /// The directory backing this cache is checked to be populated at construction. @@ -110,7 +111,7 @@ impl<'a> Cache<'a> { .filter(|path| path.is_file()); for &platform in self.config.platforms { - for language in self.config.languages { + for language in self.config.search_languages { let mut search_path = self.config.pages_directory.to_path_buf(); search_path.push(language.directory_name()); search_path.push(platform.directory_name()); @@ -159,7 +160,7 @@ impl<'a> Cache<'a> { }; let mut search_path = self.config.pages_directory.to_path_buf(); - for language in self.config.languages { + for language in self.config.search_languages { search_path.push(language.directory_name()); for platform in self.config.platforms { search_path.push(platform.directory_name()); @@ -206,15 +207,23 @@ impl<'a> Cache<'a> { }) } - pub fn update(&mut self, archive_url: &str, tls_backend: TlsBackend) -> Result<()> { + /// Download archives for the languages in `self.config().download_languages` and replace the + /// pages directory with the newly downloaded pages. As not all languages might have pages + /// available (for example, `en_US` instead of `en`), an iterator yielding all languages which + /// were successfully downloaded is returned. + pub fn update( + &mut self, + archive_url: &str, + tls_backend: TlsBackend, + ) -> Result>> { let client = Self::build_client(tls_backend); // Download everything before deleting anything - let archives = self + let mut archives = self .config - .languages + .download_languages .iter() - .map(|lang| { + .map(|&lang| { Ok(( lang, Self::download( @@ -236,16 +245,18 @@ impl<'a> Cache<'a> { fs::remove_dir_all(self.config.pages_directory)?; fs::create_dir(self.config.pages_directory)?; - for (lang, archive) in archives { - if let Some(mut archive) = archive { - debug!("Extracting archive for {lang:?}"); + for (lang, archive) in &mut archives { + if let Some(archive) = archive { + info!("Extracting archive for {lang:?}"); archive.extract(self.config.pages_directory.join(lang.directory_name()))?; } else { - debug!("No archive found for {lang:?}"); + info!("No archive found for {lang:?}"); } } - Ok(()) + Ok(archives + .into_iter() + .filter_map(|(lang, archive)| archive.is_some().then_some(lang))) } pub fn config(&self) -> &CacheConfig<'a> { @@ -347,7 +358,7 @@ impl Cache<'_> { /// Download the archive from the specified URL. fn download(client: &Agent, archive_url: &str) -> Result>> { - debug!("Downloading archive from {archive_url}"); + info!("Downloading archive from {archive_url}"); let response = client.get(archive_url).call(); match response { Ok(response) if response.status().is_success() => { diff --git a/src/config.rs b/src/config.rs index 69de9b5..bac3f74 100644 --- a/src/config.rs +++ b/src/config.rs @@ -190,6 +190,8 @@ struct RawUpdatesConfig { pub archive_source: String, #[serde(default)] pub tls_backend: RawTlsBackend, + #[serde(default)] + pub download_languages: Option>, } impl Default for RawUpdatesConfig { @@ -199,41 +201,11 @@ impl Default for RawUpdatesConfig { auto_update_interval_hours: DEFAULT_UPDATE_INTERVAL_HOURS, archive_source: default_archive_source(), tls_backend: RawTlsBackend::default(), + download_languages: None, } } } -impl<'a> TryFrom<&'a RawUpdatesConfig> for UpdatesConfig<'a> { - type Error = anyhow::Error; - - fn try_from(raw_updates_config: &'a RawUpdatesConfig) -> Result { - let tls_backend = match raw_updates_config.tls_backend { - #[cfg(feature = "native-tls")] - RawTlsBackend::NativeTls => TlsBackend::NativeTls, - #[cfg(feature = "rustls-with-webpki-roots")] - RawTlsBackend::RustlsWithWebpkiRoots => TlsBackend::RustlsWithWebpkiRoots, - #[cfg(feature = "rustls-with-native-roots")] - RawTlsBackend::RustlsWithNativeRoots => TlsBackend::RustlsWithNativeRoots, - // when compiling without all TLS backend features, we want to handle config error. - #[allow(unreachable_patterns)] - _ => return Err(anyhow!( - "Unsupported TLS backend: {}. This tealdeer build has support for the following options: {}", - raw_updates_config.tls_backend, - SUPPORTED_TLS_BACKENDS.iter().map(std::string::ToString::to_string).collect::>().join(", ") - )) - }; - - Ok(Self { - auto_update: raw_updates_config.auto_update, - auto_update_interval: Duration::from_secs( - raw_updates_config.auto_update_interval_hours * 3600, - ), - archive_source: &raw_updates_config.archive_source, - tls_backend, - }) - } -} - #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] struct RawDirectoriesConfig { #[serde(default)] @@ -299,6 +271,7 @@ pub struct UpdatesConfig<'a> { pub auto_update_interval: Duration, pub archive_source: &'a str, pub tls_backend: TlsBackend, + pub download_languages: Vec>, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -330,7 +303,7 @@ pub struct SearchConfig<'a> { pub languages: Vec>, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Language<'a>(pub &'a str); fn get_languages<'a>( @@ -410,6 +383,28 @@ pub enum TlsBackend { RustlsWithNativeRoots, } +impl TryFrom for TlsBackend { + type Error = anyhow::Error; + + fn try_from(raw: RawTlsBackend) -> Result { + match raw { + #[cfg(feature = "native-tls")] + RawTlsBackend::NativeTls => Ok(TlsBackend::NativeTls), + #[cfg(feature = "rustls-with-webpki-roots")] + RawTlsBackend::RustlsWithWebpkiRoots => Ok(TlsBackend::RustlsWithWebpkiRoots), + #[cfg(feature = "rustls-with-native-roots")] + RawTlsBackend::RustlsWithNativeRoots => Ok(TlsBackend::RustlsWithNativeRoots), + // when compiling without all TLS backend features, we want to handle config error. + #[allow(unreachable_patterns)] + _ => Err(anyhow!( + "Unsupported TLS backend: {}. This tealdeer build has support for the following options: {}", + raw, + SUPPORTED_TLS_BACKENDS.iter().map(std::string::ToString::to_string).collect::>().join(", ") + )) + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct Config<'a> { pub style: StyleConfig, @@ -428,7 +423,7 @@ impl<'a> Config<'a> { fn from_raw(raw_config: &'a RawConfig, config_file_path: PathWithSource) -> Result { let style = (&raw_config.style).into(); let display = (&raw_config.display).into(); - let updates = (&raw_config.updates).try_into()?; + let search = SearchConfig { languages: raw_config .search @@ -439,6 +434,19 @@ impl<'a> Config<'a> { }), }; + let updates = UpdatesConfig { + auto_update: raw_config.updates.auto_update, + auto_update_interval: Duration::from_secs( + raw_config.updates.auto_update_interval_hours * 3600, + ), + archive_source: &raw_config.updates.archive_source, + tls_backend: raw_config.updates.tls_backend.try_into()?, + download_languages: raw_config.updates.download_languages.as_ref().map_or_else( + || search.languages.clone(), + |languages| languages.iter().map(|lang| Language(lang)).collect(), + ), + }; + let relative_path_root = config_file_path .path() .parent() diff --git a/src/main.rs b/src/main.rs index 5bd0fa0..2f730a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,11 +82,21 @@ fn update_cache( tls_backend: TlsBackend, quietly: bool, ) -> Result<()> { - cache + let downloaded_languages = cache .update(archive_source, tls_backend) .context("Could not update cache")?; if !quietly { eprintln!("Successfully updated cache."); + eprint!("Pages for the following languages were downloaded: "); + let language_strings: Vec<_> = downloaded_languages + .into_iter() + .map(|lang| lang.0) + .collect(); + if language_strings.is_empty() { + eprintln!("(none)"); + } else { + eprintln!("{}", language_strings.join(", ")); + } } Ok(()) } @@ -250,9 +260,9 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { } let platforms = compute_platforms(args.platforms.as_ref()); - let languages = match args.language.as_deref() { - Some(lang) => &[Language(lang)] as &[_], - None => &config.search.languages, + let (search_languages, download_languages): (&[_], &[_]) = match args.language.as_deref() { + Some(lang) => (&[Language(lang)], &[Language(lang)]), + None => (&config.search.languages, &config.updates.download_languages), }; let cache_config = CacheConfig { @@ -263,7 +273,8 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { .as_ref() .map(PathWithSource::path), platforms: &platforms, - languages, + search_languages, + download_languages, }; // TODO: remove in tealdeer 1.9 diff --git a/tests/lib.rs b/tests/lib.rs index 37f3b30..eceb427 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -996,6 +996,30 @@ fn test_search_language_precedence() { run(env_cases); } +#[cfg_attr(feature = "ignore-online-tests", ignore = "online test")] +#[test] +fn test_update_language_arg() { + let testenv = TestEnv::new(); + testenv + .command() + .env("LANG", "it") + .arg("--update") + .assert() + .success() + .stderr(contains("it")) + .stderr(contains("en")); + + testenv + .command() + .env("LANG", "en") + .args(["--language", "it"]) + .arg("--update") + .assert() + .success() + .stderr(contains("it")) + .stderr(contains("en").not()); +} + #[test] fn test_list_flag_rendering() { let testenv = TestEnv::new().write_custom_pages_config();