hojicha_core/
core.rs

1//! Core traits and types for the Elm Architecture
2//!
3//! This module contains the foundational traits and types that implement
4//! The Elm Architecture (TEA) pattern in Hojicha.
5//!
6//! ## The Elm Architecture
7//!
8//! TEA is a pattern for organizing interactive applications with:
9//! - **Unidirectional data flow**: Events flow through update to modify state
10//! - **Pure functions**: Update and view are pure, side effects use commands
11//! - **Clear separation**: Model (state), Update (logic), View (presentation)
12//!
13//! ## Core Components
14//!
15//! ### Model Trait
16//! Your application struct implements this trait:
17//! ```
18//! # use hojicha_core::{Model, Cmd, Event};
19//! struct MyApp {
20//!     counter: i32,
21//! }
22//!
23//! impl Model for MyApp {
24//!     type Message = MyMessage;
25//!     
26//!     fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
27//!         // Handle events and return commands
28//!         Cmd::noop()
29//!     }
30//!     
31//!     fn view(&self) -> String {
32//!         // Render the UI
33//!         format!("Counter: {}", self.counter)
34//!     }
35//! }
36//! # enum MyMessage {}
37//! ```
38//!
39//! ### Commands
40//! Commands represent side effects that produce messages:
41//! ```
42//! # use hojicha_core::Cmd;
43//! # enum Msg { DataLoaded(String) }
44//! let cmd: Cmd<Msg> = Cmd::new(|| {
45//!     // Perform side effect
46//!     let data = std::fs::read_to_string("data.txt").ok()?;
47//!     Some(Msg::DataLoaded(data))
48//! });
49//! ```
50
51use crate::event::Event;
52use std::fmt::Debug;
53
54/// Type alias for exec process callback function
55type ExecCallback<M> = Box<dyn Fn(Option<i32>) -> M + Send>;
56
57/// Type alias for exec process details
58type ExecDetails<M> = (String, Vec<String>, ExecCallback<M>);
59
60/// A message that can be sent to update the model.
61///
62/// Messages are typically enums that represent different events
63/// or state changes in your application.
64/// Marker trait for messages that can be sent through the system.
65///
66/// Types implementing this trait must be Send + 'static to work
67/// with the async runtime.
68pub trait Message: Send + 'static {}
69
70// Blanket implementation - any type that is Send + 'static can be a Message.
71// This is appropriate because Message is purely a marker trait with no behavior.
72impl<T: Send + 'static> Message for T {}
73
74/// The core trait for your application's model.
75///
76/// Your model should implement this trait to define how it:
77/// - Initializes (`init`)
78/// - Updates in response to messages (`update`)
79/// - Renders to the screen (`view`)
80///
81/// ## Method Requirements
82///
83/// ### `init()` - Optional Initialization
84/// The `init()` method is **optional** because many models don't need special
85/// initialization logic. The default implementation returns `Cmd::noop()`,
86/// which starts the event loop immediately without side effects.
87///
88/// **Override `init()` when you need to:**
89/// - Load configuration or data files at startup
90/// - Start background timers or periodic tasks
91/// - Fetch initial data from external APIs
92/// - Set up subscriptions to external data streams
93///
94/// **Use the default when:**
95/// - Your model starts with static data
96/// - No external setup is required
97/// - All state is initialized through your struct's constructor
98///
99/// ### `update()` and `view()` - Required Core Methods
100/// These methods are **required** because they form the core of the Elm Architecture:
101///
102/// - **`update()`**: The state transition function. Must handle all events and
103///   return appropriate commands. This is where your business logic lives.
104/// - **`view()`**: The rendering function. Must render the current state to
105///   the terminal. This is where your UI layout is defined.
106///
107/// ## Complete Working Example
108///
109/// Here's a full counter application showing all lifecycle methods with proper imports:
110///
111/// ```rust
112/// use hojicha_core::{Model, Cmd, Event, Key, commands};
113/// use std::time::Duration;
114///
115/// // Your application state
116/// #[derive(Debug)]
117/// struct CounterApp {
118///     count: i32,
119///     last_action: String,
120///     timer_active: bool,
121/// }
122///
123/// // All possible messages your app can receive
124/// #[derive(Debug, Clone)]
125/// enum CounterMessage {
126///     Increment,
127///     Decrement,
128///     Reset,
129///     TimerTick,
130///     LoadConfig(String),
131/// }
132///
133/// impl CounterApp {
134///     fn new() -> Self {
135///         Self {
136///             count: 0,
137///             last_action: "Started".to_string(),
138///             timer_active: false,
139///         }
140///     }
141/// }
142///
143/// impl Model for CounterApp {
144///     type Message = CounterMessage;
145///
146///     fn init(&mut self) -> Cmd<Self::Message> {
147///         // Initialize by loading config and starting a timer
148///         commands::batch(vec![
149///             // Load configuration file
150///             Cmd::new(|| {
151///                 let config = std::fs::read_to_string("config.txt")
152///                     .unwrap_or_else(|_| "Default config".to_string());
153///                 Some(CounterMessage::LoadConfig(config))
154///             }),
155///             // Start a timer that ticks every 5 seconds
156///             commands::every(Duration::from_secs(5), |_| CounterMessage::TimerTick)
157///         ])
158///     }
159///
160///     fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
161///         match event {
162///             // Handle keyboard input
163///             Event::Key(key) => match key.key {
164///                 Key::Char('q') | Key::Esc => {
165///                     // Quit the application
166///                     commands::quit()
167///                 }
168///                 Key::Char('+') | Key::Up => {
169///                     self.count += 1;
170///                     self.last_action = "Incremented".to_string();
171///                     Cmd::noop() // Continue running
172///                 }
173///                 Key::Char('-') | Key::Down => {
174///                     self.count -= 1;
175///                     self.last_action = "Decremented".to_string();
176///                     Cmd::noop()
177///                 }
178///                 Key::Char('r') => {
179///                     self.count = 0;
180///                     self.last_action = "Reset".to_string();
181///                     Cmd::noop()
182///                 }
183///                 Key::Char('t') => {
184///                     self.timer_active = !self.timer_active;
185///                     self.last_action = format!("Timer {}",
186///                         if self.timer_active { "started" } else { "stopped" });
187///                     Cmd::noop()
188///                 }
189///                 _ => Cmd::noop() // Ignore other keys
190///             },
191///
192///             // Handle application messages
193///             Event::User(msg) => match msg {
194///                 CounterMessage::Increment => {
195///                     self.count += 1;
196///                     self.last_action = "Auto-incremented".to_string();
197///                     Cmd::noop()
198///                 }
199///                 CounterMessage::Decrement => {
200///                     self.count -= 1;
201///                     self.last_action = "Auto-decremented".to_string();
202///                     Cmd::noop()
203///                 }
204///                 CounterMessage::Reset => {
205///                     self.count = 0;
206///                     self.last_action = "Auto-reset".to_string();
207///                     Cmd::noop()
208///                 }
209///                 CounterMessage::TimerTick => {
210///                     if self.timer_active {
211///                         self.count += 1;
212///                         self.last_action = "Timer tick".to_string();
213///                     }
214///                     Cmd::noop()
215///                 }
216///                 CounterMessage::LoadConfig(config) => {
217///                     self.last_action = format!("Config loaded: {}", config);
218///                     Cmd::noop()
219///                 }
220///             },
221///
222///             // Handle other events
223///             Event::Tick => Cmd::noop(),
224///             Event::Resize { width: _, height: _ } => {
225///                 self.last_action = "Window resized".to_string();
226///                 Cmd::noop()
227///             }
228///             _ => Cmd::noop(),
229///         }
230///     }
231///
232///     fn view(&self) -> String {
233///         format!(
234///             "Counter Application\n\
235///              \n\
236///              Count: {}\n\
237///              Last Action: {}\n\
238///              Timer: {}\n\
239///              \n\
240///              Controls:\n\
241///              +/↑  - Increment\n\
242///              -/↓  - Decrement\n\
243///              r    - Reset\n\
244///              t    - Toggle timer\n\
245///              q/Esc - Quit",
246///             self.count,
247///             self.last_action,
248///             if self.timer_active { "Active" } else { "Inactive" }
249///         )
250///     }
251/// }
252///
253/// // Usage in main function:
254/// // let mut app = CounterApp::new();
255/// // hojicha::run(app).unwrap();
256/// ```
257///
258/// ## Simple Examples
259///
260/// ### Basic Model (No Initialization)
261/// ```
262/// # use hojicha_core::{Model, Cmd, Event};
263/// struct Counter { value: i32 }
264///
265/// impl Model for Counter {
266///     type Message = CounterMsg;
267///     
268///     // Using default init() - no override needed
269///     
270///     fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
271///         // Handle events...
272///         Cmd::noop()
273///     }
274///     
275///     fn view(&self) -> String {
276///         format!("Count: {}", self.value)
277///     }
278/// }
279/// # enum CounterMsg {}
280/// ```
281///
282/// ### Model with Initialization
283/// ```
284/// # use hojicha_core::{Model, Cmd, Event, commands};
285/// # use std::time::Duration;
286/// struct App { status: String }
287///
288/// impl Model for App {
289///     type Message = AppMsg;
290///     
291///     fn init(&mut self) -> Cmd<Self::Message> {
292///         // Override init() to start a timer
293///         commands::every(Duration::from_secs(1), |_| AppMsg::Tick)
294///     }
295///     
296///     fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
297///         // Handle events...
298///         Cmd::noop()
299///     }
300///     
301///     fn view(&self) -> String {
302///         format!("Status: {}", self.status)
303///     }
304/// }
305/// # enum AppMsg { Tick }
306/// ```
307pub trait Model: Sized {
308    /// The type of messages this model can receive
309    type Message: Message;
310
311    /// Initialize the model and return a command to run
312    ///
313    /// This method is called once when the program starts. Use it to:
314    /// - Load initial data
315    /// - Start timers
316    /// - Perform initial setup
317    ///
318    /// Returns:
319    /// - `Cmd::noop()` - Start the event loop without any initial command
320    /// - Any other command - Execute the command before starting the event loop
321    ///
322    /// # Example
323    /// ```
324    /// # use hojicha_core::{Model, Cmd, commands};
325    /// # use std::time::Duration;
326    /// # struct MyApp;
327    /// # enum Msg { Tick }
328    /// # impl Model for MyApp {
329    /// #     type Message = Msg;
330    /// fn init(&mut self) -> Cmd<Self::Message> {
331    ///     // Start a timer that ticks every second
332    ///     commands::every(Duration::from_secs(1), |_| Msg::Tick)
333    /// }
334    /// #     fn update(&mut self, _: hojicha_core::Event<Self::Message>) -> Cmd<Self::Message> { Cmd::noop() }
335    /// #     fn view(&self) -> String { String::new() }
336    /// # }
337    /// ```
338    fn init(&mut self) -> Cmd<Self::Message> {
339        Cmd::noop()
340    }
341
342    /// Update the model based on a message and return a command
343    ///
344    /// This is the heart of your application logic. Handle events here and
345    /// update your model's state accordingly.
346    ///
347    /// Returns:
348    /// - `Cmd::noop()` - Continue running without executing any command
349    /// - `commands::quit()` - Exit the program
350    /// - Any other command - Execute the command and continue
351    ///
352    /// # Example
353    /// ```
354    /// # use hojicha_core::{Model, Cmd, Event, Key, commands};
355    /// # struct Counter { value: i32 }
356    /// # enum Msg { Increment, Decrement }
357    /// # impl Model for Counter {
358    /// #     type Message = Msg;
359    /// fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
360    ///     match event {
361    ///         Event::Key(key) if key.key == Key::Char('q') => {
362    ///             commands::quit()
363    ///         }
364    ///         Event::User(Msg::Increment) => {
365    ///             self.value += 1;
366    ///             Cmd::noop()
367    ///         }
368    ///         Event::User(Msg::Decrement) => {
369    ///             self.value -= 1;
370    ///             Cmd::noop()
371    ///         }
372    ///         _ => Cmd::noop()
373    ///     }
374    /// }
375    /// #     fn view(&self) -> String { String::new() }
376    /// # }
377    /// ```
378    fn update(&mut self, msg: Event<Self::Message>) -> Cmd<Self::Message>;
379
380    /// Render the model to a string
381    ///
382    /// This method is called after each update to render your UI.
383    /// Return a string with ANSI escape codes for terminal rendering.
384    ///
385    /// # Example
386    /// ```
387    /// # use hojicha_core::Model;
388    /// # struct MyApp { message: String }
389    /// # impl Model for MyApp {
390    /// #     type Message = ();
391    /// #     fn update(&mut self, _: hojicha_core::Event<Self::Message>) -> hojicha_core::Cmd<Self::Message> { hojicha_core::Cmd::noop() }
392    /// fn view(&self) -> String {
393    ///     format!("My App\n{}", self.message)
394    /// }
395    /// # }
396    /// ```
397    fn view(&self) -> String;
398}
399
400/// A command is an asynchronous operation that produces a message.
401///
402/// Commands are used for side effects like:
403/// - HTTP requests
404/// - File I/O
405/// - Timers
406/// - Any async operation
407pub struct Cmd<M: Message> {
408    inner: CmdInner<M>,
409}
410
411// Type aliases for complex command types
412type AsyncFuture<M> = Box<dyn std::future::Future<Output = Option<M>> + Send>;
413type CommandFn<M> = Box<dyn FnOnce() -> Option<M> + Send>;
414type FallibleFn<M> = Box<dyn FnOnce() -> crate::Result<Option<M>> + Send>;
415
416pub(crate) enum CmdInner<M: Message> {
417    /// No operation - continue running without doing anything
418    NoOp,
419    /// A simple function command
420    Function(CommandFn<M>),
421    /// A function command that can return errors
422    Fallible(FallibleFn<M>),
423    /// An external process command
424    ExecProcess {
425        program: String,
426        args: Vec<String>,
427        callback: ExecCallback<M>,
428    },
429    /// Quit the program
430    Quit,
431    /// Execute multiple commands concurrently
432    Batch(Vec<Cmd<M>>),
433    /// Execute multiple commands sequentially
434    Sequence(Vec<Cmd<M>>),
435    /// Execute after a delay
436    Tick {
437        duration: std::time::Duration,
438        callback: Box<dyn FnOnce() -> M + Send>,
439    },
440    /// Execute repeatedly at intervals
441    Every {
442        duration: std::time::Duration,
443        callback: Box<dyn FnOnce(std::time::Instant) -> M + Send>,
444    },
445    /// Execute an async future
446    Async(AsyncFuture<M>),
447}
448
449impl<M: Message> Cmd<M> {
450    /// Create a new command from a function
451    ///
452    /// Note: If the function returns `None`, consider using `Cmd::noop()` instead
453    /// for better performance and clearer intent.
454    ///
455    /// # Example
456    /// ```
457    /// # use hojicha_core::Cmd;
458    /// # enum Msg { DataLoaded(String) }
459    /// let cmd: Cmd<Msg> = Cmd::new(|| {
460    ///     // Perform a side effect
461    ///     let data = std::fs::read_to_string("config.json").ok()?;
462    ///     Some(Msg::DataLoaded(data))
463    /// });
464    /// ```
465    pub fn new<F>(f: F) -> Self
466    where
467        F: FnOnce() -> Option<M> + Send + 'static,
468    {
469        Cmd {
470            inner: CmdInner::Function(Box::new(f)),
471        }
472    }
473
474    /// Create a new fallible command that can return errors
475    ///
476    /// Use this when your command might fail and you want to handle errors gracefully.
477    ///
478    /// # Example
479    /// ```
480    /// # use hojicha_core::{Cmd, Result};
481    /// # enum Msg { ConfigLoaded(String) }
482    /// let cmd: Cmd<Msg> = Cmd::fallible(|| {
483    ///     let data = std::fs::read_to_string("config.json")?;
484    ///     Ok(Some(Msg::ConfigLoaded(data)))
485    /// });
486    /// ```
487    pub fn fallible<F>(f: F) -> Self
488    where
489        F: FnOnce() -> crate::Result<Option<M>> + Send + 'static,
490    {
491        Cmd {
492            inner: CmdInner::Fallible(Box::new(f)),
493        }
494    }
495
496    /// Returns a no-op command that continues running without doing anything
497    ///
498    /// This is the idiomatic way to return "no command" from `update()`.
499    /// The program will continue running without executing any side effects.
500    ///
501    /// # Example
502    /// ```
503    /// # use hojicha_core::{Model, Cmd, Event};
504    /// # struct MyApp;
505    /// # impl Model for MyApp {
506    /// #     type Message = ();
507    /// #     fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
508    ///         match event {
509    ///             Event::Tick => {
510    ///                 // Update internal state but don't trigger side effects
511    ///                 Cmd::noop()
512    ///             }
513    ///             _ => Cmd::noop()
514    ///         }
515    /// #     }
516    /// #     fn view(&self) -> String { String::new() }
517    /// # }
518    /// ```
519    #[must_use]
520    pub fn noop() -> Self {
521        Cmd {
522            inner: CmdInner::NoOp,
523        }
524    }
525
526    /// Returns a no-op command that continues running without doing anything
527    ///
528    /// # Deprecated
529    /// This method is deprecated. Use `Cmd::noop()` instead for clearer intent.
530    /// The name "none" was confusing because it returns `Some(Cmd)` rather than `None`.
531    ///
532    /// # Example
533    /// ```
534    /// # use hojicha_core::Cmd;
535    /// # enum Msg {}
536    /// // Old way (deprecated):
537    /// let cmd: Cmd<Msg> = Cmd::noop();
538    ///
539    /// // New way (preferred):
540    /// let cmd: Cmd<Msg> = Cmd::noop();
541    /// ```
542    #[deprecated(
543        since = "0.2.1",
544        note = "Use Cmd::noop() instead. The name 'none' was confusing."
545    )]
546    #[must_use]
547    pub fn none() -> Self {
548        Self::noop()
549    }
550
551    /// Internal method
552    #[doc(hidden)]
553    pub fn exec_process<F>(program: String, args: Vec<String>, callback: F) -> Self
554    where
555        F: Fn(Option<i32>) -> M + Send + 'static,
556    {
557        Cmd {
558            inner: CmdInner::ExecProcess {
559                program,
560                args,
561                callback: Box::new(callback),
562            },
563        }
564    }
565
566    /// Create a batch command that executes commands concurrently
567    /// Internal method
568    #[doc(hidden)]
569    #[must_use]
570    pub fn batch(cmds: Vec<Cmd<M>>) -> Self {
571        Cmd {
572            inner: CmdInner::Batch(cmds),
573        }
574    }
575
576    /// Create a sequence command that executes commands in order
577    /// Internal method
578    #[doc(hidden)]
579    #[must_use]
580    pub fn sequence(cmds: Vec<Cmd<M>>) -> Self {
581        Cmd {
582            inner: CmdInner::Sequence(cmds),
583        }
584    }
585
586    /// Create a quit command
587    ///
588    /// # Deprecated
589    /// This method is deprecated. Use `commands::quit()` instead for consistency.
590    ///
591    /// # Example
592    /// ```no_run
593    /// # use hojicha_core::{Cmd, commands};
594    /// # enum Msg {}
595    /// // Old way (deprecated):
596    /// let cmd: Cmd<Msg> = Cmd::quit();
597    ///
598    /// // New way (preferred):
599    /// let cmd: Cmd<Msg> = commands::quit();
600    /// ```
601    #[deprecated(since = "0.2.1", note = "Use commands::quit() instead for consistency")]
602    #[must_use]
603    pub fn quit() -> Self {
604        Cmd {
605            inner: CmdInner::Quit,
606        }
607    }
608
609    /// Create a tick command
610    ///
611    /// # Deprecated
612    /// This method is deprecated. Use `commands::tick()` instead for consistency.
613    ///
614    /// # Example
615    /// ```no_run
616    /// # use hojicha_core::{Cmd, commands};
617    /// # use std::time::Duration;
618    /// # enum Msg { Tick }
619    /// // Old way (deprecated):
620    /// let cmd: Cmd<Msg> = Cmd::tick(Duration::from_secs(1), || Msg::Tick);
621    ///
622    /// // New way (preferred):
623    /// let cmd: Cmd<Msg> = commands::tick(Duration::from_secs(1), || Msg::Tick);
624    /// ```
625    #[deprecated(since = "0.2.1", note = "Use commands::tick() instead for consistency")]
626    pub fn tick<F>(duration: std::time::Duration, callback: F) -> Self
627    where
628        F: FnOnce() -> M + Send + 'static,
629    {
630        Cmd {
631            inner: CmdInner::Tick {
632                duration,
633                callback: Box::new(callback),
634            },
635        }
636    }
637
638    /// Create an every command
639    ///
640    /// # Deprecated
641    /// This method is deprecated. Use `commands::every()` instead for consistency.
642    ///
643    /// # Example
644    /// ```no_run
645    /// # use hojicha_core::{Cmd, commands};
646    /// # use std::time::{Duration, Instant};
647    /// # enum Msg { Tick(Instant) }
648    /// // Old way (deprecated):
649    /// let cmd: Cmd<Msg> = Cmd::every(Duration::from_secs(1), |instant| Msg::Tick(instant));
650    ///
651    /// // New way (preferred):
652    /// let cmd: Cmd<Msg> = commands::every(Duration::from_secs(1), |instant| Msg::Tick(instant));
653    /// ```
654    #[deprecated(
655        since = "0.2.1",
656        note = "Use commands::every() instead for consistency"
657    )]
658    pub fn every<F>(duration: std::time::Duration, callback: F) -> Self
659    where
660        F: FnOnce(std::time::Instant) -> M + Send + 'static,
661    {
662        Cmd {
663            inner: CmdInner::Every {
664                duration,
665                callback: Box::new(callback),
666            },
667        }
668    }
669
670    /// Create an async command
671    /// Internal method
672    #[doc(hidden)]
673    pub fn async_cmd<Fut>(future: Fut) -> Self
674    where
675        Fut: std::future::Future<Output = Option<M>> + Send + 'static,
676    {
677        Cmd {
678            inner: CmdInner::Async(Box::new(future)),
679        }
680    }
681
682    /// Execute the command and return its message
683    /// Internal method
684    #[doc(hidden)]
685    pub fn execute(self) -> crate::Result<Option<M>> {
686        match self.inner {
687            CmdInner::Function(func) => Ok(func()),
688            CmdInner::Fallible(func) => func(),
689            CmdInner::ExecProcess {
690                program,
691                args,
692                callback,
693            } => {
694                // In the real implementation, this would be handled by the runtime
695                // For now, we'll just run it directly
696                use std::process::Command;
697                let output = Command::new(&program).args(&args).status();
698                let exit_code = output.ok().and_then(|status| status.code());
699                Ok(Some(callback(exit_code)))
700            }
701            CmdInner::NoOp => {
702                // NoOp commands don't produce messages, just continue running
703                Ok(None)
704            }
705            CmdInner::Quit => {
706                // Quit commands don't produce messages, they're handled specially
707                Ok(None)
708            }
709            CmdInner::Batch(_) | CmdInner::Sequence(_) => {
710                // These are handled specially by the CommandExecutor
711                Ok(None)
712            }
713            CmdInner::Tick { .. } | CmdInner::Every { .. } | CmdInner::Async(_) => {
714                // These are handled specially by the CommandExecutor with async runtime
715                Ok(None)
716            }
717        }
718    }
719
720    /// Execute the command and return its message (for testing only)
721    #[doc(hidden)]
722    pub fn test_execute(self) -> crate::Result<Option<M>> {
723        self.execute()
724    }
725
726    /// Check if this is an exec process command
727    /// Check if this is an exec process command
728    #[must_use]
729    pub fn is_exec_process(&self) -> bool {
730        matches!(self.inner, CmdInner::ExecProcess { .. })
731    }
732
733    /// Check if this is a no-op command
734    #[must_use]
735    pub fn is_noop(&self) -> bool {
736        matches!(self.inner, CmdInner::NoOp)
737    }
738
739    /// Check if this is a quit command
740    #[must_use]
741    pub fn is_quit(&self) -> bool {
742        matches!(self.inner, CmdInner::Quit)
743    }
744
745    /// Extract exec process details if this is an exec process command
746    #[allow(clippy::type_complexity)]
747    /// Internal method
748    #[doc(hidden)]
749    #[must_use]
750    pub fn take_exec_process(self) -> Option<ExecDetails<M>> {
751        match self.inner {
752            CmdInner::ExecProcess {
753                program,
754                args,
755                callback,
756            } => Some((program, args, callback)),
757            _ => None,
758        }
759    }
760
761    /// Transform the messages produced by this command
762    ///
763    /// This is essential for component composition, allowing child components
764    /// to produce commands that can be lifted to parent message types.
765    ///
766    /// # Example
767    /// ```
768    /// # use hojicha_core::Cmd;
769    /// # #[derive(Clone, Debug)]
770    /// # enum ChildMsg { Click }
771    /// # #[derive(Clone, Debug)]
772    /// # enum ParentMsg { Child(ChildMsg) }
773    /// let child_cmd: Cmd<ChildMsg> = Cmd::new(|| Some(ChildMsg::Click));
774    /// let parent_cmd: Cmd<ParentMsg> = child_cmd.map(ParentMsg::Child);
775    /// ```
776    pub fn map<N, F>(self, f: F) -> Cmd<N>
777    where
778        N: Message,
779        F: Fn(M) -> N + Send + Sync + 'static + Clone,
780    {
781        // Wrap in Arc to make cloning cheaper for batch/sequence operations
782        let f = std::sync::Arc::new(f);
783        self.map_with_arc(f)
784    }
785
786    /// Internal method for mapping with Arc-wrapped function
787    fn map_with_arc<N, F>(self, f: std::sync::Arc<F>) -> Cmd<N>
788    where
789        N: Message,
790        F: Fn(M) -> N + Send + Sync + 'static,
791    {
792        match self.inner {
793            CmdInner::NoOp => Cmd {
794                inner: CmdInner::NoOp,
795            },
796            CmdInner::Quit => Cmd {
797                inner: CmdInner::Quit,
798            },
799
800            CmdInner::Function(func) => {
801                let f = f.clone(); // Cheap Arc clone
802                Cmd {
803                    inner: CmdInner::Function(Box::new(move || func().map(move |m| f(m)))),
804                }
805            }
806
807            CmdInner::Fallible(func) => {
808                let f = f.clone(); // Cheap Arc clone
809                Cmd {
810                    inner: CmdInner::Fallible(Box::new(move || {
811                        func().map(|opt| opt.map(move |m| f(m)))
812                    })),
813                }
814            }
815
816            CmdInner::ExecProcess {
817                program,
818                args,
819                callback,
820            } => {
821                let f = f.clone(); // Cheap Arc clone
822                Cmd {
823                    inner: CmdInner::ExecProcess {
824                        program,
825                        args,
826                        callback: Box::new(move |code| f(callback(code))),
827                    },
828                }
829            }
830
831            CmdInner::Batch(cmds) => {
832                let mapped_cmds = cmds
833                    .into_iter()
834                    .map(|cmd| {
835                        cmd.map_with_arc(f.clone()) // Cheap Arc clone
836                    })
837                    .collect();
838                Cmd {
839                    inner: CmdInner::Batch(mapped_cmds),
840                }
841            }
842
843            CmdInner::Sequence(cmds) => {
844                let mapped_cmds = cmds
845                    .into_iter()
846                    .map(|cmd| {
847                        cmd.map_with_arc(f.clone()) // Cheap Arc clone
848                    })
849                    .collect();
850                Cmd {
851                    inner: CmdInner::Sequence(mapped_cmds),
852                }
853            }
854
855            CmdInner::Tick { duration, callback } => {
856                let f = f.clone(); // Cheap Arc clone
857                Cmd {
858                    inner: CmdInner::Tick {
859                        duration,
860                        callback: Box::new(move || f(callback())),
861                    },
862                }
863            }
864
865            CmdInner::Every { duration, callback } => {
866                let f = f.clone(); // Cheap Arc clone
867                Cmd {
868                    inner: CmdInner::Every {
869                        duration,
870                        callback: Box::new(move |instant| f(callback(instant))),
871                    },
872                }
873            }
874
875            CmdInner::Async(_future) => {
876                // Mapping async futures requires complex type gymnastics with Pin
877                // Since async commands are primarily created through spawn() which
878                // already allows specifying the message type, and map() is rarely
879                // needed for async commands in practice, we return a no-op here.
880                // Users should use spawn() with the appropriate message type instead.
881                Cmd {
882                    inner: CmdInner::NoOp,
883                }
884            }
885        }
886    }
887
888    /// Check if this is a batch command
889    /// Internal method
890    #[doc(hidden)]
891    #[must_use]
892    pub fn is_batch(&self) -> bool {
893        matches!(self.inner, CmdInner::Batch(_))
894    }
895
896    /// Take the batch commands (consumes the command)
897    /// Internal method
898    #[doc(hidden)]
899    #[must_use]
900    pub fn take_batch(self) -> Option<Vec<Cmd<M>>> {
901        match self.inner {
902            CmdInner::Batch(cmds) => Some(cmds),
903            _ => None,
904        }
905    }
906
907    /// Check if this is a sequence command
908    /// Internal method
909    #[doc(hidden)]
910    #[must_use]
911    pub fn is_sequence(&self) -> bool {
912        matches!(self.inner, CmdInner::Sequence(_))
913    }
914
915    /// Take the sequence commands (consumes the command)
916    /// Internal method
917    #[doc(hidden)]
918    #[must_use]
919    pub fn take_sequence(self) -> Option<Vec<Cmd<M>>> {
920        match self.inner {
921            CmdInner::Sequence(cmds) => Some(cmds),
922            _ => None,
923        }
924    }
925
926    /// Internal method
927    #[doc(hidden)]
928    #[must_use]
929    pub fn is_tick(&self) -> bool {
930        matches!(self.inner, CmdInner::Tick { .. })
931    }
932
933    /// Internal method
934    #[doc(hidden)]
935    #[must_use]
936    pub fn is_every(&self) -> bool {
937        matches!(self.inner, CmdInner::Every { .. })
938    }
939
940    /// Internal method
941    #[doc(hidden)]
942    #[must_use]
943    pub fn take_tick(self) -> Option<(std::time::Duration, Box<dyn FnOnce() -> M + Send>)> {
944        match self.inner {
945            CmdInner::Tick { duration, callback } => Some((duration, callback)),
946            _ => None,
947        }
948    }
949
950    #[allow(clippy::type_complexity)]
951    /// Internal method
952    #[doc(hidden)]
953    #[must_use]
954    pub fn take_every(
955        self,
956    ) -> Option<(
957        std::time::Duration,
958        Box<dyn FnOnce(std::time::Instant) -> M + Send>,
959    )> {
960        match self.inner {
961            CmdInner::Every { duration, callback } => Some((duration, callback)),
962            _ => None,
963        }
964    }
965
966    /// Internal method
967    #[doc(hidden)]
968    #[must_use]
969    pub fn is_async(&self) -> bool {
970        matches!(self.inner, CmdInner::Async(_))
971    }
972
973    /// Internal method
974    #[doc(hidden)]
975    #[must_use]
976    pub fn take_async(self) -> Option<Box<dyn std::future::Future<Output = Option<M>> + Send>> {
977        match self.inner {
978            CmdInner::Async(future) => Some(future),
979            _ => None,
980        }
981    }
982
983    /// Inspect this command for debugging
984    ///
985    /// This allows you to observe command execution without modifying behavior.
986    ///
987    /// # Example
988    /// ```no_run
989    /// # use hojicha_core::Cmd;
990    /// # #[derive(Clone)]
991    /// # struct Msg;
992    /// let cmd: Cmd<Msg> = Cmd::noop()
993    ///     .inspect(|cmd| println!("Executing command: {:?}", cmd));
994    /// ```
995    #[must_use]
996    pub fn inspect<F>(self, f: F) -> Self
997    where
998        F: FnOnce(&Self),
999    {
1000        f(&self);
1001        self
1002    }
1003
1004    /// Conditionally inspect this command
1005    ///
1006    /// Only runs the inspection function if the condition is true.
1007    ///
1008    /// # Example
1009    /// ```
1010    /// # use hojicha_core::Cmd;
1011    /// # enum Msg { Data(String) }
1012    /// # let debug_mode = true;
1013    /// let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Data("test".into())))
1014    ///     .inspect_if(debug_mode, |cmd| {
1015    ///         eprintln!("Debug: executing {}", cmd.debug_name());
1016    ///     });
1017    /// ```
1018    #[must_use]
1019    pub fn inspect_if<F>(self, condition: bool, f: F) -> Self
1020    where
1021        F: FnOnce(&Self),
1022    {
1023        if condition {
1024            f(&self);
1025        }
1026        self
1027    }
1028
1029    /// Get a string representation of the command type for debugging
1030    ///
1031    /// # Example
1032    /// ```
1033    /// # use hojicha_core::{Cmd, commands};
1034    /// # enum Msg { Tick }
1035    /// # use std::time::Duration;
1036    /// let cmd: Cmd<Msg> = commands::tick(Duration::from_secs(1), || Msg::Tick);
1037    /// assert_eq!(cmd.debug_name(), "Tick");
1038    ///
1039    /// let noop: Cmd<Msg> = Cmd::noop();
1040    /// assert_eq!(noop.debug_name(), "NoOp");
1041    /// ```
1042    #[must_use]
1043    pub fn debug_name(&self) -> &'static str {
1044        match self.inner {
1045            CmdInner::Function(_) => "Function",
1046            CmdInner::Fallible(_) => "Fallible",
1047            CmdInner::ExecProcess { .. } => "ExecProcess",
1048            CmdInner::NoOp => "NoOp",
1049            CmdInner::Quit => "Quit",
1050            CmdInner::Batch(_) => "Batch",
1051            CmdInner::Sequence(_) => "Sequence",
1052            CmdInner::Tick { .. } => "Tick",
1053            CmdInner::Every { .. } => "Every",
1054            CmdInner::Async(_) => "Async",
1055        }
1056    }
1057
1058    // Command Composition Helpers
1059
1060    /// Chain this command with another, running them sequentially
1061    ///
1062    /// The second command will only run after the first completes.
1063    /// This is equivalent to `sequence(vec![self, other])`.
1064    ///
1065    /// # Example
1066    /// ```
1067    /// use hojicha_core::{Cmd, Message};
1068    ///
1069    /// #[derive(Debug, Clone)]
1070    /// enum Msg {
1071    ///     First,
1072    ///     Second,
1073    /// }
1074    ///
1075    /// let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::First))
1076    ///     .then(Cmd::new(|| Some(Msg::Second)));
1077    /// ```
1078    #[must_use]
1079    pub fn then(self, other: Cmd<M>) -> Cmd<M> {
1080        if self.is_noop() {
1081            return other;
1082        }
1083        if other.is_noop() {
1084            return self;
1085        }
1086        Cmd::sequence(vec![self, other])
1087    }
1088
1089    /// Combine this command with another, running them concurrently
1090    ///
1091    /// Both commands will run at the same time.
1092    /// This is equivalent to `batch(vec![self, other])`.
1093    ///
1094    /// # Example
1095    /// ```
1096    /// use hojicha_core::{Cmd, Message};
1097    /// use std::time::Duration;
1098    ///
1099    /// #[derive(Debug, Clone)]
1100    /// enum Msg {
1101    ///     Tick1,
1102    ///     Tick2,
1103    /// }
1104    ///
1105    /// let cmd: Cmd<Msg> = Cmd::tick(Duration::from_secs(1), || Msg::Tick1)
1106    ///     .and(Cmd::tick(Duration::from_secs(2), || Msg::Tick2));
1107    /// ```
1108    #[must_use]
1109    pub fn and(self, other: Cmd<M>) -> Cmd<M> {
1110        if self.is_noop() {
1111            return other;
1112        }
1113        if other.is_noop() {
1114            return self;
1115        }
1116        Cmd::batch(vec![self, other])
1117    }
1118
1119    /// Execute this command only if a condition is met
1120    ///
1121    /// If the condition is false, returns `Cmd::noop()`.
1122    ///
1123    /// # Example
1124    /// ```
1125    /// use hojicha_core::{Cmd, Message};
1126    ///
1127    /// #[derive(Debug, Clone)]
1128    /// enum Msg {
1129    ///     Action,
1130    /// }
1131    ///
1132    /// let enabled = true;
1133    /// let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Action))
1134    ///     .when(enabled);
1135    /// ```
1136    #[must_use]
1137    pub fn when(self, condition: bool) -> Cmd<M> {
1138        if condition {
1139            self
1140        } else {
1141            Cmd::noop()
1142        }
1143    }
1144
1145    /// Execute this command unless a condition is met
1146    ///
1147    /// If the condition is true, returns `Cmd::noop()`.
1148    ///
1149    /// # Example
1150    /// ```
1151    /// use hojicha_core::{Cmd, Message};
1152    ///
1153    /// #[derive(Debug, Clone)]
1154    /// enum Msg {
1155    ///     Action,
1156    /// }
1157    ///
1158    /// let disabled = false;
1159    /// let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Action))
1160    ///     .unless(disabled);
1161    /// ```
1162    #[must_use]
1163    pub fn unless(self, condition: bool) -> Cmd<M> {
1164        self.when(!condition)
1165    }
1166}
1167
1168impl<M: Message> Debug for Cmd<M> {
1169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1170        f.debug_struct("Cmd").finish()
1171    }
1172}
1173
1174#[cfg(test)]
1175mod tests {
1176    use super::*;
1177    use crate::event::Event;
1178
1179    // Test model
1180    #[derive(Clone)]
1181    struct Counter {
1182        value: i32,
1183    }
1184
1185    #[derive(Debug, Clone, PartialEq)]
1186    enum Msg {
1187        Increment,
1188        Decrement,
1189        SetValue(i32),
1190        Noop,
1191    }
1192
1193    impl Model for Counter {
1194        type Message = Msg;
1195
1196        fn init(&mut self) -> Cmd<Self::Message> {
1197            Cmd::new(|| Some(Msg::SetValue(0)))
1198        }
1199
1200        fn update(&mut self, msg: Event<Self::Message>) -> Cmd<Self::Message> {
1201            if let Event::User(msg) = msg {
1202                match msg {
1203                    Msg::Increment => {
1204                        self.value += 1;
1205                        Cmd::new(move || Some(Msg::Noop))
1206                    }
1207                    Msg::Decrement => {
1208                        self.value -= 1;
1209                        Cmd::new(move || Some(Msg::Noop))
1210                    }
1211                    Msg::SetValue(v) => {
1212                        self.value = v;
1213                        Cmd::noop()
1214                    }
1215                    Msg::Noop => Cmd::noop(),
1216                }
1217            } else {
1218                Cmd::noop()
1219            }
1220        }
1221
1222        fn view(&self) -> String {
1223            format!("Count: {}", self.value)
1224        }
1225    }
1226
1227    #[test]
1228    fn test_model_update() {
1229        let mut model = Counter { value: 0 };
1230
1231        model.update(Event::User(Msg::Increment));
1232        assert_eq!(model.value, 1);
1233
1234        model.update(Event::User(Msg::Decrement));
1235        assert_eq!(model.value, 0);
1236    }
1237
1238    #[test]
1239    fn test_cmd_creation() {
1240        let cmd = Cmd::new(|| Some(Msg::Increment));
1241        let msg = cmd.test_execute().unwrap();
1242        assert!(matches!(msg, Some(Msg::Increment)));
1243    }
1244
1245    #[test]
1246    fn test_cmd_noop() {
1247        let cmd: Cmd<Msg> = Cmd::noop();
1248        assert!(cmd.is_noop());
1249    }
1250
1251    #[test]
1252    fn test_cmd_none_deprecated() {
1253        #[allow(deprecated)]
1254        let cmd: Cmd<Msg> = Cmd::noop();
1255        assert!(cmd.is_noop());
1256    }
1257
1258    #[test]
1259    fn test_cmd_fallible_success() {
1260        let cmd = Cmd::fallible(|| Ok(Some(Msg::Increment)));
1261        let result = cmd.test_execute();
1262        assert!(result.is_ok());
1263        assert_eq!(result.unwrap(), Some(Msg::Increment));
1264    }
1265
1266    #[test]
1267    fn test_cmd_fallible_error() {
1268        let cmd: Cmd<Msg> =
1269            Cmd::fallible(|| Err(crate::Error::from(std::io::Error::other("test error"))));
1270        let result = cmd.test_execute();
1271        assert!(result.is_err());
1272    }
1273
1274    #[test]
1275    fn test_cmd_exec_process() {
1276        let cmd = Cmd::exec_process("echo".to_string(), vec!["test".to_string()], |_| {
1277            Msg::Increment
1278        });
1279        assert!(cmd.is_exec_process());
1280
1281        let exec_details = cmd.take_exec_process();
1282        assert!(exec_details.is_some());
1283        let (program, args, _) = exec_details.unwrap();
1284        assert_eq!(program, "echo");
1285        assert_eq!(args, vec!["test"]);
1286    }
1287
1288    #[test]
1289    fn test_cmd_debug() {
1290        let cmd = Cmd::new(|| Some(Msg::Increment));
1291        let debug_str = format!("{cmd:?}");
1292        assert!(debug_str.contains("Cmd"));
1293    }
1294
1295    #[test]
1296    fn test_command_composition_helpers() {
1297        // Test `then` chaining
1298        let cmd1: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
1299        let cmd2: Cmd<Msg> = Cmd::new(|| Some(Msg::Decrement));
1300        let chained = cmd1.then(cmd2);
1301        assert!(chained.is_sequence());
1302
1303        // Test `and` batching
1304        let cmd1: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
1305        let cmd2: Cmd<Msg> = Cmd::new(|| Some(Msg::Decrement));
1306        let batched = cmd1.and(cmd2);
1307        assert!(batched.is_batch());
1308
1309        // Test `when` conditional - true case
1310        let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
1311        let enabled = cmd.when(true);
1312        assert!(!enabled.is_noop());
1313
1314        // Test `when` conditional - false case
1315        let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
1316        let disabled = cmd.when(false);
1317        assert!(disabled.is_noop());
1318
1319        // Test `unless` conditional - false case
1320        let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
1321        let enabled = cmd.unless(false);
1322        assert!(!enabled.is_noop());
1323
1324        // Test `unless` conditional - true case
1325        let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
1326        let disabled = cmd.unless(true);
1327        assert!(disabled.is_noop());
1328
1329        // Test optimization - noop.then(cmd)
1330        let noop: Cmd<Msg> = Cmd::noop();
1331        let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
1332        let result = noop.then(cmd);
1333        assert!(!result.is_sequence()); // Should just return cmd
1334        assert!(!result.is_noop());
1335
1336        // Test optimization - cmd.then(noop)
1337        let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
1338        let noop: Cmd<Msg> = Cmd::noop();
1339        let result = cmd.then(noop);
1340        assert!(!result.is_sequence()); // Should just return cmd
1341        assert!(!result.is_noop());
1342
1343        // Test optimization - noop.and(cmd)
1344        let noop: Cmd<Msg> = Cmd::noop();
1345        let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
1346        let result = noop.and(cmd);
1347        assert!(!result.is_batch()); // Should just return cmd
1348        assert!(!result.is_noop());
1349
1350        // Test optimization - cmd.and(noop)
1351        let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Increment));
1352        let noop: Cmd<Msg> = Cmd::noop();
1353        let result = cmd.and(noop);
1354        assert!(!result.is_batch()); // Should just return cmd
1355        assert!(!result.is_noop());
1356    }
1357
1358    #[test]
1359    fn test_model_init() {
1360        let mut model = Counter { value: 5 };
1361        let cmd = model.init();
1362        assert!(!cmd.is_noop());
1363
1364        let result = cmd.test_execute().unwrap();
1365        assert_eq!(result, Some(Msg::SetValue(0)));
1366    }
1367}