212 lines
4.9 KiB
Go
212 lines
4.9 KiB
Go
//nolint:mnd
|
|
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
|
|
}
|