Skip to main content

hojicha_core/
commands.rs

1//! Command utilities for handling side effects
2//!
3//! This module provides functions for creating commands that perform side effects
4//! in your Hojicha application. Commands are the Elm Architecture's way of handling
5//! operations that interact with the outside world.
6//!
7//! ## Core Command Types
8//!
9//! - **Synchronous**: Simple functions that return messages
10//! - **Asynchronous**: Futures that eventually produce messages
11//! - **Timed**: Commands that execute after delays or at intervals
12//! - **Composite**: Batch and sequence commands for complex flows
13//!
14//! ## Common Patterns
15//!
16//! ### No-op Command
17//! ```
18//! # use hojicha_core::commands::noop;
19//! # use hojicha_core::{Model, Cmd, Event};
20//! # struct MyModel;
21//! # impl Model for MyModel {
22//! #     type Message = ();
23//! #     fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
24//!         // Handle event but don't trigger side effects
25//!         noop()
26//! #     }
27//! #     fn view(&self) -> String { String::new() }
28//! # }
29//! ```
30//!
31//! ### Concurrent Commands
32//! ```
33//! # use hojicha_core::commands::{batch, tick};
34//! # use hojicha_core::Cmd;
35//! # use std::time::Duration;
36//! # enum Msg { Tick1, Tick2 }
37//! let cmd: Cmd<Msg> = batch(vec![
38//!     tick(Duration::from_secs(1), || Msg::Tick1),
39//!     tick(Duration::from_secs(2), || Msg::Tick2),
40//! ]);
41//! ```
42//!
43//! ### Sequential Commands
44//! ```
45//! # use hojicha_core::commands::sequence;
46//! # use hojicha_core::Cmd;
47//! # enum Msg { First, Second }
48//! let cmd: Cmd<Msg> = sequence(vec![
49//!     Cmd::new(|| Some(Msg::First)),
50//!     Cmd::new(|| Some(Msg::Second)),
51//! ]);
52//! ```
53
54use crate::core::{Cmd, Message};
55use crate::event::WindowSize;
56use std::process::Command;
57use std::time::Duration;
58
59// Import panic recovery utilities from runtime crate
60// These are used to wrap Model methods for safe execution
61// Note: These are always available, not feature-gated
62
63/// Default maximum batch size
64///
65/// Batches larger than this will trigger a warning in debug mode.
66/// This is a soft limit - the batch will still be created.
67#[cfg(debug_assertions)]
68const DEFAULT_MAX_BATCH_SIZE: usize = 100;
69
70/// Hard maximum batch size
71///
72/// Batches larger than this will be automatically chunked.
73/// This prevents accidental memory exhaustion from massive batches.
74const HARD_MAX_BATCH_SIZE: usize = 1000;
75
76/// Special message types for terminal control
77#[derive(Debug, Clone)]
78pub enum TerminalControlMsg {
79    /// Hide the terminal cursor from view
80    HideCursor,
81    /// Show the terminal cursor
82    ShowCursor,
83    /// Enter the alternate screen buffer (like vim/less use)
84    EnterAltScreen,
85    /// Exit the alternate screen buffer and return to main screen
86    ExitAltScreen,
87    /// Set the terminal window title to the given string
88    SetWindowTitle(String),
89    /// Enable mouse tracking for cell motion (only when button pressed)
90    EnableMouseCellMotion,
91    /// Enable mouse tracking for all motion events (including hover)
92    EnableMouseAllMotion,
93    /// Disable all mouse tracking
94    DisableMouse,
95    /// Clear the entire screen
96    ClearScreen,
97    /// Clear the current line
98    ClearLine,
99}
100
101/// Create a no-op command
102///
103/// This is the most common command return value, indicating that the program
104/// should continue running without performing any side effects.
105///
106/// # Returns
107/// A command that does nothing but allows the program to continue.
108///
109/// # Example
110/// ```
111/// use hojicha_core::{Model, Cmd, Event, commands::noop};
112///
113/// #[derive(Debug)]
114/// enum AppMessage {
115///     DoNothing,
116/// }
117///
118/// struct MyApp;
119///
120/// impl Model for MyApp {
121///     type Message = AppMessage;
122///     
123///     fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
124///         match event {
125///             Event::User(AppMessage::DoNothing) => {
126///                 // Just continue running without side effects
127///                 noop()
128///             }
129///             _ => noop()
130///         }
131///     }
132///     
133///     fn view(&self) -> String {
134///         "App running".to_string()
135///     }
136/// }
137/// ```
138#[must_use]
139pub fn noop<M: Message>() -> Cmd<M> {
140    Cmd::noop()
141}
142
143/// Create a no-op command
144///
145/// # Deprecated
146/// This function is deprecated. Use `commands::noop()` instead for clearer intent.
147/// The name "none" was confusing because it returns `Some(Cmd)` rather than `None`.
148///
149/// # Example
150/// ```
151/// # use hojicha_core::{Cmd, commands};
152/// # enum Msg {}
153/// // Old way (deprecated):
154/// let cmd: Cmd<Msg> = commands::none();
155///
156/// // New way (preferred):
157/// let cmd: Cmd<Msg> = commands::noop();
158/// ```
159#[deprecated(
160    since = "0.2.1",
161    note = "Use commands::noop() instead. The name 'none' was confusing."
162)]
163#[must_use]
164pub fn none<M: Message>() -> Cmd<M> {
165    noop()
166}
167
168/// Batch multiple commands to run concurrently
169///
170/// All commands in the batch will execute simultaneously. Use this when you need
171/// multiple independent operations to happen at the same time.
172///
173/// Note: For performance optimization:
174/// - Empty vectors return `Cmd::noop()`
175/// - Single-element vectors return the element directly
176/// - Use `batch_strict()` if you need guaranteed batch semantics
177///
178/// # Safety
179/// - Batches larger than 100 commands will trigger a debug warning
180/// - Batches larger than 1000 commands will be automatically chunked
181///
182/// # Panics
183/// Panics if the input vector has exactly one element but that element cannot be retrieved.
184/// This should never happen in practice as we check the length beforehand.
185///
186/// # Returns
187/// A command that runs all input commands concurrently.
188///
189/// # Example
190/// ```
191/// use hojicha_core::{Model, Cmd, Event, commands::{batch, tick, spawn}};
192/// use std::time::Duration;
193///
194/// #[derive(Debug, Clone)]
195/// enum AppMessage {
196///     ConfigLoaded(String),
197///     TimerExpired,
198///     DataFetched(String),
199/// }
200///
201/// struct MyApp {
202///     config: String,
203///     data: String,
204/// }
205///
206/// impl Model for MyApp {
207///     type Message = AppMessage;
208///     
209///     fn init(&mut self) -> Cmd<Self::Message> {
210///         // Start multiple operations simultaneously
211///         batch(vec![
212///             // Load config file
213///             Cmd::new(|| {
214///                 let config = std::fs::read_to_string("config.json")
215///                     .unwrap_or_else(|_| "default".to_string());
216///                 Some(AppMessage::ConfigLoaded(config))
217///             }),
218///             // Start a 5-second timer
219///             tick(Duration::from_secs(5), || AppMessage::TimerExpired),
220///             // Fetch data asynchronously
221///             spawn(async {
222///                 tokio::time::sleep(Duration::from_secs(2)).await;
223///                 Some(AppMessage::DataFetched("async data".to_string()))
224///             }),
225///         ])
226///     }
227///     
228///     fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
229///         match event {
230///             Event::User(AppMessage::ConfigLoaded(config)) => {
231///                 self.config = config;
232///                 Cmd::noop()
233///             }
234///             Event::User(AppMessage::TimerExpired) => {
235///                 println!("Timer expired!");
236///                 Cmd::noop()
237///             }
238///             Event::User(AppMessage::DataFetched(data)) => {
239///                 self.data = data;
240///                 Cmd::noop()
241///             }
242///             _ => Cmd::noop()
243///         }
244///     }
245///     
246///     fn view(&self) -> String {
247///         format!("Config: {}, Data: {}", self.config, self.data)
248///     }
249/// }
250/// ```
251#[must_use]
252pub fn batch<M: Message>(cmds: Vec<Cmd<M>>) -> Cmd<M> {
253    match cmds.len() {
254        0 => Cmd::noop(),
255        1 => cmds.into_iter().next().unwrap(),
256        n if n > HARD_MAX_BATCH_SIZE => {
257            // Chunk very large batches to prevent memory issues
258            eprintln!("Warning: Batch of {n} commands exceeds hard limit of {HARD_MAX_BATCH_SIZE}. Chunking into smaller batches.");
259            batch_chunked(cmds, HARD_MAX_BATCH_SIZE)
260        }
261        n => {
262            #[cfg(debug_assertions)]
263            if n > DEFAULT_MAX_BATCH_SIZE {
264                eprintln!(
265                    "Warning: Large batch of {n} commands (recommended max: {DEFAULT_MAX_BATCH_SIZE})"
266                );
267            }
268            let _ = n; // Suppress unused variable warning in release builds
269            Cmd::batch(cmds)
270        }
271    }
272}
273
274/// Sequence commands to run one after another
275///
276/// Note: For performance optimization:
277/// - Empty vectors return `Cmd::noop()`
278/// - Single-element vectors return the element directly
279/// - Use `sequence_strict()` if you need guaranteed sequence semantics
280///
281/// # Panics
282/// Panics if the input vector has exactly one element but that element cannot be retrieved.
283/// This should never happen in practice as we check the length beforehand.
284///
285/// # Example
286/// ```
287/// # use hojicha_core::{Cmd, commands::sequence};
288/// # enum Msg { Save, Notify }
289/// # fn save_to_disk() -> Cmd<Msg> { Cmd::noop() }
290/// # fn show_notification() -> Cmd<Msg> { Cmd::noop() }
291/// // Sequence commands to run one after another
292/// let cmd: Cmd<Msg> = sequence(vec![
293///     save_to_disk(),
294///     show_notification(),
295/// ]);
296/// ```
297#[must_use]
298pub fn sequence<M: Message>(cmds: Vec<Cmd<M>>) -> Cmd<M> {
299    match cmds.len() {
300        0 => Cmd::noop(),
301        1 => cmds.into_iter().next().unwrap(),
302        _ => Cmd::sequence(cmds),
303    }
304}
305
306/// Create a batch command with strict semantics
307///
308/// Unlike `batch()`, this always returns a batch command regardless of the
309/// number of elements. Use this when you need guaranteed batch behavior.
310///
311/// # Example
312/// ```no_run
313/// # use hojicha_core::{Cmd, commands::batch_strict};
314/// # enum Msg { Action }
315/// # fn maybe_cmd() -> Cmd<Msg> { Cmd::noop() }
316/// // Always returns a batch, even with 0 or 1 elements
317/// batch_strict(vec![maybe_cmd()])
318/// # ;
319/// ```
320#[must_use]
321pub fn batch_strict<M: Message>(cmds: Vec<Cmd<M>>) -> Cmd<M> {
322    Cmd::batch(cmds)
323}
324
325/// Create a batch command with a specific size limit
326///
327/// Batches larger than the limit will be automatically chunked.
328///
329/// # Example
330/// ```no_run
331/// # use hojicha_core::{Cmd, commands::batch_with_limit};
332/// # enum Msg { Action }
333/// # let large_vec_of_commands: Vec<Cmd<Msg>> = vec![];
334/// // Create batches with max 50 commands each
335/// batch_with_limit(large_vec_of_commands, 50)
336/// # ;
337/// ```
338#[must_use]
339pub fn batch_with_limit<M: Message>(cmds: Vec<Cmd<M>>, limit: usize) -> Cmd<M> {
340    if cmds.len() <= limit {
341        batch(cmds)
342    } else {
343        batch_chunked(cmds, limit)
344    }
345}
346
347/// Internal helper to chunk large batches
348#[doc(hidden)]
349pub(crate) fn batch_chunked<M: Message>(mut cmds: Vec<Cmd<M>>, chunk_size: usize) -> Cmd<M> {
350    let mut chunks = Vec::new();
351
352    while !cmds.is_empty() {
353        let chunk: Vec<Cmd<M>> = cmds.drain(..chunk_size.min(cmds.len())).collect();
354        chunks.push(Cmd::batch(chunk));
355    }
356
357    // Batch the batches - this creates a two-level batch
358    // This ensures all commands still run concurrently
359    Cmd::batch(chunks)
360}
361
362/// Create a sequence command with strict semantics
363///
364/// Unlike `sequence()`, this always returns a sequence command regardless of the
365/// number of elements. Use this when you need guaranteed sequence behavior.
366///
367/// # Example
368/// ```no_run
369/// # use hojicha_core::{Cmd, commands::sequence_strict};
370/// # enum Msg { Action }
371/// # fn maybe_cmd() -> Cmd<Msg> { Cmd::noop() }
372/// // Always returns a sequence, even with 0 or 1 elements
373/// sequence_strict(vec![maybe_cmd()])
374/// # ;
375/// ```
376#[must_use]
377pub fn sequence_strict<M: Message>(cmds: Vec<Cmd<M>>) -> Cmd<M> {
378    Cmd::sequence(cmds)
379}
380
381/// Create a command that sends a message after a delay
382///
383/// This command waits for the specified duration, then executes the callback
384/// and sends the resulting message to your update function.
385///
386/// # Parameters
387/// - `duration`: How long to wait before executing
388/// - `f`: Function that creates the message to send
389///
390/// # Returns
391/// A command that will send a message after the specified delay.
392///
393/// # Example
394/// ```
395/// use hojicha_core::{Model, Cmd, Event, commands::tick};
396/// use std::time::Duration;
397///
398/// #[derive(Debug, Clone)]
399/// enum AppMessage {
400///     StartTimeout,
401///     TimeoutExpired,
402///     CustomTimeout(String),
403/// }
404///
405/// struct TimeoutApp {
406///     status: String,
407/// }
408///
409/// impl Model for TimeoutApp {
410///     type Message = AppMessage;
411///     
412///     fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
413///         match event {
414///             Event::User(AppMessage::StartTimeout) => {
415///                 self.status = "Timer started...".to_string();
416///                 // Wait 3 seconds, then send TimeoutExpired message
417///                 tick(Duration::from_secs(3), || AppMessage::TimeoutExpired)
418///             }
419///             Event::User(AppMessage::TimeoutExpired) => {
420///                 self.status = "Timeout completed!".to_string();
421///                 // Chain another timer with custom data
422///                 tick(Duration::from_secs(1), || {
423///                     AppMessage::CustomTimeout("Custom data".to_string())
424///                 })
425///             }
426///             Event::User(AppMessage::CustomTimeout(data)) => {
427///                 self.status = format!("Got custom timeout: {}", data);
428///                 Cmd::noop()
429///             }
430///             _ => Cmd::noop()
431///         }
432///     }
433///     
434///     fn view(&self) -> String {
435///         format!("Status: {}", self.status)
436///     }
437/// }
438/// ```
439pub fn tick<M, F>(duration: Duration, f: F) -> Cmd<M>
440where
441    M: Message,
442    F: FnOnce() -> M + Send + 'static,
443{
444    #[allow(deprecated)]
445    Cmd::tick(duration, f)
446}
447
448/// Create a command that ticks at regular intervals
449///
450/// This command creates a recurring timer that sends messages at regular intervals.
451/// Unlike `tick()` which only fires once, `every()` continues indefinitely.
452///
453/// Similar to Bubbletea's Every command, this aligns with system clock
454/// boundaries. For example, `every(Duration::from_secs(1))` will tick
455/// at the start of each second.
456///
457/// # Parameters
458/// - `duration`: Interval between ticks
459/// - `f`: Function that creates messages, receives the current timestamp
460///
461/// # Returns
462/// A command that sends messages at regular intervals until the program exits.
463///
464/// # Example
465/// ```
466/// use hojicha_core::{Model, Cmd, Event, commands::every};
467/// use std::time::{Duration, Instant};
468///
469/// #[derive(Debug, Clone)]
470/// enum ClockMessage {
471///     StartClock,
472///     Tick(Instant),
473///     StopClock,
474/// }
475///
476/// struct ClockApp {
477///     running: bool,
478///     last_tick: Option<Instant>,
479///     tick_count: u32,
480/// }
481///
482/// impl Model for ClockApp {
483///     type Message = ClockMessage;
484///     
485///     fn init(&mut self) -> Cmd<Self::Message> {
486///         // Start ticking every second
487///         every(Duration::from_secs(1), |instant| ClockMessage::Tick(instant))
488///     }
489///     
490///     fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
491///         match event {
492///             Event::User(ClockMessage::StartClock) => {
493///                 self.running = true;
494///                 // Start a new ticker
495///                 every(Duration::from_secs(1), |instant| ClockMessage::Tick(instant))
496///             }
497///             Event::User(ClockMessage::Tick(instant)) => {
498///                 if self.running {
499///                     self.last_tick = Some(instant);
500///                     self.tick_count += 1;
501///                 }
502///                 Cmd::noop()
503///             }
504///             Event::User(ClockMessage::StopClock) => {
505///                 self.running = false;
506///                 Cmd::noop()
507///             }
508///             _ => Cmd::noop()
509///         }
510///     }
511///     
512///     fn view(&self) -> String {
513///         let status = if self.running { "Running" } else { "Stopped" };
514///         format!(
515///             "Clock: {} | Ticks: {} | Last: {:?}",
516///             status, self.tick_count, self.last_tick
517///         )
518///     }
519/// }
520/// ```
521pub fn every<M, F>(duration: Duration, f: F) -> Cmd<M>
522where
523    M: Message,
524    F: FnOnce(std::time::Instant) -> M + Send + 'static,
525{
526    #[allow(deprecated)]
527    Cmd::every(duration, f)
528}
529
530/// Query the terminal for its current size
531///
532/// This command returns a `WindowSize` message with the current terminal dimensions.
533/// Note that resize events are automatically delivered when the terminal size changes,
534/// so you typically won't need to use this command directly.
535///
536/// # Example
537/// ```
538/// # use hojicha_core::{Cmd, commands::window_size, event::WindowSize};
539/// # enum Msg { GotSize(WindowSize) }
540/// // Query the terminal size
541/// let cmd: Cmd<Msg> = window_size(|size| Msg::GotSize(size));
542/// ```
543pub fn window_size<M, F>(f: F) -> Cmd<M>
544where
545    M: Message,
546    F: Fn(WindowSize) -> M + Send + Sync + 'static,
547{
548    Cmd::new(move || {
549        // Query the actual terminal size using crossterm
550        match crossterm::terminal::size() {
551            Ok((width, height)) => Some(f(WindowSize { width, height })),
552            Err(_) => {
553                // Fall back to reasonable defaults if we can't query the terminal
554                Some(f(WindowSize {
555                    width: 80,
556                    height: 24,
557                }))
558            }
559        }
560    })
561}
562
563/// Set the terminal window title
564///
565/// # Example
566/// ```
567/// # use hojicha_core::{Cmd, commands::set_window_title};
568/// # enum Msg {}
569/// // Set the terminal window title
570/// let cmd: Cmd<Msg> = set_window_title("My Awesome App");
571/// ```
572pub fn set_window_title<M: Message>(title: impl Into<String>) -> Cmd<M> {
573    let title = title.into();
574    Cmd::new(move || {
575        use crossterm::{execute, terminal::SetTitle};
576        let _ = execute!(std::io::stdout(), SetTitle(&title));
577        None
578    })
579}
580
581/// Send an interrupt signal (simulates Ctrl+C)
582///
583/// This is useful for graceful shutdown or interrupting long-running operations.
584///
585/// # Example
586/// ```
587/// # use hojicha_core::{Cmd, commands::interrupt};
588/// # enum Msg {}
589/// // Send an interrupt signal (simulates Ctrl+C)
590/// let cmd: Cmd<Msg> = interrupt();
591/// ```
592#[must_use]
593pub fn interrupt<M: Message>() -> Cmd<M> {
594    Cmd::new(|| {
595        #[cfg(unix)]
596        {
597            // Send SIGINT to current process
598            unsafe {
599                libc::kill(libc::getpid(), libc::SIGINT);
600            }
601        }
602        None
603    })
604}
605
606/// Macro to generate simple terminal control commands
607///
608/// This reduces code duplication for commands that just signal
609/// the runtime to perform a terminal operation.
610macro_rules! terminal_cmd {
611    ($(
612        $(#[$attr:meta])*
613        $vis:vis fn $name:ident() -> $doc:literal;
614    )+) => {
615        $(
616            $(#[$attr])*
617            #[doc = $doc]
618            #[doc = ""]
619            #[doc = "This command signals the runtime to perform the operation."]
620            $vis fn $name<M: Message>() -> Cmd<M> {
621                Cmd::new(|| None)
622            }
623        )+
624    };
625}
626
627// Generate all the simple terminal control commands
628terminal_cmd! {
629    /// Hide the terminal cursor
630    pub fn hide_cursor() -> "Hide the terminal cursor from view";
631
632    /// Show the terminal cursor
633    pub fn show_cursor() -> "Show the terminal cursor";
634
635    /// Enter alternate screen buffer
636    pub fn enter_alt_screen() -> "Enter the alternate screen buffer (like vim/less use)";
637
638    /// Exit alternate screen buffer
639    pub fn exit_alt_screen() -> "Exit the alternate screen buffer and return to main screen";
640}
641
642/// Create a custom command from an async function
643///
644/// This allows you to create commands that perform async operations like
645/// HTTP requests, database queries, or other I/O operations.
646///
647/// # Example
648/// ```
649/// # use hojicha_core::{Cmd, commands::custom_async};
650/// # enum Message { DataFetched(String) }
651/// // Create an async command
652/// let cmd: Cmd<Message> = custom_async(|| async {
653///     // Perform async operation
654///     let data = "example data".to_string();
655///     Some(Message::DataFetched(data))
656/// });
657/// ```
658pub fn custom_async<M, F, Fut>(f: F) -> Cmd<M>
659where
660    M: Message,
661    F: FnOnce() -> Fut + Send + 'static,
662    Fut: std::future::Future<Output = Option<M>> + Send + 'static,
663{
664    Cmd::async_cmd(f())
665}
666
667/// Spawn a simple async task
668///
669/// This command spawns an async task on the shared runtime managed by the program.
670/// Unlike `custom_async`, this uses the existing runtime rather than creating a new one.
671///
672/// # Example
673/// ```
674/// # use hojicha_core::{Cmd, commands::spawn};
675/// # use std::time::Duration;
676/// # enum Message { TimerComplete }
677/// // Spawn an async task
678/// let cmd: Cmd<Message> = spawn(async {
679///     tokio::time::sleep(Duration::from_secs(1)).await;
680///     Some(Message::TimerComplete)
681/// });
682/// ```
683pub fn spawn<M, Fut>(fut: Fut) -> Cmd<M>
684where
685    M: Message,
686    Fut: std::future::Future<Output = Option<M>> + Send + 'static,
687{
688    Cmd::async_cmd(fut)
689}
690
691/// Create a custom command from a blocking function
692///
693/// This is a convenience wrapper for creating simple custom commands.
694///
695/// # Example
696/// ```
697/// # use hojicha_core::{Cmd, commands::custom};
698/// # enum Message { ComputationComplete(i32) }
699/// # fn expensive_computation() -> i32 { 42 }
700/// // Create a custom command
701/// let cmd: Cmd<Message> = custom(|| {
702///     // Perform some custom logic
703///     let result = expensive_computation();
704///     Some(Message::ComputationComplete(result))
705/// });
706/// ```
707pub fn custom<M, F>(f: F) -> Cmd<M>
708where
709    M: Message,
710    F: FnOnce() -> Option<M> + Send + 'static,
711{
712    Cmd::new(f)
713}
714
715/// Create a custom fallible command
716///
717/// This allows you to create commands that can fail and handle errors gracefully.
718///
719/// # Example
720/// ```
721/// # use hojicha_core::{Cmd, commands::custom_fallible};
722/// # enum Message { ConfigLoaded(String) }
723/// // Create a fallible command
724/// let cmd: Cmd<Message> = custom_fallible(|| {
725///     // Perform operation that might fail
726///     let data = std::fs::read_to_string("config.json")?;
727///     Ok(Some(Message::ConfigLoaded(data)))
728/// });
729/// ```
730pub fn custom_fallible<M, F>(f: F) -> Cmd<M>
731where
732    M: Message,
733    F: FnOnce() -> crate::Result<Option<M>> + Send + 'static,
734{
735    Cmd::fallible(f)
736}
737
738/// Create a fallible command that converts errors to messages
739///
740/// This allows errors to be handled by the model's update method rather than
741/// just being logged.
742///
743/// # Example
744/// ```
745/// # use hojicha_core::{Cmd, commands::fallible_with_error};
746/// # enum Msg { DataLoaded(String), ErrorOccurred(String) }
747/// // Create a fallible command with error handling
748/// let cmd: Cmd<Msg> = fallible_with_error(
749///     || {
750///         let data = std::fs::read_to_string("data.json")?;
751///         Ok(Some(Msg::DataLoaded(data)))
752///     },
753///     |err| Msg::ErrorOccurred(err.to_string())
754/// );
755/// ```
756pub fn fallible_with_error<M, F, E>(f: F, error_handler: E) -> Cmd<M>
757where
758    M: Message,
759    F: FnOnce() -> crate::Result<Option<M>> + Send + 'static,
760    E: FnOnce(crate::error::Error) -> M + Send + 'static,
761{
762    Cmd::new(move || match f() {
763        Ok(msg) => msg,
764        Err(err) => Some(error_handler(err)),
765    })
766}
767
768/// Execute a command in a subprocess, releasing the terminal while it runs
769///
770/// This is useful for running interactive programs like editors or shells.
771/// The terminal will be restored after the command completes.
772///
773/// # Example
774/// ```
775/// # use hojicha_core::{Cmd, commands::exec};
776/// # enum Msg { EditorClosed(Option<i32>) }
777/// // Execute an external program
778/// let cmd: Cmd<Msg> = exec("vim", vec!["file.txt"], |exit_status| {
779///     Msg::EditorClosed(exit_status)
780/// });
781/// ```
782pub fn exec<M, F>(program: impl Into<String>, args: Vec<impl Into<String>>, callback: F) -> Cmd<M>
783where
784    M: Message,
785    F: Fn(Option<i32>) -> M + Send + 'static,
786{
787    let program = program.into();
788    let args: Vec<String> = args.into_iter().map(Into::into).collect();
789
790    Cmd::exec_process(program, args, callback)
791}
792
793/// Execute a shell command, releasing the terminal while it runs
794///
795/// # Example
796/// ```
797/// # use hojicha_core::{Cmd, commands::exec_command};
798/// # enum Msg { CommandFinished(Option<i32>) }
799/// // Execute a shell command
800/// let cmd: Cmd<Msg> = exec_command("ls -la", |exit_status| {
801///     Msg::CommandFinished(exit_status)
802/// });
803/// ```
804pub fn exec_command<M, F>(command: impl Into<String>, callback: F) -> Cmd<M>
805where
806    M: Message,
807    F: Fn(Option<i32>) -> M + Send + 'static,
808{
809    let command = command.into();
810
811    Cmd::new(move || {
812        let output = if cfg!(target_os = "windows") {
813            Command::new("cmd").args(["/C", &command]).status()
814        } else {
815            Command::new("sh").args(["-c", &command]).status()
816        };
817
818        let exit_code = output.ok().and_then(|status| status.code());
819        Some(callback(exit_code))
820    })
821}
822
823// Generate mouse and screen control commands
824terminal_cmd! {
825    /// Enable mouse cell motion tracking
826    ///
827    /// This enables mouse events only when a button is pressed.
828    pub fn enable_mouse_cell_motion() -> "Enable mouse tracking for cell motion (only when button pressed)";
829
830    /// Enable mouse all motion tracking
831    ///
832    /// This enables mouse movement events regardless of whether a button is pressed,
833    /// allowing for hover interactions.
834    pub fn enable_mouse_all_motion() -> "Enable mouse tracking for all motion events (including hover)";
835
836    /// Disable mouse tracking
837    pub fn disable_mouse() -> "Disable all mouse tracking";
838
839    /// Clear the entire screen
840    pub fn clear_screen() -> "Clear the entire screen";
841
842    /// Clear the current line
843    pub fn clear_line() -> "Clear the current line";
844
845    /// Suspend the program (Ctrl+Z)
846    ///
847    /// This will suspend the program and return control to the shell.
848    /// When the program is resumed, a Resume event will be sent.
849    pub fn suspend() -> "Suspend the program (Ctrl+Z)";
850}
851
852/// Quit the program gracefully
853///
854/// This command signals the program to exit cleanly. When this command is executed,
855/// the event loop will stop and the program will terminate. All cleanup operations
856/// (like restoring terminal state) will be performed automatically.
857///
858/// # Returns
859/// A command that terminates the program when executed.
860///
861/// # Example
862///
863/// ```
864/// use hojicha_core::{Model, Event, Cmd, Key, commands::quit};
865///
866/// #[derive(Debug, Clone)]
867/// enum AppMessage {
868///     RequestQuit,
869///     ConfirmQuit,
870/// }
871///
872/// struct MyApp {
873///     quit_requested: bool,
874/// }
875///
876/// impl Model for MyApp {
877///     type Message = AppMessage;
878///     
879///     fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
880///         match event {
881///             // Handle keyboard input
882///             Event::Key(key) => match key.key {
883///                 Key::Char('q') | Key::Esc => {
884///                     // Quit immediately on 'q' or Escape
885///                     quit()
886///                 }
887///                 Key::Char('c') if key.modifiers.contains(crossterm::event::KeyModifiers::CONTROL) => {
888///                     // Handle Ctrl+C gracefully
889///                     quit()
890///                 }
891///                 _ => Cmd::noop()
892///             },
893///             
894///             // Handle application messages
895///             Event::User(AppMessage::RequestQuit) => {
896///                 if self.quit_requested {
897///                     // Second request - quit immediately
898///                     quit()
899///                 } else {
900///                     // First request - ask for confirmation
901///                     self.quit_requested = true;
902///                     Cmd::noop()
903///                 }
904///             }
905///             Event::User(AppMessage::ConfirmQuit) => {
906///                 // Confirmed - quit the application
907///                 quit()
908///             }
909///             
910///             _ => Cmd::noop()
911///         }
912///     }
913///     
914///     fn view(&self) -> String {
915///         if self.quit_requested {
916///             "Are you sure you want to quit? Press 'q' again to confirm.".to_string()
917///         } else {
918///             "Press 'q' to quit, Ctrl+C to exit immediately".to_string()
919///         }
920///     }
921/// }
922/// ```
923#[must_use]
924pub fn quit<M: Message>() -> Cmd<M> {
925    #[allow(deprecated)]
926    Cmd::quit()
927}
928
929/// Macro to generate crossterm commands that execute terminal sequences
930///
931/// This reduces duplication for commands that use crossterm to send
932/// control sequences to the terminal.
933macro_rules! crossterm_cmd {
934    ($(
935        $(#[$attr:meta])*
936        $vis:vis fn $name:ident($cmd_type:path) -> $doc:literal;
937    )+) => {
938        $(
939            $(#[$attr])*
940            #[doc = $doc]
941            #[doc = ""]
942            #[doc = "This command sends a control sequence to the terminal."]
943            $vis fn $name<M: Message>() -> Cmd<M> {
944                Cmd::new(|| {
945                    use crossterm::execute;
946                    use std::io;
947                    let _ = execute!(io::stdout(), $cmd_type);
948                    None
949                })
950            }
951        )+
952    };
953}
954
955// Generate crossterm-based commands
956crossterm_cmd! {
957    /// Enable bracketed paste mode
958    ///
959    /// When enabled, pasted text will be delivered as a single `Event::Paste(String)`
960    /// instead of individual key events. This prevents pasted text from
961    /// accidentally triggering keyboard shortcuts.
962    pub fn enable_bracketed_paste(crossterm::event::EnableBracketedPaste) -> "Enable bracketed paste mode";
963
964    /// Disable bracketed paste mode
965    pub fn disable_bracketed_paste(crossterm::event::DisableBracketedPaste) -> "Disable bracketed paste mode";
966
967    /// Enable focus change reporting
968    ///
969    /// When enabled, the program will receive `Event::Focus` when the terminal
970    /// gains focus and `Event::Blur` when it loses focus.
971    pub fn enable_focus_change(crossterm::event::EnableFocusChange) -> "Enable focus change reporting";
972
973    /// Disable focus change reporting
974    pub fn disable_focus_change(crossterm::event::DisableFocusChange) -> "Disable focus change reporting";
975}
976
977#[cfg(test)]
978mod tests {
979    use super::*;
980
981    #[derive(Debug, PartialEq)]
982    enum TestMsg {
983        One,
984        Two,
985        Three,
986    }
987
988    #[test]
989    fn test_batch_empty() {
990        let result: Cmd<TestMsg> = batch(vec![]);
991        assert!(!result.is_quit());
992    }
993
994    #[test]
995    fn test_batch_single() {
996        let cmd = Cmd::new(|| Some(TestMsg::One));
997        let result = batch(vec![cmd]);
998        assert!(!result.is_quit());
999    }
1000
1001    #[test]
1002    fn test_batch_multiple() {
1003        let cmds = vec![
1004            Cmd::new(|| Some(TestMsg::One)),
1005            Cmd::new(|| Some(TestMsg::Two)),
1006            Cmd::new(|| Some(TestMsg::Three)),
1007        ];
1008        let result = batch(cmds);
1009        assert!(!result.is_quit());
1010    }
1011
1012    #[test]
1013    fn test_sequence_executes_in_order() {
1014        let cmd = sequence(vec![Cmd::new(|| Some(TestMsg::One))]);
1015        let msg = cmd.execute().unwrap();
1016        assert_eq!(msg, Some(TestMsg::One));
1017    }
1018
1019    #[test]
1020    fn test_tick_command() {
1021        let cmd = tick(Duration::from_millis(10), || TestMsg::One);
1022        // Tick commands are now async and handled by the executor
1023        // They return None from execute() since they need async handling
1024        let msg = cmd.execute().unwrap();
1025        assert_eq!(msg, None);
1026    }
1027
1028    #[test]
1029    fn test_every_command() {
1030        let cmd: Cmd<TestMsg> = every(Duration::from_millis(1), |_| TestMsg::One);
1031        // Every commands are now async and handled by the executor
1032        // They return None from execute() since they need async handling
1033        let result = cmd.test_execute().unwrap();
1034        assert_eq!(result, None);
1035    }
1036
1037    #[test]
1038    fn test_window_size_command() {
1039        // Test that window_size returns a valid WindowSize
1040        #[derive(Debug, PartialEq)]
1041        enum SizeMsg {
1042            Size(WindowSize),
1043        }
1044
1045        let cmd: Cmd<SizeMsg> = window_size(SizeMsg::Size);
1046        let result = cmd.test_execute().unwrap();
1047
1048        // Verify we got a size message
1049        assert!(matches!(result, Some(SizeMsg::Size(_))));
1050
1051        // The actual dimensions will vary based on terminal, but should be positive
1052        if let Some(SizeMsg::Size(size)) = result {
1053            assert!(size.width > 0);
1054            assert!(size.height > 0);
1055        }
1056    }
1057
1058    #[test]
1059    fn test_cursor_commands() {
1060        let hide_cmd: Cmd<TestMsg> = hide_cursor();
1061        let show_cmd: Cmd<TestMsg> = show_cursor();
1062
1063        assert!(hide_cmd.test_execute().is_ok());
1064        assert!(show_cmd.test_execute().is_ok());
1065    }
1066
1067    #[test]
1068    fn test_alt_screen_commands() {
1069        let enter_cmd: Cmd<TestMsg> = enter_alt_screen();
1070        let exit_cmd: Cmd<TestMsg> = exit_alt_screen();
1071
1072        assert!(enter_cmd.test_execute().is_ok());
1073        assert!(exit_cmd.test_execute().is_ok());
1074    }
1075
1076    #[test]
1077    fn test_custom_command() {
1078        let cmd = custom::<TestMsg, _>(|| Some(TestMsg::One));
1079        let result = cmd.execute();
1080        assert!(result.is_ok());
1081        assert_eq!(result.unwrap(), Some(TestMsg::One));
1082    }
1083
1084    #[test]
1085    fn test_custom_fallible_success() {
1086        let cmd = custom_fallible::<TestMsg, _>(|| Ok(Some(TestMsg::Two)));
1087        let result = cmd.execute();
1088        assert!(result.is_ok());
1089        assert_eq!(result.unwrap(), Some(TestMsg::Two));
1090    }
1091
1092    #[test]
1093    fn test_custom_fallible_error() {
1094        use std::io;
1095        let cmd = custom_fallible::<TestMsg, _>(|| {
1096            Err(crate::error::Error::Io(io::Error::other("test error")))
1097        });
1098        let result = cmd.execute();
1099        assert!(result.is_err());
1100    }
1101
1102    #[test]
1103    fn test_custom_async_command() {
1104        let cmd = custom_async::<TestMsg, _, _>(|| async { Some(TestMsg::Three) });
1105        let result = cmd.execute();
1106        assert!(result.is_ok());
1107        // Now async commands return None since they use shared runtime
1108        assert_eq!(result.unwrap(), None);
1109    }
1110
1111    #[test]
1112    fn test_window_title_command() {
1113        let cmd: Cmd<TestMsg> = set_window_title("Test Title");
1114        assert!(cmd.test_execute().is_ok());
1115
1116        let cmd_empty: Cmd<TestMsg> = set_window_title("");
1117        assert!(cmd_empty.test_execute().is_ok());
1118    }
1119
1120    #[test]
1121    fn test_exec_command() {
1122        let cmd: Cmd<TestMsg> = exec("echo", vec!["hello"], |_| TestMsg::One);
1123        assert!(cmd.is_exec_process());
1124
1125        let process_info = cmd.take_exec_process();
1126        assert!(process_info.is_some());
1127    }
1128
1129    #[test]
1130    fn test_mouse_commands() {
1131        let cell_motion: Cmd<TestMsg> = enable_mouse_cell_motion();
1132        let all_motion: Cmd<TestMsg> = enable_mouse_all_motion();
1133        let disable: Cmd<TestMsg> = disable_mouse();
1134
1135        assert!(cell_motion.test_execute().is_ok());
1136        assert!(all_motion.test_execute().is_ok());
1137        assert!(disable.test_execute().is_ok());
1138    }
1139
1140    #[test]
1141    fn test_clear_commands() {
1142        let clear_screen: Cmd<TestMsg> = clear_screen();
1143        let clear_line: Cmd<TestMsg> = clear_line();
1144
1145        assert!(clear_screen.test_execute().is_ok());
1146        assert!(clear_line.test_execute().is_ok());
1147    }
1148
1149    #[test]
1150    fn test_suspend_command() {
1151        let cmd: Cmd<TestMsg> = suspend();
1152        assert!(cmd.test_execute().is_ok());
1153    }
1154
1155    #[test]
1156    fn test_crossterm_commands() {
1157        let enable_paste: Cmd<TestMsg> = enable_bracketed_paste();
1158        let disable_paste: Cmd<TestMsg> = disable_bracketed_paste();
1159        let enable_focus: Cmd<TestMsg> = enable_focus_change();
1160        let disable_focus: Cmd<TestMsg> = disable_focus_change();
1161
1162        assert!(enable_paste.test_execute().is_ok());
1163        assert!(disable_paste.test_execute().is_ok());
1164        assert!(enable_focus.test_execute().is_ok());
1165        assert!(disable_focus.test_execute().is_ok());
1166    }
1167
1168    #[test]
1169    fn test_batch_with_mixed_types() {
1170        let cmds = vec![
1171            Cmd::new(|| Some(TestMsg::One)),
1172            Cmd::new(|| Some(TestMsg::Two)),
1173        ];
1174
1175        let batch_cmd = batch(cmds);
1176        // Batch commands should be recognized as batch type
1177        assert!(batch_cmd.is_batch());
1178    }
1179
1180    #[test]
1181    fn test_sequence_execution_order() {
1182        let cmds = vec![
1183            Cmd::new(|| Some(TestMsg::One)),
1184            Cmd::new(|| Some(TestMsg::Two)),
1185        ];
1186
1187        let seq_cmd = sequence(cmds);
1188        // Sequence commands should be recognized as sequence type
1189        assert!(seq_cmd.is_sequence());
1190    }
1191
1192    // Property-based tests
1193    mod property_tests {
1194        use super::*;
1195        use proptest::prelude::*;
1196
1197        proptest! {
1198            #[test]
1199            fn prop_batch_command_properties(cmd_count in 0usize..50) {
1200                let cmds: Vec<Cmd<TestMsg>> = (0..cmd_count)
1201                    .map(|_| Cmd::new(|| Some(TestMsg::One)))
1202                    .collect();
1203
1204                let batch_cmd = batch(cmds);
1205
1206                // Batch commands should never be quit commands
1207                prop_assert!(!batch_cmd.is_quit());
1208
1209                // Empty batches should be recognized as no-ops
1210                if cmd_count == 0 {
1211                    prop_assert!(batch_cmd.is_noop());
1212                } else if cmd_count >= 2 {
1213                    // Only multi-command batches are considered "batch" type
1214                    prop_assert!(batch_cmd.is_batch());
1215                }
1216                // Single commands may be optimized and not be recognized as batch
1217            }
1218        }
1219
1220        proptest! {
1221            #[test]
1222            fn prop_sequence_command_properties(cmd_count in 1usize..20) {
1223                let cmds: Vec<Cmd<TestMsg>> = (0..cmd_count)
1224                    .map(|i| Cmd::new(move || match i % 3 {
1225                        0 => Some(TestMsg::One),
1226                        1 => Some(TestMsg::Two),
1227                        _ => Some(TestMsg::Three),
1228                    }))
1229                    .collect();
1230
1231                let seq_cmd = sequence(cmds);
1232
1233                // Sequence commands should never be quit commands
1234                prop_assert!(!seq_cmd.is_quit());
1235
1236                // Only multi-command sequences are considered "sequence" type
1237                if cmd_count >= 2 {
1238                    prop_assert!(seq_cmd.is_sequence());
1239                }
1240                // Single commands may be optimized and not be recognized as sequence
1241            }
1242        }
1243
1244        proptest! {
1245            #[test]
1246            fn prop_batch_with_limit_properties(
1247                cmd_count in 2usize..50, // Start at 2 to avoid single-command optimization
1248                limit in 1usize..20
1249            ) {
1250                let cmds: Vec<Cmd<TestMsg>> = (0..cmd_count)
1251                    .map(|_| Cmd::new(|| Some(TestMsg::One)))
1252                    .collect();
1253
1254                let batch_cmd = batch_with_limit(cmds, limit);
1255
1256                // Batch with limit should preserve basic properties
1257                prop_assert!(!batch_cmd.is_quit());
1258
1259                // Should be recognized as batch for multi-command inputs
1260                prop_assert!(batch_cmd.is_batch());
1261            }
1262        }
1263    }
1264
1265    // Additional behavioral tests
1266    mod behavioral_tests {
1267        use super::*;
1268
1269        /// Behavioral test: fallible commands with error handlers
1270        #[test]
1271        fn test_fallible_with_error_behavior() {
1272            use crate::error::Error;
1273
1274            let fallible_cmd = fallible_with_error(
1275                || Err(Error::Custom("test error".to_string().into())),
1276                |_| TestMsg::Three,
1277            );
1278
1279            assert!(!fallible_cmd.is_quit());
1280            assert!(!fallible_cmd.is_noop());
1281
1282            // Should execute and handle the error
1283            let result = fallible_cmd.execute().unwrap();
1284            assert_eq!(result, Some(TestMsg::Three));
1285        }
1286    }
1287}