Merge pull request #133 from Ebullioscopic/feature/hud

Feature/hud
This commit is contained in:
Hariharan Mudaliar 2025-11-30 21:50:21 +05:30 • committed by GitHub
commit 8b9ade25c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 392 additions and 26 deletions

View file

@ -109,7 +109,7 @@ struct InlineHUD: View {
.contentTransition(.interpolate)
.frame(width: 20, height: 15, alignment: .center)
case .backlight:
Image(systemName: "keyboard")
Image(systemName: BacklightSymbol(value))
.contentTransition(.interpolate)
.frame(width: 20, height: 15, alignment: .center)
case .mic:
@ -353,6 +353,13 @@ struct InlineHUD: View {
return "sun.min"
}
}
func BacklightSymbol(_ value: CGFloat) -> String {
if value >= 0.5 {
return "light.max"
}
return "light.min"
}
func Type2Name(_ type: SneakContentType) -> String {
switch(type) {

View file

@ -46,7 +46,7 @@ struct SystemEventIndicatorModifier: View {
.frame(width: 20, height: 15)
.foregroundStyle(.white)
case .backlight:
Image(systemName: "keyboard")
Image(systemName: BacklightSymbol(value))
.contentTransition(.symbolEffect)
.frame(width: 20, height: 15)
.foregroundStyle(.white)
@ -95,6 +95,10 @@ struct SystemEventIndicatorModifier: View {
return "speaker.wave.2"
}
}
func BacklightSymbol(_ value: CGFloat) -> String {
value >= 0.5 ? "light.max" : "light.min"
}
}
struct ProgressSection: View {

View file

@ -0,0 +1,190 @@
//
// KeyboardBrightnessSensor.swift
// DynamicIsland
//
// Copied from TheBoringWorker-HUD `KeyboardManager.swift` and adapted for Atoll.
//
import Foundation
import IOKit
import IOKit.hid
import ObjectiveC.runtime
final class KeyboardBrightnessSensor {
private init() {}
private static let maxKeyboardBrightness: Float = 342
private static let coreBrightnessClient = CoreBrightnessClient()
private static var method = SensorMethod.standard
static func currentLevel() throws -> Float {
if let client = coreBrightnessClient, let value = client.currentBrightness() {
return clamp(value)
}
switch method {
case .standard:
do {
return normalizedValue(fromRaw: try readStandardRawLevel())
} catch {
method = .m1
}
case .m1:
do {
return normalizedValue(fromRaw: try readM1RawLevel())
} catch {
method = .allFailed
}
case .allFailed:
throw SensorError.Keyboard.notFound
}
return try currentLevel()
}
static func setLevel(_ level: Float) throws {
let clamped = max(0, min(1, level))
if let client = coreBrightnessClient, client.setBrightness(clamped) {
return
}
let raw = rawValue(fromNormalized: clamped)
do {
try writeStandard(rawLevel: raw)
} catch {
method = .m1
throw error
}
}
private static func clamp(_ value: Float) -> Float {
max(0, min(1, value))
}
private static func normalizedValue(fromRaw rawLevel: Float) -> Float {
let clamped = max(0, min(1, rawLevel))
if clamped <= 0.07 {
return 0
}
let value = log10(clamped + 0.03) + 1
return max(0, min(1, value))
}
private static func rawValue(fromNormalized normalized: Float) -> Float {
guard normalized > 0 else { return 0 }
let powValue = powf(10, normalized - 1) - 0.03
return max(0, min(1, powValue))
}
private static func readStandardRawLevel() throws -> Float {
let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleHIDKeyboardEventDriverV2"))
guard service != 0 else {
throw SensorError.Keyboard.notStandard
}
defer { IOObjectRelease(service) }
guard let property: CFTypeRef = IORegistryEntryCreateCFProperty(
service,
"KeyboardBacklightBrightness" as CFString,
kCFAllocatorDefault,
0
)?.takeUnretainedValue() else {
throw SensorError.Keyboard.notStandard
}
if CFGetTypeID(property) == CFNumberGetTypeID() {
var rawValue: Float = 0
if CFNumberGetValue(property as! CFNumber, .floatType, &rawValue) {
return rawValue / maxKeyboardBrightness
}
}
throw SensorError.Keyboard.notStandard
}
private static func readM1RawLevel() throws -> Float {
let task = Process()
task.launchPath = "/usr/libexec/corebrightnessdiag"
task.arguments = ["status-info"]
let pipe = Pipe()
task.standardOutput = pipe
try task.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
task.waitUntilExit()
if let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? NSDictionary,
let keyboards = plist["CBKeyboards"] as? [String: [String: Any]] {
for keyboard in keyboards.values {
if let backlightInfo = keyboard["CBKeyboardBacklightContainer"] as? [String: Any],
backlightInfo["KeyboardBacklightBuiltIn"] as? Bool == true,
let brightness = backlightInfo["KeyboardBacklightBrightness"] as? Float {
return brightness
}
}
}
throw SensorError.Keyboard.notSilicon
}
private static func writeStandard(rawLevel: Float) throws {
let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleHIDKeyboardEventDriverV2"))
guard service != 0 else {
throw SensorError.Keyboard.notStandard
}
defer { IOObjectRelease(service) }
var scaled = rawLevel * maxKeyboardBrightness
guard let number = CFNumberCreate(kCFAllocatorDefault, .floatType, &scaled) else {
throw SensorError.Keyboard.notStandard
}
let status = IORegistryEntrySetCFProperty(service, "KeyboardBacklightBrightness" as CFString, number)
if status != KERN_SUCCESS {
throw SensorError.Keyboard.notStandard
}
}
}
// MARK: - CoreBrightness bridge
private final class CoreBrightnessClient {
private static let keyboardID: UInt64 = 1
private var clientInstance: NSObject?
private let getSelector = NSSelectorFromString("brightnessForKeyboard:")
private let setSelector = NSSelectorFromString("setBrightness:forKeyboard:")
init?() {
var loaded = false
let bundlePaths = [
"/System/Library/PrivateFrameworks/CoreBrightness.framework",
"/System/Library/PrivateFrameworks/CoreBrightness.framework/CoreBrightness"
]
for path in bundlePaths where !loaded {
if let bundle = Bundle(path: path) {
loaded = bundle.load()
}
}
guard loaded, let cls = NSClassFromString("KeyboardBrightnessClient") as? NSObject.Type else {
return nil
}
clientInstance = cls.init()
}
func currentBrightness() -> Float? {
guard let clientInstance,
let getter: BrightnessGetter = methodIMP(on: clientInstance, selector: getSelector, as: BrightnessGetter.self)
else { return nil }
return getter(clientInstance, getSelector, Self.keyboardID)
}
func setBrightness(_ value: Float) -> Bool {
guard let clientInstance,
let setter: BrightnessSetter = methodIMP(on: clientInstance, selector: setSelector, as: BrightnessSetter.self)
else { return false }
return setter(clientInstance, setSelector, value, Self.keyboardID).boolValue
}
private typealias BrightnessGetter = @convention(c) (NSObject, Selector, UInt64) -> Float
private typealias BrightnessSetter = @convention(c) (NSObject, Selector, Float, UInt64) -> ObjCBool
private func methodIMP<T>(on object: NSObject, selector: Selector, as type: T.Type) -> T? {
guard let cls = object_getClass(object),
let method = class_getInstanceMethod(cls, selector)
else { return nil }
let imp = method_getImplementation(method)
return unsafeBitCast(imp, to: T.self)
}
}

View file

@ -25,13 +25,30 @@ enum MediaKeyStep {
struct MediaKeyConfiguration {
var interceptVolume: Bool
var interceptBrightness: Bool
var interceptCommandModifiedBrightness: Bool
static let disabled = MediaKeyConfiguration(interceptVolume: false, interceptBrightness: false)
static let disabled = MediaKeyConfiguration(
interceptVolume: false,
interceptBrightness: false,
interceptCommandModifiedBrightness: false
)
}
protocol MediaKeyInterceptorDelegate: AnyObject {
func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveVolumeCommand direction: MediaKeyDirection, step: MediaKeyStep, isRepeat: Bool)
func mediaKeyInterceptor(_ interceptor: MediaKeyInterceptor, didReceiveBrightnessCommand direction: MediaKeyDirection, step: MediaKeyStep, isRepeat: Bool)
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)
}
@ -129,7 +146,7 @@ final class MediaKeyInterceptor {
private func updateTapState() {
guard let tap = eventTap else { return }
let shouldEnable = configuration.interceptVolume || configuration.interceptBrightness
let shouldEnable = configuration.interceptVolume || configuration.interceptBrightness || configuration.interceptCommandModifiedBrightness
if shouldEnable != isTapEnabled {
CGEvent.tapEnable(tap: tap, enable: shouldEnable)
isTapEnabled = shouldEnable
@ -150,10 +167,11 @@ final class MediaKeyInterceptor {
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)) {
if shouldHandle(keyCode: Int32(keyCode), modifiers: modifiers) {
return nil
}
return Unmanaged.passUnretained(cgEvent)
@ -162,40 +180,47 @@ final class MediaKeyInterceptor {
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)
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)
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 configuration.interceptBrightness else { return Unmanaged.passUnretained(cgEvent) }
delegate?.mediaKeyInterceptor(self, didReceiveBrightnessCommand: .up, step: step, isRepeat: isRepeat)
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 configuration.interceptBrightness else { return Unmanaged.passUnretained(cgEvent) }
delegate?.mediaKeyInterceptor(self, didReceiveBrightnessCommand: .down, step: step, isRepeat: isRepeat)
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) -> Bool {
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
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) {

View file

@ -1,11 +1,13 @@
import Foundation
import CoreGraphics
import Defaults
import AppKit
final class SystemChangesObserver: MediaKeyInterceptorDelegate {
private weak var coordinator: DynamicIslandViewCoordinator?
private let volumeController = SystemVolumeController.shared
private let brightnessController = SystemBrightnessController.shared
private let keyboardBacklightController = SystemKeyboardBacklightController.shared
private let mediaKeyInterceptor = MediaKeyInterceptor.shared
private let standardVolumeStep: Float = 1.0 / 16.0
@ -14,14 +16,16 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
private var volumeEnabled = false
private var brightnessEnabled = false
private var keyboardBacklightEnabled = false
init(coordinator: DynamicIslandViewCoordinator) {
self.coordinator = coordinator
}
func startObserving(volumeEnabled: Bool, brightnessEnabled: Bool) {
func startObserving(volumeEnabled: Bool, brightnessEnabled: Bool, keyboardBacklightEnabled: Bool) {
self.volumeEnabled = volumeEnabled
self.brightnessEnabled = brightnessEnabled
self.keyboardBacklightEnabled = keyboardBacklightEnabled
volumeController.onVolumeChange = { [weak self] volume, muted in
guard let self, self.volumeEnabled else { return }
@ -40,6 +44,11 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
}
brightnessController.start()
configureKeyboardBacklightCallback()
if keyboardBacklightEnabled {
keyboardBacklightController.start()
}
mediaKeyInterceptor.delegate = self
let tapStarted = mediaKeyInterceptor.start()
if !tapStarted {
@ -47,16 +56,35 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
}
mediaKeyInterceptor.configuration = MediaKeyConfiguration(
interceptVolume: volumeEnabled,
interceptBrightness: brightnessEnabled
interceptBrightness: brightnessEnabled,
interceptCommandModifiedBrightness: keyboardBacklightEnabled
)
}
func update(volumeEnabled: Bool, brightnessEnabled: Bool) {
func update(volumeEnabled: Bool, brightnessEnabled: Bool, keyboardBacklightEnabled: Bool) {
self.volumeEnabled = volumeEnabled
self.brightnessEnabled = brightnessEnabled
let backlightStateChanged = self.keyboardBacklightEnabled != keyboardBacklightEnabled
self.keyboardBacklightEnabled = keyboardBacklightEnabled
if keyboardBacklightEnabled {
configureKeyboardBacklightCallback()
} else {
keyboardBacklightController.onBacklightChange = nil
}
if backlightStateChanged {
if keyboardBacklightEnabled {
keyboardBacklightController.start()
} else {
keyboardBacklightController.stop()
}
}
mediaKeyInterceptor.configuration = MediaKeyConfiguration(
interceptVolume: volumeEnabled,
interceptBrightness: brightnessEnabled
interceptBrightness: brightnessEnabled,
interceptCommandModifiedBrightness: keyboardBacklightEnabled
)
}
@ -70,6 +98,9 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
brightnessController.stop()
brightnessController.onBrightnessChange = nil
keyboardBacklightController.stop()
keyboardBacklightController.onBacklightChange = nil
}
// MARK: - MediaKeyInterceptorDelegate
@ -78,7 +109,8 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
_ interceptor: MediaKeyInterceptor,
didReceiveVolumeCommand direction: MediaKeyDirection,
step: MediaKeyStep,
isRepeat: Bool
isRepeat: Bool,
modifiers: NSEvent.ModifierFlags
) {
guard volumeEnabled else { return }
let baseStep = stepSize(for: step, base: standardVolumeStep)
@ -95,12 +127,16 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
_ interceptor: MediaKeyInterceptor,
didReceiveBrightnessCommand direction: MediaKeyDirection,
step: MediaKeyStep,
isRepeat: Bool
isRepeat: Bool,
modifiers: NSEvent.ModifierFlags
) {
guard brightnessEnabled else { return }
let baseStep = stepSize(for: step, base: standardBrightnessStep)
let delta = direction == .up ? baseStep : -baseStep
brightnessController.adjust(by: delta)
if modifiers.contains(.command) && keyboardBacklightEnabled {
keyboardBacklightController.adjust(by: delta)
} else if brightnessEnabled {
brightnessController.adjust(by: delta)
}
}
// MARK: - HUD Dispatch
@ -125,6 +161,26 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
icon: ""
)
}
private func sendKeyboardBacklightNotification(value: Float) {
coordinator?.toggleSneakPeek(
status: true,
type: .backlight,
value: CGFloat(value),
icon: ""
)
}
private func configureKeyboardBacklightCallback() {
if keyboardBacklightEnabled {
keyboardBacklightController.onBacklightChange = { [weak self] value in
guard let self, self.keyboardBacklightEnabled else { return }
self.sendKeyboardBacklightNotification(value: value)
}
} else {
keyboardBacklightController.onBacklightChange = nil
}
}
}
private extension SystemChangesObserver {

View file

@ -44,7 +44,8 @@ class SystemHUDManager {
}
self.changesObserver?.update(
volumeEnabled: change.newValue,
brightnessEnabled: Defaults[.enableBrightnessHUD]
brightnessEnabled: Defaults[.enableBrightnessHUD],
keyboardBacklightEnabled: Defaults[.enableKeyboardBacklightHUD]
)
}.store(in: &cancellables)
@ -54,7 +55,19 @@ class SystemHUDManager {
}
self.changesObserver?.update(
volumeEnabled: Defaults[.enableVolumeHUD],
brightnessEnabled: change.newValue
brightnessEnabled: change.newValue,
keyboardBacklightEnabled: Defaults[.enableKeyboardBacklightHUD]
)
}.store(in: &cancellables)
Defaults.publisher(.enableKeyboardBacklightHUD, options: []).sink { [weak self] change in
guard let self = self, self.isSetupComplete, Defaults[.enableSystemHUD] else {
return
}
self.changesObserver?.update(
volumeEnabled: Defaults[.enableVolumeHUD],
brightnessEnabled: Defaults[.enableBrightnessHUD],
keyboardBacklightEnabled: change.newValue
)
}.store(in: &cancellables)
}
@ -89,7 +102,12 @@ class SystemHUDManager {
changesObserver = SystemChangesObserver(coordinator: coordinator)
let volumeEnabled = Defaults[.enableVolumeHUD]
let brightnessEnabled = Defaults[.enableBrightnessHUD]
changesObserver?.startObserving(volumeEnabled: volumeEnabled, brightnessEnabled: brightnessEnabled)
let keyboardBacklightEnabled = Defaults[.enableKeyboardBacklightHUD]
changesObserver?.startObserving(
volumeEnabled: volumeEnabled,
brightnessEnabled: brightnessEnabled,
keyboardBacklightEnabled: keyboardBacklightEnabled
)
print("System HUD replacement started")
isSystemOperationInProgress = false

View file

@ -0,0 +1,66 @@
import Foundation
extension Notification.Name {
static let keyboardBacklightDidChange = Notification.Name("DynamicIsland.keyboardBacklightDidChange")
}
final class SystemKeyboardBacklightController {
static let shared = SystemKeyboardBacklightController()
var onBacklightChange: ((Float) -> Void)?
private let workerQueue = DispatchQueue(label: "com.atoll.keyboardBacklight", qos: .userInitiated)
private let notificationCenter = NotificationCenter.default
private var isRunning = false
private init() {}
func start() {
guard !isRunning else { return }
isRunning = true
notifyCurrentLevel()
}
func stop() {
guard isRunning else { return }
isRunning = false
}
func adjust(by delta: Float) {
let target = currentLevel + delta
setLevel(target)
}
func setLevel(_ value: Float) {
let clamped = max(0, min(1, value))
workerQueue.async {
do {
try KeyboardBrightnessSensor.setLevel(clamped)
let level = (try? KeyboardBrightnessSensor.currentLevel()) ?? clamped
self.emitChange(level: level)
} catch {
NSLog("⚠️ Failed to set keyboard backlight: \(error)")
}
}
}
var currentLevel: Float {
(try? KeyboardBrightnessSensor.currentLevel()) ?? 0
}
private func notifyCurrentLevel() {
workerQueue.async {
let level = (try? KeyboardBrightnessSensor.currentLevel()) ?? 0
self.emitChange(level: level)
}
}
private func emitChange(level: Float) {
guard isRunning else { return }
DispatchQueue.main.async { [weak self] in
guard let self, self.isRunning else { return }
self.onBacklightChange?(level)
self.notificationCenter.post(name: .keyboardBacklightDidChange, object: nil, userInfo: ["value": level])
}
}
}