A professional macOS GUI for rsync with real-time progress, AI-powered insights, launchd scheduling, multi-destination sync, desktop widgets, and a local API server.
| Feature | Description |
|---|---|
| 100+ rsync flags | Tabbed visual editor across 14 categories (Basic, Transfer, Preserve, Filters, Comparison, Bandwidth, Output, SSH, Ownership, Backup, Logging, Network, I/O, Checksum) |
| Real-time progress | Animated gradient progress circle with speed, ETA, file count, and current file display |
| Multi-source / multi-destination | Fan-out (1:N), fan-in (N:1), and full mesh (N:N) sync modes with parallel or sequential execution |
| launchd scheduling | Native macOS scheduling (hourly, daily, weekly, monthly, custom cron) that runs even when the app is closed |
| SSH remote sync | Public key authentication with Keychain credential storage, connection testing, and key path validation |
| iCloud Drive sync | One-click iCloud destination setup with automatic .icloud placeholder exclusion |
| AI insights (10 features) | Error diagnosis, change summary, anomaly detection, smart scheduling, storage prediction, exclusion suggestions, NLP job creation, health scoring, recovery assistant, sensitive file detection |
| Desktop widget | WidgetKit extension (Small / Medium / Large) showing health score, last sync, next sync, and recent activity |
| Menu bar integration | Status bar icon with quick job access and window toggle |
| Pre/post sync scripts | Run custom scripts with environment variables (JOB_NAME, JOB_STATUS, FILES_TRANSFERRED); only absolute paths accepted |
| Job dependencies | Chain jobs with conditional execution and CryptoKit-based change detection to skip unchanged sources |
| Delta reporting | Structured post-sync report of files added, modified, deleted, with byte-level statistics |
| Dry run mode | Preview all changes before execution |
| Nova API server | HTTP API on port 37424 (loopback only) for programmatic control |
graph TD
subgraph UI["SwiftUI Frontend"]
CV[ContentView] --> JL[JobListView]
CV --> JD[JobDetailView]
CV --> JE[JobEditorView]
JE -->|"Browse / iCloud / type path"| SDP["SyncJob.setDestinationPath(id:path:)"]
SDP -->|"member-wise Equatable change"| JE
CV --> SP[SyncProgressView]
CV --> AI[AIInsightsView]
CV --> HT[JobHistoryTabView]
CV --> SV[SettingsView]
MB[MenuBarManager] --> CV
end
subgraph Services["Service Layer"]
JM[JobManager] --> RE[RsyncExecutor]
JM --> AES[AdvancedExecutionService]
JM --> SM[ScheduleManager]
RE -->|Process.arguments| RSYNC["/usr/bin/rsync"]
RE -->|stdout parsing| PP[Progress Parser]
AES -->|parallel / sequential| RE
AES -->|change detection| CD[CryptoKit Checksums]
SM -->|generate plist| LA["~/Library/LaunchAgents/"]
AIS[AIInsightsService] --> ABM[AIBackendManager]
ABM --> OLLAMA["Ollama / MLX / TinyLLM"]
WDS[WidgetDataSync] -->|App Group| WK[WidgetKit Extension]
end
subgraph Data["Persistence"]
JOBS["jobs.json"]
HIST["ExecutionHistory"]
DR["DeltaReport"]
end
subgraph API["Nova API Server :37424"]
STATUS["GET /api/status"]
GETJOBS["GET /api/jobs"]
RUN["POST /api/jobs/:id/run"]
DRYRUN["POST /api/jobs/:id/dryrun"]
HISTORY["GET /api/history"]
end
UI --> Services
JE -->|"Save: addJob / updateJob → saveJobs()"| JM
JM --> JOBS
RE --> HIST
RE --> DR
API --> JM
PP --> SP
HIST --> WDS
AIS --> HIST
sequenceDiagram
participant User
participant UI as SwiftUI
participant JM as JobManager
participant AES as AdvancedExecutionService
participant RE as RsyncExecutor
participant RS as /usr/bin/rsync
participant AI as AIInsightsService
participant WK as WidgetKit
User->>UI: Click "Run Job"
UI->>JM: executeJob(id)
JM->>JM: Load SyncJob from jobs.json
JM->>JM: Check dependencies (CryptoKit hash)
alt Multi-destination (N:N)
JM->>AES: executeFanOut(job, destinations)
AES->>RE: spawn parallel/sequential tasks
else Single destination
JM->>RE: execute(job)
end
RE->>RE: Build rsync arguments (100+ flags)
RE->>RE: Run pre-sync script (if configured)
RE->>RS: Process.launch(arguments)
loop stdout lines
RS-->>RE: progress output (% complete, speed, file)
RE-->>UI: Update SyncProgressView (circle, ETA, speed)
end
RS-->>RE: Exit code + final stats
RE->>RE: Parse DeltaReport (added/modified/deleted)
RE->>RE: Run post-sync script (if configured)
RE->>JM: Save ExecutionHistory entry
JM->>AI: Analyze results (anomalies, health score)
AI-->>JM: AI insights (errors, suggestions)
JM->>WK: Sync latest stats via App Group
JM-->>UI: Job complete (success/failure)
Destination path editing (issue #4). Browsing to a destination and clicking Select, or typing a destination path by hand, could appear to do nothing — the Destination Path would not populate or would not be saved.
Root cause: SyncDestination overrode equality (==) to compare only its id.
SwiftUI relies on Equatable for change detection, so editing the path of an
existing destination (unchanged id) was seen as "no change" and the view never
refreshed — the edit looked lost. Three earlier attempts patched the picker
plumbing but left this id-only equality in place, which is why it kept
regressing.
The fix:
- Removed the custom
==so Swift synthesizes correct member-wise equality — a path change is now a real change that SwiftUI observes. - Extracted the path-setting logic into a pure, unit-tested helper,
SyncJob.setDestinationPath(id:path:), that the editor calls for Browse, iCloud, and manual entry. The behavior is now covered by a regression suite (DestinationEditingTests) spanning unit, integration, functional, security, performance, retry, and frame categories — including a test that locks in the equality fix so this cannot silently regress again.
A related destination-picker fix already shipped in 1.7.3, so updating from 1.6.0 resolves the reported behavior.
- Download the latest DMG from Releases
- Open the DMG and drag RsyncGUI.app to
/Applications - No sandbox -- full file system access for unrestricted rsync operation
| Requirement | Minimum |
|---|---|
| macOS | 14.0 (Sonoma) |
| Architecture | Universal (Apple Silicon + Intel) |
| rsync | Bundled with macOS; Homebrew version also supported |
| AI features (optional) | Ollama, MLX, TinyLLM, or any supported backend |
RsyncGUI/
|-- RsyncGUI/
| |-- RsyncGUIApp.swift App entry point, window management
| |-- NovaAPIServer.swift HTTP API server (port 37424, loopback)
| |-- Info.plist Bundle configuration
| |-- RsyncGUI.entitlements Sandbox disabled, full disk access
| |-- Design/
| | +-- ModernDesign.swift Glassmorphic theme, colors, card styles
| |-- Models/
| | |-- SyncJob.swift Job model (sources, destinations, modes, flags)
| | |-- RsyncOptions.swift 100+ rsync flags across 14 categories
| | |-- ScheduleConfig.swift launchd plist generation (cron, interval, idle)
| | |-- ExecutionHistory.swift Run history with speed, duration, byte stats
| | |-- DeltaReport.swift Post-sync change report (add/modify/delete)
| | |-- ParallelismConfig.swift Fan-out/fan-in/mesh execution strategies
| | +-- ConnectionTest.swift SSH connection validation model
| |-- Views/
| | |-- ContentView.swift Root navigation (sidebar + detail)
| | |-- JobListView.swift Job list with search and status indicators
| | |-- JobDetailView.swift Single job overview (last run, next run, health)
| | |-- JobEditorView.swift 14-tab rsync flag editor
| | |-- SyncProgressView.swift Animated progress circle with live stats
| | |-- DeltaReportView.swift Post-sync file change browser
| | |-- JobHistoryTabView.swift Execution history timeline
| | |-- ExecutionHistoryView.swift Single execution detail
| | |-- AIInsightsView.swift AI analysis panel (10 insight types)
| | |-- SettingsView.swift App preferences, AI backend config
| | +-- TestProgressView.swift Progress parser debug view
| +-- Services/
| |-- JobManager.swift Job CRUD, persistence, dependency resolution
| |-- RsyncExecutor.swift Process spawning, stdout parsing, progress
| |-- AdvancedExecutionService.swift Multi-dest orchestration, change detection
| |-- ScheduleManager.swift launchd plist install/uninstall/list
| |-- AIInsightsService.swift AI prompt construction and response parsing
| |-- AIBackendManager.swift Backend discovery, health check, routing
| |-- AIBackendManager+Enhanced.swift Extended AI capabilities
| |-- AIBackendStatusMenu.swift AI backend status indicator
| |-- MenuBarManager.swift NSStatusItem menu bar integration
| +-- WidgetDataSync.swift App Group data sharing with widget
|
|-- RsyncGUI Widget/
| |-- RsyncGUIWidget.swift WidgetKit timeline provider (S/M/L)
| |-- WidgetData.swift Shared data models for widget
| |-- SharedDataManager.swift App Group read/write
| +-- Info.plist
|
|-- RsyncGUITests/ 379 tests across 15 files
| |-- RsyncOptionsTests.swift Flag generation, archive mode, sanitization
| |-- ProgressParsingTests.swift Speed/time/bytes parsing
| |-- SyncJobTests.swift Job model, sync modes, Codable
| |-- NovaAPITests.swift API routing, response shapes
| |-- FrameTests.swift App launch, view instantiation
| |-- PathValidationTests.swift Path security, traversal detection
| |-- SecurityTests.swift Credential exposure, sanitization
| |-- ScheduleConfigTests.swift Plist generation, XML injection
| |-- CommandInjectionTests.swift SSH/rsync-path sanitization
| |-- FunctionalFlowTests.swift End-to-end job flows
| |-- WidgetDataTests.swift Widget data encoding
| |-- DeltaReportTests.swift Change report parsing
| |-- ExecutionHistoryTests.swift History entries, Codable
| |-- IntegrationTests.swift Real rsync execution
| +-- DependencyCheckTests.swift Job dependency logic
|
+-- RsyncGUI.xcodeproj/
2 targets | 25 Swift source files | 379 tests | Zero external dependencies
git clone https://github.com/kochj23/RsyncGUI.git
cd RsyncGUI
xcodebuild -project RsyncGUI.xcodeproj -scheme RsyncGUI -configuration Release buildxcodebuild -project RsyncGUI.xcodeproj -scheme RsyncGUI -destination 'platform=macOS' test403 tests across 16 test classes:
| Test Class | Tests | Category |
|---|---|---|
| RsyncOptionsTests | 57 | Unit -- argument generation for 100+ flags, archive mode, filter sanitization, Codable |
| ProgressParsingTests | 38 | Unit -- speed/time/bytes parsing, final stats extraction, to-check lines |
| SyncJobTests | 29 | Unit -- job init, sync modes, execution strategy, destination types, Codable |
| NovaAPITests | 29 | Functional -- API endpoint routing, response shapes, status payload |
| FrameTests | 28 | Frame -- app launch, view instantiation, widget data models |
| PathValidationTests | 26 | Security -- tilde expansion, iCloud validation, SMB/USB, traversal detection |
| SecurityTests | 25 | Security -- path traversal, credential exposure, filter sanitization, binary resolution |
| ScheduleConfigTests | 24 | Unit -- plist generation, RunAtLoad, idle config, XML injection prevention |
| CommandInjectionTests | 23 | Security -- SSH host/user validation, rsync-path sanitization, shell escaping |
| FunctionalFlowTests | 18 | Functional -- end-to-end job creation, edit, and execution flows |
| WidgetDataTests | 18 | Unit -- widget data encoding, App Group sync, timeline entries |
| DeltaReportTests | 17 | Unit -- report formatting, itemize parsing, mixed change detection |
| ExecutionHistoryTests | 17 | Unit -- history entries, transfer speed calculation, Codable |
| IntegrationTests | 15 | Integration -- rsync binary verification, local sync with temp dirs, dry run, delete, exclude |
| DependencyCheckTests | 15 | Unit -- satisfied/unsatisfied dependencies, missing jobs, parallel file splitting |
| DestinationEditingTests | 25 | Unit / Integration / Functional / Security / Performance / Retry / Frame -- destination path editing (issue #4) and member-wise Equatable regression lock |
Port 37424 (127.0.0.1 loopback only). No authentication required.
| Method | Path | Description |
|---|---|---|
GET |
/api/status |
App status, version, job count, uptime |
GET |
/api/ping |
Health check |
GET |
/api/jobs |
List all sync jobs |
GET |
/api/jobs/:id |
Single job detail |
POST |
/api/jobs/:id/run |
Execute a job |
POST |
/api/jobs/:id/dryrun |
Dry-run a job |
GET |
/api/history |
Recent execution history |
GET |
/api/jobs/:id/history |
History for a specific job |
curl -s http://127.0.0.1:37424/api/status | python3 -m json.tool
curl -X POST http://127.0.0.1:37424/api/jobs/<uuid>/runMIT License -- Copyright (c) 2026 Jordan Koch
See LICENSE for the full text.
Written by Jordan Koch