Files
cburn/internal/tui/components/progress.go
teernisse c15dc8b487 feat(tui): polish components with icons, gradients, and proper backgrounds
Comprehensive visual refresh of all TUI components:

card.go:
- Add semantic icons based on metric type (tokens=◈, sessions=◉,
  cost=◆, cache=◇)
- Color-code metric values using new theme colors (Cyan, Magenta,
  Green, Blue) for visual variety
- Add CardRow() helper that properly height-equalizes cards and
  fills background on shorter cards to prevent "punched out" gaps
- Set explicit background on all style components

chart.go:
- Add background styling to sparkline renderer
- Ensure bar chart respects theme.Active.Surface background

progress.go:
- Add color gradient based on progress (Cyan→Accent→AccentBright)
- Style percentage text with bold and matching color
- Fix background fill on empty bar segments

statusbar.go:
- Complete redesign with SurfaceHover background
- Style keyboard hints: dim brackets, bright keys, muted labels
- Proper spacing and background continuity across sections
- Styled refresh indicator with AccentBright

tabbar.go:
- Add TabVisualWidth() for accurate mouse hit detection
- Modern underline-style active indicator using ━ characters
- AccentBright for active tab, proper dim styling for inactive
- Consistent Surface background across all tab elements

These changes create a cohesive visual language where every element
respects the dark background, icons add visual interest without
clutter, and color coding provides semantic meaning.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-28 00:05:39 -05:00

153 lines
3.8 KiB
Go

package components
import (
"fmt"
"strings"
"time"
"github.com/theirongolddev/cburn/internal/tui/theme"
"github.com/charmbracelet/bubbles/progress"
"github.com/charmbracelet/lipgloss"
)
// ProgressBar renders a visually appealing progress bar with percentage.
func ProgressBar(pct float64, width int) string {
t := theme.Active
filled := int(pct * float64(width))
if filled > width {
filled = width
}
if filled < 0 {
filled = 0
}
// Color gradient based on progress
var barColor lipgloss.Color
switch {
case pct >= 0.8:
barColor = t.AccentBright
case pct >= 0.5:
barColor = t.Accent
default:
barColor = t.Cyan
}
filledStyle := lipgloss.NewStyle().Foreground(barColor).Background(t.Surface)
emptyStyle := lipgloss.NewStyle().Foreground(t.TextDim).Background(t.Surface)
pctStyle := lipgloss.NewStyle().Foreground(barColor).Background(t.Surface).Bold(true)
spaceStyle := lipgloss.NewStyle().Background(t.Surface)
var b strings.Builder
b.WriteString(filledStyle.Render(strings.Repeat("█", filled)))
b.WriteString(emptyStyle.Render(strings.Repeat("░", width-filled)))
return b.String() + spaceStyle.Render(" ") + pctStyle.Render(fmt.Sprintf("%.0f%%", pct*100))
}
// ColorForPct returns green/yellow/orange/red based on utilization level.
func ColorForPct(pct float64) string {
t := theme.Active
switch {
case pct >= 0.9:
return string(t.Red)
case pct >= 0.7:
return string(t.Orange)
case pct >= 0.5:
return string(t.Yellow)
default:
return string(t.Green)
}
}
// RateLimitBar renders a labeled progress bar with percentage and countdown.
func RateLimitBar(label string, pct float64, resetsAt time.Time, labelW, barWidth int) string {
t := theme.Active
if pct < 0 {
pct = 0
}
if pct > 1 {
pct = 1
}
bar := progress.New(
progress.WithSolidFill(ColorForPct(pct)),
progress.WithWidth(barWidth),
progress.WithoutPercentage(),
)
bar.EmptyColor = string(t.TextDim)
labelStyle := lipgloss.NewStyle().Foreground(t.TextMuted).Background(t.Surface)
pctStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(ColorForPct(pct))).Background(t.Surface).Bold(true)
countdownStyle := lipgloss.NewStyle().Foreground(t.TextDim).Background(t.Surface)
spaceStyle := lipgloss.NewStyle().Background(t.Surface)
pctStr := fmt.Sprintf("%3.0f%%", pct*100)
countdown := ""
if !resetsAt.IsZero() {
dur := time.Until(resetsAt)
if dur > 0 {
countdown = formatCountdown(dur)
} else {
countdown = "now"
}
}
return labelStyle.Render(fmt.Sprintf("%-*s", labelW, label)) +
spaceStyle.Render(" ") +
bar.ViewAs(pct) +
spaceStyle.Render(" ") +
pctStyle.Render(pctStr) +
spaceStyle.Render(" ") +
countdownStyle.Render(countdown)
}
// CompactRateBar renders a tiny status-bar-sized rate indicator.
func CompactRateBar(label string, pct float64, width int) string {
t := theme.Active
if pct < 0 {
pct = 0
}
if pct > 1 {
pct = 1
}
barW := width - lipgloss.Width(label) - 6
if barW < 4 {
barW = 4
}
bar := progress.New(
progress.WithSolidFill(ColorForPct(pct)),
progress.WithWidth(barW),
progress.WithoutPercentage(),
)
bar.EmptyColor = string(t.TextDim)
pctStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(ColorForPct(pct))).Background(t.Surface).Bold(true)
labelStyle := lipgloss.NewStyle().Foreground(t.TextMuted).Background(t.Surface)
spaceStyle := lipgloss.NewStyle().Background(t.Surface)
return labelStyle.Render(label) +
spaceStyle.Render(" ") +
bar.ViewAs(pct) +
spaceStyle.Render(" ") +
pctStyle.Render(fmt.Sprintf("%2.0f%%", pct*100))
}
func formatCountdown(d time.Duration) string {
h := int(d.Hours())
m := int(d.Minutes()) % 60
if h >= 24 {
days := h / 24
hours := h % 24
return fmt.Sprintf("%dd %dh", days, hours)
}
if h > 0 {
return fmt.Sprintf("%dh %dm", h, m)
}
return fmt.Sprintf("%dm", m)
}