mecha10-cli 0.8.0

Mecha10 CLI tool
Documentation
//! Interactive wizard for project initialization
//!
//! Terminal-based interactive menus for project setup, used by the init handler.

use crate::services::build_platforms::BUILD_PLATFORMS;
use crate::services::catalog_client::{self, template_node_defaults};
use crate::services::ComponentCatalogService;
use crate::ui::{InitWizardState, InitWizardTui, NodeOption, TemplateOption};
use anyhow::{Context, 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;

pub type WizardResult = (String, Option<String>, Vec<String>, 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 async fn run_wizard_tui(default_name: Option<&str>, catalog: &ComponentCatalogService) -> Result<WizardResult> {
    let templates = catalog_client::fetch_template_catalog()
        .await
        .context("Failed to fetch template catalog - mecha10 init cannot continue without it")?;
    let template_options: Vec<TemplateOption> = templates
        .iter()
        .map(|t| TemplateOption {
            id: t.name.clone(),
            name: t.name.clone(),
            description: t.description.clone(),
            nodes: t.nodes.clone(),
        })
        .collect();

    let mut node_entries = catalog_client::fetch_node_catalog()
        .await
        .context("Failed to fetch node catalog - mecha10 init cannot continue without it")?;
    node_entries.sort_by(|a, b| a.name.cmp(&b.name));

    let node_ids: Vec<String> = node_entries.iter().map(|e| e.name.clone()).collect();
    let node_options: Vec<NodeOption> = node_entries
        .iter()
        .map(|e| {
            let description = catalog
                .get(&e.name)
                .map(|c| c.description.clone())
                .unwrap_or_else(|| format!("@mecha10/{} framework node", e.name));
            NodeOption {
                id: e.name.clone(),
                name: e.name.clone(),
                description,
            }
        })
        .collect();

    let defaults = template_node_defaults(&templates, &None, &node_ids);

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

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

    let mut tui = InitWizardTui::new(state);

    let running = Arc::new(AtomicBool::new(true));
    let r = running.clone();

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

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

        if tui.is_confirmed() {
            break tui.into_state();
        }

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

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

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

    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();

    let robot_platform = BUILD_PLATFORMS[result.selected_platform].triple.to_string();

    let remote_platforms: Vec<String> = result
        .selected_remote_platforms
        .iter()
        .enumerate()
        .filter_map(|(idx, &selected)| {
            if selected {
                Some(BUILD_PLATFORMS[idx].triple.to_string())
            } else {
                None
            }
        })
        .collect();

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