mothership 0.0.100

Process supervisor with HTTP exposure - wrap, monitor, and expose your fleet
Documentation
//! TUI application state and event handling

use std::io::{self, Stdout};
use std::sync::Arc;
use std::time::Duration;

use crossterm::{
    ExecutableCommand,
    event::{self, Event, KeyCode, KeyEventKind},
    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, backend::CrosstermBackend};
use tokio::sync::watch;

use super::ui;
use crate::fleet::{Fleet, LogEntry, ShipSnapshot};

/// TUI application
pub struct TuiApp {
    /// Terminal instance
    terminal: Terminal<CrosstermBackend<Stdout>>,
    /// Fleet reference for status
    fleet: Arc<Fleet>,
    /// Shutdown signal receiver
    shutdown_rx: watch::Receiver<bool>,
    /// Selected tab (0=overview, 1=logs, 2=modules)
    selected_tab: usize,
    /// Selected ship index for detail view
    selected_ship: usize,
    /// Scroll offset for logs
    log_scroll: u16,
    /// Cached ship snapshots
    ships_cache: Vec<ShipSnapshot>,
    /// Cached logs for selected ship
    logs_cache: Vec<LogEntry>,
}

impl TuiApp {
    /// Create new TUI app
    pub async fn new(fleet: Arc<Fleet>, shutdown_rx: watch::Receiver<bool>) -> io::Result<Self> {
        enable_raw_mode()?;
        let mut stdout = io::stdout();
        stdout.execute(EnterAlternateScreen)?;
        let backend = CrosstermBackend::new(stdout);
        let terminal = Terminal::new(backend)?;

        // Initial snapshot
        let ships_cache = fleet.ships_snapshot().await;

        Ok(Self {
            terminal,
            fleet,
            shutdown_rx,
            selected_tab: 0,
            selected_ship: 0,
            log_scroll: 0,
            ships_cache,
            logs_cache: vec![],
        })
    }

    /// Run the TUI event loop
    pub async fn run(&mut self) -> io::Result<()> {
        loop {
            // Check for shutdown
            if *self.shutdown_rx.borrow() {
                break;
            }

            // Refresh ship snapshots
            self.ships_cache = self.fleet.ships_snapshot().await;

            // Clamp selected_ship to valid range
            if !self.ships_cache.is_empty() && self.selected_ship >= self.ships_cache.len() {
                self.selected_ship = self.ships_cache.len() - 1;
            }

            // Fetch logs for selected ship (only on logs tab for efficiency)
            if self.selected_tab == 1 {
                self.logs_cache = self.fleet.ship_logs(self.selected_ship, 100).await;
            }

            // Draw UI
            let ships = &self.ships_cache;
            let logs = &self.logs_cache;
            let selected_tab = self.selected_tab;
            let selected_ship = self.selected_ship;
            let log_scroll = self.log_scroll;

            self.terminal.draw(|frame| {
                ui::render(frame, ships, logs, selected_tab, selected_ship, log_scroll);
            })?;

            // Handle input with timeout
            if event::poll(Duration::from_millis(250))?
                && let Event::Key(key) = event::read()?
                && key.kind == KeyEventKind::Press
            {
                match key.code {
                    KeyCode::Char('q') | KeyCode::Esc => break,
                    KeyCode::Tab => {
                        self.selected_tab = (self.selected_tab + 1) % 3;
                    }
                    KeyCode::BackTab => {
                        self.selected_tab = if self.selected_tab == 0 {
                            2
                        } else {
                            self.selected_tab - 1
                        };
                    }
                    KeyCode::Up | KeyCode::Char('k') => {
                        if self.selected_ship > 0 {
                            self.selected_ship -= 1;
                        }
                    }
                    KeyCode::Down | KeyCode::Char('j') => {
                        if self.selected_ship + 1 < self.ships_cache.len() {
                            self.selected_ship += 1;
                        }
                    }
                    KeyCode::PageUp => {
                        self.log_scroll = self.log_scroll.saturating_sub(10);
                    }
                    KeyCode::PageDown => {
                        self.log_scroll = self.log_scroll.saturating_add(10);
                    }
                    _ => {}
                }
            }
        }

        Ok(())
    }
}

impl Drop for TuiApp {
    fn drop(&mut self) {
        let _ = disable_raw_mode();
        let _ = self.terminal.backend_mut().execute(LeaveAlternateScreen);
    }
}