summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--internal/config/config.go11
-rw-r--r--internal/config/config_test.go37
-rw-r--r--internal/display/display.go31
-rw-r--r--internal/display/display_test.go148
5 files changed, 219 insertions, 9 deletions
diff --git a/README.md b/README.md
index 4746d61..eb6b45b 100644
--- a/README.md
+++ b/README.md
@@ -113,6 +113,7 @@ Press these keys while loadbars is running (see also `h` for a short list on std
| **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) |
+| **s** | Toggle host separator lines (1px yellow vertical line between hosts) |
| **h** | Print hotkey list to stdout |
| **q** | Quit |
| **w** | Write current settings to ~/.loadbarsrc |
diff --git a/internal/config/config.go b/internal/config/config.go
index d53fd7f..1b4c9e1 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -28,8 +28,9 @@ type Config struct {
ShowIOAvgLine bool
ShowCores bool
ShowMem bool
- ShowNet bool
- SSHOpts string
+ ShowNet bool
+ ShowSeparators bool
+ SSHOpts string
Cluster string
}
@@ -106,7 +107,8 @@ 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, "showcores": true, "showmem": true,
- "showavgline": true, "showioavgline": true, "shownet": true, "sshopts": true, "cluster": true,
+ "showavgline": true, "showioavgline": true, "shownet": true, "showseparators": true,
+ "sshopts": true, "cluster": true,
}
scanner := bufio.NewScanner(f)
for scanner.Scan() {
@@ -171,6 +173,8 @@ func (c *Config) set(key, val string) {
c.ShowMem = parseBool(val)
case "shownet":
c.ShowNet = parseBool(val)
+ case "showseparators":
+ c.ShowSeparators = parseBool(val)
case "sshopts":
c.SSHOpts = val
case "cluster":
@@ -207,6 +211,7 @@ func (c *Config) writeTo(f *os.File) error {
writeBool("showcores", c.ShowCores)
writeBool("showmem", c.ShowMem)
writeBool("shownet", c.ShowNet)
+ writeBool("showseparators", c.ShowSeparators)
writeStr("sshopts", c.SSHOpts)
writeStr("cluster", c.Cluster)
return w.Flush()
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index a8535be..630fa47 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -152,6 +152,43 @@ func TestConfig_showIOAvgLineRoundTrip(t *testing.T) {
}
}
+func TestConfig_showSeparatorsRoundTrip(t *testing.T) {
+ // Write a config with showseparators=1, read it back, verify round-trip
+ c := Default()
+ c.ShowSeparators = true
+
+ dir := t.TempDir()
+ path := filepath.Join(dir, "rc")
+ f, err := os.Create(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := c.writeTo(f); err != nil {
+ f.Close()
+ t.Fatal(err)
+ }
+ f.Close()
+
+ data, _ := os.ReadFile(path)
+ if !bytes.Contains(data, []byte("showseparators=1")) {
+ t.Errorf("expected showseparators=1 in output, got:\n%s", data)
+ }
+
+ // Read it back
+ c2 := Default()
+ f2, err := os.Open(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f2.Close()
+ if err := c2.parseReader(f2); err != nil {
+ t.Fatal(err)
+ }
+ if !c2.ShowSeparators {
+ t.Error("expected ShowSeparators=true after round-trip")
+ }
+}
+
func TestGetClusterHostsFromFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "clusters")
diff --git a/internal/display/display.go b/internal/display/display.go
index 54c5c51..d3f27ca 100644
--- a/internal/display/display.go
+++ b/internal/display/display.go
@@ -25,8 +25,9 @@ type runState struct {
showIOAvgLine bool
showCores bool
showMem bool
- showNet bool
- extended bool
+ showNet bool
+ showSeparators bool
+ extended bool
winW int32
winH int32
prevCPU map[string]collector.CPULine
@@ -44,8 +45,9 @@ func newRunState(cfg *config.Config, winW, winH int32) *runState {
showIOAvgLine: cfg.ShowIOAvgLine,
showCores: cfg.ShowCores,
showMem: cfg.ShowMem,
- showNet: cfg.ShowNet,
- extended: cfg.Extended,
+ showNet: cfg.ShowNet,
+ showSeparators: cfg.ShowSeparators,
+ extended: cfg.Extended,
winW: winW,
winH: winH,
prevCPU: make(map[string]collector.CPULine),
@@ -160,6 +162,9 @@ func handleKey(sym sdl.Keycode, window *sdl.Window, cfg *config.Config, state *r
case sdl.K_i:
state.showIOAvgLine = !state.showIOAvgLine
fmt.Println("==> Toggled global I/O avg line:", state.showIOAvgLine)
+ case sdl.K_s:
+ state.showSeparators = !state.showSeparators
+ fmt.Println("==> Toggled host separators:", state.showSeparators)
case sdl.K_a:
cfg.CPUAverage++
fmt.Println("==> CPU average samples:", cfg.CPUAverage)
@@ -188,6 +193,7 @@ func handleKey(sym sdl.Keycode, window *sdl.Window, cfg *config.Config, state *r
cfg.ShowCores = state.showCores
cfg.ShowMem = state.showMem
cfg.ShowNet = state.showNet
+ cfg.ShowSeparators = state.showSeparators
cfg.Extended = state.extended
if err := cfg.Write(); err != nil {
fmt.Fprintf(os.Stderr, "!!! Write config: %v\n", err)
@@ -271,12 +277,25 @@ func countBars(snap map[string]*stats.HostStats, showCores, showMem, showNet boo
// 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, numBars int) {
barIndex := 0
- for _, host := range sortedHosts(snap) {
+ hosts := sortedHosts(snap)
+ // Track where each host's bars end so we can draw separators after all bars
+ var separatorXs []int32
+ for i, host := range hosts {
h := snap[host]
if h == nil {
continue
}
drawHostBars(renderer, h, host, cfg, state, numBars, &barIndex)
+ // Record separator position between hosts (not after the last one)
+ if state.showSeparators && i < len(hosts)-1 {
+ sepX, _ := barBounds(state.winW, numBars, barIndex)
+ separatorXs = append(separatorXs, sepX)
+ }
+ }
+ // Draw 1px yellow vertical separators on top of all bars
+ for _, sepX := range separatorXs {
+ renderer.SetDrawColor(constants.Yellow.R, constants.Yellow.G, constants.Yellow.B, 255)
+ renderer.FillRect(&sdl.Rect{X: sepX, Y: 0, W: 1, H: state.winH})
}
}
@@ -611,7 +630,7 @@ func drawMemBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, smoothed *st
}
func printHotkeys() {
- fmt.Println("=> Hotkeys: 1=cores 2/m=mem 3/n=net e=extended g=avg line i=io avg 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 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")
}
diff --git a/internal/display/display_test.go b/internal/display/display_test.go
index 734e144..f5f2e39 100644
--- a/internal/display/display_test.go
+++ b/internal/display/display_test.go
@@ -1266,3 +1266,151 @@ func TestHandleKey_WriteConfig_IOAvgLine(t *testing.T) {
t.Error("expected ShowIOAvgLine=true in config after 'w'")
}
}
+
+func TestHandleKey_ToggleSeparators(t *testing.T) {
+ cfg := defaultTestConfig()
+ state := newRunState(cfg, 200, 100)
+ if state.showSeparators {
+ t.Fatal("expected showSeparators=false initially")
+ }
+ handleKey(sdl.K_s, nil, cfg, state)
+ if !state.showSeparators {
+ t.Fatal("expected showSeparators=true after pressing s")
+ }
+ handleKey(sdl.K_s, nil, cfg, state)
+ if state.showSeparators {
+ t.Fatal("expected showSeparators=false after pressing s again")
+ }
+}
+
+func TestSeparator_TwoHosts_Enabled(t *testing.T) {
+ // Two hosts (100% system = blue) with separators enabled: yellow pixel at boundary
+ const w, h int32 = 200, 100
+
+ renderer, surface, err := createTestRenderer(w, h)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer renderer.Destroy()
+ defer surface.Free()
+
+ prev1, cur1 := makeCPUPair(100, 0, 0) // all system → blue
+ prev2, cur2 := makeCPUPair(100, 0, 0)
+
+ cfg := defaultTestConfig()
+ cfg.ShowCores = false
+ cfg.ShowMem = false
+ cfg.ShowNet = false
+ cfg.ShowSeparators = true
+
+ src := &mockSource{
+ data: map[string]*stats.HostStats{
+ "alpha": {CPU: map[string]collector.CPULine{"cpu": cur1}},
+ "beta": {CPU: map[string]collector.CPULine{"cpu": cur2}},
+ },
+ }
+
+ state := newRunState(cfg, w, h)
+ state.prevCPU["alpha;cpu"] = prev1
+ state.prevCPU["beta;cpu"] = prev2
+
+ drawFrame(renderer, src, cfg, state)
+
+ // 2 bars at 200px → each 100px. Separator at x=100 (start of second host's bars)
+ assertPixelColor(t, surface, 100, 50, constants.Yellow, 3, "separator yellow at x=100")
+}
+
+func TestSeparator_TwoHosts_Disabled(t *testing.T) {
+ // Two hosts (100% system = blue) with separators disabled: no yellow at boundary
+ const w, h int32 = 200, 100
+
+ renderer, surface, err := createTestRenderer(w, h)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer renderer.Destroy()
+ defer surface.Free()
+
+ prev1, cur1 := makeCPUPair(100, 0, 0) // all system → blue
+ prev2, cur2 := makeCPUPair(100, 0, 0)
+
+ cfg := defaultTestConfig()
+ cfg.ShowCores = false
+ cfg.ShowMem = false
+ cfg.ShowNet = false
+ cfg.ShowSeparators = false
+
+ src := &mockSource{
+ data: map[string]*stats.HostStats{
+ "alpha": {CPU: map[string]collector.CPULine{"cpu": cur1}},
+ "beta": {CPU: map[string]collector.CPULine{"cpu": cur2}},
+ },
+ }
+
+ state := newRunState(cfg, w, h)
+ state.prevCPU["alpha;cpu"] = prev1
+ state.prevCPU["beta;cpu"] = prev2
+
+ drawFrame(renderer, src, cfg, state)
+
+ // At x=100, should be blue (second host's system bar), NOT yellow separator
+ assertPixelColor(t, surface, 100, 50, constants.Blue, 3, "no separator, should be blue")
+}
+
+func TestSeparator_SingleHost(t *testing.T) {
+ // Single host: no separator should be drawn even when enabled
+ const w, h int32 = 100, 100
+
+ renderer, surface, err := createTestRenderer(w, h)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer renderer.Destroy()
+ defer surface.Free()
+
+ prev, cur := makeCPUPair(50, 30, 20)
+ cfg := defaultTestConfig()
+ cfg.ShowCores = false
+ cfg.ShowMem = false
+ cfg.ShowNet = false
+ cfg.ShowSeparators = true
+
+ src := &mockSource{
+ data: map[string]*stats.HostStats{
+ "host1": {CPU: map[string]collector.CPULine{"cpu": cur}},
+ },
+ }
+
+ state := newRunState(cfg, w, h)
+ state.prevCPU["host1;cpu"] = prev
+
+ drawFrame(renderer, src, cfg, state)
+
+ // No separator at the edges — just verify no yellow at x=0 or x=99
+ r, g, b := getPixelColor(surface, 0, 50)
+ if r == constants.Yellow.R && g == constants.Yellow.G && b == constants.Yellow.B {
+ t.Errorf("unexpected yellow separator at x=0 with single host")
+ }
+ r, g, b = getPixelColor(surface, 99, 50)
+ if r == constants.Yellow.R && g == constants.Yellow.G && b == constants.Yellow.B {
+ t.Errorf("unexpected yellow separator at x=99 with single host")
+ }
+}
+
+func TestHandleKey_WriteConfig_Separators(t *testing.T) {
+ // Verify that 'w' hotkey persists showSeparators to config
+ tmpDir := t.TempDir()
+ origHome := os.Getenv("HOME")
+ os.Setenv("HOME", tmpDir)
+ defer os.Setenv("HOME", origHome)
+
+ cfg := defaultTestConfig()
+ state := newRunState(cfg, 200, 100)
+ state.showSeparators = true
+
+ handleKey(sdl.K_w, nil, cfg, state)
+
+ if !cfg.ShowSeparators {
+ t.Error("expected ShowSeparators=true in config after 'w'")
+ }
+}