mirror of
https://github.com/tealdeer-rs/tealdeer.git
synced 2026-08-09 09:49:10 +02:00
Add an option to specify a custom config file to be used (#422)
Co-authored-by: Niklas Mohrin <dev@niklasmohrin.de>
This commit is contained in:
parent
769ef4da20
commit
9bb95ad11d
8 changed files with 236 additions and 72 deletions
|
|
@ -18,6 +18,7 @@ Options:
|
|||
-u, --update Update the local cache
|
||||
--no-auto-update If auto update is configured, disable it for this run
|
||||
-c, --clear-cache Clear the local cache
|
||||
--config-path <FILE> Override config file location
|
||||
--pager Use a pager to page output
|
||||
-r, --raw Display the raw markdown instead of rendering it
|
||||
-q, --quiet Suppress informational messages
|
||||
|
|
|
|||
|
|
@ -174,8 +174,8 @@ impl Cache {
|
|||
if let Ok(mtime) = metadata.modified() {
|
||||
let now = SystemTime::now();
|
||||
return now.duration_since(mtime).ok();
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@ pub(crate) struct Cli {
|
|||
#[arg(short = 'c', long = "clear-cache")]
|
||||
pub clear_cache: bool,
|
||||
|
||||
/// Override config file location
|
||||
#[arg(long = "config-path", value_name = "FILE")]
|
||||
pub config_path: Option<PathBuf>,
|
||||
|
||||
/// Use a pager to page output
|
||||
#[arg(long = "pager", requires = "command_or_file")]
|
||||
pub pager: bool,
|
||||
|
|
|
|||
146
src/config.rs
146
src/config.rs
|
|
@ -1,13 +1,13 @@
|
|||
use std::{
|
||||
env, fmt, fs,
|
||||
io::{Read, Write},
|
||||
env, fmt,
|
||||
fs::{self, File},
|
||||
io::{ErrorKind, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
use app_dirs::{get_app_root, AppDataType};
|
||||
use log::debug;
|
||||
use serde::Serialize as _;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use yansi::{Color, Style};
|
||||
|
|
@ -254,6 +254,14 @@ impl RawConfig {
|
|||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn load(mut config: impl Read) -> Result<RawConfig> {
|
||||
let mut content = String::new();
|
||||
config
|
||||
.read_to_string(&mut content)
|
||||
.context("Failed to read from config file")?;
|
||||
toml::from_str(&content).context("Failed to parse TOML config file")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RawConfig {
|
||||
|
|
@ -276,7 +284,7 @@ impl Default for RawConfig {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
|
||||
pub struct StyleConfig {
|
||||
pub description: Style,
|
||||
pub command_name: Style,
|
||||
|
|
@ -366,6 +374,7 @@ pub struct Config {
|
|||
pub display: DisplayConfig,
|
||||
pub updates: UpdatesConfig,
|
||||
pub directories: DirectoriesConfig,
|
||||
pub file_path: PathWithSource,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
|
@ -373,10 +382,14 @@ impl Config {
|
|||
///
|
||||
/// For this, some values need to be converted to other types and some
|
||||
/// defaults need to be set (sometimes based on env variables).
|
||||
fn from_raw(raw_config: RawConfig, relative_path_root: &Path) -> Result<Self> {
|
||||
fn from_raw(raw_config: 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 relative_path_root = config_file_path
|
||||
.path()
|
||||
.parent()
|
||||
.context("Failed to get config directory")?;
|
||||
|
||||
// Determine directories config. For this, we need to take some
|
||||
// additional factory into account, like env variables, or the
|
||||
|
|
@ -438,48 +451,48 @@ impl Config {
|
|||
display,
|
||||
updates,
|
||||
directories,
|
||||
file_path: config_file_path,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(enable_styles: bool) -> Result<Self> {
|
||||
debug!("Loading config");
|
||||
/// Load and read the config file from the given path into
|
||||
/// a [Config] and return it.
|
||||
///
|
||||
/// path: The path to the config file.
|
||||
pub fn load(path: &Path) -> Result<Self> {
|
||||
let raw_config = RawConfig::load(File::open(path)?)?;
|
||||
|
||||
let config = Self::from_raw(
|
||||
raw_config,
|
||||
PathWithSource {
|
||||
path: path.into(),
|
||||
source: PathSource::Cli,
|
||||
},
|
||||
)
|
||||
.context("Could not process raw config")?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Load and read the config file from the default path into
|
||||
/// a [Config] and return it.
|
||||
pub fn load_default_path() -> Result<Self> {
|
||||
// Determine path
|
||||
let (config_file_path, _) = get_config_path().context("Could not determine config path")?;
|
||||
let config_file_path =
|
||||
get_default_config_path().context("Could not determine config path")?;
|
||||
|
||||
// Load raw config
|
||||
let raw_config: RawConfig = if config_file_path.exists() && config_file_path.is_file() {
|
||||
let mut config_file = fs::File::open(&config_file_path).with_context(|| {
|
||||
format!("Failed to open config file path at {:?}", &config_file_path)
|
||||
})?;
|
||||
let mut contents = String::new();
|
||||
config_file.read_to_string(&mut contents).with_context(|| {
|
||||
format!("Failed to read from config file at {:?}", &config_file_path)
|
||||
})?;
|
||||
toml::from_str(&contents).with_context(|| {
|
||||
format!("Failed to parse TOML config file at {config_file_path:?}")
|
||||
})?
|
||||
} else {
|
||||
RawConfig::new()
|
||||
let raw_config = match File::open(config_file_path.path()) {
|
||||
Ok(file) => RawConfig::load(file)?,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => RawConfig::default(),
|
||||
Err(e) => {
|
||||
return Err(e).context(format!(
|
||||
"Failed to open config file at {}",
|
||||
config_file_path.path().display()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Safe to unwrap, it's a file path, so it should have a directory component
|
||||
let config_file_dir = config_file_path.parent().unwrap();
|
||||
|
||||
// Convert to config, resolve relative paths from the config file dir
|
||||
let mut config =
|
||||
Self::from_raw(raw_config, config_file_dir).context("Could not process raw config")?;
|
||||
|
||||
// Potentially override styles
|
||||
if !enable_styles {
|
||||
config.style = StyleConfig {
|
||||
command_name: Style::default(),
|
||||
description: Style::default(),
|
||||
example_text: Style::default(),
|
||||
example_code: Style::default(),
|
||||
example_variable: Style::default(),
|
||||
};
|
||||
}
|
||||
let config =
|
||||
Self::from_raw(raw_config, config_file_path).context("Could not process raw config")?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
|
@ -497,7 +510,7 @@ pub fn get_config_dir() -> Result<(PathBuf, PathSource)> {
|
|||
// $TEALDEER_CONFIG_DIR env variable.
|
||||
if let Ok(value) = env::var("TEALDEER_CONFIG_DIR") {
|
||||
return Ok((PathBuf::from(value), PathSource::EnvVar));
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, fall back to the user config directory.
|
||||
let dirs = get_app_root(AppDataType::UserConfig, &crate::APP_INFO)
|
||||
|
|
@ -509,29 +522,39 @@ pub fn get_config_dir() -> Result<(PathBuf, PathSource)> {
|
|||
///
|
||||
/// Note that this function does not verify whether the file at that location
|
||||
/// exists, or is a file.
|
||||
pub fn get_config_path() -> Result<(PathBuf, PathSource)> {
|
||||
pub fn get_default_config_path() -> Result<PathWithSource> {
|
||||
let (config_dir, source) = get_config_dir()?;
|
||||
let config_file_path = config_dir.join(CONFIG_FILE_NAME);
|
||||
Ok((config_file_path, source))
|
||||
Ok(PathWithSource {
|
||||
path: config_file_path,
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create default config file.
|
||||
pub fn make_default_config() -> Result<PathBuf> {
|
||||
let (config_dir, _) = get_config_dir()?;
|
||||
|
||||
// Ensure that config directory exists
|
||||
if config_dir.exists() {
|
||||
ensure!(
|
||||
config_dir.is_dir(),
|
||||
"Config directory could not be created: {} already exists but is not a directory",
|
||||
config_dir.to_string_lossy(),
|
||||
);
|
||||
/// path: Can be specified to create the config in that path instead of
|
||||
/// the default path.
|
||||
pub fn make_default_config(path: Option<&Path>) -> Result<PathBuf> {
|
||||
let config_file_path = if let Some(p) = path {
|
||||
p.into()
|
||||
} else {
|
||||
fs::create_dir_all(&config_dir).context("Could not create config directory")?;
|
||||
}
|
||||
let (config_dir, _) = get_config_dir()?;
|
||||
|
||||
// Ensure that config directory exists
|
||||
if config_dir.exists() {
|
||||
ensure!(
|
||||
config_dir.is_dir(),
|
||||
"Config directory could not be created: {} already exists but is not a directory",
|
||||
config_dir.to_string_lossy(),
|
||||
);
|
||||
} else {
|
||||
fs::create_dir_all(&config_dir).context("Could not create config directory")?;
|
||||
}
|
||||
|
||||
config_dir.join(CONFIG_FILE_NAME)
|
||||
};
|
||||
|
||||
// Ensure that a config file doesn't get overwritten
|
||||
let config_file_path = config_dir.join(CONFIG_FILE_NAME);
|
||||
ensure!(
|
||||
!config_file_path.is_file(),
|
||||
"A configuration file already exists at {}, no action was taken.",
|
||||
|
|
@ -544,7 +567,7 @@ pub fn make_default_config() -> Result<PathBuf> {
|
|||
|
||||
// Write default config
|
||||
let mut config_file =
|
||||
fs::File::create(&config_file_path).context("Could not create config file")?;
|
||||
File::create(&config_file_path).context("Could not create config file")?;
|
||||
let _wc = config_file
|
||||
.write(serialized_config.as_bytes())
|
||||
.context("Could not write to config file")?;
|
||||
|
|
@ -566,7 +589,14 @@ fn test_relative_path_resolution() {
|
|||
raw_config.directories.cache_dir = Some("../cache".into());
|
||||
raw_config.directories.custom_pages_dir = Some("../custom_pages".into());
|
||||
|
||||
let config = Config::from_raw(raw_config, Path::new("/path/to/config")).unwrap();
|
||||
let config = Config::from_raw(
|
||||
raw_config,
|
||||
PathWithSource {
|
||||
path: PathBuf::from("/path/to/config/config.toml"),
|
||||
source: PathSource::OsConvention,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config.directories.cache_dir.path(),
|
||||
|
|
|
|||
30
src/main.rs
30
src/main.rs
|
|
@ -36,6 +36,8 @@ use std::{
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use app_dirs::AppInfo;
|
||||
use clap::Parser;
|
||||
use config::StyleConfig;
|
||||
use log::debug;
|
||||
|
||||
mod cache;
|
||||
mod cli;
|
||||
|
|
@ -50,7 +52,7 @@ mod utils;
|
|||
use crate::{
|
||||
cache::{Cache, CacheFreshness, PageLookupResult, TLDR_PAGES_DIR},
|
||||
cli::Cli,
|
||||
config::{get_config_dir, get_config_path, make_default_config, Config, PathWithSource},
|
||||
config::{get_config_dir, make_default_config, Config, PathWithSource},
|
||||
extensions::Dedup,
|
||||
output::print_page,
|
||||
types::{ColorOptions, PlatformType},
|
||||
|
|
@ -153,10 +155,7 @@ fn show_paths(config: &Config) {
|
|||
}
|
||||
},
|
||||
);
|
||||
let config_path = get_config_path().map_or_else(
|
||||
|e| format!("[Error: {e}]"),
|
||||
|(path, _)| path.display().to_string(),
|
||||
);
|
||||
let config_path = config.file_path.to_string();
|
||||
let cache_dir = config.directories.cache_dir.to_string();
|
||||
let pages_dir = {
|
||||
let mut path = config.directories.cache_dir.path.clone();
|
||||
|
|
@ -175,8 +174,8 @@ fn show_paths(config: &Config) {
|
|||
println!("Custom pages dir: {custom_pages_dir}");
|
||||
}
|
||||
|
||||
fn create_config() -> Result<()> {
|
||||
let config_file_path = make_default_config().context("Could not create seed config")?;
|
||||
fn create_config(path: Option<&Path>) -> Result<()> {
|
||||
let config_file_path = make_default_config(path).context("Could not create seed config")?;
|
||||
eprintln!(
|
||||
"Successfully created seed config file here: {}",
|
||||
config_file_path.to_str().unwrap()
|
||||
|
|
@ -279,7 +278,18 @@ fn main() -> ExitCode {
|
|||
|
||||
fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> {
|
||||
// Look up config file, if none is found fall back to default config.
|
||||
let config = Config::load(enable_styles).context("Could not load config")?;
|
||||
debug!("Loading config");
|
||||
let mut config = match &args.config_path {
|
||||
Some(path) if !args.seed_config => {
|
||||
Config::load(path).context("Could not load config from given path")?
|
||||
}
|
||||
_ => Config::load_default_path().context("Could not load config from default path")?,
|
||||
};
|
||||
|
||||
// Override styles if needed
|
||||
if !enable_styles {
|
||||
config.style = StyleConfig::default();
|
||||
}
|
||||
|
||||
let custom_pages_dir = config
|
||||
.directories
|
||||
|
|
@ -313,7 +323,7 @@ fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> {
|
|||
|
||||
// Create a basic config and exit
|
||||
if args.seed_config {
|
||||
create_config()?;
|
||||
create_config(args.config_path.as_deref())?;
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
|
|
@ -345,7 +355,7 @@ fn try_main(args: Cli, enable_styles: bool) -> Result<ExitCode> {
|
|||
{
|
||||
// Cache is needed, but missing
|
||||
return Ok(ExitCode::FAILURE);
|
||||
};
|
||||
}
|
||||
|
||||
// List cached commands and exit
|
||||
if args.list {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ pub fn print_page(
|
|||
!config.display.compact,
|
||||
)
|
||||
.context("Could not write to stdout")?;
|
||||
};
|
||||
}
|
||||
|
||||
// We're done outputting data, flush stdout now!
|
||||
handle.flush().context("Could not flush stdout")?;
|
||||
|
|
|
|||
|
|
@ -205,6 +205,8 @@ pub enum PathSource {
|
|||
EnvVar,
|
||||
/// Config file
|
||||
ConfigFile,
|
||||
/// CLI argument override
|
||||
Cli,
|
||||
}
|
||||
|
||||
impl fmt::Display for PathSource {
|
||||
|
|
@ -216,6 +218,7 @@ impl fmt::Display for PathSource {
|
|||
Self::OsConvention => "OS convention",
|
||||
Self::EnvVar => "env variable",
|
||||
Self::ConfigFile => "config file",
|
||||
Self::Cli => "command line argument",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
118
tests/lib.rs
118
tests/lib.rs
|
|
@ -70,6 +70,24 @@ impl TestEnv {
|
|||
.expect("Failed to append to config file.");
|
||||
}
|
||||
|
||||
fn create_secondary_config(self) -> Self {
|
||||
self.append_to_secondary_config(format!(
|
||||
"directories.cache_dir = '{}'\n",
|
||||
self.cache_dir().to_str().unwrap(),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
fn append_to_secondary_config(&self, content: impl AsRef<str>) {
|
||||
File::options()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(self.config_dir().join("config-secondary.toml"))
|
||||
.expect("Failed to open config file")
|
||||
.write_all(content.as_ref().as_bytes())
|
||||
.expect("Failed to append to config file.");
|
||||
}
|
||||
|
||||
fn remove_initial_config(self) -> Self {
|
||||
let _ = fs::remove_file(self.config_dir().join("config.toml"));
|
||||
self
|
||||
|
|
@ -183,6 +201,61 @@ fn test_cannot_build_without_tls_feature() {
|
|||
let _ = TestEnv::new().no_default_features().command();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_the_correct_config() {
|
||||
let testenv = TestEnv::new()
|
||||
.install_default_cache()
|
||||
.create_secondary_config();
|
||||
testenv.append_to_secondary_config(include_str!("style-config.toml"));
|
||||
|
||||
let expected_default = include_str!("rendered/inkscape-default.expected");
|
||||
let expected_with_config = include_str!("rendered/inkscape-with-config.expected");
|
||||
|
||||
testenv
|
||||
.command()
|
||||
.args(["--color", "always", "inkscape-v2"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(diff(expected_default));
|
||||
|
||||
testenv
|
||||
.command()
|
||||
.args([
|
||||
"--color",
|
||||
"always",
|
||||
"--config-path",
|
||||
testenv
|
||||
.config_dir()
|
||||
.join("config-secondary.toml")
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"inkscape-v2",
|
||||
])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(diff(expected_with_config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fail_on_custom_config_path_is_directory() {
|
||||
let testenv = TestEnv::new();
|
||||
let error = if cfg!(windows) {
|
||||
"Access is denied"
|
||||
} else {
|
||||
"Is a directory"
|
||||
};
|
||||
testenv
|
||||
.command()
|
||||
.args([
|
||||
"--config-path",
|
||||
testenv.config_dir().to_str().unwrap(),
|
||||
"sl",
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains(error));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_cache() {
|
||||
TestEnv::new()
|
||||
|
|
@ -438,8 +511,9 @@ fn test_setup_seed_config() {
|
|||
.failure()
|
||||
.stderr(contains("A configuration file already exists"));
|
||||
|
||||
let testenv = testenv.remove_initial_config();
|
||||
assert!(testenv.config_dir().join("config.toml").is_file());
|
||||
|
||||
let testenv = testenv.remove_initial_config();
|
||||
testenv
|
||||
.command()
|
||||
.args(["--seed-config"])
|
||||
|
|
@ -448,6 +522,48 @@ fn test_setup_seed_config() {
|
|||
.stderr(contains("Successfully created seed config file here"));
|
||||
|
||||
assert!(testenv.config_dir().join("config.toml").is_file());
|
||||
|
||||
// Create parent directories as needed for the default config path.
|
||||
fs::remove_dir_all(testenv.config_dir()).unwrap();
|
||||
testenv
|
||||
.command()
|
||||
.args(["--seed-config"])
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(contains("Successfully created seed config file here"));
|
||||
|
||||
assert!(testenv.config_dir().join("config.toml").is_file());
|
||||
|
||||
// Write the default config to --config-path if specified by the user
|
||||
// at the same time.
|
||||
let custom_config_path = testenv.config_dir().join("config_custom.toml");
|
||||
testenv
|
||||
.command()
|
||||
.args([
|
||||
"--seed-config",
|
||||
"--config-path",
|
||||
custom_config_path.to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(contains("Successfully created seed config file here"));
|
||||
|
||||
assert!(custom_config_path.is_file());
|
||||
|
||||
// DON'T create parent directories for a custom config path.
|
||||
fs::remove_dir_all(testenv.config_dir()).unwrap();
|
||||
testenv
|
||||
.command()
|
||||
.args([
|
||||
"--seed-config",
|
||||
"--config-path",
|
||||
custom_config_path.to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("Could not create config file"));
|
||||
|
||||
assert!(!custom_config_path.is_file());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue