summaryrefslogtreecommitdiff
path: root/internal/display
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-16 23:03:53 +0200
committerPaul Buetow <paul@buetow.org>2026-02-16 23:03:53 +0200
commit41e25da4ccb3121ee9d22f8e9ad48568241d897c (patch)
tree2bd05c5cd5bc1fb7c52238037c647ca52015038f /internal/display
parent0f35a55c0ca2beef550d7a3fb425c53ea7b5ce88 (diff)
Add toggleable host separator lines (hotkey s)
Draw 1px yellow vertical lines between hosts when monitoring multiple servers, making it easy to see where one host's bars end and the next begins. Toggled with hotkey 's', persisted via 'w' to ~/.loadbarsrc. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/display')
-rw-r--r--internal/display/display.go31
-rw-r--r--internal/display/display_test.go148
2 files changed, 173 insertions, 6 deletions
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'")
+ }
+}