summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-13 23:06:30 +0200
committerPaul Buetow <paul@buetow.org>2026-02-13 23:06:30 +0200
commit8ce6aa0a181e123f156ef3d1b75d939f56757ac6 (patch)
tree4e89b84833f32c17ae8a8a2e1921d5b4967ecc66
parent39fcaa7a69947ee45d445e3aab7b49ce868b9d9c (diff)
Fix core toggle off and add memory bars (key 2)
- Clear renderer every frame so toggling cores off shows single bar again - Add drawMemBar: RAM (dark grey/black) and Swap (grey/black) per host - Remove redrawBg; always clear before draw - README updates Co-authored-by: Cursor <cursoragent@cursor.com>
-rw-r--r--README.md16
-rw-r--r--internal/display/display.go79
2 files changed, 71 insertions, 24 deletions
diff --git a/README.md b/README.md
index c0f90dd..dbea91c 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ loadbars servername{01..50}.example.com --showcores 1
## Description
-Loadbars is a small script that can be used to observe CPU loads of several remote servers at once in real time. It connects with SSH (using SSH public/private key auth) to several servers at once and vizualizes all server CPUs and memory statistics right next each other (either summarized or each core separately). Loadbars is not a tool for collecting CPU loads and drawing graphs for later analysis. However, since such tools require a significant amount of time before producing results, Loadbars lets you observe the current state immediately. Loadbars does not remember or record any load information. It just shows the current CPU usages like top or vmstat does.
+Loadbars is a tool that can be used to observe CPU loads of several remote servers at once in real time. It connects with SSH (using SSH public/private key auth) to several servers at once and vizualizes all server CPUs and memory statistics right next each other (either summarized or each core separately). Loadbars is not a tool for collecting CPU loads and drawing graphs for later analysis. However, since such tools require a significant amount of time before producing results, Loadbars lets you observe the current state immediately. Loadbars does not remember or record any load information. It just shows the current CPU usages like top or vmstat does.
## Build and run
@@ -49,24 +49,22 @@ go build -o loadbars ./cmd/loadbars
Or use [mage](https://magefile.org): `mage build` (default), `mage test`, `mage install` (set `DESTDIR` for install path), `mage uninstall` / `mage deinstall`.
-Remote hosts need no Perl: the binary pipes `scripts/loadbars-remote.sh` over SSH.
+Remote hosts need no Go: the binary pipes `scripts/loadbars-remote.sh` over SSH.
## Installation
### Dependencies (Fedora/RHEL/CentOS)
-To run loadbars on Fedora Linux, you need to install the following packages:
+To run loadbars on Fedora Linux, you need the install the SDL2 development libraries:
```bash
-sudo dnf install perl perl-SDL perl-Alien-SDL perl-Proc-ProcessTable
+sudo dnf install SDL2-devel
```
-### Dependencies for Remote Hosts
-
-For monitoring remote servers via SSH, the remote hosts need:
+On Ubuntu/Debian:
```bash
-sudo dnf install perl-Time-HiRes
+sudo apt install libsdl2-dev
```
### Running from Source
@@ -138,4 +136,4 @@ The Go build of loadbars links to **go-sdl2** (github.com/veandco/go-sdl2), whic
## Author
-Paul Buetow - <http://buetow.org>
+Paul Buetow - <http://paul.buetow.org>
diff --git a/internal/display/display.go b/internal/display/display.go
index fede729..49f9f10 100644
--- a/internal/display/display.go
+++ b/internal/display/display.go
@@ -53,7 +53,6 @@ func Run(ctx context.Context, cfg *config.Config, src stats.Source) error {
showNet := cfg.ShowNet
extended := cfg.Extended
winW, winH := int32(width), int32(height)
- redrawBg := true
// Previous CPU state for delta (key = host;cpuName)
prevCPU := make(map[string]collector.CPULine)
@@ -86,19 +85,15 @@ func Run(ctx context.Context, cfg *config.Config, src stats.Source) error {
return nil
case sdl.K_1:
showCores = !showCores
- redrawBg = true
fmt.Println("==> Toggled show cores:", showCores)
case sdl.K_2:
showMem = !showMem
- redrawBg = true
fmt.Println("==> Toggled show mem:", showMem)
case sdl.K_3:
showNet = !showNet
- redrawBg = true
fmt.Println("==> Toggled show net:", showNet)
case sdl.K_e:
extended = !extended
- redrawBg = true
case sdl.K_h:
printHotkeys()
case sdl.K_w:
@@ -117,30 +112,25 @@ func Run(ctx context.Context, cfg *config.Config, src stats.Source) error {
winW = 1
}
window.SetSize(winW, winH)
- redrawBg = true
case sdl.K_RIGHT:
winW += 100
if winW > int32(cfg.MaxWidth) {
winW = int32(cfg.MaxWidth)
}
window.SetSize(winW, winH)
- redrawBg = true
case sdl.K_UP:
winH -= 100
if winH < 1 {
winH = 1
}
window.SetSize(winW, winH)
- redrawBg = true
case sdl.K_DOWN:
winH += 100
window.SetSize(winW, winH)
- redrawBg = true
}
case *sdl.WindowEvent:
if ev.Event == sdl.WINDOWEVENT_RESIZED {
winW, winH = ev.Data1, ev.Data2
- redrawBg = true
}
}
}
@@ -168,11 +158,9 @@ func Run(ctx context.Context, cfg *config.Config, src stats.Source) error {
barWidth = 1
}
- if redrawBg {
- renderer.SetDrawColor(0, 0, 0, 255)
- renderer.Clear()
- redrawBg = false
- }
+ // Clear every frame so toggling cores off (or changing bar count) doesn't leave stale bars
+ renderer.SetDrawColor(0, 0, 0, 255)
+ renderer.Clear()
x := int32(0)
hosts := sortedHosts(snap)
@@ -187,6 +175,10 @@ func Run(ctx context.Context, cfg *config.Config, src stats.Source) error {
drawCPUBar(renderer, h.CPU[name], prevCPU[host+";"+name], barWidth, &x, winH)
prevCPU[host+";"+name] = h.CPU[name]
}
+ // Draw memory bar(s) for this host when showMem
+ if showMem {
+ drawMemBar(renderer, h, barWidth, &x, winH)
+ }
}
renderer.Present()
@@ -293,6 +285,63 @@ func drawCPUBar(renderer *sdl.Renderer, cur, prev collector.CPULine, barW int32,
fill(constants.Red.R, constants.Red.G, constants.Red.B, stealPct)
}
+// 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) {
+ defer func() { *x += barW + 1 }()
+ if h.Mem == nil {
+ return
+ }
+ memTotal := h.Mem["MemTotal"]
+ memFree := h.Mem["MemFree"]
+ swapTotal := h.Mem["SwapTotal"]
+ swapFree := h.Mem["SwapFree"]
+
+ 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
+ }
+ if ramUsedPct > 100 {
+ ramUsedPct = 100
+ }
+ 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})
+ }
+ 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})
+ }
+ }
+
+ // 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})
+ }
+ 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})
+ }
+ }
+}
+
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")
}