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}