Skip to main content

rusty_bubbles/
timer.rs

1//! Cleanroom Rust port of upstream Go source file: `timer/timer.go`
2//! Upstream Target Tag / Version: `v2.1.0`
3//!
4//! <public-docs>
5//! # Timer
6//!
7//! A simple timeout component.
8//! </public-docs>
9
10use crate::internal::duration::duration_string;
11use rusty_bubbletea::commands;
12use rusty_bubbletea::model::{Cmd, Msg};
13use std::fmt;
14use std::sync::atomic::{AtomicI64, Ordering};
15use std::time::Duration;
16
17static LAST_ID: AtomicI64 = AtomicI64::new(0);
18
19fn next_id() -> i32 {
20    (LAST_ID.fetch_add(1, Ordering::SeqCst)) as i32
21}
22
23/// Option is a configuration option in [`new`]. For example:
24///
25/// ```rust
26/// # use rusty_bubbles::timer;
27/// # use std::time::Duration;
28/// let timer = timer::new(Duration::from_secs(10), vec![timer::with_interval(Duration::from_secs(5))]);
29/// ```
30pub type Option = Box<dyn FnOnce(&mut Model)>;
31
32/// WithInterval is an option for setting the interval between ticks. Pass as
33/// an argument to [`new`].
34pub fn with_interval(interval: Duration) -> Option {
35    Box::new(move |m: &mut Model| {
36        m.interval = interval;
37    })
38}
39
40/// StartStopMsg is used to start and stop the timer.
41#[derive(Debug, Clone)]
42pub struct StartStopMsg {
43    /// The ID of the timer the message is intended for.
44    pub id: i32,
45    running: bool,
46}
47
48/// TickMsg is a message that is sent on every timer tick.
49#[derive(Debug, Clone)]
50pub struct TickMsg {
51    /// ID is the identifier of the timer that sends the message. This makes
52    /// it possible to determine which timer a tick belongs to when there
53    /// are multiple timers running.
54    ///
55    /// Note, however, that a timer will reject ticks from other timers, so
56    /// it's safe to flow all TickMsgs through all timers and have them still
57    /// behave appropriately.
58    pub id: i32,
59
60    /// Timeout returns whether or not this tick is a timeout tick. You can
61    /// alternatively listen for TimeoutMsg.
62    pub timeout: bool,
63
64    tag: i32,
65}
66
67/// TimeoutMsg is a message that is sent once when the timer times out.
68///
69/// It's a convenience message sent alongside a TickMsg with the Timeout value
70/// set to true.
71#[derive(Debug, Clone)]
72pub struct TimeoutMsg {
73    /// The ID of the timer that timed out.
74    pub id: i32,
75}
76
77/// Model of the timer component.
78#[derive(Clone)]
79pub struct Model {
80    /// How long until the timer expires.
81    pub timeout: Duration,
82
83    /// How long to wait before every tick. Defaults to 1 second.
84    pub interval: Duration,
85
86    id: i32,
87    tag: i32,
88    running: bool,
89}
90
91impl fmt::Debug for Model {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        f.debug_struct("timer::Model")
94            .field("id", &self.id)
95            .field("timeout", &self.timeout)
96            .field("running", &self.running)
97            .finish()
98    }
99}
100
101/// New creates a new timer with the given timeout and default 1s interval.
102pub fn new(timeout: Duration, opts: Vec<Option>) -> Model {
103    let mut m = Model {
104        timeout,
105        interval: Duration::from_secs(1),
106        running: true,
107        id: next_id(),
108        tag: 0,
109    };
110    for opt in opts {
111        opt(&mut m);
112    }
113    m
114}
115
116impl Model {
117    /// ID returns the model's identifier. This can be used to determine if
118    /// messages belong to this timer instance when there are multiple timers.
119    pub fn id(&self) -> i32 {
120        self.id
121    }
122
123    /// Running returns whether or not the timer is running. If the timer has
124    /// timed out this will always return false.
125    pub fn running(&self) -> bool {
126        if self.timedout() || !self.running {
127            return false;
128        }
129        true
130    }
131
132    /// Timedout returns whether or not the timer has timed out.
133    pub fn timedout(&self) -> bool {
134        self.timeout <= Duration::ZERO
135    }
136
137    /// Init starts the timer.
138    pub fn init(&mut self) -> Cmd {
139        self.tick()
140    }
141
142    /// Update handles the timer tick.
143    pub fn update(&mut self, msg: &dyn Msg) -> Cmd {
144        if let Some(m) = msg.as_any().downcast_ref::<StartStopMsg>() {
145            if m.id != 0 && m.id != self.id {
146                return None;
147            }
148            self.running = m.running;
149            return self.tick();
150        }
151
152        if let Some(m) = msg.as_any().downcast_ref::<TickMsg>() {
153            if !self.running() || (m.id != 0 && m.id != self.id) {
154                return None;
155            }
156
157            // If a tag is set, and it's not the one we expect, reject the
158            // message. This prevents the ticker from receiving too many
159            // messages and thus ticking too fast.
160            if m.tag > 0 && m.tag != self.tag {
161                return None;
162            }
163
164            self.timeout = self.timeout.saturating_sub(self.interval);
165            let tick_cmd = self.tick();
166            let timeout_cmd = self.timeout_msg();
167            return commands::batch(vec![tick_cmd, timeout_cmd]);
168        }
169
170        None
171    }
172
173    /// View of the timer component.
174    pub fn view(&self) -> String {
175        duration_string(self.timeout)
176    }
177
178    /// Start resumes the timer. Has no effect if the timer has timed out.
179    pub fn start(&mut self) -> Cmd {
180        self.start_stop(true)
181    }
182
183    /// Stop pauses the timer. Has no effect if the timer has timed out.
184    pub fn stop(&mut self) -> Cmd {
185        self.start_stop(false)
186    }
187
188    /// Toggle stops the timer if it's running and starts it if it's stopped.
189    pub fn toggle(&mut self) -> Cmd {
190        self.start_stop(!self.running())
191    }
192
193    fn tick(&mut self) -> Cmd {
194        let id = self.id;
195        let tag = self.tag;
196        let timeout = self.timedout();
197        let interval = self.interval;
198        commands::tick(interval, move |_| {
199            Some(Box::new(TickMsg { id, tag, timeout }))
200        })
201    }
202
203    fn timeout_msg(&self) -> Cmd {
204        if !self.timedout() {
205            return None;
206        }
207        let id = self.id;
208        Some(Box::new(move || Some(Box::new(TimeoutMsg { id }))))
209    }
210
211    fn start_stop(&mut self, v: bool) -> Cmd {
212        let id = self.id;
213        Some(Box::new(move || {
214            Some(Box::new(StartStopMsg { id, running: v }))
215        }))
216    }
217}