diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-02 12:41:08 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-02 12:41:08 +0200 |
| commit | d0b9bc84aed1898a06a9d6fc3b82beee407d3cde (patch) | |
| tree | 7bb343960ee912d77dbc9f7720cd8cdd1f0172ea | |
| parent | bbc91e8764bd83c4497f2ddac86bb8947a91765c (diff) | |
Refactor display iteration/state and harden collector runtime
| -rw-r--r-- | internal/app/app.go | 41 | ||||
| -rw-r--r-- | internal/collector/collector.go | 5 | ||||
| -rw-r--r-- | internal/collector/types.go | 52 | ||||
| -rw-r--r-- | internal/display/cpu.go | 151 | ||||
| -rw-r--r-- | internal/display/disk.go | 8 | ||||
| -rw-r--r-- | internal/display/display.go | 652 | ||||
| -rw-r--r-- | internal/display/display_test.go | 26 | ||||
| -rw-r--r-- | internal/display/hittest.go | 74 | ||||
| -rw-r--r-- | internal/display/mem.go | 58 | ||||
| -rw-r--r-- | internal/display/net.go | 122 | ||||
| -rw-r--r-- | internal/stats/stats.go | 6 | ||||
| -rw-r--r-- | internal/stats/types.go | 46 |
12 files changed, 638 insertions, 603 deletions
diff --git a/internal/app/app.go b/internal/app/app.go index c18dc30..85c348a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -2,7 +2,11 @@ package app import ( "context" + "fmt" + "os" + "strings" "sync" + "time" "codeberg.org/snonux/loadbars/internal/collector" "codeberg.org/snonux/loadbars/internal/config" @@ -23,7 +27,7 @@ func Run(cfg *config.Config) error { wg.Add(1) go func() { defer wg.Done() - _ = collector.Run(ctx, h, cfg, store) + runCollectorLoop(ctx, h, cfg, store) }() } @@ -32,3 +36,38 @@ func Run(cfg *config.Config) error { wg.Wait() return err } + +func runCollectorLoop(ctx context.Context, host string, cfg *config.Config, store *Store) { + backoff := time.Second + for { + err := collector.Run(ctx, host, cfg, store) + if err == nil || ctx.Err() != nil { + return + } + fmt.Fprintf(os.Stderr, "!!! collector %s failed: %v\n", host, err) + if !isRemoteHost(host) { + return + } + timer := time.NewTimer(backoff) + select { + case <-ctx.Done(): + timer.Stop() + return + case <-timer.C: + } + if backoff < 30*time.Second { + backoff *= 2 + if backoff > 30*time.Second { + backoff = 30 * time.Second + } + } + } +} + +func isRemoteHost(host string) bool { + host = strings.TrimSpace(host) + if i := strings.Index(host, ":"); i >= 0 { + host = strings.TrimSpace(host[:i]) + } + return host != "localhost" && host != "127.0.0.1" +} diff --git a/internal/collector/collector.go b/internal/collector/collector.go index 5c99347..9ee7e02 100644 --- a/internal/collector/collector.go +++ b/internal/collector/collector.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "fmt" + "os" "os/exec" "strings" "time" @@ -163,8 +164,8 @@ func isLocal(h string) bool { // getLocalScript returns the appropriate script for the local OS func getLocalScript() []byte { - // Check if /proc exists (Linux/Unix) - if _, err := exec.Command("test", "-d", "/proc").CombinedOutput(); err == nil { + // Check if /proc exists (Linux/Unix). + if st, err := os.Stat("/proc"); err == nil && st.IsDir() { return LinuxScript } // /proc not found - unsupported OS for local stats gathering diff --git a/internal/collector/types.go b/internal/collector/types.go index 6d1ecf0..3fac47b 100644 --- a/internal/collector/types.go +++ b/internal/collector/types.go @@ -1,46 +1,10 @@ package collector -// CPULine is one line of /proc/stat: cpu name + counters (user, nice, system, idle, ...). -type CPULine struct { - Name string - User, Nice, System, Idle, Iowait, IRQ, SoftIRQ, Steal, Guest, GuestNice int64 -} - -// Total returns sum of all CPU counters. -func (c CPULine) Total() int64 { - return c.User + c.Nice + c.System + c.Idle + c.Iowait + c.IRQ + c.SoftIRQ + c.Steal + c.Guest + c.GuestNice -} - -// MemLine is one key from /proc/meminfo (e.g. MemTotal, MemFree). -type MemLine struct { - Key string - Value int64 -} - -// NetLine is one interface line: iface and key=value pairs (b, tb, p, tp, e, te, d, td). -type NetLine struct { - Iface string - B int64 // rx bytes - Tb int64 // tx bytes - P int64 - Tp int64 - E int64 - Te int64 - D int64 - Td int64 -} - -// LoadAvg is 1/5/15 min load average. -type LoadAvg struct { - Load1, Load5, Load15 string -} - -// DiskLine is one device from /proc/diskstats with cumulative counters. -type DiskLine struct { - Device string - SectorsRead int64 // cumulative sectors read (each sector = 512 bytes) - SectorsWrite int64 // cumulative sectors written - ReadTicks int64 // cumulative ms spent reading - WriteTicks int64 // cumulative ms spent writing - IoTicks int64 // cumulative ms the device had I/O in progress -} +import "codeberg.org/snonux/loadbars/internal/stats" + +// Collector parsing uses the shared stats protocol types. +type CPULine = stats.CPULine +type MemLine = stats.MemLine +type NetLine = stats.NetLine +type LoadAvg = stats.LoadAvg +type DiskLine = stats.DiskLine diff --git a/internal/display/cpu.go b/internal/display/cpu.go new file mode 100644 index 0000000..1b9edaf --- /dev/null +++ b/internal/display/cpu.go @@ -0,0 +1,151 @@ +package display + +import ( + "sort" + + "codeberg.org/snonux/loadbars/internal/constants" + "codeberg.org/snonux/loadbars/internal/stats" + "github.com/veandco/go-sdl2/sdl" +) + +func peakPctForBar(state *runState, key string, cpuAvg int, s *[10]float64) float64 { + if !state.extended || s == nil { + return 0 + } + userSys := (*s)[0] + (*s)[1] + hist := state.peakHistory[key] + hist = append(hist, userSys) + n := cpuAvg + if n < 1 { + n = 1 + } + for len(hist) > n { + hist = hist[1:] + } + state.peakHistory[key] = hist + var max float64 + for _, v := range hist { + if v > max { + max = v + } + } + return max +} + +func sortedCPUNames(cpu map[string]stats.CPULine, cpuMode int) []string { + if cpuMode == constants.CPUModeOff { + return nil + } + var names []string + for name := range cpu { + if name == "cpu" { + names = append(names, "cpu") + continue + } + if cpuMode == constants.CPUModeCores { + names = append(names, name) + } + } + sort.Slice(names, func(i, j int) bool { + if names[i] == "cpu" { + return true + } + if names[j] == "cpu" { + return false + } + return names[i] < names[j] + }) + return names +} + +// cpuBarTargetPcts returns the 9 segment percentages (system, user, nice, idle, iowait, irq, softirq, guest, steal) from cur/prev delta. +func cpuBarTargetPcts(cur, prev stats.CPULine) (out [10]float64, ok bool) { + totalCur := cur.Total() + totalPrev := prev.Total() + if totalPrev == 0 || totalCur <= totalPrev { + return out, false + } + scale := float64(totalCur-totalPrev) / 100.0 + if scale <= 0 { + return out, false + } + out[0] = float64(cur.System-prev.System) / scale + out[1] = float64(cur.User-prev.User) / scale + out[2] = float64(cur.Nice-prev.Nice) / scale + out[3] = float64(cur.Idle-prev.Idle) / scale + out[4] = float64(cur.Iowait-prev.Iowait) / scale + out[5] = float64(cur.IRQ-prev.IRQ) / scale + out[6] = float64(cur.SoftIRQ-prev.SoftIRQ) / scale + out[7] = float64(cur.Guest-prev.Guest) / scale + out[8] = float64(cur.Steal-prev.Steal) / scale + out[9] = float64(cur.GuestNice-prev.GuestNice) / scale + for i := range out { + if out[i] < 0 { + out[i] = 0 + } + if out[i] > 100 { + out[i] = 100 + } + } + return out, true +} + +func normalizePcts(s *[10]float64) { + var sum float64 + for i := 0; i < 10; i++ { + sum += (*s)[i] + } + if sum <= 0 { + return + } + for i := 0; i < 10; i++ { + (*s)[i] = (*s)[i] * 100 / sum + } +} + +func drawCPUBarFromPcts(renderer *sdl.Renderer, s *[10]float64, barW int32, x, y, barH int32, extended bool, peakPct float64) { + renderer.SetDrawColor(constants.Black.R, constants.Black.G, constants.Black.B, 255) + renderer.FillRect(&sdl.Rect{X: x, Y: y, W: barW, H: barH}) + if s == nil { + return + } + pxPerPct := float64(barH) / 100.0 + curY := float64(y + barH) + fill := func(r, g, b uint8, pct float64) { + hh := int32(pct * pxPerPct) + if hh < 1 && pct > 0 { + hh = 1 + } + curY -= float64(hh) + renderer.SetDrawColor(r, g, b, 255) + renderer.FillRect(&sdl.Rect{X: x, Y: int32(curY), W: barW, H: hh}) + } + fill(constants.Blue.R, constants.Blue.G, constants.Blue.B, (*s)[0]) + fill(constants.Yellow.R, constants.Yellow.G, constants.Yellow.B, (*s)[1]) + fill(constants.Green.R, constants.Green.G, constants.Green.B, (*s)[2]) + fill(constants.LimeGreen.R, constants.LimeGreen.G, constants.LimeGreen.B, (*s)[9]) + fill(constants.Black.R, constants.Black.G, constants.Black.B, (*s)[3]) + fill(constants.Purple.R, constants.Purple.G, constants.Purple.B, (*s)[4]) + fill(constants.White.R, constants.White.G, constants.White.B, (*s)[5]) + fill(constants.White.R, constants.White.G, constants.White.B, (*s)[6]) + fill(constants.Red.R, constants.Red.G, constants.Red.B, (*s)[7]) + fill(constants.Red.R, constants.Red.G, constants.Red.B, (*s)[8]) + if !extended || peakPct <= 0 { + return + } + peakY := y + barH - int32(peakPct*pxPerPct) + if peakY < y { + peakY = y + } + if peakY >= y+barH { + peakY = y + barH - 1 + } + if peakPct > float64(constants.UserOrangeThreshold) { + renderer.SetDrawColor(constants.Orange.R, constants.Orange.G, constants.Orange.B, 255) + } else if peakPct > float64(constants.UserYellowThreshold) { + renderer.SetDrawColor(constants.Yellow0.R, constants.Yellow0.G, constants.Yellow0.B, 255) + } else { + renderer.SetDrawColor(constants.Yellow.R, constants.Yellow.G, constants.Yellow.B, 255) + } + renderer.FillRect(&sdl.Rect{X: x, Y: peakY, W: barW, H: 1}) +} diff --git a/internal/display/disk.go b/internal/display/disk.go index 9d03ba9..8615b21 100644 --- a/internal/display/disk.go +++ b/internal/display/disk.go @@ -115,7 +115,7 @@ func updateDiskPeak(snap map[string]*stats.HostStats, state *runState, diskMax f // drawDiskBarSmoothed draws a single disk bar with read (top, purple) and write (bottom, // darker purple). Returns the current DiskStamp to be stored as previous for the next frame. -func drawDiskBarSmoothed(renderer *sdl.Renderer, cur stats.DiskStamp, cfg *runState, smoothed *struct{ readPct, writePct float64 }, prev stats.DiskStamp, factor float64, barW, x, y, barH int32, extended bool) stats.DiskStamp { +func drawDiskBarSmoothed(renderer *sdl.Renderer, cur stats.DiskStamp, state *runState, smoothed *struct{ readPct, writePct float64 }, prev stats.DiskStamp, factor float64, barW, x, y, barH int32, extended bool) stats.DiskStamp { // Clear this slot to a dim purple so the bar is visible even when idle. // This distinguishes "disk bar present but idle" from background. renderer.SetDrawColor(0x18, 0x00, 0x28, 255) @@ -123,7 +123,7 @@ func drawDiskBarSmoothed(renderer *sdl.Renderer, cur stats.DiskStamp, cfg *runSt // Only recompute when the collector has provided new data (same guard as net bars). if cur.Stamp > prev.Stamp && prev.Stamp > 0 { - prev = smoothDiskUtilization(cur, prev, cfg, smoothed, factor) + prev = smoothDiskUtilization(cur, prev, state, smoothed, factor) } else if prev.Stamp == 0 && cur.Stamp > 0 { // First sample: record it but can't compute delta yet. prev = cur @@ -133,7 +133,7 @@ func drawDiskBarSmoothed(renderer *sdl.Renderer, cur stats.DiskStamp, cfg *runSt // In extended mode, overlay a utilization % line if extended && prev.Stamp > 0 { - drawDiskUtilLine(renderer, cur, prev, cfg, x, y, barW, barH) + drawDiskUtilLine(renderer, cur, prev, x, y, barW, barH) } return prev } @@ -192,7 +192,7 @@ func drawDiskHalves(renderer *sdl.Renderer, smoothed *struct{ readPct, writePct // drawDiskUtilLine draws a 3px-thick horizontal line showing disk utilization % // (fraction of time the device had I/O in progress) in extended mode. -func drawDiskUtilLine(renderer *sdl.Renderer, cur, prev stats.DiskStamp, state *runState, x, y, barW, barH int32) { +func drawDiskUtilLine(renderer *sdl.Renderer, cur, prev stats.DiskStamp, x, y, barW, barH int32) { dt := cur.Stamp - prev.Stamp if dt <= 0 { return diff --git a/internal/display/display.go b/internal/display/display.go index 514a62f..f5c545c 100644 --- a/internal/display/display.go +++ b/internal/display/display.go @@ -9,7 +9,6 @@ import ( "strings" "time" - "codeberg.org/snonux/loadbars/internal/collector" "codeberg.org/snonux/loadbars/internal/config" "codeberg.org/snonux/loadbars/internal/constants" "codeberg.org/snonux/loadbars/internal/stats" @@ -25,32 +24,48 @@ const smoothFactor = 0.12 // used by the f/v hotkeys to cycle through link scale values. var linkScales = []string{"mbit", "10mbit", "100mbit", "gbit", "10gbit"} -// runState holds mutable state across the display loop (hotkeys, window size, smoothed data). -type runState struct { +type displayFlags struct { showAvgLine bool showIOAvgLine bool cpuMode int // constants.CPUModeAverage / CPUModeCores / CPUModeOff showMem bool showNet bool showLoad bool - loadPeak float64 // global max load1 across all hosts (for bar scaling) showSeparators bool extended bool - winW int32 - winH int32 - prevCPU map[string]collector.CPULine - smoothedCPU map[string]*[10]float64 - smoothedMem map[string]*struct{ ramUsed, swapUsed float64 } - smoothedNet map[string]*struct{ rxPct, txPct float64 } - prevNet map[string]stats.NetStamp // aggregated (summed) previous net stamp per host - peakHistory map[string][]float64 - diskMode int // constants.DiskModeAggregate / DiskModeDevices / DiskModeOff - diskPeak float64 // auto-scale peak (bytes/sec) for disk bars - prevDisk map[string]stats.DiskStamp // previous disk stamp per host+device key - smoothedDisk map[string]*struct{ readPct, writePct float64 } - mouseX int32 // last known mouse X position (for tooltip hit testing) - mouseY int32 // last known mouse Y position (for tooltip hit testing) - mouseLastMove time.Time // timestamp of last mouse movement; tooltip hidden after 3s idle + diskMode int // constants.DiskModeAggregate / DiskModeDevices / DiskModeOff +} + +type smoothState struct { + prevCPU map[string]stats.CPULine + smoothedCPU map[string]*[10]float64 + smoothedMem map[string]*struct{ ramUsed, swapUsed float64 } + smoothedNet map[string]*struct{ rxPct, txPct float64 } + prevNet map[string]stats.NetStamp // aggregated (summed) previous net stamp per host + peakHistory map[string][]float64 + prevDisk map[string]stats.DiskStamp + smoothedDisk map[string]*struct{ readPct, writePct float64 } +} + +type peakState struct { + loadPeak float64 // global max load1 across all hosts (for bar scaling) + diskPeak float64 // auto-scale peak (bytes/sec) for disk bars +} + +type mouseState struct { + mouseX int32 // last known mouse X position (for tooltip hit testing) + mouseY int32 // last known mouse Y position (for tooltip hit testing) + mouseLastMove time.Time // timestamp of last mouse movement; tooltip hidden after 3s idle +} + +// runState holds mutable state across the display loop (hotkeys, window size, smoothed data). +type runState struct { + displayFlags + smoothState + peakState + mouseState + winW int32 + winH int32 } // newRunState builds initial run state from config. @@ -66,29 +81,37 @@ func newRunState(cfg *config.Config, winW, winH int32) *runState { initDiskPeak = cfg.DiskMax } return &runState{ - showAvgLine: cfg.ShowAvgLine, - showIOAvgLine: cfg.ShowIOAvgLine, - cpuMode: cfg.CPUMode, - showMem: cfg.ShowMem, - showNet: cfg.ShowNet, - showLoad: cfg.ShowLoad, - loadPeak: initLoadPeak, - showSeparators: cfg.ShowSeparators, - extended: cfg.Extended, - winW: winW, - winH: winH, - prevCPU: make(map[string]collector.CPULine), - smoothedCPU: make(map[string]*[10]float64), - smoothedMem: make(map[string]*struct{ ramUsed, swapUsed float64 }), - smoothedNet: make(map[string]*struct{ rxPct, txPct float64 }), - prevNet: make(map[string]stats.NetStamp), - peakHistory: make(map[string][]float64), - diskMode: cfg.DiskMode, - diskPeak: initDiskPeak, - prevDisk: make(map[string]stats.DiskStamp), - smoothedDisk: make(map[string]*struct{ readPct, writePct float64 }), - mouseX: -1, // off-screen until first mouse move - mouseY: -1, + displayFlags: displayFlags{ + showAvgLine: cfg.ShowAvgLine, + showIOAvgLine: cfg.ShowIOAvgLine, + cpuMode: cfg.CPUMode, + showMem: cfg.ShowMem, + showNet: cfg.ShowNet, + showLoad: cfg.ShowLoad, + showSeparators: cfg.ShowSeparators, + extended: cfg.Extended, + diskMode: cfg.DiskMode, + }, + smoothState: smoothState{ + prevCPU: make(map[string]stats.CPULine), + smoothedCPU: make(map[string]*[10]float64), + smoothedMem: make(map[string]*struct{ ramUsed, swapUsed float64 }), + smoothedNet: make(map[string]*struct{ rxPct, txPct float64 }), + prevNet: make(map[string]stats.NetStamp), + peakHistory: make(map[string][]float64), + prevDisk: make(map[string]stats.DiskStamp), + smoothedDisk: make(map[string]*struct{ readPct, writePct float64 }), + }, + peakState: peakState{ + loadPeak: initLoadPeak, + diskPeak: initDiskPeak, + }, + mouseState: mouseState{ + mouseX: -1, // off-screen until first mouse move + mouseY: -1, + }, + winW: winW, + winH: winH, } } @@ -192,53 +215,17 @@ func handleKey(sym sdl.Keycode, window *sdl.Window, cfg *config.Config, state *r func handleToggleKeys(sym sdl.Keycode, cfg *config.Config, state *runState) { switch sym { case sdl.K_1: - // Cycle through three CPU display modes: average → all cores → off → average - state.cpuMode = (state.cpuMode + 1) % constants.CPUModeCount - switch state.cpuMode { - case constants.CPUModeAverage: - fmt.Println("==> CPU: average bar only") - case constants.CPUModeCores: - fmt.Println("==> CPU: individual cores") - case constants.CPUModeOff: - fmt.Println("==> CPU: off") - } + cycleCPUMode(state) case sdl.K_2, sdl.K_m: - state.showMem = !state.showMem - fmt.Println("==> Toggled show mem:", state.showMem) + toggleMem(state) case sdl.K_3, sdl.K_n: - state.showNet = !state.showNet - fmt.Println("==> Toggled show net:", state.showNet) + toggleNet(state) case sdl.K_4, sdl.K_l: - state.showLoad = !state.showLoad - fmt.Println("==> Toggled show load:", state.showLoad) + toggleLoad(state) case sdl.K_5: - // Cycle through three disk display modes: aggregate → devices → off → aggregate - state.diskMode = (state.diskMode + 1) % constants.DiskModeCount - switch state.diskMode { - case constants.DiskModeAggregate: - fmt.Println("==> Disk: aggregate (all devices)") - case constants.DiskModeDevices: - fmt.Println("==> Disk: per-device") - case constants.DiskModeOff: - fmt.Println("==> Disk: off") - } + cycleDiskMode(state) case sdl.K_r: - // Reset load auto-scale peak to the floor so the bar rescales immediately. - // Has no effect when loadmax is fixed (cfg.LoadMax > 0). - if cfg.LoadMax == 0 { - state.loadPeak = 2.0 - fmt.Println("==> Load peak reset to auto-scale floor (2.0)") - } else { - fmt.Println("==> Load peak reset ignored (fixed loadmax =", cfg.LoadMax, ")") - } - // Reset disk auto-scale peak to the floor when disk bars are on and diskmax is not fixed. - if state.diskMode != constants.DiskModeOff && cfg.DiskMax == 0 { - const diskPeakFloorBps = 1048576.0 // 1 MB/s, same as in updateDiskPeak - state.diskPeak = diskPeakFloorBps - fmt.Println("==> Disk peak reset to auto-scale floor (1 MB/s)") - } else if state.diskMode != constants.DiskModeOff && cfg.DiskMax > 0 { - fmt.Println("==> Disk peak reset ignored (fixed diskmax =", cfg.DiskMax, ")") - } + resetAutoScalePeaks(cfg, state) case sdl.K_e: state.extended = !state.extended fmt.Println("==> Toggled extended (peak line):", state.extended) @@ -254,6 +241,64 @@ func handleToggleKeys(sym sdl.Keycode, cfg *config.Config, state *runState) { } } +func cycleCPUMode(state *runState) { + state.cpuMode = (state.cpuMode + 1) % constants.CPUModeCount + switch state.cpuMode { + case constants.CPUModeAverage: + fmt.Println("==> CPU: average bar only") + case constants.CPUModeCores: + fmt.Println("==> CPU: individual cores") + case constants.CPUModeOff: + fmt.Println("==> CPU: off") + } +} + +func cycleDiskMode(state *runState) { + state.diskMode = (state.diskMode + 1) % constants.DiskModeCount + switch state.diskMode { + case constants.DiskModeAggregate: + fmt.Println("==> Disk: aggregate (all devices)") + case constants.DiskModeDevices: + fmt.Println("==> Disk: per-device") + case constants.DiskModeOff: + fmt.Println("==> Disk: off") + } +} + +func toggleMem(state *runState) { + state.showMem = !state.showMem + fmt.Println("==> Toggled show mem:", state.showMem) +} + +func toggleNet(state *runState) { + state.showNet = !state.showNet + fmt.Println("==> Toggled show net:", state.showNet) +} + +func toggleLoad(state *runState) { + state.showLoad = !state.showLoad + fmt.Println("==> Toggled show load:", state.showLoad) +} + +func resetAutoScalePeaks(cfg *config.Config, state *runState) { + if cfg.LoadMax == 0 { + state.loadPeak = 2.0 + fmt.Println("==> Load peak reset to auto-scale floor (2.0)") + } else { + fmt.Println("==> Load peak reset ignored (fixed loadmax =", cfg.LoadMax, ")") + } + if state.diskMode == constants.DiskModeOff { + return + } + if cfg.DiskMax == 0 { + const diskPeakFloorBps = 1048576.0 // 1 MB/s, same as in updateDiskPeak + state.diskPeak = diskPeakFloorBps + fmt.Println("==> Disk peak reset to auto-scale floor (1 MB/s)") + return + } + fmt.Println("==> Disk peak reset ignored (fixed diskmax =", cfg.DiskMax, ")") +} + // handleAdjustAndSave processes sampling-adjust and config-write hotkeys (a, y, d, c, f, v, h, w). func handleAdjustAndSave(sym sdl.Keycode, cfg *config.Config, state *runState) { switch sym { @@ -433,23 +478,22 @@ func countBars(snap map[string]*stats.HostStats, cpuMode int, showMem, showNet, // drawBars draws CPU, memory, and network bars for all hosts in snap. // Bars wrap into multiple rows when cfg.MaxBarsPerRow is set. func drawBars(renderer *sdl.Renderer, snap map[string]*stats.HostStats, cfg *config.Config, state *runState, numBars int) { - barIndex := 0 - hosts := sortedHosts(snap) - maxPerRow := cfg.MaxBarsPerRow + bars := buildBarMap(snap, cfg, state) // Track separator rects (position + row height) for drawing after all bars type sepRect struct{ x, y, h int32 } var separators []sepRect - for i, host := range hosts { - h := snap[host] + prevHost := "" + for i := range bars { + bar := bars[i] + h := snap[bar.host] if h == nil { continue } - drawHostBars(renderer, h, host, cfg, state, numBars, maxPerRow, &barIndex) - // Record separator position between hosts (not after the last one) - if state.showSeparators && i < len(hosts)-1 { - sx, sy, _, sh := barRect(state.winW, state.winH, numBars, maxPerRow, barIndex) - separators = append(separators, sepRect{sx, sy, sh}) + if state.showSeparators && prevHost != "" && bar.host != prevHost { + separators = append(separators, sepRect{bar.rect.X, bar.rect.Y, bar.rect.H}) } + drawBar(renderer, h, bar, cfg, state) + prevHost = bar.host } // Draw 1px red vertical separators on top of all bars (same color as CPU steal) for _, sep := range separators { @@ -491,10 +535,7 @@ func drawGlobalAvgLine(renderer *sdl.Renderer, snap map[string]*stats.HostStats, avgPct := totalUsage / float64(hostCount) renderer.SetDrawColor(constants.Red.R, constants.Red.G, constants.Red.B, 255) // Draw one line per row, positioned proportionally within each row's height - numRows := 1 - if maxPerRow > 0 && maxPerRow < numBars { - numRows = (numBars + maxPerRow - 1) / maxPerRow - } + numRows := countRows(numBars, maxPerRow) for row := 0; row < numRows; row++ { rowY := (state.winH * int32(row)) / int32(numRows) rowH := (state.winH*int32(row+1))/int32(numRows) - rowY @@ -536,10 +577,7 @@ func drawGlobalIOAvgLine(renderer *sdl.Renderer, snap map[string]*stats.HostStat avgPct := totalIO / float64(hostCount) renderer.SetDrawColor(constants.Pink.R, constants.Pink.G, constants.Pink.B, 255) // Draw one line per row, positioned proportionally from the top of each row - numRows := 1 - if maxPerRow > 0 && maxPerRow < numBars { - numRows = (numBars + maxPerRow - 1) / maxPerRow - } + numRows := countRows(numBars, maxPerRow) for row := 0; row < numRows; row++ { rowY := (state.winH * int32(row)) / int32(numRows) rowH := (state.winH*int32(row+1))/int32(numRows) - rowY @@ -554,13 +592,19 @@ func drawGlobalIOAvgLine(renderer *sdl.Renderer, snap map[string]*stats.HostStat } } -// drawHostBars draws CPU, mem, and net bars for one host and advances barIndex. -// maxPerRow controls multi-row wrapping (0 = single row). -func drawHostBars(renderer *sdl.Renderer, h *stats.HostStats, host string, cfg *config.Config, state *runState, numBars, maxPerRow int, barIndex *int) { - cpuNames := sortedCPUNames(h.CPU, state.cpuMode) - for _, name := range cpuNames { - key := host + ";" + name - cur := h.CPU[name] +func countRows(numBars, maxPerRow int) int { + if maxPerRow > 0 && maxPerRow < numBars { + return (numBars + maxPerRow - 1) / maxPerRow + } + return 1 +} + +func drawBar(renderer *sdl.Renderer, h *stats.HostStats, bar barDescriptor, cfg *config.Config, state *runState) { + x, y, barW, barH := bar.rect.X, bar.rect.Y, bar.rect.W, bar.rect.H + switch bar.kind { + case barCPU: + key := bar.host + ";" + bar.cpuName + cur := h.CPU[bar.cpuName] prev := state.prevCPU[key] state.prevCPU[key] = cur target, ok := cpuBarTargetPcts(cur, prev) @@ -578,74 +622,32 @@ func drawHostBars(renderer *sdl.Renderer, h *stats.HostStats, host string, cfg * normalizePcts(s) } peakPct := peakPctForBar(state, key, cfg.CPUAverage, s) - x, y, barW, barH := barRect(state.winW, state.winH, numBars, maxPerRow, *barIndex) - *barIndex++ drawCPUBarFromPcts(renderer, s, barW, x, y, barH, state.extended, peakPct) - } - if state.showMem { - if state.smoothedMem[host] == nil { - state.smoothedMem[host] = &struct{ ramUsed, swapUsed float64 }{} + case barMem: + if state.smoothedMem[bar.host] == nil { + state.smoothedMem[bar.host] = &struct{ ramUsed, swapUsed float64 }{} } - x, y, barW, barH := barRect(state.winW, state.winH, numBars, maxPerRow, *barIndex) - *barIndex++ - drawMemBarSmoothed(renderer, h, state.smoothedMem[host], smoothFactor, barW, x, y, barH) - } - if state.showNet { - if state.smoothedNet[host] == nil { - state.smoothedNet[host] = &struct{ rxPct, txPct float64 }{} + drawMemBarSmoothed(renderer, h, state.smoothedMem[bar.host], smoothFactor, barW, x, y, barH) + case barNet: + if state.smoothedNet[bar.host] == nil { + state.smoothedNet[bar.host] = &struct{ rxPct, txPct float64 }{} } - x, y, barW, barH := barRect(state.winW, state.winH, numBars, maxPerRow, *barIndex) - *barIndex++ - state.prevNet[host] = drawNetBarSmoothed(renderer, h, cfg, state.smoothedNet[host], state.prevNet[host], smoothFactor, barW, x, y, barH) - } - if state.showLoad { - x, y, barW, barH := barRect(state.winW, state.winH, numBars, maxPerRow, *barIndex) - *barIndex++ + state.prevNet[bar.host] = drawNetBarSmoothed(renderer, h, cfg, state.smoothedNet[bar.host], state.prevNet[bar.host], smoothFactor, barW, x, y, barH) + case barLoad: drawLoadAvgBar(renderer, h, state.loadPeak, barW, x, y, barH) - } - // Disk I/O bars: aggregate (one bar) or per-device based on diskMode - diskNames := sortedDiskNames(h.Disk, state.diskMode) - for _, dname := range diskNames { - key := host + ";disk;" + dname - var cur stats.DiskStamp - if dname == "all" { + case barDisk: + key := bar.host + ";disk;" + bar.diskName + cur := h.Disk[bar.diskName] + if bar.diskName == "all" { cur = sumAllDisks(h.Disk) - } else { - cur = h.Disk[dname] } if state.smoothedDisk[key] == nil { state.smoothedDisk[key] = &struct{ readPct, writePct float64 }{} } - x, y, barW, barH := barRect(state.winW, state.winH, numBars, maxPerRow, *barIndex) - *barIndex++ state.prevDisk[key] = drawDiskBarSmoothed(renderer, cur, state, state.smoothedDisk[key], state.prevDisk[key], smoothFactor, barW, x, y, barH, state.extended) } } -func peakPctForBar(state *runState, key string, cpuAvg int, s *[10]float64) float64 { - if !state.extended || s == nil { - return 0 - } - userSys := (*s)[0] + (*s)[1] - hist := state.peakHistory[key] - hist = append(hist, userSys) - n := cpuAvg - if n < 1 { - n = 1 - } - for len(hist) > n { - hist = hist[1:] - } - state.peakHistory[key] = hist - var max float64 - for _, v := range hist { - if v > max { - max = v - } - } - return max -} - func sortedHosts(snap map[string]*stats.HostStats) []string { out := make([]string, 0, len(snap)) for h := range snap { @@ -655,189 +657,6 @@ func sortedHosts(snap map[string]*stats.HostStats) []string { return out } -func sortedCPUNames(cpu map[string]collector.CPULine, cpuMode int) []string { - // CPUModeOff: hide all CPU bars - if cpuMode == constants.CPUModeOff { - return nil - } - var names []string - for name := range cpu { - if name == "cpu" { - // Aggregate bar always shown unless CPUModeOff - names = append(names, "cpu") - continue - } - // Individual core bars only shown in CPUModeCores - if cpuMode == constants.CPUModeCores { - names = append(names, name) - } - } - sort.Slice(names, func(i, j int) bool { - if names[i] == "cpu" { - return true - } - if names[j] == "cpu" { - return false - } - return names[i] < names[j] - }) - return names -} - -// cpuBarTargetPcts returns the 9 segment percentages (system, user, nice, idle, iowait, irq, softirq, guest, steal) from cur/prev delta. ok is false if no valid sample. -func cpuBarTargetPcts(cur, prev collector.CPULine) (out [10]float64, ok bool) { - totalCur := cur.Total() - totalPrev := prev.Total() - if totalPrev == 0 || totalCur <= totalPrev { - return out, false - } - scale := float64(totalCur-totalPrev) / 100.0 - if scale <= 0 { - return out, false - } - out[0] = float64(cur.System-prev.System) / scale - out[1] = float64(cur.User-prev.User) / scale - out[2] = float64(cur.Nice-prev.Nice) / scale - out[3] = float64(cur.Idle-prev.Idle) / scale - out[4] = float64(cur.Iowait-prev.Iowait) / scale - out[5] = float64(cur.IRQ-prev.IRQ) / scale - out[6] = float64(cur.SoftIRQ-prev.SoftIRQ) / scale - out[7] = float64(cur.Guest-prev.Guest) / scale - out[8] = float64(cur.Steal-prev.Steal) / scale - out[9] = float64(cur.GuestNice-prev.GuestNice) / scale - for i := range out { - if out[i] < 0 { - out[i] = 0 - } - if out[i] > 100 { - out[i] = 100 - } - } - return out, true -} - -func normalizePcts(s *[10]float64) { - var sum float64 - for i := 0; i < 10; i++ { - sum += (*s)[i] - } - if sum <= 0 { - return - } - for i := 0; i < 10; i++ { - (*s)[i] = (*s)[i] * 100 / sum - } -} - -// drawCPUBarFromPcts draws one CPU bar from 10 smoothed segment percentages. -// The bar occupies the region (x, y) with dimensions (barW, barH). -// If s is nil, only clears the slot. When extended is true and peakPct > 0, -// draws a 1px peak line (max system+user over history). -func drawCPUBarFromPcts(renderer *sdl.Renderer, s *[10]float64, barW int32, x, y, barH int32, extended bool, peakPct float64) { - // Clear this slot so we never leave previous (e.g. mem/net) content visible - renderer.SetDrawColor(constants.Black.R, constants.Black.G, constants.Black.B, 255) - renderer.FillRect(&sdl.Rect{X: x, Y: y, W: barW, H: barH}) - if s == nil { - return - } - pxPerPct := float64(barH) / 100.0 - curY := float64(y + barH) - fill := func(r, g, b uint8, pct float64) { - hh := int32(pct * pxPerPct) - if hh < 1 && pct > 0 { - hh = 1 - } - curY -= float64(hh) - renderer.SetDrawColor(r, g, b, 255) - renderer.FillRect(&sdl.Rect{X: x, Y: int32(curY), W: barW, H: hh}) - } - fill(constants.Blue.R, constants.Blue.G, constants.Blue.B, (*s)[0]) // system - fill(constants.Yellow.R, constants.Yellow.G, constants.Yellow.B, (*s)[1]) // user - fill(constants.Green.R, constants.Green.G, constants.Green.B, (*s)[2]) // nice - fill(constants.LimeGreen.R, constants.LimeGreen.G, constants.LimeGreen.B, (*s)[9]) // guestnice - fill(constants.Black.R, constants.Black.G, constants.Black.B, (*s)[3]) // idle - fill(constants.Purple.R, constants.Purple.G, constants.Purple.B, (*s)[4]) // iowait - fill(constants.White.R, constants.White.G, constants.White.B, (*s)[5]) // irq - fill(constants.White.R, constants.White.G, constants.White.B, (*s)[6]) // softirq - fill(constants.Red.R, constants.Red.G, constants.Red.B, (*s)[7]) // guest - fill(constants.Red.R, constants.Red.G, constants.Red.B, (*s)[8]) // steal - // Extended: 1px peak line at max (system+user) over history - if extended && peakPct > 0 { - peakY := y + barH - int32(peakPct*pxPerPct) - if peakY < y { - peakY = y - } - if peakY >= y+barH { - peakY = y + barH - 1 - } - if peakPct > float64(constants.UserOrangeThreshold) { - renderer.SetDrawColor(constants.Orange.R, constants.Orange.G, constants.Orange.B, 255) - } else if peakPct > float64(constants.UserYellowThreshold) { - renderer.SetDrawColor(constants.Yellow0.R, constants.Yellow0.G, constants.Yellow0.B, 255) - } else { - renderer.SetDrawColor(constants.Yellow.R, constants.Yellow.G, constants.Yellow.B, 255) - } - renderer.FillRect(&sdl.Rect{X: x, Y: peakY, W: barW, H: 1}) - } -} - -// drawMemBarSmoothed blends mem stats toward target and draws one memory bar (RAM left, Swap right). -// The bar occupies the region (x, y) with dimensions (barW, barH). -func drawMemBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, smoothed *struct{ ramUsed, swapUsed float64 }, factor float64, barW int32, x, y, barH int32) { - // Clear this slot so we never leave previous (e.g. CPU/net) content visible - renderer.SetDrawColor(constants.Black.R, constants.Black.G, constants.Black.B, 255) - renderer.FillRect(&sdl.Rect{X: x, Y: y, W: barW, H: barH}) - if h.Mem == nil { - return - } - var targetRam, targetSwap float64 - if memTotal := h.Mem["MemTotal"]; memTotal > 0 { - targetRam = 100 - 100*float64(h.Mem["MemFree"])/float64(memTotal) - if targetRam < 0 { - targetRam = 0 - } - if targetRam > 100 { - targetRam = 100 - } - } - if swapTotal := h.Mem["SwapTotal"]; swapTotal > 0 { - targetSwap = |
