diff --git a/DynamicIsland.xcodeproj/project.pbxproj b/DynamicIsland.xcodeproj/project.pbxproj index b03bbc4..e11cfe4 100644 --- a/DynamicIsland.xcodeproj/project.pbxproj +++ b/DynamicIsland.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ 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 */; }; + 7E25ECB52E6C8BFB00700E72 /* MediaKeyTap in Frameworks */ = {isa = PBXBuildFile; productRef = 7E25ECB42E6C8BFB00700E72 /* MediaKeyTap */; }; 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 +43,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 +67,16 @@ 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 */, + 7E25ECB52E6C8BFB00700E72 /* MediaKeyTap in Frameworks */, 7E6118192E24071E0050006C /* Defaults in Frameworks */, 7E6118182E24071E0050006C /* TheBoringWorkerNotifier in Frameworks */, 7E6118172E24071E0050006C /* Collections in Frameworks */, @@ -91,6 +102,9 @@ 7E81304F2E3FB42700E08F3F /* Frameworks */ = { isa = PBXGroup; children = ( + 7E25ECA02E6C6C3800700E72 /* CoreBrightness.framework */, + 7E25ECA42E6C6E7800700E72 /* DisplayServices.framework */, + 7E25ECA22E6C6E2600700E72 /* OSD.framework */, 7EE1D73E2E6B1E1200997219 /* IOKit.framework */, 7EE1D73C2E6B1E0B00997219 /* ApplicationServices.framework */, ); @@ -145,6 +159,7 @@ B1628B912CC260C0003D8DF3 /* SwiftUIIntrospect */, B194240A2CD10017003E5DC2 /* LottieUI */, 146BC4362D366E9B0047C66A /* MacroVisionKit */, + 7E25ECB42E6C8BFB00700E72 /* MediaKeyTap */, ); productName = DynamicIsland; productReference = F54406E72CD4987A00C7AFD9 /* DynamicIsland.app */; @@ -185,6 +200,7 @@ B1628B902CC260C0003D8DF3 /* XCRemoteSwiftPackageReference "swiftui-introspect" */, B19424072CD0DB52003E5DC2 /* XCRemoteSwiftPackageReference "LottieUI" */, 146BC4352D366E9B0047C66A /* XCRemoteSwiftPackageReference "MacroVisionKit" */, + 7E25ECB32E6C8BFB00700E72 /* XCRemoteSwiftPackageReference "MediaKeyTap" */, ); preferredProjectObjectVersion = 77; productRefGroup = F54406E82CD4987A00C7AFD9 /* Products */; @@ -371,6 +387,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 +428,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; }; @@ -467,6 +491,14 @@ minimumVersion = 2.6.4; }; }; + 7E25ECB32E6C8BFB00700E72 /* XCRemoteSwiftPackageReference "MediaKeyTap" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/nhurden/MediaKeyTap"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.3.0; + }; + }; 9A987A112C73CAA1005CA465 /* XCRemoteSwiftPackageReference "Pow" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/EmergeTools/Pow"; @@ -538,6 +570,11 @@ package = 14D0321B2C68F3350096E6A1 /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; + 7E25ECB42E6C8BFB00700E72 /* MediaKeyTap */ = { + isa = XCSwiftPackageProductDependency; + package = 7E25ECB32E6C8BFB00700E72 /* XCRemoteSwiftPackageReference "MediaKeyTap" */; + productName = MediaKeyTap; + }; 9A987A0F2C73CA8D005CA465 /* Collections */ = { isa = XCSwiftPackageProductDependency; package = F54407082CD4A23F00C7AFD9 /* XCRemoteSwiftPackageReference "swift-collections" */; diff --git a/DynamicIsland.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DynamicIsland.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d8400f2..e6d1490 100644 --- a/DynamicIsland.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DynamicIsland.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "2741287b65cf0758abd89baadf314f5f8cbfcb46cb8d236a83f98731e5501ee3", + "originHash" : "424f1d62134dd1f9036d13269149d3ce62d9433217dddecb8fb77a10d8189a85", "pins" : [ { "identity" : "defaults", @@ -55,6 +55,15 @@ "version" : "0.1.0" } }, + { + "identity" : "mediakeytap", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nhurden/MediaKeyTap", + "state" : { + "revision" : "7d1b8169d228ef6e5ec7db676a348f2f3e50c734", + "version" : "2.3.0" + } + }, { "identity" : "pow", "kind" : "remoteSourceControl", diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index d0639dd..a3fce63 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -193,6 +193,16 @@ struct ContentView: View { } } } + .onChange(of: coordinator.sneakPeek.show) { _, sneakPeekShowing in + // When sneak peek finishes, check if user is still hovering and open notch if needed + if !sneakPeekShowing { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + if isHovering && vm.notchState == .closed { + doOpen() + } + } + } + } .sensoryFeedback(.alignment, trigger: haptics) .contextMenu { SettingsLink(label: { @@ -507,15 +517,23 @@ struct ContentView: View { haptics.toggle() } - // Don't open notch if there's a sneak peek showing - if coordinator.sneakPeek.show { - return - } - // Delay opening the notch let task = DispatchWorkItem { // ContentView is a struct, so we don't use weak self here guard vm.notchState == .closed, isHovering else { return } + + // Additional check: don't open if a sneak peek just started showing + // to avoid conflicts during system HUD display + if coordinator.sneakPeek.show { + // Schedule a retry after the sneak peek should be gone (default duration + small buffer) + DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) { + if isHovering && vm.notchState == .closed && !coordinator.sneakPeek.show { + doOpen() + } + } + return + } + doOpen() } 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..fc9bf04 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -87,8 +87,21 @@ class AppDelegate: NSObject, NSApplicationDelegate { private var onboardingWindowController: NSWindowController? private var cancellables = Set() + // Media key monitoring + private var globalEventMonitor: Any? + private var localEventMonitor: Any? + // Debouncing mechanism for window size updates private var windowSizeUpdateWorkItem: DispatchWorkItem? +// let calendarManager = CalendarManager.shared +// let webcamManager = WebcamManager.shared +// var closeNotchWorkItem: DispatchWorkItem? +// private var previousScreens: [NSScreen]? +// private var onboardingWindowController: NSWindowController? +// private var cancellables = Set() +// +// // Debouncing mechanism for window size updates +// private var windowSizeUpdateWorkItem: DispatchWorkItem? private func debouncedUpdateWindowSize() { // Cancel any existing work item @@ -108,9 +121,84 @@ class AppDelegate: NSObject, NSApplicationDelegate { return false } + // MARK: - Media Key Monitoring + + private func setupMediaKeyMonitoring() { + // Monitor both global and local events to catch media keys + globalEventMonitor = NSEvent.addGlobalMonitorForEvents(matching: .systemDefined) { [weak self] event in + self?.handleSystemEvent(event) + } + + localEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .systemDefined) { [weak self] event in + self?.handleSystemEvent(event) + return event + } + + print("šŸŽ¹ Media key monitoring started using NSEvent") + } + + private func handleSystemEvent(_ event: NSEvent) { + guard event.subtype.rawValue == 8 else { return } + + let data1 = event.data1 + let keyCode = Int32((data1 & 0xFFFF0000) >> 16) + let keyFlags = Int(data1 & 0x0000FFFF) + let keyState = ((keyFlags & 0xFF00) >> 8) == 0xA // KeyDown + let keyRepeat = (keyFlags & 0x1) != 0 + + guard keyState && !keyRepeat else { return } // Only handle key down, ignore repeats + + switch keyCode { + case NX_KEYTYPE_SOUND_UP, NX_KEYTYPE_SOUND_DOWN, NX_KEYTYPE_MUTE: + print("šŸ”Š Volume key detected: \(keyCode)") + SystemHUDManager.shared.handleVolumeKeyEvent() + case NX_KEYTYPE_BRIGHTNESS_UP, NX_KEYTYPE_BRIGHTNESS_DOWN: + print("ā˜€ļø Brightness key detected: \(keyCode)") + SystemHUDManager.shared.handleBrightnessKeyEvent() + default: + break + } + } + + private func stopMediaKeyMonitoring() { + if let globalMonitor = globalEventMonitor { + NSEvent.removeMonitor(globalMonitor) + globalEventMonitor = nil + } + + if let localMonitor = localEventMonitor { + NSEvent.removeMonitor(localMonitor) + localEventMonitor = nil + } + + print("šŸŽ¹ Media key monitoring stopped") + } + + // MARK: - MediaKeyApplicationDelegate (for future use) + + func didDetectVolumeKey() { + SystemHUDManager.shared.handleVolumeKeyEvent() + } + + func didDetectBrightnessKey() { + SystemHUDManager.shared.handleBrightnessKeyEvent() + } + func applicationWillTerminate(_ notification: Notification) { // Cancel any pending window size updates windowSizeUpdateWorkItem?.cancel() + + // Stop media key monitoring + stopMediaKeyMonitoring() + + print("🚪 Application terminating - ensuring system HUD is restored...") + + // Re-enable system HUD on app termination + SystemOSDManager.enableSystemHUD() + + // Give it time to complete + usleep(500000) // 500ms + NotificationCenter.default.removeObserver(self) } @@ -290,6 +378,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { coordinator.setupWorkersNotificationObservers() + // Setup SystemHUD Manager + SystemHUDManager.shared.setup(coordinator: coordinator) + + // Setup media key monitoring + setupMediaKeyMonitoring() + // Observe tab changes - use debounced updates coordinator.$currentView.sink { [weak self] newView in self?.debouncedUpdateWindowSize() diff --git a/DynamicIsland/DynamicIslandViewCoordinator.swift b/DynamicIsland/DynamicIslandViewCoordinator.swift index 23c4553..6147995 100644 --- a/DynamicIsland/DynamicIslandViewCoordinator.swift +++ b/DynamicIsland/DynamicIslandViewCoordinator.swift @@ -49,7 +49,7 @@ struct ExpandedItem { class DynamicIslandViewCoordinator: ObservableObject { static let shared = DynamicIslandViewCoordinator() - var notifier: TheBoringWorkerNotifier = .init() + var notifier: TheBoringWorkerNotifier @Published var currentView: NotchViews = .home @@ -97,8 +97,8 @@ class DynamicIslandViewCoordinator: ObservableObject { @Published var optionKeyPressed: Bool = true private init() { - selectedScreen = preferredScreen notifier = TheBoringWorkerNotifier() + selectedScreen = preferredScreen } func setupWorkersNotificationObservers() { @@ -171,7 +171,8 @@ class DynamicIslandViewCoordinator: ObservableObject { guard let self = self, !Task.isCancelled else { return } await MainActor.run { withAnimation { - self.toggleSneakPeek(status: false, type: .music) + // Hide the sneak peek with the correct type that was showing + self.toggleSneakPeek(status: false, type: self.sneakPeek.type) self.sneakPeekDuration = 1.5 } } 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/Notch/DynamicIslandHeader.swift b/DynamicIsland/components/Notch/DynamicIslandHeader.swift index 449d8b4..2e6fea2 100644 --- a/DynamicIsland/components/Notch/DynamicIslandHeader.swift +++ b/DynamicIsland/components/Notch/DynamicIslandHeader.swift @@ -14,7 +14,7 @@ struct DynamicIslandHeader: View { @ObservedObject var batteryModel = BatteryStatusViewModel.shared @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject var clipboardManager = ClipboardManager.shared - @StateObject var tvm = TrayDrop.shared + @ObservedObject var tvm = TrayDrop.shared @State private var showClipboardPopover = false @State private var showColorPickerPopover = false diff --git a/DynamicIsland/components/Notch/NotchShelfView.swift b/DynamicIsland/components/Notch/NotchShelfView.swift index 5ec7528..aa925b9 100644 --- a/DynamicIsland/components/Notch/NotchShelfView.swift +++ b/DynamicIsland/components/Notch/NotchShelfView.swift @@ -3,7 +3,7 @@ import SwiftUI struct NotchShelfView: View { @EnvironmentObject var vm: DynamicIslandViewModel - @StateObject var tvm = TrayDrop.shared + @ObservedObject var tvm = TrayDrop.shared var body: some View { HStack { 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/components/Settings/SettingsWindowController.swift b/DynamicIsland/components/Settings/SettingsWindowController.swift index c7e99da..1dbf35b 100644 --- a/DynamicIsland/components/Settings/SettingsWindowController.swift +++ b/DynamicIsland/components/Settings/SettingsWindowController.swift @@ -72,23 +72,30 @@ class SettingsWindowController: NSWindowController { // If window is already visible, bring it to front properly if window?.isVisible == true { - NSApp.activate(ignoringOtherApps: true) - window?.orderFrontRegardless() - window?.makeKeyAndOrderFront(nil) + forceWindowToFront() return } // Show the window with proper ordering + window?.center() + forceWindowToFront() + } + + private func forceWindowToFront() { + // Multi-step approach to ensure window gets focus window?.orderFrontRegardless() window?.makeKeyAndOrderFront(nil) - window?.center() - // Activate the app and ensure window gets focus + // Activate the app with maximum priority NSApp.activate(ignoringOtherApps: true) - // Force window to front after activation - DispatchQueue.main.async { [weak self] in - self?.window?.makeKeyAndOrderFront(nil) + // Double-check after a brief delay + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + guard let window = self?.window else { return } + if !window.isKeyWindow { + window.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + } } } diff --git a/DynamicIsland/components/Shelf/DropItemView.swift b/DynamicIsland/components/Shelf/DropItemView.swift index 131d70c..3c8eb50 100644 --- a/DynamicIsland/components/Shelf/DropItemView.swift +++ b/DynamicIsland/components/Shelf/DropItemView.swift @@ -13,7 +13,7 @@ struct DropItemView: View { let item: TrayDrop.DropItem @EnvironmentObject var vm: DynamicIslandViewModel @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared - @StateObject var tvm = TrayDrop.shared + @ObservedObject var tvm = TrayDrop.shared @State var hover = false 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/helpers/SystemHUDDebugger.swift b/DynamicIsland/helpers/SystemHUDDebugger.swift new file mode 100644 index 0000000..bf93cd5 --- /dev/null +++ b/DynamicIsland/helpers/SystemHUDDebugger.swift @@ -0,0 +1,83 @@ +// +// SystemHUDDebugger.swift +// DynamicIsland +// +// Debug utility for System HUD functionality +// Created by GitHub Copilot on 06/09/25. +// + +import Foundation + +class SystemHUDDebugger { + + /// Test system HUD functionality and print status + public static func testSystemHUD() { + print("\nšŸ” === System HUD Debug Report ===") + + // Check current OSDUIHelper status + let isRunning = SystemOSDManager.isOSDUIHelperRunning() + print("šŸ“Š OSDUIHelper Status: \(isRunning ? "āœ… Running" : "āŒ Not running")") + + // Test disable + print("šŸ”‡ Testing disable...") + SystemOSDManager.disableSystemHUD() + + // Wait and check + usleep(500000) + let isRunningAfterDisable = SystemOSDManager.isOSDUIHelperRunning() + print("šŸ“Š After disable: \(isRunningAfterDisable ? "āœ… Running (stopped)" : "āŒ Not running")") + + // Test enable + print("šŸ”Š Testing re-enable...") + SystemOSDManager.enableSystemHUD() + + // Wait and check + usleep(1000000) + let isRunningAfterEnable = SystemOSDManager.isOSDUIHelperRunning() + print("šŸ“Š After re-enable: \(isRunningAfterEnable ? "āœ… Running" : "āŒ Not running")") + + print("šŸ” === End Debug Report ===\n") + + if !isRunningAfterEnable { + print("āš ļø WARNING: System HUD may not be working properly!") + print("šŸ’” Try pressing volume keys to test system HUD functionality") + } + } + + /// Force restart OSDUIHelper using multiple methods + public static func forceRestartOSDUIHelper() { + print("šŸ”„ Force restarting OSDUIHelper...") + + // Method 1: Kill and kickstart + SystemOSDManager.enableSystemHUD() + + // Method 2: Try launchctl bootstrap (more aggressive) + do { + let bootstrap = Process() + bootstrap.executableURL = URL(fileURLWithPath: "/bin/launchctl") + bootstrap.arguments = ["bootstrap", "gui/\(getuid())", "/System/Library/LaunchAgents/com.apple.OSDUIHelper.plist"] + try bootstrap.run() + bootstrap.waitUntilExit() + print("āœ… Bootstrap method completed") + } catch { + print("āŒ Bootstrap method failed: \(error)") + } + + // Method 3: Try direct service restart + do { + let restart = Process() + restart.executableURL = URL(fileURLWithPath: "/bin/launchctl") + restart.arguments = ["restart", "gui/\(getuid())/com.apple.OSDUIHelper"] + try restart.run() + restart.waitUntilExit() + print("āœ… Restart method completed") + } catch { + print("āŒ Restart method failed: \(error)") + } + + // Check final status + usleep(1000000) + let finalStatus = SystemOSDManager.isOSDUIHelperRunning() + print("šŸ“Š Final OSDUIHelper status: \(finalStatus ? "āœ… Running" : "āŒ Not running")") + } +} \ No newline at end of file diff --git a/DynamicIsland/managers/DynamicIslandExtensionManager.swift b/DynamicIsland/managers/DynamicIslandExtensionManager.swift index 0f70175..4c6e417 100644 --- a/DynamicIsland/managers/DynamicIslandExtensionManager.swift +++ b/DynamicIsland/managers/DynamicIslandExtensionManager.swift @@ -32,7 +32,10 @@ class DynamicIslandExtensionManager: ObservableObject { @Published var installedExtensions: [Extension] = [] { didSet { - print("Extensions installed: \(installedExtensions)") + // Only log when there's an actual change in content, not just initialization + if oldValue != installedExtensions { + print("šŸ“¦ Extensions installed: \(installedExtensions.map { $0.name })") + } } } diff --git a/DynamicIsland/managers/SystemChangesObserver.swift b/DynamicIsland/managers/SystemChangesObserver.swift new file mode 100644 index 0000000..6db5e45 --- /dev/null +++ b/DynamicIsland/managers/SystemChangesObserver.swift @@ -0,0 +1,193 @@ +// +// 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 { + private var oldVolume: Float = 0 + private var oldMuted: Bool = false + private var oldBrightness: Float = 0 + + private weak var coordinator: DynamicIslandViewCoordinator? + + // Backup timer for edge cases only + private var backupTimer: Timer? + + init(coordinator: DynamicIslandViewCoordinator) { + self.coordinator = coordinator + + // Initialize baseline values only once + oldVolume = SystemVolumeManager.getOutputVolume() + oldMuted = SystemVolumeManager.isMuted() + + do { + oldBrightness = try SystemDisplayManager.getDisplayBrightness() + } catch { + NSLog("Failed to retrieve initial display brightness: \(error)") + } + } + + func startObserving() { + guard Defaults[.enableSystemHUD] else { return } + + startMinimalBackupTimer() + } + + private func startMinimalBackupTimer() { + // More frequent backup check (every 300ms) for better responsiveness + // This is a reasonable compromise between the original 200ms and the previous 5s + backupTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + guard Defaults[.enableSystemHUD] else { return } + self?.performBackupCheck() + } + } + + private func performBackupCheck() { + // Only check if we haven't had recent activity + checkVolumeChanges() + checkBrightnessChanges() + } + + // MARK: - Public Media Key Event Handlers (called from MediaKeyApplication) + + func handleVolumeKeyEvent() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + self.handleVolumeKeyPress() + } + } + + func handleBrightnessKeyEvent() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + self.handleBrightnessKeyPress() + } + } + + // MARK: - Key Press Event Handlers + + private func handleVolumeKeyPress() { + guard Defaults[.enableVolumeHUD] else { return } + + let newVolume = SystemVolumeManager.getOutputVolume() + let newMuted = SystemVolumeManager.isMuted() + + // Always show HUD for key press feedback, even if value doesn't change + print("šŸ”Š Volume key pressed - showing HUD: volume=\(newVolume), muted=\(newMuted)") + + if newMuted { + sendVolumeNotification(value: 0.0) + } else { + sendVolumeNotification(value: newVolume) + } + + // Update stored values + oldVolume = newVolume + oldMuted = newMuted + } + + private func handleBrightnessKeyPress() { + guard Defaults[.enableBrightnessHUD] else { return } + guard NSScreen.screens.count > 0 else { return } + + do { + let newBrightness = try SystemDisplayManager.getDisplayBrightness() + + // Always show HUD for key press feedback, even if value doesn't change + print("ā˜€ļø Brightness key pressed - showing HUD: brightness=\(newBrightness)") + sendBrightnessNotification(value: newBrightness) + + // Update stored value + oldBrightness = newBrightness + } catch { + // Silently ignore brightness errors to reduce log spam + } + } + + private func checkVolumeChanges() { + guard Defaults[.enableVolumeHUD] else { return } + + let newVolume = SystemVolumeManager.getOutputVolume() + let newMuted = SystemVolumeManager.isMuted() + + let volumeChanged = !isAlmost(firstNumber: oldVolume, secondNumber: newVolume) + let muteChanged = newMuted != oldMuted + + if volumeChanged || muteChanged { + print("šŸ”Š Volume change detected: \(oldVolume) → \(newVolume), muted: \(oldMuted) → \(newMuted)") + + if newMuted { + sendVolumeNotification(value: 0.0) + } else { + sendVolumeNotification(value: newVolume) + } + oldVolume = newVolume + oldMuted = newMuted + } + } + + private func checkBrightnessChanges() { + guard Defaults[.enableBrightnessHUD] else { return } + guard NSScreen.screens.count > 0 else { return } + + do { + let newBrightness = try SystemDisplayManager.getDisplayBrightness() + let brightnessChanged = !isAlmost(firstNumber: oldBrightness, secondNumber: newBrightness) + + if brightnessChanged { + print("ā˜€ļø Brightness change detected: \(oldBrightness) → \(newBrightness)") + sendBrightnessNotification(value: newBrightness) + oldBrightness = newBrightness + } + } catch { + // Silently ignore brightness errors to reduce log spam + } + } + + private func isAlmost(firstNumber: Float, secondNumber: Float) -> Bool { + // Convert sensitivity setting to a more responsive threshold + // Sensitivity 1-10 scale maps to 0.01-0.10 threshold range + let marginValue = Float(Defaults[.systemHUDSensitivity]) / 100.0 + let threshold = max(0.01, marginValue) // Minimum threshold for responsiveness + return abs(firstNumber - secondNumber) <= threshold + } + + // 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 { + stopObserving() + } + + func stopObserving() { + // Clean up backup timer + backupTimer?.invalidate() + backupTimer = nil + + print("šŸŽ¹ Media key detection stopped") + } +} + + 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..34153e6 --- /dev/null +++ b/DynamicIsland/managers/SystemHUDManager.swift @@ -0,0 +1,137 @@ +// +// SystemHUDManager.swift +// DynamicIsland +// +// Created by GitHub Copilot on 06/09/25. +// + +import Foundation +import Defaults +import Combine + +class SystemHUDManager { + static let shared = SystemHUDManager() + + private var changesObserver: SystemChangesObserver? + private weak var coordinator: DynamicIslandViewCoordinator? + private var isSetupComplete = false + private var isSystemOperationInProgress = false + + private init() { + // Set up observer for settings changes + setupSettingsObserver() + } + + private func setupSettingsObserver() { + // Use Defaults publisher instead of @Default property wrapper + Defaults.publisher(.enableSystemHUD, options: []).sink { [weak self] change in + guard let self = self, self.isSetupComplete else { + return + } + Task { @MainActor in + if change.newValue { + await self.startSystemHUD() + } else { + await self.stopSystemHUD() + } + } + }.store(in: &cancellables) + } + + private var cancellables = Set() + + /// Public property to check if system operations are in progress + var isOperationInProgress: Bool { + return isSystemOperationInProgress + } + + func setup(coordinator: DynamicIslandViewCoordinator) { + self.coordinator = coordinator + + if Defaults[.enableSystemHUD] { + Task { @MainActor in + await startSystemHUD() + self.isSetupComplete = true + } + } else { + isSetupComplete = true + } + } + + @MainActor + private func startSystemHUD() async { + guard let coordinator = coordinator, !isSystemOperationInProgress else { return } + + isSystemOperationInProgress = true + await stopSystemHUD() // Stop any existing observer + + changesObserver = SystemChangesObserver(coordinator: coordinator) + changesObserver?.startObserving() + + // Disable system HUD if possible + disableSystemHUD() + + print("System HUD replacement started") + isSystemOperationInProgress = false + } + + @MainActor + private func stopSystemHUD() async { + guard !isSystemOperationInProgress else { return } + + isSystemOperationInProgress = true + changesObserver?.stopObserving() + changesObserver = nil + + // Re-enable system HUD + enableOriginalSystemHUD() + + print("System HUD replacement stopped") + isSystemOperationInProgress = false + } + + private func disableSystemHUD() { + print("šŸ”‡ Disabling system HUD (OSDUIHelper running: \(SystemOSDManager.isOSDUIHelperRunning()))") + // Disable the system HUD using SystemOSDManager + SystemOSDManager.disableSystemHUD() + } + + private func enableOriginalSystemHUD() { + print("šŸ”Š Re-enabling system HUD (OSDUIHelper running: \(SystemOSDManager.isOSDUIHelperRunning()))") + // Re-enable the system HUD using SystemOSDManager + SystemOSDManager.enableSystemHUD() + + // Check status after a reasonable delay, using async to avoid blocking + Task { + try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds instead of 1 + let isRunning = await SystemOSDManager.isOSDUIHelperRunningAsync() + print("šŸ” OSDUIHelper status after re-enable: \(isRunning ? "āœ… Running" : "āŒ Not running")") + } + } + + // MARK: - Media Key Event Handling + + /// Handle volume key press events from MediaKeyApplication + public func handleVolumeKeyEvent() { + changesObserver?.handleVolumeKeyEvent() + } + + /// Handle brightness key press events from MediaKeyApplication + public func handleBrightnessKeyEvent() { + changesObserver?.handleBrightnessKeyEvent() + } + + deinit { + cancellables.removeAll() + Task { @MainActor in + await stopSystemHUD() + } + // Emergency restore is available via SystemHUDManager.emergencyRestoreSystemHUD() if needed + } + + /// Emergency restore function - call this if system HUD is not working + public static func emergencyRestoreSystemHUD() { + print("🚨 Emergency system HUD restore initiated") + SystemHUDDebugger.forceRestartOSDUIHelper() + } +} \ No newline at end of file diff --git a/DynamicIsland/managers/SystemKeyObserver.swift b/DynamicIsland/managers/SystemKeyObserver.swift new file mode 100644 index 0000000..c6e5a65 --- /dev/null +++ b/DynamicIsland/managers/SystemKeyObserver.swift @@ -0,0 +1,48 @@ +// +// SystemKeyObserver.swift +// DynamicIsland +// +// Adapted from TheBoringWorker-HUD KeyPressObserver.swift +// Created by GitHub Copilot on 06/09/25. +// +// āš ļø DEPRECATED: This class is non-functional as it extends NSApplication +// but is never used as the main app class. Event monitoring has been moved +// to SystemChangesObserver using NSEvent.addGlobalMonitorForEvents. +// + +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..9a9cfe2 --- /dev/null +++ b/DynamicIsland/managers/SystemOSDManager.swift @@ -0,0 +1,135 @@ +// +// 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() { + Task.detached(priority: .background) { + await enableSystemHUDAsync() + } + } + + private static func enableSystemHUDAsync() async { + do { + // First, stop any existing OSDUIHelper process + let stopTask = Process() + stopTask.executableURL = URL(fileURLWithPath: "/usr/bin/killall") + stopTask.arguments = ["-9", "OSDUIHelper"] + try stopTask.run() + stopTask.waitUntilExit() + + // Small delay to ensure process is fully stopped + try await Task.sleep(nanoseconds: 200_000_000) // 200ms + + // Then kickstart it again to ensure it's running properly + let kickstart = Process() + kickstart.executableURL = URL(fileURLWithPath: "/bin/launchctl") + kickstart.arguments = ["kickstart", "gui/\(getuid())/com.apple.OSDUIHelper"] + try kickstart.run() + kickstart.waitUntilExit() + + // Additional delay to ensure service is fully started + try await Task.sleep(nanoseconds: 300_000_000) // 300ms + + await MainActor.run { + print("āœ… System HUD re-enabled") + } + } catch { + await MainActor.run { + NSLog("āŒ Error while trying to re-enable OSDUIHelper: \(error)") + } + + // Fallback: Try to restart the service using launchctl load + do { + let fallbackTask = Process() + fallbackTask.executableURL = URL(fileURLWithPath: "/bin/launchctl") + fallbackTask.arguments = ["load", "-w", "/System/Library/LaunchAgents/com.apple.OSDUIHelper.plist"] + try fallbackTask.run() + fallbackTask.waitUntilExit() + + await MainActor.run { + print("āœ… System HUD re-enabled via fallback method") + } + } catch { + await MainActor.run { + NSLog("āŒ Fallback method also failed: \(error)") + } + } + } + } + + /// Disables the system HUD by stopping OSDUIHelper + public static func disableSystemHUD() { + Task.detached(priority: .background) { + await disableSystemHUDAsync() + } + } + + private static func disableSystemHUDAsync() async { + 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() + + try await Task.sleep(nanoseconds: 500_000_000) // 500ms - async wait instead of usleep + + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/bin/killall") + task.arguments = ["-STOP", "OSDUIHelper"] + try task.run() + task.waitUntilExit() + + await MainActor.run { + print("āœ… System HUD disabled") + } + } catch { + await MainActor.run { + NSLog("āŒ Error while trying to hide OSDUIHelper: \(error)") + } + } + } + + /// Check if OSDUIHelper is currently running + public static func isOSDUIHelperRunning() -> Bool { + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/bin/pgrep") + task.arguments = ["OSDUIHelper"] + + let pipe = Pipe() + task.standardOutput = pipe + + do { + try task.run() + task.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) + + return task.terminationStatus == 0 && !output!.isEmpty + } catch { + return false + } + } + + /// Async version of status checking to avoid main thread blocking + public static func isOSDUIHelperRunningAsync() async -> Bool { + return await withCheckedContinuation { continuation in + Task.detached(priority: .background) { + let result = isOSDUIHelperRunning() + continuation.resume(returning: result) + } + } + } +} \ 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, *) {