Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99eeffcbe4 |
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# 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
|
## v0.1.0
|
||||||
|
|
||||||
Initial release.
|
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
|
## Features
|
||||||
|
|
||||||
- **Tray icon** with 5 states: idle (green), syncing (blue), paused (gray), error (red), disconnected (dark gray)
|
- **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
|
- **Transfer rates** in tooltip and menu
|
||||||
- **Context menu**: folder list, recent files, conflict counter, pause/resume, rescan, restart
|
- **Context menu**: folder list, recent files, conflict counter, pause/resume, rescan, restart
|
||||||
- **Embedded admin panel** using the system browser engine (Edge WebView2 / WebKit / WebKit2GTK)
|
- **Embedded admin panel** using the system browser engine (Edge WebView2 / WebKit / WebKit2GTK)
|
||||||
- **OS notifications** for sync complete, device connect/disconnect, new device requests, conflicts
|
- **OS notifications** for sync complete, device connect/disconnect, new device requests, conflicts
|
||||||
- **Settings** toggleable via menu checkboxes (persisted to JSON config)
|
- **Settings** toggleable via menu checkboxes (persisted to JSON config)
|
||||||
- **Auto-start Syncthing** with crash recovery
|
- **Auto-start Syncthing** with crash recovery and supervised restart
|
||||||
- **API key auto-discovery** from Syncthing's config.xml
|
- **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
|
## Architecture
|
||||||
|
|
||||||
| Binary | Purpose |
|
SyncWarden ships as three separate binaries to avoid main-thread conflicts between systray and webview:
|
||||||
|--------|---------|
|
|
||||||
| `syncwarden` | Tray icon, menu, API polling, notifications, process management |
|
|
||||||
| `syncwarden-panel` | Embedded browser showing Syncthing admin panel |
|
|
||||||
| `syncwarden-setup` | Cross-platform installer |
|
|
||||||
|
|
||||||
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
|
## Installation
|
||||||
|
|
||||||
### From release
|
### 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
|
### 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
|
```bash
|
||||||
# Tray binary (pure Go on Windows)
|
# Tray binary (pure Go, no CGO needed)
|
||||||
go build -o syncwarden ./cmd/syncwarden
|
go build -o syncwarden ./cmd/syncwarden
|
||||||
|
|
||||||
# Panel binary (requires CGO + C++ compiler)
|
# Panel binary (requires CGO + C++ compiler)
|
||||||
@@ -52,14 +171,49 @@ Config file location:
|
|||||||
- **Linux**: `~/.config/syncwarden/config.json`
|
- **Linux**: `~/.config/syncwarden/config.json`
|
||||||
- **macOS**: `~/Library/Application Support/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
|
## Dependencies
|
||||||
|
|
||||||
- [energye/systray](https://github.com/energye/systray) — System tray
|
### Tray binary (`syncwarden`)
|
||||||
- [webview/webview_go](https://github.com/webview/webview_go) — Embedded browser
|
|
||||||
- [fogleman/gg](https://github.com/fogleman/gg) — Icon rendering
|
| Dependency | Purpose |
|
||||||
- [gen2brain/beeep](https://github.com/gen2brain/beeep) — OS notifications
|
|------------|---------|
|
||||||
|
| [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
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.davoryn.de/calic/syncwarden/internal/config"
|
"git.davoryn.de/calic/syncwarden/internal/config"
|
||||||
|
st "git.davoryn.de/calic/syncwarden/internal/syncthing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -173,13 +174,29 @@ func runInstall() {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Launch SyncWarden: %s\n", trayPath)
|
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 {
|
if len(errors) > 0 {
|
||||||
showMessage("SyncWarden — Setup",
|
msg := "Setup completed with warnings:\n\n" + strings.Join(errors, "\n") +
|
||||||
"Setup completed with warnings:\n\n"+strings.Join(errors, "\n")+
|
"\n\nSuccessfully installed " + fmt.Sprint(installed) + " binaries to:\n" + dst
|
||||||
"\n\nSuccessfully installed "+fmt.Sprint(installed)+" binaries to:\n"+dst, true)
|
if !st.IsInstalled() {
|
||||||
|
msg += "\n\nNote: Syncthing is not installed.\nDownload it from: " + st.DownloadURL
|
||||||
|
}
|
||||||
|
showMessage("SyncWarden — Setup", msg, true)
|
||||||
} else {
|
} else {
|
||||||
showMessage("SyncWarden — Setup",
|
msg := "Setup complete!\n\nSyncWarden is running.\n\nIt will auto-discover your Syncthing API key."
|
||||||
"Setup complete!\n\nSyncWarden is running.\n\nIt will auto-discover your Syncthing API key.", false)
|
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)
|
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 {
|
func removeAutostartLinux() error {
|
||||||
desktopPath := filepath.Join(os.Getenv("HOME"), ".config", "autostart", "syncwarden.desktop")
|
desktopPath := filepath.Join(os.Getenv("HOME"), ".config", "autostart", "syncwarden.desktop")
|
||||||
if _, err := os.Stat(desktopPath); os.IsNotExist(err) {
|
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 = systray.AddMenuItem("Last sync: —", "")
|
||||||
a.lastSyncItem.Disable()
|
a.lastSyncItem.Disable()
|
||||||
|
|
||||||
|
if a.syncthingMissing {
|
||||||
|
mInstall := systray.AddMenuItem("Install Syncthing...", "Download Syncthing")
|
||||||
|
mInstall.Click(func() {
|
||||||
|
openBrowser(st.DownloadURL)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
systray.AddSeparator()
|
systray.AddSeparator()
|
||||||
|
|
||||||
// Open Admin Panel
|
// Open Admin Panel
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
st "git.davoryn.de/calic/syncwarden/internal/syncthing"
|
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.
|
// App manages the tray icon and Syncthing monitoring.
|
||||||
type App struct {
|
type App struct {
|
||||||
@@ -37,6 +37,8 @@ type App struct {
|
|||||||
conflictItem *systray.MenuItem
|
conflictItem *systray.MenuItem
|
||||||
folderItems []*systray.MenuItem
|
folderItems []*systray.MenuItem
|
||||||
recentItems []*systray.MenuItem
|
recentItems []*systray.MenuItem
|
||||||
|
|
||||||
|
syncthingMissing bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts the tray application (blocking).
|
// Run starts the tray application (blocking).
|
||||||
@@ -59,10 +61,20 @@ func (a *App) onReady() {
|
|||||||
|
|
||||||
a.client = st.NewClient(a.cfg.BaseURL(), a.cfg.SyncthingAPIKey)
|
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
|
// Set initial icon
|
||||||
a.setState(icons.StateDisconnected)
|
a.setState(icons.StateDisconnected)
|
||||||
systray.SetTitle("SyncWarden")
|
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
|
// Right-click shows menu
|
||||||
systray.SetOnRClick(func(menu systray.IMenu) {
|
systray.SetOnRClick(func(menu systray.IMenu) {
|
||||||
|
|||||||
Reference in New Issue
Block a user