diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/tui/app.go | 60 | ||||
| -rw-r--r-- | internal/tui/history_box.go | 81 | ||||
| -rw-r--r-- | internal/tui/modal.go | 6 | ||||
| -rw-r--r-- | internal/tui/projects_box.go | 66 | ||||
| -rw-r--r-- | internal/tui/shared.go | 92 | ||||
| -rw-r--r-- | internal/tui/timer_box.go | 67 |
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 |
