135 lines
3.4 KiB
Go
135 lines
3.4 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
meter.go
|
|
|
|
The spectral capacity meter: payload-vs-carrier fill for hide, comparative table for capacity
|
|
*/
|
|
|
|
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
const (
|
|
fullPercent = 100
|
|
highWaterPercent = 80
|
|
formatColumn = 7
|
|
unboundedTag = "unbounded"
|
|
)
|
|
|
|
type capRow struct {
|
|
format string
|
|
applicable bool
|
|
unbounded bool
|
|
capacity int
|
|
maxPlaintext int
|
|
maxEncrypted int
|
|
note string
|
|
}
|
|
|
|
func renderHideMeter(payloadLen, envelope, capacity int, capErr error, barWidth int) string {
|
|
if capErr != nil {
|
|
return styleWarn.Render(reasonText(capErr))
|
|
}
|
|
if capacity >= math.MaxInt32 {
|
|
bar := gradientText(strings.Repeat(blockFull, barWidth), unboundedStops, false)
|
|
return lipgloss.JoinVertical(lipgloss.Left,
|
|
bar+" "+styleSuccess.Render(unboundedTag),
|
|
"",
|
|
kv("payload", fmt.Sprintf("%d B", payloadLen)),
|
|
kv("envelope", fmt.Sprintf("%d B", envelope)),
|
|
styleSuccess.Render("this carrier has effectively unbounded room; your payload always fits"),
|
|
)
|
|
}
|
|
if capacity <= 0 {
|
|
return styleWarn.Render("this cover is too small to hold a payload")
|
|
}
|
|
|
|
frac := float64(envelope) / float64(capacity)
|
|
over := envelope > capacity
|
|
pct := int(math.Round(math.Min(frac, 1) * fullPercent))
|
|
|
|
bar := spectralBar(barWidth, frac, capacityStops)
|
|
pctStyle := styleSuccess
|
|
if over {
|
|
pctStyle = styleError
|
|
} else if pct >= highWaterPercent {
|
|
pctStyle = styleWarn
|
|
}
|
|
|
|
lines := []string{
|
|
bar + " " + pctStyle.Render(fmt.Sprintf("%d%%", pct)),
|
|
"",
|
|
kv("payload", fmt.Sprintf("%d B", payloadLen)),
|
|
kv("envelope", fmt.Sprintf("%d B", envelope)),
|
|
kv("capacity", fmt.Sprintf("%d B", capacity)),
|
|
}
|
|
if over {
|
|
lines = append(lines, "", styleError.Render(
|
|
fmt.Sprintf("payload exceeds capacity by %d B: shorten it, enable compression, or pick a roomier carrier", envelope-capacity)))
|
|
} else {
|
|
lines = append(lines, "", styleSuccess.Render(
|
|
fmt.Sprintf("fits with %d B of headroom", capacity-envelope)))
|
|
}
|
|
return lipgloss.JoinVertical(lipgloss.Left, lines...)
|
|
}
|
|
|
|
func renderCapacityTable(rows []capRow, barWidth int) string {
|
|
maxCap := 0
|
|
for _, r := range rows {
|
|
if r.applicable && !r.unbounded && r.capacity > maxCap {
|
|
maxCap = r.capacity
|
|
}
|
|
}
|
|
out := make([]string, 0, len(rows))
|
|
for _, r := range rows {
|
|
name := styleValueBold.Width(formatColumn).Render(r.format)
|
|
switch {
|
|
case !r.applicable:
|
|
out = append(out, name+styleHint.Render(r.note))
|
|
case r.unbounded:
|
|
bar := gradientText(strings.Repeat(blockFull, barWidth), unboundedStops, false)
|
|
out = append(out, name+bar+" "+styleHint.Render(unboundedTag))
|
|
default:
|
|
frac := 0.0
|
|
if maxCap > 0 {
|
|
frac = float64(r.capacity) / float64(maxCap)
|
|
}
|
|
bar := spectralBar(barWidth, frac, capacityStops)
|
|
out = append(out, name+bar+" "+capacityFacts(r))
|
|
}
|
|
}
|
|
return lipgloss.JoinVertical(lipgloss.Left, out...)
|
|
}
|
|
|
|
func capacityFacts(r capRow) string {
|
|
plain := styleValue.Render(fmt.Sprintf("%d B", r.maxPlaintext)) + styleLabel.Render(" plain")
|
|
var enc string
|
|
if r.maxEncrypted <= 0 {
|
|
enc = styleWarn.Render("encrypted does not fit")
|
|
} else {
|
|
enc = styleValue.Render(fmt.Sprintf("%d B", r.maxEncrypted)) + styleLabel.Render(" encrypted")
|
|
}
|
|
return plain + styleHelpSep.Render(" · ") + enc
|
|
}
|
|
|
|
func reasonText(err error) string {
|
|
msg := err.Error()
|
|
if i := strings.Index(msg, ": "); i >= 0 {
|
|
return msg[i+2:]
|
|
}
|
|
return msg
|
|
}
|
|
|
|
func clampZero(n int) int {
|
|
if n < 0 {
|
|
return 0
|
|
}
|
|
return n
|
|
}
|