Fix: DevTools Access

This commit is contained in:
Hariharan Mudaliar 2026-02-21 22:53:16 +05:30
commit 53a9fdb112
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
5 changed files with 332 additions and 3 deletions

View file

@ -70,6 +70,7 @@ struct DynamicNotchApp: App {
}
}
@CommandsBuilder
var commands: some Commands {
CommandGroup(replacing: .appSettings) {
Button("Settings…") {
@ -116,6 +117,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private var cancellables = Set<AnyCancellable>()
private var windowsHiddenForLock = false
private var optionalShortcutHandlersRegistered = false
private weak var focusWithoutDevToolsMenuItem: NSMenuItem?
private weak var focusUseDevToolsMenuItem: NSMenuItem?
// Debouncing mechanism for window size updates
private var windowSizeUpdateWorkItem: DispatchWorkItem?
@ -147,6 +150,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
return false
}
func applicationDidBecomeActive(_ notification: Notification) {
installTopMenuItemsIfNeeded()
}
func applicationWillTerminate(_ notification: Notification) {
// Cancel any pending window size updates
windowSizeUpdateWorkItem?.cancel()
@ -370,6 +377,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
idleAnimationManager.initializeDefaultAnimations()
applySelectedAppIcon()
installTopMenuItemsIfNeeded()
Defaults.publisher(.focusMonitoringMode, options: [])
.sink { [weak self] _ in
self?.updateFocusMenuState()
}
.store(in: &cancellables)
// Setup SystemHUD Manager
SystemHUDManager.shared.setup(coordinator: coordinator)
@ -531,8 +545,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
forName: Notification.Name.automaticallySwitchDisplayChanged, object: nil, queue: nil
) { [weak self] _ in
guard let self = self, let window = self.window else { return }
window.alphaValue =
self.coordinator.selectedScreen == self.coordinator.preferredScreen ? 1 : 0
DispatchQueue.main.async {
window.alphaValue =
self.coordinator.selectedScreen == self.coordinator.preferredScreen ? 1 : 0
}
}
NotificationCenter.default.addObserver(
@ -644,6 +660,155 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
private func installTopMenuItemsIfNeeded() {
guard let mainMenu = NSApp.mainMenu else { return }
if mainMenu.items.contains(where: { $0.identifier?.rawValue == "Atoll.Focus.Menu" }) {
updateFocusMenuState()
return
}
let insertionIndex = preferredMenuInsertionIndex(in: mainMenu)
let focusMenuItem = NSMenuItem(title: "Focus", action: nil, keyEquivalent: "")
focusMenuItem.identifier = NSUserInterfaceItemIdentifier("Atoll.Focus.Menu")
let focusSubmenu = NSMenu(title: "Focus")
let withoutDevTools = NSMenuItem(
title: "Use without DevTools",
action: #selector(selectFocusWithoutDevTools),
keyEquivalent: ""
)
withoutDevTools.target = self
let useDevTools = NSMenuItem(
title: "Use DevTools",
action: #selector(selectFocusUseDevTools),
keyEquivalent: ""
)
useDevTools.target = self
focusSubmenu.addItem(withoutDevTools)
focusSubmenu.addItem(useDevTools)
focusMenuItem.submenu = focusSubmenu
mainMenu.insertItem(focusMenuItem, at: insertionIndex)
focusWithoutDevToolsMenuItem = withoutDevTools
focusUseDevToolsMenuItem = useDevTools
let accessibilityMenuItem = NSMenuItem(title: "Accessibility", action: nil, keyEquivalent: "")
accessibilityMenuItem.identifier = NSUserInterfaceItemIdentifier("Atoll.Accessibility.Menu")
let accessibilitySubmenu = NSMenu(title: "Accessibility")
let requestAccessibility = NSMenuItem(
title: "Request Accessibility Access",
action: #selector(requestAccessibilityAccess),
keyEquivalent: ""
)
requestAccessibility.target = self
let openAccessibility = NSMenuItem(
title: "Open Accessibility Settings",
action: #selector(openAccessibilitySettings),
keyEquivalent: ""
)
openAccessibility.target = self
accessibilitySubmenu.addItem(requestAccessibility)
accessibilitySubmenu.addItem(openAccessibility)
accessibilityMenuItem.submenu = accessibilitySubmenu
mainMenu.insertItem(accessibilityMenuItem, at: insertionIndex + 1)
let permissionsMenuItem = NSMenuItem(title: "Permissions", action: nil, keyEquivalent: "")
permissionsMenuItem.identifier = NSUserInterfaceItemIdentifier("Atoll.Permissions.Menu")
let permissionsSubmenu = NSMenu(title: "Permissions")
let requestFullDisk = NSMenuItem(
title: "Request Full Disk Access",
action: #selector(requestFullDiskAccess),
keyEquivalent: ""
)
requestFullDisk.target = self
let openFullDisk = NSMenuItem(
title: "Open Full Disk Access Settings",
action: #selector(openFullDiskAccessSettings),
keyEquivalent: ""
)
openFullDisk.target = self
let openDevTools = NSMenuItem(
title: "Open Developer Tools Settings",
action: #selector(openDeveloperToolsSettingsFromMenu),
keyEquivalent: ""
)
openDevTools.target = self
permissionsSubmenu.addItem(requestFullDisk)
permissionsSubmenu.addItem(openFullDisk)
permissionsSubmenu.addItem(NSMenuItem.separator())
permissionsSubmenu.addItem(openDevTools)
permissionsMenuItem.submenu = permissionsSubmenu
mainMenu.insertItem(permissionsMenuItem, at: insertionIndex + 2)
updateFocusMenuState()
}
private func preferredMenuInsertionIndex(in mainMenu: NSMenu) -> Int {
if let index = mainMenu.items.firstIndex(where: { $0.title == "Window" }) {
return index
}
if let index = mainMenu.items.firstIndex(where: { $0.title == "Help" }) {
return index
}
return max(mainMenu.numberOfItems, 0)
}
private func updateFocusMenuState() {
let mode = Defaults[.focusMonitoringMode]
focusWithoutDevToolsMenuItem?.state = mode == .withoutDevTools ? .on : .off
focusUseDevToolsMenuItem?.state = mode == .useDevTools ? .on : .off
}
@objc private func selectFocusWithoutDevTools() {
Defaults[.focusMonitoringMode] = .withoutDevTools
updateFocusMenuState()
}
@objc private func selectFocusUseDevTools() {
Defaults[.focusMonitoringMode] = .useDevTools
updateFocusMenuState()
}
@objc private func requestAccessibilityAccess() {
AccessibilityPermissionStore.shared.requestAuthorizationPrompt()
}
@objc private func openAccessibilitySettings() {
AccessibilityPermissionStore.shared.openSystemSettings()
}
@objc private func requestFullDiskAccess() {
FullDiskAccessPermissionStore.shared.requestAccessPrompt()
}
@objc private func openFullDiskAccessSettings() {
FullDiskAccessPermissionStore.shared.openSystemSettings()
}
@objc private func openDeveloperToolsSettingsFromMenu() {
let urls = [
"x-apple.systempreferences:com.apple.preference.security?Privacy_DevTools",
"x-apple.systempreferences:com.apple.preference.security"
]
for candidate in urls {
guard let url = URL(string: candidate) else { continue }
if NSWorkspace.shared.open(url) {
return
}
}
}
private func registerOptionalShortcutHandlers() {
guard !optionalShortcutHandlersRegistered else { return }
optionalShortcutHandlersRegistered = true

View file

@ -11006,6 +11006,10 @@
}
}
},
"Focus detection mode" : {
"comment" : "The title of a confirmation dialog that lets users choose between using the focus detection mode with or without the use of the SwiftUI Inspector (DevTools).",
"isCommentAutoGenerated" : true
},
"Focus Status" : {
"localizations" : {
"pt-BR" : {
@ -23164,6 +23168,9 @@
}
}
}
},
"This is optional. You can change it any time from the menu bar." : {
},
"This permanently removes every saved note and its attachments." : {
"localizations" : {
@ -24421,6 +24428,10 @@
}
}
},
"Use DevTools" : {
"comment" : "One option in the confirmation dialog in the `OnboardingView` that lets the user choose to use the developer tools for focus monitoring.",
"isCommentAutoGenerated" : true
},
"Use music visualizer spectrogram" : {
"localizations" : {
"ar" : {
@ -24543,6 +24554,10 @@
}
}
},
"Use without DevTools" : {
"comment" : "An option in the onboarding flow that lets users choose to use focus detection without the option provided by the developer tools.",
"isCommentAutoGenerated" : true
},
"Used" : {
"comment" : "A label describing the percentage of memory that is currently in use.",
"isCommentAutoGenerated" : true,

View file

@ -22,6 +22,7 @@
import SwiftUI
import AVFoundation
import Defaults
enum OnboardingStep {
case welcome
@ -36,6 +37,8 @@ private let calendarService = CalendarService()
struct OnboardingView: View {
@State private var step: OnboardingStep = .welcome
@State private var showFocusMonitoringChoice = false
@State private var didPresentFocusMonitoringChoice = false
let onFinish: () -> Void
let onOpenSettings: () -> Void
@ -120,6 +123,28 @@ struct OnboardingView: View {
}
}
.frame(width: 400, height: 600)
.onAppear {
guard !didPresentFocusMonitoringChoice else { return }
didPresentFocusMonitoringChoice = true
showFocusMonitoringChoice = true
}
.confirmationDialog(
"Focus detection mode",
isPresented: $showFocusMonitoringChoice,
titleVisibility: .visible
) {
Button("Use DevTools") {
Defaults[.focusMonitoringMode] = .useDevTools
}
Button("Use without DevTools") {
Defaults[.focusMonitoringMode] = .withoutDevTools
}
Button("Later", role: .cancel) {}
} message: {
Text("This is optional. You can change it any time from the menu bar.")
}
}
// MARK: - Permission Request Logic

View file

@ -32,12 +32,31 @@ final class DoNotDisturbManager: ObservableObject {
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 {
@ -63,7 +82,8 @@ final class DoNotDisturbManager: ObservableObject {
suspensionBehavior: .deliverImmediately
)
focusLogStream.start()
startAssertionsPolling()
applyMonitoringModeChange(Defaults[.focusMonitoringMode])
isMonitoring = true
}
@ -74,6 +94,7 @@ final class DoNotDisturbManager: ObservableObject {
notificationCenter.removeObserver(self, name: .focusModeDisabled, object: nil)
focusLogStream.stop()
stopAssertionsPolling()
isMonitoring = false
DispatchQueue.main.async {
@ -159,6 +180,8 @@ final class DoNotDisturbManager: ObservableObject {
}
private func handleLogMetadataUpdate(identifier: String?, name: String?) {
guard Defaults[.focusMonitoringMode] == .useDevTools else { return }
metadataExtractionQueue.async { [weak self] in
guard let self = self else { return }
let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines)
@ -308,6 +331,90 @@ private extension Notification.Name {
static let focusModeDisabled = Notification.Name("_NSDoNotDisturbDisabledNotification")
}
private extension DoNotDisturbManager {
func applyMonitoringModeChange(_ mode: FocusMonitoringMode) {
guard isMonitoring else { return }
if mode == .useDevTools {
focusLogStream.start()
} else {
focusLogStream.stop()
}
}
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 {

View file

@ -498,6 +498,22 @@ enum TimerProgressStyle: String, CaseIterable, Identifiable, Defaults.Serializab
}
}
enum FocusMonitoringMode: String, CaseIterable, Identifiable, Defaults.Serializable {
case withoutDevTools = "withoutDevTools"
case useDevTools = "useDevTools"
var id: String { rawValue }
var displayName: String {
switch self {
case .withoutDevTools:
return "Use without DevTools"
case .useDevTools:
return "Use DevTools"
}
}
}
enum ReminderPresentationStyle: String, CaseIterable, Identifiable, Defaults.Serializable {
case ringCountdown = "Ring"
case digital = "Digital"
@ -1012,6 +1028,7 @@ extension Defaults.Keys {
// MARK: Focus / Do Not Disturb Detection
static let enableDoNotDisturbDetection = Key<Bool>("enableDoNotDisturbDetection", default: true)
static let focusMonitoringMode = Key<FocusMonitoringMode>("focusMonitoringMode", default: .withoutDevTools)
static let showDoNotDisturbIndicator = Key<Bool>("showDoNotDisturbIndicator", default: true)
static let showDoNotDisturbLabel = Key<Bool>("showDoNotDisturbLabel", default: true)
static let focusIndicatorNonPersistent = Key<Bool>("focusIndicatorNonPersistent", default: false)