bubbletea_rs/
command.rs

1//! This module provides functions for creating and managing commands.
2//! Commands are asynchronous operations that can produce messages to update the model.
3
4use crate::event::{
5    next_timer_id, BatchCmdMsg, ClearScreenMsg, DisableBracketedPasteMsg, DisableMouseMsg,
6    DisableReportFocusMsg, EnableBracketedPasteMsg, EnableMouseAllMotionMsg,
7    EnableMouseCellMotionMsg, EnableReportFocusMsg, EnterAltScreenMsg, ExitAltScreenMsg,
8    HideCursorMsg, InterruptMsg, KillMsg, Msg, PrintMsg, PrintfMsg, QuitMsg, RequestWindowSizeMsg,
9    ShowCursorMsg, SuspendMsg,
10};
11use std::future::Future;
12use std::pin::Pin;
13use std::process::Command as StdCommand;
14use std::sync::OnceLock;
15use std::time::Duration;
16use tokio::process::Command as TokioCommand;
17use tokio::time::interval;
18use tokio_util::sync::CancellationToken;
19
20/// A command represents an asynchronous operation that may produce a message.
21///
22/// Commands are typically created by the `init` and `update` methods of your
23/// `Model` and are then executed by the `Program`'s event loop.
24///
25/// The `Cmd` type is a `Pin<Box<dyn Future<Output = Option<Msg>> + Send>>`,
26/// which means it's a boxed, pinned future that returns an `Option<Msg>`.
27/// If the command produces a message, it will be sent back to the `Program`
28/// to be processed by the `update` method.
29pub type Cmd = Pin<Box<dyn Future<Output = Option<Msg>> + Send>>;
30
31/// A batch command that executes multiple commands concurrently.
32///
33/// This struct is used internally by the `batch` function to group multiple
34/// commands together for concurrent execution.
35#[allow(dead_code)]
36pub struct Batch {
37    commands: Vec<Cmd>,
38}
39
40#[allow(dead_code)]
41impl Batch {
42    /// Creates a new `Batch` from a vector of `Cmd`s.
43    pub(crate) fn new(commands: Vec<Cmd>) -> Self {
44        Self { commands }
45    }
46
47    /// Consumes the `Batch` and returns the inner vector of `Cmd`s.
48    pub(crate) fn into_commands(self) -> Vec<Cmd> {
49        self.commands
50    }
51}
52
53/// Global environment variables to be applied to external process commands.
54///
55/// Set by `Program::new()` from `ProgramConfig.environment` and read by
56/// `exec_process` when spawning commands. If unset, no variables are injected.
57pub static COMMAND_ENV: OnceLock<std::collections::HashMap<String, String>> = OnceLock::new();
58
59/// Creates a command that quits the application.
60///
61/// This command sends a `QuitMsg` to the program, which will initiate the
62/// shutdown process.
63///
64/// # Examples
65///
66/// ```
67/// use bubbletea_rs::{command, Model, Msg, KeyMsg};
68/// use crossterm::event::KeyCode;
69///
70/// struct MyModel;
71///
72/// impl Model for MyModel {
73///     fn init() -> (Self, Option<command::Cmd>) {
74///         (Self {}, None)
75///     }
76///
77///     fn update(&mut self, msg: Msg) -> Option<command::Cmd> {
78///         // Quit when 'q' is pressed
79///         if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
80///             if key_msg.key == KeyCode::Char('q') {
81///                 return Some(command::quit());
82///             }
83///         }
84///         None
85///     }
86///     
87///     fn view(&self) -> String {
88///         "Press 'q' to quit".to_string()
89///     }
90/// }
91/// ```
92pub fn quit() -> Cmd {
93    Box::pin(async { Some(Box::new(QuitMsg) as Msg) })
94}
95
96/// Creates a command that kills the application immediately.
97///
98/// This command sends a `KillMsg` to the program, which will cause the event loop
99/// to terminate as soon as possible with `Error::ProgramKilled`.
100///
101/// # Examples
102///
103/// ```
104/// use bubbletea_rs::{command, Model, Msg};
105///
106/// struct MyModel {
107///     has_error: bool,
108/// }
109///
110/// impl Model for MyModel {
111///     fn init() -> (Self, Option<command::Cmd>) {
112///         (Self { has_error: false }, None)
113///     }
114///
115///     fn update(&mut self, msg: Msg) -> Option<command::Cmd> {
116///         // Force kill on critical error
117///         if self.has_error {
118///             return Some(command::kill());
119///         }
120///         None
121///     }
122///     
123///     fn view(&self) -> String {
124///         "Running...".to_string()
125///     }
126/// }
127/// ```
128pub fn kill() -> Cmd {
129    Box::pin(async { Some(Box::new(KillMsg) as Msg) })
130}
131
132/// Creates a command that interrupts the application.
133///
134/// This command sends an `InterruptMsg` to the program, typically used
135/// to signal an external interruption (e.g., Ctrl+C).
136pub fn interrupt() -> Cmd {
137    Box::pin(async { Some(Box::new(InterruptMsg) as Msg) })
138}
139
140/// Creates a command that suspends the application.
141///
142/// This command sends a `SuspendMsg` to the program, which can be used
143/// to temporarily pause the application and release terminal control.
144pub fn suspend() -> Cmd {
145    Box::pin(async { Some(Box::new(SuspendMsg) as Msg) })
146}
147
148/// Creates a command that executes a batch of commands concurrently.
149///
150/// The commands in the batch will be executed in parallel immediately when
151/// this command is processed by the program. This is a non-blocking operation
152/// that spawns each command in its own task, allowing for smooth animations
153/// and responsive user interfaces.
154///
155/// # Arguments
156///
157/// * `cmds` - A vector of commands to execute concurrently
158///
159/// # Returns
160///
161/// A command that immediately dispatches all provided commands for concurrent execution
162///
163/// # Examples
164///
165/// ```
166/// use bubbletea_rs::{command, Model, Msg};
167/// use std::time::Duration;
168///
169/// struct MyModel;
170///
171/// impl Model for MyModel {
172///     fn init() -> (Self, Option<command::Cmd>) {
173///         let model = Self {};
174///         // Execute multiple operations concurrently
175///         let cmd = command::batch(vec![
176///             command::window_size(),  // Get window dimensions
177///             command::tick(Duration::from_secs(1), |_| {
178///                 Box::new("InitialTickMsg") as Msg
179///             }),
180///             command::hide_cursor(),  // Hide the cursor
181///         ]);
182///         (model, Some(cmd))
183///     }
184///     
185///     fn update(&mut self, msg: Msg) -> Option<command::Cmd> {
186///         None
187///     }
188///     
189///     fn view(&self) -> String {
190///         "Loading...".to_string()
191///     }
192/// }
193/// ```
194pub fn batch(cmds: Vec<Cmd>) -> Cmd {
195    Box::pin(async move {
196        // Don't wait for commands - just wrap them for immediate spawning
197        Some(Box::new(BatchCmdMsg(cmds)) as Msg)
198    })
199}
200
201/// Creates a command that executes a sequence of commands sequentially.
202///
203/// The commands in the sequence will be executed one after another in order.
204/// All messages produced by the commands will be collected and returned.
205/// This is useful when you need to perform operations that depend on the
206/// completion of previous operations.
207///
208/// # Arguments
209///
210/// * `cmds` - A vector of commands to execute sequentially
211///
212/// # Returns
213///
214/// A command that executes all provided commands in sequence
215///
216/// # Examples
217///
218/// ```
219/// use bubbletea_rs::{command, Model, Msg};
220///
221/// struct MyModel;
222///
223/// impl Model for MyModel {
224///     fn init() -> (Self, Option<command::Cmd>) {
225///         let model = Self {};
226///         // Execute operations in order
227///         let cmd = command::sequence(vec![
228///             command::enter_alt_screen(),     // First, enter alt screen
229///             command::clear_screen(),         // Then clear it
230///             command::hide_cursor(),          // Finally hide the cursor
231///         ]);
232///         (model, Some(cmd))
233///     }
234///     
235///     fn update(&mut self, msg: Msg) -> Option<command::Cmd> {
236///         None
237///     }
238///     
239///     fn view(&self) -> String {
240///         "Ready".to_string()
241///     }
242/// }
243/// ```
244pub fn sequence(cmds: Vec<Cmd>) -> Cmd {
245    Box::pin(async move {
246        let mut results = Vec::new();
247        for cmd in cmds {
248            if let Some(msg) = cmd.await {
249                results.push(msg);
250            }
251        }
252        if results.is_empty() {
253            None
254        } else {
255            Some(Box::new(crate::event::BatchMsgInternal { messages: results }) as Msg)
256        }
257    })
258}
259
260/// Creates a command that produces a single message after a delay.
261///
262/// This command will send a message produced by the provided closure `f`
263/// after the specified `duration`. Unlike `every()`, this produces only
264/// one message and then completes. It's commonly used for one-shot timers
265/// that can be re-armed in the update method.
266///
267/// Note: Due to tokio's interval implementation, the first tick is consumed
268/// to ensure the message is sent after a full duration, not immediately.
269///
270/// # Arguments
271///
272/// * `duration` - The duration to wait before sending the message
273/// * `f` - A closure that takes a `Duration` and returns a `Msg`
274///
275/// # Returns
276///
277/// A command that will produce a single message after the specified duration
278///
279/// # Examples
280///
281/// ```
282/// use bubbletea_rs::{command, Model, Msg};
283/// use std::time::Duration;
284///
285/// #[derive(Debug)]
286/// struct TickMsg;
287///
288/// struct MyModel {
289///     counter: u32,
290/// }
291///
292/// impl Model for MyModel {
293///     fn init() -> (Self, Option<command::Cmd>) {
294///         let model = Self { counter: 0 };
295///         // Start a timer that fires after 1 second
296///         let cmd = command::tick(Duration::from_secs(1), |_| {
297///             Box::new(TickMsg) as Msg
298///         });
299///         (model, Some(cmd))
300///     }
301///
302///     fn update(&mut self, msg: Msg) -> Option<command::Cmd> {
303///         if msg.downcast_ref::<TickMsg>().is_some() {
304///             self.counter += 1;
305///             // Re-arm the timer for another tick
306///             return Some(command::tick(Duration::from_secs(1), |_| {
307///                 Box::new(TickMsg) as Msg
308///             }));
309///         }
310///         None
311///     }
312///     
313///     fn view(&self) -> String {
314///         format!("Counter: {}", self.counter)
315///     }
316/// }
317/// ```
318pub fn tick<F>(duration: Duration, f: F) -> Cmd
319where
320    F: Fn(Duration) -> Msg + Send + 'static,
321{
322    Box::pin(async move {
323        let mut ticker = interval(duration);
324        // The first tick completes immediately; advance once to move to the start
325        ticker.tick().await; // consume the immediate tick
326                             // Now wait for one full duration before emitting
327        ticker.tick().await;
328        Some(f(duration))
329    })
330}
331
332/// Creates a command that produces messages repeatedly at a regular interval.
333///
334/// This command will continuously send messages produced by the provided closure `f`
335/// after every `duration` until the program exits or the timer is cancelled.
336/// Unlike `tick()`, this creates a persistent timer that keeps firing.
337///
338/// Warning: Be careful not to call `every()` repeatedly for the same timer,
339/// as this will create multiple concurrent timers that can overwhelm the
340/// event loop. Instead, call it once and use `cancel_timer()` if needed.
341///
342/// # Arguments
343///
344/// * `duration` - The duration between messages
345/// * `f` - A closure that takes a `Duration` and returns a `Msg`
346///
347/// # Returns
348///
349/// A command that will produce messages repeatedly at the specified interval
350///
351/// # Examples
352///
353/// ```
354/// use bubbletea_rs::{command, Model, Msg};
355/// use std::time::Duration;
356///
357/// #[derive(Debug)]
358/// struct ClockTickMsg;
359///
360/// struct MyModel {
361///     time_elapsed: Duration,
362/// }
363///
364/// impl Model for MyModel {
365///     fn init() -> (Self, Option<command::Cmd>) {
366///         let model = Self { time_elapsed: Duration::from_secs(0) };
367///         // Start a timer that fires every second
368///         let cmd = command::every(Duration::from_secs(1), |_| {
369///             Box::new(ClockTickMsg) as Msg
370///         });
371///         (model, Some(cmd))
372///     }
373///
374///     fn update(&mut self, msg: Msg) -> Option<command::Cmd> {
375///         if msg.downcast_ref::<ClockTickMsg>().is_some() {
376///             self.time_elapsed += Duration::from_secs(1);
377///             // No need to re-arm - it keeps firing automatically
378///         }
379///         None
380///     }
381///     
382///     fn view(&self) -> String {
383///         format!("Time elapsed: {:?}", self.time_elapsed)
384///     }
385/// }
386/// ```
387pub fn every<F>(duration: Duration, f: F) -> Cmd
388where
389    F: Fn(Duration) -> Msg + Send + 'static,
390{
391    let timer_id = next_timer_id();
392    let cancellation_token = CancellationToken::new();
393
394    Box::pin(async move {
395        Some(Box::new(crate::event::EveryMsgInternal {
396            duration,
397            func: Box::new(f),
398            cancellation_token,
399            timer_id,
400        }) as Msg)
401    })
402}
403
404/// Creates a command that produces messages repeatedly at a regular interval with cancellation support.
405///
406/// This command will continuously send messages produced by the provided closure `f`
407/// after every `duration` until the program exits or the timer is cancelled.
408/// The returned timer ID can be used with `cancel_timer()` to stop the timer.
409///
410/// # Arguments
411///
412/// * `duration` - The duration between messages
413/// * `f` - A closure that takes a `Duration` and returns a `Msg`
414///
415/// # Returns
416///
417/// Returns a tuple containing:
418/// - The command to start the timer
419/// - A timer ID that can be used with `cancel_timer()`
420///
421/// # Examples
422///
423/// ```
424/// use bubbletea_rs::{command, Model, Msg};
425/// use std::time::Duration;
426///
427/// #[derive(Debug)]
428/// struct AnimationFrameMsg;
429///
430/// #[derive(Debug)]
431/// struct StartAnimationMsg(u64); // Contains timer ID
432///
433/// struct MyModel {
434///     animation_timer_id: Option<u64>,
435///     is_animating: bool,
436/// }
437///
438/// impl Model for MyModel {
439///     fn init() -> (Self, Option<command::Cmd>) {
440///         let model = Self {
441///             animation_timer_id: None,
442///             is_animating: false,
443///         };
444///         // Start animation timer and get its ID
445///         let (cmd, timer_id) = command::every_with_id(
446///             Duration::from_millis(16), // ~60 FPS
447///             |_| Box::new(AnimationFrameMsg) as Msg
448///         );
449///         // Send a message with the timer ID so we can store it
450///         let batch = command::batch(vec![
451///             cmd,
452///             Box::pin(async move {
453///                 Some(Box::new(StartAnimationMsg(timer_id)) as Msg)
454///             }),
455///         ]);
456///         (model, Some(batch))
457///     }
458///
459///     fn update(&mut self, msg: Msg) -> Option<command::Cmd> {
460///         if let Some(start_msg) = msg.downcast_ref::<StartAnimationMsg>() {
461///             self.animation_timer_id = Some(start_msg.0);
462///             self.is_animating = true;
463///         }
464///         None
465///     }
466///     
467///     fn view(&self) -> String {
468///         if self.is_animating {
469///             "Animating...".to_string()
470///         } else {
471///             "Stopped".to_string()
472///         }
473///     }
474/// }
475/// ```
476pub fn every_with_id<F>(duration: Duration, f: F) -> (Cmd, u64)
477where
478    F: Fn(Duration) -> Msg + Send + 'static,
479{
480    let timer_id = next_timer_id();
481    let cancellation_token = CancellationToken::new();
482
483    let cmd = Box::pin(async move {
484        Some(Box::new(crate::event::EveryMsgInternal {
485            duration,
486            func: Box::new(f),
487            cancellation_token,
488            timer_id,
489        }) as Msg)
490    });
491
492    (cmd, timer_id)
493}
494
495/// Creates a command that executes an external process.
496///
497/// This command spawns an external process asynchronously and returns a message
498/// produced by the provided closure with the process's output. The process runs
499/// in the background and doesn't block the UI.
500///
501/// # Arguments
502///
503/// * `cmd` - The `std::process::Command` to execute
504/// * `f` - A closure that processes the command output and returns a `Msg`
505///
506/// # Returns
507///
508/// A command that executes the external process
509///
510/// # Examples
511///
512/// ```
513/// use bubbletea_rs::{command, Model, Msg};
514/// use std::process::Command;
515///
516/// #[derive(Debug)]
517/// struct GitStatusMsg(String);
518///
519/// struct MyModel {
520///     git_status: String,
521/// }
522///
523/// impl Model for MyModel {
524///     fn init() -> (Self, Option<command::Cmd>) {
525///         let model = Self { git_status: String::new() };
526///         // Run git status command
527///         let mut cmd = Command::new("git");
528///         cmd.arg("status").arg("--short");
529///         
530///         let exec_cmd = command::exec_process(cmd, |result| {
531///             match result {
532///                 Ok(output) => {
533///                     let status = String::from_utf8_lossy(&output.stdout).to_string();
534///                     Box::new(GitStatusMsg(status)) as Msg
535///                 }
536///                 Err(e) => {
537///                     Box::new(GitStatusMsg(format!("Error: {}", e))) as Msg
538///                 }
539///             }
540///         });
541///         (model, Some(exec_cmd))
542///     }
543///
544///     fn update(&mut self, msg: Msg) -> Option<command::Cmd> {
545///         if let Some(GitStatusMsg(status)) = msg.downcast_ref::<GitStatusMsg>() {
546///             self.git_status = status.clone();
547///         }
548///         None
549///     }
550///     
551///     fn view(&self) -> String {
552///         format!("Git status:\n{}", self.git_status)
553///     }
554/// }
555/// ```
556pub fn exec_process<F>(cmd: StdCommand, f: F) -> Cmd
557where
558    F: Fn(Result<std::process::Output, std::io::Error>) -> Msg + Send + 'static,
559{
560    Box::pin(async move {
561        // Apply configured environment variables, if any
562        let mut cmd = cmd;
563        if let Some(env) = crate::command::COMMAND_ENV.get() {
564            for (k, v) in env.iter() {
565                cmd.env(k, v);
566            }
567        }
568        let output = TokioCommand::from(cmd).output().await;
569        Some(f(output))
570    })
571}
572
573/// Creates a command that enters the alternate screen buffer.
574///
575/// This command sends an `EnterAltScreenMsg` to the program, which will cause
576/// the terminal to switch to the alternate screen buffer. The alternate screen
577/// is typically used by full-screen TUI applications to preserve the user's
578/// terminal scrollback.
579///
580/// # Examples
581///
582/// ```
583/// use bubbletea_rs::{command, Model, Msg};
584///
585/// struct MyModel;
586///
587/// impl Model for MyModel {
588///     fn init() -> (Self, Option<command::Cmd>) {
589///         let model = Self {};
590///         // Enter alternate screen on startup
591///         let cmd = command::batch(vec![
592///             command::enter_alt_screen(),
593///             command::hide_cursor(),
594///         ]);
595///         (model, Some(cmd))
596///     }
597///     
598///     fn update(&mut self, msg: Msg) -> Option<command::Cmd> {
599///         None
600///     }
601///     
602///     fn view(&self) -> String {
603///         "TUI Application".to_string()
604///     }
605/// }
606/// ```
607pub fn enter_alt_screen() -> Cmd {
608    Box::pin(async { Some(Box::new(EnterAltScreenMsg) as Msg) })
609}
610
611/// Creates a command that exits the alternate screen buffer.
612///
613/// This command sends an `ExitAltScreenMsg` to the program, which will cause
614/// the terminal to switch back from the alternate screen buffer.
615pub fn exit_alt_screen() -> Cmd {
616    Box::pin(async { Some(Box::new(ExitAltScreenMsg) as Msg) })
617}
618
619/// Creates a command that enables mouse cell motion reporting.
620///
621/// This command sends an `EnableMouseCellMotionMsg` to the program, which will
622/// enable mouse events for individual cells in the terminal.
623pub fn enable_mouse_cell_motion() -> Cmd {
624    Box::pin(async { Some(Box::new(EnableMouseCellMotionMsg) as Msg) })
625}
626
627/// Creates a command that enables all mouse motion reporting.
628///
629/// This command sends an `EnableMouseAllMotionMsg` to the program, which will
630/// enable all mouse events in the terminal.
631pub fn enable_mouse_all_motion() -> Cmd {
632    Box::pin(async { Some(Box::new(EnableMouseAllMotionMsg) as Msg) })
633}
634
635/// Creates a command that disables mouse reporting.
636///
637/// This command sends a `DisableMouseMsg` to the program, which will disable
638/// all mouse events in the terminal.
639pub fn disable_mouse() -> Cmd {
640    Box::pin(async { Some(Box::new(DisableMouseMsg) as Msg) })
641}
642
643/// Creates a command that enables focus reporting.
644///
645/// This command sends an `EnableReportFocusMsg` to the program, which will
646/// enable focus events in the terminal.
647pub fn enable_report_focus() -> Cmd {
648    Box::pin(async { Some(Box::new(EnableReportFocusMsg) as Msg) })
649}
650
651/// Creates a command that disables focus reporting.
652///
653/// This command sends a `DisableReportFocusMsg` to the program, which will
654/// disable focus events in the terminal.
655pub fn disable_report_focus() -> Cmd {
656    Box::pin(async { Some(Box::new(DisableReportFocusMsg) as Msg) })
657}
658
659/// Creates a command that enables bracketed paste mode.
660///
661/// This command sends an `EnableBracketedPasteMsg` to the program, which will
662/// enable bracketed paste mode in the terminal. This helps distinguish pasted
663/// text from typed text.
664pub fn enable_bracketed_paste() -> Cmd {
665    Box::pin(async { Some(Box::new(EnableBracketedPasteMsg) as Msg) })
666}
667
668/// Creates a command that disables bracketed paste mode.
669///
670/// This command sends a `DisableBracketedPasteMsg` to the program, which will
671/// disable bracketed paste mode in the terminal.
672pub fn disable_bracketed_paste() -> Cmd {
673    Box::pin(async { Some(Box::new(DisableBracketedPasteMsg) as Msg) })
674}
675
676/// Creates a command that shows the terminal cursor.
677///
678/// This command sends a `ShowCursorMsg` to the program, which will make the
679/// terminal cursor visible.
680pub fn show_cursor() -> Cmd {
681    Box::pin(async { Some(Box::new(ShowCursorMsg) as Msg) })
682}
683
684/// Creates a command that hides the terminal cursor.
685///
686/// This command sends a `HideCursorMsg` to the program, which will make the
687/// terminal cursor invisible.
688pub fn hide_cursor() -> Cmd {
689    Box::pin(async { Some(Box::new(HideCursorMsg) as Msg) })
690}
691
692/// Creates a command that clears the terminal screen.
693///
694/// This command sends a `ClearScreenMsg` to the program, which will clear
695/// all content from the terminal screen.
696pub fn clear_screen() -> Cmd {
697    Box::pin(async { Some(Box::new(ClearScreenMsg) as Msg) })
698}
699
700/// Creates a command that requests the current window size.
701///
702/// This command sends a `RequestWindowSizeMsg` to the program. The terminal
703/// will respond with a `WindowSizeMsg` containing its current dimensions.
704/// This is useful for responsive layouts that adapt to terminal size.
705///
706/// # Examples
707///
708/// ```
709/// use bubbletea_rs::{command, Model, Msg, WindowSizeMsg};
710///
711/// struct MyModel {
712///     width: u16,
713///     height: u16,
714/// }
715///
716/// impl Model for MyModel {
717///     fn init() -> (Self, Option<command::Cmd>) {
718///         let model = Self { width: 0, height: 0 };
719///         // Get initial window size
720///         (model, Some(command::window_size()))
721///     }
722///
723///     fn update(&mut self, msg: Msg) -> Option<command::Cmd> {
724///         if let Some(size_msg) = msg.downcast_ref::<WindowSizeMsg>() {
725///             self.width = size_msg.width;
726///             self.height = size_msg.height;
727///         }
728///         None
729///     }
730///     
731///     fn view(&self) -> String {
732///         format!("Window size: {}x{}", self.width, self.height)
733///     }
734/// }
735/// ```
736pub fn window_size() -> Cmd {
737    Box::pin(async { Some(Box::new(RequestWindowSizeMsg) as Msg) })
738}
739
740/// Creates a command that prints a line to the terminal.
741///
742/// This command sends a `PrintMsg` to the program, which will print the
743/// provided string to the terminal. This is useful for debugging or
744/// outputting information that should appear outside the normal UI.
745///
746/// # Arguments
747///
748/// * `s` - The string to print
749///
750/// # Examples
751///
752/// ```
753/// use bubbletea_rs::{command, Model, Msg};
754///
755/// struct MyModel {
756///     debug_mode: bool,
757/// }
758///
759/// impl Model for MyModel {
760///     fn init() -> (Self, Option<command::Cmd>) {
761///         (Self { debug_mode: true }, None)
762///     }
763///
764///     fn update(&mut self, msg: Msg) -> Option<command::Cmd> {
765///         if self.debug_mode {
766///             // Note: In practice, msg doesn't implement Debug by default
767///             // This is just for demonstration
768///             return Some(command::println(
769///                 "Received a message".to_string()
770///             ));
771///         }
772///         None
773///     }
774///     
775///     fn view(&self) -> String {
776///         "Debug mode active".to_string()
777///     }
778/// }
779/// ```
780pub fn println(s: String) -> Cmd {
781    Box::pin(async move { Some(Box::new(PrintMsg(s)) as Msg) })
782}
783
784/// Creates a command that prints formatted text to the terminal.
785///
786/// This command sends a `PrintfMsg` to the program, which will print the
787/// provided formatted string to the terminal.
788pub fn printf(s: String) -> Cmd {
789    Box::pin(async move { Some(Box::new(PrintfMsg(s)) as Msg) })
790}
791
792/// Creates a command that sets the terminal window title.
793///
794/// This command sends a `SetWindowTitleMsg` to the program, which will update
795/// the terminal window's title. Note that not all terminals support this feature.
796///
797/// # Arguments
798///
799/// * `title` - The new window title
800///
801/// # Examples
802///
803/// ```
804/// use bubbletea_rs::{command, Model, Msg};
805///
806/// struct MyModel {
807///     app_name: String,
808///     document_name: Option<String>,
809/// }
810///
811/// impl Model for MyModel {
812///     fn init() -> (Self, Option<command::Cmd>) {
813///         let model = Self {
814///             app_name: "My App".to_string(),
815///             document_name: None,
816///         };
817///         // Set initial window title
818///         let cmd = command::set_window_title(model.app_name.clone());
819///         (model, Some(cmd))
820///     }
821///
822///     fn update(&mut self, msg: Msg) -> Option<command::Cmd> {
823///         // In a real app, you'd check for document open messages
824///         // Update title when document changes
825///         if let Some(doc_name) = &self.document_name {
826///             let title = format!("{} - {}", doc_name, self.app_name);
827///             return Some(command::set_window_title(title));
828///         }
829///         None
830///     }
831///     
832///     fn view(&self) -> String {
833///         match &self.document_name {
834///             Some(doc) => format!("Editing: {}", doc),
835///             None => "No document open".to_string(),
836///         }
837///     }
838/// }
839/// ```
840pub fn set_window_title(title: String) -> Cmd {
841    Box::pin(async move { Some(Box::new(crate::event::SetWindowTitleMsg(title)) as Msg) })
842}
843
844/// Creates a command that cancels a specific timer.
845///
846/// This command sends a `CancelTimerMsg` to the program, which will stop
847/// the timer with the given ID. Use this with timer IDs returned by
848/// `every_with_id()` to stop repeating timers.
849///
850/// # Arguments
851///
852/// * `timer_id` - The ID of the timer to cancel
853///
854/// # Returns
855///
856/// A command that cancels the specified timer
857///
858/// # Examples
859///
860/// ```
861/// use bubbletea_rs::{command, Model, Msg, KeyMsg};
862/// use crossterm::event::KeyCode;
863/// use std::time::Duration;
864///
865/// struct MyModel {
866///     timer_id: Option<u64>,
867/// }
868///
869/// impl Model for MyModel {
870///     fn init() -> (Self, Option<command::Cmd>) {
871///         (Self { timer_id: Some(123) }, None)
872///     }
873///
874///     fn update(&mut self, msg: Msg) -> Option<command::Cmd> {
875///         // Cancel timer when user presses 's' for stop
876///         if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
877///             if key_msg.key == KeyCode::Char('s') {
878///                 if let Some(id) = self.timer_id {
879///                     self.timer_id = None;
880///                     return Some(command::cancel_timer(id));
881///                 }
882///             }
883///         }
884///         None
885///     }
886///     
887///     fn view(&self) -> String {
888///         if self.timer_id.is_some() {
889///             "Timer running. Press 's' to stop.".to_string()
890///         } else {
891///             "Timer stopped.".to_string()
892///         }
893///     }
894/// }
895/// ```
896pub fn cancel_timer(timer_id: u64) -> Cmd {
897    Box::pin(async move { Some(Box::new(crate::event::CancelTimerMsg { timer_id }) as Msg) })
898}
899
900/// Creates a command that cancels all active timers.
901///
902/// This command sends a `CancelAllTimersMsg` to the program, which will stop
903/// all currently running timers.
904pub fn cancel_all_timers() -> Cmd {
905    Box::pin(async move { Some(Box::new(crate::event::CancelAllTimersMsg) as Msg) })
906}