hdmistat/internal/app/app.go
2025-07-24 14:32:50 +02:00

230 lines
4.9 KiB
Go

// Package app contains the main application logic for hdmistat
package app
import (
"context"
"log/slog"
"sync"
"time"
"git.eeqj.de/sneak/hdmistat/internal/config"
"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
log *slog.Logger
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
currentScreen int
rotationInterval time.Duration
updateInterval time.Duration
}
// Options contains all dependencies for the App
type Options struct {
fx.In
Lifecycle fx.Lifecycle
Display display.Display
Collector statcollector.Collector
Renderer *renderer.Renderer
Logger *slog.Logger
Context context.Context
Config *config.Config
}
// NewApp creates a new application instance
func NewApp(opts Options) *App {
app := &App{
display: opts.Display,
collector: opts.Collector,
renderer: opts.Renderer,
log: opts.Logger,
currentScreen: 0,
rotationInterval: opts.Config.GetRotationDuration(),
updateInterval: opts.Config.GetUpdateDuration(),
}
// Initialize screens
app.screens = []renderer.Screen{
renderer.NewStatusScreen(), // New status screen
renderer.NewOverviewScreen(), // Old overview screen
renderer.NewProcessScreenCPU(),
renderer.NewProcessScreenMemory(),
}
// Use the injected context, not the lifecycle context
app.ctx, app.cancel = context.WithCancel(opts.Context)
opts.Lifecycle.Append(fx.Hook{
OnStart: func(_ context.Context) error {
app.Start()
return nil
},
OnStop: func(_ context.Context) error {
return app.Stop()
},
})
return app
}
// Start begins the application main loop
func (a *App) Start() {
a.log.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.log.Info("stopping hdmistat app")
a.cancel()
a.wg.Wait()
// Clear display
if err := a.display.Clear(); err != nil {
a.log.Error("clearing display", "error", err)
}
// Close display
if err := a.display.Close(); err != nil {
a.log.Error("closing display", "error", err)
}
return nil
}
// updateLoop continuously updates the current screen
func (a *App) updateLoop() {
defer a.wg.Done()
defer func() {
a.log.Info("updateLoop exiting")
if r := recover(); r != nil {
a.log.Error("updateLoop panic", "error", r)
}
}()
a.log.Debug("updateLoop started")
// DISABLED FOR DEBUGGING - Only render once on screen switch
// ticker := time.NewTicker(a.updateInterval)
// defer ticker.Stop()
// Initial render
a.renderCurrentScreen()
// Just wait for context cancellation
a.log.Debug("updateLoop waiting for context cancellation")
<-a.ctx.Done()
a.log.Debug("updateLoop context cancelled")
/* COMMENTED OUT FOR DEBUGGING
for {
select {
case <-a.ctx.Done():
return
case <-ticker.C:
a.renderCurrentScreen()
}
}
*/
}
// rotationLoop rotates through screens
func (a *App) rotationLoop() {
defer a.wg.Done()
defer func() {
a.log.Info("rotationLoop exiting")
if r := recover(); r != nil {
a.log.Error("rotationLoop panic", "error", r)
}
}()
a.log.Debug("rotationLoop started", "interval", a.rotationInterval)
ticker := time.NewTicker(a.rotationInterval)
defer ticker.Stop()
for {
select {
case <-a.ctx.Done():
a.log.Debug("rotationLoop context cancelled")
return
case <-ticker.C:
a.log.Debug("rotationLoop ticker fired")
a.nextScreen()
}
}
}
// renderCurrentScreen renders and displays the current screen
func (a *App) renderCurrentScreen() {
if len(a.screens) == 0 {
return
}
// Get current screen
screen := a.screens[a.currentScreen]
a.log.Debug("rendering screen",
"index", a.currentScreen,
"name", screen.Name())
// Collect system info
info, err := a.collector.Collect()
if err != nil {
a.log.Error("collecting system info", "error", err)
return
}
// Render screen
img, err := a.renderer.RenderScreen(screen, info)
if err != nil {
a.log.Error("rendering screen",
"screen", screen.Name(),
"error", err)
return
}
// Display image
if err := a.display.Show(img); err != nil {
a.log.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.log.Info("switching screen",
"index", a.currentScreen,
"name", a.screens[a.currentScreen].Name())
// Render the new screen immediately
a.renderCurrentScreen()
}