Implement SyncWarden v0.1.0
Some checks failed
Release / build (push) Failing after 19s

Full Syncthing tray wrapper with:
- System tray with 5 icon states (idle/syncing/paused/error/disconnected)
- Syncthing REST API client with auto-discovered API key
- Long-polling event listener for real-time status
- Transfer rate monitoring, folder tracking, recent files, conflict counting
- Full context menu with folders, recent files, settings toggles
- Embedded admin panel binary (webview, requires CGO)
- OS notifications via beeep (per-event configurable)
- Syncthing process management with auto-restart
- Cross-platform installer with autostart
- CI pipeline for Linux (.deb + .tar.gz) and Windows (.zip)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Axel Meyer
2026-03-03 21:16:28 +01:00
parent 2256df9dd7
commit 34a1a94502
30 changed files with 2156 additions and 38 deletions

87
internal/tray/tooltip.go Normal file
View File

@@ -0,0 +1,87 @@
package tray
import (
"fmt"
"time"
"git.davoryn.de/calic/syncwarden/internal/icons"
"git.davoryn.de/calic/syncwarden/internal/monitor"
)
// formatTooltip generates the tooltip text from aggregate status.
func formatTooltip(s monitor.AggregateStatus, showRate bool) string {
stateStr := stateLabel(s.State)
tip := fmt.Sprintf("SyncWarden: %s", stateStr)
// Devices
tip += fmt.Sprintf(" | %d/%d devices", s.DevicesOnline, s.DevicesTotal)
// Transfer rate
if showRate && (s.DownRate > 0 || s.UpRate > 0) {
tip += fmt.Sprintf(" | ↓%s ↑%s", formatBytes(s.DownRate), formatBytes(s.UpRate))
}
// Last sync
if !s.LastSync.IsZero() {
tip += fmt.Sprintf(" | Last sync: %s", formatTimeAgo(s.LastSync))
}
return tip
}
func stateLabel(s icons.State) string {
switch s {
case icons.StateIdle:
return "Idle"
case icons.StateSyncing:
return "Syncing"
case icons.StatePaused:
return "Paused"
case icons.StateError:
return "Error"
case icons.StateDisconnected:
return "Disconnected"
default:
return "Unknown"
}
}
func formatBytes(bps float64) string {
if bps < 1024 {
return fmt.Sprintf("%.0f B/s", bps)
}
if bps < 1024*1024 {
return fmt.Sprintf("%.1f KB/s", bps/1024)
}
if bps < 1024*1024*1024 {
return fmt.Sprintf("%.1f MB/s", bps/(1024*1024))
}
return fmt.Sprintf("%.1f GB/s", bps/(1024*1024*1024))
}
func formatTimeAgo(t time.Time) string {
d := time.Since(t)
if d < time.Minute {
return "just now"
}
if d < time.Hour {
m := int(d.Minutes())
if m == 1 {
return "1 min ago"
}
return fmt.Sprintf("%d min ago", m)
}
if d < 24*time.Hour {
h := int(d.Hours())
if h == 1 {
return "1 hour ago"
}
return fmt.Sprintf("%d hours ago", h)
}
days := int(d.Hours()) / 24
if days == 1 {
return "1 day ago"
}
return fmt.Sprintf("%d days ago", days)
}