diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-13 23:35:17 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-13 23:35:17 +0200 |
| commit | f7e03c864addd6c46f49558275cbb2e7050783d6 (patch) | |
| tree | fce4a9e3c7251cf24d662c3a110cd656a96f704e | |
| parent | 8ce6aa0a181e123f156ef3d1b75d939f56757ac6 (diff) | |
Document hotkeys in README; fix display/config; net interface help
- README: add Hotkeys section (1,2,3,e,h,n,q,w,a,y,d,c,f,v,arrows)
- README: network interface and netlink config docs
- Display: default window width 1200; clear bar slots to fix CPU/mem/net mixing
- Display: numBars count only non-nil hosts (layout matches draw)
- Display: network bars (key 3), net interface choice and n=cycle
- Display: extended peak line (e), cpu/net avg keys (a,y,d,c) with feedback
- Config: default barwidth 1200; netint/netlink in --help
- Collector: parse_test Linux-style net stats
Co-authored-by: Cursor <cursoragent@cursor.com>
| -rw-r--r-- | README.md | 75 | ||||
| -rw-r--r-- | cmd/loadbars/main.go | 25 | ||||
| -rw-r--r-- | internal/collector/parse_test.go | 13 | ||||
| -rw-r--r-- | internal/config/config.go | 2 | ||||
| -rw-r--r-- | internal/config/config_test.go | 6 | ||||
| -rw-r--r-- | internal/display/display.go | 486 |
6 files changed, 482 insertions, 125 deletions
@@ -40,14 +40,53 @@ Loadbars is a tool that can be used to observe CPU loads of several remote serve ## Build and run +### Using Mage (recommended) + Build the binary: ```bash +mage build +./loadbars --hosts localhost +``` + +Install system-wide: + +```bash +sudo mage install +``` + +Uninstall: + +```bash +sudo mage uninstall +``` + +Run tests: + +```bash +mage test +``` + +### Using Go directly + +Build from source: + +```bash go build -o loadbars ./cmd/loadbars ./loadbars --hosts localhost ``` -Or use [mage](https://magefile.org): `mage build` (default), `mage test`, `mage install` (set `DESTDIR` for install path), `mage uninstall` / `mage deinstall`. +Install to $GOPATH/bin: + +```bash +go install ./cmd/loadbars +``` + +Or install the latest version from the repository: + +```bash +go install codeberg.org/snonux/loadbars/cmd/loadbars@latest +``` Remote hosts need no Go: the binary pipes `scripts/loadbars-remote.sh` over SSH. @@ -91,6 +130,28 @@ Loadbars requires SSH public/private key authentication. Make sure: ## Info +### Hotkeys + +Press these keys while loadbars is running (see also `h` for a short list on stdout): + +| Key | Action | +|-----|--------| +| **1** | Toggle CPU cores (one bar per core vs one aggregate bar per host) | +| **2** | Toggle memory bars (RAM left, Swap right per host) | +| **3** | Toggle network bars (RX/TX per host); stdout shows which interface is used | +| **e** | Toggle extended display (1px peak line on CPU bars: max system+user over last samples) | +| **h** | Print hotkey list to stdout | +| **n** | Cycle to next network interface (per host); only when net bars are shown | +| **q** | Quit | +| **w** | Write current settings to ~/.loadbarsrc | +| **a** | Increase CPU average samples (affects extended peak history length) | +| **y** | Decrease CPU average samples (min 1) | +| **d** | Increase net average samples | +| **c** | Decrease net average samples (min 1) | +| **f** | Link scale up (net utilization reference) | +| **v** | Link scale down (net utilization reference) | +| **Arrow keys** | Resize window (left/right: width, up/down: height) | + ### CPU stuff - `st` = Steal in % [see man proc] (extended), Color: Red @@ -118,12 +179,22 @@ Loadbars requires SSH public/private key authentication. Make sure: When network bar is red: The interface does not exist on the specific remote host. +**Choosing the network interface:** By default loadbars uses the first non-loopback interface (e.g. `eth0`, `enp0s3`). If stats never change, you may be watching the wrong interface (e.g. `docker0` with little traffic). Set the interface explicitly: + +- In **~/.loadbarsrc**: `netint=eth0` (use the name from `/proc/net/dev` or `ip link`) +- On the command line: `loadbars --netint eth0 --hosts localhost` +- In-app: press **3** to show net bars (stdout will print which interface is used), then **n** to cycle to the next interface. + +**Link speed** (`netlink`): Used to compute utilization %. Default is `gbit`. Set e.g. `netlink=100mbit` or `netlink=10gbit` in ~/.loadbarsrc or `--netlink 100mbit`. + #### Config file support Loadbars tries to read ~/.loadbarsrc and it's possible to configure any option you find in --help but without leading '--'. For comments just use the '#' sign. Sample config: ``` -showcores=1 # Always show cores on startup +showcores=1 # Always show cores on startup +netint=eth0 # Interface for network bars (optional) +netlink=gbit # Link speed for utilization % (optional) ``` will always show all CPU cores. If you press the 'w' hotkey during program execution your config file will be overwritten using the current settings. diff --git a/cmd/loadbars/main.go b/cmd/loadbars/main.go index fc053be..62b90db 100644 --- a/cmd/loadbars/main.go +++ b/cmd/loadbars/main.go @@ -114,15 +114,20 @@ Usage: loadbars [HOSTS...] [OPTIONS] Options: --hosts <list> Comma-separated hosts (optional user@host) --cluster <name> Cluster from %s - --barwidth <n> Bar width (default 20) - --height <n> Window height (default 150) - --showcores Show per-CPU bars - --showmem Show memory bars - --shownet Show network bars - --extended Extended display (peak line) - --help This help - --version Print version - -Press 'h' during execution for hotkeys. See README for more. + --barwidth <n> Initial window width (default 1200) + --height <n> Window height (default 150) + --showcores Show per-CPU bars + --showmem Show memory bars + --shownet Show network bars + --netint <iface> Network interface for net bars (e.g. eth0, enp0s3). + Default: first non-lo. Press 'n' in-app to cycle. + --netlink <speed> Link speed for %% (gbit, 10gbit, mbit, 100mbit, or number). + Default: gbit. + --extended Extended display (peak line) + --help This help + --version Print version + +Config: netint and netlink can be set in ~/.loadbarsrc. Press '3' then see +stdout for which interface is used; press 'n' to cycle. Press 'h' for hotkeys. `, version.Version, constants.CSSHConfFile) } diff --git a/internal/collector/parse_test.go b/internal/collector/parse_test.go index 0faa4fb..6edac33 100644 --- a/internal/collector/parse_test.go +++ b/internal/collector/parse_test.go @@ -72,16 +72,19 @@ func TestParseMemLine(t *testing.T) { func TestParseNetLine(t *testing.T) { tests := []struct { - name string - line string + name string + line string wantIface string - wantB int64 - wantTb int64 - wantErr bool + wantB int64 + wantTb int64 + wantErr bool }{ {"simple", "eth0:b=1000;tb=2000;p=10;tp=20;e=0;te=0;d=0;td=0", "eth0", 1000, 2000, false}, {"with_space", "eth0:b=100;tb=200 p=0;tp=0;e=0;te=0;d=0;td=0", "eth0", 100, 200, false}, {"no_colon", "eth0 b=1", "", 0, 0, true}, + // Linux /proc/net/dev style as emitted by loadbars-remote.sh (iface rx_bytes rx_packets ... tx_bytes ...) + {"linux_style", "eth0:b=123456789;tb=987654321;p=1000;tp=2000 e=0;te=0;d=0;td=0", "eth0", 123456789, 987654321, false}, + {"lo_interface", "lo:b=0;tb=0;p=0;tp=0 e=0;te=0;d=0;td=0", "lo", 0, 0, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/config/config.go b/internal/config/config.go index 256484a..42db523 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -35,7 +35,7 @@ type Config struct { // Default returns a Config with default values. func Default() Config { return Config{ - BarWidth: 20, + BarWidth: 1200, CPUAverage: 10, Extended: false, HasAgent: false, diff --git a/internal/config/config_test.go b/internal/config/config_test.go index d51feb7..dfa499c 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -14,10 +14,10 @@ func TestConfig_parseReader(t *testing.T) { wantBar int wantExt bool }{ - {"empty", "", 20, false}, + {"empty", "", 1200, false}, {"barwidth", "barwidth=42\n", 42, false}, - {"extended_1", "extended=1\n", 20, true}, - {"extended_true", "extended=true\n", 20, true}, + {"extended_1", "extended=1\n", 1200, true}, + {"extended_true", "extended=true\n", 1200, true}, {"comments", "# foo\nbarwidth=10\n# bar\n", 10, false}, {"unknown_key", "barwidth=5\nunknown=ignored\n", 5, false}, {"multiple", "barwidth=30\nextended=1\nshowcores=1\n", 30, true}, 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 } |
