.. | ||
carousel.go | ||
display.go | ||
doc.go | ||
EXAMPLE.md | ||
font_metrics.go | ||
grid_test.go | ||
grid.go | ||
header_wrapper_test.go | ||
header_wrapper.go | ||
interfaces.go | ||
README.md |
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)
Carousel API
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
}
Carousel Usage
// 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