diff --git a/stopwatch/stopwatch.go b/stopwatch/stopwatch.go index 65facfc5..48a98f1e 100644 --- a/stopwatch/stopwatch.go +++ b/stopwatch/stopwatch.go @@ -54,6 +54,7 @@ type ResetMsg struct { // Model for the stopwatch component. type Model struct { d time.Duration + start time.Time id int tag int running bool @@ -65,7 +66,8 @@ type Model struct { // New creates a new stopwatch with 1s interval. func New(opts ...Option) Model { m := Model{ - id: nextID(), + id: nextID(), + Interval: time.Second, } for _, opt := range opts { @@ -86,9 +88,9 @@ func (m Model) Init() tea.Cmd { // Start starts the stopwatch. func (m Model) Start() tea.Cmd { - return tea.Sequence(func() tea.Msg { + return func() tea.Msg { return StartStopMsg{ID: m.id, running: true} - }, tick(m.id, m.tag, m.Interval)) + } } // Stop stops the stopwatch. @@ -125,12 +127,28 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { if msg.ID != m.id { return m, nil } - m.running = msg.running + if msg.running { + if !m.running { + m.start = time.Now() + } + m.running = true + return m, tick(m.id, m.tag, m.Interval) + } + if m.running { + m.d += time.Since(m.start) + m.start = time.Time{} + } + m.running = false case ResetMsg: if msg.ID != m.id { return m, nil } m.d = 0 + if m.running { + m.start = time.Now() + } else { + m.start = time.Time{} + } case TickMsg: if !m.running || msg.ID != m.id { break @@ -143,7 +161,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { return m, nil } - m.d += m.Interval m.tag++ return m, tick(m.id, m.tag, m.Interval) } @@ -153,12 +170,15 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { // Elapsed returns the time elapsed. func (m Model) Elapsed() time.Duration { + if m.running && !m.start.IsZero() { + return m.d + time.Since(m.start) + } return m.d } // View of the timer component. func (m Model) View() string { - return m.d.String() + return m.Elapsed().String() } func tick(id int, tag int, d time.Duration) tea.Cmd { diff --git a/stopwatch/stopwatch_test.go b/stopwatch/stopwatch_test.go new file mode 100644 index 00000000..edc44d07 --- /dev/null +++ b/stopwatch/stopwatch_test.go @@ -0,0 +1,52 @@ +package stopwatch + +import ( + "testing" + "time" +) + +func TestNewDefaultInterval(t *testing.T) { + t.Parallel() + m := New() + if m.Interval != time.Second { + t.Fatalf("Interval = %v, want %v", m.Interval, time.Second) + } +} + +func TestWithInterval(t *testing.T) { + t.Parallel() + m := New(WithInterval(5 * time.Second)) + if m.Interval != 5*time.Second { + t.Fatalf("Interval = %v, want %v", m.Interval, 5*time.Second) + } +} + +func TestElapsedUsesWallClock(t *testing.T) { + m := New(WithInterval(time.Hour)) + m, _ = m.Update(StartStopMsg{ID: m.id, running: true}) + + time.Sleep(50 * time.Millisecond) + + elapsed := m.Elapsed() + if elapsed < 40*time.Millisecond { + t.Fatalf("Elapsed() = %v, want at least 40ms", elapsed) + } + if elapsed > 200*time.Millisecond { + t.Fatalf("Elapsed() = %v, unexpectedly large", elapsed) + } +} + +func TestStopAccumulatesElapsed(t *testing.T) { + m := New(WithInterval(time.Hour)) + m, _ = m.Update(StartStopMsg{ID: m.id, running: true}) + m.start = time.Now().Add(-100 * time.Millisecond) + + m, _ = m.Update(StartStopMsg{ID: m.id, running: false}) + + if m.Elapsed() < 90*time.Millisecond { + t.Fatalf("Elapsed() after stop = %v, want at least 90ms", m.Elapsed()) + } + if m.running { + t.Fatal("expected stopwatch to be stopped") + } +}