summaryrefslogtreecommitdiff
path: root/internal/tui
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tui')
-rw-r--r--internal/tui/app.go60
-rw-r--r--internal/tui/history_box.go81
-rw-r--r--internal/tui/modal.go6
-rw-r--r--internal/tui/projects_box.go66
-rw-r--r--internal/tui/shared.go92
-rw-r--r--internal/tui/timer_box.go67
6 files changed, 202 insertions, 170 deletions
diff --git a/internal/tui/app.go b/internal/tui/app.go
index 38457ff..9850595 100644
--- a/internal/tui/app.go
+++ b/internal/tui/app.go
@@ -459,43 +459,67 @@ func (m AppModel) View() string {
return "Loading..."
}
+ // Constrain content width for large displays
+ contentWidth := m.width
+ if contentWidth > maxContentWidth {
+ contentWidth = maxContentWidth
+ }
+
topBarHeight := 1
bottomBarHeight := 2
contentHeight := m.height - topBarHeight - bottomBarHeight
- // Timer box top-left
- timerBoxWidth := (m.width / 3)
- timerBoxHeight := (contentHeight / 2)
- if timerBoxWidth < 30 {
- timerBoxWidth = 30
+ // Sidebar: fixed comfortable width, capped to avoid dominating
+ sidebarWidth := 38
+ if sidebarWidth > contentWidth/3 {
+ sidebarWidth = contentWidth / 3
+ }
+ if sidebarWidth < 30 {
+ sidebarWidth = 30
+ }
+
+ // Timer box is compact - just needs enough for the timer display
+ timerBoxHeight := contentHeight * 2 / 5
+ if timerBoxHeight < 8 {
+ timerBoxHeight = 8
+ }
+ if timerBoxHeight > 14 {
+ timerBoxHeight = 14
}
- // Projects box bottom-left
- projectsBoxWidth := timerBoxWidth
+ // Projects box gets the rest of the left column
projectsBoxHeight := contentHeight - timerBoxHeight
// History box right side full height
- historyBoxWidth := m.width - projectsBoxWidth
+ historyBoxWidth := contentWidth - sidebarWidth
historyBoxHeight := contentHeight
- activeDur := m.timerBox.activeTime()
- stats := m.timeStats
- stats.TodayTotal += activeDur
- stats.WeekTotal += activeDur
+ topBar := RenderTopBar(m, contentWidth)
- topBar := RenderTopBar(m)
-
- timerBox := m.timerBox.View(timerBoxWidth, timerBoxHeight, m.selectedBox == TimerBox)
- projectsBox := m.projectsBox.View(projectsBoxWidth, projectsBoxHeight, m.selectedBox == ProjectsBox)
+ timerBox := m.timerBox.View(sidebarWidth, timerBoxHeight, m.selectedBox == TimerBox)
+ projectsBox := m.projectsBox.View(sidebarWidth, projectsBoxHeight, m.selectedBox == ProjectsBox)
historyBox := m.historyBox.View(historyBoxWidth, historyBoxHeight, m.selectedBox == HistoryBox, m.timerBox, m.projectsBox.clients, m.projectsBox.projects)
leftColumn := lipgloss.JoinVertical(lipgloss.Left, timerBox, projectsBox)
mainContent := lipgloss.JoinHorizontal(lipgloss.Top, leftColumn, historyBox)
keyBindings := activeBindings(m.selectedBox, m.historyBox.viewLevel, m.modalBox)
- bottomBar := RenderBottomBar(m, keyBindings, m.err)
+ bottomBar := RenderBottomBar(m, keyBindings, m.err, contentWidth)
+
+ fullView := topBar + "\n" + mainContent + "\n" + bottomBar
+
+ // Apply modal overlay (uses contentWidth for centering within the content area)
+ fullView = m.modalBox.RenderCenteredOver(fullView, m, contentWidth)
+
+ // Center the content if terminal is wider than max
+ if m.width > contentWidth {
+ fullView = lipgloss.NewStyle().
+ Width(m.width).
+ Align(lipgloss.Center).
+ Render(fullView)
+ }
- return m.modalBox.RenderCenteredOver(topBar+"\n"+mainContent+"\n"+bottomBar, m)
+ return fullView
}
// dataUpdatedMsg is sent when data is updated from the database
diff --git a/internal/tui/history_box.go b/internal/tui/history_box.go
index 17958c3..9e01f2f 100644
--- a/internal/tui/history_box.go
+++ b/internal/tui/history_box.go
@@ -184,7 +184,7 @@ func (m HistoryBoxModel) View(width, height int, isSelected bool, timer TimerBox
var content string
if len(m.entries) == 0 {
- content = "📝 Recent History\n\n"
+ content = titleStyle.Render("History") + "\n\n"
content += inactiveTimerStyle.Render("No recent entries\n\nStart tracking time to\nsee your history here.")
} else {
switch m.viewLevel {
@@ -224,23 +224,18 @@ func (m HistoryBoxModel) selectionHeight() int {
}
func (m HistoryBoxModel) summarySelectionHeight() int {
- height := 1 // "Recent History" title line
-
- if len(m.summaryItems) > 0 {
- height += 3 // 2 newlines + filter info line
- }
+ height := 1 // title line (History + filter info on same line)
var date *time.Time
for i, item := range m.summaryItems {
if date == nil || !date.Equal(item.Date) {
date = &item.Date
- height += 4 // 2 newlines, the date, 1 more newline
+ height += 2 // blank line + date header
}
- height += 1 // newline before the selectable line
+ height += 1 // the entry line
if i == m.summarySelection {
return height
}
- height += 1 // the selectable line that's not selected
}
return 0
}
@@ -259,25 +254,26 @@ func (m HistoryBoxModel) detailsSelectionHeight() int {
var (
titleStyle = lipgloss.NewStyle().Bold(true)
- dateStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("3"))
+ dateStyle = lipgloss.NewStyle().Bold(true).Foreground(colorDate)
summaryItemStyle = lipgloss.NewStyle()
- selectedItemStyle = lipgloss.NewStyle().Background(lipgloss.Color("62")).Foreground(lipgloss.Color("230"))
+ selectedItemStyle = lipgloss.NewStyle().Background(colorSelected).Foreground(colorSelectedFg)
+ durationStyle = lipgloss.NewStyle().Foreground(colorDimmed)
entryStyle = lipgloss.NewStyle()
- selectedEntryStyle = lipgloss.NewStyle().Background(lipgloss.Color("62")).Foreground(lipgloss.Color("230"))
- activeEntryStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("196"))
- selectedActiveEntryStyle = lipgloss.NewStyle().Background(lipgloss.Color("196")).Foreground(lipgloss.Color("230"))
- descriptionStyle = lipgloss.NewStyle()
- activeDescriptionStyle = lipgloss.NewStyle().Background(lipgloss.Color("62")).Foreground(lipgloss.Color("230"))
- filterInfoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("246"))
+ selectedEntryStyle = lipgloss.NewStyle().Background(colorSelected).Foreground(colorSelectedFg)
+ activeEntryStyle = lipgloss.NewStyle().Bold(true).Foreground(colorTimerText)
+ selectedActiveEntryStyle = lipgloss.NewStyle().Background(colorWarning).Foreground(colorSelectedFg)
+ descriptionStyle = lipgloss.NewStyle().Foreground(colorDimmed)
+ activeDescriptionStyle = lipgloss.NewStyle().Background(colorSelected).Foreground(colorSelectedFg)
+ filterInfoStyle = lipgloss.NewStyle().Foreground(colorDimmed)
)
// renderSummaryView renders the summary view (level 1) with date headers and client/project summaries
func (m HistoryBoxModel) renderSummaryView(timer TimerBoxModel, clients []queries.Client, projects map[int64][]queries.Project) string {
- content := titleStyle.Render("📝 Recent History")
+ content := titleStyle.Render("History")
if len(m.summaryItems) > 0 {
filterInfo := m.formatFilterInfo(clients, projects, timer)
- content += "\n\n" + filterInfoStyle.Render(filterInfo)
+ content += " " + filterInfoStyle.Render(filterInfo)
}
var activeKey HistorySummaryKey
@@ -292,19 +288,15 @@ func (m HistoryBoxModel) renderSummaryView(timer TimerBoxModel, clients []querie
}
if len(m.summaryItems) == 0 {
- return "\n\nNo recent entries found."
+ return content + "\n\n" + filterInfoStyle.Render("No entries found.")
}
var date *time.Time
for i, item := range m.summaryItems {
if date == nil || !date.Equal(item.Date) {
date = &item.Date
- content += fmt.Sprintf("\n\n%s\n", dateStyle.Render(date.Format("Mon 01/02")))
- }
-
- style := summaryItemStyle
- if m.summarySelection == i {
- style = selectedItemStyle
+ content += "\n"
+ content += fmt.Sprintf("\n%s", dateStyle.Render(date.Format("Mon Jan 02")))
}
dur := item.TotalDuration
@@ -312,8 +304,16 @@ func (m HistoryBoxModel) renderSummaryView(timer TimerBoxModel, clients []querie
dur += timer.currentTime.Sub(timer.timerInfo.StartTime)
}
- line := fmt.Sprintf(" %s (%s)", m.formatSummaryTitle(item), FormatDuration(dur))
- content += fmt.Sprintf("\n%s", style.Render(line))
+ title := m.formatSummaryTitle(item)
+ durStr := FormatDuration(dur)
+
+ if m.summarySelection == i {
+ line := fmt.Sprintf(" %s %s", title, durStr)
+ content += "\n" + selectedItemStyle.Render(line)
+ } else {
+ line := fmt.Sprintf(" %s ", title)
+ content += "\n" + summaryItemStyle.Render(line) + durationStyle.Render(durStr)
+ }
}
return content
@@ -338,12 +338,13 @@ func (m HistoryBoxModel) selectedEntries() []queries.TimeEntry {
func (m HistoryBoxModel) renderDetailsView(timer TimerBoxModel) string {
summary := m.summaryItems[m.summarySelection]
clientProject := m.formatSummaryTitle(summary)
- date := summary.Date.Format("Mon 01/02")
- content := titleStyle.Render(fmt.Sprintf("📝 %s on %s", clientProject, date)) + "\n\n"
+ date := summary.Date.Format("Mon Jan 02")
+ content := titleStyle.Render(clientProject) +
+ filterInfoStyle.Render(" "+date) + "\n\n"
entries := m.selectedEntries()
if len(entries) == 0 {
- return "No entries found for this selection."
+ return content + filterInfoStyle.Render("No entries found.")
}
for i, entry := range entries {
@@ -363,7 +364,7 @@ func (m HistoryBoxModel) renderDetailsView(timer TimerBoxModel) string {
timeRange = fmt.Sprintf("%s - now", startTime)
}
- entryLine := fmt.Sprintf("%s (%s)", timeRange, FormatDuration(duration))
+ durStr := FormatDuration(duration)
var style lipgloss.Style
if m.detailSelection == i {
@@ -380,14 +381,20 @@ func (m HistoryBoxModel) renderDetailsView(timer TimerBoxModel) string {
}
}
- content += style.Render(entryLine)
-
- descStyle := descriptionStyle
if m.detailSelection == i {
- descStyle = activeDescriptionStyle
+ entryLine := fmt.Sprintf(" %s %s", timeRange, durStr)
+ content += style.Render(entryLine)
+ } else {
+ content += style.Render(fmt.Sprintf(" %s ", timeRange))
+ content += durationStyle.Render(durStr)
}
+
if entry.Description.Valid {
- content += descStyle.Render(fmt.Sprintf(" \"%s\"", entry.Description.String))
+ descStyle := descriptionStyle
+ if m.detailSelection == i {
+ descStyle = activeDescriptionStyle
+ }
+ content += descStyle.Render(" " + entry.Description.String)
}
content += "\n"
diff --git a/internal/tui/modal.go b/internal/tui/modal.go
index b614d04..0e6c517 100644
--- a/internal/tui/modal.go
+++ b/internal/tui/modal.go
@@ -78,7 +78,7 @@ func (m *ModalBoxModel) HandleKeyPress(msg tea.KeyMsg) tea.Cmd {
return cmd
}
-func (m ModalBoxModel) RenderCenteredOver(mainContent string, app AppModel) string {
+func (m ModalBoxModel) RenderCenteredOver(mainContent string, app AppModel, contentWidth int) string {
if !m.Active {
return mainContent
}
@@ -86,12 +86,12 @@ func (m ModalBoxModel) RenderCenteredOver(mainContent string, app AppModel) stri
modalContent := m.Render()
base := lipgloss.NewLayer(mainContent)
- overlayStyle := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("62")).Padding(2, 4)
+ overlayStyle := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(colorAccent).Padding(2, 4)
overlay := lipgloss.NewLayer(overlayStyle.Render(modalContent))
overlayWidth := overlay.GetWidth()
overlayHeight := overlay.GetHeight()
- overlayLeft := (app.width - overlayWidth) / 2
+ overlayLeft := (contentWidth - overlayWidth) / 2
overlayTop := (app.height - overlayHeight) / 2
canvas := lipgloss.NewCanvas(
diff --git a/internal/tui/projects_box.go b/internal/tui/projects_box.go
index cd50d5e..d909e20 100644
--- a/internal/tui/projects_box.go
+++ b/internal/tui/projects_box.go
@@ -74,7 +74,7 @@ func (m ClientsProjectsModel) View(width, height int, isSelected bool) string {
style = selectedBoxStyle
}
- title := titleStyle.Render("👥 Clients & Projects")
+ title := titleStyle.Render("Clients & Projects")
return style.Width(width).Height(height).Render(
fmt.Sprintf("%s\n\n%s", title, content),
@@ -86,57 +86,51 @@ func (m ClientsProjectsModel) renderClientsAndProjects() string {
var content string
visibleClients := m.visibleClients()
+ rateStyle := lipgloss.NewStyle().Foreground(colorDimmed)
+ treeStyle := lipgloss.NewStyle().Foreground(colorSubtle)
+
for i, client := range visibleClients {
if i > 0 {
content += "\n"
}
- // Build client name and rate
- clientText := client.Name
- if client.BillableRate.Valid {
- rateInDollars := float64(client.BillableRate.Int64) / 100.0
- clientText += fmt.Sprintf(" ($%.2f/hr)", rateInDollars)
- }
-
// Style for client text
clientStyle := lipgloss.NewStyle().Bold(true)
if m.selectedClient == i && m.selectedProject == nil {
- clientStyle = clientStyle.Background(lipgloss.Color("62")).Foreground(lipgloss.Color("230"))
+ clientStyle = clientStyle.Background(colorSelected).Foreground(colorSelectedFg)
} else if client.Archived != 0 {
- // Gray out archived clients
- clientStyle = clientStyle.Foreground(lipgloss.Color("246"))
+ clientStyle = clientStyle.Foreground(colorDimmed)
}
- content += "• " + clientStyle.Render(clientText) + "\n"
+ clientText := client.Name
+ content += clientStyle.Render(clientText)
+ if client.BillableRate.Valid && !(m.selectedClient == i && m.selectedProject == nil) {
+ rateInDollars := float64(client.BillableRate.Int64) / 100.0
+ content += rateStyle.Render(fmt.Sprintf(" $%g", rateInDollars))
+ }
+ content += "\n"
visibleProjects := m.visibleProjects(client.ID)
- if len(visibleProjects) == 0 {
- content += " └── (no projects)\n"
- } else {
- for j, project := range visibleProjects {
- prefix := "├──"
- if j == len(visibleProjects)-1 {
- prefix = "└──"
- }
-
- // Build project name and rate
- projectText := project.Name
- if project.BillableRate.Valid {
- rateInDollars := float64(project.BillableRate.Int64) / 100.0
- projectText += fmt.Sprintf(" ($%.2f/hr)", rateInDollars)
- }
+ for j, project := range visibleProjects {
+ prefix := "├ "
+ if j == len(visibleProjects)-1 {
+ prefix = "└ "
+ }
- // Style for project text
- projectStyle := lipgloss.NewStyle()
- if m.selectedClient == i && m.selectedProject != nil && *m.selectedProject == j {
- projectStyle = projectStyle.Background(lipgloss.Color("62")).Foreground(lipgloss.Color("230"))
- } else if project.Archived != 0 {
- // Gray out archived projects
- projectStyle = projectStyle.Foreground(lipgloss.Color("246"))
- }
+ // Style for project text
+ projectStyle := lipgloss.NewStyle()
+ if m.selectedClient == i && m.selectedProject != nil && *m.selectedProject == j {
+ projectStyle = projectStyle.Background(colorSelected).Foreground(colorSelectedFg)
+ } else if project.Archived != 0 {
+ projectStyle = projectStyle.Foreground(colorDimmed)
+ }
- content += fmt.Sprintf(" %s ", prefix) + projectStyle.Render(projectText) + "\n"
+ content += treeStyle.Render(" "+prefix) + projectStyle.Render(project.Name)
+ if project.BillableRate.Valid && !(m.selectedClient == i && m.selectedProject != nil && *m.selectedProject == j) {
+ rateInDollars := float64(project.BillableRate.Int64) / 100.0
+ content += rateStyle.Render(fmt.Sprintf(" $%g", rateInDollars))
}
+ content += "\n"
}
}
diff --git a/internal/tui/shared.go b/internal/tui/shared.go
index 7271293..9025fb8 100644
--- a/internal/tui/shared.go
+++ b/internal/tui/shared.go
@@ -13,34 +13,57 @@ import (
"github.com/charmbracelet/lipgloss/v2"
)
+// Maximum content width for large displays - prevents over-stretching
+const maxContentWidth = 180
+
var (
+ // Color palette
+ colorAccent = lipgloss.Color("4") // Blue accent
+ colorAccentBright = lipgloss.Color("12") // Bright blue
+ colorTimerActive = lipgloss.Color("2") // Green for active timer
+ colorTimerText = lipgloss.Color("10") // Bright green
+ colorDimmed = lipgloss.Color("242") // Dimmed text
+ colorSubtle = lipgloss.Color("238") // Very subtle borders/separators
+ colorFg = lipgloss.Color("253") // Main foreground
+ colorBg = lipgloss.Color("235") // Slightly lighter than terminal default
+ colorBarBg = lipgloss.Color("236") // Bar background
+ colorSelected = lipgloss.Color("4") // Selection background
+ colorSelectedFg = lipgloss.Color("15") // White text on selection
+ colorDate = lipgloss.Color("6") // Cyan for dates
+ colorWarning = lipgloss.Color("1") // Red for warnings/errors
+
// Styles for the TUI
- topBarInactiveStyle = lipgloss.NewStyle().
- Background(lipgloss.Color("21")).
- Foreground(lipgloss.Color("230")).
- Padding(0, 1)
+ topBarStyle = lipgloss.NewStyle().
+ Background(colorBarBg).
+ Foreground(colorFg).
+ Padding(0, 1)
bottomBarStyle = lipgloss.NewStyle().
- Background(lipgloss.Color("238")).
- Foreground(lipgloss.Color("252"))
+ Background(colorBarBg).
+ Foreground(colorDimmed)
// Box styles
selectedBoxStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
- BorderForeground(lipgloss.Color("62")).
+ BorderForeground(colorAccent).
Padding(1, 2)
unselectedBoxStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
- BorderForeground(lipgloss.Color("238")).
+ BorderForeground(colorSubtle).
Padding(1, 2)
activeTimerStyle = lipgloss.NewStyle().
- Foreground(lipgloss.Color("196")).
+ Foreground(colorTimerText).
Bold(true)
+ activeBoxStyle = lipgloss.NewStyle().
+ Border(lipgloss.RoundedBorder()).
+ BorderForeground(colorTimerActive).
+ Padding(1, 2)
+
inactiveTimerStyle = lipgloss.NewStyle().
- Foreground(lipgloss.Color("246"))
+ Foreground(colorDimmed)
)
// FormatDuration formats a duration in a human-readable way
@@ -66,7 +89,7 @@ func getContractorInfo(ctx context.Context, q *queries.Queries) (ContractorInfo,
}
return ContractorInfo{
- name: c.Name,
+ name: c.Name,
label: c.Label,
email: c.Email,
}, nil
@@ -133,8 +156,8 @@ func getMostRecentTimerInfo(ctx context.Context, q *queries.Queries) (TimerInfo,
}
// RenderTopBar renders the top bar with view name and time stats
-func RenderTopBar(m AppModel) string {
- left := fmt.Sprintf("👊 Punchcard ♦️ - %s", m.selectedBox.String())
+func RenderTopBar(m AppModel, contentWidth int) string {
+ leftText := fmt.Sprintf("Punchcard / %s", m.selectedBox.String())
today := m.timeStats.TodayTotal
week := m.timeStats.WeekTotal
@@ -145,35 +168,35 @@ func RenderTopBar(m AppModel) string {
week += activeTime
}
- right := fmt.Sprintf("Today: %s | Week: %s", FormatDuration(today), FormatDuration(week))
+ rightText := fmt.Sprintf("today %s week %s", FormatDuration(today), FormatDuration(week))
- // Use lipgloss to create left and right aligned content
leftStyle := lipgloss.NewStyle().Align(lipgloss.Left)
rightStyle := lipgloss.NewStyle().Align(lipgloss.Right)
- // Calculate available width for content (minus padding)
- contentWidth := m.width - 2 // Account for horizontal padding
+ // Account for the 2 chars of padding in topBarStyle
+ innerWidth := contentWidth - 2
- // Create a layout with left and right content
content := lipgloss.JoinHorizontal(
lipgloss.Top,
- leftStyle.Width(contentWidth/2).Render(left),
- rightStyle.Width(contentWidth/2).Render(right),
+ leftStyle.Width(innerWidth/2).Render(leftText),
+ rightStyle.Width(innerWidth-innerWidth/2).Render(rightText),
)
- return topBarInactiveStyle.Width(m.width).Render(content)
+ return topBarStyle.Width(contentWidth).Render(content)
}
// RenderBottomBar renders the bottom bar with key bindings
-func RenderBottomBar(m AppModel, bindings []KeyBinding, err error) string {
+func RenderBottomBar(m AppModel, bindings []KeyBinding, err error, contentWidth int) string {
var content string
- // Style for keys (inherits background from bottomBarStyle)
- keyStyle := bottomBarStyle.Bold(true)
- // Style for descriptions (inherits background from bottomBarStyle)
- descStyle := bottomBarStyle
- // Style for separators (inherits background from bottomBarStyle)
- sepStyle := bottomBarStyle
+ keyStyle := lipgloss.NewStyle().
+ Background(lipgloss.Color("240")).
+ Foreground(lipgloss.Color("15")).
+ Padding(0, 1)
+ descStyle := lipgloss.NewStyle().
+ Background(colorBarBg).
+ Foreground(colorDimmed)
+ sepStyle := lipgloss.NewStyle().Background(colorBarBg)
for i, binding := range bindings {
if binding.Hide {
@@ -184,20 +207,19 @@ func RenderBottomBar(m AppModel, bindings []KeyBinding, err error) string {
continue
}
if i > 0 {
- content += sepStyle.Render(" ")
+ content += sepStyle.Render(" ")
}
- content += keyStyle.Render(fmt.Sprintf("[%s]", binding.Key))
- content += descStyle.Render(fmt.Sprintf(" %s", desc))
+ content += keyStyle.Render(binding.Key)
+ content += descStyle.Render(" " + desc)
}
if err != nil {
- // Style for errors (inherits background, adds red foreground)
- errorStyle := bottomBarStyle.Bold(true).Foreground(lipgloss.Color("196"))
+ errStyle := lipgloss.NewStyle().Background(colorBarBg).Bold(true).Foreground(colorWarning)
content += sepStyle.Render(" ")
- content += errorStyle.Render(err.Error())
+ content += errStyle.Render(err.Error())
}
- return bottomBarStyle.Width(m.width).Align(lipgloss.Left).Render(content)
+ return bottomBarStyle.Width(contentWidth).Align(lipgloss.Left).Render(content)
}
// GetAppData fetches all data needed for the TUI
diff --git a/internal/tui/timer_box.go b/internal/tui/timer_box.go
index 891004a..2b0c34a 100644
--- a/internal/tui/timer_box.go
+++ b/internal/tui/timer_box.go
@@ -1,10 +1,11 @@
package tui
import (
- "fmt"
"time"
"git.tjp.lol/punchcard/internal/queries"
+
+ "github.com/charmbracelet/lipgloss/v2"
)
// TimerInfo holds information about the current or most recent timer state
@@ -66,9 +67,13 @@ func (m TimerBoxModel) View(width, height int, isSelected bool) string {
}
// Apply box styling
- style := unselectedBoxStyle
+ 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)
@@ -76,67 +81,47 @@ func (m TimerBoxModel) View(width, height int, isSelected bool) string {
// renderActiveTimer renders the active timer display
func (m TimerBoxModel) renderActiveTimer() string {
- content := titleStyle.Render("⏰ Active Timer") + "\n\n"
+ statusStyle := lipgloss.NewStyle().Foreground(colorTimerActive).Bold(true)
+ content := statusStyle.Render("TRACKING") + "\n\n"
- // Timer duration
- timerLine := fmt.Sprintf("Duration: %s", FormatDuration(m.currentTime.Sub(m.timerInfo.StartTime)))
- content += activeTimerStyle.Render(timerLine) + "\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 != "" {
- projectLine := fmt.Sprintf("Project: %s / %s", m.timerInfo.ClientName, m.timerInfo.ProjectName)
- content += projectLine + "\n"
+ content += dimLabel.Render(m.timerInfo.ClientName+" /") + "\n"
+ content += m.timerInfo.ProjectName + "\n"
} else {
- clientLine := fmt.Sprintf("Client: %s", m.timerInfo.ClientName)
- content += clientLine + "\n"
+ content += m.timerInfo.ClientName + "\n"
}
- // Start time (convert from UTC to local)
+ // Start time
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"
- }
+ content += dimLabel.Render("since " + localStartTime.Format("3:04 PM"))
return content
}
// renderInactiveTimer renders the inactive timer display
func (m TimerBoxModel) renderInactiveTimer() string {
- content := titleStyle.Render("⌛ Last Timer (Inactive)") + "\n\n"
+ statusStyle := lipgloss.NewStyle().Foreground(colorDimmed)
+ content := statusStyle.Render("IDLE") + "\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.")
+ content += inactiveTimerStyle.Render("No entries yet.\nPunch in to start.")
return content
}
- timerLine := fmt.Sprintf("Duration: %s", FormatDuration(m.timerInfo.Duration))
- content += inactiveTimerStyle.Render(timerLine) + "\n\n"
+ content += inactiveTimerStyle.Render(FormatDuration(m.timerInfo.Duration)) + "\n\n"
+ dimLabel := lipgloss.NewStyle().Foreground(colorDimmed)
if m.timerInfo.ProjectName != "" {
- content += inactiveTimerStyle.Render(fmt.Sprintf("Project: %s / %s", m.timerInfo.ClientName, m.timerInfo.ProjectName)) + "\n"
+ content += dimLabel.Render(m.timerInfo.ClientName+" /") + "\n"
+ content += inactiveTimerStyle.Render(m.timerInfo.ProjectName)
} 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"
+ content += inactiveTimerStyle.Render(m.timerInfo.ClientName)
}
return content