104 lines
3.6 KiB
Swift
104 lines
3.6 KiB
Swift
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()
|
|
}
|
|
}
|