summaryrefslogtreecommitdiff
path: root/internal/display/display.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/display/display.go')
-rw-r--r--internal/display/display.go486
1 files changed, 382 insertions, 104 deletions
diff --git a/internal/display/display.go b/internal/display/display.go
index 49f9f10..33120dc 100644
--- a/internal/display/display.go
+++ b/internal/display/display.go
@@ -5,6 +5,8 @@ import (
"fmt"
"os"
"sort"
+ "strconv"
+ "strings"
"time"
"codeberg.org/snonux/loadbars/internal/collector"
@@ -56,10 +58,20 @@ func Run(ctx context.Context, cfg *config.Config, src stats.Source) error {
// Previous CPU state for delta (key = host;cpuName)
prevCPU := make(map[string]collector.CPULine)
- // We need collector.CPULine - use stats.HostStats.CPU which is map[string]collector.CPULine. So we need to import collector for CPULine.
- // Actually we have stats.HostStats which has CPU map[string]collector.CPULine. So we need to import collector in display for the type. Let me add the import and a type alias or use the type from collector. So display will import collector for CPULine.
- _ = prevCPU
+ // Smoothed values for transitions (blend toward target each frame)
+ const smoothFactor = 0.12 // lower = smoother, less flicker from noisy samples
+ 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) // for cycling interface per host
+ var cycleNetNext bool
+ var printNetInfoOnce bool = showNet // print chosen interface when net view is on (once at start or after toggling on)
+ // Peak history for extended mode: per CPU bar key, ring of (system+user) %
+ peakHistory := make(map[string][]float64)
+ lastNumBars := -1
+ lastWinW, lastWinH := int32(0), int32(0)
ticker := time.NewTicker(time.Duration(constants.IntervalSDL * float64(time.Second)))
defer ticker.Stop()
@@ -92,10 +104,35 @@ func Run(ctx context.Context, cfg *config.Config, src stats.Source) error {
case sdl.K_3:
showNet = !showNet
fmt.Println("==> Toggled show net:", showNet)
+ if showNet {
+ printNetInfoOnce = true
+ }
case sdl.K_e:
extended = !extended
+ fmt.Println("==> Toggled extended (peak line):", extended)
+ case sdl.K_a:
+ cfg.CPUAverage++
+ fmt.Println("==> CPU average samples:", cfg.CPUAverage)
+ case sdl.K_y:
+ if cfg.CPUAverage > 1 {
+ cfg.CPUAverage--
+ }
+ fmt.Println("==> CPU average samples:", cfg.CPUAverage)
+ case sdl.K_d:
+ cfg.NetAverage++
+ fmt.Println("==> Net average samples:", cfg.NetAverage)
+ case sdl.K_c:
+ if cfg.NetAverage > 1 {
+ cfg.NetAverage--
+ }
+ fmt.Println("==> Net average samples:", cfg.NetAverage)
case sdl.K_h:
printHotkeys()
+ case sdl.K_n:
+ cycleNetNext = true
+ if showNet {
+ fmt.Println("==> Cycling to next network interface (per host)")
+ }
case sdl.K_w:
cfg.ShowCores = showCores
cfg.ShowMem = showMem
@@ -136,19 +173,30 @@ func Run(ctx context.Context, cfg *config.Config, src stats.Source) error {
}
snap := src.Snapshot()
- // Count total CPU bars we will draw (so width is shared across all bars)
+ if cycleNetNext {
+ for _, host := range sortedHosts(snap) {
+ netIntIndex[host]++
+ }
+ cycleNetNext = false
+ }
+ // One-time: print which interface is used for net stats and how to configure
+ if printNetInfoOnce && showNet {
+ printNetInfoOnce = false
+ printNetInterfaceHelp(snap, cfg, netIntIndex)
+ }
+ // Count total bars we will draw (only non-nil hosts) so layout matches draw order
numBars := 0
for _, host := range sortedHosts(snap) {
if h := snap[host]; h != nil {
numBars += len(sortedCPUNames(h.CPU, showCores))
+ if showMem {
+ numBars++
+ }
+ if showNet {
+ numBars++
+ }
}
}
- if showMem {
- numBars += len(snap)
- }
- if showNet {
- numBars += len(snap)
- }
if numBars == 0 {
numBars = 1
}
@@ -158,9 +206,13 @@ func Run(ctx context.Context, cfg *config.Config, src stats.Source) error {
barWidth = 1
}
- // Clear every frame so toggling cores off (or changing bar count) doesn't leave stale bars
- renderer.SetDrawColor(0, 0, 0, 255)
- renderer.Clear()
+ // Clear only when layout changes (bar count or window size) to avoid full-screen flicker
+ if numBars != lastNumBars || winW != lastWinW || winH != lastWinH {
+ renderer.SetDrawColor(0, 0, 0, 255)
+ renderer.Clear()
+ lastNumBars = numBars
+ lastWinW, lastWinH = winW, winH
+ }
x := int32(0)
hosts := sortedHosts(snap)
@@ -169,15 +221,63 @@ func Run(ctx context.Context, cfg *config.Config, src stats.Source) error {
if h == nil {
continue
}
- // Draw CPU bars for this host (aggregate or per-core)
+ // Draw CPU bars for this host (aggregate or per-core), with smoothing
cpuNames := sortedCPUNames(h.CPU, showCores)
for _, name := range cpuNames {
- drawCPUBar(renderer, h.CPU[name], prevCPU[host+";"+name], barWidth, &x, winH)
- prevCPU[host+";"+name] = h.CPU[name]
+ key := host + ";" + name
+ cur := h.CPU[name]
+ prev := prevCPU[key]
+ prevCPU[key] = cur
+ target, ok := cpuBarTargetPcts(cur, prev)
+ s := smoothedCPU[key]
+ if s == nil {
+ s = &[9]float64{}
+ smoothedCPU[key] = s
+ if ok {
+ *s = target
+ }
+ } else if ok {
+ for i := 0; i < 9; i++ {
+ (*s)[i] += (target[i] - (*s)[i]) * smoothFactor
+ }
+ normalizePcts9(s)
+ }
+ // Peak line (extended): max of (system+user) over last CPUAverage samples
+ var peakPct float64
+ if extended && s != nil {
+ userSys := (*s)[0] + (*s)[1]
+ hist := peakHistory[key]
+ hist = append(hist, userSys)
+ n := cfg.CPUAverage
+ if n < 1 {
+ n = 1
+ }
+ for len(hist) > n {
+ hist = hist[1:]
+ }
+ peakHistory[key] = hist
+ for _, v := range hist {
+ if v > peakPct {
+ peakPct = v
+ }
+ }
+ }
+ // Always draw (smoothed or last state) so we never leave a blank bar and cause flicker
+ drawCPUBarFromPcts(renderer, s, barWidth, &x, winH, extended, peakPct)
}
- // Draw memory bar(s) for this host when showMem
+ // Draw memory bar(s) for this host when showMem, with smoothing
if showMem {
- drawMemBar(renderer, h, barWidth, &x, winH)
+ if smoothedMem[host] == nil {
+ smoothedMem[host] = &struct{ ramUsed, swapUsed float64 }{}
+ }
+ drawMemBarSmoothed(renderer, h, smoothedMem[host], smoothFactor, barWidth, &x, winH)
+ }
+ // Draw network bar(s) for this host when showNet
+ if showNet {
+ if smoothedNet[host] == nil {
+ smoothedNet[host] = &struct{ rxPct, txPct float64 }{}
+ }
+ prevNet[host] = drawNetBarSmoothed(renderer, h, cfg, smoothedNet[host], prevNet[host], netIntIndex, host, smoothFactor, barWidth, &x, winH)
}
}
@@ -220,128 +320,306 @@ func sortedCPUNames(cpu map[string]collector.CPULine, showCores bool) []string {
return names
}
-func drawCPUBar(renderer *sdl.Renderer, cur, prev collector.CPULine, barW int32, x *int32, winH int32) {
- defer func() { *x += barW + 1 }()
- // Compute delta and normalize to %
+// 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 [9]float64, ok bool) {
totalCur := cur.Total()
totalPrev := prev.Total()
if totalPrev == 0 || totalCur <= totalPrev {
- return
+ return out, false
}
scale := float64(totalCur-totalPrev) / 100.0
if scale <= 0 {
- return
+ return out, false
}
- userPct := int(float64(cur.User-prev.User) / scale)
- nicePct := int(float64(cur.Nice-prev.Nice) / scale)
- sysPct := int(float64(cur.System-prev.System) / scale)
- idlePct := int(float64(cur.Idle-prev.Idle) / scale)
- iowaitPct := int(float64(cur.Iowait-prev.Iowait) / scale)
- irqPct := int(float64(cur.IRQ-prev.IRQ) / scale)
- softirqPct := int(float64(cur.SoftIRQ-prev.SoftIRQ) / scale)
- guestPct := int(float64(cur.Guest-prev.Guest) / scale)
- stealPct := int(float64(cur.Steal-prev.Steal) / scale)
-
- norm := func(v int) int {
- if v < 0 {
- return 0
+ 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
+ for i := range out {
+ if out[i] < 0 {
+ out[i] = 0
}
- if v > 100 {
- return 100
+ if out[i] > 100 {
+ out[i] = 100
}
- return v
- }
- userPct = norm(userPct)
- nicePct = norm(nicePct)
- sysPct = norm(sysPct)
- idlePct = norm(idlePct)
- iowaitPct = norm(iowaitPct)
- irqPct = norm(irqPct)
- softirqPct = norm(softirqPct)
- guestPct = norm(guestPct)
- stealPct = norm(stealPct)
+ }
+ return out, true
+}
+
+func normalizePcts9(s *[9]float64) {
+ var sum float64
+ for i := 0; i < 9; i++ {
+ sum += (*s)[i]
+ }
+ if sum <= 0 {
+ return
+ }
+ for i := 0; i < 9; i++ {
+ (*s)[i] = (*s)[i] * 100 / sum
+ }
+}
+// drawCPUBarFromPcts draws one CPU bar from 9 smoothed segment percentages. If s is nil, advances x only.
+// When extended is true and peakPct > 0, draws a 1px peak line (max system+user over history).
+func drawCPUBarFromPcts(renderer *sdl.Renderer, s *[9]float64, barW int32, x *int32, winH int32, extended bool, peakPct float64) {
+ defer func() { *x += barW + 1 }()
+ // 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: 0, W: barW, H: winH})
+ if s == nil {
+ return
+ }
barH := float64(winH) / 100.0
y := float64(winH)
- fill := func(r, g, b uint8, h int) {
- hh := int32(float64(h) * barH)
- if hh < 1 && h > 0 {
+ fill := func(r, g, b uint8, pct float64) {
+ hh := int32(pct * barH)
+ if hh < 1 && pct > 0 {
hh = 1
}
y -= float64(hh)
renderer.SetDrawColor(r, g, b, 255)
- rect := sdl.Rect{X: *x, Y: int32(y), W: barW, H: hh}
- renderer.FillRect(&rect)
- }
- // Order bottom to top: system, user, nice, idle, iowait, irq, softirq, guest, steal (match Perl)
- fill(constants.Blue.R, constants.Blue.G, constants.Blue.B, sysPct)
- fill(constants.Yellow.R, constants.Yellow.G, constants.Yellow.B, userPct)
- fill(constants.Green.R, constants.Green.G, constants.Green.B, nicePct)
- fill(constants.Black.R, constants.Black.G, constants.Black.B, idlePct)
- fill(constants.Purple.R, constants.Purple.G, constants.Purple.B, iowaitPct)
- fill(constants.White.R, constants.White.G, constants.White.B, irqPct)
- fill(constants.White.R, constants.White.G, constants.White.B, softirqPct)
- fill(constants.Red.R, constants.Red.G, constants.Red.B, guestPct)
- fill(constants.Red.R, constants.Red.G, constants.Red.B, stealPct)
+ renderer.FillRect(&sdl.Rect{X: *x, Y: int32(y), 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.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 := winH - int32(peakPct*barH)
+ if peakY < 0 {
+ peakY = 0
+ }
+ if peakY >= winH {
+ peakY = winH - 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})
+ }
}
-// drawMemBar draws one memory bar (RAM left half, Swap right half) for a host.
-func drawMemBar(renderer *sdl.Renderer, h *stats.HostStats, barW int32, x *int32, winH int32) {
+// drawMemBarSmoothed blends mem stats toward target and draws one memory bar (RAM left, Swap right).
+func drawMemBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, smoothed *struct{ ramUsed, swapUsed float64 }, factor float64, barW int32, x *int32, winH int32) {
defer func() { *x += barW + 1 }()
+ // 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: 0, W: barW, H: winH})
if h.Mem == nil {
return
}
- memTotal := h.Mem["MemTotal"]
- memFree := h.Mem["MemFree"]
- swapTotal := h.Mem["SwapTotal"]
- swapFree := h.Mem["SwapFree"]
+ 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
barH := float64(winH) / 100.0
// RAM: used (dark grey) from bottom, free (black) on top
- if memTotal > 0 {
- ramUsedPct := 100 - int(100*memFree/memTotal)
- if ramUsedPct < 0 {
- ramUsedPct = 0
+ ramUsedH := int32(smoothed.ramUsed * barH)
+ if ramUsedH > 0 {
+ renderer.SetDrawColor(constants.DarkGrey.R, constants.DarkGrey.G, constants.DarkGrey.B, 255)
+ renderer.FillRect(&sdl.Rect{X: *x, Y: winH - ramUsedH, W: halfW, H: ramUsedH})
+ }
+ if ramFreeH := winH - ramUsedH; ramFreeH > 0 {
+ renderer.SetDrawColor(constants.Black.R, constants.Black.G, constants.Black.B, 255)
+ renderer.FillRect(&sdl.Rect{X: *x, Y: 0, W: halfW, H: ramFreeH})
+ }
+
+ // Swap: used (grey) from bottom, free (black) on top
+ swapUsedH := int32(smoothed.swapUsed * barH)
+ if swapUsedH > 0 {
+ renderer.SetDrawColor(constants.Grey.R, constants.Grey.G, constants.Grey.B, 255)
+ renderer.FillRect(&sdl.Rect{X: *x + halfW, Y: winH - swapUsedH, W: halfW, H: swapUsedH})
+ }
+ if swapFreeH := winH - swapUsedH; swapFreeH > 0 {
+ renderer.SetDrawColor(constants.Black.R, constants.Black.G, constants.Black.B, 255)
+ renderer.FillRect(&sdl.Rect{X: *x + halfW, Y: 0, W: halfW, H: swapFreeH})
+ }
+}
+
+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")
+}
+
+// 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
}
- if ramUsedPct > 100 {
- ramUsedPct = 100
+ iface := chooseNetIface(h, cfg, host, netIntIndex)
+ all := make([]string, 0, len(h.Net))
+ for name := range h.Net {
+ all = append(all, name)
}
- ramUsedH := int32(float64(ramUsedPct) * barH)
- if ramUsedH > 0 {
- renderer.SetDrawColor(constants.DarkGrey.R, constants.DarkGrey.G, constants.DarkGrey.B, 255)
- renderer.FillRect(&sdl.Rect{X: *x, Y: winH - ramUsedH, W: halfW, H: ramUsedH})
+ sort.Strings(all)
+ if iface == "" {
+ fmt.Printf("Net: %s => (no non-lo interface; seen: %s)\n", host, strings.Join(all, ", "))
+ continue
}
- ramFreeH := winH - ramUsedH
- if ramFreeH > 0 {
- renderer.SetDrawColor(constants.Black.R, constants.Black.G, constants.Black.B, 255)
- renderer.FillRect(&sdl.Rect{X: *x, Y: 0, W: halfW, H: ramFreeH})
+ 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")
+}
- // Swap: used (grey) from bottom, free (black) on top
- if swapTotal > 0 {
- swapUsedPct := 100 - int(100*swapFree/swapTotal)
- if swapUsedPct < 0 {
- swapUsedPct = 0
- }
- if swapUsedPct > 100 {
- swapUsedPct = 100
- }
- swapUsedH := int32(float64(swapUsedPct) * barH)
- if swapUsedH > 0 {
- renderer.SetDrawColor(constants.Grey.R, constants.Grey.G, constants.Grey.B, 255)
- renderer.FillRect(&sdl.Rect{X: *x + halfW, Y: winH - swapUsedH, W: halfW, H: swapUsedH})
+// netLinkBytesPerSec returns link speed in bytes/sec from cfg.NetLink (e.g. "gbit", "10gbit", "100mbit", or numeric mbit).
+func netLinkBytesPerSec(cfg *config.Config) int64 {
+ s := strings.ToLower(strings.TrimSpace(cfg.NetLink))
+ switch s {
+ case "gbit", "1gbit":
+ return int64(constants.BytesGbit)
+ case "10gbit":
+ return int64(constants.Bytes10Gbit)
+ case "mbit", "1mbit":
+ return int64(constants.BytesMbit)
+ case "10mbit":
+ return int64(constants.Bytes10Mbit)
+ case "100mbit":
+ return int64(constants.Bytes100Mbit)
+ case "":
+ return int64(constants.BytesGbit)
+ }
+ if n, err := strconv.ParseInt(s, 10, 64); err == nil {
+ return n * int64(constants.BytesMbit)
+ }
+ 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
}
- swapFreeH := winH - swapUsedH
- if swapFreeH > 0 {
- renderer.SetDrawColor(constants.Black.R, constants.Black.G, constants.Black.B, 255)
- renderer.FillRect(&sdl.Rect{X: *x + halfW, Y: 0, W: halfW, H: swapFreeH})
+ }
+ names := make([]string, 0, len(h.Net))
+ for iface := 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)
+ }
+ return names[idx]
}
-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")
+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 {
+ defer func() { *x += barW + 1 }()
+ // 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 {
+ 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
+ if dt > 0 {
+ deltaB := cur.B - prev.B
+ deltaTb := cur.Tb - prev.Tb
+ if deltaB < 0 {
+ deltaB = 0
+ }
+ if deltaTb < 0 {
+ deltaTb = 0
+ }
+ 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
+
+ halfW := barW / 2
+ barH := float64(winH) / 100.0
+ // Left half: RX from top (light green = used)
+ rxH := int32(smoothed.rxPct * barH)
+ if rxH > winH/2 {
+ rxH = winH / 2
+ }
+ if rxH > 0 {
+ renderer.SetDrawColor(constants.LightGreen.R, constants.LightGreen.G, constants.LightGreen.B, 255)
+ renderer.FillRect(&sdl.Rect{X: *x, Y: 0, W: halfW, H: rxH})
+ }
+ if halfW > 0 && winH/2-rxH > 0 {
+ renderer.SetDrawColor(constants.Black.R, constants.Black.G, constants.Black.B, 255)
+ renderer.FillRect(&sdl.Rect{X: *x, Y: rxH, W: halfW, H: winH/2 - rxH})
+ }
+ // Right half: TX from bottom (light green = used)
+ txH := int32(smoothed.txPct * barH)
+ if txH > winH/2 {
+ txH = winH / 2
+ }
+ if txH > 0 {
+ renderer.SetDrawColor(constants.LightGreen.R, constants.LightGreen.G, constants.LightGreen.B, 255)
+ renderer.FillRect(&sdl.Rect{X: *x + halfW, Y: winH - txH, W: halfW, H: txH})
+ }
+ if halfW > 0 && (winH - txH) > 0 {
+ 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
}