[2026-04-28] Add apple-browsers .cursor rules (43 files) + executor v2 context
This commit is contained in:
@@ -0,0 +1,424 @@
|
||||
---
|
||||
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/macos-system-integration.mdc
|
||||
confidence: 0.9
|
||||
namespace: work
|
||||
last_synced: 2026-04-28
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# macOS System Integration Patterns
|
||||
|
||||
## Background Agents and Services
|
||||
Use proper service management for background agents:
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT - Background service management
|
||||
final class BackgroundServiceManager {
|
||||
private let agentIdentifier = "com.duckduckgo.agent"
|
||||
private let extensionIdentifier = "com.duckduckgo.extension"
|
||||
|
||||
func registerBackgroundAgent() throws {
|
||||
let service = SMAppService.agent(plistName: "BackgroundAgent.plist")
|
||||
|
||||
do {
|
||||
try service.register()
|
||||
print("Background agent registered successfully")
|
||||
} catch {
|
||||
print("Failed to register background agent: \(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func unregisterBackgroundAgent() throws {
|
||||
let service = SMAppService.agent(plistName: "BackgroundAgent.plist")
|
||||
|
||||
do {
|
||||
try service.unregister()
|
||||
print("Background agent unregistered successfully")
|
||||
} catch {
|
||||
print("Failed to unregister background agent: \(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func checkServiceStatus() -> SMAppService.Status {
|
||||
let service = SMAppService.agent(plistName: "BackgroundAgent.plist")
|
||||
return service.status
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ INCORRECT - Direct background processing in main app
|
||||
final class FeatureManager {
|
||||
func startBackgroundWork() {
|
||||
// Don't run continuous background work in main app
|
||||
DispatchQueue.global().async {
|
||||
while true {
|
||||
// This will drain battery and violate sandboxing
|
||||
self.performWork()
|
||||
Thread.sleep(forTimeInterval: 60)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## System Extensions
|
||||
Use proper system extension lifecycle management:
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT - System extension management
|
||||
import SystemExtensions
|
||||
|
||||
final class SystemExtensionManager: NSObject {
|
||||
private let extensionIdentifier = "com.duckduckgo.network-extension"
|
||||
|
||||
func installExtension() {
|
||||
let request = OSSystemExtensionRequest.activationRequest(
|
||||
forExtensionWithIdentifier: extensionIdentifier,
|
||||
queue: .main
|
||||
)
|
||||
request.delegate = self
|
||||
OSSystemExtensionManager.shared.submitRequest(request)
|
||||
}
|
||||
|
||||
func uninstallExtension() {
|
||||
let request = OSSystemExtensionRequest.deactivationRequest(
|
||||
forExtensionWithIdentifier: extensionIdentifier,
|
||||
queue: .main
|
||||
)
|
||||
request.delegate = self
|
||||
OSSystemExtensionManager.shared.submitRequest(request)
|
||||
}
|
||||
|
||||
func checkExtensionStatus() async -> OSSystemExtensionRequest.Result? {
|
||||
// Check if extension is already installed
|
||||
return await withCheckedContinuation { continuation in
|
||||
let request = OSSystemExtensionRequest.propertiesRequest(
|
||||
forExtensionWithIdentifier: extensionIdentifier,
|
||||
queue: .main
|
||||
)
|
||||
|
||||
// Handle the properties request to determine status
|
||||
// Implementation details...
|
||||
continuation.resume(returning: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - OSSystemExtensionRequestDelegate
|
||||
extension SystemExtensionManager: OSSystemExtensionRequestDelegate {
|
||||
func request(
|
||||
_ request: OSSystemExtensionRequest,
|
||||
actionForReplacingExtension existing: OSSystemExtensionProperties,
|
||||
withExtension extension: OSSystemExtensionProperties
|
||||
) -> OSSystemExtensionRequest.ReplacementAction {
|
||||
return .replace
|
||||
}
|
||||
|
||||
func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {
|
||||
print("System extension requires user approval")
|
||||
// Show UI to guide user through approval process
|
||||
showUserApprovalGuidance()
|
||||
}
|
||||
|
||||
func request(
|
||||
_ request: OSSystemExtensionRequest,
|
||||
didFinishWithResult result: OSSystemExtensionRequest.Result
|
||||
) {
|
||||
switch result {
|
||||
case .completed:
|
||||
print("System extension request completed successfully")
|
||||
handleExtensionActivated()
|
||||
case .willCompleteAfterReboot:
|
||||
print("System extension will be activated after reboot")
|
||||
showRebootRequiredMessage()
|
||||
@unknown default:
|
||||
print("Unknown system extension result: \(result)")
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) {
|
||||
print("System extension request failed: \(error)")
|
||||
handleExtensionError(error)
|
||||
}
|
||||
|
||||
private func showUserApprovalGuidance() {
|
||||
// Show UI to guide user through System Preferences
|
||||
}
|
||||
|
||||
private func handleExtensionActivated() {
|
||||
// Update UI to reflect extension is active
|
||||
}
|
||||
|
||||
private func showRebootRequiredMessage() {
|
||||
// Show UI indicating reboot is required
|
||||
}
|
||||
|
||||
private func handleExtensionError(_ error: Error) {
|
||||
// Handle extension installation errors
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Login Items Management
|
||||
Use the modern SMAppService API for login items:
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT - Modern login items API
|
||||
import ServiceManagement
|
||||
|
||||
final class LoginItemsManager {
|
||||
func enableLoginItem() throws {
|
||||
do {
|
||||
try SMAppService.mainApp.register()
|
||||
print("Login item enabled successfully")
|
||||
} catch {
|
||||
print("Failed to enable login item: \(error)")
|
||||
throw LoginItemError.registrationFailed(error)
|
||||
}
|
||||
}
|
||||
|
||||
func disableLoginItem() throws {
|
||||
do {
|
||||
try SMAppService.mainApp.unregister()
|
||||
print("Login item disabled successfully")
|
||||
} catch {
|
||||
print("Failed to disable login item: \(error)")
|
||||
throw LoginItemError.unregistrationFailed(error)
|
||||
}
|
||||
}
|
||||
|
||||
var isLoginItemEnabled: Bool {
|
||||
return SMAppService.mainApp.status == .enabled
|
||||
}
|
||||
|
||||
var loginItemStatus: SMAppService.Status {
|
||||
return SMAppService.mainApp.status
|
||||
}
|
||||
}
|
||||
|
||||
enum LoginItemError: LocalizedError {
|
||||
case registrationFailed(Error)
|
||||
case unregistrationFailed(Error)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .registrationFailed(let error):
|
||||
return "Failed to register login item: \(error.localizedDescription)"
|
||||
case .unregistrationFailed(let error):
|
||||
return "Failed to unregister login item: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ INCORRECT - Deprecated APIs
|
||||
final class OldLoginItemsManager {
|
||||
func enableLoginItem() {
|
||||
// Don't use deprecated LSSharedFileList APIs
|
||||
let loginItems = LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems, nil)
|
||||
// ... deprecated implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workspace Integration
|
||||
Integrate properly with macOS workspace:
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT - Workspace integration
|
||||
final class WorkspaceIntegration {
|
||||
func openFileInFinder(at url: URL) {
|
||||
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: url.path)
|
||||
}
|
||||
|
||||
func revealInFinder(fileAt url: URL) {
|
||||
NSWorkspace.shared.selectFile(url.path, inFileViewerRootedAtPath: url.deletingLastPathComponent().path)
|
||||
}
|
||||
|
||||
func openWithDefaultApplication(url: URL) {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
|
||||
func openWithApplication(url: URL, applicationURL: URL) {
|
||||
NSWorkspace.shared.open([url], withApplicationAt: applicationURL, configuration: NSWorkspace.OpenConfiguration())
|
||||
}
|
||||
|
||||
func getDefaultApplication(for url: URL) -> URL? {
|
||||
return NSWorkspace.shared.urlForApplication(toOpen: url)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dock Integration
|
||||
Handle dock interactions properly:
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT - Dock integration
|
||||
final class DockIntegration {
|
||||
func setBadgeCount(_ count: Int) {
|
||||
NSApp.dockTile.badgeLabel = count > 0 ? "\(count)" : nil
|
||||
}
|
||||
|
||||
func clearBadge() {
|
||||
NSApp.dockTile.badgeLabel = nil
|
||||
}
|
||||
|
||||
func setDockMenu(_ menu: NSMenu) {
|
||||
NSApp.dockTile.contentView = nil
|
||||
NSApp.dockTile.showsApplicationBadge = true
|
||||
// Custom dock menu would be set through app delegate
|
||||
}
|
||||
}
|
||||
|
||||
// In AppDelegate
|
||||
extension AppDelegate: NSApplicationDelegate {
|
||||
func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
|
||||
let dockMenu = NSMenu()
|
||||
|
||||
dockMenu.addItem(NSMenuItem(
|
||||
title: "New Window",
|
||||
action: #selector(newWindow),
|
||||
keyEquivalent: ""
|
||||
))
|
||||
|
||||
dockMenu.addItem(NSMenuItem(
|
||||
title: "New Private Window",
|
||||
action: #selector(newPrivateWindow),
|
||||
keyEquivalent: ""
|
||||
))
|
||||
|
||||
return dockMenu
|
||||
}
|
||||
|
||||
@objc func newWindow() {
|
||||
WindowsManager.openNewWindow()
|
||||
}
|
||||
|
||||
@objc func newPrivateWindow() {
|
||||
WindowsManager.openNewWindow(burnerMode: .burner)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notification Center Integration
|
||||
Handle notifications properly:
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT - User notification handling
|
||||
import UserNotifications
|
||||
|
||||
final class NotificationManager: NSObject {
|
||||
func requestNotificationPermission() async -> Bool {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
|
||||
do {
|
||||
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
|
||||
return granted
|
||||
} catch {
|
||||
print("Failed to request notification permission: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func scheduleNotification(title: String, body: String, identifier: String) async {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = title
|
||||
content.body = body
|
||||
content.sound = .default
|
||||
|
||||
let request = UNNotificationRequest(
|
||||
identifier: identifier,
|
||||
content: content,
|
||||
trigger: nil
|
||||
)
|
||||
|
||||
do {
|
||||
try await UNUserNotificationCenter.current().add(request)
|
||||
} catch {
|
||||
print("Failed to schedule notification: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UNUserNotificationCenterDelegate
|
||||
extension NotificationManager: UNUserNotificationCenterDelegate {
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
withCompletionHandler completionHandler: @escaping () -> Void
|
||||
) {
|
||||
// Handle notification tap
|
||||
handleNotificationResponse(response)
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
||||
) {
|
||||
// Show notification even when app is in foreground
|
||||
completionHandler([.banner, .sound])
|
||||
}
|
||||
|
||||
private func handleNotificationResponse(_ response: UNNotificationResponse) {
|
||||
// Handle different notification actions
|
||||
switch response.actionIdentifier {
|
||||
case UNNotificationDefaultActionIdentifier:
|
||||
// User tapped the notification
|
||||
break
|
||||
case UNNotificationDismissActionIdentifier:
|
||||
// User dismissed the notification
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## App Group UserDefaults
|
||||
Use app group UserDefaults for settings shared with system extensions:
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT - App group UserDefaults
|
||||
extension AppUserDefaults {
|
||||
private static let appGroupUserDefaults = UserDefaults(suiteName: "group.com.duckduckgo.app")
|
||||
|
||||
var networkProtectionEnabled: Bool {
|
||||
get {
|
||||
appGroupUserDefaults?.bool(forKey: "network_protection_enabled") ?? false
|
||||
}
|
||||
set {
|
||||
appGroupUserDefaults?.set(newValue, forKey: "network_protection_enabled")
|
||||
// Notify system extension of change
|
||||
notifySystemExtension(of: .networkProtectionToggled(newValue))
|
||||
}
|
||||
}
|
||||
|
||||
var vpnServerLocation: String? {
|
||||
get {
|
||||
appGroupUserDefaults?.string(forKey: "vpn_server_location")
|
||||
}
|
||||
set {
|
||||
appGroupUserDefaults?.set(newValue, forKey: "vpn_server_location")
|
||||
}
|
||||
}
|
||||
|
||||
private func notifySystemExtension(of change: SystemExtensionNotification) {
|
||||
// Send notification to system extension via app group communication
|
||||
let notificationName = "com.duckduckgo.settings.changed"
|
||||
DistributedNotificationCenter.default().post(
|
||||
name: Notification.Name(notificationName),
|
||||
object: change.rawValue
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum SystemExtensionNotification: String {
|
||||
case networkProtectionToggled = "network_protection_toggled"
|
||||
case vpnServerChanged = "vpn_server_changed"
|
||||
}
|
||||
```
|
||||
|
||||
See `macos-window-management.md` for window management patterns and `macos-preferences.md` for preferences UI patterns.
|
||||
Reference in New Issue
Block a user