diff --git a/cmd/goose/main.go b/cmd/goose/main.go index 14bb076..29d5aff 100644 --- a/cmd/goose/main.go +++ b/cmd/goose/main.go @@ -132,6 +132,7 @@ type App struct { hideStaleIncoming bool loadingTurnData bool enableReminders bool // Whether to send daily reminder notifications + enableAudioCues bool // Whether to play audio cues for notifications } func main() { @@ -183,8 +184,12 @@ func main() { updateInterval: updateInterval, pendingTurnResults: make([]TurnResult, 0), enableReminders: true, + enableAudioCues: true, } + // Load saved settings + app.loadSettings() + log.Println("Initializing GitHub clients...") err = app.initClients(ctx) if err != nil { diff --git a/cmd/goose/settings.go b/cmd/goose/settings.go new file mode 100644 index 0000000..ebf06cb --- /dev/null +++ b/cmd/goose/settings.go @@ -0,0 +1,107 @@ +// Package main - settings.go provides persistent settings storage. +package main + +import ( + "encoding/json" + "log" + "os" + "path/filepath" +) + +// Settings represents persistent user settings. +type Settings struct { + EnableAudioCues bool `json:"enable_audio_cues"` + EnableReminders bool `json:"enable_reminders"` + HideStale bool `json:"hide_stale"` +} + +// getSettingsDir returns the configuration directory for settings. +func getSettingsDir() (string, error) { + configDir, err := os.UserConfigDir() + if err != nil { + return "", err + } + return filepath.Join(configDir, "ready-to-review"), nil +} + +// loadSettings loads settings from disk or returns defaults. +func (app *App) loadSettings() { + settingsDir, err := getSettingsDir() + if err != nil { + log.Printf("Failed to get settings directory: %v", err) + // Use defaults + app.enableAudioCues = true + app.enableReminders = true + app.hideStaleIncoming = true + return + } + + settingsPath := filepath.Join(settingsDir, "settings.json") + + data, err := os.ReadFile(settingsPath) + if err != nil { + if !os.IsNotExist(err) { + log.Printf("Failed to read settings: %v", err) + } + // Use defaults + app.enableAudioCues = true + app.enableReminders = true + app.hideStaleIncoming = true + return + } + + var settings Settings + if err := json.Unmarshal(data, &settings); err != nil { + log.Printf("Failed to parse settings: %v", err) + // Use defaults + app.enableAudioCues = true + app.enableReminders = true + app.hideStaleIncoming = true + return + } + + app.enableAudioCues = settings.EnableAudioCues + app.enableReminders = settings.EnableReminders + app.hideStaleIncoming = settings.HideStale + log.Printf("Loaded settings: audio_cues=%v, reminders=%v, hide_stale=%v", + app.enableAudioCues, app.enableReminders, app.hideStaleIncoming) +} + +// saveSettings saves current settings to disk. +func (app *App) saveSettings() { + settingsDir, err := getSettingsDir() + if err != nil { + log.Printf("Failed to get settings directory: %v", err) + return + } + + app.mu.RLock() + settings := Settings{ + EnableAudioCues: app.enableAudioCues, + EnableReminders: app.enableReminders, + HideStale: app.hideStaleIncoming, + } + app.mu.RUnlock() + + // Ensure directory exists + if err := os.MkdirAll(settingsDir, 0o700); err != nil { + log.Printf("Failed to create settings directory: %v", err) + return + } + + settingsPath := filepath.Join(settingsDir, "settings.json") + + data, err := json.MarshalIndent(settings, "", " ") + if err != nil { + log.Printf("Failed to marshal settings: %v", err) + return + } + + if err := os.WriteFile(settingsPath, data, 0o600); err != nil { + log.Printf("Failed to save settings: %v", err) + return + } + + log.Printf("Saved settings: audio_cues=%v, reminders=%v, hide_stale=%v", + settings.EnableAudioCues, settings.EnableReminders, settings.HideStale) +} diff --git a/cmd/goose/sound.go b/cmd/goose/sound.go index de3aeb8..e6f2b17 100644 --- a/cmd/goose/sound.go +++ b/cmd/goose/sound.go @@ -52,6 +52,16 @@ func (app *App) initSoundCache() { // playSound plays a cached sound file using platform-specific commands. func (app *App) playSound(ctx context.Context, soundType string) { + // Check if audio cues are enabled + app.mu.RLock() + audioEnabled := app.enableAudioCues + app.mu.RUnlock() + + if !audioEnabled { + log.Printf("[SOUND] Sound playback skipped (audio cues disabled): %s", soundType) + return + } + log.Printf("[SOUND] Playing %s sound", soundType) // Ensure sounds are cached app.initSoundCache() diff --git a/cmd/goose/ui.go b/cmd/goose/ui.go index dedc356..e6b489d 100644 --- a/cmd/goose/ui.go +++ b/cmd/goose/ui.go @@ -287,18 +287,24 @@ func (app *App) addStaticMenuItems(ctx context.Context) { hideStaleItem.Check() } hideStaleItem.Click(func() { + app.mu.Lock() app.hideStaleIncoming = !app.hideStaleIncoming - if app.hideStaleIncoming { + hideStale := app.hideStaleIncoming + // Clear menu state to force rebuild + app.lastMenuState = nil + app.mu.Unlock() + + if hideStale { hideStaleItem.Check() } else { hideStaleItem.Uncheck() } + + // Save settings to disk + app.saveSettings() + // Toggle hide stale PRs setting // Force menu rebuild since hideStaleIncoming changed - app.mu.Lock() - // Clear menu state to force rebuild - app.lastMenuState = nil - app.mu.Unlock() app.rebuildMenu(ctx) }) @@ -323,11 +329,40 @@ func (app *App) addStaticMenuItems(ctx context.Context) { reminderItem.Uncheck() log.Println("[SETTINGS] Daily reminders disabled") } + + // Save settings to disk + app.saveSettings() }) // Add login item option (macOS only) addLoginItemUI(ctx, app) + // Audio cues + // Add 'Audio cues' option + audioItem := systray.AddMenuItem("Audio cues", "Play sounds for notifications") + app.mu.RLock() + if app.enableAudioCues { + audioItem.Check() + } + app.mu.RUnlock() + audioItem.Click(func() { + app.mu.Lock() + app.enableAudioCues = !app.enableAudioCues + enabled := app.enableAudioCues + app.mu.Unlock() + + if enabled { + audioItem.Check() + log.Println("[SETTINGS] Audio cues enabled") + } else { + audioItem.Uncheck() + log.Println("[SETTINGS] Audio cues disabled") + } + + // Save settings to disk + app.saveSettings() + }) + // Quit // Add 'Quit' option quitItem := systray.AddMenuItem("Quit", "")