Skip to content

Repository files navigation

RsyncGUI

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.

BuildmacOS 14.0+Swift 5.9Apple SiliconLicenseVersionTests


Features

FeatureDescription
100+ rsync flagsTabbed visual editor across 14 categories (Basic, Transfer, Preserve, Filters, Comparison, Bandwidth, Output, SSH, Ownership, Backup, Logging, Network, I/O, Checksum)
Real-time progressAnimated gradient progress circle with speed, ETA, file count, and current file display
Multi-source / multi-destinationFan-out (1:N), fan-in (N:1), and full mesh (N:N) sync modes with parallel or sequential execution
launchd schedulingNative macOS scheduling (hourly, daily, weekly, monthly, custom cron) that runs even when the app is closed
SSH remote syncPublic key authentication with Keychain credential storage, connection testing, and key path validation
iCloud Drive syncOne-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
Multi-model load balancingSpread AI work across every enabled, healthy model (local Ollama + MLX, frontier OpenRouter, optional Nova Gateway) with a least-busy policy and health-gated failover. Three independent toggles; Nova is never required
Describe-it-in-English rsyncType your intent in plain English and a balanced LLM proposes a concrete rsync command. It is shown for review and pre-fills the builder — it is never auto-executed
Desktop widgetWidgetKit extension (Small / Medium / Large) showing health score, last sync, next sync, and recent activity
Menu bar integrationStatus bar icon with quick job access and window toggle
Pre/post sync scriptsRun custom scripts with environment variables (JOB_NAME, JOB_STATUS, FILES_TRANSFERRED); only absolute paths accepted
Job dependenciesChain jobs with conditional execution and CryptoKit-based change detection to skip unchanged sources
Delta reportingStructured post-sync report of files added, modified, deleted, with byte-level statistics
Dry run modePreview all changes before execution
Nova API serverHTTP API on port 37424 (loopback only) for programmatic control

Architecture

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
Loading

Sync Process Flow

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

Multi-Model Load Balancing & Describe-it-in-English rsync

RsyncGUI ships the shared multi-model LLM load balancer. AI work (including the natural-language rsync assistant) is spread across every enabled, healthy model using a least-busy policy — the single-user version of how Nova's gateway balances load. Three independent toggles compose the pool, and each backend is health-gated so an unreachable one is simply skipped:

  • All local — every discovered Ollama model (/api/tags) plus locally-installed MLX models.
  • All frontier — OpenRouter models (bring-your-own-key, stored in the macOS Keychain).
  • Nova Gatewayoptional OpenAI-compatible backend at http://127.0.0.1:18792 (health on /v1/models). A failed health check just marks it unavailable; everything else keeps working.

Nova is never a hard requirement. With zero Nova the feature works on local models and/or an OpenRouter key alone. There is no dependency on Nova, PostgreSQL, or the gateway.

Describe it in English

The Job Editor's Basic tab has a "Describe it in English (AI)" field. Type an intent — for example "mirror Photos to the NAS, skip video files, delete extras on the destination" — and the balanced LLM returns a concrete rsync command. It is surfaced for review and, on an explicit click, pre-fills the command builder.

Safety: rsync is destructive (--delete), so the generated command is never run automatically. A pure, network-free validator (parseRsyncSuggestion) extracts only a valid rsync invocation and rejects everything else — shell chaining (;&&|), command substitution (`$()), redirection (><), non-rsync programs (rm, sudo, cp), and any program-executing rsync flag (-e, --rsh, --rsync-path) via a strict flag allow-list. If no backend is enabled the feature disables itself with a clear reason rather than failing.

graph TD
U["User intent (plain English)"] --> PB["RsyncPromptBuilder<br/>(pure, network-free)"]
PB --> LB["LLMLoadBalancerService"]
subgraph Pool["Enabled + health-gated pool"]
OL["Ollama (local)"]
MLX["MLX (local)"]
OR["OpenRouter (frontier)"]
NG["Nova Gateway (optional)"]
end
LB -->|"LoadBalancer.next()<br/>least-busy"| Pool
Pool -->|"raw LLM text"| PV["parseRsyncSuggestion()<br/>strict validator / sanitizer"]
PV -->|"rejected: injection / non-rsync"| X["Discarded, nothing shown"]
PV -->|"clean rsync only"| RC["RsyncCommand"]
RC --> RV["Review card (read-only)"]
RV -->|"explicit Apply"| CB["Command builder pre-filled"]
CB -.->|"user starts the job themselves"| RUN["rsync runs"]
Loading

Fixes in 1.7.4

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.


Installation

From DMG (recommended for most users)

  1. Download the latest .dmg from Releases.
  2. Open it and drag RsyncGUI into your Applications folder.
  3. Launch it from Applications. No sandbox — full file system access for unrestricted rsync operation.

See "RsyncGUI can't be opened because the developer cannot be verified"? That means you have a build that isn't yet Developer-ID-signed and notarized. To open it anyway:

  • macOS 14 and earlier: Control-click (right-click) the app → OpenOpen.
  • macOS 15 (Sequoia) / 26 and later: double-click it, dismiss the dialog, then open System Settings → Privacy & Security, scroll down, and click Open Anyway.
  • Or from Terminal: xattr -dr com.apple.quarantine "/Applications/RsyncGUI.app"

Notarized releases open with no prompt at all — maintainers, see RELEASE.md.

From Source

Requires Xcode 16 or later. RsyncGUI is pure Swift with no third-party package dependencies, so there is nothing extra to install:

git clone git@github.com:kochj23/RsyncGUI.git
cd RsyncGUI
open RsyncGUI.xcodeproj
# Build & run: Cmd+R

Requirements

RequirementMinimum
macOS14.0 (Sonoma)
ArchitectureUniversal (Apple Silicon + Intel)
rsyncBundled with macOS; Homebrew version also supported
AI features (optional)Ollama, MLX, TinyLLM, or any supported backend

Project Structure

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


Building

git clone https://github.com/kochj23/RsyncGUI.git
cd RsyncGUI
xcodebuild -project RsyncGUI.xcodeproj -scheme RsyncGUI -configuration Release build

Testing

xcodebuild -project RsyncGUI.xcodeproj -scheme RsyncGUI -destination 'platform=macOS'test

403 tests across 16 test classes:

Test ClassTestsCategory
RsyncOptionsTests57Unit -- argument generation for 100+ flags, archive mode, filter sanitization, Codable
ProgressParsingTests38Unit -- speed/time/bytes parsing, final stats extraction, to-check lines
SyncJobTests29Unit -- job init, sync modes, execution strategy, destination types, Codable
NovaAPITests29Functional -- API endpoint routing, response shapes, status payload
FrameTests28Frame -- app launch, view instantiation, widget data models
PathValidationTests26Security -- tilde expansion, iCloud validation, SMB/USB, traversal detection
SecurityTests25Security -- path traversal, credential exposure, filter sanitization, binary resolution
ScheduleConfigTests24Unit -- plist generation, RunAtLoad, idle config, XML injection prevention
CommandInjectionTests23Security -- SSH host/user validation, rsync-path sanitization, shell escaping
FunctionalFlowTests18Functional -- end-to-end job creation, edit, and execution flows
WidgetDataTests18Unit -- widget data encoding, App Group sync, timeline entries
DeltaReportTests17Unit -- report formatting, itemize parsing, mixed change detection
ExecutionHistoryTests17Unit -- history entries, transfer speed calculation, Codable
IntegrationTests15Integration -- rsync binary verification, local sync with temp dirs, dry run, delete, exclude
DependencyCheckTests15Unit -- satisfied/unsatisfied dependencies, missing jobs, parallel file splitting
DestinationEditingTests25Unit / Integration / Functional / Security / Performance / Retry / Frame -- destination path editing (issue #4) and member-wise Equatable regression lock

Nova API Server

Port 37424 (127.0.0.1 loopback only). No authentication required.

MethodPathDescription
GET/api/statusApp status, version, job count, uptime
GET/api/pingHealth check
GET/api/jobsList all sync jobs
GET/api/jobs/:idSingle job detail
POST/api/jobs/:id/runExecute a job
POST/api/jobs/:id/dryrunDry-run a job
GET/api/historyRecent execution history
GET/api/jobs/:id/historyHistory 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>/run

License

MIT License -- Copyright (c) 2026 Jordan Koch

See LICENSE for the full text.


Written by Jordan Koch

About

Professional rsync GUI for macOS with real-time progress visualization — modern RsyncOSX alternative built with SwiftUI

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages