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 <noreply@anthropic.com>
This commit is contained in:
parent
21fd34097c
commit
a134e2c6dc
6 changed files with 262 additions and 892 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) ---
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue