Skip to main content

rusty_bubbletea/
commands.rs

1//! Cleanroom Rust port of upstream Go source file: `commands.go`
2//! Upstream Target Tag / Version: `v2.0.8`
3//!
4//! <public-docs>
5//! # Commands
6//!
7//! Built-in command functions (`batch`, `sequence`, `every`, `tick`, `request_window_size`).
8//! </public-docs>
9
10use crate::model::{Cmd, Msg};
11use std::fmt;
12use std::time::{Duration, SystemTime};
13
14/// BatchMsg is a message used to perform a bunch of commands concurrently with
15/// no ordering guarantees. You can send a BatchMsg with `batch`.
16pub struct BatchMsg(pub Vec<Cmd>);
17
18impl fmt::Debug for BatchMsg {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        write!(f, "BatchMsg({} commands)", self.0.len())
21    }
22}
23
24/// SequenceMsg is used internally to run the given commands in order.
25pub struct SequenceMsg(pub Vec<Cmd>);
26
27impl fmt::Debug for SequenceMsg {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        write!(f, "SequenceMsg({} commands)", self.0.len())
30    }
31}
32
33/// QuitMsg signals the program to exit.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct QuitMsg;
36
37/// Quit is a command that signals the program to exit.
38pub fn quit() -> Cmd {
39    Some(Box::new(|| Some(Box::new(QuitMsg))))
40}
41
42/// SuspendMsg signals the program should suspend.
43/// This usually happens when ctrl+z is pressed on common programs, but since
44/// bubbletea puts the terminal in raw mode, we need to handle it in a
45/// per-program basis.
46///
47/// You can send this message with `suspend()`.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct SuspendMsg;
50
51/// Suspend is a special command that tells the Bubble Tea program to suspend.
52pub fn suspend() -> Cmd {
53    Some(Box::new(|| Some(Box::new(SuspendMsg))))
54}
55
56/// ResumeMsg can be listened to do something once a program is resumed back
57/// from a suspend state.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct ResumeMsg;
60
61/// InterruptMsg signals the program should interrupt.
62/// This usually happens when ctrl+c is pressed on common programs, but since
63/// bubbletea puts the terminal in raw mode, we need to handle it in a
64/// per-program basis.
65///
66/// You can send this message with `interrupt()`.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct InterruptMsg;
69
70/// Interrupt is a special command that tells the Bubble Tea program to
71/// interrupt.
72pub fn interrupt() -> Cmd {
73    Some(Box::new(|| Some(Box::new(InterruptMsg))))
74}
75
76/// Batch performs a bunch of commands concurrently with no ordering guarantees
77/// about the results. Use `batch` to return several commands.
78pub fn batch(cmds: Vec<Cmd>) -> Cmd {
79    let mut valid_cmds = Vec::new();
80    for cmd in cmds {
81        if cmd.is_some() {
82            valid_cmds.push(cmd);
83        }
84    }
85    match valid_cmds.len() {
86        0 => None,
87        1 => valid_cmds.into_iter().next().unwrap(),
88        _ => Some(Box::new(move || Some(Box::new(BatchMsg(valid_cmds))))),
89    }
90}
91
92/// Sequence runs the given commands one at a time, in order. Contrast this with
93/// `batch`, which runs commands concurrently.
94pub fn sequence(cmds: Vec<Cmd>) -> Cmd {
95    let mut valid_cmds = Vec::new();
96    for cmd in cmds {
97        if cmd.is_some() {
98            valid_cmds.push(cmd);
99        }
100    }
101    match valid_cmds.len() {
102        0 => None,
103        1 => valid_cmds.into_iter().next().unwrap(),
104        _ => Some(Box::new(move || Some(Box::new(SequenceMsg(valid_cmds))))),
105    }
106}
107
108/// Every is a command that ticks in sync with the system clock. So, if you
109/// wanted to tick with the system clock every second, minute or hour you
110/// could use this. It's also handy for having different things tick in sync.
111///
112/// Because we're ticking with the system clock the tick will likely not run for
113/// the entire specified duration. For example, if we're ticking for one minute
114/// and the clock is at 12:34:20 then the next tick will happen at 12:35:00, 40
115/// seconds later.
116///
117/// To produce the command, pass a duration and a function which returns
118/// a message containing the time at which the tick occurred.
119///
120/// **Beginners' note**: `every` sends a single message and won't automatically
121/// dispatch messages at an interval. To do that, you'll want to return another
122/// `every` command after receiving your tick message.
123pub fn every<F>(duration: Duration, fn_msg: F) -> Cmd
124where
125    F: FnOnce(SystemTime) -> Option<Box<dyn Msg>> + Send + Sync + 'static,
126{
127    Some(Box::new(move || {
128        let now = SystemTime::now();
129        if let Ok(elapsed) = now.duration_since(SystemTime::UNIX_EPOCH) {
130            let nanos = elapsed.as_nanos();
131            let dur_nanos = duration.as_nanos();
132            if dur_nanos > 0 {
133                let rem = nanos % dur_nanos;
134                let sleep_nanos = dur_nanos - rem;
135                std::thread::sleep(Duration::from_nanos(sleep_nanos as u64));
136            }
137        } else {
138            std::thread::sleep(duration);
139        }
140        fn_msg(SystemTime::now())
141    }))
142}
143
144/// Tick produces a command at an interval independent of the system clock at
145/// the given duration. That is, the timer begins precisely when invoked,
146/// and runs for its entire duration.
147///
148/// To produce the command, pass a duration and a function which returns
149/// a message containing the time at which the tick occurred.
150///
151/// **Beginners' note**: `tick` sends a single message and won't automatically
152/// dispatch messages at an interval. To do that, you'll want to return another
153/// `tick` command after receiving your tick message.
154pub fn tick<F>(duration: Duration, fn_msg: F) -> Cmd
155where
156    F: FnOnce(SystemTime) -> Option<Box<dyn Msg>> + Send + Sync + 'static,
157{
158    Some(Box::new(move || {
159        std::thread::sleep(duration);
160        fn_msg(SystemTime::now())
161    }))
162}
163
164/// RequestWindowSizeMsg is a message that requests terminal window size.
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub struct RequestWindowSizeMsg;
167
168/// RequestWindowSize is a command that queries the terminal for its current
169/// size. It delivers the results to `update` via a `WindowSizeMsg`. Keep in
170/// mind that `WindowSizeMsg`s will automatically be delivered to `update` when
171/// the Program starts and when the window dimensions change, so in many cases
172/// you will not need to explicitly invoke this command.
173pub fn request_window_size() -> Cmd {
174    Some(Box::new(|| Some(Box::new(RequestWindowSizeMsg))))
175}