diff --git a/DynamicIsland.xcodeproj/project.pbxproj b/DynamicIsland.xcodeproj/project.pbxproj index b03bbc4..c730875 100644 --- a/DynamicIsland.xcodeproj/project.pbxproj +++ b/DynamicIsland.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 7E25ECA12E6C6C3800700E72 /* CoreBrightness.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E25ECA02E6C6C3800700E72 /* CoreBrightness.framework */; }; + 7E25ECA32E6C6E2600700E72 /* OSD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E25ECA22E6C6E2600700E72 /* OSD.framework */; }; + 7E25ECA52E6C6E7800700E72 /* DisplayServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E25ECA42E6C6E7800700E72 /* DisplayServices.framework */; }; 7E6118142E24071E0050006C /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 14D032192C68F32E0096E6A1 /* LaunchAtLogin */; }; 7E6118152E24071E0050006C /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 14D0321C2C68F3350096E6A1 /* Sparkle */; }; 7E6118162E24071E0050006C /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = B18654382C6F4990000B926A /* KeyboardShortcuts */; }; @@ -39,6 +42,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 7E25ECA02E6C6C3800700E72 /* CoreBrightness.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreBrightness.framework; path = /System/Library/PrivateFrameworks/CoreBrightness.framework; sourceTree = ""; }; + 7E25ECA22E6C6E2600700E72 /* OSD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OSD.framework; path = /System/Library/PrivateFrameworks/OSD.framework; sourceTree = ""; }; + 7E25ECA42E6C6E7800700E72 /* DisplayServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DisplayServices.framework; path = /System/Library/PrivateFrameworks/DisplayServices.framework; sourceTree = ""; }; 7E81303D2E3FB40500E08F3F /* mediaremote-adapter.pl */ = {isa = PBXFileReference; lastKnownFileType = text.script.perl; path = "mediaremote-adapter.pl"; sourceTree = ""; }; 7E81303E2E3FB40500E08F3F /* NowPlayingTestClient */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = NowPlayingTestClient; sourceTree = ""; }; 7EE1D73C2E6B1E0B00997219 /* ApplicationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ApplicationServices.framework; path = System/Library/Frameworks/ApplicationServices.framework; sourceTree = SDKROOT; }; @@ -60,12 +66,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7E25ECA12E6C6C3800700E72 /* CoreBrightness.framework in Frameworks */, 7EE1D73F2E6B1E1200997219 /* IOKit.framework in Frameworks */, 7EE1D73D2E6B1E0B00997219 /* ApplicationServices.framework in Frameworks */, BA90505C953246D0AB206451 /* MediaRemoteAdapter.framework in Frameworks */, 7E61181C2E24071E0050006C /* MacroVisionKit in Frameworks */, 7E61181B2E24071E0050006C /* LottieUI in Frameworks */, 7E61181A2E24071E0050006C /* SwiftUIIntrospect in Frameworks */, + 7E25ECA32E6C6E2600700E72 /* OSD.framework in Frameworks */, + 7E25ECA52E6C6E7800700E72 /* DisplayServices.framework in Frameworks */, 7E6118192E24071E0050006C /* Defaults in Frameworks */, 7E6118182E24071E0050006C /* TheBoringWorkerNotifier in Frameworks */, 7E6118172E24071E0050006C /* Collections in Frameworks */, @@ -91,6 +100,9 @@ 7E81304F2E3FB42700E08F3F /* Frameworks */ = { isa = PBXGroup; children = ( + 7E25ECA02E6C6C3800700E72 /* CoreBrightness.framework */, + 7E25ECA42E6C6E7800700E72 /* DisplayServices.framework */, + 7E25ECA22E6C6E2600700E72 /* OSD.framework */, 7EE1D73E2E6B1E1200997219 /* IOKit.framework */, 7EE1D73C2E6B1E0B00997219 /* ApplicationServices.framework */, ); @@ -371,6 +383,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; + SYSTEM_FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + ); }; name = Debug; }; @@ -408,6 +424,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; + SYSTEM_FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + ); }; name = Release; }; diff --git a/DynamicIsland/DynamicIsland.entitlements b/DynamicIsland/DynamicIsland.entitlements index 2f52f33..2e86356 100644 --- a/DynamicIsland/DynamicIsland.entitlements +++ b/DynamicIsland/DynamicIsland.entitlements @@ -2,20 +2,8 @@ - com.apple.security.app-sandbox - com.apple.security.automation.apple-events - com.apple.security.device.camera - - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - - com.apple.security.network.server - - com.apple.security.personal-information.calendars - com.apple.security.temporary-exception.apple-events com.spotify.client diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 5c14556..19fa7cd 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -111,6 +111,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationWillTerminate(_ notification: Notification) { // Cancel any pending window size updates windowSizeUpdateWorkItem?.cancel() + + // Re-enable system HUD on app termination + SystemOSDManager.enableSystemHUD() + NotificationCenter.default.removeObserver(self) } @@ -290,6 +294,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { coordinator.setupWorkersNotificationObservers() + // Setup SystemHUD Manager + SystemHUDManager.shared.setup(coordinator: coordinator) + // Observe tab changes - use debounced updates coordinator.$currentView.sink { [weak self] newView in self?.debouncedUpdateWindowSize() diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index b58aaa9..372ee37 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -202,6 +202,15 @@ } } } + }, + "Brightness HUD" : { + + }, + "Built-in System Monitoring" : { + + }, + "Built-in system monitoring detects volume, brightness, and keyboard backlight changes directly without requiring external apps." : { + }, "Buy us a coffee" : { @@ -467,6 +476,12 @@ }, "Email address" : { + }, + "Enable Built-in System HUD" : { + + }, + "Enable built-in system monitoring to replace macOS HUD notifications with Dynamic Island displays." : { + }, "Enable colored spectrograms" : { "extractionState" : "stale", @@ -638,6 +653,9 @@ }, "Installed extensions" : { + }, + "Keyboard Backlight HUD" : { + }, "Keyboard shortcuts are disabled" : { @@ -1027,6 +1045,9 @@ }, "Remember last tab" : { + }, + "Replaces system HUD notifications with Dynamic Island displays." : { + }, "Reset to Default" : { @@ -1080,6 +1101,9 @@ }, "Selected animation" : { + }, + "Sensitivity" : { + }, "Set a custom duration for the timer. This will be used when you press the 'Custom' button in the timer interface." : { @@ -1337,6 +1361,9 @@ }, "View on GitHub: th-ch/youtube-music" : { + }, + "Volume HUD" : { + }, "Welcome" : { diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index e2dee3e..9f8c4ed 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -41,11 +41,8 @@ struct SettingsView: View { NavigationLink(value: "Calendar") { Label("Calendar", systemImage: "calendar") } - if extensionManager.installedExtensions - .contains(where: { $0.bundleIdentifier == hudExtension }) { - NavigationLink(value: "HUD") { - Label("HUDs", systemImage: "dial.medium.fill") - } + NavigationLink(value: "HUD") { + Label("HUDs", systemImage: "dial.medium.fill") } NavigationLink(value: "Battery") { Label("Battery", systemImage: "battery.100.bolt") @@ -423,14 +420,66 @@ struct HUD: View { @EnvironmentObject var vm: DynamicIslandViewModel @Default(.inlineHUD) var inlineHUD @Default(.enableGradient) var enableGradient + @Default(.enableSystemHUD) var enableSystemHUD + @Default(.enableVolumeHUD) var enableVolumeHUD + @Default(.enableBrightnessHUD) var enableBrightnessHUD + @Default(.enableKeyboardBacklightHUD) var enableKeyboardBacklightHUD + @Default(.systemHUDSensitivity) var systemHUDSensitivity @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared + var body: some View { Form { Section { Toggle("Enable HUD replacement", isOn: $coordinator.hudReplacement) } header: { Text("General") + } footer: { + Text("Replaces system HUD notifications with Dynamic Island displays.") + .foregroundStyle(.secondary) + .font(.caption) } + + Section { + Toggle("Enable Built-in System HUD", isOn: $enableSystemHUD) + .onChange(of: enableSystemHUD) { _ in + // SystemHUDManager will automatically handle start/stop via its @Default observer + } + + if enableSystemHUD { + Toggle("Volume HUD", isOn: $enableVolumeHUD) + Toggle("Brightness HUD", isOn: $enableBrightnessHUD) + Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD) + + HStack { + Text("Sensitivity") + Spacer() + Slider(value: Binding( + get: { Double(systemHUDSensitivity) }, + set: { systemHUDSensitivity = Int($0) } + ), in: 1...10, step: 1) { + Text("Sensitivity") + } + .frame(width: 120) + Text("\(systemHUDSensitivity)") + .foregroundStyle(.secondary) + .font(.caption) + .frame(width: 20) + } + } + } header: { + Text("Built-in System Monitoring") + } footer: { + if enableSystemHUD { + Text("Built-in system monitoring detects volume, brightness, and keyboard backlight changes directly without requiring external apps.") + .foregroundStyle(.secondary) + .font(.caption) + } else { + Text("Enable built-in system monitoring to replace macOS HUD notifications with Dynamic Island displays.") + .foregroundStyle(.secondary) + .font(.caption) + } + } + Section { Picker("HUD style", selection: $inlineHUD) { Text("Default") diff --git a/DynamicIsland/helpers/AppleScriptError.swift b/DynamicIsland/helpers/AppleScriptError.swift new file mode 100644 index 0000000..b59cf1f --- /dev/null +++ b/DynamicIsland/helpers/AppleScriptError.swift @@ -0,0 +1,15 @@ +// +// AppleScriptError.swift +// DynamicIsland +// +// Adapted from TheBoringWorker-HUD +// Created by GitHub Copilot on 06/09/25. +// + +import Foundation + +enum AppleScriptError: Error { + case initScriptFailed + case runtimeError + case emptyOutput +} \ No newline at end of file diff --git a/DynamicIsland/helpers/AppleScriptRunner.swift b/DynamicIsland/helpers/AppleScriptRunner.swift new file mode 100644 index 0000000..bf9d0b0 --- /dev/null +++ b/DynamicIsland/helpers/AppleScriptRunner.swift @@ -0,0 +1,28 @@ +// +// AppleScriptRunner.swift +// DynamicIsland +// +// Adapted from TheBoringWorker-HUD +// Created by GitHub Copilot on 06/09/25. +// + +import Foundation + +class AppleScriptRunner { + private init() {} + + static func run(script: String) throws -> String { + var error: NSDictionary? + if let scriptObject = NSAppleScript(source: script) { + let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error) + guard error == nil else { + throw AppleScriptError.runtimeError + } + if let outputString = output.stringValue { + return outputString + } + throw AppleScriptError.emptyOutput + } + throw AppleScriptError.initScriptFailed + } +} \ No newline at end of file diff --git a/DynamicIsland/helpers/SensorError.swift b/DynamicIsland/helpers/SensorError.swift new file mode 100644 index 0000000..19a8972 --- /dev/null +++ b/DynamicIsland/helpers/SensorError.swift @@ -0,0 +1,22 @@ +// +// SensorError.swift +// DynamicIsland +// +// Adapted from TheBoringWorker-HUD +// Created by GitHub Copilot on 06/09/25. +// + +import Foundation + +enum SensorError: Error { + enum Display: Error { + case notFound + case notSilicon + case notStandard + } + enum Keyboard: Error { + case notFound + case notSilicon + case notStandard + } +} \ No newline at end of file diff --git a/DynamicIsland/helpers/SensorMethod.swift b/DynamicIsland/helpers/SensorMethod.swift new file mode 100644 index 0000000..ea149d1 --- /dev/null +++ b/DynamicIsland/helpers/SensorMethod.swift @@ -0,0 +1,15 @@ +// +// SensorMethod.swift +// DynamicIsland +// +// Adapted from TheBoringWorker-HUD +// Created by GitHub Copilot on 06/09/25. +// + +import Foundation + +enum SensorMethod { + case standard + case m1 + case allFailed +} \ No newline at end of file diff --git a/DynamicIsland/managers/SystemChangesObserver.swift b/DynamicIsland/managers/SystemChangesObserver.swift new file mode 100644 index 0000000..1156a0e --- /dev/null +++ b/DynamicIsland/managers/SystemChangesObserver.swift @@ -0,0 +1,153 @@ +// +// SystemChangesObserver.swift +// DynamicIsland +// +// Adapted from TheBoringWorker-HUD ChangesObserver.swift +// Created by GitHub Copilot on 06/09/25. +// + +import Cocoa +import Foundation +import Defaults + +class SystemChangesObserver: ObservableObject { + private var oldVolume: Float + private var oldMuted: Bool + private var oldBrightness: Float = 0 + + private weak var coordinator: DynamicIslandViewCoordinator? + + @Default(.enableSystemHUD) var enableSystemHUD + @Default(.enableVolumeHUD) var enableVolumeHUD + @Default(.enableBrightnessHUD) var enableBrightnessHUD + + init(coordinator: DynamicIslandViewCoordinator) { + self.coordinator = coordinator + oldVolume = SystemVolumeManager.getOutputVolume() + oldMuted = SystemVolumeManager.isMuted() + + do { + oldBrightness = try SystemDisplayManager.getDisplayBrightness() + } catch { + NSLog("Failed to retrieve display brightness: \(error)") + } + } + + func startObserving() { + guard enableSystemHUD else { return } + + createObservers() + createTimerForContinuousChangesCheck(with: 0.2) + } + + private func createTimerForContinuousChangesCheck(with seconds: TimeInterval) { + let timer = Timer( + timeInterval: seconds, target: self, selector: #selector(checkChanges), userInfo: nil, + repeats: true) + let mainLoop = RunLoop.main + mainLoop.add(timer, forMode: .common) + } + + private func createObservers() { + NotificationCenter.default.addObserver( + self, + selector: #selector(showVolumeHUD), + name: SystemKeyObserver.volumeChanged, + object: nil) + + NotificationCenter.default.addObserver( + self, + selector: #selector(showBrightnessHUD), + name: SystemKeyObserver.brightnessChanged, + object: nil) + } + + @objc func showVolumeHUD() { + guard enableSystemHUD && enableVolumeHUD else { return } + + if oldVolume == 0.0 || oldVolume == 1.0 { + sendVolumeNotification(value: oldVolume) + } + print("Showing volume HUD") + } + + @objc func showBrightnessHUD() { + guard enableSystemHUD && enableBrightnessHUD else { return } + + if oldBrightness == 0.0 || oldBrightness == 1.0 { + sendBrightnessNotification(value: oldBrightness) + } + print("Showing brightness HUD") + } + + @objc func checkChanges() { + guard enableSystemHUD else { return } + + checkBrightnessChanges() + checkVolumeChanges() + } + + private func isAlmost(firstNumber: Float, secondNumber: Float) -> Bool { + let marginValue = 5 / 100.0 + return (firstNumber + Float(marginValue) >= secondNumber && firstNumber - Float(marginValue) <= secondNumber) + } + + private func checkVolumeChanges() { + guard enableVolumeHUD else { return } + + let newVolume = SystemVolumeManager.getOutputVolume() + let newMuted = SystemVolumeManager.isMuted() + + if !isAlmost(firstNumber: oldVolume, secondNumber: newVolume) || newMuted != oldMuted { + if newMuted { + sendVolumeNotification(value: 0.0) + } else { + sendVolumeNotification(value: newVolume) + } + oldVolume = newVolume + oldMuted = newMuted + } + } + + private func checkBrightnessChanges() { + guard enableBrightnessHUD else { return } + + if NSScreen.screens.count == 0 { + return + } + + do { + let newBrightness = try SystemDisplayManager.getDisplayBrightness() + if !isAlmost(firstNumber: oldBrightness, secondNumber: newBrightness) { + sendBrightnessNotification(value: newBrightness) + oldBrightness = newBrightness + } + } catch { + NSLog("Failed to retrieve display brightness: \(error)") + } + } + + // MARK: - Internal Notification Methods + + private func sendVolumeNotification(value: Float) { + coordinator?.toggleSneakPeek( + status: true, + type: .volume, + value: CGFloat(value), + icon: "" + ) + } + + private func sendBrightnessNotification(value: Float) { + coordinator?.toggleSneakPeek( + status: true, + type: .brightness, + value: CGFloat(value), + icon: "" + ) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } +} \ No newline at end of file diff --git a/DynamicIsland/managers/SystemDisplayManager.swift b/DynamicIsland/managers/SystemDisplayManager.swift new file mode 100644 index 0000000..ca327f6 --- /dev/null +++ b/DynamicIsland/managers/SystemDisplayManager.swift @@ -0,0 +1,73 @@ +// +// SystemDisplayManager.swift +// DynamicIsland +// +// Adapted from TheBoringWorker-HUD DisplayManager.swift +// Created by GitHub Copilot on 06/09/25. +// + +import Foundation +import Cocoa + +class SystemDisplayManager { + private init() {} + + private static var method = SensorMethod.standard + + static func getDisplayBrightness() throws -> Float { + switch SystemDisplayManager.method { + case .standard: + do { + return try getStandardDisplayBrightness() + } catch { + method = .m1 + } + case .m1: + do { + return try getM1DisplayBrightness() + } catch { + method = .allFailed + } + case .allFailed: + throw SensorError.Display.notFound + } + return try getDisplayBrightness() + } + + private static func getStandardDisplayBrightness() throws -> Float { + var brightness: float_t = 1 + let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IODisplayConnect")) + defer { + IOObjectRelease(service) + } + + let result = IODisplayGetFloatParameter(service, 0, kIODisplayBrightnessKey as CFString, &brightness) + if result != kIOReturnSuccess { + throw SensorError.Display.notStandard + } + return brightness + } + + private static func getM1DisplayBrightness() 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 displays = plist["CBDisplays"] as? [String: [String: Any]] { + for display in displays.values { + if let displayInfo = display["Display"] as? [String: Any], + displayInfo["DisplayServicesIsBuiltInDisplay"] as? Bool == true, + let brightness = displayInfo["DisplayServicesBrightness"] as? Float { + return brightness + } + } + } + throw SensorError.Display.notSilicon + } +} \ No newline at end of file diff --git a/DynamicIsland/managers/SystemHUDManager.swift b/DynamicIsland/managers/SystemHUDManager.swift new file mode 100644 index 0000000..220665a --- /dev/null +++ b/DynamicIsland/managers/SystemHUDManager.swift @@ -0,0 +1,82 @@ +// +// SystemHUDManager.swift +// DynamicIsland +// +// Created by GitHub Copilot on 06/09/25. +// + +import Foundation +import Defaults + +class SystemHUDManager: ObservableObject { + static let shared = SystemHUDManager() + + private var changesObserver: SystemChangesObserver? + private weak var coordinator: DynamicIslandViewCoordinator? + + @Default(.enableSystemHUD) var enableSystemHUD { + didSet { + Task { @MainActor in + if enableSystemHUD { + await startSystemHUD() + } else { + await stopSystemHUD() + } + } + } + } + + private init() {} + + func setup(coordinator: DynamicIslandViewCoordinator) { + self.coordinator = coordinator + + if enableSystemHUD { + Task { @MainActor in + await startSystemHUD() + } + } + } + + @MainActor + private func startSystemHUD() async { + guard let coordinator = coordinator else { return } + + await stopSystemHUD() // Stop any existing observer + + changesObserver = SystemChangesObserver(coordinator: coordinator) + changesObserver?.startObserving() + + // Disable system HUD if possible + // Note: This would require additional implementation similar to OSDUIManager + disableSystemHUD() + + print("System HUD replacement started") + } + + @MainActor + private func stopSystemHUD() async { + changesObserver = nil + + // Re-enable system HUD + enableOriginalSystemHUD() + + print("System HUD replacement stopped") + } + + private func disableSystemHUD() { + // Disable the system HUD using SystemOSDManager + SystemOSDManager.disableSystemHUD() + } + + private func enableOriginalSystemHUD() { + // Re-enable the system HUD using SystemOSDManager + SystemOSDManager.enableSystemHUD() + } + + deinit { + Task { @MainActor in + await stopSystemHUD() + } + } +} \ No newline at end of file diff --git a/DynamicIsland/managers/SystemKeyObserver.swift b/DynamicIsland/managers/SystemKeyObserver.swift new file mode 100644 index 0000000..e0b72b8 --- /dev/null +++ b/DynamicIsland/managers/SystemKeyObserver.swift @@ -0,0 +1,44 @@ +// +// SystemKeyObserver.swift +// DynamicIsland +// +// Adapted from TheBoringWorker-HUD KeyPressObserver.swift +// Created by GitHub Copilot on 06/09/25. +// + +import Cocoa + +class SystemKeyObserver: NSApplication { + static let volumeChanged = Notification.Name("DynamicIsland.volumeChanged") + static let brightnessChanged = Notification.Name("DynamicIsland.brightnessChanged") + static let keyboardIlluminationChanged = Notification.Name("DynamicIsland.keyboardIlluminationChanged") + + override func sendEvent(_ event: NSEvent) { + if event.type == .systemDefined && event.subtype.rawValue == 8 { + let keyCode = ((event.data1 & 0xFFFF0000) >> 16) + let keyFlags = (event.data1 & 0x0000FFFF) + // Get the key state. 0xA is KeyDown, OxB is KeyUp + let keyState = ((keyFlags & 0xFF00) >> 8) == 0xA + let keyRepeat = keyFlags & 0x1 + mediaKeyEvent(key: Int32(keyCode), state: keyState, keyRepeat: Bool(truncating: keyRepeat as NSNumber)) + } + + super.sendEvent(event) + } + + func mediaKeyEvent(key: Int32, state: Bool, keyRepeat: Bool) { + // Only send events on KeyDown. Without this check, these events will happen twice + if state { + switch key { + case NX_KEYTYPE_SOUND_DOWN, NX_KEYTYPE_SOUND_UP, NX_KEYTYPE_MUTE: + NotificationCenter.default.post(name: SystemKeyObserver.volumeChanged, object: self) + case NX_KEYTYPE_BRIGHTNESS_UP, NX_KEYTYPE_BRIGHTNESS_DOWN: + NotificationCenter.default.post(name: SystemKeyObserver.brightnessChanged, object: self) + case NX_KEYTYPE_ILLUMINATION_DOWN, NX_KEYTYPE_ILLUMINATION_UP: + NotificationCenter.default.post(name: SystemKeyObserver.keyboardIlluminationChanged, object: self) + default: + break + } + } + } +} \ No newline at end of file diff --git a/DynamicIsland/managers/SystemOSDManager.swift b/DynamicIsland/managers/SystemOSDManager.swift new file mode 100644 index 0000000..319b75e --- /dev/null +++ b/DynamicIsland/managers/SystemOSDManager.swift @@ -0,0 +1,47 @@ +// +// SystemOSDManager.swift +// DynamicIsland +// +// Adapted from TheBoringWorker-HUD OSDUIManager.swift +// Created by GitHub Copilot on 06/09/25. +// + +import Foundation + +class SystemOSDManager { + private init() {} + + /// Re-enables the system HUD by restarting OSDUIHelper + public static func enableSystemHUD() { + do { + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/bin/killall") + task.arguments = ["-9", "OSDUIHelper"] + try task.run() + print("System HUD re-enabled") + } catch { + NSLog("Error while trying to re-enable OSDUIHelper. \(error)") + } + } + + /// Disables the system HUD by stopping OSDUIHelper + public static func disableSystemHUD() { + do { + let kickstart = Process() + kickstart.executableURL = URL(fileURLWithPath: "/bin/launchctl") + // When macOS boots, OSDUIHelper does not start until a volume button is pressed. We can workaround this by kickstarting it. + kickstart.arguments = ["kickstart", "gui/\(getuid())/com.apple.OSDUIHelper"] + try kickstart.run() + kickstart.waitUntilExit() + usleep(500000) // Make sure it started + + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/bin/killall") + task.arguments = ["-STOP", "OSDUIHelper"] + try task.run() + print("System HUD disabled") + } catch { + NSLog("Error while trying to hide OSDUIHelper. Please create an issue on GitHub. Error: \(error)") + } + } +} \ No newline at end of file diff --git a/DynamicIsland/managers/SystemVolumeManager.swift b/DynamicIsland/managers/SystemVolumeManager.swift new file mode 100644 index 0000000..ee2a2bb --- /dev/null +++ b/DynamicIsland/managers/SystemVolumeManager.swift @@ -0,0 +1,35 @@ +// +// SystemVolumeManager.swift +// DynamicIsland +// +// Adapted from TheBoringWorker-HUD VolumeManager.swift +// Created by GitHub Copilot on 06/09/25. +// + +import Foundation + +class SystemVolumeManager { + private init() {} + + static func isMuted() -> Bool { + do { + return try AppleScriptRunner.run(script: "return output muted of (get volume settings)") == "true" + } catch { + NSLog("Error while trying to retrieve muted properties of device: \(error). Returning default value false.") + return false + } + } + + static func getOutputVolume() -> Float { + do { + if let volumeStr = Float(try AppleScriptRunner.run(script: "return output volume of (get volume settings)")) { + return volumeStr / 100 + } else { + NSLog("Error while trying to parse volume string value. Returning default volume value 1.") + } + } catch { + NSLog("Error while trying to retrieve volume properties of device: \(error). Returning default volume value 1.") + } + return 0.01 + } +} \ No newline at end of file diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index e9b2cf6..18fd8f3 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -221,6 +221,13 @@ extension Defaults.Keys { // MARK: Keyboard Shortcuts static let enableShortcuts = Key("enableShortcuts", default: true) + // MARK: System HUD Feature + static let enableSystemHUD = Key("enableSystemHUD", default: false) + static let enableVolumeHUD = Key("enableVolumeHUD", default: true) + static let enableBrightnessHUD = Key("enableBrightnessHUD", default: true) + static let enableKeyboardBacklightHUD = Key("enableKeyboardBacklightHUD", default: true) + static let systemHUDSensitivity = Key("systemHUDSensitivity", default: 5) + // Helper to determine the default media controller based on macOS version static var defaultMediaController: MediaControllerType { if #available(macOS 15.4, *) {