making lots of progress!
This commit is contained in:
parent
c2040a5c08
commit
6b0628792a
2
Makefile
2
Makefile
@ -19,7 +19,7 @@ lint:
|
|||||||
golangci-lint run
|
golangci-lint run
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f hdmistat
|
rm -f hdmistat fbsimplestat fbhello
|
||||||
go clean
|
go clean
|
||||||
|
|
||||||
install: build
|
install: build
|
||||||
|
35
cmd/fbhello/README.md
Normal file
35
cmd/fbhello/README.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# fbhello
|
||||||
|
|
||||||
|
A simple "Hello World" framebuffer application demonstrating the hdmistat carousel and layout APIs.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Displays "Hello World" centered on the screen
|
||||||
|
- Shows current time updating at 1 FPS
|
||||||
|
- Shows uptime counter
|
||||||
|
- Decorative border around the display
|
||||||
|
- Falls back to terminal display if framebuffer is unavailable
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run with framebuffer (requires appropriate permissions)
|
||||||
|
sudo ./fbhello
|
||||||
|
|
||||||
|
# Run with terminal display (if framebuffer fails)
|
||||||
|
./fbhello
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
The application demonstrates:
|
||||||
|
|
||||||
|
1. Creating a custom screen that implements `FrameGenerator`
|
||||||
|
2. Using the layout API to draw text and borders
|
||||||
|
3. Setting up a carousel (though with only one screen)
|
||||||
|
4. Proper signal handling for clean shutdown
|
||||||
|
5. Fallback to terminal display when framebuffer is unavailable
|
||||||
|
|
||||||
|
## Exit
|
||||||
|
|
||||||
|
Press Ctrl+C to exit the application.
|
BIN
cmd/fbhello/fbhello
Executable file
BIN
cmd/fbhello/fbhello
Executable file
Binary file not shown.
176
cmd/fbhello/main.go
Normal file
176
cmd/fbhello/main.go
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
//nolint:mnd
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/hdmistat/internal/fbdraw"
|
||||||
|
"git.eeqj.de/sneak/hdmistat/internal/layout"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultFontSize is the font size used throughout the application
|
||||||
|
DefaultFontSize = 24
|
||||||
|
)
|
||||||
|
|
||||||
|
// HelloWorldScreen implements the FrameGenerator interface
|
||||||
|
type HelloWorldScreen struct {
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHelloWorldScreen creates a new hello world screen
|
||||||
|
func NewHelloWorldScreen() *HelloWorldScreen {
|
||||||
|
return &HelloWorldScreen{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the screen with the display dimensions
|
||||||
|
func (h *HelloWorldScreen) Init(width, height int) error {
|
||||||
|
h.width = width
|
||||||
|
h.height = height
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSystemUptime returns the system uptime
|
||||||
|
func getSystemUptime() (time.Duration, error) {
|
||||||
|
data, err := os.ReadFile("/proc/uptime")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := strings.Fields(string(data))
|
||||||
|
if len(fields) < 1 {
|
||||||
|
return 0, fmt.Errorf("invalid /proc/uptime format")
|
||||||
|
}
|
||||||
|
|
||||||
|
seconds, err := strconv.ParseFloat(fields[0], 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Duration(seconds * float64(time.Second)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateFrame generates a frame with "Hello World" and a timestamp
|
||||||
|
func (h *HelloWorldScreen) GenerateFrame(grid *fbdraw.CharGrid) error {
|
||||||
|
// Create a draw context that works directly on the provided grid
|
||||||
|
draw := layout.NewDraw(grid)
|
||||||
|
|
||||||
|
// Clear the screen with a dark background
|
||||||
|
draw.Clear()
|
||||||
|
|
||||||
|
// Calculate center position
|
||||||
|
centerY := grid.Height / 2
|
||||||
|
|
||||||
|
// Draw "Hello World" in the center
|
||||||
|
draw.Color(layout.Color("cyan")).Bold()
|
||||||
|
draw.TextCenter(0, centerY-2, "Hello World")
|
||||||
|
|
||||||
|
// Draw current time below in RFC format
|
||||||
|
draw.Color(layout.Color("white")).Plain()
|
||||||
|
currentTime := time.Now().Format(time.RFC1123)
|
||||||
|
draw.TextCenter(0, centerY, "%s", currentTime)
|
||||||
|
|
||||||
|
// Draw system uptime below that
|
||||||
|
uptime, err := getSystemUptime()
|
||||||
|
if err != nil {
|
||||||
|
uptime = 0
|
||||||
|
}
|
||||||
|
draw.Color(layout.Color("gray60"))
|
||||||
|
draw.TextCenter(
|
||||||
|
0,
|
||||||
|
centerY+2,
|
||||||
|
"System Uptime: %s",
|
||||||
|
formatDuration(uptime),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add a decorative border
|
||||||
|
borderGrid := draw.Grid(2, 2, grid.Width-4, grid.Height-4)
|
||||||
|
borderGrid.Border(layout.Color("gray30"))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FramesPerSecond returns the desired frame rate (1 FPS for clock updates)
|
||||||
|
func (h *HelloWorldScreen) FramesPerSecond() float64 {
|
||||||
|
return 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatDuration formats a duration in Go duration string format
|
||||||
|
func formatDuration(d time.Duration) string {
|
||||||
|
return d.Round(time.Second).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Set up signal handling
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-sigChan
|
||||||
|
log.Println("Received shutdown signal")
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create framebuffer display
|
||||||
|
display, err := fbdraw.NewFBDisplayAuto()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to open framebuffer: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := display.Close(); err != nil {
|
||||||
|
log.Printf("Failed to close display: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create carousel with no rotation (single screen)
|
||||||
|
carousel := fbdraw.NewCarousel(display, 0) // 0 means no rotation
|
||||||
|
|
||||||
|
// Set font size
|
||||||
|
if err := carousel.SetFontSize(DefaultFontSize); err != nil {
|
||||||
|
log.Fatalf("Failed to set font size: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add our hello world screen with header
|
||||||
|
helloScreen := NewHelloWorldScreen()
|
||||||
|
wrappedScreen := fbdraw.NewHeaderWrapper(helloScreen)
|
||||||
|
|
||||||
|
if err := carousel.AddScreen("Hello World", wrappedScreen); err != nil {
|
||||||
|
log.Fatalf("Failed to add screen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the carousel
|
||||||
|
log.Println("Starting fbhello...")
|
||||||
|
log.Println("Press Ctrl+C to exit")
|
||||||
|
|
||||||
|
// Run carousel in a goroutine
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
done <- carousel.Run()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for either context cancellation or carousel to finish
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Println("Stopping carousel...")
|
||||||
|
carousel.Stop()
|
||||||
|
<-done // Wait for carousel to finish
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Carousel error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("fbhello exited cleanly")
|
||||||
|
}
|
35
cmd/fbsimplestat/README.md
Normal file
35
cmd/fbsimplestat/README.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# fbhello
|
||||||
|
|
||||||
|
A simple "Hello World" framebuffer application demonstrating the hdmistat carousel and layout APIs.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Displays "Hello World" centered on the screen
|
||||||
|
- Shows current time updating at 1 FPS
|
||||||
|
- Shows uptime counter
|
||||||
|
- Decorative border around the display
|
||||||
|
- Falls back to terminal display if framebuffer is unavailable
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run with framebuffer (requires appropriate permissions)
|
||||||
|
sudo ./fbhello
|
||||||
|
|
||||||
|
# Run with terminal display (if framebuffer fails)
|
||||||
|
./fbhello
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
The application demonstrates:
|
||||||
|
|
||||||
|
1. Creating a custom screen that implements `FrameGenerator`
|
||||||
|
2. Using the layout API to draw text and borders
|
||||||
|
3. Setting up a carousel (though with only one screen)
|
||||||
|
4. Proper signal handling for clean shutdown
|
||||||
|
5. Fallback to terminal display when framebuffer is unavailable
|
||||||
|
|
||||||
|
## Exit
|
||||||
|
|
||||||
|
Press Ctrl+C to exit the application.
|
BIN
cmd/fbsimplestat/fbhello
Executable file
BIN
cmd/fbsimplestat/fbhello
Executable file
Binary file not shown.
BIN
cmd/fbsimplestat/fbsimplestat
Executable file
BIN
cmd/fbsimplestat/fbsimplestat
Executable file
Binary file not shown.
384
cmd/fbsimplestat/main.go
Normal file
384
cmd/fbsimplestat/main.go
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
//nolint:mnd
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"image/color"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/hdmistat/internal/fbdraw"
|
||||||
|
"git.eeqj.de/sneak/hdmistat/internal/layout"
|
||||||
|
"github.com/shirou/gopsutil/v3/cpu"
|
||||||
|
"github.com/shirou/gopsutil/v3/disk"
|
||||||
|
"github.com/shirou/gopsutil/v3/host"
|
||||||
|
"github.com/shirou/gopsutil/v3/mem"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultFontSize is the font size used throughout the application
|
||||||
|
DefaultFontSize = 24
|
||||||
|
)
|
||||||
|
|
||||||
|
// SimpleStatScreen implements the FrameGenerator interface
|
||||||
|
type SimpleStatScreen struct {
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSimpleStatScreen creates a new simple stat screen
|
||||||
|
func NewSimpleStatScreen() *SimpleStatScreen {
|
||||||
|
return &SimpleStatScreen{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the screen with the display dimensions
|
||||||
|
func (s *SimpleStatScreen) Init(width, height int) error {
|
||||||
|
s.width = width
|
||||||
|
s.height = height
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSystemUptime returns the system uptime
|
||||||
|
func getSystemUptime() (time.Duration, error) {
|
||||||
|
data, err := os.ReadFile("/proc/uptime")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := strings.Fields(string(data))
|
||||||
|
if len(fields) < 1 {
|
||||||
|
return 0, fmt.Errorf("invalid /proc/uptime format")
|
||||||
|
}
|
||||||
|
|
||||||
|
seconds, err := strconv.ParseFloat(fields[0], 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Duration(seconds * float64(time.Second)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateFrame generates a frame with system stats
|
||||||
|
func (s *SimpleStatScreen) GenerateFrame(grid *fbdraw.CharGrid) error {
|
||||||
|
// Create a draw context that works directly on the provided grid
|
||||||
|
draw := layout.NewDraw(grid)
|
||||||
|
|
||||||
|
// Clear the screen with a dark background
|
||||||
|
draw.Clear()
|
||||||
|
|
||||||
|
// Get hostname
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
|
||||||
|
// Title - moved down one line
|
||||||
|
draw.Color(layout.Color("cyan")).Bold()
|
||||||
|
draw.TextCenter(0, 3, "%s: info", hostname)
|
||||||
|
draw.Plain()
|
||||||
|
|
||||||
|
// Uptime
|
||||||
|
uptime, err := getSystemUptime()
|
||||||
|
if err != nil {
|
||||||
|
uptime = 0
|
||||||
|
}
|
||||||
|
draw.Color(layout.Color("gray60"))
|
||||||
|
draw.TextCenter(0, 4, "Uptime: %s", uptime.Round(time.Second).String())
|
||||||
|
|
||||||
|
// Starting Y position for stats (with extra blank line after uptime)
|
||||||
|
statY := 7
|
||||||
|
|
||||||
|
// Calculate alignment positions
|
||||||
|
labelX := 10
|
||||||
|
valueX := 30 // For right-aligned values before the bar
|
||||||
|
barX := 40 // Start of progress bars
|
||||||
|
barWidth := 50 // Extended bar width
|
||||||
|
|
||||||
|
// CPU Usage
|
||||||
|
cpuPercent, _ := cpu.Percent(time.Millisecond*100, false)
|
||||||
|
cpuUsage := 0.0
|
||||||
|
if len(cpuPercent) > 0 {
|
||||||
|
cpuUsage = cpuPercent[0]
|
||||||
|
}
|
||||||
|
draw.Color(layout.Color("white"))
|
||||||
|
draw.Text(labelX, statY, "CPU Usage:")
|
||||||
|
draw.Text(valueX-6, statY, "%6.1f%%", cpuUsage)
|
||||||
|
barGrid := draw.Grid(barX, statY, barWidth+1, 1)
|
||||||
|
barGrid.Bar(0, 0, barWidth, cpuUsage, getCPUColor(cpuUsage))
|
||||||
|
// Add CPU details below
|
||||||
|
draw.Color(layout.Color("gray60"))
|
||||||
|
cpuInfo := getCPUInfo()
|
||||||
|
draw.Text(labelX+2, statY+1, "%s", cpuInfo)
|
||||||
|
|
||||||
|
// Memory Usage
|
||||||
|
statY += 4 // Add extra blank line (3 + 1 for the CPU detail line)
|
||||||
|
vmStat, _ := mem.VirtualMemory()
|
||||||
|
memUsage := vmStat.UsedPercent
|
||||||
|
draw.Color(layout.Color("white"))
|
||||||
|
draw.Text(labelX, statY, "Memory:")
|
||||||
|
draw.Text(valueX-6, statY, "%6.1f%%", memUsage)
|
||||||
|
barGrid = draw.Grid(barX, statY, barWidth+1, 1)
|
||||||
|
barGrid.Bar(0, 0, barWidth, memUsage, getMemoryColor(memUsage))
|
||||||
|
// Add usage details below
|
||||||
|
draw.Color(layout.Color("gray60"))
|
||||||
|
draw.Text(
|
||||||
|
labelX+2,
|
||||||
|
statY+1,
|
||||||
|
"%s / %s",
|
||||||
|
layout.Bytes(vmStat.Used),
|
||||||
|
layout.Bytes(vmStat.Total),
|
||||||
|
)
|
||||||
|
|
||||||
|
// System Temperature (only if available)
|
||||||
|
temp := getSystemTemperature()
|
||||||
|
if temp > 0 {
|
||||||
|
statY += 4 // Add blank line (3 + 1 for the memory detail line)
|
||||||
|
tempPercent := (temp / 100.0) * 100.0 // Assume 100°C as max
|
||||||
|
if tempPercent > 100 {
|
||||||
|
tempPercent = 100
|
||||||
|
}
|
||||||
|
draw.Color(layout.Color("white"))
|
||||||
|
draw.Text(labelX, statY, "Temperature:")
|
||||||
|
draw.Text(valueX-8, statY, "%6.1f°C", temp)
|
||||||
|
barGrid = draw.Grid(barX, statY, barWidth+1, 1)
|
||||||
|
barGrid.Bar(0, 0, barWidth, tempPercent, getTempColor(temp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filesystem Usage
|
||||||
|
if temp > 0 {
|
||||||
|
statY += 3 // Add blank line after temperature
|
||||||
|
} else {
|
||||||
|
statY += 4 // Add blank line after memory (3 + 1 for the memory detail line)
|
||||||
|
}
|
||||||
|
fsPath, fsUsage, fsUsed, fsTotal := getLargestFilesystemUsageWithDetails()
|
||||||
|
draw.Color(layout.Color("white"))
|
||||||
|
draw.Text(labelX, statY, "Filesystem:")
|
||||||
|
draw.Text(valueX-6, statY, "%6.1f%%", fsUsage)
|
||||||
|
barGrid = draw.Grid(barX, statY, barWidth+1, 1)
|
||||||
|
barGrid.Bar(0, 0, barWidth, fsUsage, getFSColor(fsUsage))
|
||||||
|
// Add filesystem details below
|
||||||
|
draw.Color(layout.Color("gray60"))
|
||||||
|
draw.Text(
|
||||||
|
labelX+2,
|
||||||
|
statY+1,
|
||||||
|
"%s: %s / %s",
|
||||||
|
fsPath,
|
||||||
|
layout.Bytes(fsUsed),
|
||||||
|
layout.Bytes(fsTotal),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add a decorative border
|
||||||
|
borderGrid := draw.Grid(2, 2, grid.Width-4, grid.Height-4)
|
||||||
|
borderGrid.Border(layout.Color("gray30"))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FramesPerSecond returns the desired frame rate (1 FPS for stat updates)
|
||||||
|
func (s *SimpleStatScreen) FramesPerSecond() float64 {
|
||||||
|
return 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCPUColor returns a color based on CPU usage
|
||||||
|
func getCPUColor(percent float64) color.Color {
|
||||||
|
if percent > 80 {
|
||||||
|
return layout.Color("red")
|
||||||
|
} else if percent > 50 {
|
||||||
|
return layout.Color("yellow")
|
||||||
|
}
|
||||||
|
return layout.Color("green")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getMemoryColor returns a color based on memory usage
|
||||||
|
func getMemoryColor(percent float64) color.Color {
|
||||||
|
if percent > 90 {
|
||||||
|
return layout.Color("red")
|
||||||
|
} else if percent > 70 {
|
||||||
|
return layout.Color("yellow")
|
||||||
|
}
|
||||||
|
return layout.Color("green")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTempColor returns a color based on temperature
|
||||||
|
func getTempColor(temp float64) color.Color {
|
||||||
|
if temp > 80 {
|
||||||
|
return layout.Color("red")
|
||||||
|
} else if temp > 60 {
|
||||||
|
return layout.Color("yellow")
|
||||||
|
}
|
||||||
|
return layout.Color("green")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFSColor returns a color based on filesystem usage
|
||||||
|
func getFSColor(percent float64) color.Color {
|
||||||
|
if percent > 90 {
|
||||||
|
return layout.Color("red")
|
||||||
|
} else if percent > 80 {
|
||||||
|
return layout.Color("yellow")
|
||||||
|
}
|
||||||
|
return layout.Color("green")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCPUInfo returns CPU details like cores, threads, and clock speed
|
||||||
|
func getCPUInfo() string {
|
||||||
|
// Get CPU info
|
||||||
|
cpuInfos, err := cpu.Info()
|
||||||
|
if err != nil || len(cpuInfos) == 0 {
|
||||||
|
return "Unknown CPU"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get logical (threads) and physical core counts
|
||||||
|
logical, _ := cpu.Counts(true)
|
||||||
|
physical, _ := cpu.Counts(false)
|
||||||
|
|
||||||
|
// Get frequency info
|
||||||
|
freq := cpuInfos[0].Mhz
|
||||||
|
freqStr := ""
|
||||||
|
if freq > 0 {
|
||||||
|
if freq >= 1000 {
|
||||||
|
freqStr = fmt.Sprintf("%.2f GHz", freq/1000.0)
|
||||||
|
} else {
|
||||||
|
freqStr = fmt.Sprintf("%.0f MHz", freq)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%d cores, %d threads @ %s",
|
||||||
|
physical,
|
||||||
|
logical,
|
||||||
|
freqStr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%d cores, %d threads", physical, logical)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSystemTemperature returns the system temperature in Celsius
|
||||||
|
func getSystemTemperature() float64 {
|
||||||
|
// Try to get temperature from host sensors
|
||||||
|
temps, err := host.SensorsTemperatures()
|
||||||
|
if err != nil || len(temps) == 0 {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the highest temperature
|
||||||
|
maxTemp := 0.0
|
||||||
|
for _, temp := range temps {
|
||||||
|
if temp.Temperature > maxTemp {
|
||||||
|
maxTemp = temp.Temperature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxTemp
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLargestFilesystemUsageWithDetails returns the path, usage percentage, used bytes, and total bytes
|
||||||
|
func getLargestFilesystemUsageWithDetails() (string, float64, uint64, uint64) {
|
||||||
|
partitions, err := disk.Partitions(false)
|
||||||
|
if err != nil {
|
||||||
|
return "/", 0.0, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var largestPath string
|
||||||
|
var largestSize uint64
|
||||||
|
var largestUsage float64
|
||||||
|
var largestUsed uint64
|
||||||
|
|
||||||
|
for _, partition := range partitions {
|
||||||
|
// Skip special filesystems
|
||||||
|
if strings.HasPrefix(partition.Mountpoint, "/proc") ||
|
||||||
|
strings.HasPrefix(partition.Mountpoint, "/sys") ||
|
||||||
|
strings.HasPrefix(partition.Mountpoint, "/dev") ||
|
||||||
|
strings.HasPrefix(partition.Mountpoint, "/run") ||
|
||||||
|
partition.Fstype == "tmpfs" ||
|
||||||
|
partition.Fstype == "devtmpfs" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
usage, err := disk.Usage(partition.Mountpoint)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the filesystem with the largest total size
|
||||||
|
if usage.Total > largestSize {
|
||||||
|
largestSize = usage.Total
|
||||||
|
largestUsage = usage.UsedPercent
|
||||||
|
largestUsed = usage.Used
|
||||||
|
largestPath = partition.Mountpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if largestPath == "" {
|
||||||
|
return "/", 0.0, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return largestPath, largestUsage, largestUsed, largestSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Set up signal handling
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-sigChan
|
||||||
|
log.Println("Received shutdown signal")
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create framebuffer display
|
||||||
|
display, err := fbdraw.NewFBDisplayAuto()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to open framebuffer: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := display.Close(); err != nil {
|
||||||
|
log.Printf("Failed to close display: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create carousel with no rotation (single screen)
|
||||||
|
carousel := fbdraw.NewCarousel(display, 0) // 0 means no rotation
|
||||||
|
|
||||||
|
// Set font size
|
||||||
|
if err := carousel.SetFontSize(DefaultFontSize); err != nil {
|
||||||
|
log.Fatalf("Failed to set font size: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add our simple stat screen with header
|
||||||
|
statScreen := NewSimpleStatScreen()
|
||||||
|
wrappedScreen := fbdraw.NewHeaderWrapper(statScreen)
|
||||||
|
|
||||||
|
if err := carousel.AddScreen("Simple Stats", wrappedScreen); err != nil {
|
||||||
|
log.Fatalf("Failed to add screen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the carousel
|
||||||
|
log.Println("Starting fbsimplestat...")
|
||||||
|
log.Println("Press Ctrl+C to exit")
|
||||||
|
|
||||||
|
// Run carousel in a goroutine
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
done <- carousel.Run()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for either context cancellation or carousel to finish
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Println("Stopping carousel...")
|
||||||
|
carousel.Stop()
|
||||||
|
<-done // Wait for carousel to finish
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Carousel error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("fbsimplestat exited cleanly")
|
||||||
|
}
|
6
go.mod
6
go.mod
@ -9,7 +9,7 @@ require (
|
|||||||
github.com/shirou/gopsutil/v3 v3.24.1
|
github.com/shirou/gopsutil/v3 v3.24.1
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
go.uber.org/fx v1.20.1
|
go.uber.org/fx v1.20.1
|
||||||
golang.org/x/image v0.15.0
|
golang.org/x/image v0.29.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -121,10 +121,10 @@ require (
|
|||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||||
golang.org/x/net v0.41.0 // indirect
|
golang.org/x/net v0.41.0 // indirect
|
||||||
golang.org/x/oauth2 v0.30.0 // indirect
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
golang.org/x/sync v0.15.0 // indirect
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/term v0.32.0 // indirect
|
golang.org/x/term v0.32.0 // indirect
|
||||||
golang.org/x/text v0.26.0 // indirect
|
golang.org/x/text v0.27.0 // indirect
|
||||||
golang.org/x/time v0.12.0 // indirect
|
golang.org/x/time v0.12.0 // indirect
|
||||||
google.golang.org/api v0.237.0 // indirect
|
google.golang.org/api v0.237.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
|
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
|
||||||
|
6
go.sum
6
go.sum
@ -424,6 +424,8 @@ golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE
|
|||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||||
|
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
|
||||||
|
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@ -447,6 +449,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -487,6 +491,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
|
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||||
|
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
@ -5,6 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/hdmistat/internal/font"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Screen represents a single screen in the carousel
|
// Screen represents a single screen in the carousel
|
||||||
@ -21,6 +23,9 @@ type Carousel struct {
|
|||||||
screens []*Screen
|
screens []*Screen
|
||||||
currentIndex int
|
currentIndex int
|
||||||
rotationInterval time.Duration
|
rotationInterval time.Duration
|
||||||
|
fontSize float64
|
||||||
|
gridWidth int
|
||||||
|
gridHeight int
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@ -28,25 +33,104 @@ type Carousel struct {
|
|||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DefaultFontSize float64 = 24
|
||||||
|
|
||||||
// NewCarousel creates a new carousel
|
// NewCarousel creates a new carousel
|
||||||
func NewCarousel(display FramebufferDisplay, rotationInterval time.Duration) *Carousel {
|
func NewCarousel(
|
||||||
|
display FramebufferDisplay,
|
||||||
|
rotationInterval time.Duration,
|
||||||
|
) *Carousel {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
return &Carousel{
|
c := &Carousel{
|
||||||
display: display,
|
display: display,
|
||||||
screens: make([]*Screen, 0),
|
screens: make([]*Screen, 0),
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
rotationInterval: rotationInterval,
|
rotationInterval: rotationInterval,
|
||||||
|
fontSize: DefaultFontSize, // Default font size
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate grid dimensions based on font size
|
||||||
|
if err := c.calculateGridDimensions(); err != nil {
|
||||||
|
// Log error but continue with default dimensions
|
||||||
|
log.Printf("Warning: failed to calculate grid dimensions: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFontSize sets the font size and recalculates grid dimensions
|
||||||
|
func (c *Carousel) SetFontSize(size float64) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
c.fontSize = size
|
||||||
|
return c.calculateGridDimensions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateGridDimensions calculates grid size based on display and font
|
||||||
|
func (c *Carousel) calculateGridDimensions() error {
|
||||||
|
// Get pixel dimensions
|
||||||
|
pixelWidth, pixelHeight := c.display.PixelSize()
|
||||||
|
|
||||||
|
// Calculate character dimensions first
|
||||||
|
charWidth, charHeight, err := CalculateCharDimensions(
|
||||||
|
font.FamilyIBMPlexMono,
|
||||||
|
c.fontSize,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("calculating char dimensions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the calculated dimensions for debugging
|
||||||
|
fmt.Printf(
|
||||||
|
"Font size: %.0f, Char dimensions: %dx%d pixels\n",
|
||||||
|
c.fontSize,
|
||||||
|
charWidth,
|
||||||
|
charHeight,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Calculate grid dimensions
|
||||||
|
gridWidth, gridHeight, err := CalculateGridSize(
|
||||||
|
pixelWidth, pixelHeight,
|
||||||
|
font.FamilyIBMPlexMono, c.fontSize,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(
|
||||||
|
"Display: %dx%d pixels, Grid: %dx%d chars\n",
|
||||||
|
pixelWidth,
|
||||||
|
pixelHeight,
|
||||||
|
gridWidth,
|
||||||
|
gridHeight,
|
||||||
|
)
|
||||||
|
|
||||||
|
c.gridWidth = gridWidth
|
||||||
|
c.gridHeight = gridHeight
|
||||||
|
|
||||||
|
// Update display if it's an FBDisplay
|
||||||
|
if fbDisplay, ok := c.display.(*FBDisplay); ok {
|
||||||
|
fbDisplay.charWidth = charWidth
|
||||||
|
fbDisplay.charHeight = charHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddScreen adds a new screen to the carousel
|
// AddScreen adds a new screen to the carousel
|
||||||
func (c *Carousel) AddScreen(name string, generator FrameGenerator) {
|
func (c *Carousel) AddScreen(name string, generator FrameGenerator) error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
// Initialize the generator with the calculated grid dimensions
|
||||||
|
if err := generator.Init(c.gridWidth, c.gridHeight); err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize %s: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
screen := &Screen{
|
screen := &Screen{
|
||||||
Name: name,
|
Name: name,
|
||||||
Generator: generator,
|
Generator: generator,
|
||||||
@ -54,6 +138,7 @@ func (c *Carousel) AddScreen(name string, generator FrameGenerator) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.screens = append(c.screens, screen)
|
c.screens = append(c.screens, screen)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts the carousel
|
// Run starts the carousel
|
||||||
@ -62,15 +147,21 @@ func (c *Carousel) Run() error {
|
|||||||
return fmt.Errorf("no screens added to carousel")
|
return fmt.Errorf("no screens added to carousel")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start rotation timer
|
|
||||||
rotationTicker := time.NewTicker(c.rotationInterval)
|
|
||||||
defer rotationTicker.Stop()
|
|
||||||
|
|
||||||
// Start with first screen
|
// Start with first screen
|
||||||
if err := c.activateScreen(0); err != nil {
|
if err := c.activateScreen(0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no rotation, just wait for context cancellation
|
||||||
|
if c.rotationInterval <= 0 {
|
||||||
|
<-c.ctx.Done()
|
||||||
|
return c.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start rotation timer
|
||||||
|
rotationTicker := time.NewTicker(c.rotationInterval)
|
||||||
|
defer rotationTicker.Stop()
|
||||||
|
|
||||||
// Main loop
|
// Main loop
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@ -92,8 +183,31 @@ func (c *Carousel) Run() error {
|
|||||||
|
|
||||||
// Stop stops the carousel
|
// Stop stops the carousel
|
||||||
func (c *Carousel) Stop() {
|
func (c *Carousel) Stop() {
|
||||||
|
// Cancel the context
|
||||||
c.cancel()
|
c.cancel()
|
||||||
|
|
||||||
|
// Stop the current screen
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.currentIndex >= 0 && c.currentIndex < len(c.screens) {
|
||||||
|
screen := c.screens[c.currentIndex]
|
||||||
|
if screen.ticker != nil {
|
||||||
|
screen.ticker.Stop()
|
||||||
|
}
|
||||||
|
// Close stop channel if it's still open
|
||||||
|
select {
|
||||||
|
case <-screen.stop:
|
||||||
|
// Already closed
|
||||||
|
default:
|
||||||
|
close(screen.stop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
// Wait for all goroutines
|
||||||
c.wg.Wait()
|
c.wg.Wait()
|
||||||
|
|
||||||
|
// Close the display
|
||||||
|
_ = c.display.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// activateScreen switches to the specified screen
|
// activateScreen switches to the specified screen
|
||||||
@ -138,9 +252,17 @@ func (c *Carousel) activateScreen(index int) error {
|
|||||||
func (c *Carousel) runScreen(screen *Screen) {
|
func (c *Carousel) runScreen(screen *Screen) {
|
||||||
defer c.wg.Done()
|
defer c.wg.Done()
|
||||||
|
|
||||||
// Get display size
|
// Create grid with calculated dimensions
|
||||||
width, height := c.display.Size()
|
grid := NewCharGrid(c.gridWidth, c.gridHeight)
|
||||||
grid := NewCharGrid(width, height)
|
grid.FontSize = c.fontSize
|
||||||
|
|
||||||
|
// Set the character dimensions for proper rendering
|
||||||
|
charWidth, charHeight, _ := CalculateCharDimensions(
|
||||||
|
font.FamilyIBMPlexMono,
|
||||||
|
c.fontSize,
|
||||||
|
)
|
||||||
|
grid.CharWidth = charWidth
|
||||||
|
grid.CharHeight = charHeight
|
||||||
|
|
||||||
// Generate first frame immediately
|
// Generate first frame immediately
|
||||||
if err := screen.Generator.GenerateFrame(grid); err == nil {
|
if err := screen.Generator.GenerateFrame(grid); err == nil {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
//nolint:mnd
|
||||||
package fbdraw
|
package fbdraw
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -216,6 +217,11 @@ func (d *FBDisplay) Size() (width, height int) {
|
|||||||
return
|
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
|
// Close closes the framebuffer
|
||||||
func (d *FBDisplay) Close() error {
|
func (d *FBDisplay) Close() error {
|
||||||
if d.data != nil {
|
if d.data != nil {
|
||||||
@ -234,76 +240,3 @@ func (d *FBDisplay) Close() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TerminalDisplay renders to the terminal using ANSI escape codes
|
|
||||||
type TerminalDisplay struct {
|
|
||||||
width int
|
|
||||||
height int
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTerminalDisplay creates a terminal display
|
|
||||||
func NewTerminalDisplay(width, height int) *TerminalDisplay {
|
|
||||||
return &TerminalDisplay{
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write renders a grid to the terminal
|
|
||||||
func (d *TerminalDisplay) Write(grid *CharGrid) error {
|
|
||||||
// Clear screen
|
|
||||||
fmt.Print("\033[2J\033[H")
|
|
||||||
|
|
||||||
// Print ANSI representation
|
|
||||||
fmt.Print(grid.ToANSI())
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the terminal size in characters
|
|
||||||
func (d *TerminalDisplay) Size() (width, height int) {
|
|
||||||
return d.width, d.height
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close is a no-op for terminal display
|
|
||||||
func (d *TerminalDisplay) Close() error {
|
|
||||||
// Clear screen one last time
|
|
||||||
fmt.Print("\033[2J\033[H")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogDisplay renders to a logger for debugging
|
|
||||||
type LogDisplay struct {
|
|
||||||
width int
|
|
||||||
height int
|
|
||||||
logger *log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLogDisplay creates a log display
|
|
||||||
func NewLogDisplay(width, height int, logger *log.Logger) *LogDisplay {
|
|
||||||
if logger == nil {
|
|
||||||
logger = log.New(os.Stderr, "[fbdraw] ", log.LstdFlags)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &LogDisplay{
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write logs the grid as text
|
|
||||||
func (d *LogDisplay) Write(grid *CharGrid) error {
|
|
||||||
d.logger.Printf("=== Frame ===\n%s\n", grid.ToText())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the display size
|
|
||||||
func (d *LogDisplay) Size() (width, height int) {
|
|
||||||
return d.width, d.height
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close is a no-op for log display
|
|
||||||
func (d *LogDisplay) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
77
internal/fbdraw/font_metrics.go
Normal file
77
internal/fbdraw/font_metrics.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
//nolint:mnd
|
||||||
|
package fbdraw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/hdmistat/internal/font"
|
||||||
|
"github.com/golang/freetype"
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"golang.org/x/image/font/gofont/goregular"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CalculateCharDimensions calculates the character dimensions for a given font and size
|
||||||
|
func CalculateCharDimensions(
|
||||||
|
fontFamily font.FontFamily,
|
||||||
|
fontSize float64,
|
||||||
|
) (charWidth, charHeight int, err error) {
|
||||||
|
// Load a sample font to measure
|
||||||
|
f, err := font.LoadFont(fontFamily, font.WeightRegular, false)
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to built-in font
|
||||||
|
f, err = truetype.Parse(goregular.TTF)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a context to measure font metrics
|
||||||
|
c := freetype.NewContext()
|
||||||
|
c.SetFont(f)
|
||||||
|
c.SetFontSize(fontSize)
|
||||||
|
c.SetDPI(72)
|
||||||
|
|
||||||
|
// Get font face for measurements
|
||||||
|
face := truetype.NewFace(f, &truetype.Options{
|
||||||
|
Size: fontSize,
|
||||||
|
DPI: 72,
|
||||||
|
})
|
||||||
|
|
||||||
|
// For monospace fonts, get the advance width
|
||||||
|
advance, _ := face.GlyphAdvance('M')
|
||||||
|
charWidth = advance.Round() - 1 // Slightly tighter kerning
|
||||||
|
|
||||||
|
// Get line height from metrics
|
||||||
|
metrics := face.Metrics()
|
||||||
|
charHeight = metrics.Height.Round() + 3 // Add extra leading for better line spacing
|
||||||
|
|
||||||
|
fmt.Printf(
|
||||||
|
"Font metrics: advance=%v (rounded=%d), height=%v (rounded=%d, with +3 leading)\n",
|
||||||
|
advance,
|
||||||
|
charWidth,
|
||||||
|
metrics.Height,
|
||||||
|
charHeight,
|
||||||
|
)
|
||||||
|
|
||||||
|
return charWidth, charHeight, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateGridSize calculates the grid dimensions that fit in the given pixel dimensions
|
||||||
|
func CalculateGridSize(
|
||||||
|
pixelWidth, pixelHeight int,
|
||||||
|
fontFamily font.FontFamily,
|
||||||
|
fontSize float64,
|
||||||
|
) (gridWidth, gridHeight int, err error) {
|
||||||
|
charWidth, charHeight, err := CalculateCharDimensions(
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gridWidth = pixelWidth / charWidth
|
||||||
|
gridHeight = pixelHeight / charHeight
|
||||||
|
|
||||||
|
return gridWidth, gridHeight, nil
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
//nolint:mnd
|
||||||
package fbdraw
|
package fbdraw
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -48,6 +49,18 @@ type Cell struct {
|
|||||||
Italic bool
|
Italic bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String implements the Stringer interface for Cell
|
||||||
|
func (c Cell) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"Cell{Rune:'%c', FG:%v, BG:%v, Weight:%s, Italic:%v}",
|
||||||
|
c.Rune,
|
||||||
|
c.Foreground,
|
||||||
|
c.Background,
|
||||||
|
c.Weight,
|
||||||
|
c.Italic,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// CharGrid represents a monospace character grid
|
// CharGrid represents a monospace character grid
|
||||||
type CharGrid struct {
|
type CharGrid struct {
|
||||||
Width int // Width in characters
|
Width int // Width in characters
|
||||||
@ -104,7 +117,13 @@ func NewCharGrid(width, height int) *CharGrid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetCell sets a single cell's content
|
// SetCell sets a single cell's content
|
||||||
func (g *CharGrid) SetCell(x, y int, r rune, fg, bg color.Color, weight font.FontWeight, italic bool) {
|
func (g *CharGrid) SetCell(
|
||||||
|
x, y int,
|
||||||
|
r rune,
|
||||||
|
fg, bg color.Color,
|
||||||
|
weight font.FontWeight,
|
||||||
|
italic bool,
|
||||||
|
) {
|
||||||
if x < 0 || x >= g.Width || y < 0 || y >= g.Height {
|
if x < 0 || x >= g.Width || y < 0 || y >= g.Height {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -119,7 +138,13 @@ func (g *CharGrid) SetCell(x, y int, r rune, fg, bg color.Color, weight font.Fon
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WriteString writes a string starting at position (x, y)
|
// WriteString writes a string starting at position (x, y)
|
||||||
func (g *CharGrid) WriteString(x, y int, s string, fg, bg color.Color, weight font.FontWeight, italic bool) {
|
func (g *CharGrid) WriteString(
|
||||||
|
x, y int,
|
||||||
|
s string,
|
||||||
|
fg, bg color.Color,
|
||||||
|
weight font.FontWeight,
|
||||||
|
italic bool,
|
||||||
|
) {
|
||||||
runes := []rune(s)
|
runes := []rune(s)
|
||||||
for i, r := range runes {
|
for i, r := range runes {
|
||||||
g.SetCell(x+i, y, r, fg, bg, weight, italic)
|
g.SetCell(x+i, y, r, fg, bg, weight, italic)
|
||||||
@ -142,7 +167,10 @@ func (g *CharGrid) Clear(bg color.Color) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getFont retrieves a font from cache or loads it
|
// getFont retrieves a font from cache or loads it
|
||||||
func (g *CharGrid) getFont(weight font.FontWeight, italic bool) (*truetype.Font, error) {
|
func (g *CharGrid) getFont(
|
||||||
|
weight font.FontWeight,
|
||||||
|
italic bool,
|
||||||
|
) (*truetype.Font, error) {
|
||||||
key := fontKey{
|
key := fontKey{
|
||||||
family: g.FontFamily,
|
family: g.FontFamily,
|
||||||
weight: weight,
|
weight: weight,
|
||||||
@ -201,9 +229,11 @@ func (g *CharGrid) computeCharSize() error {
|
|||||||
|
|
||||||
// Render renders the grid to an image
|
// Render renders the grid to an image
|
||||||
func (g *CharGrid) Render() (*image.RGBA, error) {
|
func (g *CharGrid) Render() (*image.RGBA, error) {
|
||||||
// Ensure character dimensions are computed
|
// Only compute character dimensions if not already set
|
||||||
if err := g.computeCharSize(); err != nil {
|
if g.CharWidth == 0 || g.CharHeight == 0 {
|
||||||
return nil, err
|
if err := g.computeCharSize(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create image
|
// Create image
|
||||||
@ -276,6 +306,11 @@ func (g *CharGrid) Render() (*image.RGBA, error) {
|
|||||||
return img, nil
|
return img, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String implements the Stringer interface, returning a text representation
|
||||||
|
func (g *CharGrid) String() string {
|
||||||
|
return g.ToText()
|
||||||
|
}
|
||||||
|
|
||||||
// ToText renders the grid as text for debugging/logging
|
// ToText renders the grid as text for debugging/logging
|
||||||
func (g *CharGrid) ToText() string {
|
func (g *CharGrid) ToText() string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
@ -325,13 +360,15 @@ func (g *CharGrid) ToANSI() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Foreground color
|
// Foreground color
|
||||||
if r, g, b, _ := cell.Foreground.RGBA(); r != 0 || g != 0 || b != 0 {
|
if r, g, b, _ := cell.Foreground.RGBA(); r != 0 || g != 0 ||
|
||||||
|
b != 0 {
|
||||||
sb.WriteString(fmt.Sprintf("\033[38;2;%d;%d;%dm",
|
sb.WriteString(fmt.Sprintf("\033[38;2;%d;%d;%dm",
|
||||||
r>>8, g>>8, b>>8))
|
r>>8, g>>8, b>>8))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Background color
|
// Background color
|
||||||
if r, g, b, _ := cell.Background.RGBA(); r != 0 || g != 0 || b != 0 {
|
if r, g, b, _ := cell.Background.RGBA(); r != 0 || g != 0 ||
|
||||||
|
b != 0 {
|
||||||
sb.WriteString(fmt.Sprintf("\033[48;2;%d;%d;%dm",
|
sb.WriteString(fmt.Sprintf("\033[48;2;%d;%d;%dm",
|
||||||
r>>8, g>>8, b>>8))
|
r>>8, g>>8, b>>8))
|
||||||
}
|
}
|
||||||
@ -423,13 +460,24 @@ func (w *GridWriter) SetItalic(italic bool) *GridWriter {
|
|||||||
// Write writes a string at the current position
|
// Write writes a string at the current position
|
||||||
func (w *GridWriter) Write(format string, args ...interface{}) *GridWriter {
|
func (w *GridWriter) Write(format string, args ...interface{}) *GridWriter {
|
||||||
s := fmt.Sprintf(format, args...)
|
s := fmt.Sprintf(format, args...)
|
||||||
w.Grid.WriteString(w.X, w.Y, s, w.Foreground, w.Background, w.Weight, w.Italic)
|
w.Grid.WriteString(
|
||||||
|
w.X,
|
||||||
|
w.Y,
|
||||||
|
s,
|
||||||
|
w.Foreground,
|
||||||
|
w.Background,
|
||||||
|
w.Weight,
|
||||||
|
w.Italic,
|
||||||
|
)
|
||||||
w.X += len([]rune(s))
|
w.X += len([]rune(s))
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteLine writes a string and moves to the next line
|
// WriteLine writes a string and moves to the next line
|
||||||
func (w *GridWriter) WriteLine(format string, args ...interface{}) *GridWriter {
|
func (w *GridWriter) WriteLine(
|
||||||
|
format string,
|
||||||
|
args ...interface{},
|
||||||
|
) *GridWriter {
|
||||||
w.Write(format, args...)
|
w.Write(format, args...)
|
||||||
w.X = 0
|
w.X = 0
|
||||||
w.Y++
|
w.Y++
|
||||||
@ -451,6 +499,17 @@ func (w *GridWriter) Clear() *GridWriter {
|
|||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String implements the Stringer interface for GridWriter
|
||||||
|
func (w *GridWriter) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"GridWriter{X:%d, Y:%d, Grid:%dx%d}",
|
||||||
|
w.X,
|
||||||
|
w.Y,
|
||||||
|
w.Grid.Width,
|
||||||
|
w.Grid.Height,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// DrawMeter draws a progress meter at the current position
|
// DrawMeter draws a progress meter at the current position
|
||||||
func (w *GridWriter) DrawMeter(percent float64, width int) *GridWriter {
|
func (w *GridWriter) DrawMeter(percent float64, width int) *GridWriter {
|
||||||
if percent < 0 {
|
if percent < 0 {
|
||||||
|
167
internal/fbdraw/header_wrapper.go
Normal file
167
internal/fbdraw/header_wrapper.go
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
//nolint:mnd
|
||||||
|
package fbdraw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HeaderWrapper wraps a FrameGenerator and adds a 3-line header
|
||||||
|
type HeaderWrapper struct {
|
||||||
|
wrapped FrameGenerator
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
unameCache string
|
||||||
|
lsbCache string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHeaderWrapper creates a new header wrapper around a FrameGenerator
|
||||||
|
func NewHeaderWrapper(wrapped FrameGenerator) *HeaderWrapper {
|
||||||
|
return &HeaderWrapper{
|
||||||
|
wrapped: wrapped,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes both the wrapper and the wrapped generator
|
||||||
|
func (h *HeaderWrapper) Init(width, height int) error {
|
||||||
|
h.width = width
|
||||||
|
h.height = height
|
||||||
|
|
||||||
|
// Cache uname output since it doesn't change
|
||||||
|
// Get OS, hostname, kernel version, and architecture separately
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
// OS name (e.g., "Linux", "Darwin")
|
||||||
|
if output, err := exec.Command("uname", "-s").Output(); err == nil {
|
||||||
|
parts = append(parts, strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hostname
|
||||||
|
if output, err := exec.Command("uname", "-n").Output(); err == nil {
|
||||||
|
parts = append(parts, strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kernel version
|
||||||
|
if output, err := exec.Command("uname", "-r").Output(); err == nil {
|
||||||
|
parts = append(parts, strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Machine architecture
|
||||||
|
if output, err := exec.Command("uname", "-m").Output(); err == nil {
|
||||||
|
parts = append(parts, strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) > 0 {
|
||||||
|
h.unameCache = strings.Join(parts, " ")
|
||||||
|
} else {
|
||||||
|
h.unameCache = "Unknown System"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get LSB release info
|
||||||
|
if output, err := exec.Command("lsb_release", "-ds").Output(); err == nil {
|
||||||
|
h.lsbCache = strings.TrimSpace(string(output))
|
||||||
|
// Remove quotes if present
|
||||||
|
h.lsbCache = strings.Trim(h.lsbCache, "\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize wrapped generator with reduced height (minus 3 for header)
|
||||||
|
return h.wrapped.Init(width, height-3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateFrame generates a frame with header and wrapped content
|
||||||
|
func (h *HeaderWrapper) GenerateFrame(grid *CharGrid) error {
|
||||||
|
// Create a temporary grid for the wrapped content
|
||||||
|
contentGrid := NewCharGrid(h.width, h.height-3)
|
||||||
|
// Copy font settings from main grid
|
||||||
|
contentGrid.FontSize = grid.FontSize
|
||||||
|
contentGrid.FontFamily = grid.FontFamily
|
||||||
|
|
||||||
|
// Let the wrapped generator fill its content
|
||||||
|
if err := h.wrapped.GenerateFrame(contentGrid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we'll assemble the final grid
|
||||||
|
// First, clear the entire grid
|
||||||
|
grid.Clear(Black)
|
||||||
|
|
||||||
|
// Draw the header
|
||||||
|
h.drawHeader(grid)
|
||||||
|
|
||||||
|
// Copy content from wrapped generator below the header
|
||||||
|
for y := 0; y < contentGrid.Height; y++ {
|
||||||
|
for x := 0; x < contentGrid.Width; x++ {
|
||||||
|
if y < len(contentGrid.Cells) && x < len(contentGrid.Cells[y]) {
|
||||||
|
grid.Cells[y+3][x] = contentGrid.Cells[y][x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawHeader draws the 3-line header
|
||||||
|
func (h *HeaderWrapper) drawHeader(grid *CharGrid) {
|
||||||
|
writer := NewGridWriter(grid)
|
||||||
|
|
||||||
|
// Line 1: uname + lsb_release output (truncated if needed)
|
||||||
|
writer.MoveAbs(0, 0)
|
||||||
|
writer.SetColor(Gray60)
|
||||||
|
sysInfo := h.unameCache
|
||||||
|
if h.lsbCache != "" {
|
||||||
|
sysInfo += " " + h.lsbCache
|
||||||
|
}
|
||||||
|
// Account for the UTC time on the right - never truncate time
|
||||||
|
now := time.Now()
|
||||||
|
utcTime := now.UTC().Format("Mon 2006-01-02 15:04:05 UTC")
|
||||||
|
maxLen := h.width - len(utcTime) - 1
|
||||||
|
if len(sysInfo) > maxLen {
|
||||||
|
sysInfo = sysInfo[:maxLen-3] + "..."
|
||||||
|
}
|
||||||
|
writer.Write("%s", sysInfo)
|
||||||
|
|
||||||
|
// Check if local time is different from UTC
|
||||||
|
localZone, offset := now.Zone()
|
||||||
|
showLocalTime := offset != 0 // Only show local time if not UTC
|
||||||
|
|
||||||
|
// Line 2: uptime output
|
||||||
|
writer.MoveAbs(0, 1)
|
||||||
|
if output, err := exec.Command("uptime").Output(); err == nil {
|
||||||
|
uptime := strings.TrimSpace(string(output))
|
||||||
|
// Don't cut off at "user" - show the full uptime output
|
||||||
|
maxLen := h.width - 1
|
||||||
|
if showLocalTime {
|
||||||
|
// Account for the local time on the right - never truncate time
|
||||||
|
localTime := now.Format("Mon 2006-01-02 15:04:05 ") + localZone
|
||||||
|
maxLen = h.width - len(localTime) - 1
|
||||||
|
}
|
||||||
|
if len(uptime) > maxLen {
|
||||||
|
uptime = uptime[:maxLen-3] + "..."
|
||||||
|
}
|
||||||
|
writer.Write("%s", uptime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right side - UTC time (line 1) - always show full time
|
||||||
|
writer.MoveAbs(h.width-len(utcTime), 0)
|
||||||
|
writer.Write("%s", utcTime)
|
||||||
|
|
||||||
|
// Right side - Local time (line 2) - only show if different from UTC
|
||||||
|
if showLocalTime {
|
||||||
|
localTime := now.Format("Mon 2006-01-02 15:04:05 ") + localZone
|
||||||
|
writer.MoveAbs(h.width-len(localTime), 1)
|
||||||
|
writer.Write("%s", localTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line 3: Horizontal rule
|
||||||
|
writer.MoveAbs(0, 2)
|
||||||
|
writer.SetColor(Gray30)
|
||||||
|
for i := 0; i < h.width; i++ {
|
||||||
|
writer.Write("─")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FramesPerSecond returns the wrapped generator's frame rate
|
||||||
|
func (h *HeaderWrapper) FramesPerSecond() float64 {
|
||||||
|
return h.wrapped.FramesPerSecond()
|
||||||
|
}
|
65
internal/fbdraw/header_wrapper_test.go
Normal file
65
internal/fbdraw/header_wrapper_test.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package fbdraw_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/hdmistat/internal/fbdraw"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SimpleGenerator is a test generator
|
||||||
|
type SimpleGenerator struct {
|
||||||
|
width, height int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SimpleGenerator) Init(width, height int) error {
|
||||||
|
s.width = width
|
||||||
|
s.height = height
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SimpleGenerator) GenerateFrame(grid *fbdraw.CharGrid) error {
|
||||||
|
writer := fbdraw.NewGridWriter(grid)
|
||||||
|
writer.MoveAbs(s.width/2-5, s.height/2)
|
||||||
|
writer.SetColor(fbdraw.White)
|
||||||
|
writer.Write("Test Content")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SimpleGenerator) FramesPerSecond() float64 {
|
||||||
|
return 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHeaderWrapper(t *testing.T) {
|
||||||
|
// Create a simple generator
|
||||||
|
simple := &SimpleGenerator{}
|
||||||
|
|
||||||
|
// Wrap it with header
|
||||||
|
wrapped := fbdraw.NewHeaderWrapper(simple)
|
||||||
|
|
||||||
|
// Initialize with some dimensions
|
||||||
|
if err := wrapped.Init(80, 25); err != nil {
|
||||||
|
t.Fatalf("Failed to init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a frame
|
||||||
|
grid := fbdraw.NewCharGrid(80, 25)
|
||||||
|
if err := wrapped.GenerateFrame(grid); err != nil {
|
||||||
|
t.Fatalf("Failed to generate frame: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that header exists (line 3 should have horizontal rule)
|
||||||
|
hasRule := false
|
||||||
|
for x := 0; x < 80; x++ {
|
||||||
|
if grid.Cells[2][x].Rune == '─' {
|
||||||
|
hasRule = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasRule {
|
||||||
|
t.Error("Expected horizontal rule on line 3")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the output for visual inspection
|
||||||
|
t.Logf("Generated frame with header:\n%s", grid.String())
|
||||||
|
}
|
@ -2,6 +2,10 @@ package fbdraw
|
|||||||
|
|
||||||
// FrameGenerator generates frames for a screen
|
// FrameGenerator generates frames for a screen
|
||||||
type FrameGenerator interface {
|
type FrameGenerator interface {
|
||||||
|
// Init is called once when the screen is added to the carousel
|
||||||
|
// width and height are the character dimensions that frames will be requested at
|
||||||
|
Init(width, height int) error
|
||||||
|
|
||||||
// GenerateFrame is called to render a new frame
|
// GenerateFrame is called to render a new frame
|
||||||
GenerateFrame(grid *CharGrid) error
|
GenerateFrame(grid *CharGrid) error
|
||||||
|
|
||||||
@ -17,6 +21,9 @@ type FramebufferDisplay interface {
|
|||||||
// Size returns the display dimensions in characters
|
// Size returns the display dimensions in characters
|
||||||
Size() (width, height int)
|
Size() (width, height int)
|
||||||
|
|
||||||
|
// PixelSize returns the display dimensions in pixels
|
||||||
|
PixelSize() (width, height int)
|
||||||
|
|
||||||
// Close cleans up resources
|
// Close cleans up resources
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
// Package layout provides a simple API for creating text-based layouts
|
// Package layout provides a simple API for creating text-based layouts
|
||||||
// that can be rendered to fbdraw grids for display in a carousel.
|
// that can be rendered to fbdraw grids for display in a carousel.
|
||||||
|
//
|
||||||
|
//nolint:mnd
|
||||||
package layout
|
package layout
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -22,6 +24,20 @@ const (
|
|||||||
SourceCodePro
|
SourceCodePro
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// String implements the Stringer interface for Font
|
||||||
|
func (f Font) String() string {
|
||||||
|
switch f {
|
||||||
|
case PlexMono:
|
||||||
|
return "PlexMono"
|
||||||
|
case Terminus:
|
||||||
|
return "Terminus"
|
||||||
|
case SourceCodePro:
|
||||||
|
return "SourceCodePro"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Font(%d)", int(f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Color returns a standard color by name
|
// Color returns a standard color by name
|
||||||
func Color(name string) color.Color {
|
func Color(name string) color.Color {
|
||||||
switch name {
|
switch name {
|
||||||
@ -69,15 +85,14 @@ func Color(name string) color.Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw provides the drawing context for creating a layout.
|
// Draw provides the drawing context for creating a layout.
|
||||||
// It maintains state for font, size, colors, and text styling.
|
// It maintains state for colors and text styling.
|
||||||
type Draw struct {
|
type Draw struct {
|
||||||
// Drawing state
|
// Drawing state
|
||||||
font Font
|
font Font
|
||||||
fontSize int
|
bold bool
|
||||||
bold bool
|
italic bool
|
||||||
italic bool
|
fgColor color.Color
|
||||||
fgColor color.Color
|
bgColor color.Color
|
||||||
bgColor color.Color
|
|
||||||
|
|
||||||
// Grid to render to
|
// Grid to render to
|
||||||
grid *fbdraw.CharGrid
|
grid *fbdraw.CharGrid
|
||||||
@ -87,22 +102,27 @@ type Draw struct {
|
|||||||
Height int
|
Height int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDraw creates a new drawing context with the specified dimensions
|
// String implements the Stringer interface for Draw
|
||||||
func NewDraw(width, height int) *Draw {
|
func (d *Draw) String() string {
|
||||||
grid := fbdraw.NewCharGrid(width, height)
|
return fmt.Sprintf(
|
||||||
return &Draw{
|
"Draw{Width:%d, Height:%d, Font:%v, Bold:%v, Italic:%v}",
|
||||||
grid: grid,
|
d.Width,
|
||||||
Width: width,
|
d.Height,
|
||||||
Height: height,
|
d.font,
|
||||||
fontSize: 14,
|
d.bold,
|
||||||
fgColor: color.RGBA{255, 255, 255, 255},
|
d.italic,
|
||||||
bgColor: color.RGBA{0, 0, 0, 255},
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render returns the current grid for rendering by the carousel
|
// NewDraw creates a new drawing context that will modify the provided grid
|
||||||
func (d *Draw) Render() *fbdraw.CharGrid {
|
func NewDraw(grid *fbdraw.CharGrid) *Draw {
|
||||||
return d.grid
|
return &Draw{
|
||||||
|
grid: grid,
|
||||||
|
Width: grid.Width,
|
||||||
|
Height: grid.Height,
|
||||||
|
fgColor: color.RGBA{255, 255, 255, 255},
|
||||||
|
bgColor: color.RGBA{0, 0, 0, 255},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear fills the entire display with black.
|
// Clear fills the entire display with black.
|
||||||
@ -130,13 +150,6 @@ func (d *Draw) Font(f Font) *Draw {
|
|||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size sets the current font size in points.
|
|
||||||
func (d *Draw) Size(points int) *Draw {
|
|
||||||
d.fontSize = points
|
|
||||||
d.grid.FontSize = float64(points)
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bold enables bold text rendering.
|
// Bold enables bold text rendering.
|
||||||
func (d *Draw) Bold() *Draw {
|
func (d *Draw) Bold() *Draw {
|
||||||
d.bold = true
|
d.bold = true
|
||||||
@ -181,15 +194,18 @@ func (d *Draw) Text(x, y int, format string, args ...interface{}) {
|
|||||||
writer.SetWeight(font.WeightRegular)
|
writer.SetWeight(font.WeightRegular)
|
||||||
}
|
}
|
||||||
writer.SetItalic(d.italic)
|
writer.SetItalic(d.italic)
|
||||||
writer.Write(text)
|
writer.Write("%s", text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextCenter draws centered text at the specified y coordinate.
|
// TextCenter draws centered text at the specified y coordinate.
|
||||||
func (d *Draw) TextCenter(x, y int, format string, args ...interface{}) {
|
func (d *Draw) TextCenter(x, y int, format string, args ...interface{}) {
|
||||||
text := fmt.Sprintf(format, args...)
|
text := fmt.Sprintf(format, args...)
|
||||||
// Calculate starting position for centered text
|
// Calculate starting position for centered text
|
||||||
startX := x + (d.Width-len(text))/2
|
startX := (d.Width - len(text)) / 2
|
||||||
d.Text(startX, y, text)
|
if startX < 0 {
|
||||||
|
startX = 0
|
||||||
|
}
|
||||||
|
d.Text(startX, y, "%s", text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grid creates a text grid region for simplified text layout.
|
// Grid creates a text grid region for simplified text layout.
|
||||||
@ -218,6 +234,18 @@ type Grid struct {
|
|||||||
hasBorder bool
|
hasBorder bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String implements the Stringer interface for Grid
|
||||||
|
func (g *Grid) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"Grid{Pos:(%d,%d), Size:%dx%d, Border:%v}",
|
||||||
|
g.x,
|
||||||
|
g.y,
|
||||||
|
g.cols,
|
||||||
|
g.rows,
|
||||||
|
g.hasBorder,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Write places text at the specified row and column within the grid.
|
// Write places text at the specified row and column within the grid.
|
||||||
// Text that exceeds the grid bounds is clipped.
|
// Text that exceeds the grid bounds is clipped.
|
||||||
func (g *Grid) Write(col, row int, format string, args ...interface{}) {
|
func (g *Grid) Write(col, row int, format string, args ...interface{}) {
|
||||||
@ -249,7 +277,7 @@ func (g *Grid) Write(col, row int, format string, args ...interface{}) {
|
|||||||
if len(text) > maxLen {
|
if len(text) > maxLen {
|
||||||
text = text[:maxLen]
|
text = text[:maxLen]
|
||||||
}
|
}
|
||||||
writer.Write(text)
|
writer.Write("%s", text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteCenter centers text within the specified row.
|
// WriteCenter centers text within the specified row.
|
||||||
@ -262,7 +290,7 @@ func (g *Grid) WriteCenter(row int, format string, args ...interface{}) {
|
|||||||
if col < 0 {
|
if col < 0 {
|
||||||
col = 0
|
col = 0
|
||||||
}
|
}
|
||||||
g.Write(col, row, text)
|
g.Write(col, row, "%s", text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Color sets the foreground color for subsequent Write operations.
|
// Color sets the foreground color for subsequent Write operations.
|
||||||
@ -281,7 +309,15 @@ func (g *Grid) Background(c color.Color) *Grid {
|
|||||||
if g.draw.bold {
|
if g.draw.bold {
|
||||||
weight = font.WeightBold
|
weight = font.WeightBold
|
||||||
}
|
}
|
||||||
g.draw.grid.SetCell(g.x+col, g.y+row, ' ', g.draw.fgColor, c, weight, g.draw.italic)
|
g.draw.grid.SetCell(
|
||||||
|
g.x+col,
|
||||||
|
g.y+row,
|
||||||
|
' ',
|
||||||
|
g.draw.fgColor,
|
||||||
|
c,
|
||||||
|
weight,
|
||||||
|
g.draw.italic,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return g
|
return g
|
||||||
@ -328,7 +364,15 @@ func (g *Grid) RowBackground(row int, c color.Color) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for col := 0; col < g.cols; col++ {
|
for col := 0; col < g.cols; col++ {
|
||||||
g.draw.grid.SetCell(g.x+col, g.y+row, ' ', g.draw.fgColor, c, font.WeightRegular, false)
|
g.draw.grid.SetCell(
|
||||||
|
g.x+col,
|
||||||
|
g.y+row,
|
||||||
|
' ',
|
||||||
|
g.draw.fgColor,
|
||||||
|
c,
|
||||||
|
font.WeightRegular,
|
||||||
|
false,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,7 +459,11 @@ func Bytes(bytes uint64) string {
|
|||||||
div *= unit
|
div *= unit
|
||||||
exp++
|
exp++
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
return fmt.Sprintf(
|
||||||
|
"%.1f %cB",
|
||||||
|
float64(bytes)/float64(div),
|
||||||
|
"KMGTPE"[exp],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heat returns a color between blue and red based on the value.
|
// Heat returns a color between blue and red based on the value.
|
||||||
|
81
internal/layout/draw_test.go
Normal file
81
internal/layout/draw_test.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package layout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/hdmistat/internal/fbdraw"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBasicDrawing(t *testing.T) {
|
||||||
|
// Create a small grid for testing
|
||||||
|
grid := fbdraw.NewCharGrid(40, 10)
|
||||||
|
draw := NewDraw(grid)
|
||||||
|
|
||||||
|
// Clear the screen
|
||||||
|
draw.Clear()
|
||||||
|
|
||||||
|
// Draw some text
|
||||||
|
draw.Color(Color("white")).Text(5, 2, "Hello")
|
||||||
|
draw.TextCenter(0, 4, "Centered")
|
||||||
|
|
||||||
|
// Create a sub-grid with border
|
||||||
|
subGrid := draw.Grid(1, 1, 38, 8)
|
||||||
|
subGrid.Border(Color("gray50"))
|
||||||
|
|
||||||
|
// Basic checks
|
||||||
|
if grid.Width != 40 {
|
||||||
|
t.Errorf("Expected width 40, got %d", grid.Width)
|
||||||
|
}
|
||||||
|
if grid.Height != 10 {
|
||||||
|
t.Errorf("Expected height 10, got %d", grid.Height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that some text was written (not all cells are empty)
|
||||||
|
hasContent := false
|
||||||
|
for y := 0; y < grid.Height; y++ {
|
||||||
|
for x := 0; x < grid.Width; x++ {
|
||||||
|
if grid.Cells[y][x].Rune != ' ' {
|
||||||
|
hasContent = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasContent {
|
||||||
|
t.Error("Expected some content in the grid, but all cells are empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the grid for visual inspection
|
||||||
|
t.Logf("Rendered grid:\n%s", grid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelloWorldScenario(t *testing.T) {
|
||||||
|
// Simulate the hello world scenario
|
||||||
|
grid := fbdraw.NewCharGrid(80, 25)
|
||||||
|
draw := NewDraw(grid)
|
||||||
|
|
||||||
|
draw.Clear()
|
||||||
|
|
||||||
|
centerY := grid.Height / 2
|
||||||
|
|
||||||
|
draw.Color(Color("cyan")).Bold()
|
||||||
|
draw.TextCenter(0, centerY-2, "Hello World")
|
||||||
|
|
||||||
|
draw.Color(Color("white")).Plain()
|
||||||
|
draw.TextCenter(0, centerY, "12:34:56")
|
||||||
|
|
||||||
|
draw.Color(Color("gray60"))
|
||||||
|
draw.TextCenter(0, centerY+2, "Uptime: 1:23")
|
||||||
|
|
||||||
|
borderGrid := draw.Grid(2, 2, grid.Width-4, grid.Height-4)
|
||||||
|
borderGrid.Border(Color("gray30"))
|
||||||
|
|
||||||
|
// Check that the grid has the expected content
|
||||||
|
gridStr := grid.String()
|
||||||
|
t.Logf("Hello World grid:\n%s", gridStr)
|
||||||
|
|
||||||
|
// Very basic check - just ensure it's not empty
|
||||||
|
if len(gridStr) == 0 {
|
||||||
|
t.Error("Grid string is empty")
|
||||||
|
}
|
||||||
|
}
|
@ -10,19 +10,21 @@ import (
|
|||||||
|
|
||||||
// ExampleScreen shows how to create a screen that implements FrameGenerator
|
// ExampleScreen shows how to create a screen that implements FrameGenerator
|
||||||
type ExampleScreen struct {
|
type ExampleScreen struct {
|
||||||
name string
|
name string
|
||||||
fps float64
|
fps float64
|
||||||
|
width int
|
||||||
|
height int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ExampleScreen) GenerateFrame(grid *fbdraw.CharGrid) error {
|
func (s *ExampleScreen) GenerateFrame(grid *fbdraw.CharGrid) error {
|
||||||
// Create a draw context with the grid dimensions
|
// Create a draw context that works on the provided grid
|
||||||
draw := layout.NewDraw(grid.Width, grid.Height)
|
draw := layout.NewDraw(grid)
|
||||||
|
|
||||||
// Clear the screen
|
// Clear the screen
|
||||||
draw.Clear()
|
draw.Clear()
|
||||||
|
|
||||||
// Draw a title
|
// Draw a title
|
||||||
draw.Color(layout.Color("cyan")).Size(16).Bold()
|
draw.Color(layout.Color("cyan")).Bold()
|
||||||
draw.TextCenter(0, 2, "Example Screen: %s", s.name)
|
draw.TextCenter(0, 2, "Example Screen: %s", s.name)
|
||||||
|
|
||||||
// Create a grid for structured layout
|
// Create a grid for structured layout
|
||||||
@ -39,8 +41,6 @@ func (s *ExampleScreen) GenerateFrame(grid *fbdraw.CharGrid) error {
|
|||||||
contentGrid.Color(layout.Color("yellow")).Write(2, 8, "CPU: %.1f%%", 42.5)
|
contentGrid.Color(layout.Color("yellow")).Write(2, 8, "CPU: %.1f%%", 42.5)
|
||||||
contentGrid.Color(layout.Color("orange")).Write(2, 9, "Memory: %s / %s", layout.Bytes(4*1024*1024*1024), layout.Bytes(16*1024*1024*1024))
|
contentGrid.Color(layout.Color("orange")).Write(2, 9, "Memory: %s / %s", layout.Bytes(4*1024*1024*1024), layout.Bytes(16*1024*1024*1024))
|
||||||
|
|
||||||
// Return the rendered grid
|
|
||||||
*grid = *draw.Render()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,15 +48,21 @@ func (s *ExampleScreen) FramesPerSecond() float64 {
|
|||||||
return s.fps
|
return s.fps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ExampleScreen) Init(width, height int) error {
|
||||||
|
s.width = width
|
||||||
|
s.height = height
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestExampleUsage(t *testing.T) {
|
func TestExampleUsage(t *testing.T) {
|
||||||
// Create carousel with terminal display for testing
|
// This is just an example - in real usage you'd use NewFBDisplayAuto()
|
||||||
display := fbdraw.NewTerminalDisplay(80, 25)
|
// For testing we'll skip since we don't have a framebuffer
|
||||||
carousel := fbdraw.NewCarousel(display, 5*time.Second)
|
t.Skip("Example test - requires framebuffer")
|
||||||
|
|
||||||
// Add screens
|
// Add screens
|
||||||
carousel.AddScreen(&ExampleScreen{name: "Dashboard", fps: 1.0})
|
_ = carousel.AddScreen("Dashboard", &ExampleScreen{name: "Dashboard", fps: 1.0})
|
||||||
carousel.AddScreen(&ExampleScreen{name: "System Monitor", fps: 2.0})
|
_ = carousel.AddScreen("System Monitor", &ExampleScreen{name: "System Monitor", fps: 2.0})
|
||||||
carousel.AddScreen(&ExampleScreen{name: "Network Stats", fps: 0.5})
|
_ = carousel.AddScreen("Network Stats", &ExampleScreen{name: "Network Stats", fps: 0.5})
|
||||||
|
|
||||||
// In a real application, you would run this in a goroutine
|
// In a real application, you would run this in a goroutine
|
||||||
// ctx := context.Background()
|
// ctx := context.Background()
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// Package netmon provides network interface monitoring with historical data
|
// Package netmon provides network interface monitoring with historical data
|
||||||
|
//
|
||||||
|
//nolint:mnd
|
||||||
package netmon
|
package netmon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -103,11 +105,15 @@ func (m *Monitor) GetStats() []Stats {
|
|||||||
rate := m.calculateRate(ifaceStats, rateWindowSeconds)
|
rate := m.calculateRate(ifaceStats, rateWindowSeconds)
|
||||||
|
|
||||||
stats = append(stats, Stats{
|
stats = append(stats, Stats{
|
||||||
Name: name,
|
Name: name,
|
||||||
BytesSent: ifaceStats.lastSample.BytesSent,
|
BytesSent: ifaceStats.lastSample.BytesSent,
|
||||||
BytesRecv: ifaceStats.lastSample.BytesRecv,
|
BytesRecv: ifaceStats.lastSample.BytesRecv,
|
||||||
BitsSentRate: uint64(rate.sentRate * bitsPerByte), // Convert to bits/sec
|
BitsSentRate: uint64(
|
||||||
BitsRecvRate: uint64(rate.recvRate * bitsPerByte), // Convert to bits/sec
|
rate.sentRate * bitsPerByte,
|
||||||
|
), // Convert to bits/sec
|
||||||
|
BitsRecvRate: uint64(
|
||||||
|
rate.recvRate * bitsPerByte,
|
||||||
|
), // Convert to bits/sec
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +147,10 @@ type rateInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// calculateRate calculates the average rate over the last n seconds
|
// calculateRate calculates the average rate over the last n seconds
|
||||||
func (m *Monitor) calculateRate(ifaceStats *InterfaceStats, seconds int) rateInfo {
|
func (m *Monitor) calculateRate(
|
||||||
|
ifaceStats *InterfaceStats,
|
||||||
|
seconds int,
|
||||||
|
) rateInfo {
|
||||||
if ifaceStats.count <= 1 {
|
if ifaceStats.count <= 1 {
|
||||||
return rateInfo{}
|
return rateInfo{}
|
||||||
}
|
}
|
||||||
@ -215,7 +224,8 @@ func (m *Monitor) takeSample() {
|
|||||||
|
|
||||||
for _, counter := range counters {
|
for _, counter := range counters {
|
||||||
// Skip loopback and docker interfaces
|
// Skip loopback and docker interfaces
|
||||||
if counter.Name == "lo" || strings.HasPrefix(counter.Name, "docker") {
|
if counter.Name == "lo" ||
|
||||||
|
strings.HasPrefix(counter.Name, "docker") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// Package renderer provides screen rendering implementations for hdmistat
|
// Package renderer provides screen rendering implementations for hdmistat
|
||||||
|
//
|
||||||
|
//nolint:mnd
|
||||||
package renderer
|
package renderer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -25,7 +27,10 @@ func (s *OverviewScreen) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render draws the overview screen to the provided canvas
|
// Render draws the overview screen to the provided canvas
|
||||||
func (s *OverviewScreen) Render(canvas *layout.Canvas, info *statcollector.SystemInfo) error {
|
func (s *OverviewScreen) Render(
|
||||||
|
canvas *layout.Canvas,
|
||||||
|
info *statcollector.SystemInfo,
|
||||||
|
) error {
|
||||||
_, _ = canvas.Size()
|
_, _ = canvas.Size()
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
@ -47,11 +52,15 @@ func (s *OverviewScreen) Render(canvas *layout.Canvas, info *statcollector.Syste
|
|||||||
|
|
||||||
// Title - left aligned at consistent position
|
// Title - left aligned at consistent position
|
||||||
titleText := fmt.Sprintf("%s: status", shortHostname)
|
titleText := fmt.Sprintf("%s: status", shortHostname)
|
||||||
_ = canvas.DrawText(titleText, layout.Point{X: 50, Y: y}, layout.TextStyle{
|
_ = canvas.DrawText(
|
||||||
Size: 36, // Smaller than before
|
titleText,
|
||||||
Color: titleStyle.Color,
|
layout.Point{X: 50, Y: y},
|
||||||
Alignment: layout.AlignLeft,
|
layout.TextStyle{
|
||||||
})
|
Size: 36, // Smaller than before
|
||||||
|
Color: titleStyle.Color,
|
||||||
|
Alignment: layout.AlignLeft,
|
||||||
|
},
|
||||||
|
)
|
||||||
y += 60
|
y += 60
|
||||||
|
|
||||||
// Standard bar dimensions
|
// Standard bar dimensions
|
||||||
@ -74,8 +83,12 @@ func (s *OverviewScreen) Render(canvas *layout.Canvas, info *statcollector.Syste
|
|||||||
cpuBar := &layout.ProgressBar{
|
cpuBar := &layout.ProgressBar{
|
||||||
X: 50, Y: y,
|
X: 50, Y: y,
|
||||||
Width: barWidth, Height: barHeight,
|
Width: barWidth, Height: barHeight,
|
||||||
Value: avgCPU / 100.0,
|
Value: avgCPU / 100.0,
|
||||||
Label: fmt.Sprintf("%.1f%% average across %d cores", avgCPU, len(info.CPUPercent)),
|
Label: fmt.Sprintf(
|
||||||
|
"%.1f%% average across %d cores",
|
||||||
|
avgCPU,
|
||||||
|
len(info.CPUPercent),
|
||||||
|
),
|
||||||
LeftLabel: "0%",
|
LeftLabel: "0%",
|
||||||
RightLabel: "100%",
|
RightLabel: "100%",
|
||||||
BarColor: color.RGBA{255, 100, 100, 255},
|
BarColor: color.RGBA{255, 100, 100, 255},
|
||||||
@ -91,8 +104,12 @@ func (s *OverviewScreen) Render(canvas *layout.Canvas, info *statcollector.Syste
|
|||||||
memoryBar := &layout.ProgressBar{
|
memoryBar := &layout.ProgressBar{
|
||||||
X: 50, Y: y,
|
X: 50, Y: y,
|
||||||
Width: barWidth, Height: barHeight,
|
Width: barWidth, Height: barHeight,
|
||||||
Value: memUsedPercent,
|
Value: memUsedPercent,
|
||||||
Label: fmt.Sprintf("%s of %s", layout.FormatBytes(info.MemoryUsed), layout.FormatBytes(info.MemoryTotal)),
|
Label: fmt.Sprintf(
|
||||||
|
"%s of %s",
|
||||||
|
layout.FormatBytes(info.MemoryUsed),
|
||||||
|
layout.FormatBytes(info.MemoryTotal),
|
||||||
|
),
|
||||||
LeftLabel: "0B",
|
LeftLabel: "0B",
|
||||||
RightLabel: layout.FormatBytes(info.MemoryTotal),
|
RightLabel: layout.FormatBytes(info.MemoryTotal),
|
||||||
BarColor: color.RGBA{100, 200, 100, 255},
|
BarColor: color.RGBA{100, 200, 100, 255},
|
||||||
@ -102,7 +119,11 @@ func (s *OverviewScreen) Render(canvas *layout.Canvas, info *statcollector.Syste
|
|||||||
|
|
||||||
// Temperature section
|
// Temperature section
|
||||||
if len(info.Temperature) > 0 {
|
if len(info.Temperature) > 0 {
|
||||||
_ = canvas.DrawText("TEMPERATURE", layout.Point{X: 50, Y: y}, headerStyle)
|
_ = canvas.DrawText(
|
||||||
|
"TEMPERATURE",
|
||||||
|
layout.Point{X: 50, Y: y},
|
||||||
|
headerStyle,
|
||||||
|
)
|
||||||
y += 30
|
y += 30
|
||||||
|
|
||||||
// Find the highest temperature
|
// Find the highest temperature
|
||||||
@ -150,8 +171,13 @@ func (s *OverviewScreen) Render(canvas *layout.Canvas, info *statcollector.Syste
|
|||||||
diskBar := &layout.ProgressBar{
|
diskBar := &layout.ProgressBar{
|
||||||
X: 50, Y: y,
|
X: 50, Y: y,
|
||||||
Width: barWidth, Height: barHeight,
|
Width: barWidth, Height: barHeight,
|
||||||
Value: disk.UsedPercent / 100.0,
|
Value: disk.UsedPercent / 100.0,
|
||||||
Label: fmt.Sprintf("%s: %s of %s", disk.Path, layout.FormatBytes(disk.Used), layout.FormatBytes(disk.Total)),
|
Label: fmt.Sprintf(
|
||||||
|
"%s: %s of %s",
|
||||||
|
disk.Path,
|
||||||
|
layout.FormatBytes(disk.Used),
|
||||||
|
layout.FormatBytes(disk.Total),
|
||||||
|
),
|
||||||
LeftLabel: "0B",
|
LeftLabel: "0B",
|
||||||
RightLabel: layout.FormatBytes(disk.Total),
|
RightLabel: layout.FormatBytes(disk.Total),
|
||||||
BarColor: color.RGBA{200, 200, 100, 255},
|
BarColor: color.RGBA{200, 200, 100, 255},
|
||||||
@ -168,16 +194,28 @@ func (s *OverviewScreen) Render(canvas *layout.Canvas, info *statcollector.Syste
|
|||||||
|
|
||||||
// Network section
|
// Network section
|
||||||
if len(info.Network) > 0 {
|
if len(info.Network) > 0 {
|
||||||
_ = canvas.DrawText("NETWORK", layout.Point{X: 50, Y: y}, headerStyle)
|
_ = canvas.DrawText(
|
||||||
|
"NETWORK",
|
||||||
|
layout.Point{X: 50, Y: y},
|
||||||
|
headerStyle,
|
||||||
|
)
|
||||||
y += 30
|
y += 30
|
||||||
|
|
||||||
for _, net := range info.Network {
|
for _, net := range info.Network {
|
||||||
// Network interface info
|
// Network interface info
|
||||||
interfaceText := net.Name
|
interfaceText := net.Name
|
||||||
if len(net.IPAddresses) > 0 {
|
if len(net.IPAddresses) > 0 {
|
||||||
interfaceText = fmt.Sprintf("%s (%s)", net.Name, net.IPAddresses[0])
|
interfaceText = fmt.Sprintf(
|
||||||
|
"%s (%s)",
|
||||||
|
net.Name,
|
||||||
|
net.IPAddresses[0],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
_ = canvas.DrawText(interfaceText, layout.Point{X: 50, Y: y}, normalStyle)
|
_ = canvas.DrawText(
|
||||||
|
interfaceText,
|
||||||
|
layout.Point{X: 50, Y: y},
|
||||||
|
normalStyle,
|
||||||
|
)
|
||||||
y += 25
|
y += 25
|
||||||
|
|
||||||
// Get link speed for scaling (default to 1 Gbps if unknown)
|
// Get link speed for scaling (default to 1 Gbps if unknown)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
//nolint:mnd
|
||||||
package renderer
|
package renderer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -45,7 +46,10 @@ func (s *ProcessScreen) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render draws the process screen to the provided canvas
|
// Render draws the process screen to the provided canvas
|
||||||
func (s *ProcessScreen) Render(canvas *layout.Canvas, info *statcollector.SystemInfo) error {
|
func (s *ProcessScreen) Render(
|
||||||
|
canvas *layout.Canvas,
|
||||||
|
info *statcollector.SystemInfo,
|
||||||
|
) error {
|
||||||
width, _ := canvas.Size()
|
width, _ := canvas.Size()
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
@ -74,11 +78,15 @@ func (s *ProcessScreen) Render(canvas *layout.Canvas, info *statcollector.System
|
|||||||
} else {
|
} else {
|
||||||
titleText = fmt.Sprintf("%s: memory", shortHostname)
|
titleText = fmt.Sprintf("%s: memory", shortHostname)
|
||||||
}
|
}
|
||||||
_ = canvas.DrawText(titleText, layout.Point{X: 50, Y: y}, layout.TextStyle{
|
_ = canvas.DrawText(
|
||||||
Size: 36, // Same size as overview
|
titleText,
|
||||||
Color: titleStyle.Color,
|
layout.Point{X: 50, Y: y},
|
||||||
Alignment: layout.AlignLeft,
|
layout.TextStyle{
|
||||||
})
|
Size: 36, // Same size as overview
|
||||||
|
Color: titleStyle.Color,
|
||||||
|
Alignment: layout.AlignLeft,
|
||||||
|
},
|
||||||
|
)
|
||||||
y += 60
|
y += 60
|
||||||
|
|
||||||
// Sort processes
|
// Sort processes
|
||||||
@ -99,9 +107,17 @@ func (s *ProcessScreen) Render(canvas *layout.Canvas, info *statcollector.System
|
|||||||
x := 50
|
x := 50
|
||||||
_ = canvas.DrawText("PID", layout.Point{X: x, Y: y}, headerStyle)
|
_ = canvas.DrawText("PID", layout.Point{X: x, Y: y}, headerStyle)
|
||||||
_ = canvas.DrawText("USER", layout.Point{X: x + 100, Y: y}, headerStyle)
|
_ = canvas.DrawText("USER", layout.Point{X: x + 100, Y: y}, headerStyle)
|
||||||
_ = canvas.DrawText("PROCESS", layout.Point{X: x + 250, Y: y}, headerStyle)
|
_ = canvas.DrawText(
|
||||||
|
"PROCESS",
|
||||||
|
layout.Point{X: x + 250, Y: y},
|
||||||
|
headerStyle,
|
||||||
|
)
|
||||||
_ = canvas.DrawText("CPU %", layout.Point{X: x + 600, Y: y}, headerStyle)
|
_ = canvas.DrawText("CPU %", layout.Point{X: x + 600, Y: y}, headerStyle)
|
||||||
_ = canvas.DrawText("MEMORY", layout.Point{X: x + 700, Y: y}, headerStyle)
|
_ = canvas.DrawText(
|
||||||
|
"MEMORY",
|
||||||
|
layout.Point{X: x + 700, Y: y},
|
||||||
|
headerStyle,
|
||||||
|
)
|
||||||
|
|
||||||
y += 30
|
y += 30
|
||||||
canvas.DrawHLine(x, y, width-100, color.RGBA{100, 100, 100, 255})
|
canvas.DrawHLine(x, y, width-100, color.RGBA{100, 100, 100, 255})
|
||||||
@ -126,16 +142,42 @@ func (s *ProcessScreen) Render(canvas *layout.Canvas, info *statcollector.System
|
|||||||
|
|
||||||
// Highlight bar for high usage (draw BEFORE text)
|
// Highlight bar for high usage (draw BEFORE text)
|
||||||
if s.SortBy == "cpu" && proc.CPUPercent > cpuHighThreshold {
|
if s.SortBy == "cpu" && proc.CPUPercent > cpuHighThreshold {
|
||||||
canvas.DrawBox(x-5, y-15, width-90, 20, color.RGBA{100, 50, 50, 100})
|
canvas.DrawBox(
|
||||||
|
x-5,
|
||||||
|
y-15,
|
||||||
|
width-90,
|
||||||
|
20,
|
||||||
|
color.RGBA{100, 50, 50, 100},
|
||||||
|
)
|
||||||
} else if s.SortBy == "memory" && float64(proc.MemoryRSS)/float64(info.MemoryTotal) > memoryHighRatio {
|
} else if s.SortBy == "memory" && float64(proc.MemoryRSS)/float64(info.MemoryTotal) > memoryHighRatio {
|
||||||
canvas.DrawBox(x-5, y-15, width-90, 20, color.RGBA{50, 50, 100, 100})
|
canvas.DrawBox(x-5, y-15, width-90, 20, color.RGBA{50, 50, 100, 100})
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = canvas.DrawText(fmt.Sprintf("%d", proc.PID), layout.Point{X: x, Y: y}, normalStyle)
|
_ = canvas.DrawText(
|
||||||
_ = canvas.DrawText(user, layout.Point{X: x + 100, Y: y}, normalStyle)
|
fmt.Sprintf("%d", proc.PID),
|
||||||
_ = canvas.DrawText(name, layout.Point{X: x + 250, Y: y}, normalStyle)
|
layout.Point{X: x, Y: y},
|
||||||
_ = canvas.DrawText(fmt.Sprintf("%.1f", proc.CPUPercent), layout.Point{X: x + 600, Y: y}, normalStyle)
|
normalStyle,
|
||||||
_ = canvas.DrawText(layout.FormatBytes(proc.MemoryRSS), layout.Point{X: x + 700, Y: y}, normalStyle)
|
)
|
||||||
|
_ = canvas.DrawText(
|
||||||
|
user,
|
||||||
|
layout.Point{X: x + 100, Y: y},
|
||||||
|
normalStyle,
|
||||||
|
)
|
||||||
|
_ = canvas.DrawText(
|
||||||
|
name,
|
||||||
|
layout.Point{X: x + 250, Y: y},
|
||||||
|
normalStyle,
|
||||||
|
)
|
||||||
|
_ = canvas.DrawText(
|
||||||
|
fmt.Sprintf("%.1f", proc.CPUPercent),
|
||||||
|
layout.Point{X: x + 600, Y: y},
|
||||||
|
normalStyle,
|
||||||
|
)
|
||||||
|
_ = canvas.DrawText(
|
||||||
|
layout.FormatBytes(proc.MemoryRSS),
|
||||||
|
layout.Point{X: x + 700, Y: y},
|
||||||
|
normalStyle,
|
||||||
|
)
|
||||||
|
|
||||||
y += 25
|
y += 25
|
||||||
}
|
}
|
||||||
@ -151,17 +193,23 @@ func (s *ProcessScreen) Render(canvas *layout.Canvas, info *statcollector.System
|
|||||||
}
|
}
|
||||||
avgCPU := totalCPU / float64(len(info.CPUPercent))
|
avgCPU := totalCPU / float64(len(info.CPUPercent))
|
||||||
|
|
||||||
footerText := fmt.Sprintf("System: CPU %.1f%% | Memory: %s / %s (%.1f%%)",
|
footerText := fmt.Sprintf(
|
||||||
|
"System: CPU %.1f%% | Memory: %s / %s (%.1f%%)",
|
||||||
avgCPU,
|
avgCPU,
|
||||||
layout.FormatBytes(info.MemoryUsed),
|
layout.FormatBytes(info.MemoryUsed),
|
||||||
layout.FormatBytes(info.MemoryTotal),
|
layout.FormatBytes(info.MemoryTotal),
|
||||||
float64(info.MemoryUsed)/float64(info.MemoryTotal)*percentMultiplier)
|
float64(info.MemoryUsed)/float64(info.MemoryTotal)*percentMultiplier,
|
||||||
|
)
|
||||||
|
|
||||||
_ = canvas.DrawText(footerText, layout.Point{X: width / halfDivisor, Y: y}, layout.TextStyle{
|
_ = canvas.DrawText(
|
||||||
Size: smallStyle.Size,
|
footerText,
|
||||||
Color: smallStyle.Color,
|
layout.Point{X: width / halfDivisor, Y: y},
|
||||||
Alignment: layout.AlignCenter,
|
layout.TextStyle{
|
||||||
})
|
Size: smallStyle.Size,
|
||||||
|
Color: smallStyle.Color,
|
||||||
|
Alignment: layout.AlignCenter,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
//nolint:mnd
|
||||||
package renderer
|
package renderer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -44,7 +45,10 @@ func (r *Renderer) SetResolution(width, height int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RenderScreen renders a screen to an image
|
// RenderScreen renders a screen to an image
|
||||||
func (r *Renderer) RenderScreen(screen Screen, info *statcollector.SystemInfo) (*image.RGBA, error) {
|
func (r *Renderer) RenderScreen(
|
||||||
|
screen Screen,
|
||||||
|
info *statcollector.SystemInfo,
|
||||||
|
) (*image.RGBA, error) {
|
||||||
canvas := layout.NewCanvas(r.width, r.height, r.font, r.logger)
|
canvas := layout.NewCanvas(r.width, r.height, r.font, r.logger)
|
||||||
|
|
||||||
// Draw common header
|
// Draw common header
|
||||||
@ -58,7 +62,10 @@ func (r *Renderer) RenderScreen(screen Screen, info *statcollector.SystemInfo) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// drawHeader draws the common header with system info
|
// drawHeader draws the common header with system info
|
||||||
func (r *Renderer) drawHeader(canvas *layout.Canvas, _ *statcollector.SystemInfo) {
|
func (r *Renderer) drawHeader(
|
||||||
|
canvas *layout.Canvas,
|
||||||
|
_ *statcollector.SystemInfo,
|
||||||
|
) {
|
||||||
width, _ := canvas.Size()
|
width, _ := canvas.Size()
|
||||||
headerColor := color.RGBA{150, 150, 150, 255}
|
headerColor := color.RGBA{150, 150, 150, 255}
|
||||||
headerStyle := layout.TextStyle{Size: 14, Color: headerColor, Bold: true}
|
headerStyle := layout.TextStyle{Size: 14, Color: headerColor, Bold: true}
|
||||||
@ -97,42 +104,66 @@ func (r *Renderer) drawHeader(canvas *layout.Canvas, _ *statcollector.SystemInfo
|
|||||||
// For simplicity, we'll use a fixed position approach
|
// For simplicity, we'll use a fixed position approach
|
||||||
|
|
||||||
// Draw UTC time
|
// Draw UTC time
|
||||||
_ = canvas.DrawText(utcTime, layout.Point{X: width - 40, Y: 20}, layout.TextStyle{
|
_ = canvas.DrawText(
|
||||||
Size: headerStyle.Size,
|
utcTime,
|
||||||
Color: color.RGBA{255, 255, 255, 255}, // White
|
layout.Point{X: width - 40, Y: 20},
|
||||||
Alignment: layout.AlignRight,
|
layout.TextStyle{
|
||||||
Bold: true,
|
Size: headerStyle.Size,
|
||||||
})
|
Color: color.RGBA{255, 255, 255, 255}, // White
|
||||||
|
Alignment: layout.AlignRight,
|
||||||
|
Bold: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
// UTC sync indicators
|
// UTC sync indicators
|
||||||
_ = canvas.DrawText(syncIndicator, layout.Point{X: width - 280, Y: 20}, layout.TextStyle{
|
_ = canvas.DrawText(
|
||||||
Size: headerStyle.Size,
|
syncIndicator,
|
||||||
Color: syncColor,
|
layout.Point{X: width - 280, Y: 20},
|
||||||
Bold: true,
|
layout.TextStyle{
|
||||||
})
|
Size: headerStyle.Size,
|
||||||
_ = canvas.DrawText(syncIndicator, layout.Point{X: width - 20, Y: 20}, layout.TextStyle{
|
Color: syncColor,
|
||||||
Size: headerStyle.Size,
|
Bold: true,
|
||||||
Color: syncColor,
|
},
|
||||||
Bold: true,
|
)
|
||||||
})
|
_ = canvas.DrawText(
|
||||||
|
syncIndicator,
|
||||||
|
layout.Point{X: width - 20, Y: 20},
|
||||||
|
layout.TextStyle{
|
||||||
|
Size: headerStyle.Size,
|
||||||
|
Color: syncColor,
|
||||||
|
Bold: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// Draw local time
|
// Draw local time
|
||||||
_ = canvas.DrawText(localTime, layout.Point{X: width - 40, Y: 35}, layout.TextStyle{
|
_ = canvas.DrawText(
|
||||||
Size: headerStyle.Size,
|
localTime,
|
||||||
Color: color.RGBA{255, 255, 255, 255}, // White
|
layout.Point{X: width - 40, Y: 35},
|
||||||
Alignment: layout.AlignRight,
|
layout.TextStyle{
|
||||||
Bold: true,
|
Size: headerStyle.Size,
|
||||||
})
|
Color: color.RGBA{255, 255, 255, 255}, // White
|
||||||
|
Alignment: layout.AlignRight,
|
||||||
|
Bold: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
// Local sync indicators
|
// Local sync indicators
|
||||||
_ = canvas.DrawText(syncIndicator, layout.Point{X: width - 280, Y: 35}, layout.TextStyle{
|
_ = canvas.DrawText(
|
||||||
Size: headerStyle.Size,
|
syncIndicator,
|
||||||
Color: syncColor,
|
layout.Point{X: width - 280, Y: 35},
|
||||||
Bold: true,
|
layout.TextStyle{
|
||||||
})
|
Size: headerStyle.Size,
|
||||||
_ = canvas.DrawText(syncIndicator, layout.Point{X: width - 20, Y: 35}, layout.TextStyle{
|
Color: syncColor,
|
||||||
Size: headerStyle.Size,
|
Bold: true,
|
||||||
Color: syncColor,
|
},
|
||||||
Bold: true,
|
)
|
||||||
})
|
_ = canvas.DrawText(
|
||||||
|
syncIndicator,
|
||||||
|
layout.Point{X: width - 20, Y: 35},
|
||||||
|
layout.TextStyle{
|
||||||
|
Size: headerStyle.Size,
|
||||||
|
Color: syncColor,
|
||||||
|
Bold: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// Get uptime command output
|
// Get uptime command output
|
||||||
uptimeStr := "uptime unavailable"
|
uptimeStr := "uptime unavailable"
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
//nolint:mnd
|
||||||
package renderer
|
package renderer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -24,7 +25,10 @@ func (s *StatusScreen) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render renders the status screen
|
// Render renders the status screen
|
||||||
func (s *StatusScreen) Render(canvas *layout.Canvas, info *statcollector.SystemInfo) error {
|
func (s *StatusScreen) Render(
|
||||||
|
canvas *layout.Canvas,
|
||||||
|
info *statcollector.SystemInfo,
|
||||||
|
) error {
|
||||||
// Use consistent font size for entire screen
|
// Use consistent font size for entire screen
|
||||||
const fontSize = 16
|
const fontSize = 16
|
||||||
|
|
||||||
@ -51,37 +55,70 @@ func (s *StatusScreen) Render(canvas *layout.Canvas, info *statcollector.SystemI
|
|||||||
y += 40
|
y += 40
|
||||||
|
|
||||||
// CPU section
|
// CPU section
|
||||||
cpuLabel := fmt.Sprintf("CPU: %.1f%% average across %d cores",
|
cpuLabel := fmt.Sprintf(
|
||||||
getAverageCPU(info.CPUPercent), len(info.CPUPercent))
|
"CPU: %.1f%% average across %d cores",
|
||||||
|
getAverageCPU(info.CPUPercent),
|
||||||
|
len(info.CPUPercent),
|
||||||
|
)
|
||||||
_ = canvas.DrawText(cpuLabel, layout.Point{X: 16, Y: y}, normalStyle)
|
_ = canvas.DrawText(cpuLabel, layout.Point{X: 16, Y: y}, normalStyle)
|
||||||
y += 25
|
y += 25
|
||||||
|
|
||||||
// CPU progress bar
|
// CPU progress bar
|
||||||
_ = canvas.DrawText("0%", layout.Point{X: 100, Y: y}, dimStyle)
|
_ = canvas.DrawText("0%", layout.Point{X: 100, Y: y}, dimStyle)
|
||||||
drawProgressBar(canvas, 130, y-10, getAverageCPU(info.CPUPercent)/100.0, textColor)
|
drawProgressBar(
|
||||||
|
canvas,
|
||||||
|
130,
|
||||||
|
y-10,
|
||||||
|
getAverageCPU(info.CPUPercent)/100.0,
|
||||||
|
textColor,
|
||||||
|
)
|
||||||
_ = canvas.DrawText("100%", layout.Point{X: 985, Y: y}, dimStyle)
|
_ = canvas.DrawText("100%", layout.Point{X: 985, Y: y}, dimStyle)
|
||||||
y += 40
|
y += 40
|
||||||
|
|
||||||
// Memory section
|
// Memory section
|
||||||
memUsedPercent := float64(info.MemoryUsed) / float64(info.MemoryTotal) * 100.0
|
memUsedPercent := float64(
|
||||||
memLabel := fmt.Sprintf("MEMORY: %s of %s (%.1f%%)",
|
info.MemoryUsed,
|
||||||
|
) / float64(
|
||||||
|
info.MemoryTotal,
|
||||||
|
) * 100.0
|
||||||
|
memLabel := fmt.Sprintf(
|
||||||
|
"MEMORY: %s of %s (%.1f%%)",
|
||||||
layout.FormatBytes(info.MemoryUsed),
|
layout.FormatBytes(info.MemoryUsed),
|
||||||
layout.FormatBytes(info.MemoryTotal),
|
layout.FormatBytes(info.MemoryTotal),
|
||||||
memUsedPercent)
|
memUsedPercent,
|
||||||
|
)
|
||||||
_ = canvas.DrawText(memLabel, layout.Point{X: 16, Y: y}, normalStyle)
|
_ = canvas.DrawText(memLabel, layout.Point{X: 16, Y: y}, normalStyle)
|
||||||
y += 25
|
y += 25
|
||||||
|
|
||||||
// Memory progress bar
|
// Memory progress bar
|
||||||
_ = canvas.DrawText("0B", layout.Point{X: 100, Y: y}, dimStyle)
|
_ = canvas.DrawText("0B", layout.Point{X: 100, Y: y}, dimStyle)
|
||||||
drawProgressBar(canvas, 130, y-10, float64(info.MemoryUsed)/float64(info.MemoryTotal), textColor)
|
drawProgressBar(
|
||||||
_ = canvas.DrawText(layout.FormatBytes(info.MemoryTotal), layout.Point{X: 985, Y: y}, dimStyle)
|
canvas,
|
||||||
|
130,
|
||||||
|
y-10,
|
||||||
|
float64(info.MemoryUsed)/float64(info.MemoryTotal),
|
||||||
|
textColor,
|
||||||
|
)
|
||||||
|
_ = canvas.DrawText(
|
||||||
|
layout.FormatBytes(info.MemoryTotal),
|
||||||
|
layout.Point{X: 985, Y: y},
|
||||||
|
dimStyle,
|
||||||
|
)
|
||||||
y += 40
|
y += 40
|
||||||
|
|
||||||
// Temperature section
|
// Temperature section
|
||||||
if len(info.Temperature) > 0 {
|
if len(info.Temperature) > 0 {
|
||||||
maxTemp, maxSensor := getMaxTemperature(info.Temperature)
|
maxTemp, maxSensor := getMaxTemperature(info.Temperature)
|
||||||
tempLabel := fmt.Sprintf("TEMPERATURE: %.1f°C (%s)", maxTemp, maxSensor)
|
tempLabel := fmt.Sprintf(
|
||||||
_ = canvas.DrawText(tempLabel, layout.Point{X: 16, Y: y}, normalStyle)
|
"TEMPERATURE: %.1f°C (%s)",
|
||||||
|
maxTemp,
|
||||||
|
maxSensor,
|
||||||
|
)
|
||||||
|
_ = canvas.DrawText(
|
||||||
|
tempLabel,
|
||||||
|
layout.Point{X: 16, Y: y},
|
||||||
|
normalStyle,
|
||||||
|
)
|
||||||
y += 25
|
y += 25
|
||||||
|
|
||||||
// Temperature progress bar (30-99°C scale)
|
// Temperature progress bar (30-99°C scale)
|
||||||
@ -99,7 +136,11 @@ func (s *StatusScreen) Render(canvas *layout.Canvas, info *statcollector.SystemI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Disk usage section
|
// Disk usage section
|
||||||
_ = canvas.DrawText("DISK USAGE:", layout.Point{X: 16, Y: y}, normalStyle)
|
_ = canvas.DrawText(
|
||||||
|
"DISK USAGE:",
|
||||||
|
layout.Point{X: 16, Y: y},
|
||||||
|
normalStyle,
|
||||||
|
)
|
||||||
y += 25
|
y += 25
|
||||||
|
|
||||||
for _, disk := range info.DiskUsage {
|
for _, disk := range info.DiskUsage {
|
||||||
@ -113,12 +154,26 @@ func (s *StatusScreen) Render(canvas *layout.Canvas, info *statcollector.SystemI
|
|||||||
layout.FormatBytes(disk.Used),
|
layout.FormatBytes(disk.Used),
|
||||||
layout.FormatBytes(disk.Total),
|
layout.FormatBytes(disk.Total),
|
||||||
disk.UsedPercent)
|
disk.UsedPercent)
|
||||||
_ = canvas.DrawText(diskLabel, layout.Point{X: 16, Y: y}, normalStyle)
|
_ = canvas.DrawText(
|
||||||
|
diskLabel,
|
||||||
|
layout.Point{X: 16, Y: y},
|
||||||
|
normalStyle,
|
||||||
|
)
|
||||||
|
|
||||||
// Disk progress bar
|
// Disk progress bar
|
||||||
_ = canvas.DrawText("0B", layout.Point{X: 470, Y: y}, dimStyle)
|
_ = canvas.DrawText("0B", layout.Point{X: 470, Y: y}, dimStyle)
|
||||||
drawDiskProgressBar(canvas, 500, y-10, disk.UsedPercent/100.0, textColor)
|
drawDiskProgressBar(
|
||||||
_ = canvas.DrawText(layout.FormatBytes(disk.Total), layout.Point{X: 985, Y: y}, dimStyle)
|
canvas,
|
||||||
|
500,
|
||||||
|
y-10,
|
||||||
|
disk.UsedPercent/100.0,
|
||||||
|
textColor,
|
||||||
|
)
|
||||||
|
_ = canvas.DrawText(
|
||||||
|
layout.FormatBytes(disk.Total),
|
||||||
|
layout.Point{X: 985, Y: y},
|
||||||
|
dimStyle,
|
||||||
|
)
|
||||||
y += 30
|
y += 30
|
||||||
|
|
||||||
if y > 700 {
|
if y > 700 {
|
||||||
@ -129,16 +184,28 @@ func (s *StatusScreen) Render(canvas *layout.Canvas, info *statcollector.SystemI
|
|||||||
// Network section
|
// Network section
|
||||||
if len(info.Network) > 0 {
|
if len(info.Network) > 0 {
|
||||||
y += 15
|
y += 15
|
||||||
_ = canvas.DrawText("NETWORK:", layout.Point{X: 16, Y: y}, normalStyle)
|
_ = canvas.DrawText(
|
||||||
|
"NETWORK:",
|
||||||
|
layout.Point{X: 16, Y: y},
|
||||||
|
normalStyle,
|
||||||
|
)
|
||||||
y += 25
|
y += 25
|
||||||
|
|
||||||
for _, net := range info.Network {
|
for _, net := range info.Network {
|
||||||
// Interface header
|
// Interface header
|
||||||
interfaceText := fmt.Sprintf(" * %s", net.Name)
|
interfaceText := fmt.Sprintf(" * %s", net.Name)
|
||||||
if len(net.IPAddresses) > 0 {
|
if len(net.IPAddresses) > 0 {
|
||||||
interfaceText = fmt.Sprintf(" * %s (%s):", net.Name, net.IPAddresses[0])
|
interfaceText = fmt.Sprintf(
|
||||||
|
" * %s (%s):",
|
||||||
|
net.Name,
|
||||||
|
net.IPAddresses[0],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
_ = canvas.DrawText(interfaceText, layout.Point{X: 16, Y: y}, normalStyle)
|
_ = canvas.DrawText(
|
||||||
|
interfaceText,
|
||||||
|
layout.Point{X: 16, Y: y},
|
||||||
|
normalStyle,
|
||||||
|
)
|
||||||
y += 25
|
y += 25
|
||||||
|
|
||||||
// Get link speed for scaling (default to 1 Gbps if unknown)
|
// Get link speed for scaling (default to 1 Gbps if unknown)
|
||||||
@ -152,19 +219,59 @@ func (s *StatusScreen) Render(canvas *layout.Canvas, info *statcollector.SystemI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upload rate
|
// Upload rate
|
||||||
upLabel := fmt.Sprintf(" ↑ %7s (%s)", net.FormatSentRate(), linkSpeedText)
|
upLabel := fmt.Sprintf(
|
||||||
_ = canvas.DrawText(upLabel, layout.Point{X: 16, Y: y}, normalStyle)
|
" ↑ %7s (%s)",
|
||||||
_ = canvas.DrawText("0 bit/s", layout.Point{X: 400, Y: y}, dimStyle)
|
net.FormatSentRate(),
|
||||||
drawNetworkProgressBar(canvas, 500, y-10, float64(net.BitsSentRate)/float64(linkSpeed), textColor)
|
linkSpeedText,
|
||||||
_ = canvas.DrawText(humanize.SI(float64(linkSpeed), "bit/s"), layout.Point{X: 960, Y: y}, dimStyle)
|
)
|
||||||
|
_ = canvas.DrawText(
|
||||||
|
upLabel,
|
||||||
|
layout.Point{X: 16, Y: y},
|
||||||
|
normalStyle,
|
||||||
|
)
|
||||||
|
_ = canvas.DrawText(
|
||||||
|
"0 bit/s",
|
||||||
|
layout.Point{X: 400, Y: y},
|
||||||
|
dimStyle,
|
||||||
|
)
|
||||||
|
drawNetworkProgressBar(
|
||||||
|
canvas,
|
||||||
|
500,
|
||||||
|
y-10,
|
||||||
|
float64(net.BitsSentRate)/float64(linkSpeed),
|
||||||
|
textColor,
|
||||||
|
)
|
||||||
|
_ = canvas.DrawText(
|
||||||
|
humanize.SI(float64(linkSpeed), "bit/s"),
|
||||||
|
layout.Point{X: 960, Y: y},
|
||||||
|
dimStyle,
|
||||||
|
)
|
||||||
y += 25
|
y += 25
|
||||||
|
|
||||||
// Download rate
|
// Download rate
|
||||||
downLabel := fmt.Sprintf(" ↓ %7s", net.FormatRecvRate())
|
downLabel := fmt.Sprintf(" ↓ %7s", net.FormatRecvRate())
|
||||||
_ = canvas.DrawText(downLabel, layout.Point{X: 16, Y: y}, normalStyle)
|
_ = canvas.DrawText(
|
||||||
_ = canvas.DrawText("0 bit/s", layout.Point{X: 400, Y: y}, dimStyle)
|
downLabel,
|
||||||
drawNetworkProgressBar(canvas, 500, y-10, float64(net.BitsRecvRate)/float64(linkSpeed), textColor)
|
layout.Point{X: 16, Y: y},
|
||||||
_ = canvas.DrawText(humanize.SI(float64(linkSpeed), "bit/s"), layout.Point{X: 960, Y: y}, dimStyle)
|
normalStyle,
|
||||||
|
)
|
||||||
|
_ = canvas.DrawText(
|
||||||
|
"0 bit/s",
|
||||||
|
layout.Point{X: 400, Y: y},
|
||||||
|
dimStyle,
|
||||||
|
)
|
||||||
|
drawNetworkProgressBar(
|
||||||
|
canvas,
|
||||||
|
500,
|
||||||
|
y-10,
|
||||||
|
float64(net.BitsRecvRate)/float64(linkSpeed),
|
||||||
|
textColor,
|
||||||
|
)
|
||||||
|
_ = canvas.DrawText(
|
||||||
|
humanize.SI(float64(linkSpeed), "bit/s"),
|
||||||
|
layout.Point{X: 960, Y: y},
|
||||||
|
dimStyle,
|
||||||
|
)
|
||||||
y += 35
|
y += 35
|
||||||
|
|
||||||
if y > 900 {
|
if y > 900 {
|
||||||
@ -177,45 +284,96 @@ func (s *StatusScreen) Render(canvas *layout.Canvas, info *statcollector.SystemI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// drawProgressBar draws a progress bar matching the mockup style
|
// drawProgressBar draws a progress bar matching the mockup style
|
||||||
func drawProgressBar(canvas *layout.Canvas, x, y int, value float64, color color.Color) {
|
func drawProgressBar(
|
||||||
|
canvas *layout.Canvas,
|
||||||
|
x, y int,
|
||||||
|
value float64,
|
||||||
|
color color.Color,
|
||||||
|
) {
|
||||||
const barWidth = 850
|
const barWidth = 850
|
||||||
|
|
||||||
// Draw opening bracket
|
// Draw opening bracket
|
||||||
_ = canvas.DrawText("[", layout.Point{X: x, Y: y + 15}, layout.TextStyle{Size: 16, Color: color})
|
_ = canvas.DrawText(
|
||||||
|
"[",
|
||||||
|
layout.Point{X: x, Y: y + 15},
|
||||||
|
layout.TextStyle{Size: 16, Color: color},
|
||||||
|
)
|
||||||
|
|
||||||
// Calculate fill
|
// Calculate fill
|
||||||
fillChars := int(value * 80)
|
fillChars := int(value * 80)
|
||||||
emptyChars := 80 - fillChars
|
emptyChars := 80 - fillChars
|
||||||
|
|
||||||
// Draw bar content
|
// Draw bar content
|
||||||
barContent := strings.Repeat("█", fillChars) + strings.Repeat("▒", emptyChars)
|
barContent := strings.Repeat(
|
||||||
_ = canvas.DrawText(barContent, layout.Point{X: x + 10, Y: y + 15}, layout.TextStyle{Size: 16, Color: color})
|
"█",
|
||||||
|
fillChars,
|
||||||
|
) + strings.Repeat(
|
||||||
|
"▒",
|
||||||
|
emptyChars,
|
||||||
|
)
|
||||||
|
_ = canvas.DrawText(
|
||||||
|
barContent,
|
||||||
|
layout.Point{X: x + 10, Y: y + 15},
|
||||||
|
layout.TextStyle{Size: 16, Color: color},
|
||||||
|
)
|
||||||
|
|
||||||
// Draw closing bracket
|
// Draw closing bracket
|
||||||
_ = canvas.DrawText("]", layout.Point{X: x + barWidth - 10, Y: y + 15}, layout.TextStyle{Size: 16, Color: color})
|
_ = canvas.DrawText(
|
||||||
|
"]",
|
||||||
|
layout.Point{X: x + barWidth - 10, Y: y + 15},
|
||||||
|
layout.TextStyle{Size: 16, Color: color},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawDiskProgressBar draws a smaller progress bar for disk usage
|
// drawDiskProgressBar draws a smaller progress bar for disk usage
|
||||||
func drawDiskProgressBar(canvas *layout.Canvas, x, y int, value float64, color color.Color) {
|
func drawDiskProgressBar(
|
||||||
|
canvas *layout.Canvas,
|
||||||
|
x, y int,
|
||||||
|
value float64,
|
||||||
|
color color.Color,
|
||||||
|
) {
|
||||||
const barWidth = 480
|
const barWidth = 480
|
||||||
|
|
||||||
// Draw opening bracket
|
// Draw opening bracket
|
||||||
_ = canvas.DrawText("[", layout.Point{X: x, Y: y + 15}, layout.TextStyle{Size: 16, Color: color})
|
_ = canvas.DrawText(
|
||||||
|
"[",
|
||||||
|
layout.Point{X: x, Y: y + 15},
|
||||||
|
layout.TextStyle{Size: 16, Color: color},
|
||||||
|
)
|
||||||
|
|
||||||
// Calculate fill (50 chars total)
|
// Calculate fill (50 chars total)
|
||||||
fillChars := int(value * 50)
|
fillChars := int(value * 50)
|
||||||
emptyChars := 50 - fillChars
|
emptyChars := 50 - fillChars
|
||||||
|
|
||||||
// Draw bar content
|
// Draw bar content
|
||||||
barContent := strings.Repeat("█", fillChars) + strings.Repeat("▒", emptyChars)
|
barContent := strings.Repeat(
|
||||||
_ = canvas.DrawText(barContent, layout.Point{X: x + 10, Y: y + 15}, layout.TextStyle{Size: 16, Color: color})
|
"█",
|
||||||
|
fillChars,
|
||||||
|
) + strings.Repeat(
|
||||||
|
"▒",
|
||||||
|
emptyChars,
|
||||||
|
)
|
||||||
|
_ = canvas.DrawText(
|
||||||
|
barContent,
|
||||||
|
layout.Point{X: x + 10, Y: y + 15},
|
||||||
|
layout.TextStyle{Size: 16, Color: color},
|
||||||
|
)
|
||||||
|
|
||||||
// Draw closing bracket
|
// Draw closing bracket
|
||||||
_ = canvas.DrawText("]", layout.Point{X: x + barWidth - 10, Y: y + 15}, layout.TextStyle{Size: 16, Color: color})
|
_ = canvas.DrawText(
|
||||||
|
"]",
|
||||||
|
layout.Point{X: x + barWidth - 10, Y: y + 15},
|
||||||
|
layout.TextStyle{Size: 16, Color: color},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawNetworkProgressBar draws a progress bar for network rates
|
// drawNetworkProgressBar draws a progress bar for network rates
|
||||||
func drawNetworkProgressBar(canvas *layout.Canvas, x, y int, value float64, color color.Color) {
|
func drawNetworkProgressBar(
|
||||||
|
canvas *layout.Canvas,
|
||||||
|
x, y int,
|
||||||
|
value float64,
|
||||||
|
color color.Color,
|
||||||
|
) {
|
||||||
// Same as disk progress bar
|
// Same as disk progress bar
|
||||||
drawDiskProgressBar(canvas, x, y, value, color)
|
drawDiskProgressBar(canvas, x, y, value, color)
|
||||||
}
|
}
|
||||||
|
@ -329,7 +329,7 @@ func (c *SystemCollector) getLinkSpeed(ifaceName string) uint64 {
|
|||||||
// Look for lines like "Speed: 1000Mb/s" or "Speed: 10000Mb/s"
|
// Look for lines like "Speed: 1000Mb/s" or "Speed: 10000Mb/s"
|
||||||
speedRegex := regexp.MustCompile(`Speed:\s+(\d+)Mb/s`)
|
speedRegex := regexp.MustCompile(`Speed:\s+(\d+)Mb/s`)
|
||||||
matches := speedRegex.FindSubmatch(output)
|
matches := speedRegex.FindSubmatch(output)
|
||||||
if len(matches) < 2 {
|
if len(matches) < 2 { //nolint:mnd
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
71
test/test-fbhello.sh
Executable file
71
test/test-fbhello.sh
Executable file
@ -0,0 +1,71 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test script for fbhello in Linux VM
|
||||||
|
# Stops hdmistat service, syncs repo, builds and runs fbhello
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Get the directory where this script is located
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Starting fbhello test...${NC}"
|
||||||
|
|
||||||
|
# Stop hdmistat service if running
|
||||||
|
echo -e "${YELLOW}Checking hdmistat service...${NC}"
|
||||||
|
if systemctl is-active --quiet hdmistat 2>/dev/null; then
|
||||||
|
echo -e "${YELLOW}Stopping hdmistat service...${NC}"
|
||||||
|
sudo systemctl stop hdmistat
|
||||||
|
echo -e "${GREEN}hdmistat service stopped${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}hdmistat service not running${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create target directory
|
||||||
|
echo -e "${YELLOW}Creating target directory...${NC}"
|
||||||
|
sudo rm -rf /tmp/hdmistat
|
||||||
|
mkdir -p /tmp/hdmistat
|
||||||
|
|
||||||
|
# Rsync repo to /tmp/hdmistat (excluding test directory)
|
||||||
|
echo -e "${YELLOW}Syncing repository to /tmp/hdmistat...${NC}"
|
||||||
|
rsync -av --progress \
|
||||||
|
--exclude='test/' \
|
||||||
|
--exclude='.git/' \
|
||||||
|
--exclude='*.qcow2' \
|
||||||
|
--exclude='*.iso' \
|
||||||
|
--exclude='*.img' \
|
||||||
|
--exclude='vendor/' \
|
||||||
|
"$REPO_ROOT/" /tmp/hdmistat/
|
||||||
|
|
||||||
|
# Change to the synced directory
|
||||||
|
cd /tmp/hdmistat
|
||||||
|
|
||||||
|
# Build fbhello
|
||||||
|
echo -e "${YELLOW}Building fbhello...${NC}"
|
||||||
|
cd cmd/fbhello
|
||||||
|
go build -v .
|
||||||
|
|
||||||
|
if [ ! -f ./fbhello ]; then
|
||||||
|
echo -e "${RED}Build failed: fbhello binary not found${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}Build successful!${NC}"
|
||||||
|
|
||||||
|
# Run fbhello
|
||||||
|
echo -e "${YELLOW}Running fbhello...${NC}"
|
||||||
|
echo -e "${YELLOW}Press Ctrl+C to exit${NC}"
|
||||||
|
|
||||||
|
# Try to run with framebuffer first, fall back to terminal if needed
|
||||||
|
if [ -e /dev/fb0 ] && [ -w /dev/fb0 ]; then
|
||||||
|
echo -e "${GREEN}Running with framebuffer display${NC}"
|
||||||
|
sudo ./fbhello
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}Running with terminal display${NC}"
|
||||||
|
./fbhello
|
||||||
|
fi
|
71
test/test-fbsimplestat.sh
Executable file
71
test/test-fbsimplestat.sh
Executable file
@ -0,0 +1,71 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test script for fbsimplestat in Linux VM
|
||||||
|
# Stops hdmistat service, syncs repo, builds and runs fbsimplestat
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Get the directory where this script is located
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Starting fbsimplestat test...${NC}"
|
||||||
|
|
||||||
|
# Stop hdmistat service if running
|
||||||
|
echo -e "${YELLOW}Checking hdmistat service...${NC}"
|
||||||
|
if systemctl is-active --quiet hdmistat 2>/dev/null; then
|
||||||
|
echo -e "${YELLOW}Stopping hdmistat service...${NC}"
|
||||||
|
sudo systemctl stop hdmistat
|
||||||
|
echo -e "${GREEN}hdmistat service stopped${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}hdmistat service not running${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create target directory
|
||||||
|
echo -e "${YELLOW}Creating target directory...${NC}"
|
||||||
|
sudo rm -rf /tmp/hdmistat
|
||||||
|
mkdir -p /tmp/hdmistat
|
||||||
|
|
||||||
|
# Rsync repo to /tmp/hdmistat (excluding test directory)
|
||||||
|
echo -e "${YELLOW}Syncing repository to /tmp/hdmistat...${NC}"
|
||||||
|
rsync -av --progress \
|
||||||
|
--exclude='test/' \
|
||||||
|
--exclude='.git/' \
|
||||||
|
--exclude='*.qcow2' \
|
||||||
|
--exclude='*.iso' \
|
||||||
|
--exclude='*.img' \
|
||||||
|
--exclude='vendor/' \
|
||||||
|
"$REPO_ROOT/" /tmp/hdmistat/
|
||||||
|
|
||||||
|
# Change to the synced directory
|
||||||
|
cd /tmp/hdmistat
|
||||||
|
|
||||||
|
# Build fbsimplestat
|
||||||
|
echo -e "${YELLOW}Building fbsimplestat...${NC}"
|
||||||
|
cd cmd/fbsimplestat
|
||||||
|
go build -v .
|
||||||
|
|
||||||
|
if [ ! -f ./fbsimplestat ]; then
|
||||||
|
echo -e "${RED}Build failed: fbsimplestat binary not found${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}Build successful!${NC}"
|
||||||
|
|
||||||
|
# Run fbsimplestat
|
||||||
|
echo -e "${YELLOW}Running fbsimplestat...${NC}"
|
||||||
|
echo -e "${YELLOW}Press Ctrl+C to exit${NC}"
|
||||||
|
|
||||||
|
# Try to run with framebuffer first, fall back to terminal if needed
|
||||||
|
if [ -e /dev/fb0 ] && [ -w /dev/fb0 ]; then
|
||||||
|
echo -e "${GREEN}Running with framebuffer display${NC}"
|
||||||
|
sudo ./fbsimplestat
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}Running with terminal display${NC}"
|
||||||
|
./fbsimplestat
|
||||||
|
fi
|
Loading…
Reference in New Issue
Block a user