Skip to main content

rich_interact/
plugin.rs

1//! Components from plugins (0.0.14 workstream 4): a
2//! [`PluginComponent`](rich_ext::plugin::PluginComponent) that a plugin
3//! registered by name with `PluginRegistrar::component`, mounted as a
4//! [`Component`] beside the built-ins.
5//!
6//! A plugin depends on `rs-rich-plugin-api` and core only, so its components
7//! speak that crate's small contract: keys by name, text answers. A
8//! [`PluginView`] translates both ways. Its output is the text the plugin
9//! answers with; [`map`](crate::ComponentExt::map) it into a container's
10//! output like any other child. Its keys are listed under the registered
11//! name, so a help overlay shows them and configuration rebinds them
12//! (`counter.up = k`).
13//!
14//! A plugin component that panics does not take the app down: the view
15//! shows what went wrong, stops taking focus and ignores events.
16//!
17//! ```
18//! use std::sync::Arc;
19//!
20//! use rich_ext::plugin::component::{
21//!     ComponentContext, ComponentEvent, ComponentFlow, ComponentView, PluginComponent,
22//! };
23//! use rich_ext::plugin::{Plugin, PluginError, PluginMetadata, PluginRegistrar};
24//! use rich_ext::registry::ExtensionRegistry;
25//! use rich_interact::compose::{ComponentExt, Split};
26//! use rich_interact::plugin::PluginView;
27//! use rich_interact::{headless, Flow, Outcome, Select};
28//!
29//! #[derive(Default)]
30//! struct Counter(u32);
31//!
32//! impl PluginComponent for Counter {
33//!     fn handle(&mut self, event: &ComponentEvent, _: &ComponentContext<'_>) -> ComponentFlow {
34//!         match event.key() {
35//!             Some("up") => self.0 += 1,
36//!             Some("enter") => return ComponentFlow::Done(self.0.to_string()),
37//!             _ => return ComponentFlow::Ignored,
38//!         }
39//!         ComponentFlow::Continue
40//!     }
41//!     fn render(&self, context: &ComponentContext<'_>) -> ComponentView {
42//!         ComponentView::new(context.markup(&format!("count {}", self.0)))
43//!     }
44//! }
45//!
46//! struct Counters;
47//!
48//! impl Plugin for Counters {
49//!     fn metadata(&self) -> PluginMetadata {
50//!         PluginMetadata::new("counters", "Counters", "1.0.0")
51//!     }
52//!     fn register(&self, registrar: &mut dyn PluginRegistrar) -> Result<(), PluginError> {
53//!         registrar.component("counter", Arc::new(|| Box::new(Counter::default())));
54//!         Ok(())
55//!     }
56//! }
57//!
58//! let mut registry = ExtensionRegistry::new();
59//! registry.add_plugin(&Counters).unwrap();
60//! let counter = PluginView::mount(&registry, "counter").unwrap();
61//! let app = Split::horizontal(
62//!     counter.map(Flow::Done),
63//!     Select::new("Pick", ["a", "b"]).map(|pick| Flow::Done(pick.to_string())),
64//! );
65//! let script = headless::Script::new().keys("up up enter");
66//! let (outcome, _) = headless::run(app, script, 40, 4);
67//! assert_eq!(outcome.unwrap(), Outcome::Done("2".to_string()));
68//! ```
69
70use std::fmt;
71use std::panic::AssertUnwindSafe;
72use std::time::Duration;
73
74use rich::Style;
75use rich_ext::plugin::PluginComponent;
76use rich_ext::plugin::component::{
77    ComponentBinding, ComponentContext, ComponentEvent, ComponentFlow,
78};
79use rich_ext::registry::ExtensionRegistry;
80
81use crate::component::{Component, Context, Flow, View};
82use crate::event::{Event, Key, MouseKind};
83use crate::keymap::{Binding, Keymap};
84use crate::session::catch_panic;
85
86/// No component is registered under a name, or its factory failed.
87#[derive(Clone, Debug, PartialEq, Eq)]
88pub struct UnknownComponent {
89    /// The name asked for.
90    pub name: String,
91    /// Every name that is registered, sorted.
92    pub available: Vec<String>,
93}
94
95impl fmt::Display for UnknownComponent {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        write!(f, "no plugin component {:?}", self.name)?;
98        if self.available.is_empty() {
99            write!(f, "; no plugin registered one")
100        } else {
101            write!(f, "; available: {}", self.available.join(", "))
102        }
103    }
104}
105
106impl std::error::Error for UnknownComponent {}
107
108/// A plugin's component as a [`Component`] whose answer is the plugin's
109/// text. See the [module](self).
110pub struct PluginView {
111    name: String,
112    inner: Box<dyn PluginComponent>,
113    /// The panic that stopped it, shown instead of its view.
114    failed: Option<String>,
115    /// Keys rebound on this view, over the plugin's own.
116    keymap: Keymap,
117}
118
119impl PluginView {
120    /// Wrap `component`, listing its keys under `name`.
121    pub fn new(name: impl Into<String>, component: Box<dyn PluginComponent>) -> PluginView {
122        let name = name.into();
123        PluginView {
124            keymap: Keymap::new(name.clone()),
125            name,
126            inner: component,
127            failed: None,
128        }
129    }
130
131    /// A fresh instance of the component registered as `name` in
132    /// `registry`.
133    pub fn mount(registry: &ExtensionRegistry, name: &str) -> Result<PluginView, UnknownComponent> {
134        registry
135            .create_component(name)
136            .map(|component| PluginView::new(name, component))
137            .ok_or_else(|| UnknownComponent {
138                name: name.to_string(),
139                available: registry
140                    .component_names()
141                    .into_iter()
142                    .map(|(name, _)| name.to_string())
143                    .collect(),
144            })
145    }
146
147    /// The name its keys are listed under.
148    pub fn name(&self) -> &str {
149        &self.name
150    }
151
152    /// Make `keys` do the plugin's `action` instead of the keys it declared.
153    #[must_use]
154    pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
155        self.keymap.rebind(action, keys);
156        self
157    }
158
159    /// Why it stopped, if the plugin panicked.
160    pub fn failure(&self) -> Option<&str> {
161        self.failed.as_deref()
162    }
163
164    /// The bindings the plugin declares, as it declares them.
165    fn declared(&self) -> Vec<ComponentBinding> {
166        catch_panic(AssertUnwindSafe(|| self.inner.bindings())).unwrap_or_default()
167    }
168
169    /// The plugin's bindings, with this view's and the installed rebinds.
170    fn bindings(&self) -> Keymap {
171        self.keymap_of(self.declared())
172    }
173
174    fn keymap_of(&self, declared: Vec<ComponentBinding>) -> Keymap {
175        let mut keymap = self.keymap.clone();
176        for binding in declared {
177            let keys = binding.keys.iter().filter_map(|name| Key::parse(name));
178            keymap.add(Binding::new(
179                self.name.clone(),
180                binding.action,
181                keys,
182                binding.description,
183            ));
184        }
185        keymap
186    }
187
188    /// Run `f` on the plugin, catching a panic.
189    fn guard<T>(&mut self, f: impl FnOnce(&mut dyn PluginComponent) -> T) -> Option<T> {
190        if self.failed.is_some() {
191            return None;
192        }
193        match catch_panic(AssertUnwindSafe(|| f(&mut *self.inner))) {
194            Ok(value) => Some(value),
195            Err(panic) => {
196                self.failed = Some(panic_message(&*panic));
197                None
198            }
199        }
200    }
201}
202
203/// The name of `key` for the plugin, through `keymap` (the plugin's
204/// bindings with every rebind): a key that now does one of the plugin's
205/// actions arrives as the first key the plugin declared for it, spelled as
206/// the plugin spelled it (`esc`, `shift+tab`), so a rebound key does what
207/// the plugin knows; a declared key rebound away from its action does not
208/// arrive at all (`None`); any other key as itself.
209fn translate(keymap: &Keymap, declared: &[ComponentBinding], key: Key) -> Option<String> {
210    if let Some(action) = keymap.action(key) {
211        let name = declared
212            .iter()
213            .find(|binding| binding.action == action)
214            .and_then(|binding| binding.keys.iter().find(|name| Key::parse(name).is_some()));
215        return Some(name.cloned().unwrap_or_else(|| key.to_string()));
216    }
217    if keymap
218        .declared()
219        .iter()
220        .any(|binding| key.matches_any(&binding.keys))
221    {
222        return None;
223    }
224    Some(key.to_string())
225}
226
227fn event(value: &Event) -> Option<ComponentEvent> {
228    Some(match value {
229        Event::Key(key) => ComponentEvent::Key(key.to_string()),
230        Event::Paste(text) => ComponentEvent::Paste(text.clone()),
231        Event::Resize { columns, rows } => ComponentEvent::Resize {
232            columns: *columns,
233            rows: *rows,
234        },
235        Event::Tick => ComponentEvent::Tick,
236        Event::Mouse(mouse) => ComponentEvent::Mouse {
237            kind: match mouse.kind {
238                MouseKind::Down(_) => "down",
239                MouseKind::Up(_) => "up",
240                MouseKind::Drag(_) => "drag",
241                MouseKind::Moved => "moved",
242                MouseKind::ScrollUp => "scroll_up",
243                MouseKind::ScrollDown => "scroll_down",
244            }
245            .to_string(),
246            column: mouse.column,
247            row: mouse.row,
248        },
249        Event::Link(url) => ComponentEvent::Link(url.clone()),
250        // A plugin component cannot hand the terminal off.
251        Event::Returned(_) => return None,
252        // The plugin protocol has no releases.
253        Event::KeyUp(_) => return None,
254    })
255}
256
257impl Component for PluginView {
258    type Output = String;
259
260    fn handle(&mut self, value: &Event, context: &Context<'_>) -> Flow<String> {
261        let event = match value {
262            Event::Key(key) => {
263                let declared = self.declared();
264                match translate(&self.keymap_of(declared.clone()), &declared, *key) {
265                    Some(name) => ComponentEvent::Key(name),
266                    None => return Flow::Ignored,
267                }
268            }
269            other => match event(other) {
270                Some(event) => event,
271                None => return Flow::Ignored,
272            },
273        };
274        let inner = ComponentContext::new(context.console, context.width, context.height);
275        match self.guard(|component| component.handle(&event, &inner)) {
276            Some(ComponentFlow::Continue) => Flow::Continue,
277            Some(ComponentFlow::Done(answer)) => Flow::Done(answer),
278            Some(ComponentFlow::Cancel) => Flow::Cancel,
279            _ => Flow::Ignored,
280        }
281    }
282
283    fn render(&self, context: &Context<'_>) -> View {
284        if let Some(message) = &self.failed {
285            return self.failure_view(message, context);
286        }
287        let inner = ComponentContext::new(context.console, context.width, context.height);
288        match catch_panic(AssertUnwindSafe(|| self.inner.render(&inner))) {
289            Ok(view) => View {
290                lines: view.lines,
291                cursor: view.cursor,
292            },
293            // Rendering takes `&self`, so the panic cannot be kept: the
294            // next event finds it again, or finds the plugin recovered.
295            Err(panic) => self.failure_view(&panic_message(&*panic), context),
296        }
297    }
298
299    fn tick(&self) -> Option<Duration> {
300        if self.failed.is_some() {
301            return None;
302        }
303        catch_panic(AssertUnwindSafe(|| self.inner.tick())).unwrap_or(None)
304    }
305
306    fn mouse(&self) -> bool {
307        self.failed.is_none()
308            && catch_panic(AssertUnwindSafe(|| self.inner.mouse())).unwrap_or(false)
309    }
310
311    fn keymap(&self) -> Keymap {
312        if self.failed.is_some() {
313            return Keymap::default();
314        }
315        self.bindings()
316    }
317
318    fn focusable(&self) -> bool {
319        self.failed.is_none()
320            && catch_panic(AssertUnwindSafe(|| self.inner.focusable())).unwrap_or(false)
321    }
322}
323
324impl PluginView {
325    fn failure_view(&self, message: &str, context: &Context<'_>) -> View {
326        let message = format!("plugin component {} failed: {message}", self.name);
327        let mut text = rich::Text::new(&message);
328        let length = text.plain().chars().count();
329        text.stylize(Style::parse("red").unwrap_or_default(), 0, length);
330        View::new(context.lines(&text))
331    }
332}
333
334/// The text of a caught panic.
335fn panic_message(panic: &(dyn std::any::Any + Send)) -> String {
336    if let Some(message) = panic.downcast_ref::<&str>() {
337        (*message).to_string()
338    } else if let Some(message) = panic.downcast_ref::<String>() {
339        message.clone()
340    } else {
341        "a panic with no message".to_string()
342    }
343}