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.
140 lines
3.0 KiB
Go
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()
|
|
}
|