krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgWorkspace/FSEventsWatcher.swift

322a87d47526d505d903e05d14e65a5464e1468f
orgstar/Sources/OrgWorkspace/FSEventsWatcher.swift history · blame · raw

72 lines · 2954 bytes

 1#if os(macOS)
 2import CoreServices
 3import Foundation
 4
 5/// FSEvents with file-level events. Delivers batches on a private queue.
 6public final class FSEventsWatcher: @unchecked Sendable {
 7    private let paths: [String]
 8    private let latency: CFTimeInterval
 9    private let handler: @Sendable ([WatchEvent]) -> Void
10    private let queue = DispatchQueue(label: "orgstar.fsevents")
11    private var stream: FSEventStreamRef?
12
13    public init(roots: [URL], latency: CFTimeInterval = 0.3, handler: @escaping @Sendable ([WatchEvent]) -> Void) {
14        paths = roots.map(\.path)
15        self.latency = latency
16        self.handler = handler
17    }
18
19    deinit {
20        stop()
21    }
22
23    public func start() {
24        guard stream == nil else { return }
25        var context = FSEventStreamContext(
26            version: 0, info: Unmanaged.passUnretained(self).toOpaque(), retain: nil, release: nil, copyDescription: nil
27        )
28        let flags = FSEventStreamCreateFlags(
29            kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagUseCFTypes
30                | kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagWatchRoot
31        )
32        let callback: FSEventStreamCallback = { _, info, count, eventPaths, eventFlags, _ in
33            guard let info else { return }
34            let watcher = Unmanaged<FSEventsWatcher>.fromOpaque(info).takeUnretainedValue()
35            let paths = unsafeBitCast(eventPaths, to: NSArray.self) as? [String] ?? []
36            watcher.deliver(paths: paths, flags: Array(UnsafeBufferPointer(start: eventFlags, count: count)))
37        }
38        guard let stream = FSEventStreamCreate(
39            nil, callback, &context, paths as CFArray, FSEventStreamEventId(kFSEventStreamEventIdSinceNow), latency, flags
40        ) else { return }
41        FSEventStreamSetDispatchQueue(stream, queue)
42        FSEventStreamStart(stream)
43        self.stream = stream
44    }
45
46    public func stop() {
47        guard let stream else { return }
48        FSEventStreamStop(stream)
49        FSEventStreamInvalidate(stream)
50        FSEventStreamRelease(stream)
51        self.stream = nil
52    }
53
54    private func deliver(paths eventPaths: [String], flags: [FSEventStreamEventFlags]) {
55        let rescanFlags = FSEventStreamEventFlags(
56            kFSEventStreamEventFlagMustScanSubDirs | kFSEventStreamEventFlagUserDropped
57                | kFSEventStreamEventFlagKernelDropped | kFSEventStreamEventFlagRootChanged
58        )
59        var events: [WatchEvent] = []
60        for (path, flag) in zip(eventPaths, flags) {
61            let url = URL(fileURLWithPath: path).standardizedFileURL
62            if flag & rescanFlags != 0 {
63                let root = paths.first { path.hasPrefix($0) } ?? path
64                events.append(.rescan(URL(fileURLWithPath: root).standardizedFileURL))
65            } else {
66                events.append(.changed(url))
67            }
68        }
69        if !events.isEmpty { handler(events) }
70    }
71}
72#endif