Skip to main content

podbox/wizard/
mod.rs

1use crate::config::Config;
2use crate::profiles;
3
4mod prompts;
5pub mod shell;
6mod summary;
7
8pub use shell::{ShellInfo, apply_shell_defaults, detect_host_shell};
9
10/// Result of the interactive wizard.
11pub struct WizardResult {
12    pub config: Config,
13    pub name: String,
14    pub confirmed: bool,
15}
16
17enum ProfileChoice<'a> {
18    Custom,
19    Named(&'a profiles::Profile, bool),
20}
21
22/// Run the interactive init wizard.
23pub fn run_wizard(
24    profiles_data: &[profiles::Profile],
25    detected_shell: &shell::ShellInfo,
26) -> anyhow::Result<WizardResult> {
27    let (mut config, default_name) = match prompts::prompt_profile(profiles_data) {
28        ProfileChoice::Named(profile, customize) => {
29            let mut cfg: Config = toml::from_str(&profile.toml).map_err(|e| {
30                anyhow::anyhow!("failed to parse profile '{}': {}", profile.name, e)
31            })?;
32            if customize {
33                cfg = prompts::prompt_customize_profile(cfg)?;
34            }
35            (cfg, profile.name.clone())
36        }
37        ProfileChoice::Custom => prompts::prompt_custom_image()?,
38    };
39
40    // Phase 2: Container
41    println!("\n── Container ──\n");
42    let name = prompts::prompt_name(&default_name, &crate::config::config_dir())?;
43    config.container.name = name.clone();
44    config.image.name = name.clone();
45    config.container.home = crate::config::expand_tilde(&format!("~/containers/{}", name));
46
47    let shell = prompts::prompt_shell(detected_shell);
48    config.container.shell = shell.full_path.clone();
49    if !config
50        .image
51        .packages
52        .install
53        .iter()
54        .any(|p| p == &shell.package_name)
55    {
56        config
57            .image
58            .packages
59            .install
60            .push(shell.package_name.clone());
61    }
62
63    if let Some(mem) = prompts::prompt_memory() {
64        config.container.memory = Some(mem);
65    }
66
67    let mounts = prompts::prompt_extra_mounts();
68    config.container.mounts.extra = mounts;
69
70    // Phase 3: Integration
71    println!("\n── Integration ──\n");
72    config.integration.wayland = prompts::prompt_wayland();
73    config.integration.audio = prompts::prompt_audio();
74    config.integration.dbus = prompts::prompt_dbus();
75    config.integration.gpu = prompts::prompt_gpu();
76    config.integration.sync_themes = prompts::confirm_default("Sync themes from host?", true);
77    config.integration.sync_icons = prompts::confirm_default("Sync icons from host?", true);
78    config.integration.sync_fonts = prompts::confirm_default("Sync fonts from host?", true);
79
80    let (notify, clipboard, xdg_open, ssh_agent) = prompts::prompt_integration_extras(
81        config.integration.notify,
82        config.integration.clipboard,
83        config.integration.xdg_open,
84        config.integration.ssh_agent,
85    );
86    config.integration.notify = notify;
87    config.integration.clipboard = clipboard;
88    config.integration.xdg_open = xdg_open;
89    config.integration.ssh_agent = ssh_agent;
90
91    config.integration.xdg_dirs = prompts::prompt_xdg_dirs();
92
93    // Phase 4: Lifecycle
94    println!("\n── Lifecycle ──\n");
95    let (quadlet, autostart) = prompts::prompt_lifecycle();
96    config.lifecycle.quadlet = quadlet;
97    config.lifecycle.autostart = autostart;
98
99    config.lifecycle.on_stop = prompts::prompt_on_stop();
100    config.lifecycle.auto_update = prompts::prompt_auto_update(&config);
101
102    // Phase 5: Review
103    summary::print_summary(&config, &name);
104    let toml_str = toml::to_string_pretty(&config)
105        .map_err(|e| anyhow::anyhow!("failed to serialize config: {}", e))?;
106    let confirmed = summary::preview_and_confirm(&toml_str);
107
108    Ok(WizardResult {
109        config,
110        name,
111        confirmed,
112    })
113}