Hammerspoon / hammerspoon

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

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

Open tmandry opened 9 years ago

tmandry commented 9 years ago

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 commented 9 years ago

Might be related to #304

cmsj commented 9 years ago

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

cmsj commented 9 years ago

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 commented 9 years ago

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

lowne commented 9 years ago

@tmandry could you paste a code snippet to repro?

naorunaoru commented 9 years ago

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

prashantv commented 8 years ago

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 commented 8 years ago

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

prashantv commented 8 years ago

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

smackesey commented 8 years ago

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 commented 7 years ago

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.

dctucker commented 6 years ago

@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 commented 6 years ago

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

svermeulen commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

@cmsj

I've solved this issue in yabai, by reverse engineering some of the event-handling in the WindowServer. Relevant issue: https://github.com/koekeishiya/yabai/issues/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 commented 5 years ago

@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 commented 5 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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:

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 commented 4 years ago

@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 commented 4 years ago

@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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

Time to fire up Hopper?

https://www.hopperapp.com

cmsj commented 2 years ago

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.