tl_cli/cli/commands/
configure.rs1use 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
12pub 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 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 print_current_defaults(&config);
32
33 let provider_names: Vec<String> = config.providers.keys().cloned().collect();
35
36 let default_provider = config.tl.provider.clone();
38 let provider = select_provider(&provider_names, default_provider.as_deref())?;
39
40 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 let default_model = config.tl.model.clone();
48 let model = select_model(&available_models, default_model.as_deref())?;
49
50 let default_to = config.tl.to.clone();
52 let to = select_target_language(default_to.as_deref())?;
53
54 let default_style = config.tl.style.clone();
56 let style = select_style(&config, default_style.as_deref())?;
57
58 config.tl = TlConfig {
60 provider: Some(provider),
61 model: Some(model),
62 to: Some(to),
63 style,
64 };
65
66 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 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 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 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 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 let mut options: Vec<String> = vec!["(none)".to_string()];
187
188 for preset in PRESETS {
190 options.push(format!("{} - {}", preset.key, preset.description));
191 }
192
193 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 let default_index = default
205 .and_then(|d| {
206 if let Some(idx) = PRESETS.iter().position(|p| p.key == d) {
208 return Some(idx + 1); }
210 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 if selection == "(none)" {
224 return Ok(None);
225 }
226
227 let key = selection.split(" - ").next().unwrap_or(&selection);
229 Ok(Some(key.to_string()))
230}