gracefully handle legacy config: notify instead of crashing

This commit is contained in:
leonardotrapani
2026-02-02 11:24:56 +01:00
parent 0981317aa2
commit 76b9f08a4d
3 changed files with 57 additions and 12 deletions
+20 -6
View File
@@ -28,26 +28,40 @@ func GetConfigPath() (string, error) {
} }
func Load() (*Config, error) { func Load() (*Config, error) {
configPath, err := GetConfigPath() config, legacy, err := LoadOrLegacy()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if legacy {
log.Printf("Config: legacy configuration detected - run hyprvoice onboarding")
return nil, fmt.Errorf("%w: run hyprvoice onboarding", ErrConfigNotFound)
}
return config, nil
}
// LoadOrLegacy loads config and returns (config, isLegacy, error).
// If config is legacy, returns default config with isLegacy=true instead of error.
func LoadOrLegacy() (*Config, bool, error) {
configPath, err := GetConfigPath()
if err != nil {
return nil, false, err
}
if _, err := os.Stat(configPath); os.IsNotExist(err) { if _, err := os.Stat(configPath); os.IsNotExist(err) {
return nil, fmt.Errorf("%w: run hyprvoice onboarding", ErrConfigNotFound) return nil, false, fmt.Errorf("%w: run hyprvoice onboarding", ErrConfigNotFound)
} else if err != nil { } else if err != nil {
return nil, fmt.Errorf("failed to stat config file %s: %w", configPath, err) return nil, false, fmt.Errorf("failed to stat config file %s: %w", configPath, err)
} }
log.Printf("Config: loading configuration from %s", configPath) log.Printf("Config: loading configuration from %s", configPath)
var config Config var config Config
meta, err := toml.DecodeFile(configPath, &config) meta, err := toml.DecodeFile(configPath, &config)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse config file %s: %w", configPath, err) return nil, false, fmt.Errorf("failed to parse config file %s: %w", configPath, err)
} }
if isLegacyConfig(meta, &config) { if isLegacyConfig(meta, &config) {
log.Printf("Config: legacy configuration detected - run hyprvoice onboarding") log.Printf("Config: legacy configuration detected - run hyprvoice onboarding")
return nil, fmt.Errorf("%w: run hyprvoice onboarding", ErrConfigNotFound) return DefaultConfig(), true, nil
} }
if config.Providers == nil { if config.Providers == nil {
@@ -58,7 +72,7 @@ func Load() (*Config, error) {
config.applyThreadsDefault() config.applyThreadsDefault()
log.Printf("Config: configuration loaded successfully") log.Printf("Config: configuration loaded successfully")
return &config, nil return &config, false, nil
} }
func isLegacyConfig(meta toml.MetaData, config *Config) bool { func isLegacyConfig(meta toml.MetaData, config *Config) bool {
+26 -5
View File
@@ -22,25 +22,33 @@ type Manager struct {
debounceTimer *time.Timer debounceTimer *time.Timer
debounceMutex sync.Mutex debounceMutex sync.Mutex
debounceDelay time.Duration debounceDelay time.Duration
// legacy tracks if config is in legacy format (needs onboarding)
legacy bool
} }
func NewManager() (*Manager, error) { func NewManager() (*Manager, error) {
log.Printf("Config manager: initializing configuration system...") log.Printf("Config manager: initializing configuration system...")
config, err := Load() config, legacy, err := LoadOrLegacy()
if err != nil { if err != nil {
log.Printf("Config manager: failed to load initial configuration: %v", err) log.Printf("Config manager: failed to load initial configuration: %v", err)
return nil, err return nil, err
} }
log.Printf("Config manager: validating initial configuration...") if legacy {
if err := config.Validate(); err != nil { log.Printf("Config manager: legacy config detected, daemon will prompt for onboarding")
log.Printf("Config manager: validation warning: %v", err) } else {
log.Printf("Config manager: validating initial configuration...")
if err := config.Validate(); err != nil {
log.Printf("Config manager: validation warning: %v", err)
}
} }
m := &Manager{ m := &Manager{
config: config, config: config,
debounceDelay: 500 * time.Millisecond, // 500ms debounce delay debounceDelay: 500 * time.Millisecond, // 500ms debounce delay
legacy: legacy,
} }
log.Printf("Config manager: initialization completed successfully") log.Printf("Config manager: initialization completed successfully")
@@ -56,6 +64,13 @@ func (m *Manager) GetConfig() *Config {
return &configCopy return &configCopy
} }
// IsLegacy returns true if the config is in legacy format and needs onboarding
func (m *Manager) IsLegacy() bool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.legacy
}
func (m *Manager) StartWatching(ctx context.Context) error { func (m *Manager) StartWatching(ctx context.Context) error {
configPath, err := GetConfigPath() configPath, err := GetConfigPath()
if err != nil { if err != nil {
@@ -136,12 +151,17 @@ func (m *Manager) watchLoop(ctx context.Context, configPath string) {
func (m *Manager) reloadConfig() { func (m *Manager) reloadConfig() {
log.Printf("Config manager: starting configuration reload...") log.Printf("Config manager: starting configuration reload...")
newConfig, err := Load() newConfig, legacy, err := LoadOrLegacy()
if err != nil { if err != nil {
log.Printf("Config manager: failed to reload config: %v", err) log.Printf("Config manager: failed to reload config: %v", err)
return return
} }
if legacy {
log.Printf("Config manager: config still in legacy format, skipping reload")
return
}
log.Printf("Config manager: validating new configuration...") log.Printf("Config manager: validating new configuration...")
if err := newConfig.Validate(); err != nil { if err := newConfig.Validate(); err != nil {
log.Printf("Config manager: invalid config after reload: %v", err) log.Printf("Config manager: invalid config after reload: %v", err)
@@ -150,6 +170,7 @@ func (m *Manager) reloadConfig() {
m.mu.Lock() m.mu.Lock()
m.config = newConfig m.config = newConfig
m.legacy = false // clear legacy flag on successful reload
onConfigReload := m.onConfigReload onConfigReload := m.onConfigReload
m.mu.Unlock() m.mu.Unlock()
+11 -1
View File
@@ -39,8 +39,14 @@ func New() (*Daemon, error) {
conf := configMgr.GetConfig() conf := configMgr.GetConfig()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
// force desktop notifications when legacy config so user sees the onboarding prompt
notifType := conf.Notifications.Type
if configMgr.IsLegacy() {
notifType = "desktop"
}
d := &Daemon{ d := &Daemon{
notifier: notify.NewNotifier(conf.Notifications.Type, conf.Notifications.Messages.Resolve()), notifier: notify.NewNotifier(notifType, conf.Notifications.Messages.Resolve()),
configMgr: configMgr, configMgr: configMgr,
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
@@ -178,6 +184,10 @@ func (d *Daemon) handle(c net.Conn) {
} }
func (d *Daemon) toggle() { func (d *Daemon) toggle() {
if d.configMgr.IsLegacy() {
d.notifier.Error("Legacy config detected. Run: hyprvoice onboarding")
return
}
conf := d.configMgr.GetConfig() conf := d.configMgr.GetConfig()
switch d.status() { switch d.status() {
case pipeline.Idle: case pipeline.Idle: