168 lines
4.8 KiB
Go
168 lines
4.8 KiB
Go
package renderer
|
|
|
|
import (
|
|
"fmt"
|
|
"image/color"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.eeqj.de/sneak/hdmistat/internal/layout"
|
|
"git.eeqj.de/sneak/hdmistat/internal/statcollector"
|
|
)
|
|
|
|
const (
|
|
// Display constants
|
|
maxProcessNameLen = 30
|
|
maxUsernameLen = 12
|
|
topProcessCount = 20
|
|
cpuHighThreshold = 50.0
|
|
memoryHighRatio = 0.1
|
|
percentMultiplier = 100.0
|
|
halfDivisor = 2
|
|
)
|
|
|
|
// ProcessScreen displays top processes
|
|
type ProcessScreen struct {
|
|
SortBy string // "cpu" or "memory"
|
|
}
|
|
|
|
// NewProcessScreenCPU creates a new process screen sorted by CPU usage
|
|
func NewProcessScreenCPU() *ProcessScreen {
|
|
return &ProcessScreen{SortBy: "cpu"}
|
|
}
|
|
|
|
// NewProcessScreenMemory creates a new process screen sorted by memory usage
|
|
func NewProcessScreenMemory() *ProcessScreen {
|
|
return &ProcessScreen{SortBy: "memory"}
|
|
}
|
|
|
|
// Name returns the name of this screen
|
|
func (s *ProcessScreen) Name() string {
|
|
if s.SortBy == "cpu" {
|
|
return "Top Processes by CPU"
|
|
}
|
|
return "Top Processes by Memory"
|
|
}
|
|
|
|
// Render draws the process screen to the provided canvas
|
|
func (s *ProcessScreen) Render(canvas *layout.Canvas, info *statcollector.SystemInfo) error {
|
|
width, _ := canvas.Size()
|
|
|
|
// Colors
|
|
textColor := color.RGBA{255, 255, 255, 255}
|
|
headerColor := color.RGBA{100, 200, 255, 255}
|
|
dimColor := color.RGBA{150, 150, 150, 255}
|
|
|
|
// Styles
|
|
titleStyle := layout.TextStyle{Size: 36, Color: headerColor}
|
|
headerStyle := layout.TextStyle{Size: 20, Color: headerColor}
|
|
normalStyle := layout.TextStyle{Size: 16, Color: textColor}
|
|
smallStyle := layout.TextStyle{Size: 14, Color: dimColor}
|
|
|
|
y := 120 // Start below header - same as overview
|
|
|
|
// Get short hostname
|
|
shortHostname := info.Hostname
|
|
if idx := strings.Index(shortHostname, "."); idx > 0 {
|
|
shortHostname = shortHostname[:idx]
|
|
}
|
|
|
|
// Title - left aligned at consistent position
|
|
titleText := ""
|
|
if s.SortBy == "cpu" {
|
|
titleText = fmt.Sprintf("%s: cpu", shortHostname)
|
|
} else {
|
|
titleText = fmt.Sprintf("%s: memory", shortHostname)
|
|
}
|
|
_ = canvas.DrawText(titleText, layout.Point{X: 50, Y: y}, layout.TextStyle{
|
|
Size: 36, // Same size as overview
|
|
Color: titleStyle.Color,
|
|
Alignment: layout.AlignLeft,
|
|
})
|
|
y += 60
|
|
|
|
// Sort processes
|
|
processes := make([]statcollector.ProcessInfo, len(info.Processes))
|
|
copy(processes, info.Processes)
|
|
|
|
if s.SortBy == "cpu" {
|
|
sort.Slice(processes, func(i, j int) bool {
|
|
return processes[i].CPUPercent > processes[j].CPUPercent
|
|
})
|
|
} else {
|
|
sort.Slice(processes, func(i, j int) bool {
|
|
return processes[i].MemoryRSS > processes[j].MemoryRSS
|
|
})
|
|
}
|
|
|
|
// Table headers
|
|
x := 50
|
|
_ = canvas.DrawText("PID", layout.Point{X: x, Y: y}, headerStyle)
|
|
_ = canvas.DrawText("USER", layout.Point{X: x + 100, Y: y}, headerStyle)
|
|
_ = canvas.DrawText("PROCESS", layout.Point{X: x + 250, Y: y}, headerStyle)
|
|
_ = canvas.DrawText("CPU %", layout.Point{X: x + 600, Y: y}, headerStyle)
|
|
_ = canvas.DrawText("MEMORY", layout.Point{X: x + 700, Y: y}, headerStyle)
|
|
|
|
y += 30
|
|
canvas.DrawHLine(x, y, width-100, color.RGBA{100, 100, 100, 255})
|
|
y += 20
|
|
|
|
// Display top 20 processes
|
|
for i, proc := range processes {
|
|
if i >= topProcessCount {
|
|
break
|
|
}
|
|
|
|
// Truncate long names
|
|
name := proc.Name
|
|
if len(name) > maxProcessNameLen {
|
|
name = name[:maxProcessNameLen-3] + "..."
|
|
}
|
|
|
|
user := proc.Username
|
|
if len(user) > maxUsernameLen {
|
|
user = user[:maxUsernameLen-3] + "..."
|
|
}
|
|
|
|
// Highlight bar for high usage (draw BEFORE text)
|
|
if s.SortBy == "cpu" && proc.CPUPercent > cpuHighThreshold {
|
|
canvas.DrawBox(x-5, y-15, width-90, 20, color.RGBA{100, 50, 50, 100})
|
|
} else if s.SortBy == "memory" && float64(proc.MemoryRSS)/float64(info.MemoryTotal) > memoryHighRatio {
|
|
canvas.DrawBox(x-5, y-15, width-90, 20, color.RGBA{50, 50, 100, 100})
|
|
}
|
|
|
|
_ = canvas.DrawText(fmt.Sprintf("%d", proc.PID), layout.Point{X: x, Y: y}, normalStyle)
|
|
_ = canvas.DrawText(user, layout.Point{X: x + 100, Y: y}, normalStyle)
|
|
_ = canvas.DrawText(name, layout.Point{X: x + 250, Y: y}, normalStyle)
|
|
_ = canvas.DrawText(fmt.Sprintf("%.1f", proc.CPUPercent), layout.Point{X: x + 600, Y: y}, normalStyle)
|
|
_ = canvas.DrawText(layout.FormatBytes(proc.MemoryRSS), layout.Point{X: x + 700, Y: y}, normalStyle)
|
|
|
|
y += 25
|
|
}
|
|
|
|
// Footer with system totals
|
|
y = 950
|
|
canvas.DrawHLine(50, y, width-100, color.RGBA{100, 100, 100, 255})
|
|
y += 30
|
|
|
|
totalCPU := 0.0
|
|
for _, cpu := range info.CPUPercent {
|
|
totalCPU += cpu
|
|
}
|
|
avgCPU := totalCPU / float64(len(info.CPUPercent))
|
|
|
|
footerText := fmt.Sprintf("System: CPU %.1f%% | Memory: %s / %s (%.1f%%)",
|
|
avgCPU,
|
|
layout.FormatBytes(info.MemoryUsed),
|
|
layout.FormatBytes(info.MemoryTotal),
|
|
float64(info.MemoryUsed)/float64(info.MemoryTotal)*percentMultiplier)
|
|
|
|
_ = canvas.DrawText(footerText, layout.Point{X: width / halfDivisor, Y: y}, layout.TextStyle{
|
|
Size: smallStyle.Size,
|
|
Color: smallStyle.Color,
|
|
Alignment: layout.AlignCenter,
|
|
})
|
|
|
|
return nil
|
|
}
|