aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config/io.rs140
1 files changed, 138 insertions, 2 deletions
diff --git a/src/config/io.rs b/src/config/io.rs
index 984f25e0..f8c2eb90 100644
--- a/src/config/io.rs
+++ b/src/config/io.rs
@@ -4,6 +4,20 @@ use tracing::warn;
use super::{model::LoadedConfig, Config, CONFIG_PATH_ENV_VAR};
+const KNOWN_TOP_LEVEL_CONFIG_KEYS: &[&str] = &[
+ "advanced",
+ "experimental",
+ "keys",
+ "onboarding",
+ "remote",
+ "session",
+ "terminal",
+ "theme",
+ "ui",
+ "update",
+ "worktrees",
+];
+
pub fn app_dir_name() -> &'static str {
if cfg!(debug_assertions) {
"herdr-dev"
@@ -85,7 +99,9 @@ impl Config {
match std::fs::read_to_string(&path) {
Ok(content) => match toml::from_str::<Config>(&content) {
Ok(config) => {
- let diagnostics = config.collect_diagnostics();
+ let mut diagnostics =
+ unknown_top_level_section_diagnostics_from_str(&content);
+ diagnostics.extend(config.collect_diagnostics());
return LoadedConfig {
config,
diagnostics,
@@ -183,7 +199,7 @@ fn load_live_config_from_str(content: &str) -> Result<LoadedConfig, Vec<String>>
})?;
let mut config = Config::default();
- let mut diagnostics = Vec::new();
+ let mut diagnostics = unknown_top_level_section_diagnostics(table);
let mut invalid_sections = Vec::new();
if let Some(value) = table.get("onboarding") {
@@ -283,6 +299,48 @@ fn load_live_config_from_str(content: &str) -> Result<LoadedConfig, Vec<String>>
})
}
+fn unknown_top_level_section_diagnostics_from_str(content: &str) -> Vec<String> {
+ content
+ .parse::<toml::Value>()
+ .ok()
+ .and_then(|value| value.as_table().map(unknown_top_level_section_diagnostics))
+ .unwrap_or_default()
+}
+
+fn unknown_top_level_section_diagnostics(
+ table: &toml::map::Map<String, toml::Value>,
+) -> Vec<String> {
+ table
+ .iter()
+ .filter_map(|(key, value)| unknown_top_level_section_diagnostic(key, value))
+ .collect()
+}
+
+fn unknown_top_level_section_diagnostic(key: &str, value: &toml::Value) -> Option<String> {
+ if KNOWN_TOP_LEVEL_CONFIG_KEYS.contains(&key) {
+ return None;
+ }
+
+ let header = if value.is_table() {
+ format!("[{key}]")
+ } else if value
+ .as_array()
+ .is_some_and(|items| !items.is_empty() && items.iter().all(toml::Value::is_table))
+ {
+ format!("[[{key}]]")
+ } else {
+ return None;
+ };
+
+ if key == "toast" {
+ Some(format!(
+ "unknown config section {header}; did you mean [ui.toast]? ignoring section"
+ ))
+ } else {
+ Some(format!("unknown config section {header}; ignoring section"))
+ }
+}
+
fn load_live_section<T>(
table: &toml::map::Map<String, toml::Value>,
section: &'static str,
@@ -549,6 +607,84 @@ resume_agents_on_restore = true
}
#[test]
+ fn load_live_config_warns_about_unknown_top_level_sections() {
+ let loaded = load_live_config_from_str(
+ r#"
+[toast]
+delivery = "system"
+
+[ui.toast]
+delivery = "herdr"
+"#,
+ )
+ .unwrap();
+
+ assert_eq!(
+ loaded.diagnostics,
+ vec!["unknown config section [toast]; did you mean [ui.toast]? ignoring section"]
+ );
+ assert!(loaded.invalid_sections.is_empty());
+ assert_eq!(
+ loaded.config.ui.toast.delivery,
+ super::super::ToastDelivery::Herdr
+ );
+ }
+
+ #[test]
+ fn load_live_config_does_not_warn_about_unknown_top_level_scalar_values() {
+ let loaded = load_live_config_from_str(
+ r#"
+plugin = []
+
+[ui.toast]
+delivery = "herdr"
+"#,
+ )
+ .unwrap();
+
+ assert!(loaded.diagnostics.is_empty());
+ assert_eq!(
+ loaded.config.ui.toast.delivery,
+ super::super::ToastDelivery::Herdr
+ );
+ }
+
+ #[test]
+ fn startup_config_load_warns_about_unknown_top_level_sections() {
+ let _guard = crate::config::test_config_env_lock().lock().unwrap();
+ let path = std::env::temp_dir().join(format!(
+ "herdr-config-unknown-section-{}.toml",
+ std::process::id()
+ ));
+ std::fs::write(
+ &path,
+ r#"
+[[plugin]]
+id = "example"
+
+[ui.toast]
+delivery = "system"
+"#,
+ )
+ .unwrap();
+ std::env::set_var(CONFIG_PATH_ENV_VAR, &path);
+
+ let loaded = Config::load();
+
+ assert_eq!(
+ loaded.diagnostics,
+ vec!["unknown config section [[plugin]]; ignoring section"]
+ );
+ assert_eq!(
+ loaded.config.ui.toast.delivery,
+ super::super::ToastDelivery::System
+ );
+
+ std::env::remove_var(CONFIG_PATH_ENV_VAR);
+ let _ = std::fs::remove_file(path);
+ }
+
+ #[test]
fn remove_keybinding_config_sections_removes_keys_tables_only() {
let content = r#"onboarding = false