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) {