From 99eeffcbe4c7015c3707181f99ffc268531379e9 Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Wed, 4 Mar 2026 00:08:34 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 7 ++ README.md | 192 +++++++++++++++++++++++++++++++---- cmd/setup/main.go | 40 +++++++- internal/syncthing/detect.go | 23 +++++ internal/tray/menu.go | 7 ++ internal/tray/tray.go | 16 ++- 6 files changed, 259 insertions(+), 26 deletions(-) create mode 100644 internal/syncthing/detect.go diff --git a/CHANGELOG.md b/CHANGELOG.md index bc997cd..d4f6874 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v0.2.0 + +- Detect missing Syncthing installation (checks PATH and config file) +- Tray: show "Install Syncthing..." menu item with link to download page when not found +- Setup: print download note and open browser when Syncthing is not installed +- Rewrite README with architecture diagram, dependency breakdown, and badges + ## v0.1.0 Initial release. diff --git a/README.md b/README.md index 7c62a63..3ea7a0f 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,160 @@ -# SyncWarden +

+ SyncWarden icon
+ SyncWarden +

-Lightweight system tray wrapper for [Syncthing](https://syncthing.net/). Cross-platform, native-feeling, ~10 MB. +

+ Lightweight system tray wrapper for Syncthing. Cross-platform, native-feeling, ~10 MB. +

+ +

+ Go 1.24+ + Platform: Windows | Linux | macOS + MIT License +
+ Latest Release + CI +

+ +

+ Tray icon states: idle, syncing, paused, error, disconnected +

## Features - **Tray icon** with 5 states: idle (green), syncing (blue), paused (gray), error (red), disconnected (dark gray) -- **Real-time monitoring** via Syncthing event API (long-polling) +- **Real-time monitoring** via Syncthing event API (long-polling) + periodic health checks - **Transfer rates** in tooltip and menu - **Context menu**: folder list, recent files, conflict counter, pause/resume, rescan, restart - **Embedded admin panel** using the system browser engine (Edge WebView2 / WebKit / WebKit2GTK) - **OS notifications** for sync complete, device connect/disconnect, new device requests, conflicts - **Settings** toggleable via menu checkboxes (persisted to JSON config) -- **Auto-start Syncthing** with crash recovery -- **API key auto-discovery** from Syncthing's config.xml +- **Auto-start Syncthing** with crash recovery and supervised restart +- **API key auto-discovery** from Syncthing's `config.xml` +- **Syncthing detection** — prompts with download link if Syncthing is not installed + +## How It Works + +SyncWarden sits in the system tray and communicates with a local Syncthing instance via its REST API. It never touches your files directly — all sync operations are handled by Syncthing itself. + +```mermaid +graph TB + subgraph OS["Operating System"] + tray["System Tray Icon"] + notif["OS Notifications"] + browser["Default Browser"] + fm["File Manager"] + end + + subgraph SW["SyncWarden"] + subgraph tray_bin["syncwarden (tray binary)"] + app["Tray App"] + menu["Context Menu"] + mon["Monitor"] + evl["Event Listener
long-poll /rest/events"] + poll["Health Poller
every 3s"] + proc["Process Manager
auto-start + restart"] + cfg["Config
JSON file"] + end + + panel_bin["syncwarden-panel
(embedded browser)"] + setup_bin["syncwarden-setup
(installer)"] + end + + subgraph ST["Syncthing (localhost:8384)"] + api["REST API"] + webui["Web UI"] + sync["Sync Engine"] + end + + remote["Remote Devices"] + + %% Tray app connections + app --> tray + app --> notif + app --> menu + menu -- "Open folder" --> fm + menu -- "Open Admin Panel" --> panel_bin + panel_bin -- "renders" --> webui + menu -- "Install Syncthing..." --> browser + + %% Monitor connections + app --> mon + mon --> evl + mon --> poll + evl -- "events" --> api + poll -- "health + connections" --> api + menu -- "pause / rescan / restart" --> api + + %% Process management + app --> proc + proc -- "start / stop / supervise" --> ST + + %% Config + app --> cfg + + %% Syncthing + sync <--> remote + + %% Styling + classDef swbin fill:#2d5a27,stroke:#4a8,color:#fff + classDef stbox fill:#1a4a6a,stroke:#3a8abf,color:#fff + classDef osbox fill:#555,stroke:#888,color:#fff + + class app,menu,mon,evl,poll,proc,cfg swbin + class panel_bin,setup_bin swbin + class api,webui,sync stbox + class tray,notif,browser,fm osbox +``` + +### Data Flow + +1. **Event Listener** long-polls `GET /rest/events` — receives real-time sync events (file changes, device connections, folder state changes) +2. **Health Poller** checks `/rest/noauth/health` and `/rest/system/connections` every 3 seconds — detects connection loss and calculates transfer rates +3. **Monitor** aggregates both streams into a single status snapshot (icon state, device count, rates, recent files, conflicts) +4. **Tray App** renders the status as an icon + tooltip + menu updates, and dispatches OS notifications for configured events ## Architecture -| Binary | Purpose | -|--------|---------| -| `syncwarden` | Tray icon, menu, API polling, notifications, process management | -| `syncwarden-panel` | Embedded browser showing Syncthing admin panel | -| `syncwarden-setup` | Cross-platform installer | +SyncWarden ships as three separate binaries to avoid main-thread conflicts between systray and webview: -Separate binaries avoid main-thread conflicts between systray and webview. +| Binary | Purpose | CGO | +|--------|---------|-----| +| `syncwarden` | Tray icon, menu, monitoring, notifications, process management | No | +| `syncwarden-panel` | Embedded browser showing Syncthing admin UI | Yes | +| `syncwarden-setup` | Cross-platform installer / uninstaller | No | + +### Project Layout + +``` +cmd/ + syncwarden/ Entry point for tray binary + panel/ Entry point for embedded browser panel + setup/ Cross-platform installer + icongen/ Icon generation utility (dev tool) +internal/ + config/ Config persistence + platform-specific paths + icons/ Pre-rendered tray icons (5 states, PNG + ICO) + monitor/ State aggregation: folders, speed, recent files, conflicts + notify/ OS notification wrapper + syncthing/ REST API client, event listener, process manager, detection + tray/ Tray UI: menu, tooltip, panel launcher +``` ## Installation ### From release -Download the latest release for your platform and run `syncwarden-setup`. +Download the latest release for your platform from [Releases](https://git.davoryn.de/calic/syncwarden/releases) and run `syncwarden-setup`. + +If Syncthing is not installed, the setup will open the [download page](https://syncthing.net/downloads/) for you. ### From source -Requires Go 1.24+ and CGO (MinGW-w64 on Windows for the panel binary). +Requires Go 1.24+ and CGO (MinGW-w64 on Windows) for the panel binary. ```bash -# Tray binary (pure Go on Windows) +# Tray binary (pure Go, no CGO needed) go build -o syncwarden ./cmd/syncwarden # Panel binary (requires CGO + C++ compiler) @@ -52,14 +171,49 @@ Config file location: - **Linux**: `~/.config/syncwarden/config.json` - **macOS**: `~/Library/Application Support/syncwarden/config.json` -The API key is auto-discovered from Syncthing's config.xml on first run. All settings are configurable via the tray menu. +The API key is auto-discovered from Syncthing's `config.xml` on first run. All settings are configurable via the tray menu. ## Dependencies -- [energye/systray](https://github.com/energye/systray) — System tray -- [webview/webview_go](https://github.com/webview/webview_go) — Embedded browser -- [fogleman/gg](https://github.com/fogleman/gg) — Icon rendering -- [gen2brain/beeep](https://github.com/gen2brain/beeep) — OS notifications +### Tray binary (`syncwarden`) + +| Dependency | Purpose | +|------------|---------| +| [energye/systray](https://github.com/energye/systray) | System tray integration (icon, menu, click events) | +| [fogleman/gg](https://github.com/fogleman/gg) | 2D graphics for icon rendering | +| [gen2brain/beeep](https://github.com/gen2brain/beeep) | Cross-platform OS notifications | + +### Panel binary (`syncwarden-panel`) + +| Dependency | Purpose | +|------------|---------| +| [webview/webview_go](https://github.com/webview/webview_go) | Embedded browser (Edge WebView2 / WebKit / WebKit2GTK) | + +### Setup binary (`syncwarden-setup`) + +No external dependencies — uses only the Go standard library and `internal/config` for path resolution. + +### Build-time only + +| Tool | Purpose | +|------|---------| +| [goreleaser/nfpm](https://github.com/goreleaser/nfpm) | `.deb` package generation (CI only) | + +## Syncthing REST API Usage + +SyncWarden communicates with Syncthing via its local REST API. All requests go to `localhost:8384` (configurable) and are authenticated with the `X-API-Key` header. + +| Endpoint | Purpose | +|----------|---------| +| `GET /rest/noauth/health` | Connection health check (no auth) | +| `GET /rest/system/connections` | Device status + byte counters for rate calculation | +| `GET /rest/config` | Folder and device configuration | +| `GET /rest/db/status` | Per-folder sync state | +| `GET /rest/events` | Long-poll event stream (30s timeout) | +| `POST /rest/system/pause` | Pause all syncing | +| `POST /rest/system/resume` | Resume all syncing | +| `POST /rest/db/scan` | Trigger folder rescan | +| `POST /rest/system/restart` | Restart Syncthing | ## License diff --git a/cmd/setup/main.go b/cmd/setup/main.go index cd401a4..605ddda 100644 --- a/cmd/setup/main.go +++ b/cmd/setup/main.go @@ -11,6 +11,7 @@ import ( "strings" "git.davoryn.de/calic/syncwarden/internal/config" + st "git.davoryn.de/calic/syncwarden/internal/syncthing" ) func main() { @@ -173,13 +174,29 @@ func runInstall() { fmt.Println() fmt.Printf("Launch SyncWarden: %s\n", trayPath) + // Check if Syncthing is installed + if !st.IsInstalled() { + fmt.Println() + fmt.Println("Note: Syncthing is not installed.") + fmt.Printf("Download it from: %s\n", st.DownloadURL) + if isInteractive() { + openURL(st.DownloadURL) + } + } + if len(errors) > 0 { - showMessage("SyncWarden — Setup", - "Setup completed with warnings:\n\n"+strings.Join(errors, "\n")+ - "\n\nSuccessfully installed "+fmt.Sprint(installed)+" binaries to:\n"+dst, true) + msg := "Setup completed with warnings:\n\n" + strings.Join(errors, "\n") + + "\n\nSuccessfully installed " + fmt.Sprint(installed) + " binaries to:\n" + dst + if !st.IsInstalled() { + msg += "\n\nNote: Syncthing is not installed.\nDownload it from: " + st.DownloadURL + } + showMessage("SyncWarden — Setup", msg, true) } else { - showMessage("SyncWarden — Setup", - "Setup complete!\n\nSyncWarden is running.\n\nIt will auto-discover your Syncthing API key.", false) + msg := "Setup complete!\n\nSyncWarden is running.\n\nIt will auto-discover your Syncthing API key." + if !st.IsInstalled() { + msg += "\n\nNote: Syncthing is not installed.\nDownload it from: " + st.DownloadURL + } + showMessage("SyncWarden — Setup", msg, false) } } @@ -328,6 +345,19 @@ func removeAutostartWindows() error { return os.Remove(lnkPath) } +func openURL(url string) { + var cmd *exec.Cmd + switch runtime.GOOS { + case "windows": + cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) + case "darwin": + cmd = exec.Command("open", url) + default: + cmd = exec.Command("xdg-open", url) + } + _ = cmd.Start() +} + func removeAutostartLinux() error { desktopPath := filepath.Join(os.Getenv("HOME"), ".config", "autostart", "syncwarden.desktop") if _, err := os.Stat(desktopPath); os.IsNotExist(err) { diff --git a/internal/syncthing/detect.go b/internal/syncthing/detect.go new file mode 100644 index 0000000..7486633 --- /dev/null +++ b/internal/syncthing/detect.go @@ -0,0 +1,23 @@ +package syncthing + +import ( + "os" + "os/exec" + + "git.davoryn.de/calic/syncwarden/internal/config" +) + +// DownloadURL is the official Syncthing downloads page. +const DownloadURL = "https://syncthing.net/downloads/" + +// IsInstalled returns true if Syncthing appears to be installed. +// It checks both PATH and the existence of a Syncthing config file. +func IsInstalled() bool { + if _, err := exec.LookPath("syncthing"); err == nil { + return true + } + if _, err := os.Stat(config.SyncthingConfigPath()); err == nil { + return true + } + return false +} diff --git a/internal/tray/menu.go b/internal/tray/menu.go index 4c1afaf..941766c 100644 --- a/internal/tray/menu.go +++ b/internal/tray/menu.go @@ -25,6 +25,13 @@ func (a *App) buildMenu() { 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 diff --git a/internal/tray/tray.go b/internal/tray/tray.go index 1c54610..898bafd 100644 --- a/internal/tray/tray.go +++ b/internal/tray/tray.go @@ -14,7 +14,7 @@ import ( st "git.davoryn.de/calic/syncwarden/internal/syncthing" ) -const version = "0.1.0" +const version = "0.2.0" // App manages the tray icon and Syncthing monitoring. type App struct { @@ -37,6 +37,8 @@ type App struct { conflictItem *systray.MenuItem folderItems []*systray.MenuItem recentItems []*systray.MenuItem + + syncthingMissing bool } // Run starts the tray application (blocking). @@ -59,10 +61,20 @@ func (a *App) onReady() { a.client = st.NewClient(a.cfg.BaseURL(), a.cfg.SyncthingAPIKey) + // Check if Syncthing is installed + if !st.IsInstalled() { + a.syncthingMissing = true + log.Println("Syncthing not found on this system") + } + // Set initial icon a.setState(icons.StateDisconnected) systray.SetTitle("SyncWarden") - systray.SetTooltip("SyncWarden: connecting...") + if a.syncthingMissing { + systray.SetTooltip("SyncWarden: Syncthing not found") + } else { + systray.SetTooltip("SyncWarden: connecting...") + } // Right-click shows menu systray.SetOnRClick(func(menu systray.IMenu) {