From a7d9f4c65ed5049348124d00d1e4c6ecd28667db Mon Sep 17 00:00:00 2001 From: Dylan Ravel Date: Tue, 16 Dec 2025 20:37:11 -0700 Subject: [PATCH 1/3] Add interactive edit command for time entries Introduces a new 'edit' command with an interactive UI for editing completed time entries. Adds database methods to fetch projects with completed entries, retrieve completed entries by project, and update time entries. Enhances user experience by allowing selection and modification of entry details with validation and confirmation. --- cmd/edit.go | 351 +++++++++++++++++++++++++++++++++++++++++ internal/storage/db.go | 119 ++++++++++++++ 2 files changed, 470 insertions(+) create mode 100644 cmd/edit.go diff --git a/cmd/edit.go b/cmd/edit.go new file mode 100644 index 0000000..4607b8a --- /dev/null +++ b/cmd/edit.go @@ -0,0 +1,351 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/DylanDevelops/tmpo/internal/storage" + "github.com/DylanDevelops/tmpo/internal/ui" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" +) + +var showAllProjects bool + +var editCmd = &cobra.Command{ + Use: "edit", + Short: "Edit an existing time entry", + Long: `Edit an existing time entry using an interactive menu.`, + Run: func(cmd *cobra.Command, args []string) { + ui.NewlineAbove() + ui.PrintSuccess("✏️", "Edit Time Entry") + fmt.Println() + + db, err := storage.Initialize() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + defer db.Close() + + var entries []*storage.TimeEntry + var projectName string + + if showAllProjects { + // Show project selection first + projects, err := db.GetProjectsWithCompletedEntries() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + if len(projects) == 0 { + ui.PrintError(ui.EmojiError, "No completed time entries found") + ui.NewlineBelow() + os.Exit(1) + } + + projectPrompt := promptui.Select{ + Label: "Select project", + Items: projects, + } + + _, selectedProject, err := projectPrompt.Run() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + projectName = selectedProject + } else { + // Use current project + detectedProject, err := DetectProjectName() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("detecting project: %v", err)) + os.Exit(1) + } + projectName = detectedProject + } + + // Get completed entries for the selected/detected project + entries, err = db.GetCompletedEntriesByProject(projectName) + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + if len(entries) == 0 { + ui.PrintError(ui.EmojiError, fmt.Sprintf("No completed time entries found for project '%s'", projectName)) + if !showAllProjects { + ui.PrintMuted(0, "Use 'tmpo edit --show-all-projects' to see entries from all projects") + } + ui.NewlineBelow() + os.Exit(1) + } + + // Format entries for selection + templates := &promptui.SelectTemplates{ + Label: "{{ . }}", + Active: "▸ {{ .Label }}", + Inactive: " {{ .Label }}", + Selected: "{{ .Label }}", + } + + type entryItem struct { + Label string + Entry *storage.TimeEntry + } + + var items []entryItem + for _, entry := range entries { + label := formatEntryLabel(entry) + items = append(items, entryItem{Label: label, Entry: entry}) + } + + entryPrompt := promptui.Select{ + Label: "Select entry to edit", + Items: items, + Templates: templates, + } + + idx, _, err := entryPrompt.Run() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + selectedEntry := items[idx].Entry + + // Create a copy of the entry for editing + editedEntry := &storage.TimeEntry{ + ID: selectedEntry.ID, + ProjectName: selectedEntry.ProjectName, + StartTime: selectedEntry.StartTime, + EndTime: selectedEntry.EndTime, + Description: selectedEntry.Description, + HourlyRate: selectedEntry.HourlyRate, + } + + // Edit start date + currentStartDate := selectedEntry.StartTime.Format("01-02-2006") + startDatePrompt := promptui.Prompt{ + Label: fmt.Sprintf("Start date (MM-DD-YYYY): (%s)", currentStartDate), + Validate: validateDateOptional, + AllowEdit: true, + } + + startDateInput, err := startDatePrompt.Run() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + startDateInput = strings.TrimSpace(startDateInput) + if startDateInput == "" { + startDateInput = currentStartDate + } + + // Edit start time + currentStartTime := selectedEntry.StartTime.Format("3:04 PM") + startTimePrompt := promptui.Prompt{ + Label: fmt.Sprintf("Start time (e.g., 9:30 AM or 14:30): (%s)", currentStartTime), + Validate: validateTimeOptional, + AllowEdit: true, + } + + startTimeInput, err := startTimePrompt.Run() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + startTimeInput = strings.TrimSpace(startTimeInput) + if startTimeInput == "" { + startTimeInput = currentStartTime + } + + // Edit end date + currentEndDate := selectedEntry.EndTime.Format("01-02-2006") + endDatePrompt := promptui.Prompt{ + Label: fmt.Sprintf("End date (MM-DD-YYYY): (%s)", currentEndDate), + Validate: validateDateOptional, + AllowEdit: true, + } + + endDateInput, err := endDatePrompt.Run() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + endDateInput = strings.TrimSpace(endDateInput) + if endDateInput == "" { + endDateInput = currentEndDate + } + + // Edit end time + currentEndTime := selectedEntry.EndTime.Format("3:04 PM") + endTimePrompt := promptui.Prompt{ + Label: fmt.Sprintf("End time (e.g., 5:00 PM or 17:00): (%s)", currentEndTime), + Validate: validateTimeOptional, + AllowEdit: true, + } + + endTimeInput, err := endTimePrompt.Run() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + endTimeInput = strings.TrimSpace(endTimeInput) + if endTimeInput == "" { + endTimeInput = currentEndTime + } + + // Validate that end is after start + if err := validateEndDateTime(startDateInput, startTimeInput, endDateInput, endTimeInput); err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + // Edit description + currentDescription := selectedEntry.Description + descriptionPrompt := promptui.Prompt{ + Label: fmt.Sprintf("Description: (%s)", currentDescription), + AllowEdit: true, + } + + descriptionInput, err := descriptionPrompt.Run() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + descriptionInput = strings.TrimSpace(descriptionInput) + if descriptionInput == "" { + descriptionInput = currentDescription + } + + // Parse the new times + newStartTime, err := parseDateTime(startDateInput, startTimeInput) + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("parsing start time: %v", err)) + os.Exit(1) + } + + newEndTime, err := parseDateTime(endDateInput, endTimeInput) + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("parsing end time: %v", err)) + os.Exit(1) + } + + // Update the edited entry + editedEntry.StartTime = newStartTime + editedEntry.EndTime = &newEndTime + editedEntry.Description = descriptionInput + + // Show confirmation with diff + fmt.Println() + ui.PrintInfo(0, "Changes to entry", fmt.Sprintf("#%d", selectedEntry.ID)) + fmt.Println() + + hasChanges := false + + if !selectedEntry.StartTime.Equal(editedEntry.StartTime) { + 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) + } + + 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) + } + + 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)) + } + + if !hasChanges { + ui.PrintWarning(ui.EmojiWarning, "No changes detected") + ui.NewlineBelow() + os.Exit(0) + } + + fmt.Println() + + // Confirm save + confirmPrompt := promptui.Select{ + Label: "Save changes?", + Items: []string{"Yes", "No"}, + } + + _, result, err := confirmPrompt.Run() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + if result == "No" { + ui.PrintWarning(ui.EmojiWarning, "Changes discarded") + ui.NewlineBelow() + os.Exit(0) + } + + // Save to database + if err := db.UpdateTimeEntry(editedEntry.ID, editedEntry); err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + fmt.Println() + ui.PrintSuccess(ui.EmojiSuccess, "Entry updated successfully") + ui.NewlineBelow() + }, +} + +// formatEntryLabel formats a time entry for display in the selection list +// Format: "2024-05-21 9:30 AM → 10:30 AM (1h) - Fixed bug in UI" +func formatEntryLabel(entry *storage.TimeEntry) string { + startStr := entry.StartTime.Format("2006-01-02 3:04 PM") + endStr := entry.EndTime.Format("3:04 PM") + duration := entry.Duration() + durationStr := ui.FormatDuration(duration) + + description := entry.Description + if description == "" { + description = "(no description)" + } + + return fmt.Sprintf("%s → %s (%s) - %s", startStr, endStr, durationStr, description) +} + +// validateDateOptional validates date input for edit mode, allowing empty input +// Empty input is valid (indicates keeping current value) +// Non-empty input is validated using the same rules as validateDate +func validateDateOptional(input string) error { + if input == "" { + return nil + } + return validateDate(input) +} + +// validateTimeOptional validates time input for edit mode, allowing empty input +// Empty input is valid (indicates keeping current value) +// Non-empty input is validated using the same rules as validateTime +func validateTimeOptional(input string) error { + if input == "" { + return nil + } + return validateTime(input) +} + +func init() { + editCmd.Flags().BoolVar(&showAllProjects, "show-all-projects", false, "Show project selection before entry selection") + rootCmd.AddCommand(editCmd) +} diff --git a/internal/storage/db.go b/internal/storage/db.go index 3288f2e..b32e4ba 100644 --- a/internal/storage/db.go +++ b/internal/storage/db.go @@ -426,6 +426,125 @@ func (d *Database) GetAllProjects() ([]string, error) { return projects, nil } +// GetProjectsWithCompletedEntries retrieves all distinct project names that have at least +// one completed time entry (end_time IS NOT NULL) from the time_entries table. +// The results are returned in ascending order by project_name. +// On success it returns a slice of project names (which will be empty if no completed entries exist) +// and a nil error. If the underlying database query or a row scan fails, it returns a +// non-nil error describing the failure. +func (d *Database) GetProjectsWithCompletedEntries() ([]string, error) { + rows, err := d.db.Query(` + SELECT DISTINCT project_name + FROM time_entries + WHERE end_time IS NOT NULL + ORDER BY project_name + `) + + if err != nil { + return nil, fmt.Errorf("failed to query projects: %w", err) + } + + defer rows.Close() + + var projects []string + + for rows.Next() { + var project string + if err := rows.Scan(&project); err != nil { + return nil, fmt.Errorf("failed to scan project: %w", err) + } + + projects = append(projects, project) + } + + return projects, nil +} + +// GetCompletedEntriesByProject retrieves completed time entries (where end_time IS NOT NULL) +// for the specified projectName from the time_entries table. Results are ordered by start_time +// in descending order (newest first). +// +// For each row a TimeEntry is populated. Since this query only returns completed entries, +// the EndTime field will always be non-nil. If the hourly_rate column is NULL the returned +// TimeEntry.HourlyRate will be nil; otherwise HourlyRate will point to the scanned float64. +// +// On success the function returns a slice of pointers to TimeEntry. If there are no +// matching rows the returned slice will have length 0 (it may be nil). On failure the +// function returns a non-nil error and a nil slice. Errors may originate from the +// query execution, row scanning, or row iteration. +func (d *Database) GetCompletedEntriesByProject(projectName string) ([]*TimeEntry, error) { + rows, err := d.db.Query(` + SELECT id, project_name, start_time, end_time, description, hourly_rate + FROM time_entries + WHERE project_name = ? AND end_time IS NOT NULL + ORDER BY start_time DESC + `, projectName) + + if err != nil { + return nil, fmt.Errorf("failed to query entries: %w", err) + } + + defer rows.Close() + + var entries []*TimeEntry + + for rows.Next() { + var entry TimeEntry + var endTime sql.NullTime + var hourlyRate sql.NullFloat64 + + err := rows.Scan(&entry.ID, &entry.ProjectName, &entry.StartTime, &endTime, &entry.Description, &hourlyRate) + if err != nil { + return nil, fmt.Errorf("failed to scan entry: %w", err) + } + + if endTime.Valid { + entry.EndTime = &endTime.Time + } + + if hourlyRate.Valid { + entry.HourlyRate = &hourlyRate.Float64 + } + + entries = append(entries, &entry) + } + + return entries, nil +} + +// UpdateTimeEntry updates an existing time entry in the database with the values from the provided +// TimeEntry. It updates the project_name, start_time, end_time, description and hourly_rate fields +// for the entry with the matching ID. +// +// If the provided entry's EndTime is nil, the end_time column will be set to NULL. +// If the provided entry's HourlyRate is nil, the hourly_rate column will be set to NULL. +// +// Returns an error if the update fails. Does not verify that a row with the given ID exists; +// if no rows are affected the function will still return nil (no error). +func (d *Database) UpdateTimeEntry(id int64, entry *TimeEntry) error { + var endTime sql.NullTime + if entry.EndTime != nil { + endTime = sql.NullTime{Time: *entry.EndTime, Valid: true} + } + + var hourlyRate sql.NullFloat64 + if entry.HourlyRate != nil { + hourlyRate = sql.NullFloat64{Float64: *entry.HourlyRate, Valid: true} + } + + _, err := d.db.Exec(` + UPDATE time_entries + SET project_name = ?, start_time = ?, end_time = ?, description = ?, hourly_rate = ? + WHERE id = ? + `, entry.ProjectName, entry.StartTime, endTime, entry.Description, hourlyRate, id) + + if err != nil { + return fmt.Errorf("failed to update entry: %w", err) + } + + return nil +} + // Close closes the Database, releasing any underlying resources. // It delegates to the wrapped database's Close method and returns any error encountered. // After Close is called, the Database must not be used for further operations. From 9870c67d42b22c129aa920ea7b50a088b462d37d Mon Sep 17 00:00:00 2001 From: Dylan Ravel Date: Tue, 16 Dec 2025 20:50:43 -0700 Subject: [PATCH 2/3] Add delete command for time entries Introduces a new 'delete' command with interactive prompts to select and delete time entries, supporting both current and all projects. Adds DeleteTimeEntry method to the database layer for removing entries by ID. Also fixes the order of flag registration in the edit command initialization. --- cmd/delete.go | 198 +++++++++++++++++++++++++++++++++++++++++ cmd/edit.go | 3 +- internal/storage/db.go | 11 +++ 3 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 cmd/delete.go diff --git a/cmd/delete.go b/cmd/delete.go new file mode 100644 index 0000000..65393cd --- /dev/null +++ b/cmd/delete.go @@ -0,0 +1,198 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/DylanDevelops/tmpo/internal/storage" + "github.com/DylanDevelops/tmpo/internal/ui" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" +) + +var showAllProjectsDelete bool + +var deleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a time entry", + Long: `Delete a time entry using an interactive menu.`, + Run: func(cmd *cobra.Command, args []string) { + ui.NewlineAbove() + ui.PrintSuccess("🗑️", "Delete Time Entry") + fmt.Println() + + db, err := storage.Initialize() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + defer db.Close() + + var entries []*storage.TimeEntry + var projectName string + + if showAllProjectsDelete { + // Show project selection first + projects, err := db.GetAllProjects() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + if len(projects) == 0 { + ui.PrintError(ui.EmojiError, "No time entries found") + ui.NewlineBelow() + os.Exit(1) + } + + projectPrompt := promptui.Select{ + Label: "Select project", + Items: projects, + } + + _, selectedProject, err := projectPrompt.Run() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + projectName = selectedProject + } else { + // Use current project + detectedProject, err := DetectProjectName() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("detecting project: %v", err)) + os.Exit(1) + } + projectName = detectedProject + } + + // Get all entries for the selected/detected project + entries, err = db.GetEntriesByProject(projectName) + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + if len(entries) == 0 { + ui.PrintError(ui.EmojiError, fmt.Sprintf("No time entries found for project '%s'", projectName)) + if !showAllProjectsDelete { + ui.PrintMuted(0, "Use 'tmpo delete --show-all-projects' to see entries from all projects") + } + ui.NewlineBelow() + os.Exit(1) + } + + // Format entries for selection + templates := &promptui.SelectTemplates{ + Label: "{{ . }}", + Active: "▸ {{ .Label }}", + Inactive: " {{ .Label }}", + Selected: "{{ .Label }}", + } + + type entryItem struct { + Label string + Entry *storage.TimeEntry + } + + var items []entryItem + for _, entry := range entries { + label := formatEntryLabelForDelete(entry) + items = append(items, entryItem{Label: label, Entry: entry}) + } + + entryPrompt := promptui.Select{ + Label: "Select entry to delete", + Items: items, + Templates: templates, + } + + idx, _, err := entryPrompt.Run() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + selectedEntry := items[idx].Entry + + // Show entry details and confirmation + 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")) + 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())) + } else { + ui.PrintInfo(4, "Status", ui.Warning("Running")) + } + if selectedEntry.Description != "" { + ui.PrintInfo(4, "Description", selectedEntry.Description) + } + fmt.Println() + + // Confirm deletion + confirmPrompt := promptui.Select{ + Label: "Are you sure you want to delete this entry?", + Items: []string{"No", "Yes"}, + } + + _, result, err := confirmPrompt.Run() + if err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + if result == "No" { + ui.PrintWarning(ui.EmojiWarning, "Deletion cancelled") + ui.NewlineBelow() + os.Exit(0) + } + + // Delete from database + if err := db.DeleteTimeEntry(selectedEntry.ID); err != nil { + ui.PrintError(ui.EmojiError, fmt.Sprintf("%v", err)) + os.Exit(1) + } + + fmt.Println() + ui.PrintSuccess(ui.EmojiSuccess, "Entry deleted successfully") + ui.NewlineBelow() + }, +} + +// formatEntryLabelForDelete formats a time entry for display in the delete selection list +// Shows running entries differently than completed ones +func formatEntryLabelForDelete(entry *storage.TimeEntry) string { + startStr := entry.StartTime.Format("2006-01-02 3:04 PM") + + if entry.EndTime == nil { + // Running entry + description := entry.Description + if description == "" { + description = "(no description)" + } + return fmt.Sprintf("%s → Running - %s", startStr, description) + } + + // Completed entry + endStr := entry.EndTime.Format("3:04 PM") + duration := entry.Duration() + durationStr := ui.FormatDuration(duration) + + description := entry.Description + if description == "" { + description = "(no description)" + } + + return fmt.Sprintf("%s → %s (%s) - %s", startStr, endStr, durationStr, description) +} + +func init() { + rootCmd.AddCommand(deleteCmd) + + deleteCmd.Flags().BoolVar(&showAllProjectsDelete, "show-all-projects", false, "Show project selection before entry selection") +} diff --git a/cmd/edit.go b/cmd/edit.go index 4607b8a..d29e3be 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -346,6 +346,7 @@ func validateTimeOptional(input string) error { } func init() { - editCmd.Flags().BoolVar(&showAllProjects, "show-all-projects", false, "Show project selection before entry selection") rootCmd.AddCommand(editCmd) + + editCmd.Flags().BoolVar(&showAllProjects, "show-all-projects", false, "Show project selection before entry selection") } diff --git a/internal/storage/db.go b/internal/storage/db.go index b32e4ba..046d100 100644 --- a/internal/storage/db.go +++ b/internal/storage/db.go @@ -545,6 +545,17 @@ func (d *Database) UpdateTimeEntry(id int64, entry *TimeEntry) error { return nil } +// DeleteTimeEntry deletes a time entry from the database by its ID. +// Returns an error if the deletion fails. Does not verify that a row with the given ID exists; +// if no rows are affected the function will still return nil (no error). +func (d *Database) DeleteTimeEntry(id int64) error { + _, err := d.db.Exec("DELETE FROM time_entries WHERE id = ?", id) + if err != nil { + return fmt.Errorf("failed to delete entry: %w", err) + } + return nil +} + // Close closes the Database, releasing any underlying resources. // It delegates to the wrapped database's Close method and returns any error encountered. // After Close is called, the Database must not be used for further operations. From 87209195dcc03c89ff7ec37e5021703c450aefde Mon Sep 17 00:00:00 2001 From: Dylan Ravel Date: Tue, 16 Dec 2025 21:01:08 -0700 Subject: [PATCH 3/3] Add text formatting and combined color-format functions Introduced ANSI text formatting constants (bold, dim, italic, underline) and corresponding formatting functions. Added combined color and bold formatting functions for success, error, info, and warning messages to enhance UI output flexibility. --- internal/ui/ui.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 707d99d..00ca277 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -17,6 +17,14 @@ const ( ColorGray = "\033[90m" // Muted text ) +// ANSI Text Formatting Constants +const ( + FormatBold = "\033[1m" + FormatDim = "\033[2m" + FormatItalic = "\033[3m" + FormatUnderline = "\033[4m" +) + // Emoji Constants const ( EmojiStart = "✨" @@ -33,27 +41,71 @@ const ( EmojiInfo = "ℹ️" ) -// Colored output functions that return colored strings +// Success colored output functions that returns colored string func Success(message string) string { return ColorGreen + message + ColorReset } +// Error colored output functions that returns colored string func Error(message string) string { return ColorRed + message + ColorReset } +// Info colored output functions that returns colored string func Info(message string) string { return ColorBlue + message + ColorReset } +// Warning colored output functions that returns colored string func Warning(message string) string { return ColorYellow + message + ColorReset } +// Muted colored output functions that returns colored string func Muted(message string) string { return ColorGray + message + ColorReset } +// Bold text formatting functions that return formatted string +func Bold(message string) string { + return FormatBold + message + ColorReset +} + +// Dim text formatting functions that return formatted string +func Dim(message string) string { + return FormatDim + message + ColorReset +} + +// Italic text formatting functions that return formatted string +func Italic(message string) string { + return FormatItalic + message + ColorReset +} + +// Underline text formatting functions that return formatted string +func Underline(message string) string { + return FormatUnderline + message + ColorReset +} + +// Bold success combined formatting functions for common use cases +func BoldSuccess(message string) string { + return FormatBold + ColorGreen + message + ColorReset +} + +// Bold error combined formatting functions for common use cases +func BoldError(message string) string { + return FormatBold + ColorRed + message + ColorReset +} + +// Bold info combined formatting functions for common use cases +func BoldInfo(message string) string { + return FormatBold + ColorBlue + message + ColorReset +} + +// Bold warning combined formatting functions for common use cases +func BoldWarning(message string) string { + return FormatBold + ColorYellow + message + ColorReset +} + // PrintSuccess prints a success message with emoji and color to stdout func PrintSuccess(emoji, message string) { fmt.Println(Success(fmt.Sprintf("%s %s", emoji, message)))