1#![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
59fn 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
74fn check_cache(_args: &Args, _enable_styles: bool) -> CheckCacheResult {
76 match Cache::freshness() {
77 CacheFreshness::Fresh => CheckCacheResult::CacheFound,
78 CacheFreshness::Stale(_) => {
80 CheckCacheResult::CacheMissing
89 }
90 CacheFreshness::Missing => {
91 CheckCacheResult::CacheMissing
106 }
107 }
108}
109
110fn 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
121async 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
132fn 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
145fn 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(""); 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(""); 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(""); 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
195fn 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 if env_lang.is_none() {
225 return vec!["en".to_string()];
226 }
227 let env_lang = env_lang.unwrap();
228
229 let locales = env_language.unwrap_or("").split(':').chain([env_lang]);
231
232 let mut lang_list = Vec::new();
233 for locale in locales {
234 if locale.len() >= 5 && locale.chars().nth(2) == Some('_') {
236 lang_list.push(&locale[..5]);
237 }
238 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 init_log();
280
281 let mut args = Args {
283 command: vec![cmd],
284 update: true,
285 quiet: true,
286 ..Args::default()
287 };
288
289 #[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 ColorOptions::Always => true,
297 ColorOptions::Auto => {
302 ansi_support && env::var_os("NO_COLOR").is_none() && atty::is(Stream::Stdout)
303 }
304 ColorOptions::Never => false,
306 };
307
308 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 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 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 if args.show_paths {
344 show_paths(&config);
345 }
346
347 if args.seed_config {
349 create_config_and_exit(enable_styles);
350 }
351
352 let platform: PlatformType = args.platform.unwrap_or_else(PlatformType::current);
354
355 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 let cache = Cache::new(ARCHIVE_URL, platform);
368
369 if args.clear_cache {
371 clear_cache(args.quiet, enable_styles);
372 }
373
374 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 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 if args.list {
396 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 eprintln!("{}", pages.join("\n"));
404 process::exit(0);
405 }
406
407 if !args.command.is_empty() {
409 let command = args.command.join("-").to_lowercase();
413
414 let languages = args
416 .language
417 .map_or_else(get_languages_from_env, |lang| vec![lang]);
418
419 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 } }
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}