Files
syncwarden/internal/tray/menu.go
Axel Meyer 99eeffcbe4
Some checks failed
Release / build (push) Failing after 2m53s
Detect missing Syncthing, rewrite README with architecture diagram
Add Syncthing installation detection (PATH + config file check) to both
the tray app and setup installer. When missing, the tray shows an
"Install Syncthing..." menu item and the setup opens the download page.

Rewrite README with Mermaid topology graph, per-binary dependency tables,
project layout, API endpoint reference, and shields.io badges.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:08:34 +01:00

233 lines
5.5 KiB
Go

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