mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Working volume
This commit is contained in:
parent
86f57c88b7
commit
b4f9e51c1f
6 changed files with 638 additions and 574 deletions
204
DynamicIsland/managers/MediaKeyInterceptor.swift
Normal file
204
DynamicIsland/managers/MediaKeyInterceptor.swift
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
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
|
||||
}
|
||||
|
||||
struct MediaKeyConfiguration {
|
||||
var interceptVolume: Bool
|
||||
var interceptBrightness: Bool
|
||||
|
||||
static let disabled = MediaKeyConfiguration(interceptVolume: false, interceptBrightness: false)
|
||||
}
|
||||
|
||||
protocol MediaKeyInterceptorDelegate: AnyObject {
|
||||
func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveVolumeCommand direction: MediaKeyDirection, isRepeat: Bool)
|
||||
func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveBrightnessCommand direction: MediaKeyDirection, isRepeat: Bool)
|
||||
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<MediaKeyInterceptor>.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
|
||||
if shouldEnable != isTapEnabled {
|
||||
CGEvent.tapEnable(tap: tap, enable: shouldEnable)
|
||||
isTapEnabled = shouldEnable
|
||||
}
|
||||
}
|
||||
|
||||
private func handleEvent(cgEvent: CGEvent, type: CGEventType) -> Unmanaged<CGEvent>? {
|
||||
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
|
||||
|
||||
guard keyState else {
|
||||
// Swallow key-up events only when intercepting, otherwise let them pass through
|
||||
if shouldHandle(keyCode: Int32(keyCode)) {
|
||||
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, isRepeat: isRepeat)
|
||||
return nil
|
||||
case NX_KEYTYPE_SOUND_DOWN:
|
||||
guard configuration.interceptVolume else { return Unmanaged.passUnretained(cgEvent) }
|
||||
delegate?.mediaKeyInterceptor(self, didReceiveVolumeCommand: .down, isRepeat: isRepeat)
|
||||
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 configuration.interceptBrightness else { return Unmanaged.passUnretained(cgEvent) }
|
||||
delegate?.mediaKeyInterceptor(self, didReceiveBrightnessCommand: .up, isRepeat: isRepeat)
|
||||
return nil
|
||||
case NX_KEYTYPE_BRIGHTNESS_DOWN:
|
||||
guard configuration.interceptBrightness else { return Unmanaged.passUnretained(cgEvent) }
|
||||
delegate?.mediaKeyInterceptor(self, didReceiveBrightnessCommand: .down, isRepeat: isRepeat)
|
||||
return nil
|
||||
default:
|
||||
return Unmanaged.passUnretained(cgEvent)
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldHandle(keyCode: Int32) -> 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
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#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
|
||||
Loading…
Add table
Add a link
Reference in a new issue