From 892e855207e7264ae7ab2e486c1af3b4af60a60f Mon Sep 17 00:00:00 2001 From: flamingcow Date: Sat, 25 Jul 2026 19:48:15 -0700 Subject: [PATCH] Group the display into instant and cumulative sections on a shared grid --- main.go | 2 - ui.go | 224 +++++++++++++++++++++++++++++++++++--------------------- 2 files changed, 140 insertions(+), 86 deletions(-) diff --git a/main.go b/main.go index 124eceb..150c0c1 100644 --- a/main.go +++ b/main.go @@ -469,8 +469,6 @@ func run(aName, bName, sizesArg, patArg string, return fmt.Errorf("display: %w", err) } defer disp.close() - disp.config = fmt.Sprintf("%s sizes %s %d streams batch %d crc32c verify", - patterns[patIdx].name, strings.Join(sizeStrs, ","), nStreams, batch) start := time.Now() close(startTx) diff --git a/ui.go b/ui.go index a6b7cb8..6bf8793 100644 --- a/ui.go +++ b/ui.go @@ -8,6 +8,7 @@ import ( var ( uiBg = rgb{0x12, 0x14, 0x18} uiPanel = rgb{0x1c, 0x20, 0x26} + uiRule = rgb{0x2e, 0x34, 0x3c} uiFg = rgb{0xe6, 0xe8, 0xea} uiDim = rgb{0x7a, 0x82, 0x8c} uiCyan = rgb{0x5c, 0xc8, 0xe0} @@ -16,12 +17,34 @@ var ( uiYellow = rgb{0xe0, 0xb0, 0x40} ) +// Everything tabular sits on one monospace cell grid and every number is +// right-aligned to a column end, so columns line up by construction. +const ( + uiMargin = 16 + + cellDirA = 0 + cellArrow = 2 + cellDirB = 4 + + colTxGbEnd = 14 + colTxPPSEnd = 27 + colRxGbEnd = 37 + colRxPPSEnd = 50 + + colFramesEnd = 18 + colDataEnd = 27 + colLostEnd = 38 + colLateEnd = 46 + colCRCEnd = 53 + colKdropEnd = 62 +) + type display struct { - fb *framebuffer - huge *textFace - big *textFace - small *textFace - config string + fb *framebuffer + huge *textFace + grid *textFace + gridB *textFace + small *textFace } func newDisplay() (*display, error) { @@ -29,34 +52,58 @@ func newDisplay() (*display, error) { if err != nil { return nil, err } - huge, err := loadFace(true, 46) - if err != nil { - fb.close() - return nil, err + d := &display{fb: fb} + for _, spec := range []struct { + dst **textFace + bold bool + size float64 + }{ + {&d.huge, true, 72}, + {&d.grid, false, 24}, + {&d.gridB, true, 24}, + {&d.small, false, 18}, + } { + face, err := loadFace(spec.bold, spec.size) + if err != nil { + fb.close() + return nil, err + } + *spec.dst = face } - big, err := loadFace(true, 24) - if err != nil { + if d.grid.cellW != d.gridB.cellW { fb.close() - return nil, err + return nil, fmt.Errorf("grid faces disagree on cell width: %d vs %d", + d.grid.cellW, d.gridB.cellW) } - small, err := loadFace(false, 18) - if err != nil { - fb.close() - return nil, err - } - return &display{fb: fb, huge: huge, big: big, small: small}, nil + return d, nil } func (d *display) close() { d.fb.close() } -func (d *display) right(f *textFace, xEnd, y int, s string, c rgb) { - f.draw(d.fb, xEnd-len([]rune(s))*f.cellW, y, s, c) +func (d *display) cellX(c int) int { + return uiMargin + c*d.grid.cellW } -func (d *display) center(f *textFace, x0, w, y int, s string, c rgb) { - f.draw(d.fb, x0+(w-len([]rune(s))*f.cellW)/2, y, s, c) +func (d *display) at(c, y int, s string, col rgb) { + d.grid.draw(d.fb, d.cellX(c), y, s, col) +} + +func (d *display) atBold(c, y int, s string, col rgb) { + d.gridB.draw(d.fb, d.cellX(c), y, s, col) +} + +func (d *display) rightAt(cEnd, y int, s string, col rgb) { + d.grid.draw(d.fb, d.cellX(cEnd)-len([]rune(s))*d.grid.cellW, y, s, col) +} + +func (d *display) right(f *textFace, xEnd, y int, s string, col rgb) { + f.draw(d.fb, xEnd-len([]rune(s))*f.cellW, y, s, col) +} + +func (d *display) center(f *textFace, y int, s string, col rgb) { + f.draw(d.fb, (d.fb.w-len([]rune(s))*f.cellW)/2, y, s, col) } func errColor(n uint64) rgb { @@ -66,7 +113,7 @@ func errColor(n uint64) rgb { return uiRed } -func (d *display) rateColor(gb, target float64) rgb { +func rateColor(gb, target float64) rgb { switch { case gb >= target*rateGreenFrac: return uiGreen @@ -81,85 +128,94 @@ func (d *display) render(dirs []*direction, views []view, elapsed time.Duration, fb := d.fb fb.fill(uiBg) - d.small.draw(fb, 16, 12, "cabletest", uiCyan) - if len(dirs) > 0 { - d.small.draw(fb, 16+11*d.small.cellW, 12, - fmt.Sprintf("%s %s %s %s", dirs[0].tx.tag, dirs[0].tx.name, - dirs[0].rx.tag, dirs[0].rx.name), uiDim) - } - d.right(d.small, fb.w-16, 12, uptime(elapsed), uiDim) + d.small.draw(fb, uiMargin, 10, "cabletest", uiCyan) + d.small.draw(fb, uiMargin+11*d.small.cellW, 10, + fmt.Sprintf("%s %s %s %s", dirs[0].tx.tag, dirs[0].tx.name, + dirs[0].rx.tag, dirs[0].rx.name), uiDim) + d.right(d.small, fb.w-uiMargin, 10, uptime(elapsed), uiDim) var total uint64 for _, v := range views { total += v.errors } - // Status band, readable from across the room. - bandY, bandH := 44, 74 + bandY, bandH := 40, 150 fb.rect(0, bandY, fb.w, bandH, uiPanel) - fb.rect(0, bandY, 6, bandH, errColor(total)) + fb.rect(0, bandY, 8, bandH, errColor(total)) word, wc := "NO ERRORS", uiGreen if total > 0 { word, wc = fmt.Sprintf("%s ERRORS", commas(total)), uiRed } - d.center(d.huge, 0, fb.w, bandY+(bandH-d.huge.cellH)/2+2, word, wc) + d.center(d.huge, bandY+(bandH-d.huge.cellH)/2, word, wc) - panelY := bandY + bandH + 12 - footerH := d.small.cellH + 16 - panelH := (fb.h - panelY - footerH) / max(len(dirs), 1) + // Two tight blocks rather than rows spread over the whole panel, centred in + // what is left below the band. + lineH := d.grid.cellH + 4 + sectionH := d.small.cellH + 10 + (1+len(dirs))*lineH + gap := 30 + avail := fb.h - (bandY + bandH) - 16 + y := bandY + bandH + 8 + (avail-2*sectionH-gap)/2 + + y = d.section(y, "INSTANT", "this interval") + d.rightAt(colTxGbEnd, y, "TX Gb/s", uiDim) + d.rightAt(colTxPPSEnd, y, "TX pps", uiDim) + d.rightAt(colRxGbEnd, y, "RX Gb/s", uiDim) + d.rightAt(colRxPPSEnd, y, "RX pps", uiDim) + y += lineH for i, dir := range dirs { - d.renderDirection(dir, views[i], 12, panelY+i*panelH, fb.w-24, panelH-10, target) + d.dirTag(dir, y) + v := views[i] + d.rightAt(colTxGbEnd, y, fmt.Sprintf("%.2f", v.txGbps), rateColor(v.txGbps, target)) + d.rightAt(colTxPPSEnd, y, commas(uint64(v.txPPS)), uiFg) + d.rightAt(colRxGbEnd, y, fmt.Sprintf("%.2f", v.rxGbps), rateColor(v.rxGbps, target)) + d.rightAt(colRxPPSEnd, y, commas(uint64(v.rxPPS)), uiFg) + y += lineH } - d.footer(fb.h - footerH + 4) + + y += gap + y = d.section(y, "CUMULATIVE", "since reset") + d.rightAt(colFramesEnd, y, "frames", uiDim) + d.rightAt(colDataEnd, y, "data", uiDim) + d.rightAt(colLostEnd, y, "lost", uiDim) + d.rightAt(colLateEnd, y, "late", uiDim) + d.rightAt(colCRCEnd, y, "crc", uiDim) + d.rightAt(colKdropEnd, y, "kdrop", uiDim) + y += lineH + for i, dir := range dirs { + d.dirTag(dir, y) + v := views[i] + d.rightAt(colFramesEnd, y, commas(v.txFrames), uiFg) + d.rightAt(colDataEnd, y, humanBytes(v.txSent), uiFg) + d.rightAt(colLostEnd, y, commas(v.lost), errColor(v.lost)) + d.rightAt(colLateEnd, y, commas(v.late), errColor(v.late)) + d.rightAt(colCRCEnd, y, commas(v.crc), errColor(v.crc)) + d.rightAt(colKdropEnd, y, commas(v.kdrop), errColor(v.kdrop)) + y += lineH + } + fb.flush() } -func (d *display) renderDirection(dir *direction, v view, x, y, w, h int, target float64) { - fb := d.fb - fb.rect(x, y, w, h, uiPanel) - - // The font has no arrow glyph, so the direction marker is drawn. - d.big.draw(fb, x+14, y+12, dir.tx.tag, uiCyan) - d.arrow(x+14+2*d.big.cellW, y+12+d.big.cellH/2, 22, uiDim) - d.big.draw(fb, x+14+2*d.big.cellW+30, y+12, dir.rx.tag, uiCyan) - - col := x + 110 - d.rateBlock(col, y+10, "TX", v.txPPS, v.txGbps, target) - d.rateBlock(col+300, y+10, "RX", v.rxPPS, v.rxGbps, target) - - ex := col + 620 - d.errLine(ex, y+10, "lost", v.lost) - d.errLine(ex, y+10+d.small.cellH+2, "crc", v.crc) - d.errLine(ex, y+10+2*(d.small.cellH+2), "kdrop", v.kdrop) - d.errLine(ex, y+10+3*(d.small.cellH+2), "late", v.late) - - totals := fmt.Sprintf("sent %s frames %s received %s frames %s", - commas(v.txFrames), humanBytes(v.txSent), - commas(v.rxFrames), humanBytes(v.rxGot)) - d.small.draw(fb, x+14, y+h-d.small.cellH-8, totals, uiDim) +func (d *display) section(y int, title, sub string) int { + d.small.draw(d.fb, uiMargin, y, title, uiFg) + d.small.draw(d.fb, uiMargin+(len(title)+2)*d.small.cellW, y, sub, uiDim) + ruleY := y + d.small.cellH + 3 + d.fb.rect(uiMargin, ruleY, d.fb.w-2*uiMargin, 1, uiRule) + return ruleY + 7 } -func (d *display) arrow(x, y, w int, c rgb) { - d.fb.rect(x, y-1, w-6, 3, c) - for i := 0; i < 7; i++ { - d.fb.rect(x+w-7+i, y-6+i, 1, 13-2*i, c) +func (d *display) dirTag(dir *direction, y int) { + d.atBold(cellDirA, y, dir.tx.tag, uiCyan) + d.arrow(d.cellX(cellArrow), y+d.grid.cellH/2, uiDim) + d.atBold(cellDirB, y, dir.rx.tag, uiCyan) +} + +// Drawn because the font has no arrow glyph. Occupies exactly one cell so the +// grid is unaffected. +func (d *display) arrow(x, y int, c rgb) { + w := d.grid.cellW + d.fb.rect(x, y-1, w-5, 2, c) + for i := 0; i < 6; i++ { + d.fb.rect(x+w-6+i, y-5+i, 1, 11-2*i, c) } } - -func (d *display) rateBlock(x, y int, label string, pps, gb, target float64) { - d.small.draw(d.fb, x, y, label, uiDim) - d.big.draw(d.fb, x+3*d.small.cellW, y-2, - fmt.Sprintf("%6.2f Gb/s", gb), d.rateColor(gb, target)) - d.small.draw(d.fb, x+3*d.small.cellW, y+d.big.cellH, - fmt.Sprintf("%s pps", commas(uint64(pps))), uiFg) -} - -func (d *display) footer(y int) { - d.small.draw(d.fb, 16, y, d.config, uiDim) - d.right(d.small, d.fb.w-16, y, "space resets counts", uiDim) -} - -func (d *display) errLine(x, y int, label string, n uint64) { - d.small.draw(d.fb, x, y, label, uiDim) - d.small.draw(d.fb, x+7*d.small.cellW, y, commas(n), errColor(n)) -}