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
+
-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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
## 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) {