335 lines
9.1 KiB
Markdown
335 lines
9.1 KiB
Markdown
# 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 |