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    pub fn with_cursor(mut self, row: usize, column: usize) -> ComponentView {
133        self.cursor = Some((row, column));
134        self
135    }
136}
137
138/// What a component renders and handles events for: the console to render
139/// with, and the space it has.
140#[non_exhaustive]
141pub struct ComponentContext<'a> {
142    pub console: &'a Console,
143    /// Columns available.
144    pub width: usize,
145    /// Rows available; a taller view is cut off at the bottom.
146    pub height: usize,
147}
148
149impl<'a> ComponentContext<'a> {
150    pub fn new(console: &'a Console, width: usize, height: usize) -> ComponentContext<'a> {
151        ComponentContext {
152            console,
153            width,
154            height,
155        }
156    }
157
158    /// Render `renderable` at the context's width into lines.
159    pub fn lines(&self, renderable: &dyn Renderable) -> Vec<Vec<Segment>> {
160        let options = self.console.options().update_width(self.width.max(1));
161        self.console.render_lines(renderable, &options, false)
162    }
163
164    /// Render console markup into lines. Markup that does not parse is
165    /// shown as it is.
166    pub fn markup(&self, markup: &str) -> Vec<Vec<Segment>> {
167        let text = Text::from_markup(markup).unwrap_or_else(|_| Text::new(markup));
168        self.lines(&text)
169    }
170}
171
172/// One thing a key does in a component, for a help overlay, a shortcut
173/// sheet or status-bar hints. A host lists it under the component's
174/// registered name, and configuration can rebind it there.
175#[derive(Clone, Debug, PartialEq, Eq)]
176#[non_exhaustive]
177pub struct ComponentBinding {
178    /// What it does, for code and configuration: `down`.
179    pub action: String,
180    /// The keys that do it, by name (see [`ComponentEvent::Key`]); the
181    /// first is the one to show.
182    pub keys: Vec<String>,
183    /// What it does, for people: `move down`.
184    pub description: String,
185}
186
187impl ComponentBinding {
188    pub fn new(
189        action: impl Into<String>,
190        keys: impl IntoIterator<Item = impl Into<String>>,
191        description: impl Into<String>,
192    ) -> ComponentBinding {
193        ComponentBinding {
194            action: action.into(),
195            keys: keys.into_iter().map(Into::into).collect(),
196            description: description.into(),
197        }
198    }
199}
200
201/// An interactive component: a state machine that handles one event at a
202/// time and renders its state. Registered with
203/// [`PluginRegistrar::component`](crate::PluginRegistrar::component)
204/// through a [`ComponentFactory`].
205///
206/// Key presses and pastes come as text from the user, and so does anything
207/// a component shows of them: treat them as untrusted. The host keeps
208/// terminal controls out of what is painted.
209pub trait PluginComponent: Send {
210    /// Handle one event. Ctrl+C never arrives: the host ends the run.
211    fn handle(&mut self, event: &ComponentEvent, context: &ComponentContext<'_>) -> ComponentFlow;
212
213    /// Render the current state.
214    fn render(&self, context: &ComponentContext<'_>) -> ComponentView;
215
216    /// The keys it uses now, for help and hints. None, by default.
217    fn bindings(&self) -> Vec<ComponentBinding> {
218        Vec::new()
219    }
220
221    /// Whether it takes focus in a container. Yes, by default; a view that
222    /// only shows something says no.
223    fn focusable(&self) -> bool {
224        true
225    }
226
227    /// How often it wants [`ComponentEvent::Tick`]. Never, by default.
228    fn tick(&self) -> Option<Duration> {
229        None
230    }
231
232    /// Whether it wants mouse events. No, by default: reporting the mouse
233    /// takes text selection away from the terminal.
234    fn mouse(&self) -> bool {
235        false
236    }
237}