Features: - Beautiful system statistics display using IBM Plex Mono font - Direct framebuffer rendering without X11/Wayland - Multiple screens with automatic carousel rotation - Real-time system monitoring (CPU, memory, disk, network, processes) - Systemd service integration with install command - Clean architecture using uber/fx dependency injection Architecture: - Cobra CLI with daemon, install, status, and info commands - Modular design with separate packages for display, rendering, and stats - Font embedding for zero runtime dependencies - Layout API for clean text rendering - Support for multiple screen types (overview, top CPU, top memory) Technical details: - Uses gopsutil for cross-platform system stats collection - Direct Linux framebuffer access via memory mapping - Anti-aliased text rendering with freetype - Configurable screen rotation and update intervals - Structured logging with slog - Comprehensive test coverage and linting setup This initial version provides a solid foundation for displaying rich system information on resource-constrained devices like Raspberry Pis.
159 lines
4.4 KiB
Go
159 lines
4.4 KiB
Go
package renderer
|
|
|
|
import (
|
|
"fmt"
|
|
"image/color"
|
|
|
|
"git.eeqj.de/sneak/hdmistat/internal/layout"
|
|
"git.eeqj.de/sneak/hdmistat/internal/statcollector"
|
|
)
|
|
|
|
// OverviewScreen displays system overview
|
|
type OverviewScreen struct{}
|
|
|
|
func NewOverviewScreen() *OverviewScreen {
|
|
return &OverviewScreen{}
|
|
}
|
|
|
|
func (s *OverviewScreen) Name() string {
|
|
return "System Overview"
|
|
}
|
|
|
|
func (s *OverviewScreen) Render(canvas *layout.Canvas, info *statcollector.SystemInfo) error {
|
|
width, height := 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: 48, Color: headerColor}
|
|
headerStyle := layout.TextStyle{Size: 24, Color: headerColor}
|
|
normalStyle := layout.TextStyle{Size: 18, Color: textColor}
|
|
smallStyle := layout.TextStyle{Size: 16, Color: dimColor}
|
|
|
|
y := 50
|
|
|
|
// Title
|
|
canvas.DrawText(info.Hostname, layout.Point{X: width / 2, Y: y}, layout.TextStyle{
|
|
Size: titleStyle.Size,
|
|
Color: titleStyle.Color,
|
|
Alignment: layout.AlignCenter,
|
|
})
|
|
y += 80
|
|
|
|
// Uptime
|
|
uptimeText := fmt.Sprintf("Uptime: %s", layout.FormatDuration(info.Uptime.Seconds()))
|
|
canvas.DrawText(uptimeText, layout.Point{X: width / 2, Y: y}, layout.TextStyle{
|
|
Size: smallStyle.Size,
|
|
Color: smallStyle.Color,
|
|
Alignment: layout.AlignCenter,
|
|
})
|
|
y += 60
|
|
|
|
// Two column layout
|
|
leftX := 50
|
|
rightX := width/2 + 50
|
|
|
|
// Memory section (left)
|
|
canvas.DrawText("MEMORY", layout.Point{X: leftX, Y: y}, headerStyle)
|
|
y += 35
|
|
|
|
memUsedPercent := float64(info.MemoryUsed) / float64(info.MemoryTotal) * 100
|
|
canvas.DrawText(fmt.Sprintf("Total: %s", layout.FormatBytes(info.MemoryTotal)),
|
|
layout.Point{X: leftX, Y: y}, normalStyle)
|
|
y += 25
|
|
canvas.DrawText(fmt.Sprintf("Used: %s (%.1f%%)", layout.FormatBytes(info.MemoryUsed), memUsedPercent),
|
|
layout.Point{X: leftX, Y: y}, normalStyle)
|
|
y += 25
|
|
canvas.DrawText(fmt.Sprintf("Free: %s", layout.FormatBytes(info.MemoryFree)),
|
|
layout.Point{X: leftX, Y: y}, normalStyle)
|
|
y += 35
|
|
|
|
// Memory progress bar
|
|
canvas.DrawProgress(leftX, y, 400, 20, memUsedPercent,
|
|
color.RGBA{100, 200, 100, 255},
|
|
color.RGBA{50, 50, 50, 255})
|
|
|
|
// CPU section (right)
|
|
cpuY := y - 115
|
|
canvas.DrawText("CPU", layout.Point{X: rightX, Y: cpuY}, headerStyle)
|
|
cpuY += 35
|
|
|
|
// Show per-core CPU usage
|
|
for i, percent := range info.CPUPercent {
|
|
if i >= 8 {
|
|
// Limit display to 8 cores
|
|
canvas.DrawText(fmt.Sprintf("... and %d more cores", len(info.CPUPercent)-8),
|
|
layout.Point{X: rightX, Y: cpuY}, smallStyle)
|
|
break
|
|
}
|
|
canvas.DrawText(fmt.Sprintf("Core %d:", i), layout.Point{X: rightX, Y: cpuY}, smallStyle)
|
|
canvas.DrawProgress(rightX+80, cpuY-12, 200, 15, percent,
|
|
color.RGBA{255, 100, 100, 255},
|
|
color.RGBA{50, 50, 50, 255})
|
|
cpuY += 20
|
|
}
|
|
|
|
y += 60
|
|
|
|
// Disk usage section
|
|
canvas.DrawText("DISK USAGE", layout.Point{X: leftX, Y: y}, headerStyle)
|
|
y += 35
|
|
|
|
for i, disk := range info.DiskUsage {
|
|
if i >= 4 {
|
|
break // Limit to 4 disks
|
|
}
|
|
canvas.DrawText(disk.Path, layout.Point{X: leftX, Y: y}, normalStyle)
|
|
usageText := fmt.Sprintf("%s / %s", layout.FormatBytes(disk.Used), layout.FormatBytes(disk.Total))
|
|
canvas.DrawText(usageText, layout.Point{X: leftX + 200, Y: y}, smallStyle)
|
|
canvas.DrawProgress(leftX+400, y-12, 300, 15, disk.UsedPercent,
|
|
color.RGBA{200, 200, 100, 255},
|
|
color.RGBA{50, 50, 50, 255})
|
|
y += 30
|
|
}
|
|
|
|
y += 30
|
|
|
|
// Network section
|
|
canvas.DrawText("NETWORK", layout.Point{X: leftX, Y: y}, headerStyle)
|
|
y += 35
|
|
|
|
for i, net := range info.Network {
|
|
if i >= 3 {
|
|
break // Limit to 3 interfaces
|
|
}
|
|
canvas.DrawText(net.Name, layout.Point{X: leftX, Y: y}, normalStyle)
|
|
|
|
if len(net.IPAddresses) > 0 {
|
|
canvas.DrawText(net.IPAddresses[0], layout.Point{X: leftX + 150, Y: y}, smallStyle)
|
|
}
|
|
|
|
trafficText := fmt.Sprintf("TX: %s RX: %s",
|
|
layout.FormatBytes(net.BytesSent),
|
|
layout.FormatBytes(net.BytesRecv))
|
|
canvas.DrawText(trafficText, layout.Point{X: leftX + 400, Y: y}, smallStyle)
|
|
y += 30
|
|
}
|
|
|
|
// Temperature section (bottom right)
|
|
if len(info.Temperature) > 0 {
|
|
tempY := height - 200
|
|
canvas.DrawText("TEMPERATURE", layout.Point{X: rightX, Y: tempY}, headerStyle)
|
|
tempY += 35
|
|
|
|
for sensor, temp := range info.Temperature {
|
|
if tempY > height-50 {
|
|
break
|
|
}
|
|
canvas.DrawText(fmt.Sprintf("%s: %.1f°C", sensor, temp),
|
|
layout.Point{X: rightX, Y: tempY}, normalStyle)
|
|
tempY += 25
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|