hdmistat/internal/app/app.go
sneak 2f8256b310 Refactor hdmistat to use dependency injection and fix linter issues
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.
2025-07-23 15:01:51 +02:00

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