/* * Atoll (DynamicIsland) * Copyright (C) 2024-2026 Atoll Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import AppKit import Combine import Defaults import Foundation import SwiftUI final class DoNotDisturbManager: ObservableObject { static let shared = DoNotDisturbManager() @Published private(set) var isMonitoring = false @Published var isDoNotDisturbActive = false @Published var currentFocusModeName: String = "" @Published var currentFocusModeIdentifier: String = "" // Used by the brief-toast UI to show an ON toast when the active mode switches while Focus stays enabled. @Published var focusToastTrigger: UUID = UUID() private let notificationCenter = DistributedNotificationCenter.default() private let metadataExtractionQueue = DispatchQueue(label: "com.dynamicisland.focus.metadata", qos: .userInitiated) private let pollingQueue = DispatchQueue(label: "com.dynamicisland.focus.polling", qos: .utility) private let focusLogStream = FocusLogStream() private let assertionsURL = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/DoNotDisturb/DB/Assertions.json") private var pollingSource: DispatchSourceTimer? private var lastAssertionsModificationDate: Date? private var modeCancellable: AnyCancellable? @Published private(set) var monitoringMode: FocusMonitoringMode = Defaults[.focusMonitoringMode] private init() { focusLogStream.onMetadataUpdate = { [weak self] identifier, name in self?.handleLogMetadataUpdate(identifier: identifier, name: name) } modeCancellable = Defaults.publisher(.focusMonitoringMode, options: []) .sink { [weak self] change in guard let self else { return } DispatchQueue.main.async { self.monitoringMode = change.newValue } self.applyMonitoringModeChange(change.newValue) } } deinit { stopMonitoring() } func startMonitoring() { guard !isMonitoring else { return } notificationCenter.addObserver( self, selector: #selector(handleFocusEnabled(_:)), name: .focusModeEnabled, object: nil, suspensionBehavior: .deliverImmediately ) notificationCenter.addObserver( self, selector: #selector(handleFocusDisabled(_:)), name: .focusModeDisabled, object: nil, suspensionBehavior: .deliverImmediately ) isMonitoring = true applyMonitoringModeChange(Defaults[.focusMonitoringMode]) } func stopMonitoring() { guard isMonitoring else { return } notificationCenter.removeObserver(self, name: .focusModeEnabled, object: nil) notificationCenter.removeObserver(self, name: .focusModeDisabled, object: nil) focusLogStream.stop() stopAssertionsPolling() isMonitoring = false DispatchQueue.main.async { self.isDoNotDisturbActive = false self.currentFocusModeIdentifier = "" self.currentFocusModeName = "" } } @objc private func handleFocusEnabled(_ notification: Notification) { apply(notification: notification, isActive: true) } @objc private func handleFocusDisabled(_ notification: Notification) { apply(notification: notification, isActive: false) } private func apply(notification: Notification, isActive: Bool) { metadataExtractionQueue.async { [weak self] in 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) } } private func publishMetadata(identifier: String?, name: String?, isActive: Bool?, source: String) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedName = name?.trimmingCharacters(in: .whitespacesAndNewlines) let isGenericFocusIdentifier = (trimmedIdentifier?.lowercased() == "com.apple.focus") let usableIdentifier = (trimmedIdentifier?.isEmpty == false && !isGenericFocusIdentifier) ? trimmedIdentifier : nil let usableName = (trimmedName?.isEmpty == false) ? trimmedName : nil let previousIdentifier = self.currentFocusModeIdentifier let previousName = self.currentFocusModeName let previousActive = self.isDoNotDisturbActive // When neither identifier nor name is available, only update the active // state without overwriting existing mode metadata. This prevents the // assertions-poll (which often lacks metadata) from reverting a correct // mode set by the log-stream or notification handler. if usableIdentifier == nil && usableName == nil { if let isActive = isActive, isActive != previousActive { withAnimation(.smooth(duration: 0.25)) { self.isDoNotDisturbActive = isActive } // First activation with no prior mode: default to DND. if isActive && previousIdentifier.isEmpty { self.currentFocusModeIdentifier = FocusModeType.doNotDisturb.rawValue self.currentFocusModeName = FocusModeType.doNotDisturb.displayName } debugPrint("[DoNotDisturbManager] Focus active-only update -> source: \(source) | isActive: \(isActive)") } return } let resolvedMode = FocusModeType.resolve(identifier: usableIdentifier, name: usableName) let finalIdentifier: String = usableIdentifier ?? resolvedMode.rawValue // Compute display name let finalName: String if resolvedMode == .custom, !FullDiskAccessAuthorization.hasPermission() { finalName = "Focus" } else if resolvedMode == .custom, FullDiskAccessAuthorization.hasPermission() { // With FDA, prefer the real name from ModeConfigurations.json (fixes slug names like graduationcap.fill). let lookedUp = FocusMetadataReader.shared.getDisplayName(for: trimmedName ?? "", identifier: finalIdentifier) finalName = lookedUp.isEmpty ? "Focus" : lookedUp } else if let name = trimmedName, !name.isEmpty { let lower = name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() switch lower { case "work": finalName = "Work" case "personal", "personal-time": finalName = "Personal" case "reduce-interruptions": finalName = "Reduce Interruptions" case "sleep", "sleep-mode": finalName = "Sleep" case "driving": finalName = "Driving" case "default", "dnd", "do-not-disturb", "do not disturb", "donotdisturb": finalName = "Do Not Disturb" default: finalName = name } } else if !resolvedMode.displayName.isEmpty { finalName = resolvedMode.displayName } else { finalName = "Focus" } let identifierChanged = finalIdentifier != previousIdentifier let nameChanged = finalName != previousName let shouldToggleActive = isActive.map { $0 != previousActive } ?? false if identifierChanged { self.currentFocusModeIdentifier = finalIdentifier } if nameChanged { self.currentFocusModeName = finalName.localizedCaseInsensitiveContains("Reduce Interruptions") ? "Reduce Interr." : finalName } // If Focus remains active and the mode switches (e.g., DND -> Sleep), trigger an ON toast for the new mode. if isActive == nil, previousActive == true, identifierChanged { self.focusToastTrigger = UUID() } 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 } // If Focus turned OFF, clear cached mode metadata after the OFF toast can use it. if isActive == false { self.currentFocusModeIdentifier = previousIdentifier self.currentFocusModeName = previousName } } } 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) 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", "focusModeIdentifier", "FocusModeUUID", "focusModeUUID", "UUID", "uuid", "identifier", "Identifier" ] let nameKeys = [ "FocusModeName", "focusModeName", "FocusMode", "focusMode", "displayName", "display_name", "name", "Name" ] 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 } } } return (name, identifier) } } private extension Notification.Name { static let focusModeEnabled = Notification.Name("_NSDoNotDisturbEnabledNotification") static let focusModeDisabled = Notification.Name("_NSDoNotDisturbDisabledNotification") } private extension DoNotDisturbManager { func applyMonitoringModeChange(_ mode: FocusMonitoringMode) { guard isMonitoring else { return } if mode == .useDevTools { stopAssertionsPolling() focusLogStream.start() } else { focusLogStream.stop() startAssertionsPolling() } } func startAssertionsPolling() { stopAssertionsPolling() lastAssertionsModificationDate = nil let timer = DispatchSource.makeTimerSource(queue: pollingQueue) timer.schedule(deadline: .now() + .seconds(1), repeating: .seconds(2), leeway: .milliseconds(250)) timer.setEventHandler { [weak self] in self?.pollAssertionsState() } timer.resume() pollingSource = timer } func stopAssertionsPolling() { pollingSource?.cancel() pollingSource = nil lastAssertionsModificationDate = nil } func pollAssertionsState() { guard FullDiskAccessAuthorization.hasPermission() else { return } if let attributes = try? FileManager.default.attributesOfItem(atPath: assertionsURL.path), let modifiedAt = attributes[.modificationDate] as? Date, let lastObservedModificationDate = lastAssertionsModificationDate, modifiedAt <= lastObservedModificationDate { return } guard let data = try? Data(contentsOf: assertionsURL), !data.isEmpty, let root = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any], let dataArray = root["data"] as? [[String: Any]], let firstItem = dataArray.first else { return } let assertions = (firstItem["storeAssertionRecords"] as? [Any]) ?? [] let isActive = !assertions.isEmpty if let attributes = try? FileManager.default.attributesOfItem(atPath: assertionsURL.path), let modifiedAt = attributes[.modificationDate] as? Date { lastAssertionsModificationDate = modifiedAt } let identifierKeys = [ "modeIdentifier", "FocusModeIdentifier", "focusModeIdentifier", "identifier", "Identifier", "focusModeUUID", "UUID", "uuid" ] let nameKeys = [ "activityDisplayName", "displayName", "FocusModeName", "focusModeName", "focusMode", "name", "Name" ] let identifier = isActive ? firstMatch(for: identifierKeys, in: assertions) : nil let name = isActive ? firstMatch(for: nameKeys, in: assertions) : nil publishMetadata(identifier: identifier, name: name, isActive: isActive, source: "assertions-poll") } } // MARK: - Focus Mode Types enum FocusModeType: String, CaseIterable { case doNotDisturb = "com.apple.donotdisturb.mode" case work = "com.apple.focus.work" case personal = "com.apple.focus.personal" case sleep = "com.apple.focus.sleep" case driving = "com.apple.focus.driving" case fitness = "com.apple.focus.fitness" case gaming = "com.apple.focus.gaming" case mindfulness = "com.apple.focus.mindfulness" case reading = "com.apple.focus.reading" case reduceInterruptions = "com.apple.focus.reduce-interruptions" case custom = "com.apple.focus.custom" case unknown = "" var displayName: String { switch self { case .doNotDisturb: return "Do Not Disturb" case .work: return "Work" case .personal: return "Personal" case .sleep: return "Sleep" case .driving: return "Driving" case .fitness: return "Fitness" case .gaming: return "Gaming" case .mindfulness: return "Mindfulness" case .reading: return "Reading" case .reduceInterruptions: return "Reduce Interr." case .custom: return "Focus" case .unknown: return "Focus Mode" } } var sfSymbol: String { switch self { case .doNotDisturb: return "moon.fill" case .work: return "briefcase.fill" case .personal: return "person.fill" case .sleep: return "bed.double.fill" case .driving: return "car.fill" case .fitness: return "figure.run" case .gaming: return "gamecontroller.fill" case .mindfulness: return "circle.hexagongrid" case .reading: return "book.closed.fill" case .reduceInterruptions: return "apple.intelligence" case .custom: return "app.badge" case .unknown: return "moon.fill" } } 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 { resolvedActiveIcon() } func resolvedActiveIcon(usePrivateSymbol: Bool = true) -> Image { if usePrivateSymbol, let internalSymbolName, let image = Image(internalSystemName: internalSymbolName) { return image } return Image( systemName: self == .custom ? self.getCustomIconFromFile() : sfSymbol ) } var accentColor: Color { switch self { case .doNotDisturb: return Color(red: 0.370, green: 0.360, blue: 0.902) case .work: return Color(red: 0.414, green: 0.769, blue: 0.863, opacity: 1.0) case .personal: 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: return Color(red: 0.988, green: 0.561, blue: 0.153) case .fitness: return Color(red: 0.176, green: 0.804, blue: 0.459) case .gaming: return Color(red: 0.043, green: 0.518, blue: 1.000, opacity: 1.0) case .mindfulness: return Color(red: 0.361, green: 0.898, blue: 0.883, opacity: 1.0) case .reading: return Color(red: 1.000, green: 0.622, blue: 0.044, opacity: 1.0) case .reduceInterruptions: return Color(red: 0.686, green: 0.322, blue: 0.871, opacity: 1.0) case .custom: return self.getCustomAccentColorFromFile() case .unknown: return Color(red: 0.370, green: 0.360, blue: 0.902) } } var inactiveSymbol: String { switch self { case .doNotDisturb: return "moon.circle.fill" default: return sfSymbol } } } extension FocusModeType { init(identifier: String) { let normalized = identifier.trimmingCharacters(in: .whitespacesAndNewlines) let normalizedLowercased = normalized.lowercased() guard !normalized.isEmpty else { self = .doNotDisturb return } // 1. Exact rawValue match (e.g., "com.apple.focus.work", "com.apple.donotdisturb.mode"). if let direct = FocusModeType(rawValue: normalized) ?? FocusModeType(rawValue: normalizedLowercased) { self = direct return } // 2. macOS 26.3 custom Focus modes use `com.apple.donotdisturb.mode.`. // Must be checked BEFORE the generic prefix match, otherwise the prefix // `com.apple.donotdisturb.mode` would incorrectly resolve to .doNotDisturb. if normalizedLowercased.hasPrefix("com.apple.donotdisturb.mode.") { let suffix = String(normalizedLowercased.dropFirst("com.apple.donotdisturb.mode.".count)) if !suffix.isEmpty && suffix != "default" { self = .custom return } } // 3. Prefix match for known built-in modes (e.g., com.apple.focus.personal-time -> .personal). if let resolved = FocusModeType.allCases.first(where: { guard !$0.rawValue.isEmpty else { return false } return normalized.hasPrefix($0.rawValue) || normalizedLowercased.hasPrefix($0.rawValue) }) { self = resolved return } // 4. Anything else under com.apple.focus is custom. if normalizedLowercased.hasPrefix("com.apple.focus") { self = .custom return } self = .doNotDisturb } static func resolve(identifier: String?, name: String?) -> FocusModeType { if let name, !name.isEmpty { if let match = FocusModeType.allCases.first(where: { guard !$0.displayName.isEmpty else { return false } return $0.displayName.compare(name, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame }) { return match } // Also try matching the name against known raw identifiers (e.g., a log line may emit "work" or "sleep" as the name). let lower = name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() switch lower { case "work": return .work case "personal", "personal-time": return .personal case "sleep", "sleep-mode": return .sleep case "driving": return .driving case "fitness": return .fitness case "gaming": return .gaming case "mindfulness": return .mindfulness case "reading": return .reading case "reduce-interruptions", "reduce interruptions": return .reduceInterruptions case "do not disturb", "dnd", "donotdisturb", "do-not-disturb", "default": return .doNotDisturb default: break } } if let identifier, !identifier.isEmpty { return FocusModeType(identifier: identifier) } return .doNotDisturb } func getCustomIconFromFile() -> String { return FocusMetadataReader.shared.getIcon( for: DoNotDisturbManager.shared.currentFocusModeName, identifier: DoNotDisturbManager.shared.currentFocusModeIdentifier ) } func getCustomAccentColorFromFile() -> Color { return FocusMetadataReader.shared.getAccentColor( for: DoNotDisturbManager.shared.currentFocusModeName, identifier: DoNotDisturbManager.shared.currentFocusModeIdentifier ) } } // MARK: - Metadata helpers private extension DoNotDisturbManager { func firstMatch(for keys: [String], in value: Any) -> String? { if let dictionary = value as? [AnyHashable: Any] { for key in keys { if let candidate = dictionary[key], let string = normalizedString(from: candidate) { return string } } for nestedValue in dictionary.values { if let nestedMatch = firstMatch(for: keys, in: nestedValue) { return nestedMatch } } } else if let array = value as? [Any] { for element in array { if let nestedMatch = firstMatch(for: keys, in: element) { return nestedMatch } } } return nil } func normalizedString(from value: Any) -> String? { switch value { case let string as String: let cleaned = FocusMetadataDecoder.cleanedString(string) return cleaned.isEmpty ? nil : cleaned case let number as NSNumber: 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) { let cleaned = FocusMetadataDecoder.cleanedString(string) return cleaned.isEmpty ? nil : cleaned } return nil case let dict as [AnyHashable: Any]: // Attempt to pull common keys from nested dictionaries if let nested = firstMatch(for: ["identifier", "Identifier", "uuid", "UUID"], in: dict) { return nested } if let name = firstMatch(for: ["name", "Name", "displayName", "display_name"], in: dict) { return name } return nil default: 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 } } } 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 var didTerminate = 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", "--no-backtrace", "--style", "compact", "--level", "info", "--predicate", "process == \"duetexpertd\" AND eventMessage CONTAINS \"semanticModeIdentifier\"" ] 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 self.didTerminate = false debugPrint("[FocusLogStream] Started unified log tail for duetexpertd/donotdisturbd Focus metadata") } 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.lowercased().contains("error") && trimmed.lowercased().contains("predicate") { debugPrint("[FocusLogStream] log stream error: \(trimmed)") } // Clear only when logs explicitly indicate no active mode (helps avoid wiping state during transitions). if trimmed.contains("active mode assertion: (null)") || trimmed.contains("activeModeIdentifier: (null)") { clearMetadata() return } var updatedIdentifier: String? var updatedName: String? // Special-case parsing for donotdisturbd logs which include a full DNDMode description. // Example: if trimmed.contains(" String? { guard let r = trimmed.range(of: key) else { return nil } let suffix = trimmed[r.upperBound...] guard let end = suffix.range(of: ";") else { return nil } let value = suffix[..\\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 } } private final class FocusMetadataReader { private let pathToDatabase:URL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/DoNotDisturb/DB/ModeConfigurations.json") struct DNDConfigRoot: Codable { let data: [DNDDataEntry] } struct DNDDataEntry: Codable { let modeConfigurations: [String: DNDModeWrapper] } struct DNDModeWrapper: Codable { let mode: DNDMode } struct DNDMode: Codable { let name: String let modeIdentifier: String let symbolImageName: String? let tintColorName: String? } private init(){} static let shared = FocusMetadataReader() private func getModeConfig(for focusName: String, identifier: String? = nil) -> DNDMode? { guard FullDiskAccessAuthorization.hasPermission() else { return nil } let trimmedName = focusName.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines) do { let data = try Data(contentsOf: pathToDatabase) let root = try JSONDecoder().decode(DNDConfigRoot.self, from: data) for entry in root.data { for wrapper in entry.modeConfigurations.values { let mode = wrapper.mode if let id = trimmedIdentifier, !id.isEmpty, mode.modeIdentifier.compare(id, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame { return mode } if !trimmedName.isEmpty, mode.name.compare(trimmedName, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame { return mode } } } } catch { print("ModeConfigurations.json decode error: \(error)") } return nil } func getDisplayName(for focus: String, identifier: String? = nil) -> String { guard let mode = getModeConfig(for: focus, identifier: identifier) else { return "" } return mode.name } /// Fetch the icon for the current focus from disk. If the focus is not found return the placeholder `app.badge` /// - Returns A string representing the sfSymbol of the current focus func getIcon(for focus: String, identifier: String? = nil) -> String { guard let mode = getModeConfig(for: focus, identifier: identifier) else { return "app.badge" } return mode.symbolImageName ?? "app.badge" } /// Fetch the accent color for the current focus from disk. If the focus is not found return the placeholder `Color.indigo` /// - Returns A Color representing the accent color for the current focus func getAccentColor(for focus: String, identifier: String? = nil) -> Color { guard let mode = getModeConfig(for: focus, identifier: identifier), let colorName = mode.tintColorName else { return .indigo } return Color.stringToColor(for: colorName) } } extension Color { static func stringToColor(for string:String) -> Color { let cleanName = string.lowercased() .replacingOccurrences(of: "system", with: "") .replacingOccurrences(of: "color", with: "") switch cleanName { case "red": return .red case "orange": return .orange case "yellow": return .yellow case "green": return .green case "mint": return .mint case "teal": return .teal case "cyan": return .cyan case "blue": return .blue case "indigo": return .indigo case "purple": return .purple case "pink": return .pink case "gray", "grey": return .gray default: return .indigo } } }