1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
|
package tui
import (
"context"
"fmt"
"time"
"punchcard/internal/queries"
tea "github.com/charmbracelet/bubbletea"
)
// TimerModel represents the timer view model
type TimerModel struct {
ctx context.Context
queries *queries.Queries
timerInfo TimerInfo
stats TimeStats
lastTick time.Time
}
// NewTimerModel creates a new timer model
func NewTimerModel(ctx context.Context, q *queries.Queries) TimerModel {
return TimerModel{
ctx: ctx,
queries: q,
}
}
// Init initializes the timer model
func (m TimerModel) Init() tea.Cmd {
return tea.Batch(
m.updateData(),
m.tickCmd(),
)
}
// Update handles messages for the timer model
func (m TimerModel) Update(msg tea.Msg) (TimerModel, tea.Cmd) {
switch msg := msg.(type) {
case TickMsg:
// Update timer duration if active
if m.timerInfo.IsActive {
m.timerInfo.Duration = time.Since(m.timerInfo.StartTime)
}
m.lastTick = time.Time(msg)
return m, m.tickCmd()
}
return m, nil
}
// View renders the timer view
func (m TimerModel) View(width, height int) string {
var content string
if m.timerInfo.IsActive {
content += m.renderActiveTimer()
} else {
content += m.renderInactiveTimer()
}
return content
}
// renderActiveTimer renders the active timer display
func (m TimerModel) renderActiveTimer() string {
var content string
// Timer status
timerLine := fmt.Sprintf("⏱ Tracking: %s", FormatDuration(m.timerInfo.Duration))
content += activeTimerStyle.Render(timerLine) + "\n"
// Project/Client info
if m.timerInfo.ProjectName != "" {
projectLine := fmt.Sprintf("Project: %s / %s", m.timerInfo.ClientName, m.timerInfo.ProjectName)
content += projectLine + "\n"
} else {
clientLine := fmt.Sprintf("Client: %s", m.timerInfo.ClientName)
content += clientLine + "\n"
}
// Description if available
if m.timerInfo.Description != "" {
descLine := fmt.Sprintf("Description: %s", m.timerInfo.Description)
content += descLine + "\n"
}
// Billable rate if available
if m.timerInfo.BillableRate != nil {
rateLine := fmt.Sprintf("Rate: $%.2f/hr", *m.timerInfo.BillableRate)
content += rateLine + "\n"
}
// Start time (convert from UTC to local)
localStartTime := m.timerInfo.StartTime.Local()
startLine := fmt.Sprintf("Started: %s", localStartTime.Format("3:04 PM"))
content += startLine + "\n"
return content
}
// renderInactiveTimer renders the inactive timer display
func (m TimerModel) renderInactiveTimer() string {
var content string
content += inactiveTimerStyle.Render("⚪ No active timer") + "\n"
content += "\n"
content += "Ready to start tracking time.\n"
return content
}
// updateData fetches fresh data from the database
func (m TimerModel) updateData() tea.Cmd {
return func() tea.Msg {
// Get timer info
timerInfo, err := GetTimerInfo(m.ctx, m.queries)
if err != nil {
// Handle error silently for now
return nil
}
// Get time stats
stats, err := GetTimeStats(m.ctx, m.queries)
if err != nil {
// Handle error silently for now
return nil
}
return dataUpdatedMsg{
timerInfo: timerInfo,
stats: stats,
}
}
}
// tickCmd returns a command that sends a tick message every second
func (m TimerModel) tickCmd() tea.Cmd {
return tea.Tick(time.Second, func(t time.Time) tea.Msg {
return TickMsg(t)
})
}
// UpdateData updates the model with fresh data
func (m TimerModel) UpdateData(timerInfo TimerInfo, stats TimeStats) TimerModel {
m.timerInfo = timerInfo
m.stats = stats
return m
}
|