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