tl_cli/cli/commands/
configure.rs

1//! Configure command handler for editing default settings.
2
3use anyhow::{Result, bail};
4use inquire::{Select, Text};
5
6use super::load_config;
7use crate::config::{ConfigFile, TlConfig};
8use crate::style::{PRESETS, sorted_custom_keys};
9use crate::translation::SUPPORTED_LANGUAGES;
10use crate::ui::{Style, handle_prompt_cancellation};
11
12/// Runs the configure command to edit default settings.
13///
14/// Allows the user to interactively set the default provider, model, and target language.
15pub fn run_configure() -> Result<()> {
16    handle_prompt_cancellation(run_configure_inner)
17}
18
19fn run_configure_inner() -> Result<()> {
20    let (manager, mut config) = load_config()?;
21
22    // Check if at least one provider is configured
23    if config.providers.is_empty() {
24        bail!(
25            "No providers configured.\n\n\
26             Run 'tl providers add' to add a provider first."
27        );
28    }
29
30    // Display current defaults
31    print_current_defaults(&config);
32
33    // Get provider names for selection
34    let provider_names: Vec<String> = config.providers.keys().cloned().collect();
35
36    // Select default provider
37    let default_provider = config.tl.provider.clone();
38    let provider = select_provider(&provider_names, default_provider.as_deref())?;
39
40    // Get models for the selected provider
41    let provider_config = config.providers.get(&provider);
42    let available_models: Vec<String> = provider_config
43        .map(|p| p.models.clone())
44        .unwrap_or_default();
45
46    // Select default model
47    let default_model = config.tl.model.clone();
48    let model = select_model(&available_models, default_model.as_deref())?;
49
50    // Select default target language
51    let default_to = config.tl.to.clone();
52    let to = select_target_language(default_to.as_deref())?;
53
54    // Select default style (optional)
55    let default_style = config.tl.style.clone();
56    let style = select_style(&config, default_style.as_deref())?;
57
58    // Update config
59    config.tl = TlConfig {
60        provider: Some(provider),
61        model: Some(model),
62        to: Some(to),
63        style,
64    };
65
66    // Save config
67    manager.save(&config)?;
68
69    println!();
70    println!(
71        "{} Configuration saved to {}",
72        Style::success("✓"),
73        Style::secondary(manager.config_path().display().to_string())
74    );
75
76    Ok(())
77}
78
79fn print_current_defaults(config: &ConfigFile) {
80    println!("{}", Style::header("Current defaults"));
81    println!(
82        "  {}  {}",
83        Style::label("provider"),
84        config
85            .tl
86            .provider
87            .as_deref()
88            .map_or_else(|| Style::secondary("(not set)"), Style::value)
89    );
90    println!(
91        "  {}     {}",
92        Style::label("model"),
93        config
94            .tl
95            .model
96            .as_deref()
97            .map_or_else(|| Style::secondary("(not set)"), Style::value)
98    );
99    println!(
100        "  {}        {}",
101        Style::label("to"),
102        config
103            .tl
104            .to
105            .as_deref()
106            .map_or_else(|| Style::secondary("(not set)"), Style::value)
107    );
108    println!(
109        "  {}     {}",
110        Style::label("style"),
111        config
112            .tl
113            .style
114            .as_deref()
115            .map_or_else(|| Style::secondary("(not set)"), Style::value)
116    );
117    println!();
118}
119
120fn select_provider(providers: &[String], default: Option<&str>) -> Result<String> {
121    let default_index = default
122        .and_then(|d| providers.iter().position(|p| p == d))
123        .unwrap_or(0);
124
125    let selection = Select::new("Default provider:", providers.to_vec())
126        .with_starting_cursor(default_index)
127        .prompt()?;
128
129    Ok(selection)
130}
131
132fn select_model(available_models: &[String], default: Option<&str>) -> Result<String> {
133    if available_models.is_empty() {
134        // No models configured, fall back to text input
135        let mut prompt = Text::new("Default model:").with_help_message("Enter the model name");
136
137        if let Some(d) = default {
138            prompt = prompt.with_default(d);
139        }
140
141        let model = prompt.prompt()?;
142
143        if model.trim().is_empty() {
144            bail!("Model name cannot be empty");
145        }
146
147        Ok(model.trim().to_string())
148    } else {
149        // Models available, use selection
150        let default_index = default
151            .and_then(|d| available_models.iter().position(|m| m == d))
152            .unwrap_or(0);
153
154        let selection = Select::new("Default model:", available_models.to_vec())
155            .with_starting_cursor(default_index)
156            .prompt()?;
157
158        Ok(selection)
159    }
160}
161
162fn select_target_language(default: Option<&str>) -> Result<String> {
163    // Build options with format "code - Name"
164    let options: Vec<String> = SUPPORTED_LANGUAGES
165        .iter()
166        .map(|(code, name)| format!("{code} - {name}"))
167        .collect();
168
169    let default_index = default
170        .and_then(|d| SUPPORTED_LANGUAGES.iter().position(|(code, _)| *code == d))
171        .unwrap_or(0);
172
173    let selection = Select::new("Default target language:", options)
174        .with_starting_cursor(default_index)
175        .prompt()?;
176
177    // Extract code from "code - Name" format
178    // split() always returns at least one element, but we use unwrap_or as fallback
179    let code = selection.split(" - ").next().unwrap_or(&selection);
180
181    Ok(code.to_string())
182}
183
184fn select_style(config: &ConfigFile, default: Option<&str>) -> Result<Option<String>> {
185    // Build options: "(none)" + presets + custom styles
186    let mut options: Vec<String> = vec!["(none)".to_string()];
187
188    // Add presets
189    for preset in PRESETS {
190        options.push(format!("{} - {}", preset.key, preset.description));
191    }
192
193    // Add custom styles
194    let custom_keys = sorted_custom_keys(&config.styles);
195    for key in &custom_keys {
196        let desc = config
197            .styles
198            .get(*key)
199            .map_or("", |s| s.description.as_str());
200        options.push(format!("{key} - {desc}"));
201    }
202
203    // Find default index
204    let default_index = default
205        .and_then(|d| {
206            // Check presets
207            if let Some(idx) = PRESETS.iter().position(|p| p.key == d) {
208                return Some(idx + 1); // +1 for "(none)"
209            }
210            // Check custom styles
211            if let Some(idx) = custom_keys.iter().position(|k| *k == d) {
212                return Some(PRESETS.len() + 1 + idx);
213            }
214            None
215        })
216        .unwrap_or(0);
217
218    let selection = Select::new("Default style:", options)
219        .with_starting_cursor(default_index)
220        .prompt()?;
221
222    // Parse selection
223    if selection == "(none)" {
224        return Ok(None);
225    }
226
227    // Extract key from "key - description" format
228    let key = selection.split(" - ").next().unwrap_or(&selection);
229    Ok(Some(key.to_string()))
230}