sync app filled in

This commit is contained in:
2026-06-01 10:53:28 -04:00
parent ebe0920f89
commit fb0673a1c5
36 changed files with 4527 additions and 98 deletions

View File

@@ -0,0 +1,103 @@
import Foundation
import CoreServices
public protocol FolderWatcherDelegate: AnyObject {
func folderWatcher(_ watcher: FolderWatcher, didDetectChanges paths: [String])
}
public class FolderWatcher {
public weak var delegate: FolderWatcherDelegate?
private var stream: FSEventStreamRef?
private let folderPath: String
private var debounceTimer: Timer?
private var pendingPaths: Set<String> = []
private let queue = DispatchQueue(label: "com.cairnquire.folderwatcher")
public init(folderPath: String) {
self.folderPath = folderPath
}
public func start() {
stop()
let pathsToWatch = [folderPath as CFString]
var context = FSEventStreamContext(
version: 0,
info: Unmanaged.passUnretained(self).toOpaque(),
retain: nil,
release: nil,
copyDescription: nil
)
let callback: FSEventStreamCallback = { (streamRef, clientCallBackInfo, numEvents, eventPaths, eventFlags, eventIds) in
let watcher = Unmanaged<FolderWatcher>.fromOpaque(clientCallBackInfo!).takeUnretainedValue()
watcher.handleEvents(numEvents: numEvents, eventPaths: eventPaths, eventFlags: eventFlags)
}
stream = FSEventStreamCreate(
kCFAllocatorDefault,
callback,
&context,
pathsToWatch as CFArray,
FSEventStreamEventId(kFSEventStreamEventIdSinceNow),
0.5, // latency in seconds
FSEventStreamCreateFlags(kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagUseCFTypes)
)
if let stream = stream {
FSEventStreamSetDispatchQueue(stream, queue)
FSEventStreamStart(stream)
}
}
public func stop() {
if let stream = stream {
FSEventStreamStop(stream)
FSEventStreamInvalidate(stream)
FSEventStreamRelease(stream)
self.stream = nil
}
}
private func handleEvents(numEvents: Int, eventPaths: UnsafeMutableRawPointer, eventFlags: UnsafePointer<FSEventStreamEventFlags>) {
guard let paths = Unmanaged<CFArray>.fromOpaque(eventPaths).takeUnretainedValue() as? [String] else { return }
var changedPaths: [String] = []
for i in 0..<numEvents {
let path = paths[i]
let flags = eventFlags[i]
// Filter for relevant file events
if flags & UInt32(kFSEventStreamEventFlagItemCreated) != 0 ||
flags & UInt32(kFSEventStreamEventFlagItemModified) != 0 ||
flags & UInt32(kFSEventStreamEventFlagItemRemoved) != 0 ||
flags & UInt32(kFSEventStreamEventFlagItemRenamed) != 0 {
// Only track markdown files
if path.hasSuffix(".md") {
changedPaths.append(path)
}
}
}
guard !changedPaths.isEmpty else { return }
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.pendingPaths.formUnion(changedPaths)
self.debounceTimer?.invalidate()
self.debounceTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { [weak self] _ in
guard let self = self else { return }
let paths = Array(self.pendingPaths)
self.pendingPaths.removeAll()
self.delegate?.folderWatcher(self, didDetectChanges: paths)
}
}
}
deinit {
stop()
}
}