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}