v0.4.3 : persistance du theme actif choisi
release / linux (push) Successful in 13m24s
release / windows (push) Canceled after 0s
release / macos (push) Canceled after 0s

- le REPL 'theme <name>' applique ET sauvegarde le theme (settings.theme dans le fichier de config actif, cree s'il manque) via persist_setting
- correction racine : config::merge ignorait settings.theme - la valeur persistee n etait jamais relue au chargement, le theme par defaut etait toujours reapplique apres un redemarrage
- 'am --theme <name>' reste un one-shot ; config set settings.theme <nom> fonctionne egalement
- aides mises a jour (banniere, help theme)
- tests de persistance + relecture au demarrage, man pages regenerees, version 0.4.3
This commit is contained in:
2026-08-17 15:23:28 -04:00
parent 91253b79c5
commit 53893f2967
10 changed files with 166 additions and 25 deletions
+112 -5
View File
@@ -103,7 +103,28 @@ pub fn run(app: &App, cmd: &ConfigCmd) -> Result<i32> {
/// configuration file, then validate the result.
pub fn set_value(app: &App, key: &str, value: &str) -> Result<i32> {
let path = user_config_path(app)?;
let text = std::fs::read_to_string(&path)
write_dotted_key(&path, key, value)?;
reload_and_validate(&path)?;
app.log.success(&format!("{key} = {value}"));
Ok(0)
}
/// Persist one setting into the configuration file that is actually in
/// effect (the loaded origin), creating the user config file when none
/// exists. Used by the REPL 'theme <name>' command so a switched theme
/// survives a restart.
pub fn persist_setting(app: &App, key: &str, value: &str) -> Result<()> {
let path = match &app.config_origin {
Some(p) => p.clone(),
None => user_config_path(app)?,
};
write_dotted_key(&path, key, value)?;
reload_and_validate(&path)
}
/// Set one dotted key (e.g. settings.default_shell) in a YAML config file.
fn write_dotted_key(path: &Path, key: &str, value: &str) -> Result<()> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("cannot read {}", path.display()))?;
let mut root: serde_yaml::Value = serde_yaml::from_str(&text)
.with_context(|| format!("{} is not valid YAML", path.display()))?;
@@ -128,16 +149,18 @@ pub fn set_value(app: &App, key: &str, value: &str) -> Result<i32> {
serde_yaml::Value::String(parts.last().unwrap().to_string()),
parsed,
);
std::fs::write(&path, serde_yaml::to_string(&root)?)
std::fs::write(path, serde_yaml::to_string(&root)?)
.with_context(|| format!("cannot write {}", path.display()))?;
Ok(())
}
// Validate the new configuration end to end.
/// Reload a written config file to make sure the change stays valid.
fn reload_and_validate(path: &Path) -> Result<()> {
let cli = crate::cli::Cli::parse_from(["am", "--config", path.to_str().unwrap_or_default()]);
config::load(&cli).map_err(|e| {
anyhow!("the new value makes the configuration invalid: {e:#}")
})?;
app.log.success(&format!("{key} = {value}"));
Ok(0)
Ok(())
}
/// Parse a scalar: booleans and integers keep their type, everything else
@@ -235,3 +258,87 @@ fn add_file(app: &App, file: &Path) -> Result<i32> {
));
Ok(0)
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
/// App whose active config is a file inside a leaked TempDir (the same
/// pattern as profile_cmd's tests: the guard would delete the directory
/// when the helper returns).
fn app_with_config(text: &str) -> (App, PathBuf) {
let guard = tempfile::tempdir().unwrap();
let dir = guard.path().to_path_buf();
std::mem::forget(guard);
let cfg = dir.join("config.yaml");
std::fs::write(&cfg, text).unwrap();
let cli = crate::cli::Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]);
let mut app = App::from_cli(cli).unwrap();
let mut p = app.paths.clone();
p.install_dir = dir.join("agents");
p.bin_dir = dir.join("agents").join("bin");
p.log_dir = dir.join("logs");
p.state_file = dir.join("state.json");
p.probe_cache_file = dir.join("probe-cache.json");
p.config_dir = Some(dir.join("config"));
app.paths = p;
(app, cfg)
}
#[test]
fn persist_setting_writes_into_the_active_config() {
let (app, cfg) = app_with_config("version: \"1.0\"
settings:
theme: default
agents: []
");
persist_setting(&app, "settings.theme", "ocean").unwrap();
let text = std::fs::read_to_string(&cfg).unwrap();
let value: serde_yaml::Value = serde_yaml::from_str(&text).unwrap();
assert_eq!(value["settings"]["theme"], "ocean");
}
#[test]
fn persisted_theme_is_used_on_next_start() {
let (app, cfg) = app_with_config("version: \"1.0\"
settings:
theme: default
agents: []
");
persist_setting(&app, "settings.theme", "ocean").unwrap();
// A fresh start reads settings.theme from the config file.
let cli = crate::cli::Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]);
let reloaded = App::from_cli(cli).unwrap();
assert_eq!(reloaded.theme().name, "ocean");
}
#[test]
fn persist_setting_creates_the_user_config_when_none_exists() {
let (mut app, _cfg) = app_with_config("version: \"1.0\"
agents: []
");
// Simulate a run without any user/local config: no origin.
app.config_origin = None;
persist_setting(&app, "settings.theme", "mono").unwrap();
let user_file = app.paths.config_dir.as_ref().unwrap().join(config::CONFIG_FILE_NAME);
assert!(user_file.is_file(), "user config was not created");
let text = std::fs::read_to_string(&user_file).unwrap();
let value: serde_yaml::Value = serde_yaml::from_str(&text).unwrap();
assert_eq!(value["settings"]["theme"], "mono");
}
#[test]
fn write_dotted_key_descends_into_nested_mappings() {
let guard = tempfile::tempdir().unwrap();
let path = guard.path().join("c.yaml");
std::fs::write(&path, "version: \"1.0\"
agents: []
").unwrap();
write_dotted_key(&path, "settings.default_shell", "pwsh").unwrap();
let value: serde_yaml::Value =
serde_yaml::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(value["settings"]["default_shell"], "pwsh");
assert!(value["agents"].is_sequence());
}
}