bubbletea_widgets/stopwatch.rs
1//! High-precision stopwatch component for measuring elapsed time in Bubble Tea applications.
2//!
3//! This module provides a fully-featured stopwatch that can be started, stopped, paused,
4//! and reset through Bubble Tea's message-driven architecture. It mirrors the functionality
5//! of Go's bubbles stopwatch component while providing Rust-idiomatic interfaces and
6//! memory safety guarantees.
7//!
8//! # Features
9//!
10//! - **Precise timing**: Uses Rust's `Duration` type for high-precision time measurement
11//! - **Configurable intervals**: Customizable tick frequency from nanoseconds to seconds
12//! - **Multiple instances**: Each stopwatch has a unique ID for managing multiple timers
13//! - **Start/stop/pause/reset**: Full control over stopwatch lifecycle
14//! - **Go-compatible formatting**: Duration display matches Go's time.Duration format
15//! - **Message-driven**: Integrates seamlessly with Bubble Tea's update cycle
16//!
17//! # Quick Start
18//!
19//! ```rust
20//! use bubbletea_widgets::stopwatch::{new, Model};
21//! use bubbletea_rs::{Model as BubbleTeaModel, Msg};
22//!
23//! // Create a stopwatch with 1-second precision
24//! let mut stopwatch = new();
25//!
26//! // Start timing
27//! let start_cmd = stopwatch.start();
28//!
29//! // In your update loop, handle the start message
30//! // stopwatch.update(start_msg); // This would start the timer
31//!
32//! // Check elapsed time
33//! println!("Elapsed: {}", stopwatch.view()); // "0s" initially
34//! ```
35//!
36//! # Integration with Bubble Tea
37//!
38//! ```rust
39//! use bubbletea_widgets::stopwatch::{new, Model as StopwatchModel};
40//! use bubbletea_rs::{Model as BubbleTeaModel, Cmd, Msg};
41//!
42//! struct App {
43//! stopwatch: StopwatchModel,
44//! }
45//!
46//! impl BubbleTeaModel for App {
47//! fn init() -> (Self, Option<Cmd>) {
48//! let stopwatch = new();
49//! let start_cmd = stopwatch.start();
50//! (
51//! App { stopwatch },
52//! Some(start_cmd)
53//! )
54//! }
55//!
56//! fn update(&mut self, msg: Msg) -> Option<Cmd> {
57//! // Forward messages to the stopwatch
58//! self.stopwatch.update(msg)
59//! }
60//!
61//! fn view(&self) -> String {
62//! format!("Elapsed time: {}", self.stopwatch.view())
63//! }
64//! }
65//! ```
66//!
67//! # Custom Tick Intervals
68//!
69//! ```rust
70//! use bubbletea_widgets::stopwatch::new_with_interval;
71//! use std::time::Duration;
72//!
73//! // High-precision stopwatch updating every 10ms
74//! let precise_stopwatch = new_with_interval(Duration::from_millis(10));
75//!
76//! // Low-precision stopwatch updating every 5 seconds
77//! let coarse_stopwatch = new_with_interval(Duration::from_secs(5));
78//! ```
79//!
80//! # Message Types
81//!
82//! The stopwatch communicates through three message types:
83//! - `StartStopMsg`: Controls running state (start/stop/pause)
84//! - `ResetMsg`: Resets elapsed time to zero
85//! - `TickMsg`: Internal timing pulses (automatically generated)
86//!
87//! # Performance Notes
88//!
89//! - Each stopwatch instance has minimal memory overhead (< 64 bytes)
90//! - Tick frequency directly impacts CPU usage - choose appropriately for your use case
91//! - Multiple stopwatches can run concurrently without interference
92//! - Duration formatting is optimized for common time ranges
93
94use bubbletea_rs::{tick as bubbletea_tick, Cmd, Model as BubbleTeaModel, Msg};
95use std::sync::atomic::{AtomicI64, Ordering};
96use std::time::{Duration, Instant};
97
98// Internal ID management for stopwatch instances
99static LAST_ID: AtomicI64 = AtomicI64::new(0);
100
101fn next_id() -> i64 {
102 LAST_ID.fetch_add(1, Ordering::SeqCst) + 1
103}
104
105/// Format duration similar to Go's time.Duration String() output
106fn format_duration(d: Duration) -> String {
107 let total_nanos = d.as_nanos();
108
109 if total_nanos == 0 {
110 return "0s".to_string();
111 }
112
113 if total_nanos >= 1_000_000_000 {
114 // Seconds or more
115 let secs = d.as_secs_f64();
116 if secs >= 60.0 {
117 let minutes = (secs / 60.0) as u64;
118 let remaining_secs = secs % 60.0;
119 if remaining_secs == 0.0 {
120 format!("{}m", minutes)
121 } else {
122 format!("{}m{:.0}s", minutes, remaining_secs)
123 }
124 } else if secs >= 1.0 {
125 if (secs - secs.floor()).abs() < f64::EPSILON {
126 format!("{:.0}s", secs)
127 } else {
128 format!("{:.1}s", secs)
129 }
130 } else {
131 format!("{:.3}s", secs)
132 }
133 } else if total_nanos >= 1_000_000 {
134 // Milliseconds
135 format!("{}ms", d.as_millis())
136 } else if total_nanos >= 1_000 {
137 // Microseconds
138 format!("{}µs", d.as_micros())
139 } else {
140 // Nanoseconds
141 format!("{}ns", total_nanos)
142 }
143}
144
145/// Message sent on every stopwatch tick to increment the elapsed time.
146///
147/// This message is generated automatically by the stopwatch at regular intervals
148/// (determined by the `interval` field) when the stopwatch is running. The message
149/// carries timing information and anti-flooding protection.
150///
151/// # Message Flow
152///
153/// 1. Stopwatch starts → generates first `TickMsg`
154/// 2. `TickMsg` processed → elapsed time incremented → next `TickMsg` scheduled
155/// 3. Continues until stopwatch is stopped or reset
156///
157/// # Examples
158///
159/// You typically don't create `TickMsg` instances manually, but you may need to
160/// handle them in your application's update loop:
161///
162/// ```rust
163/// use bubbletea_widgets::stopwatch::{TickMsg, Model};
164/// use bubbletea_rs::Msg;
165///
166/// fn handle_message(stopwatch: &mut Model, msg: Msg) {
167/// if let Some(tick) = msg.downcast_ref::<TickMsg>() {
168/// println!("Tick from stopwatch ID: {}", tick.id);
169/// }
170/// stopwatch.update(msg);
171/// }
172/// ```
173///
174/// # Anti-flooding Protection
175///
176/// The `tag` field prevents message flooding by ensuring only the expected
177/// tick sequence is processed. Out-of-order or duplicate ticks are ignored.
178#[derive(Debug, Clone)]
179pub struct TickMsg {
180 /// Unique identifier of the stopwatch that generated this tick.
181 ///
182 /// Used to route messages to the correct stopwatch instance when multiple
183 /// stopwatches are running in the same application.
184 pub id: i64,
185 /// Internal sequence number to prevent message flooding.
186 ///
187 /// Each tick increments this tag, and only ticks with the expected tag
188 /// are processed. This prevents the stopwatch from receiving too many
189 /// messages if the system gets backed up.
190 tag: i64,
191}
192
193/// Message to control the running state of a stopwatch.
194///
195/// This message starts or stops a stopwatch, allowing you to pause and resume
196/// timing as needed. When a stopwatch is stopped, it retains its current elapsed
197/// time until started again or reset.
198///
199/// # Usage Pattern
200///
201/// Send this message through your application's command system:
202///
203/// ```rust
204/// use bubbletea_widgets::stopwatch::new;
205///
206/// let stopwatch = new();
207///
208/// // Start the stopwatch
209/// let start_cmd = stopwatch.start(); // Generates StartStopMsg { running: true }
210///
211/// // Stop the stopwatch
212/// let stop_cmd = stopwatch.stop(); // Generates StartStopMsg { running: false }
213///
214/// // Toggle running state
215/// let toggle_cmd = stopwatch.toggle(); // Generates appropriate StartStopMsg
216/// ```
217///
218/// # Examples
219///
220/// Manual creation (rarely needed):
221/// ```rust
222/// use bubbletea_widgets::stopwatch::new;
223///
224/// let stopwatch = new();
225/// // Use the public API methods instead of constructing messages directly
226/// let start_cmd = stopwatch.start(); // Generates StartStopMsg { running: true }
227/// let stop_cmd = stopwatch.stop(); // Generates StartStopMsg { running: false }
228/// ```
229///
230/// # State Transitions
231///
232/// - `running: true` → Stopwatch starts/resumes timing
233/// - `running: false` → Stopwatch pauses (elapsed time preserved)
234#[derive(Debug, Clone)]
235pub struct StartStopMsg {
236 /// Unique identifier of the target stopwatch.
237 ///
238 /// Must match the stopwatch's ID for the message to be processed.
239 /// Use `stopwatch.id()` to get the correct value.
240 pub id: i64,
241 /// Whether the stopwatch should be running after processing this message.
242 ///
243 /// - `true`: Start or resume the stopwatch
244 /// - `false`: Pause the stopwatch (preserving elapsed time)
245 running: bool,
246}
247
248/// Message to reset a stopwatch's elapsed time to zero.
249///
250/// This message clears the accumulated elapsed time while preserving the stopwatch's
251/// running state. A running stopwatch will continue timing from zero after reset,
252/// while a stopped stopwatch will remain stopped with zero elapsed time.
253///
254/// # Usage Pattern
255///
256/// ```rust
257/// use bubbletea_widgets::stopwatch::new;
258///
259/// let stopwatch = new();
260/// let reset_cmd = stopwatch.reset(); // Generates ResetMsg
261/// ```
262///
263/// # Examples
264///
265/// Manual creation (rarely needed):
266/// ```rust
267/// use bubbletea_widgets::stopwatch::{ResetMsg, new};
268///
269/// let stopwatch = new();
270/// let reset_msg = ResetMsg {
271/// id: stopwatch.id(),
272/// };
273/// // Send through your Bubble Tea update system
274/// ```
275///
276/// # Behavior
277///
278/// - Resets `elapsed()` to `Duration::ZERO`
279/// - Does not affect the running state
280/// - Resets internal timing state for accurate subsequent measurements
281#[derive(Debug, Clone)]
282pub struct ResetMsg {
283 /// Unique identifier of the target stopwatch.
284 ///
285 /// Must match the stopwatch's ID for the message to be processed.
286 /// Use `stopwatch.id()` to get the correct value.
287 pub id: i64,
288}
289
290/// A high-precision stopwatch for measuring elapsed time in Bubble Tea applications.
291///
292/// The `Model` represents a single stopwatch instance that can be started, stopped,
293/// paused, and reset through Bubble Tea's message system. Each stopwatch maintains
294/// its own elapsed time, running state, and unique identifier for use in
295/// multi-stopwatch applications.
296///
297/// # Core Functionality
298///
299/// - **Timing**: Accumulates elapsed time with configurable tick intervals
300/// - **State Management**: Tracks running/stopped state independently
301/// - **Identity**: Each instance has a unique ID for message routing
302/// - **Precision**: Uses Rust's `Duration` for sub-second accuracy
303///
304/// # Examples
305///
306/// Basic usage:
307/// ```rust
308/// use bubbletea_widgets::stopwatch::{new, Model};
309/// use std::time::Duration;
310///
311/// let mut stopwatch = new();
312/// assert_eq!(stopwatch.elapsed(), Duration::ZERO);
313/// assert!(!stopwatch.running());
314/// ```
315///
316/// Custom interval for high-precision timing:
317/// ```rust
318/// use bubbletea_widgets::stopwatch::new_with_interval;
319/// use std::time::Duration;
320///
321/// let high_precision = new_with_interval(Duration::from_millis(10));
322/// assert_eq!(high_precision.interval, Duration::from_millis(10));
323/// ```
324///
325/// Integration with Bubble Tea:
326/// ```rust
327/// use bubbletea_widgets::stopwatch::{new, Model as StopwatchModel};
328/// use bubbletea_rs::{Model as BubbleTeaModel, Cmd, Msg};
329/// use std::time::Duration;
330///
331/// struct TimerApp {
332/// stopwatch: StopwatchModel,
333/// }
334///
335/// impl BubbleTeaModel for TimerApp {
336/// fn init() -> (Self, Option<Cmd>) {
337/// let stopwatch = new();
338/// let start_cmd = stopwatch.start();
339/// (TimerApp { stopwatch }, Some(start_cmd))
340/// }
341///
342/// fn update(&mut self, msg: Msg) -> Option<Cmd> {
343/// self.stopwatch.update(msg)
344/// }
345///
346/// fn view(&self) -> String {
347/// format!("Timer: {} ({})",
348/// self.stopwatch.view(),
349/// if self.stopwatch.running() { "running" } else { "stopped" }
350/// )
351/// }
352/// }
353/// ```
354///
355/// # Thread Safety
356///
357/// `Model` is `Clone` and can be shared across threads, but individual instances
358/// should be updated from a single thread to maintain timing accuracy.
359#[derive(Debug, Clone)]
360pub struct Model {
361 /// Current elapsed time accumulated by the stopwatch.
362 d: Duration,
363 /// Unique identifier for this stopwatch instance.
364 id: i64,
365 /// Anti-flooding tag for tick message validation.
366 tag: i64,
367 /// Whether the stopwatch is currently accumulating time.
368 running: bool,
369 /// Time interval between ticks when running.
370 ///
371 /// This determines how frequently the elapsed time is updated and how
372 /// precise the timing measurements will be. Shorter intervals provide
373 /// higher precision but consume more CPU resources.
374 ///
375 /// # Default
376 ///
377 /// New stopwatches default to 1-second intervals (`Duration::from_secs(1)`)
378 /// which is suitable for most applications displaying elapsed time to users.
379 ///
380 /// # Precision Trade-offs
381 ///
382 /// - `Duration::from_millis(10)`: High precision, higher CPU usage
383 /// - `Duration::from_secs(1)`: Good balance for UI display
384 /// - `Duration::from_secs(5)`: Low precision, minimal CPU usage
385 pub interval: Duration,
386 /// The time when this stopwatch was started.
387 ///
388 /// Used for accurate timing calculations. Set when the stopwatch first
389 /// starts running and updated when resumed after pausing.
390 start_instant: Option<Instant>,
391 /// The time when the last tick was processed.
392 ///
393 /// Used to calculate actual elapsed time between ticks, providing
394 /// more accurate elapsed time tracking than interval-based calculations.
395 last_tick: Option<Instant>,
396}
397
398/// Creates a new stopwatch with a custom tick interval.
399///
400/// This function creates a stopwatch that updates its elapsed time at the specified
401/// interval. Shorter intervals provide more precise timing but consume more system
402/// resources, while longer intervals are more efficient but less precise.
403///
404/// # Arguments
405///
406/// * `interval` - How frequently the stopwatch should update its elapsed time
407///
408/// # Returns
409///
410/// A new `Model` instance with the specified interval, initially stopped at zero elapsed time
411///
412/// # Examples
413///
414/// High-precision stopwatch for performance measurement:
415/// ```rust
416/// use bubbletea_widgets::stopwatch::new_with_interval;
417/// use std::time::Duration;
418///
419/// let precise = new_with_interval(Duration::from_millis(1));
420/// assert_eq!(precise.interval, Duration::from_millis(1));
421/// assert_eq!(precise.elapsed(), Duration::ZERO);
422/// assert!(!precise.running());
423/// ```
424///
425/// Low-frequency stopwatch for long-running processes:
426/// ```rust
427/// use bubbletea_widgets::stopwatch::new_with_interval;
428/// use std::time::Duration;
429///
430/// let coarse = new_with_interval(Duration::from_secs(10));
431/// assert_eq!(coarse.interval, Duration::from_secs(10));
432/// ```
433///
434/// Microsecond precision for benchmarking:
435/// ```rust
436/// use bubbletea_widgets::stopwatch::new_with_interval;
437/// use std::time::Duration;
438///
439/// let benchmark = new_with_interval(Duration::from_micros(100));
440/// assert_eq!(benchmark.interval, Duration::from_micros(100));
441/// ```
442///
443/// # Performance Considerations
444///
445/// - **Nanosecond intervals**: Maximum precision, very high CPU usage
446/// - **Millisecond intervals**: High precision, moderate CPU usage
447/// - **Second intervals**: Human-readable precision, low CPU usage
448/// - **Minute+ intervals**: Minimal precision, negligible CPU usage
449///
450/// Choose the interval based on your application's precision requirements and
451/// performance constraints.
452pub fn new_with_interval(interval: Duration) -> Model {
453 Model {
454 d: Duration::ZERO,
455 id: next_id(),
456 tag: 0,
457 running: false,
458 interval,
459 start_instant: None,
460 last_tick: None,
461 }
462}
463
464/// Creates a new stopwatch with a default 1-second tick interval.
465///
466/// This is the most commonly used constructor, providing a good balance between
467/// timing precision and system resource usage. The 1-second interval is suitable
468/// for most user-facing applications where elapsed time is displayed in seconds.
469///
470/// # Returns
471///
472/// A new `Model` instance with 1-second tick interval, initially stopped at zero elapsed time
473///
474/// # Examples
475///
476/// Basic stopwatch creation:
477/// ```rust
478/// use bubbletea_widgets::stopwatch::new;
479/// use std::time::Duration;
480///
481/// let stopwatch = new();
482/// assert_eq!(stopwatch.interval, Duration::from_secs(1));
483/// assert_eq!(stopwatch.elapsed(), Duration::ZERO);
484/// assert!(!stopwatch.running());
485/// assert!(stopwatch.id() > 0); // Has unique ID
486/// ```
487///
488/// Multiple independent stopwatches:
489/// ```rust
490/// use bubbletea_widgets::stopwatch::new;
491///
492/// let timer1 = new();
493/// let timer2 = new();
494/// // Each has a unique ID for independent operation
495/// assert_ne!(timer1.id(), timer2.id());
496/// ```
497///
498/// # Equivalent To
499///
500/// ```rust
501/// use bubbletea_widgets::stopwatch::new_with_interval;
502/// use std::time::Duration;
503///
504/// let stopwatch = new_with_interval(Duration::from_secs(1));
505/// ```
506pub fn new() -> Model {
507 new_with_interval(Duration::from_secs(1))
508}
509
510impl Model {
511 /// Returns the unique identifier of this stopwatch instance.
512 ///
513 /// Each stopwatch has a globally unique ID that's used for message routing
514 /// when multiple stopwatches are running in the same application. This ID
515 /// is automatically assigned during construction and never changes.
516 ///
517 /// # Returns
518 ///
519 /// A unique `i64` identifier for this stopwatch
520 ///
521 /// # Examples
522 ///
523 /// ```rust
524 /// use bubbletea_widgets::stopwatch::new;
525 ///
526 /// let stopwatch1 = new();
527 /// let stopwatch2 = new();
528 ///
529 /// // Each stopwatch has a unique ID
530 /// assert_ne!(stopwatch1.id(), stopwatch2.id());
531 /// assert!(stopwatch1.id() > 0);
532 /// assert!(stopwatch2.id() > 0);
533 /// ```
534 ///
535 /// Using ID for message filtering:
536 /// ```rust
537 /// use bubbletea_widgets::stopwatch::{new, StartStopMsg};
538 /// use bubbletea_rs::Msg;
539 ///
540 /// let stopwatch = new();
541 /// let my_id = stopwatch.id();
542 ///
543 /// // Create a start command for this specific stopwatch
544 /// let start_cmd = stopwatch.start(); // Generates appropriate StartStopMsg
545 /// ```
546 ///
547 /// # Thread Safety
548 ///
549 /// The ID is assigned using atomic operations and is safe to access
550 /// from multiple threads.
551 pub fn id(&self) -> i64 {
552 self.id
553 }
554
555 /// Returns whether the stopwatch is currently running and accumulating time.
556 ///
557 /// A running stopwatch actively updates its elapsed time at each tick interval.
558 /// A stopped stopwatch preserves its current elapsed time without further updates.
559 ///
560 /// # Returns
561 ///
562 /// `true` if the stopwatch is running, `false` if stopped/paused
563 ///
564 /// # Examples
565 ///
566 /// ```rust
567 /// use bubbletea_widgets::stopwatch::new;
568 ///
569 /// let stopwatch = new();
570 /// assert!(!stopwatch.running()); // Initially stopped
571 /// ```
572 ///
573 /// Checking state after operations:
574 /// ```rust
575 /// use bubbletea_widgets::stopwatch::{new, StartStopMsg};
576 ///
577 /// let mut stopwatch = new();
578 /// assert!(!stopwatch.running());
579 ///
580 /// // After processing a start command, it would be running
581 /// let start_cmd = stopwatch.start(); // Generates StartStopMsg internally
582 /// // Process the command through your Bubble Tea app to set running = true
583 /// ```
584 ///
585 /// # State Persistence
586 ///
587 /// The running state persists across:
588 /// - Clone operations
589 /// - Tick message processing
590 /// - Reset operations (reset preserves running state)
591 ///
592 /// Only `StartStopMsg` messages change the running state.
593 pub fn running(&self) -> bool {
594 self.running
595 }
596
597 /// Initializes the stopwatch by generating a start command.
598 ///
599 /// This method is typically called when setting up the stopwatch in a Bubble Tea
600 /// application's initialization phase. It generates a command that will start
601 /// the stopwatch when processed by the update loop.
602 ///
603 /// # Returns
604 ///
605 /// A `Cmd` that will start the stopwatch when executed
606 ///
607 /// # Examples
608 ///
609 /// Using in a Bubble Tea application:
610 /// ```rust
611 /// use bubbletea_widgets::stopwatch::{new, Model as StopwatchModel};
612 /// use bubbletea_rs::{Model as BubbleTeaModel, Cmd, Msg};
613 ///
614 /// struct App {
615 /// stopwatch: StopwatchModel,
616 /// }
617 ///
618 /// impl BubbleTeaModel for App {
619 /// fn init() -> (Self, Option<Cmd>) {
620 /// let stopwatch = new();
621 /// let init_cmd = stopwatch.init(); // Generates start command
622 /// (App { stopwatch }, Some(init_cmd))
623 /// }
624 /// # fn update(&mut self, _msg: Msg) -> Option<Cmd> { None }
625 /// # fn view(&self) -> String { String::new() }
626 /// }
627 /// ```
628 ///
629 /// Manual initialization:
630 /// ```rust
631 /// use bubbletea_widgets::stopwatch::new;
632 ///
633 /// let stopwatch = new();
634 /// let init_cmd = stopwatch.init();
635 /// // Execute this command in your Bubble Tea runtime
636 /// ```
637 ///
638 /// # Note
639 ///
640 /// This method is equivalent to calling `start()` and is provided for
641 /// consistency with the Bubble Tea component lifecycle.
642 pub fn init(&self) -> Cmd {
643 self.start()
644 }
645
646 /// Generates a command to start the stopwatch.
647 ///
648 /// Creates a command that, when processed, will start the stopwatch and begin
649 /// accumulating elapsed time. If the stopwatch is already running, this command
650 /// has no additional effect.
651 ///
652 /// # Returns
653 ///
654 /// A `Cmd` that will start the stopwatch when executed by the Bubble Tea runtime
655 ///
656 /// # Examples
657 ///
658 /// Basic start operation:
659 /// ```rust
660 /// use bubbletea_widgets::stopwatch::new;
661 ///
662 /// let stopwatch = new();
663 /// let start_cmd = stopwatch.start();
664 /// // Execute this command in your update loop to start timing
665 /// ```
666 ///
667 /// Integration with application logic:
668 /// ```rust
669 /// use bubbletea_widgets::stopwatch::{new, Model as StopwatchModel};
670 /// use bubbletea_rs::{KeyMsg, Cmd, Msg};
671 /// use crossterm::event::{KeyCode, KeyModifiers};
672 ///
673 /// fn handle_keypress(stopwatch: &StopwatchModel, key: KeyMsg) -> Option<Cmd> {
674 /// match key.key {
675 /// KeyCode::Char('s') => {
676 /// if key.modifiers.is_empty() {
677 /// Some(stopwatch.start()) // Start on 's' key
678 /// } else {
679 /// None
680 /// }
681 /// }
682 /// _ => None,
683 /// }
684 /// }
685 /// ```
686 ///
687 /// # State Transition
688 ///
689 /// - **Stopped → Running**: Begins accumulating elapsed time
690 /// - **Running → Running**: No change (idempotent)
691 ///
692 /// # Message Flow
693 ///
694 /// This method generates a `StartStopMsg` with `running: true` that will be
695 /// processed by the `update()` method to change the stopwatch state.
696 pub fn start(&self) -> Cmd {
697 let id = self.id;
698 bubbletea_tick(Duration::from_nanos(1), move |_| {
699 Box::new(StartStopMsg { id, running: true }) as Msg
700 })
701 }
702
703 /// Generates a command to stop/pause the stopwatch.
704 ///
705 /// Creates a command that, when processed, will stop the stopwatch and pause
706 /// elapsed time accumulation. The current elapsed time is preserved and can
707 /// be resumed later with `start()`. If the stopwatch is already stopped,
708 /// this command has no additional effect.
709 ///
710 /// # Returns
711 ///
712 /// A `Cmd` that will stop the stopwatch when executed by the Bubble Tea runtime
713 ///
714 /// # Examples
715 ///
716 /// Basic stop operation:
717 /// ```rust
718 /// use bubbletea_widgets::stopwatch::new;
719 ///
720 /// let stopwatch = new();
721 /// let stop_cmd = stopwatch.stop();
722 /// // Execute this command to pause timing
723 /// ```
724 ///
725 /// Stop-watch pattern:
726 /// ```rust
727 /// use bubbletea_widgets::stopwatch::{new, Model as StopwatchModel};
728 /// use bubbletea_rs::{KeyMsg, Cmd};
729 /// use crossterm::event::KeyCode;
730 ///
731 /// fn handle_spacebar(stopwatch: &StopwatchModel) -> Cmd {
732 /// if stopwatch.running() {
733 /// stopwatch.stop() // Pause if running
734 /// } else {
735 /// stopwatch.start() // Resume if stopped
736 /// }
737 /// }
738 /// ```
739 ///
740 /// # State Transition
741 ///
742 /// - **Running → Stopped**: Pauses elapsed time accumulation
743 /// - **Stopped → Stopped**: No change (idempotent)
744 ///
745 /// # Time Preservation
746 ///
747 /// Stopping a stopwatch preserves the current elapsed time:
748 /// ```rust
749 /// use bubbletea_widgets::stopwatch::new;
750 /// use std::time::Duration;
751 ///
752 /// // Imagine stopwatch has been running and shows some elapsed time
753 /// let stopwatch = new();
754 /// // let elapsed_before_stop = stopwatch.elapsed();
755 ///
756 /// // After processing stop command:
757 /// // assert_eq!(stopwatch.elapsed(), elapsed_before_stop); // Time preserved
758 /// // assert!(!stopwatch.running()); // But no longer accumulating
759 /// ```
760 pub fn stop(&self) -> Cmd {
761 let id = self.id;
762 bubbletea_tick(Duration::from_nanos(1), move |_| {
763 Box::new(StartStopMsg { id, running: false }) as Msg
764 })
765 }
766
767 /// Generates a command to toggle the stopwatch's running state.
768 ///
769 /// This is a convenience method that starts the stopwatch if it's currently
770 /// stopped, or stops it if it's currently running. Useful for implementing
771 /// play/pause functionality with a single key or button.
772 ///
773 /// # Returns
774 ///
775 /// A `Cmd` that will toggle the stopwatch state when executed:
776 /// - If running: generates a stop command
777 /// - If stopped: generates a start command
778 ///
779 /// # Examples
780 ///
781 /// Basic toggle functionality:
782 /// ```rust
783 /// use bubbletea_widgets::stopwatch::new;
784 ///
785 /// let stopwatch = new();
786 /// assert!(!stopwatch.running());
787 ///
788 /// let toggle_cmd = stopwatch.toggle(); // Will generate start command
789 /// ```
790 ///
791 /// Implementing spacebar toggle:
792 /// ```rust
793 /// use bubbletea_widgets::stopwatch::{new, Model as StopwatchModel};
794 /// use bubbletea_rs::{KeyMsg, Cmd};
795 /// use crossterm::event::{KeyCode, KeyModifiers};
796 ///
797 /// fn handle_key(stopwatch: &StopwatchModel, key: KeyMsg) -> Option<Cmd> {
798 /// match key.key {
799 /// KeyCode::Char(' ') if key.modifiers == KeyModifiers::NONE => {
800 /// Some(stopwatch.toggle()) // Spacebar toggles play/pause
801 /// }
802 /// _ => None,
803 /// }
804 /// }
805 /// ```
806 ///
807 /// # State Transitions
808 ///
809 /// - **Stopped → Running**: Equivalent to `start()`
810 /// - **Running → Stopped**: Equivalent to `stop()`
811 ///
812 /// # Equivalent Implementation
813 ///
814 /// ```rust
815 /// use bubbletea_widgets::stopwatch::new;
816 ///
817 /// let stopwatch = new();
818 /// let toggle_cmd = if stopwatch.running() {
819 /// stopwatch.stop()
820 /// } else {
821 /// stopwatch.start()
822 /// };
823 /// ```
824 pub fn toggle(&self) -> Cmd {
825 if self.running() {
826 self.stop()
827 } else {
828 self.start()
829 }
830 }
831
832 /// Generates a command to reset the stopwatch's elapsed time to zero.
833 ///
834 /// Creates a command that, when processed, will clear the accumulated elapsed
835 /// time while preserving the running state. A running stopwatch will continue
836 /// timing from zero, while a stopped stopwatch will remain stopped with zero
837 /// elapsed time.
838 ///
839 /// # Returns
840 ///
841 /// A `Cmd` that will reset the elapsed time when executed by the Bubble Tea runtime
842 ///
843 /// # Examples
844 ///
845 /// Basic reset operation:
846 /// ```rust
847 /// use bubbletea_widgets::stopwatch::new;
848 ///
849 /// let stopwatch = new();
850 /// let reset_cmd = stopwatch.reset();
851 /// // Execute this command to clear elapsed time
852 /// ```
853 ///
854 /// Reset with state preservation:
855 /// ```rust
856 /// use bubbletea_widgets::stopwatch::new;
857 /// use std::time::Duration;
858 ///
859 /// // Imagine a running stopwatch with accumulated time
860 /// let stopwatch = new();
861 /// let was_running = stopwatch.running();
862 ///
863 /// let reset_cmd = stopwatch.reset();
864 /// // After processing reset command:
865 /// // assert_eq!(stopwatch.elapsed(), Duration::ZERO); // Time cleared
866 /// // assert_eq!(stopwatch.running(), was_running); // State preserved
867 /// ```
868 ///
869 /// Implementing a reset button:
870 /// ```rust
871 /// use bubbletea_widgets::stopwatch::{new, Model as StopwatchModel};
872 /// use bubbletea_rs::{KeyMsg, Cmd};
873 /// use crossterm::event::{KeyCode, KeyModifiers};
874 ///
875 /// fn handle_key(stopwatch: &StopwatchModel, key: KeyMsg) -> Option<Cmd> {
876 /// match key.key {
877 /// KeyCode::Char('r') if key.modifiers == KeyModifiers::NONE => {
878 /// Some(stopwatch.reset()) // 'r' key resets timer
879 /// }
880 /// _ => None,
881 /// }
882 /// }
883 /// ```
884 ///
885 /// # Behavior Details
886 ///
887 /// - **Elapsed time**: Set to `Duration::ZERO`
888 /// - **Running state**: Unchanged (preserved)
889 /// - **Internal timing**: Reset for accurate subsequent measurements
890 /// - **ID and interval**: Unchanged
891 ///
892 /// # Use Cases
893 ///
894 /// - Lap timing (reset while continuing)
895 /// - Error recovery (clear invalid measurements)
896 /// - User-initiated restart
897 /// - Preparation for new timing session
898 pub fn reset(&self) -> Cmd {
899 let id = self.id;
900 bubbletea_tick(Duration::from_nanos(1), move |_| {
901 Box::new(ResetMsg { id }) as Msg
902 })
903 }
904
905 /// Processes messages and updates the stopwatch state.
906 ///
907 /// This method handles all incoming messages for the stopwatch, updating its
908 /// internal state and scheduling follow-up commands as needed. It processes
909 /// three types of messages: `StartStopMsg`, `ResetMsg`, and `TickMsg`.
910 ///
911 /// # Arguments
912 ///
913 /// * `msg` - The message to process, typically from the Bubble Tea runtime
914 ///
915 /// # Returns
916 ///
917 /// - `Some(Cmd)` if a follow-up command should be executed
918 /// - `None` if no further action is needed
919 ///
920 /// # Message Types
921 ///
922 /// ## StartStopMsg
923 /// Changes the running state and schedules the next tick if starting.
924 ///
925 /// ## ResetMsg
926 /// Clears elapsed time to zero without affecting running state.
927 ///
928 /// ## TickMsg
929 /// Increments elapsed time and schedules the next tick if running.
930 ///
931 /// # Examples
932 ///
933 /// Basic usage in a Bubble Tea application:
934 /// ```rust,ignore
935 /// use bubbletea_widgets::stopwatch::new;
936 ///
937 /// let mut stopwatch = new();
938 ///
939 /// // Start the stopwatch using the public API
940 /// let start_cmd = stopwatch.start();
941 /// // In a real app, you'd send this command through bubbletea
942 /// assert!(!stopwatch.running()); // Initially not running
943 /// ```
944 ///
945 /// Handling multiple message types:
946 /// ```rust,ignore
947 /// use bubbletea_widgets::stopwatch::{new, ResetMsg};
948 /// use std::time::Duration;
949 ///
950 /// let mut stopwatch = new();
951 ///
952 /// // Start the stopwatch using the public API
953 /// let start_cmd = stopwatch.start();
954 ///
955 /// // Reset to zero
956 /// let reset = ResetMsg { id: stopwatch.id() };
957 /// stopwatch.update(Box::new(reset));
958 /// assert_eq!(stopwatch.elapsed(), Duration::ZERO);
959 /// ```
960 ///
961 /// # Message Filtering
962 ///
963 /// Messages are filtered by ID to ensure they're intended for this stopwatch:
964 /// ```rust
965 /// use bubbletea_widgets::stopwatch::new;
966 ///
967 /// let mut stopwatch = new();
968 /// // Messages with wrong IDs don't affect state
969 /// assert!(!stopwatch.running()); // Initially not running
970 /// ```
971 ///
972 /// # Thread Safety
973 ///
974 /// This method should be called from a single thread to maintain timing accuracy,
975 /// though the stopwatch can be cloned and used across threads.
976 pub fn update(&mut self, msg: Msg) -> Option<Cmd> {
977 if let Some(start_stop) = msg.downcast_ref::<StartStopMsg>() {
978 if start_stop.id != self.id {
979 return None;
980 }
981
982 let was_running = self.running;
983 self.running = start_stop.running;
984
985 // Reset timing when starting from stopped state
986 if !was_running && self.running {
987 self.start_instant = None;
988 self.last_tick = None;
989 }
990
991 // When starting or stopping, schedule the next tick so we keep updating
992 return Some(self.tick());
993 }
994
995 if let Some(reset) = msg.downcast_ref::<ResetMsg>() {
996 if reset.id != self.id {
997 return None;
998 }
999 self.d = Duration::ZERO;
1000 // Reset timing state as well
1001 self.start_instant = None;
1002 self.last_tick = None;
1003 return None;
1004 }
1005
1006 if let Some(tick) = msg.downcast_ref::<TickMsg>() {
1007 if !self.running || tick.id != self.id {
1008 return None;
1009 }
1010 // Reject unexpected tags to avoid too-frequent ticks
1011 if tick.tag > 0 && tick.tag != self.tag {
1012 return None;
1013 }
1014
1015 // Use high-precision elapsed time tracking for accurate measurement
1016 let now = Instant::now();
1017
1018 // Initialize timing on first tick
1019 if self.last_tick.is_none() {
1020 self.start_instant = Some(now);
1021 self.last_tick = Some(now);
1022 // On first tick, just use the interval as fallback
1023 self.d = self.d.saturating_add(self.interval);
1024 } else {
1025 // Calculate actual elapsed time since last tick
1026 let actual_elapsed = now.duration_since(self.last_tick.unwrap());
1027 self.d = self.d.saturating_add(actual_elapsed);
1028 self.last_tick = Some(now);
1029 }
1030
1031 self.tag += 1;
1032 return Some(self.tick());
1033 }
1034
1035 None
1036 }
1037
1038 /// Returns a human-readable string representation of the elapsed time.
1039 ///
1040 /// Formats the accumulated elapsed time using Go-compatible duration formatting.
1041 /// The output format adapts to the magnitude of the elapsed time for optimal
1042 /// readability across different time scales.
1043 ///
1044 /// # Returns
1045 ///
1046 /// A formatted string representing the elapsed time
1047 ///
1048 /// # Format Examples
1049 ///
1050 /// - `"0s"` for zero duration
1051 /// - `"150ms"` for sub-second durations
1052 /// - `"2.5s"` for fractional seconds
1053 /// - `"45s"` for whole seconds under a minute
1054 /// - `"2m30s"` for minutes with seconds
1055 /// - `"5m"` for whole minutes without seconds
1056 ///
1057 /// # Examples
1058 ///
1059 /// ```rust
1060 /// use bubbletea_widgets::stopwatch::new;
1061 /// use std::time::Duration;
1062 ///
1063 /// let stopwatch = new();
1064 /// assert_eq!(stopwatch.view(), "0s"); // Initially zero
1065 /// ```
1066 ///
1067 /// Displaying elapsed time in UI:
1068 /// ```rust
1069 /// use bubbletea_widgets::stopwatch::{new, Model as StopwatchModel};
1070 /// use bubbletea_rs::{Model as BubbleTeaModel, Cmd, Msg};
1071 ///
1072 /// struct TimerDisplay {
1073 /// stopwatch: StopwatchModel,
1074 /// }
1075 ///
1076 /// impl BubbleTeaModel for TimerDisplay {
1077 /// fn init() -> (Self, Option<Cmd>) {
1078 /// (TimerDisplay { stopwatch: new() }, None)
1079 /// }
1080 /// # fn update(&mut self, _msg: Msg) -> Option<Cmd> { None }
1081 ///
1082 /// fn view(&self) -> String {
1083 /// format!("Timer: {}", self.stopwatch.view())
1084 /// }
1085 /// }
1086 /// ```
1087 ///
1088 /// # Performance
1089 ///
1090 /// String formatting is optimized for common time ranges and involves minimal
1091 /// allocations. Suitable for real-time UI updates.
1092 ///
1093 /// # Consistency
1094 ///
1095 /// The format matches Go's `time.Duration.String()` output for cross-language
1096 /// compatibility in applications that interoperate with Go services.
1097 pub fn view(&self) -> String {
1098 format_duration(self.d)
1099 }
1100
1101 /// Returns the total elapsed time as a `Duration`.
1102 ///
1103 /// Provides access to the raw elapsed time for precise calculations,
1104 /// comparisons, or custom formatting. This is the accumulated time
1105 /// since the stopwatch was started, minus any time it was stopped.
1106 ///
1107 /// # Returns
1108 ///
1109 /// The total elapsed time as a `Duration`
1110 ///
1111 /// # Examples
1112 ///
1113 /// ```rust
1114 /// use bubbletea_widgets::stopwatch::new;
1115 /// use std::time::Duration;
1116 ///
1117 /// let stopwatch = new();
1118 /// assert_eq!(stopwatch.elapsed(), Duration::ZERO); // Initially zero
1119 /// ```
1120 ///
1121 /// Precise timing calculations:
1122 /// ```rust
1123 /// use bubbletea_widgets::stopwatch::new;
1124 /// use std::time::Duration;
1125 ///
1126 /// let stopwatch = new();
1127 /// let elapsed = stopwatch.elapsed();
1128 ///
1129 /// // Convert to different units
1130 /// let millis = elapsed.as_millis();
1131 /// let secs = elapsed.as_secs_f64();
1132 /// let nanos = elapsed.as_nanos();
1133 /// ```
1134 ///
1135 /// Performance measurement:
1136 /// ```rust,ignore
1137 /// use bubbletea_widgets::stopwatch::new;
1138 /// use std::time::Duration;
1139 ///
1140 /// let mut stopwatch = new();
1141 ///
1142 /// // Start timing using the public API
1143 /// let start_cmd = stopwatch.start();
1144 /// // In a real app, you'd send this command through bubbletea
1145 ///
1146 /// let elapsed = stopwatch.elapsed();
1147 /// assert!(elapsed >= Duration::ZERO);
1148 ///
1149 /// // Check if operation was fast enough
1150 /// if elapsed < Duration::from_millis(100) {
1151 /// println!("Operation completed quickly: {:?}", elapsed);
1152 /// }
1153 /// ```
1154 ///
1155 /// Comparison with thresholds:
1156 /// ```rust
1157 /// use bubbletea_widgets::stopwatch::new;
1158 /// use std::time::Duration;
1159 ///
1160 /// let stopwatch = new();
1161 /// let elapsed = stopwatch.elapsed();
1162 /// let threshold = Duration::from_secs(30);
1163 ///
1164 /// if elapsed > threshold {
1165 /// println!("Timer has exceeded 30 seconds");
1166 /// }
1167 /// ```
1168 ///
1169 /// # Precision
1170 ///
1171 /// The returned `Duration` has the same precision as Rust's `Duration` type,
1172 /// which supports nanosecond-level timing on most platforms.
1173 pub fn elapsed(&self) -> Duration {
1174 self.d
1175 }
1176
1177 /// Internal: schedule the next tick.
1178 fn tick(&self) -> Cmd {
1179 let id = self.id;
1180 let tag = self.tag;
1181 let interval = self.interval;
1182 bubbletea_tick(interval, move |_| Box::new(TickMsg { id, tag }) as Msg)
1183 }
1184}
1185
1186impl BubbleTeaModel for Model {
1187 /// Creates a new stopwatch and starts it automatically.
1188 ///
1189 /// This implementation provides default behavior for using a stopwatch
1190 /// as a standalone Bubble Tea component. The returned stopwatch will
1191 /// begin timing immediately when the application starts.
1192 ///
1193 /// # Returns
1194 ///
1195 /// A tuple containing:
1196 /// - A new stopwatch model with default settings
1197 /// - A command to start the stopwatch
1198 ///
1199 /// # Examples
1200 ///
1201 /// Using stopwatch as a standalone component:
1202 /// ```rust
1203 /// use bubbletea_widgets::stopwatch::new;
1204 ///
1205 /// let stopwatch = new();
1206 /// let _init_cmd = stopwatch.init();
1207 /// assert!(!stopwatch.running()); // Will be running after cmd execution
1208 /// ```
1209 fn init() -> (Self, Option<Cmd>) {
1210 let model = new();
1211 let cmd = model.init();
1212 (model, Some(cmd))
1213 }
1214
1215 /// Forwards messages to the stopwatch's update method.
1216 ///
1217 /// This delegates message handling to the stopwatch's own update logic,
1218 /// maintaining the same behavior when used as a standalone component.
1219 fn update(&mut self, msg: Msg) -> Option<Cmd> {
1220 self.update(msg)
1221 }
1222
1223 /// Returns the formatted elapsed time string.
1224 ///
1225 /// Displays the stopwatch's current elapsed time in a human-readable
1226 /// format suitable for direct display in terminal UIs.
1227 fn view(&self) -> String {
1228 self.view()
1229 }
1230}
1231
1232impl Default for Model {
1233 /// Creates a new stopwatch with default settings.
1234 ///
1235 /// Equivalent to calling `new()`, providing a stopwatch with:
1236 /// - Zero elapsed time
1237 /// - Stopped state
1238 /// - 1-second tick interval
1239 /// - Unique ID
1240 ///
1241 /// # Examples
1242 ///
1243 /// ```rust
1244 /// use bubbletea_widgets::stopwatch::Model;
1245 /// use std::time::Duration;
1246 ///
1247 /// let stopwatch = Model::default();
1248 /// assert_eq!(stopwatch.elapsed(), Duration::ZERO);
1249 /// assert!(!stopwatch.running());
1250 /// assert_eq!(stopwatch.interval, Duration::from_secs(1));
1251 /// ```
1252 ///
1253 /// Using with struct initialization:
1254 /// ```rust
1255 /// use bubbletea_widgets::stopwatch::Model as StopwatchModel;
1256 ///
1257 /// #[derive(Default)]
1258 /// struct App {
1259 /// timer: StopwatchModel,
1260 /// }
1261 ///
1262 /// let app = App::default(); // Uses stopwatch default
1263 /// ```
1264 fn default() -> Self {
1265 new()
1266 }
1267}
1268
1269#[cfg(test)]
1270mod tests {
1271 use super::*;
1272
1273 #[test]
1274 fn test_new_defaults() {
1275 let sw = new();
1276 assert_eq!(sw.elapsed(), Duration::ZERO);
1277 assert!(!sw.running());
1278 assert!(sw.id() > 0);
1279 assert_eq!(sw.interval, Duration::from_secs(1));
1280 }
1281
1282 #[test]
1283 fn test_start_stop_toggle_reset_cmds() {
1284 let sw = new();
1285 std::mem::drop(sw.start());
1286 std::mem::drop(sw.stop());
1287 std::mem::drop(sw.toggle());
1288 std::mem::drop(sw.reset());
1289 }
1290
1291 #[test]
1292 fn test_update_flow() {
1293 let mut sw = new_with_interval(Duration::from_millis(10));
1294
1295 // Start
1296 let start = StartStopMsg {
1297 id: sw.id(),
1298 running: true,
1299 };
1300 let next = sw.update(Box::new(start));
1301 assert!(next.is_some());
1302 assert!(sw.running());
1303
1304 // Tick increments
1305 let before = sw.elapsed();
1306 let tick = TickMsg {
1307 id: sw.id(),
1308 tag: sw.tag,
1309 };
1310 let _ = sw.update(Box::new(tick));
1311 assert!(sw.elapsed() > before);
1312
1313 // Reset
1314 let _ = sw.update(Box::new(ResetMsg { id: sw.id() }));
1315 assert_eq!(sw.elapsed(), Duration::ZERO);
1316 }
1317}