Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[READY] Provide functionality to hs.spaces for basic manipulation of desktop spaces #3154

Closed
wants to merge 4 commits into from
Closed
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
125 changes: 114 additions & 11 deletions Hammerspoon.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Hammerspoon/setup.lua
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ package.preload['hs.network.ping'] = preload 'hs.network_ping'
package.preload['hs.pasteboard.watcher'] = preload 'hs.libpasteboardwatcher'
package.preload['hs.screen.watcher'] = preload 'hs.libscreenwatcher'
package.preload['hs.socket.udp'] = preload 'hs.libsocketudp'
package.preload['hs.spaces.watcher'] = preload 'hs.libspaceswatcher'
package.preload['hs.spaces.watcher'] = preload 'hs.libspaces_watcher'
package.preload['hs.uielement.watcher'] = preload 'hs.libuielementwatcher'
package.preload['hs.usb.watcher'] = preload 'hs.libusbwatcher'
package.preload['hs.webview.datastore'] = preload 'hs.libwebviewdatastore'
Expand Down
Empty file removed extensions/spaces/internal.m
Empty file.
274 changes: 274 additions & 0 deletions extensions/spaces/libspaces.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
@import Cocoa ;
@import LuaSkin ;

#import "private.h"

static const char * const USERDATA_TAG = "hs.spaces" ;
static LSRefTable refTable = LUA_NOREF;

static NSRegularExpression *regEx_UUID ;

static int g_connection ;

#pragma mark - Support Functions and Classes

#pragma mark - Module Functions

/// hs.spaces.screensHaveSeparateSpaces() -> bool
/// Function
/// Determine if the user has enabled the "Displays Have Separate Spaces" option within Mission Control.
///
/// Parameters:
/// * None
///
/// Returns:
/// * true or false representing the status of the "Displays Have Separate Spaces" option within Mission Control.
static int spaces_screensHaveSeparateSpaces(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L] ;
[skin checkArgs:LS_TBREAK] ;
lua_pushboolean(L, [NSScreen screensHaveSeparateSpaces]) ;
return 1 ;
}

/// hs.spaces.data_managedDisplaySpaces() -> table | nil, error
/// Function
/// Returns a table containing information about the managed display spaces
///
/// Parameters:
/// * None
///
/// Returns:
/// * a table containing information about all of the displays and spaces managed by the OS.
///
/// Notes:
/// * the format and detail of this table is too complex and varied to describe here; suffice it to say this is the workhorse for this module and a careful examination of this table may be informative, but is not required in the normal course of using this module.
static int spaces_managedDisplaySpaces(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L] ;
[skin checkArgs:LS_TBREAK] ;
CFArrayRef managedDisplaySpaces = SLSCopyManagedDisplaySpaces(g_connection) ;
if (managedDisplaySpaces) {
[skin pushNSObject:(__bridge NSArray *)managedDisplaySpaces withOptions:LS_NSDescribeUnknownTypes] ;
CFRelease(managedDisplaySpaces) ;
} else {
lua_pushnil(L) ;
lua_pushstring(L, "SLSCopyManagedDisplaySpaces returned NULL") ;
return 2 ;
}
return 1 ;
}


/// hs.spaces.focusedSpace() -> integer
/// Function
/// Returns the space ID of the currently focused space
///
/// Parameters:
/// * None
///
/// Returns:
/// * the space ID for the currently focused space. The focused space is the currently active space on the currently active screen (i.e. that the user is working on)
///
/// Notes:
/// * *usually* the currently active screen will be returned by `hs.screen.mainScreen()`; however some full screen applications may have focus without updating which screen is considered "main". You can use this function, and look up the screen UUID with [hs.spaces.spaceDisplay](#spaceDisplay) to determine the "true" focused screen if required.
static int spaces_getActiveSpace(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L] ;
[skin checkArgs:LS_TBREAK] ;
lua_pushinteger(L, (lua_Integer)SLSGetActiveSpace(g_connection)) ;
return 1 ;
}

/// hs.spaces.windowsForSpace(spaceID) -> table | nil, error
/// Function
/// Returns a table containing the window IDs of *all* windows on the specified space
///
/// Parameters:
/// * `spaceID` - an integer specifying the ID of the space
///
/// Returns:
/// * a table containing the window IDs for *all* windows on the specified space
///
/// Notes:
/// * the table returned has its __tostring metamethod set to `hs.inspect` to simplify inspecting the results when using the Hammerspoon Console.
/// * The list of windows includes all items which are considered "windows" by macOS -- this includes visual elements usually considered unimportant like overlays, tooltips, graphics, off-screen windows, etc. so expect a lot of false positives in the results.
/// * In addition, due to the way Accessibility objects work, only those window IDs that are present on the currently visible spaces will be finable with `hs.window` or exist within `hs.window.allWindows()`.
/// * This function *will* prune Hammerspoon canvas elements from the list because we "own" these and can identify their window ID's programmatically. This does not help with other applications, however.
/// * Reviewing how third-party applications have generally pruned this list, I believe it will be necessary to use `hs.window.filter` to prune the list and access `hs.window` objects that are on the non-visible spaces.
/// * as `hs.window.filter` is scheduled to undergo a re-write soon to (hopefully) dramatically speed it up, I am providing this function *as is* at present for those who wish to experiment with it; however, I hope to make it more useful in the coming months and the contents may change in the future (the format won't, but hopefully the useless extras will disappear requiring less pruning logic on your end).
static int spaces_windowsForSpace(lua_State *L) { // NOTE: wrapped in init.lua
LuaSkin *skin = [LuaSkin sharedWithState:L] ;
[skin checkArgs:LS_TNUMBER | LS_TINTEGER, LS_TBOOLEAN | LS_TOPTIONAL, LS_TBREAK] ;
uint64_t sid = (uint64_t)lua_tointeger(L, 1) ;
BOOL includeMinimized = (lua_gettop(L) > 1) ? (BOOL)(lua_toboolean(L, 2)) : YES ;

uint32_t owner = 0 ;
uint32_t options = includeMinimized ? 0x7 : 0x2 ;
uint64_t setTags = 0 ;
uint64_t clearTags = 0 ;

int type = SLSSpaceGetType(g_connection, sid) ;
if (type != 0 && type != 4) {
lua_pushnil(L) ;
lua_pushstring(L, "not a user or fullscreen managed space") ;
return 2 ;
}

NSArray *spacesList = @[ [NSNumber numberWithUnsignedLongLong:sid] ] ;

CFArrayRef windowListRef = SLSCopyWindowsWithOptionsAndTags(g_connection, owner, (__bridge CFArrayRef)spacesList, options, &setTags, &clearTags) ;

if (windowListRef) {
[skin pushNSObject:(__bridge NSArray *)windowListRef] ;
lua_newtable(L) ;
[skin requireModule:"hs.inspect"] ; lua_setfield(L, -2, "__tostring") ;
lua_setmetatable(L, -2) ;

CFRelease(windowListRef) ;
} else {
lua_pushnil(L) ;
lua_pushfstring(L, "SLSCopyWindowsWithOptionsAndTags returned NULL for %d", sid) ;
return 2 ;
}
return 1 ;
}

/// hs.spaces.moveWindowToSpace(window, spaceID) -> true | nil, error
/// Function
/// Moves the window with the specified windowID to the space specified by spaceID.
///
/// Parameters:
/// * `window` - an integer specifying the ID of the window, or an `hs.window` object
/// * `spaceID` - an integer specifying the ID of the space
///
/// Returns:
/// * true if the window was moved; otherwise nil and an error message.
///
/// Notes:
/// * a window can only be moved from a user space to another user space -- you cannot move the window of a full screen (or tiled) application to another space and you cannot move a window *to* the same space as a full screen application.
static int spaces_moveWindowToSpace(lua_State *L) { // NOTE: wrapped in init.lua
LuaSkin *skin = [LuaSkin sharedWithState:L] ;
[skin checkArgs:LS_TNUMBER | LS_TINTEGER, LS_TNUMBER | LS_TINTEGER, LS_TBREAK] ;
uint32_t wid = (uint32_t)lua_tointeger(L, 1) ;
uint64_t sid = (uint64_t)lua_tointeger(L, 2) ;

if (SLSSpaceGetType(g_connection, sid) != 0) {
lua_pushnil(L) ;
lua_pushfstring(L, "target space ID %d does not refer to a user space", sid) ;
return 2 ;
}

NSArray *windows = @[ [NSNumber numberWithUnsignedLong:wid] ] ;
// 0x7 : kCGSAllSpacesMask = CGSSpaceIncludesUser | CGSSpaceIncludesOthers | CGSSpaceIncludesCurrent
// from https://github.com/NUIKit/CGSInternal/blob/master/CGSSpace.h
CFArrayRef spacesList = SLSCopySpacesForWindows(g_connection, 0x7, (__bridge CFArrayRef)windows) ;
if (spacesList) {
if (![(__bridge NSArray *)spacesList containsObject:[NSNumber numberWithUnsignedLongLong:sid]]) {
NSNumber *sourceSpace = [(__bridge NSArray *)spacesList firstObject] ;
if (SLSSpaceGetType(g_connection, sourceSpace.unsignedLongLongValue) != 0) {
lua_pushnil(L) ;
lua_pushfstring(L, "source space for windowID %d is not a user space", wid) ;
return 2 ;
}
SLSMoveWindowsToManagedSpace(g_connection, (__bridge CFArrayRef)windows, sid) ;
}
lua_pushboolean(L, true) ;
CFRelease(spacesList) ;
} else {
lua_pushnil(L) ;
lua_pushfstring(L, "SLSCopySpacesForWindows returned NULL for window ID %d", wid) ;
return 2 ;
}
return 1 ;
}

/// hs.spaces.windowSpaces(window) -> table | nil, error
/// Function
/// Returns a table containing the space IDs for all spaces that the specified window is on.
///
/// Parameters:
/// * `window` - an integer specifying the ID of the window, or an `hs.window` object
///
/// Returns:
/// * a table containing the space IDs of all spaces the window is on, or nil and an error message if an error occurs.
///
/// Notes:
/// * the table returned has its __tostring metamethod set to `hs.inspect` to simplify inspecting the results when using the Hammerspoon Console.
/// * If the window ID does not specify a valid window, then an empty array will be returned.
/// * For most windows, this will be a single element table; however some applications may create "sticky" windows that may appear on more than one space.
/// * For example, the container windows for `hs.canvas` objects which have the `canJoinAllSpaces` behavior set will appear on all spaces and the table returned by this function will contain all spaceIDs for the screen which displays the canvas.
static int spaces_windowSpaces(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L] ;
[skin checkArgs:LS_TNUMBER | LS_TINTEGER, LS_TBREAK] ;
uint32_t wid = (uint32_t)lua_tointeger(L, 1) ;

NSArray *windows = @[ [NSNumber numberWithUnsignedLong:wid] ] ;
// 0x7 : kCGSAllSpacesMask = CGSSpaceIncludesUser | CGSSpaceIncludesOthers | CGSSpaceIncludesCurrent
// from https://github.com/NUIKit/CGSInternal/blob/master/CGSSpace.h
CFArrayRef spacesList = SLSCopySpacesForWindows(g_connection, 0x7, (__bridge CFArrayRef)windows) ;
if (spacesList) {
[skin pushNSObject:(__bridge NSArray *)spacesList] ;
lua_newtable(L) ;
[skin requireModule:"hs.inspect"] ; lua_setfield(L, -2, "__tostring") ;
lua_setmetatable(L, -2) ;

CFRelease(spacesList) ;
} else {
lua_pushnil(L) ;
lua_pushfstring(L, "SLSCopySpacesForWindows returned NULL for window ID %d", wid) ;
return 2 ;
}
return 1 ;
}

static int spaces_coreDesktopSendNotification(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L] ;
[skin checkArgs:LS_TSTRING, LS_TBREAK] ;
NSString *message = [skin toNSObjectAtIndex:1] ;

lua_pushinteger(L, (lua_Integer)(CoreDockSendNotification((__bridge CFStringRef)message, 0))) ;
return 1 ;
}

#pragma mark - Module Constants

#pragma mark - Hammerspoon/Lua Infrastructure

// Functions for returned object when module loads
static luaL_Reg moduleLib[] = {
{"screensHaveSeparateSpaces", spaces_screensHaveSeparateSpaces},
{"data_managedDisplaySpaces", spaces_managedDisplaySpaces},

// hs.spaces.activeSpaceOnScreen(hs.screen.mainScreen()) wrong for full screen apps, so keep
{"focusedSpace", spaces_getActiveSpace},

{"moveWindowToSpace", spaces_moveWindowToSpace},
{"windowsForSpace", spaces_windowsForSpace},
{"windowSpaces", spaces_windowSpaces},

{"_coreDesktopNotification", spaces_coreDesktopSendNotification},

{NULL, NULL}
};

// // Metatable for module, if needed
// static const luaL_Reg module_metaLib[] = {
// {"__gc", meta_gc},
// {NULL, NULL}
// };

int luaopen_hs_libspaces(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L] ;
refTable = [skin registerLibrary:USERDATA_TAG functions:moduleLib metaFunctions:nil] ; // or module_metaLib

g_connection = SLSMainConnectionID() ;

NSError *error = nil ;
regEx_UUID = [NSRegularExpression regularExpressionWithPattern:@"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
options:NSRegularExpressionCaseInsensitive
error:&error] ;
if (error) {
regEx_UUID = nil ;
[skin logError:[NSString stringWithFormat:@"%s.luaopen - unable to create UUID regular expression: %@", USERDATA_TAG, error.localizedDescription]] ;
}

return 1;
}
17 changes: 9 additions & 8 deletions extensions/spaces/libspaces_watcher.m
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
#import <CoreGraphics/CGWindow.h>
#import <LuaSkin/LuaSkin.h>
@import Foundation ;
@import Cocoa ;
@import CoreGraphics ;
@import LuaSkin ;

/// === hs.spaces.watcher ===
///
Expand All @@ -25,7 +25,7 @@ - (id)initWithObject:(spacewatcher_t*)object;

@implementation SpaceWatcher
- (id)initWithObject:(spacewatcher_t*)object {
if (self = [super init]) {
if ((self = [super init])) {
self.object = object;
}
return self;
Expand Down Expand Up @@ -54,9 +54,10 @@ - (void)spaceChanged:(NSNotification*)notification {
for (NSMutableDictionary *thisWindow in windowsInSpace) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
if ([thisWindow objectForKey:(id)kCGWindowWorkspace]) {
currentSpace = [[thisWindow objectForKey:(id)kCGWindowWorkspace] intValue];
NSNumber *potentialID = [thisWindow objectForKey:(id)kCGWindowWorkspace] ;
#pragma clang diagnostic pop
if (potentialID) {
currentSpace = [potentialID intValue];
break;
}
}
Expand Down Expand Up @@ -178,7 +179,7 @@ static int userdata_tostring(lua_State* L) {
{NULL, NULL}
};

int luaopen_hs_libspaceswatcher(lua_State* L) {
int luaopen_hs_libspaces_watcher(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
refTable = [skin registerLibraryWithObject:USERDATA_TAG functions:watcherlib metaFunctions:nil objectFunctions:watcher_objectlib];

Expand Down
27 changes: 27 additions & 0 deletions extensions/spaces/private.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#pragma once

// Most of these mirror similarly named functions in the CGS* name space used by my previous hs._asm.undocumented.spaces, but as the
// SkyLight Server framework seems to be cropping up more and more with new OS features (esp the virtual touchbar), I suspect it may
// be more "future proof"... I'm going to use these instead.

extern int SLSMainConnectionID(void) ;
extern CGError CoreDockSendNotification(CFStringRef notification, int unknown);

extern CFArrayRef SLSCopyManagedDisplaySpaces(int cid) ;
extern int SLSSpaceGetType(int cid, uint64_t sid);
extern CFArrayRef SLSCopyWindowsWithOptionsAndTags(int cid, uint32_t owner, CFArrayRef spaces, uint32_t options, uint64_t *set_tags, uint64_t *clear_tags);
extern void SLSMoveWindowsToManagedSpace(int cid, CFArrayRef window_list, uint64_t sid);
extern CFArrayRef SLSCopySpacesForWindows(int cid, int selector, CFArrayRef window_list);

// extern uint64_t SLSManagedDisplayGetCurrentSpace(int cid, CFStringRef uuid) ;
// extern CFStringRef SLSCopyManagedDisplayForSpace(int cid, uint64_t sid);
// extern CFStringRef SLSSpaceCopyName(int cid, uint64_t sid);
// extern CGError SLSProcessAssignToSpace(int cid, pid_t pid, uint64_t sid);
// extern CGError SLSProcessAssignToAllSpaces(int cid, pid_t pid);


// Not used in Yabai, but still potentially useful
extern uint64_t SLSGetActiveSpace(int cid) ;

// no longer seems to work, even for regular space changes, so not using
// extern bool SLSManagedDisplayIsAnimating(int cid, CFStringRef uuid) ;
Loading