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}