ralph-tui 2.1.1

Terminal UI for Ralph Orchestrator using ratatui
Documentation
//! # ralph-tui
//!
//! Terminal user interface for the Ralph Orchestrator framework.
//!
//! Built with `ratatui` and `crossterm`, this crate provides:
//! - Read-only observation dashboard for monitoring agent orchestration
//! - Real-time display of agent messages and state
//! - Keyboard navigation and search

mod app;
pub mod input;
pub mod state;
pub mod widgets;

use anyhow::Result;
use app::App;
use ralph_proto::{Event, HatId};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::sync::watch;

pub use app::dispatch_action;
pub use state::TuiState;
pub use widgets::{footer, header};

/// Main TUI handle that integrates with the event bus.
pub struct Tui {
    state: Arc<Mutex<TuiState>>,
    terminated_rx: Option<watch::Receiver<bool>>,
    /// Channel to signal main loop on Ctrl+C.
    /// In raw terminal mode, SIGINT is not generated by the OS, so TUI must
    /// detect Ctrl+C via crossterm events and signal the main loop directly.
    interrupt_tx: Option<watch::Sender<bool>>,
}

impl Tui {
    /// Creates a new TUI instance with shared state.
    pub fn new() -> Self {
        Self {
            state: Arc::new(Mutex::new(TuiState::new())),
            terminated_rx: None,
            interrupt_tx: None,
        }
    }

    /// Sets the hat map for dynamic topic-to-hat resolution.
    ///
    /// This allows the TUI to display the correct hat for custom topics
    /// without hardcoding them in TuiState::update().
    #[must_use]
    pub fn with_hat_map(self, hat_map: HashMap<String, (HatId, String)>) -> Self {
        if let Ok(mut state) = self.state.lock() {
            *state = TuiState::with_hat_map(hat_map);
        }
        self
    }

    /// Sets the termination signal receiver for graceful shutdown.
    ///
    /// The TUI will exit when this receiver signals `true`.
    #[must_use]
    pub fn with_termination_signal(mut self, terminated_rx: watch::Receiver<bool>) -> Self {
        self.terminated_rx = Some(terminated_rx);
        self
    }

    /// Sets the interrupt channel for Ctrl+C signaling.
    ///
    /// In raw terminal mode, SIGINT is not generated by the OS when the user
    /// presses Ctrl+C. The TUI detects Ctrl+C via crossterm events and uses
    /// this channel to signal the main orchestration loop to terminate.
    #[must_use]
    pub fn with_interrupt_tx(mut self, interrupt_tx: watch::Sender<bool>) -> Self {
        self.interrupt_tx = Some(interrupt_tx);
        self
    }

    /// Returns the shared state for external updates.
    pub fn state(&self) -> Arc<Mutex<TuiState>> {
        Arc::clone(&self.state)
    }

    /// Returns an observer closure that updates TUI state from events.
    pub fn observer(&self) -> impl Fn(&Event) + Send + 'static {
        let state = Arc::clone(&self.state);
        move |event: &Event| {
            if let Ok(mut s) = state.lock() {
                s.update(event);
            }
        }
    }

    /// Runs the TUI application loop.
    ///
    /// # Panics
    ///
    /// Panics if `with_termination_signal()` was not called before running.
    ///
    /// # Errors
    ///
    /// Returns an error if the terminal cannot be initialized or
    /// if the application loop encounters an unrecoverable error.
    pub async fn run(self) -> Result<()> {
        let terminated_rx = self
            .terminated_rx
            .expect("Termination signal not set - call with_termination_signal() first");
        let app = App::new(Arc::clone(&self.state), terminated_rx, self.interrupt_tx);
        app.run().await
    }
}

impl Default for Tui {
    fn default() -> Self {
        Self::new()
    }
}