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}