package main import ( "fmt" "strconv" "strings" "time" ) 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" ) func paint(s, code string) string { if 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...), ",") } // The magnitude letter goes with the figure so the unit can stay a fixed word // on the label. Below a thousand no letter is left dangling, since a trailing // space would push the figure off centre. func scaleSI(v float64) string { // %.2f rounds 999.995 and up to a fourth digit, so the magnitude rolls over // where the rounding does rather than at the bare thousand. for _, mag := range []string{"", "k", "M", "G", "T"} { if v < 999.995 { if mag == "" { return fmt.Sprintf("%.2f", v) } return fmt.Sprintf("%.2f %s", v, mag) } v /= 1000 } return fmt.Sprintf("%.2f P", v) } // The integer counterpart, for whole things counted rather than a rate // measured. Below a thousand the figure is the count itself, since two decimals // on a quantity that cannot have them read as precision that is not there. func scaleCount(v uint64) string { if v < 1000 { return strconv.FormatUint(v, 10) } return scaleSI(float64(v)) } // The same shape for time, whose magnitudes are sixties and twenty-fours. func scaleTime(d time.Duration) string { switch { case d < time.Minute: return fmt.Sprintf("%.2f s", d.Seconds()) case d < time.Hour: return fmt.Sprintf("%.2f m", d.Minutes()) case d < 24*time.Hour: return fmt.Sprintf("%.2f h", d.Hours()) } return fmt.Sprintf("%.2f d", d.Hours()/24) } type colSpec struct { group string title string width int right bool } type streamTable struct { cols []colSpec sinceHeader int headerEvery int } // A group change is drawn as a vertical break, so the two halves of the row // read apart without a second header line naming them. func (t *streamTable) join(cells []string, brk string) string { var b strings.Builder for i, c := range cells { if i > 0 { if t.cols[i].group != t.cols[i-1].group { b.WriteString(brk) } else { b.WriteByte(' ') } } b.WriteString(c) } return b.String() } func (t *streamTable) headerLines() []string { titles := make([]string, len(t.cols)) rules := make([]string, len(t.cols)) for i, c := range t.cols { titles[i] = paint(pad(c.title, c.width, c.right), cBold) rules[i] = strings.Repeat("─", c.width) } return []string{ t.join(titles, paint(" │ ", cGrey)), paint(t.join(rules, "─┼─"), cGrey), } } func (t *streamTable) width() int { w := 0 for i, c := range t.cols { w += c.width if i > 0 { w++ if t.cols[i].group != t.cols[i-1].group { w += 2 } } } return w } // A labelled horizontal rule spanning the table, for marking a break in the // stream of rows. func (t *streamTable) rule(label string) string { const lead = 3 text := " " + label + " " trail := t.width() - lead - visWidth(text) if trail < 0 { trail = 0 } return paint(strings.Repeat("─", lead), cGrey) + paint(text, cDim) + paint(strings.Repeat("─", trail), 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, t.join(padded, paint(" │ ", cGrey))) } 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 writeln := func(s string) { b.WriteString(s) b.WriteByte('\n') } if title != "" { writeln(paint(title, cBold+cCyan)) } writeln(line("┌", "┬", "┐")) writeln(rowText(headers, true)) writeln(line("├", "┼", "┤")) for _, r := range rows { writeln(rowText(r, false)) } b.WriteString(line("└", "┴", "┘")) return b.String() } // Whether rather than how many, matching the panel's top chips: over a window // this short a count changes faster than it can be read. func flagCell(v uint64) string { if v == 0 { return paint("ok", cGreen) } return paint("ERR", cRed) } // Exact rather than scaled: scaled, one lost frame and a thousand both read as // 1.00, separated only by a letter. func statusCell(v uint64) string { s := commas(v) if v == 0 { return paint(s, cGreen) } return paint(s, cRed) } func snrCell(phy phyDisplay) string { if !phy.haveSNR { return paint("-", cGrey) } s := fmt.Sprintf("%+.1f", phy.worstMargin) switch snrClass(phy.worstMargin) { case clsGood: return paint(s, cGreen) case clsWarn: return paint(s, cYellow) default: return paint(s, cRed) } } func correctedCell(v uint64) string { if v == 0 { return paint("0", cGreen) } return paint(commas(v), cYellow) } func correctedFlag(v uint64) string { if v == 0 { return paint("ok", cGreen) } return paint("warn", cYellow) } // Green either way while the cable is present — the cycle's phase is state, // not health. Red stays what it was: the cable missing. func noiseCell(nv noiseView) string { switch { case nv.missing > 0: return paint("ERR", cRed) case nv.on: return paint("on", cGreen) } return paint("off", cGreen) } // Per-interval rates jitter by a couple of percent at line rate, so green has // to cover that. Yellow means a real shortfall, red means badly off. const ( rateGreenFrac = 0.95 rateYellowFrac = 0.80 ) func rateCell(bits float64, target float64) string { s := scaleSI(bits) switch { case bits >= target*rateGreenFrac: return paint(s, cGreen) case bits >= target*rateYellowFrac: return paint(s, cYellow) default: return paint(s, cRed) } }