diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/config/config.go | 15 | ||||
| -rw-r--r-- | internal/display/display.go | 228 | ||||
| -rw-r--r-- | internal/display/display_test.go | 171 |
3 files changed, 322 insertions, 92 deletions
diff --git a/internal/config/config.go b/internal/config/config.go index 9daf6d2..ba7886e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -30,6 +30,7 @@ type Config struct { ShowMem bool ShowNet bool ShowSeparators bool + MaxBarsPerRow int SSHOpts string Cluster string } @@ -45,9 +46,10 @@ func Default() Config { MaxWidth: 1900, NetAverage: 15, NetLink: "gbit", - ShowCores: false, - ShowMem: false, - ShowNet: false, + ShowCores: false, + ShowMem: false, + ShowNet: false, + MaxBarsPerRow: 0, } } @@ -108,7 +110,7 @@ func (c *Config) parseReader(f *os.File) error { "hasagent": true, "height": true, "maxwidth": true, "netaverage": true, "netlink": true, "showcores": true, "showmem": true, "showavgline": true, "showioavgline": true, "shownet": true, "showseparators": true, - "sshopts": true, "cluster": true, + "maxbarsperrow": true, "sshopts": true, "cluster": true, } scanner := bufio.NewScanner(f) for scanner.Scan() { @@ -175,6 +177,10 @@ func (c *Config) set(key, val string) { c.ShowNet = parseBool(val) case "showseparators": c.ShowSeparators = parseBool(val) + case "maxbarsperrow": + if n, err := strconv.Atoi(val); err == nil { + c.MaxBarsPerRow = n + } case "sshopts": c.SSHOpts = val case "cluster": @@ -207,6 +213,7 @@ func (c *Config) writeTo(f *os.File) error { writeBool("showmem", c.ShowMem) writeBool("shownet", c.ShowNet) writeBool("showseparators", c.ShowSeparators) + writeInt("maxbarsperrow", c.MaxBarsPerRow) writeStr("sshopts", c.SSHOpts) writeStr("cluster", c.Cluster) return w.Flush() diff --git a/internal/display/display.go b/internal/display/display.go index d31bdcb..623bde3 100644 --- a/internal/display/display.go +++ b/internal/display/display.go @@ -241,6 +241,31 @@ func barBounds(winW int32, numBars int, barIndex int) (x int32, width int32) { return startX, endX - startX } +// barRect computes the x, y, width, and height for a bar in a multi-row layout. +// When maxPerRow <= 0 or maxPerRow >= numBars, all bars fit in a single row (full height). +// Otherwise, bars wrap into multiple rows of equal height. The last row may have +// fewer bars, which become wider to fill the full window width. +func barRect(winW, winH int32, numBars, maxPerRow, barIndex int) (x, y, w, h int32) { + if maxPerRow <= 0 || maxPerRow >= numBars { + // Single row: full window height + bx, bw := barBounds(winW, numBars, barIndex) + return bx, 0, bw, winH + } + numRows := (numBars + maxPerRow - 1) / maxPerRow // ceil(numBars / maxPerRow) + row := barIndex / maxPerRow + col := barIndex % maxPerRow + // Count how many bars are in this row (last row may have fewer) + barsInRow := maxPerRow + if row == numRows-1 { + barsInRow = numBars - row*maxPerRow + } + // Divide window height evenly across rows + rowY := (winH * int32(row)) / int32(numRows) + rowH := (winH*int32(row+1))/int32(numRows) - rowY + bx, bw := barBounds(winW, barsInRow, col) + return bx, rowY, bw, rowH +} + // drawFrame updates state from snapshot, clears if layout changed, and draws all bars. // When showAvgLine/showIOAvgLine are enabled, global average lines are drawn on top. func drawFrame(renderer *sdl.Renderer, src stats.Source, cfg *config.Config, state *runState) { @@ -252,10 +277,10 @@ func drawFrame(renderer *sdl.Renderer, src stats.Source, cfg *config.Config, sta renderer.Clear() drawBars(renderer, snap, cfg, state, numBars) if state.showAvgLine { - drawGlobalAvgLine(renderer, snap, state) + drawGlobalAvgLine(renderer, snap, state, numBars, cfg.MaxBarsPerRow) } if state.showIOAvgLine { - drawGlobalIOAvgLine(renderer, snap, state) + drawGlobalIOAvgLine(renderer, snap, state, numBars, cfg.MaxBarsPerRow) } } @@ -279,34 +304,38 @@ func countBars(snap map[string]*stats.HostStats, showCores, showMem, showNet boo } // drawBars draws CPU, memory, and network bars for all hosts in snap. +// Bars wrap into multiple rows when cfg.MaxBarsPerRow is set. func drawBars(renderer *sdl.Renderer, snap map[string]*stats.HostStats, cfg *config.Config, state *runState, numBars int) { barIndex := 0 hosts := sortedHosts(snap) - // Track where each host's bars end so we can draw separators after all bars - var separatorXs []int32 + maxPerRow := cfg.MaxBarsPerRow + // Track separator rects (position + row height) for drawing after all bars + type sepRect struct{ x, y, h int32 } + var separators []sepRect for i, host := range hosts { h := snap[host] if h == nil { continue } - drawHostBars(renderer, h, host, cfg, state, numBars, &barIndex) + drawHostBars(renderer, h, host, cfg, state, numBars, maxPerRow, &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) + sx, sy, _, sh := barRect(state.winW, state.winH, numBars, maxPerRow, barIndex) + separators = append(separators, sepRect{sx, sy, sh}) } } // Draw 1px yellow vertical separators on top of all bars - for _, sepX := range separatorXs { + for _, sep := range separators { 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}) + renderer.FillRect(&sdl.Rect{X: sep.x, Y: sep.y, W: 1, H: sep.h}) } } -// drawGlobalAvgLine draws a 1px red horizontal line spanning the full window width -// at the Y position corresponding to the mean CPU usage across all hosts. -// CPU usage per host is the sum of all smoothed segments except idle (index 3). -func drawGlobalAvgLine(renderer *sdl.Renderer, snap map[string]*stats.HostStats, state *runState) { +// drawGlobalAvgLine draws a 1px red horizontal line at the Y position +// corresponding to the mean CPU usage across all hosts. When bars are +// split into multiple rows, one line is drawn per row at the correct +// proportional position within that row. +func drawGlobalAvgLine(renderer *sdl.Renderer, snap map[string]*stats.HostStats, state *runState, numBars, maxPerRow int) { var totalUsage float64 var hostCount int for _, host := range sortedHosts(snap) { @@ -333,21 +362,31 @@ func drawGlobalAvgLine(renderer *sdl.Renderer, snap map[string]*stats.HostStats, return } avgPct := totalUsage / float64(hostCount) - lineY := state.winH - int32(avgPct*float64(state.winH)/100) - if lineY < 0 { - lineY = 0 + renderer.SetDrawColor(constants.Red.R, constants.Red.G, constants.Red.B, 255) + // Draw one line per row, positioned proportionally within each row's height + numRows := 1 + if maxPerRow > 0 && maxPerRow < numBars { + numRows = (numBars + maxPerRow - 1) / maxPerRow } - if lineY >= state.winH { - lineY = state.winH - 1 + for row := 0; row < numRows; row++ { + rowY := (state.winH * int32(row)) / int32(numRows) + rowH := (state.winH*int32(row+1))/int32(numRows) - rowY + lineY := rowY + rowH - int32(avgPct*float64(rowH)/100) + if lineY < rowY { + lineY = rowY + } + if lineY >= rowY+rowH { + lineY = rowY + rowH - 1 + } + renderer.FillRect(&sdl.Rect{X: 0, Y: lineY, W: state.winW, H: 1}) } - renderer.SetDrawColor(constants.Red.R, constants.Red.G, constants.Red.B, 255) - renderer.FillRect(&sdl.Rect{X: 0, Y: lineY, W: state.winW, H: 1}) } // drawGlobalIOAvgLine draws a 1px pink horizontal line from the top of the window // at the Y position corresponding to the mean I/O overhead (iowait + IRQ + softIRQ, -// indices 4, 5, 6 in the smoothed CPU array) across all hosts. -func drawGlobalIOAvgLine(renderer *sdl.Renderer, snap map[string]*stats.HostStats, state *runState) { +// indices 4, 5, 6 in the smoothed CPU array) across all hosts. When bars are split +// into multiple rows, one line is drawn per row. +func drawGlobalIOAvgLine(renderer *sdl.Renderer, snap map[string]*stats.HostStats, state *runState, numBars, maxPerRow int) { var totalIO float64 var hostCount int for _, host := range sortedHosts(snap) { @@ -368,21 +407,29 @@ func drawGlobalIOAvgLine(renderer *sdl.Renderer, snap map[string]*stats.HostStat return } avgPct := totalIO / float64(hostCount) - // Draw from top: lineY = percentage of window height from the top - lineY := int32(avgPct * float64(state.winH) / 100) - if lineY < 0 { - lineY = 0 + renderer.SetDrawColor(constants.Pink.R, constants.Pink.G, constants.Pink.B, 255) + // Draw one line per row, positioned proportionally from the top of each row + numRows := 1 + if maxPerRow > 0 && maxPerRow < numBars { + numRows = (numBars + maxPerRow - 1) / maxPerRow } - if lineY >= state.winH { - lineY = state.winH - 1 + for row := 0; row < numRows; row++ { + rowY := (state.winH * int32(row)) / int32(numRows) + rowH := (state.winH*int32(row+1))/int32(numRows) - rowY + lineY := rowY + int32(avgPct*float64(rowH)/100) + if lineY < rowY { + lineY = rowY + } + if lineY >= rowY+rowH { + lineY = rowY + rowH - 1 + } + renderer.FillRect(&sdl.Rect{X: 0, Y: lineY, W: state.winW, H: 1}) } - renderer.SetDrawColor(constants.Pink.R, constants.Pink.G, constants.Pink.B, 255) - renderer.FillRect(&sdl.Rect{X: 0, Y: lineY, W: state.winW, H: 1}) } // drawHostBars draws CPU, mem, and net bars for one host and advances barIndex. -func drawHostBars(renderer *sdl.Renderer, h *stats.HostStats, host string, cfg *config.Config, state *runState, numBars int, barIndex *int) { - winH := state.winH +// maxPerRow controls multi-row wrapping (0 = single row). +func drawHostBars(renderer *sdl.Renderer, h *stats.HostStats, host string, cfg *config.Config, state *runState, numBars, maxPerRow int, barIndex *int) { cpuNames := sortedCPUNames(h.CPU, state.showCores) for _, name := range cpuNames { key := host + ";" + name @@ -404,25 +451,25 @@ func drawHostBars(renderer *sdl.Renderer, h *stats.HostStats, host string, cfg * normalizePcts(s) } peakPct := peakPctForBar(state, key, cfg.CPUAverage, s) - x, barW := barBounds(state.winW, numBars, *barIndex) + x, y, barW, barH := barRect(state.winW, state.winH, numBars, maxPerRow, *barIndex) *barIndex++ - drawCPUBarFromPcts(renderer, s, barW, x, winH, state.extended, peakPct) + drawCPUBarFromPcts(renderer, s, barW, x, y, barH, state.extended, peakPct) } if state.showMem { if state.smoothedMem[host] == nil { state.smoothedMem[host] = &struct{ ramUsed, swapUsed float64 }{} } - x, barW := barBounds(state.winW, numBars, *barIndex) + x, y, barW, barH := barRect(state.winW, state.winH, numBars, maxPerRow, *barIndex) *barIndex++ - drawMemBarSmoothed(renderer, h, state.smoothedMem[host], smoothFactor, barW, x, winH) + drawMemBarSmoothed(renderer, h, state.smoothedMem[host], smoothFactor, barW, x, y, barH) } if state.showNet { if state.smoothedNet[host] == nil { state.smoothedNet[host] = &struct{ rxPct, txPct float64 }{} } - x, barW := barBounds(state.winW, numBars, *barIndex) + x, y, barW, barH := barRect(state.winW, state.winH, numBars, maxPerRow, *barIndex) *barIndex++ - state.prevNet[host] = drawNetBarSmoothed(renderer, h, cfg, state.smoothedNet[host], state.prevNet[host], smoothFactor, barW, x, winH) + state.prevNet[host] = drawNetBarSmoothed(renderer, h, cfg, state.smoothedNet[host], state.prevNet[host], smoothFactor, barW, x, y, barH) } } @@ -527,44 +574,46 @@ func normalizePcts(s *[10]float64) { } } -// drawCPUBarFromPcts draws one CPU bar from 10 smoothed segment percentages. If s is nil, advances x only. -// When extended is true and peakPct > 0, draws a 1px peak line (max system+user over history). -func drawCPUBarFromPcts(renderer *sdl.Renderer, s *[10]float64, barW int32, x int32, winH int32, extended bool, peakPct float64) { +// drawCPUBarFromPcts draws one CPU bar from 10 smoothed segment percentages. +// The bar occupies the region (x, y) with dimensions (barW, barH). +// If s is nil, only clears the slot. When extended is true and peakPct > 0, +// draws a 1px peak line (max system+user over history). +func drawCPUBarFromPcts(renderer *sdl.Renderer, s *[10]float64, barW int32, x, y, barH int32, extended bool, peakPct float64) { // Clear this slot so we never leave previous (e.g. mem/net) content visible renderer.SetDrawColor(constants.Black.R, constants.Black.G, constants.Black.B, 255) - renderer.FillRect(&sdl.Rect{X: x, Y: 0, W: barW, H: winH}) + renderer.FillRect(&sdl.Rect{X: x, Y: y, W: barW, H: barH}) if s == nil { return } - barH := float64(winH) / 100.0 - y := float64(winH) + pxPerPct := float64(barH) / 100.0 + curY := float64(y + barH) fill := func(r, g, b uint8, pct float64) { - hh := int32(pct * barH) + hh := int32(pct * pxPerPct) if hh < 1 && pct > 0 { hh = 1 } - y -= float64(hh) + curY -= float64(hh) renderer.SetDrawColor(r, g, b, 255) - renderer.FillRect(&sdl.Rect{X: x, Y: int32(y), W: barW, H: hh}) + renderer.FillRect(&sdl.Rect{X: x, Y: int32(curY), W: barW, H: hh}) } - 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.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.LimeGreen.R, constants.LimeGreen.G, constants.LimeGreen.B, (*s)[9]) // guestnice - 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.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 // Extended: 1px peak line at max (system+user) over history if extended && peakPct > 0 { - peakY := winH - int32(peakPct*barH) - if peakY < 0 { - peakY = 0 + peakY := y + barH - int32(peakPct*pxPerPct) + if peakY < y { + peakY = y } - if peakY >= winH { - peakY = winH - 1 + if peakY >= y+barH { + peakY = y + barH - 1 } if peakPct > float64(constants.UserOrangeThreshold) { renderer.SetDrawColor(constants.Orange.R, constants.Orange.G, constants.Orange.B, 255) @@ -578,10 +627,11 @@ func drawCPUBarFromPcts(renderer *sdl.Renderer, s *[10]float64, barW int32, x in } // drawMemBarSmoothed blends mem stats toward target and draws one memory bar (RAM left, Swap right). -func drawMemBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, smoothed *struct{ ramUsed, swapUsed float64 }, factor float64, barW int32, x int32, winH int32) { +// The bar occupies the region (x, y) with dimensions (barW, barH). +func drawMemBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, smoothed *struct{ ramUsed, swapUsed float64 }, factor float64, barW int32, x, y, barH int32) { // Clear this slot so we never leave previous (e.g. CPU/net) content visible renderer.SetDrawColor(constants.Black.R, constants.Black.G, constants.Black.B, 255) - renderer.FillRect(&sdl.Rect{X: x, Y: 0, W: barW, H: winH}) + renderer.FillRect(&sdl.Rect{X: x, Y: y, W: barW, H: barH}) if h.Mem == nil { return } @@ -608,28 +658,28 @@ func drawMemBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, smoothed *st smoothed.swapUsed += (targetSwap - smoothed.swapUsed) * factor halfW := barW / 2 - barH := float64(winH) / 100.0 + pxPerPct := float64(barH) / 100.0 // RAM: used (dark grey) from bottom, free (black) on top - ramUsedH := int32(smoothed.ramUsed * barH) + ramUsedH := int32(smoothed.ramUsed * pxPerPct) 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}) + renderer.FillRect(&sdl.Rect{X: x, Y: y + barH - ramUsedH, W: halfW, H: ramUsedH}) } - if ramFreeH := winH - ramUsedH; ramFreeH > 0 { + if ramFreeH := barH - ramUsedH; 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}) + renderer.FillRect(&sdl.Rect{X: x, Y: y, W: halfW, H: ramFreeH}) } // Swap: used (grey) from bottom, free (black) on top - swapUsedH := int32(smoothed.swapUsed * barH) + swapUsedH := int32(smoothed.swapUsed * pxPerPct) 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}) + renderer.FillRect(&sdl.Rect{X: x + halfW, Y: y + barH - swapUsedH, W: halfW, H: swapUsedH}) } - if swapFreeH := winH - swapUsedH; swapFreeH > 0 { + if swapFreeH := barH - swapUsedH; 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}) + renderer.FillRect(&sdl.Rect{X: x + halfW, Y: y, W: halfW, H: swapFreeH}) } } @@ -713,18 +763,19 @@ func sumNonLoNet(h *stats.HostStats) (sum stats.NetStamp, hasIface bool) { // drawNetBarSmoothed sums RX/TX across all non-lo interfaces, computes utilization // vs link speed, smooths toward target, and draws one net bar (RX left from top, TX right from bottom). +// The bar occupies the region (x, y) with dimensions (barW, barH). // Smoothed values and prevNet are only updated when new collector data arrives // (cur.Stamp > prev.Stamp), so the bar holds steady between collector cycles // instead of decaying toward zero on frames with no new data. -func drawNetBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, cfg *config.Config, smoothed *struct{ rxPct, txPct float64 }, prev stats.NetStamp, factor float64, barW int32, x int32, winH int32) stats.NetStamp { +func drawNetBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, cfg *config.Config, smoothed *struct{ rxPct, txPct float64 }, prev stats.NetStamp, factor float64, barW int32, x, y, barH int32) stats.NetStamp { // Clear this slot so we never leave previous (e.g. CPU/mem) content visible renderer.SetDrawColor(constants.Black.R, constants.Black.G, constants.Black.B, 255) - renderer.FillRect(&sdl.Rect{X: x, Y: 0, W: barW, H: winH}) + renderer.FillRect(&sdl.Rect{X: x, Y: y, W: barW, H: barH}) cur, hasIface := sumNonLoNet(h) if !hasIface { // No non-lo interface: show red bar renderer.SetDrawColor(constants.Red.R, constants.Red.G, constants.Red.B, 255) - renderer.FillRect(&sdl.Rect{X: x, Y: 0, W: barW, H: winH}) + renderer.FillRect(&sdl.Rect{X: x, Y: y, W: barW, H: barH}) return prev } // Only recompute and smooth when the collector has provided new data. @@ -759,32 +810,33 @@ func drawNetBarSmoothed(renderer *sdl.Renderer, h *stats.HostStats, cfg *config. } halfW := barW / 2 - barH := float64(winH) / 100.0 + pxPerPct := float64(barH) / 100.0 + halfH := barH / 2 // Left half: RX from top (light green = used) - rxH := int32(smoothed.rxPct * barH) - if rxH > winH/2 { - rxH = winH / 2 + rxH := int32(smoothed.rxPct * pxPerPct) + if rxH > halfH { + rxH = halfH } if rxH > 0 { renderer.SetDrawColor(constants.LightGreen.R, constants.LightGreen.G, constants.LightGreen.B, 255) - renderer.FillRect(&sdl.Rect{X: x, Y: 0, W: halfW, H: rxH}) + renderer.FillRect(&sdl.Rect{X: x, Y: y, W: halfW, H: rxH}) } - if halfW > 0 && winH/2-rxH > 0 { + if halfW > 0 && halfH-rxH > 0 { renderer.SetDrawColor(constants.Black.R, constants.Black.G, constants.Black.B, 255) - renderer.FillRect(&sdl.Rect{X: x, Y: rxH, W: halfW, H: winH/2 - rxH}) + renderer.FillRect(&sdl.Rect{X: x, Y: y + rxH, W: halfW, H: halfH - rxH}) } // Right half: TX from bottom (light green = used) - txH := int32(smoothed.txPct * barH) - if txH > winH/2 { - txH = winH / 2 + txH := int32(smoothed.txPct * pxPerPct) + if txH > halfH { + txH = halfH } if txH > 0 { 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}) + renderer.FillRect(&sdl.Rect{X: x + halfW, Y: y + barH - txH, W: halfW, H: txH}) } - if halfW > 0 && (winH-txH) > 0 { + if halfW > 0 && (barH-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}) + renderer.FillRect(&sdl.Rect{X: x + halfW, Y: y, W: halfW, H: barH - txH}) } return prev } diff --git a/internal/display/display_test.go b/internal/display/display_test.go index f5f2e39..786291f 100644 --- a/internal/display/display_test.go +++ b/internal/display/display_test.go @@ -1397,6 +1397,177 @@ func TestSeparator_SingleHost(t *testing.T) { } } +// --- barRect tests --- + +func TestBarRect_Unlimited(t *testing.T) { + // maxPerRow=0 means unlimited → single row, same as barBounds with y=0, h=winH + const winW, winH int32 = 600, 200 + numBars := 6 + for i := 0; i < numBars; i++ { + x, y, w, h := barRect(winW, winH, numBars, 0, i) + bx, bw := barBounds(winW, numBars, i) + if x != bx || w != bw { + t.Errorf("bar %d: barRect x/w = (%d,%d), barBounds = (%d,%d)", i, x, w, bx, bw) + } + if y != 0 || h != winH { + t.Errorf("bar %d: expected y=0, h=%d; got y=%d, h=%d", i, winH, y, h) + } + } +} + +func TestBarRect_MultiRow(t *testing.T) { + // 6 bars, maxPerRow=4 → 2 rows: row 0 has 4 bars, row 1 has 2 bars + const winW, winH int32 = 400, 200 + numBars, maxPerRow := 6, 4 + + // Row 0: bars 0-3, each 100px wide, y=0, h=100 + for i := 0; i < 4; i++ { + x, y, w, h := barRect(winW, winH, numBars, maxPerRow, i) + if y != 0 || h != 100 { + t.Errorf("bar %d: expected y=0 h=100, got y=%d h=%d", i, y, h) + } + expectedX := int32(i) * 100 + if x != expectedX || w != 100 { + t.Errorf("bar %d: expected x=%d w=100, got x=%d w=%d", i, expectedX, x, w) + } + } + + // Row 1: bars 4-5, each 200px wide (2 bars fill 400px), y=100, h=100 + for i := 4; i < 6; i++ { + x, y, w, h := barRect(winW, winH, numBars, maxPerRow, i) + if y != 100 || h != 100 { + t.Errorf("bar %d: expected y=100 h=100, got y=%d h=%d", i, y, h) + } + col := i - 4 + expectedX := int32(col) * 200 + if x != expectedX || w != 200 { + t.Errorf("bar %d: expected x=%d w=200, got x=%d w=%d", i, expectedX, x, w) + } + } +} + +func TestBarRect_LastRowWider(t *testing.T) { + // 5 bars, maxPerRow=3 → 2 rows: row 0 has 3 bars (133px), row 1 has 2 bars (200px) + const winW, winH int32 = 400, 100 + numBars, maxPerRow := 5, 3 + + // Last row bars should be wider since fewer bars fill the full width + _, _, w0, _ := barRect(winW, winH, numBars, maxPerRow, 0) + _, _, w3, _ := barRect(winW, winH, numBars, maxPerRow, 3) + if w3 <= w0 { + t.Errorf("last row bars should be wider: row0 bar w=%d, row1 bar w=%d", w0, w3) + } +} + +func TestBarRect_SingleBar(t *testing.T) { + // Edge case: 1 bar fills the entire window + const winW, winH int32 = 300, 150 + x, y, w, h := barRect(winW, winH, 1, 0, 0) + if x != 0 || y != 0 || w != winW || h != winH { + t.Errorf("expected (0,0,%d,%d), got (%d,%d,%d,%d)", winW, winH, x, y, w, h) + } + // Also with maxPerRow=1 + x, y, w, h = barRect(winW, winH, 1, 1, 0) + if x != 0 || y != 0 || w != winW || h != winH { + t.Errorf("maxPerRow=1: expected (0,0,%d,%d), got (%d,%d,%d,%d)", winW, winH, x, y, w, h) + } +} + +func TestBarRect_MaxPerRowOne(t *testing.T) { + // maxPerRow=1: each bar gets its own row, full width + const winW, winH int32 = 200, 300 + numBars := 3 + for i := 0; i < numBars; i++ { + x, y, w, h := barRect(winW, winH, numBars, 1, i) + if x != 0 || w != winW { + t.Errorf("bar %d: expected x=0 w=%d, got x=%d w=%d", i, winW, x, w) + } + expectedY := winH * int32(i) / 3 + expectedH := winH*int32(i+1)/3 - expectedY + if y != expectedY || h != expectedH { + t.Errorf("bar %d: expected y=%d h=%d, got y=%d h=%d", i, expectedY, expectedH, y, h) + } + } +} + +func TestBarRect_NegativeMaxPerRow(t *testing.T) { + // Negative maxPerRow treated as unlimited (same as 0) + const winW, winH int32 = 400, 100 + numBars := 4 + for i := 0; i < numBars; i++ { + x, y, w, h := barRect(winW, winH, numBars, -1, i) + bx, bw := barBounds(winW, numBars, i) + if x != bx || w != bw || y != 0 || h != winH { + t.Errorf("bar %d: negative maxPerRow should act as unlimited", i) + } + } +} + +func TestBarRect_MaxPerRowExceedsNumBars(t *testing.T) { + // maxPerRow >= numBars → single row + const winW, winH int32 = 400, 100 + numBars := 3 + for i := 0; i < numBars; i++ { + x, y, w, h := barRect(winW, winH, numBars, 10, i) + bx, bw := barBounds(winW, numBars, i) + if x != bx || w != bw || y != 0 || h != winH { + t.Errorf("bar %d: maxPerRow >= numBars should act as single row", i) + } + } +} + +func TestMultiRow_DrawFrame(t *testing.T) { + // Verify that multi-row layout draws bars in correct positions + const w, h int32 = 200, 200 + + renderer, surface, err := createTestRenderer(w, h) + if err != nil { + t.Fatal(err) + } + defer renderer.Destroy() + defer surface.Free() + + // 4 hosts × 1 CPU bar = 4 bars, maxPerRow=2 → 2 rows of 2 bars + cfg := defaultTestConfig() + cfg.MaxBarsPerRow = 2 + + prev1, cur1 := makeCPUPair(100, 0, 0) // all system → blue + prev2, cur2 := makeCPUPair(0, 100, 0) // all user → yellow + prev3, cur3 := makeCPUPair(0, 0, 100) // all idle → black + prev4, cur4 := makeCPUPair(100, 0, 0) // all system → blue + + src := &mockSource{ + data: map[string]*stats.HostStats{ + "alpha": {CPU: map[string]collector.CPULine{"cpu": cur1}}, + "beta": {CPU: map[string]collector.CPULine{"cpu": cur2}}, + "gamma": {CPU: map[string]collector.CPULine{"cpu": cur3}}, + "delta": {CPU: map[string]collector.CPULine{"cpu": cur4}}, + }, + } + + state := newRunState(cfg, w, h) + state.prevCPU["alpha;cpu"] = prev1 + state.prevCPU["beta;cpu"] = prev2 + state.prevCPU["gamma;cpu"] = prev3 + state.prevCPU["delta;cpu"] = prev4 + + drawFrame(renderer, src, cfg, state) + + const tol = 5 + // Row 0 (y=0..99): 2 bars each 100px wide + // Bar 0 (alpha, blue) at x=50, y=90 + assertPixelColor(t, surface, 50, 90, constants.Blue, tol, "row0 bar0 blue") + // Bar 1 (beta, yellow) at x=150, y=90 + assertPixelColor(t, surface, 150, 90, constants.Yellow, tol, "row0 bar1 yellow") + + // Row 1 (y=100..199): 2 bars each 100px wide + // Hosts sorted alphabetically: alpha, beta, delta, gamma + // Bar 2 (delta, blue) at x=50, y=190 + assertPixelColor(t, surface, 50, 190, constants.Blue, tol, "row1 bar2 blue") + // Bar 3 (gamma, idle/black) at x=150, y=150 + assertPixelColor(t, surface, 150, 150, constants.Black, tol, "row1 bar3 black") +} + func TestHandleKey_WriteConfig_Separators(t *testing.T) { // Verify that 'w' hotkey persists showSeparators to config tmpDir := t.TempDir() |
