mirror of
https://github.com/tealdeer-rs/tealdeer.git
synced 2026-08-27 02:20:40 +02:00
826 lines
26 KiB
Rust
826 lines
26 KiB
Rust
use std::{
|
|
env, fmt,
|
|
fs::{self, File},
|
|
io::{ErrorKind, Write},
|
|
path::{Path, PathBuf},
|
|
sync::LazyLock,
|
|
time::Duration,
|
|
};
|
|
|
|
use anyhow::{anyhow, bail, ensure, Context, Result};
|
|
use app_dirs::{get_app_root, AppDataType};
|
|
use clap::ValueEnum;
|
|
use serde::Serialize as _;
|
|
use serde_derive::{Deserialize, Serialize};
|
|
use yansi::{Color, Style};
|
|
|
|
use crate::{
|
|
extensions::Dedup as _,
|
|
types::{PathSource, PlatformType},
|
|
};
|
|
|
|
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
|
|
const SUPPORTED_TLS_BACKENDS: &[RawTlsBackend] = &[
|
|
#[cfg(feature = "native-tls")]
|
|
RawTlsBackend::NativeTls,
|
|
#[cfg(feature = "rustls-with-webpki-roots")]
|
|
RawTlsBackend::RustlsWithWebpkiRoots,
|
|
#[cfg(feature = "rustls-with-native-roots")]
|
|
RawTlsBackend::RustlsWithNativeRoots,
|
|
];
|
|
|
|
pub(crate) fn supported_tls_backends_string() -> String {
|
|
SUPPORTED_TLS_BACKENDS
|
|
.iter()
|
|
.map(std::string::ToString::to_string)
|
|
.collect::<Vec<String>>()
|
|
.join(", ")
|
|
}
|
|
|
|
fn default_underline() -> bool {
|
|
false
|
|
}
|
|
|
|
fn default_bold() -> bool {
|
|
false
|
|
}
|
|
|
|
fn default_italic() -> bool {
|
|
false
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum RawColor {
|
|
Black,
|
|
Red,
|
|
Green,
|
|
Yellow,
|
|
Blue,
|
|
Magenta,
|
|
Purple, // Backwards compatibility with ansi_term (until tealdeer 1.5.0)
|
|
Cyan,
|
|
White,
|
|
Ansi(u8),
|
|
Rgb { r: u8, g: u8, b: u8 },
|
|
}
|
|
|
|
impl From<RawColor> for Color {
|
|
fn from(raw_color: RawColor) -> Self {
|
|
match raw_color {
|
|
RawColor::Black => Self::Black,
|
|
RawColor::Red => Self::Red,
|
|
RawColor::Green => Self::Green,
|
|
RawColor::Yellow => Self::Yellow,
|
|
RawColor::Blue => Self::Blue,
|
|
RawColor::Magenta | RawColor::Purple => Self::Magenta,
|
|
RawColor::Cyan => Self::Cyan,
|
|
RawColor::White => Self::White,
|
|
RawColor::Ansi(num) => Self::Fixed(num),
|
|
RawColor::Rgb { r, g, b } => Self::Rgb(r, g, b),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
struct RawStyle {
|
|
pub foreground: Option<RawColor>,
|
|
pub background: Option<RawColor>,
|
|
#[serde(default = "default_underline")]
|
|
pub underline: bool,
|
|
#[serde(default = "default_bold")]
|
|
pub bold: bool,
|
|
#[serde(default = "default_italic")]
|
|
pub italic: bool,
|
|
}
|
|
|
|
#[allow(clippy::derivable_impls)] // Explicitly control defaults
|
|
impl Default for RawStyle {
|
|
fn default() -> Self {
|
|
Self {
|
|
foreground: None,
|
|
background: None,
|
|
underline: false,
|
|
bold: false,
|
|
italic: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<RawStyle> for Style {
|
|
fn from(raw_style: RawStyle) -> Self {
|
|
let mut style = Self::default();
|
|
|
|
if let Some(foreground) = raw_style.foreground {
|
|
style = style.fg(Color::from(foreground));
|
|
}
|
|
|
|
if let Some(background) = raw_style.background {
|
|
style = style.bg(Color::from(background));
|
|
}
|
|
|
|
if raw_style.underline {
|
|
style = style.underline();
|
|
}
|
|
|
|
if raw_style.bold {
|
|
style = style.bold();
|
|
}
|
|
|
|
if raw_style.italic {
|
|
style = style.italic();
|
|
}
|
|
|
|
style
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
struct RawStyleConfig {
|
|
#[serde(default)]
|
|
pub description: RawStyle,
|
|
#[serde(default)]
|
|
pub command_name: RawStyle,
|
|
#[serde(default)]
|
|
pub example_text: RawStyle,
|
|
#[serde(default)]
|
|
pub example_code: RawStyle,
|
|
#[serde(default)]
|
|
pub example_variable: RawStyle,
|
|
}
|
|
|
|
impl From<&RawStyleConfig> for StyleConfig {
|
|
fn from(raw_style_config: &RawStyleConfig) -> Self {
|
|
Self {
|
|
command_name: raw_style_config.command_name.into(),
|
|
description: raw_style_config.description.into(),
|
|
example_text: raw_style_config.example_text.into(),
|
|
example_code: raw_style_config.example_code.into(),
|
|
example_variable: raw_style_config.example_variable.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
struct RawDisplayConfig {
|
|
#[serde(default)]
|
|
pub compact: bool,
|
|
#[serde(default)]
|
|
pub use_pager: bool,
|
|
#[serde(default)]
|
|
pub show_title: bool,
|
|
}
|
|
|
|
impl From<&RawDisplayConfig> for DisplayConfig {
|
|
fn from(raw_display_config: &RawDisplayConfig) -> Self {
|
|
Self {
|
|
compact: raw_display_config.compact,
|
|
use_pager: raw_display_config.use_pager,
|
|
show_title: raw_display_config.show_title,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
|
|
fn default_archive_source() -> String {
|
|
"https://github.com/tldr-pages/tldr/releases/latest/download/".to_owned()
|
|
}
|
|
|
|
#[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,
|
|
#[serde(default = "default_archive_source")]
|
|
pub archive_source: String,
|
|
#[serde(default)]
|
|
pub tls_backend: RawTlsBackend,
|
|
#[serde(default)]
|
|
pub download_languages: Option<Vec<String>>,
|
|
}
|
|
|
|
impl Default for RawUpdatesConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
auto_update: false,
|
|
auto_update_interval_hours: DEFAULT_UPDATE_INTERVAL_HOURS,
|
|
archive_source: default_archive_source(),
|
|
tls_backend: RawTlsBackend::default(),
|
|
download_languages: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
struct RawDirectoriesConfig {
|
|
#[serde(default)]
|
|
pub cache_dir: Option<PathBuf>,
|
|
#[serde(default)]
|
|
pub custom_pages_dir: Option<PathBuf>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
enum RawPlatformType {
|
|
Current,
|
|
All,
|
|
MacOs, // alias for Platform(PlatformType::OsX)
|
|
#[serde(untagged)]
|
|
Platform(PlatformType),
|
|
}
|
|
|
|
impl RawPlatformType {
|
|
pub fn flatten(raw_platforms: impl IntoIterator<Item = Self>) -> Vec<PlatformType> {
|
|
let mut flattened = Vec::new();
|
|
for raw_platform in raw_platforms {
|
|
match raw_platform {
|
|
RawPlatformType::Current => flattened.push(PlatformType::current()),
|
|
RawPlatformType::Platform(platform) => flattened.push(platform),
|
|
RawPlatformType::MacOs => flattened.push(PlatformType::OsX),
|
|
RawPlatformType::All => flattened.extend(PlatformType::value_variants()),
|
|
}
|
|
}
|
|
flattened.clear_duplicates();
|
|
flattened
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
struct RawSearchConfig {
|
|
pub languages: Option<Vec<String>>,
|
|
pub platforms: Option<Vec<RawPlatformType>>,
|
|
}
|
|
|
|
impl<'a> From<&'a RawSearchConfig> for SearchConfig<'a> {
|
|
fn from(raw_search_config: &'a RawSearchConfig) -> Self {
|
|
let languages = raw_search_config
|
|
.languages
|
|
.as_ref()
|
|
.map_or_else(get_languages_from_env, |langs| {
|
|
langs.iter().map(|lang| Language(lang)).collect()
|
|
});
|
|
let platforms = if let Some(raw_platforms) = raw_search_config.platforms.as_ref() {
|
|
RawPlatformType::flatten(raw_platforms.iter().copied())
|
|
} else {
|
|
RawPlatformType::flatten([
|
|
RawPlatformType::Current,
|
|
RawPlatformType::Platform(PlatformType::Common),
|
|
RawPlatformType::All,
|
|
])
|
|
};
|
|
|
|
Self {
|
|
languages,
|
|
platforms,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(default)]
|
|
struct RawConfig {
|
|
style: RawStyleConfig,
|
|
display: RawDisplayConfig,
|
|
updates: RawUpdatesConfig,
|
|
directories: RawDirectoriesConfig,
|
|
search: RawSearchConfig,
|
|
}
|
|
|
|
impl Default for RawConfig {
|
|
fn default() -> Self {
|
|
let mut raw_config = RawConfig {
|
|
style: RawStyleConfig::default(),
|
|
display: RawDisplayConfig::default(),
|
|
updates: RawUpdatesConfig::default(),
|
|
directories: RawDirectoriesConfig::default(),
|
|
search: RawSearchConfig::default(),
|
|
};
|
|
|
|
// Set default config
|
|
raw_config.style.example_text.foreground = Some(RawColor::Green);
|
|
raw_config.style.command_name.foreground = Some(RawColor::Cyan);
|
|
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
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
|
|
pub struct StyleConfig {
|
|
pub description: Style,
|
|
pub command_name: Style,
|
|
pub example_text: Style,
|
|
pub example_code: Style,
|
|
pub example_variable: Style,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
pub struct DisplayConfig {
|
|
pub compact: bool,
|
|
pub use_pager: bool,
|
|
pub show_title: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct UpdatesConfig<'a> {
|
|
pub auto_update: bool,
|
|
pub auto_update_interval: Duration,
|
|
pub archive_source: &'a str,
|
|
pub tls_backend: TlsBackend,
|
|
pub download_languages: Vec<Language<'a>>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct PathWithSource {
|
|
pub path: PathBuf,
|
|
pub source: PathSource,
|
|
}
|
|
|
|
impl PathWithSource {
|
|
pub fn path(&self) -> &Path {
|
|
&self.path
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for PathWithSource {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{} ({})", self.path.display(), self.source)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct DirectoriesConfig {
|
|
pub cache_dir: PathWithSource,
|
|
pub custom_pages_dir: Option<PathWithSource>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct SearchConfig<'a> {
|
|
pub languages: Vec<Language<'a>>,
|
|
pub platforms: Vec<PlatformType>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct Language<'a>(pub &'a str);
|
|
|
|
fn get_languages<'a>(
|
|
env_lang: Option<&'a str>,
|
|
env_language: Option<&'a str>,
|
|
) -> Vec<Language<'a>> {
|
|
// Language list according to
|
|
// https://github.com/tldr-pages/tldr/blob/main/CLIENT-SPECIFICATION.md#language
|
|
|
|
let Some(env_lang) = env_lang else {
|
|
return vec![Language("en")];
|
|
};
|
|
|
|
// Create an iterator that contains $LANGUAGE (':' separated list) followed by $LANG (single language)
|
|
let locales = env_language.unwrap_or("").split(':').chain([env_lang]);
|
|
|
|
let mut lang_list = Vec::new();
|
|
for locale in locales {
|
|
// Language plus country code (e.g. `en_US`)
|
|
if locale.len() >= 5 && locale.chars().nth(2) == Some('_') {
|
|
lang_list.push(Language(&locale[..5]));
|
|
}
|
|
// Language code only (e.g. `en`)
|
|
if locale.len() >= 2 && locale != "POSIX" {
|
|
lang_list.push(Language(&locale[..2]));
|
|
}
|
|
}
|
|
|
|
lang_list.push(Language("en"));
|
|
lang_list.clear_duplicates();
|
|
lang_list
|
|
}
|
|
|
|
pub fn get_languages_from_env<'a>() -> Vec<Language<'a>> {
|
|
static LANG: LazyLock<Option<String>> = LazyLock::new(|| std::env::var("LANG").ok());
|
|
static LANGUAGE: LazyLock<Option<String>> = LazyLock::new(|| std::env::var("LANGUAGE").ok());
|
|
get_languages(
|
|
LANG.as_ref().map(String::as_str),
|
|
LANGUAGE.as_ref().map(String::as_str),
|
|
)
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub enum RawTlsBackend {
|
|
/// Native TLS (`SChannel` on Windows, Secure Transport on macOS and OpenSSL otherwise)
|
|
NativeTls,
|
|
/// Rustls with `WebPKI` roots.
|
|
RustlsWithWebpkiRoots,
|
|
/// Rustls with native roots.
|
|
RustlsWithNativeRoots,
|
|
}
|
|
|
|
impl Default for RawTlsBackend {
|
|
fn default() -> Self {
|
|
*SUPPORTED_TLS_BACKENDS.first().unwrap()
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for RawTlsBackend {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
self.serialize(f)
|
|
}
|
|
}
|
|
|
|
/// Allows choosing a `reqwest`'s TLS backend. Available TLS backends:
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
pub enum TlsBackend {
|
|
/// Native TLS (`SChannel` on Windows, Secure Transport on macOS and OpenSSL otherwise)
|
|
#[cfg(feature = "native-tls")]
|
|
NativeTls,
|
|
/// Rustls with `WebPKI` roots.
|
|
#[cfg(feature = "rustls-with-webpki-roots")]
|
|
RustlsWithWebpkiRoots,
|
|
/// Rustls with native roots.
|
|
#[cfg(feature = "rustls-with-native-roots")]
|
|
RustlsWithNativeRoots,
|
|
}
|
|
|
|
impl TryFrom<RawTlsBackend> for TlsBackend {
|
|
type Error = anyhow::Error;
|
|
|
|
fn try_from(raw: RawTlsBackend) -> Result<Self, Self::Error> {
|
|
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_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct Config<'a> {
|
|
pub style: StyleConfig,
|
|
pub display: DisplayConfig,
|
|
pub updates: UpdatesConfig<'a>,
|
|
pub directories: DirectoriesConfig,
|
|
pub search: SearchConfig<'a>,
|
|
pub file_path: PathWithSource,
|
|
}
|
|
|
|
impl<'a> Config<'a> {
|
|
/// Convert a `RawConfig` to a high-level `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: &'a RawConfig, config_file_path: PathWithSource) -> Result<Self> {
|
|
let style = (&raw_config.style).into();
|
|
let display = (&raw_config.display).into();
|
|
let search: SearchConfig<'a> = (&raw_config.search).into();
|
|
|
|
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()
|
|
.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
|
|
// user config.
|
|
let cache_dir_env_var = "TEALDEER_CACHE_DIR";
|
|
let cache_dir = if let Ok(env_var) = env::var(cache_dir_env_var) {
|
|
// For backwards compatibility reasons, the cache directory can be
|
|
// overridden using an env variable. This is deprecated and will be
|
|
// phased out in the future.
|
|
eprintln!("Warning: The ${cache_dir_env_var} env variable is deprecated, use the `cache_dir` option in the config file instead.");
|
|
PathWithSource {
|
|
path: PathBuf::from(env_var),
|
|
source: PathSource::EnvVar,
|
|
}
|
|
} else if let Some(config_value) = &raw_config.directories.cache_dir {
|
|
// If the user explicitly configured a cache directory, use that.
|
|
PathWithSource {
|
|
// Resolve possible relative path. It would be nicer to clean up the path, but Rust stdlib
|
|
// does not give any method for that that does not need the paths to exist.
|
|
path: relative_path_root.join(config_value),
|
|
source: PathSource::ConfigFile,
|
|
}
|
|
} else if let Ok(default_dir) = get_app_root(AppDataType::UserCache, &crate::APP_INFO) {
|
|
// Otherwise, fall back to the default user cache directory.
|
|
PathWithSource {
|
|
path: default_dir,
|
|
source: PathSource::OsConvention,
|
|
}
|
|
} else {
|
|
// If everything fails, give up
|
|
bail!("Could not determine user cache directory");
|
|
};
|
|
let custom_pages_dir = raw_config
|
|
.directories
|
|
.custom_pages_dir
|
|
.as_ref()
|
|
.map(|path| PathWithSource {
|
|
// Resolve possible relative path.
|
|
path: relative_path_root.join(path),
|
|
source: PathSource::ConfigFile,
|
|
})
|
|
.or_else(|| {
|
|
get_app_root(AppDataType::UserData, &crate::APP_INFO)
|
|
.map(|path| {
|
|
// Note: The `join("")` call ensures that there's a trailing slash
|
|
PathWithSource {
|
|
path: path.join("pages").join(""),
|
|
source: PathSource::OsConvention,
|
|
}
|
|
})
|
|
.ok()
|
|
});
|
|
let directories = DirectoriesConfig {
|
|
cache_dir,
|
|
custom_pages_dir,
|
|
};
|
|
|
|
Ok(Self {
|
|
style,
|
|
display,
|
|
updates,
|
|
directories,
|
|
search,
|
|
file_path: config_file_path,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// The [`ConfigLoader`] is used to load a [`Config`] from a file.
|
|
///
|
|
/// Since the rich [`Config`] keeps references to [`RawConfig`], the raw config needs to be kept alive outside of the
|
|
/// [`Config`]. The [`ConfigLoader`] thus offers the following flow:
|
|
/// 1. Read a raw config using [`ConfigLoader::read`] or [`ConfigLoader::read_default_path`].
|
|
/// 2. Validate the contents to a [`Config`] that borrows the [`ConfigLoader`].
|
|
pub struct ConfigLoader {
|
|
raw: RawConfig,
|
|
path: PathWithSource,
|
|
}
|
|
|
|
impl ConfigLoader {
|
|
fn read_internal(path: PathWithSource, allow_not_found: bool) -> Result<Self> {
|
|
match fs::read_to_string(&path.path) {
|
|
Ok(content) => Ok(Self {
|
|
raw: toml::from_str(&content).with_context(|| {
|
|
format!(
|
|
"Could not parse config file contents as toml from {}.",
|
|
path.path.display()
|
|
)
|
|
})?,
|
|
path,
|
|
}),
|
|
Err(e) if allow_not_found && e.kind() == ErrorKind::NotFound => Ok(Self {
|
|
raw: RawConfig::default(),
|
|
path,
|
|
}),
|
|
Err(e) => Err(e).context(format!(
|
|
"Could not read config file contents from {}.",
|
|
path.path().display()
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Create a loader that uses the config at `path`.
|
|
pub fn read(path: PathBuf) -> Result<Self> {
|
|
Self::read_internal(
|
|
PathWithSource {
|
|
path,
|
|
source: PathSource::Cli,
|
|
},
|
|
false,
|
|
)
|
|
}
|
|
|
|
/// Create a loader that uses the default config file location. If no file is present at the default location, the
|
|
/// default configuration is used.
|
|
pub fn read_default_path() -> Result<Self> {
|
|
let path = get_default_config_path().context("Could not determine default config path.")?;
|
|
Self::read_internal(path, true)
|
|
}
|
|
|
|
/// Parse the read [`RawConfig`] into a [`Config`].
|
|
pub fn load(&self) -> Result<Config<'_>> {
|
|
Config::from_raw(&self.raw, self.path.clone())
|
|
.context("Could not process raw config into rich config")
|
|
}
|
|
}
|
|
|
|
/// Return the path to the config directory.
|
|
///
|
|
/// The config dir path can be overridden using the `TEALDEER_CONFIG_DIR` env
|
|
/// variable. Otherwise, the user config directory is returned.
|
|
///
|
|
/// Note that this function does not verify whether the directory at that
|
|
/// location exists, or is a directory.
|
|
pub fn get_config_dir() -> Result<(PathBuf, PathSource)> {
|
|
// Allow overriding the config directory by setting the
|
|
// $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)
|
|
.context("Failed to determine the user config directory")?;
|
|
Ok((dirs, PathSource::OsConvention))
|
|
}
|
|
|
|
/// Return the path to the config file.
|
|
///
|
|
/// Note that this function does not verify whether the file at that location
|
|
/// exists, or is a file.
|
|
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(PathWithSource {
|
|
path: config_file_path,
|
|
source,
|
|
})
|
|
}
|
|
|
|
/// Create default config file.
|
|
/// 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 {
|
|
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
|
|
ensure!(
|
|
!config_file_path.is_file(),
|
|
"A configuration file already exists at {}, no action was taken.",
|
|
config_file_path.to_str().unwrap()
|
|
);
|
|
|
|
// Create default config
|
|
let serialized_config =
|
|
toml::to_string(&RawConfig::default()).context("Failed to serialize default config")?;
|
|
|
|
// Write default config
|
|
let mut 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")?;
|
|
|
|
Ok(config_file_path)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn serialize_deserialize() {
|
|
let raw_config = RawConfig::default();
|
|
let serialized = toml::to_string(&raw_config).unwrap();
|
|
let deserialized: RawConfig = toml::from_str(&serialized).unwrap();
|
|
assert_eq!(raw_config, deserialized);
|
|
}
|
|
|
|
#[test]
|
|
fn relative_path_resolution() {
|
|
let mut raw_config = RawConfig::default();
|
|
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,
|
|
PathWithSource {
|
|
path: PathBuf::from("/path/to/config/config.toml"),
|
|
source: PathSource::OsConvention,
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
config.directories.cache_dir.path(),
|
|
Path::new("/path/to/config/../cache")
|
|
);
|
|
assert_eq!(
|
|
config.directories.custom_pages_dir.unwrap().path(),
|
|
Path::new("/path/to/config/../custom_pages")
|
|
);
|
|
}
|
|
|
|
mod language {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn missing_lang_env() {
|
|
let lang_list = get_languages(None, Some("de:fr"));
|
|
assert_eq!(lang_list, [Language("en")]);
|
|
let lang_list = get_languages(None, None);
|
|
assert_eq!(lang_list, [Language("en")]);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_language_env() {
|
|
let lang_list = get_languages(Some("de"), None);
|
|
assert_eq!(lang_list, [Language("de"), Language("en")]);
|
|
}
|
|
|
|
#[test]
|
|
fn preference_order() {
|
|
let lang_list = get_languages(Some("de"), Some("fr:cn"));
|
|
assert_eq!(
|
|
lang_list,
|
|
[
|
|
Language("fr"),
|
|
Language("cn"),
|
|
Language("de"),
|
|
Language("en")
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn country_code_expansion() {
|
|
let lang_list = get_languages(Some("pt_BR"), None);
|
|
assert_eq!(
|
|
lang_list,
|
|
[Language("pt_BR"), Language("pt"), Language("en")]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn with_encoding() {
|
|
let lang_list = get_languages(Some("de_DE.UTF-8"), None);
|
|
assert_eq!(
|
|
lang_list,
|
|
[Language("de_DE"), Language("de"), Language("en")]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ignore_posix_and_c() {
|
|
let lang_list = get_languages(Some("POSIX"), None);
|
|
assert_eq!(lang_list, [Language("en")]);
|
|
let lang_list = get_languages(Some("C"), None);
|
|
assert_eq!(lang_list, [Language("en")]);
|
|
}
|
|
|
|
#[test]
|
|
fn no_duplicates() {
|
|
let lang_list = get_languages(Some("de"), Some("fr:de:cn:de"));
|
|
assert_eq!(
|
|
lang_list,
|
|
[
|
|
Language("fr"),
|
|
Language("de"),
|
|
Language("cn"),
|
|
Language("en")
|
|
]
|
|
);
|
|
}
|
|
}
|
|
}
|