@@ -9,6 +9,7 @@ use std::{
99 LazyLock , RwLock ,
1010 } , // Use LazyLock from std
1111 thread,
12+ time:: Instant ,
1213} ;
1314use tracing:: { info, warn} ;
1415
@@ -26,6 +27,11 @@ static TIMERS: LazyLock<RwLock<Vec<(u64, String, DateTime<Local>)>>> = LazyLock:
2627 RwLock :: new ( timers)
2728} ) ;
2829
30+ // Stopwatch state: (start_time, total_elapsed_before_pause)
31+ // When running: start_time is Some, when paused: start_time is None
32+ static STOPWATCH : LazyLock < RwLock < ( Option < Instant > , std:: time:: Duration ) > > =
33+ LazyLock :: new ( || RwLock :: new ( ( None , std:: time:: Duration :: ZERO ) ) ) ;
34+
2935fn load_timers_from_disk (
3036 path : & Path ,
3137) -> Result < Vec < ( u64 , String , DateTime < Local > ) > , anyhow:: Error > {
@@ -228,12 +234,84 @@ impl AudibleTimers {
228234 }
229235}
230236
237+ // Stopwatch API
238+
239+ /// Starts the stopwatch. If already running, this does nothing.
240+ /// If paused, it resumes from where it left off.
241+ pub fn start_stopwatch ( ) -> String {
242+ let mut sw = STOPWATCH . write ( ) . unwrap ( ) ;
243+ if sw. 0 . is_some ( ) {
244+ "Stopwatch is already running." . to_string ( )
245+ } else {
246+ sw. 0 = Some ( Instant :: now ( ) ) ;
247+ if sw. 1 . as_secs ( ) == 0 {
248+ "Stopwatch started." . to_string ( )
249+ } else {
250+ "Stopwatch resumed." . to_string ( )
251+ }
252+ }
253+ }
254+
255+ /// Stops (pauses) the stopwatch and returns the elapsed time.
256+ pub fn stop_stopwatch ( ) -> String {
257+ let mut sw = STOPWATCH . write ( ) . unwrap ( ) ;
258+ match sw. 0 {
259+ Some ( start) => {
260+ let elapsed = start. elapsed ( ) ;
261+ sw. 1 += elapsed;
262+ sw. 0 = None ;
263+ let total = sw. 1 ;
264+ format ! (
265+ "Stopwatch stopped. Total elapsed time: {}" ,
266+ humantime:: format_duration( total)
267+ )
268+ }
269+ None => {
270+ if sw. 1 . as_secs ( ) == 0 {
271+ "Stopwatch is not running and has no recorded time." . to_string ( )
272+ } else {
273+ format ! (
274+ "Stopwatch is already stopped. Total elapsed time: {}" ,
275+ humantime:: format_duration( sw. 1 )
276+ )
277+ }
278+ }
279+ }
280+ }
281+
282+ /// Returns the current elapsed time without stopping the stopwatch.
283+ pub fn check_stopwatch ( ) -> String {
284+ let sw = STOPWATCH . read ( ) . unwrap ( ) ;
285+ let total = match sw. 0 {
286+ Some ( start) => sw. 1 + start. elapsed ( ) ,
287+ None => sw. 1 ,
288+ } ;
289+
290+ if sw. 0 . is_none ( ) && sw. 1 . as_secs ( ) == 0 {
291+ "Stopwatch has not been started yet." . to_string ( )
292+ } else {
293+ let status = if sw. 0 . is_some ( ) { "running" } else { "stopped" } ;
294+ format ! (
295+ "Stopwatch is {}. Elapsed time: {}" ,
296+ status,
297+ humantime:: format_duration( total)
298+ )
299+ }
300+ }
301+
302+ /// Resets the stopwatch to zero and stops it if running.
303+ pub fn reset_stopwatch ( ) -> String {
304+ let mut sw = STOPWATCH . write ( ) . unwrap ( ) ;
305+ * sw = ( None , std:: time:: Duration :: ZERO ) ;
306+ "Stopwatch has been reset to zero." . to_string ( )
307+ }
308+
231309#[ cfg( test) ]
232310mod tests {
233311 use super :: * ;
234- use tempfile:: tempdir;
235- use chrono:: { Local , Duration } ;
312+ use chrono:: { Duration , Local } ;
236313 use std:: env;
314+ use tempfile:: tempdir;
237315
238316 #[ test]
239317 fn set_get_delete_timer ( ) {
@@ -254,4 +332,94 @@ mod tests {
254332 delete_timer ( timers[ 0 ] . 0 ) . unwrap ( ) ;
255333 assert ! ( get_timers( ) . is_empty( ) ) ;
256334 }
335+
336+ #[ test]
337+ fn test_stopwatch_basic_operations ( ) {
338+ // Reset stopwatch to ensure clean state
339+ reset_stopwatch ( ) ;
340+
341+ // Give it a moment to settle
342+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 10 ) ) ;
343+
344+ // Check initial state (after reset it's zero but not started)
345+ let result = check_stopwatch ( ) ;
346+ assert ! (
347+ result. contains( "not been started" ) || result. contains( "Elapsed time: 0s" ) ,
348+ "Got: {}" ,
349+ result
350+ ) ;
351+
352+ // Start the stopwatch
353+ let result = start_stopwatch ( ) ;
354+ assert ! (
355+ result. contains( "started" ) || result. contains( "resumed" ) ,
356+ "Got: {}" ,
357+ result
358+ ) ;
359+
360+ // Starting again should indicate it's already running
361+ let result = start_stopwatch ( ) ;
362+ assert ! ( result. contains( "already running" ) , "Got: {}" , result) ;
363+
364+ // Sleep for a bit
365+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) ;
366+
367+ // Check while running
368+ let result = check_stopwatch ( ) ;
369+ assert ! ( result. contains( "running" ) , "Got: {}" , result) ;
370+
371+ // Stop the stopwatch
372+ let result = stop_stopwatch ( ) ;
373+ assert ! (
374+ result. contains( "stopped" ) || result. contains( "Total elapsed time" ) ,
375+ "Got: {}" ,
376+ result
377+ ) ;
378+
379+ // Reset the stopwatch
380+ let result = reset_stopwatch ( ) ;
381+ assert ! ( result. contains( "reset" ) , "Got: {}" , result) ;
382+
383+ // Check after reset
384+ let result = check_stopwatch ( ) ;
385+ assert ! (
386+ result. contains( "not been started" ) || result. contains( "Elapsed time: 0s" ) ,
387+ "Got: {}" ,
388+ result
389+ ) ;
390+ }
391+
392+ #[ test]
393+ fn test_stopwatch_pause_resume ( ) {
394+ reset_stopwatch ( ) ;
395+
396+ // Start stopwatch
397+ start_stopwatch ( ) ;
398+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 50 ) ) ;
399+
400+ // Stop (pause)
401+ stop_stopwatch ( ) ;
402+
403+ // Sleep while paused
404+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 50 ) ) ;
405+
406+ // Resume (after stopping, starting again should say resumed)
407+ let result = start_stopwatch ( ) ;
408+ assert ! (
409+ result. contains( "resumed" ) || result. contains( "started" ) ,
410+ "Got: {}" ,
411+ result
412+ ) ;
413+
414+ // Check elapsed time
415+ let result = check_stopwatch ( ) ;
416+ assert ! (
417+ result. contains( "running" ) || result. contains( "Elapsed time" ) ,
418+ "Got: {}" ,
419+ result
420+ ) ;
421+
422+ // Clean up
423+ reset_stopwatch ( ) ;
424+ }
257425}
0 commit comments