From 5de02efd824db28cb1f000a0ccd740de37356e4a Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 31 Jan 2026 22:53:34 +0000 Subject: [PATCH] feat(cli): add zero-dependency ANSI styling Replaces lipgloss with ~100 lines of owned code. Supports bold, dim, italic, underline, RGB/hex colors. Co-Authored-By: Claude Opus 4.5 --- pkg/cli/ansi.go | 123 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 pkg/cli/ansi.go diff --git a/pkg/cli/ansi.go b/pkg/cli/ansi.go new file mode 100644 index 0000000..5a28a06 --- /dev/null +++ b/pkg/cli/ansi.go @@ -0,0 +1,123 @@ +package cli + +import ( + "fmt" + "strconv" + "strings" +) + +// ANSI escape codes +const ( + ansiReset = "\033[0m" + ansiBold = "\033[1m" + ansiDim = "\033[2m" + ansiItalic = "\033[3m" + ansiUnderline = "\033[4m" +) + +// AnsiStyle represents terminal text styling. +// Use NewStyle() to create, chain methods, call Render(). +type AnsiStyle struct { + bold bool + dim bool + italic bool + underline bool + fg string + bg string +} + +// NewStyle creates a new empty style. +func NewStyle() *AnsiStyle { + return &AnsiStyle{} +} + +// Bold enables bold text. +func (s *AnsiStyle) Bold() *AnsiStyle { + s.bold = true + return s +} + +// Dim enables dim text. +func (s *AnsiStyle) Dim() *AnsiStyle { + s.dim = true + return s +} + +// Italic enables italic text. +func (s *AnsiStyle) Italic() *AnsiStyle { + s.italic = true + return s +} + +// Underline enables underlined text. +func (s *AnsiStyle) Underline() *AnsiStyle { + s.underline = true + return s +} + +// Foreground sets foreground color from hex string. +func (s *AnsiStyle) Foreground(hex string) *AnsiStyle { + s.fg = fgColorHex(hex) + return s +} + +// Background sets background color from hex string. +func (s *AnsiStyle) Background(hex string) *AnsiStyle { + s.bg = bgColorHex(hex) + return s +} + +// Render applies the style to text. +func (s *AnsiStyle) Render(text string) string { + if s == nil { + return text + } + + var codes []string + if s.bold { + codes = append(codes, ansiBold) + } + if s.dim { + codes = append(codes, ansiDim) + } + if s.italic { + codes = append(codes, ansiItalic) + } + if s.underline { + codes = append(codes, ansiUnderline) + } + if s.fg != "" { + codes = append(codes, s.fg) + } + if s.bg != "" { + codes = append(codes, s.bg) + } + + if len(codes) == 0 { + return text + } + + return strings.Join(codes, "") + text + ansiReset +} + +// Hex color support +func fgColorHex(hex string) string { + r, g, b := hexToRGB(hex) + return fmt.Sprintf("\033[38;2;%d;%d;%dm", r, g, b) +} + +func bgColorHex(hex string) string { + r, g, b := hexToRGB(hex) + return fmt.Sprintf("\033[48;2;%d;%d;%dm", r, g, b) +} + +func hexToRGB(hex string) (int, int, int) { + hex = strings.TrimPrefix(hex, "#") + if len(hex) != 6 { + return 255, 255, 255 + } + r, _ := strconv.ParseInt(hex[0:2], 16, 64) + g, _ := strconv.ParseInt(hex[2:4], 16, 64) + b, _ := strconv.ParseInt(hex[4:6], 16, 64) + return int(r), int(g), int(b) +}