Adopt repo standards: scaffold, policies, lint-clean (closes #1)
check / check (push) Failing after 0s
check / check (push) Failing after 0s
Standard scaffold: script/ entrypoints with the Makefile as thin shims, a Dockerfile whose lint and test phases gate the build, a Gitea CI workflow running script/cibuild, REPO_POLICIES.md, TODO.md, .editorconfig, .dockerignore, LICENSE, wider .gitignore. .golangci.yml is byte-identical to the canonical copy in the prompts repo. 211 lint findings fixed in code: package globals became functions/fields and a cobra command constructor, magic numbers became named constants, ctx is threaded into the probes, monitor loops split. Tests moved to external _test packages with export_test.go. Behavior unchanged. Readers will trip over: make lint/test/check now need a Docker daemon; the personal rsync copy/run targets are gone and make run runs locally. Disclosure: four //nolint:gosec remain on the fixed-argv ping/curl calls and the operator-chosen log file, as the reference repo annotates the same class. Disclosure: module path left as-is. Model: opus-4-8 (implementation); fable-5-1 (merge)
This commit was merged in pull request #5.
This commit is contained in:
+230
-198
@@ -1,5 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package monitor
|
||||
|
||||
@@ -13,283 +12,331 @@ import (
|
||||
tcell "github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
// Frame counter for the timestamp spinner (ticks once per second)
|
||||
var (
|
||||
timestampSpinFrame = 0
|
||||
lastTimestampSpinUpdate = time.Now()
|
||||
// Fixed rows at the top of the display.
|
||||
const (
|
||||
rowTopRule = 0
|
||||
rowClock = 1
|
||||
rowBottomRule = 2
|
||||
rowRuntime = 3
|
||||
rowFirstIface = 5
|
||||
)
|
||||
|
||||
// Put writes text to the screen at the specified position with style
|
||||
// Column offsets and spacing within the drawn output.
|
||||
const (
|
||||
colLeft = 0
|
||||
colSpinner = 3 // after the "== " prefix
|
||||
colMeter = 5 // after the spinner
|
||||
colGap = 1 // single-space gap between fields
|
||||
clockGap = 5 // space between the two clocks
|
||||
lineStep = 1
|
||||
blockGap = 2 // blank line plus the following line
|
||||
headerAdvance = 4 // rule + content + rule + blank line
|
||||
)
|
||||
|
||||
// ICMP summary table geometry.
|
||||
const (
|
||||
icmpLabelWidth = 8
|
||||
icmpValWidth = 9
|
||||
icmpColGap = 4
|
||||
)
|
||||
|
||||
// Put writes text to the screen at the specified position with style.
|
||||
func Put(scr tcell.Screen, x, y int, txt string, st tcell.Style) {
|
||||
for i, r := range txt {
|
||||
scr.SetContent(x+i, y, r, nil, st)
|
||||
}
|
||||
}
|
||||
|
||||
// HLine creates a horizontal line of the specified width
|
||||
// HLine returns a horizontal line of the specified width.
|
||||
func HLine(w int) string { return strings.Repeat("=", w) }
|
||||
|
||||
// DrawRainbowText draws text with rainbow colors
|
||||
// DrawRainbowText draws text cycling through the given color styles.
|
||||
func DrawRainbowText(scr tcell.Screen, x, y int, text string, colors []tcell.Style) {
|
||||
for i, char := range text {
|
||||
// Cycle through the rainbow colors
|
||||
colorIndex := i % len(colors)
|
||||
style := colors[colorIndex]
|
||||
// Draw the character with the current rainbow color
|
||||
style := colors[i%len(colors)]
|
||||
scr.SetContent(x+i, y, char, nil, style)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateMeter creates a visual meter using ASCII characters
|
||||
// msStr formats a millisecond value as a compact "%.0fms" string.
|
||||
func msStr(v float64) string {
|
||||
return fmt.Sprintf("%.0fms", v)
|
||||
}
|
||||
|
||||
// CreateMeter creates a visual meter using ASCII characters.
|
||||
func (m *Monitor) CreateMeter(value int) (string, tcell.Style) {
|
||||
if value > m.MaxMeterValue {
|
||||
value = m.MaxMeterValue
|
||||
}
|
||||
if value < 0 {
|
||||
value = 0
|
||||
}
|
||||
value = max(min(value, m.MaxMeterValue), 0)
|
||||
|
||||
// Calculate the number of fill characters to show
|
||||
fillCount := value * m.MeterFillWidth / m.MaxMeterValue
|
||||
if fillCount > m.MeterFillWidth {
|
||||
fillCount = m.MeterFillWidth
|
||||
}
|
||||
fillCount := min(value*m.MeterFillWidth/m.MaxMeterValue, m.MeterFillWidth)
|
||||
|
||||
// Build the meter string
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteRune(MeterStart)
|
||||
|
||||
// Add fill characters
|
||||
for i := 0; i < fillCount; i++ {
|
||||
for range fillCount {
|
||||
b.WriteRune(MeterFill)
|
||||
}
|
||||
|
||||
// Add empty spaces
|
||||
for i := 0; i < m.MeterFillWidth-fillCount; i++ {
|
||||
for range m.MeterFillWidth - fillCount {
|
||||
b.WriteRune(MeterEmpty)
|
||||
}
|
||||
|
||||
b.WriteRune(MeterEnd)
|
||||
|
||||
// Get the appropriate style for this meter value
|
||||
// Using reverse=true because for packet loss, low values are good
|
||||
// reverse=true: for packet loss, low values are good.
|
||||
style := MeterColorForValue(value, m.MaxMeterValue, true)
|
||||
|
||||
return b.String(), style
|
||||
}
|
||||
|
||||
// DrawInterface draws the interface status on the screen
|
||||
// DrawInterface draws the interface status on the screen, returning the next
|
||||
// free row.
|
||||
func (m *Monitor) DrawInterface(scr tcell.Screen, y, w int, st *InterfaceStatus) int {
|
||||
Put(scr, 0, y, HLine(w), CDefault)
|
||||
Put(scr, colLeft, y, HLine(w), tcell.StyleDefault)
|
||||
|
||||
// header line
|
||||
healthy := st.IsHealthy(m.TCPTimeout)
|
||||
style := CBrightGreen
|
||||
y = m.drawHeader(scr, y, w, st, healthy)
|
||||
|
||||
m.mu.RLock()
|
||||
tcpHosts := append([]string{}, m.tcpHosts...)
|
||||
m.mu.RUnlock()
|
||||
|
||||
st.mu.RLock()
|
||||
defer st.mu.RUnlock()
|
||||
|
||||
y = drawReachability(scr, y, st)
|
||||
y = drawPacketLoss(scr, y, st)
|
||||
y = m.drawTCPTable(scr, y, st, tcpHosts)
|
||||
y = drawICMPStats(scr, y, st)
|
||||
|
||||
return y
|
||||
}
|
||||
|
||||
// drawHeader renders the interface title, spinner and meter line.
|
||||
func (m *Monitor) drawHeader(
|
||||
scr tcell.Screen, y, w int, st *InterfaceStatus, healthy bool,
|
||||
) int {
|
||||
style := styleBrightGreen()
|
||||
if !healthy {
|
||||
style = CBrightRed
|
||||
style = styleBrightRed()
|
||||
}
|
||||
|
||||
st.mu.RLock()
|
||||
header := fmt.Sprintf("%s: %s — %s", st.Name, st.Label, st.IPInfo)
|
||||
spinChar := Spins[st.SpinFrame]
|
||||
spinChar := spins()[st.SpinFrame]
|
||||
spinnerFrame := st.SpinFrame
|
||||
meterValue := st.MeterValue
|
||||
st.mu.RUnlock()
|
||||
|
||||
// Create the meter with appropriate style
|
||||
meter, meterStyle := m.CreateMeter(meterValue)
|
||||
colors := rainbow()
|
||||
spinnerStyle := colors[spinnerFrame%len(colors)]
|
||||
|
||||
// Get rainbow color for spinner (cycle through colors)
|
||||
spinnerStyle := CRainbow[spinnerFrame%len(CRainbow)]
|
||||
Put(scr, colLeft, y+lineStep, "== ", tcell.StyleDefault)
|
||||
Put(scr, colSpinner, y+lineStep, string(spinChar)+" ", spinnerStyle)
|
||||
Put(scr, colMeter, y+lineStep, meter+" ", meterStyle)
|
||||
Put(scr, colMeter+m.MeterWidth+colGap, y+lineStep, header, style)
|
||||
Put(scr, colLeft, y+blockGap, HLine(w), tcell.StyleDefault)
|
||||
|
||||
Put(scr, 0, y+1, "== ", CDefault)
|
||||
Put(scr, 3, y+1, string(spinChar)+" ", spinnerStyle) // Colorful spinner
|
||||
Put(scr, 5, y+1, meter+" ", meterStyle) // Colored meter
|
||||
Put(scr, 5+m.MeterWidth+1, y+1, header, style) // Move the header after the meter
|
||||
Put(scr, 0, y+2, HLine(w), CDefault)
|
||||
y += 4
|
||||
return y + headerAdvance
|
||||
}
|
||||
|
||||
/* reachability */
|
||||
st.mu.RLock()
|
||||
// drawReachability renders the reachability summary. The caller holds
|
||||
// st.mu.RLock.
|
||||
func drawReachability(scr tcell.Screen, y int, st *InterfaceStatus) int {
|
||||
total := len(st.Reachable)
|
||||
good := 0
|
||||
|
||||
for _, ok := range st.Reachable {
|
||||
if ok {
|
||||
good++
|
||||
}
|
||||
}
|
||||
|
||||
age := time.Since(st.LastPing).Round(time.Second)
|
||||
reachStyle := CBrightGreen
|
||||
|
||||
reachStyle := styleBrightGreen()
|
||||
if good != total {
|
||||
reachStyle = CBrightRed
|
||||
reachStyle = styleBrightRed()
|
||||
}
|
||||
|
||||
// Get last drop time and age
|
||||
dropTimeStr := "never"
|
||||
dropAgeStr := "N/A"
|
||||
|
||||
if !st.LastDrop.IsZero() {
|
||||
dropTimeStr = st.LastDrop.Format("15:04:05")
|
||||
dropAgeStr = time.Since(st.LastDrop).Round(time.Second).String()
|
||||
}
|
||||
|
||||
// Simplified reachability line without drop info
|
||||
Put(scr, 0, y, fmt.Sprintf("Reachable: %d/%d (at %s, age %s)",
|
||||
Put(scr, colLeft, y, fmt.Sprintf("Reachable: %d/%d (at %s, age %s)",
|
||||
good, total, st.LastPing.Format("15:04:05"), age), reachStyle)
|
||||
y++
|
||||
y += lineStep
|
||||
|
||||
if good == total {
|
||||
Put(scr, 0, y, "Unreachable: none", CDefault)
|
||||
Put(scr, colLeft, y, "Unreachable: none", tcell.StyleDefault)
|
||||
} else {
|
||||
var down []string
|
||||
down := make([]string, 0, len(st.Reachable))
|
||||
|
||||
for h, ok := range st.Reachable {
|
||||
if !ok {
|
||||
down = append(down, h)
|
||||
}
|
||||
}
|
||||
// Sort for consistent display
|
||||
|
||||
sort.Strings(down)
|
||||
// Add drop info to the end of the unreachable line
|
||||
Put(scr, 0, y, fmt.Sprintf("Unreachable: %s (last drop at %s, age %s)",
|
||||
strings.Join(down, ", "), dropTimeStr, dropAgeStr), CBrightRed)
|
||||
Put(scr, colLeft, y, fmt.Sprintf("Unreachable: %s (last drop at %s, age %s)",
|
||||
strings.Join(down, ", "), dropTimeStr, dropAgeStr), styleBrightRed())
|
||||
}
|
||||
y += 2
|
||||
|
||||
/* packet loss */
|
||||
Put(scr, 0, y, "Packet Loss:", CDefault)
|
||||
y++
|
||||
return y + blockGap
|
||||
}
|
||||
|
||||
// Get all hosts and sort them for consistent display order
|
||||
var lossHosts []string
|
||||
// drawPacketLoss renders the per-host packet-loss list. The caller holds
|
||||
// st.mu.RLock.
|
||||
func drawPacketLoss(scr tcell.Screen, y int, st *InterfaceStatus) int {
|
||||
Put(scr, colLeft, y, "Packet Loss:", tcell.StyleDefault)
|
||||
y += lineStep
|
||||
|
||||
lossHosts := make([]string, 0, len(st.Loss))
|
||||
for host := range st.Loss {
|
||||
lossHosts = append(lossHosts, host)
|
||||
}
|
||||
|
||||
sort.Strings(lossHosts)
|
||||
|
||||
// Find the maximum length of host names for alignment
|
||||
maxHostLen := 0
|
||||
for _, host := range lossHosts {
|
||||
if len(host) > maxHostLen {
|
||||
maxHostLen = len(host)
|
||||
}
|
||||
maxHostLen = max(maxHostLen, len(host))
|
||||
}
|
||||
|
||||
// Add 1 for the colon
|
||||
maxHostLen += 1
|
||||
maxHostLen++ // room for the colon
|
||||
|
||||
for _, host := range lossHosts {
|
||||
p := st.Loss[host] * 100
|
||||
// Use the maxHostLen for consistent alignment
|
||||
Put(scr, 0, y, fmt.Sprintf("%-*s %5.0f%%", maxHostLen, host+":", p), StyleLoss(p))
|
||||
y++
|
||||
p := st.Loss[host] * percentFull
|
||||
Put(scr, colLeft, y, fmt.Sprintf("%-*s %5.0f%%", maxHostLen, host+":", p),
|
||||
StyleLoss(p))
|
||||
y += lineStep
|
||||
}
|
||||
|
||||
dAge := "N/A"
|
||||
if !st.LastDrop.IsZero() {
|
||||
dAge = time.Since(st.LastDrop).Round(time.Second).String()
|
||||
}
|
||||
Put(scr, 0, y, fmt.Sprintf("Dropped: %d (last at %s, age %s)",
|
||||
st.DroppedCount, st.LastDrop.Format("15:04:05"), dAge), CDefault)
|
||||
y += 2
|
||||
|
||||
/* TCP table */
|
||||
Put(scr, 0, y, "TCP Connect Stats:", CDefault)
|
||||
y++
|
||||
// header row
|
||||
Put(scr, colLeft, y, fmt.Sprintf("Dropped: %d (last at %s, age %s)",
|
||||
st.DroppedCount, st.LastDrop.Format("15:04:05"), dAge), tcell.StyleDefault)
|
||||
|
||||
return y + blockGap
|
||||
}
|
||||
|
||||
// drawTCPTable renders the TCP connect-stats table. The caller holds
|
||||
// st.mu.RLock.
|
||||
func (m *Monitor) drawTCPTable(
|
||||
scr tcell.Screen, y int, st *InterfaceStatus, tcpHosts []string,
|
||||
) int {
|
||||
Put(scr, colLeft, y, "TCP Connect Stats:", tcell.StyleDefault)
|
||||
y += lineStep
|
||||
|
||||
headerRow := fmt.Sprintf("%-*s %*s %*s %*s %*s %*s %*s %*s",
|
||||
m.HostWidth, "Host", m.NumWidth, "last", m.NumWidth, "min", m.NumWidth, "avg",
|
||||
m.NumWidth, "max", m.StdWidth, "stddev", m.NWidth, "n", m.LostWidth, "lost")
|
||||
Put(scr, 0, y, headerRow, CDefault)
|
||||
y++
|
||||
|
||||
m.mu.RLock()
|
||||
tcpHosts := append([]string{}, m.tcpHosts...)
|
||||
m.mu.RUnlock()
|
||||
Put(scr, colLeft, y, headerRow, tcell.StyleDefault)
|
||||
y += lineStep
|
||||
|
||||
for _, hp := range tcpHosts {
|
||||
hist := st.TCP[hp]
|
||||
if len(hist) == 0 {
|
||||
continue
|
||||
}
|
||||
last := hist[len(hist)-1]
|
||||
mi, ma, av, sd := MinMaxAvgStd(hist)
|
||||
|
||||
// Get host name without port for lost packets lookup
|
||||
hostName := strings.Split(hp, ":")[0]
|
||||
lost := st.LostPackets[hostName]
|
||||
|
||||
// Add lost column
|
||||
row := fmt.Sprintf("%-*s %*s %*s %*s %*s %*s %*d %*d",
|
||||
m.HostWidth, hp,
|
||||
m.NumWidth, fmt.Sprintf("%.0fms", last),
|
||||
m.NumWidth, fmt.Sprintf("%.0fms", mi),
|
||||
m.NumWidth, fmt.Sprintf("%.0fms", av),
|
||||
m.NumWidth, fmt.Sprintf("%.0fms", ma),
|
||||
m.StdWidth, fmt.Sprintf("%.0fms", sd),
|
||||
m.NWidth, len(hist),
|
||||
m.LostWidth, lost,
|
||||
)
|
||||
Put(scr, 0, y, row, CDefault)
|
||||
// colourise individual numbers
|
||||
Put(scr, m.HostWidth+1, y, fmt.Sprintf("%*s", m.NumWidth, fmt.Sprintf("%.0fms", last)), StyleLatency(last))
|
||||
Put(scr, m.HostWidth+1+m.NumWidth+1, y, fmt.Sprintf("%*s", m.NumWidth, fmt.Sprintf("%.0fms", mi)), StyleLatency(mi))
|
||||
Put(scr, m.HostWidth+1+m.NumWidth*2+2, y, fmt.Sprintf("%*s", m.NumWidth, fmt.Sprintf("%.0fms", av)), StyleLatency(av))
|
||||
Put(scr, m.HostWidth+1+m.NumWidth*3+3, y, fmt.Sprintf("%*s", m.NumWidth, fmt.Sprintf("%.0fms", ma)), StyleLatency(ma))
|
||||
Put(scr, m.HostWidth+1+m.NumWidth*4+4, y, fmt.Sprintf("%*s", m.StdWidth, fmt.Sprintf("%.0fms", sd)), CDefault)
|
||||
// Colorize the lost packets column
|
||||
lostStyle := CDefault
|
||||
if lost > 0 {
|
||||
lostStyle = CRed
|
||||
}
|
||||
Put(scr, m.HostWidth+1+m.NumWidth*4+4+m.StdWidth+1+m.NWidth+1, y, fmt.Sprintf("%*d", m.LostWidth, lost), lostStyle)
|
||||
y++
|
||||
}
|
||||
y++
|
||||
|
||||
/* ICMP stats - combined into a single line with headers */
|
||||
// Calculate total lost packets
|
||||
lost := st.TotalICMPReq - st.TotalICMPRep
|
||||
if lost < 0 {
|
||||
lost = 0
|
||||
m.drawTCPRow(scr, y, st, hp, hist)
|
||||
y += lineStep
|
||||
}
|
||||
|
||||
// Create styles based on values
|
||||
lostStyle := CDefault
|
||||
if lost > 0 {
|
||||
lostStyle = CRed
|
||||
}
|
||||
|
||||
// Define column widths and positions
|
||||
const valWidth = 9
|
||||
|
||||
// Format the values with right alignment
|
||||
reqVal := fmt.Sprintf("%9d", st.TotalICMPReq)
|
||||
repVal := fmt.Sprintf("%9d", st.TotalICMPRep)
|
||||
lostVal := fmt.Sprintf("%9d", lost)
|
||||
|
||||
// Header texts with the same width as values for right alignment
|
||||
reqHeader := fmt.Sprintf("%9s", "Requests")
|
||||
repHeader := fmt.Sprintf("%9s", "Replies")
|
||||
lostHeader := fmt.Sprintf("%9s", "Lost")
|
||||
|
||||
// Draw the header line
|
||||
Put(scr, 0, y, " ", CDefault)
|
||||
Put(scr, 8, y, reqHeader, CDefault)
|
||||
Put(scr, 8+valWidth+4, y, repHeader, CDefault)
|
||||
Put(scr, 8+2*(valWidth+4), y, lostHeader, CDefault)
|
||||
y++
|
||||
|
||||
// Draw the values line
|
||||
Put(scr, 0, y, "ICMP: ", CDefault)
|
||||
Put(scr, 8, y, reqVal, CDefault)
|
||||
Put(scr, 8+valWidth+4, y, repVal, CDefault)
|
||||
Put(scr, 8+2*(valWidth+4), y, lostVal, lostStyle)
|
||||
|
||||
y += 2
|
||||
st.mu.RUnlock()
|
||||
return y
|
||||
return y + lineStep
|
||||
}
|
||||
|
||||
// uiLoop runs the UI event loop
|
||||
// drawTCPRow renders one TCP host's statistics row. The caller holds
|
||||
// st.mu.RLock.
|
||||
func (m *Monitor) drawTCPRow(
|
||||
scr tcell.Screen, y int, st *InterfaceStatus, hp string, hist []float64,
|
||||
) {
|
||||
last := hist[len(hist)-1]
|
||||
mi, ma, av, sd := MinMaxAvgStd(hist)
|
||||
|
||||
host := strings.Split(hp, ":")[0]
|
||||
lost := st.LostPackets[host]
|
||||
|
||||
row := fmt.Sprintf("%-*s %*s %*s %*s %*s %*s %*d %*d",
|
||||
m.HostWidth, hp,
|
||||
m.NumWidth, msStr(last),
|
||||
m.NumWidth, msStr(mi),
|
||||
m.NumWidth, msStr(av),
|
||||
m.NumWidth, msStr(ma),
|
||||
m.StdWidth, msStr(sd),
|
||||
m.NWidth, len(hist),
|
||||
m.LostWidth, lost,
|
||||
)
|
||||
Put(scr, colLeft, y, row, tcell.StyleDefault)
|
||||
|
||||
// Overlay the numeric columns colored by value, at the same offsets the
|
||||
// base row above laid them out.
|
||||
x := m.HostWidth + colGap
|
||||
Put(scr, x, y, fmt.Sprintf("%*s", m.NumWidth, msStr(last)), StyleLatency(last))
|
||||
x += m.NumWidth + colGap
|
||||
Put(scr, x, y, fmt.Sprintf("%*s", m.NumWidth, msStr(mi)), StyleLatency(mi))
|
||||
x += m.NumWidth + colGap
|
||||
Put(scr, x, y, fmt.Sprintf("%*s", m.NumWidth, msStr(av)), StyleLatency(av))
|
||||
x += m.NumWidth + colGap
|
||||
Put(scr, x, y, fmt.Sprintf("%*s", m.NumWidth, msStr(ma)), StyleLatency(ma))
|
||||
x += m.NumWidth + colGap
|
||||
Put(scr, x, y, fmt.Sprintf("%*s", m.StdWidth, msStr(sd)), tcell.StyleDefault)
|
||||
x += m.StdWidth + colGap
|
||||
x += m.NWidth + colGap // n column already drawn by the base row
|
||||
|
||||
lostStyle := tcell.StyleDefault
|
||||
if lost > 0 {
|
||||
lostStyle = styleRed()
|
||||
}
|
||||
|
||||
Put(scr, x, y, fmt.Sprintf("%*d", m.LostWidth, lost), lostStyle)
|
||||
}
|
||||
|
||||
// drawICMPStats renders the ICMP request/reply/lost summary. The caller
|
||||
// holds st.mu.RLock.
|
||||
func drawICMPStats(scr tcell.Screen, y int, st *InterfaceStatus) int {
|
||||
lost := max(st.TotalICMPReq-st.TotalICMPRep, 0)
|
||||
|
||||
lostStyle := tcell.StyleDefault
|
||||
if lost > 0 {
|
||||
lostStyle = styleRed()
|
||||
}
|
||||
|
||||
reqCol := icmpLabelWidth
|
||||
repCol := reqCol + icmpValWidth + icmpColGap
|
||||
lostCol := repCol + icmpValWidth + icmpColGap
|
||||
|
||||
Put(scr, reqCol, y, fmt.Sprintf("%*s", icmpValWidth, "Requests"), tcell.StyleDefault)
|
||||
Put(scr, repCol, y, fmt.Sprintf("%*s", icmpValWidth, "Replies"), tcell.StyleDefault)
|
||||
Put(scr, lostCol, y, fmt.Sprintf("%*s", icmpValWidth, "Lost"), tcell.StyleDefault)
|
||||
y += lineStep
|
||||
|
||||
reqStr := fmt.Sprintf("%*d", icmpValWidth, st.TotalICMPReq)
|
||||
repStr := fmt.Sprintf("%*d", icmpValWidth, st.TotalICMPRep)
|
||||
lostStr := fmt.Sprintf("%*d", icmpValWidth, lost)
|
||||
|
||||
Put(scr, colLeft, y, "ICMP: ", tcell.StyleDefault)
|
||||
Put(scr, reqCol, y, reqStr, tcell.StyleDefault)
|
||||
Put(scr, repCol, y, repStr, tcell.StyleDefault)
|
||||
Put(scr, lostCol, y, lostStr, lostStyle)
|
||||
|
||||
return y + blockGap
|
||||
}
|
||||
|
||||
// uiLoop runs the UI event loop.
|
||||
func (m *Monitor) uiLoop(ctx context.Context) {
|
||||
m.logf("UI loop started")
|
||||
|
||||
defer func() {
|
||||
m.logf("UI loop cleanup")
|
||||
m.screen.Clear()
|
||||
@@ -298,67 +345,53 @@ func (m *Monitor) uiLoop(ctx context.Context) {
|
||||
m.logf("Screen finalized")
|
||||
}()
|
||||
|
||||
// Load PST location
|
||||
pstLoc, err := time.LoadLocation("America/Los_Angeles")
|
||||
if err != nil {
|
||||
m.logf("Error loading PST location: %v", err)
|
||||
|
||||
pstLoc = time.UTC
|
||||
}
|
||||
|
||||
// Function to draw the screen
|
||||
timestampSpinFrame := 0
|
||||
lastTimestampSpinUpdate := time.Now()
|
||||
|
||||
drawScreen := func() {
|
||||
w, _ := m.screen.Size()
|
||||
m.screen.Clear()
|
||||
|
||||
// Draw the top horizontal line
|
||||
Put(m.screen, 0, 0, HLine(w), CDefault)
|
||||
Put(m.screen, colLeft, rowTopRule, HLine(w), tcell.StyleDefault)
|
||||
|
||||
// Get current time and format it
|
||||
now := time.Now()
|
||||
timeStr := now.Format(time.RFC1123Z)
|
||||
|
||||
// Format time in PST
|
||||
pstTimeStr := now.In(pstLoc).Format(time.RFC1123Z)
|
||||
|
||||
// Update the timestamp spinner once per second
|
||||
if time.Since(lastTimestampSpinUpdate) >= time.Second {
|
||||
timestampSpinFrame = (timestampSpinFrame + 1) % len(BrailleSpins)
|
||||
timestampSpinFrame = (timestampSpinFrame + 1) % len(brailleSpins())
|
||||
lastTimestampSpinUpdate = now
|
||||
}
|
||||
|
||||
// Get braille spinner character
|
||||
brailleChar := BrailleSpins[timestampSpinFrame]
|
||||
brailleChar := brailleSpins()[timestampSpinFrame]
|
||||
colors := rainbow()
|
||||
|
||||
// Draw the header with timestamp and spinner
|
||||
Put(m.screen, 0, 1, "== ", CDefault)
|
||||
Put(m.screen, 3, 1, string(brailleChar)+" ", CDefault)
|
||||
Put(m.screen, colLeft, rowClock, "== ", tcell.StyleDefault)
|
||||
Put(m.screen, colSpinner, rowClock, string(brailleChar)+" ", tcell.StyleDefault)
|
||||
DrawRainbowText(m.screen, colMeter, rowClock, timeStr, colors)
|
||||
DrawRainbowText(m.screen, colMeter+len(timeStr)+clockGap, rowClock,
|
||||
pstTimeStr, colors)
|
||||
Put(m.screen, colLeft, rowBottomRule, HLine(w), tcell.StyleDefault)
|
||||
|
||||
// Draw the timestamp with rainbow colors
|
||||
DrawRainbowText(m.screen, 5, 1, timeStr, CRainbow)
|
||||
runtime := time.Since(m.startTime).Round(time.Second).String()
|
||||
Put(m.screen, colLeft, rowRuntime, "Runtime: "+runtime, tcell.StyleDefault)
|
||||
|
||||
// Add 5 space gap and PST time with rainbow colors
|
||||
DrawRainbowText(m.screen, 5+len(timeStr)+5, 1, pstTimeStr, CRainbow)
|
||||
|
||||
// Draw the bottom horizontal line
|
||||
Put(m.screen, 0, 2, HLine(w), CDefault)
|
||||
|
||||
// Draw the runtime
|
||||
Put(m.screen, 0, 3, "Runtime: "+time.Since(m.startTime).Round(time.Second).String(), CDefault)
|
||||
|
||||
// Draw interfaces
|
||||
y := 5
|
||||
y := rowFirstIface
|
||||
y = m.DrawInterface(m.screen, y, w, m.interfaceA)
|
||||
_ = m.DrawInterface(m.screen, y, w, m.interfaceB)
|
||||
|
||||
// Show the screen
|
||||
m.screen.Show()
|
||||
}
|
||||
|
||||
// Initial draw
|
||||
drawScreen()
|
||||
|
||||
// Even without a ticker, ensure we update at least every second
|
||||
// This is a backup in case there are no spinner updates
|
||||
backupTicker := time.NewTicker(time.Second)
|
||||
defer backupTicker.Stop()
|
||||
|
||||
@@ -366,12 +399,11 @@ func (m *Monitor) uiLoop(ctx context.Context) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
m.logf("Context cancelled, exiting UI loop")
|
||||
|
||||
return
|
||||
case <-UIUpdateChan:
|
||||
// Update on spinner ticks (no rate limiting)
|
||||
case <-m.uiUpdate:
|
||||
drawScreen()
|
||||
case <-backupTicker.C:
|
||||
// Fallback to ensure we update at least once per second
|
||||
drawScreen()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user