Files
llmgw/internal/cli/tui_chart.go
T
Ilker ManapandClaude Opus 5.5 a1b1663e9d A btop-like monitor in the console, from Prometheus; node exporters discovered and installed
Monitor draws CPU, memory, GPUs, requests, tokens, latency, network and disk
as Braille charts over 5m/1h/24h/7d, then per node, card and model
sparklines; node and model pages have their own. History is read from
monitoring.prometheus through POST /api/v1/metrics/range, or without it
from what the console saw. The control plane serves the cluster's machines
for Prometheus http_sd at /prometheus/targets on its metrics port, and node
add installs prometheus-node-exporter.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 01:51:05 +02:00

231 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package cli
import (
"fmt"
"math"
"strings"
"github.com/charmbracelet/lipgloss"
)
// Charts drawn in a terminal the way btop draws them: Braille characters,
// each cell two dots across and four down, so a chart forty cells wide holds
// eighty points and five rows twenty levels. Filled below the line, and
// coloured by height — green low, yellow, red near the top — so how hard
// something is working reads before any number does.
// Braille dot bits by column (0 left, 1 right) and row (0 top .. 3 bottom).
var brailleBit = [2][4]rune{{0x01, 0x02, 0x04, 0x40}, {0x08, 0x10, 0x20, 0x80}}
// gradient is the colour at a fraction of a chart's height, 0 bottom, 1 top.
func gradient(f float64) lipgloss.Color {
stops := [][3]float64{{80, 200, 120}, {230, 200, 60}, {230, 80, 70}}
f = math.Max(0, math.Min(1, f))
seg := f * float64(len(stops)-1)
i := int(seg)
if i >= len(stops)-1 {
i, seg = len(stops)-2, float64(len(stops)-1)
}
t := seg - float64(i)
c := [3]float64{}
for k := range c {
c[k] = stops[i][k] + (stops[i+1][k]-stops[i][k])*t
}
return lipgloss.Color(fmt.Sprintf("#%02x%02x%02x", int(c[0]), int(c[1]), int(c[2])))
}
// lineColors are the colours of the lines of a chart with several.
var lineColors = []lipgloss.Color{"#61afef", "#e5c07b", "#c678dd", "#56b6c2", "#e06c75", "#98c379", "#d19a66", "#abb2bf"}
// chartSeries is one line: its values, oldest first, and a name for the
// legend. NaN is no sample.
type chartSeries struct {
name string
values []float64
}
// brailleChart draws series into width×height cells. top is the value at the
// top of the chart; 0 takes the largest value. One series is filled and
// coloured by height; several are lines, each its own colour.
func brailleChart(series []chartSeries, width, height int, top float64) []string {
width, height = max(width, 4), max(height, 1)
if top <= 0 {
for _, s := range series {
for _, v := range s.values {
if !math.IsNaN(v) && v > top {
top = v
}
}
}
}
if top <= 0 {
top = 1
}
cols, rows := width*2, height*4
dots := make([][]rune, height)
owner := make([][]int, height)
for r := range dots {
dots[r] = make([]rune, width)
owner[r] = make([]int, width)
for c := range owner[r] {
owner[r][c] = -1
}
}
set := func(x, y, who int) { // y from the bottom
if x < 0 || x >= cols || y < 0 || y >= rows {
return
}
r := height - 1 - y/4
dots[r][x/2] |= brailleBit[x%2][3-y%4]
owner[r][x/2] = who
}
area := len(series) == 1
for si, s := range series {
vals := resample(s.values, cols)
prev := -1
for x, v := range vals {
if math.IsNaN(v) {
prev = -1
continue
}
h := int(math.Round(v / top * float64(rows-1)))
h = max(0, min(rows-1, h))
if area {
for y := 0; y <= h; y++ {
set(x, y, si)
}
continue
}
// A line: this point, and the rise or fall from the last.
lo, hi := h, h
if prev >= 0 {
lo, hi = min(prev, h), max(prev, h)
}
for y := lo; y <= hi; y++ {
set(x, y, si)
}
prev = h
}
}
out := make([]string, height)
for r := range dots {
var b strings.Builder
for c, d := range dots[r] {
if owner[r][c] < 0 {
b.WriteString(" ")
continue
}
color := lineColors[owner[r][c]%len(lineColors)]
if area {
color = gradient(float64(height-1-r) / float64(max(1, height-1)))
}
b.WriteString(lipgloss.NewStyle().Foreground(color).Render(string(0x2800 + d)))
}
out[r] = b.String()
}
return out
}
// resample fits values into n points: the most recent n, each the average of
// what falls in it, the left padded with no sample when there are fewer.
func resample(vals []float64, n int) []float64 {
out := make([]float64, n)
for i := range out {
out[i] = math.NaN()
}
if len(vals) == 0 {
return out
}
if len(vals) <= n {
copy(out[n-len(vals):], vals)
return out
}
per := float64(len(vals)) / float64(n)
for i := range out {
from, to := int(float64(i)*per), int(float64(i+1)*per)
sum, k := 0.0, 0
for _, v := range vals[from:max(to, from+1)] {
if !math.IsNaN(v) {
sum += v
k++
}
}
if k > 0 {
out[i] = sum / float64(k)
}
}
return out
}
var sparkRunes = []rune("▁▂▃▄▅▆▇█")
// sparkline is values in one row of block characters, coloured by height.
func sparkline(vals []float64, width int, top float64) string {
vals = resample(vals, width)
if top <= 0 {
for _, v := range vals {
if !math.IsNaN(v) && v > top {
top = v
}
}
}
if top <= 0 {
top = 1
}
var b strings.Builder
for _, v := range vals {
if math.IsNaN(v) {
b.WriteString(" ")
continue
}
f := math.Max(0, math.Min(1, v/top))
r := sparkRunes[int(math.Round(f*float64(len(sparkRunes)-1)))]
b.WriteString(lipgloss.NewStyle().Foreground(gradient(f)).Render(string(r)))
}
return b.String()
}
// meter is a fraction as a bar of width cells, btop's ■, coloured by level.
func meter(f float64, width int) string {
f = math.Max(0, math.Min(1, f))
n := int(math.Round(f * float64(width)))
var b strings.Builder
for i := 0; i < width; i++ {
if i < n {
b.WriteString(lipgloss.NewStyle().Foreground(gradient(float64(i) / float64(max(1, width-1)))).Render("■"))
} else {
b.WriteString(sDim.Render("■"))
}
}
return b.String()
}
// chartBox is a chart with its title and current value above it, and the
// window's span below: what a panel of the monitor shows.
func chartBox(title, now string, series []chartSeries, width, height int, top float64, span string) string {
var b strings.Builder
head := mdBold.Render(title)
if now != "" {
head += " " + now
}
if len(series) > 1 {
var leg []string
for i, s := range series {
if i == 6 {
leg = append(leg, sDim.Render(fmt.Sprintf("+%d", len(series)-6)))
break
}
leg = append(leg, lipgloss.NewStyle().Foreground(lineColors[i%len(lineColors)]).Render("━ ")+truncate(s.name, 18))
}
head += " " + strings.Join(leg, " ")
}
b.WriteString(truncateWidth(head, width) + "\n")
for _, l := range brailleChart(series, width, height, top) {
b.WriteString(l + "\n")
}
left, right := "-"+span, "now"
gap := max(1, width-len(left)-len(right))
b.WriteString(sDim.Render(left + strings.Repeat("─", gap) + right))
return b.String()
}