1616 DimStyle = lipgloss .NewStyle ().Foreground (lipgloss .Color ("8" ))
1717 StatStyle = lipgloss .NewStyle ().Bold (true ).Foreground (lipgloss .Color ("14" ))
1818 WarnStyle = lipgloss .NewStyle ().Foreground (lipgloss .Color ("11" ))
19+ GreenStyle = lipgloss .NewStyle ().Foreground (lipgloss .Color ("10" ))
20+ YellowStyle = lipgloss .NewStyle ().Foreground (lipgloss .Color ("11" ))
21+ RedStyle = lipgloss .NewStyle ().Foreground (lipgloss .Color ("9" ))
1922)
2023
2124// IsTerminal returns true if stdout is a TTY.
@@ -45,21 +48,151 @@ func FormatSeparator(width int) string {
4548 return strings .Repeat ("═" , width )
4649}
4750
51+ // ColorSavings returns a savings percentage string colored by tier.
52+ // ≥70% green, 30-70% yellow, <30% red.
53+ func ColorSavings (pct float64 ) string {
54+ text := fmt .Sprintf ("%.1f%%" , pct )
55+ if ! IsTerminal () {
56+ return text
57+ }
58+ switch {
59+ case pct >= 70 :
60+ return GreenStyle .Render (text )
61+ case pct >= 30 :
62+ return YellowStyle .Render (text )
63+ default :
64+ return RedStyle .Render (text )
65+ }
66+ }
67+
68+ // TierLabel returns an efficiency tier label based on savings percentage.
69+ func TierLabel (pct float64 ) string {
70+ switch {
71+ case pct >= 90 :
72+ return "Elite"
73+ case pct >= 70 :
74+ return "Great"
75+ case pct >= 50 :
76+ return "Good"
77+ case pct >= 30 :
78+ return "Fair"
79+ default :
80+ return "Low"
81+ }
82+ }
83+
84+ // ColorTier returns a tier label colored by level.
85+ func ColorTier (tier string ) string {
86+ if ! IsTerminal () {
87+ return tier
88+ }
89+ switch tier {
90+ case "Elite" :
91+ return GreenStyle .Bold (true ).Render (tier )
92+ case "Great" :
93+ return GreenStyle .Render (tier )
94+ case "Good" :
95+ return YellowStyle .Render (tier )
96+ case "Fair" :
97+ return WarnStyle .Render (tier )
98+ default :
99+ return RedStyle .Render (tier )
100+ }
101+ }
102+
103+ // ColorBar returns a colored impact bar (green filled, dim empty).
104+ // Guarantees at least 1 filled block when value > 0.
105+ func ColorBar (value , maxVal , width int ) string {
106+ if maxVal <= 0 || width <= 0 {
107+ return strings .Repeat ("░" , width )
108+ }
109+ filled := min (max (value * width / maxVal , 0 ), width )
110+ if filled == 0 && value > 0 {
111+ filled = 1
112+ }
113+ filledStr := strings .Repeat ("█" , filled )
114+ emptyStr := strings .Repeat ("░" , width - filled )
115+ if ! IsTerminal () {
116+ return filledStr + emptyStr
117+ }
118+ return GreenStyle .Render (filledStr ) + DimStyle .Render (emptyStr )
119+ }
120+
121+ // FormatBar renders a horizontal bar proportional to value/maxVal.
122+ func FormatBar (value , maxVal , width int ) string {
123+ if maxVal <= 0 || width <= 0 {
124+ return strings .Repeat ("░" , width )
125+ }
126+ filled := min (max (value * width / maxVal , 0 ), width )
127+ return strings .Repeat ("█" , filled ) + strings .Repeat ("░" , width - filled )
128+ }
129+
130+ // FormatSparkline renders a sparkline from a slice of values.
131+ // Uses Unicode block characters ▁▂▃▄▅▆▇█.
132+ func FormatSparkline (values []float64 ) string {
133+ blocks := []rune ("▁▂▃▄▅▆▇█" )
134+
135+ if len (values ) == 0 {
136+ return ""
137+ }
138+
139+ max := values [0 ]
140+ for _ , v := range values [1 :] {
141+ if v > max {
142+ max = v
143+ }
144+ }
145+
146+ var b strings.Builder
147+ for _ , v := range values {
148+ idx := 0
149+ if max > 0 {
150+ idx = int (v / max * float64 (len (blocks )- 1 ))
151+ }
152+ if idx >= len (blocks ) {
153+ idx = len (blocks ) - 1
154+ }
155+ if idx < 0 {
156+ idx = 0
157+ }
158+ b .WriteRune (blocks [idx ])
159+ }
160+ return b .String ()
161+ }
162+
163+ // visualWidth returns the visible width of a string, ignoring ANSI escape codes.
164+ func visualWidth (s string ) int {
165+ return lipgloss .Width (s )
166+ }
167+
168+ // padRight pads a string to the target visual width with spaces,
169+ // correctly handling ANSI escape codes.
170+ func padRight (s string , targetWidth int ) string {
171+ vw := visualWidth (s )
172+ if vw >= targetWidth {
173+ return s
174+ }
175+ return s + strings .Repeat (" " , targetWidth - vw )
176+ }
177+
48178// FormatTable formats data as a simple aligned table.
179+ // Handles ANSI-colored cells correctly for alignment.
49180func FormatTable (headers []string , rows [][]string ) string {
50181 if len (headers ) == 0 {
51182 return ""
52183 }
53184
54- // Calculate column widths
185+ // Calculate column widths using visual width (ANSI-safe)
55186 widths := make ([]int , len (headers ))
56187 for i , h := range headers {
57- widths [i ] = len (h )
188+ widths [i ] = visualWidth (h )
58189 }
59190 for _ , row := range rows {
60191 for i , cell := range row {
61- if i < len (widths ) && len (cell ) > widths [i ] {
62- widths [i ] = len (cell )
192+ if i < len (widths ) {
193+ if w := visualWidth (cell ); w > widths [i ] {
194+ widths [i ] = w
195+ }
63196 }
64197 }
65198 }
@@ -71,7 +204,7 @@ func FormatTable(headers []string, rows [][]string) string {
71204 if i > 0 {
72205 b .WriteString (" " )
73206 }
74- fmt . Fprintf ( & b , "%-*s" , widths [i ], h )
207+ b . WriteString ( padRight ( h , widths [i ]) )
75208 }
76209 b .WriteString ("\n " )
77210
@@ -91,7 +224,7 @@ func FormatTable(headers []string, rows [][]string) string {
91224 b .WriteString (" " )
92225 }
93226 if i < len (widths ) {
94- fmt . Fprintf ( & b , "%-*s" , widths [i ], cell )
227+ b . WriteString ( padRight ( cell , widths [i ]) )
95228 }
96229 }
97230 b .WriteString ("\n " )
0 commit comments