hdmistat/internal/display/display.go
sneak 2f8256b310 Refactor hdmistat to use dependency injection and fix linter issues
Major changes:
- Converted all cobra commands from global variables to CLI struct methods
- Eliminated global logger variable in favor of dependency injection
- Fixed all errcheck linter issues by properly handling errors
- Fixed Makefile to check formatting instead of modifying files
- Integrated smartconfig library for configuration management
- Added CLAUDE.md with project-specific development guidelines

Key improvements:
- All commands (daemon, install, status, info) now use CLI struct methods
- Logger is injected as dependency through fx providers
- Proper error handling for all DrawText and file.Close() calls
- Configuration loading now uses smartconfig with proper defaults
- Fixed formatting check in Makefile (make test no longer modifies files)

Technical details:
- Created CLI struct with log field (renamed from logger per request)
- All command constructors return *cobra.Command from CLI methods
- Config package uses smartconfig.NewFromAppName() correctly
- Fixed all critical errcheck issues throughout the codebase
- Maintained backward compatibility with existing functionality

All tests passing, code formatted, and ready for deployment.
2025-07-23 15:01:51 +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()
}