use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use derive_more::{Debug, Eq, PartialEq};
use super::event::Event;
type BoxError = Box<dyn std::error::Error + Sync + Send>;
type BoxFuture<'a> = Pin<Box<dyn Future<Output = Result<Vec<Event>, BoxError>> + Send + 'a>>;
trait AsyncCallbackFn: Send {
fn call<'a>(&'a self, app: &'a mut crate::tui::App) -> BoxFuture<'a>;
}
struct AsyncFnWrapper<F>(F);
impl<F, Fut> AsyncCallbackFn for AsyncFnWrapper<F>
where
F: for<'a> Fn(&'a mut crate::tui::App) -> Fut + Send,
Fut: Future<Output = Result<Vec<Event>, BoxError>> + Send + 'static,
{
fn call<'a>(&'a self, app: &'a mut crate::tui::App) -> BoxFuture<'a> {
Box::pin((self.0)(app))
}
}
struct SyncFnWrapper<F>(F);
impl<F> AsyncCallbackFn for SyncFnWrapper<F>
where
F: Fn(&mut crate::tui::App) -> Result<Vec<Event>, BoxError> + Send,
{
fn call<'a>(&'a self, app: &'a mut crate::tui::App) -> BoxFuture<'a> {
Box::pin(std::future::ready((self.0)(app)))
}
}
#[derive(Clone)]
pub struct ActionCallback(Arc<Mutex<dyn AsyncCallbackFn>>);
impl std::fmt::Debug for ActionCallback {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ActionCallback").finish()
}
}
impl ActionCallback {
pub fn new<F, Fut>(f: F) -> Self
where
F: for<'a> Fn(&'a mut crate::tui::App) -> Fut + Send + 'static,
Fut: Future<Output = Result<Vec<Event>, BoxError>> + Send + 'static,
{
Self(Arc::new(Mutex::new(AsyncFnWrapper(f))))
}
pub fn new_sync<F>(f: F) -> Self
where
F: Fn(&mut crate::tui::App) -> Result<Vec<Event>, BoxError> + Send + 'static,
{
Self(Arc::new(Mutex::new(SyncFnWrapper(f))))
}
pub(crate) fn call(&self, app: &mut crate::tui::App) -> Result<Vec<Event>, BoxError> {
let callback = self.0.lock().unwrap();
let fut = callback.call(app);
tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut))
}
}
fn parse_conditional(arg: Option<String>, constructor: fn(String, Option<String>) -> Action) -> Option<Action> {
let arg = arg?;
let (then, otherwise) = match arg.split_once('+') {
Some((then, "")) => (then, None),
Some((then, otherwise)) => (then, Some(otherwise.to_string())),
None => (arg.as_str(), None),
};
Some(constructor(then.to_string(), otherwise))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ActionDoc {
pub name: &'static str,
pub takes_arg: bool,
pub doc: &'static str,
}
impl ActionDoc {
#[must_use]
pub fn display_name(&self) -> String {
if self.takes_arg {
format!("{}(...)", self.name)
} else {
self.name.to_string()
}
}
#[must_use]
pub fn summary(&self) -> String {
self.doc
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join(" ")
}
#[must_use]
pub fn is_bindable(&self) -> bool {
parse_action(self.name)
.or_else(|| parse_action(&format!("{}()", self.name)))
.is_some()
}
}
macro_rules! action_arg_marker {
($($payload:tt)*) => {
true
};
}
macro_rules! define_action_catalog {
(
$arg:ident;
$(
$(#[doc = $doc:literal])*
$(@attrs[$($attr:meta),+ $(,)?])?
$variant:ident $(($($payload:ty),+ $(,)?))? => $name:literal => $parsed:expr
),+ $(,)?
) => {
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "listen", derive(serde::Serialize, serde::Deserialize))]
pub enum Action {
$(
$(#[doc = $doc])*
$($(#[$attr])+)?
$variant $(($($payload),+))?,
)+
}
pub const ACTION_CATALOG: &[ActionDoc] = &[
$(ActionDoc {
name: $name,
takes_arg: false $(|| action_arg_marker!($($payload)+))?,
doc: concat!($($doc, "\n"),*),
}),+
];
impl Action {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
$(Self::$variant { .. } => $name),+
}
}
}
fn parse_named_action(action: &str, $arg: Option<String>) -> Option<Action> {
#[allow(clippy::enum_glob_use)]
use Action::*;
match action {
$($name => $parsed),+,
_ => None,
}
}
};
}
define_action_catalog! {
arg;
Abort => "abort" => Some(Abort),
Accept(Option<String>) => "accept" => Some(Accept(arg)),
AddChar(char) => "add-char" => arg.map(|s| AddChar(s.chars().next().unwrap_or_default())),
AppendAndSelect => "append-and-select" => Some(AppendAndSelect),
BackwardChar => "backward-char" => Some(BackwardChar),
BackwardDeleteChar => "backward-delete-char" => Some(BackwardDeleteChar),
BackwardDeleteCharEof => "backward-delete-char/eof" => Some(BackwardDeleteCharEof),
BackwardKillWord => "backward-kill-word" => Some(BackwardKillWord),
BackwardWord => "backward-word" => Some(BackwardWord),
BeginningOfLine => "beginning-of-line" => Some(BeginningOfLine),
Bind(String) => "bind" => arg.map(Bind),
Cancel => "cancel" => Some(Cancel),
ClearScreen => "clear-screen" => Some(ClearScreen),
DeleteChar => "delete-char" => Some(DeleteChar),
DeleteCharEof => "delete-char/eof" => Some(DeleteCharEof),
DeselectAll => "deselect-all" => Some(DeselectAll),
Down(u16) => "down" => Some(Down(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
EndOfLine => "end-of-line" => Some(EndOfLine),
Execute(String) => "execute" => arg.map(Execute),
ExecuteSilent(String) => "execute-silent" => arg.map(ExecuteSilent),
First => "first" => Some(First),
ForwardChar => "forward-char" => Some(ForwardChar),
ForwardWord => "forward-word" => Some(ForwardWord),
IfQueryEmpty(String, Option<String>) => "if-query-empty" => parse_conditional(arg, IfQueryEmpty),
IfQueryNotEmpty(String, Option<String>) => "if-query-not-empty" => parse_conditional(arg, IfQueryNotEmpty),
IfNonMatched(String, Option<String>) => "if-non-matched" => parse_conditional(arg, IfNonMatched),
Ignore => "ignore" => Some(Ignore),
KillLine => "kill-line" => Some(KillLine),
KillWord => "kill-word" => Some(KillWord),
Last => "last" => Some(Last),
NextHistory => "next-history" => Some(NextHistory),
HalfPageDown(i32) => "half-page-down" => Some(HalfPageDown(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
HalfPageUp(i32) => "half-page-up" => Some(HalfPageUp(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PageDown(i32) => "page-down" => Some(PageDown(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PageUp(i32) => "page-up" => Some(PageUp(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PreviewUp(i32) => "preview-up" => Some(PreviewUp(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PreviewDown(i32) => "preview-down" => Some(PreviewDown(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PreviewLeft(i32) => "preview-left" => Some(PreviewLeft(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PreviewRight(i32) => "preview-right" => Some(PreviewRight(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PreviewPageUp(i32) => "preview-page-up" => Some(PreviewPageUp(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PreviewPageDown(i32) => "preview-page-down" => Some(PreviewPageDown(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PreviousHistory => "previous-history" => Some(PreviousHistory),
Redraw => "redraw" => Some(Redraw),
RefreshCmd => "refresh-cmd" => Some(RefreshCmd),
RefreshPreview => "refresh-preview" => Some(RefreshPreview),
RestartMatcher => "restart-matcher" => Some(RestartMatcher),
Reload(Option<String>) => "reload" => Some(Reload(arg)),
RotateMode => "rotate-mode" => Some(RotateMode),
ScrollLeft(i32) => "scroll-left" => Some(ScrollLeft(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
ScrollRight(i32) => "scroll-right" => Some(ScrollRight(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
SelectAll => "select-all" => Some(SelectAll),
SelectRow(usize) => "select-row" => Some(SelectRow(arg.and_then(|s| s.parse().ok()).unwrap_or_default())),
Select => "select" => Some(Select),
Suppress => "suppress" => Some(Suppress),
SetCmd(String) => "set-cmd" => arg.map(SetCmd),
SetHeader(Option<String>) => "set-header" => Some(SetHeader(arg)),
SetPreviewCmd(String) => "set-preview-cmd" => arg.map(SetPreviewCmd),
SetQuery(String) => "set-query" => arg.map(SetQuery),
Toggle => "toggle" => Some(Toggle),
ToggleAll => "toggle-all" => Some(ToggleAll),
ToggleIn => "toggle-in" => Some(ToggleIn),
ToggleInteractive => "toggle-interactive" => Some(ToggleInteractive),
ToggleOut => "toggle-out" => Some(ToggleOut),
TogglePreview => "toggle-preview" => Some(TogglePreview),
TogglePreviewWrap => "toggle-preview-wrap" => Some(TogglePreviewWrap),
ToggleSort => "toggle-sort" => Some(ToggleSort),
Top => "top" => Some(Top),
Unbind(String) => "unbind" => arg.map(Unbind),
UnixLineDiscard => "unix-line-discard" => Some(UnixLineDiscard),
UnixWordRubout => "unix-word-rubout" => Some(UnixWordRubout),
Up(u16) => "up" => Some(Up(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
Yank => "yank" => Some(Yank),
@attrs[
debug("custom"),
eq(skip),
partial_eq(skip),
cfg_attr(feature = "listen", serde(skip)),
]
Custom(ActionCallback) => "custom" => None,
}
#[must_use]
pub fn parse_action(raw_action: &str) -> Option<Action> {
let parts = raw_action.split_once([':', '(', ')']);
let action;
let mut arg = None;
match parts {
None => action = raw_action,
Some((act, "")) => action = act,
Some((act, a)) => {
action = act;
arg = Some(a.trim_end_matches(')').to_string());
}
}
debug!("parse_action: action={action}, arg={arg:?}");
parse_named_action(action, arg)
}
#[cfg(test)]
#[path = "actions_tests.rs"]
mod tests;