Major changes: - Converted all cobra commands from global variables to CLI struct methods - Eliminated global logger variable in favor of dependency injection - Fixed all errcheck linter issues by properly handling errors - Fixed Makefile to check formatting instead of modifying files - Integrated smartconfig library for configuration management - Added CLAUDE.md with project-specific development guidelines Key improvements: - All commands (daemon, install, status, info) now use CLI struct methods - Logger is injected as dependency through fx providers - Proper error handling for all DrawText and file.Close() calls - Configuration loading now uses smartconfig with proper defaults - Fixed formatting check in Makefile (make test no longer modifies files) Technical details: - Created CLI struct with log field (renamed from logger per request) - All command constructors return *cobra.Command from CLI methods - Config package uses smartconfig.NewFromAppName() correctly - Fixed all critical errcheck issues throughout the codebase - Maintained backward compatibility with existing functionality All tests passing, code formatted, and ready for deployment.
140 lines
3.8 KiB
Go
140 lines
3.8 KiB
Go
package renderer
|
|
|
|
import (
|
|
"fmt"
|
|
"image/color"
|
|
"sort"
|
|
|
|
"git.eeqj.de/sneak/hdmistat/internal/layout"
|
|
"git.eeqj.de/sneak/hdmistat/internal/statcollector"
|
|
)
|
|
|
|
// ProcessScreen displays top processes
|
|
type ProcessScreen struct {
|
|
SortBy string // "cpu" or "memory"
|
|
}
|
|
|
|
func NewProcessScreenCPU() *ProcessScreen {
|
|
return &ProcessScreen{SortBy: "cpu"}
|
|
}
|
|
|
|
func NewProcessScreenMemory() *ProcessScreen {
|
|
return &ProcessScreen{SortBy: "memory"}
|
|
}
|
|
|
|
func (s *ProcessScreen) Name() string {
|
|
if s.SortBy == "cpu" {
|
|
return "Top Processes by CPU"
|
|
}
|
|
return "Top Processes by Memory"
|
|
}
|
|
|
|
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 := 50
|
|
|
|
// Title
|
|
_ = canvas.DrawText(s.Name(), layout.Point{X: width / 2, Y: y}, layout.TextStyle{
|
|
Size: titleStyle.Size,
|
|
Color: titleStyle.Color,
|
|
Alignment: layout.AlignCenter,
|
|
})
|
|
y += 70
|
|
|
|
// 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 >= 20 {
|
|
break
|
|
}
|
|
|
|
// Truncate long names
|
|
name := proc.Name
|
|
if len(name) > 30 {
|
|
name = name[:27] + "..."
|
|
}
|
|
|
|
user := proc.Username
|
|
if len(user) > 12 {
|
|
user = user[:9] + "..."
|
|
}
|
|
|
|
_ = 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)
|
|
|
|
// Highlight bar for high usage
|
|
if s.SortBy == "cpu" && proc.CPUPercent > 50 {
|
|
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) > 0.1 {
|
|
canvas.DrawBox(x-5, y-15, width-90, 20, color.RGBA{50, 50, 100, 100})
|
|
}
|
|
|
|
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)*100)
|
|
|
|
_ = canvas.DrawText(footerText, layout.Point{X: width / 2, Y: y}, layout.TextStyle{
|
|
Size: smallStyle.Size,
|
|
Color: smallStyle.Color,
|
|
Alignment: layout.AlignCenter,
|
|
})
|
|
|
|
return nil
|
|
}
|