This is the multi-page printable view of this section. Click here to print.
Blog
Version 2.7.5
Version 2.7.5 (build 166) - 27 October, 2025 release candidate (RC)
I intend to refrain from releasing a new public version until sometime in first week of November 2025. However, I will release new RC versions when there are updates.
Almost every button is adapted to the new Liquid Glass style on macOS Tahoe. On previous macOS versions there is applied a .buttonStyle(.borderedProminent) which makes the buttons blue, please see below. I am not sure if I should keep this style or remove it. If there are any comments, please let me know.
I think the applied Liquid Glass style on macOS Tahoe is very nice. In my opinion it makes the views more pleasant. Below are four views, with and without the updated button style.
The functions Schedule and Verify remote have been moved from the main sidebar to the toolbar. This modification enhances the cleanliness of the main sidebar by consolidating major functions. Additionally, there is an update to the progress bar. To display and access the toolbar actions for the two above mentioned functions, they must be enabled through the RsyncUI settings.
The primary sidebar retains its context sensitivity, but its functionality is limited to the Snapshots and Restore options, which are specifically tailored to certain tasks. For comprehensive information regarding the context-sensitive sidebar menu, please refer to the section titled Getting started.
The main view
Schedule and Verify remote moved to the toolbar.

Liquid Glass style and default button style side by side






Version 2.7.4
Version 2.7.4 (build 165) - October 21, 2025
The following updates have been made, including some code cleanups. Apart from the majority of changes within the Schedule section, this release is a maintenance release.
- there is details about the changed files
In the process of refactoring the Schedule code, I discovered that the model was too fragmented. Consequently, I have refactored most of the model code for Schedule. The UI component remains unchanged. The refactoring process is completed in two stages:
- refactoring the model code without any improvements for sleep and wakefulness
- refactoring the model code to enhance its functionality for sleep and wakefulness
Tasks added to the schedule are validated. The planned next task schedule added must either:
- be 10 minutes ahead of the first schedule in queue
- the first schedule in queue is always ahead of now
- be 10 minutes subtracted from the first schedule in queue
- as above, the planned next schedule must also be greater than now
- the Schedule function is enhanced to tolerate when the Mac goes to sleep
- when a scheduled task is not executed when the Mac enters sleep mode, the Schedule function retrieves unexecuted task and display the tasks in a table
- the user may move the unexecuted tasks to the schedule table
The Schedule view visually distinguishes invalid times in red font, eliminating the need for additional popups. Only validated task schedules are subsequently incorporated into the schedule.
- in Settings view, all toggles are changed to
.toggleStyle(.switch) - every
.onAppear {...}is replaced with.task {...}if the closure includes asynchronous code- the .task modifier handles asynchronous functions directly
AI and RsyncUI
I have commenced experimenting with artificial intelligence (AI) in coding, specifically utilizing Copilot (GPT-5), a service accessible on GitHub and Claude AI by Anthropic. I am permitted to utilize Copilot for free due to the open-source nature of RsyncUI. Claude AI offers a free version with limitations on the number of questions asked within a specified time frame. I inquired with Copilot and Claude AI about optimizing the Schedule code to enhance its reliability during sleep mode on macOS. I am genuinely impressed by the capabilities of AI in assisting with coding tasks.
Issues when testing awake on Mac
During the testing of the revised code, I encountered an issue with the Mac’s behavior when it returns from being asleep and an external attached disc is present. Upon waking, the Mac ejects the attached volume, which subsequently disrupts the synchronization process of RsyncUI. You may test this yourself to verify if there is an issue regarding this.
I asked ChatGPT about this and the answer was: “Your Mac ejects external disks when waking from sleep due to a common bug or power management issue, where the system incorrectly unmounts the drive during the sleep-wake cycle. To fix this, you can update your macOS, adjust energy saver settings, check for issues with hubs or docks, or use a third-party app like Jettison to automatically unmount the drive before sleep and remount it upon waking.”
Number of files
Numbers updated: October 17, 2025, version 2.7.4
There is a very nice and excellent tool, cloc (https://github.com/AlDanial/cloc), for counting of files and lines of code. Below are the numbers for Swift files which are part of the repository for compiling RsyncUI. 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
191 text files.
190 unique files.
1 file ignored.
github.com/AlDanial/cloc v 2.06 T=0.04 s (4423.1 files/s, 489359.9 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Swift 184 2187 2393 16016
C 2 36 72 254
XML 2 0 0 53
JSON 1 0 0 6
C/C++ Header 1 1 3 0
-------------------------------------------------------------------------------
SUM: 190 2224 2468 16329
-------------------------------------------------------------------------------
Main Repository:
- RsyncUI (https://github.com/rsyncOSX/RsyncUI) - The primary repository for RsyncUI.
Local RsyncUI packages:
SPM, Swift Package Manager, makes it easy to create local packages. And each package containes their own tests by Swift Testing, the new framwork for creating tests. All packages are created by me.
- RsyncArguments (https://github.com/rsyncOSX/RsyncArguments) - Generate parameters for
rsyncbased on configurations. - 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.
- 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 end return valid URL deeplink for execute tasks direct within RsyncUI
Observers
A key feature of RsyncUI is observation for two notifications:
NSNotification.Name.NSFileHandleDataAvailableProcess.didTerminateNotification
Without observation and required actions when observed, RsyncUI becomes useless. Both observations are linked to the external task executing the actual rsync task.
Notifications
The initial notification, NSNotification.Name.NSFileHandleDataAvailable, monitors the generation of output by the external task. To display the progress of a synchronization task, RsyncUI relies on monitoring the output from rsync. Consequently, the —verbose parameter to rsync is essential. This parameter instructs rsync to output information during execution.
The second notification, Process.didTerminateNotification, monitors the termination of the task, such as an abrupt halt. Typically, a termination signifies task completion. However, it can also indicate an abort action from the user, which subsequently sends an interrupt signal to the external task. If RsyncUI fails to detect this signal, it will be unable to discern when a synchronization task has been completed.
In the latest version of RsyncUI (2.6.4), a new method for observing notifications have been introduced. After some investigation and recommendation, the AsyncSequence method emerges as the preferred approach. By applying this the Combine Framework and depended code is removed.
NotificationCenter.Notifications and AsyncSequence
If you search for the following: NotificationCenter.Notifications + AsyncSequence, by AI Google summarize the search by:
NotificationCenter.Notifications provides a method to receive notifications from NotificationCenter as an AsyncSequence, enabling asynchronous iteration using the for await syntax. This modern approach to handling notifications in Swift concurrency contrasts with older methods that may utilize Combine or require explicit handling of NSNotification objects.
Quote Apple documentation: “An asynchronous sequence of notifications generated by a notification center.” The for await loops iterate over the notifications and respond to each one. Upon detecting the termination signal, the tasks must also be canceled, and the observers must be removed.
// AsyncSequence
let sequencefilehandler = NotificationCenter.default.notifications(named: NSNotification.Name.NSFileHandleDataAvailable, object: nil)
let sequencetermination = NotificationCenter.default.notifications(named: Process.didTerminateNotification, object: nil)
// Tasks
var sequenceFileHandlerTask: Task<Void, Never>?
var sequenceTerminationTask: Task<Void, Never>?
....
sequenceFileHandlerTask = Task {
for await _ in sequencefilehandler {
await self.datahandle(pipe)
}
}
sequenceTerminationTask = Task {
for await _ in sequencetermination {
Task {
try await Task.sleep(seconds: 0.5)
await self.termination()
}
}
}
Upon task completion, the Observers must be released, and corresponding Tasks must be canceled to prevent a retain cycle and memory leak.
func datahandle(_ pipe: Pipe) async {
let outHandle = pipe.fileHandleForReading
let data = outHandle.availableData
if data.count > 0 {
if let str = NSString(data: data, encoding: String.Encoding.utf8.rawValue) {
str.enumerateLines { line, _ in
self.output.append(line)
if SharedReference.shared.checkforerrorinrsyncoutput,
self.errordiscovered == false
{
do {
try self.checklineforerror?.checkforrsyncerror(line)
} catch let e {
self.errordiscovered = true
let error = e
self.propogateerror(error: error)
}
}
}
// Send message about files
if usefilehandler {
filehandler(output.count)
}
}
outHandle.waitForDataInBackgroundAndNotify()
}
}
func termination() async {
processtermination(output, config?.hiddenID)
// Log error in rsync output to file
if errordiscovered, let config {
Task {
await ActorLogToFile(command: config.backupID,
stringoutputfromrsync: output)
}
}
SharedReference.shared.process = nil
// Remove observers
NotificationCenter.default.removeObserver(sequencefilehandler as Any,
name: NSNotification.Name.NSFileHandleDataAvailable,
object: nil)
NotificationCenter.default.removeObserver(sequencetermination as Any,
name: Process.didTerminateNotification,
object: nil)
// Cancel Tasks
sequenceFileHandlerTask?.cancel()
sequenceTerminationTask?.cancel()
Logger.process.info("ProcessRsyncAsyncSequence: process = nil and termination discovered")
}
Removed methods
The two following methods, which still works very well, are removed from the code.
Combine and Publisher
The Combine framework is utilized within the ProcessRsync and ProcessCommand objects, which is responsible for initiating external tasks, such as the rsync synchronize task. The rsync synchronize task is completed when the last notification is observed. By using Combine, a publisher is added to the Notification center. Every time the Notification center discover one of the notifications, it publish a message.
// Combine, subscribe to NSNotification.Name.NSFileHandleDataAvailable
NotificationCenter.default.publisher(
for: NSNotification.Name.NSFileHandleDataAvailable)
.sink { [self] _ in
....
}.store(in: &subscriptons)
// Combine, subscribe to Process.didTerminateNotification
NotificationCenter.default.publisher(
for: Process.didTerminateNotification)
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { [self] _ in
....
subscriptons.removeAll()
}.store(in: &subscriptons)
NotificationCenter and addObserver
The second method for observing notifications involves adding Observers to the Notification center. Upon the discovery of a notification, the completion handler is executed. The Process object is annotated to execute on the main thread. It appears that the addObserver closure is marked as Sendable, indicating that mutating properties within the closure must be asynchronous. This is due to the Swift 6 language mode and strict concurrency checking.
// Observers
var notificationsfilehandle: NSObjectProtocol?
var notificationstermination: NSObjectProtocol?
....
notificationsfilehandle =
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable,
object: nil, queue: nil)
{ _ in
Task {
await self.datahandle(pipe)
}
}
notificationstermination =
NotificationCenter.default.addObserver(forName: Process.didTerminateNotification,
object: task, queue: nil)
{ _ in
Task {
// Debounce termination for 500 ms
try await Task.sleep(seconds: 0.5)
await self.termination()
}
}
Upon task completion, the Observers must be released to prevent a retain cycle and memory leak.
func datahandle(_ pipe: Pipe) async {
let outHandle = pipe.fileHandleForReading
let data = outHandle.availableData
if data.count > 0 {
if let str = NSString(data: data, encoding: String.Encoding.utf8.rawValue) {
str.enumerateLines { line, _ in
self.output.append(line)
if SharedReference.shared.checkforerrorinrsyncoutput,
self.errordiscovered == false
{
do {
try self.checklineforerror?.checkforrsyncerror(line)
} catch let e {
self.errordiscovered = true
let error = e
self.propogateerror(error: error)
}
}
}
// Send message about files
if usefilehandler {
filehandler(output.count)
}
}
outHandle.waitForDataInBackgroundAndNotify()
}
}
func termination() async {
processtermination(output, config?.hiddenID)
// Log error in rsync output to file
if errordiscovered, let config {
Task {
await ActorLogToFile(command: config.backupID,
stringoutputfromrsync: output)
}
}
SharedReference.shared.process = nil
NotificationCenter.default.removeObserver(notificationsfilehandle as Any,
name: NSNotification.Name.NSFileHandleDataAvailable,
object: nil)
NotificationCenter.default.removeObserver(notificationstermination as Any,
name: Process.didTerminateNotification,
object: nil)
Logger.process.info("ProcessRsyncObserving: process = nil and termination discovered")
}
Swift concurrency
To commence, I must acknowledge that my proficiency in Swift concurrency is limited. While I possess a rudimentary comprehension of the subject matter, if you are perusing this blog and seeking further elaboration on the topic, I strongly recommend conducting a search and perusing articles from alternative sources that offer a more comprehensive understanding 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 to the executors and CTP works and interacts is details I dont know about, and it is managed by the Swift runtime. There are three kinds of executors:
- the main executor manage jobs on the main thread
- the global concurrent executor and the serial executor, both executes jobs on threads from the CTP
Most work in RsyncUI are executed on the main thread. And by default, SwiftUI makes sure all UI-updates are performed on the main thread. Below are some tasks within RsyncUI, which are executed on the main thread:
- preparing of and execution of
rsyncsynchronize tasks, preparing is computing the correct arguments for rsync - monitoring progress and termination of the real rsync tasks
- monitoring progress is an asynchronous sequence running on the main thread, by 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.
Quote 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 in Swift. The latest version of Swift simplifies the writing of asynchronous code using Swift async and await keywords, as well as the actor protocol. RsyncUI does not require concurrency, but concurrency is automatically introduced by using actors , async and await keywords. It is an objective to execute most work synchronous on the main thread as long as it does not 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 utilize 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 tasks are executed asynchronous on threads from the CTP, adhering to the actor protocol:
- read synchronize tasks from file
- JSON data decoding, asynchronous decoding which inherits the thread from the actor reading data
- JSON data encoding, synchronous encoding on the main thread
- read and sort log records
- preparing output from rsync for display
- preparing data from the logfile 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 are running on the main thread as well.
Structured concurrency
Some concurrent functions within RsyncUI are structured by using async let. You may have several async let, and they will all be executed in parallel or concurrent. 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 where execution waits until the asynchronous function is completed before continuing the execution. If there are several await after each other, the next one will be executed when the current asynchronous task is completed.
func readconfigurations() {
Task {
let monitornetworkconnection = SharedReference.shared.monitornetworkconnection
let sshport = SharedReference.shared.sshport
let actorReadSynchronizeConfigurationJSON = ActorReadSynchronizeConfigurationJSON()
async let data = actorReadSynchronizeConfigurationJSON
.readjsonfilesynchronizeconfigurations(profile,
monitornetworkconnection,
sshport)
rsyncUIdata.configurations = await data
}
}
Unstructured concurrency
The code snippet below presents an unstructured concurrency. The code within the Task {} does not have a parent/child relationship to the caller. And it may be completed either before or after the execution of the calling function, the parent, is completed.
@MainActor
func somefunction() {
// Some code
Task {
newversion.notifynewversion = await GetversionofRsyncUI().getversionsofrsyncui()
}
// Some code
}
The primary reason for utilizing an actor is to execute the task on a separate thread from the CTP, thereby preventing blocking of the main thread. The calling function operates on the main thread, while the asynchronous function executes on a background thread. In the event that the remote call takes an extended period or times out, the UI remains unblocked. The call retrieves a file from GitHub to determine whether a new version of RsyncUI is available.
actor GetversionofRsyncUI {
@concurrent
nonisolated func getversionsofrsyncui() async -> Bool {
do {
let versions = await DecodeGeneric()
if let versionsofrsyncui =
try await versions.decodearraydata(VersionsofRsyncUI.self,
fromwhere: Resources().getResource(resource: .urlJSON))
{
let runningversion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
let check = versionsofrsyncui.filter { runningversion.isEmpty ? true : $0.version == runningversion }
if check.count > 0 {
return true
} else {
return false
}
}
} catch {
return false
}
return false
}
}
Tagging of data
Tagging of data to be synchronized
It is imperative that RsyncUI tags tasks with data to be synchronized correctly. If the tagging fails, there may be source data that is not synchronized. RsyncUI supports the latest version of rsync and the older default version of rsync included in macOS 14 and macOS 15.
The tagging of data to be synchronized is computed within the package ParseRsyncOutput, a local Swift Package for RsyncUI.
From version 2.4.0, there is a verification of the tagging, if the output from rsync is greater than 20 lines and tagging for data to synchronize is not set, an alert is thrown. Normally, if there are no data to synchronize output from rsync is about 20 lines. Extract numbers from a string containing letters and digits is from version 2.4.0 of RsyncUI is now a one line code.
Example:
- the string
Number of created files: 7,191 (reg: 6,846, dir: 345)as input - is converted to
[7191,6846,345], the thousand mark is also removed from string ahead parsing
The function below extract numbers only 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 the output of rsync is not particularly complex, and it is somewhat different for the latest version of rsync compared to the default versions of rsync.
Latest version of rsync
The trail of output from latest version of rsync, version 3.4.1, is 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)
Default version of rsync
The trail of output from default version of rsync is 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 does the tagging work
The output from rsync is parsed and numbers are extracted. After parsing of output, the numbers decide if there is tagging of data to be synchronized.
Latest version of rsync
There are three numbers which decide data to synchronize or not: number of updates (regular files transferred), new files or deleted files. And they all may be 0 or a number, all three must be verified.
Default versions
There is only one number which decide data to synchronize or not: number of updates (files transferred).
URLs Notepad
A few samples of URL´s I am executing from Notepad. URL´s must start with rsyncuiapp://, if not RsyncUI will not recognize the command. My URL´s saved in Notepad for easy access and execution of my most used tasks. The Notepad page might also be saved as a PDF document which includes til URL links.
Sample URLs which I am using
URLs for estimate and execute.
Remote server - raspberrypi with zfs filesystem
rsyncuiapp://loadprofileandestimate?profile=Picturesrsyncuiapp://loadprofileandestimate?profile=default
NVME SSD disks
The mount point, such as /Volumes/WDBackup, and the profile name are set to the same value solely for the purpose of simplifying the identification of the mounted disk and the corresponding profile to use in RsyncUI. As documented here, I am performing backups on several SSD disks and also to a remote server. The rationale behind having multiple backups is that it is straightforward to update all backups on a regular basis using RsyncUI. Additionally, if one disk or the server fails, I always have an updated backup to restore from.
Mounted as WDBackup
rsyncuiapp://loadprofileandestimate?profile=WDBackup
Mounted as Samsung
rsyncuiapp://loadprofileandestimate?profile=Samsung
Mounted as LaCie
rsyncuiapp://loadprofileandestimate?profile=LaCie
URL for verify a remote
Verify remote is only for remote destinations. Remote server - raspberrypi with zfs filesystem. Profile is Pictures and Synchronize ID = Pictures backup, the space is replaced by a _ in URL for the id tag in URL, which is the search for the wanted Synchronize ID.
rsyncuiapp://loadprofileandverify?profile=Pictures&id=Pictures_backup
Console and OSLog
Included in Swift 5 is a unified logging feature called OSLog. This feature provides several methods for logging and investigating the application’s activities. By utilizing OSLog, print statements are no longer necessary to follow the execution of code. All logging is performed through OSLog, which is displayed as part of Xcode. OSLog is integrated into all objects that perform work, making it straightforward to identify which commands RsyncUI is executing.
OSLog information in Xcode. The logging displays commands and arguments as shown below. This feature facilitates the verification that RsyncUI is executing the correct command.

And the OSLogs might be read by using the Console app. Be sure to set:
- the Action in Console app menu to
Include Info Messages - enter
no.blogspot.RsyncUIas subsystem within the Search field
