hammerspoon icon indicating copy to clipboard operation
hammerspoon copied to clipboard

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

Open tmandry opened this issue 9 years ago • 40 comments

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 avatar Jul 06 '15 04:07 tmandry

Might be related to #304

tmandry avatar Jul 06 '15 04:07 tmandry

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

cmsj avatar Jul 06 '15 05:07 cmsj

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?

cmsj avatar Jul 06 '15 06:07 cmsj

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

tmandry avatar Jul 06 '15 06:07 tmandry

@tmandry could you paste a code snippet to repro?

lowne avatar Jul 06 '15 11:07 lowne

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

naorunaoru avatar Aug 07 '15 15:08 naorunaoru

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

prashantv avatar Mar 22 '16 23:03 prashantv

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

dconlonAMZN avatar Apr 06 '16 07:04 dconlonAMZN

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

prashantv avatar Apr 13 '16 18:04 prashantv

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.

smackesey avatar Oct 02 '16 06:10 smackesey

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 avatar Apr 27 '17 14:04 cmsj

@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 avatar Jan 24 '18 19:01 dctucker

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

dctucker avatar Jan 24 '18 19:01 dctucker

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)

svermeulen avatar Apr 22 '19 00:04 svermeulen

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.

mrkam2 avatar Jun 04 '19 22:06 mrkam2

@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.

koekeishiya avatar Oct 23 '19 14:10 koekeishiya

@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+.

cmsj avatar Oct 23 '19 15:10 cmsj

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

koekeishiya avatar Oct 23 '19 17:10 koekeishiya

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?

dsdshcym avatar Jan 11 '20 11:01 dsdshcym

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.

koekeishiya avatar Jan 11 '20 12:01 koekeishiya

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().

smackesey avatar Apr 18 '20 01:04 smackesey

@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.

asmagill avatar Apr 18 '20 08:04 asmagill

@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!

smackesey avatar Apr 19 '20 17:04 smackesey

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.

greneholt avatar Dec 11 '20 23:12 greneholt

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 avatar Jan 10 '21 20:01 mrkam2

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

mrkam2 avatar Jan 11 '21 19:01 mrkam2

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?

jkelleyrtp avatar Apr 26 '22 08:04 jkelleyrtp

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

latenitefilms avatar Apr 26 '22 09:04 latenitefilms

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.

cmsj avatar Apr 26 '22 09:04 cmsj

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.

jkelleyrtp avatar Apr 26 '22 09:04 jkelleyrtp