Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Loop/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -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" : {
Expand Down Expand Up @@ -29998,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" : {
Expand Down
136 changes: 136 additions & 0 deletions Loop/Private APIs/SkyLightBridgedSPI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//
// 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 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("SLSBridgedCopyManagedDisplaySpacesOperation") 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("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("SLSBridgedMoveWindowsToManagedSpaceOperation") as? NSObject.Type
else {
return nil
}

let initializer = NSSelectorFromString("initWithWindows:spaceID:")
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("SLSBridgedCopySpacesForWindowsOperation") as? NSObject.Type
else {
return nil
}

let initializer = NSSelectorFromString("initWithOptions:windows:")
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
)
}
}
6 changes: 6 additions & 0 deletions Loop/Private APIs/SkyLightSymbolLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,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
157 changes: 157 additions & 0 deletions Loop/Private APIs/SkyLightToolBelt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -211,6 +258,116 @@ 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 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] {
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.
Expand Down
Loading
Loading