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}