use crate::cli::{ConfigAction, GlobalArgs};
use crate::config::{config_path, provider_dir, Config};
use crate::error::{OlError, OL_4270_CONFIG_UNREADABLE, OL_4271_PROFILE_NOT_FOUND};
use crate::telemetry::consent_file::{read_consent, write_consent};
use crate::ui::output::OutputConfig;
pub async fn run(g: &GlobalArgs, action: ConfigAction) -> Result<(), OlError> {
match action {
ConfigAction::Get { key } => get(g, &key).await,
ConfigAction::Set { key, value } => set(g, &key, &value).await,
ConfigAction::List => list(g).await,
}
}
async fn get(g: &GlobalArgs, key: &str) -> Result<(), OlError> {
let out = OutputConfig::resolve(g);
let value = read_value(key)?;
if out.is_machine() {
out.print_json(&serde_json::json!({ "key": key, "value": value }));
} else {
println!("{value}");
}
Ok(())
}
async fn set(g: &GlobalArgs, key: &str, value: &str) -> Result<(), OlError> {
let out = OutputConfig::resolve(g);
let current = read_value(key).unwrap_or_default();
if current == value {
out.print_step(&format!("`{key}` already set to `{value}` — no-op"));
return Ok(());
}
write_value(key, value)?;
out.print_step(&format!("Set `{key}` to `{value}`"));
Ok(())
}
async fn list(g: &GlobalArgs) -> Result<(), OlError> {
let out = OutputConfig::resolve(g);
let cfg = Config::load().unwrap_or_default();
let telemetry_enabled = read_consent(&provider_dir().join("telemetry.json"))
.ok()
.flatten()
.map(|f| f.enabled);
let snapshot = serde_json::json!({
"config_path": config_path().display().to_string(),
"machine_id": cfg.telemetry.machine_id,
"crashreport.enabled": cfg.crashreport.enabled,
"telemetry.enabled": telemetry_enabled,
"profiles": cfg.profiles,
});
if out.is_machine() {
out.print_json(&snapshot);
} else {
out.print_step(&format!("Config: {}", config_path().display()));
if let Some(id) = &cfg.telemetry.machine_id {
out.print_substep(&format!("machine_id = {id}"));
}
out.print_substep(&format!(
"crashreport.enabled = {}",
cfg.crashreport.enabled
));
out.print_substep(&format!(
"telemetry.enabled = {}",
telemetry_enabled
.map(|b| b.to_string())
.unwrap_or_else(|| "(not set)".into())
));
for (name, p) in &cfg.profiles {
out.print_substep(&format!(
"profiles.{name}.api_url = {}",
p.api_url.as_deref().unwrap_or("(unset)")
));
}
}
Ok(())
}
fn read_value(key: &str) -> Result<String, OlError> {
let cfg = Config::load().unwrap_or_default();
match key {
"crashreport.enabled" => Ok(cfg.crashreport.enabled.to_string()),
"telemetry.machine_id" => Ok(cfg.telemetry.machine_id.clone().unwrap_or_default()),
"telemetry.enabled" => Ok(read_consent(&provider_dir().join("telemetry.json"))
.ok()
.flatten()
.map(|f| f.enabled.to_string())
.unwrap_or_else(|| "false".into())),
k if k.starts_with("profiles.") => {
let rest = &k["profiles.".len()..];
let (name, field) = rest.split_once('.').ok_or_else(|| {
OlError::new(
OL_4271_PROFILE_NOT_FOUND,
format!("expected profiles.<name>.<field>, got `{k}`"),
)
})?;
let profile = cfg.profiles.get(name).ok_or_else(|| {
OlError::new(
OL_4271_PROFILE_NOT_FOUND,
format!("profile `{name}` not defined"),
)
})?;
match field {
"api_url" => Ok(profile.api_url.clone().unwrap_or_default()),
other => Err(OlError::new(
OL_4270_CONFIG_UNREADABLE,
format!("unknown profile field `{other}` (only `api_url` is supported)"),
)),
}
}
other => Err(OlError::new(
OL_4270_CONFIG_UNREADABLE,
format!("unknown config key `{other}`"),
)),
}
}
fn write_value(key: &str, value: &str) -> Result<(), OlError> {
match key {
"crashreport.enabled" => {
let mut cfg = Config::load().unwrap_or_default();
cfg.crashreport.enabled = parse_bool(value)?;
cfg.save()
}
"telemetry.enabled" => {
let enabled = parse_bool(value)?;
write_consent(&provider_dir().join("telemetry.json"), enabled)
}
k if k.starts_with("profiles.") => {
let rest = &k["profiles.".len()..];
let (name, field) = rest.split_once('.').ok_or_else(|| {
OlError::new(
OL_4271_PROFILE_NOT_FOUND,
format!("expected profiles.<name>.<field>, got `{k}`"),
)
})?;
if field != "api_url" {
return Err(OlError::new(
OL_4270_CONFIG_UNREADABLE,
format!("unknown profile field `{field}` (only `api_url` is supported)"),
));
}
let mut cfg = Config::load().unwrap_or_default();
let profile = cfg.profiles.entry(name.to_string()).or_default();
profile.api_url = Some(value.to_string());
cfg.save()
}
other => Err(OlError::new(
OL_4270_CONFIG_UNREADABLE,
format!("config key `{other}` is read-only or unknown"),
)),
}
}
fn parse_bool(s: &str) -> Result<bool, OlError> {
match s.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Ok(true),
"0" | "false" | "no" | "off" => Ok(false),
other => Err(OlError::new(
OL_4270_CONFIG_UNREADABLE,
format!("expected boolean, got `{other}`"),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_bool_accepts_common_forms() {
assert!(parse_bool("true").unwrap());
assert!(parse_bool("Yes").unwrap());
assert!(!parse_bool("off").unwrap());
assert!(parse_bool("maybe").is_err());
}
}