Add Granular Control for Volume/Brightness

This commit is contained in:
Hariharan Mudaliar 2025-11-06 23:54:25 +05:30
commit 1bdad16eff
3 changed files with 64 additions and 17 deletions

View file

@ -17,6 +17,11 @@ enum MediaKeyDirection {
case down
}
enum MediaKeyStep {
case standard
case fine
}
struct MediaKeyConfiguration {
var interceptVolume: Bool
var interceptBrightness: Bool
@ -25,8 +30,8 @@ struct MediaKeyConfiguration {
}
protocol MediaKeyInterceptorDelegate: AnyObject {
func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveVolumeCommand direction: MediaKeyDirection, isRepeat: Bool)
func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveBrightnessCommand direction: MediaKeyDirection, isRepeat: Bool)
func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveVolumeCommand direction: MediaKeyDirection, step: MediaKeyStep, isRepeat: Bool)
func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveBrightnessCommand direction: MediaKeyDirection, step: MediaKeyStep, isRepeat: Bool)
func mediaKeyInterceptorDidToggleMute(_ interceptor: MediaKeyInterceptor)
}
@ -132,8 +137,8 @@ final class MediaKeyInterceptor {
}
private func handleEvent(cgEvent: CGEvent, type: CGEventType) -> Unmanaged<CGEvent>? {
guard let systemDefinedType = systemDefinedEventType,
type == systemDefinedType,
guard let systemDefinedType = systemDefinedEventType,
type == systemDefinedType,
let nsEvent = NSEvent(cgEvent: cgEvent),
nsEvent.subtype.rawValue == 8 else {
return Unmanaged.passUnretained(cgEvent)
@ -144,6 +149,7 @@ final class MediaKeyInterceptor {
let keyFlags = data1 & 0x0000FFFF
let keyState = ((keyFlags & 0xFF00) >> 8) == 0xA // 0xA = keyDown, 0xB = keyUp
let isRepeat = (keyFlags & 0x0001) == 1
let step = step(for: nsEvent)
guard keyState else {
// Swallow key-up events only when intercepting, otherwise let them pass through
@ -156,11 +162,11 @@ final class MediaKeyInterceptor {
switch Int32(keyCode) {
case NX_KEYTYPE_SOUND_UP:
guard configuration.interceptVolume else { return Unmanaged.passUnretained(cgEvent) }
delegate?.mediaKeyInterceptor(self, didReceiveVolumeCommand: .up, isRepeat: isRepeat)
delegate?.mediaKeyInterceptor(self, didReceiveVolumeCommand: .up, step: step, isRepeat: isRepeat)
return nil
case NX_KEYTYPE_SOUND_DOWN:
guard configuration.interceptVolume else { return Unmanaged.passUnretained(cgEvent) }
delegate?.mediaKeyInterceptor(self, didReceiveVolumeCommand: .down, isRepeat: isRepeat)
delegate?.mediaKeyInterceptor(self, didReceiveVolumeCommand: .down, step: step, isRepeat: isRepeat)
return nil
case NX_KEYTYPE_MUTE:
guard configuration.interceptVolume else { return Unmanaged.passUnretained(cgEvent) }
@ -168,11 +174,11 @@ final class MediaKeyInterceptor {
return nil
case NX_KEYTYPE_BRIGHTNESS_UP:
guard configuration.interceptBrightness else { return Unmanaged.passUnretained(cgEvent) }
delegate?.mediaKeyInterceptor(self, didReceiveBrightnessCommand: .up, isRepeat: isRepeat)
delegate?.mediaKeyInterceptor(self, didReceiveBrightnessCommand: .up, step: step, isRepeat: isRepeat)
return nil
case NX_KEYTYPE_BRIGHTNESS_DOWN:
guard configuration.interceptBrightness else { return Unmanaged.passUnretained(cgEvent) }
delegate?.mediaKeyInterceptor(self, didReceiveBrightnessCommand: .down, isRepeat: isRepeat)
delegate?.mediaKeyInterceptor(self, didReceiveBrightnessCommand: .down, step: step, isRepeat: isRepeat)
return nil
default:
return Unmanaged.passUnretained(cgEvent)
@ -189,6 +195,14 @@ final class MediaKeyInterceptor {
return false
}
}
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)

View file

@ -8,8 +8,9 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
private let brightnessController = SystemBrightnessController.shared
private let mediaKeyInterceptor = MediaKeyInterceptor.shared
private let volumeStep: Float = 1.0 / 16.0
private let brightnessStep: Float = 1.0 / 16.0
private let standardVolumeStep: Float = 1.0 / 16.0
private let standardBrightnessStep: Float = 1.0 / 16.0
private let fineStepDivisor: Float = 4.0
private var volumeEnabled = false
private var brightnessEnabled = false
@ -73,9 +74,15 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
// MARK: - MediaKeyInterceptorDelegate
func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveVolumeCommand direction: MediaKeyDirection, isRepeat: Bool) {
func mediaKeyInterceptor(
_ interceptor: MediaKeyInterceptor,
didReceiveVolumeCommand direction: MediaKeyDirection,
step: MediaKeyStep,
isRepeat: Bool
) {
guard volumeEnabled else { return }
let delta = (direction == .up ? volumeStep : -volumeStep)
let baseStep = stepSize(for: step, base: standardVolumeStep)
let delta = direction == .up ? baseStep : -baseStep
volumeController.adjust(by: delta)
}
@ -84,9 +91,15 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
volumeController.toggleMute()
}
func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveBrightnessCommand direction: MediaKeyDirection, isRepeat: Bool) {
func mediaKeyInterceptor(
_ interceptor: MediaKeyInterceptor,
didReceiveBrightnessCommand direction: MediaKeyDirection,
step: MediaKeyStep,
isRepeat: Bool
) {
guard brightnessEnabled else { return }
let delta = (direction == .up ? brightnessStep : -brightnessStep)
let baseStep = stepSize(for: step, base: standardBrightnessStep)
let delta = direction == .up ? baseStep : -baseStep
brightnessController.adjust(by: delta)
}
@ -114,4 +127,15 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
}
}
private extension SystemChangesObserver {
func stepSize(for step: MediaKeyStep, base: Float) -> Float {
switch step {
case .standard:
return base
case .fine:
return base / fineStepDivisor
}
}
}

View file

@ -445,8 +445,11 @@ final class SystemBrightnessController {
private var brightnessAnimationStart: Float = 0
private var brightnessAnimationTarget: Float = 0
private var brightnessAnimationStartDate: Date?
private let brightnessAnimationDuration: TimeInterval = 0.18
private var currentBrightnessAnimationDuration: TimeInterval = 0.18
private let brightnessAnimationSteps = 10
private let minimumBrightnessAnimationDuration: TimeInterval = 0.08
private let maximumBrightnessAnimationDuration: TimeInterval = 0.3
private let brightnessAnimationDurationScale: TimeInterval = 1.6
private var lastEmittedBrightness: Float = 0.5
private init() {
@ -507,8 +510,9 @@ final class SystemBrightnessController {
brightnessAnimationStart = start
brightnessAnimationTarget = target
brightnessAnimationStartDate = Date()
currentBrightnessAnimationDuration = animationDuration(forDelta: abs(target - start))
let interval = brightnessAnimationDuration / Double(brightnessAnimationSteps)
let interval = currentBrightnessAnimationDuration / Double(brightnessAnimationSteps)
let timer = Timer(timeInterval: interval, repeats: true) { [weak self] timer in
guard let self else { return }
guard let startDate = self.brightnessAnimationStartDate else {
@ -517,7 +521,7 @@ final class SystemBrightnessController {
return
}
let elapsed = Date().timeIntervalSince(startDate)
let progress = min(elapsed / self.brightnessAnimationDuration, 1)
let progress = min(elapsed / self.currentBrightnessAnimationDuration, 1)
let eased = self.ease(progress)
let value = self.brightnessAnimationStart + (self.brightnessAnimationTarget - self.brightnessAnimationStart) * Float(eased)
self.applyBrightness(value)
@ -532,6 +536,11 @@ final class SystemBrightnessController {
timer.fire()
}
private func animationDuration(forDelta delta: Float) -> TimeInterval {
let scaled = minimumBrightnessAnimationDuration + TimeInterval(delta) * brightnessAnimationDurationScale
return min(maximumBrightnessAnimationDuration, max(minimumBrightnessAnimationDuration, scaled))
}
private func applyBrightness(_ value: Float) {
let clamped = max(0, min(1, value))
if setBrightnessViaDisplayServices(clamped) {