//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 }