1use {
2 crate::{config::GlobalConfig, error::CliResult, style, ConfigAction},
3 dialoguer::{theme::ColorfulTheme, Select},
4};
5
6pub fn run(action: Option<ConfigAction>) -> CliResult {
7 let mut config = GlobalConfig::load();
8
9 match action {
10 None => run_interactive(&mut config)?,
12 Some(ConfigAction::Get { key }) => {
13 let val = get_value(&config, &key);
14 match val {
15 Some(v) => println!("{v}"),
16 None => unknown_key(&key),
17 }
18 }
19 Some(ConfigAction::Set { key, value }) => {
20 if let Err(valid) = validate_value(&key, &value) {
21 eprintln!(
22 " {}",
23 style::fail(&format!("invalid value for {key}: {value}"))
24 );
25 eprintln!(" {}", style::dim(&format!("valid: {valid}")));
26 std::process::exit(1);
27 }
28 if set_value(&mut config, &key, &value) {
29 config.save()?;
30 println!(" {}", style::success(&format!("{key} = {value}")));
31 } else {
32 unknown_key(&key);
33 }
34 }
35 Some(ConfigAction::List) => print_all(&config),
36 Some(ConfigAction::Reset) => {
37 let was_animated = config.ui.animation;
38 config = GlobalConfig::default();
39 if !was_animated {
41 config.ui.animation = false;
42 }
43 config.save()?;
44 println!(" {}", style::success("config reset to defaults"));
45 println!();
46 print_all(&config);
47 }
48 }
49
50 Ok(())
51}
52
53fn unknown_key(key: &str) -> ! {
54 eprintln!(" {}", style::fail(&format!("unknown config key: {key}")));
55 eprintln!();
56 eprintln!(" Available keys:");
57 eprintln!(" defaults.toolchain, defaults.framework, defaults.template");
58 eprintln!(" ui.animation, ui.color");
59 std::process::exit(1);
60}
61
62fn print_all(config: &GlobalConfig) {
63 let path = GlobalConfig::path();
64 println!(" {}", style::dim(&format!("config: {}", path.display())));
65 println!();
66 println!(" [defaults]");
67 println!(
68 " toolchain = {}",
69 config.defaults.toolchain.as_deref().unwrap_or("(not set)")
70 );
71 println!(
72 " framework = {}",
73 config.defaults.framework.as_deref().unwrap_or("(not set)")
74 );
75 println!(
76 " template = {}",
77 config.defaults.template.as_deref().unwrap_or("(not set)")
78 );
79 println!();
80 println!(" [ui]");
81 println!(" animation = {}", config.ui.animation);
82 println!(" color = {}", config.ui.color);
83}
84
85struct ConfigItem {
90 key: &'static str,
91 label: &'static str,
92 kind: ConfigKind,
93}
94
95enum ConfigKind {
96 Bool,
97 Choice(&'static [&'static str]),
98}
99
100const ITEMS: &[ConfigItem] = &[
101 ConfigItem {
102 key: "defaults.toolchain",
103 label: "Default toolchain",
104 kind: ConfigKind::Choice(&["solana", "upstream"]),
105 },
106 ConfigItem {
107 key: "defaults.framework",
108 label: "Default test framework",
109 kind: ConfigKind::Choice(&[
110 "none",
111 "mollusk",
112 "quasarsvm-rust",
113 "quasarsvm-web3js",
114 "quasarsvm-kit",
115 ]),
116 },
117 ConfigItem {
118 key: "defaults.template",
119 label: "Default template",
120 kind: ConfigKind::Choice(&["minimal", "full"]),
121 },
122 ConfigItem {
123 key: "ui.animation",
124 label: "Show init animation",
125 kind: ConfigKind::Bool,
126 },
127 ConfigItem {
128 key: "ui.color",
129 label: "Colored output",
130 kind: ConfigKind::Bool,
131 },
132];
133
134fn run_interactive(config: &mut GlobalConfig) -> CliResult {
135 let theme = ColorfulTheme::default();
136 let path = GlobalConfig::path();
137
138 loop {
139 let items: Vec<String> = ITEMS
140 .iter()
141 .map(|item| {
142 let val = get_value(config, item.key).unwrap_or_default();
143 format!("{:<24} {}", item.label, style::dim(&val))
144 })
145 .chain(std::iter::once(String::from("Exit")))
146 .collect();
147
148 println!();
149 println!(" {}", style::dim(&format!("config: {}", path.display())));
150
151 let selection = Select::with_theme(&theme)
152 .with_prompt(" Settings")
153 .items(&items)
154 .default(0)
155 .interact_opt()
156 .unwrap_or(None);
157
158 let Some(idx) = selection else {
159 break;
160 };
161
162 if idx >= ITEMS.len() {
163 break;
164 }
165
166 let item = &ITEMS[idx];
167 let changed = match &item.kind {
168 ConfigKind::Bool => toggle_bool(config, item, &theme),
169 ConfigKind::Choice(options) => pick_choice(config, item, options, &theme),
170 };
171
172 if changed {
173 config.save()?;
174 println!(" {}", style::success(&format!("{} saved", item.key)));
175 }
176 }
177
178 Ok(())
179}
180
181fn toggle_bool(config: &mut GlobalConfig, item: &ConfigItem, theme: &ColorfulTheme) -> bool {
182 let current = get_value(config, item.key).unwrap_or_default();
183 let current_bool = current == "true";
184 let options = ["true", "false"];
185 let default = if current_bool { 0 } else { 1 };
186
187 let sel = Select::with_theme(theme)
188 .with_prompt(format!(" {}", item.label))
189 .items(&options)
190 .default(default)
191 .interact_opt()
192 .unwrap_or(None);
193
194 if let Some(idx) = sel {
195 let new_val = options[idx];
196 if new_val != current {
197 set_value(config, item.key, new_val);
198 return true;
199 }
200 }
201 false
202}
203
204fn pick_choice(
205 config: &mut GlobalConfig,
206 item: &ConfigItem,
207 options: &[&str],
208 theme: &ColorfulTheme,
209) -> bool {
210 let current = get_value(config, item.key).unwrap_or_default();
211 let default = options.iter().position(|&o| o == current).unwrap_or(0);
212
213 let sel = Select::with_theme(theme)
214 .with_prompt(format!(" {}", item.label))
215 .items(options)
216 .default(default)
217 .interact_opt()
218 .unwrap_or(None);
219
220 if let Some(idx) = sel {
221 let new_val = options[idx];
222 if new_val != current {
223 set_value(config, item.key, new_val);
224 return true;
225 }
226 }
227 false
228}
229
230fn get_value(config: &GlobalConfig, key: &str) -> Option<String> {
235 match key {
236 "defaults.toolchain" => Some(
237 config
238 .defaults
239 .toolchain
240 .as_deref()
241 .unwrap_or("(not set)")
242 .to_string(),
243 ),
244 "defaults.framework" => Some(
245 config
246 .defaults
247 .framework
248 .as_deref()
249 .unwrap_or("(not set)")
250 .to_string(),
251 ),
252 "defaults.template" => Some(
253 config
254 .defaults
255 .template
256 .as_deref()
257 .unwrap_or("(not set)")
258 .to_string(),
259 ),
260 "ui.animation" => Some(config.ui.animation.to_string()),
261 "ui.color" => Some(config.ui.color.to_string()),
262 _ => None,
263 }
264}
265
266fn set_value(config: &mut GlobalConfig, key: &str, value: &str) -> bool {
267 match key {
268 "defaults.toolchain" => config.defaults.toolchain = some_or_none(value),
269 "defaults.framework" => config.defaults.framework = some_or_none(value),
270 "defaults.template" => config.defaults.template = some_or_none(value),
271 "ui.animation" => config.ui.animation = parse_bool(value),
272 "ui.color" => config.ui.color = parse_bool(value),
273 _ => return false,
274 }
275 true
276}
277
278fn validate_value(key: &str, value: &str) -> Result<(), &'static str> {
280 match key {
281 "defaults.toolchain" => {
282 if matches!(value, "solana" | "upstream" | "none" | "null" | "") {
283 Ok(())
284 } else {
285 Err("solana, upstream")
286 }
287 }
288 "defaults.framework" => {
289 if matches!(
290 value,
291 "none"
292 | "mollusk"
293 | "quasarsvm-rust"
294 | "quasarsvm-web3js"
295 | "quasarsvm-kit"
296 | "null"
297 | ""
298 ) {
299 Ok(())
300 } else {
301 Err("none, mollusk, quasarsvm-rust, quasarsvm-web3js, quasarsvm-kit")
302 }
303 }
304 "defaults.template" => {
305 if matches!(value, "minimal" | "full" | "none" | "null" | "") {
306 Ok(())
307 } else {
308 Err("minimal, full")
309 }
310 }
311 "ui.animation" | "ui.color" => {
312 if matches!(
313 value,
314 "true" | "false" | "1" | "0" | "yes" | "no" | "on" | "off"
315 ) {
316 Ok(())
317 } else {
318 Err("true, false")
319 }
320 }
321 _ => Ok(()), }
323}
324
325fn some_or_none(s: &str) -> Option<String> {
326 if s.is_empty() || s == "none" || s == "null" {
327 None
328 } else {
329 Some(s.to_string())
330 }
331}
332
333fn parse_bool(s: &str) -> bool {
334 matches!(s, "true" | "1" | "yes" | "on")
335}