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