bubbletea_widgets/
timer.rs

1//! Timer component for Bubble Tea applications.
2//!
3//! Package timer provides a simple timeout component for Bubble Tea applications.
4//! It closely matches the Go bubbles timer component API for 1-1 compatibility.
5//!
6//! # Basic Usage
7//!
8//! ```rust
9//! use bubbletea_widgets::timer::{new, new_with_interval};
10//! use std::time::Duration;
11//!
12//! // Create a timer with default 1 second interval
13//! let timer = new(Duration::from_secs(30));
14//!
15//! // Create a timer with custom interval
16//! let timer = new_with_interval(Duration::from_secs(60), Duration::from_millis(100));
17//! ```
18//!
19//! # bubbletea-rs Integration
20//!
21//! ```rust
22//! use bubbletea_rs::{Model as BubbleTeaModel, Msg, Cmd};
23//! use bubbletea_widgets::timer::{new, Model, TickMsg, StartStopMsg, TimeoutMsg};
24//! use std::time::Duration;
25//!
26//! struct MyApp {
27//!     timer: Model,
28//! }
29//!
30//! impl BubbleTeaModel for MyApp {
31//!     fn init() -> (Self, Option<Cmd>) {
32//!         let timer = new(Duration::from_secs(10));
33//!         let cmd = timer.init();
34//!         (Self { timer }, Some(cmd))
35//!     }
36//!
37//!     fn update(&mut self, msg: Msg) -> Option<Cmd> {
38//!         // Handle timeout
39//!         if let Some(timeout) = msg.downcast_ref::<TimeoutMsg>() {
40//!             if timeout.id == self.timer.id() {
41//!                 // Timer finished!
42//!             }
43//!         }
44//!         
45//!         // Forward timer messages
46//!         self.timer.update(msg)
47//!     }
48//!
49//!     fn view(&self) -> String {
50//!         format!("Time remaining: {}", self.timer.view())
51//!     }
52//! }
53//! ```
54//!
55//! # Start/Stop Control
56//!
57//! ```rust
58//! use bubbletea_widgets::timer::new;
59//! use std::time::Duration;
60//!
61//! let timer = new(Duration::from_secs(30));
62//!
63//! // These return commands that send StartStopMsg
64//! let start_cmd = timer.start();   // Resume timer
65//! let stop_cmd = timer.stop();     // Pause timer  
66//! let toggle_cmd = timer.toggle(); // Toggle running state
67//! ```
68
69use bubbletea_rs::{tick as bubbletea_tick, Cmd, Model as BubbleTeaModel, Msg};
70use std::sync::atomic::{AtomicI64, Ordering};
71use std::time::{Duration, Instant};
72
73// Internal ID management for timer instances
74static LAST_ID: AtomicI64 = AtomicI64::new(0);
75
76/// Generates unique identifiers for timer instances.
77///
78/// This function ensures that each timer created gets a unique ID, allowing
79/// multiple timers to coexist in the same application without message conflicts.
80/// The IDs are generated atomically and start from 1.
81///
82/// # Returns
83///
84/// A unique `i64` identifier for a timer instance
85///
86/// # Thread Safety
87///
88/// This function is thread-safe and can be called from multiple threads
89/// concurrently without risk of duplicate IDs.
90fn next_id() -> i64 {
91    LAST_ID.fetch_add(1, Ordering::SeqCst) + 1
92}
93
94/// Formats a duration using Go's Duration.String() format for compatibility.
95///
96/// This function converts a Rust `Duration` into a string representation that
97/// matches Go's duration formatting exactly, ensuring consistent display across
98/// different language implementations of the bubbles library.
99///
100/// # Arguments
101///
102/// * `d` - The duration to format
103///
104/// # Returns
105///
106/// A formatted string representation of the duration
107///
108/// # Format Examples
109///
110/// - `0s` for zero duration
111/// - `500ms` for milliseconds
112/// - `1.5s` for seconds with decimals
113/// - `2m30s` for minutes and seconds
114/// - `1m` for exact minutes
115///
116/// # Examples
117///
118/// ```rust,ignore
119/// use std::time::Duration;
120///
121/// assert_eq!(format_duration(Duration::from_secs(0)), "0s");
122/// assert_eq!(format_duration(Duration::from_millis(500)), "500ms");
123/// assert_eq!(format_duration(Duration::from_secs(90)), "1m30s");
124/// ```
125fn format_duration(d: Duration) -> String {
126    let total_nanos = d.as_nanos();
127
128    if total_nanos == 0 {
129        return "0s".to_string();
130    }
131
132    // Convert to go-like format
133    if total_nanos >= 1_000_000_000 {
134        // Seconds or more
135        let secs = d.as_secs_f64();
136        if secs >= 60.0 {
137            let minutes = (secs / 60.0) as u64;
138            let remaining_secs = secs % 60.0;
139            if remaining_secs == 0.0 {
140                format!("{}m", minutes)
141            } else {
142                format!("{}m{:.0}s", minutes, remaining_secs)
143            }
144        } else if secs >= 1.0 {
145            if secs == secs.floor() {
146                format!("{:.0}s", secs)
147            } else {
148                format!("{:.1}s", secs)
149            }
150        } else {
151            format!("{:.3}s", secs)
152        }
153    } else if total_nanos >= 1_000_000 {
154        // Milliseconds
155        format!("{}ms", d.as_millis())
156    } else if total_nanos >= 1_000 {
157        // Microseconds
158        format!("{}µs", d.as_micros())
159    } else {
160        // Nanoseconds
161        format!("{}ns", total_nanos)
162    }
163}
164
165/// Message used to start and stop timer instances.
166///
167/// This message is sent by the timer's control methods (`start()`, `stop()`, `toggle()`)
168/// to change the running state of a specific timer. The message includes the timer's
169/// unique ID to ensure it only affects the intended timer instance.
170///
171/// # Examples
172///
173/// ```rust
174/// use bubbletea_widgets::timer::new;
175/// use std::time::Duration;
176///
177/// let timer = new(Duration::from_secs(30));
178///
179/// // Use the public API to control the timer
180/// let start_cmd = timer.start();  // Creates StartStopMsg internally
181/// let stop_cmd = timer.stop();    // Creates StartStopMsg internally
182/// ```
183///
184/// # Note
185///
186/// The `running` field is intentionally private to ensure it can only be set
187/// through the timer's control methods, maintaining proper state management.
188#[derive(Debug, Clone)]
189pub struct StartStopMsg {
190    /// The unique identifier of the timer this message targets.
191    ///
192    /// Only timers with matching IDs will respond to this message,
193    /// allowing multiple timers to coexist safely.
194    pub id: i64,
195    /// Whether the timer should be running after processing this message.
196    ///
197    /// This field is private to ensure proper state management through
198    /// the timer's public control methods.
199    running: bool,
200}
201
202/// Message sent on every timer tick to update the countdown.
203///
204/// This message is generated automatically by the timer at regular intervals
205/// (determined by the timer's `interval` setting). Each tick reduces the remaining
206/// timeout duration and triggers the next tick command.
207///
208/// # Message Filtering
209///
210/// Timers automatically filter tick messages to ensure they only process their own:
211/// - Messages with mismatched IDs are ignored
212/// - Messages with incorrect tags are rejected (prevents double-ticking)
213/// - Messages sent to stopped timers are ignored
214///
215/// # Examples
216///
217/// ```rust
218/// use bubbletea_widgets::timer::{TickMsg, new};
219/// use std::time::Duration;
220///
221/// let timer = new(Duration::from_secs(30));
222/// let timer_id = timer.id();
223///
224/// // This message would be generated internally by the timer
225/// // Use the timer's tick() method instead of constructing manually:
226/// let tick_cmd = timer.init(); // Starts the timer and generates tick messages
227/// ```
228///
229/// # Timeout Detection
230///
231/// The `timeout` field indicates whether this tick represents the final
232/// expiration of the timer. You can either check this field or listen
233/// for separate `TimeoutMsg` messages.
234#[derive(Debug, Clone)]
235pub struct TickMsg {
236    /// The unique identifier of the timer that generated this tick.
237    ///
238    /// This allows multiple timers to run simultaneously without interfering
239    /// with each other. Each timer only processes ticks with its own ID.
240    pub id: i64,
241
242    /// Whether this tick represents a timeout (timer expiration).
243    ///
244    /// When `true`, this indicates the timer has reached zero and expired.
245    /// You can alternatively listen for `TimeoutMsg` for timeout notifications.
246    pub timeout: bool,
247
248    /// Internal synchronization tag to prevent message overflow.
249    ///
250    /// This field is used internally to ensure timers don't process too many
251    /// tick messages simultaneously, which could cause timing inaccuracies.
252    /// Application code should not modify this field.
253    tag: i64,
254}
255
256/// Message sent when a timer reaches zero and expires.
257///
258/// This is a convenience message that provides a clear notification when a timer
259/// completes its countdown. It's sent in addition to the final `TickMsg` (which
260/// will have `timeout: true`), giving applications two ways to detect timer expiration.
261///
262/// # Usage Pattern
263///
264/// Applications typically handle this message in their update loop to respond
265/// to timer completion events, such as showing notifications, triggering actions,
266/// or starting new timers.
267///
268/// # Examples
269///
270/// ```rust
271/// use bubbletea_widgets::timer::{TimeoutMsg, new};
272/// use bubbletea_rs::{Model as BubbleTeaModel, Msg};
273/// use std::time::Duration;
274///
275/// struct App {
276///     timer: bubbletea_widgets::timer::Model,
277///     message: String,
278/// }
279///
280/// impl BubbleTeaModel for App {
281///     fn update(&mut self, msg: Msg) -> Option<bubbletea_rs::Cmd> {
282///         // Handle timer timeout
283///         if let Some(timeout) = msg.downcast_ref::<TimeoutMsg>() {
284///             if timeout.id == self.timer.id() {
285///                 self.message = "Timer expired!".to_string();
286///             }
287///         }
288///         
289///         self.timer.update(msg)
290///     }
291///
292///     // ... other methods
293/// #   fn init() -> (Self, Option<bubbletea_rs::Cmd>) { unimplemented!() }
294/// #   fn view(&self) -> String { unimplemented!() }
295/// }
296/// ```
297///
298/// # Relationship to TickMsg
299///
300/// While `TickMsg` with `timeout: true` and `TimeoutMsg` both indicate timer
301/// expiration, `TimeoutMsg` provides a cleaner, more semantic way to handle
302/// completion events in your application logic.
303#[derive(Debug, Clone)]
304pub struct TimeoutMsg {
305    /// The unique identifier of the timer that expired.
306    ///
307    /// Use this to identify which timer expired when multiple timers
308    /// are running in the same application.
309    pub id: i64,
310}
311
312/// High-precision countdown timer component for Bubble Tea applications.
313///
314/// This struct represents a timer that counts down from an initial timeout value
315/// at regular intervals. It provides fine-grained control over timing behavior
316/// and integrates seamlessly with the Bubble Tea message-passing architecture.
317///
318/// # Core Features
319///
320/// - **Precise Timing**: Configurable tick intervals for smooth countdown display
321/// - **State Management**: Start, stop, and toggle operations with proper state tracking
322/// - **Message Filtering**: Automatic ID-based filtering prevents cross-timer interference
323/// - **Timeout Detection**: Multiple ways to detect and handle timer expiration
324/// - **Go Compatibility**: API matches Go's bubbles timer for easy migration
325///
326/// # Examples
327///
328/// Basic timer usage:
329/// ```rust
330/// use bubbletea_widgets::timer::{new, new_with_interval};
331/// use std::time::Duration;
332///
333/// // Create a 30-second timer with default 1-second ticks
334/// let timer = new(Duration::from_secs(30));
335/// assert_eq!(timer.timeout, Duration::from_secs(30));
336/// assert!(timer.running());
337///
338/// // Create a timer with custom tick rate
339/// let fast_timer = new_with_interval(
340///     Duration::from_secs(10),
341///     Duration::from_millis(100)
342/// );
343/// assert_eq!(fast_timer.interval, Duration::from_millis(100));
344/// ```
345///
346/// Integration with Bubble Tea:
347/// ```rust
348/// use bubbletea_widgets::timer::{new, Model as TimerModel, TimeoutMsg};
349/// use bubbletea_rs::{Model as BubbleTeaModel, Cmd, Msg};
350/// use std::time::Duration;
351///
352/// struct App {
353///     timer: TimerModel,
354///     status: String,
355/// }
356///
357/// impl BubbleTeaModel for App {
358///     fn init() -> (Self, Option<Cmd>) {
359///         let timer = new(Duration::from_secs(5));
360///         let cmd = timer.init();
361///         (App {
362///             timer,
363///             status: "Timer running...".to_string(),
364///         }, Some(cmd))
365///     }
366///
367///     fn update(&mut self, msg: Msg) -> Option<Cmd> {
368///         // Handle timeout events
369///         if let Some(timeout) = msg.downcast_ref::<TimeoutMsg>() {
370///             if timeout.id == self.timer.id() {
371///                 self.status = "Time's up!".to_string();
372///                 return None;
373///             }
374///         }
375///         
376///         // Forward other messages to timer
377///         self.timer.update(msg)
378///     }
379///
380///     fn view(&self) -> String {
381///         format!("{} - Remaining: {}", self.status, self.timer.view())
382///     }
383/// }
384/// ```
385///
386/// # State Management
387///
388/// The timer maintains several important states:
389/// - **Running**: Whether the timer is actively counting down
390/// - **Timeout**: The remaining time until expiration  
391/// - **ID**: Unique identifier for message filtering
392/// - **Tag**: Internal synchronization for accurate timing
393///
394/// # Thread Safety
395///
396/// The Model struct is `Clone` and can be safely passed between threads.
397/// Each timer instance maintains its own unique ID to prevent conflicts.
398///
399/// # Performance Considerations
400///
401/// - Faster intervals (< 100ms) provide smoother display but use more CPU
402/// - Multiple timers can run simultaneously without significant overhead
403/// - Message filtering ensures efficient processing in complex applications
404#[derive(Debug, Clone)]
405pub struct Model {
406    /// The remaining time until the timer expires.
407    ///
408    /// This value decreases by `interval` on each tick. When it reaches zero
409    /// or below, the timer is considered expired and will stop automatically.
410    pub timeout: Duration,
411
412    /// The time between each timer tick.
413    ///
414    /// This controls how frequently the timer updates its display and sends
415    /// tick messages. Smaller intervals provide smoother countdown display
416    /// but consume more resources. Default is 1 second.
417    pub interval: Duration,
418
419    /// Unique identifier for this timer instance.
420    ///
421    /// Used to filter messages and ensure timers only respond to their own
422    /// tick and control messages. Generated automatically on creation.
423    id: i64,
424    /// Internal synchronization tag for accurate timing.
425    ///
426    /// Used to prevent the timer from processing too many tick messages
427    /// simultaneously, which could cause timing drift or inaccuracies.
428    tag: i64,
429    /// Whether the timer is currently counting down.
430    ///
431    /// When `false`, the timer ignores tick messages and remains paused.
432    /// Can be controlled via `start()`, `stop()`, and `toggle()` methods.
433    running: bool,
434    /// The time when this timer was started.
435    ///
436    /// Used for accurate timing calculations. Set when the timer first
437    /// starts running and updated when resumed after pausing.
438    start_instant: Option<Instant>,
439    /// The time when the last tick was processed.
440    ///
441    /// Used to calculate actual elapsed time between ticks, providing
442    /// more accurate countdown timing than interval-based calculations.
443    last_tick: Option<Instant>,
444}
445
446/// Creates a new timer with custom timeout and tick interval.
447///
448/// This function provides full control over timer behavior by allowing you to specify
449/// both the initial countdown duration and how frequently the timer updates. Use this
450/// when you need precise control over timing granularity or want smoother display updates.
451///
452/// # Arguments
453///
454/// * `timeout` - The initial countdown duration (how long until the timer expires)
455/// * `interval` - How frequently the timer ticks and updates its display
456///
457/// # Returns
458///
459/// A new `Model` instance configured with the specified timing parameters
460///
461/// # Examples
462///
463/// ```rust
464/// use bubbletea_widgets::timer::new_with_interval;
465/// use std::time::Duration;
466///
467/// // Create a 30-second timer that updates every 100ms (smooth display)
468/// let smooth_timer = new_with_interval(
469///     Duration::from_secs(30),
470///     Duration::from_millis(100)
471/// );
472/// assert_eq!(smooth_timer.timeout, Duration::from_secs(30));
473/// assert_eq!(smooth_timer.interval, Duration::from_millis(100));
474/// assert!(smooth_timer.running());
475/// ```
476///
477/// Different use cases:
478/// ```rust
479/// use bubbletea_widgets::timer::new_with_interval;
480/// use std::time::Duration;
481///
482/// // High-precision timer for animations (60 FPS)
483/// let animation_timer = new_with_interval(
484///     Duration::from_secs(5),
485///     Duration::from_millis(16) // ~60 FPS
486/// );
487///
488/// // Battery-friendly timer for long countdowns
489/// let efficient_timer = new_with_interval(
490///     Duration::from_secs(3600), // 1 hour
491///     Duration::from_secs(5)     // Update every 5 seconds
492/// );
493///
494/// // Precise scientific timer
495/// let precise_timer = new_with_interval(
496///     Duration::from_millis(500),
497///     Duration::from_millis(10)
498/// );
499/// ```
500///
501/// # Performance Considerations
502///
503/// - **Smaller intervals** provide smoother display but use more CPU and battery
504/// - **Larger intervals** are more efficient but may appear jerky
505/// - Consider your application's needs when choosing interval duration
506/// - For display-only timers, 100ms-1000ms intervals work well
507/// - For animations, 16ms (60 FPS) provides smooth motion
508///
509/// # Timing Accuracy
510///
511/// The timer's accuracy depends on the underlying system's timer resolution
512/// and the Bubble Tea framework's message processing speed. Very small intervals
513/// (< 10ms) may not be achievable on all systems.
514///
515/// # Note
516///
517/// This function matches Go's `NewWithInterval` function exactly for compatibility
518/// with existing bubbles applications.
519pub fn new_with_interval(timeout: Duration, interval: Duration) -> Model {
520    Model {
521        timeout,
522        interval,
523        running: true,
524        id: next_id(),
525        tag: 0,
526        start_instant: None,
527        last_tick: None,
528    }
529}
530
531/// Creates a new timer with the specified timeout and default 1-second interval.
532///
533/// This is the most common way to create a timer. It uses a sensible default of
534/// 1-second intervals, which provides a good balance between display smoothness
535/// and resource usage for most applications.
536///
537/// # Arguments
538///
539/// * `timeout` - The initial countdown duration (how long until the timer expires)
540///
541/// # Returns
542///
543/// A new `Model` instance with 1-second tick intervals
544///
545/// # Examples
546///
547/// ```rust
548/// use bubbletea_widgets::timer::new;
549/// use std::time::Duration;
550///
551/// // Create a 30-second countdown timer
552/// let timer = new(Duration::from_secs(30));
553/// assert_eq!(timer.timeout, Duration::from_secs(30));
554/// assert_eq!(timer.interval, Duration::from_secs(1)); // Default interval
555/// assert!(timer.running());
556/// assert!(!timer.timedout());
557/// ```
558///
559/// Common timer durations:
560/// ```rust
561/// use bubbletea_widgets::timer::new;
562/// use std::time::Duration;
563///
564/// // Short timer for notifications
565/// let notification = new(Duration::from_secs(5));
566///
567/// // Medium timer for breaks
568/// let break_timer = new(Duration::from_secs(300)); // 5 minutes
569///
570/// // Long timer for cooking
571/// let cooking_timer = new(Duration::from_secs(1800)); // 30 minutes
572///
573/// // Sub-second timer for quick actions
574/// let quick_timer = new(Duration::from_millis(750));
575/// ```
576///
577/// Integration with Bubble Tea:
578/// ```rust
579/// use bubbletea_widgets::timer::new;
580/// use bubbletea_rs::{Model as BubbleTeaModel, Cmd};
581/// use std::time::Duration;
582///
583/// struct App {
584///     timer: bubbletea_widgets::timer::Model,
585/// }
586///
587/// impl BubbleTeaModel for App {
588///     fn init() -> (Self, Option<Cmd>) {
589///         let timer = new(Duration::from_secs(60));
590///         let init_cmd = timer.init(); // Start the timer
591///         (App { timer }, Some(init_cmd))
592///     }
593///     
594///     // ... other methods
595/// #   fn update(&mut self, _: bubbletea_rs::Msg) -> Option<Cmd> { None }
596/// #   fn view(&self) -> String { self.timer.view() }
597/// }
598/// ```
599///
600/// # Default Configuration
601///
602/// - **Interval**: 1 second (good balance of smoothness and efficiency)
603/// - **State**: Running (timer starts immediately)
604/// - **ID**: Unique identifier generated automatically
605/// - **Display**: Shows remaining time in Go's duration format
606///
607/// # When to Use
608///
609/// Use this function when:
610/// - You want standard 1-second timer updates
611/// - Resource efficiency is important
612/// - You don't need sub-second display precision
613/// - Building typical countdown or timeout functionality
614///
615/// Use `new_with_interval()` instead when you need:
616/// - Smoother display updates (< 1 second intervals)
617/// - High-precision timing
618/// - Custom update frequencies
619///
620/// # Note
621///
622/// This function matches Go's `New` function exactly for compatibility
623/// with existing bubbles applications.
624pub fn new(timeout: Duration) -> Model {
625    new_with_interval(timeout, Duration::from_secs(1))
626}
627
628impl Model {
629    /// Returns the unique identifier of this timer instance.
630    ///
631    /// Each timer gets a unique ID when created, allowing multiple timers to coexist
632    /// in the same application without interfering with each other. This ID is used
633    /// internally for message filtering and can be used by applications to identify
634    /// which timer generated specific messages.
635    ///
636    /// # Returns
637    ///
638    /// The unique `i64` identifier for this timer
639    ///
640    /// # Examples
641    ///
642    /// ```rust
643    /// use bubbletea_widgets::timer::new;
644    /// use std::time::Duration;
645    ///
646    /// let timer1 = new(Duration::from_secs(30));
647    /// let timer2 = new(Duration::from_secs(60));
648    ///
649    /// // Each timer has a unique ID
650    /// assert_ne!(timer1.id(), timer2.id());
651    /// assert!(timer1.id() > 0);
652    /// assert!(timer2.id() > 0);
653    /// ```
654    ///
655    /// Using ID to identify timer messages:
656    /// ```rust
657    /// use bubbletea_widgets::timer::{new, TimeoutMsg};
658    /// use bubbletea_rs::{Model as BubbleTeaModel, Msg};
659    /// use std::time::Duration;
660    ///
661    /// struct App {
662    ///     work_timer: bubbletea_widgets::timer::Model,
663    ///     break_timer: bubbletea_widgets::timer::Model,
664    /// }
665    ///
666    /// impl BubbleTeaModel for App {
667    ///     fn update(&mut self, msg: Msg) -> Option<bubbletea_rs::Cmd> {
668    ///         if let Some(timeout) = msg.downcast_ref::<TimeoutMsg>() {
669    ///             if timeout.id == self.work_timer.id() {
670    ///                 // Work period finished
671    ///             } else if timeout.id == self.break_timer.id() {
672    ///                 // Break period finished
673    ///             }
674    ///         }
675    ///         None
676    ///     }
677    ///     
678    ///     // ... other methods
679    /// #   fn init() -> (Self, Option<bubbletea_rs::Cmd>) { unimplemented!() }
680    /// #   fn view(&self) -> String { unimplemented!() }
681    /// }
682    /// ```
683    ///
684    /// # Thread Safety
685    ///
686    /// The ID is assigned atomically during timer creation and remains constant
687    /// throughout the timer's lifetime, making it safe to use across threads.
688    ///
689    /// # Note
690    ///
691    /// This method matches Go's `ID()` method exactly for compatibility.
692    pub fn id(&self) -> i64 {
693        self.id
694    }
695
696    /// Returns whether the timer is currently counting down.
697    ///
698    /// A timer is considered running when it's actively counting down and has not
699    /// expired. This method returns `false` if the timer has been manually stopped
700    /// or if it has reached zero and timed out.
701    ///
702    /// # Returns
703    ///
704    /// `true` if the timer is actively counting down, `false` otherwise
705    ///
706    /// # Examples
707    ///
708    /// ```rust
709    /// use bubbletea_widgets::timer::new;
710    /// use std::time::Duration;
711    ///
712    /// let mut timer = new(Duration::from_secs(30));
713    ///
714    /// // Timer starts in running state
715    /// assert!(timer.running());
716    ///
717    /// // Manually stopping the timer
718    /// let _stop_cmd = timer.stop();
719    /// // Note: timer.running() would still return true until the stop message is processed
720    /// ```
721    ///
722    /// Checking timer state in different scenarios:
723    /// ```rust
724    /// use bubbletea_widgets::timer::new;
725    /// use std::time::Duration;
726    ///
727    /// let mut timer = new(Duration::from_secs(5));
728    /// assert!(timer.running()); // Initially running
729    ///
730    /// // Simulate timer expiration
731    /// timer.timeout = Duration::ZERO;
732    /// assert!(!timer.running()); // Not running when expired
733    ///
734    /// // Reset timeout but manually stop the timer
735    /// timer.timeout = Duration::from_secs(10);
736    /// // Use stop() method instead of accessing private field
737    /// let _stop_cmd = timer.stop();
738    /// // Note: timer.running() may still return true until stop message is processed
739    /// ```
740    ///
741    /// Integration with control commands:
742    /// ```rust
743    /// use bubbletea_widgets::timer::new;
744    /// use std::time::Duration;
745    ///
746    /// let timer = new(Duration::from_secs(60));
747    ///
748    /// // These commands change the running state
749    /// let start_cmd = timer.start();   // Will make running() return true
750    /// let stop_cmd = timer.stop();     // Will make running() return false
751    /// let toggle_cmd = timer.toggle(); // Will flip the running state
752    /// ```
753    ///
754    /// # State Priority
755    ///
756    /// The running state is determined by multiple factors in this priority order:
757    /// 1. **Timeout**: If the timer has expired (`timedout() == true`), it's not running
758    /// 2. **Manual State**: If manually stopped via `stop()`, it's not running
759    /// 3. **Default**: Otherwise, it follows the internal running flag
760    ///
761    /// # Note
762    ///
763    /// This method matches Go's `Running()` method exactly for compatibility.
764    pub fn running(&self) -> bool {
765        if self.timedout() || !self.running {
766            return false;
767        }
768        true
769    }
770
771    /// Returns whether the timer has reached zero and expired.
772    ///
773    /// A timer is considered timed out when its remaining timeout duration has
774    /// reached zero or below. Once timed out, the timer automatically stops
775    /// running and will not process further tick messages.
776    ///
777    /// # Returns
778    ///
779    /// `true` if the timer has expired, `false` if time remains
780    ///
781    /// # Examples
782    ///
783    /// ```rust
784    /// use bubbletea_widgets::timer::new;
785    /// use std::time::Duration;
786    ///
787    /// let mut timer = new(Duration::from_secs(30));
788    ///
789    /// // Timer starts with time remaining
790    /// assert!(!timer.timedout());
791    ///
792    /// // Simulate timer expiration
793    /// timer.timeout = Duration::ZERO;
794    /// assert!(timer.timedout());
795    /// ```
796    ///
797    /// Checking expiration in different states:
798    /// ```rust
799    /// use bubbletea_widgets::timer::new;
800    /// use std::time::Duration;
801    ///
802    /// let mut timer = new(Duration::from_millis(100));
803    ///
804    /// // With remaining time
805    /// assert!(!timer.timedout());
806    ///
807    /// // Exactly at zero
808    /// timer.timeout = Duration::ZERO;
809    /// assert!(timer.timedout());
810    ///
811    /// // Very small remaining time
812    /// timer.timeout = Duration::from_nanos(1);
813    /// assert!(!timer.timedout());
814    /// ```
815    ///
816    /// Using in timeout detection:
817    /// ```rust
818    /// use bubbletea_widgets::timer::new;
819    /// use std::time::Duration;
820    ///
821    /// let mut timer = new(Duration::from_secs(5));
822    ///
823    /// // Application loop simulation
824    /// while !timer.timedout() {
825    ///     // Process timer ticks
826    ///     // timer.update(tick_msg) would reduce timeout
827    ///     
828    ///     // Break for this example to avoid infinite loop
829    ///     timer.timeout = Duration::ZERO;
830    /// }
831    ///
832    /// assert!(timer.timedout());
833    /// assert!(!timer.running()); // Timed out timers are not running
834    /// ```
835    ///
836    /// # Relationship to Running State
837    ///
838    /// When a timer times out:
839    /// - `timedout()` returns `true`
840    /// - `running()` returns `false` (expired timers don't run)
841    /// - The timer stops processing tick messages automatically
842    /// - A `TimeoutMsg` is typically sent to notify the application
843    ///
844    /// # Precision Note
845    ///
846    /// The timeout check uses `Duration::ZERO` as the threshold. Due to the
847    /// discrete nature of tick intervals, the actual remaining time when
848    /// expiration is detected may be slightly negative (saturated to zero).
849    ///
850    /// # Note
851    ///
852    /// This method matches Go's `Timedout()` method exactly for compatibility.
853    pub fn timedout(&self) -> bool {
854        self.timeout <= Duration::ZERO
855    }
856
857    /// Generates a command to start or resume the timer.
858    ///
859    /// This method returns a command that, when executed by the Bubble Tea runtime,
860    /// will send a `StartStopMsg` to resume the timer's countdown. If the timer is
861    /// already running, this has no effect. If the timer has timed out, the command
862    /// has no effect.
863    ///
864    /// # Returns
865    ///
866    /// A `Cmd` that will start the timer when executed
867    ///
868    /// # Examples
869    ///
870    /// ```rust
871    /// use bubbletea_widgets::timer::new;
872    /// use bubbletea_rs::{Model as BubbleTeaModel, Cmd};
873    /// use std::time::Duration;
874    ///
875    /// struct App {
876    ///     timer: bubbletea_widgets::timer::Model,
877    /// }
878    ///
879    /// impl BubbleTeaModel for App {
880    ///     fn init() -> (Self, Option<Cmd>) {
881    ///         let timer = new(Duration::from_secs(60));
882    ///         // Start the timer immediately
883    ///         let start_cmd = timer.start();
884    ///         (App { timer }, Some(start_cmd))
885    ///     }
886    ///
887    ///     fn update(&mut self, msg: bubbletea_rs::Msg) -> Option<Cmd> {
888    ///         // Handle user input to start timer
889    ///         // if space_key_pressed {
890    ///         //     return Some(self.timer.start());
891    ///         // }
892    ///         
893    ///         self.timer.update(msg)
894    ///     }
895    ///     
896    ///     // ... other methods
897    /// #   fn view(&self) -> String { self.timer.view() }
898    /// }
899    /// ```
900    ///
901    /// Manual timer control:
902    /// ```rust
903    /// use bubbletea_widgets::timer::new;
904    /// use std::time::Duration;
905    ///
906    /// let timer = new(Duration::from_secs(30));
907    ///
908    /// // Generate control commands
909    /// let start_cmd = timer.start();  // Resume countdown
910    /// let stop_cmd = timer.stop();    // Pause countdown
911    /// let toggle_cmd = timer.toggle(); // Toggle running state
912    ///
913    /// // Commands are executed by the Bubble Tea runtime
914    /// // The actual state change happens when the StartStopMsg is processed
915    /// ```
916    ///
917    /// # Command Execution
918    ///
919    /// The returned command is not executed immediately. Instead, it's returned
920    /// to the Bubble Tea runtime, which will execute it asynchronously. The actual
921    /// state change occurs when the resulting `StartStopMsg` is processed by the
922    /// timer's `update()` method.
923    ///
924    /// # State Change Sequence
925    ///
926    /// 1. `start()` is called → returns `Cmd`
927    /// 2. Bubble Tea executes the command → generates `StartStopMsg`
928    /// 3. Message is sent to `update()` → timer state changes to running
929    /// 4. Timer begins processing tick messages and counting down
930    ///
931    /// # No Effect Scenarios
932    ///
933    /// The start command has no effect when:
934    /// - The timer has already timed out (`timedout() == true`)
935    /// - The timer is already running (redundant operation)
936    ///
937    /// # Note
938    ///
939    /// This method matches Go's `Start()` method exactly for compatibility.
940    pub fn start(&self) -> Cmd {
941        self.start_stop(true)
942    }
943
944    /// Generates a command to stop or pause the timer.
945    ///
946    /// This method returns a command that, when executed by the Bubble Tea runtime,
947    /// will send a `StartStopMsg` to pause the timer's countdown. The timer retains
948    /// its current timeout value and can be resumed later with `start()`. If the
949    /// timer has already timed out, this command has no effect.
950    ///
951    /// # Returns
952    ///
953    /// A `Cmd` that will stop the timer when executed
954    ///
955    /// # Examples
956    ///
957    /// ```rust
958    /// use bubbletea_widgets::timer::new;
959    /// use bubbletea_rs::{Model as BubbleTeaModel, Cmd, KeyMsg};
960    /// use crossterm::event::{KeyCode, KeyModifiers};
961    /// use std::time::Duration;
962    ///
963    /// struct App {
964    ///     timer: bubbletea_widgets::timer::Model,
965    ///     paused: bool,
966    /// }
967    ///
968    /// impl BubbleTeaModel for App {
969    ///     fn update(&mut self, msg: bubbletea_rs::Msg) -> Option<Cmd> {
970    ///         // Handle spacebar to pause/resume
971    ///         if let Some(key) = msg.downcast_ref::<KeyMsg>() {
972    ///             if key.key == KeyCode::Char(' ') {
973    ///                 self.paused = !self.paused;
974    ///                 return if self.paused {
975    ///                     Some(self.timer.stop())
976    ///                 } else {
977    ///                     Some(self.timer.start())
978    ///                 };
979    ///             }
980    ///         }
981    ///         
982    ///         self.timer.update(msg)
983    ///     }
984    ///     
985    ///     // ... other methods
986    /// #   fn init() -> (Self, Option<Cmd>) { unimplemented!() }
987    /// #   fn view(&self) -> String { format!("Timer: {} {}", self.timer.view(), if self.paused { "(PAUSED)" } else { "" }) }
988    /// }
989    /// ```
990    ///
991    /// Timer control pattern:
992    /// ```rust
993    /// use bubbletea_widgets::timer::new;
994    /// use std::time::Duration;
995    ///
996    /// let timer = new(Duration::from_secs(300)); // 5 minute timer
997    ///
998    /// // Control commands for different scenarios
999    /// let pause_cmd = timer.stop();    // Pause for a break
1000    /// let resume_cmd = timer.start();  // Resume after break
1001    /// let toggle_cmd = timer.toggle(); // Quick pause/resume
1002    /// ```
1003    ///
1004    /// # Pause vs. Reset
1005    ///
1006    /// Important distinction:
1007    /// - **Stop/Pause**: Halts countdown but preserves remaining time
1008    /// - **Reset**: Would require creating a new timer with original timeout
1009    ///
1010    /// ```rust
1011    /// use bubbletea_widgets::timer::new;
1012    /// use std::time::Duration;
1013    ///
1014    /// let mut timer = new(Duration::from_secs(60));
1015    ///
1016    /// // Simulate some time passing
1017    /// timer.timeout = Duration::from_secs(45); // 15 seconds elapsed
1018    ///
1019    /// // Stopping preserves the remaining 45 seconds
1020    /// let _stop_cmd = timer.stop();
1021    /// // timer.timeout is still 45 seconds after stop command is processed
1022    ///
1023    /// // To reset, create a new timer
1024    /// let reset_timer = new(Duration::from_secs(60));
1025    /// ```
1026    ///
1027    /// # Command Execution
1028    ///
1029    /// Like all timer control methods, the returned command is executed
1030    /// asynchronously by the Bubble Tea runtime. The actual pause occurs
1031    /// when the `StartStopMsg` is processed.
1032    ///
1033    /// # No Effect Scenarios
1034    ///
1035    /// The stop command has no effect when:
1036    /// - The timer has already timed out (`timedout() == true`)
1037    /// - The timer is already stopped (redundant operation)
1038    ///
1039    /// # Note
1040    ///
1041    /// This method matches Go's `Stop()` method exactly for compatibility.
1042    pub fn stop(&self) -> Cmd {
1043        self.start_stop(false)
1044    }
1045
1046    /// Generates a command to toggle the timer's running state.
1047    ///
1048    /// This method provides a convenient way to switch between running and stopped
1049    /// states. If the timer is currently running, it will be stopped. If it's stopped,
1050    /// it will be started. This is particularly useful for pause/resume functionality
1051    /// controlled by a single user action.
1052    ///
1053    /// # Returns
1054    ///
1055    /// A `Cmd` that will toggle the timer's state when executed
1056    ///
1057    /// # Examples
1058    ///
1059    /// ```rust
1060    /// use bubbletea_widgets::timer::new;
1061    /// use bubbletea_rs::{Model as BubbleTeaModel, Cmd, KeyMsg};
1062    /// use crossterm::event::{KeyCode, KeyModifiers};
1063    /// use std::time::Duration;
1064    ///
1065    /// struct PomodoroApp {
1066    ///     work_timer: bubbletea_widgets::timer::Model,
1067    /// }
1068    ///
1069    /// impl BubbleTeaModel for PomodoroApp {
1070    ///     fn init() -> (Self, Option<Cmd>) {
1071    ///         let timer = new(Duration::from_secs(25 * 60)); // 25 minute work session
1072    ///         let start_cmd = timer.init();
1073    ///         (PomodoroApp { work_timer: timer }, Some(start_cmd))
1074    ///     }
1075    ///
1076    ///     fn update(&mut self, msg: bubbletea_rs::Msg) -> Option<Cmd> {
1077    ///         // Spacebar toggles timer
1078    ///         if let Some(key) = msg.downcast_ref::<KeyMsg>() {
1079    ///             if key.key == KeyCode::Char(' ') {
1080    ///                 return Some(self.work_timer.toggle());
1081    ///             }
1082    ///         }
1083    ///         
1084    ///         self.work_timer.update(msg)
1085    ///     }
1086    ///
1087    ///     fn view(&self) -> String {
1088    ///         format!(
1089    ///             "Work Timer: {}\n\n[SPACE] to {}",
1090    ///             self.work_timer.view(),
1091    ///             if self.work_timer.running() { "pause" } else { "resume" }
1092    ///         )
1093    ///     }
1094    /// }
1095    /// ```
1096    ///
1097    /// Simple toggle pattern:
1098    /// ```rust
1099    /// use bubbletea_widgets::timer::new;
1100    /// use std::time::Duration;
1101    ///
1102    /// let timer = new(Duration::from_secs(120));
1103    ///
1104    /// // One command handles both pause and resume
1105    /// let toggle_cmd = timer.toggle();
1106    ///
1107    /// // Equivalent to:
1108    /// // if timer.running() {
1109    /// //     timer.stop()
1110    /// // } else {
1111    /// //     timer.start()
1112    /// // }
1113    /// ```
1114    ///
1115    /// # State Determination
1116    ///
1117    /// The toggle decision is based on the current result of `running()`:
1118    /// - If `running()` returns `true` → generates stop command
1119    /// - If `running()` returns `false` → generates start command
1120    ///
1121    /// This means the toggle respects both manual stops and timeout states:
1122    /// ```rust
1123    /// use bubbletea_widgets::timer::new;
1124    /// use std::time::Duration;
1125    ///
1126    /// let mut timer = new(Duration::from_secs(30));
1127    ///
1128    /// // Normal toggle (running → stopped)
1129    /// assert!(timer.running());
1130    /// let _toggle1 = timer.toggle(); // Will stop
1131    ///
1132    /// // Toggle on expired timer (not running → no effect)
1133    /// timer.timeout = Duration::ZERO;
1134    /// assert!(!timer.running()); // Not running due to timeout
1135    /// let _toggle2 = timer.toggle(); // Will attempt start, but no effect due to timeout
1136    /// ```
1137    ///
1138    /// # User Interface Benefits
1139    ///
1140    /// Toggle is ideal for:
1141    /// - Single-key pause/resume (spacebar pattern)
1142    /// - Play/pause buttons in UI
1143    /// - Touch/click interfaces
1144    /// - Reducing cognitive load (one action vs. two separate actions)
1145    ///
1146    /// # Command Execution
1147    ///
1148    /// The toggle command evaluates the current state when called, not when executed.
1149    /// The actual state change occurs asynchronously when the resulting `StartStopMsg`
1150    /// is processed by the timer's `update()` method.
1151    ///
1152    /// # Note
1153    ///
1154    /// This method matches Go's `Toggle()` method exactly for compatibility.
1155    pub fn toggle(&self) -> Cmd {
1156        self.start_stop(!self.running())
1157    }
1158
1159    /// Internal tick function - matches Go's tick method.
1160    fn tick(&self) -> Cmd {
1161        let id = self.id;
1162        let tag = self.tag;
1163        let timeout = self.timedout();
1164        let interval = self.interval;
1165
1166        bubbletea_tick(interval, move |_| {
1167            Box::new(TickMsg { id, timeout, tag }) as Msg
1168        })
1169    }
1170
1171    /// Internal timedout command - matches Go's timedout method.
1172    #[allow(dead_code)]
1173    fn timedout_cmd(&self) -> std::option::Option<Cmd> {
1174        if !self.timedout() {
1175            return std::option::Option::None;
1176        }
1177        let id = self.id;
1178        std::option::Option::Some(bubbletea_tick(Duration::from_nanos(1), move |_| {
1179            Box::new(TimeoutMsg { id }) as Msg
1180        }))
1181    }
1182
1183    /// Internal start/stop command - matches Go's startStop method.
1184    fn start_stop(&self, running: bool) -> Cmd {
1185        let id = self.id;
1186        bubbletea_tick(Duration::from_nanos(1), move |_| {
1187            Box::new(StartStopMsg { id, running }) as Msg
1188        })
1189    }
1190
1191    /// Initializes the timer and returns the command to start its first tick.
1192    ///
1193    /// This method should be called once when the timer is first created to begin
1194    /// the countdown process. It generates the initial tick command that starts
1195    /// the timer's regular interval-based updates.
1196    ///
1197    /// # Returns
1198    ///
1199    /// A `Cmd` that will start the timer's tick cycle when executed
1200    ///
1201    /// # Examples
1202    ///
1203    /// ```rust
1204    /// use bubbletea_widgets::timer::new;
1205    /// use bubbletea_rs::{Model as BubbleTeaModel, Cmd};
1206    /// use std::time::Duration;
1207    ///
1208    /// struct App {
1209    ///     timer: bubbletea_widgets::timer::Model,
1210    /// }
1211    ///
1212    /// impl BubbleTeaModel for App {
1213    ///     fn init() -> (Self, Option<Cmd>) {
1214    ///         let timer = new(Duration::from_secs(60));
1215    ///         // Initialize the timer to start ticking
1216    ///         let timer_cmd = timer.init();
1217    ///         (App { timer }, Some(timer_cmd))
1218    ///     }
1219    ///     
1220    ///     // ... other methods
1221    /// #   fn update(&mut self, _: bubbletea_rs::Msg) -> Option<Cmd> { None }
1222    /// #   fn view(&self) -> String { self.timer.view() }
1223    /// }
1224    /// ```
1225    ///
1226    /// Multiple timer initialization:
1227    /// ```rust
1228    /// use bubbletea_widgets::timer::new;
1229    /// use bubbletea_rs::{Model as BubbleTeaModel, Cmd};
1230    /// use std::time::Duration;
1231    ///
1232    /// struct MultiTimerApp {
1233    ///     work_timer: bubbletea_widgets::timer::Model,
1234    ///     break_timer: bubbletea_widgets::timer::Model,
1235    /// }
1236    ///
1237    /// impl BubbleTeaModel for MultiTimerApp {
1238    ///     fn init() -> (Self, Option<Cmd>) {
1239    ///         let work_timer = new(Duration::from_secs(25 * 60));
1240    ///         let break_timer = new(Duration::from_secs(5 * 60));
1241    ///         
1242    ///         // Start the work timer initially
1243    ///         let init_cmd = work_timer.init();
1244    ///         
1245    ///         (MultiTimerApp { work_timer, break_timer }, Some(init_cmd))
1246    ///     }
1247    ///     
1248    ///     // ... other methods
1249    /// #   fn update(&mut self, _: bubbletea_rs::Msg) -> Option<Cmd> { None }
1250    /// #   fn view(&self) -> String { self.work_timer.view() }
1251    /// }
1252    /// ```
1253    ///
1254    /// # When to Call
1255    ///
1256    /// - **Application startup**: In your app's `init()` method
1257    /// - **Timer creation**: Immediately after creating a new timer
1258    /// - **Timer reset**: When restarting a timer with new settings
1259    ///
1260    /// # What It Does
1261    ///
1262    /// The `init()` method:
1263    /// 1. Creates the first tick command
1264    /// 2. Sets up the timer's internal tick cycle
1265    /// 3. Returns immediately (non-blocking)
1266    /// 4. The actual ticking starts when Bubble Tea executes the command
1267    ///
1268    /// # Timing Behavior
1269    ///
1270    /// The first tick occurs after the timer's `interval` duration. For example,
1271    /// with a 1-second interval, the first countdown update happens 1 second after
1272    /// the init command is executed.
1273    ///
1274    /// # Alternative to Start
1275    ///
1276    /// While `start()` is used to resume a paused timer, `init()` is specifically
1277    /// for initial timer setup and should be called once per timer instance.
1278    ///
1279    /// # Note
1280    ///
1281    /// This method matches Go's `Init()` method exactly for compatibility.
1282    pub fn init(&self) -> Cmd {
1283        self.tick()
1284    }
1285
1286    /// Processes messages and updates the timer state.
1287    ///
1288    /// This method handles all messages related to timer operation, including tick
1289    /// messages that advance the countdown and control messages that change the
1290    /// running state. It should be called from your application's update loop
1291    /// for proper timer functionality.
1292    ///
1293    /// # Arguments
1294    ///
1295    /// * `msg` - The message to process (typically `TickMsg` or `StartStopMsg`)
1296    ///
1297    /// # Returns
1298    ///
1299    /// An optional `Cmd` for the next timer operation, or `None` if no action needed
1300    ///
1301    /// # Message Types Handled
1302    ///
1303    /// - **`StartStopMsg`**: Changes the timer's running state
1304    /// - **`TickMsg`**: Advances the countdown and schedules the next tick
1305    /// - **Other messages**: Ignored (returns `None`)
1306    ///
1307    /// # Examples
1308    ///
1309    /// ```rust
1310    /// use bubbletea_widgets::timer::{new, TickMsg, StartStopMsg, TimeoutMsg};
1311    /// use bubbletea_rs::{Model as BubbleTeaModel, Msg, Cmd};
1312    /// use std::time::Duration;
1313    ///
1314    /// struct App {
1315    ///     timer: bubbletea_widgets::timer::Model,
1316    ///     status: String,
1317    /// }
1318    ///
1319    /// impl BubbleTeaModel for App {
1320    ///     fn update(&mut self, msg: Msg) -> Option<Cmd> {
1321    ///         // Handle application-specific timer events
1322    ///         if let Some(timeout) = msg.downcast_ref::<TimeoutMsg>() {
1323    ///             if timeout.id == self.timer.id() {
1324    ///                 self.status = "Timer completed!".to_string();
1325    ///             }
1326    ///         }
1327    ///         
1328    ///         // Forward all messages to timer for processing
1329    ///         self.timer.update(msg)
1330    ///     }
1331    ///     
1332    ///     // ... other methods
1333    /// #   fn init() -> (Self, Option<Cmd>) { unimplemented!() }
1334    /// #   fn view(&self) -> String { format!("{}: {}", self.status, self.timer.view()) }
1335    /// }
1336    /// ```
1337    ///
1338    /// Manual message handling:
1339    /// ```rust,ignore
1340    /// use bubbletea_widgets::timer::new;
1341    /// use std::time::Duration;
1342    ///
1343    /// let mut timer = new(Duration::from_secs(10));
1344    ///
1345    /// // Start the timer using the public API
1346    /// let start_cmd = timer.start();
1347    /// // In a real app, you'd send this command through bubbletea
1348    ///
1349    /// // Check initial timeout
1350    /// assert_eq!(timer.timeout(), Duration::from_secs(10));
1351    /// ```
1352    ///
1353    /// # Message Filtering
1354    ///
1355    /// The timer automatically filters messages to ensure it only processes
1356    /// messages intended for it:
1357    ///
1358    /// - **ID Matching**: Only processes messages with matching timer IDs
1359    /// - **Tag Validation**: Rejects tick messages with incorrect tags
1360    /// - **State Checks**: Ignores ticks when not running
1361    ///
1362    /// ```rust
1363    /// use bubbletea_widgets::timer::new;
1364    /// use std::time::Duration;
1365    ///
1366    /// let mut timer1 = new(Duration::from_secs(10));
1367    /// let _timer2 = new(Duration::from_secs(20));
1368    ///
1369    /// // Start timer1
1370    /// let _cmd = timer1.start();
1371    ///
1372    /// // Messages with wrong IDs are ignored
1373    /// // In a real app, messages are routed by ID
1374    /// assert_eq!(timer1.timeout, Duration::from_secs(10)); // No change
1375    /// ```
1376    ///
1377    /// # State Changes
1378    ///
1379    /// Different message types cause different state changes:
1380    ///
1381    /// - **`StartStopMsg`**: Changes `running` state, returns tick command
1382    /// - **`TickMsg`**: Reduces `timeout` by `interval`, returns next tick command
1383    /// - **Invalid messages**: No state change, returns `None`
1384    ///
1385    /// # Timeout Detection
1386    ///
1387    /// When a tick reduces the timeout to zero or below, the timer:
1388    /// 1. Automatically stops running
1389    /// 2. May send a `TimeoutMsg` (implementation detail)
1390    /// 3. Returns a tick command that will be ignored (since not running)
1391    ///
1392    /// # Error Handling
1393    ///
1394    /// This method never panics and handles invalid messages gracefully by
1395    /// ignoring them and returning `None`.
1396    ///
1397    /// # Performance
1398    ///
1399    /// Message processing is very fast, involving only basic comparisons and
1400    /// arithmetic operations. The method is designed to be called frequently
1401    /// in the Bubble Tea update loop.
1402    ///
1403    /// # Note
1404    ///
1405    /// This method matches Go's `Update()` method exactly for compatibility.
1406    pub fn update(&mut self, msg: Msg) -> std::option::Option<Cmd> {
1407        if let Some(start_stop_msg) = msg.downcast_ref::<StartStopMsg>() {
1408            if start_stop_msg.id != 0 && start_stop_msg.id != self.id {
1409                return std::option::Option::None;
1410            }
1411
1412            let was_running = self.running;
1413            self.running = start_stop_msg.running;
1414
1415            // Reset timing when starting from stopped state
1416            if !was_running && self.running {
1417                self.start_instant = None;
1418                self.last_tick = None;
1419            }
1420
1421            return std::option::Option::Some(self.tick());
1422        }
1423
1424        if let Some(tick_msg) = msg.downcast_ref::<TickMsg>() {
1425            if !self.running() || (tick_msg.id != 0 && tick_msg.id != self.id) {
1426                return std::option::Option::None;
1427            }
1428
1429            // If a tag is set, and it's not the one we expect, reject the message.
1430            // This prevents the ticker from receiving too many messages and
1431            // thus ticking too fast.
1432            if tick_msg.tag > 0 && tick_msg.tag != self.tag {
1433                return std::option::Option::None;
1434            }
1435
1436            // Use high-precision elapsed time tracking for accurate countdown
1437            let now = Instant::now();
1438
1439            // Initialize timing on first tick
1440            if self.last_tick.is_none() {
1441                self.start_instant = Some(now);
1442                self.last_tick = Some(now);
1443                // On first tick, just use the interval as fallback
1444                self.timeout = self.timeout.saturating_sub(self.interval);
1445            } else {
1446                // Calculate actual elapsed time since last tick
1447                let actual_elapsed = now.duration_since(self.last_tick.unwrap());
1448                self.timeout = self.timeout.saturating_sub(actual_elapsed);
1449                self.last_tick = Some(now);
1450            }
1451
1452            // In Go this uses tea.Batch to return multiple commands.
1453            // For simplicity in Rust, we'll return just the tick command.
1454            // The TimeoutMsg will be sent automatically when timeout is detected.
1455            return std::option::Option::Some(self.tick());
1456        }
1457
1458        std::option::Option::None
1459    }
1460
1461    /// Renders the timer as a formatted string showing remaining time.
1462    ///
1463    /// This method converts the timer's current timeout duration into a
1464    /// human-readable string using Go's duration formatting conventions.
1465    /// The output is suitable for direct display in terminal applications.
1466    ///
1467    /// # Returns
1468    ///
1469    /// A formatted string representation of the remaining time
1470    ///
1471    /// # Format Examples
1472    ///
1473    /// The format matches Go's `Duration.String()` output:
1474    /// - `"5m30s"` for 5 minutes 30 seconds
1475    /// - `"2m"` for exactly 2 minutes
1476    /// - `"45.5s"` for 45.5 seconds
1477    /// - `"750ms"` for milliseconds
1478    /// - `"0s"` when expired
1479    ///
1480    /// # Examples
1481    ///
1482    /// ```rust
1483    /// use bubbletea_widgets::timer::new;
1484    /// use std::time::Duration;
1485    ///
1486    /// // Various timer displays
1487    /// let timer1 = new(Duration::from_secs(90));
1488    /// assert!(timer1.view().contains("1m"));
1489    ///
1490    /// let timer2 = new(Duration::from_millis(500));
1491    /// assert!(timer2.view().contains("500ms"));
1492    ///
1493    /// let mut timer3 = new(Duration::from_secs(1));
1494    /// timer3.timeout = Duration::ZERO;
1495    /// assert_eq!(timer3.view(), "0s");
1496    /// ```
1497    ///
1498    /// Integration in UI:
1499    /// ```rust
1500    /// use bubbletea_widgets::timer::new;
1501    /// use bubbletea_rs::{Model as BubbleTeaModel};
1502    /// use std::time::Duration;
1503    ///
1504    /// struct App {
1505    ///     cooking_timer: bubbletea_widgets::timer::Model,
1506    ///     recipe: String,
1507    /// }
1508    ///
1509    /// impl BubbleTeaModel for App {
1510    ///     fn view(&self) -> String {
1511    ///         format!(
1512    ///             "Cooking: {}\n\nTime remaining: {}\n\n[SPACE] to pause",
1513    ///             self.recipe,
1514    ///             self.cooking_timer.view()
1515    ///         )
1516    ///     }
1517    ///     
1518    ///     // ... other methods
1519    /// #   fn init() -> (Self, Option<bubbletea_rs::Cmd>) { unimplemented!() }
1520    /// #   fn update(&mut self, _: bubbletea_rs::Msg) -> Option<bubbletea_rs::Cmd> { None }
1521    /// }
1522    /// ```
1523    ///
1524    /// Dynamic display updates:
1525    /// ```rust
1526    /// use bubbletea_widgets::timer::new;
1527    /// use std::time::Duration;
1528    ///
1529    /// let mut timer = new(Duration::from_secs(125)); // 2m5s
1530    ///
1531    /// // Display updates as timer counts down
1532    /// println!("Start: {}", timer.view()); // "2m5s"
1533    ///
1534    /// // Simulate 30 seconds passing
1535    /// timer.timeout -= Duration::from_secs(30);
1536    /// println!("After 30s: {}", timer.view()); // "1m35s"
1537    ///
1538    /// // Simulate completion
1539    /// timer.timeout = Duration::ZERO;
1540    /// println!("Finished: {}", timer.view()); // "0s"
1541    /// ```
1542    ///
1543    /// # Precision Display
1544    ///
1545    /// The display precision depends on the remaining time:
1546    /// - **Minutes**: Shows minutes and seconds (e.g., "3m45s")
1547    /// - **Seconds**: Shows seconds with decimals if needed (e.g., "1.5s")
1548    /// - **Milliseconds**: Shows millisecond precision (e.g., "250ms")
1549    /// - **Microseconds/Nanoseconds**: For very short durations
1550    ///
1551    /// # Use Cases
1552    ///
1553    /// Perfect for:
1554    /// - Countdown displays in TUIs
1555    /// - Progress indicators with time remaining
1556    /// - Cooking/work timers
1557    /// - Game time limits
1558    /// - Session timeouts
1559    ///
1560    /// # Performance
1561    ///
1562    /// String formatting is performed on each call, so for high-frequency
1563    /// updates, consider caching the result if the timeout hasn't changed.
1564    ///
1565    /// # Localization
1566    ///
1567    /// The format uses English abbreviations ("m", "s", "ms") and follows
1568    /// Go's conventions. For different locales, you may need to parse the
1569    /// `timeout` duration and format it according to local preferences.
1570    ///
1571    /// # Note
1572    ///
1573    /// This method matches Go's `View()` method exactly for compatibility.
1574    pub fn view(&self) -> String {
1575        format_duration(self.timeout)
1576    }
1577}
1578
1579impl BubbleTeaModel for Model {
1580    /// Creates a new timer model with default settings for standalone use.
1581    ///
1582    /// This implementation provides a default timer configuration suitable for
1583    /// applications that want to use the timer as a standalone component without
1584    /// custom initialization. It creates a 60-second timer with 1-second intervals.
1585    ///
1586    /// # Returns
1587    ///
1588    /// A tuple containing the new timer model and its initialization command
1589    ///
1590    /// # Examples
1591    ///
1592    /// ```rust
1593    /// use bubbletea_widgets::timer::Model as TimerModel;
1594    /// use bubbletea_rs::{Model as BubbleTeaModel};
1595    ///
1596    /// // Use timer as a standalone Bubble Tea application
1597    /// let model = TimerModel::default();
1598    /// let cmd = model.init();
1599    /// // Would start a 60-second timer app
1600    /// ```
1601    ///
1602    /// # Default Configuration
1603    ///
1604    /// - **Timeout**: 60 seconds
1605    /// - **Interval**: 1 second
1606    /// - **State**: Running (starts immediately)
1607    /// - **Display**: Shows countdown in "1m0s" format
1608    ///
1609    /// # Note
1610    ///
1611    /// Most applications will want to use `new()` or `new_with_interval()` instead
1612    /// to create timers with specific durations rather than this default.
1613    fn init() -> (Self, std::option::Option<Cmd>) {
1614        let model = new(Duration::from_secs(60));
1615        let cmd = model.init();
1616        (model, std::option::Option::Some(cmd))
1617    }
1618
1619    /// Forwards messages to the timer's update method.
1620    ///
1621    /// This implementation delegates all message processing to the timer's
1622    /// own `update()` method, ensuring consistent behavior whether the timer
1623    /// is used standalone or as part of a larger application.
1624    ///
1625    /// # Arguments
1626    ///
1627    /// * `msg` - The message to process
1628    ///
1629    /// # Returns
1630    ///
1631    /// An optional command for continued timer operation
1632    ///
1633    /// # Examples
1634    ///
1635    /// ```rust
1636    /// use bubbletea_widgets::timer::Model as TimerModel;
1637    /// use bubbletea_rs::{Model as BubbleTeaModel};
1638    ///
1639    /// let mut timer = TimerModel::default();
1640    /// // Start the timer
1641    /// let _start_cmd = timer.start();
1642    ///
1643    /// // In a real app, tick messages are generated automatically
1644    /// // Timer processes updates and returns commands for next ticks
1645    /// ```
1646    fn update(&mut self, msg: Msg) -> std::option::Option<Cmd> {
1647        self.update(msg)
1648    }
1649
1650    /// Renders the timer display using the timer's view method.
1651    ///
1652    /// This implementation delegates to the timer's own `view()` method,
1653    /// providing a consistent display format regardless of how the timer
1654    /// is integrated into the application.
1655    ///
1656    /// # Returns
1657    ///
1658    /// A formatted string showing the remaining time
1659    ///
1660    /// # Examples
1661    ///
1662    /// ```rust
1663    /// use bubbletea_widgets::timer::Model as TimerModel;
1664    /// use bubbletea_rs::Model as BubbleTeaModel;
1665    ///
1666    /// let timer = TimerModel::default();
1667    /// let display = timer.view();
1668    /// assert!(display.contains("1m") || display.contains("60s"));
1669    /// ```
1670    fn view(&self) -> String {
1671        self.view()
1672    }
1673}
1674
1675impl Default for Model {
1676    /// Creates a timer with sensible default settings.
1677    ///
1678    /// This implementation provides a standard 60-second timer with 1-second
1679    /// intervals, suitable for most common timing needs. The timer starts in
1680    /// a running state and is ready for immediate use.
1681    ///
1682    /// # Returns
1683    ///
1684    /// A new timer configured with default settings
1685    ///
1686    /// # Examples
1687    ///
1688    /// ```rust
1689    /// use bubbletea_widgets::timer::Model;
1690    /// use std::time::Duration;
1691    ///
1692    /// // Create timer with defaults
1693    /// let timer = Model::default();
1694    /// assert_eq!(timer.timeout, Duration::from_secs(60));
1695    /// assert_eq!(timer.interval, Duration::from_secs(1));
1696    /// assert!(timer.running());
1697    /// assert!(!timer.timedout());
1698    /// ```
1699    ///
1700    /// Using with struct initialization:
1701    /// ```rust
1702    /// use bubbletea_widgets::timer::Model as TimerModel;
1703    ///
1704    /// struct App {
1705    ///     timer: TimerModel,
1706    ///     // other fields...
1707    /// }
1708    ///
1709    /// impl Default for App {
1710    ///     fn default() -> Self {
1711    ///         Self {
1712    ///             timer: TimerModel::default(),
1713    ///             // initialize other fields...
1714    ///         }
1715    ///     }
1716    /// }
1717    /// ```
1718    ///
1719    /// # Default Values
1720    ///
1721    /// - **Timeout**: 60 seconds (1 minute)
1722    /// - **Interval**: 1 second (standard clock tick)
1723    /// - **Running**: `true` (starts immediately)
1724    /// - **ID**: Unique identifier (generated automatically)
1725    ///
1726    /// # When to Use
1727    ///
1728    /// Use `Default` when:
1729    /// - You need a quick timer for testing or prototyping
1730    /// - 60 seconds is an appropriate duration for your use case
1731    /// - You want to rely on struct field defaults in larger structures
1732    /// - Building utilities or examples that need reasonable timer behavior
1733    ///
1734    /// Use `new()` or `new_with_interval()` when:
1735    /// - You need specific timeout durations
1736    /// - Custom tick intervals are required
1737    /// - Explicit configuration is preferred for clarity
1738    ///
1739    /// # Equivalent Creation
1740    ///
1741    /// This default implementation is equivalent to:
1742    /// ```rust
1743    /// use bubbletea_widgets::timer::new;
1744    /// use std::time::Duration;
1745    ///
1746    /// let timer = new(Duration::from_secs(60));
1747    /// ```
1748    fn default() -> Self {
1749        new(Duration::from_secs(60))
1750    }
1751}
1752
1753#[cfg(test)]
1754mod tests {
1755    use super::*;
1756    use std::time::Duration;
1757
1758    #[test]
1759    fn test_new_with_timeout() {
1760        // Test Go's: New(timeout)
1761        let timeout = Duration::from_secs(30);
1762        let timer = new(timeout);
1763
1764        assert_eq!(timer.timeout, timeout);
1765        assert_eq!(timer.interval, Duration::from_secs(1)); // Default interval
1766        assert!(timer.id() > 0); // Should have unique ID
1767        assert!(timer.running()); // Should start running
1768        assert!(!timer.timedout()); // Should not be timed out initially
1769    }
1770
1771    #[test]
1772    fn test_new_with_interval() {
1773        // Test Go's: NewWithInterval(timeout, interval)
1774        let timeout = Duration::from_secs(60);
1775        let interval = Duration::from_millis(500);
1776        let timer = new_with_interval(timeout, interval);
1777
1778        assert_eq!(timer.timeout, timeout);
1779        assert_eq!(timer.interval, interval);
1780        assert!(timer.id() > 0);
1781        assert!(timer.running());
1782        assert!(!timer.timedout());
1783    }
1784
1785    #[test]
1786    fn test_unique_ids() {
1787        // Test that multiple timers get unique IDs
1788        let timer1 = new(Duration::from_secs(10));
1789        let timer2 = new(Duration::from_secs(20));
1790
1791        assert_ne!(timer1.id(), timer2.id());
1792    }
1793
1794    #[test]
1795    fn test_running_logic() {
1796        // Test Go's: Running() bool
1797        let mut timer = new(Duration::from_secs(5));
1798
1799        // Initially running
1800        assert!(timer.running());
1801
1802        // After manual stop
1803        timer.running = false;
1804        assert!(!timer.running());
1805
1806        // After timeout
1807        timer.running = true;
1808        timer.timeout = Duration::ZERO;
1809        assert!(!timer.running()); // Should be false when timed out
1810    }
1811
1812    #[test]
1813    fn test_timedout_logic() {
1814        // Test Go's: Timedout() bool
1815        let mut timer = new(Duration::from_secs(5));
1816
1817        assert!(!timer.timedout());
1818
1819        // Set to zero
1820        timer.timeout = Duration::ZERO;
1821        assert!(timer.timedout());
1822
1823        // Set to negative (should be handled as zero)
1824        timer.timeout = Duration::from_nanos(0);
1825        assert!(timer.timedout());
1826    }
1827
1828    #[test]
1829    fn test_id_method() {
1830        // Test Go's: ID() int
1831        let timer = new(Duration::from_secs(10));
1832        let id = timer.id();
1833
1834        assert!(id > 0);
1835        assert_eq!(timer.id(), id); // Should return same ID consistently
1836    }
1837
1838    #[test]
1839    fn test_start_stop_toggle_commands() {
1840        // Test Go's: Start(), Stop(), Toggle() return commands
1841        let timer = new(Duration::from_secs(10));
1842
1843        // These should return commands (not panic)
1844        let _start_cmd = timer.start();
1845        let _stop_cmd = timer.stop();
1846        let _toggle_cmd = timer.toggle();
1847
1848        // Commands should be callable (we can't easily test their content without executing)
1849        // In a real app, these would send StartStopMsg messages
1850    }
1851
1852    #[test]
1853    fn test_init_command() {
1854        // Test Go's: Init() tea.Cmd
1855        let timer = new(Duration::from_secs(10));
1856        let _cmd = timer.init();
1857
1858        // Should return a command (tick command)
1859    }
1860
1861    #[test]
1862    fn test_update_with_start_stop_msg() {
1863        // Test Go's: Update with StartStopMsg
1864        let mut timer = new(Duration::from_secs(10));
1865        timer.running = false; // Stop it first
1866
1867        let start_msg = StartStopMsg {
1868            id: timer.id(),
1869            running: true,
1870        };
1871
1872        let result = timer.update(Box::new(start_msg));
1873        assert!(result.is_some()); // Should return tick command
1874        assert!(timer.running); // Should now be running
1875    }
1876
1877    #[test]
1878    fn test_update_with_wrong_id() {
1879        // Test that timer rejects StartStopMsg with wrong ID
1880        let mut timer = new(Duration::from_secs(10));
1881
1882        let wrong_msg = StartStopMsg {
1883            id: timer.id() + 999, // Wrong ID
1884            running: false,
1885        };
1886
1887        let original_running = timer.running;
1888        let result = timer.update(Box::new(wrong_msg));
1889
1890        assert!(result.is_none()); // Should reject
1891        assert_eq!(timer.running, original_running); // State unchanged
1892    }
1893
1894    #[test]
1895    fn test_update_with_tick_msg() {
1896        // Test Go's: Update with TickMsg
1897        let mut timer = new(Duration::from_secs(5));
1898        let original_timeout = timer.timeout;
1899
1900        let tick_msg = TickMsg {
1901            id: timer.id(),
1902            timeout: false,
1903            tag: 0,
1904        };
1905
1906        let result = timer.update(Box::new(tick_msg));
1907        assert!(result.is_some()); // Should return next tick command
1908        assert!(timer.timeout < original_timeout); // Should have decreased
1909    }
1910
1911    #[test]
1912    fn test_update_tick_reduces_timeout() {
1913        // Test that TickMsg reduces timeout by interval
1914        let mut timer = new_with_interval(Duration::from_secs(10), Duration::from_secs(2));
1915        let original_timeout = timer.timeout;
1916
1917        let tick_msg = TickMsg {
1918            id: timer.id(),
1919            timeout: false,
1920            tag: 0,
1921        };
1922
1923        timer.update(Box::new(tick_msg));
1924
1925        assert_eq!(
1926            timer.timeout,
1927            original_timeout.saturating_sub(Duration::from_secs(2))
1928        );
1929    }
1930
1931    #[test]
1932    fn test_view_format() {
1933        // Test Go's: View() string (using timeout.String() format)
1934        let timer = new(Duration::from_secs(65)); // 1m5s
1935        let view = timer.view();
1936
1937        // Should be formatted like Go's Duration.String()
1938        assert!(view.contains("m") || view.contains("s"));
1939
1940        // Test zero duration
1941        let mut timer_zero = new(Duration::from_secs(1));
1942        timer_zero.timeout = Duration::ZERO;
1943        assert_eq!(timer_zero.view(), "0s");
1944    }
1945
1946    #[test]
1947    fn test_view_various_durations() {
1948        // Test various duration formats
1949        let test_cases = vec![
1950            (Duration::from_millis(500), "500ms"),
1951            (Duration::from_secs(1), "1s"),
1952            (Duration::from_secs(61), "1m1s"),
1953            (Duration::from_secs(120), "2m"),
1954        ];
1955
1956        for (duration, expected_contains) in test_cases {
1957            let mut timer = new(duration);
1958            timer.timeout = duration;
1959            let view = timer.view();
1960
1961            // At least check that it contains expected parts
1962            // (exact format matching with Go is complex due to precision)
1963            if expected_contains.contains("m") {
1964                assert!(
1965                    view.contains("m"),
1966                    "Duration {:?} should contain 'm' in view: {}",
1967                    duration,
1968                    view
1969                );
1970            }
1971            if expected_contains.contains("s") && !expected_contains.contains("ms") {
1972                assert!(
1973                    view.contains("s"),
1974                    "Duration {:?} should contain 's' in view: {}",
1975                    duration,
1976                    view
1977                );
1978            }
1979        }
1980    }
1981
1982    #[test]
1983    fn test_tag_filtering() {
1984        // Test that timer rejects TickMsg with wrong tag
1985        let mut timer = new(Duration::from_secs(10));
1986        timer.tag = 5; // Set specific tag
1987
1988        let wrong_tick = TickMsg {
1989            id: timer.id(),
1990            timeout: false,
1991            tag: 999, // Wrong tag
1992        };
1993
1994        let result = timer.update(Box::new(wrong_tick));
1995        assert!(result.is_none()); // Should reject
1996    }
1997
1998    #[test]
1999    fn test_not_running_rejects_ticks() {
2000        // Test that stopped timer rejects TickMsg
2001        let mut timer = new(Duration::from_secs(10));
2002        timer.running = false;
2003
2004        let tick_msg = TickMsg {
2005            id: timer.id(),
2006            timeout: false,
2007            tag: 0,
2008        };
2009
2010        let result = timer.update(Box::new(tick_msg));
2011        assert!(result.is_none()); // Should reject when not running
2012    }
2013
2014    #[test]
2015    fn test_default_timer() {
2016        // Test Default implementation
2017        let timer = Model::default();
2018        assert_eq!(timer.timeout, Duration::from_secs(60));
2019        assert_eq!(timer.interval, Duration::from_secs(1));
2020        assert!(timer.running());
2021    }
2022
2023    #[test]
2024    fn test_timeout_msg_semantics() {
2025        // Test TimeoutMsg structure
2026        let timeout_msg = TimeoutMsg { id: 123 };
2027        assert_eq!(timeout_msg.id, 123);
2028    }
2029
2030    #[test]
2031    fn test_timing_fields_initialization() {
2032        // Test that timing fields are properly initialized
2033        let timer = new(Duration::from_secs(10));
2034        assert!(timer.start_instant.is_none());
2035        assert!(timer.last_tick.is_none());
2036
2037        let timer_with_interval =
2038            new_with_interval(Duration::from_secs(30), Duration::from_millis(100));
2039        assert!(timer_with_interval.start_instant.is_none());
2040        assert!(timer_with_interval.last_tick.is_none());
2041    }
2042
2043    #[test]
2044    fn test_timing_initialization_on_first_tick() {
2045        // Test that timing is initialized on first tick message
2046        let mut timer = new(Duration::from_secs(5));
2047        assert!(timer.start_instant.is_none());
2048        assert!(timer.last_tick.is_none());
2049
2050        let tick_msg = TickMsg {
2051            id: timer.id(),
2052            timeout: false,
2053            tag: 0,
2054        };
2055
2056        // First tick should initialize timing
2057        let start_time = std::time::Instant::now();
2058        let result = timer.update(Box::new(tick_msg));
2059        let end_time = std::time::Instant::now();
2060
2061        assert!(result.is_some());
2062        assert!(timer.start_instant.is_some());
2063        assert!(timer.last_tick.is_some());
2064
2065        // Check that the timing was set to approximately the current time
2066        let tick_time = timer.last_tick.unwrap();
2067        assert!(tick_time >= start_time);
2068        assert!(tick_time <= end_time);
2069    }
2070
2071    #[test]
2072    fn test_timing_reset_on_restart() {
2073        // Test that timing is reset when starting from stopped state
2074        let mut timer = new(Duration::from_secs(10));
2075
2076        // Stop the timer
2077        timer.running = false;
2078        timer.start_instant = Some(std::time::Instant::now());
2079        timer.last_tick = Some(std::time::Instant::now());
2080
2081        // Start the timer (should reset timing)
2082        let start_msg = StartStopMsg {
2083            id: timer.id(),
2084            running: true,
2085        };
2086
2087        let result = timer.update(Box::new(start_msg));
2088        assert!(result.is_some());
2089        assert!(timer.running);
2090        assert!(timer.start_instant.is_none()); // Should be reset
2091        assert!(timer.last_tick.is_none()); // Should be reset
2092    }
2093
2094    #[test]
2095    fn test_timing_preserved_on_stop() {
2096        // Test that timing is preserved when stopping (not reset)
2097        let mut timer = new(Duration::from_secs(10));
2098
2099        // Initialize timing with a tick
2100        let tick_msg = TickMsg {
2101            id: timer.id(),
2102            timeout: false,
2103            tag: 0,
2104        };
2105        timer.update(Box::new(tick_msg));
2106
2107        let preserved_start = timer.start_instant;
2108        let preserved_tick = timer.last_tick;
2109
2110        // Stop the timer (should preserve timing)
2111        let stop_msg = StartStopMsg {
2112            id: timer.id(),
2113            running: false,
2114        };
2115
2116        let result = timer.update(Box::new(stop_msg));
2117        assert!(result.is_some());
2118        assert!(!timer.running);
2119        assert_eq!(timer.start_instant, preserved_start); // Should be preserved
2120        assert_eq!(timer.last_tick, preserved_tick); // Should be preserved
2121    }
2122}