Detect missing Syncthing, rewrite README with architecture diagram
Some checks failed
Release / build (push) Failing after 2m53s
Some checks failed
Release / build (push) Failing after 2m53s
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>
This commit is contained in:
@@ -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.
|
||||
|
||||
192
README.md
192
README.md
@@ -1,41 +1,160 @@
|
||||
# SyncWarden
|
||||
<h1 align="center">
|
||||
<img src="assets/icon-128.png" width="80" alt="SyncWarden icon" /><br />
|
||||
SyncWarden
|
||||
</h1>
|
||||
|
||||
Lightweight system tray wrapper for [Syncthing](https://syncthing.net/). Cross-platform, native-feeling, ~10 MB.
|
||||
<p align="center">
|
||||
Lightweight system tray wrapper for <a href="https://syncthing.net/">Syncthing</a>. Cross-platform, native-feeling, ~10 MB.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go&logoColor=white" alt="Go 1.24+" />
|
||||
<img src="https://img.shields.io/badge/Platform-Windows%20|%20Linux%20|%20macOS-informational" alt="Platform: Windows | Linux | macOS" />
|
||||
<img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License" />
|
||||
<br />
|
||||
<a href="https://git.davoryn.de/calic/syncwarden/releases"><img src="https://img.shields.io/badge/release-v0.2.0-blue?logo=gitea&logoColor=white" alt="Latest Release" /></a>
|
||||
<a href="https://git.davoryn.de/calic/syncwarden/actions"><img src="https://img.shields.io/badge/CI-Gitea%20Actions-success?logo=gitea&logoColor=white" alt="CI" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/icon-preview.png" alt="Tray icon states: idle, syncing, paused, error, disconnected" />
|
||||
</p>
|
||||
|
||||
## 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<br/><small>long-poll /rest/events</small>"]
|
||||
poll["Health Poller<br/><small>every 3s</small>"]
|
||||
proc["Process Manager<br/><small>auto-start + restart</small>"]
|
||||
cfg["Config<br/><small>JSON file</small>"]
|
||||
end
|
||||
|
||||
panel_bin["syncwarden-panel<br/><small>(embedded browser)</small>"]
|
||||
setup_bin["syncwarden-setup<br/><small>(installer)</small>"]
|
||||
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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
23
internal/syncthing/detect.go
Normal file
23
internal/syncthing/detect.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
if a.syncthingMissing {
|
||||
systray.SetTooltip("SyncWarden: Syncthing not found")
|
||||
} else {
|
||||
systray.SetTooltip("SyncWarden: connecting...")
|
||||
}
|
||||
|
||||
// Right-click shows menu
|
||||
systray.SetOnRClick(func(menu systray.IMenu) {
|
||||
|
||||
Reference in New Issue
Block a user