hdmistat/internal/renderer/renderer.go
2025-07-24 14:32:50 +02:00

181 lines
4.8 KiB
Go

package renderer
import (
"image"
"image/color"
"log/slog"
"os/exec"
"strings"
"time"
"git.eeqj.de/sneak/hdmistat/internal/layout"
"git.eeqj.de/sneak/hdmistat/internal/statcollector"
"github.com/golang/freetype/truetype"
)
// Screen represents a displayable screen
type Screen interface {
Name() string
Render(canvas *layout.Canvas, info *statcollector.SystemInfo) error
}
// Renderer manages screen rendering
type Renderer struct {
font *truetype.Font
logger *slog.Logger
width int
height int
}
// NewRenderer creates a new renderer
func NewRenderer(font *truetype.Font, logger *slog.Logger) *Renderer {
return &Renderer{
font: font,
logger: logger,
width: 1920, // Default HD resolution
height: 1080,
}
}
// SetResolution sets the rendering resolution
func (r *Renderer) SetResolution(width, height int) {
r.width = width
r.height = height
}
// RenderScreen renders a screen to an image
func (r *Renderer) RenderScreen(screen Screen, info *statcollector.SystemInfo) (*image.RGBA, error) {
canvas := layout.NewCanvas(r.width, r.height, r.font, r.logger)
// Draw common header
r.drawHeader(canvas, info)
if err := screen.Render(canvas, info); err != nil {
return nil, err
}
return canvas.Image(), nil
}
// drawHeader draws the common header with system info
func (r *Renderer) drawHeader(canvas *layout.Canvas, _ *statcollector.SystemInfo) {
width, _ := canvas.Size()
headerColor := color.RGBA{150, 150, 150, 255}
headerStyle := layout.TextStyle{Size: 14, Color: headerColor, Bold: true}
// Get uname info
uname := "Unknown System"
if output, err := exec.Command("uname", "-a").Output(); err == nil {
uname = strings.TrimSpace(string(output))
// Truncate if too long
if len(uname) > 150 {
uname = uname[:147] + "..."
}
}
// Draw uname on left
_ = canvas.DrawText(uname, layout.Point{X: 20, Y: 20}, headerStyle)
// Check NTP sync status
ntpSynced := r.checkNTPSync()
var syncIndicator string
var syncColor color.Color
if ntpSynced {
syncIndicator = "*"
syncColor = color.RGBA{0, 255, 0, 255} // Green
} else {
syncIndicator = "?"
syncColor = color.RGBA{255, 0, 0, 255} // Red
}
// Time formats
now := time.Now()
utcTime := now.UTC().Format("Mon 2006-01-02 15:04:05 UTC")
localTime := now.Format("Mon 2006-01-02 15:04:05 MST")
// Draw times on the right with sync indicators
// For simplicity, we'll use a fixed position approach
// Draw UTC time
_ = canvas.DrawText(utcTime, layout.Point{X: width - 40, Y: 20}, layout.TextStyle{
Size: headerStyle.Size,
Color: color.RGBA{255, 255, 255, 255}, // White
Alignment: layout.AlignRight,
Bold: true,
})
// UTC sync indicators
_ = canvas.DrawText(syncIndicator, layout.Point{X: width - 280, Y: 20}, layout.TextStyle{
Size: headerStyle.Size,
Color: syncColor,
Bold: true,
})
_ = canvas.DrawText(syncIndicator, layout.Point{X: width - 20, Y: 20}, layout.TextStyle{
Size: headerStyle.Size,
Color: syncColor,
Bold: true,
})
// Draw local time
_ = canvas.DrawText(localTime, layout.Point{X: width - 40, Y: 35}, layout.TextStyle{
Size: headerStyle.Size,
Color: color.RGBA{255, 255, 255, 255}, // White
Alignment: layout.AlignRight,
Bold: true,
})
// Local sync indicators
_ = canvas.DrawText(syncIndicator, layout.Point{X: width - 280, Y: 35}, layout.TextStyle{
Size: headerStyle.Size,
Color: syncColor,
Bold: true,
})
_ = canvas.DrawText(syncIndicator, layout.Point{X: width - 20, Y: 35}, layout.TextStyle{
Size: headerStyle.Size,
Color: syncColor,
Bold: true,
})
// Get uptime command output
uptimeStr := "uptime unavailable"
if output, err := exec.Command("uptime").Output(); err == nil {
uptimeStr = strings.TrimSpace(string(output))
}
// Draw uptime on second line
_ = canvas.DrawText(uptimeStr, layout.Point{X: 20, Y: 40}, headerStyle)
// Draw horizontal rule with more space
canvas.DrawHLine(0, 70, width, color.RGBA{100, 100, 100, 255})
}
// checkNTPSync checks if the system clock is synchronized with NTP
func (r *Renderer) checkNTPSync() bool {
// Try timedatectl first (systemd systems)
if output, err := exec.Command("timedatectl", "status").Output(); err == nil {
outputStr := string(output)
// Look for "System clock synchronized: yes" or "NTP synchronized: yes"
if strings.Contains(outputStr, "synchronized: yes") {
return true
}
return false
}
// Try chronyc (chrony)
if output, err := exec.Command("chronyc", "tracking").Output(); err == nil {
outputStr := string(output)
// Look for "Leap status : Normal"
if strings.Contains(outputStr, "Leap status : Normal") {
return true
}
return false
}
// Try ntpstat (ntpd)
if err := exec.Command("ntpstat").Run(); err == nil {
// ntpstat returns 0 if synchronized
return true
}
// Default to not synced if we can't determine
return false
}