mecha10-cli 0.8.0

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;

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

pub struct InteractiveModeState {
    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: Result<()> = async {
        loop {
            if !running.load(Ordering::SeqCst) {
                return Ok(());
            }

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

            terminal.draw(|f| dev_tui.draw(f))?;

            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);
                                return 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);
                                    return 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?;
                            }
                            _ => {}
                        }
                    }
                }
            }
        }
    }
    .await;

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

    cleanup(ctx)?;

    result
}