summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/app/app.go41
-rw-r--r--internal/collector/collector.go5
-rw-r--r--internal/collector/types.go52
-rw-r--r--internal/display/cpu.go151
-rw-r--r--internal/display/disk.go8
-rw-r--r--internal/display/display.go652
-rw-r--r--internal/display/display_test.go26
-rw-r--r--internal/display/hittest.go74
-rw-r--r--internal/display/mem.go58
-rw-r--r--internal/display/net.go122
-rw-r--r--internal/stats/stats.go6
-rw-r--r--internal/stats/types.go46
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 = 100 - 100*float64(h.Mem["SwapFree"])/float64(swapTotal)
- if targetSwap < 0 {
- targetSwap = 0
- }
- if targetSwap > 100 {
- targetSwap = 100
- }
- }
- smoothed.ramUsed += (targetRam - smoothed.ramUsed) * factor
- smoothed.swapUsed += (targetSwap - smoothed.swapUsed) * factor
-
- halfW := barW / 2
- pxPerPct := float64(barH) / 100.0
-</