diff --git a/internal/config/load.go b/internal/config/load.go index 575908f..f38f510 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -28,26 +28,40 @@ func GetConfigPath() (string, error) { } func Load() (*Config, error) { - configPath, err := GetConfigPath() + config, legacy, err := LoadOrLegacy() if err != nil { 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) { - return nil, fmt.Errorf("%w: run hyprvoice onboarding", ErrConfigNotFound) + return nil, false, fmt.Errorf("%w: run hyprvoice onboarding", ErrConfigNotFound) } 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) var config Config meta, err := toml.DecodeFile(configPath, &config) 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) { 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 { @@ -58,7 +72,7 @@ func Load() (*Config, error) { config.applyThreadsDefault() log.Printf("Config: configuration loaded successfully") - return &config, nil + return &config, false, nil } func isLegacyConfig(meta toml.MetaData, config *Config) bool { diff --git a/internal/config/manager.go b/internal/config/manager.go index 93c2e30..6cd587e 100644 --- a/internal/config/manager.go +++ b/internal/config/manager.go @@ -22,25 +22,33 @@ type Manager struct { debounceTimer *time.Timer debounceMutex sync.Mutex debounceDelay time.Duration + + // legacy tracks if config is in legacy format (needs onboarding) + legacy bool } func NewManager() (*Manager, error) { log.Printf("Config manager: initializing configuration system...") - config, err := Load() + config, legacy, err := LoadOrLegacy() if err != nil { log.Printf("Config manager: failed to load initial configuration: %v", err) return nil, err } - log.Printf("Config manager: validating initial configuration...") - if err := config.Validate(); err != nil { - log.Printf("Config manager: validation warning: %v", err) + if legacy { + log.Printf("Config manager: legacy config detected, daemon will prompt for onboarding") + } else { + log.Printf("Config manager: validating initial configuration...") + if err := config.Validate(); err != nil { + log.Printf("Config manager: validation warning: %v", err) + } } m := &Manager{ config: config, debounceDelay: 500 * time.Millisecond, // 500ms debounce delay + legacy: legacy, } log.Printf("Config manager: initialization completed successfully") @@ -56,6 +64,13 @@ func (m *Manager) GetConfig() *Config { 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 { configPath, err := GetConfigPath() if err != nil { @@ -136,12 +151,17 @@ func (m *Manager) watchLoop(ctx context.Context, configPath string) { func (m *Manager) reloadConfig() { log.Printf("Config manager: starting configuration reload...") - newConfig, err := Load() + newConfig, legacy, err := LoadOrLegacy() if err != nil { log.Printf("Config manager: failed to reload config: %v", err) return } + if legacy { + log.Printf("Config manager: config still in legacy format, skipping reload") + return + } + log.Printf("Config manager: validating new configuration...") if err := newConfig.Validate(); err != nil { log.Printf("Config manager: invalid config after reload: %v", err) @@ -150,6 +170,7 @@ func (m *Manager) reloadConfig() { m.mu.Lock() m.config = newConfig + m.legacy = false // clear legacy flag on successful reload onConfigReload := m.onConfigReload m.mu.Unlock() diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index d93799c..4b9e8e5 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -39,8 +39,14 @@ func New() (*Daemon, error) { conf := configMgr.GetConfig() 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{ - notifier: notify.NewNotifier(conf.Notifications.Type, conf.Notifications.Messages.Resolve()), + notifier: notify.NewNotifier(notifType, conf.Notifications.Messages.Resolve()), configMgr: configMgr, ctx: ctx, cancel: cancel, @@ -178,6 +184,10 @@ func (d *Daemon) handle(c net.Conn) { } func (d *Daemon) toggle() { + if d.configMgr.IsLegacy() { + d.notifier.Error("Legacy config detected. Run: hyprvoice onboarding") + return + } conf := d.configMgr.GetConfig() switch d.status() { case pipeline.Idle: