use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use crossterm::event::{KeyEvent, MouseEvent};
use derive_more::{Debug, Eq, PartialEq};
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))
}
}
#[derive(Clone, Debug)]
pub enum Event {
Quit,
Error(String),
Close,
Tick,
Render,
Key(KeyEvent),
Paste(String),
Mouse(MouseEvent),
PreviewReady,
InvalidInput,
Action(Action),
AppendItems(Vec<Arc<dyn crate::SkimItem>>),
ClearItems,
Clear,
Heartbeat,
RunPreview,
RunExecute(String),
Redraw,
Reload(String),
Resize(u16, u16),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "listen", derive(serde::Serialize, serde::Deserialize))]
pub enum Action {
Abort,
Accept(Option<String>),
AddChar(char),
AppendAndSelect,
BackwardChar,
BackwardDeleteChar,
BackwardDeleteCharEof,
BackwardKillWord,
BackwardWord,
BeginningOfLine,
Bind(String),
Cancel,
ClearScreen,
DeleteChar,
DeleteCharEof,
DeselectAll,
Down(u16),
EndOfLine,
Execute(String),
ExecuteSilent(String),
First,
ForwardChar,
ForwardWord,
IfQueryEmpty(String, Option<String>),
IfQueryNotEmpty(String, Option<String>),
IfNonMatched(String, Option<String>),
Ignore,
KillLine,
KillWord,
Last,
NextHistory,
HalfPageDown(i32),
HalfPageUp(i32),
PageDown(i32),
PageUp(i32),
PreviewUp(i32),
PreviewDown(i32),
PreviewLeft(i32),
PreviewRight(i32),
PreviewPageUp(i32),
PreviewPageDown(i32),
PreviousHistory,
Redraw,
RefreshCmd,
RefreshPreview,
RestartMatcher,
Reload(Option<String>),
RotateMode,
ScrollLeft(i32),
ScrollRight(i32),
SelectAll,
SelectRow(usize),
Select,
Suppress,
SetHeader(Option<String>),
SetPreviewCmd(String),
SetQuery(String),
Toggle,
ToggleAll,
ToggleIn,
ToggleInteractive,
ToggleOut,
TogglePreview,
TogglePreviewWrap,
ToggleSort,
Top,
Unbind(String),
UnixLineDiscard,
UnixWordRubout,
Up(u16),
Yank,
#[debug("custom")]
#[eq(skip)]
#[partial_eq(skip)]
#[cfg_attr(feature = "listen", serde(skip))]
Custom(ActionCallback),
}
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))
}
macro_rules! define_action_catalog {
($arg:ident; $($pattern:pat => $name:literal => $parsed:expr),+ $(,)?) => {
impl Action {
#[must_use]
pub fn name(&self) -> &'static str {
#[allow(clippy::enum_glob_use)]
use Action::*;
match self {
$($pattern => $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(_) => "accept" => Some(Accept(arg)),
AddChar(_) => "add-char" => Some(AddChar(arg.unwrap_or_default().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(_) => "bind" => Some(Bind(arg.unwrap_or_default())),
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(_) => "down" => Some(Down(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
EndOfLine => "end-of-line" => Some(EndOfLine),
Execute(_) => "execute" => Some(Execute(arg.unwrap_or_default())),
ExecuteSilent(_) => "execute-silent" => Some(ExecuteSilent(arg.unwrap_or_default())),
First => "first" => Some(First),
ForwardChar => "forward-char" => Some(ForwardChar),
ForwardWord => "forward-word" => Some(ForwardWord),
IfQueryEmpty(..) => "if-query-empty" => parse_conditional(arg, IfQueryEmpty),
IfQueryNotEmpty(..) => "if-query-not-empty" => parse_conditional(arg, IfQueryNotEmpty),
IfNonMatched(..) => "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(_) => "half-page-down" => Some(HalfPageDown(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
HalfPageUp(_) => "half-page-up" => Some(HalfPageUp(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PageDown(_) => "page-down" => Some(PageDown(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PageUp(_) => "page-up" => Some(PageUp(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PreviewUp(_) => "preview-up" => Some(PreviewUp(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PreviewDown(_) => "preview-down" => Some(PreviewDown(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PreviewLeft(_) => "preview-left" => Some(PreviewLeft(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PreviewRight(_) => "preview-right" => Some(PreviewRight(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PreviewPageUp(_) => "preview-page-up" => Some(PreviewPageUp(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
PreviewPageDown(_) => "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(_) => "reload" => Some(Reload(arg)),
RotateMode => "rotate-mode" => Some(RotateMode),
ScrollLeft(_) => "scroll-left" => Some(ScrollLeft(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
ScrollRight(_) => "scroll-right" => Some(ScrollRight(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
SelectAll => "select-all" => Some(SelectAll),
SelectRow(_) => "select-row" => Some(SelectRow(arg.and_then(|s| s.parse().ok()).unwrap_or_default())),
Select => "select" => Some(Select),
SetHeader(_) => "set-header" => Some(SetHeader(arg)),
SetPreviewCmd(_) => "set-preview-cmd" => Some(SetPreviewCmd(arg.unwrap_or_default())),
SetQuery(_) => "set-query" => Some(SetQuery(arg.unwrap_or_default())),
Suppress => "suppress" => Some(Suppress),
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(_) => "unbind" => Some(Unbind(arg.unwrap_or_default())),
UnixLineDiscard => "unix-line-discard" => Some(UnixLineDiscard),
UnixWordRubout => "unix-word-rubout" => Some(UnixWordRubout),
Up(_) => "up" => Some(Up(arg.and_then(|s| s.parse().ok()).unwrap_or(1))),
Yank => "yank" => Some(Yank),
Custom(_) => "custom" => None,
}
#[allow(clippy::too_many_lines)]
#[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:?}");
if matches!(
action,
"add-char" | "bind" | "execute" | "execute-silent" | "set-preview-cmd" | "set-query" | "unbind"
) && arg.is_none()
{
None
} else {
parse_named_action(action, arg)
}
}
#[cfg(test)]
#[path = "event_tests.rs"]
mod tests;