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 }