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

window:focus() focuses different window of app on same screen #370

Open
tmandry opened this issue Jul 6, 2015 · 40 comments
Open

window:focus() focuses different window of app on same screen #370

tmandry opened this issue Jul 6, 2015 · 40 comments

Comments

@tmandry
Copy link
Contributor

tmandry commented Jul 6, 2015

If I have a Google Chrome window on each screen, and I try to :focus() the window that's on the screen other than the one currently focused, it will instead focus the Google Chrome window on the current screen.

Reproduceable on master using the console on OSX 10.10.1.

I know this didn't used to happen, and haven't run into it until today (hadn't updated in awhile), so I'm curious if anyone else can reproduce it.

@tmandry
Copy link
Contributor Author

tmandry commented Jul 6, 2015

Might be related to #304

@cmsj
Copy link
Member

cmsj commented Jul 6, 2015

@tmandry what happens if you call :becomeMain() first, on the window you want to focus?

@cmsj
Copy link
Member

cmsj commented Jul 6, 2015

hmm, never mind that question, :focus() calls :becomeMain(). In that case, I suspect a useful test would be to see if you can reproduce it with any other apps. Chrome might be overriding main window stuff maybe?

@tmandry
Copy link
Contributor Author

tmandry commented Jul 6, 2015

Sorry, that was just a placeholder; I tried it with apps other than Chrome and still had the same issue.

@lowne
Copy link
Contributor

lowne commented Jul 6, 2015

@tmandry could you paste a code snippet to repro?

@naorunaoru
Copy link

I call hs.hints.windowHints() and experiencing the same issue.

@prashantv
Copy link

Running into the same issue, it seems to focus the application rather than the window.

I have this snippet which lets me choose a specific window to focus, but it isn't able to focus windows on other screens:

function createWindowChooser()
  choseWindow = function(w)
    local window = hs.window.get(w["id"])
    hs.alert.show("Switching to" .. window:title())
    window:focus()
  end

  local chooser = hs.chooser.new(choseWindow)
  hs.hotkey.bind({"alt"}, "tab", function()
    local windows = {}
    local wf = hs.window.filter.new()

    for _, w in pairs(wf:getWindows()) do
      table.insert(windows, {
        ["text"] = w:title(),
        ["subText"] = w:application():name(),
        ["id"] = w:id(),
      })
    end
    chooser:choices(windows)
    chooser:show()
  end)


end
createWindowChooser()

@dconlonAMZN
Copy link

FWIW I worked around this issue by calling window:raise() then window:focus(), and the application I was having issue with was iTerm2.

@prashantv
Copy link

I wasn't able to get raise then focus to work -- even with iTerm. Is there anything else needed before the focus?

@smackesey
Copy link

smackesey commented Oct 2, 2016

I also have this problem. I tried doing raise then focus and it didn't work. I did notice that if I already have a window of the target application focused, then it will work correctly.

For example, say I have two screens, S1 and S2, and three windows, Chrome1, Chrome2, and Term. S1 has Chrome1 and Term. S2 just has Chrome2. If I am focusing Term on Chrome1 and attempt to focus() Chrome2, then it will actually focus Chrome1 (the Chrome window on the same screen). If I am already focusing Chrome1 and attempt to focus Chrome2, then it works as expected.

I'm using 0.9.48 on Sierra.

@cmsj
Copy link
Member

cmsj commented Apr 27, 2017

I think this is an OS thing - it's activating the application and even though we've said we want to focus another window, it's flipping to the "nearest" window instead, possibly the last window that was the key window.

My workaround is to get the application, call :activate() on it, and then call :focus() on its window that I actually want. It's not great, but I don't have a better idea at the moment.

@cmsj cmsj closed this as completed Apr 27, 2017
@dctucker
Copy link
Contributor

dctucker commented Jan 24, 2018

@cmsj I tried activating the application first and then focusing the window, but I'm getting the same result, it's not activating the window I want, but another focused window of the same application on a different screen. Could we re-open this issue and try to find a solution?

My issue affects Firefox.

Here is the code I'm running:

function focusScreen(screen)
	wf_target = wf.new():setScreens(screen)
	if hs.screen.mainScreen() == hs.screen.find(screen) then
		-- cycle windows on current screen
		target = wf_target:getWindows(wf.sortByFocused)[1]
		target:application():activate()
		target:focus()
	else
		-- activate most recently focused on target screen
		target = wf_target:getWindows(wf.sortByFocusedLast)[1]
		target:application():activate()
		target:focus()
	end
end

@dctucker
Copy link
Contributor

Update: I'm in High Sierra 10.13.2. I think calling target:becomeMain() seems to result in the expected behavior. It'd be great though, if calling hs.window:focus() could yield the expected behavior without this workaround, as it seems strange that this would occur considering becomeMain is called from window/init.lua:508

@cmsj cmsj reopened this Feb 10, 2018
@svermeulen
Copy link

I can confirm that calling activate on the application, and then calling focus after that with the specific window I want, fixes the issue for me (Mojave 10.14.4)

@mrkam2
Copy link

mrkam2 commented Jun 4, 2019

None of these solution seems to work for me on (Mojave 10.14.5)... :-(

My code is finding a particular Chrome window it wants to show to the user, but it fails to reliably show it. It either ends up showing different Chrome window, or showing it but not focusing on it.

Here is the best one (showing correct window but not focusing on it):

function focusTab(tabName)
   local appName = 'Google Chrome'
   local app = hs.appfinder.appFromName(appName)
   if app == nil then
      hs.application.launchOrFocus(appName)
   else
      local windows = app:allWindows()
      for i = 1, #windows do
        print(windows[i]:title())
        if windows[i]:title():find(tabName) then
          print("Focusing")
          windows[i]:focus()
          windows[i]:raise()
          break
        end
      end
   end
end

I have two particular usecases in mind. Let's say I have two chrome windows (A and B) on each screen (1 and 2): A1 and B1 on one screen and A2 and B2 on another. I also have some other app C2 open on screen 2. Expected outcome in both cases to open and focus A2.

Use case 1:

  1. B1 and B2 are on top, B1 is currently focused.
  2. I use hammerspoon to focus on A2.
  3. RESULT: The code above popups up A2 but it is not focused. I need to click it to focus.

Use case 2:

  1. B1 and B2 are on top, C2 is currently focused.
  2. I use hammerspoon to focus on A2.
  3. RESULT: The code above popups up A2 but it is not focused. I need to click it to focus.

@koekeishiya
Copy link

@cmsj

I've solved this issue in yabai, by reverse engineering some of the event-handling in the WindowServer. Relevant issue: koekeishiya/yabai#102

The solution relies on some private functions implemented in the SkyLight.framework. I'd be happy to explain the solution if Hammerspoon finds the usage of private APIs acceptable. There is probably a hard limit to which macOS versions are supported. I'm not familiar with when these functions were introduced - I've only tested this on High Sierra 10.13.6 and newer.

@cmsj
Copy link
Member

cmsj commented Oct 23, 2019

@koekeishiya I would definitely be interested to learn about that! We do use private APIs where necessary, and our current minimum supported version is 10.12, but it's fine if we have new Hammerspoon API that only works on 10.13+.

@koekeishiya
Copy link

koekeishiya commented Oct 23, 2019

Basically what I discovered is that there is a certain category of events that are passed by the system to applications depending on how it gains focus. I'm not exactly sure what this event category is, but I'd refer to them as either system or control events. Anyway, we can then synthesize such an event and send it directly to the target process using its process serial number.

The background information that helped me discover this was that I was trying to implement focus follows mouse (autofocus) by looking at how macOS was able to focus a window without raising it when clicking inside the window belonging to an unfocused application while holding the ctrl + alt modifiers. There were multiple such system events being triggered in this scenario.

It has been quite some time since I implemented this, so I don't remember all the nitty gritty details of all the events, but the solution for this particular issue boils down to combining the following steps:

First just some definitions that are necessary

#define kCPSUserGenerated 0x200

extern CGError _SLPSSetFrontProcessWithOptions(ProcessSerialNumber *psn, uint32_t wid, uint32_t mode);
extern CGError SLPSPostEventRecordTo(ProcessSerialNumber *psn, uint8_t *bytes);

static void window_manager_make_key_window(ProcessSerialNumber *window_psn, uint32_t window_id)
{
    // the information specified in the events below consists of the "special" category, event type, and modifiers,
    // basically synthesizing a mouse-down and up event targetted at a specific window of the application,
    // but it doesn't actually get treated as a mouse-click normally would.
 
    uint8_t bytes1[0xf8] = {
        [0x04] = 0xF8,
        [0x08] = 0x01,
        [0x3a] = 0x10
    };

    uint8_t bytes2[0xf8] = {
        [0x04] = 0xF8,
        [0x08] = 0x02,
        [0x3a] = 0x10
    };

    memcpy(bytes1 + 0x3c, &window_id, sizeof(uint32_t));
    memset(bytes1 + 0x20, 0xFF, 0x10);
    memcpy(bytes2 + 0x3c, &window_id, sizeof(uint32_t));
    memset(bytes2 + 0x20, 0xFF, 0x10);
    SLPSPostEventRecordTo(window_psn, bytes1);
    SLPSPostEventRecordTo(window_psn, bytes2);
}

Actual change in focus:

// focus the process, and tell it which window should get key-focus.
_SLPSSetFrontProcessWithOptions(window_psn, window_id, kCPSUserGenerated);

// synthesize an event to have the process update the key-window internally
window_manager_make_key_window(window_psn, window_id);

// standard way to focus a window through the accessibility API
AXUIElementPerformAction(window_ref, kAXRaiseAction);

This method requires the caller to know the psn of the target process, the CGWindowId, and the corresponding AXUIElementRef to perform the operation successfully.

Assuming you have the AXUIElementRef, the window id can be retrieved using

extern AXError _AXUIElementGetWindow(AXUIElementRef ref, uint32_t *wid);

which you probably knew already. The remaining information can be retrieved as follows:

extern int SLSMainConnectionID(void);
extern CGError SLSGetWindowOwner(int cid, uint32_t wid, int *wcid);
extern CGError SLSGetConnectionPSN(int cid, ProcessSerialNumber *psn);

int element_connection;
ProcessSerialNumber element_psn;

// g_connection here is the result of calling SLSMainConnectionID(); (cached at startup)
SLSGetWindowOwner(g_connection, element_id, &element_connection);
SLSGetConnectionPSN(element_connection, &element_psn);

@dsdshcym
Copy link
Contributor

I found that application:_bringttofront can accept an argument to call SetFrontProcessFrontWindowOnly, maybe calling app:_bringtofront(true) in window:focus() can help?

But I cannot reproduce this issue consistently (I could reproduce this issue yesterday with my work setup, but I cannot reproduce this issue today at home). So I'm not sure if this really works.

Maybe we can try the private API listed above?

@koekeishiya
Copy link

These are the steps I used to reliably reproduce this problem:

How to reproduce the original problem:

Display 1: Open Terminal (A) and a Chrome window (B)
Display 2: Open a Chrome window (C)

Focus Chrome (B) on Display 1, and then focus Terminal (A) on Display 1.
Try to focus Chrome (C) on Display 2.

When using the accessibility API to focus the window, Chrome (B) on Display 1 would be focused.

@smackesey
Copy link

Adding my two cents as a user dealing with this issue for a long time. Given some application app and a window of that application win, here is what has not worked for me as a workaround:

  • win:focus()
  • win:focus(); win:focus()
  • win:becomeMain(); win:focus()
  • app:activate(); win:focus()

Here is what has worked:

app:activate()
hs.timer.doAfter(0.001, function ()
  win:focus()
end)

Despite the very short nominal delay of 0.001 seconds, the actual lag is longer but tolerable on my machine. Lowering the delay further does not affect it, it must be caused by the overhead of win:focus().

@asmagill
Copy link
Member

@smackesey a question: does app:activate() ; hs.timer.usleep(10000) ; win:focus() (you can try any number between 1000 and 10000 to fine tune it, I just chose 10000 because if that doesn't work, then 1000 won't either) work?

I ask because this will tell us whether it's an issue of giving the app time to activate, or whether its an issue of requiring the Hammerspoon application event loop to advance. (The doAfter won't happen before 0.001 seconds, but may actually happen later because it requires the Hammerspoon application event loop to advance so that the timer can trigger the callback function)


More detail as to why I'm asking, if you're curious -- you don't need to read this, but I would appreciate an answer to the above, if it's not too much trouble.

As we see more complex examples that people come up with, we've found that some combined actions are timing dependent, and we can insert delays at specific points to make them more reliable, while others require the main thread of Hammerspoon to be idle, if only for a few nanoseconds-to-microseconds, in between actions... its one of the reasons I've been working to get coroutines supported and will be introducing a couple of new modules soon which may allow us to rewrite some of these more common actions in a way that gives the application loop more idle time to do the macOS maintenance and upkeep that is expected between such actions.

@smackesey
Copy link

@asmagill Just tried app:activate() ; hs.timer.usleep(10000) ; win:focus() and it works just like the doAfter code I posted above. Great news that you're working on deeper solutions to this (and similar) flukes in HS-- thanks for your hard work!

@greneholt
Copy link

I encountered this problem while using multiple windows in Kitty, and the issue was resolved by turning off the "Displays have separate Spaces" option for Mission Control. Note that you have to logout for this change to take effect.

You can observe a similar effect when using cmd-tab. If you have two windows of an application open on different displays, you will notice that when switching to that application with cmd-tab it will always focus on one of the windows, regardless of which window was focused when the app was last active. Turning off this mission control option also fixes this issue.

My guess is that Mission Control is interfering with window focus, such that when you focus either window of a non-active application, it activates that application and then focuses the "favored" window.

@mrkam2
Copy link

mrkam2 commented Jan 10, 2021

I encountered this problem while using multiple windows in Kitty, and the issue was resolved by turning off the "Displays have separate Spaces" option for Mission Control. Note that you have to logout for this change to take effect.

I tried turning off this option and it seemed to improve the behavior of the Hammerspoon. Will test it more.

@mrkam2
Copy link

mrkam2 commented Jan 11, 2021

I also noticed that this setting changes the user experience significantly so it may not be an appropriate solution to the problem. For example, "Entering Full Screen" action on a window, hides windows on all other displays. Also, the menu bar has to be fixed in a single display and the ordering of windows changes. I used to have 0, 1, 2 ordering for displays (used in hs.screen.find({x=screenPos, y=0}), but with this feature disabled, the ordering is -1, 0, 1 (where 0 - is the display with the menu).

@jkelleyrtp
Copy link

jkelleyrtp commented Apr 26, 2022

My comment will be somewhat unrelated, but where is the definition of ProcessSerialNumber? I'm trying to FFI it from Rust but I can't find a good def anywhere.

Closest thing: https://docs.rs/MacTypes-sys/latest/MacTypes_sys/struct.ProcessSerialNumber.html

I have some code that I think should be working but I don't get any focusing happening.

My PID is coming from CGWindowListCopyWindowInfo.

/// Type for unique process identifier.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct ProcessSerialNumber {
    padding: u32,
    val: u32,
}

pub type ProcessSerialNumberPtr = *mut ProcessSerialNumber;

#[test]
pub fn focus_window() {
    let pid = 7020;
    let wid = 55440;

    let mut psn = ProcessSerialNumber {
        padding: 0,
        val: pid,
    };

    let ptr = &mut psn as *mut ProcessSerialNumber;

    let r = unsafe { _SLPSSetFrontProcessWithOptions(ptr, wid, 0x100) };

    println!("{:?}", r);
}

#[link(name = "SkyLight", kind = "framework")]
extern "C" {
    fn _SLPSSetFrontProcessWithOptions(
        psn: *mut ProcessSerialNumber,
        wid: mach_port_t,
        mode: mach_port_t,
    ) -> CFErrorRef;
}

Edit:

it looks like I have a PID but need to get a PSN. Aren't PSNs deprecated/removed in 12.3.1? How does _SLPSSetFrontProcessWithOptions still work?

@latenitefilms
Copy link
Contributor

I'm not sure if this answers your question or not, but to get ProcessSerialNumber you can use:

ProcessSerialNumber psn;
psn.highLongOfPSN = 0;
psn.lowLongOfPSN = kCurrentProcess;

Header:

https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.5.sdk/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers/Processes.h

@cmsj
Copy link
Member

cmsj commented Apr 26, 2022

Edit: derp, I misread. Processes.h does indeed contain a definition that can be used to get the process serial number from a PID: GetProcessForPID() which is deprecated, unfortunately.

@jkelleyrtp
Copy link

jkelleyrtp commented Apr 26, 2022

I'm not sure if this answers your question or not, but to get ProcessSerialNumber you can use:

ProcessSerialNumber psn;
psn.highLongOfPSN = 0;
psn.lowLongOfPSN = kCurrentProcess;

Header:

https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.5.sdk/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers/Processes.h

How do I get kCurrentProcess of another process?

From the linked header:

 *    Lastly, it is usually not necessary to call GetCurrentProcess()
 *    to get the 'current' process psn merely to pass it to another
 *    Process Manager routine. Instead, just construct a
 *    ProcessSerialNumber with 0 in highLongOfPSN and kCurrentProcess
 *    in lowLongOfPSN and pass that. For example, to make the current
 *    process the frontmost process, use ( C code follows )
 *    
 *    ProcessSerialNumber psn = { 0, kCurrentProcess }; 
 *    
 *    OSErr err = SetFrontProcess( & psn );
 *    
 *    If you need to pass a ProcessSerialNumber to another application
 *    or use it in an AppleEvent, you do need to get the canonical PSN
 *    with this routine.

I don't want to focus my PSN, I want to focus the PSN of other apps (that I've scraped CGWindowListCopyWindowInfo).

Maybe I'm barking up the wrong tree here because I really just want to focus a space - but I can't seem to find any way to switch the current space to another one. Ideally I'd focus a space with a given window in it, hence why I'm taking this route with _SLPSSetFrontProcessWithOptions.

@koekeishiya
Copy link

If you already have the PID, use GetProcessForPID(pid_t pid, ProcessSerialNumber *psn);. Yes, this function was marked as deprecated ages ago, and yes it still works on Monterey 12.3.1; there is no replacement function, and the PSN concept is still very much a non-replaceable core part of macOS that is alive and well in the current WindowServer.

@jkelleyrtp
Copy link

jkelleyrtp commented Apr 26, 2022

If you already have the PID, use GetProcessForPID(pid_t pid, ProcessSerialNumber *psn);. Yes, this function was marked as deprecated ages ago, and yes it still works on Monterey 12.3.1; there is no replacement function, and the PSN concept is still very much a non-replaceable core part of macOS that is alive and well in the current WindowServer.

Holy cow that works!

However, this particular method seems to be setting the front process but not actually moving to that particular space. I see the app take over the menubar but the space doesn't navigate to open it. Is there a different method for navigating spaces that I'm missing?

#[test]
pub fn focus_window() {
    let wid: u32 = 55440;
    let pid = 7020;

    let mut psn = ProcessSerialNumber { padding: 0, val: 0 };
    unsafe { GetProcessForPID(pid, &mut psn) };

    let mut bytes1 = [0; 0xf8];
    bytes1[0x04] = 0xF8;
    bytes1[0x08] = 0x01;
    bytes1[0x3a] = 0x10;

    let mut bytes2 = [0; 0xf8];
    bytes2[0x04] = 0xF8;
    bytes2[0x08] = 0x02;
    bytes2[0x3a] = 0x10;

    bytes1[0x3c..0x3c + 4].copy_from_slice(&wid.to_le_bytes());
    bytes1[0x20..(0x20 + 0x10)].fill(0xFF);

    bytes2[0x3c..0x3c + 4].copy_from_slice(&wid.to_le_bytes());
    bytes2[0x20..(0x20 + 0x10)].fill(0xFF);

    unsafe {
        _SLPSSetFrontProcessWithOptions(&mut psn, wid, 0x400);
        let e1 = SLPSPostEventRecordTo(&mut psn, &bytes1);
        let e2 = SLPSPostEventRecordTo(&mut psn, &bytes2);

        // TODO: need to get the AXUIElementRef for the window we want to focus
        // AXUIElementPerformAction(e1, kAXRaiseAction);
    }
}

Edit: I came across the need to get the AXUIElementRef but that is currently unclear to me. Looking in yabai, I noticed that you get it by finding the element under a particular point?

Do you know of any api to get the AXUIElementRef given a window id?

@koekeishiya
Copy link

koekeishiya commented Apr 27, 2022

Do you know of any api to get the AXUIElementRef given a window id?

There is no API for this. However one possible solution would be to retrieve a handle to the application and iterating over its windows AXUIElementRef, request the window id from the AXUIElementRef and see if it matches your target. Something like:

uint32_t target_window_id = <your window id>;

pid_t application_pid = <your pid>;
AXUIElementRef application_ref = AXUIElementCreateApplication(application_pid);

if (application_ref) {
    CFTypeRef window_list_ref = NULL;
    AXUIElementCopyAttributeValue(application_ref, kAXWindowsAttribute, &window_list_ref);

    if (window_list_ref) {
        int window_count = CFArrayGetCount(window_list_ref);
      
        for (int i = 0; i < window_count; ++i) {
            uint32_t window_id = 0;
            AXUIElementRef window_ref = CFArrayGetValueAtIndex(window_list_ref, i);
            _AXUIElementGetWindow(window_ref, &window_id);
            
            if (window_id == target_window_id) {
                // window_ref is the correct AXUIElementRef, use CFRetain(window_ref) if you need ownership.
            }
        }

        CFRelease(window_list_ref);
    }

    CFRelease(application_ref);
}

@jkelleyrtp
Copy link

Do you know of any api to get the AXUIElementRef given a window id?

There is no API for this. However one possible solution would be to retrieve a handle to the application and iterating over its windows AXUIElementRef, request the window id from the AXUIElementRef and see if it matches your target. Something like:

uint32_t target_window_id = <your window id>;

pid_t application_pid = <your pid>;
AXUIElementRef application_ref = AXUIElementCreateApplication(application_pid);

if (application_ref) {
    CFTypeRef window_list_ref = NULL;
    AXUIElementCopyAttributeValue(application_ref, kAXWindowsAttribute, &window_list_ref);

    if (window_list_ref) {
        int window_count = CFArrayGetCount(window_list_ref);
      
        for (int i = 0; i < window_count; ++i) {
            uint32_t window_id = 0;
            AXUIElementRef window_ref = CFArrayGetValueAtIndex(window_list_ref, i);
            _AXUIElementGetWindow(window_ref, &window_id);
            
            if (window_id == target_window_id) {
                // window_ref is the correct AXUIElementRef, use CFRetain(window_ref) if you need ownership.
            }
        }

        CFRelease(window_list_ref);
    }

    CFRelease(application_ref);
}

Thanks for the help.

Is it intended that this method only returns a list of windows currently visible on screen?

    AXUIElementCopyAttributeValue(application_ref, kAXWindowsAttribute, &window_list_ref);

I'm trying to get the AXUIElementRef for off-screen windows - only having them being on screen is almost useless.

@koekeishiya
Copy link

koekeishiya commented Apr 27, 2022

Is it intended that this method only returns a list of windows currently visible on screen?

Yes, this is an API limitation. You can retrieve the AXUIElementRefs and cache them for later, and you can perform actions on them regardless of which space is active afterwards, but the actual act of retrieving these references must be done in the space that the window is currently in. You need to get creative to work around this issue -- if it is even possible in the latest version of macOS.

I'm not sure what exactly you are trying to build, but if it is anything resembling a remotely sophisticated window / spaces tool, prepare to have to spend a lot of time as you wrangle with weird macOS quirks.

@jkelleyrtp
Copy link

jkelleyrtp commented Apr 27, 2022

Is it intended that this method only returns a list of windows currently visible on screen?

Yes, this is an API limitation. You can retrieve the AXUIElementRefs and cache them for later, and you can perform actions on them regardless of which space is active afterwards, but the actual act of retrieving these references must be done in the space that the window is currently in. You need to get creative to work around this issue -- if it is even possible in the latest version of macOS.

I'm not sure what exactly you are trying to build, but if it is anything resembling a remotely sophisticated window / spaces tool, prepare to have to spend a lot of time as you wrangle with weird macOS quirks.

I'm trying to build an updated TotalSpaces app. I have their TotalSpaces3 beta and am trying to reverse engineer their reverse engineering.

The primary function I'm trying to attain is programmatically switching to a space (ideally without sending keyboard shortcuts as this will conflict with the keyboard shortcut used to trigger the change).

TS3 has this figured out, but looking through the various apps (hammerspoon, ts3, yabai, alt-tab), it seems like this AXUIElementRef dance is the way everyone goes to switch spaces. I don't quite get how TS3 figured it out to make it work without visiting each space once (something alt-tab requires and seems to be somewhat broken around). I've gotten to the part where I get a list of all spaces / their active windows, I just need to somehow switch to the space.

I'm not sure what exactly you are trying to build, but if it is anything resembling a remotely sophisticated window / spaces tool, prepare to have to spend a lot of time as you wrangle with weird macOS quirks.

I've already spent many hours navigating this, so I've got that "sunk cost" thing going on...

@koekeishiya
Copy link

I am not sure what TotalSpaces3 is doing if they are able to focus arbitrary windows across spaces without first doing the "discovery" phase. Yabai is capable of doing what you want using only the window id, but that solution requires disabling SIP and injecting code into Dock.app.

A workaround I have observed in the wild is that your application can create hidden windows (one on every space), then focus your own window to trigger the space switch, and then return focus to the window that macOS claims should be focused on that space, but this is sort of janky as it messes with focus history and what not.

@jkelleyrtp
Copy link

jkelleyrtp commented Apr 27, 2022

A workaround I have observed in the wild is that your application can create hidden windows (one on every space), then focus your own window to trigger the space switch, and then return focus to the window that macOS claims should be focused on that space, but this is sort of janky as it messes with focus history and what not.

Yep - that's the solution. I use contexts (another cmd-tab alternative) and they fill my window search tool with hidden windows.

Hopper Disassembler[53797:7:25726]: Contexts.hop
Hopper Disassembler[53783:9:25726]: Untitled
Contexts[51910:9:4605]: Contexts H
Visual Studio Code[50782:8:495]: util.rs — karusel
Visual Studio Code[46782:11:495]: tag.rs — tag
Contexts[26649:9:4605]: Contexts H
TotalSpaces3[22766:5:64328]: TransitionLeadWindow
Contexts[53725:8:4605]: Contexts H
Contexts[19874:5:4605]: Contexts H
Contexts[19858:7:4605]: Contexts H
Contexts[19185:6:4605]: Contexts H
Contexts[15608:7:4605]: Contexts H
Contexts[15255:7:4605]: Contexts H
Safari[52305:389:463]: window:focus() focuses different window of app on same screen · Issue #370 · Hammerspoon/hammerspoon
Contexts[12455:6:4605]: Contexts H
Contexts[11581:6:4605]: Contexts H
Contexts[10868:6:4605]: Contexts H
Contexts[17931:6:4605]: Contexts H
Contexts[7114:7:4605]: Contexts H
Contexts[6048:6:4605]: Contexts H
Contexts[5938:6:4605]: Contexts H
Contexts[5228:6:4605]: Contexts H
Contexts[4745:7:4605]: Contexts H
Contexts[4729:5:4605]: Contexts H
Contexts[4682:7:4605]: Contexts H
Contexts[4651:7:4605]: Contexts H
Contexts[3081:6:4605]: Contexts H
Contexts[2746:7:4605]: Contexts H
Contexts[2517:6:4605]: Contexts H
Contexts[2214:6:4605]: Contexts H
Contexts[4713:7:4605]: Contexts H
Visual Studio Code[1525:18:495]: lib.rs — autofmt
Visual Studio Code[1428:15:495]: extension.ts — cli
Visual Studio Code[570:5:495]: mod.rs — gateway
Finder[569:17:509]: leaf
Contexts[41208:11:4605]: Contexts H
Contexts[5147:7:4605]: Contexts H
Contexts[5101:6:4605]: Contexts H
Contexts[25821:9:4605]: Contexts H
Contexts[28140:9:4605]: Contexts H
Contexts[4230:6:4605]: Contexts H
Contexts[29731:9:4605]: Contexts H
Contexts[3396:6:4605]: Contexts H
Contexts[29650:9:4605]: Contexts H
Notion[153:16:492]: Untitled
Contexts[8120:7:4605]: Contexts H
Contexts[3335:7:4605]: Contexts H
Contexts[29634:11:4605]: Contexts H
Contexts[29618:9:4605]: Contexts H
Spotify[123:35:498]: Spotify Premium
Contexts[9685:6:4605]: Contexts H
Contexts[14026:6:4605]: Contexts H
Contexts[3427:6:4605]: Contexts H
Contexts[8196:6:4605]: Contexts H
Contexts[13172:7:4605]: Contexts H
Contexts[4995:7:4605]: Contexts H
Contexts[4810:7:4605]: Contexts H
Visual Studio Code[52826:9:495]: simulate.rs — rdev
QuickTime Player[50215:5:35556]: Screen Recording 2022-04-27 at 11.41.22 AM.mov

I don't think totalspaces does that (maybe they throw their window from space to space) but that's exactly what contexts is doing (with multiple windows per space too).

I think the code to do that is roughly contained here:

        let currentSpace = CGSGetActiveSpace(CGSMainConnectionID())
        let ids = [cgID()]
        CGSRemoveWindowsFromSpaces(CGSMainConnectionID(), ids as CFArray, [currentSpace] as CFArray)
        CGSAddWindowsToSpaces(CGSMainConnectionID(), ids as CFArray, [spaceID] as CFArray)

Does this sound about right? Thank you for all the help so far - you're a wealth of information :)

Edit: I got the invisible window trick + switching to app working! Just need to somehow throw windows onto all the available spaces.

@latenitefilms
Copy link
Contributor

Time to fire up Hopper?

https://www.hopperapp.com

@cmsj
Copy link
Member

cmsj commented Apr 27, 2022

I've never used TotalSpaces2, but their install docs seem to say that you have to disable SIP because they are also injecting code into Dock.app.

mogenson added a commit to mogenson/PaperWM.spoon that referenced this issue Jul 31, 2023
Following a tip in this Hammerspoon issue thread:
Hammerspoon/hammerspoon#370
Activate the app a window belongs to, wait a small amount, then try to
focus the window on a new Space. Do this before switching to the new
Space so a window is focused and PaperWM.spoon is ready to navigate via
keyboard shortcuts after switching.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests