/*
* Atoll (DynamicIsland)
* Copyright (C) 2024-2026 Atoll Contributors
*
* Originally from boring.notch project
* Modified and adapted for Atoll (DynamicIsland)
* See NOTICE for details.
*
* 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 Foundation
import AppKit
import CoreGraphics
#if canImport(ApplicationServices)
import ApplicationServices
#endif
private let NX_SYSDEFINED_EVENT_TYPE: UInt32 = 14
private let NX_KEYTYPE_SOUND_UP: Int32 = 0
private let NX_KEYTYPE_SOUND_DOWN: Int32 = 1
private let NX_KEYTYPE_BRIGHTNESS_UP: Int32 = 2
private let NX_KEYTYPE_BRIGHTNESS_DOWN: Int32 = 3
private let NX_KEYTYPE_MUTE: Int32 = 7
enum MediaKeyDirection {
case up
case down
}
enum MediaKeyStep {
case standard
case fine
}
struct MediaKeyConfiguration {
var interceptVolume: Bool
var interceptBrightness: Bool
var interceptCommandModifiedBrightness: Bool
static let disabled = MediaKeyConfiguration(
interceptVolume: false,
interceptBrightness: false,
interceptCommandModifiedBrightness: false
)
}
protocol MediaKeyInterceptorDelegate: AnyObject {
func mediaKeyInterceptor(
_ interceptor: MediaKeyInterceptor,
didReceiveVolumeCommand direction: MediaKeyDirection,
step: MediaKeyStep,
isRepeat: Bool,
modifiers: NSEvent.ModifierFlags
)
func mediaKeyInterceptor(
_ interceptor: MediaKeyInterceptor,
didReceiveBrightnessCommand direction: MediaKeyDirection,
step: MediaKeyStep,
isRepeat: Bool,
modifiers: NSEvent.ModifierFlags
)
func mediaKeyInterceptorDidToggleMute(_ interceptor: MediaKeyInterceptor)
}
final class MediaKeyInterceptor {
static let shared = MediaKeyInterceptor()
weak var delegate: MediaKeyInterceptorDelegate?
var configuration: MediaKeyConfiguration = .disabled {
didSet {
updateTapState()
}
}
private var eventTap: CFMachPort?
private var runLoopSource: CFRunLoopSource?
private var isTapEnabled = false
#if canImport(ApplicationServices)
private var didRequestAccessibilityPrompt = false
#endif
private let systemDefinedEventType = CGEventType(rawValue: NX_SYSDEFINED_EVENT_TYPE)
private let eventTapLocations: [CGEventTapLocation] = [.cghidEventTap, .cgSessionEventTap]
private init() {}
@discardableResult
func start() -> Bool {
guard eventTap == nil else {
updateTapState()
return true
}
#if canImport(ApplicationServices)
requestAccessibilityPermissionIfNeeded()
#endif
guard let systemDefinedType = systemDefinedEventType else {
NSLog("❌ Unable to resolve system-defined event type")
return false
}
let mask = CGEventMask(1) << systemDefinedType.rawValue
let callback: CGEventTapCallBack = { _, type, cgEvent, userInfo in
guard let userInfo else { return Unmanaged.passUnretained(cgEvent) }
let interceptor = Unmanaged.fromOpaque(userInfo).takeUnretainedValue()
return interceptor.handleEvent(cgEvent: cgEvent, type: type)
}
var createdTap: CFMachPort?
for location in eventTapLocations {
if let tap = CGEvent.tapCreate(
tap: location,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: mask,
callback: callback,
userInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
) {
createdTap = tap
break
}
}
guard let tap = createdTap else {
#if canImport(ApplicationServices)
if !AXIsProcessTrusted() {
NSLog("⚠️ Accessibility permission missing; grant access in System Settings › Privacy & Security › Accessibility")
}
#endif
NSLog("❌ Failed to create media key event tap")
return false
}
eventTap = tap
runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
if let source = runLoopSource {
CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes)
}
CGEvent.tapEnable(tap: tap, enable: true)
isTapEnabled = true
NSLog("✅ Media key event tap installed (HID)")
return true
}
func stop() {
if let tap = eventTap {
CGEvent.tapEnable(tap: tap, enable: false)
CFMachPortInvalidate(tap)
}
if let source = runLoopSource {
CFRunLoopRemoveSource(CFRunLoopGetMain(), source, .commonModes)
}
eventTap = nil
runLoopSource = nil
isTapEnabled = false
}
private func updateTapState() {
guard let tap = eventTap else { return }
let shouldEnable = configuration.interceptVolume || configuration.interceptBrightness || configuration.interceptCommandModifiedBrightness
if shouldEnable != isTapEnabled {
CGEvent.tapEnable(tap: tap, enable: shouldEnable)
isTapEnabled = shouldEnable
}
}
private func handleEvent(cgEvent: CGEvent, type: CGEventType) -> Unmanaged? {
guard let systemDefinedType = systemDefinedEventType,
type == systemDefinedType,
let nsEvent = NSEvent(cgEvent: cgEvent),
nsEvent.subtype.rawValue == 8 else {
return Unmanaged.passUnretained(cgEvent)
}
let data1 = nsEvent.data1
let keyCode = (data1 & 0xFFFF0000) >> 16
let keyFlags = data1 & 0x0000FFFF
let keyState = ((keyFlags & 0xFF00) >> 8) == 0xA // 0xA = keyDown, 0xB = keyUp
let isRepeat = (keyFlags & 0x0001) == 1
let step = step(for: nsEvent)
let modifiers = nsEvent.modifierFlags
guard keyState else {
// Swallow key-up events only when intercepting, otherwise let them pass through
if shouldHandle(keyCode: Int32(keyCode), modifiers: modifiers) {
return nil
}
return Unmanaged.passUnretained(cgEvent)
}
switch Int32(keyCode) {
case NX_KEYTYPE_SOUND_UP:
guard configuration.interceptVolume else { return Unmanaged.passUnretained(cgEvent) }
delegate?.mediaKeyInterceptor(self, didReceiveVolumeCommand: .up, step: step, isRepeat: isRepeat, modifiers: modifiers)
return nil
case NX_KEYTYPE_SOUND_DOWN:
guard configuration.interceptVolume else { return Unmanaged.passUnretained(cgEvent) }
delegate?.mediaKeyInterceptor(self, didReceiveVolumeCommand: .down, step: step, isRepeat: isRepeat, modifiers: modifiers)
return nil
case NX_KEYTYPE_MUTE:
guard configuration.interceptVolume else { return Unmanaged.passUnretained(cgEvent) }
delegate?.mediaKeyInterceptorDidToggleMute(self)
return nil
case NX_KEYTYPE_BRIGHTNESS_UP:
guard shouldHandleBrightness(modifiers: modifiers) else { return Unmanaged.passUnretained(cgEvent) }
delegate?.mediaKeyInterceptor(self, didReceiveBrightnessCommand: .up, step: step, isRepeat: isRepeat, modifiers: modifiers)
return nil
case NX_KEYTYPE_BRIGHTNESS_DOWN:
guard shouldHandleBrightness(modifiers: modifiers) else { return Unmanaged.passUnretained(cgEvent) }
delegate?.mediaKeyInterceptor(self, didReceiveBrightnessCommand: .down, step: step, isRepeat: isRepeat, modifiers: modifiers)
return nil
default:
return Unmanaged.passUnretained(cgEvent)
}
}
private func shouldHandle(keyCode: Int32, modifiers: NSEvent.ModifierFlags) -> Bool {
switch keyCode {
case NX_KEYTYPE_SOUND_UP, NX_KEYTYPE_SOUND_DOWN, NX_KEYTYPE_MUTE:
return configuration.interceptVolume
case NX_KEYTYPE_BRIGHTNESS_UP, NX_KEYTYPE_BRIGHTNESS_DOWN:
return configuration.interceptBrightness || (configuration.interceptCommandModifiedBrightness && modifiers.contains(.command))
default:
return false
}
}
private func shouldHandleBrightness(modifiers: NSEvent.ModifierFlags) -> Bool {
if configuration.interceptBrightness {
return true
}
return configuration.interceptCommandModifiedBrightness && modifiers.contains(.command)
}
private func step(for event: NSEvent) -> MediaKeyStep {
let modifiers = event.modifierFlags
if modifiers.contains(.option) && modifiers.contains(.shift) {
return .fine
}
return .standard
}
}
#if canImport(ApplicationServices)
extension MediaKeyInterceptor {
private func requestAccessibilityPermissionIfNeeded() {
guard !AXIsProcessTrusted(), !didRequestAccessibilityPrompt else { return }
let promptKey = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String
let options: CFDictionary = [promptKey: true] as CFDictionary
_ = AXIsProcessTrustedWithOptions(options)
didRequestAccessibilityPrompt = true
}
}
#endif