Features: - Beautiful system statistics display using IBM Plex Mono font - Direct framebuffer rendering without X11/Wayland - Multiple screens with automatic carousel rotation - Real-time system monitoring (CPU, memory, disk, network, processes) - Systemd service integration with install command - Clean architecture using uber/fx dependency injection Architecture: - Cobra CLI with daemon, install, status, and info commands - Modular design with separate packages for display, rendering, and stats - Font embedding for zero runtime dependencies - Layout API for clean text rendering - Support for multiple screen types (overview, top CPU, top memory) Technical details: - Uses gopsutil for cross-platform system stats collection - Direct Linux framebuffer access via memory mapping - Anti-aliased text rendering with freetype - Configurable screen rotation and update intervals - Structured logging with slog - Comprehensive test coverage and linting setup This initial version provides a solid foundation for displaying rich system information on resource-constrained devices like Raspberry Pis.
208 lines
4.0 KiB
Go
208 lines
4.0 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/hdmistat/internal/display"
|
|
"git.eeqj.de/sneak/hdmistat/internal/renderer"
|
|
"git.eeqj.de/sneak/hdmistat/internal/statcollector"
|
|
"go.uber.org/fx"
|
|
)
|
|
|
|
// App is the main application
|
|
type App struct {
|
|
display display.Display
|
|
collector statcollector.Collector
|
|
renderer *renderer.Renderer
|
|
screens []renderer.Screen
|
|
logger *slog.Logger
|
|
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
wg sync.WaitGroup
|
|
|
|
currentScreen int
|
|
rotationInterval time.Duration
|
|
updateInterval time.Duration
|
|
}
|
|
|
|
// Config holds application configuration
|
|
type Config struct {
|
|
RotationInterval time.Duration
|
|
UpdateInterval time.Duration
|
|
}
|
|
|
|
// AppOptions contains all dependencies for the App
|
|
type AppOptions struct {
|
|
fx.In
|
|
|
|
Lifecycle fx.Lifecycle
|
|
Display display.Display
|
|
Collector statcollector.Collector
|
|
Renderer *renderer.Renderer
|
|
Logger *slog.Logger
|
|
Context context.Context
|
|
Config *Config `optional:"true"`
|
|
}
|
|
|
|
// NewApp creates a new application instance
|
|
func NewApp(opts AppOptions) *App {
|
|
config := &Config{
|
|
RotationInterval: 10 * time.Second,
|
|
UpdateInterval: 1 * time.Second,
|
|
}
|
|
|
|
if opts.Config != nil {
|
|
config = opts.Config
|
|
}
|
|
|
|
app := &App{
|
|
display: opts.Display,
|
|
collector: opts.Collector,
|
|
renderer: opts.Renderer,
|
|
logger: opts.Logger,
|
|
currentScreen: 0,
|
|
rotationInterval: config.RotationInterval,
|
|
updateInterval: config.UpdateInterval,
|
|
}
|
|
|
|
// Initialize screens
|
|
app.screens = []renderer.Screen{
|
|
renderer.NewOverviewScreen(),
|
|
renderer.NewProcessScreenCPU(),
|
|
renderer.NewProcessScreenMemory(),
|
|
}
|
|
|
|
opts.Lifecycle.Append(fx.Hook{
|
|
OnStart: func(ctx context.Context) error {
|
|
app.ctx, app.cancel = context.WithCancel(ctx)
|
|
app.Start()
|
|
return nil
|
|
},
|
|
OnStop: func(ctx context.Context) error {
|
|
return app.Stop()
|
|
},
|
|
})
|
|
|
|
return app
|
|
}
|
|
|
|
// Start begins the application main loop
|
|
func (a *App) Start() {
|
|
a.logger.Info("starting hdmistat app",
|
|
"screens", len(a.screens),
|
|
"rotation_interval", a.rotationInterval,
|
|
"update_interval", a.updateInterval)
|
|
|
|
// Start update loop
|
|
a.wg.Add(1)
|
|
go a.updateLoop()
|
|
|
|
// Start rotation loop
|
|
a.wg.Add(1)
|
|
go a.rotationLoop()
|
|
}
|
|
|
|
// Stop stops the application
|
|
func (a *App) Stop() error {
|
|
a.logger.Info("stopping hdmistat app")
|
|
|
|
a.cancel()
|
|
a.wg.Wait()
|
|
|
|
// Clear display
|
|
if err := a.display.Clear(); err != nil {
|
|
a.logger.Error("clearing display", "error", err)
|
|
}
|
|
|
|
// Close display
|
|
if err := a.display.Close(); err != nil {
|
|
a.logger.Error("closing display", "error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// updateLoop continuously updates the current screen
|
|
func (a *App) updateLoop() {
|
|
defer a.wg.Done()
|
|
|
|
ticker := time.NewTicker(a.updateInterval)
|
|
defer ticker.Stop()
|
|
|
|
// Initial render
|
|
a.renderCurrentScreen()
|
|
|
|
for {
|
|
select {
|
|
case <-a.ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
a.renderCurrentScreen()
|
|
}
|
|
}
|
|
}
|
|
|
|
// rotationLoop rotates through screens
|
|
func (a *App) rotationLoop() {
|
|
defer a.wg.Done()
|
|
|
|
ticker := time.NewTicker(a.rotationInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-a.ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
a.nextScreen()
|
|
}
|
|
}
|
|
}
|
|
|
|
// renderCurrentScreen renders and displays the current screen
|
|
func (a *App) renderCurrentScreen() {
|
|
if len(a.screens) == 0 {
|
|
return
|
|
}
|
|
|
|
// Collect system info
|
|
info, err := a.collector.Collect()
|
|
if err != nil {
|
|
a.logger.Error("collecting system info", "error", err)
|
|
return
|
|
}
|
|
|
|
// Get current screen
|
|
screen := a.screens[a.currentScreen]
|
|
|
|
// Render screen
|
|
img, err := a.renderer.RenderScreen(screen, info)
|
|
if err != nil {
|
|
a.logger.Error("rendering screen",
|
|
"screen", screen.Name(),
|
|
"error", err)
|
|
return
|
|
}
|
|
|
|
// Display image
|
|
if err := a.display.Show(img); err != nil {
|
|
a.logger.Error("displaying image", "error", err)
|
|
}
|
|
}
|
|
|
|
// nextScreen advances to the next screen
|
|
func (a *App) nextScreen() {
|
|
if len(a.screens) == 0 {
|
|
return
|
|
}
|
|
|
|
a.currentScreen = (a.currentScreen + 1) % len(a.screens)
|
|
a.logger.Info("switching screen",
|
|
"index", a.currentScreen,
|
|
"name", a.screens[a.currentScreen].Name())
|
|
}
|