mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Working Brightness and Volume HUDs
Add Brightness and Volume Indicators
This commit is contained in:
commit
cba0c7cbdb
25 changed files with 1062 additions and 38 deletions
|
|
@ -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 = "<absolute>"; };
|
||||
7E25ECA22E6C6E2600700E72 /* OSD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OSD.framework; path = /System/Library/PrivateFrameworks/OSD.framework; sourceTree = "<absolute>"; };
|
||||
7E25ECA42E6C6E7800700E72 /* DisplayServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DisplayServices.framework; path = /System/Library/PrivateFrameworks/DisplayServices.framework; sourceTree = "<absolute>"; };
|
||||
7E81303D2E3FB40500E08F3F /* mediaremote-adapter.pl */ = {isa = PBXFileReference; lastKnownFileType = text.script.perl; path = "mediaremote-adapter.pl"; sourceTree = "<group>"; };
|
||||
7E81303E2E3FB40500E08F3F /* NowPlayingTestClient */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = NowPlayingTestClient; sourceTree = "<group>"; };
|
||||
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" */;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,20 +2,8 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.calendars</key>
|
||||
<true/>
|
||||
<key>com.apple.security.temporary-exception.apple-events</key>
|
||||
<array>
|
||||
<string>com.spotify.client</string>
|
||||
|
|
|
|||
|
|
@ -87,8 +87,21 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
private var onboardingWindowController: NSWindowController?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// 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<AnyCancellable>()
|
||||
//
|
||||
// // 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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" : {
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
15
DynamicIsland/helpers/AppleScriptError.swift
Normal file
15
DynamicIsland/helpers/AppleScriptError.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
28
DynamicIsland/helpers/AppleScriptRunner.swift
Normal file
28
DynamicIsland/helpers/AppleScriptRunner.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
22
DynamicIsland/helpers/SensorError.swift
Normal file
22
DynamicIsland/helpers/SensorError.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
15
DynamicIsland/helpers/SensorMethod.swift
Normal file
15
DynamicIsland/helpers/SensorMethod.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
83
DynamicIsland/helpers/SystemHUDDebugger.swift
Normal file
83
DynamicIsland/helpers/SystemHUDDebugger.swift
Normal file
|
|
@ -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")")
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
193
DynamicIsland/managers/SystemChangesObserver.swift
Normal file
193
DynamicIsland/managers/SystemChangesObserver.swift
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
73
DynamicIsland/managers/SystemDisplayManager.swift
Normal file
73
DynamicIsland/managers/SystemDisplayManager.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
137
DynamicIsland/managers/SystemHUDManager.swift
Normal file
137
DynamicIsland/managers/SystemHUDManager.swift
Normal file
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
48
DynamicIsland/managers/SystemKeyObserver.swift
Normal file
48
DynamicIsland/managers/SystemKeyObserver.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
135
DynamicIsland/managers/SystemOSDManager.swift
Normal file
135
DynamicIsland/managers/SystemOSDManager.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
DynamicIsland/managers/SystemVolumeManager.swift
Normal file
35
DynamicIsland/managers/SystemVolumeManager.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -221,6 +221,13 @@ extension Defaults.Keys {
|
|||
// MARK: Keyboard Shortcuts
|
||||
static let enableShortcuts = Key<Bool>("enableShortcuts", default: true)
|
||||
|
||||
// MARK: System HUD Feature
|
||||
static let enableSystemHUD = Key<Bool>("enableSystemHUD", default: false)
|
||||
static let enableVolumeHUD = Key<Bool>("enableVolumeHUD", default: true)
|
||||
static let enableBrightnessHUD = Key<Bool>("enableBrightnessHUD", default: true)
|
||||
static let enableKeyboardBacklightHUD = Key<Bool>("enableKeyboardBacklightHUD", default: true)
|
||||
static let systemHUDSensitivity = Key<Int>("systemHUDSensitivity", default: 5)
|
||||
|
||||
// Helper to determine the default media controller based on macOS version
|
||||
static var defaultMediaController: MediaControllerType {
|
||||
if #available(macOS 15.4, *) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue