mecha10-cli 0.6.5

Mecha10 CLI tool
Documentation
//! Interactive TUI event loop for dev mode

mod keys;

use super::health_tail::HealthTailer;
use super::lifecycle::cleanup;
use super::registry::NodeRegistry;
use crate::context::CliContext;
use crate::dev::node_selection::NodeToRun;
use crate::dev::termination::{classify_key, TerminationIntent};
use crate::types::ProjectConfig;
use crate::ui::tui_layout::SidebarTui;
use crate::ui::LogBuffer;
use anyhow::Result;

/// Configuration for interactive mode
pub struct InteractiveModeConfig<'a> {
    pub project_config: &'a ProjectConfig,
    pub nodes_to_run: &'a [NodeToRun],
    pub project_name: &'a str,
}

/// Mutable state for interactive mode
pub struct InteractiveModeState {
    /// Node PID/failure bookkeeping, carried over from `DevSession` startup and updated by every
    /// `handle_mode_change` call. See `NodeRegistry`'s module docs for why this needs to persist
    /// beyond the single call that produced a given failure.
    pub node_registry: NodeRegistry,
    pub lifecycle: Option<crate::dev::lifecycle_adapter::CliLifecycleManager>,
    pub log_buffer: LogBuffer,
}

/// Run interactive mode with keyboard controls
pub async fn run_interactive_mode(
    ctx: &mut CliContext,
    config: InteractiveModeConfig<'_>,
    state: &mut InteractiveModeState,
) -> Result<()> {
    use crate::ui::DevModeTui;
    use crossterm::event::{self, Event};
    use crossterm::execute;
    use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
    use ratatui::{backend::CrosstermBackend, Terminal};
    use std::io::stdout;
    use std::sync::atomic::Ordering;
    use std::time::Duration;

    let running = crate::dev::termination::install_quit_signal()?;

    let redis_url = ctx.redis_url().to_string();

    if enable_raw_mode().is_err() {
        println!("\n(No interactive terminal — running in headless mode. Press Ctrl+C to stop.)\n");
        while running.load(Ordering::SeqCst) {
            tokio::time::sleep(Duration::from_millis(200)).await;
        }
        cleanup(ctx)?;
        return Ok(());
    }

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

    terminal.clear()?;

    let current_mode = state
        .lifecycle
        .as_ref()
        .map(|l| l.current_mode().to_string())
        .unwrap_or_else(|| "startup".to_string());
    let running_nodes: Vec<String> = state.node_registry.running_node_names();
    let dashboard_url = config.project_config.environments.dashboard_url();
    let mut dev_tui = DevModeTui::new(state.log_buffer.clone(), current_mode, running_nodes, dashboard_url);

    let mut health_tailer = HealthTailer::default();

    let result = loop {
        if !running.load(Ordering::SeqCst) {
            break Ok(());
        }

        health_tailer.poll(&state.node_registry.running_node_names(), &state.log_buffer);

        if let Err(e) = terminal.draw(|f| dev_tui.draw(f)) {
            break Err(e.into());
        }

        if event::poll(Duration::from_millis(100))? {
            if let Event::Key(key_event) = event::read()? {
                if let Some(intent) = classify_key(&key_event) {
                    match intent {
                        TerminationIntent::QuitAll => {
                            running.store(false, Ordering::SeqCst);
                            break Ok(());
                        }
                        TerminationIntent::StopCurrent => match key_event.code {
                            crossterm::event::KeyCode::Char('x') => {
                                if state.lifecycle.as_ref().map(|l| l.current_mode().to_lowercase())
                                    == Some("simulation".to_string())
                                {
                                    keys::handle_stop_simulation_key(ctx, &config, state, &mut terminal, &mut dev_tui)
                                        .await?;
                                }
                            }
                            _ => {
                                running.store(false, Ordering::SeqCst);
                                break Ok(());
                            }
                        },
                    }
                    continue;
                }

                if let Some(c) = dev_tui.handle_key(key_event) {
                    match c {
                        's' => {
                            keys::handle_simulation_key(
                                ctx,
                                &config,
                                state,
                                &mut terminal,
                                &mut dev_tui,
                                &redis_url,
                                &running,
                            )
                            .await?;
                        }
                        't' => {
                            keys::handle_teleop_key(
                                ctx,
                                &config,
                                state,
                                &mut terminal,
                                &mut dev_tui,
                                &redis_url,
                                &running,
                            )
                            .await?;
                        }
                        _ => {}
                    }
                }
            }
        }
    };

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

    cleanup(ctx)?;

    result
}