Skip to main content

quasar_cli/
cfg.rs

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        // No subcommand: interactive menu
11        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            // Preserve animation=false once it's been shown
40            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
85// ---------------------------------------------------------------------------
86// Interactive config menu
87// ---------------------------------------------------------------------------
88
89struct 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
230// ---------------------------------------------------------------------------
231// Get / Set helpers
232// ---------------------------------------------------------------------------
233
234fn 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
278/// Returns Ok(()) if valid, Err(valid_options_string) if not.
279fn 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(()), // unknown keys are handled elsewhere
322    }
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}