zinit 0.3.9

Process supervisor with dependency management
Documentation
//! Async TUI for zinit service management.
//!
//! This module provides a fully async, non-blocking terminal interface using:
//! - `ratatui` for terminal rendering
//! - `crossterm` with event-stream for async input
//! - `tokio` for async runtime and background tasks
//!
//! The architecture follows an event-driven pattern where:
//! - User input and timer events are handled via `tokio::select!`
//! - Long-running operations (RPC calls) run in background tasks
//! - Results are sent back via channels and processed in the main loop
//! - The UI never blocks, even during slow operations

mod actions;
mod app;
mod events;
mod ui;

use std::io;
use std::path::Path;

use crossterm::{
    execute,
    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, backend::CrosstermBackend};
use tokio::sync::mpsc;

use actions::{Action, ActionExecutor};
use app::App;
use events::{AppEvent, EventHandler};

/// Run the TUI interface.
///
/// This is the main entry point for the TUI. It sets up the terminal,
/// creates the async runtime, and runs the event loop.
pub fn run(socket_path: &Path) -> Result<(), String> {
    // Create tokio runtime
    let rt =
        tokio::runtime::Runtime::new().map_err(|e| format!("Failed to create runtime: {}", e))?;

    rt.block_on(async { run_async(socket_path).await })
}

async fn run_async(socket_path: &Path) -> Result<(), String> {
    // Setup terminal
    let mut terminal = setup_terminal().map_err(|e| format!("Failed to setup terminal: {}", e))?;

    // Run the app
    let result = run_app(&mut terminal, socket_path).await;

    // Always restore terminal, even on error
    if let Err(e) = restore_terminal(&mut terminal) {
        eprintln!("Warning: failed to restore terminal: {}", e);
    }

    result
}

async fn run_app(
    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
    socket_path: &Path,
) -> Result<(), String> {
    // Create channels for action dispatch and results
    let (action_tx, mut action_rx) = mpsc::unbounded_channel::<Action>();
    let (result_tx, result_rx) = mpsc::unbounded_channel();

    // Create the action executor for background tasks
    let executor = ActionExecutor::new(socket_path.to_path_buf(), result_tx);

    // Create the event handler
    let mut events = EventHandler::new(result_rx);

    // Create the app state
    let mut app = App::new(action_tx.clone());

    // Initial data fetch
    executor.spawn(Action::FetchServices);

    // Main event loop
    loop {
        // Draw the UI (non-blocking)
        terminal
            .draw(|f| ui::draw(f, &app))
            .map_err(|e| format!("Draw error: {}", e))?;

        // Process any queued actions from app.handle_key() that sent via channel
        while let Ok(action) = action_rx.try_recv() {
            executor.spawn(action);
        }

        // Wait for next event
        let event = events.next().await;

        match event {
            AppEvent::Tick => {
                // Periodic refresh
                if app.should_refresh() {
                    executor.spawn(Action::FetchServices);
                    if let Some(name) = app.selected_service_name() {
                        executor.spawn(Action::FetchLogs(name.clone()));
                        executor.spawn(Action::FetchChildren(name));
                    }
                }

                // Update pending operation timers
                app.update_pending_operations();
            }

            AppEvent::Render => {
                // Just triggers redraw, handled at top of loop
            }

            AppEvent::Key(key) => {
                if let Some(action) = app.handle_key(key) {
                    match action {
                        Action::Quit => break,
                        action => executor.spawn(action),
                    }
                }
            }

            AppEvent::Mouse(_) => {
                // Mouse events not handled yet
            }

            AppEvent::Resize(_, _) => {
                // Terminal will redraw automatically
            }

            AppEvent::ActionResult(result) => {
                app.handle_action_result(result);
            }

            AppEvent::Error(msg) => {
                app.set_status(msg, app::StatusLevel::Error);
            }

            AppEvent::Quit => break,
        }
    }

    Ok(())
}

fn setup_terminal() -> io::Result<Terminal<CrosstermBackend<io::Stdout>>> {
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen)?;
    let backend = CrosstermBackend::new(stdout);
    Terminal::new(backend)
}

fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> io::Result<()> {
    disable_raw_mode()?;
    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
    terminal.show_cursor()?;
    Ok(())
}