From a134e2c6dce66e195e672a5cec10a147b3143643 Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 31 Jan 2026 22:56:52 +0000 Subject: [PATCH] refactor(cli): rewrite styles and output with zero-dep ANSI - Replaces lipgloss with internal ANSI styling - Updates output functions to use new style and glyph system - Removes external dependencies from strings.go and errors.go - Fixes redeclarations in utils.go and runtime.go Co-Authored-By: Claude Opus 4.5 --- pkg/cli/errors.go | 14 +- pkg/cli/output.go | 245 +++++---------- pkg/cli/runtime.go | 38 --- pkg/cli/strings.go | 76 ++--- pkg/cli/styles.go | 754 +++++++++------------------------------------ pkg/cli/utils.go | 27 +- 6 files changed, 262 insertions(+), 892 deletions(-) diff --git a/pkg/cli/errors.go b/pkg/cli/errors.go index 503005fd..3e482a25 100644 --- a/pkg/cli/errors.go +++ b/pkg/cli/errors.go @@ -81,10 +81,18 @@ func Join(errs ...error) error { // Fatal Functions (print and exit) // ───────────────────────────────────────────────────────────────────────────── +// Fatal prints an error message and exits with code 1. +func Fatal(err error) { + if err != nil { + fmt.Println(ErrorStyle.Render(Glyph(":cross:") + " " + err.Error())) + os.Exit(1) + } +} + // Fatalf prints a formatted error message and exits with code 1. func Fatalf(format string, args ...any) { msg := fmt.Sprintf(format, args...) - fmt.Println(ErrorStyle.Render(SymbolCross + " " + msg)) + fmt.Println(ErrorStyle.Render(Glyph(":cross:") + " " + msg)) os.Exit(1) } @@ -97,7 +105,7 @@ func FatalWrap(err error, msg string) { return } fullMsg := fmt.Sprintf("%s: %v", msg, err) - fmt.Println(ErrorStyle.Render(SymbolCross + " " + fullMsg)) + fmt.Println(ErrorStyle.Render(Glyph(":cross:") + " " + fullMsg)) os.Exit(1) } @@ -111,6 +119,6 @@ func FatalWrapVerb(err error, verb, subject string) { } msg := i18n.ActionFailed(verb, subject) fullMsg := fmt.Sprintf("%s: %v", msg, err) - fmt.Println(ErrorStyle.Render(SymbolCross + " " + fullMsg)) + fmt.Println(ErrorStyle.Render(Glyph(":cross:") + " " + fullMsg)) os.Exit(1) } diff --git a/pkg/cli/output.go b/pkg/cli/output.go index 0a2c5649..24be804d 100644 --- a/pkg/cli/output.go +++ b/pkg/cli/output.go @@ -3,174 +3,99 @@ package cli import ( "fmt" - "github.com/charmbracelet/lipgloss" "github.com/host-uk/core/pkg/i18n" ) -// ───────────────────────────────────────────────────────────────────────────── -// Style Namespace -// ───────────────────────────────────────────────────────────────────────────── - -// Styles provides namespaced access to CLI styles. -// Usage: cli.Style.Dim.Render("text"), cli.Style.Success.Render("done") -var Style = struct { - // Text styles - Dim lipgloss.Style - Muted lipgloss.Style - Bold lipgloss.Style - Value lipgloss.Style - Accent lipgloss.Style - Code lipgloss.Style - Key lipgloss.Style - Number lipgloss.Style - Link lipgloss.Style - Header lipgloss.Style - Title lipgloss.Style - Stage lipgloss.Style - PrNum lipgloss.Style - AccentL lipgloss.Style - - // Status styles - Success lipgloss.Style - Error lipgloss.Style - Warning lipgloss.Style - Info lipgloss.Style - - // Git styles - Dirty lipgloss.Style - Ahead lipgloss.Style - Behind lipgloss.Style - Clean lipgloss.Style - Conflict lipgloss.Style - - // Repo name style - Repo lipgloss.Style - - // Coverage styles - CoverageHigh lipgloss.Style - CoverageMed lipgloss.Style - CoverageLow lipgloss.Style - - // Priority styles - PriorityHigh lipgloss.Style - PriorityMedium lipgloss.Style - PriorityLow lipgloss.Style - - // Severity styles - SeverityCritical lipgloss.Style - SeverityHigh lipgloss.Style - SeverityMedium lipgloss.Style - SeverityLow lipgloss.Style - - // Status indicator styles - StatusPending lipgloss.Style - StatusRunning lipgloss.Style - StatusSuccess lipgloss.Style - StatusError lipgloss.Style - StatusWarning lipgloss.Style - - // Deploy styles - DeploySuccess lipgloss.Style - DeployPending lipgloss.Style - DeployFailed lipgloss.Style - - // Box styles - Box lipgloss.Style - BoxHeader lipgloss.Style - ErrorBox lipgloss.Style - SuccessBox lipgloss.Style -}{ - // Text styles - Dim: DimStyle, - Muted: MutedStyle, - Bold: BoldStyle, - Value: ValueStyle, - Accent: AccentStyle, - Code: CodeStyle, - Key: KeyStyle, - Number: NumberStyle, - Link: LinkStyle, - Header: HeaderStyle, - Title: TitleStyle, - Stage: StageStyle, - PrNum: PrNumberStyle, - AccentL: AccentLabelStyle, - - // Status styles - Success: SuccessStyle, - Error: ErrorStyle, - Warning: WarningStyle, - Info: InfoStyle, - - // Git styles - Dirty: GitDirtyStyle, - Ahead: GitAheadStyle, - Behind: GitBehindStyle, - Clean: GitCleanStyle, - Conflict: GitConflictStyle, - - // Repo name style - Repo: RepoNameStyle, - - // Coverage styles - CoverageHigh: CoverageHighStyle, - CoverageMed: CoverageMedStyle, - CoverageLow: CoverageLowStyle, - - // Priority styles - PriorityHigh: PriorityHighStyle, - PriorityMedium: PriorityMediumStyle, - PriorityLow: PriorityLowStyle, - - // Severity styles - SeverityCritical: SeverityCriticalStyle, - SeverityHigh: SeverityHighStyle, - SeverityMedium: SeverityMediumStyle, - SeverityLow: SeverityLowStyle, - - // Status indicator styles - StatusPending: StatusPendingStyle, - StatusRunning: StatusRunningStyle, - StatusSuccess: StatusSuccessStyle, - StatusError: StatusErrorStyle, - StatusWarning: StatusWarningStyle, - - // Deploy styles - DeploySuccess: DeploySuccessStyle, - DeployPending: DeployPendingStyle, - DeployFailed: DeployFailedStyle, - - // Box styles - Box: BoxStyle, - BoxHeader: BoxHeaderStyle, - ErrorBox: ErrorBoxStyle, - SuccessBox: SuccessBoxStyle, +// Blank prints an empty line. +func Blank() { + fmt.Println() } -// ───────────────────────────────────────────────────────────────────────────── -// Core Output Functions -// ───────────────────────────────────────────────────────────────────────────── - -// Line translates a key via i18n.T and prints with newline. -// If no key is provided, prints an empty line. -// -// cli.Line("i18n.progress.check") // prints "Checking...\n" -// cli.Line("cmd.dev.ci.short") // prints translated text + \n -// cli.Line("greeting", map[string]any{"Name": "World"}) // with args -// cli.Line("") // prints empty line -func Line(key string, args ...any) { - if key == "" { - fmt.Println() - return - } +// Echo translates a key via i18n.T and prints with newline. +// No automatic styling - use Success/Error/Warn/Info for styled output. +func Echo(key string, args ...any) { fmt.Println(i18n.T(key, args...)) } -// ───────────────────────────────────────────────────────────────────────────── -// Input Functions -// ───────────────────────────────────────────────────────────────────────────── +// Print outputs formatted text (no newline). +// Glyph shortcodes like :check: are converted. +func Print(format string, args ...any) { + fmt.Print(compileGlyphs(fmt.Sprintf(format, args...))) +} -// Scanln reads from stdin, similar to fmt.Scanln. +// Println outputs formatted text with newline. +// Glyph shortcodes like :check: are converted. +func Println(format string, args ...any) { + fmt.Println(compileGlyphs(fmt.Sprintf(format, args...))) +} + +// Success prints a success message with checkmark (green). +func Success(msg string) { + fmt.Println(SuccessStyle.Render(Glyph(":check:") + " " + msg)) +} + +// Successf prints a formatted success message. +func Successf(format string, args ...any) { + Success(fmt.Sprintf(format, args...)) +} + +// Error prints an error message with cross (red). +func Error(msg string) { + fmt.Println(ErrorStyle.Render(Glyph(":cross:") + " " + msg)) +} + +// Errorf prints a formatted error message. +func Errorf(format string, args ...any) { + Error(fmt.Sprintf(format, args...)) +} + +// Warn prints a warning message with warning symbol (amber). +func Warn(msg string) { + fmt.Println(WarningStyle.Render(Glyph(":warn:") + " " + msg)) +} + +// Warnf prints a formatted warning message. +func Warnf(format string, args ...any) { + Warn(fmt.Sprintf(format, args...)) +} + +// Info prints an info message with info symbol (blue). +func Info(msg string) { + fmt.Println(InfoStyle.Render(Glyph(":info:") + " " + msg)) +} + +// Infof prints a formatted info message. +func Infof(format string, args ...any) { + Info(fmt.Sprintf(format, args...)) +} + +// Dim prints dimmed text. +func Dim(msg string) { + fmt.Println(DimStyle.Render(msg)) +} + +// Progress prints a progress indicator that overwrites the current line. +// Uses i18n.Progress for gerund form ("Checking..."). +func Progress(verb string, current, total int, item ...string) { + msg := i18n.Progress(verb) + if len(item) > 0 && item[0] != "" { + fmt.Printf("\033[2K\r%s %d/%d %s", DimStyle.Render(msg), current, total, item[0]) + } else { + fmt.Printf("\033[2K\r%s %d/%d", DimStyle.Render(msg), current, total) + } +} + +// ProgressDone clears the progress line. +func ProgressDone() { + fmt.Print("\033[2K\r") +} + +// Label prints a "Label: value" line. +func Label(word, value string) { + fmt.Printf("%s %s\n", KeyStyle.Render(i18n.Label(word)), value) +} + +// Scanln reads from stdin. func Scanln(a ...any) (int, error) { return fmt.Scanln(a...) -} +} \ No newline at end of file diff --git a/pkg/cli/runtime.go b/pkg/cli/runtime.go index 6054ef34..1e14e71b 100644 --- a/pkg/cli/runtime.go +++ b/pkg/cli/runtime.go @@ -144,45 +144,7 @@ func Shutdown() { instance.core.ServiceShutdown(instance.ctx) } -// --- Output Functions --- -// Success prints a success message with checkmark. -func Success(msg string) { - fmt.Println(SuccessStyle.Render(SymbolCheck + " " + msg)) -} - -// Error prints an error message with cross. -func Error(msg string) { - fmt.Println(ErrorStyle.Render(SymbolCross + " " + msg)) -} - -// Fatal prints an error message and exits with code 1. -func Fatal(err error) { - if err != nil { - Error(err.Error()) - os.Exit(1) - } -} - -// Warning prints a warning message. -func Warning(msg string) { - fmt.Println(WarningStyle.Render(SymbolWarning + " " + msg)) -} - -// Info prints an info message. -func Info(msg string) { - fmt.Println(InfoStyle.Render(SymbolInfo + " " + msg)) -} - -// Title prints a title/header. -func Title(msg string) { - fmt.Println(TitleStyle.Render(msg)) -} - -// Dim prints dimmed/subtle text. -func Dim(msg string) { - fmt.Println(DimStyle.Render(msg)) -} // --- Signal Service (internal) --- diff --git a/pkg/cli/strings.go b/pkg/cli/strings.go index 9322ced6..9e4240b9 100644 --- a/pkg/cli/strings.go +++ b/pkg/cli/strings.go @@ -1,88 +1,48 @@ package cli -import ( - "fmt" +import "fmt" - "github.com/charmbracelet/lipgloss" -) - -// ───────────────────────────────────────────────────────────────────────────── -// String Formatting (replace fmt.Sprintf) -// ───────────────────────────────────────────────────────────────────────────── - -// Sprintf formats a string. -// This is a direct replacement for fmt.Sprintf. +// Sprintf formats a string (fmt.Sprintf wrapper). func Sprintf(format string, args ...any) string { return fmt.Sprintf(format, args...) } -// Sprint formats using the default formats for its operands. -// This is a direct replacement for fmt.Sprint. +// Sprint formats using default formats (fmt.Sprint wrapper). func Sprint(args ...any) string { return fmt.Sprint(args...) } -// ───────────────────────────────────────────────────────────────────────────── -// Styled String Functions -// ───────────────────────────────────────────────────────────────────────────── - -// Styled returns text formatted with a style. -// Example: cli.Styled(cli.Style.Success, "Done!") -func Styled(style lipgloss.Style, text string) string { +// Styled returns text with a style applied. +func Styled(style *AnsiStyle, text string) string { return style.Render(text) } -// Styledf returns formatted text with a style. -// Example: cli.Styledf(cli.Style.Success, "Processed %d items", count) -func Styledf(style lipgloss.Style, format string, args ...any) string { +// Styledf returns formatted text with a style applied. +func Styledf(style *AnsiStyle, format string, args ...any) string { return style.Render(fmt.Sprintf(format, args...)) } -// ───────────────────────────────────────────────────────────────────────────── -// Pre-styled Formatting Functions -// ───────────────────────────────────────────────────────────────────────────── - -// SuccessStr returns a success-styled string with checkmark. +// SuccessStr returns success-styled string. func SuccessStr(msg string) string { - return SuccessStyle.Render(SymbolCheck + " " + msg) + return SuccessStyle.Render(Glyph(":check:") + " " + msg) } -// ErrorStr returns an error-styled string with cross. +// ErrorStr returns error-styled string. func ErrorStr(msg string) string { - return ErrorStyle.Render(SymbolCross + " " + msg) + return ErrorStyle.Render(Glyph(":cross:") + " " + msg) } -// WarningStr returns a warning-styled string with warning symbol. -func WarningStr(msg string) string { - return WarningStyle.Render(SymbolWarning + " " + msg) +// WarnStr returns warning-styled string. +func WarnStr(msg string) string { + return WarningStyle.Render(Glyph(":warn:") + " " + msg) } -// InfoStr returns an info-styled string with info symbol. +// InfoStr returns info-styled string. func InfoStr(msg string) string { - return InfoStyle.Render(SymbolInfo + " " + msg) + return InfoStyle.Render(Glyph(":info:") + " " + msg) } -// DimStr returns a dim-styled string. +// DimStr returns dim-styled string. func DimStr(msg string) string { return DimStyle.Render(msg) -} - -// BoldStr returns a bold-styled string. -func BoldStr(msg string) string { - return BoldStyle.Render(msg) -} - -// ───────────────────────────────────────────────────────────────────────────── -// Numeric Formatting -// ───────────────────────────────────────────────────────────────────────────── - -// Itoa converts an integer to a string. -// This is a convenience function similar to strconv.Itoa. -func Itoa(n int) string { - return fmt.Sprintf("%d", n) -} - -// Itoa64 converts an int64 to a string. -func Itoa64(n int64) string { - return fmt.Sprintf("%d", n) -} +} \ No newline at end of file diff --git a/pkg/cli/styles.go b/pkg/cli/styles.go index f79809ef..985d3dea 100644 --- a/pkg/cli/styles.go +++ b/pkg/cli/styles.go @@ -1,646 +1,186 @@ -// Package cli provides common utilities and styles for CLI commands. -// -// This package contains: -// - Terminal styling using lipgloss with Tailwind colours -// - Unicode symbols for consistent visual indicators -// - Helper functions for common output patterns -// - Git and GitHub CLI utilities +// Package cli provides semantic CLI output with zero external dependencies. package cli import ( "fmt" "strings" - - "github.com/charmbracelet/lipgloss" + "time" ) -// ───────────────────────────────────────────────────────────────────────────── -// Tailwind Colour Palette -// ───────────────────────────────────────────────────────────────────────────── - -// Tailwind colours for consistent theming across the CLI. +// Tailwind colour palette (hex strings) const ( - ColourBlue50 = lipgloss.Color("#eff6ff") - ColourBlue100 = lipgloss.Color("#dbeafe") - ColourBlue200 = lipgloss.Color("#bfdbfe") - ColourBlue300 = lipgloss.Color("#93c5fd") - ColourBlue400 = lipgloss.Color("#60a5fa") - ColourBlue500 = lipgloss.Color("#3b82f6") - ColourBlue600 = lipgloss.Color("#2563eb") - ColourBlue700 = lipgloss.Color("#1d4ed8") - ColourGreen400 = lipgloss.Color("#4ade80") - ColourGreen500 = lipgloss.Color("#22c55e") - ColourGreen600 = lipgloss.Color("#16a34a") - ColourRed400 = lipgloss.Color("#f87171") - ColourRed500 = lipgloss.Color("#ef4444") - ColourRed600 = lipgloss.Color("#dc2626") - ColourAmber400 = lipgloss.Color("#fbbf24") - ColourAmber500 = lipgloss.Color("#f59e0b") - ColourAmber600 = lipgloss.Color("#d97706") - ColourOrange500 = lipgloss.Color("#f97316") - ColourYellow500 = lipgloss.Color("#eab308") - ColourEmerald500 = lipgloss.Color("#10b981") - ColourPurple500 = lipgloss.Color("#a855f7") - ColourViolet400 = lipgloss.Color("#a78bfa") - ColourViolet500 = lipgloss.Color("#8b5cf6") - ColourIndigo500 = lipgloss.Color("#6366f1") - ColourCyan500 = lipgloss.Color("#06b6d4") - ColourGray50 = lipgloss.Color("#f9fafb") - ColourGray100 = lipgloss.Color("#f3f4f6") - ColourGray200 = lipgloss.Color("#e5e7eb") - ColourGray300 = lipgloss.Color("#d1d5db") - ColourGray400 = lipgloss.Color("#9ca3af") - ColourGray500 = lipgloss.Color("#6b7280") - ColourGray600 = lipgloss.Color("#4b5563") - ColourGray700 = lipgloss.Color("#374151") - ColourGray800 = lipgloss.Color("#1f2937") - ColourGray900 = lipgloss.Color("#111827") + ColourBlue50 = "#eff6ff" + ColourBlue100 = "#dbeafe" + ColourBlue200 = "#bfdbfe" + ColourBlue300 = "#93c5fd" + ColourBlue400 = "#60a5fa" + ColourBlue500 = "#3b82f6" + ColourBlue600 = "#2563eb" + ColourBlue700 = "#1d4ed8" + ColourGreen400 = "#4ade80" + ColourGreen500 = "#22c55e" + ColourGreen600 = "#16a34a" + ColourRed400 = "#f87171" + ColourRed500 = "#ef4444" + ColourRed600 = "#dc2626" + ColourAmber400 = "#fbbf24" + ColourAmber500 = "#f59e0b" + ColourAmber600 = "#d97706" + ColourOrange500 = "#f97316" + ColourYellow500 = "#eab308" + ColourEmerald500= "#10b981" + ColourPurple500 = "#a855f7" + ColourViolet400 = "#a78bfa" + ColourViolet500 = "#8b5cf6" + ColourIndigo500 = "#6366f1" + ColourCyan500 = "#06b6d4" + ColourGray50 = "#f9fafb" + ColourGray100 = "#f3f4f6" + ColourGray200 = "#e5e7eb" + ColourGray300 = "#d1d5db" + ColourGray400 = "#9ca3af" + ColourGray500 = "#6b7280" + ColourGray600 = "#4b5563" + ColourGray700 = "#374151" + ColourGray800 = "#1f2937" + ColourGray900 = "#111827" ) -// ───────────────────────────────────────────────────────────────────────────── -// Unicode Symbols -// ───────────────────────────────────────────────────────────────────────────── - -// Symbols for consistent visual indicators across commands. -const ( - // Status indicators - SymbolCheck = "✓" // Success, pass, complete - SymbolCross = "✗" // Error, fail, incomplete - SymbolWarning = "⚠" // Warning, caution - SymbolInfo = "ℹ" // Information - SymbolQuestion = "?" // Unknown, needs attention - SymbolSkip = "○" // Skipped, neutral - SymbolDot = "●" // Active, selected - SymbolCircle = "◯" // Inactive, unselected - - // Arrows and pointers - SymbolArrowRight = "→" - SymbolArrowLeft = "←" - SymbolArrowUp = "↑" - SymbolArrowDown = "↓" - SymbolPointer = "▶" - - // Decorative - SymbolBullet = "•" - SymbolDash = "─" - SymbolPipe = "│" - SymbolCorner = "└" - SymbolTee = "├" - SymbolSeparator = " │ " - - // Progress - SymbolSpinner = "⠋" // First frame of braille spinner - SymbolPending = "…" -) - -// ───────────────────────────────────────────────────────────────────────────── -// Base Styles -// ───────────────────────────────────────────────────────────────────────────── - -// Terminal styles using Tailwind colour palette. -// These are shared across command packages for consistent output. +// Core styles var ( - // RepoNameStyle highlights repository names (blue, bold). - RepoNameStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(ColourBlue500) - - // SuccessStyle indicates successful operations (green, bold). - SuccessStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(ColourGreen500) - - // ErrorStyle indicates errors and failures (red, bold). - ErrorStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(ColourRed500) - - // WarningStyle indicates warnings and cautions (amber, bold). - WarningStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(ColourAmber500) - - // InfoStyle for informational messages (blue). - InfoStyle = lipgloss.NewStyle(). - Foreground(ColourBlue400) - - // DimStyle for secondary/muted text (gray). - DimStyle = lipgloss.NewStyle(). - Foreground(ColourGray500) - - // MutedStyle for very subtle text (darker gray). - MutedStyle = lipgloss.NewStyle(). - Foreground(ColourGray600) - - // ValueStyle for data values and output (light gray). - ValueStyle = lipgloss.NewStyle(). - Foreground(ColourGray200) - - // AccentStyle for highlighted values (cyan). - AccentStyle = lipgloss.NewStyle(). - Foreground(ColourCyan500) - - // LinkStyle for URLs and clickable references (blue, underlined). - LinkStyle = lipgloss.NewStyle(). - Foreground(ColourBlue500). - Underline(true) - - // HeaderStyle for section headers (light gray, bold). - HeaderStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(ColourGray200) - - // TitleStyle for command titles (blue, bold). - TitleStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(ColourBlue500) - - // BoldStyle for emphasis without colour. - BoldStyle = lipgloss.NewStyle(). - Bold(true) - - // CodeStyle for inline code or paths. - CodeStyle = lipgloss.NewStyle(). - Foreground(ColourGray300). - Background(ColourGray800). - Padding(0, 1) - - // KeyStyle for labels/keys in key-value pairs. - KeyStyle = lipgloss.NewStyle(). - Foreground(ColourGray400) - - // NumberStyle for numeric values. - NumberStyle = lipgloss.NewStyle(). - Foreground(ColourBlue300) - - // PrNumberStyle for pull request numbers (purple, bold). - PrNumberStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(ColourPurple500) - - // AccentLabelStyle for highlighted labels (violet). - AccentLabelStyle = lipgloss.NewStyle(). - Foreground(ColourViolet400) - - // StageStyle for pipeline/QA stage headers (indigo, bold). - StageStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(ColourIndigo500) + SuccessStyle = NewStyle().Bold().Foreground(ColourGreen500) + ErrorStyle = NewStyle().Bold().Foreground(ColourRed500) + WarningStyle = NewStyle().Bold().Foreground(ColourAmber500) + InfoStyle = NewStyle().Foreground(ColourBlue400) + DimStyle = NewStyle().Dim().Foreground(ColourGray500) + MutedStyle = NewStyle().Foreground(ColourGray600) + BoldStyle = NewStyle().Bold() + KeyStyle = NewStyle().Foreground(ColourGray400) + ValueStyle = NewStyle().Foreground(ColourGray200) + AccentStyle = NewStyle().Foreground(ColourCyan500) + LinkStyle = NewStyle().Foreground(ColourBlue500).Underline() + HeaderStyle = NewStyle().Bold().Foreground(ColourGray200) + TitleStyle = NewStyle().Bold().Foreground(ColourBlue500) + CodeStyle = NewStyle().Foreground(ColourGray300) + NumberStyle = NewStyle().Foreground(ColourBlue300) + RepoStyle = NewStyle().Bold().Foreground(ColourBlue500) ) -// ───────────────────────────────────────────────────────────────────────────── -// Status Styles (for consistent status indicators) -// ───────────────────────────────────────────────────────────────────────────── - -var ( - // StatusPendingStyle for pending/waiting states. - StatusPendingStyle = lipgloss.NewStyle().Foreground(ColourGray500) - - // StatusRunningStyle for in-progress states. - StatusRunningStyle = lipgloss.NewStyle().Foreground(ColourBlue500) - - // StatusSuccessStyle for completed/success states. - StatusSuccessStyle = lipgloss.NewStyle().Foreground(ColourGreen500) - - // StatusErrorStyle for failed/error states. - StatusErrorStyle = lipgloss.NewStyle().Foreground(ColourRed500) - - // StatusWarningStyle for warning states. - StatusWarningStyle = lipgloss.NewStyle().Foreground(ColourAmber500) -) - -// ───────────────────────────────────────────────────────────────────────────── -// Coverage Styles (for test/code coverage display) -// ───────────────────────────────────────────────────────────────────────────── - -var ( - // CoverageHighStyle for good coverage (80%+). - CoverageHighStyle = lipgloss.NewStyle().Foreground(ColourGreen500) - - // CoverageMedStyle for moderate coverage (50-79%). - CoverageMedStyle = lipgloss.NewStyle().Foreground(ColourAmber500) - - // CoverageLowStyle for low coverage (<50%). - CoverageLowStyle = lipgloss.NewStyle().Foreground(ColourRed500) -) - -// ───────────────────────────────────────────────────────────────────────────── -// Priority Styles (for task/issue priority levels) -// ───────────────────────────────────────────────────────────────────────────── - -var ( - // PriorityHighStyle for high/critical priority (red, bold). - PriorityHighStyle = lipgloss.NewStyle().Bold(true).Foreground(ColourRed500) - - // PriorityMediumStyle for medium priority (amber). - PriorityMediumStyle = lipgloss.NewStyle().Foreground(ColourAmber500) - - // PriorityLowStyle for low priority (green). - PriorityLowStyle = lipgloss.NewStyle().Foreground(ColourGreen500) -) - -// ───────────────────────────────────────────────────────────────────────────── -// Severity Styles (for security/QA severity levels) -// ───────────────────────────────────────────────────────────────────────────── - -var ( - // SeverityCriticalStyle for critical issues (red, bold). - SeverityCriticalStyle = lipgloss.NewStyle().Bold(true).Foreground(ColourRed500) - - // SeverityHighStyle for high severity issues (orange, bold). - SeverityHighStyle = lipgloss.NewStyle().Bold(true).Foreground(ColourOrange500) - - // SeverityMediumStyle for medium severity issues (amber). - SeverityMediumStyle = lipgloss.NewStyle().Foreground(ColourAmber500) - - // SeverityLowStyle for low severity issues (gray). - SeverityLowStyle = lipgloss.NewStyle().Foreground(ColourGray500) -) - -// ───────────────────────────────────────────────────────────────────────────── -// Git Status Styles (for repo state indicators) -// ───────────────────────────────────────────────────────────────────────────── - -var ( - // GitDirtyStyle for uncommitted changes (red). - GitDirtyStyle = lipgloss.NewStyle().Foreground(ColourRed500) - - // GitAheadStyle for unpushed commits (green). - GitAheadStyle = lipgloss.NewStyle().Foreground(ColourGreen500) - - // GitBehindStyle for unpulled commits (amber). - GitBehindStyle = lipgloss.NewStyle().Foreground(ColourAmber500) - - // GitCleanStyle for clean state (gray). - GitCleanStyle = lipgloss.NewStyle().Foreground(ColourGray500) - - // GitConflictStyle for merge conflicts (red, bold). - GitConflictStyle = lipgloss.NewStyle().Bold(true).Foreground(ColourRed500) -) - -// ───────────────────────────────────────────────────────────────────────────── -// Deploy Styles (for deployment status display) -// ───────────────────────────────────────────────────────────────────────────── - -var ( - // DeploySuccessStyle for successful deployments (emerald). - DeploySuccessStyle = lipgloss.NewStyle().Foreground(ColourEmerald500) - - // DeployPendingStyle for pending deployments (amber). - DeployPendingStyle = lipgloss.NewStyle().Foreground(ColourAmber500) - - // DeployFailedStyle for failed deployments (red). - DeployFailedStyle = lipgloss.NewStyle().Foreground(ColourRed500) -) - -// ───────────────────────────────────────────────────────────────────────────── -// Box Styles (for bordered content) -// ───────────────────────────────────────────────────────────────────────────── - -var ( - // BoxStyle creates a rounded border box. - BoxStyle = lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(ColourGray600). - Padding(0, 1) - - // BoxHeaderStyle for box titles. - BoxHeaderStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(ColourBlue400). - MarginBottom(1) - - // ErrorBoxStyle for error message boxes. - ErrorBoxStyle = lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(ColourRed500). - Padding(0, 1) - - // SuccessBoxStyle for success message boxes. - SuccessBoxStyle = lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(ColourGreen500). - Padding(0, 1) -) - -// ───────────────────────────────────────────────────────────────────────────── -// Helper Functions -// ───────────────────────────────────────────────────────────────────────────── - -// FmtSuccess returns a styled success message with checkmark. -func FmtSuccess(msg string) string { - return fmt.Sprintf("%s %s", SuccessStyle.Render(SymbolCheck), msg) -} - -// FmtError returns a styled error message with cross. -func FmtError(msg string) string { - return fmt.Sprintf("%s %s", ErrorStyle.Render(SymbolCross), msg) -} - -// FmtWarning returns a styled warning message with warning symbol. -func FmtWarning(msg string) string { - return fmt.Sprintf("%s %s", WarningStyle.Render(SymbolWarning), msg) -} - -// FmtInfo returns a styled info message with info symbol. -func FmtInfo(msg string) string { - return fmt.Sprintf("%s %s", InfoStyle.Render(SymbolInfo), msg) -} - -// Pending returns a styled pending message with ellipsis. -func Pending(msg string) string { - return fmt.Sprintf("%s %s", DimStyle.Render(SymbolPending), DimStyle.Render(msg)) -} - -// Skip returns a styled skipped message with circle. -func Skip(msg string) string { - return fmt.Sprintf("%s %s", DimStyle.Render(SymbolSkip), DimStyle.Render(msg)) -} - -// KeyValue returns a styled "key: value" string. -func KeyValue(key, value string) string { - return fmt.Sprintf("%s %s", KeyStyle.Render(key+":"), value) -} - -// KeyValueBold returns a styled "key: value" with bold value. -func KeyValueBold(key, value string) string { - return fmt.Sprintf("%s %s", KeyStyle.Render(key+":"), BoldStyle.Render(value)) -} - -// StatusLine creates a pipe-separated status line from parts. -// Each part should be pre-styled. -func StatusLine(parts ...string) string { - return strings.Join(parts, DimStyle.Render(SymbolSeparator)) -} - -// StatusPart creates a styled count + label for status lines. -// Example: StatusPart(5, "repos", SuccessStyle) -> "5 repos" in green -func StatusPart(count int, label string, style lipgloss.Style) string { - return style.Render(fmt.Sprintf("%d", count)) + DimStyle.Render(" "+label) -} - -// StatusText creates a styled label for status lines. -// Example: StatusText("clean", SuccessStyle) -> "clean" in green -func StatusText(text string, style lipgloss.Style) string { - return style.Render(text) -} - -// CheckMark returns a styled checkmark (✓) or dash (—) based on presence. -// Useful for showing presence/absence in tables and lists. -func CheckMark(present bool) string { - if present { - return SuccessStyle.Render(SymbolCheck) +// Truncate shortens a string to max length with ellipsis. +func Truncate(s string, max int) string { + if len(s) <= max { + return s } - return DimStyle.Render("—") -} - -// CheckMarkCustom returns a styled indicator with custom symbols and styles. -func CheckMarkCustom(present bool, presentStyle, absentStyle lipgloss.Style, presentSymbol, absentSymbol string) string { - if present { - return presentStyle.Render(presentSymbol) + if max <= 3 { + return s[:max] } - return absentStyle.Render(absentSymbol) + return s[:max-3] + "..." } -// Label returns a styled label for key-value display. -// Example: Label("Status") -> "Status:" in dim gray -func Label(text string) string { - return KeyStyle.Render(text + ":") -} - -// LabelValue returns a styled "label: value" pair. -// Example: LabelValue("Branch", "main") -> "Branch: main" -func LabelValue(label, value string) string { - return fmt.Sprintf("%s %s", Label(label), value) -} - -// LabelValueStyled returns a styled "label: value" pair with custom value style. -func LabelValueStyled(label, value string, valueStyle lipgloss.Style) string { - return fmt.Sprintf("%s %s", Label(label), valueStyle.Render(value)) -} - -// CheckResult formats a check result with name and optional version. -// Used for environment checks like `✓ go 1.22.0` or `✗ docker`. -func CheckResult(ok bool, name string, version string) string { - symbol := ErrorStyle.Render(SymbolCross) - if ok { - symbol = SuccessStyle.Render(SymbolCheck) +// Pad right-pads a string to width. +func Pad(s string, width int) string { + if len(s) >= width { + return s } - if version != "" { - return fmt.Sprintf(" %s %s %s", symbol, name, DimStyle.Render(version)) - } - return fmt.Sprintf(" %s %s", symbol, name) + return s + strings.Repeat(" ", width-len(s)) } -// Bullet returns a bulleted item. -func Bullet(text string) string { - return fmt.Sprintf(" %s %s", DimStyle.Render(SymbolBullet), text) -} - -// TreeItem returns a tree item with branch indicator. -func TreeItem(text string, isLast bool) string { - prefix := SymbolTee - if isLast { - prefix = SymbolCorner - } - return fmt.Sprintf(" %s %s", DimStyle.Render(prefix), text) -} - -// Indent returns text indented by the specified level (2 spaces per level). -func Indent(level int, text string) string { - return strings.Repeat(" ", level) + text -} - -// Separator returns a horizontal separator line. -func Separator(width int) string { - return DimStyle.Render(strings.Repeat(SymbolDash, width)) -} - -// Header returns a styled section header with optional separator. -func Header(title string, withSeparator bool) string { - if withSeparator { - return fmt.Sprintf("\n%s\n%s", HeaderStyle.Render(title), Separator(len(title))) - } - return fmt.Sprintf("\n%s", HeaderStyle.Render(title)) -} - -// FmtTitle returns a styled command/section title. -func FmtTitle(text string) string { - return TitleStyle.Render(text) -} - -// FmtDim returns dimmed text. -func FmtDim(text string) string { - return DimStyle.Render(text) -} - -// Bold returns bold text. -func Bold(text string) string { - return BoldStyle.Render(text) -} - -// Highlight returns text with accent colour. -func Highlight(text string) string { - return AccentStyle.Render(text) -} - -// Path renders a file path in code style. -func Path(p string) string { - return CodeStyle.Render(p) -} - -// CommandStr renders a command string in code style. -func CommandStr(cmd string) string { - return CodeStyle.Render(cmd) -} - -// Number renders a number with styling. -func Number(n int) string { - return NumberStyle.Render(fmt.Sprintf("%d", n)) -} - -// FormatCoverage formats a coverage percentage with colour based on thresholds. -// High (green) >= 80%, Medium (amber) >= 50%, Low (red) < 50%. -func FormatCoverage(percent float64) string { - var style lipgloss.Style +// FormatAge formats a time as human-readable age (e.g., "2h ago", "3d ago"). +func FormatAge(t time.Time) string { + d := time.Since(t) switch { - case percent >= 80: - style = CoverageHighStyle - case percent >= 50: - style = CoverageMedStyle + case d < time.Minute: + return "just now" + case d < time.Hour: + return fmt.Sprintf("%dm ago", int(d.Minutes())) + case d < 24*time.Hour: + return fmt.Sprintf("%dh ago", int(d.Hours())) + case d < 7*24*time.Hour: + return fmt.Sprintf("%dd ago", int(d.Hours()/24)) + case d < 30*24*time.Hour: + return fmt.Sprintf("%dw ago", int(d.Hours()/(24*7))) default: - style = CoverageLowStyle - } - return style.Render(fmt.Sprintf("%.1f%%", percent)) -} - -// FormatCoverageCustom formats coverage with custom thresholds. -func FormatCoverageCustom(percent, highThreshold, medThreshold float64) string { - var style lipgloss.Style - switch { - case percent >= highThreshold: - style = CoverageHighStyle - case percent >= medThreshold: - style = CoverageMedStyle - default: - style = CoverageLowStyle - } - return style.Render(fmt.Sprintf("%.1f%%", percent)) -} - -// FormatSeverity returns styled text for a severity level. -func FormatSeverity(level string) string { - switch strings.ToLower(level) { - case "critical": - return SeverityCriticalStyle.Render(level) - case "high": - return SeverityHighStyle.Render(level) - case "medium", "med": - return SeverityMediumStyle.Render(level) - default: - return SeverityLowStyle.Render(level) + return fmt.Sprintf("%dmo ago", int(d.Hours()/(24*30))) } } -// FormatPriority returns styled text for a priority level. -func FormatPriority(level string) string { - switch strings.ToLower(level) { - case "high", "critical", "urgent": - return PriorityHighStyle.Render(level) - case "medium", "med", "normal": - return PriorityMediumStyle.Render(level) - default: - return PriorityLowStyle.Render(level) - } -} - -// FormatTaskStatus returns styled text for a task status. -// Supports: pending, in_progress, completed, blocked, failed. -func FormatTaskStatus(status string) string { - switch strings.ToLower(status) { - case "in_progress", "in-progress", "running", "active": - return StatusRunningStyle.Render(status) - case "completed", "done", "finished", "success": - return StatusSuccessStyle.Render(status) - case "blocked", "failed", "error": - return StatusErrorStyle.Render(status) - default: // pending, waiting, queued - return StatusPendingStyle.Render(status) - } -} - -// StatusPrefix returns a styled ">>" prefix for status messages. -func StatusPrefix(style lipgloss.Style) string { - return style.Render(">>") -} - -// ProgressLabel returns a dimmed label with colon for progress output. -// Example: ProgressLabel("Installing") -> "Installing:" in dim gray -func ProgressLabel(label string) string { - return DimStyle.Render(label + ":") -} - -// ───────────────────────────────────────────────────────────────────────────── -// Table Helpers -// ───────────────────────────────────────────────────────────────────────────── - -// TableRow represents a row in a simple table. -type TableRow []string - -// Table provides simple table rendering. +// Table renders tabular data with aligned columns. +// HLCRF is for layout; Table is for tabular data - they serve different purposes. type Table struct { Headers []string - Rows []TableRow - Widths []int // Optional: fixed column widths + Rows [][]string + Style TableStyle } -// Render returns the table as a formatted string. -func (t *Table) Render() string { - if len(t.Rows) == 0 && len(t.Headers) == 0 { +type TableStyle struct { + HeaderStyle *AnsiStyle + CellStyle *AnsiStyle + Separator string +} + +// DefaultTableStyle returns sensible defaults. +func DefaultTableStyle() TableStyle { + return TableStyle{ + HeaderStyle: HeaderStyle, + CellStyle: nil, + Separator: " ", + } +} + +// NewTable creates a table with headers. +func NewTable(headers ...string) *Table { + return &Table{ + Headers: headers, + Style: DefaultTableStyle(), + } +} + +// AddRow adds a row to the table. +func (t *Table) AddRow(cells ...string) *Table { + t.Rows = append(t.Rows, cells) + return t +} + +// String renders the table. +func (t *Table) String() string { + if len(t.Headers) == 0 && len(t.Rows) == 0 { return "" } - // Calculate column widths if not provided - widths := t.Widths - if len(widths) == 0 { - cols := len(t.Headers) - if cols == 0 && len(t.Rows) > 0 { - cols = len(t.Rows[0]) - } - widths = make([]int, cols) + // Calculate column widths + cols := len(t.Headers) + if cols == 0 && len(t.Rows) > 0 { + cols = len(t.Rows[0]) + } + widths := make([]int, cols) - for i, h := range t.Headers { - if len(h) > widths[i] { - widths[i] = len(h) - } + for i, h := range t.Headers { + if len(h) > widths[i] { + widths[i] = len(h) } - for _, row := range t.Rows { - for i, cell := range row { - if i < len(widths) && len(cell) > widths[i] { - widths[i] = len(cell) - } + } + for _, row := range t.Rows { + for i, cell := range row { + if i < cols && len(cell) > widths[i] { + widths[i] = len(cell) } } } var sb strings.Builder + sep := t.Style.Separator // Headers if len(t.Headers) > 0 { for i, h := range t.Headers { if i > 0 { - sb.WriteString(" ") + sb.WriteString(sep) } - sb.WriteString(HeaderStyle.Render(padRight(h, widths[i]))) - } - sb.WriteString("\n") - - // Separator - for i, w := range widths { - if i > 0 { - sb.WriteString(" ") + styled := Pad(h, widths[i]) + if t.Style.HeaderStyle != nil { + styled = t.Style.HeaderStyle.Render(styled) } - sb.WriteString(DimStyle.Render(strings.Repeat("─", w))) + sb.WriteString(styled) } sb.WriteString("\n") } @@ -649,13 +189,13 @@ func (t *Table) Render() string { for _, row := range t.Rows { for i, cell := range row { if i > 0 { - sb.WriteString(" ") + sb.WriteString(sep) } - if i < len(widths) { - sb.WriteString(padRight(cell, widths[i])) - } else { - sb.WriteString(cell) + styled := Pad(cell, widths[i]) + if t.Style.CellStyle != nil { + styled = t.Style.CellStyle.Render(styled) } + sb.WriteString(styled) } sb.WriteString("\n") } @@ -663,9 +203,7 @@ func (t *Table) Render() string { return sb.String() } -func padRight(s string, width int) string { - if len(s) >= width { - return s - } - return s + strings.Repeat(" ", width-len(s)) -} +// Render prints the table to stdout. +func (t *Table) Render() { + fmt.Print(t.String()) +} \ No newline at end of file diff --git a/pkg/cli/utils.go b/pkg/cli/utils.go index edd82fee..e7294aa4 100644 --- a/pkg/cli/utils.go +++ b/pkg/cli/utils.go @@ -20,13 +20,7 @@ func GhAuthenticated() bool { return strings.Contains(string(output), "Logged in") } -// Truncate shortens a string to max characters, adding "..." if truncated. -func Truncate(s string, max int) string { - if len(s) <= max { - return s - } - return s[:max-3] + "..." -} + // ConfirmOption configures Confirm behaviour. type ConfirmOption func(*confirmConfig) @@ -479,24 +473,7 @@ func ChooseMultiAction[T any](verb, subject string, items []T, opts ...ChooseOpt return ChooseMulti(question, items, opts...) } -// FormatAge formats a time as a human-readable age string. -// Examples: "5m ago", "2h ago", "3d ago", "1w ago", "2mo ago" -func FormatAge(t time.Time) string { - d := time.Since(t) - if d < time.Hour { - return fmt.Sprintf("%dm ago", int(d.Minutes())) - } - if d < 24*time.Hour { - return fmt.Sprintf("%dh ago", int(d.Hours())) - } - if d < 7*24*time.Hour { - return fmt.Sprintf("%dd ago", int(d.Hours()/24)) - } - if d < 30*24*time.Hour { - return fmt.Sprintf("%dw ago", int(d.Hours()/(24*7))) - } - return fmt.Sprintf("%dmo ago", int(d.Hours()/(24*30))) -} + // GitClone clones a GitHub repository to the specified path. // Prefers 'gh repo clone' if authenticated, falls back to SSH.