rs-rich-interact 0.0.12

Interactive terminal components for rs-rich: event loop, viewport, item model, headless testing
Documentation
//! Interactive terminal components for rs-rich (#451): the layer between
//! printing and a full TUI framework.
//!
//! A [`Component`] is a state machine: it handles one [`Event`] and renders
//! one [`View`]. Two drivers run it:
//!
//! - **blocking:** [`run`] takes the terminal, drives the component to an
//!   [`Outcome`] and gives the terminal back, like `Prompt.ask`;
//! - **event loop:** an [`EventLoop`] runs one or more components at once,
//!   with ticks and timers, and repaints only when something changed.
//!
//! Views are painted through [`rich_ext::frame::Frame::diff`], cell by cell,
//! so an idle loop writes nothing. Around them:
//!
//! - [`session`]: raw mode, the alternate screen, mouse and paste, restored
//!   on every way out, including a panic, Ctrl+C and a hand-off to another
//!   program (#489), driven by crossterm, or by termion or termwiz behind
//!   the features of those names ([`BackendKind`]), and each frame written
//!   as one synchronized update where the terminal has it;
//! - [`event`]: keys, mouse, resizes and pastes, from whichever drives the
//!   terminal;
//! - [`viewport`]: a scrollable window over rendered lines (#495);
//! - [`item`]: one item model behind every picker (#452);
//! - [`policy`]: with no terminal, under CI or with `TERM=dumb`, a
//!   component asks line by line, returns its default, or fails, as the
//!   caller chooses, and never blocks on a pipe (#492);
//! - [`headless`]: scripted events in, frames out, for tests;
//! - [`components`]: [`Select`], [`MultiSelect`], [`TableSelect`],
//!   [`TreeSelect`], [`Input`], [`TextArea`], [`Confirm`], [`Form`],
//!   [`Pager`], [`Paginator`], [`FilePicker`], [`ColorPicker`], [`AssetPicker`],
//!   [`ThemePicker`], [`Reorder`] (#720), and [`DatePicker`],
//!   [`TimePicker`] and [`DateTimePicker`] (#719, on the [`datetime`]
//!   types), [`LogView`] (#724, a live view of log records), built on
//!   the above with a [`fuzzy`] matcher, [`ApprovalQueue`] (#471, a review
//!   of pending items) and [`Jump`] (#484, one fuzzy box over several
//!   sources), and with
//!   the `data` feature `DataExplorer`, for JSON, YAML, TOML and the other
//!   formats `rich_ext::data` reads;
//! - [`clipboard`]: copy to the terminal's clipboard with OSC 52, when it
//!   takes it.
//!
//! Components compose (0.0.14): [`compose`] has containers that are
//! components themselves ([`Column`], [`Row`], [`Stack`], [`Split`],
//! [`Tabs`], [`Layers`]), with focus, routing and bubbling
//! ([`Flow::Ignored`]); [`kit`] has the line helpers and state types the
//! built-ins are made of; and [`keymap`] declares, rebinds and lists every
//! component's keys. On top of those, [`overlay`] has a command palette, a
//! help overlay, a shortcut sheet and an action menu read from the keymap,
//! and [`chrome`] a status bar and breadcrumbs; [`Overlays`] wires them all
//! into any component.
//!
//! Mouse support (#476) is opt-in per component ([`Component::mouse`]):
//! clicks, drags and the wheel arrive in the component's own coordinates, a
//! click on a hyperlink arrives as [`Event::Link`], and the border beside a
//! preview drags. Actions (#491) attach to list items, table rows, tree
//! nodes and file entries through [`Actions`] and an [`ActionTarget`], and
//! open in a modal menu (Ctrl+K); a region takes actions too, through
//! [`Overlays::actions`]; plugins register them through
//! `rs-rich-plugin-api`. Plugins register whole components too, and
//! [`PluginView`] mounts one by name beside the built-ins ([`plugin`]).
//!
//! ```
//! use rich_interact::{headless, Component, Context, Event, Flow, KeyCode, Outcome, View};
//!
//! /// Counts Up presses; Enter returns the count.
//! struct Counter(u32);
//!
//! impl Component for Counter {
//!     type Output = u32;
//!     fn handle(&mut self, event: &Event, _: &Context<'_>) -> Flow<u32> {
//!         match event.key().map(|key| key.code) {
//!             Some(KeyCode::Up) => self.0 += 1,
//!             Some(KeyCode::Enter) => return Flow::Done(self.0),
//!             _ => {}
//!         }
//!         Flow::Continue
//!     }
//!     fn render(&self, context: &Context<'_>) -> View {
//!         View::new(context.markup(&format!("count: [bold]{}[/]", self.0)))
//!     }
//! }
//!
//! let script = headless::Script::new().keys("up up enter");
//! let (outcome, record) = headless::run(Counter(0), script, 40, 5);
//! assert_eq!(outcome.unwrap(), Outcome::Done(2));
//! assert_eq!(record.last_frame(), "count: 2");
//! ```

// A builder method that returns `Self` is `#[must_use]`: dropping its
// result drops the value (CONTRIBUTING.md, "API conventions").
#![warn(clippy::return_self_not_must_use)]

pub mod caret;
pub mod chrome;
pub mod clipboard;
pub mod component;
pub mod components;
pub mod compose;
#[cfg(all(feature = "control", unix))]
pub mod control;
pub mod dates;
pub mod datetime;
pub mod edit;
pub mod edit_keys;
pub mod event;
pub mod event_loop;
pub mod fuzzy;
pub mod headless;
#[cfg(all(feature = "host", not(target_arch = "wasm32")))]
pub mod host;
pub mod item;
pub mod keymap;
pub mod kit;
mod names;
pub mod overlay;
pub mod paint;
pub mod plugin;
pub mod policy;
pub mod push;
pub mod session;
pub mod viewport;
#[cfg(feature = "vim")]
pub mod vim;
mod zone;

pub use chrome::{Breadcrumbs, StatusBar, StatusItem};
pub use component::{Component, Context, Flow, View};
#[cfg(feature = "data")]
pub use components::DataExplorer;
pub use components::{
    Answers, ApprovalQueue, ApprovalReport, AssetKind, AssetPicker, Choice, ColorFormat,
    ColorPicker, Confirm, DatePicker, DateTimePicker, Decision, FileMode, FilePicker, Form, Input,
    Jump, JumpIndex, JumpSource, Jumped, LogEvent, LogView, MultiSelect, Pager, PreviewLayout,
    Reorder, Select, Suggestion, TableSelect, TextArea, Theme, ThemePicker, TimePicker, TreeSelect,
    Value, Verdict,
};
pub use components::{Paginator, PaginatorStyle};
pub use compose::{
    Axis, Column, ComponentExt, Label, Layer, LayerHandle, Layers, Rect, Row, Size, Split, Stack,
    Tabs,
};
pub use event::{
    Button, Event, HeldButton, Key, KeyCode, ModifierKey, Modifiers, Mouse, MouseKind, Releases,
    Subcell,
};
pub use event_loop::{
    Error, EventLoop, Handle, LoopOptions, Outcome, RunOptions, degrade, run, run_with_graphics,
};
pub use item::{Action, ActionFilter, ActionTarget, Actions, Item, Preview, TargetKind};
pub use keymap::{Binding, Keymap};
pub use overlay::{Command, Help, Menu, Overlays, Palette, Shortcuts};
pub use plugin::PluginView;
pub use policy::{Fallback, LineIo, NotInteractive, Policy, Reason};
pub use push::{PushBackend, PushPort};
pub use session::{
    Backend, BackendKind, Output, Session, SessionOptions, Waker, synchronized_update,
};
pub use viewport::Viewport;

/// Forward every [`Component`] method to `(**self)`.
macro_rules! forward_component {
    () => {
        type Output = C::Output;

        fn start(&mut self, context: &Context<'_>) -> Flow<C::Output> {
            (**self).start(context)
        }

        fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<C::Output> {
            (**self).handle(event, context)
        }

        fn render(&self, context: &Context<'_>) -> View {
            (**self).render(context)
        }

        fn tick(&self) -> Option<std::time::Duration> {
            (**self).tick()
        }

        fn mouse(&self) -> bool {
            (**self).mouse()
        }

        fn keymap(&self) -> keymap::Keymap {
            (**self).keymap()
        }

        fn focusable(&self) -> bool {
            (**self).focusable()
        }

        fn focus_step(&mut self, forward: bool) -> bool {
            (**self).focus_step(forward)
        }

        fn focus_enter(&mut self, forward: bool) -> bool {
            (**self).focus_enter(forward)
        }

        fn default_value(&self) -> Option<C::Output> {
            (**self).default_value()
        }

        fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<C::Output>, NotInteractive> {
            (**self).prompt(io)
        }
    };
}

/// A component borrowed mutably is a component, so a caller can mount one
/// and still read its state after the loop.
impl<C: Component + ?Sized> Component for &mut C {
    forward_component!();
}

/// A boxed component is a component: how containers hold children of
/// different types with one output ([`compose::Child`]).
impl<C: Component + ?Sized> Component for Box<C> {
    forward_component!();
}

/// crossterm's surface on wasm32, where there is no terminal: every call
/// fails with [`Unsupported`](std::io::ErrorKind::Unsupported), so a
/// [`Session`] never starts and [`run`] follows its [`Policy`]'s fallback.
/// A browser drives components through [`EventLoop::step`] and a
/// [`PushBackend`] instead.
#[cfg(target_arch = "wasm32")]
pub(crate) mod crossterm_stub {
    fn no<T>() -> std::io::Result<T> {
        Err(std::io::Error::new(
            std::io::ErrorKind::Unsupported,
            "no terminal on wasm",
        ))
    }
    pub mod terminal {
        pub fn enable_raw_mode() -> std::io::Result<()> {
            super::no()
        }
        pub fn disable_raw_mode() -> std::io::Result<()> {
            super::no()
        }
        pub fn size() -> std::io::Result<(u16, u16)> {
            super::no()
        }
        pub fn supports_keyboard_enhancement() -> std::io::Result<bool> {
            super::no()
        }
        pub struct WindowSize {
            pub rows: u16,
            pub columns: u16,
            pub width: u16,
            pub height: u16,
        }
        pub fn window_size() -> std::io::Result<WindowSize> {
            super::no()
        }
    }
    pub mod cursor {
        pub fn position() -> std::io::Result<(u16, u16)> {
            super::no()
        }
    }
    /// The stub event is uninhabited: nothing to translate.
    pub fn from_crossterm(event: event::Event) -> Option<crate::event::Event> {
        match event {}
    }
    pub use from_crossterm as from_crossterm_kitty;
    pub mod event {
        pub enum Event {}
        pub fn poll(_: std::time::Duration) -> std::io::Result<bool> {
            super::no()
        }
        pub fn read() -> std::io::Result<Event> {
            super::no()
        }
    }
}