From e0b39b9405820b6b780fea693b1fd4c8ab4ba7f8 Mon Sep 17 00:00:00 2001 From: Dylan Ravel Date: Wed, 17 Dec 2025 18:39:29 -0700 Subject: [PATCH] Improve UI formatting with precise ANSI resets Replaces generic color resets with specific ANSI reset codes for bold, dim, italic, and underline formatting in the UI package. Updates all command outputs to use bold formatting for key labels and values, enhancing readability and consistency. Adjusts related tests to check for the new reset codes and ensures all formatting functions use the correct resets. --- cmd/delete.go | 14 +++++++------- cmd/edit.go | 8 ++++---- cmd/export.go | 2 +- cmd/init.go | 6 +++--- cmd/log.go | 6 +++--- cmd/manual.go | 14 +++++++------- cmd/start.go | 2 +- cmd/stats.go | 26 +++++++++++++------------- cmd/status.go | 8 ++++---- cmd/stop.go | 4 ++-- internal/ui/ui.go | 13 +++++++++---- internal/ui/ui_test.go | 11 +++++++---- 12 files changed, 61 insertions(+), 53 deletions(-) diff --git a/cmd/delete.go b/cmd/delete.go index 65393cd..411e17e 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -120,17 +120,17 @@ var deleteCmd = &cobra.Command{ fmt.Println() ui.PrintWarning(ui.EmojiWarning, "You are about to delete this entry:") fmt.Println() - ui.PrintInfo(4, "ID", fmt.Sprintf("%d", selectedEntry.ID)) - ui.PrintInfo(4, "Project", selectedEntry.ProjectName) - ui.PrintInfo(4, "Start", selectedEntry.StartTime.Format("Jan 2, 2006 at 3:04 PM")) + ui.PrintInfo(4, ui.Bold("ID"), fmt.Sprintf("%d", selectedEntry.ID)) + ui.PrintInfo(4, ui.Bold("Project"), selectedEntry.ProjectName) + ui.PrintInfo(4, ui.Bold("Start"), selectedEntry.StartTime.Format("Jan 2, 2006 at 3:04 PM")) if selectedEntry.EndTime != nil { - ui.PrintInfo(4, "End", selectedEntry.EndTime.Format("Jan 2, 2006 at 3:04 PM")) - ui.PrintInfo(4, "Duration", ui.FormatDuration(selectedEntry.Duration())) + ui.PrintInfo(4, ui.Bold("End"), selectedEntry.EndTime.Format("Jan 2, 2006 at 3:04 PM")) + ui.PrintInfo(4, ui.Bold("Duration"), ui.FormatDuration(selectedEntry.Duration())) } else { - ui.PrintInfo(4, "Status", ui.Warning("Running")) + ui.PrintInfo(4, ui.Bold("Status"), ui.Warning("Running")) } if selectedEntry.Description != "" { - ui.PrintInfo(4, "Description", selectedEntry.Description) + ui.PrintInfo(4, ui.Bold("Description"), selectedEntry.Description) } fmt.Println() diff --git a/cmd/edit.go b/cmd/edit.go index fb1b7bd..75fef93 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -251,7 +251,7 @@ var editCmd = &cobra.Command{ // Show confirmation with diff fmt.Println() - ui.PrintInfo(0, "Changes to entry", fmt.Sprintf("#%d", selectedEntry.ID)) + ui.PrintInfo(0, ui.Bold("Changes to entry"), fmt.Sprintf("#%d", selectedEntry.ID)) fmt.Println() hasChanges := false @@ -260,19 +260,19 @@ var editCmd = &cobra.Command{ hasChanges = true oldStr := selectedEntry.StartTime.Format("01-02-2006 3:04 PM") newStr := editedEntry.StartTime.Format("01-02-2006 3:04 PM") - fmt.Printf(" Start time: %s → %s\n", ui.Muted(oldStr), newStr) + fmt.Printf(" %s %s → %s\n", ui.Bold("Start time:"), ui.Muted(oldStr), newStr) } if !selectedEntry.EndTime.Equal(*editedEntry.EndTime) { hasChanges = true oldStr := selectedEntry.EndTime.Format("01-02-2006 3:04 PM") newStr := editedEntry.EndTime.Format("01-02-2006 3:04 PM") - fmt.Printf(" End time: %s → %s\n", ui.Muted(oldStr), newStr) + fmt.Printf(" %s %s → %s\n", ui.Bold("End time:"), ui.Muted(oldStr), newStr) } if selectedEntry.Description != editedEntry.Description { hasChanges = true - fmt.Printf(" Description: %s → %s\n", ui.Muted(fmt.Sprintf("%q", selectedEntry.Description)), fmt.Sprintf("%q", editedEntry.Description)) + fmt.Printf(" %s %s → %s\n", ui.Bold("Description:"), ui.Muted(fmt.Sprintf("%q", selectedEntry.Description)), fmt.Sprintf("%q", editedEntry.Description)) } if !hasChanges { diff --git a/cmd/export.go b/cmd/export.go index 0500898..6915492 100644 --- a/cmd/export.go +++ b/cmd/export.go @@ -101,7 +101,7 @@ var exportCmd = &cobra.Command{ os.Exit(1) } - ui.PrintSuccess(ui.EmojiExport, fmt.Sprintf("Exported %d entries to %s", len(entries), filename)) + ui.PrintSuccess(ui.EmojiExport, fmt.Sprintf("Exported %s to %s", ui.Bold(fmt.Sprintf("%d entries", len(entries))), ui.Bold(filename))) ui.NewlineBelow() }, diff --git a/cmd/init.go b/cmd/init.go index f1901b3..0864290 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -107,12 +107,12 @@ var initCmd = &cobra.Command{ } fmt.Println() - ui.PrintSuccess(ui.EmojiSuccess, fmt.Sprintf("Created .tmporc for project '%s'", name)) + ui.PrintSuccess(ui.EmojiSuccess, fmt.Sprintf("Created .tmporc for project %s", ui.Bold(name))) if hourlyRate > 0 { - ui.PrintInfo(4, "Hourly Rate", fmt.Sprintf("$%.2f", hourlyRate)) + ui.PrintInfo(4, ui.Bold("Hourly Rate"), fmt.Sprintf("$%.2f", hourlyRate)) } if description != "" { - ui.PrintInfo(4, "Description", description) + ui.PrintInfo(4, ui.Bold("Description"), description) } fmt.Println() diff --git a/cmd/log.go b/cmd/log.go index 801cdf9..7ee735a 100644 --- a/cmd/log.go +++ b/cmd/log.go @@ -79,7 +79,7 @@ var logCmd = &cobra.Command{ fmt.Println() } - fmt.Println(ui.Muted(fmt.Sprintf("─── %s ───", entryDate))) + fmt.Println(ui.Bold(ui.Muted(fmt.Sprintf("─── %s ───", entryDate)))) currentDate = entryDate } @@ -93,7 +93,7 @@ var logCmd = &cobra.Command{ timeRange += ui.Warning("(running)") + " " } - fmt.Printf(" %s %-20s %s\n", timeRange, entry.ProjectName, ui.FormatDuration(duration)) + fmt.Printf(" %s %s %s\n", timeRange, ui.Bold(fmt.Sprintf("%-20s", entry.ProjectName)), ui.FormatDuration(duration)) if entry.Description != "" { fmt.Printf(" %s %s\n", ui.Muted("└─"), entry.Description) } @@ -101,7 +101,7 @@ var logCmd = &cobra.Command{ fmt.Println() ui.PrintSeparator() - fmt.Printf("%s %s\n", ui.Info("Total Time:"), ui.FormatDuration(totalDuration)) + fmt.Printf("%s %s\n", ui.BoldInfo("Total Time:"), ui.Bold(ui.FormatDuration(totalDuration))) ui.NewlineBelow() }, diff --git a/cmd/manual.go b/cmd/manual.go index f7a2e4c..40587f1 100644 --- a/cmd/manual.go +++ b/cmd/manual.go @@ -156,19 +156,19 @@ var manualCmd = &cobra.Command{ duration := entry.Duration() fmt.Println() - ui.PrintSuccess(ui.EmojiSuccess, fmt.Sprintf("Created manual entry for '%s'", entry.ProjectName)) - ui.PrintInfo(4, "Start", startTime.Format("Jan 2, 2006 at 3:04 PM")) - ui.PrintInfo(4, "End", endTime.Format("Jan 2, 2006 at 3:04 PM")) - ui.PrintInfo(4, "Duration", ui.FormatDuration(duration)) + ui.PrintSuccess(ui.EmojiSuccess, fmt.Sprintf("Created manual entry for %s", ui.Bold(entry.ProjectName))) + ui.PrintInfo(4, ui.Bold("Start"), startTime.Format("Jan 2, 2006 at 3:04 PM")) + ui.PrintInfo(4, ui.Bold("End"), endTime.Format("Jan 2, 2006 at 3:04 PM")) + ui.PrintInfo(4, ui.Bold("Duration"), ui.FormatDuration(duration)) if entry.Description != "" { - ui.PrintInfo(4, "Description", entry.Description) + ui.PrintInfo(4, ui.Bold("Description"), entry.Description) } if entry.HourlyRate != nil { earnings := duration.Hours() * *entry.HourlyRate - fmt.Printf(" %s %s\n", ui.Info("Hourly Rate:"), fmt.Sprintf("$%.2f", *entry.HourlyRate)) - fmt.Printf(" %s %s\n", ui.Info("Earnings:"), fmt.Sprintf("$%.2f", earnings)) + fmt.Printf(" %s %s\n", ui.BoldInfo("Hourly Rate:"), fmt.Sprintf("$%.2f", *entry.HourlyRate)) + fmt.Printf(" %s %s\n", ui.BoldInfo("Earnings:"), fmt.Sprintf("$%.2f", earnings)) } ui.NewlineBelow() diff --git a/cmd/start.go b/cmd/start.go index e34a9e8..b641c7c 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -62,7 +62,7 @@ var startCmd = &cobra.Command{ os.Exit(1) } - ui.PrintSuccess(ui.EmojiStart, fmt.Sprintf("Started tracking time for '%s'", entry.ProjectName)) + ui.PrintSuccess(ui.EmojiStart, fmt.Sprintf("Started tracking time for %s", ui.Bold(entry.ProjectName))) if cfg, _, err := config.FindAndLoad(); err == nil && cfg != nil { ui.PrintMuted(4, "└─ Config Source: .tmporc") diff --git a/cmd/stats.go b/cmd/stats.go index f5cd1e5..5dee16e 100644 --- a/cmd/stats.go +++ b/cmd/stats.go @@ -110,20 +110,20 @@ func ShowPeriodStats(entries []*storage.TimeEntry, periodName string) { } } - ui.PrintSuccess(ui.EmojiStats, fmt.Sprintf("Stats for %s", periodName)) + ui.PrintSuccess(ui.EmojiStats, fmt.Sprintf("Stats for %s", ui.Bold(periodName))) fmt.Println() - ui.PrintInfo(4, "Total Time", fmt.Sprintf("%s (%.2f hours)", ui.FormatDuration(totalDuration), totalDuration.Hours())) - ui.PrintInfo(4, "Total Entries", fmt.Sprintf("%d", len(entries))) + ui.PrintInfo(4, ui.Bold("Total Time"), fmt.Sprintf("%s (%.2f hours)", ui.FormatDuration(totalDuration), totalDuration.Hours())) + ui.PrintInfo(4, ui.Bold("Total Entries"), fmt.Sprintf("%d", len(entries))) if hasAnyEarnings { - ui.PrintInfo(4, "Earnings", fmt.Sprintf("$%.2f", totalEarnings)) + ui.PrintInfo(4, ui.Bold("Earnings"), fmt.Sprintf("$%.2f", totalEarnings)) } fmt.Println() - ui.PrintInfo(4, "By Project", "") + ui.PrintInfo(4, ui.Bold("By Project"), "") for project, duration := range projectStats { percentage := (duration.Seconds() / totalDuration.Seconds()) * 100 - fmt.Printf(" %-20s %s (%.1f%%)\n", project, ui.FormatDuration(duration), percentage) + fmt.Printf(" %s %s (%.1f%%)\n", ui.Bold(fmt.Sprintf("%-20s", project)), ui.FormatDuration(duration), percentage) if earnings, ok := projectEarnings[project]; ok && earnings > 0 { fmt.Printf(" %s %s\n", ui.Muted("└─ Earnings:"), fmt.Sprintf("$%.2f", earnings)) @@ -176,20 +176,20 @@ func ShowAllTimeStats(entries []*storage.TimeEntry, db *storage.Database) { projects, _ := db.GetAllProjects() - ui.PrintSuccess(ui.EmojiStats, "All-Time Statistics") - ui.PrintInfo(4, "Total Time", fmt.Sprintf("%s (%.2f hours)", ui.FormatDuration(totalDuration), totalDuration.Hours())) - ui.PrintInfo(4, "Total Entries", fmt.Sprintf("%d", len(entries))) - ui.PrintInfo(4, "Projects Tracked", fmt.Sprintf("%d", len(projects))) + ui.PrintSuccess(ui.EmojiStats, ui.Bold("All-Time Statistics")) + ui.PrintInfo(4, ui.Bold("Total Time"), fmt.Sprintf("%s (%.2f hours)", ui.FormatDuration(totalDuration), totalDuration.Hours())) + ui.PrintInfo(4, ui.Bold("Total Entries"), fmt.Sprintf("%d", len(entries))) + ui.PrintInfo(4, ui.Bold("Projects Tracked"), fmt.Sprintf("%d", len(projects))) if hasAnyEarnings { - ui.PrintInfo(4, "Earnings", fmt.Sprintf("$%.2f", totalEarnings)) + ui.PrintInfo(4, ui.Bold("Earnings"), fmt.Sprintf("$%.2f", totalEarnings)) } fmt.Println() - ui.PrintInfo(4, "By Project", "") + ui.PrintInfo(4, ui.Bold("By Project"), "") for project, duration := range projectStats { percentage := (duration.Seconds() / totalDuration.Seconds()) * 100 - fmt.Printf(" %-20s %s (%.1f%%)\n", project, ui.FormatDuration(duration), percentage) + fmt.Printf(" %s %s (%.1f%%)\n", ui.Bold(fmt.Sprintf("%-20s", project)), ui.FormatDuration(duration), percentage) if earnings, ok := projectEarnings[project]; ok && earnings > 0 { fmt.Printf(" %s %s\n", ui.Muted("└─ Earnings:"), fmt.Sprintf("$%.2f", earnings)) diff --git a/cmd/status.go b/cmd/status.go index 3b84d7a..8363438 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -42,12 +42,12 @@ var statusCmd = &cobra.Command{ duration := time.Since(running.StartTime) - ui.PrintSuccess(ui.EmojiStatus, fmt.Sprintf("Currently tracking: %s", running.ProjectName)) - ui.PrintInfo(4, "Started", running.StartTime.Format("3:04 PM")) - ui.PrintInfo(4, "Duration", ui.FormatDuration(duration)) + ui.PrintSuccess(ui.EmojiStatus, fmt.Sprintf("Currently tracking: %s", ui.Bold(running.ProjectName))) + ui.PrintInfo(4, ui.Bold("Started"), running.StartTime.Format("3:04 PM")) + ui.PrintInfo(4, ui.Bold("Duration"), ui.FormatDuration(duration)) if running.Description != "" { - ui.PrintInfo(4, "Description", running.Description) + ui.PrintInfo(4, ui.Bold("Description"), running.Description) } ui.NewlineBelow() diff --git a/cmd/stop.go b/cmd/stop.go index 020aacf..dc6cb40 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -45,8 +45,8 @@ var stopCmd = &cobra.Command{ duration := time.Since(running.StartTime) - ui.PrintSuccess(ui.EmojiStop, fmt.Sprintf("Stopped tracking '%s'", running.ProjectName)) - ui.PrintInfo(4, "Total Duration", ui.FormatDuration(duration)) + ui.PrintSuccess(ui.EmojiStop, fmt.Sprintf("Stopped tracking %s", ui.Bold(running.ProjectName))) + ui.PrintInfo(4, ui.Bold("Total Duration"), ui.FormatDuration(duration)) ui.NewlineBelow() }, diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 00ca277..36fe5b9 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -23,6 +23,11 @@ const ( FormatDim = "\033[2m" FormatItalic = "\033[3m" FormatUnderline = "\033[4m" + + // Specific reset codes (don't reset colors) + ResetBoldDim = "\033[22m" // Reset bold and dim + ResetItalic = "\033[23m" // Reset italic + ResetUnderline = "\033[24m" // Reset underline ) // Emoji Constants @@ -68,22 +73,22 @@ func Muted(message string) string { // Bold text formatting functions that return formatted string func Bold(message string) string { - return FormatBold + message + ColorReset + return FormatBold + message + ResetBoldDim } // Dim text formatting functions that return formatted string func Dim(message string) string { - return FormatDim + message + ColorReset + return FormatDim + message + ResetBoldDim } // Italic text formatting functions that return formatted string func Italic(message string) string { - return FormatItalic + message + ColorReset + return FormatItalic + message + ResetItalic } // Underline text formatting functions that return formatted string func Underline(message string) string { - return FormatUnderline + message + ColorReset + return FormatUnderline + message + ResetUnderline } // Bold success combined formatting functions for common use cases diff --git a/internal/ui/ui_test.go b/internal/ui/ui_test.go index 49444f2..9a6b8c8 100644 --- a/internal/ui/ui_test.go +++ b/internal/ui/ui_test.go @@ -49,28 +49,28 @@ func TestFormattingFunctions(t *testing.T) { result := Bold("test") assert.Contains(t, result, FormatBold) assert.Contains(t, result, "test") - assert.Contains(t, result, ColorReset) + assert.Contains(t, result, ResetBoldDim) }) t.Run("Dim adds dim formatting", func(t *testing.T) { result := Dim("test") assert.Contains(t, result, FormatDim) assert.Contains(t, result, "test") - assert.Contains(t, result, ColorReset) + assert.Contains(t, result, ResetBoldDim) }) t.Run("Italic adds italic formatting", func(t *testing.T) { result := Italic("test") assert.Contains(t, result, FormatItalic) assert.Contains(t, result, "test") - assert.Contains(t, result, ColorReset) + assert.Contains(t, result, ResetItalic) }) t.Run("Underline adds underline formatting", func(t *testing.T) { result := Underline("test") assert.Contains(t, result, FormatUnderline) assert.Contains(t, result, "test") - assert.Contains(t, result, ColorReset) + assert.Contains(t, result, ResetUnderline) }) } @@ -180,6 +180,9 @@ func TestConstants(t *testing.T) { assert.NotEmpty(t, FormatDim) assert.NotEmpty(t, FormatItalic) assert.NotEmpty(t, FormatUnderline) + assert.NotEmpty(t, ResetBoldDim) + assert.NotEmpty(t, ResetItalic) + assert.NotEmpty(t, ResetUnderline) }) t.Run("Emoji constants are defined", func(t *testing.T) {