From a35800b96def18ab1c3fc082f4b8de0cd26c584e Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:33:22 +0800 Subject: [PATCH] fix(config): read a shell entry with no program as the login shell `shell` was the one structured field still read strictly, so a hand-edited {"args": [...]} without a program failed the whole file. It was quarantined and every other setting fell back to its default, and changes made in Settings afterwards were never saved. --- crates/tty7-core/src/core/config.rs | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/crates/tty7-core/src/core/config.rs b/crates/tty7-core/src/core/config.rs index adfbd6c8..153dcfa7 100644 --- a/crates/tty7-core/src/core/config.rs +++ b/crates/tty7-core/src/core/config.rs @@ -191,6 +191,7 @@ pub struct Config { pub keybinding_preset: String, #[serde(default = "default_prefix")] pub prefix: String, + #[serde(default, deserialize_with = "de_shell")] pub shell: Option, /// Lenient on purpose: this is a hand-edited key with a nested shape, and a /// typo in one entry must not fail the whole `Config` and hand the user @@ -1675,10 +1676,56 @@ where })) } +/// `shell`, read the way Settings writes it: no program means the login +/// shell. A hand-edited `"shell": { "args": ["-l"] }` — or any other shape +/// that does not name a program — used to fail the whole file, and every other +/// setting went with it into quarantine. +fn de_shell<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = serde_json::Value::deserialize(deserializer)?; + if value.is_null() { + return Ok(None); + } + match ShellConfig::deserialize(&value) { + Ok(shell) if !shell.program.trim().is_empty() => Ok(Some(shell)), + Ok(_) => Ok(None), + Err(e) => { + log::warn!("ignoring invalid shell {value}: {e}; using the login shell"); + Ok(None) + } + } +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn a_shell_without_a_program_is_the_login_shell_not_a_broken_file() { + for shell in [ + r#"{"args": ["-l"]}"#, + r#"{"program": "", "args": []}"#, + r#"{"program": 5}"#, + "null", + ] { + let text = format!(r#"{{"font_size": 17.0, "shell": {shell}}}"#); + let cfg: Config = serde_json::from_str(&text).expect("the file still parses"); + assert_eq!(cfg.shell, None, "{shell}"); + assert_eq!(cfg.font_size, 17.0, "the rest of the file is kept"); + } + let cfg: Config = + serde_json::from_str(r#"{"shell": {"program": "fish", "args": ["-l"]}}"#).unwrap(); + assert_eq!( + cfg.shell, + Some(ShellConfig { + program: "fish".into(), + args: vec!["-l".into()] + }) + ); + } + #[test] fn profile_usage_score_ranks_frequency_and_recency() { let now = 100_000_000u64;