mod exec;
mod executor;
mod msg;
mod tasks;
pub use exec::{ExecConfig, ExecResult};
pub use executor::{CmdExecutor, RenderHandle, run_exec_process};
pub use msg::{AppMsg, BoxedMsg, TypedCmd};
pub use tasks::{HttpRequest, HttpResponse, ProcessOutput};
pub(crate) use exec::ExecRequest;
use std::future::Future;
use std::pin::Pin;
use std::time::{Duration, Instant};
#[derive(Default)]
pub enum Cmd {
#[default]
None,
Batch(Vec<Cmd>),
Sequence(Vec<Cmd>),
Perform {
future: Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
},
Sleep {
duration: Duration,
then: Box<Cmd>,
},
Tick {
duration: Duration,
callback: Box<dyn FnOnce(Instant) + Send + 'static>,
},
Every {
duration: Duration,
callback: Box<dyn FnOnce(Instant) + Send + 'static>,
},
Exec {
config: ExecConfig,
callback: Box<dyn FnOnce(ExecResult) + Send + 'static>,
},
ClearScreen,
HideCursor,
ShowCursor,
SetWindowTitle(String),
WindowSize,
EnterAltScreen,
ExitAltScreen,
EnableMouse,
DisableMouse,
EnableBracketedPaste,
DisableBracketedPaste,
}
impl Cmd {
pub fn none() -> Self {
Cmd::None
}
pub fn batch(cmds: impl IntoIterator<Item = Cmd>) -> Self {
let mut cmds: Vec<Cmd> = 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>) -> Self {
let mut cmds: Vec<Cmd> = 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 = ()> + 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, callback: F) -> Self
where
F: FnOnce(Instant) + Send + 'static,
{
Cmd::Tick {
duration,
callback: Box::new(callback),
}
}
pub fn every<F>(duration: Duration, callback: F) -> Self
where
F: FnOnce(Instant) + Send + 'static,
{
Cmd::Every {
duration,
callback: Box::new(callback),
}
}
pub fn exec<F>(config: ExecConfig, callback: F) -> Self
where
F: FnOnce(ExecResult) + Send + 'static,
{
Cmd::Exec {
config,
callback: Box::new(callback),
}
}
pub fn exec_cmd<F>(program: &str, args: &[&str], callback: F) -> Self
where
F: FnOnce(ExecResult) + Send + 'static,
{
let config = ExecConfig::new(program).args(args.iter().map(|s| s.to_string()));
Cmd::exec(config, callback)
}
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) -> 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<F>(self, f: F) -> Self
where
F: FnOnce(Self) -> Self,
{
f(self)
}
}
impl std::fmt::Debug for Cmd {
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::*;
#[test]
fn test_cmd_none() {
let cmd = Cmd::none();
assert!(cmd.is_none());
assert!(matches!(cmd, Cmd::None));
}
#[test]
fn test_cmd_default() {
let cmd = Cmd::default();
assert!(cmd.is_none());
}
#[test]
fn test_cmd_batch_empty() {
let cmd = Cmd::batch(vec![]);
assert!(cmd.is_none());
}
#[test]
fn test_cmd_batch_single() {
let cmd = Cmd::batch(vec![Cmd::none()]);
assert!(cmd.is_none());
}
#[test]
fn test_cmd_batch_filters_none() {
let cmd = Cmd::batch(vec![Cmd::none(), Cmd::none(), Cmd::none()]);
assert!(cmd.is_none());
}
#[test]
fn test_cmd_batch_single_non_none() {
let cmd = Cmd::batch(vec![
Cmd::none(),
Cmd::sleep(Duration::from_secs(1)),
Cmd::none(),
]);
assert!(matches!(cmd, Cmd::Sleep { .. }));
}
#[test]
fn test_cmd_batch_multiple() {
let cmd = Cmd::batch(vec![
Cmd::sleep(Duration::from_secs(1)),
Cmd::sleep(Duration::from_secs(2)),
]);
assert!(matches!(cmd, Cmd::Batch(_)));
if let Cmd::Batch(cmds) = cmd {
assert_eq!(cmds.len(), 2);
}
}
#[test]
fn test_cmd_perform() {
let cmd = Cmd::perform(|| async {
println!("test");
});
assert!(matches!(cmd, Cmd::Perform { .. }));
assert!(!cmd.is_none());
}
#[test]
fn test_cmd_sleep() {
let duration = Duration::from_secs(1);
let cmd = Cmd::sleep(duration);
assert!(matches!(cmd, Cmd::Sleep { .. }));
if let Cmd::Sleep {
duration: d,
then: t,
} = cmd
{
assert_eq!(d, duration);
assert!(t.is_none());
}
}
#[test]
fn test_cmd_and_then_none() {
let cmd = Cmd::none().and_then(Cmd::sleep(Duration::from_secs(1)));
assert!(matches!(cmd, Cmd::Sleep { .. }));
}
#[test]
fn test_cmd_and_then_sleep() {
let cmd = Cmd::sleep(Duration::from_secs(1)).and_then(Cmd::sleep(Duration::from_secs(2)));
assert!(matches!(cmd, Cmd::Sleep { .. }));
if let Cmd::Sleep { duration, then } = cmd {
assert_eq!(duration, Duration::from_secs(1));
assert!(matches!(*then, Cmd::Sleep { .. }));
if let Cmd::Sleep { duration, .. } = *then {
assert_eq!(duration, Duration::from_secs(2));
}
}
}
#[test]
fn test_cmd_and_then_perform() {
let cmd = Cmd::perform(|| async {}).and_then(Cmd::perform(|| async {}));
assert!(matches!(cmd, Cmd::Batch(_)));
if let Cmd::Batch(cmds) = cmd {
assert_eq!(cmds.len(), 2);
}
}
#[test]
fn test_cmd_and_then_chain() {
let cmd = 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() {
let cmd = Cmd::none().map(|_| Cmd::sleep(Duration::from_secs(1)));
assert!(matches!(cmd, Cmd::Sleep { .. }));
}
#[test]
fn test_cmd_map_wrap() {
let cmd = Cmd::perform(|| async {}).map(|c| {
Cmd::batch(vec![
Cmd::perform(|| async {
println!("before");
}),
c,
Cmd::perform(|| async {
println!("after");
}),
])
});
assert!(matches!(cmd, Cmd::Batch(_)));
if let Cmd::Batch(cmds) = cmd {
assert_eq!(cmds.len(), 3);
}
}
#[test]
fn test_cmd_debug() {
let cmd = Cmd::none();
let debug_str = format!("{:?}", cmd);
assert_eq!(debug_str, "Cmd::None");
let cmd = Cmd::batch(vec![Cmd::none(), Cmd::none()]);
let debug_str = format!("{:?}", cmd);
assert_eq!(debug_str, "Cmd::None");
let cmd = Cmd::sleep(Duration::from_secs(1));
let debug_str = format!("{:?}", cmd);
assert!(debug_str.contains("Cmd::Sleep"));
let cmd = Cmd::perform(|| async {});
let debug_str = format!("{:?}", cmd);
assert!(debug_str.contains("Cmd::Perform"));
}
#[test]
fn test_cmd_nested_batch() {
let cmd = Cmd::batch(vec![
Cmd::batch(vec![Cmd::sleep(Duration::from_secs(1))]),
Cmd::batch(vec![Cmd::sleep(Duration::from_secs(2))]),
]);
assert!(matches!(cmd, Cmd::Batch(_)));
}
#[test]
fn test_cmd_complex_composition() {
let cmd = Cmd::batch(vec![
Cmd::sleep(Duration::from_secs(1)).and_then(Cmd::perform(|| async {})),
Cmd::perform(|| async {}).and_then(Cmd::sleep(Duration::from_secs(2))),
Cmd::none(),
]);
assert!(matches!(cmd, Cmd::Batch(_)));
if let Cmd::Batch(cmds) = cmd {
assert_eq!(cmds.len(), 2);
}
}
#[test]
fn test_cmd_sequence_empty() {
let cmd = Cmd::sequence(vec![]);
assert!(cmd.is_none());
}
#[test]
fn test_cmd_sequence_single() {
let cmd = Cmd::sequence(vec![Cmd::none()]);
assert!(cmd.is_none());
}
#[test]
fn test_cmd_sequence_filters_none() {
let cmd = Cmd::sequence(vec![Cmd::none(), Cmd::none(), Cmd::none()]);
assert!(cmd.is_none());
}
#[test]
fn test_cmd_sequence_single_non_none() {
let cmd = Cmd::sequence(vec![
Cmd::none(),
Cmd::sleep(Duration::from_secs(1)),
Cmd::none(),
]);
assert!(matches!(cmd, Cmd::Sleep { .. }));
}
#[test]
fn test_cmd_sequence_multiple() {
let cmd = Cmd::sequence(vec![
Cmd::sleep(Duration::from_secs(1)),
Cmd::sleep(Duration::from_secs(2)),
]);
assert!(matches!(cmd, Cmd::Sequence(_)));
if let Cmd::Sequence(cmds) = cmd {
assert_eq!(cmds.len(), 2);
}
}
#[test]
fn test_cmd_sequence_preserves_order() {
let cmd = Cmd::sequence(vec![
Cmd::sleep(Duration::from_secs(1)),
Cmd::sleep(Duration::from_secs(2)),
Cmd::sleep(Duration::from_secs(3)),
]);
if let Cmd::Sequence(cmds) = cmd {
assert_eq!(cmds.len(), 3);
if let Cmd::Sleep { duration, .. } = &cmds[0] {
assert_eq!(*duration, Duration::from_secs(1));
}
if let Cmd::Sleep { duration, .. } = &cmds[1] {
assert_eq!(*duration, Duration::from_secs(2));
}
if let Cmd::Sleep { duration, .. } = &cmds[2] {
assert_eq!(*duration, Duration::from_secs(3));
}
}
}
#[test]
fn test_cmd_sequence_debug() {
let cmd = Cmd::sequence(vec![
Cmd::sleep(Duration::from_secs(1)),
Cmd::sleep(Duration::from_secs(2)),
]);
let debug_str = format!("{:?}", cmd);
assert!(debug_str.contains("Cmd::Sequence"));
}
#[test]
fn test_cmd_nested_sequence() {
let cmd = Cmd::sequence(vec![
Cmd::sequence(vec![Cmd::sleep(Duration::from_secs(1))]),
Cmd::sequence(vec![Cmd::sleep(Duration::from_secs(2))]),
]);
assert!(matches!(cmd, Cmd::Sequence(_)));
}
#[test]
fn test_cmd_sequence_with_batch() {
let cmd = Cmd::sequence(vec![
Cmd::batch(vec![
Cmd::sleep(Duration::from_millis(100)),
Cmd::sleep(Duration::from_millis(100)),
]),
Cmd::perform(|| async {}),
]);
assert!(matches!(cmd, Cmd::Sequence(_)));
if let Cmd::Sequence(cmds) = cmd {
assert_eq!(cmds.len(), 2);
assert!(matches!(cmds[0], Cmd::Batch(_)));
assert!(matches!(cmds[1], Cmd::Perform { .. }));
}
}
#[test]
fn test_cmd_tick() {
let duration = Duration::from_secs(1);
let cmd = Cmd::tick(duration, |_| {});
assert!(matches!(cmd, Cmd::Tick { .. }));
if let Cmd::Tick {
duration: d,
callback: _,
} = cmd
{
assert_eq!(d, duration);
}
}
#[test]
fn test_cmd_tick_debug() {
let cmd = Cmd::tick(Duration::from_secs(1), |_| {});
let debug_str = format!("{:?}", cmd);
assert!(debug_str.contains("Cmd::Tick"));
assert!(debug_str.contains("duration"));
}
#[test]
fn test_cmd_tick_is_not_none() {
let cmd = Cmd::tick(Duration::from_millis(100), |_| {});
assert!(!cmd.is_none());
}
#[test]
fn test_cmd_every() {
let duration = Duration::from_secs(1);
let cmd = Cmd::every(duration, |_| {});
assert!(matches!(cmd, Cmd::Every { .. }));
if let Cmd::Every {
duration: d,
callback: _,
} = cmd
{
assert_eq!(d, duration);
}
}
#[test]
fn test_cmd_every_debug() {
let cmd = Cmd::every(Duration::from_secs(1), |_| {});
let debug_str = format!("{:?}", cmd);
assert!(debug_str.contains("Cmd::Every"));
assert!(debug_str.contains("duration"));
}
#[test]
fn test_cmd_every_is_not_none() {
let cmd = Cmd::every(Duration::from_millis(100), |_| {});
assert!(!cmd.is_none());
}
#[test]
fn test_cmd_batch_with_tick() {
let cmd = Cmd::batch(vec![
Cmd::tick(Duration::from_millis(100), |_| {}),
Cmd::tick(Duration::from_millis(200), |_| {}),
]);
assert!(matches!(cmd, Cmd::Batch(_)));
if let Cmd::Batch(cmds) = cmd {
assert_eq!(cmds.len(), 2);
}
}
#[test]
fn test_cmd_sequence_with_tick() {
let cmd = Cmd::sequence(vec![
Cmd::tick(Duration::from_millis(100), |_| {}),
Cmd::perform(|| async {}),
]);
assert!(matches!(cmd, Cmd::Sequence(_)));
if let Cmd::Sequence(cmds) = cmd {
assert_eq!(cmds.len(), 2);
}
}
#[test]
fn test_cmd_batch_with_every() {
let cmd = Cmd::batch(vec![
Cmd::every(Duration::from_secs(1), |_| {}),
Cmd::every(Duration::from_secs(2), |_| {}),
]);
assert!(matches!(cmd, Cmd::Batch(_)));
if let Cmd::Batch(cmds) = cmd {
assert_eq!(cmds.len(), 2);
}
}
#[test]
fn test_cmd_complex_mixed_composition() {
let cmd = Cmd::sequence(vec![
Cmd::batch(vec![
Cmd::tick(Duration::from_millis(50), |_| {}),
Cmd::perform(|| async {}),
]),
Cmd::sleep(Duration::from_millis(100)),
Cmd::every(Duration::from_secs(1), |_| {}),
]);
assert!(matches!(cmd, Cmd::Sequence(_)));
if let Cmd::Sequence(cmds) = cmd {
assert_eq!(cmds.len(), 3);
assert!(matches!(cmds[0], Cmd::Batch(_)));
assert!(matches!(cmds[1], Cmd::Sleep { .. }));
assert!(matches!(cmds[2], Cmd::Every { .. }));
}
}
#[test]
fn test_cmd_exec() {
let cmd = Cmd::exec(ExecConfig::new("vim").arg("file.txt"), |_| {});
assert!(matches!(cmd, Cmd::Exec { .. }));
if let Cmd::Exec { config, .. } = cmd {
assert_eq!(config.command, "vim");
assert_eq!(config.args, vec!["file.txt"]);
}
}
#[test]
fn test_cmd_exec_cmd() {
let cmd = Cmd::exec_cmd("less", &["README.md", "-N"], |_| {});
assert!(matches!(cmd, Cmd::Exec { .. }));
if let Cmd::Exec { config, .. } = cmd {
assert_eq!(config.command, "less");
assert_eq!(config.args, vec!["README.md", "-N"]);
}
}
#[test]
fn test_cmd_exec_debug() {
let cmd = Cmd::exec(ExecConfig::new("vim"), |_| {});
let debug_str = format!("{:?}", cmd);
assert!(debug_str.contains("Cmd::Exec"));
assert!(debug_str.contains("vim"));
}
#[test]
fn test_cmd_exec_is_not_none() {
let cmd = Cmd::exec(ExecConfig::new("echo"), |_| {});
assert!(!cmd.is_none());
}
#[test]
fn test_cmd_exec_in_sequence() {
let cmd = Cmd::sequence(vec![
Cmd::exec(ExecConfig::new("vim"), |_| {}),
Cmd::perform(|| async {}),
]);
assert!(matches!(cmd, Cmd::Sequence(_)));
if let Cmd::Sequence(cmds) = cmd {
assert_eq!(cmds.len(), 2);
assert!(matches!(cmds[0], Cmd::Exec { .. }));
assert!(matches!(cmds[1], Cmd::Perform { .. }));
}
}
}