summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-14 00:12:54 +0200
committerPaul Buetow <paul@buetow.org>2026-02-14 00:12:54 +0200
commit5e416d613f61cfc16277d343e30c3c8a00b21b35 (patch)
treed73b06932e9806c21f4ff7a8bf3f985b361f82cf /internal
parent956f84321860bd8e318545564474037cbd3b6fd9 (diff)
remove version and some refactoring
Diffstat (limited to 'internal')
-rw-r--r--internal/app/app.go3
-rw-r--r--internal/app/script.go29
-rw-r--r--internal/app/store.go32
-rw-r--r--internal/collector/collector.go20
-rw-r--r--internal/collector/parse_test.go10
-rw-r--r--internal/collector/types.go2
-rw-r--r--internal/config/config.go62
-rw-r--r--internal/constants/constants.go54
-rw-r--r--internal/display/display.go500
-rw-r--r--internal/stats/stats.go6
10 files changed, 367 insertions, 351 deletions
diff --git a/internal/app/app.go b/internal/app/app.go
index 4544f0f..c18dc30 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -15,7 +15,6 @@ func Run(cfg *config.Config) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- scriptPath := ScriptPath()
store := NewStore()
var wg sync.WaitGroup
@@ -24,7 +23,7 @@ func Run(cfg *config.Config) error {
wg.Add(1)
go func() {
defer wg.Done()
- _ = collector.Run(ctx, h, cfg, store, scriptPath)
+ _ = collector.Run(ctx, h, cfg, store)
}()
}
diff --git a/internal/app/script.go b/internal/app/script.go
deleted file mode 100644
index 2701cb2..0000000
--- a/internal/app/script.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package app
-
-import (
- "os"
- "path/filepath"
-)
-
-// ScriptPath returns the path to the loadbars-remote.sh script.
-// It looks for scripts/loadbars-remote.sh relative to the executable, then current dir.
-func ScriptPath() string {
- exe, err := os.Executable()
- if err == nil {
- dir := filepath.Dir(exe)
- // When installed: exe is /usr/bin/loadbars, script might be /usr/share/loadbars/scripts/
- for _, sub := range []string{"scripts/loadbars-remote.sh", "../scripts/loadbars-remote.sh", "../../scripts/loadbars-remote.sh"} {
- p := filepath.Join(dir, sub)
- if _, err := os.Stat(p); err == nil {
- return p
- }
- }
- }
- // Development: run from repo root
- if p, err := filepath.Abs("scripts/loadbars-remote.sh"); err == nil {
- if _, err := os.Stat(p); err == nil {
- return p
- }
- }
- return "scripts/loadbars-remote.sh"
-}
diff --git a/internal/app/store.go b/internal/app/store.go
index 424bb17..9e046a4 100644
--- a/internal/app/store.go
+++ b/internal/app/store.go
@@ -26,18 +26,7 @@ func NewStore() *Store {
return &Store{hosts: make(map[string]*hostData)}
}
-func (s *Store) getOrCreate(host string) *hostData {
- if s.hosts[host] == nil {
- s.hosts[host] = &hostData{
- mem: make(map[string]int64),
- net: make(map[string]stats.NetStamp),
- cpu: make(map[string]collector.CPULine),
- }
- }
- return s.hosts[host]
-}
-
-// SetLoadAvg implements collector.StatsStore.
+// SetLoadAvg sets the load average for the given host.
func (s *Store) SetLoadAvg(host, load1, load5, load15 string) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -45,7 +34,7 @@ func (s *Store) SetLoadAvg(host, load1, load5, load15 string) {
d.load1, d.load5, d.load15 = load1, load5, load15
}
-// SetCPU implements collector.StatsStore.
+// SetCPU sets the CPU line for the given host and CPU name.
func (s *Store) SetCPU(host, name string, line collector.CPULine) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -53,7 +42,7 @@ func (s *Store) SetCPU(host, name string, line collector.CPULine) {
d.cpu[name] = line
}
-// SetMem implements collector.StatsStore.
+// SetMem sets a meminfo key/value for the given host.
func (s *Store) SetMem(host, key string, value int64) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -61,7 +50,7 @@ func (s *Store) SetMem(host, key string, value int64) {
d.mem[key] = value
}
-// SetNet implements collector.StatsStore.
+// SetNet sets the network stamp for the given host and interface.
func (s *Store) SetNet(host, iface string, net collector.NetLine, stamp float64) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -69,7 +58,7 @@ func (s *Store) SetNet(host, iface string, net collector.NetLine, stamp float64)
d.net[iface] = stats.NetStamp{B: net.B, Tb: net.Tb, Stamp: stamp}
}
-// Snapshot returns a copy of current stats for all hosts (for display).
+// Snapshot returns a copy of current stats for all hosts for the display.
func (s *Store) Snapshot() map[string]*stats.HostStats {
s.mu.RLock()
defer s.mu.RUnlock()
@@ -99,5 +88,16 @@ func (s *Store) Snapshot() map[string]*stats.HostStats {
return out
}
+func (s *Store) getOrCreate(host string) *hostData {
+ if s.hosts[host] == nil {
+ s.hosts[host] = &hostData{
+ mem: make(map[string]int64),
+ net: make(map[string]stats.NetStamp),
+ cpu: make(map[string]collector.CPULine),
+ }
+ }
+ return s.hosts[host]
+}
+
var _ stats.Source = (*Store)(nil)
var _ collector.StatsStore = (*Store)(nil)
diff --git a/internal/collector/collector.go b/internal/collector/collector.go
index 42ab689..dea88c7 100644
--- a/internal/collector/collector.go
+++ b/internal/collector/collector.go
@@ -2,9 +2,9 @@ package collector
import (
"bufio"
+ "bytes"
"context"
"fmt"
- "os"
"os/exec"
"strings"
"time"
@@ -20,13 +20,16 @@ type StatsStore interface {
SetNet(host, iface string, net NetLine, stamp float64)
}
-// Run starts a collector for one host: runs the remote (or local) script and parses the stream into store.
+// Run starts a collector for one host: runs the embedded remote script (local or over SSH) and parses the stream into store.
// Host may be "host" or "host:user". It runs until ctx is cancelled or the command exits.
-func Run(ctx context.Context, host string, cfg *config.Config, store StatsStore, scriptPath string) error {
+// The script is embedded in the binary; no external script file is required.
+func Run(ctx context.Context, host string, cfg *config.Config, store StatsStore) error {
hostKey, user := splitHostUser(host)
+ script := bytes.NewReader(RemoteScript)
var scanner *bufio.Scanner
if isLocal(hostKey) {
- cmd := exec.CommandContext(ctx, "bash", scriptPath)
+ cmd := exec.CommandContext(ctx, "bash", "-s")
+ cmd.Stdin = script
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("%s: %w", hostKey, err)
@@ -46,12 +49,7 @@ func Run(ctx context.Context, host string, cfg *config.Config, store StatsStore,
}
args = append(args, hostKey, "bash -s")
cmd := exec.CommandContext(ctx, "ssh", args...)
- scriptFile, err := os.Open(scriptPath)
- if err != nil {
- return fmt.Errorf("%s: open script: %w", hostKey, err)
- }
- defer scriptFile.Close()
- cmd.Stdin = scriptFile
+ cmd.Stdin = script
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("%s: %w", hostKey, err)
@@ -123,5 +121,3 @@ func splitHostUser(host string) (h, u string) {
func isLocal(h string) bool {
return h == "localhost" || h == "127.0.0.1"
}
-
-
diff --git a/internal/collector/parse_test.go b/internal/collector/parse_test.go
index 6edac33..ec77067 100644
--- a/internal/collector/parse_test.go
+++ b/internal/collector/parse_test.go
@@ -6,12 +6,12 @@ import (
func TestParseCPULine(t *testing.T) {
tests := []struct {
- name string
- line string
- wantName string
- wantUser int64
+ name string
+ line string
+ wantName string
+ wantUser int64
wantTotal int64
- wantErr bool
+ wantErr bool
}{
{"normal", "cpu 100 0 50 200 0 0 0 0 0 0", "cpu", 100, 350, false},
{"cpu0", "cpu0 10 0 5 80 0 0 0 0 0 0", "cpu0", 10, 95, false},
diff --git a/internal/collector/types.go b/internal/collector/types.go
index 1b10979..b0db600 100644
--- a/internal/collector/types.go
+++ b/internal/collector/types.go
@@ -2,7 +2,7 @@ package collector
// CPULine is one line of /proc/stat: cpu name + counters (user, nice, system, idle, ...).
type CPULine struct {
- Name string
+ Name string
User, Nice, System, Idle, Iowait, IRQ, SoftIRQ, Steal, Guest, GuestNice int64
}
diff --git a/internal/config/config.go b/internal/config/config.go
index 42db523..e523372 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -39,13 +39,13 @@ func Default() Config {
CPUAverage: 10,
Extended: false,
HasAgent: false,
- Height: 150,
- MaxWidth: 1900,
+ Height: 150,
+ MaxWidth: 1900,
NetAverage: 15,
- NetLink: "gbit",
- ShowCores: false,
- ShowMem: false,
- ShowNet: false,
+ NetLink: "gbit",
+ ShowCores: false,
+ ShowMem: false,
+ ShowNet: false,
}
}
@@ -75,6 +75,31 @@ func (c *Config) Load() error {
return c.parseReader(f)
}
+// Write saves the current config to the config file (excluding title).
+func (c *Config) Write() error {
+ path, err := ConfFilePath()
+ if err != nil {
+ return err
+ }
+ f, err := os.Create(path)
+ if err != nil {
+ return fmt.Errorf("create config: %w", err)
+ }
+ defer f.Close()
+ return c.writeTo(f)
+}
+
+// GetClusterHosts resolves a cluster name from /etc/clusters into a list of hosts.
+func GetClusterHosts(cluster string) ([]string, error) {
+ return GetClusterHostsFromFile(cluster, constants.CSSHConfFile)
+}
+
+// GetClusterHostsFromFile resolves a cluster from a clusters file (for testing or custom path).
+// Supports recursive cluster references with cycle detection.
+func GetClusterHostsFromFile(cluster, path string) ([]string, error) {
+ return getClusterHostsRec(cluster, path, 1, nil)
+}
+
func (c *Config) parseReader(f *os.File) error {
validKeys := map[string]bool{
"title": true, "barwidth": true, "cpuaverage": true, "extended": true,
@@ -155,20 +180,6 @@ func parseBool(s string) bool {
return s == "1" || s == "true" || s == "yes"
}
-// Write saves the current config to the config file (excluding title).
-func (c *Config) Write() error {
- path, err := ConfFilePath()
- if err != nil {
- return err
- }
- f, err := os.Create(path)
- if err != nil {
- return fmt.Errorf("create config: %w", err)
- }
- defer f.Close()
- return c.writeTo(f)
-}
-
func (c *Config) writeTo(f *os.File) error {
w := bufio.NewWriter(f)
writeInt := func(key string, v int) { fmt.Fprintf(w, "%s=%d\n", key, v) }
@@ -197,17 +208,6 @@ func (c *Config) writeTo(f *os.File) error {
return w.Flush()
}
-// GetClusterHosts resolves a cluster name from /etc/clusters into a list of hosts.
-func GetClusterHosts(cluster string) ([]string, error) {
- return GetClusterHostsFromFile(cluster, constants.CSSHConfFile)
-}
-
-// GetClusterHostsFromFile resolves a cluster from a clusters file (for testing or custom path).
-// Supports recursive cluster references with cycle detection.
-func GetClusterHostsFromFile(cluster, path string) ([]string, error) {
- return getClusterHostsRec(cluster, path, 1, nil)
-}
-
func getClusterHostsRec(cluster, path string, depth int, seen map[string]bool) ([]string, error) {
if depth > constants.CSSHMaxRecursion {
return nil, fmt.Errorf("cluster recursion limit reached in %s (possible cycle)", path)
diff --git a/internal/constants/constants.go b/internal/constants/constants.go
index 44a0478..6787045 100644
--- a/internal/constants/constants.go
+++ b/internal/constants/constants.go
@@ -2,18 +2,18 @@ package constants
// Paths and limits
const (
- ConfFile = ".loadbarsrc"
- CSSHConfFile = "/etc/clusters"
- CSSHMaxRecursion = 10
- ColorDepth = 32
- Interval = 0.14 // CPU sampling interval (seconds)
- IntervalNet = 3.0 // Network stats interval
- IntervalMem = 1.0 // Memory stats interval
- IntervalSDL = 0.14 // Display refresh interval
- IntervalSDLWarn = 1.0 // Warn if loop is slower than this
- SystemBlueThreshold = 30 // CPU system % for lighter blue
- UserOrangeThreshold = 70 // CPU user % for orange
- UserYellowThreshold = 50 // CPU user % for dark yellow
+ ConfFile = ".loadbarsrc"
+ CSSHConfFile = "/etc/clusters"
+ CSSHMaxRecursion = 10
+ ColorDepth = 32
+ Interval = 0.14 // CPU sampling interval (seconds)
+ IntervalNet = 3.0 // Network stats interval
+ IntervalMem = 1.0 // Memory stats interval
+ IntervalSDL = 0.14 // Display refresh interval
+ IntervalSDLWarn = 1.0 // Warn if loop is slower than this
+ SystemBlueThreshold = 30 // CPU system % for lighter blue
+ UserOrangeThreshold = 70 // CPU user % for orange
+ UserYellowThreshold = 50 // CPU user % for dark yellow
)
// Exit codes
@@ -26,27 +26,27 @@ const (
// Copyright string
const Copyright = "2010-2026 (c) Paul Buetow <loadbars@dev.buetow.org>"
-// RGB returns R, G, B in 0-255 for SDL.
+// RGB holds R, G, B in 0-255 for SDL.
type RGB struct{ R, G, B uint8 }
// Colors used by the display (matching Perl Constants.pm)
var (
- Black = RGB{0x00, 0x00, 0x00}
- Blue0 = RGB{0x00, 0x00, 0xff}
- LightBlue = RGB{0x00, 0x00, 0xdd}
+ Black = RGB{0x00, 0x00, 0x00}
+ Blue0 = RGB{0x00, 0x00, 0xff}
+ LightBlue = RGB{0x00, 0x00, 0xdd}
LightBlue0 = RGB{0x00, 0x00, 0xcc}
- Blue = RGB{0x00, 0x00, 0x88}
- Green = RGB{0x00, 0x90, 0x00}
+ Blue = RGB{0x00, 0x00, 0x88}
+ Green = RGB{0x00, 0x90, 0x00}
LightGreen = RGB{0x00, 0xf0, 0x00}
- Orange = RGB{0xff, 0x70, 0x00}
- Purple = RGB{0xa0, 0x20, 0xf0}
- Red = RGB{0xff, 0x00, 0x00}
- White = RGB{0xff, 0xff, 0xff}
- Grey0 = RGB{0x11, 0x11, 0x11}
- Grey = RGB{0xaa, 0xaa, 0xaa}
- DarkGrey = RGB{0x15, 0x15, 0x15}
- Yellow0 = RGB{0xff, 0xa0, 0x00}
- Yellow = RGB{0xff, 0xc0, 0x00}
+ Orange = RGB{0xff, 0x70, 0x00}
+ Purple = RGB{0xa0, 0x20, 0xf0}
+ Red = RGB{0xff, 0x00, 0x00}
+ White = RGB{0xff, 0xff, 0xff}
+ Grey0 = RGB{0x11, 0x11, 0x11}
+ Grey = RGB{0xaa, 0xaa, 0xaa}
+ DarkGrey = RGB{0x15, 0x15, 0x15}
+ Yellow0 = RGB{0xff, 0xa0, 0x00}
+ Yellow = RGB{0xff, 0xc0, 0x00}
)
// BytesPerSec for link speed reference (bytes per second at given mbit)
diff --git a/internal/display/display.go b/internal/display/display.go
index 45b7728..231b8c6 100644
--- a/internal/display/display.go
+++ b/internal/display/display.go
@@ -17,6 +17,51 @@ import (
"github.com/veandco/go-sdl2/sdl"
)
+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
+ lastNumBars int
+ lastWinW int32
+ lastWinH 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
+}
+
+// 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,
+ lastNumBars: -1,
+ 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),
+ }
+}
+
// Run runs the SDL display loop until ctx is cancelled or user presses 'q'.
func Run(ctx context.Context, cfg *config.Config, src stats.Source) error {
if err := sdl.Init(sdl.INIT_VIDEO); err != nil {
@@ -24,55 +69,25 @@ func Run(ctx context.Context, cfg *config.Config, src stats.Source) error {
}
defer sdl.Quit()
- width := cfg.BarWidth
- if width < 1 {
- width = 1
- }
- if width > cfg.MaxWidth {
- width = cfg.MaxWidth
- }
+ const minWindowWidth = 800
+ width := clampInt(cfg.BarWidth, minWindowWidth, cfg.MaxWidth)
height := cfg.Height
if height < 1 {
height = 1
}
-
title := cfg.Title
if title == "" {
title = "Loadbars " + version.Version + " (press h for help on stdout)"
}
-
window, renderer, err := sdl.CreateWindowAndRenderer(int32(width), int32(height), sdl.WINDOW_RESIZABLE)
if err != nil {
return fmt.Errorf("create window: %w", err)
}
defer window.Destroy()
defer renderer.Destroy()
-
window.SetTitle(title)
- // Mutable copy of config for hotkey toggles (only what display needs)
- showCores := cfg.ShowCores
- showMem := cfg.ShowMem
- showNet := cfg.ShowNet
- extended := cfg.Extended
- winW, winH := int32(width), int32(height)
-
- // Previous CPU state for delta (key = host;cpuName)
- prevCPU := make(map[string]collector.CPULine)
- // 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)
+ state := newRunState(cfg, int32(width), int32(height))
ticker := time.NewTicker(time.Duration(constants.IntervalSDL * float64(time.Second)))
defer ticker.Stop()
@@ -82,211 +97,246 @@ func Run(ctx context.Context, cfg *config.Config, src stats.Source) error {
return ctx.Err()
default:
}
+ if handleEvents(window, cfg, state) {
+ return nil
+ }
+ drawFrame(renderer, src, cfg, state)
+ renderer.Present()
+ sdl.Delay(10)
+ <-ticker.C
+ }
+}
- // Poll all pending events
- for e := sdl.PollEvent(); e != nil; e = sdl.PollEvent() {
- switch ev := e.(type) {
- case *sdl.QuitEvent:
- return nil
- case *sdl.KeyboardEvent:
- if ev.Type != sdl.KEYDOWN || ev.Repeat != 0 {
- continue
- }
- sym := ev.Keysym.Sym
- switch sym {
- case sdl.K_q:
- return nil
- case sdl.K_1:
- showCores = !showCores
- fmt.Println("==> Toggled show cores:", showCores)
- case sdl.K_2:
- showMem = !showMem
- fmt.Println("==> Toggled show mem:", showMem)
- 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
- cfg.ShowNet = showNet
- cfg.Extended = extended
- if err := cfg.Write(); err != nil {
- fmt.Fprintf(os.Stderr, "!!! Write config: %v\n", err)
- } else {
- fmt.Println("==> Config written to ~/.loadbarsrc")
- }
- case sdl.K_LEFT:
- winW -= 100
- if winW < 1 {
- winW = 1
- }
- window.SetSize(winW, winH)
- case sdl.K_RIGHT:
- winW += 100
- if winW > int32(cfg.MaxWidth) {
- winW = int32(cfg.MaxWidth)
- }
- window.SetSize(winW, winH)
- case sdl.K_UP:
- winH -= 100
- if winH < 1 {
- winH = 1
- }
- window.SetSize(winW, winH)
- case sdl.K_DOWN:
- winH += 100
- window.SetSize(winW, winH)
- }
- case *sdl.WindowEvent:
- if ev.Event == sdl.WINDOWEVENT_RESIZED {
- winW, winH = ev.Data1, ev.Data2
- }
+func clampInt(v, min, max int) int {
+ if v < min {
+ return min
+ }
+ if v > max {
+ return max
+ }
+ return v
+}
+
+// handleEvents processes all pending SDL events and updates state. Returns true if the user quit.
+func handleEvents(window *sdl.Window, cfg *config.Config, state *runState) bool {
+ for e := sdl.PollEvent(); e != nil; e = sdl.PollEvent() {
+ switch ev := e.(type) {
+ case *sdl.QuitEvent:
+ return true
+ case *sdl.KeyboardEvent:
+ if ev.Type != sdl.KEYDOWN || ev.Repeat != 0 {
+ continue
+ }
+ if handleKey(ev.Keysym.Sym, window, cfg, state) {
+ return true
+ }
+ case *sdl.WindowEvent:
+ if ev.Event == sdl.WINDOWEVENT_RESIZED {
+ state.winW, state.winH = ev.Data1, ev.Data2
}
}
+ }
+ return false
+}
- snap := src.Snapshot()
- if cycleNetNext {
- for _, host := range sortedHosts(snap) {
- netIntIndex[host]++
- }
- cycleNetNext = false
+// handleKey handles one key press; returns true to quit.
+func handleKey(sym sdl.Keycode, window *sdl.Window, cfg *config.Config, state *runState) bool {
+ switch sym {
+ case sdl.K_q:
+ return true
+ case sdl.K_1:
+ state.showCores = !state.showCores
+ fmt.Println("==> Toggled show cores:", state.showCores)
+ case sdl.K_2:
+ state.showMem = !state.showMem
+ fmt.Println("==> Toggled show mem:", state.showMem)
+ case sdl.K_3:
+ state.showNet = !state.showNet
+ fmt.Println("==> Toggled show net:", state.showNet)
+ if state.showNet {
+ state.printNetInfoOnce = true
}
- // One-time: print which interface is used for net stats and how to configure
- if printNetInfoOnce && showNet {
- printNetInfoOnce = false
- printNetInterfaceHelp(snap, cfg, netIntIndex)
+ case sdl.K_e:
+ state.extended = !state.extended
+ fmt.Println("==> Toggled extended (peak line):", state.extended)
+ case sdl.K_a:
+ cfg.CPUAverage++
+ fmt.Println("==> CPU average samples:", cfg.CPUAverage)
+ case sdl.K_y:
+ if cfg.CPUAverage > 1 {
+ cfg.CPUAverage--
}
- // 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++
- }
- }
+ 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--
}
- if numBars == 0 {
- numBars = 1
+ fmt.Println("==> Net average samples:", cfg.NetAverage)
+ case sdl.K_h:
+ printHotkeys()
+ case sdl.K_n:
+ state.cycleNetNext = true
+ if state.showNet {
+ fmt.Println("==> Cycling to next network interface (per host)")
}
-
- barWidth := winW / int32(numBars)
- if barWidth < 1 {
- barWidth = 1
+ case sdl.K_w:
+ cfg.ShowCores = state.showCores
+ cfg.ShowMem = state.showMem
+ cfg.ShowNet = state.showNet
+ cfg.Extended = state.extended
+ if err := cfg.Write(); err != nil {
+ fmt.Fprintf(os.Stderr, "!!! Write config: %v\n", err)
+ } else {
+ fmt.Println("==> Config written to ~/.loadbarsrc")
}
+ case sdl.K_LEFT:
+ state.winW -= 100
+ if state.winW < 1 {
+ state.winW = 1
+ }
+ window.SetSize(state.winW, state.winH)
+ case sdl.K_RIGHT:
+ state.winW += 100
+ if state.winW > int32(cfg.MaxWidth) {
+ state.winW = int32(cfg.MaxWidth)
+ }
+ window.SetSize(state.winW, state.winH)
+ case sdl.K_UP:
+ state.winH -= 100
+ if state.winH < 1 {
+ state.winH = 1
+ }
+ window.SetSize(state.winW, state.winH)
+ case sdl.K_DOWN:
+ state.winH += 100
+ window.SetSize(state.winW, state.winH)
+ }
+ return false
+}
- // 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
+// 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 {
+ barWidth = 1
+ }
+ if numBars != state.lastNumBars || state.winW != state.lastWinW || state.winH != state.lastWinH {
+ renderer.SetDrawColor(0, 0, 0, 255)
+ renderer.Clear()
+ state.lastNumBars = numBars
+ state.lastWinW, state.lastWinH = state.winW, state.winH
+ }
+ drawBars(renderer, snap, cfg, state, barWidth)
+}
- x := int32(0)
- hosts := sortedHosts(snap)
- for _, host := range hosts {
- h := snap[host]
- if h == nil {
- continue
- }
- // Draw CPU bars for this host (aggregate or per-core), with smoothing
- cpuNames := sortedCPUNames(h.CPU, showCores)
- for _, name := range cpuNames {
- 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, with smoothing
+func countBars(snap map[string]*stats.HostStats, showCores, showMem, showNet bool) int {
+ n := 0
+ for _, host := range sortedHosts(snap) {
+ if h := snap[host]; h != nil {
+ n += len(sortedCPUNames(h.CPU, showCores))
if showMem {
- if smoothedMem[host] == nil {
- smoothedMem[host] = &struct{ ramUsed, swapUsed float64 }{}
- }
- drawMemBarSmoothed(renderer, h, smoothedMem[host], smoothFactor, barWidth, &x, winH)
+ n++
}
- // 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)
+ n++
}
}
+ }
+ if n == 0 {
+ n = 1
+ }
+ return n
+}
- renderer.Present()
- sdl.Delay(10)
+// drawBars draws CPU, memory, and network bars for all hosts in snap.
+func drawBars(renderer *sdl.Renderer, snap map[string]*stats.HostStats, cfg *config.Config, state *runState, barWidth int32) {
+ x := int32(0)
+ for _, host := range sortedHosts(snap) {
+ h := snap[host]
+ if h == nil {
+ continue
+ }
+ drawHostBars(renderer, h, host, cfg, state, barWidth, &x)
+ }
+}
- <-ticker.C
+// drawHostBars draws CPU, mem, and net bars for one host and advances x.
+func drawHostBars(renderer *sdl.Renderer, h *stats.HostStats, host string, cfg *config.Config, state *runState, barWidth int32, x *int32) {
+ winH := state.winH
+ cpuNames := sortedCPUNames(h.CPU, state.showCores)
+ for _, name := range cpuNames {
+ key := host + ";" + name
+ cur := h.CPU[name]
+ prev := state.prevCPU[key]
+ state.prevCPU[key] = cur
+ target, ok := cpuBarTargetPcts(cur, prev)
+ s := state.smoothedCPU[key]
+ if s == nil {
+ s = &[9]float64{}
+ state.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)
+ }
+ peakPct := peakPctForBar(state, key, cfg.CPUAverage, s)
+ drawCPUBarFromPcts(renderer, s, barWidth, x, winH, state.extended, peakPct)
+ }
+ if state.showMem {
+ if state.smoothedMem[host] == nil {
+ state.smoothedMem[host] = &struct{ ramUsed, swapUsed float64 }{}
+ }
+ drawMemBarSmoothed(renderer, h, state.smoothedMem[host], smoothFactor, barWidth, x, winH)
+ }
+ if state.showNet {
+ 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)
+ }
+}
+
+func peakPctForBar(state *runState, key string, cpuAvg int, s *[9]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 {
@@ -386,15 +436,15 @@ func drawCPUBarFromPcts(renderer *sdl.Renderer, s *[9]float64, barW int32, x *in
renderer.SetDrawColor(r, g, b, 255)
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.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.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
+ 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)
@@ -618,7 +668,7 @@ func drawNetBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, cfg *config.
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 {
+ 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})
}
diff --git a/internal/stats/stats.go b/internal/stats/stats.go
index f2ef85b..ebb81b3 100644
--- a/internal/stats/stats.go
+++ b/internal/stats/stats.go
@@ -14,9 +14,9 @@ type NetStamp struct {
// HostStats holds the latest stats for one host (read-only snapshot).
type HostStats struct {
LoadAvg1, LoadAvg5, LoadAvg15 string
- Mem map[string]int64
- Net map[string]NetStamp
- CPU map[string]collector.CPULine
+ Mem map[string]int64
+ Net map[string]NetStamp
+ CPU map[string]collector.CPULine
}
// Source is the interface the display uses to read current stats.