krz/orgstar

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

Sources/OrgWorkspace/FolderPresenter.swift

15f6b0709d88971fb62ed432c3e5b8032643670a
orgstar/Sources/OrgWorkspace/FolderPresenter.swift history · blame · raw

66 lines · 2587 bytes

 1import Foundation
 2
 3public enum WatchEvent: Sendable, Equatable {
 4    case changed(URL)
 5    /// Events were dropped or coalesced for this folder, or a root moved: rescan it.
 6    case rescan(URL)
 7}
 8
 9/// Watches a folder as an `NSFilePresenter`: told of coordinated writes inside it, which is
10/// how iCloud and File Provider syncing (Möbius Sync for Syncthing) change files on iOS, where
11/// there are no FSEvents. Events arrive in batches after `latency`.
12public final class FolderPresenter: NSObject, NSFilePresenter, @unchecked Sendable {
13    public let presentedItemURL: URL?
14    public let presentedItemOperationQueue = OperationQueue()
15    private let latency: TimeInterval
16    private let handler: @Sendable ([WatchEvent]) -> Void
17    private let lock = NSLock()
18    private var pending: [WatchEvent] = []
19    private var scheduled = false
20
21    public init(root: URL, latency: TimeInterval = 0.3, handler: @escaping @Sendable ([WatchEvent]) -> Void) {
22        presentedItemURL = root
23        self.latency = latency
24        self.handler = handler
25        presentedItemOperationQueue.maxConcurrentOperationCount = 1
26        super.init()
27    }
28
29    public func start() { NSFileCoordinator.addFilePresenter(self) }
30
31    public func stop() { NSFileCoordinator.removeFilePresenter(self) }
32
33    private func add(_ event: WatchEvent) {
34        let schedule: Bool = lock.withLock {
35            if !pending.contains(event) { pending.append(event) }
36            defer { scheduled = true }
37            return !scheduled
38        }
39        guard schedule else { return }
40        DispatchQueue.global().asyncAfter(deadline: .now() + latency) { [self] in
41            let batch: [WatchEvent] = lock.withLock {
42                defer {
43                    pending = []
44                    scheduled = false
45                }
46                return pending
47            }
48            handler(batch)
49        }
50    }
51
52    public func presentedSubitemDidChange(at url: URL) { add(.changed(url)) }
53    public func presentedSubitemDidAppear(at url: URL) { add(.changed(url)) }
54    public func presentedSubitem(at oldURL: URL, didMoveTo newURL: URL) {
55        add(.changed(oldURL))
56        add(.changed(newURL))
57    }
58    public func accommodatePresentedSubitemDeletion(at url: URL, completionHandler: @escaping @Sendable ((any Error)?) -> Void) {
59        add(.changed(url))
60        completionHandler(nil)
61    }
62    public func presentedItemDidChange() {
63        if let presentedItemURL { add(.rescan(presentedItemURL)) }
64    }
65    public func presentedItemDidMove(to newURL: URL) { add(.rescan(newURL)) }
66}