Hammerspoon / hammerspoon

Staggeringly powerful macOS desktop automation with Lua
http://www.hammerspoon.org
MIT License
11.9k stars 578 forks source link

Thoughts on a new approach to spaces... #2776

Open asmagill opened 3 years ago

asmagill commented 3 years ago

Seeing some of the workarounds people have come up with for the fact that even yabai has limited spaces functionality on the M1 macs got me thinking I should look closer at the Dock's axuielement structure.

I've come up with https://github.com/asmagill/hammerspoon-config-take2/blob/master/_scratch/mcSpacesAxuielement.lua which is (at present) an entirely lua based set of functions, using hs.axuielement, which can get what spaces are on each screen, which spaces are currently active on each screen, add a space to a screen, remove a space from a screen, and go to a specific space on a specific screen.

The down side: it has to activate Mission Control for each of these, so there is a slight flash (and not so slight for going to a different space and removing a space, as they need Mission Control to be fully formed to work). This is still better than killing the Dock, which hs._asm.undocumented.spaces does, and it doesn't require elevated privileges for injection, like yabai does (for some of its spaces support).

@cmsj, @latenitefilms what are your thoughts on possibly putting this into hs.spaces so that it's not just a placeholder for the watcher? I'm ambivalent, but it would give us the ability to say that we have some support for spaces without requiring third party tools.

I still need to document it, but there are inline comments above each function that should tell you enough if you'd like to try it out.

I do want to look closer at yabai (see the inline notes in the linked file for what I want to look into) and my original module and see if there might be a few private api things worth sticking in as well... we have never shied away from reasonably stable (and a few not-so-stable) private APIs if they were useful enough and worked reliably.

I think yabai will still have a place for advanced users, so I'm going to continue working on my socket interface to it (and then maybe create a spoon) and it will likely always be a "cleaner" solution in that (at least on Intel macs, for the moment) doesn't have the flicker that we will, but... we'd have something for the casual user again, rather than having to say "Sorry", or "well you can try the undocumented module, but it is clunky and limited".

cmsj commented 3 years ago

Sounds good to me, probably the closest we'll come to any kind of real Spaces API.

As for why yabai needs SIP disabled, I believe it's because they're installing a scripting extension that loads into the Dock process.

latenitefilms commented 3 years ago

All sounds good to me!

Random idea... you could probably hide the flicker, but quickly "freezing" the screen using a canvas object?

asmagill commented 3 years ago

Mission Control is considered a full screen app and didn't we start have problems displaying a canvas that floated above full screen apps around 10.12 or 10.13? Still, it's been a while since I've tried, and things have changed that I've missed before, so I'll run a few tests and see what falls out...

@cmsj, yeah even after sip is disabled, sudo is required to actually perform the injection and loading of the additions. Both areas we have assiduously avoided. But on the M1, injection is failing, even with sip fully disabled, and even if it wasn't, there is specific assembly code to align variables passed into specific Dock functions that hasn't been reversed engineered for M1 yet...

bviefhues commented 3 years ago

@asmagill I am trying to get mcSpacesAxuielement.lua working. Copied the lua file into my env, as ~/.hammerspoon/mcspaces/init.lua.

Then on Hammerspoon console:

> mcspaces=require("mcspaces")

> mcspaces.spacesForScreen()
/Users/bviefhues/.hammerspoon/mcspaces/init.lua:135: close flag must be boolean
stack traceback:
    [C]: in function 'assert'
    /Users/bviefhues/.hammerspoon/mcspaces/init.lua:135: in function 'mcspaces.spacesForScreen'
    (...tail calls...)
    [C]: in function 'xpcall'
    ...app/Contents/Resources/extensions/hs/_coresetup/init.lua:514: in function <...app/Contents/Resources/extensions/hs/_coresetup/init.lua:494>

> mcspaces.spacesForScreen(hs.screen.mainScreen():id())
{ "Zu Schreibtisch 1 wechseln", "Zu Schreibtisch 2 wechseln", "Zu Schreibtisch 3 wechseln", "Zu Schreibtisch 4 wechseln", "Zu Schreibtisch 5 wechseln", "Zu Schreibtisch 6 wechseln", "Zu Schreibtisch 7 wechseln", "Zu Schreibtisch 8 wechseln", "Zu Schreibtisch 9 wechseln", "Zu Schreibtisch 10 wechseln" }

> mcspaces.allSpaces()
{
  [69734464] = { "Zu Schreibtisch 1 wechseln", "Zu Schreibtisch 2 wechseln", "Zu Schreibtisch 3 wechseln", "Zu Schreibtisch 4 wechseln", "Zu Schreibtisch 5 wechseln", "Zu Schreibtisch 6 wechseln", "Zu Schreibtisch 7 wechseln", "Zu Schreibtisch 8 wechseln", "Zu Schreibtisch 9 wechseln", "Zu Schreibtisch 10 wechseln" }
}

> mcspaces.activeSpaceOnScreen()
/Users/bviefhues/.hammerspoon/mcspaces/init.lua:197: close flag must be boolean
stack traceback:
    [C]: in function 'assert'
    /Users/bviefhues/.hammerspoon/mcspaces/init.lua:197: in function 'mcspaces.activeSpaceOnScreen'
    (...tail calls...)
    [C]: in function 'xpcall'
    ...app/Contents/Resources/extensions/hs/_coresetup/init.lua:514: in function <...app/Contents/Resources/extensions/hs/_coresetup/init.lua:494>

> mcspaces.activeSpaceOnScreen(hs.screen.mainScreen():id())
Zu Schreibtisch 10 wechseln

> mcspaces.activeSpaces()
{
  [69734464] = "Zu Schreibtisch 10 wechseln"
}

Seems the default argument handling does not work sometimes.

Is it intentional that the space ID's are Strings ("Zu Schreibtisch 10 wechseln")?

bviefhues commented 3 years ago

What I would miss from asmagill/hs._asm.undocumented.spaces is spaces.windowOnSpaces(windowID) -> spacesArray.

bviefhues commented 3 years ago

Re space ID's being strings, this is due to localization, I guess. Seems you are gsub-ing parts of the returned string away, which fails for non-English locale. Maybe better to regex the actual number from the string?

asmagill commented 3 years ago

Out ATM, so I'll have to look into the errors later today... localization... crap, completely forgot about that... yeah, with Mission Control, I have to filter the name from the button representing "going" to that space -- there is no convenient label or ID we can grab.

Going to have to give this some thought...

And as to what remains from hs._asm.undocumented.spaces, not sure yet... I know that anything which required killing the Dock to work should be removed, but the rest might be stable enough, assuming I can map back to Mission Control name so gotoSpaceOnScreen and removeSpaceFromScreen can work.

asmagill commented 3 years ago

Ok, argument checking has been fixed.

Re gsub and localization and space name... there is no identifier or label in the axuielement properties for each space, but each space has a button with the (english) description of "exit to Desktop <#>" or "exit to full screen ". The obvious was to use gsub to suppress "exit to "; since it's gsub, if it's not there, we still have a string, even though it still reads (in the given language) "exit to ...", so it's usable, if ugly/confusing. And the functions that require a spaces name (gotoSpaceOnScreen and removeSpaceFromScreen) use string.match, so you only need to specify the number or application name (for full screen apps).

As to removing the proper localized text... I'm looking into it, but not sure yet... I also need to re-look at my original hs._asm.undocumented.spaces module and see... some of the "safer" methods may be worth including and if there is a way to map the id numbers it reports to the names MC uses, then it might just be easier to use the ids and convert internally...

ID numbers would also probably be consistent with the ids used by yabai, but I'm trying to create a subset that Hammerspoon users can use with some confidence without requiring a third party application, so that's a secondary concern.

asmagill commented 3 years ago

Ok, I've added support for localization of the button names based on the Accessibility.strings found in the Dock bundle. Let me know if this helps.

It may be a few days before I can look closer at the existing undocumented module src and yabai's src and see how we want to turn this into a formal module, so consider this a work in progress, but in the mean time, anyone who does try it out, let me know your experience and thoughts.

bviefhues commented 3 years ago

@asmagill default args seem to work now. Localization does not work for me:

> mcspaces=require("mcspaces")

> mcspaces.spacesForScreen()
{ "Zu Schreibtisch 1 wechseln", "Zu Schreibtisch 2 wechseln", "Zu Schreibtisch 3 wechseln", "Zu Schreibtisch 4 wechseln", "Zu Schreibtisch 5 wechseln", "Zu Schreibtisch 6 wechseln", "Zu Schreibtisch 7 wechseln", "Zu Schreibtisch 8 wechseln", "Zu Schreibtisch 9 wechseln", "Zu Schreibtisch 10 wechseln" }

> mcspaces.allSpaces()
{
  [69734464] = { "Zu Schreibtisch 1 wechseln", "Zu Schreibtisch 2 wechseln", "Zu Schreibtisch 3 wechseln", "Zu Schreibtisch 4 wechseln", "Zu Schreibtisch 5 wechseln", "Zu Schreibtisch 6 wechseln", "Zu Schreibtisch 7 wechseln", "Zu Schreibtisch 8 wechseln", "Zu Schreibtisch 9 wechseln", "Zu Schreibtisch 10 wechseln" }
}

> mcspaces.activeSpaceOnScreen()
Zu Schreibtisch 10 wechseln

> mcspaces.activeSpaces()
{
  [69734464] = "Zu Schreibtisch 10 wechseln"
}

I still vote for regex-ing the number from the string :-)

Thanks for building this, amazing what is possible with Hammerspoon.

asmagill commented 3 years ago

@bviefhues can you tell me what you get when you type each of the following into the Hammerspoon console (for easiest reading, enter each one as a separate command, rather then pasting as a group):

hs.host.locale.current()

hs.host.locale.details()

hs.host.locale.preferredLanguages()

Thanks!

bviefhues commented 3 years ago

@asmagill

> hs.host.locale.current()
2021-03-17 16:54:02: -- Loading extension: host
en_DE

> hs.host.locale.details()
alternateQuotationBeginDelimiterKey ‘
alternateQuotationEndDelimiterKey   ’
calendar                            { table }
collatorIdentifier                  de-DE
countryCode                         DE
currencyCode                        EUR
currencySymbol                      €
decimalSeparator                    ,
exemplarCharacterSet                { table }
groupingSeparator                   .
identifier                          en_DE
languageCode                        en
measurementSystem                   Metric
quotationBeginDelimiterKey          “
quotationEndDelimiterKey            ”
temperatureUnit                     Celsius
timeFormatIs24Hour                  true
usesMetricSystem                    true

> hs.host.locale.preferredLanguages()
de-DE

No idea why it's en_DE. Shoud be de all the way.

asmagill commented 3 years ago

Ok, looks like I should iterate through the preferred languages instead of the locale identifier, maybe falling back to it before finally falling back to "en". I'll update the code tonight and drop a note here when it's available for additional testing. I suppose if all else fails, maybe it should check for application names of running apps (for the full screen apps), and then parse out the numbers, as you suggest.

Longer term, I'm not sure yet if I'm going to stick with the names or not... looking closer at my previous module and yabai a little last night, I haven't yet found a good way to map the names to the ID that the internal functions require for getting the window ids on each space... in any case, as much as I appreciate and need the feedback as this progresses, don't get too tied to the specific syntax/responses yet... it's still evolving.

bviefhues commented 3 years ago

Playing on console only, for now. But, I have some code I run as part of my set-up which would be nice for testing.

That localization parsing sounds tricky to get right for each and every locale...

asmagill commented 3 years ago

https://github.com/asmagill/hammerspoon_asm/tree/master/spaces is the current WIP that will hopefully eventually make it into Hammerspoon.

Documentation is incomplete, but most of the inline documentation is in place, so you can review the sources.

I've moved all but the addSpace, removeSpace, and gotoSpace functions to a subset of the functions that were previously provided by hs._asm.undocumented.spaces (which I intend to archive in the near future)... it's really only three or four private functions that we require for basic spaces usage. I'm still experimenting with window id and movement -- I think it's going to to require a rewritten hs.window.filter to be truly useful there, but the framework is almost in place and window filter is on my list to get to in the next month or so.

The upside is that now only those three will cause visual side-effects (still better then killing the Dock) but the downside is that now most of the functions use space ID numbers (integers) while the three that require hs.axuielement and manipulating Mission Control require the Mission Control names, but I hope to address this shortly... in the mean time, use hs.spaces.missionControlNameForSpace to convert ID's to the appropriate name.

@bviefhues I think I've fixed the locale stuff in this version as well, if you want to give it a shot. If not, the key section is https://github.com/asmagill/hammerspoon_asm/blob/master/spaces/init.lua#L61-L90 which you should be able to paste into the file you've already been working with, replacing the existing similar code.

A precompiled version is present, if you are willing to trust it; otherwise you can download the files in the directory (it's not a separate submodule yet... forgot to do that and it's late where I am, so that will happen tomorrowish) and build it yourself... if you've used my third-party modules before, you know the drill -- otherwise, I'll make sure to add compilation instructions tomorrow as well.

asmagill commented 3 years ago

I'm giving up on working with the Mission Control names... I mean, I'll still include a function for converting space IDs to them for information purposes, but it will have to open Mission Control to capture them and map them to the ids.

But as for accepting them as arguments to functions? No way...

Just a few of the fun little tidbits I've uncovered about the MC names for spaces:

(Can you tell that I'm venting a bit? I have some other words I'd like to use, but I'm trying to keep this civil and "family friendly"... )

The id's stay unique and do track with the order displayed by Mission Control, so... after some reversions tomorrow to make the core functionality stable and predictable, I'll update with what I hope will be pretty close to the final version with full documentation and installation instructions.

edit -- got R-to-L language order backwards... I need to take a break...

asmagill commented 3 years ago

Ok, what I hope is pretty close to the final release can be found at https://github.com/asmagill/hammerspoon_asm/tree/master/spaces. If you follow any of the installation instructions given, it will "replace" the build in hs.spaces module (which was an empty placeholder anyways) in the load order (i.e. it doesn't modify your Hammerspoon application) while still leaving hs.spaces.watcher accessible.

@cmsj, I have kept the private API stuff to the basics, leaving out the more "dangerous" functions that are in hs._asm.undocumented.spaces and putting in protections to make sure that only valid user and full desktop ID's are used. And it no longer even considers killing the Dock app... I think its about as good as we're going to get without embarking on the same path that programs like Yabai have, so unless you have objections I intend to create a pull request with this after it's had some more testing.

The biggest lack at the moment is that the function to return window ids found on a give space has no pruning, so it includes the IDs of graphic elements in "hidden" windows, and other system windows that hs.window doesn't... I don't prune it by checking hs.window.allWindows because that wouldn't work for valid windows found on alternate spaces. The real fix is to get a working, faster, hs.window.filter... and while that's on my list, I went ahead and left the function in this module in its current state for people who really want to start mucking around with it (and documented this problem there as well).

latenitefilms commented 2 years ago

Dumb idea, that I haven't tested myself yet... Could you just modify com.apple.spaces.plist to change spaces and get information about spaces?

asmagill commented 2 years ago

Looks like that contains similar information as hs.spaces.data_managedDisplaySpaces and hs.spaces.windowsForSpace but merged together. I wonder how often it's updated?

Give it a go, if you're inclined.

While it looks like it could be useful to parsing out the data (not sure yet what, if anything, we'd gain yet, though), I'm less sure about editing/modifying it... what would you change? How do you make the system recognize the changes?

This is my experience, but editing preferences files directly seldom works reliably or the way you hope -- again only my experience, but what I've seen is that the "owning" application, if running (and I'm going to assume till someone shows me otherwise that for com.apple.spaces, the owner is the Dock) actually holds a copy in memory, so changes to the file via defaults (or CFPreferences, or an expanded hs.settings) may or may not be read -- it depends on if the owning Application checks for changes that it hasn't made itself. Often, the owning application flushes the preferences back to the file at scheduled intervals or when certain activities occur, and if it didn't first check for the changes you made, then they are lost. Its why applications like OnyX that allow you to modify some well known hidden settings in various applications easily asks to "restart" any running application that it makes a change to.

And even if restarting the Dock did allow your changes to be accepted, it would be the same experience as my original hs._asm.undocumented.spaces which killed the Dock to add and remove spaces.

latenitefilms commented 2 years ago

I know with Apple's Final Cut Pro, if we make changes to the Final Cut Pro preferences file, it actually changes things automatically in the application (they must be "watching" for changes) - so I wonder if this is just something built-in to macOS?

I also noticed it has an "age" key - which might be relevant?