a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
165 lines
4.6 KiB
Go
165 lines
4.6 KiB
Go
package progress
|
||
|
||
import (
|
||
"strings"
|
||
|
||
"github.com/charmbracelet/lipgloss"
|
||
)
|
||
|
||
// The gortex mark is a 5×5 dot-matrix "G": an open ring with a tongue in the
|
||
// middle row whose tip — the accent dot — always renders in brand green. The
|
||
// live renderer lights the remaining dots progressively as work completes, so
|
||
// the logo itself is the coarse progress gauge; banners render it fully lit.
|
||
type gDot struct {
|
||
r, c int
|
||
accent bool
|
||
}
|
||
|
||
// gDots lists the mark's dots in row-major order — the order they light up.
|
||
var gDots = buildGDots()
|
||
|
||
func buildGDots() []gDot {
|
||
const cells = 5
|
||
mid := cells / 2
|
||
var dots []gDot
|
||
for r := 0; r < cells; r++ {
|
||
for c := 0; c < cells; c++ {
|
||
onTop := r == 0 && c >= 1 && c <= cells-2
|
||
onBot := r == cells-1 && c >= 1 && c <= cells-2
|
||
onLeft := c == 0 && r >= 1 && r <= cells-2
|
||
onTongue := r == mid && c >= mid && c <= cells-1
|
||
onTopRight := c == cells-1 && r == 1
|
||
onBotRight := c == cells-1 && r == cells-2
|
||
if onTop || onBot || onLeft || onTongue || onTopRight || onBotRight {
|
||
dots = append(dots, gDot{r: r, c: c, accent: r == mid && c == cells-1})
|
||
}
|
||
}
|
||
}
|
||
return dots
|
||
}
|
||
|
||
// litDotCount is the number of dots that participate in progressive lighting
|
||
// (every dot except the always-lit accent tip).
|
||
func litDotCount() int { return len(gDots) - 1 }
|
||
|
||
// logoLines renders the mark as 5 rows. frac in [0,1] lights that fraction of
|
||
// the dots bottom-up in row-major order; frac < 0 selects the indeterminate
|
||
// marquee — a short lit window orbiting the mark at the given tick. glint, when
|
||
// >= 0, additionally paints dot (glint % len) in accent green: the traveling
|
||
// highlight used by animated banners over a fully lit mark.
|
||
func logoLines(lit, dim, accent lipgloss.Style, g glyphSet, frac float64, tick, glint int) [5]string {
|
||
const cells = 5
|
||
byPos := make(map[[2]int]int, len(gDots))
|
||
for i, d := range gDots {
|
||
byPos[[2]int{d.r, d.c}] = i
|
||
}
|
||
|
||
litN := -1
|
||
if frac >= 0 {
|
||
if frac > 1 {
|
||
frac = 1
|
||
}
|
||
litN = int(frac*float64(litDotCount()) + 0.5)
|
||
}
|
||
|
||
// Non-accent ordinal per dot index, for both fill and marquee windows.
|
||
ordinal := make([]int, len(gDots))
|
||
ord := 0
|
||
for i, d := range gDots {
|
||
if d.accent {
|
||
ordinal[i] = -1
|
||
continue
|
||
}
|
||
ordinal[i] = ord
|
||
ord++
|
||
}
|
||
|
||
const marqueeWindow = 4
|
||
var lines [5]string
|
||
for r := 0; r < cells; r++ {
|
||
var row strings.Builder
|
||
for c := 0; c < cells; c++ {
|
||
if c > 0 {
|
||
row.WriteString(" ")
|
||
}
|
||
idx, isDot := byPos[[2]int{r, c}]
|
||
if !isDot {
|
||
row.WriteString(" ")
|
||
continue
|
||
}
|
||
d := gDots[idx]
|
||
switch {
|
||
case d.accent:
|
||
row.WriteString(accent.Render(g.Dot))
|
||
case glint >= 0 && idx == glint%len(gDots):
|
||
row.WriteString(accent.Render(g.Dot))
|
||
case litN >= 0 && ordinal[idx] < litN:
|
||
row.WriteString(lit.Render(g.Dot))
|
||
case litN < 0 && marqueeHit(ordinal[idx], tick, marqueeWindow):
|
||
row.WriteString(lit.Render(g.Dot))
|
||
default:
|
||
row.WriteString(dim.Render(g.DotDim))
|
||
}
|
||
}
|
||
lines[r] = row.String()
|
||
}
|
||
return lines
|
||
}
|
||
|
||
// marqueeHit reports whether the dot with the given non-accent ordinal falls
|
||
// inside the lit window at this tick.
|
||
func marqueeHit(ord, tick, window int) bool {
|
||
n := litDotCount()
|
||
if ord < 0 || n == 0 {
|
||
return false
|
||
}
|
||
start := tick % n
|
||
for i := 0; i < window; i++ {
|
||
if (start+i)%n == ord {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// MeshLogo renders one static frame of the gortex mark, fully lit, styled
|
||
// with the package palette. A positive tick adds a traveling green glint —
|
||
// animated banners (the wizard dashboard) pass a rising tick; static banners
|
||
// pass 0 for the pure mark.
|
||
func MeshLogo(tick int) string {
|
||
glint := -1
|
||
if tick > 0 {
|
||
glint = tick
|
||
}
|
||
lines := logoLines(stylePerim, styleInner, styleAccent, activeGlyphs(), 1, tick, glint)
|
||
return strings.Join(lines[:], "\n")
|
||
}
|
||
|
||
// MeshFrame returns the gortex mark with label (bold) and sub (dim) beside
|
||
// it. Used by watch loops or custom views that want the brand block without
|
||
// owning a live tracker.
|
||
func MeshFrame(tick int, label, sub string) string {
|
||
mesh := MeshLogo(tick)
|
||
if label == "" && sub == "" {
|
||
return mesh + "\n"
|
||
}
|
||
right := lipgloss.JoinVertical(
|
||
lipgloss.Left,
|
||
"",
|
||
styleLabel.Render(label),
|
||
"",
|
||
styleSub.Render(sub),
|
||
"",
|
||
)
|
||
return lipgloss.JoinHorizontal(lipgloss.Top, mesh, " ", right) + "\n"
|
||
}
|
||
|
||
// MeshLogoLines returns the number of vertical rows the mark occupies.
|
||
// Exported so wizard / dashboard layouts can reserve space without re-counting
|
||
// the constant.
|
||
func MeshLogoLines() int { return 5 }
|
||
|
||
// MeshLogoWidth returns the rendered visual width of the mark in cells.
|
||
// 5 cells × "● " spacing = 9 chars.
|
||
func MeshLogoWidth() int { return 9 }
|