how_install_tealdeer/
lib.rs

1//! An implementation of [tldr](https://github.com/tldr-pages/tldr) in Rust.
2//
3// Copyright (c) 2015-2021 tealdeer developers
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8// option. All files in the project carrying such notice may not be
9// copied, modified, or distributed except according to those terms.
10
11#![deny(clippy::all)]
12#![warn(clippy::pedantic)]
13#![allow(clippy::enum_glob_use)]
14#![allow(clippy::module_name_repetitions)]
15#![allow(clippy::similar_names)]
16#![allow(clippy::struct_excessive_bools)]
17#![allow(clippy::too_many_lines)]
18
19#[cfg(any(
20    all(feature = "native-roots", feature = "webpki-roots"),
21    not(any(feature = "native-roots", feature = "webpki-roots")),
22))]
23compile_error!(
24    "exactly one of feature \"native-roots\" and feature \"webpki-roots\" must be enabled"
25);
26
27use std::{env, process};
28
29use app_dirs::AppInfo;
30use atty::Stream;
31
32mod cache;
33mod cli;
34mod config;
35pub mod extensions;
36mod formatter;
37mod line_iterator;
38mod output;
39mod types;
40mod utils;
41
42use crate::{
43    cache::{Cache, CacheFreshness, PageLookupResult, TLDR_PAGES_DIR},
44    cli::Args,
45    config::{get_config_dir, get_config_path, make_default_config, Config},
46    extensions::Dedup,
47    output::print_page,
48    types::{ColorOptions, PlatformType},
49    utils::{print_error, print_warning},
50};
51
52const NAME: &str = "tealdeer";
53const APP_INFO: AppInfo = AppInfo {
54    name: NAME,
55    author: NAME,
56};
57const ARCHIVE_URL: &str = "https://tldr.sh/assets/tldr.zip";
58
59/// The cache should be updated if it was explicitly requested,
60/// or if an automatic update is due and allowed.
61fn should_update_cache(args: &Args, config: &Config) -> bool {
62    args.update
63        || (!args.no_auto_update
64            && config.updates.auto_update
65            && Cache::last_update().map_or(true, |ago| ago >= config.updates.auto_update_interval))
66}
67
68#[derive(PartialEq)]
69enum CheckCacheResult {
70    CacheFound,
71    CacheMissing,
72}
73
74/// Check the cache for freshness. If it's stale or missing, show a warning.
75fn check_cache(_args: &Args, _enable_styles: bool) -> CheckCacheResult {
76    match Cache::freshness() {
77        CacheFreshness::Fresh => CheckCacheResult::CacheFound,
78        //CacheFreshness::Stale(_) if args.quiet => CheckCacheResult::CacheFound,
79        CacheFreshness::Stale(_) => {
80            //print_warning(
81            //    enable_styles,
82            //    &format!(
83            //        "The cache hasn't been updated for {} days.\n\
84            //         You should probably run `tldr --update` soon.",
85            //        age.as_secs() / 24 / 3600
86            //    ),
87            //);
88            CheckCacheResult::CacheMissing
89        }
90        CacheFreshness::Missing => {
91            //print_error(
92            //    enable_styles,
93            //    &anyhow::anyhow!(
94            //        "Page cache not found. Please run `tldr --update` to download the cache."
95            //    ),
96            //);
97            //eprintln!("\nNote: You can optionally enable automatic cache updates by adding the");
98            //eprintln!("following config to your config file:\n");
99            //eprintln!("  [updates]");
100            //eprintln!("  auto_update = true\n");
101            //eprintln!("The path to your config file can be looked up with `tldr --show-paths`.");
102            //eprintln!("To create an initial config file, use `tldr --seed-config`.\n");
103            //eprintln!("You can find more tips and tricks in our docs:\n");
104            //eprintln!("  https://dbrgn.github.io/tealdeer/config_updates.html");
105            CheckCacheResult::CacheMissing
106        }
107    }
108}
109
110/// Clear the cache
111fn clear_cache(quietly: bool, enable_styles: bool) {
112    Cache::clear().unwrap_or_else(|e| {
113        print_error(enable_styles, &e.context("Could not clear cache"));
114        process::exit(1);
115    });
116    if !quietly {
117        eprintln!("Successfully deleted cache.");
118    }
119}
120
121/// Update the cache
122async fn update_cache(cache: &Cache, quietly: bool, enable_styles: bool) {
123    cache.update().await.unwrap_or_else(|e| {
124        print_error(enable_styles, &e.context("Could not update cache"));
125        process::exit(1);
126    });
127    if !quietly {
128        eprintln!("Successfully updated cache.");
129    }
130}
131
132/// Show the config path (DEPRECATED)
133fn show_config_path(enable_styles: bool) {
134    match get_config_path() {
135        Ok((config_file_path, _)) => {
136            eprintln!("Config path is: {}", config_file_path.to_str().unwrap());
137        }
138        Err(e) => {
139            print_error(enable_styles, &e.context("Could not look up config path"));
140            process::exit(1);
141        }
142    }
143}
144
145/// Show file paths
146fn show_paths(config: &Config) {
147    let config_dir = get_config_dir().map_or_else(
148        |e| format!("[Error: {}]", e),
149        |(mut path, source)| {
150            path.push(""); // Trailing path separator
151            match path.to_str() {
152                Some(path) => format!("{} ({})", path, source),
153                None => "[Invalid]".to_string(),
154            }
155        },
156    );
157    let config_path = get_config_path().map_or_else(
158        |e| format!("[Error: {}]", e),
159        |(path, _)| path.to_str().unwrap_or("[Invalid]").to_string(),
160    );
161    let cache_dir = Cache::get_cache_dir().map_or_else(
162        |e| format!("[Error: {}]", e),
163        |(mut path, source)| {
164            path.push(""); // Trailing path separator
165            match path.to_str() {
166                Some(path) => format!("{} ({})", path, source),
167                None => "[Invalid]".to_string(),
168            }
169        },
170    );
171    let pages_dir = Cache::get_cache_dir().map_or_else(
172        |e| format!("[Error: {}]", e),
173        |(mut path, _)| {
174            path.push(TLDR_PAGES_DIR);
175            path.push(""); // Trailing path separator
176            path.into_os_string()
177                .into_string()
178                .unwrap_or_else(|_| "[Invalid]".to_string())
179        },
180    );
181    let custom_pages_dir = config.directories.custom_pages_dir.as_deref().map_or_else(
182        || "[None]".to_string(),
183        |path| {
184            path.to_str()
185                .map_or_else(|| "[Invalid]".to_string(), ToString::to_string)
186        },
187    );
188    eprintln!("Config dir:       {}", config_dir);
189    eprintln!("Config path:      {}", config_path);
190    eprintln!("Cache dir:        {}", cache_dir);
191    eprintln!("Pages dir:        {}", pages_dir);
192    eprintln!("Custom pages dir: {}", custom_pages_dir);
193}
194
195/// Create seed config file and exit
196fn create_config_and_exit(enable_styles: bool) {
197    match make_default_config() {
198        Ok(config_file_path) => {
199            eprintln!(
200                "Successfully created seed config file here: {}",
201                config_file_path.to_str().unwrap()
202            );
203            process::exit(0);
204        }
205        Err(e) => {
206            print_error(enable_styles, &e.context("Could not create seed config"));
207            process::exit(1);
208        }
209    }
210}
211
212#[cfg(feature = "logging")]
213fn init_log() {
214    env_logger::init();
215}
216
217#[cfg(not(feature = "logging"))]
218fn init_log() {}
219
220fn get_languages(env_lang: Option<&str>, env_language: Option<&str>) -> Vec<String> {
221    // Language list according to
222    // https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#language
223
224    if env_lang.is_none() {
225        return vec!["en".to_string()];
226    }
227    let env_lang = env_lang.unwrap();
228
229    // Create an iterator that contains $LANGUAGE (':' separated list) followed by $LANG (single language)
230    let locales = env_language.unwrap_or("").split(':').chain([env_lang]);
231
232    let mut lang_list = Vec::new();
233    for locale in locales {
234        // Language plus country code (e.g. `en_US`)
235        if locale.len() >= 5 && locale.chars().nth(2) == Some('_') {
236            lang_list.push(&locale[..5]);
237        }
238        // Language code only (e.g. `en`)
239        if locale.len() >= 2 && locale != "POSIX" {
240            lang_list.push(&locale[..2]);
241        }
242    }
243
244    lang_list.push("en");
245    lang_list.clear_duplicates();
246    lang_list.into_iter().map(str::to_string).collect()
247}
248
249fn get_languages_from_env() -> Vec<String> {
250    get_languages(
251        std::env::var("LANG").ok().as_deref(),
252        std::env::var("LANGUAGE").ok().as_deref(),
253    )
254}
255
256pub async fn list() -> Vec<String> {
257    let args = Args {
258        command: vec![],
259        update: true,
260        quiet: true,
261        ..Args::default()
262    };
263
264    let config = Config::load(false).unwrap();
265    let platform: PlatformType = args.platform.unwrap_or_else(PlatformType::current);
266    let cache = Cache::new(ARCHIVE_URL, platform);
267
268    if let CheckCacheResult::CacheMissing = check_cache(&args, false) {
269        if should_update_cache(&args, &config) {
270            update_cache(&cache, args.quiet, false).await;
271        }
272    }
273
274    cache.list_pages().unwrap()
275}
276
277pub async fn main(cmd: String) {
278    // Initialize logger
279    init_log();
280
281    // Parse arguments
282    let mut args = Args {
283        command: vec![cmd],
284        update: true,
285        quiet: true,
286        ..Args::default()
287    };
288
289    // Determine the usage of styles
290    #[cfg(target_os = "windows")]
291    let ansi_support = ansi_term::enable_ansi_support().is_ok();
292    #[cfg(not(target_os = "windows"))]
293    let ansi_support = true;
294    let enable_styles = match args.color.unwrap_or_default() {
295        // Attempt to use styling if instructed
296        ColorOptions::Always => true,
297        // Enable styling if:
298        // * There is `ansi_support`
299        // * NO_COLOR env var isn't set: https://no-color.org/
300        // * The output stream is stdout (not being piped)
301        ColorOptions::Auto => {
302            ansi_support && env::var_os("NO_COLOR").is_none() && atty::is(Stream::Stdout)
303        }
304        // Disable styling
305        ColorOptions::Never => false,
306    };
307
308    // Handle renamed arguments
309    if args.markdown {
310        args.raw = true;
311        print_warning(
312            enable_styles,
313            "The -m / --markdown flag is deprecated, use -r / --raw instead",
314        );
315    }
316    if args.os.is_some() {
317        print_warning(
318            enable_styles,
319            "The -o / --os flag is deprecated, use -p / --platform instead",
320        );
321    }
322    args.platform = args.platform.or(args.os);
323
324    // Show config file and path, pass through
325    if args.config_path {
326        print_warning(
327            enable_styles,
328            "The --config-path flag is deprecated, use --show-paths instead",
329        );
330        show_config_path(enable_styles);
331    }
332
333    // Look up config file, if none is found fall back to default config.
334    let config = match Config::load(enable_styles) {
335        Ok(config) => config,
336        Err(e) => {
337            print_error(enable_styles, &e.context("Could not load config"));
338            process::exit(1);
339        }
340    };
341
342    // Show various paths
343    if args.show_paths {
344        show_paths(&config);
345    }
346
347    // Create a basic config and exit
348    if args.seed_config {
349        create_config_and_exit(enable_styles);
350    }
351
352    // Specify target OS
353    let platform: PlatformType = args.platform.unwrap_or_else(PlatformType::current);
354
355    // If a local file was passed in, render it and exit
356    if let Some(file) = args.render {
357        let path = PageLookupResult::with_page(file);
358        if let Err(ref e) = print_page(&path, args.raw, enable_styles, args.pager, &config) {
359            print_error(enable_styles, e);
360            process::exit(1);
361        } else {
362            process::exit(0);
363        };
364    }
365
366    // Initialize cache
367    let cache = Cache::new(ARCHIVE_URL, platform);
368
369    // Clear cache, pass through
370    if args.clear_cache {
371        clear_cache(args.quiet, enable_styles);
372    }
373
374    // Cache update, pass through
375    let cache_updated = if let CheckCacheResult::CacheMissing = check_cache(&args, enable_styles) {
376        if should_update_cache(&args, &config) {
377            update_cache(&cache, args.quiet, enable_styles).await;
378            true
379        } else {
380            false
381        }
382    } else {
383        false
384    };
385
386    // Check cache presence and freshness
387    if !cache_updated
388        && (args.list || !args.command.is_empty())
389        && check_cache(&args, enable_styles) == CheckCacheResult::CacheMissing
390    {
391        process::exit(1);
392    }
393
394    // List cached commands and exit
395    if args.list {
396        // Get list of pages
397        let pages = cache.list_pages().unwrap_or_else(|e| {
398            print_error(enable_styles, &e.context("Could not get list of pages"));
399            process::exit(1);
400        });
401
402        // Print pages
403        eprintln!("{}", pages.join("\n"));
404        process::exit(0);
405    }
406
407    // Show command from cache
408    if !args.command.is_empty() {
409        // Note: According to the TLDR client spec, page names must be transparently
410        // lowercased before lookup:
411        // https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#page-names
412        let command = args.command.join("-").to_lowercase();
413
414        // Collect languages
415        let languages = args
416            .language
417            .map_or_else(get_languages_from_env, |lang| vec![lang]);
418
419        // Search for command in cache
420        if let Some(lookup_result) = cache.find_page(
421            &command,
422            &languages,
423            config.directories.custom_pages_dir.as_deref(),
424        ) {
425            if let Err(ref e) =
426                print_page(&lookup_result, args.raw, enable_styles, args.pager, &config)
427            {
428                print_error(enable_styles, e);
429                process::exit(1);
430            }
431            //process::exit(0);
432        } /*else {
433              if !args.quiet {
434                  print_warning(
435                      enable_styles,
436                      &format!(
437                          "Page `{}` not found in cache.\n\
438                           Try updating with `tldr --update`, or submit a pull request to:\n\
439                           https://github.com/tldr-pages/tldr",
440                          &command
441                      ),
442                  );
443              }
444              //process::exit(1);
445          }*/
446    }
447}
448
449#[cfg(test)]
450mod test {
451    use crate::get_languages;
452
453    mod language {
454        use super::*;
455
456        #[test]
457        fn missing_lang_env() {
458            let lang_list = get_languages(None, Some("de:fr"));
459            assert_eq!(lang_list, ["en"]);
460            let lang_list = get_languages(None, None);
461            assert_eq!(lang_list, ["en"]);
462        }
463
464        #[test]
465        fn missing_language_env() {
466            let lang_list = get_languages(Some("de"), None);
467            assert_eq!(lang_list, ["de", "en"]);
468        }
469
470        #[test]
471        fn preference_order() {
472            let lang_list = get_languages(Some("de"), Some("fr:cn"));
473            assert_eq!(lang_list, ["fr", "cn", "de", "en"]);
474        }
475
476        #[test]
477        fn country_code_expansion() {
478            let lang_list = get_languages(Some("pt_BR"), None);
479            assert_eq!(lang_list, ["pt_BR", "pt", "en"]);
480        }
481
482        #[test]
483        fn ignore_posix_and_c() {
484            let lang_list = get_languages(Some("POSIX"), None);
485            assert_eq!(lang_list, ["en"]);
486            let lang_list = get_languages(Some("C"), None);
487            assert_eq!(lang_list, ["en"]);
488        }
489
490        #[test]
491        fn no_duplicates() {
492            let lang_list = get_languages(Some("de"), Some("fr:de:cn:de"));
493            assert_eq!(lang_list, ["fr", "de", "cn", "en"]);
494        }
495    }
496}