Skip to main content

rich_plugin_api/
component.rs

1//! Interactive components a plugin registers by name (0.0.14), with
2//! [`PluginRegistrar::component`](crate::PluginRegistrar::component).
3//!
4//! A plugin depends on this crate and core only, never on
5//! `rs-rich-interact`, so the contract here is its own small one: events
6//! arrive as [`ComponentEvent`]s, with keys by name (`down`, `ctrl+k`); a
7//! component answers with a [`ComponentFlow`] and renders a
8//! [`ComponentView`] of core [`Segment`]s. A host adapts it to its own
9//! component model: `rs-rich-interact`'s `PluginView` mounts one in a split,
10//! tabs or a modal beside the built-ins, and runs it through the same
11//! drivers, headless included.
12//!
13//! A component finishes with text ([`ComponentFlow::Done`]): whatever it
14//! chose or typed, for the app that mounted it to interpret.
15//!
16//! ```
17//! use rich_plugin_api::component::{
18//!     ComponentContext, ComponentEvent, ComponentFlow, ComponentView, PluginComponent,
19//! };
20//!
21//! /// Counts Up presses; Enter answers with the count.
22//! #[derive(Default)]
23//! struct Counter(u32);
24//!
25//! impl PluginComponent for Counter {
26//!     fn handle(&mut self, event: &ComponentEvent, _: &ComponentContext<'_>) -> ComponentFlow {
27//!         match event.key() {
28//!             Some("up") => self.0 += 1,
29//!             Some("enter") => return ComponentFlow::Done(self.0.to_string()),
30//!             _ => return ComponentFlow::Ignored,
31//!         }
32//!         ComponentFlow::Continue
33//!     }
34//!
35//!     fn render(&self, context: &ComponentContext<'_>) -> ComponentView {
36//!         ComponentView::new(context.markup(&format!("count: [bold]{}[/]", self.0)))
37//!     }
38//! }
39//!
40//! let console = rich::Console::new();
41//! let context = ComponentContext::new(&console, 20, 1);
42//! let mut counter = Counter::default();
43//! counter.handle(&ComponentEvent::Key("up".into()), &context);
44//! assert!(matches!(
45//!     counter.handle(&ComponentEvent::Key("enter".into()), &context),
46//!     ComponentFlow::Done(count) if count == "1"
47//! ));
48//! ```
49
50use std::sync::Arc;
51use std::time::Duration;
52
53use rich::{Console, Renderable, Segment, Text};
54
55/// Makes a fresh component each time a host mounts one, so one
56/// registration can be mounted many times.
57pub type ComponentFactory = Arc<dyn Fn() -> Box<dyn PluginComponent> + Send + Sync>;
58
59/// An event for a [`PluginComponent`].
60#[derive(Clone, Debug, PartialEq, Eq)]
61#[non_exhaustive]
62pub enum ComponentEvent {
63    /// A key, by name: `a`, `A`, `enter`, `down`, `space`, `tab`,
64    /// `backspace`, `f5`, with `ctrl+`, `alt+` and `shift+` before it, in
65    /// that order (`ctrl+shift+left`, `shift+tab`). A key that does one of
66    /// the component's [`bindings`](PluginComponent::bindings) arrives as
67    /// the first key declared for that action, spelled as declared (`esc`
68    /// stays `esc`), even when the user rebound it.
69    Key(String),
70    /// Text pasted at once.
71    Paste(String),
72    /// The terminal is now this size.
73    Resize { columns: u16, rows: u16 },
74    /// The component's [`tick`](PluginComponent::tick) interval passed.
75    Tick,
76    /// The mouse, in the component's own view (0-based), when it asked for
77    /// it with [`mouse`](PluginComponent::mouse).
78    Mouse {
79        /// `down`, `up`, `drag`, `moved`, `scroll_up` or `scroll_down`.
80        kind: String,
81        column: u16,
82        row: u16,
83    },
84    /// A click on a hyperlink in the component's view: its URL.
85    Link(String),
86}
87
88impl ComponentEvent {
89    /// The key's name, when this is a key.
90    pub fn key(&self) -> Option<&str> {
91        match self {
92            ComponentEvent::Key(name) => Some(name),
93            _ => None,
94        }
95    }
96}
97
98/// What a component wants after an event.
99#[derive(Clone, Debug, PartialEq, Eq)]
100#[non_exhaustive]
101pub enum ComponentFlow {
102    /// Keep going; the view is rendered again.
103    Continue,
104    /// Finished, with an answer for the app that mounted it.
105    Done(String),
106    /// The user backed out.
107    Cancel,
108    /// The event was not for this component: the container it is in may
109    /// use it (Tab to move focus, a binding of its own).
110    Ignored,
111}
112
113/// A rendered view: lines of segments, and where the terminal cursor goes
114/// (a text caret), or `None` to hide it.
115#[derive(Clone, Debug, Default)]
116#[non_exhaustive]
117pub struct ComponentView {
118    pub lines: Vec<Vec<Segment>>,
119    /// (row, column) within the view.
120    pub cursor: Option<(usize, usize)>,
121}
122
123impl ComponentView {
124    pub fn new(lines: Vec<Vec<Segment>>) -> ComponentView {
125        ComponentView {
126            lines,
127            cursor: None,
128        }
129    }
130
131    /// Put the cursor at `row`, `column` of the view.
132    #[must_use]
133    pub fn with_cursor(mut self, row: usize, column: usize) -> ComponentView {
134        self.cursor = Some((row, column));
135        self
136    }
137}
138
139/// What a component renders and handles events for: the console to render
140/// with, and the space it has.
141#[non_exhaustive]
142pub struct ComponentContext<'a> {
143    pub console: &'a Console,
144    /// Columns available.
145    pub width: usize,
146    /// Rows available; a taller view is cut off at the bottom.
147    pub height: usize,
148}
149
150impl<'a> ComponentContext<'a> {
151    pub fn new(console: &'a Console, width: usize, height: usize) -> ComponentContext<'a> {
152        ComponentContext {
153            console,
154            width,
155            height,
156        }
157    }
158
159    /// Render `renderable` at the context's width into lines.
160    pub fn lines(&self, renderable: &dyn Renderable) -> Vec<Vec<Segment>> {
161        let options = self.console.options().update_width(self.width.max(1));
162        self.console.render_lines(renderable, &options, false)
163    }
164
165    /// Render console markup into lines. Markup that does not parse is
166    /// shown as it is.
167    pub fn markup(&self, markup: &str) -> Vec<Vec<Segment>> {
168        let text = Text::from_markup(markup).unwrap_or_else(|_| Text::new(markup));
169        self.lines(&text)
170    }
171}
172
173/// One thing a key does in a component, for a help overlay, a shortcut
174/// sheet or status-bar hints. A host lists it under the component's
175/// registered name, and configuration can rebind it there.
176#[derive(Clone, Debug, PartialEq, Eq)]
177#[non_exhaustive]
178pub struct ComponentBinding {
179    /// What it does, for code and configuration: `down`.
180    pub action: String,
181    /// The keys that do it, by name (see [`ComponentEvent::Key`]); the
182    /// first is the one to show.
183    pub keys: Vec<String>,
184    /// What it does, for people: `move down`.
185    pub description: String,
186}
187
188impl ComponentBinding {
189    pub fn new(
190        action: impl Into<String>,
191        keys: impl IntoIterator<Item = impl Into<String>>,
192        description: impl Into<String>,
193    ) -> ComponentBinding {
194        ComponentBinding {
195            action: action.into(),
196            keys: keys.into_iter().map(Into::into).collect(),
197            description: description.into(),
198        }
199    }
200}
201
202/// An interactive component: a state machine that handles one event at a
203/// time and renders its state. Registered with
204/// [`PluginRegistrar::component`](crate::PluginRegistrar::component)
205/// through a [`ComponentFactory`].
206///
207/// Key presses and pastes come as text from the user, and so does anything
208/// a component shows of them: treat them as untrusted. The host keeps
209/// terminal controls out of what is painted.
210pub trait PluginComponent: Send {
211    /// Handle one event. Ctrl+C never arrives: the host ends the run.
212    fn handle(&mut self, event: &ComponentEvent, context: &ComponentContext<'_>) -> ComponentFlow;
213
214    /// Render the current state.
215    fn render(&self, context: &ComponentContext<'_>) -> ComponentView;
216
217    /// The keys it uses now, for help and hints. None, by default.
218    fn bindings(&self) -> Vec<ComponentBinding> {
219        Vec::new()
220    }
221
222    /// Whether it takes focus in a container. Yes, by default; a view that
223    /// only shows something says no.
224    fn focusable(&self) -> bool {
225        true
226    }
227
228    /// How often it wants [`ComponentEvent::Tick`]. Never, by default.
229    fn tick(&self) -> Option<Duration> {
230        None
231    }
232
233    /// Whether it wants mouse events. No, by default: reporting the mouse
234    /// takes text selection away from the terminal.
235    fn mouse(&self) -> bool {
236        false
237    }
238}