use anyhow::{Context, Result};
use dialoguer::{theme::ColorfulTheme, Input, Password, Select};
use merlion_config::{ensure_home, Config, ModelConfig};
use crate::setup::{append_env_line, catalog_entry, CATALOG};
pub fn run(cfg: Config, id: Option<String>) -> Result<()> {
match id {
Some(new_id) => set_shortcut(cfg, new_id),
None => wizard(cfg),
}
}
fn set_shortcut(mut cfg: Config, new_id: String) -> Result<()> {
cfg.model = ModelConfig {
id: new_id,
base_url: None,
api_key_env: None,
temperature: cfg.model.temperature,
max_tokens: cfg.model.max_tokens,
};
let path = merlion_config::save(&cfg)?;
println!("Set model = {} ({})", cfg.model.id, path.display());
let resolved = cfg.resolve_provider()?;
let key_env = resolved.api_key_env;
if std::env::var(&key_env)
.ok()
.filter(|v| !v.is_empty())
.is_none()
{
let home = ensure_home()?;
let env_path = home.join(".env");
prompt_and_save_key(&env_path, &key_env)?;
}
Ok(())
}
fn wizard(mut cfg: Config) -> Result<()> {
let theme = ColorfulTheme::default();
let home = ensure_home()?;
let env_path = home.join(".env");
println!("Current model: {}", cfg.model.id);
println!();
let (current_provider, current_model) = match cfg.model.id.split_once(':') {
Some((p, m)) => (p.to_string(), m.to_string()),
None => (String::new(), cfg.model.id.clone()),
};
let labels: Vec<String> = CATALOG
.iter()
.map(|e| {
if e.prefix == current_provider {
format!("{} ← current", e.label)
} else {
e.label.to_string()
}
})
.collect();
let default_idx = CATALOG
.iter()
.position(|e| e.prefix == current_provider)
.unwrap_or(0);
let provider_idx = Select::with_theme(&theme)
.with_prompt("Provider")
.items(&labels)
.default(default_idx)
.interact()
.context("provider picker")?;
let entry = &CATALOG[provider_idx];
let model = pick_model(&theme, entry, ¤t_model, ¤t_provider)?;
let Some(model) = model else {
println!("No change.");
return Ok(());
};
cfg.model = ModelConfig {
id: format!("{}:{}", entry.prefix, model),
base_url: None,
api_key_env: None,
temperature: cfg.model.temperature,
max_tokens: cfg.model.max_tokens,
};
let path = merlion_config::save(&cfg)?;
println!();
println!("Set model = {} ({})", cfg.model.id, path.display());
let resolved = cfg.resolve_provider()?;
let key_env = resolved.api_key_env;
if std::env::var(&key_env)
.ok()
.filter(|v| !v.is_empty())
.is_none()
{
prompt_and_save_key(&env_path, &key_env)?;
} else {
println!("Using existing {key_env} from environment.");
}
Ok(())
}
fn pick_model(
theme: &ColorfulTheme,
entry: &crate::setup::ProviderEntry,
current_model: &str,
current_provider: &str,
) -> Result<Option<String>> {
const CUSTOM: &str = "Enter custom model name…";
const KEEP: &str = "Keep current (no change)";
let mut items: Vec<String> = entry
.models
.iter()
.map(|m| {
if entry.prefix == current_provider && *m == current_model {
format!("{m} ← current")
} else {
(*m).to_string()
}
})
.collect();
items.push(CUSTOM.to_string());
items.push(KEEP.to_string());
let default_idx = if entry.prefix == current_provider {
entry
.models
.iter()
.position(|m| *m == current_model)
.unwrap_or(0)
} else {
0
};
let idx = Select::with_theme(theme)
.with_prompt("Model")
.items(&items)
.default(default_idx)
.interact()
.context("model picker")?;
let custom_idx = entry.models.len();
let keep_idx = entry.models.len() + 1;
if idx == keep_idx {
return Ok(None);
}
if idx == custom_idx {
let custom: String = Input::with_theme(theme)
.with_prompt("Model name")
.default(entry.default_model().to_string())
.interact_text()
.context("custom model input")?;
return Ok(Some(custom));
}
Ok(Some(entry.models[idx].to_string()))
}
fn prompt_and_save_key(env_path: &std::path::Path, key_env: &str) -> Result<()> {
let theme = ColorfulTheme::default();
let prompt = format!("{key_env} (press Enter to skip and add it manually later)");
let key: String = Password::with_theme(&theme)
.with_prompt(prompt)
.allow_empty_password(true)
.interact()
.context("API key input")?;
let trimmed = key.trim();
if trimmed.is_empty() {
println!(
"No API key entered. Add `{key_env}=...` to {} before running `merlion`.",
env_path.display()
);
} else {
append_env_line(env_path, key_env, trimmed)?;
println!("Saved {key_env} to {}", env_path.display());
}
Ok(())
}
#[allow(dead_code)]
pub fn label_for_id(id: &str) -> Option<&'static str> {
let (prefix, _) = id.split_once(':')?;
catalog_entry(prefix).map(|e| e.label)
}