diff --git a/tmlshock b/tmlshock index 8c6c098..853c452 100755 Binary files a/tmlshock and b/tmlshock differ diff --git a/util/stopwatch.go b/util/stopwatch.go index df6c0e9..2a9ba8d 100644 --- a/util/stopwatch.go +++ b/util/stopwatch.go @@ -5,12 +5,54 @@ import ( "github.com/nsf/termbox-go" "github.com/urfave/cli/v2" "os" + "sync" "time" ) var SmallStopWatchDiff = [11]int{7, 6, 6, 7, 6, 6, 7, 6, 4, 7, 7} var SmallStopWatchWithoutHourDiff = [8]int{7, 6, 6, 7, 6, 4, 7, 7} +// stopwatchState tracks the running/paused state so the space bar can +// pause, resume, or reset-and-restart the stopwatch from the event loop +// while the render goroutine keeps reading the current elapsed time. +type stopwatchState struct { + mu sync.Mutex + running bool + elapsed time.Duration + lastStart time.Time +} + +func (s *stopwatchState) current() time.Duration { + s.mu.Lock() + defer s.mu.Unlock() + if s.running { + return s.elapsed + time.Since(s.lastStart) + } + return s.elapsed +} + +// togglePauseResume pauses a running stopwatch or resumes a paused one +// from where it left off. +func (s *stopwatchState) togglePauseResume() { + s.mu.Lock() + defer s.mu.Unlock() + if s.running { + s.elapsed += time.Since(s.lastStart) + s.running = false + } else { + s.lastStart = time.Now() + s.running = true + } +} + +func (s *stopwatchState) resetAndStart() { + s.mu.Lock() + defer s.mu.Unlock() + s.elapsed = 0 + s.lastStart = time.Now() + s.running = true +} + func Stopwatch(cCtx *cli.Context) error { err := termbox.Init() if err != nil { @@ -27,11 +69,12 @@ func Stopwatch(cCtx *cli.Context) error { } termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) + state := &stopwatchState{running: true, lastStart: time.Now()} + go func() { - start := time.Now() for { termWidth, termHeight := termbox.Size() - current := time.Since(start).Round(time.Millisecond) + current := state.current().Round(time.Millisecond) termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) NowTime := "" if cCtx.String("disable-hour") == "true" { @@ -65,6 +108,8 @@ func Stopwatch(cCtx *cli.Context) error { } }() + var lastSpacePress time.Time + mainLoop: for { ev := termbox.PollEvent() @@ -75,6 +120,15 @@ mainLoop: break mainLoop case ev.Key == termbox.KeyCtrlC: break mainLoop + case ev.Key == termbox.KeySpace: + now := time.Now() + if !lastSpacePress.IsZero() && now.Sub(lastSpacePress) <= doublePressWindow { + state.resetAndStart() + lastSpacePress = time.Time{} + } else { + state.togglePauseResume() + lastSpacePress = now + } } case termbox.EventError: os.Exit(1) diff --git a/util/timer.go b/util/timer.go index 498a3bd..6c6156d 100644 --- a/util/timer.go +++ b/util/timer.go @@ -6,6 +6,7 @@ import ( "github.com/urfave/cli/v2" "os" "strconv" + "sync" "time" ) @@ -15,6 +16,48 @@ var SmallTimerWithoutHourDiff = [11]int{7, 6, 6, 7, 6, 4, 7, 7} var SmallTimerWithoutHourAndMillisecondDiff = [11]int{7, 6, 6, 7, 6} var SmallTimerWithoutMillisecondDiff = [11]int{7, 6, 6, 7, 6, 6, 7, 6} +// timerState tracks the running/paused state so the space bar can pause, +// resume, or reset-and-restart the countdown from the event loop while the +// render goroutine keeps reading the current remaining time. +type timerState struct { + mu sync.Mutex + running bool + elapsed time.Duration + lastStart time.Time +} + +func (s *timerState) remaining(total time.Duration) time.Duration { + s.mu.Lock() + defer s.mu.Unlock() + elapsed := s.elapsed + if s.running { + elapsed += time.Since(s.lastStart) + } + return total - elapsed +} + +// togglePauseResume pauses a running countdown or resumes a paused one +// from where it left off. +func (s *timerState) togglePauseResume() { + s.mu.Lock() + defer s.mu.Unlock() + if s.running { + s.elapsed += time.Since(s.lastStart) + s.running = false + } else { + s.lastStart = time.Now() + s.running = true + } +} + +func (s *timerState) resetAndStart() { + s.mu.Lock() + defer s.mu.Unlock() + s.elapsed = 0 + s.lastStart = time.Now() + s.running = true +} + func Timer(cCtx *cli.Context) error { err := termbox.Init() if err != nil { @@ -45,12 +88,13 @@ func Timer(cCtx *cli.Context) error { if cCtx.String("colon-color") == "" && cCtx.String("color") != "" { colonColor = FlagColor(cCtx.String("color")) } + duration := time.Duration(total) * time.Second + state := &timerState{running: true, lastStart: time.Now()} + go func() { - duration := time.Duration(total) * time.Second - end := time.Now().Add(duration) for { termWidth, termHeight := termbox.Size() - current := time.Until(end) + current := state.remaining(duration) termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) NowTime := "" @@ -104,6 +148,8 @@ func Timer(cCtx *cli.Context) error { } }() + var lastSpacePress time.Time + mainLoop: for { ev := termbox.PollEvent() @@ -114,6 +160,15 @@ mainLoop: break mainLoop case ev.Key == termbox.KeyCtrlC: break mainLoop + case ev.Key == termbox.KeySpace: + now := time.Now() + if !lastSpacePress.IsZero() && now.Sub(lastSpacePress) <= doublePressWindow { + state.resetAndStart() + lastSpacePress = time.Time{} + } else { + state.togglePauseResume() + lastSpacePress = now + } } case termbox.EventError: os.Exit(1) diff --git a/util/util.go b/util/util.go index eca724f..52400b6 100644 --- a/util/util.go +++ b/util/util.go @@ -9,6 +9,11 @@ import ( "github.com/nsf/termbox-go" ) +// doublePressWindow is the maximum gap between two space bar presses for +// them to count as a double press (reset and auto-restart) rather than +// two separate single presses (pause/resume). +const doublePressWindow = 500 * time.Millisecond + func StopwatchFormatTime(d time.Duration, disableMillisecond bool) string { h := d / time.Hour d -= h * time.Hour