From c3dff3a950568034fe73912939229fad7069b2b2 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Fri, 14 Nov 2025 01:46:10 +0530 Subject: [PATCH] Create Working Focus Detection for all types --- .../managers/DoNotDisturbManager.swift | 930 +++++++++++++++++- ReadMe.md | 2 +- 2 files changed, 890 insertions(+), 42 deletions(-) diff --git a/DynamicIsland/managers/DoNotDisturbManager.swift b/DynamicIsland/managers/DoNotDisturbManager.swift index ce3ad64..577833a 100644 --- a/DynamicIsland/managers/DoNotDisturbManager.swift +++ b/DynamicIsland/managers/DoNotDisturbManager.swift @@ -22,8 +22,14 @@ final class DoNotDisturbManager: ObservableObject { private let notificationCenter = DistributedNotificationCenter.default() private let metadataExtractionQueue = DispatchQueue(label: "com.dynamicisland.focus.metadata", qos: .userInitiated) + private let focusStateResolver = FocusStateResolver() + private let focusLogStream = FocusLogStream() - private init() {} + private init() { + focusLogStream.onMetadataUpdate = { [weak self] identifier, name in + self?.handleLogMetadataUpdate(identifier: identifier, name: name) + } + } deinit { stopMonitoring() @@ -48,6 +54,7 @@ final class DoNotDisturbManager: ObservableObject { suspensionBehavior: .deliverImmediately ) + focusLogStream.start() isMonitoring = true } @@ -57,6 +64,7 @@ final class DoNotDisturbManager: ObservableObject { notificationCenter.removeObserver(self, name: .focusModeEnabled, object: nil) notificationCenter.removeObserver(self, name: .focusModeDisabled, object: nil) + focusLogStream.stop() isMonitoring = false DispatchQueue.main.async { @@ -79,54 +87,83 @@ final class DoNotDisturbManager: ObservableObject { guard let self = self else { return } let metadata = self.extractMetadata(from: notification) + self.publishMetadata(identifier: metadata.identifier, name: metadata.name, isActive: isActive, source: notification.name.rawValue) + } + } - DispatchQueue.main.async { - let trimmedIdentifier = metadata.identifier?.trimmingCharacters(in: .whitespacesAndNewlines) - let trimmedName = metadata.name?.trimmingCharacters(in: .whitespacesAndNewlines) + private func publishMetadata(identifier: String?, name: String?, isActive: Bool?, source: String) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } - let resolvedMode = FocusModeType.resolve(identifier: trimmedIdentifier, name: trimmedName) + let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines) + let trimmedName = name?.trimmingCharacters(in: .whitespacesAndNewlines) - debugPrint("[DoNotDisturbManager] Focus update -> notification: \(notification.name.rawValue) | identifier: \(trimmedIdentifier ?? "") | name: \(trimmedName ?? "") | resolved: \(resolvedMode.rawValue)") + let resolvedMode = FocusModeType.resolve(identifier: trimmedIdentifier, name: trimmedName) - let finalIdentifier: String - if let identifier = trimmedIdentifier, !identifier.isEmpty { - finalIdentifier = identifier - } else { - finalIdentifier = resolvedMode.rawValue - } + let finalIdentifier: String + if let identifier = trimmedIdentifier, !identifier.isEmpty { + finalIdentifier = identifier + } else { + finalIdentifier = resolvedMode.rawValue + } - if finalIdentifier != self.currentFocusModeIdentifier { - self.currentFocusModeIdentifier = finalIdentifier - } + let finalName: String + if let name = trimmedName, !name.isEmpty { + finalName = name + } else if !resolvedMode.displayName.isEmpty { + finalName = resolvedMode.displayName + } else if let identifier = trimmedIdentifier, !identifier.isEmpty { + finalName = identifier + } else { + finalName = "Focus" + } - let finalName: String - if let name = trimmedName, !name.isEmpty { - finalName = name - } else if !resolvedMode.displayName.isEmpty { - finalName = resolvedMode.displayName - } else if let identifier = trimmedIdentifier, !identifier.isEmpty { - finalName = identifier - } else { - finalName = "Focus" - } + let previousIdentifier = self.currentFocusModeIdentifier + let previousName = self.currentFocusModeName + let previousActive = self.isDoNotDisturbActive - if finalName != self.currentFocusModeName { - self.currentFocusModeName = finalName - } + let identifierChanged = finalIdentifier != previousIdentifier + let nameChanged = finalName != previousName + let shouldToggleActive = isActive.map { $0 != previousActive } ?? false - guard self.isDoNotDisturbActive != isActive else { return } + if identifierChanged { + self.currentFocusModeIdentifier = finalIdentifier + } - withAnimation(.smooth(duration: 0.25)) { - self.isDoNotDisturbActive = isActive - } + if nameChanged { + self.currentFocusModeName = finalName + } + + if identifierChanged || nameChanged || shouldToggleActive { + debugPrint("[DoNotDisturbManager] Focus update -> source: \(source) | identifier: \(trimmedIdentifier ?? "") | name: \(trimmedName ?? "") | resolved: \(resolvedMode.rawValue)") + } + + guard let isActive = isActive, shouldToggleActive else { return } + + withAnimation(.smooth(duration: 0.25)) { + self.isDoNotDisturbActive = isActive } } } - private func extractMetadata(from notification: Notification) -> (name: String?, identifier: String?) { - guard let userInfo = notification.userInfo else { return (nil, nil) } + private func handleLogMetadataUpdate(identifier: String?, name: String?) { + metadataExtractionQueue.async { [weak self] in + guard let self = self else { return } + let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines) + let trimmedName = name?.trimmingCharacters(in: .whitespacesAndNewlines) - debugPrint("[DoNotDisturbManager] raw focus payload -> name: \(notification.name.rawValue), object: \(String(describing: notification.object)), userInfo: \(userInfo)") + let hasIdentifier = (trimmedIdentifier?.isEmpty == false) + let hasName = (trimmedName?.isEmpty == false) + + guard hasIdentifier || hasName else { return } + + self.publishMetadata(identifier: trimmedIdentifier, name: trimmedName, isActive: nil, source: "log-stream") + } + } + + private func extractMetadata(from notification: Notification) -> (name: String?, identifier: String?) { + var identifier: String? + var name: String? let identifierKeys = [ "FocusModeIdentifier", @@ -144,12 +181,121 @@ final class DoNotDisturbManager: ObservableObject { "focusModeName", "FocusMode", "focusMode", + "displayName", + "display_name", "name", "Name" ] - let identifier = firstMatch(for: identifierKeys, in: userInfo) - let name = firstMatch(for: nameKeys, in: userInfo) + var candidates: [Any] = [] + if let userInfo = notification.userInfo { + candidates.append(userInfo) + } + + if let object = notification.object { + candidates.append(object) + } + + debugPrint("[DoNotDisturbManager] raw focus payload -> name: \(notification.name.rawValue), object: \(String(describing: notification.object)), userInfo: \(String(describing: notification.userInfo))") + + for candidate in candidates { + if identifier == nil { + identifier = firstMatch(for: identifierKeys, in: candidate) + } + + if name == nil { + name = firstMatch(for: nameKeys, in: candidate) + } + + if identifier != nil && name != nil { + break + } + } + + if identifier == nil || name == nil { + for candidate in candidates { + if let decoded = decodeFocusPayloadIfNeeded(candidate) { + if identifier == nil { + identifier = firstMatch(for: identifierKeys, in: decoded) + } + + if name == nil { + name = firstMatch(for: nameKeys, in: decoded) + } + + if identifier != nil && name != nil { + break + } + } + } + } + + if identifier == nil || name == nil { + for candidate in candidates { + if let object = candidate as? NSObject { + if identifier == nil, let extractedIdentifier = extractIdentifier(fromFocusObject: object) { + identifier = extractedIdentifier + } + + if name == nil, let extractedName = extractDisplayName(fromFocusObject: object) { + name = extractedName + } + + if identifier != nil && name != nil { + break + } + } + } + } + + if identifier == nil || name == nil { + var descriptionSources: [Any] = candidates + + for candidate in candidates { + if let decoded = decodeFocusPayloadIfNeeded(candidate) { + descriptionSources.append(decoded) + } + } + + for candidate in descriptionSources { + let description = String(describing: candidate) + + if identifier == nil, let inferredIdentifier = FocusMetadataDecoder.extractIdentifier(from: description) { + identifier = inferredIdentifier + } + + if name == nil, let inferredName = FocusMetadataDecoder.extractName(from: description) { + name = inferredName + } + + if identifier != nil && name != nil { + break + } + } + } + + if identifier == nil || name == nil { + if let logMetadata = focusLogStream.latestMetadata() { + if identifier == nil { + identifier = logMetadata.identifier + } + + if name == nil { + name = logMetadata.name + } + } + } + + if identifier == nil || name == nil { + if let fallback = focusStateResolver.loadActiveFocusState(identifierMissing: identifier == nil, nameMissing: name == nil) { + if identifier == nil { + identifier = fallback.identifier + } + if name == nil { + name = fallback.name + } + } + } return (name, identifier) } @@ -326,17 +472,26 @@ private extension DoNotDisturbManager { func normalizedString(from value: Any) -> String? { switch value { case let string as String: - let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines) - return trimmed.isEmpty ? nil : trimmed + let cleaned = FocusMetadataDecoder.cleanedString(string) + return cleaned.isEmpty ? nil : cleaned case let number as NSNumber: - return number.stringValue + return FocusMetadataDecoder.cleanedString(number.stringValue) case let uuid as UUID: return uuid.uuidString case let uuid as NSUUID: return uuid.uuidString case let data as Data: + if let decoded = decodeFocusPayload(from: data) { + if let nested = firstMatch(for: ["identifier", "Identifier", "uuid", "UUID"], in: decoded) { + return nested + } + if let name = firstMatch(for: ["name", "Name", "displayName", "display_name"], in: decoded) { + return name + } + } if let string = String(data: data, encoding: .utf8) { - return string.trimmingCharacters(in: .whitespacesAndNewlines) + let cleaned = FocusMetadataDecoder.cleanedString(string) + return cleaned.isEmpty ? nil : cleaned } return nil case let dict as [AnyHashable: Any]: @@ -352,4 +507,697 @@ private extension DoNotDisturbManager { return nil } } + + func decodeFocusPayloadIfNeeded(_ value: Any) -> Any? { + switch value { + case let data as Data: + return decodeFocusPayload(from: data) + case let data as NSData: + return decodeFocusPayload(from: data as Data) + default: + return nil + } + } + + func decodeFocusPayload(from data: Data) -> Any? { + guard !data.isEmpty else { return nil } + + if let propertyList = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) { + return propertyList + } + + if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) { + return jsonObject + } + + if let string = String(data: data, encoding: .utf8) { + let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } + + return nil + } + + func extractIdentifier(fromFocusObject object: NSObject) -> String? { + if let array = object as? [Any] { + for element in array { + if let nested = element as? NSObject, let identifier = extractIdentifier(fromFocusObject: nested) { + return identifier + } + } + return nil + } + + if let identifier = focusString(object, selector: "modeIdentifier") { + return identifier + } + + if let identifier = focusString(object, selector: "identifier") { + return identifier + } + + if let details = focusObject(object, selector: "details"), let identifier = extractIdentifier(fromFocusObject: details) { + return identifier + } + + if let metadata = focusObject(object, selector: "activeModeAssertionMetadata"), let identifier = extractIdentifier(fromFocusObject: metadata) { + return identifier + } + + if let configuration = focusObject(object, selector: "activeModeConfiguration"), let identifier = extractIdentifier(fromFocusObject: configuration) { + return identifier + } + + if let modeConfiguration = focusObject(object, selector: "modeConfiguration"), let identifier = extractIdentifier(fromFocusObject: modeConfiguration) { + return identifier + } + + if let mode = focusObject(object, selector: "mode") { + return extractIdentifier(fromFocusObject: mode) + } + + if let identifiers = focusObject(object, selector: "activeModeIdentifiers") { + if let stringArray = identifiers as? [String] { + if let first = stringArray.compactMap({ FocusMetadataDecoder.cleanedString($0) }).first(where: { !$0.isEmpty }) { + return first + } + } else if let array = identifiers as? NSArray { + for case let string as String in array { + let trimmed = FocusMetadataDecoder.cleanedString(string) + if !trimmed.isEmpty { + return trimmed + } + } + } + } + + return nil + } + + func extractDisplayName(fromFocusObject object: NSObject) -> String? { + if let array = object as? [Any] { + for element in array { + if let nested = element as? NSObject, let name = extractDisplayName(fromFocusObject: nested) { + return name + } + } + return nil + } + + if let name = focusString(object, selector: "name") { + return name + } + + if let name = focusString(object, selector: "displayName") { + return name + } + + if let name = focusString(object, selector: "activityDisplayName") { + return name + } + + if let descriptor = focusObject(object, selector: "symbolDescriptor"), let name = focusString(descriptor, selector: "name") { + return name + } + + if let mode = focusObject(object, selector: "mode"), let name = extractDisplayName(fromFocusObject: mode) { + return name + } + + if let details = focusObject(object, selector: "details"), let name = extractDisplayName(fromFocusObject: details) { + return name + } + + if let configuration = focusObject(object, selector: "modeConfiguration"), let name = extractDisplayName(fromFocusObject: configuration) { + return name + } + + return nil + } + + func focusObject(_ object: NSObject, selector selectorName: String) -> NSObject? { + let selector = NSSelectorFromString(selectorName) + guard object.responds(to: selector) else { return nil } + guard let value = object.perform(selector)?.takeUnretainedValue() else { return nil } + return value as? NSObject + } + + func focusString(_ object: NSObject, selector selectorName: String) -> String? { + let selector = NSSelectorFromString(selectorName) + guard object.responds(to: selector) else { return nil } + guard let value = object.perform(selector)?.takeUnretainedValue() else { return nil } + + switch value { + case let string as String: + return FocusMetadataDecoder.cleanedString(string) + case let string as NSString: + return FocusMetadataDecoder.cleanedString(string as String) + case let number as NSNumber: + return FocusMetadataDecoder.cleanedString(number.stringValue) + default: + return nil + } + } + +} + +// MARK: - Filesystem fallback for Focus metadata + +private final class FocusStateResolver { + private struct CacheKey: Hashable { let url: URL } + private struct FocusRecord { let identifier: String?; let name: String? } + + private let fileManager = FileManager.default + + private let identifierKeys = [ + "FocusModeIdentifier", + "focusModeIdentifier", + "FocusModeUUID", + "focusModeUUID", + "UUID", + "uuid", + "identifier", + "Identifier", + "activeModeIdentifier", + "activeModeUUID", + "modeIdentifier", + "modeUUID" + ] + + private let nameKeys = [ + "FocusModeName", + "focusModeName", + "FocusMode", + "focusMode", + "displayName", + "display_name", + "name", + "Name", + "modeName" + ] + + private var cache: [CacheKey: (modified: Date, records: [FocusRecord])] = [:] + + private lazy var activeStateURLs: [URL] = { + let home = fileManager.homeDirectoryForCurrentUser + return [ + "Library/DoNotDisturb/ActiveState.plist", + "Library/DoNotDisturb/DB/ActiveState.plist", + "Library/Focus/ActiveState.plist" + ].map { home.appendingPathComponent($0) } + }() + + private lazy var catalogURLs: [URL] = { + let home = fileManager.homeDirectoryForCurrentUser + return [ + "Library/DoNotDisturb/DB/FocusPreferences.plist", + "Library/DoNotDisturb/DB/FocusModeDefaults.plist", + "Library/DoNotDisturb/DB/FocusModes.plist", + "Library/Focus/FocusModeDefaults.plist", + "Library/Preferences/com.apple.controlcenter.plist" + ].map { home.appendingPathComponent($0) } + }() + + func loadActiveFocusState(identifierMissing: Bool, nameMissing: Bool) -> (identifier: String?, name: String?)? { + guard identifierMissing || nameMissing else { return nil } + + let activeRecords = loadRecords(from: activeStateURLs) + let catalogRecords = loadRecords(from: catalogURLs) + let allRecords = activeRecords + catalogRecords + + guard !allRecords.isEmpty else { return nil } + + var identifier: String? + var name: String? + + if let record = activeRecords.first(where: { record in + guard let id = record.identifier, !id.isEmpty else { return false } + guard let name = record.name, !name.isEmpty else { return false } + return true + }) { + identifier = record.identifier + name = record.name + } + + if identifierMissing && (identifier == nil || identifier?.isEmpty == true) { + identifier = activeRecords.compactMap { $0.identifier?.trimmingCharacters(in: .whitespacesAndNewlines) } + .first(where: { !$0.isEmpty }) + + if identifier == nil { + identifier = catalogRecords.compactMap { $0.identifier?.trimmingCharacters(in: .whitespacesAndNewlines) } + .first(where: { !$0.isEmpty }) + } + } + + if nameMissing && (name == nil || name?.isEmpty == true) { + if let id = identifier { + name = allRecords.first(where: { + guard let candidateIdentifier = $0.identifier else { return false } + return candidateIdentifier.compare(id, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame && ($0.name?.isEmpty == false) + })?.name + } + + if name == nil { + name = activeRecords.compactMap { $0.name?.trimmingCharacters(in: .whitespacesAndNewlines) } + .first(where: { !$0.isEmpty }) + } + + if name == nil { + name = catalogRecords.compactMap { $0.name?.trimmingCharacters(in: .whitespacesAndNewlines) } + .first(where: { !$0.isEmpty }) + } + } + + if identifier == nil && name == nil { + return nil + } + + return (identifierMissing ? identifier : nil, nameMissing ? name : nil) + } + + private func loadRecords(from urls: [URL]) -> [FocusRecord] { + var records: [FocusRecord] = [] + + for url in urls { + guard fileManager.fileExists(atPath: url.path) else { continue } + + if let cached = cachedRecords(for: url) { + records.append(contentsOf: cached) + continue + } + + guard let attributes = try? fileManager.attributesOfItem(atPath: url.path), + let modified = attributes[.modificationDate] as? Date, + let data = try? Data(contentsOf: url) else { + continue + } + + let decoded = decodeData(data) + let extracted = extractRecords(from: decoded) + + cache[CacheKey(url: url)] = (modified, extracted) + records.append(contentsOf: extracted) + } + + return records + } + + private func cachedRecords(for url: URL) -> [FocusRecord]? { + let key = CacheKey(url: url) + guard let cached = cache[key], + let attributes = try? fileManager.attributesOfItem(atPath: url.path), + let modified = attributes[.modificationDate] as? Date, + cached.modified == modified else { + cache.removeValue(forKey: key) + return nil + } + + return cached.records + } + + private func decodeData(_ data: Data) -> Any { + if let propertyList = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) { + return propertyList + } + + if let unarchived = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSDictionary.self, NSArray.self, NSString.self, NSNumber.self, NSData.self], from: data) { + return unarchived as Any + } + + if let unarchived = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) { + return unarchived as Any + } + + if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) { + return jsonObject + } + + if let string = String(data: data, encoding: .utf8) { + return string + } + + return [:] + } + + private func extractRecords(from value: Any) -> [FocusRecord] { + var results: [FocusRecord] = [] + + if let dictionary = value as? [AnyHashable: Any] { + let identifier = directMatch(in: dictionary, keys: identifierKeys) + let name = directMatch(in: dictionary, keys: nameKeys) + + if identifier != nil || name != nil { + results.append(FocusRecord(identifier: identifier, name: name)) + } + + for nested in dictionary.values { + results.append(contentsOf: extractRecords(from: nested)) + } + } else if let array = value as? [Any] { + for element in array { + results.append(contentsOf: extractRecords(from: element)) + } + } else if let data = value as? Data { + let decoded = decodeData(data) + results.append(contentsOf: extractRecords(from: decoded)) + } else if let data = value as? NSData { + let decoded = decodeData(data as Data) + results.append(contentsOf: extractRecords(from: decoded)) + } else if let string = value as? String { + let cleaned = FocusMetadataDecoder.cleanedString(string) + if !cleaned.isEmpty { + let identifier = matchIdentifier(in: cleaned) + let name = identifier == nil ? matchName(in: cleaned) : matchName(in: cleaned, matching: identifier) + + if identifier != nil || name != nil { + results.append(FocusRecord(identifier: identifier, name: name)) + } + } + } + + return results + } + + private func directMatch(in dictionary: [AnyHashable: Any], keys: [String]) -> String? { + for key in keys { + if let candidate = dictionary[key], let string = normalizedString(from: candidate) { + return string + } + } + return nil + } + + private func normalizedString(from value: Any) -> String? { + switch value { + case let string as String: + return FocusMetadataDecoder.cleanedString(string) + case let string as NSString: + return FocusMetadataDecoder.cleanedString(string as String) + case let number as NSNumber: + return FocusMetadataDecoder.cleanedString(number.stringValue) + case let data as Data: + if let string = String(data: data, encoding: .utf8) { + return FocusMetadataDecoder.cleanedString(string) + } + return nil + case let data as NSData: + return normalizedString(from: data as Data) + default: + return nil + } + } + + private func matchIdentifier(in string: String) -> String? { + FocusMetadataDecoder.extractIdentifier(from: string) + } + + private func matchName(in string: String, matching identifier: String? = nil) -> String? { + if let name = FocusMetadataDecoder.extractName(from: string) { + return name + } + + if let identifier = identifier { + let components = identifier.split(separator: ".") + if let last = components.last { + let derived = FocusMetadataDecoder.cleanedString(String(last).replacingOccurrences(of: "_", with: " ")) + if !derived.isEmpty { + return derived.capitalized + } + } + } + + return nil + } +} + +private final class FocusLogStream { + private let queue = DispatchQueue(label: "com.dynamicisland.focus.logstream", qos: .utility) + private var process: Process? + private var pipe: Pipe? + private var buffer = Data() + private var isRunning = false + + private let metadataLock = NSLock() + private var lastIdentifier: String? + private var lastName: String? + + var onMetadataUpdate: ((String?, String?) -> Void)? + + func start() { + queue.async { [weak self] in + guard let self = self else { return } + guard !self.isRunning else { return } + + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/log") + process.arguments = [ + "stream", + "--style", + "compact", + "--level", + "info", + "--predicate", + "subsystem == \"com.apple.focus\"" + ] + + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = pipe + + pipe.fileHandleForReading.readabilityHandler = { [weak self] handle in + guard let self else { return } + let data = handle.availableData + + if data.isEmpty { + self.queue.async { [weak self] in + self?.handleTermination() + } + return + } + + self.queue.async { [weak self] in + self?.handleIncomingData(data) + } + } + + process.terminationHandler = { [weak self] _ in + self?.queue.async { + self?.handleTermination() + } + } + + do { + try process.run() + self.process = process + self.pipe = pipe + self.isRunning = true + debugPrint("[FocusLogStream] Started unified log tail for com.apple.focus") + } catch { + debugPrint("[FocusLogStream] Failed to start log stream: \(error)") + pipe.fileHandleForReading.readabilityHandler = nil + self.process = nil + self.pipe = nil + } + } + } + + func stop() { + queue.async { [weak self] in + guard let self else { return } + guard self.isRunning else { return } + self.handleTermination(terminateProcess: true) + } + } + + func latestMetadata() -> (identifier: String?, name: String?)? { + metadataLock.lock() + let identifier = lastIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines) + let name = lastName?.trimmingCharacters(in: .whitespacesAndNewlines) + metadataLock.unlock() + + let normalizedIdentifier = (identifier?.isEmpty == false) ? identifier : nil + let normalizedName = (name?.isEmpty == false) ? name : nil + + if normalizedIdentifier == nil && normalizedName == nil { + return nil + } + + return (normalizedIdentifier, normalizedName) + } + + private func handleIncomingData(_ data: Data) { + buffer.append(data) + + let newline: UInt8 = 0x0A + + while let newlineIndex = buffer.firstIndex(of: newline) { + let lineData = buffer.prefix(upTo: newlineIndex) + buffer.removeSubrange(buffer.startIndex...newlineIndex) + + let trimmedLineData: Data + if let lastByte = lineData.last, lastByte == 0x0D { + trimmedLineData = lineData.dropLast() + } else { + trimmedLineData = lineData + } + + guard !trimmedLineData.isEmpty, + let line = String(data: trimmedLineData, encoding: .utf8) else { + continue + } + + processLine(line) + } + } + + private func processLine(_ line: String) { + let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return } + + if trimmed.hasPrefix("Filtering the log data") || trimmed.hasPrefix("Timestamp") { + return + } + + if trimmed.contains("active mode assertion: (null)") || trimmed.contains("active activity: (null)") { + clearMetadata() + return + } + + var updatedIdentifier: String? + var updatedName: String? + + if let identifier = FocusMetadataDecoder.extractIdentifier(from: trimmed), !identifier.isEmpty { + updatedIdentifier = identifier + } + + if let name = FocusMetadataDecoder.extractName(from: trimmed), !name.isEmpty { + updatedName = name + } + + guard updatedIdentifier != nil || updatedName != nil else { return } + + var identifierToSend: String? + var nameToSend: String? + + metadataLock.lock() + if let identifier = updatedIdentifier, !identifier.isEmpty { + lastIdentifier = identifier + } + + if let name = updatedName, !name.isEmpty { + lastName = name + } + + identifierToSend = lastIdentifier + nameToSend = lastName + metadataLock.unlock() + + notifyMetadataUpdate(identifier: identifierToSend, name: nameToSend) + } + + private func clearMetadata() { + metadataLock.lock() + lastIdentifier = nil + lastName = nil + metadataLock.unlock() + notifyMetadataUpdate(identifier: nil, name: nil) + } + + private func handleTermination(terminateProcess: Bool = false) { + if terminateProcess, let process, process.isRunning { + process.terminate() + } + + pipe?.fileHandleForReading.readabilityHandler = nil + pipe?.fileHandleForReading.closeFile() + pipe = nil + + process = nil + buffer.removeAll(keepingCapacity: false) + isRunning = false + clearMetadata() + debugPrint("[FocusLogStream] Stopped unified log tail for com.apple.focus") + } + + private func notifyMetadataUpdate(identifier: String?, name: String?) { + guard let handler = onMetadataUpdate else { return } + handler(identifier, name) + } +} + +private enum FocusNotificationParsing { + static let identifierPattern: NSRegularExpression? = { + let pattern = "com\\.apple\\.(?:focus|donotdisturb)[A-Za-z0-9_.-]*" + return try? NSRegularExpression(pattern: pattern, options: []) + }() + + static let identifierDetailPatterns: [NSRegularExpression] = { + let patterns = [ + "modeIdentifier:\\s*'([^'\\s]+)'", + "activityIdentifier:\\s*([A-Za-z0-9._-]+)" + ] + return patterns.compactMap { try? NSRegularExpression(pattern: $0, options: []) } + }() + + static let namePatterns: [NSRegularExpression] = { + let patterns = [ + "(?i)(?:focusModeName|focusMode|displayName|name)\\s*=\\s*\"([^\"]+)\"", + "(?i)(?:focusModeName|focusMode|displayName|name)\\s*=\\s*([^;\\n]+)", + "activityDisplayName:\\s*([^;>\\n]+)", + "modeIdentifier:\\s*'com\\.apple\\.focus\\.([A-Za-z0-9._-]+)'" + ] + return patterns.compactMap { try? NSRegularExpression(pattern: $0, options: []) } + }() +} + +private enum FocusMetadataDecoder { + static func cleanedString(_ string: String) -> String { + var trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines) + trimmed = trimmed.trimmingCharacters(in: CharacterSet(charactersIn: "'\"")) + return trimmed + } + + static func extractIdentifier(from description: String) -> String? { + let fullRange = NSRange(description.startIndex.. 0, + let identifierRange = Range(match.range(at: 0), in: description) { + let candidate = cleanedString(String(description[identifierRange])) + if !candidate.isEmpty { + return candidate + } + } + + for regex in FocusNotificationParsing.identifierDetailPatterns { + if let match = regex.firstMatch(in: description, options: [], range: fullRange), + match.numberOfRanges > 1, + let identifierRange = Range(match.range(at: 1), in: description) { + let candidate = cleanedString(String(description[identifierRange])) + if !candidate.isEmpty { + return candidate + } + } + } + + return nil + } + + static func extractName(from description: String) -> String? { + let fullRange = NSRange(description.startIndex.. 1, + let nameRange = Range(match.range(at: 1), in: description) { + let candidate = cleanedString(String(description[nameRange])) + if !candidate.isEmpty { + return candidate + } + } + } + + return nil + } } diff --git a/ReadMe.md b/ReadMe.md index 031788d..0ae4746 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -13,7 +13,7 @@ GitHub downloads - Discord server + Discord server