ratatui-command-palette 0.1.0

Experimental command palette state and rendering for Ratatui applications.
Documentation
//! Renderable command palette view data.
//!
//! Renderers should use this module instead of reading [`PaletteState`](crate::state::PaletteState)
//! internals:
//!
//! - [`PaletteView`] is the full snapshot for a render pass.
//! - [`PaletteRow`] is one action, choice, or boolean option prepared for rendering.

use ratatui_action::id::ActionId;
use ratatui_action::input::{ActionChoice, ActionInput};
use ratatui_action::spec::{ActionSpec, Availability};

use crate::event::PaletteMode;
use crate::shortcut::ShortcutLabels;

/// A prepared row for command palette rendering.
///
/// Rows are generated by [`PaletteState::view`](crate::state::PaletteState::view).
/// Custom renderers can read the row through accessors without depending on
/// the storage layout.
///
/// Read row data with:
///
/// - [`action_id`](Self::action_id) for the semantic action behind the row.
/// - [`title`](Self::title), [`subtitle`](Self::subtitle), and [`category`](Self::category) for
///   visible text.
/// - [`shortcut`](Self::shortcut) for presentation-only keybinding labels.
/// - [`availability`](Self::availability) to distinguish enabled and disabled rows.
///
/// # Examples
///
/// ```
/// use ratatui_action::spec::{ActionSpec, Availability};
/// use ratatui_command_palette::shortcut::ShortcutLabels;
/// use ratatui_command_palette::state::PaletteState;
///
/// let actions = vec![
///     ActionSpec::new("workspace.close", "Close workspace")
///         .with_category("Workspace")
///         .with_availability(Availability::Disabled {
///             reason: "No workspace is open".into(),
///         }),
/// ];
/// let mut shortcuts = ShortcutLabels::new();
/// shortcuts.insert("workspace.close", "Ctrl-W");
///
/// let mut palette = PaletteState::new();
/// palette.open(&actions);
/// let view = palette.view_with_shortcuts(&actions, &shortcuts);
/// let row = &view.rows()[0];
///
/// assert_eq!(row.action_id().as_str(), "workspace.close");
/// assert_eq!(row.category(), Some("Workspace"));
/// assert_eq!(row.shortcut(), Some("Ctrl-W"));
/// assert!(matches!(row.availability(), Availability::Disabled { .. }));
/// ```
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PaletteRow {
    action_id: ActionId,
    title: String,
    subtitle: Option<String>,
    category: Option<String>,
    shortcut: Option<String>,
    availability: Availability,
}

impl PaletteRow {
    pub(crate) fn from_action_with_shortcuts(
        action: &ActionSpec,
        shortcuts: &ShortcutLabels,
    ) -> Self {
        Self {
            action_id: action.id().clone(),
            title: action.title().to_string(),
            subtitle: action.description().map(str::to_string),
            category: action.category().map(str::to_string),
            shortcut: shortcuts.get(action.id()).map(str::to_string),
            availability: action.availability().clone(),
        }
    }

    pub(crate) fn from_choice(
        action_id: &ActionId,
        input: &ActionInput,
        choice: &ActionChoice,
    ) -> Self {
        Self {
            action_id: action_id.clone(),
            title: choice.label().to_string(),
            subtitle: choice.description().map(str::to_string),
            category: Some(input.label().to_string()),
            shortcut: None,
            availability: Availability::Enabled,
        }
    }

    pub(crate) fn from_bool(action_id: &ActionId, input: &ActionInput, value: bool) -> Self {
        Self {
            action_id: action_id.clone(),
            title: if value { "Yes" } else { "No" }.into(),
            subtitle: Some(value.to_string()),
            category: Some(input.label().to_string()),
            shortcut: None,
            availability: Availability::Enabled,
        }
    }

    /// Returns the action represented by this row.
    pub fn action_id(&self) -> &ActionId {
        &self.action_id
    }

    /// Returns the primary user-facing row text.
    pub fn title(&self) -> &str {
        &self.title
    }

    /// Returns secondary row text when available.
    pub fn subtitle(&self) -> Option<&str> {
        self.subtitle.as_deref()
    }

    /// Returns the grouping label when available.
    pub fn category(&self) -> Option<&str> {
        self.category.as_deref()
    }

    /// Returns the shortcut text when available.
    pub fn shortcut(&self) -> Option<&str> {
        self.shortcut.as_deref()
    }

    /// Returns whether this row can be accepted.
    pub fn availability(&self) -> &Availability {
        &self.availability
    }
}

/// Prepared command palette view data.
///
/// A view is a snapshot for renderers. It currently owns row strings to keep
/// rendering and Betamax state capture simple during the experiment.
///
/// Renderers usually read:
///
/// - [`prompt`](Self::prompt) and [`query`](Self::query) for the input line.
/// - [`rows`](Self::rows) and [`selected`](Self::selected) for the visible list.
/// - [`mode`](Self::mode) to distinguish search rows from input collection rows.
///
/// # Examples
///
/// ```
/// use ratatui_action::spec::ActionSpec;
/// use ratatui_command_palette::state::PaletteState;
///
/// let actions = vec![ActionSpec::new("document.open", "Open document")];
/// let mut palette = PaletteState::new();
/// palette.open(&actions);
///
/// let view = palette.view(&actions);
///
/// assert_eq!(view.prompt(), ">");
/// assert_eq!(view.rows()[0].title(), "Open document");
/// ```
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PaletteView {
    pub(crate) prompt: String,
    pub(crate) query: String,
    pub(crate) rows: Vec<PaletteRow>,
    pub(crate) selected: Option<usize>,
    pub(crate) mode: PaletteMode,
}

impl PaletteView {
    /// Returns the prompt shown before the query or choice input.
    pub fn prompt(&self) -> &str {
        &self.prompt
    }

    /// Returns the current search query.
    pub fn query(&self) -> &str {
        &self.query
    }

    /// Returns rows prepared for the current mode.
    pub fn rows(&self) -> &[PaletteRow] {
        &self.rows
    }

    /// Returns the selected row index.
    pub fn selected(&self) -> Option<usize> {
        self.selected
    }

    /// Returns the mode that produced this view.
    pub fn mode(&self) -> &PaletteMode {
        &self.mode
    }
}