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            if timeout {
1168                Box::new(TimeoutMsg { id }) as Msg
1169            } else {
1170                Box::new(TickMsg { id, timeout, tag }) as Msg
1171            }
1172        })
1173    }
1174
1175    /// Internal timedout command - matches Go's timedout method.
1176    #[allow(dead_code)]
1177    fn timedout_cmd(&self) -> std::option::Option<Cmd> {
1178        if !self.timedout() {
1179            return std::option::Option::None;
1180        }
1181        let id = self.id;
1182        std::option::Option::Some(bubbletea_tick(Duration::from_nanos(1), move |_| {
1183            Box::new(TimeoutMsg { id }) as Msg
1184        }))
1185    }
1186
1187    /// Internal start/stop command - matches Go's startStop method.
1188    fn start_stop(&self, running: bool) -> Cmd {
1189        let id = self.id;
1190        bubbletea_tick(Duration::from_nanos(1), move |_| {
1191            Box::new(StartStopMsg { id, running }) as Msg
1192        })
1193    }
1194
1195    /// Initializes the timer and returns the command to start its first tick.
1196    ///
1197    /// This method should be called once when the timer is first created to begin
1198    /// the countdown process. It generates the initial tick command that starts
1199    /// the timer's regular interval-based updates.
1200    ///
1201    /// # Returns
1202    ///
1203    /// A `Cmd` that will start the timer's tick cycle when executed
1204    ///
1205    /// # Examples
1206    ///
1207    /// ```rust
1208    /// use bubbletea_widgets::timer::new;
1209    /// use bubbletea_rs::{Model as BubbleTeaModel, Cmd};
1210    /// use std::time::Duration;
1211    ///
1212    /// struct App {
1213    ///     timer: bubbletea_widgets::timer::Model,
1214    /// }
1215    ///
1216    /// impl BubbleTeaModel for App {
1217    ///     fn init() -> (Self, Option<Cmd>) {
1218    ///         let timer = new(Duration::from_secs(60));
1219    ///         // Initialize the timer to start ticking
1220    ///         let timer_cmd = timer.init();
1221    ///         (App { timer }, Some(timer_cmd))
1222    ///     }
1223    ///     
1224    ///     // ... other methods
1225    /// #   fn update(&mut self, _: bubbletea_rs::Msg) -> Option<Cmd> { None }
1226    /// #   fn view(&self) -> String { self.timer.view() }
1227    /// }
1228    /// ```
1229    ///
1230    /// Multiple timer initialization:
1231    /// ```rust
1232    /// use bubbletea_widgets::timer::new;
1233    /// use bubbletea_rs::{Model as BubbleTeaModel, Cmd};
1234    /// use std::time::Duration;
1235    ///
1236    /// struct MultiTimerApp {
1237    ///     work_timer: bubbletea_widgets::timer::Model,
1238    ///     break_timer: bubbletea_widgets::timer::Model,
1239    /// }
1240    ///
1241    /// impl BubbleTeaModel for MultiTimerApp {
1242    ///     fn init() -> (Self, Option<Cmd>) {
1243    ///         let work_timer = new(Duration::from_secs(25 * 60));
1244    ///         let break_timer = new(Duration::from_secs(5 * 60));
1245    ///         
1246    ///         // Start the work timer initially
1247    ///         let init_cmd = work_timer.init();
1248    ///         
1249    ///         (MultiTimerApp { work_timer, break_timer }, Some(init_cmd))
1250    ///     }
1251    ///     
1252    ///     // ... other methods
1253    /// #   fn update(&mut self, _: bubbletea_rs::Msg) -> Option<Cmd> { None }
1254    /// #   fn view(&self) -> String { self.work_timer.view() }
1255    /// }
1256    /// ```
1257    ///
1258    /// # When to Call
1259    ///
1260    /// - **Application startup**: In your app's `init()` method
1261    /// - **Timer creation**: Immediately after creating a new timer
1262    /// - **Timer reset**: When restarting a timer with new settings
1263    ///
1264    /// # What It Does
1265    ///
1266    /// The `init()` method:
1267    /// 1. Creates the first tick command
1268    /// 2. Sets up the timer's internal tick cycle
1269    /// 3. Returns immediately (non-blocking)
1270    /// 4. The actual ticking starts when Bubble Tea executes the command
1271    ///
1272    /// # Timing Behavior
1273    ///
1274    /// The first tick occurs after the timer's `interval` duration. For example,
1275    /// with a 1-second interval, the first countdown update happens 1 second after
1276    /// the init command is executed.
1277    ///
1278    /// # Alternative to Start
1279    ///
1280    /// While `start()` is used to resume a paused timer, `init()` is specifically
1281    /// for initial timer setup and should be called once per timer instance.
1282    ///
1283    /// # Note
1284    ///
1285    /// This method matches Go's `Init()` method exactly for compatibility.
1286    pub fn init(&self) -> Cmd {
1287        self.tick()
1288    }
1289
1290    /// Processes messages and updates the timer state.
1291    ///
1292    /// This method handles all messages related to timer operation, including tick
1293    /// messages that advance the countdown and control messages that change the
1294    /// running state. It should be called from your application's update loop
1295    /// for proper timer functionality.
1296    ///
1297    /// # Arguments
1298    ///
1299    /// * `msg` - The message to process (typically `TickMsg` or `StartStopMsg`)
1300    ///
1301    /// # Returns
1302    ///
1303    /// An optional `Cmd` for the next timer operation, or `None` if no action needed
1304    ///
1305    /// # Message Types Handled
1306    ///
1307    /// - **`StartStopMsg`**: Changes the timer's running state
1308    /// - **`TickMsg`**: Advances the countdown and schedules the next tick
1309    /// - **Other messages**: Ignored (returns `None`)
1310    ///
1311    /// # Examples
1312    ///
1313    /// ```rust
1314    /// use bubbletea_widgets::timer::{new, TickMsg, StartStopMsg, TimeoutMsg};
1315    /// use bubbletea_rs::{Model as BubbleTeaModel, Msg, Cmd};
1316    /// use std::time::Duration;
1317    ///
1318    /// struct App {
1319    ///     timer: bubbletea_widgets::timer::Model,
1320    ///     status: String,
1321    /// }
1322    ///
1323    /// impl BubbleTeaModel for App {
1324    ///     fn update(&mut self, msg: Msg) -> Option<Cmd> {
1325    ///         // Handle application-specific timer events
1326    ///         if let Some(timeout) = msg.downcast_ref::<TimeoutMsg>() {
1327    ///             if timeout.id == self.timer.id() {
1328    ///                 self.status = "Timer completed!".to_string();
1329    ///             }
1330    ///         }
1331    ///         
1332    ///         // Forward all messages to timer for processing
1333    ///         self.timer.update(msg)
1334    ///     }
1335    ///     
1336    ///     // ... other methods
1337    /// #   fn init() -> (Self, Option<Cmd>) { unimplemented!() }
1338    /// #   fn view(&self) -> String { format!("{}: {}", self.status, self.timer.view()) }
1339    /// }
1340    /// ```
1341    ///
1342    /// Manual message handling:
1343    /// ```rust,ignore
1344    /// use bubbletea_widgets::timer::new;
1345    /// use std::time::Duration;
1346    ///
1347    /// let mut timer = new(Duration::from_secs(10));
1348    ///
1349    /// // Start the timer using the public API
1350    /// let start_cmd = timer.start();
1351    /// // In a real app, you'd send this command through bubbletea
1352    ///
1353    /// // Check initial timeout
1354    /// assert_eq!(timer.timeout(), Duration::from_secs(10));
1355    /// ```
1356    ///
1357    /// # Message Filtering
1358    ///
1359    /// The timer automatically filters messages to ensure it only processes
1360    /// messages intended for it:
1361    ///
1362    /// - **ID Matching**: Only processes messages with matching timer IDs
1363    /// - **Tag Validation**: Rejects tick messages with incorrect tags
1364    /// - **State Checks**: Ignores ticks when not running
1365    ///
1366    /// ```rust
1367    /// use bubbletea_widgets::timer::new;
1368    /// use std::time::Duration;
1369    ///
1370    /// let mut timer1 = new(Duration::from_secs(10));
1371    /// let _timer2 = new(Duration::from_secs(20));
1372    ///
1373    /// // Start timer1
1374    /// let _cmd = timer1.start();
1375    ///
1376    /// // Messages with wrong IDs are ignored
1377    /// // In a real app, messages are routed by ID
1378    /// assert_eq!(timer1.timeout, Duration::from_secs(10)); // No change
1379    /// ```
1380    ///
1381    /// # State Changes
1382    ///
1383    /// Different message types cause different state changes:
1384    ///
1385    /// - **`StartStopMsg`**: Changes `running` state, returns tick command
1386    /// - **`TickMsg`**: Reduces `timeout` by `interval`, returns next tick command
1387    /// - **Invalid messages**: No state change, returns `None`
1388    ///
1389    /// # Timeout Detection
1390    ///
1391    /// When a tick reduces the timeout to zero or below, the timer:
1392    /// 1. Automatically stops running
1393    /// 2. May send a `TimeoutMsg` (implementation detail)
1394    /// 3. Returns a tick command that will be ignored (since not running)
1395    ///
1396    /// # Error Handling
1397    ///
1398    /// This method never panics and handles invalid messages gracefully by
1399    /// ignoring them and returning `None`.
1400    ///
1401    /// # Performance
1402    ///
1403    /// Message processing is very fast, involving only basic comparisons and
1404    /// arithmetic operations. The method is designed to be called frequently
1405    /// in the Bubble Tea update loop.
1406    ///
1407    /// # Note
1408    ///
1409    /// This method matches Go's `Update()` method exactly for compatibility.
1410    pub fn update(&mut self, msg: Msg) -> std::option::Option<Cmd> {
1411        if let Some(start_stop_msg) = msg.downcast_ref::<StartStopMsg>() {
1412            if start_stop_msg.id != 0 && start_stop_msg.id != self.id {
1413                return std::option::Option::None;
1414            }
1415
1416            let was_running = self.running;
1417            self.running = start_stop_msg.running;
1418
1419            // Reset timing when starting from stopped state
1420            if !was_running && self.running {
1421                self.start_instant = None;
1422                self.last_tick = None;
1423            }
1424
1425            return std::option::Option::Some(self.tick());
1426        }
1427
1428        if let Some(tick_msg) = msg.downcast_ref::<TickMsg>() {
1429            if !self.running() || (tick_msg.id != 0 && tick_msg.id != self.id) {
1430                return std::option::Option::None;
1431            }
1432
1433            // If a tag is set, and it's not the one we expect, reject the message.
1434            // This prevents the ticker from receiving too many messages and
1435            // thus ticking too fast.
1436            if tick_msg.tag > 0 && tick_msg.tag != self.tag {
1437                return std::option::Option::None;
1438            }
1439
1440            // Use high-precision elapsed time tracking for accurate countdown
1441            let now = Instant::now();
1442
1443            // Initialize timing on first tick
1444            if self.last_tick.is_none() {
1445                self.start_instant = Some(now);
1446                self.last_tick = Some(now);
1447                // On first tick, just use the interval as fallback
1448                self.timeout = self.timeout.saturating_sub(self.interval);
1449            } else {
1450                // Calculate actual elapsed time since last tick
1451                let actual_elapsed = now.duration_since(self.last_tick.unwrap());
1452                self.timeout = self.timeout.saturating_sub(actual_elapsed);
1453                self.last_tick = Some(now);
1454            }
1455
1456            // Check if timer has expired after this tick
1457            if self.timedout() {
1458                // Timer has expired, send TimeoutMsg
1459                let id = self.id;
1460                return std::option::Option::Some(bubbletea_tick(
1461                    Duration::from_nanos(1),
1462                    move |_| Box::new(TimeoutMsg { id }) as Msg,
1463                ));
1464            }
1465
1466            // Timer still running, continue with next tick
1467            return std::option::Option::Some(self.tick());
1468        }
1469
1470        std::option::Option::None
1471    }
1472
1473    /// Renders the timer as a formatted string showing remaining time.
1474    ///
1475    /// This method converts the timer's current timeout duration into a
1476    /// human-readable string using Go's duration formatting conventions.
1477    /// The output is suitable for direct display in terminal applications.
1478    ///
1479    /// # Returns
1480    ///
1481    /// A formatted string representation of the remaining time
1482    ///
1483    /// # Format Examples
1484    ///
1485    /// The format matches Go's `Duration.String()` output:
1486    /// - `"5m30s"` for 5 minutes 30 seconds
1487    /// - `"2m"` for exactly 2 minutes
1488    /// - `"45.5s"` for 45.5 seconds
1489    /// - `"750ms"` for milliseconds
1490    /// - `"0s"` when expired
1491    ///
1492    /// # Examples
1493    ///
1494    /// ```rust
1495    /// use bubbletea_widgets::timer::new;
1496    /// use std::time::Duration;
1497    ///
1498    /// // Various timer displays
1499    /// let timer1 = new(Duration::from_secs(90));
1500    /// assert!(timer1.view().contains("1m"));
1501    ///
1502    /// let timer2 = new(Duration::from_millis(500));
1503    /// assert!(timer2.view().contains("500ms"));
1504    ///
1505    /// let mut timer3 = new(Duration::from_secs(1));
1506    /// timer3.timeout = Duration::ZERO;
1507    /// assert_eq!(timer3.view(), "0s");
1508    /// ```
1509    ///
1510    /// Integration in UI:
1511    /// ```rust
1512    /// use bubbletea_widgets::timer::new;
1513    /// use bubbletea_rs::{Model as BubbleTeaModel};
1514    /// use std::time::Duration;
1515    ///
1516    /// struct App {
1517    ///     cooking_timer: bubbletea_widgets::timer::Model,
1518    ///     recipe: String,
1519    /// }
1520    ///
1521    /// impl BubbleTeaModel for App {
1522    ///     fn view(&self) -> String {
1523    ///         format!(
1524    ///             "Cooking: {}\n\nTime remaining: {}\n\n[SPACE] to pause",
1525    ///             self.recipe,
1526    ///             self.cooking_timer.view()
1527    ///         )
1528    ///     }
1529    ///     
1530    ///     // ... other methods
1531    /// #   fn init() -> (Self, Option<bubbletea_rs::Cmd>) { unimplemented!() }
1532    /// #   fn update(&mut self, _: bubbletea_rs::Msg) -> Option<bubbletea_rs::Cmd> { None }
1533    /// }
1534    /// ```
1535    ///
1536    /// Dynamic display updates:
1537    /// ```rust
1538    /// use bubbletea_widgets::timer::new;
1539    /// use std::time::Duration;
1540    ///
1541    /// let mut timer = new(Duration::from_secs(125)); // 2m5s
1542    ///
1543    /// // Display updates as timer counts down
1544    /// println!("Start: {}", timer.view()); // "2m5s"
1545    ///
1546    /// // Simulate 30 seconds passing
1547    /// timer.timeout -= Duration::from_secs(30);
1548    /// println!("After 30s: {}", timer.view()); // "1m35s"
1549    ///
1550    /// // Simulate completion
1551    /// timer.timeout = Duration::ZERO;
1552    /// println!("Finished: {}", timer.view()); // "0s"
1553    /// ```
1554    ///
1555    /// # Precision Display
1556    ///
1557    /// The display precision depends on the remaining time:
1558    /// - **Minutes**: Shows minutes and seconds (e.g., "3m45s")
1559    /// - **Seconds**: Shows seconds with decimals if needed (e.g., "1.5s")
1560    /// - **Milliseconds**: Shows millisecond precision (e.g., "250ms")
1561    /// - **Microseconds/Nanoseconds**: For very short durations
1562    ///
1563    /// # Use Cases
1564    ///
1565    /// Perfect for:
1566    /// - Countdown displays in TUIs
1567    /// - Progress indicators with time remaining
1568    /// - Cooking/work timers
1569    /// - Game time limits
1570    /// - Session timeouts
1571    ///
1572    /// # Performance
1573    ///
1574    /// String formatting is performed on each call, so for high-frequency
1575    /// updates, consider caching the result if the timeout hasn't changed.
1576    ///
1577    /// # Localization
1578    ///
1579    /// The format uses English abbreviations ("m", "s", "ms") and follows
1580    /// Go's conventions. For different locales, you may need to parse the
1581    /// `timeout` duration and format it according to local preferences.
1582    ///
1583    /// # Note
1584    ///
1585    /// This method matches Go's `View()` method exactly for compatibility.
1586    pub fn view(&self) -> String {
1587        format_duration(self.timeout)
1588    }
1589}
1590
1591impl BubbleTeaModel for Model {
1592    /// Creates a new timer model with default settings for standalone use.
1593    ///
1594    /// This implementation provides a default timer configuration suitable for
1595    /// applications that want to use the timer as a standalone component without
1596    /// custom initialization. It creates a 60-second timer with 1-second intervals.
1597    ///
1598    /// # Returns
1599    ///
1600    /// A tuple containing the new timer model and its initialization command
1601    ///
1602    /// # Examples
1603    ///
1604    /// ```rust
1605    /// use bubbletea_widgets::timer::Model as TimerModel;
1606    /// use bubbletea_rs::{Model as BubbleTeaModel};
1607    ///
1608    /// // Use timer as a standalone Bubble Tea application
1609    /// let model = TimerModel::default();
1610    /// let cmd = model.init();
1611    /// // Would start a 60-second timer app
1612    /// ```
1613    ///
1614    /// # Default Configuration
1615    ///
1616    /// - **Timeout**: 60 seconds
1617    /// - **Interval**: 1 second
1618    /// - **State**: Running (starts immediately)
1619    /// - **Display**: Shows countdown in "1m0s" format
1620    ///
1621    /// # Note
1622    ///
1623    /// Most applications will want to use `new()` or `new_with_interval()` instead
1624    /// to create timers with specific durations rather than this default.
1625    fn init() -> (Self, std::option::Option<Cmd>) {
1626        let model = new(Duration::from_secs(60));
1627        let cmd = model.init();
1628        (model, std::option::Option::Some(cmd))
1629    }
1630
1631    /// Forwards messages to the timer's update method.
1632    ///
1633    /// This implementation delegates all message processing to the timer's
1634    /// own `update()` method, ensuring consistent behavior whether the timer
1635    /// is used standalone or as part of a larger application.
1636    ///
1637    /// # Arguments
1638    ///
1639    /// * `msg` - The message to process
1640    ///
1641    /// # Returns
1642    ///
1643    /// An optional command for continued timer operation
1644    ///
1645    /// # Examples
1646    ///
1647    /// ```rust
1648    /// use bubbletea_widgets::timer::Model as TimerModel;
1649    /// use bubbletea_rs::{Model as BubbleTeaModel};
1650    ///
1651    /// let mut timer = TimerModel::default();
1652    /// // Start the timer
1653    /// let _start_cmd = timer.start();
1654    ///
1655    /// // In a real app, tick messages are generated automatically
1656    /// // Timer processes updates and returns commands for next ticks
1657    /// ```
1658    fn update(&mut self, msg: Msg) -> std::option::Option<Cmd> {
1659        self.update(msg)
1660    }
1661
1662    /// Renders the timer display using the timer's view method.
1663    ///
1664    /// This implementation delegates to the timer's own `view()` method,
1665    /// providing a consistent display format regardless of how the timer
1666    /// is integrated into the application.
1667    ///
1668    /// # Returns
1669    ///
1670    /// A formatted string showing the remaining time
1671    ///
1672    /// # Examples
1673    ///
1674    /// ```rust
1675    /// use bubbletea_widgets::timer::Model as TimerModel;
1676    /// use bubbletea_rs::Model as BubbleTeaModel;
1677    ///
1678    /// let timer = TimerModel::default();
1679    /// let display = timer.view();
1680    /// assert!(display.contains("1m") || display.contains("60s"));
1681    /// ```
1682    fn view(&self) -> String {
1683        self.view()
1684    }
1685}
1686
1687impl Default for Model {
1688    /// Creates a timer with sensible default settings.
1689    ///
1690    /// This implementation provides a standard 60-second timer with 1-second
1691    /// intervals, suitable for most common timing needs. The timer starts in
1692    /// a running state and is ready for immediate use.
1693    ///
1694    /// # Returns
1695    ///
1696    /// A new timer configured with default settings
1697    ///
1698    /// # Examples
1699    ///
1700    /// ```rust
1701    /// use bubbletea_widgets::timer::Model;
1702    /// use std::time::Duration;
1703    ///
1704    /// // Create timer with defaults
1705    /// let timer = Model::default();
1706    /// assert_eq!(timer.timeout, Duration::from_secs(60));
1707    /// assert_eq!(timer.interval, Duration::from_secs(1));
1708    /// assert!(timer.running());
1709    /// assert!(!timer.timedout());
1710    /// ```
1711    ///
1712    /// Using with struct initialization:
1713    /// ```rust
1714    /// use bubbletea_widgets::timer::Model as TimerModel;
1715    ///
1716    /// struct App {
1717    ///     timer: TimerModel,
1718    ///     // other fields...
1719    /// }
1720    ///
1721    /// impl Default for App {
1722    ///     fn default() -> Self {
1723    ///         Self {
1724    ///             timer: TimerModel::default(),
1725    ///             // initialize other fields...
1726    ///         }
1727    ///     }
1728    /// }
1729    /// ```
1730    ///
1731    /// # Default Values
1732    ///
1733    /// - **Timeout**: 60 seconds (1 minute)
1734    /// - **Interval**: 1 second (standard clock tick)
1735    /// - **Running**: `true` (starts immediately)
1736    /// - **ID**: Unique identifier (generated automatically)
1737    ///
1738    /// # When to Use
1739    ///
1740    /// Use `Default` when:
1741    /// - You need a quick timer for testing or prototyping
1742    /// - 60 seconds is an appropriate duration for your use case
1743    /// - You want to rely on struct field defaults in larger structures
1744    /// - Building utilities or examples that need reasonable timer behavior
1745    ///
1746    /// Use `new()` or `new_with_interval()` when:
1747    /// - You need specific timeout durations
1748    /// - Custom tick intervals are required
1749    /// - Explicit configuration is preferred for clarity
1750    ///
1751    /// # Equivalent Creation
1752    ///
1753    /// This default implementation is equivalent to:
1754    /// ```rust
1755    /// use bubbletea_widgets::timer::new;
1756    /// use std::time::Duration;
1757    ///
1758    /// let timer = new(Duration::from_secs(60));
1759    /// ```
1760    fn default() -> Self {
1761        new(Duration::from_secs(60))
1762    }
1763}
1764
1765#[cfg(test)]
1766mod tests {
1767    use super::*;
1768    use std::time::Duration;
1769
1770    #[test]
1771    fn test_new_with_timeout() {
1772        // Test Go's: New(timeout)
1773        let timeout = Duration::from_secs(30);
1774        let timer = new(timeout);
1775
1776        assert_eq!(timer.timeout, timeout);
1777        assert_eq!(timer.interval, Duration::from_secs(1)); // Default interval
1778        assert!(timer.id() > 0); // Should have unique ID
1779        assert!(timer.running()); // Should start running
1780        assert!(!timer.timedout()); // Should not be timed out initially
1781    }
1782
1783    #[test]
1784    fn test_new_with_interval() {
1785        // Test Go's: NewWithInterval(timeout, interval)
1786        let timeout = Duration::from_secs(60);
1787        let interval = Duration::from_millis(500);
1788        let timer = new_with_interval(timeout, interval);
1789
1790        assert_eq!(timer.timeout, timeout);
1791        assert_eq!(timer.interval, interval);
1792        assert!(timer.id() > 0);
1793        assert!(timer.running());
1794        assert!(!timer.timedout());
1795    }
1796
1797    #[test]
1798    fn test_unique_ids() {
1799        // Test that multiple timers get unique IDs
1800        let timer1 = new(Duration::from_secs(10));
1801        let timer2 = new(Duration::from_secs(20));
1802
1803        assert_ne!(timer1.id(), timer2.id());
1804    }
1805
1806    #[test]
1807    fn test_running_logic() {
1808        // Test Go's: Running() bool
1809        let mut timer = new(Duration::from_secs(5));
1810
1811        // Initially running
1812        assert!(timer.running());
1813
1814        // After manual stop
1815        timer.running = false;
1816        assert!(!timer.running());
1817
1818        // After timeout
1819        timer.running = true;
1820        timer.timeout = Duration::ZERO;
1821        assert!(!timer.running()); // Should be false when timed out
1822    }
1823
1824    #[test]
1825    fn test_timedout_logic() {
1826        // Test Go's: Timedout() bool
1827        let mut timer = new(Duration::from_secs(5));
1828
1829        assert!(!timer.timedout());
1830
1831        // Set to zero
1832        timer.timeout = Duration::ZERO;
1833        assert!(timer.timedout());
1834
1835        // Set to negative (should be handled as zero)
1836        timer.timeout = Duration::from_nanos(0);
1837        assert!(timer.timedout());
1838    }
1839
1840    #[test]
1841    fn test_id_method() {
1842        // Test Go's: ID() int
1843        let timer = new(Duration::from_secs(10));
1844        let id = timer.id();
1845
1846        assert!(id > 0);
1847        assert_eq!(timer.id(), id); // Should return same ID consistently
1848    }
1849
1850    #[test]
1851    fn test_start_stop_toggle_commands() {
1852        // Test Go's: Start(), Stop(), Toggle() return commands
1853        let timer = new(Duration::from_secs(10));
1854
1855        // These should return commands (not panic)
1856        let _start_cmd = timer.start();
1857        let _stop_cmd = timer.stop();
1858        let _toggle_cmd = timer.toggle();
1859
1860        // Commands should be callable (we can't easily test their content without executing)
1861        // In a real app, these would send StartStopMsg messages
1862    }
1863
1864    #[test]
1865    fn test_init_command() {
1866        // Test Go's: Init() tea.Cmd
1867        let timer = new(Duration::from_secs(10));
1868        let _cmd = timer.init();
1869
1870        // Should return a command (tick command)
1871    }
1872
1873    #[test]
1874    fn test_update_with_start_stop_msg() {
1875        // Test Go's: Update with StartStopMsg
1876        let mut timer = new(Duration::from_secs(10));
1877        timer.running = false; // Stop it first
1878
1879        let start_msg = StartStopMsg {
1880            id: timer.id(),
1881            running: true,
1882        };
1883
1884        let result = timer.update(Box::new(start_msg));
1885        assert!(result.is_some()); // Should return tick command
1886        assert!(timer.running); // Should now be running
1887    }
1888
1889    #[test]
1890    fn test_update_with_wrong_id() {
1891        // Test that timer rejects StartStopMsg with wrong ID
1892        let mut timer = new(Duration::from_secs(10));
1893
1894        let wrong_msg = StartStopMsg {
1895            id: timer.id() + 999, // Wrong ID
1896            running: false,
1897        };
1898
1899        let original_running = timer.running;
1900        let result = timer.update(Box::new(wrong_msg));
1901
1902        assert!(result.is_none()); // Should reject
1903        assert_eq!(timer.running, original_running); // State unchanged
1904    }
1905
1906    #[test]
1907    fn test_update_with_tick_msg() {
1908        // Test Go's: Update with TickMsg
1909        let mut timer = new(Duration::from_secs(5));
1910        let original_timeout = timer.timeout;
1911
1912        let tick_msg = TickMsg {
1913            id: timer.id(),
1914            timeout: false,
1915            tag: 0,
1916        };
1917
1918        let result = timer.update(Box::new(tick_msg));
1919        assert!(result.is_some()); // Should return next tick command
1920        assert!(timer.timeout < original_timeout); // Should have decreased
1921    }
1922
1923    #[test]
1924    fn test_update_tick_reduces_timeout() {
1925        // Test that TickMsg reduces timeout by interval
1926        let mut timer = new_with_interval(Duration::from_secs(10), Duration::from_secs(2));
1927        let original_timeout = timer.timeout;
1928
1929        let tick_msg = TickMsg {
1930            id: timer.id(),
1931            timeout: false,
1932            tag: 0,
1933        };
1934
1935        timer.update(Box::new(tick_msg));
1936
1937        assert_eq!(
1938            timer.timeout,
1939            original_timeout.saturating_sub(Duration::from_secs(2))
1940        );
1941    }
1942
1943    #[test]
1944    fn test_view_format() {
1945        // Test Go's: View() string (using timeout.String() format)
1946        let timer = new(Duration::from_secs(65)); // 1m5s
1947        let view = timer.view();
1948
1949        // Should be formatted like Go's Duration.String()
1950        assert!(view.contains("m") || view.contains("s"));
1951
1952        // Test zero duration
1953        let mut timer_zero = new(Duration::from_secs(1));
1954        timer_zero.timeout = Duration::ZERO;
1955        assert_eq!(timer_zero.view(), "0s");
1956    }
1957
1958    #[test]
1959    fn test_view_various_durations() {
1960        // Test various duration formats
1961        let test_cases = vec![
1962            (Duration::from_millis(500), "500ms"),
1963            (Duration::from_secs(1), "1s"),
1964            (Duration::from_secs(61), "1m1s"),
1965            (Duration::from_secs(120), "2m"),
1966        ];
1967
1968        for (duration, expected_contains) in test_cases {
1969            let mut timer = new(duration);
1970            timer.timeout = duration;
1971            let view = timer.view();
1972
1973            // At least check that it contains expected parts
1974            // (exact format matching with Go is complex due to precision)
1975            if expected_contains.contains("m") {
1976                assert!(
1977                    view.contains("m"),
1978                    "Duration {:?} should contain 'm' in view: {}",
1979                    duration,
1980                    view
1981                );
1982            }
1983            if expected_contains.contains("s") && !expected_contains.contains("ms") {
1984                assert!(
1985                    view.contains("s"),
1986                    "Duration {:?} should contain 's' in view: {}",
1987                    duration,
1988                    view
1989                );
1990            }
1991        }
1992    }
1993
1994    #[test]
1995    fn test_tag_filtering() {
1996        // Test that timer rejects TickMsg with wrong tag
1997        let mut timer = new(Duration::from_secs(10));
1998        timer.tag = 5; // Set specific tag
1999
2000        let wrong_tick = TickMsg {
2001            id: timer.id(),
2002            timeout: false,
2003            tag: 999, // Wrong tag
2004        };
2005
2006        let result = timer.update(Box::new(wrong_tick));
2007        assert!(result.is_none()); // Should reject
2008    }
2009
2010    #[test]
2011    fn test_not_running_rejects_ticks() {
2012        // Test that stopped timer rejects TickMsg
2013        let mut timer = new(Duration::from_secs(10));
2014        timer.running = false;
2015
2016        let tick_msg = TickMsg {
2017            id: timer.id(),
2018            timeout: false,
2019            tag: 0,
2020        };
2021
2022        let result = timer.update(Box::new(tick_msg));
2023        assert!(result.is_none()); // Should reject when not running
2024    }
2025
2026    #[test]
2027    fn test_default_timer() {
2028        // Test Default implementation
2029        let timer = Model::default();
2030        assert_eq!(timer.timeout, Duration::from_secs(60));
2031        assert_eq!(timer.interval, Duration::from_secs(1));
2032        assert!(timer.running());
2033    }
2034
2035    #[test]
2036    fn test_timeout_msg_semantics() {
2037        // Test TimeoutMsg structure
2038        let timeout_msg = TimeoutMsg { id: 123 };
2039        assert_eq!(timeout_msg.id, 123);
2040    }
2041
2042    #[test]
2043    fn test_timing_fields_initialization() {
2044        // Test that timing fields are properly initialized
2045        let timer = new(Duration::from_secs(10));
2046        assert!(timer.start_instant.is_none());
2047        assert!(timer.last_tick.is_none());
2048
2049        let timer_with_interval =
2050            new_with_interval(Duration::from_secs(30), Duration::from_millis(100));
2051        assert!(timer_with_interval.start_instant.is_none());
2052        assert!(timer_with_interval.last_tick.is_none());
2053    }
2054
2055    #[test]
2056    fn test_timing_initialization_on_first_tick() {
2057        // Test that timing is initialized on first tick message
2058        let mut timer = new(Duration::from_secs(5));
2059        assert!(timer.start_instant.is_none());
2060        assert!(timer.last_tick.is_none());
2061
2062        let tick_msg = TickMsg {
2063            id: timer.id(),
2064            timeout: false,
2065            tag: 0,
2066        };
2067
2068        // First tick should initialize timing
2069        let start_time = std::time::Instant::now();
2070        let result = timer.update(Box::new(tick_msg));
2071        let end_time = std::time::Instant::now();
2072
2073        assert!(result.is_some());
2074        assert!(timer.start_instant.is_some());
2075        assert!(timer.last_tick.is_some());
2076
2077        // Check that the timing was set to approximately the current time
2078        let tick_time = timer.last_tick.unwrap();
2079        assert!(tick_time >= start_time);
2080        assert!(tick_time <= end_time);
2081    }
2082
2083    #[test]
2084    fn test_timing_reset_on_restart() {
2085        // Test that timing is reset when starting from stopped state
2086        let mut timer = new(Duration::from_secs(10));
2087
2088        // Stop the timer
2089        timer.running = false;
2090        timer.start_instant = Some(std::time::Instant::now());
2091        timer.last_tick = Some(std::time::Instant::now());
2092
2093        // Start the timer (should reset timing)
2094        let start_msg = StartStopMsg {
2095            id: timer.id(),
2096            running: true,
2097        };
2098
2099        let result = timer.update(Box::new(start_msg));
2100        assert!(result.is_some());
2101        assert!(timer.running);
2102        assert!(timer.start_instant.is_none()); // Should be reset
2103        assert!(timer.last_tick.is_none()); // Should be reset
2104    }
2105
2106    #[test]
2107    fn test_timing_preserved_on_stop() {
2108        // Test that timing is preserved when stopping (not reset)
2109        let mut timer = new(Duration::from_secs(10));
2110
2111        // Initialize timing with a tick
2112        let tick_msg = TickMsg {
2113            id: timer.id(),
2114            timeout: false,
2115            tag: 0,
2116        };
2117        timer.update(Box::new(tick_msg));
2118
2119        let preserved_start = timer.start_instant;
2120        let preserved_tick = timer.last_tick;
2121
2122        // Stop the timer (should preserve timing)
2123        let stop_msg = StartStopMsg {
2124            id: timer.id(),
2125            running: false,
2126        };
2127
2128        let result = timer.update(Box::new(stop_msg));
2129        assert!(result.is_some());
2130        assert!(!timer.running);
2131        assert_eq!(timer.start_instant, preserved_start); // Should be preserved
2132        assert_eq!(timer.last_tick, preserved_tick); // Should be preserved
2133    }
2134}