Skip to content

native spaces option #404

Description

@acsandmann

one can now move windows between spaces programmatically in a nonhacky way without disabling SIP

use objc2::{
    msg_send, sel,
    rc::Retained,
    runtime::{AnyClass, AnyObject, Bool},
};
use objc2_foundation::{ns_string, NSArray, NSNumber};
use std::{ffi::c_char, sync::OnceLock};

pub type CGWindowID = u32;
pub type SLSSpaceMask = i32;

pub fn move_window_to_space(window: CGWindowID, space: u64) -> bool {
    op_move(&[window], space)
        .and_then(|op| perform(&op))
        .is_some()
}

pub fn copy_spaces_for_windows(windows: &[CGWindowID], options: SLSSpaceMask) -> Option<Vec<u64>> {
    let result = perform(&op_copy_spaces(windows, options)?)?;
    let nums: Retained<AnyObject> =
        unsafe { msg_send![&*result, valueForKey: ns_string!("numbers")] };
    Some((0..unsafe { msg_send![&*nums, count] })
        .map(|i: usize| unsafe {
            let n: *mut AnyObject = msg_send![&*nums, objectAtIndex: i];
            msg_send![n, unsignedLongLongValue]
        })
        .collect())
}

pub fn copy_managed_display_spaces() -> Option<Retained<AnyObject>> {
    let obj: Retained<AnyObject> = unsafe { msg_send![class(c"SLSBridgedCopyManagedDisplaySpacesOperation")?, new] };
    let result = perform(&obj)?;
    Some(unsafe { msg_send![&*result, valueForKey: ns_string!("propertyListArray")] })
}

fn op_move(windows: &[CGWindowID], space: u64) -> Option<Retained<AnyObject>> {
    let cls = class(c"SLSBridgedMoveWindowsToManagedSpaceOperation")?;
    let arr = nsnums(windows);
    let obj: *mut AnyObject = unsafe { msg_send![cls, alloc] };
    Some(unsafe { msg_send![obj, initWithWindows: &*arr, spaceID: space] })
}

fn op_copy_spaces(windows: &[CGWindowID], options: SLSSpaceMask) -> Option<Retained<AnyObject>> {
    let cls = class(c"SLSBridgedCopySpacesForWindowsOperation")?;
    let arr = nsnums(windows);
    let obj: *mut AnyObject = unsafe { msg_send![cls, alloc] };
    Some(unsafe { msg_send![obj, initWithOptions: options, windows: &*arr] })
}

fn perform(op: &AnyObject) -> Option<Retained<AnyObject>> {
    let ok: Bool = unsafe { msg_send![op, respondsToSelector: sel!(performWithWMBridgeDelegate)] };
    ok.as_bool()
        .then(|| unsafe { msg_send![op, performWithWMBridgeDelegate] })
        .flatten()
}

fn nsnums(xs: &[CGWindowID]) -> Retained<NSArray<NSNumber>> {
    NSArray::from_vec(xs.iter().copied().map(NSNumber::new_u32).collect())
}

fn class(name: &'static std::ffi::CStr) -> Option<&'static AnyClass> {
    static LOAD: OnceLock<()> = OnceLock::new();
    LOAD.get_or_init(|| unsafe {
        libc::dlopen(
            c"/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight"
                .as_ptr()
                .cast::<c_char>(),
            libc::RTLD_LAZY | libc::RTLD_LOCAL,
        );
    });
    AnyClass::get(name)
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions