use std::any::Any;
use std::future::Future;
use std::pin::Pin;
use std::time::{Duration, Instant};
use super::{ExecConfig, ExecResult};
#[derive(Debug, Clone, Default)]
pub enum AppMsg {
WindowResize { width: u16, height: u16 },
KeyInput(String),
Tick(Instant),
FocusChanged(Option<String>),
Blur,
#[default]
None,
}
pub struct BoxedMsg(Box<dyn Any + Send + 'static>);
impl BoxedMsg {
pub fn new<M: Any + Send + 'static>(msg: M) -> Self {
BoxedMsg(Box::new(msg))
}
pub fn downcast<M: Any + 'static>(self) -> Result<M, Self> {
match self.0.downcast::<M>() {
Ok(msg) => Ok(*msg),
Err(boxed) => Err(BoxedMsg(boxed)),
}
}
pub fn downcast_ref<M: Any + 'static>(&self) -> Option<&M> {
self.0.downcast_ref::<M>()
}
pub fn is<M: Any + 'static>(&self) -> bool {
self.0.is::<M>()
}
}
impl std::fmt::Debug for BoxedMsg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "BoxedMsg(...)")
}
}
#[derive(Default)]
pub enum Cmd<M = ()>
where
M: Send + 'static,
{
#[default]
None,
Batch(Vec<Cmd<M>>),
Sequence(Vec<Cmd<M>>),
Perform {
future: Pin<Box<dyn Future<Output = M> + Send + 'static>>,
},
Sleep {
duration: Duration,
then: Box<Cmd<M>>,
},
Tick {
duration: Duration,
msg_fn: Box<dyn FnOnce(Instant) -> M + Send + 'static>,
},
Every {
duration: Duration,
msg_fn: Box<dyn FnOnce(Instant) -> M + Send + 'static>,
},
Exec {
config: ExecConfig,
msg_fn: Box<dyn FnOnce(ExecResult) -> M + Send + 'static>,
},
ClearScreen,
HideCursor,
ShowCursor,
SetWindowTitle(String),
WindowSize,
EnterAltScreen,
ExitAltScreen,
EnableMouse,
DisableMouse,
EnableBracketedPaste,
DisableBracketedPaste,
}
impl<M> Cmd<M>
where
M: Send + 'static,
{
pub fn none() -> Self {
Cmd::None
}
pub fn batch(cmds: impl IntoIterator<Item = Cmd<M>>) -> Self {
let mut cmds: Vec<Cmd<M>> = cmds
.into_iter()
.filter(|cmd| !matches!(cmd, Cmd::None))
.collect();
match cmds.len() {
0 => Cmd::None,
1 => cmds.pop().unwrap(),
_ => Cmd::Batch(cmds),
}
}
pub fn sequence(cmds: impl IntoIterator<Item = Cmd<M>>) -> Self {
let mut cmds: Vec<Cmd<M>> = cmds
.into_iter()
.filter(|cmd| !matches!(cmd, Cmd::None))
.collect();
match cmds.len() {
0 => Cmd::None,
1 => cmds.pop().unwrap(),
_ => Cmd::Sequence(cmds),
}
}
pub fn perform<F, Fut>(f: F) -> Self
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = M> + Send + 'static,
{
Cmd::Perform {
future: Box::pin(async move { f().await }),
}
}
pub fn sleep(duration: Duration) -> Self {
Cmd::Sleep {
duration,
then: Box::new(Cmd::None),
}
}
pub fn tick<F>(duration: Duration, msg_fn: F) -> Self
where
F: FnOnce(Instant) -> M + Send + 'static,
{
Cmd::Tick {
duration,
msg_fn: Box::new(msg_fn),
}
}
pub fn every<F>(duration: Duration, msg_fn: F) -> Self
where
F: FnOnce(Instant) -> M + Send + 'static,
{
Cmd::Every {
duration,
msg_fn: Box::new(msg_fn),
}
}
pub fn exec<F>(config: ExecConfig, msg_fn: F) -> Self
where
F: FnOnce(ExecResult) -> M + Send + 'static,
{
Cmd::Exec {
config,
msg_fn: Box::new(msg_fn),
}
}
pub fn exec_cmd<F>(program: &str, args: &[&str], msg_fn: F) -> Self
where
F: FnOnce(ExecResult) -> M + Send + 'static,
{
let config = ExecConfig::new(program).args(args.iter().map(|s| s.to_string()));
Cmd::exec(config, msg_fn)
}
pub fn clear_screen() -> Self {
Cmd::ClearScreen
}
pub fn hide_cursor() -> Self {
Cmd::HideCursor
}
pub fn show_cursor() -> Self {
Cmd::ShowCursor
}
pub fn set_window_title(title: impl Into<String>) -> Self {
Cmd::SetWindowTitle(title.into())
}
pub fn window_size() -> Self {
Cmd::WindowSize
}
pub fn enter_alt_screen() -> Self {
Cmd::EnterAltScreen
}
pub fn exit_alt_screen() -> Self {
Cmd::ExitAltScreen
}
pub fn enable_mouse() -> Self {
Cmd::EnableMouse
}
pub fn disable_mouse() -> Self {
Cmd::DisableMouse
}
pub fn enable_bracketed_paste() -> Self {
Cmd::EnableBracketedPaste
}
pub fn disable_bracketed_paste() -> Self {
Cmd::DisableBracketedPaste
}
pub fn and_then(self, next: Cmd<M>) -> Self {
match self {
Cmd::None => next,
Cmd::Sleep { duration, then } => {
let chained = then.and_then(next);
Cmd::Sleep {
duration,
then: Box::new(chained),
}
}
other => Cmd::batch(vec![other, next]),
}
}
pub fn is_none(&self) -> bool {
matches!(self, Cmd::None)
}
pub fn map<N, F>(self, f: F) -> Cmd<N>
where
N: Send + 'static,
F: FnOnce(M) -> N + Send + 'static + Clone,
{
match self {
Cmd::None => Cmd::None,
Cmd::Batch(cmds) => Cmd::Batch(cmds.into_iter().map(|c| c.map(f.clone())).collect()),
Cmd::Sequence(cmds) => {
Cmd::Sequence(cmds.into_iter().map(|c| c.map(f.clone())).collect())
}
Cmd::Perform { future } => Cmd::Perform {
future: Box::pin(async move {
let msg = future.await;
f(msg)
}),
},
Cmd::Sleep { duration, then } => Cmd::Sleep {
duration,
then: Box::new(then.map(f)),
},
Cmd::Tick { duration, msg_fn } => Cmd::Tick {
duration,
msg_fn: Box::new(move |t| f(msg_fn(t))),
},
Cmd::Every { duration, msg_fn } => Cmd::Every {
duration,
msg_fn: Box::new(move |t| f(msg_fn(t))),
},
Cmd::Exec { config, msg_fn } => Cmd::Exec {
config,
msg_fn: Box::new(move |r| f(msg_fn(r))),
},
Cmd::ClearScreen => Cmd::ClearScreen,
Cmd::HideCursor => Cmd::HideCursor,
Cmd::ShowCursor => Cmd::ShowCursor,
Cmd::SetWindowTitle(title) => Cmd::SetWindowTitle(title),
Cmd::WindowSize => Cmd::WindowSize,
Cmd::EnterAltScreen => Cmd::EnterAltScreen,
Cmd::ExitAltScreen => Cmd::ExitAltScreen,
Cmd::EnableMouse => Cmd::EnableMouse,
Cmd::DisableMouse => Cmd::DisableMouse,
Cmd::EnableBracketedPaste => Cmd::EnableBracketedPaste,
Cmd::DisableBracketedPaste => Cmd::DisableBracketedPaste,
}
}
}
impl<M> std::fmt::Debug for Cmd<M>
where
M: Send + 'static,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Cmd::None => write!(f, "Cmd::None"),
Cmd::Batch(cmds) => f.debug_tuple("Cmd::Batch").field(cmds).finish(),
Cmd::Sequence(cmds) => f.debug_tuple("Cmd::Sequence").field(cmds).finish(),
Cmd::Perform { .. } => write!(f, "Cmd::Perform {{ ... }}"),
Cmd::Sleep { duration, then } => f
.debug_struct("Cmd::Sleep")
.field("duration", duration)
.field("then", then)
.finish(),
Cmd::Tick { duration, .. } => f
.debug_struct("Cmd::Tick")
.field("duration", duration)
.finish(),
Cmd::Every { duration, .. } => f
.debug_struct("Cmd::Every")
.field("duration", duration)
.finish(),
Cmd::Exec { config, .. } => {
f.debug_struct("Cmd::Exec").field("config", config).finish()
}
Cmd::ClearScreen => write!(f, "Cmd::ClearScreen"),
Cmd::HideCursor => write!(f, "Cmd::HideCursor"),
Cmd::ShowCursor => write!(f, "Cmd::ShowCursor"),
Cmd::SetWindowTitle(title) => {
f.debug_tuple("Cmd::SetWindowTitle").field(title).finish()
}
Cmd::WindowSize => write!(f, "Cmd::WindowSize"),
Cmd::EnterAltScreen => write!(f, "Cmd::EnterAltScreen"),
Cmd::ExitAltScreen => write!(f, "Cmd::ExitAltScreen"),
Cmd::EnableMouse => write!(f, "Cmd::EnableMouse"),
Cmd::DisableMouse => write!(f, "Cmd::DisableMouse"),
Cmd::EnableBracketedPaste => write!(f, "Cmd::EnableBracketedPaste"),
Cmd::DisableBracketedPaste => write!(f, "Cmd::DisableBracketedPaste"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, PartialEq)]
enum TestMsg {
Loaded(String),
Tick(u64),
}
#[test]
fn test_cmd_none_default() {
let cmd: Cmd<TestMsg> = Cmd::none();
assert!(cmd.is_none());
let default_cmd: Cmd<TestMsg> = Cmd::default();
assert!(default_cmd.is_none());
}
#[test]
fn test_cmd_batch_and_sequence() {
let batch: Cmd<TestMsg> = Cmd::batch(vec![
Cmd::sleep(Duration::from_millis(10)),
Cmd::sleep(Duration::from_millis(20)),
]);
assert!(matches!(batch, Cmd::Batch(_)));
let seq: Cmd<TestMsg> = Cmd::sequence(vec![
Cmd::sleep(Duration::from_millis(10)),
Cmd::sleep(Duration::from_millis(20)),
]);
assert!(matches!(seq, Cmd::Sequence(_)));
}
#[test]
fn test_cmd_perform_tick_every_exec() {
let perform: Cmd<TestMsg> = Cmd::perform(|| async { TestMsg::Loaded("ok".into()) });
assert!(matches!(perform, Cmd::Perform { .. }));
let tick: Cmd<TestMsg> = Cmd::tick(Duration::from_secs(1), |_| TestMsg::Tick(1));
assert!(matches!(tick, Cmd::Tick { .. }));
let every: Cmd<TestMsg> = Cmd::every(Duration::from_secs(1), |_| TestMsg::Tick(2));
assert!(matches!(every, Cmd::Every { .. }));
let exec: Cmd<TestMsg> = Cmd::exec(ExecConfig::new("echo").arg("hi"), |_| {
TestMsg::Loaded("done".into())
});
assert!(matches!(exec, Cmd::Exec { .. }));
}
#[test]
fn test_cmd_and_then_chains_sleep() {
let cmd: Cmd<TestMsg> = Cmd::sleep(Duration::from_secs(1))
.and_then(Cmd::sleep(Duration::from_secs(2)))
.and_then(Cmd::sleep(Duration::from_secs(3)));
assert!(matches!(cmd, Cmd::Sleep { .. }));
}
#[test]
fn test_cmd_map_message_type() {
#[derive(Debug, PartialEq)]
enum ParentMsg {
Child(TestMsg),
}
let child_cmd: Cmd<TestMsg> = Cmd::batch(vec![
Cmd::sleep(Duration::from_secs(1)),
Cmd::perform(|| async { TestMsg::Loaded("data".into()) }),
]);
let parent_cmd: Cmd<ParentMsg> = child_cmd.map(ParentMsg::Child);
assert!(matches!(parent_cmd, Cmd::Batch(_)));
}
#[test]
fn test_terminal_cmd_variants_exist() {
assert!(matches!(Cmd::<()>::clear_screen(), Cmd::ClearScreen));
assert!(matches!(Cmd::<()>::hide_cursor(), Cmd::HideCursor));
assert!(matches!(Cmd::<()>::show_cursor(), Cmd::ShowCursor));
assert!(matches!(Cmd::<()>::window_size(), Cmd::WindowSize));
assert!(matches!(Cmd::<()>::enter_alt_screen(), Cmd::EnterAltScreen));
assert!(matches!(Cmd::<()>::exit_alt_screen(), Cmd::ExitAltScreen));
assert!(matches!(Cmd::<()>::enable_mouse(), Cmd::EnableMouse));
assert!(matches!(Cmd::<()>::disable_mouse(), Cmd::DisableMouse));
assert!(matches!(
Cmd::<()>::enable_bracketed_paste(),
Cmd::EnableBracketedPaste
));
assert!(matches!(
Cmd::<()>::disable_bracketed_paste(),
Cmd::DisableBracketedPaste
));
}
#[test]
fn test_app_msg_default() {
assert!(matches!(AppMsg::default(), AppMsg::None));
}
#[test]
fn test_boxed_msg_downcast() {
let msg = BoxedMsg::new(TestMsg::Tick(42));
assert!(msg.is::<TestMsg>());
let downcasted = msg.downcast::<TestMsg>();
assert!(downcasted.is_ok());
assert_eq!(downcasted.unwrap(), TestMsg::Tick(42));
}
}