mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Interactive wizard for project initialization
//!
//! Provides terminal-based interactive menus for project setup.
//!
//! This UI component is used by the init handler to collect user input
//! for project creation in interactive mode.

use crate::init::{get_all_templates, get_template_defaults};
use crate::services::ComponentCatalogService;
use crate::ui::{InitWizardState, InitWizardTui, NodeOption, TemplateOption};
use anyhow::Result;
use crossterm::{
    event::{self, Event},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io::stdout;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;

/// Result type for wizard: (project_name, template, nodes)
pub type WizardResult = (String, Option<String>, Vec<String>);

/// Run the interactive wizard with TUI
///
/// Provides a 3-panel TUI interface for project initialization.
///
/// Returns a tuple of (name, template, nodes)
pub fn run_wizard_tui(default_name: Option<&str>, catalog: &ComponentCatalogService) -> Result<WizardResult> {
    // Get templates
    let templates = get_all_templates();
    let template_options: Vec<TemplateOption> = templates
        .iter()
        .map(|t| TemplateOption {
            id: t.id().to_string(),
            name: t.name().to_string(),
            description: t.description().to_string(),
        })
        .collect();

    // Get nodes
    let mut node_components = catalog.get_by_category("nodes");
    node_components.sort_by(|a, b| a.name.cmp(&b.name));

    let node_ids: Vec<String> = node_components.iter().map(|c| c.id.clone()).collect();
    let node_options: Vec<NodeOption> = node_components
        .iter()
        .map(|c| NodeOption {
            id: c.id.clone(),
            name: c.name.clone(),
            description: c.description.clone(),
        })
        .collect();

    // Preselect nodes based on template defaults (basic template since none selected yet)
    let defaults = get_template_defaults(&None, &node_ids);

    // Create initial state
    let mut state = InitWizardState::new(template_options, node_options, defaults);
    if let Some(name) = default_name {
        state.project_name = name.to_string();
    }

    // Setup terminal
    enable_raw_mode()?;
    let mut stdout_handle = stdout();
    execute!(stdout_handle, EnterAlternateScreen)?;
    let backend = CrosstermBackend::new(stdout_handle);
    let mut terminal = Terminal::new(backend)?;

    // Create TUI
    let mut tui = InitWizardTui::new(state);

    // Setup signal handler
    let running = Arc::new(AtomicBool::new(true));
    let r = running.clone();

    ctrlc::set_handler(move || {
        r.store(false, Ordering::SeqCst);
    })?;

    // Main event loop
    let result = loop {
        // Check interrupt flag
        if !running.load(Ordering::SeqCst) || tui.should_quit() {
            // Cleanup terminal
            disable_raw_mode()?;
            execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
            terminal.show_cursor()?;
            return Err(anyhow::anyhow!("Wizard cancelled"));
        }

        // Check if confirmed
        if tui.is_confirmed() {
            break tui.into_state();
        }

        // Draw TUI
        if let Err(e) = terminal.draw(|f| tui.draw(f)) {
            // Cleanup terminal
            disable_raw_mode()?;
            execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
            terminal.show_cursor()?;
            return Err(e.into());
        }

        // Poll for events
        if event::poll(Duration::from_millis(100))? {
            if let Event::Key(key_event) = event::read()? {
                tui.handle_key(key_event);
            }
        }
    };

    // Cleanup terminal
    disable_raw_mode()?;
    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
    terminal.show_cursor()?;

    // Extract results
    let template = result.selected_template.map(|idx| result.templates[idx].id.clone());
    let nodes: Vec<String> = result
        .selected_nodes
        .iter()
        .enumerate()
        .filter_map(|(idx, &selected)| {
            if selected {
                result.nodes.get(idx).map(|n| n.id.clone())
            } else {
                None
            }
        })
        .collect();

    Ok((result.project_name, template, nodes))
}