This is the multi-page printable view of this section. Click here to print.
Blog
Version 2.8.5
Version 2.8.5 - In development
This version introduces several modifications. Additionally, I anticipate the implementation of minor updates and bug fixes, as well as a quality assurance and review process of changes. A release candidate is scheduled for early January 2026, followed by a final release in January 2026.
The primary advancement in this version is the implementation of significant user interface (UI) enhancements. These updates were requested by GitHub user @timreichen (Tim Reichen) to enhance the intuitiveness of RsyncUI. Tim Reichen’s input is greatly appreciated.
The UI improvements utilize the Inspector view to provide detailed information about tasks. Adding new tasks is now facilitated through a dedicated sheet. Selecting a task displays its associated details. Additionally, a toggle on the toolbar allows users to switch between viewing task data and parameters.
The detail views are implemented by utilizing SwiftUI Inspector view.


Version 2.8.4
Version 2.8.4 - Dec 26, 2025
Version 2.8.4rc2 has been released as the new version, with no new builds released.
In version 2.8.2, there is an issue with the “empty stats file”. For more information, please refer to last on this page. I believe I have identified the cause of this issue. The previous RsyncProcess utilize AsyncSequence (AsyncStream), which is part of version 2.8.2, a feature that continuously listens for data. A few days ago, I modified parts of the new RsyncProcessStreaming package to try it as well. AsyncSequence is a robust concurrency feature, but attempting to use it in the new package caused the “empty stats file” issue again.
Version, 2.8.4 incorporates the version of the RsyncProcessStreaming package that eliminates the occurrence of the “empty stats file”.
I am uncertain as to why the AsyncStream encounters issues with the aforementioned problem. Nevertheless, I intend to investigate and determine if I can resolve this issue for my own learning and interest. Specifically, I will implement a mechanism to halt the process and drain any remaining data before the termination task is executed.
Major Features & Improvements
- Total Commits (git) Since 2.8.2: 137+
- Development Period: December 12-24, 2025
🔄 RsyncProcessStreaming Migration - Complete
- Unified Process Execution Model: All process execution now uses the
RsyncProcessStreamingpackage for event-driven, streaming process handling - Simplified Handler Management: Introduced
ProcessHandlersfactory with built-in cleanup hooks to prevent memory leaks and retain cycles - Real-time Output Streaming: Process output is now streamed in real-time, enabling immediate UI updates and better user feedback
- Files Updated: Estimate.swift, Execute.swift, RestoreTableView.swift, OneTaskDetailsView.swift, ExecutePushPullView.swift, VerifyTasks.swift
📦 ParseRsyncOutput Swift Package Integration
- Extracted rsync output parsing into a reusable Swift Package for improved code reusability and testability
- Unified parsing logic across all rsync output processing operations
- Package repository: ParseRsyncOutput
✅ Comprehensive Testing Framework
- New RsyncUITests target added with initial test suite
- UI tests for arguments and deeplink URLs
- Output processing tests covering:
- Tail trimming (20-line limit)
- Directory line filtering
- Error detection patterns
- Malformed/empty output edge cases
Core Changes
Process Execution & Streaming
- Refactored
ObservableRestoreto useRsyncProcessStreaming - Refactored task verification to use streaming process handlers
- Added streaming support to QuicktaskView
- Refactored rsync version check to use streaming process
- Remove RsyncProcess dependency - replaced with RsyncProcessStreaming
- Streaming handler cleanup refactoring to prevent retain cycles and memory leaks
UI/UX Enhancements
- Improved UI feedback for task progress and completion
- Enhanced progress view with better styling and layout
- Added configuration table view to ProfileView
- Renamed table column from ‘Time’ to ‘Time last’ across multiple views
- Refactored profile deletion UI in ProfileView
- Improved help text for –delete parameter usage
- Better handling of hiddenID in Estimate and Execute operations
- Removed RsyncRealtimeView and related obsolete UI references
Code Quality & Refactoring
- Refactor optional handling patterns with guard-chain flattening and single binding for counts
- Refactor enum cases to camelCase: RsyncCommand, PushPullCommand, OtherRsyncCommand, PlanSnapshots, DayOfWeek
- Variable naming improvements for consistency and clarity across codebase
- Removed unnecessary whitespace and improved code formatting
- SwiftLint configuration updates and inline rule directives
- Refactored UI views for improved readability (AddTaskView, SidebarMainView, TasksView, QuicktaskView)
- Refactored parameter field handling in configuration models with optional fields
Configuration & Settings
- New argument validation feature: Added toggle for argument validation in execution flow
- Added ‘validate arguments’ setting to user configuration
- Improved argument validation with nil checks and proper error handling
- Add dry-run validation to argument checks
- Configuration model improvements with optional parameter handling
Logging & Debugging
- Refactored and cleaned up logging and test code
- Removed rsync output logfile support from log views
- Gated per-line streaming logs behind DEBUG flag
- Added debug threading check for streaming handlers
- Silenced debug print statements in streaming handlers
- Improved logging to use
errorMessageOnlywith better formatting
Dependencies & Version Updates
- Updated all Swift package dependencies to main branch
Bug Fixes & Stability
- Fixed guard statement order and typo in snapshot and restore operations
- Safely unwrap streamingHandlers before use
- Remove unnecessary
[weak self]in processTermination handlers - Made parameter4 optional to better handle delete flag
- Fix potential nil issues in argument execution
Architecture Improvements
- Unified streaming architecture with simplified process lifecycle
- Improved memory management through explicit cleanup patterns
- Enhanced testability with extracted ParseRsyncOutput package
- Better separation of concerns through handler factory pattern
Dependencies Updated
- RsyncProcessStreaming (streaming process execution)
- ParseRsyncOutput (output parsing and processing)
- RsyncArguments (rsync command arguments)
- RsyncProcess (legacy - being phased out in favor of streaming)
Empty stats file
The process termination signal indicates that the external rsync process has completed and the process has been terminated. All tasks within the main synchronize view are updated with the latest run, but there is also a separate logging that records the main result of each task with a timestamp.
Occasionally, when synchronizing a small amount of data, the termination signal is detected before all output from rsync has been drained. In such cases, the separate logging may be missing. The process termination signal serves as a message to perform logging, but if the last summarized rsync output is missing, there is nothing to log.
Output from rsync refers to the information that rsync provides to the terminal during the execution of a task.
If RsyncUI detects an empty statistics file, it will append a default log record stating 0 files : 0.00 MB in 0.00 seconds.
If logging fails due to the aforementioned reason, this error will be generated, if enabled.

Version 2.8.2
Version 2.8.2 (build 173) - Dec 14, 2025
This version has undergone numerous linting updates. Additionally, a bug related to enabling or disabling the —delete parameter has been resolved. Furthermore, a user settings flag for enabling or disabling the Silence missing stats parameter, which generates an error if the summarized stats file is empty.
As part of my ongoing experimentation with Visual Studio Code (VSC) and the GitHub MCP services for direct integration with GitHub Copilot (AI), several code updates have been made. The majority of these updates are attributed to the implementation of additional SwiftLint rules. VSC has proven to be an effective tool for identifying and resolving linting issues and updating code.
From this version, VSC has become an integral part of my development tools for the RsyncUI project. While Xcode 26 remains my primary development environment, all release builds, including notifications and signing, are executed via command line and a Makefile.
The primary supporting tools include:
- SwiftLint: Utilized for code quality assurance.
- SwiftFormat: Employed for code formatting.
- periphery: Utilized to eliminate unused code.
All of these tools are widely recognized and utilized by numerous developers.
The periphery application is specifically designed to eliminate unused code. SwiftFormat is utilized for code formatting, while SwiftLint ensures code quality. All instructions for SwiftFormat within the code have been removed, and both tools now share a single configuration file containing their respective instructions. This integration has proven to be highly effective.
Verify remote
Overview
This post describes how I use the Verify remote function.
The Remote Setup
I back up my bird photography to multiple locations:
- a Raspberry Pi 5 server configured with two WD Red SA500 2.5" SSD 1TB drives set up as a mirrored ZFS pool
- a remote cloud service (JottaCloud)
- two 1TB NVMe external SSD drives attached to my computers
The first two locations are updated whenever data changes. JottaCloud updates are managed by a service running on my Macs. Updates to the Raspberry Pi 5 server are performed using RsyncUI.
Currently, my bird photography consists of 140GB and 8,000 files, including RAW and sidecar files. During travel, I use my MacBook Pro for photo editing, while at home, I use my Mac Mini M4. I always keep track of which Mac has been updated and stores the most recent files.
For this post, I am using my MacBook Pro as the local device, while the remote server contains more recently updated data.
Requirements for Using Verify Remote
The Procedure
Select the Pictures profile that stores my task to synchronize my local picture raw catalog with my remote server. After selecting the task, open the Verify remote function.

During the initial run, the Adjust output toggle is disabled. When enabled, the function eliminates the last 16 lines of output from both pull and push operations, as well as all other lines that are identical in both. To initiate the evaluation, select the upward arrow.

The remote server has less data than my local Mac because I deleted some photos locally that were previously synced to the remote. The output from rsync is tagged with information that is evaluated by looking at the first characters of each line. The push data in the left table indicates that my local Mac contains more data, which makes sense because the remote needs to be updated to reflect the deleted photos.

The Adjust output toggle is enabled, and a new evaluation begins.

The remote table on the right indicates that there are updates to sidecars and local data that need to be deleted.

I perform a pull of data from the remote to update my local Mac.

Following the pull, Verify remote displays that my local Mac and remote device are in sync.

Number of files
Numbers updated: December 21, 2025, version 2.8.4
RsyncUI does not rely on external libraries; it is constructed using default Swift libraries and Swift/SwiftUI code exclusively.
cloc DecodeEncodeGeneric/Sources ParseRsyncOutput/Sources RsyncArguments/Sources RsyncUI/RsyncUI RsyncUIDeepLinks/Sources SSHCreateKey/
Sources RsyncProcessStreaming/Sources ProcessCommand/Sources
207 text files.
207 unique files.
15 files ignored.
github.com/AlDanial/cloc v 2.06 T=0.06 s (3336.2 files/s, 362295.4 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Swift 201 2551 2447 17056
C 2 36 72 254
XML 2 0 0 53
JSON 1 0 0 6
C/C++ Header 1 1 3 0
-------------------------------------------------------------------------------
SUM: 207 2588 2522 17369
-------------------------------------------------------------------------------
Main Repository
- RsyncUI (https://github.com/rsyncOSX/RsyncUI) - the main repository for RsyncUI
Swift Packages used by RsyncUI
All SPM packages are refactored, updated, and checked into the main branch. RsyncUI is a depended on all packages, but the last one is not mandatory. SSH keys can be generated via command line.
- RsyncProcessStreaming (https://github.com/rsyncOSX/RsyncProcessStreaming) - A core function of RsyncUI
- listens for output from the rsync process as well as termination signal
- ProcessCommand (https://github.com/rsyncOSX/ProcessCommand) - As above, but for commands other than rsync
- RsyncArguments (https://github.com/rsyncOSX/RsyncArguments) - Generate parameters for
rsyncbased on configurations - DecodeEncodeGeneric (https://github.com/rsyncOSX/DecodeEncodeGeneric) - Generic code for decoding and encoding JSON data
- ParseRsyncOutput (https://github.com/rsyncOSX/ParseRsyncOutput) - Parse and extract numerical values from the output of
rsync- this data is used to display details and log results for synchronized tasks
- RsyncUIDeepLinks (https://github.com/rsyncOSX/RsyncUIDeepLinks) - Parse and return valid URL deeplinks to execute tasks directly within RsyncUI
- sshCreateKey (https://github.com/rsyncOSX/sshCreateKey) - Assist in creating an SSH identity file and key using RsyncUI
- generate an RSA-based SSH key for default and user-defined keys, including the SSH port number
Swift concurrency
First, I must acknowledge that my understanding of Swift concurrency is limited. While I have a basic grasp of the subject, if you are reading this and seeking more detailed information, I strongly recommend searching for and reading articles from other sources that provide a more comprehensive explanation of Swift concurrency.
RsyncUI is a graphical user interface (GUI) application; the majority of its operations are executed on the main thread. However, some resource-intensive tasks are performed on other threads managed by the cooperative thread pool (CTP), excluding and not blocking the main thread. How the executors and CTP work and interact are details I don’t know about—they are managed by the Swift runtime. There are three kinds of executors:
- the main executor manages jobs on the main thread
- the global concurrent executor and the serial executor, both execute jobs on threads from the CTP
Most work in RsyncUI is executed on the main thread. By default, SwiftUI ensures all UI updates are performed on the main thread. Below are some tasks within RsyncUI that are executed on the main thread:
- preparing and executing
rsyncsynchronization tasks (preparing involves computing the correct arguments for rsync) - monitoring progress and termination of the actual
rsynctasks- monitoring progress is an asynchronous sequence running on the main thread, created by an asynchronous sequence of two specific notifications generated by the notification center
- some write and read operations
Swift Version 6 and the New Concurrency Model
Swift version 6 introduced strict concurrency checking. By enabling Swift 6 language mode and strict concurrency checking, Xcode assists in identifying and resolving possible data races at compile time.
However, it is important to note that there are several compiler directives, configured within Xcode, that pertain to concurrency. I strongly recommend researching these directives and their respective meanings. Additionally, if you are migrating an existing project to the new concurrency settings, I suggest researching the process of migrating projects. I recommend reading blog posts about Swift concurrency from Matt Massicotte and Antoine van der Lee.
Quote from swift.org: “More formally, a data race occurs when one thread accesses memory while the same memory is being modified by another thread. The Swift 6 language mode eliminates these issues by preventing data races at compile time.”
RsyncUI adheres to the new concurrency model of Swift 6.
Swift Concurrency and Asynchronous Execution
Concurrency and asynchronous execution are important parts of Swift. The latest version of Swift simplifies writing asynchronous code using the async and await keywords, as well as the actor protocol. While RsyncUI doesn’t inherently require concurrency, it is automatically introduced through the use of actors, async, and await keywords. The objective is to execute most work synchronously on the main thread, provided it doesn’t block the GUI.
Asynchronous execution can be performed on both the main thread and background threads from the CTP. When executing asynchronous operations on the main thread, it is crucial to use Swift’s structured concurrency, specifically the async/await keywords. The await keyword serves as a suspension point, allowing other and more critical tasks to access the main thread.
Cooperative Thread Pool (CTP)
The following RsyncUI tasks are executed asynchronously on threads from the CTP, adhering to the actor protocol:
- read synchronization tasks from file
- JSON data decoding: asynchronous decoding that inherits the thread from the actor reading data
- JSON data encoding: synchronous encoding on the main thread
- read and sort log records
- delete log records
- preparing output from rsync for display
- preparing data from the log file for display
- checking for updates to RsyncUI
Adhering to the actor protocol, all access to properties within an actor must be performed asynchronously. There are only five actors in RsyncUI, but there are more asynchronous functions, some of which run on the main thread as well.
Structured Concurrency
Some concurrent functions within RsyncUI are structured using async let. You may have several async let statements, and they will all execute in parallel or concurrently. When all async let tasks are completed, the root task or parent task will continue execution.
Structured concurrency might also dictate the order of execution. The keyword await is a suspension point where execution waits until the asynchronous function is completed before continuing. If there are several await statements in sequence, the next one will execute when the current asynchronous task is completed.
Example of structured concurrency
Approximately 800 log records, comprising the total of 1500, are selected for deletion. Two operations are concurrently executed on a background thread: the actual deletion of log records and the updating of non-deleted records for display.
The deletion of logs commences on the main thread. The user selects the logs to be deleted, and the delete button either initiates the deletion process or aborts it. The deletelogs() function, an asynchronous function, is initiated on the main thread.
Button("Delete", role: .destructive) {
Task {
await deletelogs(selectedloguuids)
}
}
The deletelogs starts on the main thread and jumps off to an background thread inside the Task {}.
func deletelogs(_ uuids: Set<UUID>) async {
Task {
print("(1) start async let updatedRecords deletelogs")
async let updatedRecords: [LogRecords]? = ActorReadLogRecordsJSON().deletelogs(
uuids,
logrecords: logrecords,
profile: rsyncUIdata.profile,
validhiddenIDs: validhiddenIDs
)
let records = await updatedRecords
print("(2) awaited updatedRecords deletelogs from (1))")
print("(3) start async let updatedRecords updatelogsbyhiddenID)")
async let updatedLogs: [Log]? = ActorReadLogRecordsJSON().updatelogsbyhiddenID(records, hiddenID)
logrecords = records
logs = await (updatedLogs ?? [])
print("(4) awaited updatedLogs from (3)")
WriteLogRecordsJSON(rsyncUIdata.profile, records)
selectedloguuids.removeAll()
}
}
The debug windows in Xcode display the following:
The actors also print whether they execute on the main thread. The first async let statement initiates execution, and the subsequent await statement for the result above (2) suspends the function’s execution until the asynchronous result is computed. The await statement is crucial for suspending the execution of the function until the asynchronous result is available. And then the next (3) and (4).
(1) start async let updatedRecords deletelogs
ActorReadLogRecordsJSON: deletelogs() NOT on main thread, currently on <NSThread: 0xa49e3c200>{number = 18}
ActorReadLogRecordsJSON: DEINIT
(2) awaited updatedRecords deletelogs from (1))
(3) start async let updatedRecords updatelogsbyhiddenID)
ActorReadLogRecordsJSON: updatelogsbyhiddenID() NOT on main thread, currently on <NSThread: 0xa49e3c280>{number = 17}
ActorReadLogRecordsJSON: DEINIT
(4) awaited updatedLogs from (3)
WriteLogRecordsJSON: writeJSONToPersistentStore file:///Users/thomas/.rsyncosx/VPxxxxxxxx/WDBackup/logrecords.json
WriteLogRecordsJSON DEINIT
ActorReadLogRecordsJSON: updatelogsbyhiddenID() NOT on main thread, currently on <NSThread: 0xa4bb72800>{number = 19}
ActorReadLogRecordsJSON: DEINIT
Tagging of data
Overview
It is imperative that RsyncUI correctly tags tasks with data to be synchronized. If the tagging fails, some source data may not be synchronized. RsyncUI supports both the latest version of rsync and the older default version included in macOS.
The tagging of data to be synchronized is computed within the ParseRsyncOutput package, a local Swift Package for RsyncUI.
Example:
- Input string:
Number of created files: 7,191 (reg: 6,846, dir: 345) - Converted to:
[7191, 6846, 345](the thousands separator is also removed from the string before parsing)
The function below extracts only numbers from the input:
public func returnIntNumber(_ input: String) -> [Int] {
var numbers: [Int] = []
let str = input.replacingOccurrences(of: ",", with: "")
let stringArray = str.components(separatedBy: CharacterSet.decimalDigits.inverted)
.compactMap { $0.isEmpty == true ? nil : $0 }
for item in stringArray where item.isEmpty == false {
if let number = Int(item) {
numbers.append(number)
}
}
if numbers.count == 0 {
return [0]
} else {
return numbers
}
}
The parsing of rsync output is not particularly complex, though it differs somewhat between the latest version of rsync and the default versions.
Version 3.4.x
The trailing output from the latest version of rsync (version 3.4.1) looks like:
....
Number of files: 7,192 (reg: 6,846, dir: 346)
Number of created files: 7,191 (reg: 6,846, dir: 345)
Number of deleted files: 0
Number of regular files transferred: 6,846
Total file size: 24,788,299 bytes
Total transferred file size: 24,788,299 bytes
Literal data: 0 bytes
Matched data: 0 bytes
File list size: 0
File list generation time: 0.003 seconds
File list transfer time: 0.000 seconds
Total bytes sent: 394,304
Total bytes received: 22,226
sent 394,304 bytes received 22,226 bytes 833,060.00 bytes/sec
total size is 24,788,299 speedup is 59.51 (DRY RUN)
Openrsync
The trailing output from the default version of rsync looks like:
....
Number of files: 7192
Number of files transferred: 6846
Total file size: 24788299 bytes
Total transferred file size: 24788299 bytes
Literal data: 0 bytes
Matched data: 0 bytes
File list size: 336861
File list generation time: 0.052 seconds
File list transfer time: 0.000 seconds
Total bytes sent: 380178
Total bytes received: 43172
sent 380178 bytes received 43172 bytes 169340.00 bytes/sec
total size is 24788299 speedup is 58.55
How the Tagging Works
The output from rsync is parsed and numbers are extracted. After parsing, these numbers determine whether data should be tagged for synchronization.
Latest Version of rsync
Three numbers determine whether data needs to be synchronized: the number of updates (regular files transferred), new files, and deleted files. Each can be either 0 or a positive number, and all three must be verified.
Default Versions
Only one number determines whether data needs to be synchronized: the number of updates (files transferred).