Render output as colored tables with pass/fail verdict
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
cReset = "\x1b[0m"
|
||||
cBold = "\x1b[1m"
|
||||
cDim = "\x1b[2m"
|
||||
cRed = "\x1b[31m"
|
||||
cGreen = "\x1b[32m"
|
||||
cYellow = "\x1b[33m"
|
||||
cCyan = "\x1b[36m"
|
||||
cGrey = "\x1b[90m"
|
||||
)
|
||||
|
||||
var useColor bool
|
||||
|
||||
func initColor(mode string) error {
|
||||
switch mode {
|
||||
case "always":
|
||||
useColor = true
|
||||
case "never":
|
||||
useColor = false
|
||||
case "auto":
|
||||
if os.Getenv("NO_COLOR") != "" || os.Getenv("TERM") == "dumb" {
|
||||
useColor = false
|
||||
return nil
|
||||
}
|
||||
st, err := os.Stdout.Stat()
|
||||
useColor = err == nil && st.Mode()&os.ModeCharDevice != 0
|
||||
default:
|
||||
return fmt.Errorf("unknown color mode %q (want auto, always, never)", mode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func paint(s, code string) string {
|
||||
if !useColor || s == "" {
|
||||
return s
|
||||
}
|
||||
return code + s + cReset
|
||||
}
|
||||
|
||||
func stripANSI(s string) string {
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(s); {
|
||||
if s[i] == 0x1b {
|
||||
for i < len(s) && s[i] != 'm' {
|
||||
i++
|
||||
}
|
||||
if i < len(s) {
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
b.WriteByte(s[i])
|
||||
i++
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func visWidth(s string) int {
|
||||
return len([]rune(stripANSI(s)))
|
||||
}
|
||||
|
||||
func pad(s string, w int, right bool) string {
|
||||
gap := w - visWidth(s)
|
||||
if gap < 0 {
|
||||
gap = 0
|
||||
}
|
||||
if right {
|
||||
return strings.Repeat(" ", gap) + s
|
||||
}
|
||||
return s + strings.Repeat(" ", gap)
|
||||
}
|
||||
|
||||
func commas(v uint64) string {
|
||||
s := fmt.Sprintf("%d", v)
|
||||
if len(s) <= 3 {
|
||||
return s
|
||||
}
|
||||
var parts []string
|
||||
for len(s) > 3 {
|
||||
parts = append([]string{s[len(s)-3:]}, parts...)
|
||||
s = s[:len(s)-3]
|
||||
}
|
||||
return strings.Join(append([]string{s}, parts...), ",")
|
||||
}
|
||||
|
||||
func humanBytes(b uint64) string {
|
||||
const unit = 1000.0
|
||||
v := float64(b)
|
||||
for _, suffix := range []string{"B", "kB", "MB", "GB", "TB"} {
|
||||
if v < unit {
|
||||
return fmt.Sprintf("%.1f %s", v, suffix)
|
||||
}
|
||||
v /= unit
|
||||
}
|
||||
return fmt.Sprintf("%.1f PB", v)
|
||||
}
|
||||
|
||||
type colSpec struct {
|
||||
title string
|
||||
width int
|
||||
right bool
|
||||
}
|
||||
|
||||
type streamTable struct {
|
||||
cols []colSpec
|
||||
sinceHeader int
|
||||
headerEvery int
|
||||
}
|
||||
|
||||
func (t *streamTable) headerLines() []string {
|
||||
var titles, rules []string
|
||||
for _, c := range t.cols {
|
||||
titles = append(titles, pad(c.title, c.width, c.right))
|
||||
rules = append(rules, strings.Repeat("─", c.width))
|
||||
}
|
||||
return []string{
|
||||
paint(strings.Join(titles, " "), cBold),
|
||||
paint(strings.Join(rules, " "), cGrey),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *streamTable) emit(cells []string) []string {
|
||||
var out []string
|
||||
if t.sinceHeader == 0 || (t.headerEvery > 0 && t.sinceHeader >= t.headerEvery) {
|
||||
out = append(out, t.headerLines()...)
|
||||
t.sinceHeader = 0
|
||||
}
|
||||
var padded []string
|
||||
for i, c := range t.cols {
|
||||
v := ""
|
||||
if i < len(cells) {
|
||||
v = cells[i]
|
||||
}
|
||||
padded = append(padded, pad(v, c.width, c.right))
|
||||
}
|
||||
t.sinceHeader++
|
||||
return append(out, strings.Join(padded, " "))
|
||||
}
|
||||
|
||||
func renderBox(title string, headers []string, rights []bool, rows [][]string) string {
|
||||
n := len(headers)
|
||||
widths := make([]int, n)
|
||||
for i, h := range headers {
|
||||
widths[i] = visWidth(h)
|
||||
}
|
||||
for _, r := range rows {
|
||||
for i := 0; i < n && i < len(r); i++ {
|
||||
if w := visWidth(r[i]); w > widths[i] {
|
||||
widths[i] = w
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
line := func(l, m, r string) string {
|
||||
var parts []string
|
||||
for _, w := range widths {
|
||||
parts = append(parts, strings.Repeat("─", w+2))
|
||||
}
|
||||
return paint(l+strings.Join(parts, m)+r, cGrey)
|
||||
}
|
||||
rowText := func(cells []string, bold bool) string {
|
||||
var parts []string
|
||||
for i := 0; i < n; i++ {
|
||||
v := ""
|
||||
if i < len(cells) {
|
||||
v = cells[i]
|
||||
}
|
||||
if bold {
|
||||
v = paint(v, cBold)
|
||||
}
|
||||
parts = append(parts, " "+pad(v, widths[i], rights[i])+" ")
|
||||
}
|
||||
bar := paint("│", cGrey)
|
||||
return bar + strings.Join(parts, bar) + bar
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
if title != "" {
|
||||
b.WriteString(paint(title, cBold+cCyan) + "\n")
|
||||
}
|
||||
b.WriteString(line("┌", "┬", "┐") + "\n")
|
||||
b.WriteString(rowText(headers, true) + "\n")
|
||||
b.WriteString(line("├", "┼", "┤") + "\n")
|
||||
for _, r := range rows {
|
||||
b.WriteString(rowText(r, false) + "\n")
|
||||
}
|
||||
b.WriteString(line("└", "┴", "┘"))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func statusCell(v uint64) string {
|
||||
s := commas(v)
|
||||
if v == 0 {
|
||||
return paint(s, cGreen)
|
||||
}
|
||||
return paint(s, cRed)
|
||||
}
|
||||
|
||||
func gapCell(v int64) string {
|
||||
if v <= 0 {
|
||||
return paint("0", cGreen)
|
||||
}
|
||||
return paint(commas(uint64(v)), cYellow)
|
||||
}
|
||||
|
||||
func lostCell(v int64) string {
|
||||
if v <= 0 {
|
||||
return paint("0", cGreen)
|
||||
}
|
||||
return paint(commas(uint64(v)), cRed)
|
||||
}
|
||||
|
||||
func rateCell(gb float64, target float64) string {
|
||||
s := fmt.Sprintf("%.2f", gb)
|
||||
switch {
|
||||
case gb >= target*0.995:
|
||||
return paint(s, cGreen)
|
||||
case gb >= target*0.90:
|
||||
return paint(s, cYellow)
|
||||
default:
|
||||
return paint(s, cRed)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user