polyc-tui 2026.9.0

Operator cockpit TUI (pc-tui) for the polychrome control plane: fleet, transcript, approvals, and tools panes.
//! The component contract and the cockpit's panes.
//!
//! `handle` is a state transition that may request a single follow-up
//! [`Action`] (re-queued by the loop); `draw` renders into a sub-area.

use color_eyre::Result;
use ratatui::{Frame, layout::Rect};

use crate::action::Action;

pub(crate) mod approvals;
pub(crate) mod fleet;
pub(crate) mod questions;
pub(crate) mod tools;
pub(crate) mod transcript;

/// Step a list selection by `delta`, clamped to `[0, len)`.
///
/// Returns `None` when the list is empty. Shared by every pane's cursor
/// movement so the clamp semantics (and the `usize`/`isize` boundary handling)
/// live in exactly one place.
#[must_use]
pub(crate) fn step_selection(selected: Option<usize>, len: usize, delta: isize) -> Option<usize> {
    if len == 0 {
        return None;
    }
    let cur = isize::try_from(selected.unwrap_or(0)).unwrap_or(isize::MAX);
    let max = isize::try_from(len - 1).unwrap_or(isize::MAX);
    Some((cur + delta).clamp(0, max).unsigned_abs())
}

/// The trait every UI piece implements.
pub(crate) trait Component {
    /// React to an action; optionally emit a follow-up action to be re-queued.
    fn handle(&mut self, action: &Action) -> Option<Action>;

    /// Render this component into `area`.
    ///
    /// # Errors
    /// Returns an error if rendering cannot proceed.
    fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()>;

    /// Pane-specific key hints `(key, action)` for the global controls footer,
    /// shown whenever this pane is focused. The loop appends the global keys
    /// (pane switching + quit), so every screen always advertises a way out.
    fn controls(&self) -> Vec<(&'static str, &'static str)> {
        Vec::new()
    }

    /// Whether this pane is currently capturing free text (e.g. typing an
    /// approval reason). While true, the loop suppresses global single-key
    /// bindings (`q`, `1`-`4`, `Tab`) so they reach the editor literally; only
    /// `Ctrl-C` still quits.
    fn capturing_input(&self) -> bool {
        false
    }
}