diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-18 09:33:08 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-18 09:33:08 +0200 |
| commit | f1951f2ee1e83d802030c257d4a1df099ec08976 (patch) | |
| tree | bce1d38cc5b78e4cb5d80d6e0d316267bb15cb9f | |
| parent | d845cf3208c3bbdb7e3dd3041d1ae491b88d4d21 (diff) | |
feat: add fixed load scale (--loadmax), load peak reset (r key), update README
- config: add LoadMax float64 field; loadmax key in ~/.loadbarsrc; written by 'w'
- main: add --loadmax flag (overrides rc file when > 0)
- display: newRunState initialises loadPeak from LoadMax when fixed
- display: updateLoadPeak accepts loadMax param; short-circuits to fixed value when set
- display: add 'r' hotkey to reset auto-scale peak to floor (2.0); no-op when fixed
- tooltip: loadTooltipLines shows 'Max:' label when scale is fixed, 'Peak:' for auto
- README: document 4/l load toggle, r reset, --showload, --loadmax, load average bars section; fix --cpumode entry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | README.md | 17 | ||||
| -rw-r--r-- | cmd/loadbars/main.go | 7 | ||||
| -rw-r--r-- | internal/config/config.go | 11 | ||||
| -rw-r--r-- | internal/display/display.go | 36 | ||||
| -rw-r--r-- | internal/display/tooltip.go | 14 |
5 files changed, 69 insertions, 16 deletions
@@ -72,13 +72,15 @@ All options can also be set in `~/.loadbarsrc` (key=value, no leading `--`). CLI | `--cpuaverage <n>` | Number of CPU samples used for average (and extended peak history) | 10 | | `--netaverage <n>` | Number of network samples used for average | 15 | | `--netlink <speed>` | Link speed for network utilization %: `mbit`, `10mbit`, `100mbit`, `gbit`, `10gbit` or a number | gbit | -| `--showcores` | Show one bar per CPU core (vs one aggregate bar per host) | off | +| `--cpumode <n>` | CPU display mode: 0 = aggregate bar, 1 = per-core bars, 2 = off | 0 | | `--showmem` | Show memory bars (RAM left, Swap right per host) | off | | `--shownet` | Show network bars (RX/TX across non-lo interfaces per host) | off | | `--extended` | Show extended display (1px peak line on CPU bars) | off | | `--title <text>` | Set title bar text | (empty) | | `--sshopts <opts>` | Extra SSH options passed to `ssh` (e.g. `-o ConnectTimeout=5`) | (empty) | | `--hasagent` | SSH key is already loaded in agent (skip extra agent checks) | off | +| `--showload` | Show load average bars (1-min teal fill, 5-min yellow line, 15-min white line per host) | off | +| `--loadmax <n>` | Fix the load bar full-height reference to `n` (e.g. `8` = core count); 0 = auto-scale | 0 | | `--maxbarsperrow <n>` | Max bars per row; 0 = unlimited (single row) | 0 | | `--help` | Show usage and exit | — | | `--version` | Print version and exit | — | @@ -134,9 +136,11 @@ Press these keys while loadbars is running (see also `h` for a short list on std | Key | Action | |-----|--------| -| **1** | Toggle CPU cores (one bar per core vs one aggregate bar per host) | +| **1** | Toggle CPU display mode: aggregate bar → per-core bars → off → aggregate | | **2** / **m** | Toggle memory bars (RAM left, Swap right per host) | | **3** / **n** | Toggle network bars (RX/TX summed across all non-lo interfaces per host) | +| **4** / **l** | Toggle load average bars (1-min teal fill, 5-min yellow line, 15-min white line) | +| **r** | Reset load auto-scale peak to floor (2.0) — has no effect when `loadmax` is fixed | | **e** | Toggle extended display (1px peak line on CPU bars: max system+user over last samples) | | **g** | Toggle global average CPU line (1px red line showing mean CPU usage across all hosts) | | **i** | Toggle global I/O average line (1px pink line showing mean iowait+IRQ across all hosts) | @@ -179,6 +183,15 @@ Press these keys while loadbars is running (see also `h` for a short list on std When network bar is red: No non-loopback interface exists on the specific remote host. +### Load average stuff + +- **Teal fill** from top downward: 1-minute load average, height proportional to the scale reference. +- **Yellow 1px line**: 5-minute load average. Appears inside the fill when load is rising, below it when falling. +- **White 1px line**: 15-minute load average. Same direction convention as the 5-min line. +- **Scale reference** (shown in hover tooltip): + - *Auto-scale* (`loadmax=0`, default): the reference tracks the global maximum 1-min load across all hosts, decaying slowly over time (floor 2.0). Press **r** to reset it back to 2.0 immediately. + - *Fixed scale* (`loadmax=N`): the bar's full height always equals `N`. Useful when you know the core count and want a stable reference across sessions. Tooltip shows `Max:` instead of `Peak:`. + **Aggregated interfaces:** Loadbars sums RX/TX across all non-loopback interfaces (e.g. `eth0`, `wlan0`, `enp0s3`) and shows the combined total. Loopback (`lo`) is always excluded. **Link speed** (`netlink`): Used to compute utilization %. Default is `gbit`. Set e.g. `netlink=100mbit` or `netlink=10gbit` in ~/.loadbarsrc or `--netlink 100mbit`. diff --git a/cmd/loadbars/main.go b/cmd/loadbars/main.go index 0ee8071..dd86723 100644 --- a/cmd/loadbars/main.go +++ b/cmd/loadbars/main.go @@ -35,6 +35,7 @@ func main() { flag.StringVar(&cfg.SSHOpts, "sshopts", cfg.SSHOpts, "Set SSH options") flag.BoolVar(&cfg.HasAgent, "hasagent", cfg.HasAgent, "SSH key already known by agent") flag.IntVar(&cfg.MaxBarsPerRow, "maxbarsperrow", cfg.MaxBarsPerRow, "Max bars per row (0=unlimited)") + loadmax := flag.Float64("loadmax", 0, "Fixed load bar full-height reference value (0 = auto-scale)") flag.Parse() @@ -48,7 +49,11 @@ func main() { os.Exit(constants.EUnknown) } - // Flags override config file; re-parse into cfg for hosts/cluster + // Flags override config file; re-parse into cfg for hosts/cluster. + // --loadmax overrides the rc file value when explicitly set to a positive number. + if *loadmax > 0 { + cfg.LoadMax = *loadmax + } if *cluster != "" { cfg.Cluster = *cluster } diff --git a/internal/config/config.go b/internal/config/config.go index 99e9c6d..3625341 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -30,6 +30,7 @@ type Config struct { ShowMem bool ShowNet bool ShowLoad bool + LoadMax float64 // 0 = auto-scale; >0 = fixed full-height reference value ShowSeparators bool MaxBarsPerRow int SSHOpts string @@ -110,7 +111,7 @@ func (c *Config) parseReader(f *os.File) error { "title": true, "barwidth": true, "cpuaverage": true, "extended": true, "hasagent": true, "height": true, "maxwidth": true, "netaverage": true, "netlink": true, "cpumode": true, "showcores": true, "showmem": true, - "showavgline": true, "showioavgline": true, "shownet": true, "showload": true, "showseparators": true, + "showavgline": true, "showioavgline": true, "shownet": true, "showload": true, "loadmax": true, "showseparators": true, "maxbarsperrow": true, "sshopts": true, "cluster": true, } scanner := bufio.NewScanner(f) @@ -188,6 +189,11 @@ func (c *Config) set(key, val string) { c.ShowNet = parseBool(val) case "showload": c.ShowLoad = parseBool(val) + case "loadmax": + // Accept any non-negative float; 0 means auto-scale. + if f, err := strconv.ParseFloat(val, 64); err == nil && f >= 0 { + c.LoadMax = f + } case "showseparators": c.ShowSeparators = parseBool(val) case "maxbarsperrow": @@ -205,6 +211,8 @@ 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) } writeStr := func(key, v string) { fmt.Fprintf(w, "%s=%s\n", key, v) } + // writeFloat uses %g to strip trailing zeros (e.g. 8 → "8", 8.5 → "8.5"). + writeFloat := func(key string, v float64) { fmt.Fprintf(w, "%s=%g\n", key, v) } writeBool := func(key string, v bool) { val := "0" if v { @@ -226,6 +234,7 @@ func (c *Config) writeTo(f *os.File) error { writeBool("showmem", c.ShowMem) writeBool("shownet", c.ShowNet) writeBool("showload", c.ShowLoad) + writeFloat("loadmax", c.LoadMax) writeBool("showseparators", c.ShowSeparators) writeInt("maxbarsperrow", c.MaxBarsPerRow) writeStr("sshopts", c.SSHOpts) diff --git a/internal/display/display.go b/internal/display/display.go index ffe5ca2..555e904 100644 --- a/internal/display/display.go +++ b/internal/display/display.go @@ -106,7 +106,13 @@ func newRunState(cfg *config.Config, winW, winH int32) *runState { showMem: cfg.ShowMem, showNet: cfg.ShowNet, showLoad: cfg.ShowLoad, - loadPeak: 2.0, // minimum floor ensures meaningful scale on idle systems + // Use the fixed cap when set; otherwise start at the auto-scale floor of 2.0. + loadPeak: func() float64 { + if cfg.LoadMax > 0 { + return cfg.LoadMax + } + return 2.0 + }(), showSeparators: cfg.ShowSeparators, extended: cfg.Extended, winW: winW, @@ -182,6 +188,15 @@ func handleKey(sym sdl.Keycode, window *sdl.Window, cfg *config.Config, state *r case sdl.K_4, sdl.K_l: state.showLoad = !state.showLoad fmt.Println("==> Toggled show load:", state.showLoad) + case sdl.K_r: + // Reset load auto-scale peak to the floor so the bar rescales immediately. + // Has no effect when loadmax is fixed (cfg.LoadMax > 0). + if cfg.LoadMax == 0 { + state.loadPeak = 2.0 + fmt.Println("==> Load peak reset to auto-scale floor (2.0)") + } else { + fmt.Println("==> Load peak reset ignored (fixed loadmax =", cfg.LoadMax, ")") + } case sdl.K_e: state.extended = !state.extended fmt.Println("==> Toggled extended (peak line):", state.extended) @@ -303,7 +318,7 @@ func drawFrame(renderer *sdl.Renderer, src stats.Source, cfg *config.Config, sta renderer.Clear() if state.showLoad { // Update the global load peak before drawing so bar scale is current. - updateLoadPeak(snap, state) + updateLoadPeak(snap, state, cfg.LoadMax) } drawBars(renderer, snap, cfg, state, numBars) if state.showAvgLine { @@ -730,7 +745,7 @@ func drawMemBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, smoothed *st } func printHotkeys() { - fmt.Println("=> Hotkeys: 1=cores 2/m=mem 3/n=net 4/l=load e=extended g=avg line i=io avg s=separators h=help 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/m=mem 3/n=net 4/l=load r=reset load peak e=extended g=avg line i=io avg s=separators h=help q=quit w=write config a/y=cpu avg d/c=net avg f/v=link scale arrows=resize") } // scaleLinkUp moves cfg.NetLink to the next higher link speed in linkScales. @@ -887,11 +902,16 @@ func drawNetBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, cfg *config. return prev } -// updateLoadPeak decays the global load peak and updates it with the current -// maximum 1-min load across all hosts. The floor of 2.0 prevents a zero scale -// on idle systems. The slow per-frame decay (× 0.9999) lets the scale recover -// gradually after a spike rather than snapping back immediately. -func updateLoadPeak(snap map[string]*stats.HostStats, state *runState) { +// updateLoadPeak maintains the load scale used by the bar renderer. +// When loadMax > 0, the scale is pinned to that fixed value every frame +// (no decay, no tracking). When loadMax == 0, auto-scale is used: the +// global peak decays slowly (× 0.9999 per frame) with a floor of 2.0, +// and is updated with the current maximum 1-min load across all hosts. +func updateLoadPeak(snap map[string]*stats.HostStats, state *runState, loadMax float64) { + if loadMax > 0 { + state.loadPeak = loadMax // fixed scale: override every frame, skip auto logic + return + } state.loadPeak *= 0.9999 // slow per-frame decay toward idle baseline if state.loadPeak < 2.0 { state.loadPeak = 2.0 diff --git a/internal/display/tooltip.go b/internal/display/tooltip.go index e5cf951..77ee365 100644 --- a/internal/display/tooltip.go +++ b/internal/display/tooltip.go @@ -37,7 +37,7 @@ func tooltipLines(bar *barDescriptor, snap map[string]*stats.HostStats, cfg *con case barNet: return netTooltipLines(bar, cfg, state) case barLoad: - return loadTooltipLines(bar, h, state) + return loadTooltipLines(bar, h, cfg, state) } return nil } @@ -101,8 +101,10 @@ func netTooltipLines(bar *barDescriptor, cfg *config.Config, state *runState) [] } // loadTooltipLines returns tooltip text for a load-average bar showing -// the smoothed 1/5/15-min averages and the current global peak scale. -func loadTooltipLines(bar *barDescriptor, h *stats.HostStats, state *runState) []string { +// the 1/5/15-min averages and the current scale reference. +// When cfg.LoadMax > 0 the scale is fixed and the label reads "Max:"; +// otherwise it tracks the auto-scale peak and reads "Peak:". +func loadTooltipLines(bar *barDescriptor, h *stats.HostStats, cfg *config.Config, state *runState) []string { lines := []string{fmt.Sprintf("%s [load]", bar.host)} l1, err1 := strconv.ParseFloat(strings.TrimSpace(h.LoadAvg1), 64) l5, err5 := strconv.ParseFloat(strings.TrimSpace(h.LoadAvg5), 64) @@ -111,11 +113,15 @@ func loadTooltipLines(bar *barDescriptor, h *stats.HostStats, state *runState) [ lines = append(lines, "No data yet") return lines } + scaleLabel := "Peak: " + if cfg.LoadMax > 0 { + scaleLabel = "Max: " + } lines = append(lines, fmt.Sprintf("1min: %.2f", l1), fmt.Sprintf("5min: %.2f", l5), fmt.Sprintf("15min: %.2f", l15), - fmt.Sprintf("Peak: %.2f", state.loadPeak), + fmt.Sprintf(scaleLabel+"%.2f", state.loadPeak), ) return lines } |
