Skip to main content

rich_interact/
lib.rs

1//! Interactive terminal components for rs-rich (#451): the layer between
2//! printing and a full TUI framework.
3//!
4//! A [`Component`] is a state machine: it handles one [`Event`] and renders
5//! one [`View`]. Two drivers run it:
6//!
7//! - **blocking:** [`run`] takes the terminal, drives the component to an
8//!   [`Outcome`] and gives the terminal back, like `Prompt.ask`;
9//! - **event loop:** an [`EventLoop`] runs one or more components at once,
10//!   with ticks and timers, and repaints only when something changed.
11//!
12//! Views are painted through [`rich_ext::frame::Frame::diff`], cell by cell,
13//! so an idle loop writes nothing. Around them:
14//!
15//! - [`session`]: raw mode, the alternate screen, mouse and paste, restored
16//!   on every way out, including a panic, Ctrl+C and a hand-off to another
17//!   program (#489), driven by crossterm, or by termion or termwiz behind
18//!   the features of those names ([`BackendKind`]), and each frame written
19//!   as one synchronized update where the terminal has it;
20//! - [`event`]: keys, mouse, resizes and pastes, from whichever drives the
21//!   terminal;
22//! - [`viewport`]: a scrollable window over rendered lines (#495);
23//! - [`item`]: one item model behind every picker (#452);
24//! - [`policy`]: with no terminal, under CI or with `TERM=dumb`, a
25//!   component asks line by line, returns its default, or fails, as the
26//!   caller chooses, and never blocks on a pipe (#492);
27//! - [`headless`]: scripted events in, frames out, for tests;
28//! - [`components`]: [`Select`], [`MultiSelect`], [`TableSelect`],
29//!   [`TreeSelect`], [`Input`], [`TextArea`], [`Confirm`], [`Form`],
30//!   [`Pager`], [`Paginator`], [`FilePicker`], [`ColorPicker`], [`AssetPicker`],
31//!   [`ThemePicker`], [`Reorder`] (#720), and [`DatePicker`],
32//!   [`TimePicker`] and [`DateTimePicker`] (#719, on the [`datetime`]
33//!   types), [`LogView`] (#724, a live view of log records), built on
34//!   the above with a [`fuzzy`] matcher, [`ApprovalQueue`] (#471, a review
35//!   of pending items) and [`Jump`] (#484, one fuzzy box over several
36//!   sources), and with
37//!   the `data` feature `DataExplorer`, for JSON, YAML, TOML and the other
38//!   formats `rich_ext::data` reads;
39//! - [`clipboard`]: copy to the terminal's clipboard with OSC 52, when it
40//!   takes it.
41//!
42//! Components compose (0.0.14): [`compose`] has containers that are
43//! components themselves ([`Column`], [`Row`], [`Stack`], [`Split`],
44//! [`Tabs`], [`Layers`]), with focus, routing and bubbling
45//! ([`Flow::Ignored`]); [`kit`] has the line helpers and state types the
46//! built-ins are made of; and [`keymap`] declares, rebinds and lists every
47//! component's keys. On top of those, [`overlay`] has a command palette, a
48//! help overlay, a shortcut sheet and an action menu read from the keymap,
49//! and [`chrome`] a status bar and breadcrumbs; [`Overlays`] wires them all
50//! into any component.
51//!
52//! Mouse support (#476) is opt-in per component ([`Component::mouse`]):
53//! clicks, drags and the wheel arrive in the component's own coordinates, a
54//! click on a hyperlink arrives as [`Event::Link`], and the border beside a
55//! preview drags. Actions (#491) attach to list items, table rows, tree
56//! nodes and file entries through [`Actions`] and an [`ActionTarget`], and
57//! open in a modal menu (Ctrl+K); a region takes actions too, through
58//! [`Overlays::actions`]; plugins register them through
59//! `rs-rich-plugin-api`. Plugins register whole components too, and
60//! [`PluginView`] mounts one by name beside the built-ins ([`plugin`]).
61//!
62//! ```
63//! use rich_interact::{headless, Component, Context, Event, Flow, KeyCode, Outcome, View};
64//!
65//! /// Counts Up presses; Enter returns the count.
66//! struct Counter(u32);
67//!
68//! impl Component for Counter {
69//!     type Output = u32;
70//!     fn handle(&mut self, event: &Event, _: &Context<'_>) -> Flow<u32> {
71//!         match event.key().map(|key| key.code) {
72//!             Some(KeyCode::Up) => self.0 += 1,
73//!             Some(KeyCode::Enter) => return Flow::Done(self.0),
74//!             _ => {}
75//!         }
76//!         Flow::Continue
77//!     }
78//!     fn render(&self, context: &Context<'_>) -> View {
79//!         View::new(context.markup(&format!("count: [bold]{}[/]", self.0)))
80//!     }
81//! }
82//!
83//! let script = headless::Script::new().keys("up up enter");
84//! let (outcome, record) = headless::run(Counter(0), script, 40, 5);
85//! assert_eq!(outcome.unwrap(), Outcome::Done(2));
86//! assert_eq!(record.last_frame(), "count: 2");
87//! ```
88
89// A builder method that returns `Self` is `#[must_use]`: dropping its
90// result drops the value (CONTRIBUTING.md, "API conventions").
91#![warn(clippy::return_self_not_must_use)]
92
93pub mod caret;
94pub mod chrome;
95pub mod clipboard;
96pub mod component;
97pub mod components;
98pub mod compose;
99#[cfg(all(feature = "control", unix))]
100pub mod control;
101pub mod dates;
102pub mod datetime;
103pub mod edit;
104pub mod edit_keys;
105pub mod event;
106pub mod event_loop;
107pub mod fuzzy;
108pub mod headless;
109#[cfg(all(feature = "host", not(target_arch = "wasm32")))]
110pub mod host;
111pub mod item;
112pub mod keymap;
113pub mod kit;
114mod names;
115pub mod overlay;
116pub mod paint;
117pub mod plugin;
118pub mod policy;
119pub mod push;
120pub mod session;
121pub mod viewport;
122#[cfg(feature = "vim")]
123pub mod vim;
124mod zone;
125
126pub use chrome::{Breadcrumbs, StatusBar, StatusItem};
127pub use component::{Component, Context, Flow, View};
128#[cfg(feature = "data")]
129pub use components::DataExplorer;
130pub use components::{
131    Answers, ApprovalQueue, ApprovalReport, AssetKind, AssetPicker, Choice, ColorFormat,
132    ColorPicker, Confirm, DatePicker, DateTimePicker, Decision, FileMode, FilePicker, Form, Input,
133    Jump, JumpIndex, JumpSource, Jumped, LogEvent, LogView, MultiSelect, Pager, PreviewLayout,
134    Reorder, Select, Suggestion, TableSelect, TextArea, Theme, ThemePicker, TimePicker, TreeSelect,
135    Value, Verdict,
136};
137pub use components::{Paginator, PaginatorStyle};
138pub use compose::{
139    Axis, Column, ComponentExt, Label, Layer, LayerHandle, Layers, Rect, Row, Size, Split, Stack,
140    Tabs,
141};
142pub use event::{
143    Button, Event, HeldButton, Key, KeyCode, ModifierKey, Modifiers, Mouse, MouseKind, Releases,
144    Subcell,
145};
146pub use event_loop::{
147    Error, EventLoop, Handle, LoopOptions, Outcome, RunOptions, degrade, run, run_with_graphics,
148};
149pub use item::{Action, ActionFilter, ActionTarget, Actions, Item, Preview, TargetKind};
150pub use keymap::{Binding, Keymap};
151pub use overlay::{Command, Help, Menu, Overlays, Palette, Shortcuts};
152pub use plugin::PluginView;
153pub use policy::{Fallback, LineIo, NotInteractive, Policy, Reason};
154pub use push::{PushBackend, PushPort};
155pub use session::{
156    Backend, BackendKind, Output, Session, SessionOptions, Waker, synchronized_update,
157};
158pub use viewport::Viewport;
159
160/// Forward every [`Component`] method to `(**self)`.
161macro_rules! forward_component {
162    () => {
163        type Output = C::Output;
164
165        fn start(&mut self, context: &Context<'_>) -> Flow<C::Output> {
166            (**self).start(context)
167        }
168
169        fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<C::Output> {
170            (**self).handle(event, context)
171        }
172
173        fn render(&self, context: &Context<'_>) -> View {
174            (**self).render(context)
175        }
176
177        fn tick(&self) -> Option<std::time::Duration> {
178            (**self).tick()
179        }
180
181        fn mouse(&self) -> bool {
182            (**self).mouse()
183        }
184
185        fn keymap(&self) -> keymap::Keymap {
186            (**self).keymap()
187        }
188
189        fn focusable(&self) -> bool {
190            (**self).focusable()
191        }
192
193        fn focus_step(&mut self, forward: bool) -> bool {
194            (**self).focus_step(forward)
195        }
196
197        fn focus_enter(&mut self, forward: bool) -> bool {
198            (**self).focus_enter(forward)
199        }
200
201        fn default_value(&self) -> Option<C::Output> {
202            (**self).default_value()
203        }
204
205        fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<C::Output>, NotInteractive> {
206            (**self).prompt(io)
207        }
208    };
209}
210
211/// A component borrowed mutably is a component, so a caller can mount one
212/// and still read its state after the loop.
213impl<C: Component + ?Sized> Component for &mut C {
214    forward_component!();
215}
216
217/// A boxed component is a component: how containers hold children of
218/// different types with one output ([`compose::Child`]).
219impl<C: Component + ?Sized> Component for Box<C> {
220    forward_component!();
221}
222
223/// crossterm's surface on wasm32, where there is no terminal: every call
224/// fails with [`Unsupported`](std::io::ErrorKind::Unsupported), so a
225/// [`Session`] never starts and [`run`] follows its [`Policy`]'s fallback.
226/// A browser drives components through [`EventLoop::step`] and a
227/// [`PushBackend`] instead.
228#[cfg(target_arch = "wasm32")]
229pub(crate) mod crossterm_stub {
230    fn no<T>() -> std::io::Result<T> {
231        Err(std::io::Error::new(
232            std::io::ErrorKind::Unsupported,
233            "no terminal on wasm",
234        ))
235    }
236    pub mod terminal {
237        pub fn enable_raw_mode() -> std::io::Result<()> {
238            super::no()
239        }
240        pub fn disable_raw_mode() -> std::io::Result<()> {
241            super::no()
242        }
243        pub fn size() -> std::io::Result<(u16, u16)> {
244            super::no()
245        }
246        pub fn supports_keyboard_enhancement() -> std::io::Result<bool> {
247            super::no()
248        }
249        pub struct WindowSize {
250            pub rows: u16,
251            pub columns: u16,
252            pub width: u16,
253            pub height: u16,
254        }
255        pub fn window_size() -> std::io::Result<WindowSize> {
256            super::no()
257        }
258    }
259    pub mod cursor {
260        pub fn position() -> std::io::Result<(u16, u16)> {
261            super::no()
262        }
263    }
264    /// The stub event is uninhabited: nothing to translate.
265    pub fn from_crossterm(event: event::Event) -> Option<crate::event::Event> {
266        match event {}
267    }
268    pub use from_crossterm as from_crossterm_kitty;
269    pub mod event {
270        pub enum Event {}
271        pub fn poll(_: std::time::Duration) -> std::io::Result<bool> {
272            super::no()
273        }
274        pub fn read() -> std::io::Result<Event> {
275            super::no()
276        }
277    }
278}