hdmistat/cmd/fbhello/main.go
2025-07-24 16:09:00 +02:00

177 lines
4.0 KiB
Go

//nolint:mnd
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
"git.eeqj.de/sneak/hdmistat/internal/fbdraw"
"git.eeqj.de/sneak/hdmistat/internal/layout"
)
const (
// DefaultFontSize is the font size used throughout the application
DefaultFontSize = 24
)
// HelloWorldScreen implements the FrameGenerator interface
type HelloWorldScreen struct {
width int
height int
}
// NewHelloWorldScreen creates a new hello world screen
func NewHelloWorldScreen() *HelloWorldScreen {
return &HelloWorldScreen{}
}
// Init initializes the screen with the display dimensions
func (h *HelloWorldScreen) Init(width, height int) error {
h.width = width
h.height = height
return nil
}
// getSystemUptime returns the system uptime
func getSystemUptime() (time.Duration, error) {
data, err := os.ReadFile("/proc/uptime")
if err != nil {
return 0, err
}
fields := strings.Fields(string(data))
if len(fields) < 1 {
return 0, fmt.Errorf("invalid /proc/uptime format")
}
seconds, err := strconv.ParseFloat(fields[0], 64)
if err != nil {
return 0, err
}
return time.Duration(seconds * float64(time.Second)), nil
}
// GenerateFrame generates a frame with "Hello World" and a timestamp
func (h *HelloWorldScreen) GenerateFrame(grid *fbdraw.CharGrid) error {
// Create a draw context that works directly on the provided grid
draw := layout.NewDraw(grid)
// Clear the screen with a dark background
draw.Clear()
// Calculate center position
centerY := grid.Height / 2
// Draw "Hello World" in the center
draw.Color(layout.Color("cyan")).Bold()
draw.TextCenter(0, centerY-2, "Hello World")
// Draw current time below in RFC format
draw.Color(layout.Color("white")).Plain()
currentTime := time.Now().Format(time.RFC1123)
draw.TextCenter(0, centerY, "%s", currentTime)
// Draw system uptime below that
uptime, err := getSystemUptime()
if err != nil {
uptime = 0
}
draw.Color(layout.Color("gray60"))
draw.TextCenter(
0,
centerY+2,
"System Uptime: %s",
formatDuration(uptime),
)
// Add a decorative border
borderGrid := draw.Grid(2, 2, grid.Width-4, grid.Height-4)
borderGrid.Border(layout.Color("gray30"))
return nil
}
// FramesPerSecond returns the desired frame rate (1 FPS for clock updates)
func (h *HelloWorldScreen) FramesPerSecond() float64 {
return 1.0
}
// formatDuration formats a duration in Go duration string format
func formatDuration(d time.Duration) string {
return d.Round(time.Second).String()
}
func main() {
// Set up signal handling
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
log.Println("Received shutdown signal")
cancel()
}()
// Create framebuffer display
display, err := fbdraw.NewFBDisplayAuto()
if err != nil {
log.Fatalf("Failed to open framebuffer: %v", err)
}
defer func() {
if err := display.Close(); err != nil {
log.Printf("Failed to close display: %v", err)
}
}()
// Create carousel with no rotation (single screen)
carousel := fbdraw.NewCarousel(display, 0) // 0 means no rotation
// Set font size
if err := carousel.SetFontSize(DefaultFontSize); err != nil {
log.Fatalf("Failed to set font size: %v", err)
}
// Add our hello world screen with header
helloScreen := NewHelloWorldScreen()
wrappedScreen := fbdraw.NewHeaderWrapper(helloScreen)
if err := carousel.AddScreen("Hello World", wrappedScreen); err != nil {
log.Fatalf("Failed to add screen: %v", err)
}
// Run the carousel
log.Println("Starting fbhello...")
log.Println("Press Ctrl+C to exit")
// Run carousel in a goroutine
done := make(chan error, 1)
go func() {
done <- carousel.Run()
}()
// Wait for either context cancellation or carousel to finish
select {
case <-ctx.Done():
log.Println("Stopping carousel...")
carousel.Stop()
<-done // Wait for carousel to finish
case err := <-done:
if err != nil {
log.Printf("Carousel error: %v", err)
}
}
log.Println("fbhello exited cleanly")
}