use crate::event::Event;
use std::fmt::Debug;
type ExecCallback<M> = Box<dyn Fn(Option<i32>) -> M + Send>;
type ExecDetails<M> = (String, Vec<String>, ExecCallback<M>);
pub trait Message: Send + 'static {}
impl<T: Send + 'static> Message for T {}
pub trait Model: Sized {
type Message: Message;
fn init(&mut self) -> Cmd<Self::Message> {
Cmd::noop()
}
fn update(&mut self, msg: Event<Self::Message>) -> Cmd<Self::Message>;
fn view(&self) -> String;
}
pub struct Cmd<M: Message> {
inner: CmdInner<M>,
}
type AsyncFuture<M> = Box<dyn std::future::Future<Output = Option<M>> + Send>;
type CommandFn<M> = Box<dyn FnOnce() -> Option<M> + Send>;
type FallibleFn<M> = Box<dyn FnOnce() -> crate::Result<Option<M>> + Send>;
pub(crate) enum CmdInner<M: Message> {
NoOp,
Function(CommandFn<M>),
Fallible(FallibleFn<M>),
ExecProcess {
program: String,
args: Vec<String>,
callback: ExecCallback<M>,
},
Quit,
Batch(Vec<Cmd<M>>),
Sequence(Vec<Cmd<M>>),
Tick {
duration: std::time::Duration,
callback: Box<dyn FnOnce() -> M + Send>,
},
Every {
duration: std::time::Duration,
callback: Box<dyn FnOnce(std::time::Instant) -> M + Send>,
},
Async(AsyncFuture<M>),
}
impl<M: Message> Cmd<M> {
pub fn new<F>(f: F) -> Self
where
F: FnOnce() -> Option<M> + Send + 'static,
{
Cmd {
inner: CmdInner::Function(Box::new(f)),
}
}
pub fn fallible<F>(f: F) -> Self
where
F: FnOnce() -> crate::Result<Option<M>> + Send + 'static,
{
Cmd {
inner: CmdInner::Fallible(Box::new(f)),
}
}
#[must_use]
pub fn noop() -> Self {
Cmd {
inner: CmdInner::NoOp,
}
}
#[deprecated(
since = "0.2.1",
note = "Use Cmd::noop() instead. The name 'none' was confusing."
)]
#[must_use]
pub fn none() -> Self {
Self::noop()
}
#[doc(hidden)]
pub fn exec_process<F>(program: String, args: Vec<String>, callback: F) -> Self
where
F: Fn(Option<i32>) -> M + Send + 'static,
{
Cmd {
inner: CmdInner::ExecProcess {
program,
args,
callback: Box::new(callback),
},
}
}
#[doc(hidden)]
#[must_use]
pub fn batch(cmds: Vec<Cmd<M>>) -> Self {
Cmd {
inner: CmdInner::Batch(cmds),
}
}
#[doc(hidden)]
#[must_use]
pub fn sequence(cmds: Vec<Cmd<M>>) -> Self {
Cmd {
inner: CmdInner::Sequence(cmds),
}
}
#[deprecated(since = "0.2.1", note = "Use commands::quit() instead for consistency")]
#[must_use]
pub fn quit() -> Self {
Cmd {
inner: CmdInner::Quit,
}
}
#[deprecated(since = "0.2.1", note = "Use commands::tick() instead for consistency")]
pub fn tick<F>(duration: std::time::Duration, callback: F) -> Self
where
F: FnOnce() -> M + Send + 'static,
{
Cmd {
inner: CmdInner::Tick {
duration,
callback: Box::new(callback),
},
}
}
#[deprecated(
since = "0.2.1",
note = "Use commands::every() instead for consistency"
)]
pub fn every<F>(duration: std::time::Duration, callback: F) -> Self
where
F: FnOnce(std::time::Instant) -> M + Send + 'static,
{
Cmd {
inner: CmdInner::Every {
duration,
callback: Box::new(callback),
},
}
}
#[doc(hidden)]
pub fn async_cmd<Fut>(future: Fut) -> Self
where
Fut: std::future::Future<Output = Option<M>> + Send + 'static,
{
Cmd {
inner: CmdInner::Async(Box::new(future)),
}
}
#[doc(hidden)]
pub fn execute(self) -> crate::Result<Option<M>> {
match self.inner {
CmdInner::Function(func) => Ok(func()),
CmdInner::Fallible(func) => func(),
CmdInner::ExecProcess {
program,
args,
callback,
} => {
use std::process::Command;
let output = Command::new(&program).args(&args).status();
let exit_code = output.ok().and_then(|status| status.code());
Ok(Some(callback(exit_code)))
}
CmdInner::NoOp => {
Ok(None)
}
CmdInner::Quit => {
Ok(None)
}
CmdInner::Batch(_) | CmdInner::Sequence(_) => {
Ok(None)
}
CmdInner::Tick { .. } | CmdInner::Every { .. } | CmdInner::Async(_) => {
Ok(None)
}
}
}
#[doc(hidden)]
pub fn test_execute(self) -> crate::Result<Option<M>> {
self.execute()
}
#[must_use]
pub fn is_exec_process(&self) -> bool {
matches!(self.inner, CmdInner::ExecProcess { .. })
}
#[must_use]
pub fn is_noop(&self) -> bool {
matches!(self.inner, CmdInner::NoOp)
}
#[must_use]
pub fn is_quit(&self) -> bool {
matches!(self.inner, CmdInner::Quit)
}
#[allow(clippy::type_complexity)]
#[doc(hidden)]
#[must_use]
pub fn take_exec_process(self) -> Option<ExecDetails<M>> {
match self.inner {
CmdInner::ExecProcess {
program,
args,
callback,
} => Some((program, args, callback)),
_ => None,
}
}
pub fn map<N, F>(self, f: F) -> Cmd<N>
where
N: Message,
F: Fn(M) -> N + Send + Sync + 'static + Clone,
{
let f = std::sync::Arc::new(f);
self.map_with_arc(f)
}
fn map_with_arc<N, F>(self, f: std::sync::Arc<F>) -> Cmd<N>
where
N: Message,
F: Fn(M) -> N + Send + Sync + 'static,
{
match self.inner {
CmdInner::NoOp => Cmd {
inner: CmdInner::NoOp,
},
CmdInner::Quit => Cmd {
inner: CmdInner::Quit,
},
CmdInner::Function(func) => {
let f = f.clone(); Cmd {
inner: CmdInner::Function(Box::new(move || func().map(move |m| f(m)))),
}
}
CmdInner::Fallible(func) => {
let f = f.clone(); Cmd {
inner: CmdInner::Fallible(Box::new(move || {
func().map(|opt| opt.map(move |m| f(m)))
})),
}
}
CmdInner::ExecProcess {
program,
args,
callback,
} => {
let f = f.clone(); Cmd {
inner: CmdInner::ExecProcess {
program,
args,
callback: Box::new(move |code| f(callback(code))),
},
}
}
CmdInner::Batch(cmds) => {
let mapped_cmds = cmds
.into_iter()
.map(|cmd| {
cmd.map_with_arc(f.clone()) })
.collect();
Cmd {
inner: CmdInner::Batch(mapped_cmds),
}
}
CmdInner::Sequence(cmds) => {
let mapped_cmds = cmds
.into_iter()
.map(|cmd| {
cmd.map_with_arc(f.clone()) })
.collect();
Cmd {
inner: CmdInner::Sequence(mapped_cmds),
}
}
CmdInner::Tick { duration, callback } => {
let f = f.clone(); Cmd {
inner: CmdInner::Tick {
duration,
callback: Box::new(move || f(callback())),
},
}
}
CmdInner::Every { duration, callback } => {
let f = f.clone(); Cmd {
inner: CmdInner::Every {
duration,
callback: Box::new(move |instant| f(callback(instant))),
},
}
}
CmdInner::Async(_future) => {
Cmd {
inner: CmdInner::NoOp,
}
}
}
}
#[doc(hidden)]
#[must_use]
pub fn is_batch(&self) -> bool {
matches!(self.inner, CmdInner::Batch(_))
}
#[doc(hidden)]
#[must_use]
pub fn take_batch(self) -> Option<Vec<Cmd<M>>> {
match self.inner {
CmdInner::Batch(cmds) => Some(cmds),
_ => None,
}
}
#[doc(hidden)]
#[must_use]
pub fn is_sequence(&self) -> bool {
matches!(self.inner, CmdInner::Sequence(_))
}
#[doc(hidden)]
#[must_use]
pub fn take_sequence(self) -> Option<Vec<Cmd<M>>> {
match self.inner {
CmdInner::Sequence(cmds) => Some(cmds),
_ => None,
}
}
#[doc(hidden)]
#[must_use]
pub fn is_tick(&self) -> bool {
matches!(self.inner, CmdInner::Tick { .. })
}
#[doc(hidden)]
#[must_use]
pub fn is_every(&self) -> bool {
matches!(self.inner, CmdInner::Every { .. })
}
#[doc(hidden)]
#[must_use]
pub fn take_tick(self) -> Option<(std::time::Duration, Box<dyn FnOnce() -> M + Send>)> {
match self.inner {
CmdInner::Tick { duration, callback } => Some((duration, callback)),
_ => None,
}
}
#[allow(clippy::type_complexity)]
#[doc(hidden)]
#[must_use]
pub fn take_every(
self,
) -> Option<(
std::time::Duration,
Box<dyn FnOnce(std::time::Instant) -> M + Send>,
)> {
match self.inner {
CmdInner::Every { duration, callback } => Some((duration, callback)),
_ => None,
}
}
#[doc(hidden)]
#[must_use]
pub fn is_async(&self) -> bool {
matches!(self.inner, CmdInner::Async(_))
}
#[doc(hidden)]
#[must_use]
pub fn take_async(self) -> Option<Box<dyn std::future::Future<Output = Option<M>> + Send>> {
match self.inner {
CmdInner::Async(future) => Some(future),
_ => None,
}
}
#[must_use]
pub fn inspect<F>(self, f: F) -> Self
where
F: FnOnce(&Self),
{
f(&self);
self
}
#[must_use]
pub fn inspect_if<F>(self, condition: bool, f: F) -> Self
where
F: FnOnce(&Self),
{
if condition {
f(&self);
}
self
}
#[must_use]
pub fn debug_name(&self) -> &'static str {
match self.inner {
CmdInner::Function(_) => "Function",
CmdInner::Fallible(_) => "Fallible",
CmdInner::ExecProcess { .. } => "ExecProcess",
CmdInner::NoOp => "NoOp",
CmdInner::Quit => "Quit",
CmdInner::Batch(_) => "Batch",
CmdInner::Sequence(_) => "Sequence",
CmdInner::Tick { .. } => "Tick",
CmdInner::Every { .. } => "Every",
CmdInner::Async(_) => "Async",
}
}
#[must_use]
pub fn then(self, other: Cmd<M>) -> Cmd<M> {
if self.is_noop() {
return other;
}
if other.is_noop() {
return self;
}
Cmd::sequence(vec![self, other])
}
#[must_use]
pub fn and(self, other: Cmd<M>) -> Cmd<M> {
if self.is_noop() {
return other;
}
if other.is_noop() {
return self;
}
Cmd::batch(vec![self, other])
}
#[must_use]
pub fn when(self, condition: bool) -> Cmd<M> {
if condition {
self
} else {
Cmd::noop()
}
}
#[must_use]
pub fn unless(self, condition: bool) -> Cmd<M> {
self.when(!condition)
}
}
impl<M: Message> Debug for Cmd<M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Cmd").finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::Event;
#[derive(Clone)]
struct Counter {
value: i32,
}
#[derive(Debug, Clone, PartialEq)]
enum Msg {
Increment,
Decrement,
SetValue(i32),
Noop,
}
impl Model for Counter {
type Message = Msg;
fn init(&mut self) -> Cmd<Self::Message> {
Cmd::new(|| Some(Msg::SetValue(0)))
}
fn update(&mut self, msg: Event<Self::Message>) -> Cmd<Self::Message> {
if let Event::User(msg) = msg {
match msg {
Msg::Increment => {
self.value += 1;
Cmd::new(move || Some(Msg::Noop))
}
Msg::Decrement => {
self.value -= 1;
Cmd::new(move || Some(Msg::Noop))
}
Msg::SetValue(v) => {
self.value = v;
Cmd::noop()
}
Msg::Noop => Cmd::noop(),
}
} else {
Cmd::noop()
}
}
fn view(&self) -> String {
format!("Count: {}", self.value)
}
}
#[test]
fn test_model_update() {
let mut model = Counter { value: 0 };
model.update(Event::User(Msg::Increment));
assert_eq!(model.value, 1);
model.update(Event::User(Msg::Decrement));
assert_eq!(model.value, 0);
}
#[test]
fn test_cmd_creation() {
let cmd = Cmd::new(|| Some(Msg::Increment));
let msg = cmd.test_execute().unwrap();
assert!(matches!(msg, Some(Msg::Increment)));
}
#[test]
fn test_cmd_noop() {
let cmd: Cmd<Msg> = Cmd::noop();
assert!(cmd.is_noop());
}
#[test]
fn test_cmd_none_deprecated() {
#[allow(deprecated)]
let cmd: Cmd<Msg> = Cmd::noop();
assert!(cmd.is_noop());
}
#[test]
fn test_cmd_fallible_success() {
let cmd = Cmd::fallible(|| Ok(Some(Msg::Increment)));
let result = cmd.test_execute();
assert!(result.is_ok());
assert_eq!(result.unwrap(), Some(Msg::Increment));
}
#[test]
fn test_cmd_fallible_error() {
let cmd: Cmd<Msg> =
Cmd::fallible(|| Err(crate::Error::from(std::io::Error::other("test error"))));
let result = cmd.test_execute();
assert!(result.is_err());
}
#[test]
fn test_cmd_exec_process() {
let cmd = Cmd::exec_process("echo".to_string(), vec!["test".to_string()], |_| {
Msg::Increment
});
assert!(cmd.is_exec_process());
let exec_details = cmd.take_exec_process();
assert!(exec_details.is_some());
let (program, args, _) = exec_details.unwrap();
assert_eq!(program, "echo");
assert_eq!(args, vec!["test"]);
}
#[test]
fn test_cmd_debug() {
let cmd = Cmd::new(|| Some(Msg::Increment));
let debug_str = format!("{cmd:?}");
assert!(debug_str.contains("Cmd"));
}
#[test]
fn test_command_composition_helpers() {
let cmd1: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
let cmd2: Cmd<Msg> = Cmd::new(|| Some(Msg::Decrement));
let chained = cmd1.then(cmd2);
assert!(chained.is_sequence());
let cmd1: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
let cmd2: Cmd<Msg> = Cmd::new(|| Some(Msg::Decrement));
let batched = cmd1.and(cmd2);
assert!(batched.is_batch());
let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
let enabled = cmd.when(true);
assert!(!enabled.is_noop());
let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
let disabled = cmd.when(false);
assert!(disabled.is_noop());
let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
let enabled = cmd.unless(false);
assert!(!enabled.is_noop());
let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
let disabled = cmd.unless(true);
assert!(disabled.is_noop());
let noop: Cmd<Msg> = Cmd::noop();
let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
let result = noop.then(cmd);
assert!(!result.is_sequence()); assert!(!result.is_noop());
let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
let noop: Cmd<Msg> = Cmd::noop();
let result = cmd.then(noop);
assert!(!result.is_sequence()); assert!(!result.is_noop());
let noop: Cmd<Msg> = Cmd::noop();
let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
let result = noop.and(cmd);
assert!(!result.is_batch()); assert!(!result.is_noop());
let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
let noop: Cmd<Msg> = Cmd::noop();
let result = cmd.and(noop);
assert!(!result.is_batch()); assert!(!result.is_noop());
}
#[test]
fn test_model_init() {
let mut model = Counter { value: 5 };
let cmd = model.init();
assert!(!cmd.is_noop());
let result = cmd.test_execute().unwrap();
assert_eq!(result, Some(Msg::SetValue(0)));
}
}