diff --git a/Cargo.lock b/Cargo.lock index ad45566..7aaf94f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,7 +194,7 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", @@ -996,7 +996,7 @@ name = "tar" version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "filetime 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "filetime 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", "xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1012,6 +1012,7 @@ dependencies = [ "docopt 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "escargot 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "filetime 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", "flate2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "pager 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1022,7 +1023,6 @@ dependencies = [ "tar 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "utime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "xdg 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1180,16 +1180,6 @@ dependencies = [ "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "utime" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "vcpkg" version = "0.2.8" @@ -1420,7 +1410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum errno 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c2a071601ed01b988f896ab14b95e67335d1eeb50190932a1320f7fe3cadc84e" "checksum errno-dragonfly 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "14ca354e36190500e1e1fb267c647932382b54053c50b14970856c0b00a35067" "checksum escargot 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "19db1f7e74438642a5018cdf263bb1325b2e792f02dd0a3ca6d6c0f0d7b1d5a5" -"checksum filetime 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1ff6d4dab0aa0c8e6346d46052e93b13a16cf847b54ed357087c35011048cc7d" +"checksum filetime 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "affc17579b132fc2461adf7c575cc6e8b134ebca52c51f5411388965227dc695" "checksum flate2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6bd6d6f4752952feb71363cffc9ebac9411b75b87c6ab6058c40c8900cf43c0f" "checksum float-cmp 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "da62c4f1b81918835a8c6a484a397775fff5953fe83529afd51b05f5c6a6617d" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" @@ -1533,7 +1523,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" "checksum url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" -"checksum utime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "055058552ca15c566082fc61da433ae678f78986a6f16957e33162d1b218792a" "checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" "checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" diff --git a/Cargo.toml b/Cargo.toml index 5af42ff..efd4553 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ assert_cmd = "0.10" escargot = "0.3" predicates = "1.0" tempdir = "^0.3" -utime = "0.2.0" +filetime = "0.2.10" [features] logging = ["env_logger"] diff --git a/README.md b/README.md index a527d6a..3f6857b 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,31 @@ Set this to enforce more compact output, where empty lines are stripped out compact = true +### Automatic updates + +tealdeer can refresh the cache automatically when it is outdated. This +behavior can be configured in the `updates` section and is disabled by +default. + +#### `auto_update` + +Specifies whether the auto-update feature should be enabled (defaults to +`false`). + + [updates] + auto_update = true + +#### `auto_update_interval_hours` + +Duration, since the last cache update, after which the cache will be +refreshed (defaults to 720 hours). This parameter is ignored if `auto_update` +is set to `false`. + + [updates] + auto_update = true + auto_update_interval_hours = 24 + + ## Autocompletion - *Bash*: copy `bash_tealdeer` to `/usr/share/bash-completion/completions/tldr` diff --git a/src/config.rs b/src/config.rs index cfa810c..51d9b28 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,7 @@ use std::env; use std::fs; use std::io::{Error as IoError, Read, Write}; use std::path::PathBuf; +use std::time::Duration; use ansi_term::{Color, Style}; use app_dirs::{get_app_root, AppDataType}; @@ -12,6 +13,8 @@ use toml; use crate::error::TealdeerError::{self, ConfigError}; pub const CONFIG_FILE_NAME: &str = "config.toml"; +pub const MAX_CACHE_AGE: Duration = Duration::from_secs(2_592_000); // 30 days +const DEFAULT_UPDATE_INTERVAL_HOURS: u64 = MAX_CACHE_AGE.as_secs() / 3600; // 30 days fn default_underline() -> bool { false @@ -116,12 +119,38 @@ struct RawDisplayConfig { pub use_pager: bool, } +/// Serde doesn't support default values yet (tracking issue: +/// https://github.com/serde-rs/serde/issues/368), so we need to wrap DEFAULT_UPDATE_INTERVAL_HOURS +/// in a function to be able to use #[serde(default = ...)] +const fn default_auto_update_interval_hours() -> u64 { + DEFAULT_UPDATE_INTERVAL_HOURS +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +struct RawUpdatesConfig { + #[serde(default)] + pub auto_update: bool, + #[serde(default = "default_auto_update_interval_hours")] + pub auto_update_interval_hours: u64, +} + +impl Default for RawUpdatesConfig { + fn default() -> Self { + Self { + auto_update: false, + auto_update_interval_hours: DEFAULT_UPDATE_INTERVAL_HOURS, + } + } +} + #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] struct RawConfig { #[serde(default)] style: RawStyleConfig, #[serde(default)] display: RawDisplayConfig, + #[serde(default)] + updates: RawUpdatesConfig, } impl RawConfig { @@ -134,7 +163,6 @@ impl RawConfig { raw_config.style.example_code.foreground = Some(RawColor::Cyan); raw_config.style.example_variable.foreground = Some(RawColor::Cyan); raw_config.style.example_variable.underline = true; - raw_config.display.use_pager = false; raw_config } @@ -155,10 +183,17 @@ pub struct DisplayConfig { pub use_pager: bool, } +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct UpdatesConfig { + pub auto_update: bool, + pub auto_update_interval: Duration, +} + #[derive(Copy, Clone, Debug, PartialEq)] pub struct Config { pub style: StyleConfig, pub display: DisplayConfig, + pub updates: UpdatesConfig, } impl From for Config { @@ -175,6 +210,12 @@ impl From for Config { compact: raw_config.display.compact, use_pager: raw_config.display.use_pager, }, + updates: UpdatesConfig { + auto_update: raw_config.updates.auto_update, + auto_update_interval: Duration::from_secs( + raw_config.updates.auto_update_interval_hours * 3600, + ), + }, } } } diff --git a/src/main.rs b/src/main.rs index 38f646e..3d8622d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,6 @@ use std::io::BufReader; use std::io::BufRead; use std::path::{Path, PathBuf}; use std::process; -use std::time::Duration; use ansi_term::Color; use app_dirs::AppInfo; @@ -38,7 +37,7 @@ mod tokenizer; mod types; use crate::cache::Cache; -use crate::config::{get_config_path, make_default_config, Config}; +use crate::config::{get_config_path, make_default_config, Config, MAX_CACHE_AGE}; use crate::error::TealdeerError::{CacheError, ConfigError, UpdateError}; use crate::formatter::print_lines; use crate::tokenizer::Tokenizer; @@ -86,7 +85,6 @@ To render a local file (for testing): $ tldr --render /path/to/file.md "; const ARCHIVE_URL: &str = "https://github.com/tldr-pages/tldr/archive/master.tar.gz"; -const MAX_CACHE_AGE: Duration = Duration::from_secs(2_592_000); // 30 days #[cfg(not(target_os = "windows"))] const PAGER_COMMAND: &str = "less -R"; @@ -108,24 +106,11 @@ struct Args { } /// Print page by path -fn print_page(path: &Path, enable_markdown: bool, enable_styles: bool) -> Result<(), String> { +fn print_page(path: &Path, enable_markdown: bool, config: &Config) -> Result<(), String> { // Open file let file = File::open(path).map_err(|msg| format!("Could not open file: {}", msg))?; let reader = BufReader::new(file); - // Look up config file, if none is found fall back to default config. - let config = match Config::load(enable_styles) { - Ok(config) => config, - Err(ConfigError(msg)) => { - eprintln!("Could not load config: {}", msg); - process::exit(1); - } - Err(e) => { - eprintln!("Could not load config: {}", e); - process::exit(1); - } - }; - if enable_markdown { // Print the raw markdown of the file. for line in reader.lines() { @@ -142,59 +127,42 @@ fn print_page(path: &Path, enable_markdown: bool, enable_styles: bool) -> Result /// Set up display pager #[cfg(not(target_os = "windows"))] -fn configure_pager(args: &Args, enable_styles: bool) { - // Flags have precedence - if args.flag_pager { - Pager::with_default_pager(PAGER_COMMAND).setup(); - return; - } - - // Then check config - let config = match Config::load(enable_styles) { - Ok(config) => config, - Err(ConfigError(msg)) => { - eprintln!("Could not load config: {}", msg); - process::exit(1); - } - Err(e) => { - eprintln!("Could not load config: {}", e); - process::exit(1); - } - }; - - if config.display.use_pager { - Pager::with_default_pager(PAGER_COMMAND).setup(); - } +fn configure_pager() { + Pager::with_default_pager(PAGER_COMMAND).setup(); } #[cfg(target_os = "windows")] -fn configure_pager(_args: &Args, _enable_styles: bool) { +fn configure_pager() { eprintln!("Warning: -p / --pager flag not available on Windows!"); } +fn should_update_cache(args: &Args, config: &Config) -> bool { + args.flag_update + || (config.updates.auto_update + && Cache::last_update().map_or(true, |ago| ago >= config.updates.auto_update_interval)) +} + /// Check the cache for freshness fn check_cache(args: &Args) { - if !args.flag_update { - match Cache::last_update() { - Some(ago) if ago > MAX_CACHE_AGE => { - if args.flag_quiet { - return; - } - eprintln!( - "{}", - Color::Yellow.paint(format!( - "The cache hasn't been updated for more than {} days.\n\ + match Cache::last_update() { + Some(ago) if ago > MAX_CACHE_AGE => { + if args.flag_quiet { + return; + } + eprintln!( + "{}", + Color::Yellow.paint(format!( + "The cache hasn't been updated for more than {} days.\n\ You should probably run `tldr --update` soon.", - MAX_CACHE_AGE.as_secs() / 24 / 3600 - )) - ); - } - None => { - eprintln!("Cache not found. Please run `tldr --update`."); - process::exit(1); - } - _ => {} + MAX_CACHE_AGE.as_secs() / 24 / 3600 + )) + ); } + None => { + eprintln!("Cache not found. Please run `tldr --update`."); + process::exit(1); + } + _ => {} }; } @@ -320,14 +288,38 @@ fn main() { process::exit(0); } + // Show config file and path, pass through + if args.flag_config_path { + show_config_path(); + } + + // Create a basic config and exit + if args.flag_seed_config { + create_config_and_exit(); + } + // Determine the usage of styles #[cfg(target_os = "windows")] let enable_styles = ansi_term::enable_ansi_support().is_ok(); #[cfg(not(target_os = "windows"))] let enable_styles = true; - // Configure pager - configure_pager(&args, enable_styles); + // Look up config file, if none is found fall back to default config. + let config = match Config::load(enable_styles) { + Ok(config) => config, + Err(ConfigError(msg)) => { + eprintln!("Could not load config: {}", msg); + process::exit(1); + } + Err(e) => { + eprintln!("Could not load config: {}", e); + process::exit(1); + } + }; + + if args.flag_pager || config.display.use_pager { + configure_pager(); + } // Specify target OS let os: OsType = match args.flag_os { @@ -344,24 +336,17 @@ fn main() { } // Update cache, pass through - if args.flag_update { + let cache_updated = if should_update_cache(&args, &config) { update_cache(&cache, args.flag_quiet); - } - - // Show config file and path, pass through - if args.flag_config_path { - show_config_path(); - } - - // Create a basic config and exit - if args.flag_seed_config { - create_config_and_exit(); - } + true + } else { + false + }; // Render local file and exit if let Some(ref file) = args.flag_render { let path = PathBuf::from(file); - if let Err(msg) = print_page(&path, args.flag_markdown, enable_styles) { + if let Err(msg) = print_page(&path, args.flag_markdown, &config) { eprintln!("{}", msg); process::exit(1); } else { @@ -371,8 +356,10 @@ fn main() { // List cached commands and exit if args.flag_list { - // Check cache for freshness - check_cache(&args); + if !cache_updated { + // Check cache for freshness + check_cache(&args); + } // Get list of pages let pages = cache.list_pages().unwrap_or_else(|e| { @@ -392,12 +379,15 @@ fn main() { // Show command from cache if let Some(ref command) = args.arg_command { let command = command.join("-"); - // Check cache for freshness - check_cache(&args); + + if !cache_updated { + // Check cache for freshness + check_cache(&args); + } // Search for command in cache if let Some(path) = cache.find_page(&command) { - if let Err(msg) = print_page(&path, args.flag_markdown, enable_styles) { + if let Err(msg) = print_page(&path, args.flag_markdown, &config) { eprintln!("{}", msg); process::exit(1); } else { diff --git a/tests/config.toml b/tests/config.toml index c3a9cd6..d700672 100644 --- a/tests/config.toml +++ b/tests/config.toml @@ -19,3 +19,7 @@ bold = false [display] use_pager = false compact = false + +[updates] +auto_update = false +auto_update_interval_hours = 720 diff --git a/tests/lib.rs b/tests/lib.rs index 42c4c43..5f33f28 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -2,13 +2,14 @@ extern crate assert_cmd; extern crate escargot; +extern crate filetime; extern crate predicates; extern crate tempdir; -extern crate utime; use std::fs::{create_dir_all, File}; use std::io::Write; use std::process::Command; +use std::time::{Duration, SystemTime}; use assert_cmd::prelude::*; use tempdir::TempDir; @@ -160,7 +161,11 @@ fn test_quiet_old_cache() { .success() .stdout(is_empty()); - let _ = utime::set_file_times(testenv.cache_dir.path().join("tldr-master"), 1, 1).unwrap(); + filetime::set_file_mtime( + testenv.cache_dir.path().join("tldr-master"), + filetime::FileTime::from_unix_time(1, 0), + ) + .unwrap(); testenv .command() @@ -354,3 +359,59 @@ fn test_list_flag_rendering() { .success() .stdout("bar\nbaz\nfoo\nqux\n"); } + +#[test] +fn test_autoupdate_cache() { + let testenv = TestEnv::new(); + + // The first time, if automatic updates are disabled, the cache should not be found + testenv + .command() + .args(&["--list"]) + .assert() + .failure() + .stderr(contains("Cache not found. Please run `tldr --update`.")); + + let config_file_path = testenv.config_dir.path().join("config.toml"); + let cache_file_path = testenv.cache_dir.path().join("tldr-master"); + + // Activate automatic updates, set the auto-update interval to 24 hours + let mut config_file = File::create(&config_file_path).unwrap(); + config_file + .write("[updates]\nauto_update = true\nauto_update_interval_hours = 24".as_bytes()) + .unwrap(); + config_file.flush().unwrap(); + + // Helper function that runs `tldr --list` and asserts that the cache is automatically updated + // or not, depending on the value of `expected`. + let check_cache_updated = |expected| { + let assert = testenv.command().args(&["--list"]).assert().success(); + let pred = contains("Successfully updated cache"); + if expected { + assert.stdout(pred) + } else { + assert.stdout(pred.not()) + }; + }; + + // The cache is updated the first time we run `tldr --list` + check_cache_updated(true); + + // The cache is not updated with a subsequent call + check_cache_updated(false); + + // We update the modification and access times such that they are about 23 hours from now. + // auto-update interval is 24 hours, the cache should not be updated + let new_mtime = SystemTime::now() - Duration::from_secs(82_800); + filetime::set_file_mtime(&cache_file_path, new_mtime.into()).unwrap(); + check_cache_updated(false); + + // We update the modification and access times such that they are about 25 hours from now. + // auto-update interval is 24 hours, the cache should be updated + let new_mtime = SystemTime::now() - Duration::from_secs(90_000); + filetime::set_file_mtime(&cache_file_path, new_mtime.into()).unwrap(); + check_cache_updated(true); + + // The cache is not updated with a subsequent call + check_cache_updated(false); +}