From 49d029fefc5b4bb6f66e95c564edb9033e7858e4 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Wed, 29 Apr 2026 23:35:52 -0600 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8=20Add=20`SLSGetSymbolicHotKeyValu?= =?UTF-8?q?e`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Private APIs/SkyLightSymbolLoader.swift | 16 ++++++++++++ Loop/Private APIs/SkyLightToolBelt.swift | 27 ++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/Loop/Private APIs/SkyLightSymbolLoader.swift b/Loop/Private APIs/SkyLightSymbolLoader.swift index 67ac6f91..4d8db020 100644 --- a/Loop/Private APIs/SkyLightSymbolLoader.swift +++ b/Loop/Private APIs/SkyLightSymbolLoader.swift @@ -119,8 +119,24 @@ extension SkyLightSymbolLoader { typealias SLPSPostEventRecordToFunc = @convention(c) (_ psn: UnsafeMutablePointer, _ bytes: UnsafeMutablePointer) -> CGError static let SLPSPostEventRecordTo: SLPSPostEventRecordToFunc? = loadSymbol("SLPSPostEventRecordTo") + + typealias SLSGetSymbolicHotKeyValueFunc = @convention(c) ( + _ hotKey: SLSSymbolicHotKey, + _ outChar: UnsafeMutablePointer?, + _ outKeyCode: UnsafeMutablePointer?, + _ outFlags: UnsafeMutablePointer? + ) -> CGError + static let SLSGetSymbolicHotKeyValue: SLSGetSymbolicHotKeyValueFunc? = loadSymbol("SLSGetSymbolicHotKeyValue") + + typealias SLSIsSymbolicHotKeyEnabledFunc = @convention(c) (_ hotKey: SLSSymbolicHotKey) -> Bool + static let SLSIsSymbolicHotKeyEnabled: SLSIsSymbolicHotKeyEnabledFunc? = loadSymbol("SLSIsSymbolicHotKeyEnabled") + + typealias SLSSetSymbolicHotKeyEnabledFunc = @convention(c) (_ hotKey: SLSSymbolicHotKey, _ enabled: Bool) -> CGError + static let SLSSetSymbolicHotKeyEnabled: SLSSetSymbolicHotKeyEnabledFunc? = loadSymbol("SLSSetSymbolicHotKeyEnabled") } +typealias SLSSymbolicHotKey = Int32 + typealias SLSConnectionID = UInt32 struct SLSWindowCaptureOptions: OptionSet { diff --git a/Loop/Private APIs/SkyLightToolBelt.swift b/Loop/Private APIs/SkyLightToolBelt.swift index 907ed754..151c64fc 100644 --- a/Loop/Private APIs/SkyLightToolBelt.swift +++ b/Loop/Private APIs/SkyLightToolBelt.swift @@ -189,6 +189,33 @@ enum SkyLightToolBelt { return images } + /// Resolves a symbolic hotkey ID to its current `(keyCode, flags)` binding, auto-enabling + /// it if the user has it disabled. Returns `nil` if the symbol lookup fails or the hotkey + /// has no binding (e.g. the user cleared the shortcut in System Settings). + static func resolveSymbolicHotKey(_ hotKey: SLSSymbolicHotKey) -> (keyCode: CGKeyCode, flags: CGEventFlags)? { + guard let SLSGetSymbolicHotKeyValue = SkyLightSymbolLoader.SLSGetSymbolicHotKeyValue, + let SLSIsSymbolicHotKeyEnabled = SkyLightSymbolLoader.SLSIsSymbolicHotKeyEnabled, + let SLSSetSymbolicHotKeyEnabled = SkyLightSymbolLoader.SLSSetSymbolicHotKeyEnabled + else { + log.error("Failed to load SkyLight symbols in \(#function)") + return nil + } + + var keyCode: CGKeyCode = 0 + var rawFlags: UInt32 = 0 + let status = SLSGetSymbolicHotKeyValue(hotKey, nil, &keyCode, &rawFlags) + guard status == .success else { + log.error("Failed to resolve symbolic hotkey \(hotKey): \(status.rawValue) — is the shortcut bound in System Settings?") + return nil + } + + if !SLSIsSymbolicHotKeyEnabled(hotKey) { + _ = SLSSetSymbolicHotKeyEnabled(hotKey, true) + } + + return (keyCode, CGEventFlags(rawValue: UInt64(rawFlags))) + } + /// Retrieves the CGWindowLevel for a specific window. /// - Parameter windowID: The `CGWindowID` of the window to query. /// - Returns: The window's level, or `nil` if the lookup failed. From 87dd19399e8b9517b7e4f328e12e3651a0ceff3f Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Wed, 29 Apr 2026 23:44:48 -0600 Subject: [PATCH 2/7] =?UTF-8?q?=E2=9C=A8=20Cross-space=20throws=20inspired?= =?UTF-8?q?=20by=20Silica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WindowDirection+LocalizedString.swift | 36 ++++++++ .../Window Action/WindowDirection.swift | 41 +++++++++ .../WindowActionEngine.swift | 91 +++++++++++++++++++ 3 files changed, 168 insertions(+) diff --git a/Loop/Window Management/Window Action/WindowDirection+LocalizedString.swift b/Loop/Window Management/Window Action/WindowDirection+LocalizedString.swift index 7d501c3f..9be9796d 100644 --- a/Loop/Window Management/Window Action/WindowDirection+LocalizedString.swift +++ b/Loop/Window Management/Window Action/WindowDirection+LocalizedString.swift @@ -111,6 +111,42 @@ extension WindowDirection { String(localized: "Top Screen", comment: "Window action") case .bottomScreen: String(localized: "Bottom Screen", comment: "Window action") + case .nextSpace: + String(localized: "Move to Next Space", comment: "Window action") + case .previousSpace: + String(localized: "Move to Previous Space", comment: "Window action") + case .moveToSpace1: + String(localized: "Move to Desktop 1", comment: "Window action") + case .moveToSpace2: + String(localized: "Move to Desktop 2", comment: "Window action") + case .moveToSpace3: + String(localized: "Move to Desktop 3", comment: "Window action") + case .moveToSpace4: + String(localized: "Move to Desktop 4", comment: "Window action") + case .moveToSpace5: + String(localized: "Move to Desktop 5", comment: "Window action") + case .moveToSpace6: + String(localized: "Move to Desktop 6", comment: "Window action") + case .moveToSpace7: + String(localized: "Move to Desktop 7", comment: "Window action") + case .moveToSpace8: + String(localized: "Move to Desktop 8", comment: "Window action") + case .moveToSpace9: + String(localized: "Move to Desktop 9", comment: "Window action") + case .moveToSpace10: + String(localized: "Move to Desktop 10", comment: "Window action") + case .moveToSpace11: + String(localized: "Move to Desktop 11", comment: "Window action") + case .moveToSpace12: + String(localized: "Move to Desktop 12", comment: "Window action") + case .moveToSpace13: + String(localized: "Move to Desktop 13", comment: "Window action") + case .moveToSpace14: + String(localized: "Move to Desktop 14", comment: "Window action") + case .moveToSpace15: + String(localized: "Move to Desktop 15", comment: "Window action") + case .moveToSpace16: + String(localized: "Move to Desktop 16", comment: "Window action") case .larger: String(localized: "Larger", comment: "Window action") case .smaller: diff --git a/Loop/Window Management/Window Action/WindowDirection.swift b/Loop/Window Management/Window Action/WindowDirection.swift index ac2d750c..e12ae58e 100644 --- a/Loop/Window Management/Window Action/WindowDirection.swift +++ b/Loop/Window Management/Window Action/WindowDirection.swift @@ -48,6 +48,13 @@ enum WindowDirection: String, CaseIterable, Identifiable, Codable { /// Screen Switching case nextScreen = "NextScreen", previousScreen = "PreviousScreen", leftScreen = "LeftScreen", rightScreen = "RightScreen", topScreen = "TopScreen", bottomScreen = "BottomScreen" + /// Space Switching (move the window to another Mission Control space). + case nextSpace = "NextSpace", previousSpace = "PreviousSpace" + case moveToSpace1 = "MoveToSpace1", moveToSpace2 = "MoveToSpace2", moveToSpace3 = "MoveToSpace3", moveToSpace4 = "MoveToSpace4" + case moveToSpace5 = "MoveToSpace5", moveToSpace6 = "MoveToSpace6", moveToSpace7 = "MoveToSpace7", moveToSpace8 = "MoveToSpace8" + case moveToSpace9 = "MoveToSpace9", moveToSpace10 = "MoveToSpace10", moveToSpace11 = "MoveToSpace11", moveToSpace12 = "MoveToSpace12" + case moveToSpace13 = "MoveToSpace13", moveToSpace14 = "MoveToSpace14", moveToSpace15 = "MoveToSpace15", moveToSpace16 = "MoveToSpace16" + // Size Adjustment case larger = "Larger", smaller = "Smaller" case scaleUp = "ScaleUp", scaleDown = "ScaleDown" @@ -79,6 +86,15 @@ enum WindowDirection: String, CaseIterable, Identifiable, Codable { static var verticalThirds: [WindowDirection] { [.topThird, .topTwoThirds, .verticalCenterThird, .bottomTwoThirds, .bottomThird] } static var horizontalFourths: [WindowDirection] { [.firstFourth, .secondFourth, .thirdFourth, .fourthFourth, .leftThreeFourths, .rightThreeFourths] } static var screenSwitching: [WindowDirection] { [.nextScreen, .previousScreen, .leftScreen, .rightScreen, .topScreen, .bottomScreen] } + static var spaceSwitching: [WindowDirection] { + [ + .nextSpace, .previousSpace, + .moveToSpace1, .moveToSpace2, .moveToSpace3, .moveToSpace4, + .moveToSpace5, .moveToSpace6, .moveToSpace7, .moveToSpace8, + .moveToSpace9, .moveToSpace10, .moveToSpace11, .moveToSpace12, + .moveToSpace13, .moveToSpace14, .moveToSpace15, .moveToSpace16, + ] + } static var sizeAdjustment: [WindowDirection] { [.larger, .smaller, .scaleUp, .scaleDown] } static var shrink: [WindowDirection] { [.shrinkTop, .shrinkBottom, .shrinkRight, .shrinkLeft, .shrinkHorizontal, .shrinkVertical] } static var grow: [WindowDirection] { [.growTop, .growBottom, .growRight, .growLeft, .growHorizontal, .growVertical] } @@ -155,6 +171,31 @@ enum WindowDirection: String, CaseIterable, Identifiable, Codable { default: nil } } + + /// If this direction targets a Mission Control space, returns the resolved destination. + var spaceDestination: WindowActionEngine.SpaceDestination? { + switch self { + case .nextSpace: .nextSpace + case .previousSpace: .previousSpace + case .moveToSpace1: .desktop(1) + case .moveToSpace2: .desktop(2) + case .moveToSpace3: .desktop(3) + case .moveToSpace4: .desktop(4) + case .moveToSpace5: .desktop(5) + case .moveToSpace6: .desktop(6) + case .moveToSpace7: .desktop(7) + case .moveToSpace8: .desktop(8) + case .moveToSpace9: .desktop(9) + case .moveToSpace10: .desktop(10) + case .moveToSpace11: .desktop(11) + case .moveToSpace12: .desktop(12) + case .moveToSpace13: .desktop(13) + case .moveToSpace14: .desktop(14) + case .moveToSpace15: .desktop(15) + case .moveToSpace16: .desktop(16) + default: nil + } + } } extension WindowDirection: CustomDebugStringConvertible { diff --git a/Loop/Window Management/Window Manipulation/WindowActionEngine.swift b/Loop/Window Management/Window Manipulation/WindowActionEngine.swift index 308d097d..6c5534f1 100644 --- a/Loop/Window Management/Window Manipulation/WindowActionEngine.swift +++ b/Loop/Window Management/Window Manipulation/WindowActionEngine.swift @@ -111,6 +111,12 @@ final class WindowActionEngine { return handleFocusAction(context.action, currentWindow: context.window) } + // Cross-space throws + if let window = context.window, let destination = direction.spaceDestination { + await throwWindow(window, to: destination) + return .noOp + } + // Quick actions that don't require resize logic if let result = handleQuickAction(context.action, window: context.window) { return result @@ -181,4 +187,89 @@ final class WindowActionEngine { window.minimized = true } } + + // MARK: - Cross-Space Window Throw + + enum SpaceDestination { + case previousSpace + case nextSpace + case desktop(UInt) + + var hotKey: SLSSymbolicHotKey? { + switch self { + case .previousSpace: 79 + case .nextSpace: 81 + case let .desktop(n) where (1 ... 16).contains(n): SLSSymbolicHotKey(118 + n - 1) + case .desktop: nil + } + } + + var description: String { + switch self { + case .previousSpace: "previous space" + case .nextSpace: "next space" + case let .desktop(n): "desktop \(n)" + } + } + } + + private func throwWindow(_ window: Window, to space: SpaceDestination) async { + guard let hotKey = space.hotKey else { + log.error("invalid destination \(space.description)") + return + } + + // Capture the cursor position so we can restore it after the throw. + let originalCursor = CGEvent(source: nil)?.location ?? .zero + + // Resolve the symbolic hotkey (also auto-enables it if disabled). + guard let (keyCode, flags) = SkyLightToolBelt.resolveSymbolicHotKey(hotKey) else { + return + } + + // Compute the drag anchor: midX of minimize button, midY between window top and minimize button center. + guard let minimizeButton: AXUIElement = try? window.axWindow.getValue(.minimizeButton), + let minPos: CGPoint = try? minimizeButton.getValue(.position), + let minSize: CGSize = try? minimizeButton.getValue(.size) + else { + log.error("could not resolve minimize button on focused window") + return + } + let winFrame = window.frame + let minRect = CGRect(origin: minPos, size: minSize) + let anchor = CGPoint( + x: minRect.midX, + y: winFrame.origin.y + abs(winFrame.origin.y - minRect.minY) / 2.0 + ) + + // Synthesize move + mouseDown + drag + mouseUp at anchor + let move = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: anchor, mouseButton: .left) + let down = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: anchor, mouseButton: .left) + let drag = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDragged, mouseCursorPosition: anchor, mouseButton: .left) + let up = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: anchor, mouseButton: .left) + move?.flags = [] + down?.flags = [] + drag?.flags = [] + up?.flags = [] + + move?.post(tap: .cghidEventTap) + down?.post(tap: .cghidEventTap) + drag?.post(tap: .cghidEventTap) + + // Wait 50 ms before sending the space-switch hotkey + try? await Task.sleep(for: .milliseconds(50)) + let kDown = CGEvent(keyboardEventSource: nil, virtualKey: keyCode, keyDown: true) + let kUp = CGEvent(keyboardEventSource: nil, virtualKey: keyCode, keyDown: false) + kDown?.flags = flags + kUp?.flags = [] + kDown?.post(tap: .cghidEventTap) + kUp?.post(tap: .cghidEventTap) + + // Wait 400 ms for the space transition animation, then release the mouse + try? await Task.sleep(for: .milliseconds(400)) + up?.post(tap: .cghidEventTap) + + // Restore the cursor to its pre-throw position. + CGWarpMouseCursorPosition(originalCursor) + } } From 9c3aa2f5556ec592bfea6ebb7152a7b8aa318a36 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Wed, 1 Jul 2026 02:20:46 -0600 Subject: [PATCH 3/7] =?UTF-8?q?=E2=9C=A8=20Replace=20Space=20throw=20drag?= =?UTF-8?q?=20simulation=20with=20SkyLight=20move=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Stephan Casas --- Loop/Localizable.xcstrings | 54 +++++++ Loop/Private APIs/SkyLightBridgedSPI.swift | 148 ++++++++++++++++++ Loop/Private APIs/SkyLightSymbolLoader.swift | 6 + Loop/Private APIs/SkyLightToolBelt.swift | 147 +++++++++++++++++ .../Window Action/WindowDirection.swift | 3 +- .../WindowActionEngine.swift | 72 ++------- 6 files changed, 370 insertions(+), 60 deletions(-) create mode 100644 Loop/Private APIs/SkyLightBridgedSPI.swift diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 0f72a46a..7bb84655 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -18073,6 +18073,60 @@ } } }, + "Move to Desktop 1" : { + "comment" : "Window action" + }, + "Move to Desktop 2" : { + "comment" : "Window action" + }, + "Move to Desktop 3" : { + "comment" : "Window action" + }, + "Move to Desktop 4" : { + "comment" : "Window action" + }, + "Move to Desktop 5" : { + "comment" : "Window action" + }, + "Move to Desktop 6" : { + "comment" : "Window action" + }, + "Move to Desktop 7" : { + "comment" : "Window action" + }, + "Move to Desktop 8" : { + "comment" : "Window action" + }, + "Move to Desktop 9" : { + "comment" : "Window action" + }, + "Move to Desktop 10" : { + "comment" : "Window action" + }, + "Move to Desktop 11" : { + "comment" : "Window action" + }, + "Move to Desktop 12" : { + "comment" : "Window action" + }, + "Move to Desktop 13" : { + "comment" : "Window action" + }, + "Move to Desktop 14" : { + "comment" : "Window action" + }, + "Move to Desktop 15" : { + "comment" : "Window action" + }, + "Move to Desktop 16" : { + "comment" : "Window action" + }, + "Move to Next Space" : { + "comment" : "Window action" + }, + "Move to Previous Space" : { + "comment" : "Window action" + }, "Move Up" : { "comment" : "Window action", "localizations" : { diff --git a/Loop/Private APIs/SkyLightBridgedSPI.swift b/Loop/Private APIs/SkyLightBridgedSPI.swift new file mode 100644 index 00000000..284ee507 --- /dev/null +++ b/Loop/Private APIs/SkyLightBridgedSPI.swift @@ -0,0 +1,148 @@ +// +// SkyLightBridgedSPI.swift +// Loop +// +// Created by Kai Azim on 2026-07-02. +// Thanks to Stephan Casas for originally showing me these private space-switching APIs :) +// + +import CoreGraphics +import Darwin +import Foundation + +@available(macOS 14.0, *) +enum SkyLightBridgedSPI { + private enum OperationClassName { + static let moveWindowsToManagedSpace = "SLSBridgedMoveWindowsToManagedSpaceOperation" + static let copyManagedDisplaySpaces = "SLSBridgedCopyManagedDisplaySpacesOperation" + static let copySpacesForWindows = "SLSBridgedCopySpacesForWindowsOperation" + } + + private enum SelectorName { + static let performWithWMBridgeDelegate = "performWithWMBridgeDelegate" + static let initWithWindowsSpaceID = "initWithWindows:spaceID:" + static let initWithOptionsWindows = "initWithOptions:windows:" + } + + private static let objcMessageSend: UnsafeMutableRawPointer? = { + guard let handle = dlopen(nil, RTLD_LAZY) else { + return nil + } + + return dlsym(handle, "objc_msgSend") + }() + + static func moveWindow(_ windowID: CGWindowID, toSpace spaceID: UInt64) -> Bool { + guard let operation = makeMoveWindowsToManagedSpaceOperation(windowIDs: [windowID], spaceID: spaceID) else { + return false + } + + _ = performWithWMBridgeDelegate(operation) + return true + } + + static func copyManagedDisplaySpaces() -> [NSDictionary]? { + guard let operationClass = NSClassFromString(OperationClassName.copyManagedDisplaySpaces) as? NSObject.Type else { + return nil + } + + let operation = operationClass.init() + guard let result = performWithWMBridgeDelegate(operation), + let propertyList = result.value(forKey: "propertyListArray") as? [NSDictionary] + else { + return nil + } + + return propertyList + } + + static func copySpaces(forWindows windowIDs: [CGWindowID], options: SLSSpaceMask) -> [NSNumber]? { + guard let operation = makeCopySpacesForWindowsOperation(windowIDs: windowIDs, options: options), + let result = performWithWMBridgeDelegate(operation), + let numbers = result.value(forKey: "numbers") as? [NSNumber] + else { + return nil + } + + return numbers + } + + private static func performWithWMBridgeDelegate(_ operation: AnyObject) -> AnyObject? { + guard let objcMessageSend else { + return nil + } + + let selector = NSSelectorFromString(SelectorName.performWithWMBridgeDelegate) + guard operation.responds(to: selector) else { + return nil + } + + typealias PerformWithWMBridgeDelegate = @convention(c) (AnyObject, Selector) -> AnyObject? + let performWithWMBridgeDelegate = unsafeBitCast(objcMessageSend, to: PerformWithWMBridgeDelegate.self) + return performWithWMBridgeDelegate(operation, selector) + } + + private static func allocate(_ operationClass: NSObject.Type) -> AnyObject? { + guard let objcMessageSend else { + return nil + } + + typealias AllocateOperation = @convention(c) (AnyClass, Selector) -> AnyObject? + let allocateOperation = unsafeBitCast(objcMessageSend, to: AllocateOperation.self) + return allocateOperation(operationClass, NSSelectorFromString("alloc")) + } + + private static func makeMoveWindowsToManagedSpaceOperation( + windowIDs: [CGWindowID], + spaceID: UInt64 + ) -> AnyObject? { + guard let objcMessageSend, + let operationClass = NSClassFromString(OperationClassName.moveWindowsToManagedSpace) as? NSObject.Type + else { + return nil + } + + let initializer = NSSelectorFromString(SelectorName.initWithWindowsSpaceID) + guard operationClass.instancesRespond(to: initializer), + let allocatedOperation = allocate(operationClass) + else { + return nil + } + + typealias InitMoveWindowsToManagedSpace = @convention(c) (AnyObject, Selector, NSArray, UInt64) -> AnyObject? + let initMoveWindowsToManagedSpace = unsafeBitCast(objcMessageSend, to: InitMoveWindowsToManagedSpace.self) + return initMoveWindowsToManagedSpace( + allocatedOperation, + initializer, + windowIDs.map { NSNumber(value: $0) } as NSArray, + spaceID + ) + } + + private static func makeCopySpacesForWindowsOperation( + windowIDs: [CGWindowID], + options: SLSSpaceMask + ) -> AnyObject? { + guard let objcMessageSend, + let operationClass = NSClassFromString(OperationClassName.copySpacesForWindows) as? NSObject.Type + else { + return nil + } + + let initializer = NSSelectorFromString(SelectorName.initWithOptionsWindows) + guard operationClass.instancesRespond(to: initializer), + let allocatedOperation = allocate(operationClass) + else { + return nil + } + + typealias InitCopySpacesForWindows = @convention(c) (AnyObject, Selector, Int32, NSArray) -> AnyObject? + let initCopySpacesForWindows = unsafeBitCast(objcMessageSend, to: InitCopySpacesForWindows.self) + return initCopySpacesForWindows( + allocatedOperation, + initializer, + options.rawValue, + windowIDs.map { NSNumber(value: $0) } as NSArray + ) + } +} diff --git a/Loop/Private APIs/SkyLightSymbolLoader.swift b/Loop/Private APIs/SkyLightSymbolLoader.swift index 4d8db020..4fbd88ae 100644 --- a/Loop/Private APIs/SkyLightSymbolLoader.swift +++ b/Loop/Private APIs/SkyLightSymbolLoader.swift @@ -154,4 +154,10 @@ struct SLSWindowCaptureOptions: OptionSet { static let fullSize = Self(rawValue: 1 << 19) } +enum SLSSpaceMask: Int32 { + case onScreenManaged = 5 + case offScreenManaged = 6 + case allManaged = 7 +} + let kCPSUserGenerated: UInt32 = 0x200 diff --git a/Loop/Private APIs/SkyLightToolBelt.swift b/Loop/Private APIs/SkyLightToolBelt.swift index 151c64fc..59ac25f9 100644 --- a/Loop/Private APIs/SkyLightToolBelt.swift +++ b/Loop/Private APIs/SkyLightToolBelt.swift @@ -11,6 +11,53 @@ import SwiftUI /// A wrapper for functions defined in `SkyLightSymbolLoader` @Loggable(style: .static) enum SkyLightToolBelt { + struct ManagedDisplay { + let identifier: UUID + let spaces: [ManagedSpace] + let currentSpace: ManagedSpace + + init?(dictionary: NSDictionary) { + guard + let identifier = UUID(uuidString: dictionary["Display Identifier"] as? String ?? ""), + let spaces = (dictionary["Spaces"] as? [NSDictionary])?.compactMap({ ManagedSpace(dictionary: $0) }), + let currentSpace = ManagedSpace(dictionary: dictionary["Current Space"] as? NSDictionary) + else { + return nil + } + + self.identifier = identifier + self.spaces = spaces + self.currentSpace = currentSpace + } + } + + struct ManagedSpace { + let id: UInt64 + let managedID: UInt64 + let type: UInt64 + let uuid: UUID? + + var isDesktop: Bool { + type == 0 + } + + init?(dictionary: NSDictionary?) { + guard + let dictionary, + let id = dictionary["id64"] as? UInt64, + let managedID = dictionary["ManagedSpaceID"] as? UInt64, + let type = dictionary["type"] as? UInt64 + else { + return nil + } + + self.id = id + self.managedID = managedID + self.type = type + self.uuid = UUID(uuidString: dictionary["uuid"] as? String ?? "") + } + } + /// Brings the window’s owning process to the front using SkyLight APIs. /// - Parameters: /// - windowID: The `CGWindowID` of the window to make the frontmost process. @@ -238,6 +285,106 @@ enum SkyLightToolBelt { return level } + /// Moves the window to a Mission Control desktop space. + /// - Parameters: + /// - windowID: The `CGWindowID` of the window to move. + /// - spaceID: The target managed space id64. + /// - Returns: Whether the window is on, or was moved to, the target space. + @available(macOS 14.0, *) + @discardableResult + static func moveWindow(_ windowID: CGWindowID, toSpace spaceID: UInt64) -> Bool { + if copySpaces(forWindows: [windowID]).contains(spaceID) { + return true + } + + let moved = SkyLightBridgedSPI.moveWindow(windowID, toSpace: spaceID) + if !moved { + log.error("SkyLight bridged window movement is unavailable") + } + + return moved + } + + /// Returns all managed displays and their spaces in Mission Control order. + @available(macOS 14.0, *) + static func copyDisplaysWithSpaces() -> [ManagedDisplay] { + guard let result = SkyLightBridgedSPI.copyManagedDisplaySpaces() else { + log.error("SkyLight bridged display space lookup is unavailable") + return [] + } + + return result.compactMap(ManagedDisplay.init(dictionary:)) + } + + /// Returns the managed space ids for the given windows. + @available(macOS 14.0, *) + static func copySpaces(forWindows windowIDs: [CGWindowID]) -> [UInt64] { + guard let numbers = SkyLightBridgedSPI.copySpaces(forWindows: windowIDs, options: .allManaged) else { + log.error("SkyLight bridged window space lookup is unavailable") + return [] + } + + return numbers.map { UInt64(truncating: $0) } + } + + /// Returns the first managed space id for a window, or `nil` if SkyLight cannot resolve it. + @available(macOS 14.0, *) + static func copySpace(forWindow windowID: CGWindowID) -> UInt64? { + let spaceID = copySpaces(forWindows: [windowID]).first ?? 0 + return spaceID == 0 ? nil : spaceID + } + + /// Resolves the desktop space adjacent to the window's current desktop. + /// - Parameters: + /// - windowID: The target window. + /// - offset: `-1` for previous desktop, `1` for next desktop. + @available(macOS 14.0, *) + static func desktopSpace(forWindow windowID: CGWindowID, offset: Int) -> ManagedSpace? { + guard offset != 0, + let currentSpaceID = copySpace(forWindow: windowID), + let display = copyDisplaysWithSpaces().first(where: { display in + display.spaces.contains { $0.id == currentSpaceID } + }) + else { + return nil + } + + let desktops = display.spaces.filter(\.isDesktop) + guard let currentIndex = desktops.firstIndex(where: { $0.id == currentSpaceID }) else { + log.error("Window \(windowID) is not on a regular desktop space") + return nil + } + + let targetIndex = currentIndex + offset + guard desktops.indices.contains(targetIndex) else { + return nil + } + + return desktops[targetIndex] + } + + /// Resolves a 1-based desktop number on the display hosting the window's current space. + @available(macOS 14.0, *) + static func desktopSpace(forWindow windowID: CGWindowID, desktopNumber: UInt) -> ManagedSpace? { + guard desktopNumber > 0, + let currentSpaceID = copySpace(forWindow: windowID), + let display = copyDisplaysWithSpaces().first(where: { display in + display.spaces.contains { $0.id == currentSpaceID } + }) + else { + return nil + } + + let desktops = display.spaces.filter(\.isDesktop) + let targetIndex = Int(desktopNumber - 1) + + guard desktops.indices.contains(targetIndex) else { + return nil + } + + return desktops[targetIndex] + } + /// Retrieves the corner radii for a specific window. /// - Parameter windowID: The `CGWindowID` of the window /// - Returns: The corner radii of the window if the operation was successful, or `nil` otherwise. diff --git a/Loop/Window Management/Window Action/WindowDirection.swift b/Loop/Window Management/Window Action/WindowDirection.swift index e12ae58e..e618b48f 100644 --- a/Loop/Window Management/Window Action/WindowDirection.swift +++ b/Loop/Window Management/Window Action/WindowDirection.swift @@ -92,9 +92,10 @@ enum WindowDirection: String, CaseIterable, Identifiable, Codable { .moveToSpace1, .moveToSpace2, .moveToSpace3, .moveToSpace4, .moveToSpace5, .moveToSpace6, .moveToSpace7, .moveToSpace8, .moveToSpace9, .moveToSpace10, .moveToSpace11, .moveToSpace12, - .moveToSpace13, .moveToSpace14, .moveToSpace15, .moveToSpace16, + .moveToSpace13, .moveToSpace14, .moveToSpace15, .moveToSpace16 ] } + static var sizeAdjustment: [WindowDirection] { [.larger, .smaller, .scaleUp, .scaleDown] } static var shrink: [WindowDirection] { [.shrinkTop, .shrinkBottom, .shrinkRight, .shrinkLeft, .shrinkHorizontal, .shrinkVertical] } static var grow: [WindowDirection] { [.growTop, .growBottom, .growRight, .growLeft, .growHorizontal, .growVertical] } diff --git a/Loop/Window Management/Window Manipulation/WindowActionEngine.swift b/Loop/Window Management/Window Manipulation/WindowActionEngine.swift index 6c5534f1..8037aa12 100644 --- a/Loop/Window Management/Window Manipulation/WindowActionEngine.swift +++ b/Loop/Window Management/Window Manipulation/WindowActionEngine.swift @@ -195,15 +195,6 @@ final class WindowActionEngine { case nextSpace case desktop(UInt) - var hotKey: SLSSymbolicHotKey? { - switch self { - case .previousSpace: 79 - case .nextSpace: 81 - case let .desktop(n) where (1 ... 16).contains(n): SLSSymbolicHotKey(118 + n - 1) - case .desktop: nil - } - } - var description: String { switch self { case .previousSpace: "previous space" @@ -214,62 +205,25 @@ final class WindowActionEngine { } private func throwWindow(_ window: Window, to space: SpaceDestination) async { - guard let hotKey = space.hotKey else { - log.error("invalid destination \(space.description)") + guard #available(macOS 14.0, *) else { + log.error("Cross-space window movement requires macOS 14.0 or later") return } - // Capture the cursor position so we can restore it after the throw. - let originalCursor = CGEvent(source: nil)?.location ?? .zero - - // Resolve the symbolic hotkey (also auto-enables it if disabled). - guard let (keyCode, flags) = SkyLightToolBelt.resolveSymbolicHotKey(hotKey) else { - return + let targetSpace: SkyLightToolBelt.ManagedSpace? = switch space { + case .previousSpace: + SkyLightToolBelt.desktopSpace(forWindow: window.cgWindowID, offset: -1) + case .nextSpace: + SkyLightToolBelt.desktopSpace(forWindow: window.cgWindowID, offset: 1) + case let .desktop(number): + SkyLightToolBelt.desktopSpace(forWindow: window.cgWindowID, desktopNumber: number) } - // Compute the drag anchor: midX of minimize button, midY between window top and minimize button center. - guard let minimizeButton: AXUIElement = try? window.axWindow.getValue(.minimizeButton), - let minPos: CGPoint = try? minimizeButton.getValue(.position), - let minSize: CGSize = try? minimizeButton.getValue(.size) - else { - log.error("could not resolve minimize button on focused window") + guard let targetSpace else { + log.error("Could not resolve target \(space.description) for window \(window.cgWindowID)") return } - let winFrame = window.frame - let minRect = CGRect(origin: minPos, size: minSize) - let anchor = CGPoint( - x: minRect.midX, - y: winFrame.origin.y + abs(winFrame.origin.y - minRect.minY) / 2.0 - ) - - // Synthesize move + mouseDown + drag + mouseUp at anchor - let move = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: anchor, mouseButton: .left) - let down = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: anchor, mouseButton: .left) - let drag = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDragged, mouseCursorPosition: anchor, mouseButton: .left) - let up = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: anchor, mouseButton: .left) - move?.flags = [] - down?.flags = [] - drag?.flags = [] - up?.flags = [] - - move?.post(tap: .cghidEventTap) - down?.post(tap: .cghidEventTap) - drag?.post(tap: .cghidEventTap) - - // Wait 50 ms before sending the space-switch hotkey - try? await Task.sleep(for: .milliseconds(50)) - let kDown = CGEvent(keyboardEventSource: nil, virtualKey: keyCode, keyDown: true) - let kUp = CGEvent(keyboardEventSource: nil, virtualKey: keyCode, keyDown: false) - kDown?.flags = flags - kUp?.flags = [] - kDown?.post(tap: .cghidEventTap) - kUp?.post(tap: .cghidEventTap) - - // Wait 400 ms for the space transition animation, then release the mouse - try? await Task.sleep(for: .milliseconds(400)) - up?.post(tap: .cghidEventTap) - - // Restore the cursor to its pre-throw position. - CGWarpMouseCursorPosition(originalCursor) + + SkyLightToolBelt.moveWindow(window.cgWindowID, toSpace: targetSpace.id) } } From 398b06b19e6b03f9ff40200acab1a4db60466a52 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Wed, 1 Jul 2026 02:39:57 -0600 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=92=84=20Expose=20all=20space-switchi?= =?UTF-8?q?ng=20actions=20inside=20UI=20with=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Localizable.xcstrings | 3 ++ Loop/Utilities/PickerList.swift | 1 + .../Window Action/IconView.swift | 28 ++++++++--- .../Window Action/WindowAction+Image.swift | 47 +++++++++++++++++-- .../WindowActionEngine.swift | 7 +++ 5 files changed, 76 insertions(+), 10 deletions(-) diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 7bb84655..2d865d65 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -30052,6 +30052,9 @@ } } }, + "Space Switching" : { + "comment" : "Section header in the action picker of the Keybinds tab" + }, "Stage Manager" : { "comment" : "Section header shown in settings", "localizations" : { diff --git a/Loop/Utilities/PickerList.swift b/Loop/Utilities/PickerList.swift index 3d75e1de..46cc808e 100644 --- a/Loop/Utilities/PickerList.swift +++ b/Loop/Utilities/PickerList.swift @@ -187,6 +187,7 @@ extension PickerSection where V == WindowDirection { .init(String(localized: "Vertical Thirds", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.verticalThirds), .init(String(localized: "Horizontal Fourths", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.horizontalFourths), .init(String(localized: "Screen Switching", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.screenSwitching), + .init(String(localized: "Space Switching", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.spaceSwitching), .init(String(localized: "Size Adjustment", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.sizeAdjustment), .init(String(localized: "Shrink", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.shrink), .init(String(localized: "Grow", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.grow), diff --git a/Loop/Window Management/Window Action/IconView.swift b/Loop/Window Management/Window Action/IconView.swift index 413fc167..eeac548c 100644 --- a/Loop/Window Management/Window Action/IconView.swift +++ b/Loop/Window Management/Window Action/IconView.swift @@ -190,7 +190,7 @@ final class IconRenderView: NSView { animatePath(layer: fillLayer, to: newPath, duration: duration) case let .image(image): imageLayer.contents = processImage(image, color: .textColor) - imageLayer.frame = getImageBounds() + imageLayer.frame = getImageBounds(for: image) animateAlpha(layer: fillLayer, to: 0, duration: duration) animateAlpha(layer: imageLayer, to: 1, duration: duration) } @@ -290,14 +290,28 @@ final class IconRenderView: NSView { return sizedImage } - private func getImageBounds() -> NSRect { + private func getImageBounds(for image: NSImage) -> NSRect { let insetBounds = bounds.insetBy(dx: strokeWidth, dy: strokeWidth) - let side = min(insetBounds.width, insetBounds.height) + let imageAspectRatio = image.size.width / max(image.size.height, 1) + let boundsAspectRatio = insetBounds.width / max(insetBounds.height, 1) + + let imageSize = if imageAspectRatio > boundsAspectRatio { + CGSize( + width: insetBounds.width, + height: insetBounds.width / imageAspectRatio + ) + } else { + CGSize( + width: insetBounds.height * imageAspectRatio, + height: insetBounds.height + ) + } + let squareRect = CGRect( - x: insetBounds.midX - side / 2, - y: insetBounds.midY - side / 2, - width: side, - height: side + x: insetBounds.midX - imageSize.width / 2, + y: insetBounds.midY - imageSize.height / 2, + width: imageSize.width, + height: imageSize.height ) return squareRect } diff --git a/Loop/Window Management/Window Action/WindowAction+Image.swift b/Loop/Window Management/Window Action/WindowAction+Image.swift index 54520a2a..d6f4ca99 100644 --- a/Loop/Window Management/Window Action/WindowAction+Image.swift +++ b/Loop/Window Management/Window Action/WindowAction+Image.swift @@ -11,6 +11,7 @@ import SwiftUI enum WindowActionImage { case systemImage(String) case resource(ImageResource) + case number(Int) var image: Image { switch self { @@ -18,6 +19,8 @@ enum WindowActionImage { Image(systemName: string) case let .resource(resource): Image(resource) + case .number: + Image(nsImage: nsImage) } } @@ -28,13 +31,51 @@ enum WindowActionImage { return image?.withSymbolConfiguration(.init(pointSize: 20, weight: .bold)) ?? image ?? NSImage() case let .resource(resource): return NSImage(resource: resource) + case let .number(number): + return Self.numberImage(number) } } + + private static func numberImage(_ number: Int) -> NSImage { + let size = CGSize(width: 22, height: 16) + let image = NSImage(size: size) + + image.lockFocus() + defer { + image.unlockFocus() + image.isTemplate = true + } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + + let attributes: [NSAttributedString.Key: Any] = [ + .font: NSFont.monospacedDigitSystemFont(ofSize: 14, weight: .bold), + .foregroundColor: NSColor.black, + .paragraphStyle: paragraphStyle + ] + + let text = "\(number)" as NSString + let textSize = text.size(withAttributes: attributes) + let textRect = CGRect( + x: 0, + y: (size.height - textSize.height) / 2, + width: size.width, + height: textSize.height + ) + text.draw(in: textRect, withAttributes: attributes) + + return image + } } extension WindowAction { var image: WindowActionImage? { - switch direction { + if let desktopNumber = direction.spaceDestination?.desktopNumber { + return .number(Int(desktopNumber)) + } + + return switch direction { case .noAction: .systemImage("questionmark") case .undo: @@ -51,9 +92,9 @@ extension WindowAction { .systemImage("arrow.up.and.down") case .maximizeWidth: .systemImage("arrow.left.and.right") - case .nextScreen: + case .nextScreen, .nextSpace: .systemImage("arrow.forward") - case .previousScreen: + case .previousScreen, .previousSpace: .systemImage("arrow.backward") case .leftScreen: .systemImage("arrow.left.to.line") diff --git a/Loop/Window Management/Window Manipulation/WindowActionEngine.swift b/Loop/Window Management/Window Manipulation/WindowActionEngine.swift index 8037aa12..d1f3c398 100644 --- a/Loop/Window Management/Window Manipulation/WindowActionEngine.swift +++ b/Loop/Window Management/Window Manipulation/WindowActionEngine.swift @@ -202,6 +202,13 @@ final class WindowActionEngine { case let .desktop(n): "desktop \(n)" } } + + var desktopNumber: UInt? { + if case let .desktop(number) = self { + return number + } + return nil + } } private func throwWindow(_ window: Window, to space: SpaceDestination) async { From 238af3c29e0f53cb527f5be1e24ad3d4ec7d2555 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Wed, 1 Jul 2026 02:47:31 -0600 Subject: [PATCH 5/7] =?UTF-8?q?=E2=9C=A8=20Only=20show=20max=20amount=20of?= =?UTF-8?q?=20spaces=20user=20has=20configured=20in=20picker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Private APIs/SkyLightToolBelt.swift | 10 +++++++ Loop/Utilities/PickerList.swift | 27 ++++++++++++++++--- .../Window Action/WindowDirection.swift | 6 ++++- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Loop/Private APIs/SkyLightToolBelt.swift b/Loop/Private APIs/SkyLightToolBelt.swift index 59ac25f9..ca435792 100644 --- a/Loop/Private APIs/SkyLightToolBelt.swift +++ b/Loop/Private APIs/SkyLightToolBelt.swift @@ -316,6 +316,16 @@ enum SkyLightToolBelt { return result.compactMap(ManagedDisplay.init(dictionary:)) } + /// Returns the largest number of regular desktop spaces on any managed display. + @available(macOS 14.0, *) + static func maximumDesktopCount() -> Int { + copyDisplaysWithSpaces() + .map { display in + display.spaces.filter(\.isDesktop).count + } + .max() ?? 0 + } + /// Returns the managed space ids for the given windows. @available(macOS 14.0, *) static func copySpaces(forWindows windowIDs: [CGWindowID]) -> [UInt64] { diff --git a/Loop/Utilities/PickerList.swift b/Loop/Utilities/PickerList.swift index 46cc808e..4c46608c 100644 --- a/Loop/Utilities/PickerList.swift +++ b/Loop/Utilities/PickerList.swift @@ -179,15 +179,20 @@ struct PickerSection: Identifiable, Hashable where V: Hashable, V: Identifiab extension PickerSection where V == WindowDirection { static var windowDirections: [PickerSection] { - [ + let spaceSwitching = visibleSpaceSwitchingDirections() + let spaceSwitchingSections: [PickerSection] = spaceSwitching.isEmpty ? [] : [ + .init(String(localized: "Space Switching", comment: "Section header in the action picker of the Keybinds tab"), spaceSwitching) + ] + + return [ .init(String(localized: "General", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.general), .init(String(localized: "Halves", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.halves), .init(String(localized: "Quarters", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.quarters), .init(String(localized: "Horizontal Thirds", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.horizontalThirds), .init(String(localized: "Vertical Thirds", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.verticalThirds), .init(String(localized: "Horizontal Fourths", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.horizontalFourths), - .init(String(localized: "Screen Switching", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.screenSwitching), - .init(String(localized: "Space Switching", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.spaceSwitching), + .init(String(localized: "Screen Switching", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.screenSwitching) + ] + spaceSwitchingSections + [ .init(String(localized: "Size Adjustment", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.sizeAdjustment), .init(String(localized: "Shrink", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.shrink), .init(String(localized: "Grow", comment: "Section header in the action picker of the Keybinds tab"), WindowDirection.grow), @@ -197,4 +202,20 @@ extension PickerSection where V == WindowDirection { .init(String(localized: "Go Back", comment: "Section header in the action picker of the Keybinds tab"), [WindowDirection.initialFrame, WindowDirection.undo]) ] } + + private static func visibleSpaceSwitchingDirections() -> [WindowDirection] { + guard #available(macOS 14.0, *) else { + return [] + } + + let maximumDesktopCount = SkyLightToolBelt.maximumDesktopCount() + guard maximumDesktopCount > 1 else { + return [] + } + + let visibleNumberedActions = WindowDirection.numberedSpaceSwitching + .prefix(maximumDesktopCount) + + return WindowDirection.relativeSpaceSwitching + visibleNumberedActions + } } diff --git a/Loop/Window Management/Window Action/WindowDirection.swift b/Loop/Window Management/Window Action/WindowDirection.swift index e618b48f..1e1aadaa 100644 --- a/Loop/Window Management/Window Action/WindowDirection.swift +++ b/Loop/Window Management/Window Action/WindowDirection.swift @@ -96,6 +96,9 @@ enum WindowDirection: String, CaseIterable, Identifiable, Codable { ] } + static var relativeSpaceSwitching: [WindowDirection] { [.nextSpace, .previousSpace] } + static var numberedSpaceSwitching: [WindowDirection] { Array(spaceSwitching.dropFirst(relativeSpaceSwitching.count)) } + static var sizeAdjustment: [WindowDirection] { [.larger, .smaller, .scaleUp, .scaleDown] } static var shrink: [WindowDirection] { [.shrinkTop, .shrinkBottom, .shrinkRight, .shrinkLeft, .shrinkHorizontal, .shrinkVertical] } static var grow: [WindowDirection] { [.growTop, .growBottom, .growRight, .growLeft, .growHorizontal, .growVertical] } @@ -106,6 +109,7 @@ enum WindowDirection: String, CaseIterable, Identifiable, Codable { // Computed properties for checking conditions var isNoOp: Bool { [.noSelection, .noAction].contains(self) } var willChangeScreen: Bool { WindowDirection.screenSwitching.contains(self) } + var willChangeSpace: Bool { WindowDirection.spaceSwitching.contains(self) } var willAdjustSize: Bool { WindowDirection.sizeAdjustment.contains(self) } var willShrink: Bool { WindowDirection.shrink.contains(self) } var willGrow: Bool { WindowDirection.grow.contains(self) } @@ -116,7 +120,7 @@ enum WindowDirection: String, CaseIterable, Identifiable, Codable { var hasRadialMenuAngle: Bool { let noAngleActions: [WindowDirection] = [.noAction, .noSelection, .minimize, .minimizeOthers, .hide, .initialFrame, .undo, .cycle] - return !(noAngleActions.contains(self) || shouldFillRadialMenu || willChangeScreen || willAdjustSize || willShrink || willGrow || willMove || willFocusWindow) + return !(noAngleActions.contains(self) || shouldFillRadialMenu || willChangeScreen || willChangeSpace || willAdjustSize || willShrink || willGrow || willMove || willFocusWindow) } var shouldFillRadialMenu: Bool { From 0af7e8dbada1867cfd8fcefef7ca5786e97461bb Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Wed, 1 Jul 2026 13:01:42 -0600 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=94=A5=20Remove=20dead=20symbolic=20h?= =?UTF-8?q?otkey=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Private APIs/SkyLightSymbolLoader.swift | 15 ----------- Loop/Private APIs/SkyLightToolBelt.swift | 27 -------------------- 2 files changed, 42 deletions(-) diff --git a/Loop/Private APIs/SkyLightSymbolLoader.swift b/Loop/Private APIs/SkyLightSymbolLoader.swift index 4fbd88ae..8f5c030d 100644 --- a/Loop/Private APIs/SkyLightSymbolLoader.swift +++ b/Loop/Private APIs/SkyLightSymbolLoader.swift @@ -120,23 +120,8 @@ extension SkyLightSymbolLoader { typealias SLPSPostEventRecordToFunc = @convention(c) (_ psn: UnsafeMutablePointer, _ bytes: UnsafeMutablePointer) -> CGError static let SLPSPostEventRecordTo: SLPSPostEventRecordToFunc? = loadSymbol("SLPSPostEventRecordTo") - typealias SLSGetSymbolicHotKeyValueFunc = @convention(c) ( - _ hotKey: SLSSymbolicHotKey, - _ outChar: UnsafeMutablePointer?, - _ outKeyCode: UnsafeMutablePointer?, - _ outFlags: UnsafeMutablePointer? - ) -> CGError - static let SLSGetSymbolicHotKeyValue: SLSGetSymbolicHotKeyValueFunc? = loadSymbol("SLSGetSymbolicHotKeyValue") - - typealias SLSIsSymbolicHotKeyEnabledFunc = @convention(c) (_ hotKey: SLSSymbolicHotKey) -> Bool - static let SLSIsSymbolicHotKeyEnabled: SLSIsSymbolicHotKeyEnabledFunc? = loadSymbol("SLSIsSymbolicHotKeyEnabled") - - typealias SLSSetSymbolicHotKeyEnabledFunc = @convention(c) (_ hotKey: SLSSymbolicHotKey, _ enabled: Bool) -> CGError - static let SLSSetSymbolicHotKeyEnabled: SLSSetSymbolicHotKeyEnabledFunc? = loadSymbol("SLSSetSymbolicHotKeyEnabled") } -typealias SLSSymbolicHotKey = Int32 - typealias SLSConnectionID = UInt32 struct SLSWindowCaptureOptions: OptionSet { diff --git a/Loop/Private APIs/SkyLightToolBelt.swift b/Loop/Private APIs/SkyLightToolBelt.swift index ca435792..43def4e7 100644 --- a/Loop/Private APIs/SkyLightToolBelt.swift +++ b/Loop/Private APIs/SkyLightToolBelt.swift @@ -236,33 +236,6 @@ enum SkyLightToolBelt { return images } - /// Resolves a symbolic hotkey ID to its current `(keyCode, flags)` binding, auto-enabling - /// it if the user has it disabled. Returns `nil` if the symbol lookup fails or the hotkey - /// has no binding (e.g. the user cleared the shortcut in System Settings). - static func resolveSymbolicHotKey(_ hotKey: SLSSymbolicHotKey) -> (keyCode: CGKeyCode, flags: CGEventFlags)? { - guard let SLSGetSymbolicHotKeyValue = SkyLightSymbolLoader.SLSGetSymbolicHotKeyValue, - let SLSIsSymbolicHotKeyEnabled = SkyLightSymbolLoader.SLSIsSymbolicHotKeyEnabled, - let SLSSetSymbolicHotKeyEnabled = SkyLightSymbolLoader.SLSSetSymbolicHotKeyEnabled - else { - log.error("Failed to load SkyLight symbols in \(#function)") - return nil - } - - var keyCode: CGKeyCode = 0 - var rawFlags: UInt32 = 0 - let status = SLSGetSymbolicHotKeyValue(hotKey, nil, &keyCode, &rawFlags) - guard status == .success else { - log.error("Failed to resolve symbolic hotkey \(hotKey): \(status.rawValue) — is the shortcut bound in System Settings?") - return nil - } - - if !SLSIsSymbolicHotKeyEnabled(hotKey) { - _ = SLSSetSymbolicHotKeyEnabled(hotKey, true) - } - - return (keyCode, CGEventFlags(rawValue: UInt64(rawFlags))) - } - /// Retrieves the CGWindowLevel for a specific window. /// - Parameter windowID: The `CGWindowID` of the window to query. /// - Returns: The window's level, or `nil` if the lookup failed. From 73347d13a24fc67587857077d8582b0d940fcbc6 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Wed, 1 Jul 2026 13:28:28 -0600 Subject: [PATCH 7/7] =?UTF-8?q?=E2=9C=A8=20Simplify=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Private APIs/SkyLightBridgedSPI.swift | 24 ++++++---------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/Loop/Private APIs/SkyLightBridgedSPI.swift b/Loop/Private APIs/SkyLightBridgedSPI.swift index 284ee507..f5a1d467 100644 --- a/Loop/Private APIs/SkyLightBridgedSPI.swift +++ b/Loop/Private APIs/SkyLightBridgedSPI.swift @@ -12,18 +12,6 @@ import Foundation @available(macOS 14.0, *) enum SkyLightBridgedSPI { - private enum OperationClassName { - static let moveWindowsToManagedSpace = "SLSBridgedMoveWindowsToManagedSpaceOperation" - static let copyManagedDisplaySpaces = "SLSBridgedCopyManagedDisplaySpacesOperation" - static let copySpacesForWindows = "SLSBridgedCopySpacesForWindowsOperation" - } - - private enum SelectorName { - static let performWithWMBridgeDelegate = "performWithWMBridgeDelegate" - static let initWithWindowsSpaceID = "initWithWindows:spaceID:" - static let initWithOptionsWindows = "initWithOptions:windows:" - } - private static let objcMessageSend: UnsafeMutableRawPointer? = { guard let handle = dlopen(nil, RTLD_LAZY) else { return nil @@ -42,7 +30,7 @@ enum SkyLightBridgedSPI { } static func copyManagedDisplaySpaces() -> [NSDictionary]? { - guard let operationClass = NSClassFromString(OperationClassName.copyManagedDisplaySpaces) as? NSObject.Type else { + guard let operationClass = NSClassFromString("SLSBridgedCopyManagedDisplaySpacesOperation") as? NSObject.Type else { return nil } @@ -72,7 +60,7 @@ enum SkyLightBridgedSPI { return nil } - let selector = NSSelectorFromString(SelectorName.performWithWMBridgeDelegate) + let selector = NSSelectorFromString("performWithWMBridgeDelegate") guard operation.responds(to: selector) else { return nil } @@ -97,12 +85,12 @@ enum SkyLightBridgedSPI { spaceID: UInt64 ) -> AnyObject? { guard let objcMessageSend, - let operationClass = NSClassFromString(OperationClassName.moveWindowsToManagedSpace) as? NSObject.Type + let operationClass = NSClassFromString("SLSBridgedMoveWindowsToManagedSpaceOperation") as? NSObject.Type else { return nil } - let initializer = NSSelectorFromString(SelectorName.initWithWindowsSpaceID) + let initializer = NSSelectorFromString("initWithWindows:spaceID:") guard operationClass.instancesRespond(to: initializer), let allocatedOperation = allocate(operationClass) else { @@ -124,12 +112,12 @@ enum SkyLightBridgedSPI { options: SLSSpaceMask ) -> AnyObject? { guard let objcMessageSend, - let operationClass = NSClassFromString(OperationClassName.copySpacesForWindows) as? NSObject.Type + let operationClass = NSClassFromString("SLSBridgedCopySpacesForWindowsOperation") as? NSObject.Type else { return nil } - let initializer = NSSelectorFromString(SelectorName.initWithOptionsWindows) + let initializer = NSSelectorFromString("initWithOptions:windows:") guard operationClass.instancesRespond(to: initializer), let allocatedOperation = allocate(operationClass) else {