70 lines
2.5 KiB
Swift
70 lines
2.5 KiB
Swift
import AppKit
|
|
import Darwin
|
|
|
|
@MainActor
|
|
final class ColorSchemeObservation {
|
|
private struct Watch: @unchecked Sendable {
|
|
let source: any DispatchSourceFileSystemObject
|
|
let device: dev_t
|
|
let inode: ino_t
|
|
}
|
|
|
|
private let directory: URL
|
|
private let onChange: @MainActor () -> Void
|
|
private var watches: [String: Watch] = [:]
|
|
|
|
init(directory: URL, onChange: @escaping @MainActor () -> Void) {
|
|
self.directory = directory.standardizedFileURL
|
|
self.onChange = onChange
|
|
refreshWatches()
|
|
}
|
|
|
|
deinit {
|
|
for watch in watches.values { watch.source.cancel() }
|
|
}
|
|
|
|
private func filesChanged() {
|
|
refreshWatches()
|
|
onChange()
|
|
}
|
|
|
|
private func refreshWatches() {
|
|
var ancestor = directory
|
|
var isDirectory: ObjCBool = false
|
|
while !FileManager.default.fileExists(atPath: ancestor.path, isDirectory: &isDirectory)
|
|
|| !isDirectory.boolValue {
|
|
let parent = ancestor.deletingLastPathComponent()
|
|
guard parent.path != ancestor.path else { break }
|
|
ancestor = parent
|
|
}
|
|
let urls = [ancestor, ancestor.deletingLastPathComponent(),
|
|
directory.appendingPathComponent("config.toml"), directory.appendingPathComponent("theme.json")]
|
|
var obsolete = Set(watches.keys)
|
|
for url in urls {
|
|
let path = url.path
|
|
let descriptor = open(path, O_EVTONLY | O_CLOEXEC)
|
|
guard descriptor >= 0 else { continue }
|
|
var identity = stat()
|
|
guard fstat(descriptor, &identity) == 0 else {
|
|
close(descriptor)
|
|
continue
|
|
}
|
|
obsolete.remove(path)
|
|
if let existing = watches[path], existing.device == identity.st_dev, existing.inode == identity.st_ino {
|
|
close(descriptor)
|
|
continue
|
|
}
|
|
watches.removeValue(forKey: path)?.source.cancel()
|
|
let source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor,
|
|
eventMask: [.write, .extend, .delete, .rename, .attrib, .revoke], queue: .main)
|
|
source.setEventHandler { [weak self] in
|
|
Task { @MainActor [weak self] in self?.filesChanged() }
|
|
}
|
|
source.setCancelHandler { close(descriptor) }
|
|
watches[path] = Watch(source: source, device: identity.st_dev, inode: identity.st_ino)
|
|
source.resume()
|
|
}
|
|
for path in obsolete { watches.removeValue(forKey: path)?.source.cancel() }
|
|
}
|
|
}
|