// 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() }