use std::fmt;
use std::panic::AssertUnwindSafe;
use std::time::Duration;
use rich::Style;
use rich_ext::plugin::PluginComponent;
use rich_ext::plugin::component::{
ComponentBinding, ComponentContext, ComponentEvent, ComponentFlow,
};
use rich_ext::registry::ExtensionRegistry;
use crate::component::{Component, Context, Flow, View};
use crate::event::{Event, Key, MouseKind};
use crate::keymap::{Binding, Keymap};
use crate::session::catch_panic;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnknownComponent {
pub name: String,
pub available: Vec<String>,
}
impl fmt::Display for UnknownComponent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "no plugin component {:?}", self.name)?;
if self.available.is_empty() {
write!(f, "; no plugin registered one")
} else {
write!(f, "; available: {}", self.available.join(", "))
}
}
}
impl std::error::Error for UnknownComponent {}
pub struct PluginView {
name: String,
inner: Box<dyn PluginComponent>,
failed: Option<String>,
keymap: Keymap,
}
impl PluginView {
pub fn new(name: impl Into<String>, component: Box<dyn PluginComponent>) -> PluginView {
let name = name.into();
PluginView {
keymap: Keymap::new(name.clone()),
name,
inner: component,
failed: None,
}
}
pub fn mount(registry: &ExtensionRegistry, name: &str) -> Result<PluginView, UnknownComponent> {
registry
.create_component(name)
.map(|component| PluginView::new(name, component))
.ok_or_else(|| UnknownComponent {
name: name.to_string(),
available: registry
.component_names()
.into_iter()
.map(|(name, _)| name.to_string())
.collect(),
})
}
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
self.keymap.rebind(action, keys);
self
}
pub fn failure(&self) -> Option<&str> {
self.failed.as_deref()
}
fn declared(&self) -> Vec<ComponentBinding> {
catch_panic(AssertUnwindSafe(|| self.inner.bindings())).unwrap_or_default()
}
fn bindings(&self) -> Keymap {
self.keymap_of(self.declared())
}
fn keymap_of(&self, declared: Vec<ComponentBinding>) -> Keymap {
let mut keymap = self.keymap.clone();
for binding in declared {
let keys = binding.keys.iter().filter_map(|name| Key::parse(name));
keymap.add(Binding::new(
self.name.clone(),
binding.action,
keys,
binding.description,
));
}
keymap
}
fn guard<T>(&mut self, f: impl FnOnce(&mut dyn PluginComponent) -> T) -> Option<T> {
if self.failed.is_some() {
return None;
}
match catch_panic(AssertUnwindSafe(|| f(&mut *self.inner))) {
Ok(value) => Some(value),
Err(panic) => {
self.failed = Some(panic_message(&*panic));
None
}
}
}
}
fn translate(keymap: &Keymap, declared: &[ComponentBinding], key: Key) -> Option<String> {
if let Some(action) = keymap.action(key) {
let name = declared
.iter()
.find(|binding| binding.action == action)
.and_then(|binding| binding.keys.iter().find(|name| Key::parse(name).is_some()));
return Some(name.cloned().unwrap_or_else(|| key.to_string()));
}
if keymap
.declared()
.iter()
.any(|binding| key.matches_any(&binding.keys))
{
return None;
}
Some(key.to_string())
}
fn event(value: &Event) -> Option<ComponentEvent> {
Some(match value {
Event::Key(key) => ComponentEvent::Key(key.to_string()),
Event::Paste(text) => ComponentEvent::Paste(text.clone()),
Event::Resize { columns, rows } => ComponentEvent::Resize {
columns: *columns,
rows: *rows,
},
Event::Tick => ComponentEvent::Tick,
Event::Mouse(mouse) => ComponentEvent::Mouse {
kind: match mouse.kind {
MouseKind::Down(_) => "down",
MouseKind::Up(_) => "up",
MouseKind::Drag(_) => "drag",
MouseKind::Moved => "moved",
MouseKind::ScrollUp => "scroll_up",
MouseKind::ScrollDown => "scroll_down",
}
.to_string(),
column: mouse.column,
row: mouse.row,
},
Event::Link(url) => ComponentEvent::Link(url.clone()),
Event::Returned(_) => return None,
Event::KeyUp(_) => return None,
})
}
impl Component for PluginView {
type Output = String;
fn handle(&mut self, value: &Event, context: &Context<'_>) -> Flow<String> {
let event = match value {
Event::Key(key) => {
let declared = self.declared();
match translate(&self.keymap_of(declared.clone()), &declared, *key) {
Some(name) => ComponentEvent::Key(name),
None => return Flow::Ignored,
}
}
other => match event(other) {
Some(event) => event,
None => return Flow::Ignored,
},
};
let inner = ComponentContext::new(context.console, context.width, context.height);
match self.guard(|component| component.handle(&event, &inner)) {
Some(ComponentFlow::Continue) => Flow::Continue,
Some(ComponentFlow::Done(answer)) => Flow::Done(answer),
Some(ComponentFlow::Cancel) => Flow::Cancel,
_ => Flow::Ignored,
}
}
fn render(&self, context: &Context<'_>) -> View {
if let Some(message) = &self.failed {
return self.failure_view(message, context);
}
let inner = ComponentContext::new(context.console, context.width, context.height);
match catch_panic(AssertUnwindSafe(|| self.inner.render(&inner))) {
Ok(view) => View {
lines: view.lines,
cursor: view.cursor,
},
Err(panic) => self.failure_view(&panic_message(&*panic), context),
}
}
fn tick(&self) -> Option<Duration> {
if self.failed.is_some() {
return None;
}
catch_panic(AssertUnwindSafe(|| self.inner.tick())).unwrap_or(None)
}
fn mouse(&self) -> bool {
self.failed.is_none()
&& catch_panic(AssertUnwindSafe(|| self.inner.mouse())).unwrap_or(false)
}
fn keymap(&self) -> Keymap {
if self.failed.is_some() {
return Keymap::default();
}
self.bindings()
}
fn focusable(&self) -> bool {
self.failed.is_none()
&& catch_panic(AssertUnwindSafe(|| self.inner.focusable())).unwrap_or(false)
}
}
impl PluginView {
fn failure_view(&self, message: &str, context: &Context<'_>) -> View {
let message = format!("plugin component {} failed: {message}", self.name);
let mut text = rich::Text::new(&message);
let length = text.plain().chars().count();
text.stylize(Style::parse("red").unwrap_or_default(), 0, length);
View::new(context.lines(&text))
}
}
fn panic_message(panic: &(dyn std::any::Any + Send)) -> String {
if let Some(message) = panic.downcast_ref::<&str>() {
(*message).to_string()
} else if let Some(message) = panic.downcast_ref::<String>() {
message.clone()
} else {
"a panic with no message".to_string()
}
}