hdmistat/internal/renderer/process_screen.go
2025-07-24 16:09:00 +02:00

216 lines
5.0 KiB
Go

//nolint:mnd
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
}