hdmistat/internal/display/display.go
sneak 402c0797d5 Initial implementation of hdmistat - Linux framebuffer system stats display
Features:
- Beautiful system statistics display using IBM Plex Mono font
- Direct framebuffer rendering without X11/Wayland
- Multiple screens with automatic carousel rotation
- Real-time system monitoring (CPU, memory, disk, network, processes)
- Systemd service integration with install command
- Clean architecture using uber/fx dependency injection

Architecture:
- Cobra CLI with daemon, install, status, and info commands
- Modular design with separate packages for display, rendering, and stats
- Font embedding for zero runtime dependencies
- Layout API for clean text rendering
- Support for multiple screen types (overview, top CPU, top memory)

Technical details:
- Uses gopsutil for cross-platform system stats collection
- Direct Linux framebuffer access via memory mapping
- Anti-aliased text rendering with freetype
- Configurable screen rotation and update intervals
- Structured logging with slog
- Comprehensive test coverage and linting setup

This initial version provides a solid foundation for displaying rich
system information on resource-constrained devices like Raspberry Pis.
2025-07-23 12:55:42 +02:00

140 lines
3.0 KiB
Go

package display
import (
"fmt"
"image"
"log/slog"
"os"
"syscall"
"unsafe"
)
// Display interface for showing images
type Display interface {
Show(img *image.RGBA) error
Clear() error
Close() error
}
// FramebufferDisplay implements Display for Linux framebuffer
type FramebufferDisplay struct {
file *os.File
info *fbVarScreenInfo
memory []byte
logger *slog.Logger
}
type fbVarScreenInfo struct {
XRes uint32
YRes uint32
XResVirtual uint32
YResVirtual uint32
XOffset uint32
YOffset uint32
BitsPerPixel uint32
Grayscale uint32
Red fbBitfield
Green fbBitfield
Blue fbBitfield
Transp fbBitfield
_ [4]byte
}
type fbBitfield struct {
Offset uint32
Length uint32
Right uint32
}
const (
fbiogetVscreeninfo = 0x4600
)
// NewFramebufferDisplay creates a new framebuffer display
func NewFramebufferDisplay(device string, logger *slog.Logger) (*FramebufferDisplay, error) {
file, err := os.OpenFile(device, os.O_RDWR, 0)
if err != nil {
return nil, fmt.Errorf("opening framebuffer: %w", err)
}
var info fbVarScreenInfo
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, file.Fd(), fbiogetVscreeninfo, uintptr(unsafe.Pointer(&info)))
if errno != 0 {
file.Close()
return nil, fmt.Errorf("getting screen info: %v", errno)
}
size := int(info.XRes * info.YRes * info.BitsPerPixel / 8)
memory, err := syscall.Mmap(int(file.Fd()), 0, size, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
if err != nil {
file.Close()
return nil, fmt.Errorf("mapping framebuffer: %w", err)
}
logger.Info("framebuffer initialized",
"device", device,
"width", info.XRes,
"height", info.YRes,
"bpp", info.BitsPerPixel)
return &FramebufferDisplay{
file: file,
info: &info,
memory: memory,
logger: logger,
}, nil
}
// Show displays an image on the framebuffer
func (d *FramebufferDisplay) Show(img *image.RGBA) error {
bounds := img.Bounds()
width := bounds.Dx()
height := bounds.Dy()
if width > int(d.info.XRes) {
width = int(d.info.XRes)
}
if height > int(d.info.YRes) {
height = int(d.info.YRes)
}
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
r, g, b, a := img.At(x, y).RGBA()
r, g, b = r>>8, g>>8, b>>8
offset := (y*int(d.info.XRes) + x) * int(d.info.BitsPerPixel/8)
if offset+3 < len(d.memory) {
if d.info.BitsPerPixel == 32 {
d.memory[offset] = byte(b)
d.memory[offset+1] = byte(g)
d.memory[offset+2] = byte(r)
d.memory[offset+3] = byte(a >> 8)
} else if d.info.BitsPerPixel == 24 {
d.memory[offset] = byte(b)
d.memory[offset+1] = byte(g)
d.memory[offset+2] = byte(r)
}
}
}
}
return nil
}
// Clear clears the framebuffer
func (d *FramebufferDisplay) Clear() error {
for i := range d.memory {
d.memory[i] = 0
}
return nil
}
// Close closes the framebuffer
func (d *FramebufferDisplay) Close() error {
if err := syscall.Munmap(d.memory); err != nil {
d.logger.Error("unmapping framebuffer", "error", err)
}
return d.file.Close()
}