Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions stopwatch/stopwatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand All @@ -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 {
Expand Down
52 changes: 52 additions & 0 deletions stopwatch/stopwatch_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}