//nolint:mnd package fbdraw import ( "fmt" "log" "os" "syscall" "unsafe" ) // FBDisplay renders to a Linux framebuffer device type FBDisplay struct { device string file *os.File info fbVarScreeninfo fixInfo fbFixScreeninfo data []byte charWidth int charHeight int } // fbFixScreeninfo from linux/fb.h type fbFixScreeninfo struct { ID [16]byte SMEMStart uint64 SMEMLen uint32 Type uint32 TypeAux uint32 Visual uint32 XPanStep uint16 YPanStep uint16 YWrapStep uint16 _ uint16 LineLength uint32 MMIOStart uint64 MMIOLen uint32 Accel uint32 Capabilities uint16 Reserved [2]uint16 } // fbVarScreeninfo from linux/fb.h 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 NonStd uint32 Activate uint32 Height uint32 Width uint32 AccelFlags uint32 PixClock uint32 LeftMargin uint32 RightMargin uint32 UpperMargin uint32 LowerMargin uint32 HSyncLen uint32 VSyncLen uint32 Sync uint32 VMode uint32 Rotate uint32 Colorspace uint32 Reserved [4]uint32 } type fbBitfield struct { Offset uint32 Length uint32 Right uint32 } const ( fbiogetVscreeninfo = 0x4600 fbiogetFscreeninfo = 0x4602 ) // NewFBDisplay creates a display for a specific framebuffer device func NewFBDisplay(device string) (*FBDisplay, error) { file, err := os.OpenFile(device, os.O_RDWR, 0) //nolint:gosec if err != nil { return nil, fmt.Errorf("opening framebuffer %s: %w", device, err) } display := &FBDisplay{ device: device, file: file, } // Get variable screen info if err := display.getScreenInfo(); err != nil { _ = file.Close() return nil, err } // Get fixed screen info if err := display.getFixedInfo(); err != nil { _ = file.Close() return nil, err } // Memory map the framebuffer size := int(display.fixInfo.SMEMLen) display.data, 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("mmap framebuffer: %w", err) } // Calculate character dimensions (rough approximation) display.charWidth = 8 display.charHeight = 16 return display, nil } // NewFBDisplayAuto auto-detects and opens the framebuffer func NewFBDisplayAuto() (*FBDisplay, error) { // Try common framebuffer devices devices := []string{"/dev/fb0", "/dev/fb1", "/dev/graphics/fb0"} for _, device := range devices { if _, err := os.Stat(device); err == nil { display, err := NewFBDisplay(device) if err == nil { return display, nil } } } return nil, fmt.Errorf("no framebuffer device found") } func (d *FBDisplay) getScreenInfo() error { _, _, errno := syscall.Syscall( syscall.SYS_IOCTL, d.file.Fd(), fbiogetVscreeninfo, uintptr(unsafe.Pointer(&d.info)), //nolint:gosec ) if errno != 0 { return fmt.Errorf("ioctl FBIOGET_VSCREENINFO: %w", errno) } return nil } func (d *FBDisplay) getFixedInfo() error { _, _, errno := syscall.Syscall( syscall.SYS_IOCTL, d.file.Fd(), fbiogetFscreeninfo, uintptr(unsafe.Pointer(&d.fixInfo)), //nolint:gosec ) if errno != 0 { return fmt.Errorf("ioctl FBIOGET_FSCREENINFO: %w", errno) } return nil } // Write renders a grid to the framebuffer func (d *FBDisplay) Write(grid *CharGrid) error { // Render grid to image img, err := grid.Render() if err != nil { return fmt.Errorf("rendering grid: %w", err) } // Clear framebuffer for i := range d.data { d.data[i] = 0 } // Copy image to framebuffer bounds := img.Bounds() bytesPerPixel := int(d.info.BitsPerPixel / 8) lineLength := int(d.fixInfo.LineLength) for y := bounds.Min.Y; y < bounds.Max.Y && y < int(d.info.YRes); y++ { for x := bounds.Min.X; x < bounds.Max.X && x < int(d.info.XRes); x++ { r, g, b, _ := img.At(x, y).RGBA() offset := y*lineLength + x*bytesPerPixel if offset+bytesPerPixel <= len(d.data) { // Assuming 32-bit BGRA format (most common) if bytesPerPixel == 4 { d.data[offset+0] = byte(b >> 8) d.data[offset+1] = byte(g >> 8) d.data[offset+2] = byte(r >> 8) d.data[offset+3] = 0xFF } } } } return nil } // Size returns the display size in characters func (d *FBDisplay) Size() (width, height int) { width = int(d.info.XRes) / d.charWidth height = int(d.info.YRes) / d.charHeight return } // PixelSize returns the framebuffer dimensions in pixels func (d *FBDisplay) PixelSize() (width, height int) { return int(d.info.XRes), int(d.info.YRes) } // Close closes the framebuffer func (d *FBDisplay) Close() error { if d.data != nil { if err := syscall.Munmap(d.data); err != nil { log.Printf("munmap error: %v", err) } d.data = nil } if d.file != nil { if err := d.file.Close(); err != nil { return err } d.file = nil } return nil }