From c3dff3a950568034fe73912939229fad7069b2b2 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Fri, 14 Nov 2025 01:46:10 +0530 Subject: [PATCH 1/5] 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

From 5b081ec8a742c25ed4fb734bc19499c7d7912f07 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Fri, 14 Nov 2025 02:30:51 +0530 Subject: [PATCH 2/5] Update Icons --- .../managers/DoNotDisturbManager.swift | 295 +----------------- 1 file changed, 7 insertions(+), 288 deletions(-) diff --git a/DynamicIsland/managers/DoNotDisturbManager.swift b/DynamicIsland/managers/DoNotDisturbManager.swift index 577833a..135a845 100644 --- a/DynamicIsland/managers/DoNotDisturbManager.swift +++ b/DynamicIsland/managers/DoNotDisturbManager.swift @@ -22,7 +22,6 @@ 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() { @@ -286,17 +285,6 @@ final class DoNotDisturbManager: ObservableObject { } } - 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) } @@ -347,8 +335,8 @@ enum FocusModeType: String, CaseIterable { case .driving: return "car.fill" case .fitness: return "figure.run" case .gaming: return "gamecontroller.fill" - case .mindfulness: return "brain.head.profile" - case .reading: return "book.fill" + case .mindfulness: return "circle.hexagongrid" + case .reading: return "book.closed.fill" case .custom: return "app.badge" case .unknown: return "moon.fill" } @@ -359,9 +347,9 @@ enum FocusModeType: String, CaseIterable { case .doNotDisturb: return Color(red: 0.370, green: 0.360, blue: 0.902) case .work: - return Color(red: 0.133, green: 0.475, blue: 0.992) + return Color(red: 0.414, green: 0.769, blue: 0.863, opacity: 1.0) case .personal: - return Color(red: 0.937, green: 0.282, blue: 0.624) + return Color(red: 0.748, green: 0.354, blue: 0.948, opacity: 1.0) case .sleep: return Color(red: 0.341, green: 0.384, blue: 0.980) case .driving: @@ -369,11 +357,11 @@ enum FocusModeType: String, CaseIterable { case .fitness: return Color(red: 0.176, green: 0.804, blue: 0.459) case .gaming: - return Color(red: 0.639, green: 0.329, blue: 0.937) + return Color(red: 0.043, green: 0.518, blue: 1.000, opacity: 1.0) case .mindfulness: - return Color(red: 0.239, green: 0.718, blue: 0.682) + return Color(red: 0.361, green: 0.898, blue: 0.883, opacity: 1.0) case .reading: - return Color(red: 0.239, green: 0.596, blue: 0.965) + return Color(red: 1.000, green: 0.622, blue: 0.044, opacity: 1.0) case .custom: return Color(red: 0.513, green: 0.478, blue: 0.965) case .unknown: @@ -661,275 +649,6 @@ private extension DoNotDisturbManager { } -// 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? From 45df2bce99195b9289d6097122802e21eca0165b Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Fri, 14 Nov 2025 11:22:16 +0530 Subject: [PATCH 3/5] Added Private Icons --- .../Focus/DoNotDisturbLiveActivity.swift | 10 +++++----- .../components/UI/FocusIndicator.swift | 6 +++--- .../extensions/Image+InternalSystemName.swift | 14 ++++++++++++++ .../managers/DoNotDisturbManager.swift | 18 ++++++++++++++++++ 4 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 DynamicIsland/extensions/Image+InternalSystemName.swift diff --git a/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift b/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift index c01aca0..962111e 100644 --- a/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift +++ b/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift @@ -110,13 +110,13 @@ struct DoNotDisturbLiveActivity: View { } } - private var currentIconName: String { + private var currentIcon: Image { if manager.isDoNotDisturbActive { - return focusMode.sfSymbol + return focusMode.activeIcon } else if showInactiveIcon { - return focusMode.inactiveSymbol + return Image(systemName: focusMode.inactiveSymbol) } else { - return focusMode.sfSymbol + return focusMode.activeIcon } } @@ -136,7 +136,7 @@ struct DoNotDisturbLiveActivity: View { Color.clear .overlay(alignment: .center) { if iconWingWidth > 0 { - Image(systemName: currentIconName) + currentIcon .font(.system(size: 13, weight: .semibold)) .foregroundColor(currentIconColor) .contentTransition(.opacity) diff --git a/DynamicIsland/components/UI/FocusIndicator.swift b/DynamicIsland/components/UI/FocusIndicator.swift index f887d13..0430cb9 100644 --- a/DynamicIsland/components/UI/FocusIndicator.swift +++ b/DynamicIsland/components/UI/FocusIndicator.swift @@ -14,7 +14,7 @@ struct FocusIndicator: View { Capsule() .fill(Color.black) .overlay { - Image(systemName: focusSymbol) + focusIcon .font(.system(size: 12, weight: .semibold)) .foregroundStyle(accentColor) .frame(width: 24, height: 24) @@ -30,8 +30,8 @@ struct FocusIndicator: View { ) } - private var focusSymbol: String { - focusMode.sfSymbol + private var focusIcon: Image { + focusMode.activeIcon } private var accentColor: Color { diff --git a/DynamicIsland/extensions/Image+InternalSystemName.swift b/DynamicIsland/extensions/Image+InternalSystemName.swift new file mode 100644 index 0000000..1fccf44 --- /dev/null +++ b/DynamicIsland/extensions/Image+InternalSystemName.swift @@ -0,0 +1,14 @@ +import SwiftUI + +@_silgen_name("$s7SwiftUI5ImageV19_internalSystemNameACSS_tcfC") +private func _swiftUI_image(internalSystemName: String) -> Image? + +extension Image { + init?(internalSystemName systemName: String) { + guard let systemImage = _swiftUI_image(internalSystemName: systemName) else { + return nil + } + + self = systemImage + } +} diff --git a/DynamicIsland/managers/DoNotDisturbManager.swift b/DynamicIsland/managers/DoNotDisturbManager.swift index 135a845..e323fed 100644 --- a/DynamicIsland/managers/DoNotDisturbManager.swift +++ b/DynamicIsland/managers/DoNotDisturbManager.swift @@ -342,6 +342,24 @@ enum FocusModeType: String, CaseIterable { } } + var internalSymbolName: String? { + switch self { + case .work: return "person.lanyardcard.fill" + case .mindfulness: return "apple.mindfulness" + case .gaming: return "rocket.fill" + default: return nil + } + } + + var activeIcon: Image { + if let internalSymbolName, + let image = Image(internalSystemName: internalSymbolName) { + return image + } + + return Image(systemName: sfSymbol) + } + var accentColor: Color { switch self { case .doNotDisturb: From cb57da47e25d92aaee3d574a634c58d10ba7fd2e Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Fri, 14 Nov 2025 11:36:08 +0530 Subject: [PATCH 4/5] Support for custom focus --- .../components/Focus/DoNotDisturbLiveActivity.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift b/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift index 962111e..a4b3f9e 100644 --- a/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift +++ b/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift @@ -66,7 +66,13 @@ struct DoNotDisturbLiveActivity: View { private var desiredLabelWidth: CGFloat { let measuredWidth = labelIntrinsicWidth + 8 // horizontal padding inside the label let fallbackWidth = max(vm.closedNotchSize.width * 0.52, 136) - return max(measuredWidth, fallbackWidth) + var width = max(measuredWidth, fallbackWidth) + + if focusMode == .doNotDisturb && shouldShowLabel { + width = max(width, 164) + } + + return width } private var shouldShowLabel: Bool { @@ -89,15 +95,12 @@ struct DoNotDisturbLiveActivity: View { private var labelText: String { let trimmed = manager.currentFocusModeName.trimmingCharacters(in: .whitespacesAndNewlines) if !trimmed.isEmpty { - if focusMode == .doNotDisturb { - return "Focus" - } return trimmed } let fallback = focusMode.displayName if focusMode == .doNotDisturb { - return "Focus" + return "Do Not Disturb" } return fallback.isEmpty ? "Focus" : fallback } From 8de7d8b0334eaf778966c47e0b6592424e011cb9 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Fri, 14 Nov 2025 12:35:27 +0530 Subject: [PATCH 5/5] BugFix: Pulsating causing high energy usage --- .../components/Privacy/PrivacyLiveActivity.swift | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/DynamicIsland/components/Privacy/PrivacyLiveActivity.swift b/DynamicIsland/components/Privacy/PrivacyLiveActivity.swift index 380c6f6..a28b992 100644 --- a/DynamicIsland/components/Privacy/PrivacyLiveActivity.swift +++ b/DynamicIsland/components/Privacy/PrivacyLiveActivity.swift @@ -110,8 +110,7 @@ struct PrivacyLiveActivity: View { // Individual privacy icon component struct PrivacyIcon: View { let type: PrivacyIndicatorType - @State private var isPulsing = false - + // Color based on type: camera = #26CC41, mic = #FF9402 private var iconColor: Color { type == .camera ? Color(red: 0.152, green: 0.804, blue: 0.256) : Color(red: 1.000, green: 0.584, blue: 0.010) @@ -123,12 +122,7 @@ struct PrivacyIcon: View { .font(.system(size: 13, weight: .semibold)) .foregroundColor(iconColor) .frame(width: 24, height: 24) - .scaleEffect(isPulsing ? 1.1 : 1.0) - .opacity(isPulsing ? 1.0 : 0.85) - .onAppear { - withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { - isPulsing = true - } - } + .scaleEffect(1.0) + .opacity(1.0) } }