making lots of progress!

This commit is contained in:
2025-07-24 16:09:00 +02:00
parent c2040a5c08
commit 6b0628792a
28 changed files with 1917 additions and 289 deletions

35
cmd/fbhello/README.md Normal file
View File

@@ -0,0 +1,35 @@
# fbhello
A simple "Hello World" framebuffer application demonstrating the hdmistat carousel and layout APIs.
## Features
- Displays "Hello World" centered on the screen
- Shows current time updating at 1 FPS
- Shows uptime counter
- Decorative border around the display
- Falls back to terminal display if framebuffer is unavailable
## Usage
```bash
# Run with framebuffer (requires appropriate permissions)
sudo ./fbhello
# Run with terminal display (if framebuffer fails)
./fbhello
```
## Implementation
The application demonstrates:
1. Creating a custom screen that implements `FrameGenerator`
2. Using the layout API to draw text and borders
3. Setting up a carousel (though with only one screen)
4. Proper signal handling for clean shutdown
5. Fallback to terminal display when framebuffer is unavailable
## Exit
Press Ctrl+C to exit the application.

BIN
cmd/fbhello/fbhello Executable file

Binary file not shown.

176
cmd/fbhello/main.go Normal file
View File

@@ -0,0 +1,176 @@
//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")
}