o7 0.1.1

O7 workflow DSL runner
Documentation
//! Wizard module — `o7 wiz` interactive configuration TUI.

pub mod app;
pub mod flow;
pub mod global_config;
pub mod types;
pub mod ui;
pub mod writer;

use std::io::{self, stdout};
use std::time::Duration;

use crossterm::event::{
    Event as CtEvent, EventStream, KeyEventKind, KeyboardEnhancementFlags,
    PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
};
use crossterm::execute;
use crossterm::terminal::{
    EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use futures::StreamExt;
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use tokio::time::timeout;

use self::flow::build_wiz_flow;
use self::global_config::load_global_config;
use self::types::WizPhase;
use self::ui::draw_wiz;
use self::writer::perform_write;
use crate::harness::config::load_harness_config;

const TERMINAL_PHASE_EXIT_DELAY: Duration = Duration::from_millis(1500);

/// Run the interactive wizard TUI.
pub async fn run_wiz(project_root: &str) -> Result<(), Box<dyn std::error::Error>> {
    // Load existing config for pre-filling.
    let existing_harnesses = load_harness_config(project_root).ok();
    let existing_global = load_global_config().ok();

    let questions = build_wiz_flow(existing_harnesses.as_ref(), existing_global.as_ref());

    let mut state = self::types::WizState::new(questions);

    // Set up terminal.
    enable_raw_mode()?;
    let mut stdout = stdout();
    execute!(
        stdout,
        EnterAlternateScreen,
        PushKeyboardEnhancementFlags(
            KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
                | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
                | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
                | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
        )
    )?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    // Restore terminal on panic.
    let panic_hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        let _ = disable_raw_mode();
        let _ = execute!(
            io::stdout(),
            PopKeyboardEnhancementFlags,
            LeaveAlternateScreen
        );
        panic_hook(info);
    }));

    let mut reader = EventStream::new();

    // Initial draw.
    terminal.draw(|frame| draw_wiz(frame, &state))?;

    loop {
        match state.phase {
            WizPhase::Writing => {
                // Perform write synchronously, then transition.
                match perform_write(&state, project_root) {
                    Ok(()) => state.phase = WizPhase::Done,
                    Err(e) => {
                        state.error = Some(e);
                        state.phase = WizPhase::Done;
                    }
                }
                terminal.draw(|frame| draw_wiz(frame, &state))?;
            }
            WizPhase::Done | WizPhase::Aborted => {
                // Auto-exit after a brief pause, but allow immediate exit on keypress.
                match timeout(TERMINAL_PHASE_EXIT_DELAY, reader.next()).await {
                    Ok(Some(Ok(CtEvent::Key(key)))) if key.kind == KeyEventKind::Press => break,
                    Ok(Some(Ok(_))) => continue,
                    Ok(Some(Err(_))) | Ok(None) | Err(_) => break,
                }
            }
            _ => {
                if let Some(Ok(CtEvent::Key(key))) = reader.next().await {
                    if key.kind == KeyEventKind::Press {
                        let should_exit = state.handle_key(key);
                        if should_exit {
                            break;
                        }
                    }
                } else {
                    break;
                }
                terminal.draw(|frame| draw_wiz(frame, &state))?;
            }
        }
    }

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

    Ok(())
}