hdmistat/internal/fbdraw
2025-07-24 14:32:50 +02:00
..
carousel.go checkpointing, heavy dev 2025-07-24 14:32:50 +02:00
display.go checkpointing, heavy dev 2025-07-24 14:32:50 +02:00
doc.go checkpointing, heavy dev 2025-07-24 14:32:50 +02:00
EXAMPLE.md checkpointing, heavy dev 2025-07-24 14:32:50 +02:00
grid_test.go checkpointing, heavy dev 2025-07-24 14:32:50 +02:00
grid.go checkpointing, heavy dev 2025-07-24 14:32:50 +02:00
interfaces.go checkpointing, heavy dev 2025-07-24 14:32:50 +02:00
README.md checkpointing, heavy dev 2025-07-24 14:32:50 +02:00

fbdraw - Simple Framebuffer Drawing for Go

A high-level Go package for creating text-based displays on Linux framebuffers. Designed for system monitors, status displays, and embedded systems.

API Design

Basic Example

package main

import (
    "github.com/example/fbdraw"
)

func main() {
    // Auto-detect and initialize
    fb := fbdraw.Init()
    defer fb.Close()
    
    // Main render loop
    fb.Loop(func(d *fbdraw.Draw) {
        // Clear screen
        d.Clear()
        
        // Draw header - state is maintained between calls
        d.Font(fbdraw.PlexSans).Size(36).Bold()
        d.Color(fbdraw.White)
        d.TextCenter(d.Width/2, 50, "System Monitor")
        
        // Switch to normal text
        d.Font(fbdraw.PlexMono).Size(14).Plain()
        
        // Create a text grid for the main content area
        grid := d.Grid(20, 100, 80, 30)  // x, y, cols, rows
        
        // Write to the grid using row,col coordinates
        grid.Color(fbdraw.Green)
        grid.Write(0, 0, "Hostname:")
        grid.Write(0, 15, getHostname())
        
        grid.Write(2, 0, "Uptime:")
        grid.Write(2, 15, getUptime())
        
        grid.Write(4, 0, "Load Average:")
        if load := getLoad(); load > 2.0 {
            grid.Color(fbdraw.Red)
        }
        grid.Write(4, 15, "%.2f %.2f %.2f", getLoad())
        
        // CPU section with meter characters
        grid.Color(fbdraw.Blue).Bold()
        grid.Write(8, 0, "CPU Usage:")
        grid.Plain()
        
        cpus := getCPUPercents()
        for i, pct := range cpus {
            grid.Write(10+i, 0, "CPU%d [%s] %5.1f%%", i, 
                fbdraw.Meter(pct, 20), pct)
        }
        
        // Memory section
        grid.Color(fbdraw.Yellow).Bold() 
        grid.Write(20, 0, "Memory:")
        grid.Plain().Color(fbdraw.White)
        
        mem := getMemoryInfo()
        grid.Write(22, 0, "Used:  %s / %s", 
            fbdraw.Bytes(mem.Used), fbdraw.Bytes(mem.Total))
        grid.Write(23, 0, "Free:  %s", fbdraw.Bytes(mem.Free))
        grid.Write(24, 0, "Cache: %s", fbdraw.Bytes(mem.Cache))
        
        // Present the frame
        d.Present()
    })
}

Dashboard Example with Multiple Grids

func renderDashboard(d *fbdraw.Draw) {
    d.Clear(fbdraw.Gray10)  // Dark background
    
    // Header
    d.Font(fbdraw.PlexSans).Size(48).Bold().Color(fbdraw.Cyan)
    d.TextCenter(d.Width/2, 60, "SERVER STATUS")
    
    // Reset to default for grids
    d.Font(fbdraw.PlexMono).Size(16).Plain()
    
    // Left panel - System Info
    leftGrid := d.Grid(20, 120, 40, 25)
    leftGrid.Background(fbdraw.Gray20)
    leftGrid.Border(fbdraw.Gray40)
    
    leftGrid.Color(fbdraw.Green).Bold()
    leftGrid.WriteCenter(0, "SYSTEM")
    leftGrid.Plain().Color(fbdraw.White)
    
    info := getSystemInfo()
    leftGrid.Write(2, 1, "OS:       %s", info.OS)
    leftGrid.Write(3, 1, "Kernel:   %s", info.Kernel) 
    leftGrid.Write(4, 1, "Arch:     %s", info.Arch)
    leftGrid.Write(6, 1, "Hostname: %s", info.Hostname)
    leftGrid.Write(7, 1, "Uptime:   %s", info.Uptime)
    
    // Right panel - Performance
    rightGrid := d.Grid(d.Width/2+20, 120, 40, 25)
    rightGrid.Background(fbdraw.Gray20)
    rightGrid.Border(fbdraw.Gray40)
    
    rightGrid.Color(fbdraw.Orange).Bold()
    rightGrid.WriteCenter(0, "PERFORMANCE")
    rightGrid.Plain()
    
    // CPU bars
    rightGrid.Color(fbdraw.Blue)
    cpus := getCPUCores()
    for i, cpu := range cpus {
        rightGrid.Write(2+i, 1, "CPU%d", i)
        rightGrid.Bar(2+i, 6, 30, cpu.Percent, fbdraw.Heat(cpu.Percent))
    }
    
    // Memory meter
    rightGrid.Color(fbdraw.Purple) 
    mem := getMemory()
    rightGrid.Write(12, 1, "Memory")
    rightGrid.Bar(12, 8, 28, mem.Percent, fbdraw.Green)
    rightGrid.Write(13, 8, "%s / %s", 
        fbdraw.Bytes(mem.Used), fbdraw.Bytes(mem.Total))
    
    // Bottom status bar
    statusGrid := d.Grid(0, d.Height-40, d.Width/12, 1)
    statusGrid.Background(fbdraw.Black)
    statusGrid.Color(fbdraw.Gray60).Size(12)
    statusGrid.Write(0, 1, "Updated: %s | Load: %.2f | Temp: %d°C | Net: %s",
        time.Now().Format("15:04:05"),
        getLoad1Min(),
        getCPUTemp(),
        getNetRate())
}

Process Table Example

func renderProcessTable(d *fbdraw.Draw) {
    d.Clear()
    
    // Header
    d.Font(fbdraw.PlexSans).Size(24).Bold()
    d.Text(20, 30, "Top Processes by CPU")
    
    // Create table grid
    table := d.Grid(20, 70, 100, 40)
    table.Font(fbdraw.PlexMono).Size(14)
    
    // Table headers
    table.Background(fbdraw.Gray30).Color(fbdraw.White).Bold()
    table.Write(0, 0,  "PID")
    table.Write(0, 8,  "USER") 
    table.Write(0, 20, "PROCESS")
    table.Write(0, 60, "CPU%")
    table.Write(0, 70, "MEM%")
    table.Write(0, 80, "TIME")
    
    // Reset style for data
    table.Background(fbdraw.Black).Plain()
    
    // Process rows
    processes := getTopProcesses(38)  // 38 rows of data
    for i, p := range processes {
        row := i + 2  // Skip header row and blank row
        
        // Alternate row backgrounds
        if i%2 == 0 {
            table.RowBackground(row, fbdraw.Gray10)
        }
        
        // Highlight high CPU usage
        if p.CPU > 50.0 {
            table.RowColor(row, fbdraw.Red)
        } else if p.CPU > 20.0 {
            table.RowColor(row, fbdraw.Yellow)
        } else {
            table.RowColor(row, fbdraw.Gray80)
        }
        
        table.Write(row, 0,  "%5d", p.PID)
        table.Write(row, 8,  "%-8s", p.User)
        table.Write(row, 20, "%-38s", truncate(p.Name, 38))
        table.Write(row, 60, "%5.1f", p.CPU)
        table.Write(row, 70, "%5.1f", p.Memory)
        table.Write(row, 80, "%8s", p.Time)
    }
}

Key Features

Bundled Fonts

const (
    PlexMono = iota  // IBM Plex Mono - great for tables/code
    PlexSans         // IBM Plex Sans - clean headers  
    Terminus         // Terminus - sharp bitmap font
)

Drawing State

The Draw object maintains state between calls:

  • Current font, size, bold/italic
  • Foreground and background colors
  • Transformations

Text Grids

  • Define rectangular regions with rows/columns
  • Automatic text positioning
  • Built-in backgrounds, borders, styling
  • Row/column operations

Utilities

// Format bytes nicely (1.2GB, 456MB, etc)
fbdraw.Bytes(1234567890) // "1.2GB"

// Create text-based meter/progress bars
fbdraw.Meter(75.5, 20) // "███████████████     "

// Color based on value
fbdraw.Heat(temp)  // Returns color from blue->green->yellow->red

// Truncate with ellipsis
truncate("very long string", 10) // "very lo..."

Colors

// Basic colors
fbdraw.Black, White, Red, Green, Blue, Yellow, Cyan, Purple, Orange

// Grays  
fbdraw.Gray10, Gray20, Gray30, Gray40, Gray50, Gray60, Gray70, Gray80, Gray90

// Custom
fbdraw.RGB(128, 200, 255)
fbdraw.HSL(180, 0.5, 0.5)

The carousel system provides automatic screen rotation with independent frame rates for each screen.

Core Interfaces

// FrameGenerator generates frames for a screen
type FrameGenerator interface {
    // GenerateFrame is called to render a new frame
    GenerateFrame(grid *Grid) error
    
    // FramesPerSecond returns the desired frame rate
    FramesPerSecond() float64
}

// Display represents the output device (framebuffer, terminal, etc)
type Display interface {
    // Write renders a grid to the display
    Write(grid *Grid) error
    
    // Size returns the display dimensions in characters
    Size() (width, height int)
    
    // Close cleans up resources
    Close() error
}
// Create display
display, err := fbdraw.NewDisplay("") // auto-detect

// Create carousel with rotation interval
carousel := fbdraw.NewCarousel(display, 10*time.Second)

// Add screens
carousel.AddScreen("System Status", &SystemStatusGenerator{})
carousel.AddScreen("Network", &NetworkMonitorGenerator{}) 
carousel.AddScreen("Processes", &ProcessListGenerator{})

// Run carousel (blocks)
carousel.Run()

Screen Implementation

type MyScreenGenerator struct {
    // Internal state
}

func (g *MyScreenGenerator) GenerateFrame(grid *Grid) error {
    w := NewGridWriter(grid)
    w.Clear()
    
    // Draw your content
    w.MoveTo(0, 0).Write("Hello World")
    
    return nil
}

func (g *MyScreenGenerator) FramesPerSecond() float64 {
    return 10.0 // 10 FPS
}

Features

  • Independent frame rates: Each screen can update at its own rate
  • Automatic rotation: Screens rotate on a timer
  • Clean separation: Generators just draw, carousel handles timing
  • Resource efficient: Only the active screen generates frames
  • Graceful shutdown: Handles signals properly

Design Philosophy

  • Immediate mode: Draw calls happen immediately in your render function
  • Stateful: Common properties persist until changed
  • Grid-based: Most text UIs are grids - embrace it
  • Batteries included: Common fonts, colors, and utilities built-in
  • Zero allocation: Reuse buffers where possible for smooth updates
  • Simple interfaces: Easy to implement custom screens