summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-14 22:22:57 +0200
committerPaul Buetow <paul@buetow.org>2026-02-14 22:22:57 +0200
commitd0bed33cf41ac4917a9427c98e63351367d71298 (patch)
tree9a49a06516aa20dc78a42fa873d6f77a0e411488 /internal
parent2265b31a6eeaae8d6aac52e1fa32a33863733192 (diff)
Aggregate all net interfaces, remove n hotkey, fix net bar decay bugv0.9.0
Sum RX/TX across all non-lo interfaces instead of picking a single one. Remove the n hotkey (cycle interface), netint config field, and --netint flag since they are no longer needed. Fix pre-existing bug where net bars decayed to zero between collector updates (~19 of 20 frames had target=0, making bars invisible even during heavy downloads). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/config/config.go6
-rw-r--r--internal/display/display.go185
-rw-r--r--internal/display/display_test.go80
-rw-r--r--internal/version/version.go2
4 files changed, 132 insertions, 141 deletions
diff --git a/internal/config/config.go b/internal/config/config.go
index e523372..0fc80d4 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -23,7 +23,6 @@ type Config struct {
Height int
MaxWidth int
NetAverage int
- NetInt string
NetLink string
ShowCores bool
ShowMem bool
@@ -104,7 +103,7 @@ func (c *Config) parseReader(f *os.File) error {
validKeys := map[string]bool{
"title": true, "barwidth": true, "cpuaverage": true, "extended": true,
"hasagent": true, "height": true, "maxwidth": true, "netaverage": true,
- "netint": true, "netlink": true, "showcores": true, "showmem": true,
+ "netlink": true, "showcores": true, "showmem": true,
"shownet": true, "sshopts": true, "cluster": true,
}
scanner := bufio.NewScanner(f)
@@ -158,8 +157,6 @@ func (c *Config) set(key, val string) {
if n, err := strconv.Atoi(val); err == nil {
c.NetAverage = n
}
- case "netint":
- c.NetInt = val
case "netlink":
c.NetLink = val
case "showcores":
@@ -198,7 +195,6 @@ func (c *Config) writeTo(f *os.File) error {
writeInt("height", c.Height)
writeInt("maxwidth", c.MaxWidth)
writeInt("netaverage", c.NetAverage)
- writeStr("netint", c.NetInt)
writeStr("netlink", c.NetLink)
writeBool("showcores", c.ShowCores)
writeBool("showmem", c.ShowMem)
diff --git a/internal/display/display.go b/internal/display/display.go
index b5ed092..3b4dd42 100644
--- a/internal/display/display.go
+++ b/internal/display/display.go
@@ -21,40 +21,35 @@ const smoothFactor = 0.12 // blend toward target each frame; lower = smoother
// runState holds mutable state across the display loop (hotkeys, window size, smoothed data).
type runState struct {
- showCores bool
- showMem bool
- showNet bool
- extended bool
- winW int32
- winH int32
- prevCPU map[string]collector.CPULine
- smoothedCPU map[string]*[9]float64
- smoothedMem map[string]*struct{ ramUsed, swapUsed float64 }
- smoothedNet map[string]*struct{ rxPct, txPct float64 }
- prevNet map[string]stats.NetStamp
- netIntIndex map[string]int
- cycleNetNext bool
- printNetInfoOnce bool
- peakHistory map[string][]float64
+ showCores bool
+ showMem bool
+ showNet bool
+ extended bool
+ winW int32
+ winH int32
+ prevCPU map[string]collector.CPULine
+ smoothedCPU map[string]*[9]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
}
// newRunState builds initial run state from config.
func newRunState(cfg *config.Config, winW, winH int32) *runState {
return &runState{
- showCores: cfg.ShowCores,
- showMem: cfg.ShowMem,
- showNet: cfg.ShowNet,
- extended: cfg.Extended,
- winW: winW,
- winH: winH,
- prevCPU: make(map[string]collector.CPULine),
- smoothedCPU: make(map[string]*[9]float64),
- smoothedMem: make(map[string]*struct{ ramUsed, swapUsed float64 }),
- smoothedNet: make(map[string]*struct{ rxPct, txPct float64 }),
- prevNet: make(map[string]stats.NetStamp),
- netIntIndex: make(map[string]int),
- printNetInfoOnce: cfg.ShowNet,
- peakHistory: make(map[string][]float64),
+ showCores: cfg.ShowCores,
+ showMem: cfg.ShowMem,
+ showNet: cfg.ShowNet,
+ extended: cfg.Extended,
+ winW: winW,
+ winH: winH,
+ prevCPU: make(map[string]collector.CPULine),
+ smoothedCPU: make(map[string]*[9]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),
}
}
@@ -152,9 +147,6 @@ func handleKey(sym sdl.Keycode, window *sdl.Window, cfg *config.Config, state *r
case sdl.K_3:
state.showNet = !state.showNet
fmt.Println("==> Toggled show net:", state.showNet)
- if state.showNet {
- state.printNetInfoOnce = true
- }
case sdl.K_e:
state.extended = !state.extended
fmt.Println("==> Toggled extended (peak line):", state.extended)
@@ -180,11 +172,6 @@ func handleKey(sym sdl.Keycode, window *sdl.Window, cfg *config.Config, state *r
scaleLinkDown(cfg)
case sdl.K_h:
printHotkeys()
- case sdl.K_n:
- state.cycleNetNext = true
- if state.showNet {
- fmt.Println("==> Cycling to next network interface (per host)")
- }
case sdl.K_w:
cfg.ShowCores = state.showCores
cfg.ShowMem = state.showMem
@@ -223,16 +210,6 @@ func handleKey(sym sdl.Keycode, window *sdl.Window, cfg *config.Config, state *r
// drawFrame updates state from snapshot, clears if layout changed, and draws all bars.
func drawFrame(renderer *sdl.Renderer, src stats.Source, cfg *config.Config, state *runState) {
snap := src.Snapshot()
- if state.cycleNetNext {
- for _, host := range sortedHosts(snap) {
- state.netIntIndex[host]++
- }
- state.cycleNetNext = false
- }
- if state.printNetInfoOnce && state.showNet {
- state.printNetInfoOnce = false
- printNetInterfaceHelp(snap, cfg, state.netIntIndex)
- }
numBars := countBars(snap, state.showCores, state.showMem, state.showNet)
barWidth := state.winW / int32(numBars)
if barWidth < 1 {
@@ -314,7 +291,7 @@ func drawHostBars(renderer *sdl.Renderer, h *stats.HostStats, host string, cfg *
if state.smoothedNet[host] == nil {
state.smoothedNet[host] = &struct{ rxPct, txPct float64 }{}
}
- state.prevNet[host] = drawNetBarSmoothed(renderer, h, cfg, state.smoothedNet[host], state.prevNet[host], state.netIntIndex, host, smoothFactor, barWidth, x, winH)
+ state.prevNet[host] = drawNetBarSmoothed(renderer, h, cfg, state.smoothedNet[host], state.prevNet[host], smoothFactor, barWidth, x, winH)
}
}
@@ -526,35 +503,9 @@ func drawMemBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, smoothed *st
}
func printHotkeys() {
- fmt.Println("=> Hotkeys: 1=cores 2=mem 3=net e=extended h=help n=next net q=quit w=write config a/y=cpu avg d/c=net avg f/v=link scale arrows=resize")
+ fmt.Println("=> Hotkeys: 1=cores 2=mem 3=net e=extended h=help q=quit w=write config a/y=cpu avg d/c=net avg f/v=link scale arrows=resize")
}
-// printNetInterfaceHelp prints which interface is used per host and how to set netint (when net view is toggled on).
-func printNetInterfaceHelp(snap map[string]*stats.HostStats, cfg *config.Config, netIntIndex map[string]int) {
- for _, host := range sortedHosts(snap) {
- h := snap[host]
- if h == nil || h.Net == nil || len(h.Net) == 0 {
- fmt.Printf("Net: %s => (no interfaces yet, wait for data)\n", host)
- continue
- }
- iface := chooseNetIface(h, cfg, host, netIntIndex)
- all := make([]string, 0, len(h.Net))
- for name := range h.Net {
- all = append(all, name)
- }
- sort.Strings(all)
- if iface == "" {
- fmt.Printf("Net: %s => (no non-lo interface; seen: %s)\n", host, strings.Join(all, ", "))
- continue
- }
- hint := "set netint=IFACE in ~/.loadbarsrc or --netint IFACE"
- if cfg.NetInt != "" {
- hint = "using netint=" + cfg.NetInt + " from config"
- }
- fmt.Printf("Net: %s => %s (all: %s). %s\n", host, iface, strings.Join(all, ", "), hint)
- }
- fmt.Println("=> Link speed: netlink=" + cfg.NetLink + " (gbit/mbit/10mbit/100mbit/10gbit or number). Change in ~/.loadbarsrc or --netlink")
-}
// netLinkBytesPerSec returns link speed in bytes/sec from cfg.NetLink (e.g. "gbit", "10gbit", "100mbit", or numeric mbit).
@@ -616,58 +567,54 @@ func netLinkBytesPerSec(cfg *config.Config) int64 {
return int64(constants.BytesGbit)
}
-// chooseNetIface returns the interface name to use for this host: cfg.NetInt if set and present, else first non-lo, cycling with n key.
-func chooseNetIface(h *stats.HostStats, cfg *config.Config, host string, netIntIndex map[string]int) string {
- if h.Net == nil || len(h.Net) == 0 {
- return ""
- }
- if cfg.NetInt != "" {
- if _, ok := h.Net[cfg.NetInt]; ok {
- return cfg.NetInt
- }
+// sumNonLoNet aggregates RX (B) and TX (Tb) bytes across all non-lo interfaces,
+// using the latest timestamp from any interface.
+func sumNonLoNet(h *stats.HostStats) (sum stats.NetStamp, hasIface bool) {
+ if h.Net == nil {
+ return sum, false
}
- names := make([]string, 0, len(h.Net))
- for iface := range h.Net {
+ for iface, ns := range h.Net {
if iface == "lo" {
continue
}
- names = append(names, iface)
- }
- sort.Strings(names)
- if len(names) == 0 {
- return ""
- }
- idx := netIntIndex[host] % len(names)
- if idx < 0 {
- idx += len(names)
+ hasIface = true
+ sum.B += ns.B
+ sum.Tb += ns.Tb
+ if ns.Stamp > sum.Stamp {
+ sum.Stamp = ns.Stamp
+ }
}
- return names[idx]
+ return sum, hasIface
}
-func drawNetBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, cfg *config.Config, smoothed *struct{ rxPct, txPct float64 }, prev stats.NetStamp, netIntIndex map[string]int, host string, factor float64, barW int32, x *int32, winH int32) stats.NetStamp {
+// drawNetBarSmoothed sums RX/TX across all non-lo interfaces, computes utilization
+// vs link speed, smooths toward target, and draws one net bar (RX left from top, TX right from bottom).
+// Smoothed values and prevNet are only updated when new collector data arrives
+// (cur.Stamp > prev.Stamp), so the bar holds steady between collector cycles
+// instead of decaying toward zero on frames with no new data.
+func drawNetBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, cfg *config.Config, smoothed *struct{ rxPct, txPct float64 }, prev stats.NetStamp, factor float64, barW int32, x *int32, winH int32) stats.NetStamp {
defer func() { *x += barW }()
// Clear this slot so we never leave previous (e.g. CPU/mem) content visible
renderer.SetDrawColor(constants.Black.R, constants.Black.G, constants.Black.B, 255)
renderer.FillRect(&sdl.Rect{X: *x, Y: 0, W: barW, H: winH})
- iface := chooseNetIface(h, cfg, host, netIntIndex)
- if iface == "" {
- renderer.SetDrawColor(constants.Red.R, constants.Red.G, constants.Red.B, 255)
- renderer.FillRect(&sdl.Rect{X: *x, Y: 0, W: barW, H: winH})
- return prev
- }
- cur, ok := h.Net[iface]
- if !ok {
+ cur, hasIface := sumNonLoNet(h)
+ if !hasIface {
+ // No non-lo interface: show red bar
renderer.SetDrawColor(constants.Red.R, constants.Red.G, constants.Red.B, 255)
renderer.FillRect(&sdl.Rect{X: *x, Y: 0, W: barW, H: winH})
return prev
}
- linkBps := netLinkBytesPerSec(cfg)
- if linkBps <= 0 {
- linkBps = int64(constants.BytesGbit)
- }
- var targetRx, targetTx float64
- if prev.Stamp > 0 && cur.Stamp > prev.Stamp {
- dt := float64(cur.Stamp-prev.Stamp) / 1e9
+ // Only recompute and smooth when the collector has provided new data.
+ // The collector updates net stamps every ~2.8s, but drawFrame runs every
+ // ~0.14s. Without this guard, the 19 intermediate frames would set
+ // target to 0 (no delta) and smooth the bar toward zero, making real
+ // traffic invisible.
+ if cur.Stamp > prev.Stamp && prev.Stamp > 0 {
+ linkBps := netLinkBytesPerSec(cfg)
+ if linkBps <= 0 {
+ linkBps = int64(constants.BytesGbit)
+ }
+ dt := cur.Stamp - prev.Stamp
if dt > 0 {
deltaB := cur.B - prev.B
deltaTb := cur.Tb - prev.Tb
@@ -677,12 +624,16 @@ func drawNetBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, cfg *config.
if deltaTb < 0 {
deltaTb = 0
}
- targetRx = 100 * float64(deltaB) / (float64(linkBps) * dt)
- targetTx = 100 * float64(deltaTb) / (float64(linkBps) * dt)
+ targetRx := 100 * float64(deltaB) / (float64(linkBps) * dt)
+ targetTx := 100 * float64(deltaTb) / (float64(linkBps) * dt)
+ smoothed.rxPct += (targetRx - smoothed.rxPct) * factor
+ smoothed.txPct += (targetTx - smoothed.txPct) * factor
}
+ prev = cur // only advance prev when we consumed new data
+ } else if prev.Stamp == 0 {
+ // First sample: record it but don't draw yet (no delta available)
+ prev = cur
}
- smoothed.rxPct += (targetRx - smoothed.rxPct) * factor
- smoothed.txPct += (targetTx - smoothed.txPct) * factor
halfW := barW / 2
barH := float64(winH) / 100.0
@@ -712,5 +663,5 @@ func drawNetBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, cfg *config.
renderer.SetDrawColor(constants.Black.R, constants.Black.G, constants.Black.B, 255)
renderer.FillRect(&sdl.Rect{X: *x + halfW, Y: 0, W: halfW, H: winH - txH})
}
- return cur
+ return prev
}
diff --git a/internal/display/display_test.go b/internal/display/display_test.go
index 841b1d4..94068f6 100644
--- a/internal/display/display_test.go
+++ b/internal/display/display_test.go
@@ -278,7 +278,7 @@ func TestNetBar_RxTx(t *testing.T) {
"cpu": {User: 100, System: 100, Idle: 800},
},
Net: map[string]stats.NetStamp{
- "eth0": {B: 12500000, Tb: 6250000, Stamp: 2e9}, // current sample
+ "eth0": {B: 12500000, Tb: 6250000, Stamp: 2.0}, // current sample
},
},
},
@@ -288,7 +288,7 @@ func TestNetBar_RxTx(t *testing.T) {
state.prevCPU["host1;cpu"] = collector.CPULine{}
// Pre-populate prevNet so delta calculation works:
// RX: delta=12500000 bytes in 1s = 10% of gbit, TX: 6250000 = 5% of gbit
- state.prevNet["host1"] = stats.NetStamp{B: 0, Tb: 0, Stamp: 1e9}
+ state.prevNet["host1"] = stats.NetStamp{B: 0, Tb: 0, Stamp: 1.0}
// Pre-populate smoothed net so first frame is near target
state.smoothedNet["host1"] = &struct{ rxPct, txPct float64 }{
rxPct: 10, txPct: 5,
@@ -307,6 +307,60 @@ func TestNetBar_RxTx(t *testing.T) {
assertPixelColor(t, surface, 85, 10, constants.Black, tol, "TX free area")
}
+func TestNetBar_AggregatesAllInterfaces(t *testing.T) {
+ const w, h int32 = 100, 100
+
+ renderer, surface, err := createTestRenderer(w, h)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer renderer.Destroy()
+ defer surface.Free()
+
+ cfg := defaultTestConfig()
+ cfg.ShowCores = false
+ cfg.ShowMem = false
+ cfg.ShowNet = true
+ cfg.NetLink = "gbit"
+
+ // Two non-lo interfaces: combined RX = 12500000+12500000 = 25000000 → 20% of gbit
+ src := &mockSource{
+ data: map[string]*stats.HostStats{
+ "host1": {
+ CPU: map[string]collector.CPULine{
+ "cpu": {User: 100, System: 100, Idle: 800},
+ },
+ Net: map[string]stats.NetStamp{
+ "eth0": {B: 12500000, Tb: 6250000, Stamp: 2.0},
+ "wlan0": {B: 12500000, Tb: 6250000, Stamp: 2.0},
+ "lo": {B: 99999999, Tb: 99999999, Stamp: 2.0}, // lo must be excluded
+ },
+ },
+ },
+ }
+
+ state := newRunState(cfg, w, h)
+ state.prevCPU["host1;cpu"] = collector.CPULine{}
+ // Previous aggregated stamp: all zeros at t=1
+ state.prevNet["host1"] = stats.NetStamp{B: 0, Tb: 0, Stamp: 1.0}
+ // Pre-populate smoothed to 20% RX, 10% TX (the expected combined values)
+ state.smoothedNet["host1"] = &struct{ rxPct, txPct float64 }{
+ rxPct: 20, txPct: 10,
+ }
+
+ drawFrame(renderer, src, cfg, state)
+
+ const tol = 5
+ // Net bar: 1 CPU + 1 net = 2 bars, each 50px. Net bar at x=50, halfW=25
+ // Left half (RX from top): 20% = 20px of LightGreen from top
+ assertPixelColor(t, surface, 60, 5, constants.LightGreen, tol, "aggregated RX at top")
+ assertPixelColor(t, surface, 60, 45, constants.Black, tol, "RX free area")
+
+ // Right half (TX from bottom): 10% = 10px of LightGreen from bottom
+ assertPixelColor(t, surface, 85, 98, constants.LightGreen, tol, "aggregated TX at bottom")
+ assertPixelColor(t, surface, 85, 10, constants.Black, tol, "TX free area")
+}
+
func TestMultiHost_BarCount(t *testing.T) {
const w, h int32 = 600, 100
@@ -332,12 +386,12 @@ func TestMultiHost_BarCount(t *testing.T) {
"alpha": {
CPU: map[string]collector.CPULine{"cpu": alphaCur},
Mem: map[string]int64{"MemTotal": 100, "MemFree": 50, "SwapTotal": 0, "SwapFree": 0},
- Net: map[string]stats.NetStamp{"eth0": {B: 0, Tb: 0, Stamp: 1e9}},
+ Net: map[string]stats.NetStamp{"eth0": {B: 0, Tb: 0, Stamp: 1.0}},
},
"beta": {
CPU: map[string]collector.CPULine{"cpu": betaCur},
Mem: map[string]int64{"MemTotal": 100, "MemFree": 50, "SwapTotal": 0, "SwapFree": 0},
- Net: map[string]stats.NetStamp{"eth0": {B: 0, Tb: 0, Stamp: 1e9}},
+ Net: map[string]stats.NetStamp{"eth0": {B: 0, Tb: 0, Stamp: 1.0}},
},
},
}
@@ -443,7 +497,7 @@ func TestNetBar_NoInterface(t *testing.T) {
"cpu": {User: 100, System: 100, Idle: 800},
},
Net: map[string]stats.NetStamp{
- "lo": {B: 0, Tb: 0, Stamp: 1e9}, // only loopback
+ "lo": {B: 0, Tb: 0, Stamp: 1.0}, // only loopback
},
},
},
@@ -573,8 +627,8 @@ func newHotkeyTestEnv(t *testing.T, showCores, showMem, showNet bool) (
"SwapFree": 600,
},
Net: map[string]stats.NetStamp{
- "eth0": {B: 12500000, Tb: 6250000, Stamp: 2e9},
- "wlan0": {B: 1000000, Tb: 500000, Stamp: 2e9},
+ "eth0": {B: 12500000, Tb: 6250000, Stamp: 2.0},
+ "wlan0": {B: 1000000, Tb: 500000, Stamp: 2.0},
},
},
},
@@ -584,7 +638,7 @@ func newHotkeyTestEnv(t *testing.T, showCores, showMem, showNet bool) (
state.prevCPU["host1;cpu"] = prev
state.prevCPU["host1;cpu0"] = prev0
state.prevCPU["host1;cpu1"] = prev1
- state.prevNet["host1"] = stats.NetStamp{B: 0, Tb: 0, Stamp: 1e9}
+ state.prevNet["host1"] = stats.NetStamp{B: 0, Tb: 0, Stamp: 1.0}
state.smoothedNet["host1"] = &struct{ rxPct, txPct float64 }{
rxPct: 10, txPct: 5,
}
@@ -755,16 +809,6 @@ func TestHandleKey_NetAverage(t *testing.T) {
}
}
-func TestHandleKey_CycleNet(t *testing.T) {
- cfg := defaultTestConfig()
- state := newRunState(cfg, 200, 100)
-
- handleKey(sdl.K_n, nil, cfg, state)
- if !state.cycleNetNext {
- t.Error("expected cycleNetNext=true after pressing 'n'")
- }
-}
-
func TestHandleKey_WriteConfig(t *testing.T) {
// Set HOME to a temp dir so we don't touch real ~/.loadbarsrc
tmpDir := t.TempDir()
diff --git a/internal/version/version.go b/internal/version/version.go
index 2b4ace2..209fae7 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -1,4 +1,4 @@
package version
// Version is the application version (set at build time or here for development).
-const Version = "0.8.4"
+const Version = "0.9.0"