package tui import ( "time" "git.tjp.lol/punchcard/internal/queries" "github.com/charmbracelet/lipgloss/v2" ) // 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 var style lipgloss.Style if isSelected { style = selectedBoxStyle } else if m.timerInfo.IsActive { style = activeBoxStyle } else { style = unselectedBoxStyle } return style.Width(width).Height(height).Render(content) } // renderActiveTimer renders the active timer display func (m TimerBoxModel) renderActiveTimer() string { statusStyle := lipgloss.NewStyle().Foreground(colorTimerActive).Bold(true) content := statusStyle.Render("TRACKING") + "\n\n" // Timer duration - big and bold dur := FormatDuration(m.currentTime.Sub(m.timerInfo.StartTime)) content += activeTimerStyle.Render(dur) + "\n\n" // Project/Client info dimLabel := lipgloss.NewStyle().Foreground(colorDimmed) if m.timerInfo.ProjectName != "" { content += dimLabel.Render(m.timerInfo.ClientName+" /") + "\n" content += m.timerInfo.ProjectName + "\n" } else { content += m.timerInfo.ClientName + "\n" } // Start time localStartTime := m.timerInfo.StartTime.Local() content += dimLabel.Render("since " + localStartTime.Format("3:04 PM")) return content } // renderInactiveTimer renders the inactive timer display func (m TimerBoxModel) renderInactiveTimer() string { statusStyle := lipgloss.NewStyle().Foreground(colorDimmed) content := statusStyle.Render("IDLE") + "\n\n" if m.timerInfo.EntryID == 0 { content += inactiveTimerStyle.Render("No entries yet.\nPunch in to start.") return content } content += inactiveTimerStyle.Render(FormatDuration(m.timerInfo.Duration)) + "\n\n" dimLabel := lipgloss.NewStyle().Foreground(colorDimmed) if m.timerInfo.ProjectName != "" { content += dimLabel.Render(m.timerInfo.ClientName+" /") + "\n" content += inactiveTimerStyle.Render(m.timerInfo.ProjectName) } else { content += inactiveTimerStyle.Render(m.timerInfo.ClientName) } return content } func (m TimerBoxModel) activeTime() time.Duration { if !m.timerInfo.IsActive { return 0 } return m.currentTime.Sub(m.timerInfo.StartTime) }