package tray import ( "fmt" "log" "github.com/energye/systray" "git.davoryn.de/calic/syncwarden/internal/config" st "git.davoryn.de/calic/syncwarden/internal/syncthing" ) // buildMenu creates the full context menu. func (a *App) buildMenu() { // Status info section a.statusItem = systray.AddMenuItem("Status: Connecting...", "") a.statusItem.Disable() a.rateItem = systray.AddMenuItem("↓ 0 B/s ↑ 0 B/s", "") a.rateItem.Disable() a.devicesItem = systray.AddMenuItem("Devices: —", "") a.devicesItem.Disable() a.lastSyncItem = systray.AddMenuItem("Last sync: —", "") a.lastSyncItem.Disable() if a.syncthingMissing { mInstall := systray.AddMenuItem("Install Syncthing...", "Download Syncthing") mInstall.Click(func() { openBrowser(st.DownloadURL) }) } systray.AddSeparator() // Open Admin Panel mOpenPanel := systray.AddMenuItem("Open Admin Panel", "Open Syncthing admin panel") mOpenPanel.Click(func() { a.openPanel() }) // Pause/Resume toggle a.pauseItem = systray.AddMenuItem("Pause All", "Pause/Resume all syncing") a.pauseItem.Click(func() { a.togglePause() }) systray.AddSeparator() // Folders submenu a.foldersMenu = systray.AddMenuItem("Folders", "") emptyFolder := a.foldersMenu.AddSubMenuItem("(loading...)", "") emptyFolder.Disable() a.folderItems = []*systray.MenuItem{emptyFolder} // Recent Files submenu a.recentMenu = systray.AddMenuItem("Recent Files", "") emptyRecent := a.recentMenu.AddSubMenuItem("(none)", "") emptyRecent.Disable() a.recentItems = []*systray.MenuItem{emptyRecent} // Conflicts a.conflictItem = systray.AddMenuItem("Conflicts (0)", "Open conflicts page") a.conflictItem.Hide() a.conflictItem.Click(func() { a.openConflicts() }) systray.AddSeparator() // Actions mRescan := systray.AddMenuItem("Rescan All", "Trigger rescan of all folders") mRescan.Click(func() { go func() { if err := a.client.RescanAll(); err != nil { log.Printf("rescan error: %v", err) } }() }) mRestart := systray.AddMenuItem("Restart Syncthing", "Restart the Syncthing process") mRestart.Click(func() { go func() { if err := a.client.Restart(); err != nil { log.Printf("restart error: %v", err) } }() }) systray.AddSeparator() // Settings submenu mSettings := systray.AddMenuItem("Settings", "") chkNotify := mSettings.AddSubMenuItem("Notifications", "Enable/disable notifications") if a.cfg.EnableNotifications { chkNotify.Check() } chkNotify.Click(func() { a.toggleSetting(&a.cfg.EnableNotifications, chkNotify) }) chkRecent := mSettings.AddSubMenuItem("Show Recent Files", "Show recently synced files") if a.cfg.EnableRecentFiles { chkRecent.Check() } chkRecent.Click(func() { a.toggleSetting(&a.cfg.EnableRecentFiles, chkRecent) }) chkConflict := mSettings.AddSubMenuItem("Conflict Alerts", "Alert on sync conflicts") if a.cfg.EnableConflictAlerts { chkConflict.Check() } chkConflict.Click(func() { a.toggleSetting(&a.cfg.EnableConflictAlerts, chkConflict) }) chkRate := mSettings.AddSubMenuItem("Transfer Rate in Tooltip", "Show transfer rate in tooltip") if a.cfg.EnableTransferRate { chkRate.Check() } chkRate.Click(func() { a.toggleSetting(&a.cfg.EnableTransferRate, chkRate) }) chkAutoStart := mSettings.AddSubMenuItem("Auto-start Syncthing", "Start Syncthing when SyncWarden starts") if a.cfg.AutoStartSyncthing { chkAutoStart.Check() } chkAutoStart.Click(func() { a.toggleSetting(&a.cfg.AutoStartSyncthing, chkAutoStart) }) chkLogin := mSettings.AddSubMenuItem("Start on Login", "Start SyncWarden at system login") if a.cfg.StartOnLogin { chkLogin.Check() } chkLogin.Click(func() { a.toggleSetting(&a.cfg.StartOnLogin, chkLogin) }) // API key info apiKeyDisplay := "API Key: (none)" if len(a.cfg.SyncthingAPIKey) > 8 { apiKeyDisplay = fmt.Sprintf("API Key: %s...%s", a.cfg.SyncthingAPIKey[:4], a.cfg.SyncthingAPIKey[len(a.cfg.SyncthingAPIKey)-4:]) } else if a.cfg.SyncthingAPIKey != "" { apiKeyDisplay = fmt.Sprintf("API Key: %s", a.cfg.SyncthingAPIKey) } mAPIKey := mSettings.AddSubMenuItem(apiKeyDisplay, "") mAPIKey.Disable() mRediscover := mSettings.AddSubMenuItem("Re-discover API Key", "Re-read API key from Syncthing config") mRediscover.Click(func() { go a.rediscoverAPIKey() }) mAddr := mSettings.AddSubMenuItem(fmt.Sprintf("Address: %s", a.cfg.SyncthingAddress), "") mAddr.Disable() // About mAbout := systray.AddMenuItem(fmt.Sprintf("About (v%s)", version), "") mAbout.Disable() systray.AddSeparator() // Quit mQuit := systray.AddMenuItem("Quit", "Exit SyncWarden") mQuit.Click(func() { log.Println("Quit clicked") systray.Quit() }) } func (a *App) togglePause() { a.mu.Lock() paused := a.lastStatus.Paused a.mu.Unlock() go func() { var err error if paused { err = a.client.ResumeAll() } else { err = a.client.PauseAll() } if err != nil { log.Printf("pause/resume error: %v", err) } }() } func (a *App) openConflicts() { // Open the Syncthing conflicts page in the panel launchPanel(a.cfg.BaseURL()) } func (a *App) toggleSetting(field *bool, item *systray.MenuItem) { a.mu.Lock() *field = !*field val := *field cfg := a.cfg a.mu.Unlock() if val { item.Check() } else { item.Uncheck() } _ = config.Save(cfg) } func (a *App) rediscoverAPIKey() { key, err := st.DiscoverAPIKey() if err != nil { log.Printf("API key discovery failed: %v", err) return } if key == "" { log.Println("no API key found in Syncthing config") return } a.mu.Lock() a.cfg.SyncthingAPIKey = key a.mu.Unlock() a.client.SetAPIKey(key) _ = config.Save(a.cfg) log.Printf("re-discovered API key") }