From 4c55f785007d21ec29c1c7505cbd5b880c63e3a1 Mon Sep 17 00:00:00 2001 From: Giftzwerg02 Date: Sun, 11 Jan 2026 23:22:31 +0100 Subject: [PATCH 01/11] added --override-config cli flag --- src/cli.rs | 4 ++ src/config.rs | 175 +++++++++++++++++++++++++++++++++++++++++++++++--- src/main.rs | 4 +- 3 files changed, 173 insertions(+), 10 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index d461a3e..9092a09 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -78,6 +78,10 @@ pub(crate) struct Cli { #[arg(long = "config-path", value_name = "FILE")] pub config_path: Option, + /// Override config values, also overrides values set in the config file (e.g. `'updates.auto_update = true'`) + #[arg(long = "override-config")] + pub override_config: Option, + /// Use a pager to page output #[arg(long = "pager", requires = "command_or_file")] pub pager: bool, diff --git a/src/config.rs b/src/config.rs index e5e85e8..2279792 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,6 +3,7 @@ use std::{ fs::{self, File}, io::{ErrorKind, Write}, path::{Path, PathBuf}, + str::FromStr, sync::LazyLock, time::Duration, }; @@ -583,9 +584,33 @@ pub struct ConfigLoader { } impl ConfigLoader { - fn read_internal(path: PathWithSource, allow_not_found: bool) -> Result { - match fs::read_to_string(&path.path) { - Ok(content) => Ok(Self { + fn read_internal( + path: PathWithSource, + allow_not_found: bool, + override_str: Option, + ) -> Result { + match (fs::read_to_string(&path.path), override_str) { + (Ok(content), Some(override_str)) => { + let mut config_table = toml::Table::from_str(&content).with_context(|| { + format!( + "Could not parse config file contents as toml from {}.", + path.path.display() + ) + })?; + + let override_config_table = + toml::Table::from_str(&override_str).with_context(|| { + format!("Could not parse override-config string as toml: {override_str}") + })?; + + Self::override_config_with(&mut config_table, override_config_table); + + Ok(Self { + raw: config_table.try_into()?, + path, + }) + } + (Ok(content), None) => Ok(Self { raw: toml::from_str(&content).with_context(|| { format!( "Could not parse config file contents as toml from {}.", @@ -594,33 +619,63 @@ impl ConfigLoader { })?, path, }), - Err(e) if allow_not_found && e.kind() == ErrorKind::NotFound => Ok(Self { + (Err(e), Some(override_str)) if allow_not_found && e.kind() == ErrorKind::NotFound => { + let default_override_config = toml::from_str(&override_str).with_context(|| { + format!("Could not parse override-config string as toml: {override_str}") + })?; + Ok(Self { + raw: default_override_config, + path, + }) + } + (Err(e), None) if allow_not_found && e.kind() == ErrorKind::NotFound => Ok(Self { raw: RawConfig::default(), path, }), - Err(e) => Err(e).context(format!( + (Err(e), _) => Err(e).context(format!( "Could not read config file contents from {}.", path.path().display() )), } } + fn override_config_with(config_table: &mut toml::Table, override_config_table: toml::Table) { + for (key, override_value) in override_config_table { + let Some(entry) = config_table.get_mut(&key) else { + config_table.insert(key, override_value); + continue; + }; + + match (entry, override_value) { + (toml::Value::Table(entry_table), toml::Value::Table(override_table)) => { + Self::override_config_with(entry_table, override_table); + } + (entry, override_value) => { + *entry = override_value; + } + } + } + } + /// Create a loader that uses the config at `path`. - pub fn read(path: PathBuf) -> Result { + /// `override_str`: If set, overrides the default value of the config + pub fn read(path: PathBuf, override_str: Option) -> Result { Self::read_internal( PathWithSource { path, source: PathSource::Cli, }, false, + override_str, ) } /// 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 { + /// `override_str`: If set, overrides the default value of the config + pub fn read_default_path(override_str: Option) -> Result { let path = get_default_config_path().context("Could not determine default config path.")?; - Self::read_internal(path, true) + Self::read_internal(path, true, override_str) } /// Parse the read [`RawConfig`] into a [`Config`]. @@ -744,6 +799,110 @@ mod test { ); } + mod override_config { + use super::*; + + fn base_config() -> toml::Table { + toml::Table::from_str( + " + global_value = false + + [some] + value = 0 + + [some.inner] + value1 = 1 + value2 = \"a string\" + + [some.other] + value1 = 3 + value2 = [ 1, \"text\", true ] + ", + ) + .unwrap() + } + + #[test] + fn add_value() { + let mut original_config = base_config(); + let override_config = toml::Table::from_str("some.new.value = 'new text'").unwrap(); + + ConfigLoader::override_config_with(&mut original_config, override_config); + + assert_eq!( + original_config["some"]["value"].as_integer().unwrap(), + 0 + ); + + assert_eq!( + original_config["some"]["inner"]["value1"].as_integer().unwrap(), + 1 + ); + + assert_eq!( + original_config["some"]["inner"]["value2"].as_str().unwrap(), + "a string" + ); + + assert_eq!( + original_config["some"]["other"]["value1"] + .as_integer() + .unwrap(), + 3, + ); + + let v2array = original_config["some"]["other"]["value2"] + .as_array() + .unwrap(); + assert_eq!(v2array[0].as_integer().unwrap(), 1); + assert_eq!(v2array[1].as_str().unwrap(), "text"); + assert_eq!(v2array[2].as_bool().unwrap(), true); + + assert_eq!(original_config["global_value"].as_bool().unwrap(), false); + + assert_eq!(original_config["some"]["new"]["value"].as_str().unwrap(), "new text"); + } + + #[test] + fn change_value() { + let mut original_config = base_config(); + let override_config = toml::Table::from_str("some.inner.value1 = 'some text'").unwrap(); + + ConfigLoader::override_config_with(&mut original_config, override_config); + + assert_eq!( + original_config["some"]["value"].as_integer().unwrap(), + 0 + ); + + assert_eq!( + original_config["some"]["inner"]["value1"].as_str().unwrap(), + "some text" + ); + + assert_eq!( + original_config["some"]["inner"]["value2"].as_str().unwrap(), + "a string" + ); + + assert_eq!( + original_config["some"]["other"]["value1"] + .as_integer() + .unwrap(), + 3, + ); + + let v2array = original_config["some"]["other"]["value2"] + .as_array() + .unwrap(); + assert_eq!(v2array[0].as_integer().unwrap(), 1); + assert_eq!(v2array[1].as_str().unwrap(), "text"); + assert_eq!(v2array[2].as_bool().unwrap(), true); + + assert_eq!(original_config["global_value"].as_bool().unwrap(), false); + } + } + mod language { use super::*; diff --git a/src/main.rs b/src/main.rs index a0cd593..1c18646 100644 --- a/src/main.rs +++ b/src/main.rs @@ -205,10 +205,10 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { debug!("Loading config"); let config_loader = match &args.config_path { Some(path) if !args.seed_config => { - ConfigLoader::read(path.clone()).context("Could not read config from given path")? + ConfigLoader::read(path.clone(), args.override_config).context("Could not read config from given path")? } _ => { - ConfigLoader::read_default_path().context("Could not read config from default path")? + ConfigLoader::read_default_path(args.override_config).context("Could not read config from default path")? } }; let mut config = config_loader.load()?; From cfb9c2b38664178388d9db78b238e37fb41c8821 Mon Sep 17 00:00:00 2001 From: Giftzwerg02 Date: Sat, 7 Feb 2026 16:38:53 +0100 Subject: [PATCH 02/11] changed override-config to manual key=value parsing --- src/cli.rs | 7 ++- src/config.rs | 154 ++++++++++++++++++++++---------------------------- 2 files changed, 71 insertions(+), 90 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 9092a09..f04ef2c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -79,8 +79,11 @@ pub(crate) struct Cli { pub config_path: Option, /// Override config values, also overrides values set in the config file (e.g. `'updates.auto_update = true'`) - #[arg(long = "override-config")] - pub override_config: Option, + #[arg( + long = "override-config", + action = ArgAction::Append, + )] + pub override_config: Option>, /// Use a pager to page output #[arg(long = "pager", requires = "command_or_file")] diff --git a/src/config.rs b/src/config.rs index 2279792..57d7a63 100644 --- a/src/config.rs +++ b/src/config.rs @@ -587,23 +587,19 @@ impl ConfigLoader { fn read_internal( path: PathWithSource, allow_not_found: bool, - override_str: Option, + overrides: Option>, ) -> Result { - match (fs::read_to_string(&path.path), override_str) { - (Ok(content), Some(override_str)) => { - let mut config_table = toml::Table::from_str(&content).with_context(|| { + match (fs::read_to_string(&path.path), overrides) { + (Ok(content), Some(overrides)) => { + let config: RawConfig = toml::from_str(&content).with_context(|| { format!( "Could not parse config file contents as toml from {}.", path.path.display() ) })?; - let override_config_table = - toml::Table::from_str(&override_str).with_context(|| { - format!("Could not parse override-config string as toml: {override_str}") - })?; - - Self::override_config_with(&mut config_table, override_config_table); + let config_table = toml::Table::try_from(config)?; + let config_table = Self::override_config_with(config_table, overrides)?; Ok(Self { raw: config_table.try_into()?, @@ -619,12 +615,15 @@ impl ConfigLoader { })?, path, }), - (Err(e), Some(override_str)) if allow_not_found && e.kind() == ErrorKind::NotFound => { - let default_override_config = toml::from_str(&override_str).with_context(|| { - format!("Could not parse override-config string as toml: {override_str}") - })?; + (Err(e), Some(overrides)) if allow_not_found && e.kind() == ErrorKind::NotFound => { + // In order to override the default config we first need to generate the default + // RawConfig and then serialize it into the toml-Table variant + let default_config_table = RawConfig::default(); + let default_config_table = toml::Table::try_from(default_config_table)?; + let config_table = Self::override_config_with(default_config_table, overrides)?; + Ok(Self { - raw: default_override_config, + raw: config_table.try_into()?, path, }) } @@ -639,43 +638,65 @@ impl ConfigLoader { } } - fn override_config_with(config_table: &mut toml::Table, override_config_table: toml::Table) { - for (key, override_value) in override_config_table { - let Some(entry) = config_table.get_mut(&key) else { - config_table.insert(key, override_value); - continue; - }; + fn override_config_with( + mut config_table: toml::Table, + overrides: Vec, + ) -> Result { + for override_str in overrides { + let (name, value) = override_str + .split_once('=') + .ok_or(anyhow!("Invalid override-string: {override_str}"))?; - match (entry, override_value) { - (toml::Value::Table(entry_table), toml::Value::Table(override_table)) => { - Self::override_config_with(entry_table, override_table); - } - (entry, override_value) => { - *entry = override_value; - } + let name = name.trim(); + + let mut keypath = name.split('.'); + let key_start = keypath + .next() + .expect("split returns always at least one element"); + let mut entry = config_table.get_mut(key_start).ok_or(anyhow!( + "\"{name}\" is not a valid key starting at \"{key_start}\"" + ))?; + + for subkey in keypath { + let toml::Value::Table(ref mut entry_table) = entry else { + bail!("\"{name}\" is not a valid identifier since \"{subkey}\" already refers to a value which is not a toml-Table."); + }; + + entry = entry_table.get_mut(subkey).ok_or(anyhow!( + "\"{name}\" is not a valid key starting at \"{subkey}\"" + ))?; } + + let ugly_intermediate_key = "ugly_intermediate"; + let value = format!("{ugly_intermediate_key} = {value}"); + let mut value = toml::Value::from_str(&value)?; + let value = value.as_table_mut().unwrap(); + let value = value.remove(ugly_intermediate_key).unwrap(); + + *entry = value; } + Ok(config_table) } /// Create a loader that uses the config at `path`. - /// `override_str`: If set, overrides the default value of the config - pub fn read(path: PathBuf, override_str: Option) -> Result { + /// `overrides`: If set, overrides the default values of the config + pub fn read(path: PathBuf, overrides: Option>) -> Result { Self::read_internal( PathWithSource { path, source: PathSource::Cli, }, false, - override_str, + overrides, ) } /// Create a loader that uses the default config file location. If no file is present at the default location, the /// default configuration is used. - /// `override_str`: If set, overrides the default value of the config - pub fn read_default_path(override_str: Option) -> Result { + /// `overrides`: If set, overrides the default values of the config + pub fn read_default_path(overrides: Option>) -> Result { let path = get_default_config_path().context("Could not determine default config path.")?; - Self::read_internal(path, true, override_str) + Self::read_internal(path, true, overrides) } /// Parse the read [`RawConfig`] into a [`Config`]. @@ -822,84 +843,41 @@ mod test { .unwrap() } - #[test] - fn add_value() { - let mut original_config = base_config(); - let override_config = toml::Table::from_str("some.new.value = 'new text'").unwrap(); - - ConfigLoader::override_config_with(&mut original_config, override_config); - - assert_eq!( - original_config["some"]["value"].as_integer().unwrap(), - 0 - ); - - assert_eq!( - original_config["some"]["inner"]["value1"].as_integer().unwrap(), - 1 - ); - - assert_eq!( - original_config["some"]["inner"]["value2"].as_str().unwrap(), - "a string" - ); - - assert_eq!( - original_config["some"]["other"]["value1"] - .as_integer() - .unwrap(), - 3, - ); - - let v2array = original_config["some"]["other"]["value2"] - .as_array() - .unwrap(); - assert_eq!(v2array[0].as_integer().unwrap(), 1); - assert_eq!(v2array[1].as_str().unwrap(), "text"); - assert_eq!(v2array[2].as_bool().unwrap(), true); - - assert_eq!(original_config["global_value"].as_bool().unwrap(), false); - - assert_eq!(original_config["some"]["new"]["value"].as_str().unwrap(), "new text"); - } - #[test] fn change_value() { - let mut original_config = base_config(); - let override_config = toml::Table::from_str("some.inner.value1 = 'some text'").unwrap(); + let original_config = base_config(); + let overrides = vec!["some.inner.value1 = 'some text'".to_string()]; - ConfigLoader::override_config_with(&mut original_config, override_config); + let new_config = ConfigLoader::override_config_with(original_config, overrides) + .expect("config should be successfully overwritten"); + + assert_eq!(new_config["some"]["value"].as_integer().unwrap(), 0); assert_eq!( - original_config["some"]["value"].as_integer().unwrap(), - 0 - ); - - assert_eq!( - original_config["some"]["inner"]["value1"].as_str().unwrap(), + new_config["some"]["inner"]["value1"].as_str().unwrap(), "some text" ); assert_eq!( - original_config["some"]["inner"]["value2"].as_str().unwrap(), + new_config["some"]["inner"]["value2"].as_str().unwrap(), "a string" ); assert_eq!( - original_config["some"]["other"]["value1"] + new_config["some"]["other"]["value1"] .as_integer() .unwrap(), 3, ); - let v2array = original_config["some"]["other"]["value2"] + let v2array = new_config["some"]["other"]["value2"] .as_array() .unwrap(); assert_eq!(v2array[0].as_integer().unwrap(), 1); assert_eq!(v2array[1].as_str().unwrap(), "text"); assert_eq!(v2array[2].as_bool().unwrap(), true); - assert_eq!(original_config["global_value"].as_bool().unwrap(), false); + assert_eq!(new_config["global_value"].as_bool().unwrap(), false); } } From fee50fd9d70473980d5c41972be78b59a028ac6a Mon Sep 17 00:00:00 2001 From: Giftzwerg02 Date: Sat, 7 Feb 2026 16:39:27 +0100 Subject: [PATCH 03/11] cargo clippy --- src/config.rs | 10 +++------- src/main.rs | 10 ++++------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/config.rs b/src/config.rs index 57d7a63..d348858 100644 --- a/src/config.rs +++ b/src/config.rs @@ -669,7 +669,7 @@ impl ConfigLoader { let ugly_intermediate_key = "ugly_intermediate"; let value = format!("{ugly_intermediate_key} = {value}"); - let mut value = toml::Value::from_str(&value)?; + let mut value = toml::Value::from_str(&value)?; let value = value.as_table_mut().unwrap(); let value = value.remove(ugly_intermediate_key).unwrap(); @@ -864,15 +864,11 @@ mod test { ); assert_eq!( - new_config["some"]["other"]["value1"] - .as_integer() - .unwrap(), + new_config["some"]["other"]["value1"].as_integer().unwrap(), 3, ); - let v2array = new_config["some"]["other"]["value2"] - .as_array() - .unwrap(); + let v2array = new_config["some"]["other"]["value2"].as_array().unwrap(); assert_eq!(v2array[0].as_integer().unwrap(), 1); assert_eq!(v2array[1].as_str().unwrap(), "text"); assert_eq!(v2array[2].as_bool().unwrap(), true); diff --git a/src/main.rs b/src/main.rs index 1c18646..875947e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -204,12 +204,10 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { // Look up config file, if none is found fall back to default config. debug!("Loading config"); let config_loader = match &args.config_path { - Some(path) if !args.seed_config => { - ConfigLoader::read(path.clone(), args.override_config).context("Could not read config from given path")? - } - _ => { - ConfigLoader::read_default_path(args.override_config).context("Could not read config from default path")? - } + Some(path) if !args.seed_config => ConfigLoader::read(path.clone(), args.override_config) + .context("Could not read config from given path")?, + _ => ConfigLoader::read_default_path(args.override_config) + .context("Could not read config from default path")?, }; let mut config = config_loader.load()?; From 30652839773acedaef63083f0f489f8deeeda512 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Thu, 6 Aug 2026 00:52:55 +0200 Subject: [PATCH 04/11] Update to toml 0.9 and remove ugly parsing hack --- Cargo.lock | 51 ++++++++++++++++++++++++++------------------------- Cargo.toml | 2 +- src/config.rs | 7 +------ 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4fba42b..d831c75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1057,11 +1057,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -1178,44 +1178,42 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "serde", + "indexmap", + "serde_core", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_parser", + "toml_writer", + "winnow 0.7.13", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "serde", + "serde_core", ] [[package]] -name = "toml_edit" -version = "0.22.27" +name = "toml_parser" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", + "winnow 1.0.4", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_writer" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "unicode-ident" @@ -1616,9 +1614,12 @@ name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" [[package]] name = "wit-bindgen" diff --git a/Cargo.toml b/Cargo.toml index 1d98992..5849184 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ log = "0.4" serde = "1.0.21" serde_derive = "1.0.21" ureq = { version = "3.0.8", default-features = false, features = ["gzip", "socks-proxy"] } -toml = "0.8.19" +toml = "0.9" yansi = "1" zip = { version = "5.1.1", default-features = false, features = ["deflate"] } diff --git a/src/config.rs b/src/config.rs index e057f1b..25056f4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -807,6 +807,7 @@ impl ConfigLoader { .ok_or(anyhow!("Invalid override-string: {override_str}"))?; let name = name.trim(); + let value = toml::Value::from_str(value.trim())?; let mut keypath = name.split('.'); let key_start = keypath @@ -826,12 +827,6 @@ impl ConfigLoader { ))?; } - let ugly_intermediate_key = "ugly_intermediate"; - let value = format!("{ugly_intermediate_key} = {value}"); - let mut value = toml::Value::from_str(&value)?; - let value = value.as_table_mut().unwrap(); - let value = value.remove(ugly_intermediate_key).unwrap(); - *entry = value; } Ok(config_table) From 95816580c89117f93dd631e89008f08edb0a2de8 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Thu, 6 Aug 2026 00:57:16 +0200 Subject: [PATCH 05/11] Change overrides type to non-optional Vec --- src/cli.rs | 2 +- src/config.rs | 27 +++++++-------------------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 3a16189..8366abd 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -85,7 +85,7 @@ pub(crate) struct Cli { long = "override-config", action = ArgAction::Append, )] - pub override_config: Option>, + pub override_config: Vec, /// Use a pager to page output #[arg(long = "pager", requires = "command_or_file")] diff --git a/src/config.rs b/src/config.rs index 25056f4..61928b6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -746,10 +746,10 @@ impl ConfigLoader { fn read_internal( path: PathWithSource, allow_not_found: bool, - overrides: Option>, + overrides: Vec, ) -> Result { - match (fs::read_to_string(&path.path), overrides) { - (Ok(content), Some(overrides)) => { + match fs::read_to_string(&path.path) { + Ok(content) => { let config: RawConfig = toml::from_str(&content).with_context(|| { format!( "Could not parse config file contents as toml from {}.", @@ -765,16 +765,7 @@ impl ConfigLoader { path, }) } - (Ok(content), None) => 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), Some(overrides)) if allow_not_found && e.kind() == ErrorKind::NotFound => { + Err(e) if allow_not_found && e.kind() == ErrorKind::NotFound => { // In order to override the default config we first need to generate the default // RawConfig and then serialize it into the toml-Table variant let default_config_table = RawConfig::default(); @@ -786,11 +777,7 @@ impl ConfigLoader { path, }) } - (Err(e), None) if allow_not_found && e.kind() == ErrorKind::NotFound => Ok(Self { - raw: RawConfig::default(), - path, - }), - (Err(e), _) => Err(e).context(format!( + Err(e) => Err(e).context(format!( "Could not read config file contents from {}.", path.path().display() )), @@ -834,7 +821,7 @@ impl ConfigLoader { /// Create a loader that uses the config at `path`. /// `overrides`: If set, overrides the default values of the config - pub fn read(path: PathBuf, overrides: Option>) -> Result { + pub fn read(path: PathBuf, overrides: Vec) -> Result { Self::read_internal( PathWithSource { path, @@ -848,7 +835,7 @@ impl ConfigLoader { /// Create a loader that uses the default config file location. If no file is present at the default location, the /// default configuration is used. /// `overrides`: If set, overrides the default values of the config - pub fn read_default_path(overrides: Option>) -> Result { + pub fn read_default_path(overrides: Vec) -> Result { let path = get_default_config_path(); Self::read_internal(path, true, overrides) } From 1f700e562f42cb02970d6caeb85beff1c4017737 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Thu, 6 Aug 2026 01:02:54 +0200 Subject: [PATCH 06/11] Simplify read_internal --- src/config.rs | 52 ++++++++++++++++++++------------------------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/src/config.rs b/src/config.rs index 61928b6..916b814 100644 --- a/src/config.rs +++ b/src/config.rs @@ -748,40 +748,28 @@ impl ConfigLoader { allow_not_found: bool, overrides: Vec, ) -> Result { - match fs::read_to_string(&path.path) { - Ok(content) => { - let config: RawConfig = toml::from_str(&content).with_context(|| { - format!( - "Could not parse config file contents as toml from {}.", - path.path.display() - ) - })?; - - let config_table = toml::Table::try_from(config)?; - let config_table = Self::override_config_with(config_table, overrides)?; - - Ok(Self { - raw: config_table.try_into()?, - path, - }) - } + let read_config_table = match fs::read_to_string(&path.path) { + Ok(content) => toml::from_str(&content).with_context(|| { + format!( + "Could not parse config file contents as toml from {}.", + path.path.display() + ) + })?, Err(e) if allow_not_found && e.kind() == ErrorKind::NotFound => { - // In order to override the default config we first need to generate the default - // RawConfig and then serialize it into the toml-Table variant - let default_config_table = RawConfig::default(); - let default_config_table = toml::Table::try_from(default_config_table)?; - let config_table = Self::override_config_with(default_config_table, overrides)?; - - Ok(Self { - raw: config_table.try_into()?, - path, - }) + toml::Table::try_from(RawConfig::default())? } - Err(e) => Err(e).context(format!( - "Could not read config file contents from {}.", - path.path().display() - )), - } + Err(e) => { + return Err(e).context(format!( + "Could not read config file contents from {}.", + path.path().display() + )) + } + }; + + let used_config_table = Self::override_config_with(read_config_table, overrides)?; + let raw = used_config_table.try_into()?; + + Ok(Self { raw, path }) } fn override_config_with( From 0582777933567b92014dde7d0d333121d22f79a0 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Thu, 6 Aug 2026 01:24:01 +0200 Subject: [PATCH 07/11] Better error messages, simplify override_config_path --- src/config.rs | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/config.rs b/src/config.rs index 916b814..feb5751 100644 --- a/src/config.rs +++ b/src/config.rs @@ -748,16 +748,14 @@ impl ConfigLoader { allow_not_found: bool, overrides: Vec, ) -> Result { - let read_config_table = match fs::read_to_string(&path.path) { + let read_raw_config = match fs::read_to_string(&path.path) { Ok(content) => toml::from_str(&content).with_context(|| { format!( "Could not parse config file contents as toml from {}.", path.path.display() ) })?, - Err(e) if allow_not_found && e.kind() == ErrorKind::NotFound => { - toml::Table::try_from(RawConfig::default())? - } + Err(e) if allow_not_found && e.kind() == ErrorKind::NotFound => RawConfig::default(), Err(e) => { return Err(e).context(format!( "Could not read config file contents from {}.", @@ -766,34 +764,30 @@ impl ConfigLoader { } }; - let used_config_table = Self::override_config_with(read_config_table, overrides)?; + let read_config_table = toml::Table::try_from(read_raw_config)?; + let used_config_table = Self::override_config_with(read_config_table, overrides) + .context("Failed to apply config overrides")?; let raw = used_config_table.try_into()?; Ok(Self { raw, path }) } fn override_config_with( - mut config_table: toml::Table, + config_table: toml::Table, overrides: Vec, ) -> Result { + let mut config_table = toml::Value::Table(config_table); for override_str in overrides { let (name, value) = override_str .split_once('=') - .ok_or(anyhow!("Invalid override-string: {override_str}"))?; + .ok_or(anyhow!("Invalid override-string: {override_str} (correct example: \"display.compact = true\")"))?; let name = name.trim(); let value = toml::Value::from_str(value.trim())?; - let mut keypath = name.split('.'); - let key_start = keypath - .next() - .expect("split returns always at least one element"); - let mut entry = config_table.get_mut(key_start).ok_or(anyhow!( - "\"{name}\" is not a valid key starting at \"{key_start}\"" - ))?; - - for subkey in keypath { - let toml::Value::Table(ref mut entry_table) = entry else { + let mut entry = &mut config_table; + for subkey in name.split('.') { + let toml::Value::Table(entry_table) = entry else { bail!("\"{name}\" is not a valid identifier since \"{subkey}\" already refers to a value which is not a toml-Table."); }; @@ -804,7 +798,11 @@ impl ConfigLoader { *entry = value; } - Ok(config_table) + + match config_table { + toml::Value::Table(config_table) => Ok(config_table), + _ => unreachable!("root table is never modified"), + } } /// Create a loader that uses the config at `path`. From 61bdc4612ce50677f282243d03ecfc2a363196e2 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Thu, 6 Aug 2026 01:31:39 +0200 Subject: [PATCH 08/11] Change Vec to slice --- src/config.rs | 10 +++++----- src/main.rs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/config.rs b/src/config.rs index feb5751..636c8f8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -746,7 +746,7 @@ impl ConfigLoader { fn read_internal( path: PathWithSource, allow_not_found: bool, - overrides: Vec, + overrides: &[String], ) -> Result { let read_raw_config = match fs::read_to_string(&path.path) { Ok(content) => toml::from_str(&content).with_context(|| { @@ -774,7 +774,7 @@ impl ConfigLoader { fn override_config_with( config_table: toml::Table, - overrides: Vec, + overrides: &[String], ) -> Result { let mut config_table = toml::Value::Table(config_table); for override_str in overrides { @@ -807,7 +807,7 @@ impl ConfigLoader { /// Create a loader that uses the config at `path`. /// `overrides`: If set, overrides the default values of the config - pub fn read(path: PathBuf, overrides: Vec) -> Result { + pub fn read(path: PathBuf, overrides: &[String]) -> Result { Self::read_internal( PathWithSource { path, @@ -821,7 +821,7 @@ impl ConfigLoader { /// Create a loader that uses the default config file location. If no file is present at the default location, the /// default configuration is used. /// `overrides`: If set, overrides the default values of the config - pub fn read_default_path(overrides: Vec) -> Result { + pub fn read_default_path(overrides: &[String]) -> Result { let path = get_default_config_path(); Self::read_internal(path, true, overrides) } @@ -1024,7 +1024,7 @@ mod test { #[test] fn change_value() { let original_config = base_config(); - let overrides = vec!["some.inner.value1 = 'some text'".to_string()]; + let overrides = &["some.inner.value1 = 'some text'".to_string()]; let new_config = ConfigLoader::override_config_with(original_config, overrides) .expect("config should be successfully overwritten"); diff --git a/src/main.rs b/src/main.rs index 641b956..f077c7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -202,9 +202,9 @@ fn try_main(args: Cli, enable_styles: bool) -> Result { // Look up config file, if none is found fall back to default config. debug!("Loading config"); let config_loader = match &args.config_path { - Some(path) if !args.seed_config => ConfigLoader::read(path.clone(), args.override_config) + Some(path) if !args.seed_config => ConfigLoader::read(path.clone(), &args.override_config) .context("Could not read config from given path")?, - _ => ConfigLoader::read_default_path(args.override_config) + _ => ConfigLoader::read_default_path(&args.override_config) .context("Could not read config from default path")?, }; let mut config = config_loader.load()?; From aac67b9660c332ced93f016f4dc1d0a267a1b2e9 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Thu, 6 Aug 2026 01:59:22 +0200 Subject: [PATCH 09/11] More tests --- src/config.rs | 83 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 22 deletions(-) diff --git a/src/config.rs b/src/config.rs index 636c8f8..84b1dac 100644 --- a/src/config.rs +++ b/src/config.rs @@ -246,16 +246,22 @@ impl Default for RawIndent { } } +impl From for Indent { + fn from(raw_indent: RawIndent) -> Self { + Self { + base: raw_indent.base, + command: raw_indent.command, + } + } +} + 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, - indent: Indent { - base: raw_display_config.indent.base, - command: raw_display_config.indent.command, - }, + indent: raw_display_config.indent.into(), } } } @@ -791,9 +797,9 @@ impl ConfigLoader { bail!("\"{name}\" is not a valid identifier since \"{subkey}\" already refers to a value which is not a toml-Table."); }; - entry = entry_table.get_mut(subkey).ok_or(anyhow!( - "\"{name}\" is not a valid key starting at \"{subkey}\"" - ))?; + entry = entry_table + .entry(subkey) + .or_insert(toml::Value::Table(Default::default())); } *entry = value; @@ -1000,6 +1006,7 @@ mod test { mod override_config { use super::*; + use toml::Value; fn base_config() -> toml::Table { toml::Table::from_str( @@ -1022,36 +1029,68 @@ mod test { } #[test] - fn change_value() { + fn basic() { let original_config = base_config(); let overrides = &["some.inner.value1 = 'some text'".to_string()]; let new_config = ConfigLoader::override_config_with(original_config, overrides) .expect("config should be successfully overwritten"); - assert_eq!(new_config["some"]["value"].as_integer().unwrap(), 0); - + assert_eq!(new_config["some"]["value"], Value::Integer(0)); assert_eq!( - new_config["some"]["inner"]["value1"].as_str().unwrap(), - "some text" + new_config["some"]["inner"]["value1"], + Value::String("some text".to_string()), ); - assert_eq!( - new_config["some"]["inner"]["value2"].as_str().unwrap(), - "a string" + new_config["some"]["inner"]["value2"], + Value::String("a string".to_string()), ); + assert_eq!(new_config["some"]["other"]["value1"], Value::Integer(3)); + assert_eq!(new_config["global_value"], Value::Boolean(false)); + } + macro_rules! style_config_with { + ($config:ident, $overrides:expr) => { + let loader = ConfigLoader::read( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/style-config.toml"), + $overrides, + ) + .unwrap(); + let $config = loader.load().unwrap(); + }; + } + + #[test] + fn dependent_config() { + style_config_with!(config, &["search.languages = ['de', 'it']".to_string()]); + assert_eq!(config.search.languages, [Language("de"), Language("it")]); + // Value is copied after override is applied assert_eq!( - new_config["some"]["other"]["value1"].as_integer().unwrap(), - 3, + config.updates.download_languages, + [Language("de"), Language("it")] ); + } - let v2array = new_config["some"]["other"]["value2"].as_array().unwrap(); - assert_eq!(v2array[0].as_integer().unwrap(), 1); - assert_eq!(v2array[1].as_str().unwrap(), "text"); - assert_eq!(v2array[2].as_bool().unwrap(), true); + #[test] + fn order() { + style_config_with!( + config, + &[ + "display.compact = false".to_string(), + "display.compact = true".to_string(), + ] + ); + assert!(config.display.compact); + } - assert_eq!(new_config["global_value"].as_bool().unwrap(), false); + #[test] + fn override_with_table() { + style_config_with!(config, &["display = {'compact' = true}".to_string()]); + assert!(config.display.compact); + assert_eq!( + config.display.indent, + RawConfig::default().display.indent.into() + ); } } From e7f5ca042b38edfb0c08ad44508757ad462f6929 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Fri, 7 Aug 2026 01:07:42 +0200 Subject: [PATCH 10/11] update usage --- docs/src/usage.txt | 43 +++++++++++++++++++++++-------------------- src/cli.rs | 7 ++----- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/src/usage.txt b/docs/src/usage.txt index 6a04de7..0db9cc9 100644 --- a/docs/src/usage.txt +++ b/docs/src/usage.txt @@ -7,26 +7,29 @@ Arguments: [COMMAND]... The command to show (e.g. `tar` or `git log`) Options: - -l, --list List all commands in the cache - --edit-page Edit custom page with `EDITOR` - --edit-patch Edit custom patch with `EDITOR` - -f, --render Render a specific markdown file - -p, --platform Override the operating system, can be specified multiple times in order - of preference [possible values: linux, macos, sunos, windows, android, - freebsd, netbsd, openbsd, common] - -L, --language Override the language - -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 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 - --show-paths Show file and directory paths used by tealdeer - --seed-config Create a basic config - --color Control whether to use color [possible values: always, auto, never] - -v, --version Print the version - -h, --help Print help + -l, --list List all commands in the cache + --edit-page Edit custom page with `EDITOR` + --edit-patch Edit custom patch with `EDITOR` + -f, --render Render a specific markdown file + -p, --platform Override the operating system, can be specified multiple times + in order of preference [possible values: linux, macos, sunos, + windows, android, freebsd, netbsd, openbsd, common] + -L, --language Override the language + -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 Override config file location + --override-config Override config values after reading config file (example: + `updates.auto_update = true`) + --pager Use a pager to page output + -r, --raw Display the raw markdown instead of rendering it + -q, --quiet Suppress informational messages + --show-paths Show file and directory paths used by tealdeer + --seed-config Create a basic config + --color Control whether to use color [possible values: always, auto, + never] + -v, --version Print the version + -h, --help Print help To view the user documentation, please visit https://tealdeer-rs.github.io/tealdeer/. diff --git a/src/cli.rs b/src/cli.rs index 8366abd..761a25b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -80,11 +80,8 @@ pub(crate) struct Cli { #[arg(long = "config-path", value_name = "FILE")] pub config_path: Option, - /// Override config values, also overrides values set in the config file (e.g. `'updates.auto_update = true'`) - #[arg( - long = "override-config", - action = ArgAction::Append, - )] + /// Override config values after reading config file (example: `updates.auto_update = true`) + #[arg(long, action = ArgAction::Append, value_name = "OVERRIDE")] pub override_config: Vec, /// Use a pager to page output From 9ceeefe5c2b7034c324556b3cdf8184a2db75eba Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Fri, 7 Aug 2026 01:14:28 +0200 Subject: [PATCH 11/11] Add docs --- docs/src/config.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/src/config.md b/docs/src/config.md index a662b57..de3460e 100644 --- a/docs/src/config.md +++ b/docs/src/config.md @@ -57,3 +57,16 @@ auto_update = true The directory where the configuration file resides may be overwritten by the environment variable `TEALDEER_CONFIG_DIR`. Remember to use an absolute path. Variable expansion will not be performed on the path. + +## Override Config Values + +Individual config values can be overridden using the `--override-config` command +line argument. The overrides take place after reading the user config file, but +before the raw config is evaluated. + +```shell +$ tldr --override-config "display.compact = true" tealdeer +``` + +Each override is of the form ` = ` where `name` is a config key and +`value` is any TOML value.