mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Dev mode interactive UI component
//!
//! Provides a terminal-based keyboard event loop for development mode.
//! Handles 's' (simulator), 't' (teleop), and Ctrl+C/Ctrl+X (exit) commands.

#![allow(dead_code)] // Component designed for reuse, not all functions used yet

use anyhow::Result;
use crossterm::{
    cursor,
    event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
    terminal,
};
use std::io::stdout;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;

/// Terminal cleanup guard to ensure terminal is restored even on panic/kill
struct TerminalGuard;

impl TerminalGuard {
    fn new() -> Result<Self> {
        terminal::enable_raw_mode()?;
        Ok(Self)
    }
}

impl Drop for TerminalGuard {
    fn drop(&mut self) {
        use crossterm::execute;

        // Always restore terminal state, even on panic
        let _ = terminal::disable_raw_mode();

        // Show cursor for clean exit
        let mut stdout = stdout();
        let _ = execute!(stdout, cursor::Show);
    }
}

/// Dev mode interactive UI
///
/// Handles keyboard events for development mode:
/// - 's': Start simulator
/// - 't': Enter teleop mode
/// - Ctrl+C or Ctrl+X: Exit
pub struct DevModeUi {
    running: Arc<AtomicBool>,
}

impl DevModeUi {
    /// Create a new dev mode UI
    pub fn new(running: Arc<AtomicBool>) -> Self {
        Self { running }
    }

    /// Display interactive instructions
    pub fn display_instructions() {
        println!("Development mode active");
        println!();
        println!("Commands:");
        println!("  's' - Switch to simulation mode and start simulator");
        println!("  't' - Switch to teleop mode");
        println!("  Ctrl+C or Ctrl+X - Stop all nodes and exit");
        println!();
        println!("Note: If terminal gets messed up, run: reset");
        println!();
    }

    /// Run the interactive event loop with callbacks
    ///
    /// # Arguments
    ///
    /// * `on_simulator` - Async callback for 's' key press
    /// * `on_teleop` - Async callback for 't' key press
    ///
    /// # Returns
    ///
    /// Ok(()) if user exits cleanly, Err if an error occurs
    pub async fn run<SF, TF, SFut, TFut>(&mut self, mut on_simulator: SF, mut on_teleop: TF) -> Result<()>
    where
        SF: FnMut() -> SFut,
        TF: FnMut() -> TFut,
        SFut: std::future::Future<Output = Result<()>>,
        TFut: std::future::Future<Output = Result<()>>,
    {
        // Display instructions before entering raw mode
        Self::display_instructions();

        // Enable raw mode with cleanup guard
        let _guard = TerminalGuard::new()?;

        // Main event loop
        while self.running.load(Ordering::SeqCst) {
            // Poll for keyboard events with timeout
            if event::poll(Duration::from_millis(100))? {
                if let Event::Key(key_event) = event::read()? {
                    // Handle key event
                    match self
                        .handle_key_event(key_event, &mut on_simulator, &mut on_teleop)
                        .await
                    {
                        Ok(true) => continue,    // Continue loop
                        Ok(false) => break,      // Exit requested
                        Err(e) => return Err(e), // Error occurred
                    }
                }
            }
        }

        // Guard drops here - terminal is cleaned up automatically

        Ok(())
    }

    /// Handle a single key event
    ///
    /// # Returns
    ///
    /// - Ok(true): Continue event loop
    /// - Ok(false): Exit event loop
    /// - Err: Error occurred
    async fn handle_key_event<SF, TF, SFut, TFut>(
        &mut self,
        key_event: KeyEvent,
        on_simulator: &mut SF,
        on_teleop: &mut TF,
    ) -> Result<bool>
    where
        SF: FnMut() -> SFut,
        TF: FnMut() -> TFut,
        SFut: std::future::Future<Output = Result<()>>,
        TFut: std::future::Future<Output = Result<()>>,
    {
        match key_event.code {
            // Ctrl+C or Ctrl+X exits (in raw mode, this is a keyboard event, not a signal)
            KeyCode::Char('c') | KeyCode::Char('x') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
                self.running.store(false, Ordering::SeqCst);
                Ok(false)
            }
            KeyCode::Char('s') => {
                // Temporarily disable raw mode for clean printing
                terminal::disable_raw_mode()?;

                // Call simulator callback
                on_simulator().await?;

                // Re-display instructions
                Self::display_instructions();

                // Re-enable raw mode
                terminal::enable_raw_mode()?;

                Ok(true)
            }
            KeyCode::Char('t') => {
                // Temporarily disable raw mode for clean printing
                terminal::disable_raw_mode()?;

                // Call teleop callback
                on_teleop().await?;

                // Re-display instructions
                Self::display_instructions();

                // Re-enable raw mode
                terminal::enable_raw_mode()?;

                Ok(true)
            }
            _ => {
                // Ignore other keys
                Ok(true)
            }
        }
    }

    /// Explicitly disable terminal raw mode (for cleanup)
    pub fn disable_raw_mode() -> Result<()> {
        terminal::disable_raw_mode()?;
        Ok(())
    }

    /// Explicitly enable terminal raw mode
    pub fn enable_raw_mode() -> Result<()> {
        terminal::enable_raw_mode()?;
        Ok(())
    }
}