package wizard import ( "context" "errors" "fmt" "strings" "github.com/charmbracelet/huh" "github.com/MDietermann/ArduinoArchSetup/internal/module" "github.com/MDietermann/ArduinoArchSetup/internal/tui" ) // Run presents the six-screen wizard. Returns the final state and whether // the user confirmed. A context cancellation returns (state, false, nil). func Run(ctx context.Context, reg *module.Registry, initial tui.WizardState) (tui.WizardState, bool, error) { s := initial themeOptions := make([]huh.Option[string], 0, len(tui.BuiltinThemes)) for _, t := range tui.BuiltinThemes { themeOptions = append(themeOptions, huh.NewOption(t.Name, t.ID)) } moduleOptions := make([]huh.Option[string], 0, len(s.Modules)) defaults := make([]string, 0, len(s.Modules)) for _, m := range s.Modules { label := m.Name if m.Description != "" { label = fmt.Sprintf("%s — %s", m.Name, m.Description) } moduleOptions = append(moduleOptions, huh.NewOption(label, m.ID)) if m.Selected { defaults = append(defaults, m.ID) } } selectedModules := defaults parallelismStr := fmt.Sprintf("%d", s.Parallelism) var proceed bool form := huh.NewForm( huh.NewGroup(huh.NewNote(). Title("Archduino Installer"). Description("A TUI Arduino development environment.\n\nPress Enter to continue.")), huh.NewGroup(huh.NewSelect[string](). Title("Pick a colorscheme"). Options(themeOptions...). Value(&s.ThemeID)), huh.NewGroup(huh.NewMultiSelect[string](). Title("Select modules to install"). Options(moduleOptions...). Value(&selectedModules)), huh.NewGroup( huh.NewInput(). Title("Parallelism (advanced)"). Description("How many modules to run concurrently. 1 is safe."). Value(¶llelismStr), huh.NewConfirm(). Title("Halt on first error?"). Value(&s.HaltOnError), ), huh.NewGroup(huh.NewNote(). Title("Review plan"). DescriptionFunc(func() string { s.Modules = syncSelected(s.Modules, selectedModules) return renderPlan(ctx, reg, s.SelectedIDs()) }, &selectedModules)), huh.NewGroup(huh.NewConfirm(). Title("Proceed with installation?"). Value(&proceed)), ) if err := form.RunWithContext(ctx); err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, huh.ErrUserAborted) { return s, false, nil } return s, false, err } s.Modules = syncSelected(s.Modules, selectedModules) // Parse parallelism: clamp to 1 on any parse failure, since the wizard // uses a free-form Input; a stricter schema can come later. if n, perr := parseIntClamp(parallelismStr, 1); perr == nil { s.Parallelism = n } s.Confirmed = proceed return s, proceed, nil } // syncSelected returns a copy of mods with Selected set to whether the ID // appears in selected. func syncSelected(mods []tui.ModuleChoice, selected []string) []tui.ModuleChoice { set := map[string]bool{} for _, id := range selected { set[id] = true } out := make([]tui.ModuleChoice, len(mods)) copy(out, mods) for i := range out { out[i].Selected = set[out[i].ID] } return out } // renderPlan runs Detect + Plan for every selected module and renders a // human-readable summary. A failing Detect/Plan is shown inline but does // not abort the wizard. func renderPlan(ctx context.Context, reg *module.Registry, selectedIDs []string) string { if len(selectedIDs) == 0 { return "No modules selected." } var b strings.Builder for _, id := range selectedIDs { m, ok := reg.Get(id) if !ok { fmt.Fprintf(&b, "• %s — (unknown)\n", id) continue } state, err := m.Detect(ctx) if err != nil { fmt.Fprintf(&b, "• %s — detect failed: %v\n", id, err) continue } plan, err := m.Plan(ctx, state) if err != nil { fmt.Fprintf(&b, "• %s — plan failed: %v\n", id, err) continue } if plan.IsEmpty() { fmt.Fprintf(&b, "• %s — already up to date\n", id) continue } fmt.Fprintf(&b, "• %s\n", id) for _, a := range plan.Actions { fmt.Fprintf(&b, " - %s\n", a.Description) } } return b.String() } // parseIntClamp parses s as int. Empty or invalid input returns fallback. func parseIntClamp(s string, fallback int) (int, error) { s = strings.TrimSpace(s) if s == "" { return fallback, nil } var n int if _, err := fmt.Sscanf(s, "%d", &n); err != nil || n < 1 { return fallback, nil } return n, nil }