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 (
"fmt"
"time"
"git.tjp.lol/punchcard/internal/queries"
)
// TimerInfo holds information about the current or most recent timer state
type TimerInfo struct {
IsActive bool
EntryID int64
StartTime time.Time
Duration time.Duration
ClientID int64
ClientName string
ProjectID *int64
ProjectName string
Description *string
BillableRate *float64
}
func (ti *TimerInfo) setNames(clients []queries.Client, projects map[int64][]queries.Project) {
for _, cl := range clients {
if cl.ID == ti.ClientID {
ti.ClientName = cl.Name
break
}
}
if ti.ProjectID == nil {
return
}
for _, group := range projects {
for _, proj := range group {
if proj.ID == *ti.ProjectID {
ti.ProjectName = proj.Name
return
}
}
}
}
// Box models for the three main components
type TimerBoxModel struct {
timerInfo TimerInfo
currentTime time.Time
}
// NewTimerBoxModel creates a new timer box model
func NewTimerBoxModel() TimerBoxModel {
return TimerBoxModel{
currentTime: time.Now(),
}
}
// View renders the timer box
func (m TimerBoxModel) View(width, height int, isSelected bool) string {
var content string
if m.timerInfo.IsActive {
content = m.renderActiveTimer()
} else {
content = m.renderInactiveTimer()
}
// Apply box styling
style := unselectedBoxStyle
if isSelected {
style = selectedBoxStyle
}
return style.Width(width).Height(height).Render(content)
}
// renderActiveTimer renders the active timer display
func (m TimerBoxModel) renderActiveTimer() string {
content := titleStyle.Render("⏰ Active Timer") + "\n\n"
// Timer duration
timerLine := fmt.Sprintf("Duration: %s", FormatDuration(m.currentTime.Sub(m.timerInfo.StartTime)))
content += activeTimerStyle.Render(timerLine) + "\n\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"
}
// 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"
// Description if available
if m.timerInfo.Description != nil {
content += "\n"
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"
}
return content
}
// renderInactiveTimer renders the inactive timer display
func (m TimerBoxModel) renderInactiveTimer() string {
content := titleStyle.Render("⌛ Last Timer (Inactive)") + "\n\n"
if m.timerInfo.EntryID == 0 {
content += inactiveTimerStyle.Render("No time entries yet.\nSelect a client or project and\npunch in to start tracking time.")
return content
}
timerLine := fmt.Sprintf("Duration: %s", FormatDuration(m.timerInfo.Duration))
content += inactiveTimerStyle.Render(timerLine) + "\n\n"
if m.timerInfo.ProjectName != "" {
content += inactiveTimerStyle.Render(fmt.Sprintf("Project: %s / %s", m.timerInfo.ClientName, m.timerInfo.ProjectName)) + "\n"
} else {
content += inactiveTimerStyle.Render(fmt.Sprintf("Client: %s", m.timerInfo.ClientName)) + "\n"
}
content += inactiveTimerStyle.Render(fmt.Sprintf("Started: %s", m.timerInfo.StartTime.Local().Format("2006/01/02 3:04 PM"))) + "\n"
if m.timerInfo.Description != nil {
content += "\n" + inactiveTimerStyle.Render(fmt.Sprintf("Description: %s", *m.timerInfo.Description)) + "\n"
}
if m.timerInfo.BillableRate != nil {
content += inactiveTimerStyle.Render(fmt.Sprintf("Rate: $%.2f/hr", *m.timerInfo.BillableRate)) + "\n"
}
return content
}
func (m TimerBoxModel) activeTime() time.Duration {
if !m.timerInfo.IsActive {
return 0
}
return m.currentTime.Sub(m.timerInfo.StartTime)
}
|