# 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 ```go 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 ```go 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 ```go 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 ```go 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 ```go // 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 ```go // 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) ``` ## Carousel API The carousel system provides automatic screen rotation with independent frame rates for each screen. ### Core Interfaces ```go // 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 } ``` ### Carousel Usage ```go // 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 ```go 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