native spaces option · Issue #404 · acsandmann/rift · GitHub
Skip to content

native spaces option #404

Open
Open

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)
}

Activity

  1. jakubdonovan commented on Sep 7, 2026

    @jakubdonovan

    Is this still something you're planning to pursue, or more of a note to self for
    now? Happy to test a branch

  2. acsandmann commented on Sep 7, 2026

    @acsandmann
    OwnerAuthor

    Is this still something you're planning to pursue, or more of a note to self for now? Happy to test a branch

    Just an idea so far, it would be a significant change internally. once rift is a little more stable (we are pretty close) i will explore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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