Major changes: - Converted all cobra commands from global variables to CLI struct methods - Eliminated global logger variable in favor of dependency injection - Fixed all errcheck linter issues by properly handling errors - Fixed Makefile to check formatting instead of modifying files - Integrated smartconfig library for configuration management - Added CLAUDE.md with project-specific development guidelines Key improvements: - All commands (daemon, install, status, info) now use CLI struct methods - Logger is injected as dependency through fx providers - Proper error handling for all DrawText and file.Close() calls - Configuration loading now uses smartconfig with proper defaults - Fixed formatting check in Makefile (make test no longer modifies files) Technical details: - Created CLI struct with log field (renamed from logger per request) - All command constructors return *cobra.Command from CLI methods - Config package uses smartconfig.NewFromAppName() correctly - Fixed all critical errcheck issues throughout the codebase - Maintained backward compatibility with existing functionality All tests passing, code formatted, and ready for deployment.
194 lines
3.8 KiB
Go
194 lines
3.8 KiB
Go
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
|
|
logger *slog.Logger
|
|
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
wg sync.WaitGroup
|
|
|
|
currentScreen int
|
|
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.Config
|
|
}
|
|
|
|
// NewApp creates a new application instance
|
|
func NewApp(opts AppOptions) *App {
|
|
app := &App{
|
|
display: opts.Display,
|
|
collector: opts.Collector,
|
|
renderer: opts.Renderer,
|
|
logger: opts.Logger,
|
|
currentScreen: 0,
|
|
rotationInterval: opts.Config.GetRotationDuration(),
|
|
updateInterval: opts.Config.GetUpdateDuration(),
|
|
}
|
|
|
|
// 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())
|
|
}
|