refactor: simplify TUI theme struct and extract chart/progress components
Theme simplification (theme/theme.go): - Remove Purple and BorderHover fields from Theme struct — neither was referenced in the new tab renderers, reducing per-theme boilerplate from 14 to 12 color definitions Component extraction (components/): - Move Sparkline() and BarChart() from card.go to new chart.go, giving visualization components their own file as complexity grows - card.go retains MetricCard, ContentCard, LayoutRow, and CardInnerWidth which are layout-focused - New chart.go: Sparkline (unicode block chars) and BarChart (multi-row with anchored Y-axis, optional X-axis labels, dynamic height/width) - New progress.go: ProgressBar component with customizable width, color, and percentage display — used by rate-limit and budget views Status bar and tab bar updates: - statusbar.go: adapt to simplified theme struct - tabbar.go: adapt to simplified theme struct
This commit is contained in:
@@ -1,10 +1,7 @@
|
|||||||
|
// Package components provides reusable TUI widgets for the cburn dashboard.
|
||||||
package components
|
package components
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"cburn/internal/tui/theme"
|
"cburn/internal/tui/theme"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
@@ -126,312 +123,3 @@ func CardInnerWidth(outerWidth int) int {
|
|||||||
}
|
}
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sparkline renders a unicode sparkline from values.
|
|
||||||
func Sparkline(values []float64, color lipgloss.Color) string {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
blocks := []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
|
||||||
|
|
||||||
max := values[0]
|
|
||||||
for _, v := range values[1:] {
|
|
||||||
if v > max {
|
|
||||||
max = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if max == 0 {
|
|
||||||
max = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
style := lipgloss.NewStyle().Foreground(color)
|
|
||||||
|
|
||||||
var result string
|
|
||||||
for _, v := range values {
|
|
||||||
idx := int(v / max * float64(len(blocks)-1))
|
|
||||||
if idx >= len(blocks) {
|
|
||||||
idx = len(blocks) - 1
|
|
||||||
}
|
|
||||||
if idx < 0 {
|
|
||||||
idx = 0
|
|
||||||
}
|
|
||||||
result += string(blocks[idx])
|
|
||||||
}
|
|
||||||
|
|
||||||
return style.Render(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BarChart renders a multi-row bar chart with anchored Y-axis and optional X-axis labels.
|
|
||||||
// labels (if non-nil) should correspond 1:1 with values for x-axis display.
|
|
||||||
// height is a target; actual height adjusts slightly so Y-axis ticks are evenly spaced.
|
|
||||||
func BarChart(values []float64, labels []string, color lipgloss.Color, width, height int) string {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if width < 15 || height < 3 {
|
|
||||||
return Sparkline(values, color)
|
|
||||||
}
|
|
||||||
|
|
||||||
t := theme.Active
|
|
||||||
|
|
||||||
// Find max value
|
|
||||||
maxVal := 0.0
|
|
||||||
for _, v := range values {
|
|
||||||
if v > maxVal {
|
|
||||||
maxVal = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if maxVal == 0 {
|
|
||||||
maxVal = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Y-axis: compute tick step and ceiling, then fit within requested height.
|
|
||||||
// Each interval needs at least 2 rows for readable spacing, so
|
|
||||||
// maxIntervals = height/2. If the initial step gives too many intervals,
|
|
||||||
// double it until they fit.
|
|
||||||
tickStep := chartTickStep(maxVal)
|
|
||||||
maxIntervals := height / 2
|
|
||||||
if maxIntervals < 2 {
|
|
||||||
maxIntervals = 2
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
n := int(math.Ceil(maxVal / tickStep))
|
|
||||||
if n <= maxIntervals {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
tickStep *= 2
|
|
||||||
}
|
|
||||||
ceiling := math.Ceil(maxVal/tickStep) * tickStep
|
|
||||||
numIntervals := int(math.Round(ceiling / tickStep))
|
|
||||||
if numIntervals < 1 {
|
|
||||||
numIntervals = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Each interval gets the same number of rows; chart height is an exact multiple.
|
|
||||||
rowsPerTick := height / numIntervals
|
|
||||||
if rowsPerTick < 2 {
|
|
||||||
rowsPerTick = 2
|
|
||||||
}
|
|
||||||
chartH := rowsPerTick * numIntervals
|
|
||||||
|
|
||||||
// Pre-compute tick labels at evenly-spaced row positions
|
|
||||||
yLabelW := len(formatChartLabel(ceiling)) + 1
|
|
||||||
if yLabelW < 4 {
|
|
||||||
yLabelW = 4
|
|
||||||
}
|
|
||||||
tickLabels := make(map[int]string)
|
|
||||||
for i := 1; i <= numIntervals; i++ {
|
|
||||||
row := i * rowsPerTick
|
|
||||||
tickLabels[row] = formatChartLabel(tickStep * float64(i))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chart area width (excluding y-axis label and axis line char)
|
|
||||||
chartW := width - yLabelW - 1
|
|
||||||
if chartW < 5 {
|
|
||||||
chartW = 5
|
|
||||||
}
|
|
||||||
|
|
||||||
n := len(values)
|
|
||||||
|
|
||||||
// Bar sizing: always use 1-char gaps, target barW >= 2.
|
|
||||||
// If bars don't fit at width 2, subsample to fewer bars.
|
|
||||||
gap := 1
|
|
||||||
if n <= 1 {
|
|
||||||
gap = 0
|
|
||||||
}
|
|
||||||
barW := 2
|
|
||||||
if n > 1 {
|
|
||||||
barW = (chartW - (n - 1)) / n
|
|
||||||
} else if n == 1 {
|
|
||||||
barW = chartW
|
|
||||||
}
|
|
||||||
if barW < 2 && n > 1 {
|
|
||||||
// Subsample so bars fit at width 2 with 1-char gaps
|
|
||||||
maxN := (chartW + 1) / 3 // each bar = 2 chars + 1 gap (last bar no gap)
|
|
||||||
if maxN < 2 {
|
|
||||||
maxN = 2
|
|
||||||
}
|
|
||||||
sampled := make([]float64, maxN)
|
|
||||||
var sampledLabels []string
|
|
||||||
if len(labels) == n {
|
|
||||||
sampledLabels = make([]string, maxN)
|
|
||||||
}
|
|
||||||
for i := range sampled {
|
|
||||||
srcIdx := i * (n - 1) / (maxN - 1)
|
|
||||||
sampled[i] = values[srcIdx]
|
|
||||||
if sampledLabels != nil {
|
|
||||||
sampledLabels[i] = labels[srcIdx]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
values = sampled
|
|
||||||
labels = sampledLabels
|
|
||||||
n = maxN
|
|
||||||
barW = 2
|
|
||||||
}
|
|
||||||
if barW > 6 {
|
|
||||||
barW = 6
|
|
||||||
}
|
|
||||||
axisLen := n*barW + max(0, n-1)*gap
|
|
||||||
|
|
||||||
blocks := []rune{' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
|
||||||
barStyle := lipgloss.NewStyle().Foreground(color)
|
|
||||||
axisStyle := lipgloss.NewStyle().Foreground(t.TextDim)
|
|
||||||
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
// Render rows top to bottom using chartH (aligned to tick intervals)
|
|
||||||
for row := chartH; row >= 1; row-- {
|
|
||||||
rowTop := ceiling * float64(row) / float64(chartH)
|
|
||||||
rowBottom := ceiling * float64(row-1) / float64(chartH)
|
|
||||||
|
|
||||||
label := tickLabels[row]
|
|
||||||
b.WriteString(axisStyle.Render(fmt.Sprintf("%*s", yLabelW, label)))
|
|
||||||
b.WriteString(axisStyle.Render("│"))
|
|
||||||
|
|
||||||
for i, v := range values {
|
|
||||||
if i > 0 && gap > 0 {
|
|
||||||
b.WriteString(strings.Repeat(" ", gap))
|
|
||||||
}
|
|
||||||
if v >= rowTop {
|
|
||||||
b.WriteString(barStyle.Render(strings.Repeat("█", barW)))
|
|
||||||
} else if v > rowBottom {
|
|
||||||
frac := (v - rowBottom) / (rowTop - rowBottom)
|
|
||||||
idx := int(frac * 8)
|
|
||||||
if idx > 8 {
|
|
||||||
idx = 8
|
|
||||||
}
|
|
||||||
if idx < 1 {
|
|
||||||
idx = 1
|
|
||||||
}
|
|
||||||
b.WriteString(barStyle.Render(strings.Repeat(string(blocks[idx]), barW)))
|
|
||||||
} else {
|
|
||||||
b.WriteString(strings.Repeat(" ", barW))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// X-axis line with 0 label
|
|
||||||
b.WriteString(axisStyle.Render(fmt.Sprintf("%*s", yLabelW, "0")))
|
|
||||||
b.WriteString(axisStyle.Render("└"))
|
|
||||||
b.WriteString(axisStyle.Render(strings.Repeat("─", axisLen)))
|
|
||||||
|
|
||||||
// X-axis labels
|
|
||||||
if len(labels) == n && n > 0 {
|
|
||||||
buf := make([]byte, axisLen)
|
|
||||||
for i := range buf {
|
|
||||||
buf[i] = ' '
|
|
||||||
}
|
|
||||||
|
|
||||||
// Place labels at bar start positions, skip overlaps
|
|
||||||
minSpacing := 8
|
|
||||||
labelStep := max(1, (n*minSpacing)/(axisLen+1))
|
|
||||||
|
|
||||||
lastEnd := -1
|
|
||||||
for i := 0; i < n; i += labelStep {
|
|
||||||
pos := i * (barW + gap)
|
|
||||||
lbl := labels[i]
|
|
||||||
end := pos + len(lbl)
|
|
||||||
if pos <= lastEnd {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if end > axisLen {
|
|
||||||
end = axisLen
|
|
||||||
if end-pos < 3 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lbl = lbl[:end-pos]
|
|
||||||
}
|
|
||||||
copy(buf[pos:end], lbl)
|
|
||||||
lastEnd = end + 1
|
|
||||||
}
|
|
||||||
// Always place the last label, right-aligned to axis edge if needed.
|
|
||||||
// Overwrites any truncated label underneath.
|
|
||||||
if n > 1 && len(labels[n-1]) <= axisLen {
|
|
||||||
lbl := labels[n-1]
|
|
||||||
pos := axisLen - len(lbl)
|
|
||||||
end := axisLen
|
|
||||||
// Clear the area first in case a truncated label is there
|
|
||||||
for j := pos; j < end; j++ {
|
|
||||||
buf[j] = ' '
|
|
||||||
}
|
|
||||||
copy(buf[pos:end], lbl)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(strings.Repeat(" ", yLabelW+1))
|
|
||||||
b.WriteString(axisStyle.Render(strings.TrimRight(string(buf), " ")))
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// chartTickStep computes a nice tick interval targeting ~5 ticks.
|
|
||||||
func chartTickStep(maxVal float64) float64 {
|
|
||||||
if maxVal <= 0 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
rough := maxVal / 5
|
|
||||||
exp := math.Floor(math.Log10(rough))
|
|
||||||
base := math.Pow(10, exp)
|
|
||||||
frac := rough / base
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case frac < 1.5:
|
|
||||||
return base
|
|
||||||
case frac < 3.5:
|
|
||||||
return 2 * base
|
|
||||||
default:
|
|
||||||
return 5 * base
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatChartLabel(v float64) string {
|
|
||||||
switch {
|
|
||||||
case v >= 1e9:
|
|
||||||
if v == math.Trunc(v/1e9)*1e9 {
|
|
||||||
return fmt.Sprintf("%.0fB", v/1e9)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.1fB", v/1e9)
|
|
||||||
case v >= 1e6:
|
|
||||||
if v == math.Trunc(v/1e6)*1e6 {
|
|
||||||
return fmt.Sprintf("%.0fM", v/1e6)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.1fM", v/1e6)
|
|
||||||
case v >= 1e3:
|
|
||||||
if v == math.Trunc(v/1e3)*1e3 {
|
|
||||||
return fmt.Sprintf("%.0fk", v/1e3)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.1fk", v/1e3)
|
|
||||||
case v >= 1:
|
|
||||||
return fmt.Sprintf("%.0f", v)
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%.2f", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProgressBar renders a colored progress bar.
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
filledStyle := lipgloss.NewStyle().Foreground(t.Accent)
|
|
||||||
emptyStyle := lipgloss.NewStyle().Foreground(t.TextDim)
|
|
||||||
|
|
||||||
bar := ""
|
|
||||||
for i := 0; i < filled; i++ {
|
|
||||||
bar += filledStyle.Render("█")
|
|
||||||
}
|
|
||||||
for i := filled; i < width; i++ {
|
|
||||||
bar += emptyStyle.Render("░")
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s %.1f%%", bar, pct*100)
|
|
||||||
}
|
|
||||||
|
|||||||
303
internal/tui/components/chart.go
Normal file
303
internal/tui/components/chart.go
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"cburn/internal/tui/theme"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sparkline renders a unicode sparkline from values.
|
||||||
|
func Sparkline(values []float64, color lipgloss.Color) string {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks := []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
||||||
|
|
||||||
|
peak := values[0]
|
||||||
|
for _, v := range values[1:] {
|
||||||
|
if v > peak {
|
||||||
|
peak = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if peak == 0 {
|
||||||
|
peak = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
style := lipgloss.NewStyle().Foreground(color)
|
||||||
|
|
||||||
|
var buf strings.Builder
|
||||||
|
buf.Grow(len(values) * 4) // UTF-8 block chars are up to 3 bytes
|
||||||
|
for _, v := range values {
|
||||||
|
idx := int(v / peak * float64(len(blocks)-1))
|
||||||
|
if idx >= len(blocks) {
|
||||||
|
idx = len(blocks) - 1
|
||||||
|
}
|
||||||
|
if idx < 0 {
|
||||||
|
idx = 0
|
||||||
|
}
|
||||||
|
buf.WriteRune(blocks[idx]) //nolint:gosec // bounds checked above
|
||||||
|
}
|
||||||
|
|
||||||
|
return style.Render(buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// BarChart renders a multi-row bar chart with anchored Y-axis and optional X-axis labels.
|
||||||
|
// labels (if non-nil) should correspond 1:1 with values for x-axis display.
|
||||||
|
// height is a target; actual height adjusts slightly so Y-axis ticks are evenly spaced.
|
||||||
|
func BarChart(values []float64, labels []string, color lipgloss.Color, width, height int) string {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if width < 15 || height < 3 {
|
||||||
|
return Sparkline(values, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := theme.Active
|
||||||
|
|
||||||
|
// Find max value
|
||||||
|
maxVal := 0.0
|
||||||
|
for _, v := range values {
|
||||||
|
if v > maxVal {
|
||||||
|
maxVal = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if maxVal == 0 {
|
||||||
|
maxVal = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Y-axis: compute tick step and ceiling, then fit within requested height.
|
||||||
|
// Each interval needs at least 2 rows for readable spacing, so
|
||||||
|
// maxIntervals = height/2. If the initial step gives too many intervals,
|
||||||
|
// double it until they fit.
|
||||||
|
tickStep := chartTickStep(maxVal)
|
||||||
|
maxIntervals := height / 2
|
||||||
|
if maxIntervals < 2 {
|
||||||
|
maxIntervals = 2
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
n := int(math.Ceil(maxVal / tickStep))
|
||||||
|
if n <= maxIntervals {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
tickStep *= 2
|
||||||
|
}
|
||||||
|
ceiling := math.Ceil(maxVal/tickStep) * tickStep
|
||||||
|
numIntervals := int(math.Round(ceiling / tickStep))
|
||||||
|
if numIntervals < 1 {
|
||||||
|
numIntervals = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each interval gets the same number of rows; chart height is an exact multiple.
|
||||||
|
rowsPerTick := height / numIntervals
|
||||||
|
if rowsPerTick < 2 {
|
||||||
|
rowsPerTick = 2
|
||||||
|
}
|
||||||
|
chartH := rowsPerTick * numIntervals
|
||||||
|
|
||||||
|
// Pre-compute tick labels at evenly-spaced row positions
|
||||||
|
yLabelW := len(formatChartLabel(ceiling)) + 1
|
||||||
|
if yLabelW < 4 {
|
||||||
|
yLabelW = 4
|
||||||
|
}
|
||||||
|
tickLabels := make(map[int]string)
|
||||||
|
for i := 1; i <= numIntervals; i++ {
|
||||||
|
row := i * rowsPerTick
|
||||||
|
tickLabels[row] = formatChartLabel(tickStep * float64(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart area width (excluding y-axis label and axis line char)
|
||||||
|
chartW := width - yLabelW - 1
|
||||||
|
if chartW < 5 {
|
||||||
|
chartW = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
n := len(values)
|
||||||
|
|
||||||
|
// Bar sizing: always use 1-char gaps, target barW >= 2.
|
||||||
|
// If bars don't fit at width 2, subsample to fewer bars.
|
||||||
|
gap := 1
|
||||||
|
if n <= 1 {
|
||||||
|
gap = 0
|
||||||
|
}
|
||||||
|
barW := 2
|
||||||
|
if n > 1 {
|
||||||
|
barW = (chartW - (n - 1)) / n
|
||||||
|
} else if n == 1 {
|
||||||
|
barW = chartW
|
||||||
|
}
|
||||||
|
if barW < 2 && n > 1 {
|
||||||
|
// Subsample so bars fit at width 2 with 1-char gaps
|
||||||
|
maxN := (chartW + 1) / 3 // each bar = 2 chars + 1 gap (last bar no gap)
|
||||||
|
if maxN < 2 {
|
||||||
|
maxN = 2
|
||||||
|
}
|
||||||
|
sampled := make([]float64, maxN)
|
||||||
|
var sampledLabels []string
|
||||||
|
if len(labels) == n {
|
||||||
|
sampledLabels = make([]string, maxN)
|
||||||
|
}
|
||||||
|
for i := range sampled {
|
||||||
|
srcIdx := i * (n - 1) / (maxN - 1)
|
||||||
|
sampled[i] = values[srcIdx]
|
||||||
|
if sampledLabels != nil {
|
||||||
|
sampledLabels[i] = labels[srcIdx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values = sampled
|
||||||
|
labels = sampledLabels
|
||||||
|
n = maxN
|
||||||
|
barW = 2
|
||||||
|
}
|
||||||
|
if barW > 6 {
|
||||||
|
barW = 6
|
||||||
|
}
|
||||||
|
axisLen := n*barW + max(0, n-1)*gap
|
||||||
|
|
||||||
|
blocks := []rune{' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
||||||
|
barStyle := lipgloss.NewStyle().Foreground(color)
|
||||||
|
axisStyle := lipgloss.NewStyle().Foreground(t.TextDim)
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
// Render rows top to bottom using chartH (aligned to tick intervals)
|
||||||
|
for row := chartH; row >= 1; row-- {
|
||||||
|
rowTop := ceiling * float64(row) / float64(chartH)
|
||||||
|
rowBottom := ceiling * float64(row-1) / float64(chartH)
|
||||||
|
|
||||||
|
label := tickLabels[row]
|
||||||
|
b.WriteString(axisStyle.Render(fmt.Sprintf("%*s", yLabelW, label)))
|
||||||
|
b.WriteString(axisStyle.Render("│"))
|
||||||
|
|
||||||
|
for i, v := range values {
|
||||||
|
if i > 0 && gap > 0 {
|
||||||
|
b.WriteString(strings.Repeat(" ", gap))
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case v >= rowTop:
|
||||||
|
b.WriteString(barStyle.Render(strings.Repeat("\u2588", barW)))
|
||||||
|
case v > rowBottom:
|
||||||
|
frac := (v - rowBottom) / (rowTop - rowBottom)
|
||||||
|
idx := int(frac * 8)
|
||||||
|
if idx > 8 {
|
||||||
|
idx = 8
|
||||||
|
}
|
||||||
|
if idx < 1 {
|
||||||
|
idx = 1
|
||||||
|
}
|
||||||
|
b.WriteString(barStyle.Render(strings.Repeat(string(blocks[idx]), barW)))
|
||||||
|
default:
|
||||||
|
b.WriteString(strings.Repeat(" ", barW))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// X-axis line with 0 label
|
||||||
|
b.WriteString(axisStyle.Render(fmt.Sprintf("%*s", yLabelW, "0")))
|
||||||
|
b.WriteString(axisStyle.Render("└"))
|
||||||
|
b.WriteString(axisStyle.Render(strings.Repeat("─", axisLen)))
|
||||||
|
|
||||||
|
// X-axis labels
|
||||||
|
if len(labels) == n && n > 0 {
|
||||||
|
buf := make([]byte, axisLen)
|
||||||
|
for i := range buf {
|
||||||
|
buf[i] = ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place labels at bar start positions, skip overlaps
|
||||||
|
minSpacing := 8
|
||||||
|
labelStep := max(1, (n*minSpacing)/(axisLen+1))
|
||||||
|
|
||||||
|
lastEnd := -1
|
||||||
|
for i := 0; i < n; i += labelStep {
|
||||||
|
pos := i * (barW + gap)
|
||||||
|
lbl := labels[i]
|
||||||
|
end := pos + len(lbl)
|
||||||
|
if pos <= lastEnd {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if end > axisLen {
|
||||||
|
end = axisLen
|
||||||
|
if end-pos < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lbl = lbl[:end-pos]
|
||||||
|
}
|
||||||
|
copy(buf[pos:end], lbl)
|
||||||
|
lastEnd = end + 1
|
||||||
|
}
|
||||||
|
// Always attempt the last label (the loop may skip it due to labelStep).
|
||||||
|
// Right-align to axis edge if it would overflow.
|
||||||
|
if n > 1 {
|
||||||
|
lbl := labels[n-1]
|
||||||
|
pos := (n - 1) * (barW + gap)
|
||||||
|
end := pos + len(lbl)
|
||||||
|
if end > axisLen {
|
||||||
|
// Right-align: shift left so it ends at the axis edge
|
||||||
|
pos = axisLen - len(lbl)
|
||||||
|
end = axisLen
|
||||||
|
}
|
||||||
|
if pos >= 0 && pos > lastEnd {
|
||||||
|
for j := pos; j < end; j++ {
|
||||||
|
buf[j] = ' '
|
||||||
|
}
|
||||||
|
copy(buf[pos:end], lbl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(strings.Repeat(" ", yLabelW+1))
|
||||||
|
b.WriteString(axisStyle.Render(strings.TrimRight(string(buf), " ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// chartTickStep computes a nice tick interval targeting ~5 ticks.
|
||||||
|
func chartTickStep(maxVal float64) float64 {
|
||||||
|
if maxVal <= 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
rough := maxVal / 5
|
||||||
|
exp := math.Floor(math.Log10(rough))
|
||||||
|
base := math.Pow(10, exp)
|
||||||
|
frac := rough / base
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case frac < 1.5:
|
||||||
|
return base
|
||||||
|
case frac < 3.5:
|
||||||
|
return 2 * base
|
||||||
|
default:
|
||||||
|
return 5 * base
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatChartLabel(v float64) string {
|
||||||
|
switch {
|
||||||
|
case v >= 1e9:
|
||||||
|
if v == math.Trunc(v/1e9)*1e9 {
|
||||||
|
return fmt.Sprintf("%.0fB", v/1e9)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1fB", v/1e9)
|
||||||
|
case v >= 1e6:
|
||||||
|
if v == math.Trunc(v/1e6)*1e6 {
|
||||||
|
return fmt.Sprintf("%.0fM", v/1e6)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1fM", v/1e6)
|
||||||
|
case v >= 1e3:
|
||||||
|
if v == math.Trunc(v/1e3)*1e3 {
|
||||||
|
return fmt.Sprintf("%.0fk", v/1e3)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1fk", v/1e3)
|
||||||
|
case v >= 1:
|
||||||
|
return fmt.Sprintf("%.0f", v)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%.2f", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
142
internal/tui/components/progress.go
Normal file
142
internal/tui/components/progress.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cburn/internal/tui/theme"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/progress"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProgressBar renders a colored progress bar.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
filledStyle := lipgloss.NewStyle().Foreground(t.Accent)
|
||||||
|
emptyStyle := lipgloss.NewStyle().Foreground(t.TextDim)
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
for i := 0; i < filled; i++ {
|
||||||
|
b.WriteString(filledStyle.Render("\u2588"))
|
||||||
|
}
|
||||||
|
for i := filled; i < width; i++ {
|
||||||
|
b.WriteString(emptyStyle.Render("\u2591"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s %.1f%%", b.String(), pct*100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorForPct returns green/yellow/red based on utilization level.
|
||||||
|
func ColorForPct(pct float64) string {
|
||||||
|
t := theme.Active
|
||||||
|
switch {
|
||||||
|
case pct >= 0.8:
|
||||||
|
return string(t.Red)
|
||||||
|
case pct >= 0.5:
|
||||||
|
return string(t.Orange)
|
||||||
|
default:
|
||||||
|
return string(t.Green)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitBar renders a labeled progress bar with percentage and countdown.
|
||||||
|
// label: "5-hour", "Weekly", etc. pct: 0.0-1.0. resetsAt: zero means no countdown.
|
||||||
|
// barWidth: width allocated for the progress bar portion only.
|
||||||
|
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)
|
||||||
|
pctStyle := lipgloss.NewStyle().Foreground(t.TextPrimary).Bold(true)
|
||||||
|
countdownStyle := lipgloss.NewStyle().Foreground(t.TextDim)
|
||||||
|
|
||||||
|
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 fmt.Sprintf("%s %s %s %s",
|
||||||
|
labelStyle.Render(fmt.Sprintf("%-*s", labelW, label)),
|
||||||
|
bar.ViewAs(pct),
|
||||||
|
pctStyle.Render(pctStr),
|
||||||
|
countdownStyle.Render(countdown),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompactRateBar renders a tiny status-bar-sized rate indicator.
|
||||||
|
// Example output: "5h ████░░░░ 42%"
|
||||||
|
func CompactRateBar(label string, pct float64, width int) string {
|
||||||
|
t := theme.Active
|
||||||
|
|
||||||
|
if pct < 0 {
|
||||||
|
pct = 0
|
||||||
|
}
|
||||||
|
if pct > 1 {
|
||||||
|
pct = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// label + space + bar + space + pct(4 chars)
|
||||||
|
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)))
|
||||||
|
labelStyle := lipgloss.NewStyle().Foreground(t.TextMuted)
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s %s %s",
|
||||||
|
labelStyle.Render(label),
|
||||||
|
bar.ViewAs(pct),
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -2,14 +2,17 @@ package components
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"cburn/internal/claudeai"
|
||||||
"cburn/internal/tui/theme"
|
"cburn/internal/tui/theme"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/progress"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RenderStatusBar renders the bottom status bar.
|
// RenderStatusBar renders the bottom status bar with optional rate limit indicators.
|
||||||
func RenderStatusBar(width int, dataAge string) string {
|
func RenderStatusBar(width int, dataAge string, subData *claudeai.SubscriptionData) string {
|
||||||
t := theme.Active
|
t := theme.Active
|
||||||
|
|
||||||
style := lipgloss.NewStyle().
|
style := lipgloss.NewStyle().
|
||||||
@@ -17,22 +20,82 @@ func RenderStatusBar(width int, dataAge string) string {
|
|||||||
Width(width)
|
Width(width)
|
||||||
|
|
||||||
left := " [?]help [q]uit"
|
left := " [?]help [q]uit"
|
||||||
|
|
||||||
|
// Build rate limit indicators for the middle section
|
||||||
|
ratePart := renderStatusRateLimits(subData)
|
||||||
|
|
||||||
right := ""
|
right := ""
|
||||||
if dataAge != "" {
|
if dataAge != "" {
|
||||||
right = fmt.Sprintf("Data: %s ", dataAge)
|
right = fmt.Sprintf("Data: %s ", dataAge)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pad middle
|
// Layout: left + ratePart + right, with padding distributed
|
||||||
padding := width - lipgloss.Width(left) - lipgloss.Width(right)
|
usedWidth := lipgloss.Width(left) + lipgloss.Width(ratePart) + lipgloss.Width(right)
|
||||||
|
padding := width - usedWidth
|
||||||
if padding < 0 {
|
if padding < 0 {
|
||||||
padding = 0
|
padding = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
bar := left
|
// Split padding: more on the left side of rate indicators
|
||||||
for i := 0; i < padding; i++ {
|
leftPad := padding / 2
|
||||||
bar += " "
|
rightPad := padding - leftPad
|
||||||
}
|
|
||||||
bar += right
|
bar := left + strings.Repeat(" ", leftPad) + ratePart + strings.Repeat(" ", rightPad) + right
|
||||||
|
|
||||||
return style.Render(bar)
|
return style.Render(bar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renderStatusRateLimits renders compact rate limit bars for the status bar.
|
||||||
|
func renderStatusRateLimits(subData *claudeai.SubscriptionData) string {
|
||||||
|
if subData == nil || subData.Usage == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
t := theme.Active
|
||||||
|
sepStyle := lipgloss.NewStyle().Foreground(t.TextDim)
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
if w := subData.Usage.FiveHour; w != nil {
|
||||||
|
parts = append(parts, compactStatusBar("5h", w.Pct))
|
||||||
|
}
|
||||||
|
if w := subData.Usage.SevenDay; w != nil {
|
||||||
|
parts = append(parts, compactStatusBar("Wk", w.Pct))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, sepStyle.Render(" | "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// compactStatusBar renders a tiny inline progress indicator for the status bar.
|
||||||
|
// Format: "5h ████░░░░ 42%"
|
||||||
|
func compactStatusBar(label string, pct float64) string {
|
||||||
|
t := theme.Active
|
||||||
|
|
||||||
|
if pct < 0 {
|
||||||
|
pct = 0
|
||||||
|
}
|
||||||
|
if pct > 1 {
|
||||||
|
pct = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
barW := 8
|
||||||
|
bar := progress.New(
|
||||||
|
progress.WithSolidFill(ColorForPct(pct)),
|
||||||
|
progress.WithWidth(barW),
|
||||||
|
progress.WithoutPercentage(),
|
||||||
|
)
|
||||||
|
bar.EmptyColor = string(t.TextDim)
|
||||||
|
|
||||||
|
labelStyle := lipgloss.NewStyle().Foreground(t.TextMuted)
|
||||||
|
pctStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(ColorForPct(pct)))
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s %s %s",
|
||||||
|
labelStyle.Render(label),
|
||||||
|
bar.ViewAs(pct),
|
||||||
|
pctStyle.Render(fmt.Sprintf("%2.0f%%", pct*100)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,16 +17,11 @@ type Tab struct {
|
|||||||
|
|
||||||
// Tabs defines all available tabs.
|
// Tabs defines all available tabs.
|
||||||
var Tabs = []Tab{
|
var Tabs = []Tab{
|
||||||
{Name: "Dashboard", Key: 'd', KeyPos: 0},
|
{Name: "Overview", Key: 'o', KeyPos: 0},
|
||||||
{Name: "Costs", Key: 'c', KeyPos: 0},
|
{Name: "Costs", Key: 'c', KeyPos: 0},
|
||||||
{Name: "Sessions", Key: 's', KeyPos: 0},
|
{Name: "Sessions", Key: 's', KeyPos: 0},
|
||||||
{Name: "Models", Key: 'm', KeyPos: 0},
|
{Name: "Breakdown", Key: 'b', KeyPos: 0},
|
||||||
{Name: "Projects", Key: 'p', KeyPos: 0},
|
{Name: "Settings", Key: 'x', KeyPos: -1},
|
||||||
{Name: "Trends", Key: 't', KeyPos: 0},
|
|
||||||
{Name: "Efficiency", Key: 'e', KeyPos: 0},
|
|
||||||
{Name: "Activity", Key: 'a', KeyPos: 0},
|
|
||||||
{Name: "Budget", Key: 'b', KeyPos: 0},
|
|
||||||
{Name: "Settings", Key: 'x', KeyPos: -1}, // x is not in "Settings"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderTabBar renders the tab bar with the given active index.
|
// RenderTabBar renders the tab bar with the given active index.
|
||||||
@@ -47,7 +42,7 @@ func RenderTabBar(activeIdx int, width int) string {
|
|||||||
dimKeyStyle := lipgloss.NewStyle().
|
dimKeyStyle := lipgloss.NewStyle().
|
||||||
Foreground(t.TextDim)
|
Foreground(t.TextDim)
|
||||||
|
|
||||||
var parts []string
|
parts := make([]string, 0, len(Tabs))
|
||||||
for i, tab := range Tabs {
|
for i, tab := range Tabs {
|
||||||
var rendered string
|
var rendered string
|
||||||
if i == activeIdx {
|
if i == activeIdx {
|
||||||
@@ -70,25 +65,9 @@ func RenderTabBar(activeIdx int, width int) string {
|
|||||||
parts = append(parts, rendered)
|
parts = append(parts, rendered)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single row if all tabs fit
|
bar := " " + strings.Join(parts, " ")
|
||||||
full := " " + strings.Join(parts, " ")
|
if lipgloss.Width(bar) <= width {
|
||||||
if lipgloss.Width(full) <= width {
|
return bar
|
||||||
return full
|
|
||||||
}
|
}
|
||||||
|
return lipgloss.NewStyle().MaxWidth(width).Render(bar)
|
||||||
// Fall back to two rows
|
|
||||||
row1 := strings.Join(parts[:5], " ")
|
|
||||||
row2 := strings.Join(parts[5:], " ")
|
|
||||||
|
|
||||||
return " " + row1 + "\n " + row2
|
|
||||||
}
|
|
||||||
|
|
||||||
// TabIdxByKey returns the tab index for a given key press, or -1.
|
|
||||||
func TabIdxByKey(key rune) int {
|
|
||||||
for i, tab := range Tabs {
|
|
||||||
if tab.Key == key {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package theme defines color themes for the cburn TUI dashboard.
|
||||||
package theme
|
package theme
|
||||||
|
|
||||||
import "github.com/charmbracelet/lipgloss"
|
import "github.com/charmbracelet/lipgloss"
|
||||||
@@ -8,7 +9,6 @@ type Theme struct {
|
|||||||
Background lipgloss.Color
|
Background lipgloss.Color
|
||||||
Surface lipgloss.Color
|
Surface lipgloss.Color
|
||||||
Border lipgloss.Color
|
Border lipgloss.Color
|
||||||
BorderHover lipgloss.Color
|
|
||||||
TextDim lipgloss.Color
|
TextDim lipgloss.Color
|
||||||
TextMuted lipgloss.Color
|
TextMuted lipgloss.Color
|
||||||
TextPrimary lipgloss.Color
|
TextPrimary lipgloss.Color
|
||||||
@@ -17,7 +17,6 @@ type Theme struct {
|
|||||||
Orange lipgloss.Color
|
Orange lipgloss.Color
|
||||||
Red lipgloss.Color
|
Red lipgloss.Color
|
||||||
Blue lipgloss.Color
|
Blue lipgloss.Color
|
||||||
Purple lipgloss.Color
|
|
||||||
Yellow lipgloss.Color
|
Yellow lipgloss.Color
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +29,6 @@ var FlexokiDark = Theme{
|
|||||||
Background: lipgloss.Color("#100F0F"),
|
Background: lipgloss.Color("#100F0F"),
|
||||||
Surface: lipgloss.Color("#1C1B1A"),
|
Surface: lipgloss.Color("#1C1B1A"),
|
||||||
Border: lipgloss.Color("#282726"),
|
Border: lipgloss.Color("#282726"),
|
||||||
BorderHover: lipgloss.Color("#343331"),
|
|
||||||
TextDim: lipgloss.Color("#575653"),
|
TextDim: lipgloss.Color("#575653"),
|
||||||
TextMuted: lipgloss.Color("#6F6E69"),
|
TextMuted: lipgloss.Color("#6F6E69"),
|
||||||
TextPrimary: lipgloss.Color("#FFFCF0"),
|
TextPrimary: lipgloss.Color("#FFFCF0"),
|
||||||
@@ -39,7 +37,6 @@ var FlexokiDark = Theme{
|
|||||||
Orange: lipgloss.Color("#DA702C"),
|
Orange: lipgloss.Color("#DA702C"),
|
||||||
Red: lipgloss.Color("#D14D41"),
|
Red: lipgloss.Color("#D14D41"),
|
||||||
Blue: lipgloss.Color("#4385BE"),
|
Blue: lipgloss.Color("#4385BE"),
|
||||||
Purple: lipgloss.Color("#8B7EC8"),
|
|
||||||
Yellow: lipgloss.Color("#D0A215"),
|
Yellow: lipgloss.Color("#D0A215"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +46,6 @@ var CatppuccinMocha = Theme{
|
|||||||
Background: lipgloss.Color("#1E1E2E"),
|
Background: lipgloss.Color("#1E1E2E"),
|
||||||
Surface: lipgloss.Color("#313244"),
|
Surface: lipgloss.Color("#313244"),
|
||||||
Border: lipgloss.Color("#45475A"),
|
Border: lipgloss.Color("#45475A"),
|
||||||
BorderHover: lipgloss.Color("#585B70"),
|
|
||||||
TextDim: lipgloss.Color("#6C7086"),
|
TextDim: lipgloss.Color("#6C7086"),
|
||||||
TextMuted: lipgloss.Color("#A6ADC8"),
|
TextMuted: lipgloss.Color("#A6ADC8"),
|
||||||
TextPrimary: lipgloss.Color("#CDD6F4"),
|
TextPrimary: lipgloss.Color("#CDD6F4"),
|
||||||
@@ -58,7 +54,6 @@ var CatppuccinMocha = Theme{
|
|||||||
Orange: lipgloss.Color("#FAB387"),
|
Orange: lipgloss.Color("#FAB387"),
|
||||||
Red: lipgloss.Color("#F38BA8"),
|
Red: lipgloss.Color("#F38BA8"),
|
||||||
Blue: lipgloss.Color("#89B4FA"),
|
Blue: lipgloss.Color("#89B4FA"),
|
||||||
Purple: lipgloss.Color("#CBA6F7"),
|
|
||||||
Yellow: lipgloss.Color("#F9E2AF"),
|
Yellow: lipgloss.Color("#F9E2AF"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +63,6 @@ var TokyoNight = Theme{
|
|||||||
Background: lipgloss.Color("#1A1B26"),
|
Background: lipgloss.Color("#1A1B26"),
|
||||||
Surface: lipgloss.Color("#24283B"),
|
Surface: lipgloss.Color("#24283B"),
|
||||||
Border: lipgloss.Color("#414868"),
|
Border: lipgloss.Color("#414868"),
|
||||||
BorderHover: lipgloss.Color("#565F89"),
|
|
||||||
TextDim: lipgloss.Color("#565F89"),
|
TextDim: lipgloss.Color("#565F89"),
|
||||||
TextMuted: lipgloss.Color("#A9B1D6"),
|
TextMuted: lipgloss.Color("#A9B1D6"),
|
||||||
TextPrimary: lipgloss.Color("#C0CAF5"),
|
TextPrimary: lipgloss.Color("#C0CAF5"),
|
||||||
@@ -77,7 +71,6 @@ var TokyoNight = Theme{
|
|||||||
Orange: lipgloss.Color("#FF9E64"),
|
Orange: lipgloss.Color("#FF9E64"),
|
||||||
Red: lipgloss.Color("#F7768E"),
|
Red: lipgloss.Color("#F7768E"),
|
||||||
Blue: lipgloss.Color("#7AA2F7"),
|
Blue: lipgloss.Color("#7AA2F7"),
|
||||||
Purple: lipgloss.Color("#BB9AF7"),
|
|
||||||
Yellow: lipgloss.Color("#E0AF68"),
|
Yellow: lipgloss.Color("#E0AF68"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +80,6 @@ var Terminal = Theme{
|
|||||||
Background: lipgloss.Color("0"),
|
Background: lipgloss.Color("0"),
|
||||||
Surface: lipgloss.Color("0"),
|
Surface: lipgloss.Color("0"),
|
||||||
Border: lipgloss.Color("8"),
|
Border: lipgloss.Color("8"),
|
||||||
BorderHover: lipgloss.Color("7"),
|
|
||||||
TextDim: lipgloss.Color("8"),
|
TextDim: lipgloss.Color("8"),
|
||||||
TextMuted: lipgloss.Color("7"),
|
TextMuted: lipgloss.Color("7"),
|
||||||
TextPrimary: lipgloss.Color("15"),
|
TextPrimary: lipgloss.Color("15"),
|
||||||
@@ -96,7 +88,6 @@ var Terminal = Theme{
|
|||||||
Orange: lipgloss.Color("3"),
|
Orange: lipgloss.Color("3"),
|
||||||
Red: lipgloss.Color("1"),
|
Red: lipgloss.Color("1"),
|
||||||
Blue: lipgloss.Color("4"),
|
Blue: lipgloss.Color("4"),
|
||||||
Purple: lipgloss.Color("5"),
|
|
||||||
Yellow: lipgloss.Color("3"),
|
Yellow: lipgloss.Color("3"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user