v3.42.32: Add fatih/color for cross-platform terminal colors
All checks were successful
CI/CD / Test (push) Successful in 1m17s
CI/CD / Lint (push) Successful in 1m26s
CI/CD / Build & Release (push) Successful in 3m13s

- Windows-compatible colors via native console API
- Color helper functions: Success(), Error(), Warning(), Info()
- Text styling: Header(), Dim(), Bold(), Green(), Red(), Yellow(), Cyan()
- Logger CleanFormatter uses fatih/color instead of raw ANSI
- All progress indicators use colored [OK]/[FAIL] status
- Automatic color detection for non-TTY environments
This commit is contained in:
2026-01-14 16:13:00 +01:00
parent dc6dfd8b2c
commit 6a24ee39be
8 changed files with 212 additions and 29 deletions

118
internal/logger/colors.go Normal file
View File

@@ -0,0 +1,118 @@
package logger
import (
"fmt"
"os"
"github.com/fatih/color"
)
// CLI output helpers using fatih/color for cross-platform support
// Success prints a success message with green checkmark
func Success(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
SuccessColor.Fprint(os.Stdout, "✓ ")
fmt.Println(msg)
}
// Error prints an error message with red X
func Error(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
ErrorColor.Fprint(os.Stderr, "✗ ")
fmt.Fprintln(os.Stderr, msg)
}
// Warning prints a warning message with yellow exclamation
func Warning(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
WarnColor.Fprint(os.Stdout, "⚠ ")
fmt.Println(msg)
}
// Info prints an info message with blue arrow
func Info(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
InfoColor.Fprint(os.Stdout, "→ ")
fmt.Println(msg)
}
// Header prints a bold header
func Header(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
HighlightColor.Println(msg)
}
// Dim prints dimmed/secondary text
func Dim(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
DimColor.Println(msg)
}
// Bold returns bold text
func Bold(text string) string {
return color.New(color.Bold).Sprint(text)
}
// Green returns green text
func Green(text string) string {
return SuccessColor.Sprint(text)
}
// Red returns red text
func Red(text string) string {
return ErrorColor.Sprint(text)
}
// Yellow returns yellow text
func Yellow(text string) string {
return WarnColor.Sprint(text)
}
// Cyan returns cyan text
func Cyan(text string) string {
return InfoColor.Sprint(text)
}
// StatusLine prints a key-value status line
func StatusLine(key, value string) {
DimColor.Printf(" %s: ", key)
fmt.Println(value)
}
// ProgressStatus prints operation status with timing
func ProgressStatus(operation string, status string, isSuccess bool) {
if isSuccess {
SuccessColor.Print("[OK] ")
} else {
ErrorColor.Print("[FAIL] ")
}
fmt.Printf("%s: %s\n", operation, status)
}
// Table prints a simple formatted table row
func TableRow(cols ...string) {
for i, col := range cols {
if i == 0 {
InfoColor.Printf("%-20s", col)
} else {
fmt.Printf("%-15s", col)
}
}
fmt.Println()
}
// DisableColors disables all color output (for non-TTY or --no-color flag)
func DisableColors() {
color.NoColor = true
}
// EnableColors enables color output
func EnableColors() {
color.NoColor = false
}
// IsColorEnabled returns whether colors are enabled
func IsColorEnabled() bool {
return !color.NoColor
}

View File

@@ -7,9 +7,29 @@ import (
"strings"
"time"
"github.com/fatih/color"
"github.com/sirupsen/logrus"
)
// Color printers for consistent output across the application
var (
// Status colors
SuccessColor = color.New(color.FgGreen, color.Bold)
ErrorColor = color.New(color.FgRed, color.Bold)
WarnColor = color.New(color.FgYellow, color.Bold)
InfoColor = color.New(color.FgCyan)
DebugColor = color.New(color.FgWhite)
// Highlight colors
HighlightColor = color.New(color.FgMagenta, color.Bold)
DimColor = color.New(color.FgHiBlack)
// Data colors
NumberColor = color.New(color.FgYellow)
PathColor = color.New(color.FgBlue, color.Underline)
TimeColor = color.New(color.FgCyan)
)
// Logger defines the interface for logging
type Logger interface {
Debug(msg string, args ...any)
@@ -226,34 +246,32 @@ type CleanFormatter struct{}
func (f *CleanFormatter) Format(entry *logrus.Entry) ([]byte, error) {
timestamp := entry.Time.Format("2006-01-02T15:04:05")
// Color codes for different log levels
var levelColor, levelText string
// Get level color and text using fatih/color
var levelPrinter *color.Color
var levelText string
switch entry.Level {
case logrus.DebugLevel:
levelColor = "\033[36m" // Cyan
levelPrinter = DebugColor
levelText = "DEBUG"
case logrus.InfoLevel:
levelColor = "\033[32m" // Green
levelPrinter = SuccessColor
levelText = "INFO "
case logrus.WarnLevel:
levelColor = "\033[33m" // Yellow
levelPrinter = WarnColor
levelText = "WARN "
case logrus.ErrorLevel:
levelColor = "\033[31m" // Red
levelPrinter = ErrorColor
levelText = "ERROR"
default:
levelColor = "\033[0m" // Reset
levelPrinter = InfoColor
levelText = "INFO "
}
resetColor := "\033[0m"
// Build the message with perfectly aligned columns
var output strings.Builder
// Column 1: Level (with color, fixed width 5 chars)
output.WriteString(levelColor)
output.WriteString(levelText)
output.WriteString(resetColor)
output.WriteString(levelPrinter.Sprint(levelText))
output.WriteString(" ")
// Column 2: Timestamp (fixed format)

View File

@@ -7,9 +7,17 @@ import (
"strings"
"time"
"github.com/fatih/color"
"github.com/schollz/progressbar/v3"
)
// Color printers for progress indicators
var (
okColor = color.New(color.FgGreen, color.Bold)
failColor = color.New(color.FgRed, color.Bold)
warnColor = color.New(color.FgYellow, color.Bold)
)
// Indicator represents a progress indicator interface
type Indicator interface {
Start(message string)
@@ -94,13 +102,15 @@ func (s *Spinner) Update(message string) {
// Complete stops the spinner with a success message
func (s *Spinner) Complete(message string) {
s.Stop()
fmt.Fprintf(s.writer, "\n[OK] %s\n", message)
okColor.Fprint(s.writer, "[OK] ")
fmt.Fprintln(s.writer, message)
}
// Fail stops the spinner with a failure message
func (s *Spinner) Fail(message string) {
s.Stop()
fmt.Fprintf(s.writer, "\n[FAIL] %s\n", message)
failColor.Fprint(s.writer, "[FAIL] ")
fmt.Fprintln(s.writer, message)
}
// Stop stops the spinner
@@ -169,13 +179,15 @@ func (d *Dots) Update(message string) {
// Complete stops the dots with a success message
func (d *Dots) Complete(message string) {
d.Stop()
fmt.Fprintf(d.writer, " [OK] %s\n", message)
okColor.Fprint(d.writer, " [OK] ")
fmt.Fprintln(d.writer, message)
}
// Fail stops the dots with a failure message
func (d *Dots) Fail(message string) {
d.Stop()
fmt.Fprintf(d.writer, " [FAIL] %s\n", message)
failColor.Fprint(d.writer, " [FAIL] ")
fmt.Fprintln(d.writer, message)
}
// Stop stops the dots indicator
@@ -241,14 +253,16 @@ func (p *ProgressBar) Complete(message string) {
p.current = p.total
p.message = message
p.render()
fmt.Fprintf(p.writer, " [OK] %s\n", message)
okColor.Fprint(p.writer, " [OK] ")
fmt.Fprintln(p.writer, message)
p.Stop()
}
// Fail stops the progress bar with failure
func (p *ProgressBar) Fail(message string) {
p.render()
fmt.Fprintf(p.writer, " [FAIL] %s\n", message)
failColor.Fprint(p.writer, " [FAIL] ")
fmt.Fprintln(p.writer, message)
p.Stop()
}
@@ -300,12 +314,14 @@ func (s *Static) Update(message string) {
// Complete shows completion message
func (s *Static) Complete(message string) {
fmt.Fprintf(s.writer, " [OK] %s\n", message)
okColor.Fprint(s.writer, " [OK] ")
fmt.Fprintln(s.writer, message)
}
// Fail shows failure message
func (s *Static) Fail(message string) {
fmt.Fprintf(s.writer, " [FAIL] %s\n", message)
failColor.Fprint(s.writer, " [FAIL] ")
fmt.Fprintln(s.writer, message)
}
// Stop does nothing for static indicator
@@ -382,12 +398,14 @@ func (l *LineByLine) SetEstimator(estimator *ETAEstimator) {
// Complete shows completion message
func (l *LineByLine) Complete(message string) {
fmt.Fprintf(l.writer, "[OK] %s\n\n", message)
okColor.Fprint(l.writer, "[OK] ")
fmt.Fprintf(l.writer, "%s\n\n", message)
}
// Fail shows failure message
func (l *LineByLine) Fail(message string) {
fmt.Fprintf(l.writer, "[FAIL] %s\n\n", message)
failColor.Fprint(l.writer, "[FAIL] ")
fmt.Fprintf(l.writer, "%s\n\n", message)
}
// Stop does nothing for line-by-line (no cleanup needed)
@@ -410,13 +428,15 @@ func (l *Light) Update(message string) {
func (l *Light) Complete(message string) {
if !l.silent {
fmt.Fprintf(l.writer, "[OK] %s\n", message)
okColor.Fprint(l.writer, "[OK] ")
fmt.Fprintln(l.writer, message)
}
}
func (l *Light) Fail(message string) {
if !l.silent {
fmt.Fprintf(l.writer, "[FAIL] %s\n", message)
failColor.Fprint(l.writer, "[FAIL] ")
fmt.Fprintln(l.writer, message)
}
}
@@ -594,13 +614,15 @@ func (s *SchollzBar) ChangeMax64(max int64) {
// Complete finishes with success (Indicator interface)
func (s *SchollzBar) Complete(message string) {
_ = s.bar.Finish()
fmt.Printf("\n[green][OK][reset] %s\n", message)
okColor.Print("[OK] ")
fmt.Println(message)
}
// Fail finishes with failure (Indicator interface)
func (s *SchollzBar) Fail(message string) {
_ = s.bar.Clear()
fmt.Printf("\n[red][FAIL][reset] %s\n", message)
failColor.Print("[FAIL] ")
fmt.Println(message)
}
// Stop stops the progress bar (Indicator interface)