diff options
Diffstat (limited to 'internal/tui/shared.go')
-rw-r--r-- | internal/tui/shared.go | 225 |
1 files changed, 225 insertions, 0 deletions
diff --git a/internal/tui/shared.go b/internal/tui/shared.go new file mode 100644 index 0000000..77b282d --- /dev/null +++ b/internal/tui/shared.go @@ -0,0 +1,225 @@ +package tui + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "punchcard/internal/queries" + + "github.com/charmbracelet/lipgloss/v2" +) + +var ( + // Styles for the TUI + topBarInactiveStyle = lipgloss.NewStyle(). + Background(lipgloss.Color("21")). + Foreground(lipgloss.Color("230")). + Padding(0, 1) + + bottomBarStyle = lipgloss.NewStyle(). + Background(lipgloss.Color("238")). + Foreground(lipgloss.Color("252")). + Padding(0, 1) + + // Box styles + selectedBoxStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("62")). + Padding(1, 2) + + unselectedBoxStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("238")). + Padding(1, 2) + + activeTimerStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("196")). + Bold(true) + + inactiveTimerStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("246")) +) + +// FormatDuration formats a duration in a human-readable way +func FormatDuration(d time.Duration) string { + d = d.Round(time.Second) + hours := int(d.Hours()) + minutes := int(d.Minutes()) % 60 + seconds := int(d.Seconds()) % 60 + + if hours > 0 { + return fmt.Sprintf("%dh %02dm", hours, minutes) + } + if minutes > 0 { + return fmt.Sprintf("%dm %02ds", minutes, seconds) + } + return fmt.Sprintf("%ds", seconds) +} + +// GetTimeStats retrieves today's and week's time statistics +func GetTimeStats(ctx context.Context, q *queries.Queries) (TimeStats, error) { + var stats TimeStats + + // Get today's total + todaySeconds, err := q.GetTodaySummary(ctx) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return stats, fmt.Errorf("failed to get today's summary: %w", err) + } + if err == nil { + stats.TodayTotal = time.Duration(todaySeconds) * time.Second + } + + // Get week's total + weekSummary, err := q.GetWeekSummaryByProject(ctx) + if err != nil { + return stats, fmt.Errorf("failed to get week summary: %w", err) + } + + var weekTotal time.Duration + for _, row := range weekSummary { + weekTotal += time.Duration(row.TotalSeconds) * time.Second + } + stats.WeekTotal = weekTotal + + return stats, nil +} + +// GetTimerInfo retrieves current timer information +func GetTimerInfo(ctx context.Context, q *queries.Queries) (TimerInfo, error) { + var info TimerInfo + + activeEntry, err := q.GetActiveTimeEntry(ctx) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return info, fmt.Errorf("failed to get active timer: %w", err) + } + + if errors.Is(err, sql.ErrNoRows) { + // No active timer + return info, nil + } + + // Active timer found + info.IsActive = true + info.StartTime = activeEntry.StartTime + info.Duration = time.Since(activeEntry.StartTime) + + // Get client information + client, err := q.FindClient(ctx, queries.FindClientParams{ + ID: activeEntry.ClientID, + Name: "", + }) + if err == nil && len(client) > 0 { + info.ClientName = client[0].Name + if client[0].BillableRate.Valid { + rate := float64(client[0].BillableRate.Int64) / 100.0 + info.BillableRate = &rate + } + } + + // Get project information if exists + if activeEntry.ProjectID.Valid { + project, err := q.FindProject(ctx, queries.FindProjectParams{ + ID: activeEntry.ProjectID.Int64, + Name: "", + }) + if err == nil && len(project) > 0 { + info.ProjectName = project[0].Name + if project[0].BillableRate.Valid { + projectRate := float64(project[0].BillableRate.Int64) / 100.0 + info.BillableRate = &projectRate + } + } + } + + // Get description + if activeEntry.Description.Valid { + info.Description = activeEntry.Description.String + } + + // Use entry-specific billable rate if set + if activeEntry.BillableRate.Valid { + entryRate := float64(activeEntry.BillableRate.Int64) / 100.0 + info.BillableRate = &entryRate + } + + return info, nil +} + +// RenderTopBar renders the top bar with view name and time stats +func RenderTopBar(viewName string, stats TimeStats, width int) string { + left := viewName + right := fmt.Sprintf("Today: %s | Week: %s", + FormatDuration(stats.TodayTotal), + FormatDuration(stats.WeekTotal)) + + // 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 := width - 2 // Account for horizontal padding + + // 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), + ) + + return topBarInactiveStyle.Width(width).Render(content) +} + +// RenderBottomBar renders the bottom bar with key bindings +func RenderBottomBar(bindings []KeyBinding, width int) string { + var content string + for i, binding := range bindings { + if i > 0 { + content += " " + } + // Format key with bold and square brackets + keyStyle := lipgloss.NewStyle().Bold(true) + formattedKey := keyStyle.Render(fmt.Sprintf("[%s]", binding.Key)) + content += fmt.Sprintf("%s %s", formattedKey, binding.Description) + } + + return bottomBarStyle.Width(width).Render(content) +} + +// GetAppData fetches all data needed for the TUI +func GetAppData(ctx context.Context, q *queries.Queries) (TimerInfo, TimeStats, []queries.Client, []queries.ListAllProjectsRow, []queries.TimeEntry, error) { + // Get timer info + timerInfo, err := GetTimerInfo(ctx, q) + if err != nil { + return TimerInfo{}, TimeStats{}, nil, nil, nil, fmt.Errorf("failed to get timer info: %w", err) + } + + // Get time stats + stats, err := GetTimeStats(ctx, q) + if err != nil { + return TimerInfo{}, TimeStats{}, nil, nil, nil, fmt.Errorf("failed to get time stats: %w", err) + } + + // Get clients + clients, err := q.ListAllClients(ctx) + if err != nil { + return TimerInfo{}, TimeStats{}, nil, nil, nil, fmt.Errorf("failed to get clients: %w", err) + } + + // Get projects + projects, err := q.ListAllProjects(ctx) + if err != nil { + return TimerInfo{}, TimeStats{}, nil, nil, nil, fmt.Errorf("failed to get projects: %w", err) + } + + // Get recent entries + entries, err := q.GetRecentTimeEntries(ctx, 20) + if err != nil { + return TimerInfo{}, TimeStats{}, nil, nil, nil, fmt.Errorf("failed to get recent entries: %w", err) + } + + return timerInfo, stats, clients, projects, entries, nil +} + |