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//! # use ratatui::{Frame, layout::Rect};
20//! struct MyApp {
21//!     counter: i32,
22//! }
23//! 
24//! impl Model for MyApp {
25//!     type Message = MyMessage;
26//!     
27//!     fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
28//!         // Handle events and return commands
29//!         Cmd::none()
30//!     }
31//!     
32//!     fn view(&self, frame: &mut Frame, area: Rect) {
33//!         // Render the UI
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 ratatui::layout::Rect;
53use ratatui::Frame;
54use std::fmt::Debug;
55
56/// Type alias for exec process callback function
57type ExecCallback<M> = Box<dyn Fn(Option<i32>) -> M + Send>;
58
59/// Type alias for exec process details
60type ExecDetails<M> = (String, Vec<String>, ExecCallback<M>);
61
62/// A message that can be sent to update the model.
63///
64/// Messages are typically enums that represent different events
65/// or state changes in your application.
66pub trait Message: Send + 'static {}
67
68// Blanket implementation for all types that meet the bounds
69impl<T: Send + 'static> Message for T {}
70
71/// The core trait for your application's model.
72///
73/// Your model should implement this trait to define how it:
74/// - Initializes (`init`)
75/// - Updates in response to messages (`update`)
76/// - Renders to the screen (`view`)
77pub trait Model: Sized {
78    /// The type of messages this model can receive
79    type Message: Message;
80
81    /// Initialize the model and return a command to run
82    ///
83    /// This method is called once when the program starts. Use it to:
84    /// - Load initial data
85    /// - Start timers
86    /// - Perform initial setup
87    ///
88    /// Returns:
89    /// - `Cmd::none()` - Start the event loop without any initial command
90    /// - Any other command - Execute the command before starting the event loop
91    /// 
92    /// # Example
93    /// ```
94    /// # use hojicha_core::{Model, Cmd, commands};
95    /// # use std::time::Duration;
96    /// # struct MyApp;
97    /// # enum Msg { Tick }
98    /// # impl Model for MyApp {
99    /// #     type Message = Msg;
100    /// fn init(&mut self) -> Cmd<Self::Message> {
101    ///     // Start a timer that ticks every second
102    ///     commands::every(Duration::from_secs(1), |_| Msg::Tick)
103    /// }
104    /// #     fn update(&mut self, _: hojicha_core::Event<Self::Message>) -> Cmd<Self::Message> { Cmd::none() }
105    /// #     fn view(&self, _: &mut ratatui::Frame, _: ratatui::layout::Rect) {}
106    /// # }
107    /// ```
108    fn init(&mut self) -> Cmd<Self::Message> {
109        Cmd::none()
110    }
111
112    /// Update the model based on a message and return a command
113    ///
114    /// This is the heart of your application logic. Handle events here and
115    /// update your model's state accordingly.
116    ///
117    /// Returns:
118    /// - `Cmd::none()` - Continue running without executing any command
119    /// - `commands::quit()` - Exit the program
120    /// - Any other command - Execute the command and continue
121    /// 
122    /// # Example
123    /// ```
124    /// # use hojicha_core::{Model, Cmd, Event, Key, commands};
125    /// # struct Counter { value: i32 }
126    /// # enum Msg { Increment, Decrement }
127    /// # impl Model for Counter {
128    /// #     type Message = Msg;
129    /// fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
130    ///     match event {
131    ///         Event::Key(key) if key.key == Key::Char('q') => {
132    ///             commands::quit()
133    ///         }
134    ///         Event::User(Msg::Increment) => {
135    ///             self.value += 1;
136    ///             Cmd::none()
137    ///         }
138    ///         Event::User(Msg::Decrement) => {
139    ///             self.value -= 1;
140    ///             Cmd::none()
141    ///         }
142    ///         _ => Cmd::none()
143    ///     }
144    /// }
145    /// #     fn view(&self, _: &mut ratatui::Frame, _: ratatui::layout::Rect) {}
146    /// # }
147    /// ```
148    fn update(&mut self, msg: Event<Self::Message>) -> Cmd<Self::Message>;
149
150    /// Render the model to the screen
151    /// 
152    /// This method is called after each update to render your UI.
153    /// Use ratatui widgets to draw your interface.
154    /// 
155    /// # Example
156    /// ```
157    /// # use hojicha_core::Model;
158    /// # use ratatui::{Frame, layout::Rect, widgets::{Block, Borders, Paragraph}};
159    /// # struct MyApp { message: String }
160    /// # impl Model for MyApp {
161    /// #     type Message = ();
162    /// #     fn update(&mut self, _: hojicha_core::Event<Self::Message>) -> hojicha_core::Cmd<Self::Message> { hojicha_core::Cmd::none() }
163    /// fn view(&self, frame: &mut Frame, area: Rect) {
164    ///     let widget = Paragraph::new(self.message.as_str())
165    ///         .block(Block::default()
166    ///             .title("My App")
167    ///             .borders(Borders::ALL));
168    ///     frame.render_widget(widget, area);
169    /// }
170    /// # }
171    /// ```
172    fn view(&self, frame: &mut Frame, area: Rect);
173}
174
175/// A command is an asynchronous operation that produces a message.
176///
177/// Commands are used for side effects like:
178/// - HTTP requests
179/// - File I/O
180/// - Timers
181/// - Any async operation
182pub struct Cmd<M: Message> {
183    inner: CmdInner<M>,
184}
185
186pub(crate) enum CmdInner<M: Message> {
187    /// No operation - continue running without doing anything
188    NoOp,
189    /// A simple function command
190    Function(Box<dyn FnOnce() -> Option<M> + Send>),
191    /// A function command that can return errors
192    Fallible(Box<dyn FnOnce() -> crate::Result<Option<M>> + Send>),
193    /// An external process command
194    ExecProcess {
195        program: String,
196        args: Vec<String>,
197        callback: ExecCallback<M>,
198    },
199    /// Quit the program
200    Quit,
201    /// Execute multiple commands concurrently
202    Batch(Vec<Cmd<M>>),
203    /// Execute multiple commands sequentially
204    Sequence(Vec<Cmd<M>>),
205    /// Execute after a delay
206    Tick {
207        duration: std::time::Duration,
208        callback: Box<dyn FnOnce() -> M + Send>,
209    },
210    /// Execute repeatedly at intervals
211    Every {
212        duration: std::time::Duration,
213        callback: Box<dyn FnOnce(std::time::Instant) -> M + Send>,
214    },
215    /// Execute an async future
216    Async(Box<dyn std::future::Future<Output = Option<M>> + Send>),
217}
218
219impl<M: Message> Cmd<M> {
220    /// Create a new command from a function
221    /// 
222    /// Note: If the function returns `None`, consider using `Cmd::none()` instead
223    /// for better performance and clearer intent.
224    /// 
225    /// # Example
226    /// ```
227    /// # use hojicha_core::Cmd;
228    /// # enum Msg { DataLoaded(String) }
229    /// let cmd: Cmd<Msg> = Cmd::new(|| {
230    ///     // Perform a side effect
231    ///     let data = std::fs::read_to_string("config.json").ok()?;
232    ///     Some(Msg::DataLoaded(data))
233    /// });
234    /// ```
235    pub fn new<F>(f: F) -> Self
236    where
237        F: FnOnce() -> Option<M> + Send + 'static,
238    {
239        Cmd {
240            inner: CmdInner::Function(Box::new(f)),
241        }
242    }
243
244    /// Create a new fallible command that can return errors
245    /// 
246    /// Use this when your command might fail and you want to handle errors gracefully.
247    /// 
248    /// # Example
249    /// ```
250    /// # use hojicha_core::{Cmd, Result};
251    /// # enum Msg { ConfigLoaded(String) }
252    /// let cmd: Cmd<Msg> = Cmd::fallible(|| {
253    ///     let data = std::fs::read_to_string("config.json")?;
254    ///     Ok(Some(Msg::ConfigLoaded(data)))
255    /// });
256    /// ```
257    pub fn fallible<F>(f: F) -> Self
258    where
259        F: FnOnce() -> crate::Result<Option<M>> + Send + 'static,
260    {
261        Cmd {
262            inner: CmdInner::Fallible(Box::new(f)),
263        }
264    }
265
266    /// Returns a no-op command that continues running without doing anything
267    ///
268    /// This is the idiomatic way to return "no command" from update().
269    /// The program will continue running without executing any side effects.
270    /// 
271    /// # Example
272    /// ```
273    /// # use hojicha_core::{Model, Cmd, Event};
274    /// # use ratatui::{Frame, layout::Rect};
275    /// # struct MyApp;
276    /// # impl Model for MyApp {
277    /// #     type Message = ();
278    /// fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
279    ///     match event {
280    ///         Event::Tick => {
281    ///             // Update internal state but don't trigger side effects
282    ///             Cmd::none()
283    ///         }
284    ///         _ => Cmd::none()
285    ///     }
286    /// }
287    /// #     fn view(&self, _: &mut Frame, _: Rect) {}
288    /// # }
289    /// ```
290    pub fn none() -> Self {
291        Cmd {
292            inner: CmdInner::NoOp,
293        }
294    }
295
296    /// Internal method
297    #[doc(hidden)]
298    pub fn exec_process<F>(program: String, args: Vec<String>, callback: F) -> Self
299    where
300        F: Fn(Option<i32>) -> M + Send + 'static,
301    {
302        Cmd {
303            inner: CmdInner::ExecProcess {
304                program,
305                args,
306                callback: Box::new(callback),
307            },
308        }
309    }
310
311    /// Create a batch command that executes commands concurrently
312    /// Internal method
313    #[doc(hidden)]
314    pub fn batch(cmds: Vec<Cmd<M>>) -> Self {
315        Cmd {
316            inner: CmdInner::Batch(cmds),
317        }
318    }
319
320    /// Create a sequence command that executes commands in order
321    /// Internal method
322    #[doc(hidden)]
323    pub fn sequence(cmds: Vec<Cmd<M>>) -> Self {
324        Cmd {
325            inner: CmdInner::Sequence(cmds),
326        }
327    }
328
329    /// Create a quit command
330    /// Internal method
331    #[doc(hidden)]
332    pub fn quit() -> Self {
333        Cmd {
334            inner: CmdInner::Quit,
335        }
336    }
337
338    /// Create a tick command
339    /// Internal method
340    #[doc(hidden)]
341    pub fn tick<F>(duration: std::time::Duration, callback: F) -> Self
342    where
343        F: FnOnce() -> M + Send + 'static,
344    {
345        Cmd {
346            inner: CmdInner::Tick {
347                duration,
348                callback: Box::new(callback),
349            },
350        }
351    }
352
353    /// Create an every command
354    /// Internal method
355    #[doc(hidden)]
356    pub fn every<F>(duration: std::time::Duration, callback: F) -> Self
357    where
358        F: FnOnce(std::time::Instant) -> M + Send + 'static,
359    {
360        Cmd {
361            inner: CmdInner::Every {
362                duration,
363                callback: Box::new(callback),
364            },
365        }
366    }
367
368    /// Create an async command
369    /// Internal method
370    #[doc(hidden)]
371    pub fn async_cmd<Fut>(future: Fut) -> Self
372    where
373        Fut: std::future::Future<Output = Option<M>> + Send + 'static,
374    {
375        Cmd {
376            inner: CmdInner::Async(Box::new(future)),
377        }
378    }
379
380    /// Execute the command and return its message
381    /// Internal method
382    #[doc(hidden)]
383    pub fn execute(self) -> crate::Result<Option<M>> {
384        match self.inner {
385            CmdInner::Function(func) => Ok(func()),
386            CmdInner::Fallible(func) => func(),
387            CmdInner::ExecProcess {
388                program,
389                args,
390                callback,
391            } => {
392                // In the real implementation, this would be handled by the runtime
393                // For now, we'll just run it directly
394                use std::process::Command;
395                let output = Command::new(&program).args(&args).status();
396                let exit_code = output.ok().and_then(|status| status.code());
397                Ok(Some(callback(exit_code)))
398            }
399            CmdInner::NoOp => {
400                // NoOp commands don't produce messages, just continue running
401                Ok(None)
402            }
403            CmdInner::Quit => {
404                // Quit commands don't produce messages, they're handled specially
405                Ok(None)
406            }
407            CmdInner::Batch(_) | CmdInner::Sequence(_) => {
408                // These are handled specially by the CommandExecutor
409                Ok(None)
410            }
411            CmdInner::Tick { .. } | CmdInner::Every { .. } | CmdInner::Async(_) => {
412                // These are handled specially by the CommandExecutor with async runtime
413                Ok(None)
414            }
415        }
416    }
417
418    /// Execute the command and return its message (for testing only)
419    #[doc(hidden)]
420    pub fn test_execute(self) -> crate::Result<Option<M>> {
421        self.execute()
422    }
423
424    /// Check if this is an exec process command
425    /// Check if this is an exec process command
426    pub fn is_exec_process(&self) -> bool {
427        matches!(self.inner, CmdInner::ExecProcess { .. })
428    }
429
430    /// Check if this is a no-op command
431    pub fn is_noop(&self) -> bool {
432        matches!(self.inner, CmdInner::NoOp)
433    }
434
435    /// Check if this is a quit command
436    pub fn is_quit(&self) -> bool {
437        matches!(self.inner, CmdInner::Quit)
438    }
439
440    /// Extract exec process details if this is an exec process command
441    #[allow(clippy::type_complexity)]
442    /// Internal method
443    #[doc(hidden)]
444    pub fn take_exec_process(self) -> Option<ExecDetails<M>> {
445        match self.inner {
446            CmdInner::ExecProcess {
447                program,
448                args,
449                callback,
450            } => Some((program, args, callback)),
451            _ => None,
452        }
453    }
454
455    /// Check if this is a batch command
456    /// Internal method
457    #[doc(hidden)]
458    pub fn is_batch(&self) -> bool {
459        matches!(self.inner, CmdInner::Batch(_))
460    }
461
462    /// Take the batch commands (consumes the command)
463    /// Internal method
464    #[doc(hidden)]
465    pub fn take_batch(self) -> Option<Vec<Cmd<M>>> {
466        match self.inner {
467            CmdInner::Batch(cmds) => Some(cmds),
468            _ => None,
469        }
470    }
471
472    /// Check if this is a sequence command
473    /// Internal method
474    #[doc(hidden)]
475    pub fn is_sequence(&self) -> bool {
476        matches!(self.inner, CmdInner::Sequence(_))
477    }
478
479    /// Take the sequence commands (consumes the command)
480    /// Internal method
481    #[doc(hidden)]
482    pub fn take_sequence(self) -> Option<Vec<Cmd<M>>> {
483        match self.inner {
484            CmdInner::Sequence(cmds) => Some(cmds),
485            _ => None,
486        }
487    }
488
489    /// Internal method
490    #[doc(hidden)]
491    pub fn is_tick(&self) -> bool {
492        matches!(self.inner, CmdInner::Tick { .. })
493    }
494
495    /// Internal method
496    #[doc(hidden)]
497    pub fn is_every(&self) -> bool {
498        matches!(self.inner, CmdInner::Every { .. })
499    }
500
501    /// Internal method
502    #[doc(hidden)]
503    pub fn take_tick(self) -> Option<(std::time::Duration, Box<dyn FnOnce() -> M + Send>)> {
504        match self.inner {
505            CmdInner::Tick { duration, callback } => Some((duration, callback)),
506            _ => None,
507        }
508    }
509
510    #[allow(clippy::type_complexity)]
511    /// Internal method
512    #[doc(hidden)]
513    pub fn take_every(
514        self,
515    ) -> Option<(
516        std::time::Duration,
517        Box<dyn FnOnce(std::time::Instant) -> M + Send>,
518    )> {
519        match self.inner {
520            CmdInner::Every { duration, callback } => Some((duration, callback)),
521            _ => None,
522        }
523    }
524
525    /// Internal method
526    #[doc(hidden)]
527    pub fn is_async(&self) -> bool {
528        matches!(self.inner, CmdInner::Async(_))
529    }
530
531    /// Internal method
532    #[doc(hidden)]
533    pub fn take_async(self) -> Option<Box<dyn std::future::Future<Output = Option<M>> + Send>> {
534        match self.inner {
535            CmdInner::Async(future) => Some(future),
536            _ => None,
537        }
538    }
539
540    /// Inspect this command for debugging
541    ///
542    /// This allows you to observe command execution without modifying behavior.
543    ///
544    /// # Example
545    /// ```no_run
546    /// # use hojicha_core::Cmd;
547    /// # #[derive(Clone)]
548    /// # struct Msg;
549    /// let cmd: Cmd<Msg> = Cmd::none()
550    ///     .inspect(|cmd| println!("Executing command: {:?}", cmd));
551    /// ```
552    pub fn inspect<F>(self, f: F) -> Self
553    where
554        F: FnOnce(&Self),
555    {
556        f(&self);
557        self
558    }
559
560    /// Conditionally inspect this command
561    ///
562    /// Only runs the inspection function if the condition is true.
563    /// 
564    /// # Example
565    /// ```
566    /// # use hojicha_core::Cmd;
567    /// # enum Msg { Data(String) }
568    /// # let debug_mode = true;
569    /// let cmd: Cmd<Msg> = Cmd::new(|| Some(Msg::Data("test".into())))
570    ///     .inspect_if(debug_mode, |cmd| {
571    ///         eprintln!("Debug: executing {}", cmd.debug_name());
572    ///     });
573    /// ```
574    pub fn inspect_if<F>(self, condition: bool, f: F) -> Self
575    where
576        F: FnOnce(&Self),
577    {
578        if condition {
579            f(&self);
580        }
581        self
582    }
583
584    /// Get a string representation of the command type for debugging
585    /// 
586    /// # Example
587    /// ```
588    /// # use hojicha_core::{Cmd, commands};
589    /// # enum Msg { Tick }
590    /// # use std::time::Duration;
591    /// let cmd: Cmd<Msg> = commands::tick(Duration::from_secs(1), || Msg::Tick);
592    /// assert_eq!(cmd.debug_name(), "Tick");
593    /// 
594    /// let noop: Cmd<Msg> = Cmd::none();
595    /// assert_eq!(noop.debug_name(), "NoOp");
596    /// ```
597    pub fn debug_name(&self) -> &'static str {
598        match self.inner {
599            CmdInner::Function(_) => "Function",
600            CmdInner::Fallible(_) => "Fallible",
601            CmdInner::ExecProcess { .. } => "ExecProcess",
602            CmdInner::NoOp => "NoOp",
603            CmdInner::Quit => "Quit",
604            CmdInner::Batch(_) => "Batch",
605            CmdInner::Sequence(_) => "Sequence",
606            CmdInner::Tick { .. } => "Tick",
607            CmdInner::Every { .. } => "Every",
608            CmdInner::Async(_) => "Async",
609        }
610    }
611}
612
613impl<M: Message> Debug for Cmd<M> {
614    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
615        f.debug_struct("Cmd").finish()
616    }
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622    use crate::event::Event;
623    use ratatui::{layout::Rect, Frame};
624
625    // Test model
626    #[derive(Clone)]
627    struct Counter {
628        value: i32,
629    }
630
631    #[derive(Debug, Clone, PartialEq)]
632    enum Msg {
633        Increment,
634        Decrement,
635        SetValue(i32),
636        Noop,
637    }
638
639    impl Model for Counter {
640        type Message = Msg;
641
642        fn init(&mut self) -> Cmd<Self::Message> {
643            Cmd::new(|| Some(Msg::SetValue(0)))
644        }
645
646        fn update(&mut self, msg: Event<Self::Message>) -> Cmd<Self::Message> {
647            if let Event::User(msg) = msg {
648                match msg {
649                    Msg::Increment => {
650                        self.value += 1;
651                        Cmd::new(move || Some(Msg::Noop))
652                    }
653                    Msg::Decrement => {
654                        self.value -= 1;
655                        Cmd::new(move || Some(Msg::Noop))
656                    }
657                    Msg::SetValue(v) => {
658                        self.value = v;
659                        Cmd::none()
660                    }
661                    Msg::Noop => Cmd::none(),
662                }
663            } else {
664                Cmd::none()
665            }
666        }
667
668        fn view(&self, frame: &mut Frame, area: Rect) {
669            frame.render_widget(
670                ratatui::widgets::Paragraph::new(format!("Count: {}", self.value)),
671                area,
672            );
673        }
674    }
675
676    #[test]
677    fn test_model_update() {
678        let mut model = Counter { value: 0 };
679
680        model.update(Event::User(Msg::Increment));
681        assert_eq!(model.value, 1);
682
683        model.update(Event::User(Msg::Decrement));
684        assert_eq!(model.value, 0);
685    }
686
687    #[test]
688    fn test_cmd_creation() {
689        let cmd = Cmd::new(|| Some(Msg::Increment));
690        let msg = cmd.test_execute().unwrap();
691        assert!(matches!(msg, Some(Msg::Increment)));
692    }
693
694    #[test]
695    fn test_cmd_none() {
696        let cmd: Cmd<Msg> = Cmd::none();
697        assert!(cmd.is_noop());
698    }
699
700    #[test]
701    fn test_cmd_fallible_success() {
702        let cmd = Cmd::fallible(|| Ok(Some(Msg::Increment)));
703        let result = cmd.test_execute();
704        assert!(result.is_ok());
705        assert_eq!(result.unwrap(), Some(Msg::Increment));
706    }
707
708    #[test]
709    fn test_cmd_fallible_error() {
710        let cmd: Cmd<Msg> =
711            Cmd::fallible(|| Err(crate::Error::from(std::io::Error::other("test error"))));
712        let result = cmd.test_execute();
713        assert!(result.is_err());
714    }
715
716    #[test]
717    fn test_cmd_exec_process() {
718        let cmd = Cmd::exec_process("echo".to_string(), vec!["test".to_string()], |_| {
719            Msg::Increment
720        });
721        assert!(cmd.is_exec_process());
722
723        let exec_details = cmd.take_exec_process();
724        assert!(exec_details.is_some());
725        let (program, args, _) = exec_details.unwrap();
726        assert_eq!(program, "echo");
727        assert_eq!(args, vec!["test"]);
728    }
729
730    #[test]
731    fn test_cmd_debug() {
732        let cmd = Cmd::new(|| Some(Msg::Increment));
733        let debug_str = format!("{cmd:?}");
734        assert!(debug_str.contains("Cmd"));
735    }
736
737    #[test]
738    fn test_model_init() {
739        let mut model = Counter { value: 5 };
740        let cmd = model.init();
741        assert!(!cmd.is_noop());
742
743        let result = cmd.test_execute().unwrap();
744        assert_eq!(result, Some(Msg::SetValue(0)));
745    }
746}