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