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() } } }