Hammerspoon / hammerspoon

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

Using newKeyEventSequence #1881

Open EdanMaor opened 6 years ago

EdanMaor commented 6 years ago

Hi,

I'm trying to figure out how to use newKeyEventSequence. Sorry, fairly novice at Lua.

If I understand properly, I call newKeyEventSequence with a character and modifiers, and get back a list of key events. These have not been "posted", so if I want to actually execute them, I need to run through them and call :post().

I've done this using the below code, and it doesn't appear to work. Specifically, calling the following function with "shift+left" will cause the left to happen, but not the shift, or so it seems. But I'm not even sure I'm using newKeyEventSequence in the right way, so please correct me if not:

my_l = event.newKeyEventSequence(modifiers, character)
  for i, eve in pairs(my_l) do
-- print(i)
-- print(eve)
  print("posting event")
eve:post()

end

cmsj commented 6 years ago

@Edan-Purple is there a specific reason you're fetching the event sequence and posting it manually, vs just using https://www.hammerspoon.org/docs/hs.eventtap.html#keyStroke ?

AFAICT you're using it properly, and it also doesn't seem to behave properly for me in the same way (modifiers not being sent).

@asmagill any ideas?

asmagill commented 6 years ago

I can confirm that this is not working as expected... not sure why and its going to be a few days before I can really dig into this and pull up my original tests to see if I'm forgetting something or if they even work any more either.

Apple has been moving away from using CoreGraphics for event tapping and event generation for a while now, so it wouldn't surprise me if something has changed in the last macOS version or so... it wouldn't be the first time we've had to modify the module to keep up. The NSEvent class is significantly limited in comparison, but from a security standpoint, I can understand their position... it just makes tools like this a pain because they need constant tweaking and changes to keep up.

EdanMaor commented 6 years ago

@cmsj I'm not sure why I'm not using keyStroke. I'm updating code that I wrote several years ago to fix some issues (which I think are unrelated). I believe the original reason was that I wanted to to create a keystroke without a delay, or at least that's what the comment before the function says. I see that the keyStroke function has a delay param, but perhaps this is something new?

I'm also doing some "manual toggling" of certain buttons, e.g. making sure that alt is not pressed, which requires posting directly, I believe.

EdanMaor commented 6 years ago

Ok I tried out using keyStroke, and it seems to work for 99% of my use-cases, however, it fails in the same place my original solution failed.

I'm not sure whether to open a new issue about it, so I'll describe it here and let me know if I should.

Basically, I'm trying to simulate typing "()", then putting the cursor in between them. I can do this using keyStroke, by passing in keyStroke({"shift"}, "9"), etc. This works fine in most places. However, the one place it doesn't seem to work for me is in Anki (a flashcard application).

This is true in general of things requiring the modifier keys - it seems to have some kind of weird interaction with anything to do with modifiers. For example, I usually map the key combo "alt+something" to do the typing, and in Anki, it seems to consider the "alt" key pressed, even though that's not true of other applications (I added a specific call to lift the alt key, which fixed this).

The one thing I found which seems to fix this issue, but not perfectly, is to simulate all the steps of pressing shift myself - which is why I was interested in the newEventKeySequence, obviously. When "simulating" keyStroke by myself, Anki does work, but when just using keyStroke, it doesn't. However, even this isn't perfect, as simulating the shift by itself seems to cause the shift not to be pressed sometimes (resulting in typing "9" instead of "(").

Ok that was a bit long, hope it's at least clear.

EdanMaor commented 6 years ago

OK, just to make the above clearer, I made a stripped-down version, which also shows interesting behavior. Here is my code:

key_t = function() hs.eventtap.event.newKeyEvent(hs.keycodes.map.alt, false):post() -- hs.eventtap.event.newKeyEvent(hs.keycodes.map.shift, true):post() hs.eventtap.keyStroke({"shift"}, "9", 0) hs.eventtap.event.newKeyEvent(hs.keycodes.map.alt, true):post() end hs.hotkey.bind({"alt"}, "t", key_t , nil, key_t)

What I want to have happen is that if I press alt-t, I get a "(". This works in most places I've checked, except in Anki. In that program, I will first get a "9", but then if I keep triggering the shortcut by pressing alt+t, I'll consequently get only "("s.

I'm not sure why this behavior happens.

cmsj commented 6 years ago

@Edan-Purple bear in mind that when you use a keyboard hotkey to emit keyboard events, you're still physically holding one or more keys down. This can cause confusion in many situations.

If you're just doing straight remapping, it may well be better to use a tool like Karabiner Elements.

EdanMaor commented 6 years ago

@cmsj I originally used Karabiner, but switched to Hammerspoon when Karabiner stopped working, and really loved that I could now actually use code to program it as opposed to an XML file. I am doing mostly remapping, but some of the remappings aren't straight "key->key" but more like "key->sequence". I also use tricks to simulate pressing shift in certain situations.

asmagill commented 6 years ago

This may be a change to the way macOS handles events... it appears to be contingent on when the events are created not just posted.

The following works to type "Å" if you paste the whole thing into the Hammerspoon console:

hs.eventtap.event.newKeyEvent(hs.keycodes.map.shift, true):post()
hs.eventtap.event.newKeyEvent(hs.keycodes.map.alt, true):post()
hs.eventtap.event.newKeyEvent("a", true):post()
hs.eventtap.event.newKeyEvent("a", false):post()
hs.eventtap.event.newKeyEvent(hs.keycodes.map.alt, false):post()
hs.eventtap.event.newKeyEvent(hs.keycodes.map.shift, false):post()

But if I do the following (which is essentially what hs.eventtap.event.newKeyEventSequence does), it just types a single lower case "a":

a = {
    hs.eventtap.event.newKeyEvent(hs.keycodes.map.shift, true),
    hs.eventtap.event.newKeyEvent(hs.keycodes.map.alt, true),
    hs.eventtap.event.newKeyEvent("a", true),
    hs.eventtap.event.newKeyEvent("a", false),
    hs.eventtap.event.newKeyEvent(hs.keycodes.map.alt, false),
    hs.eventtap.event.newKeyEvent(hs.keycodes.map.shift, false),
}
for i,v in ipairs(a) do v:post() end

I'm not sure if this is something we can fix (easily or otherwise) or not... it gets into some pretty low level intricacies of event creation and these seem to change slightly with each new macOS version... I was working on a replacement for eventtap that would give more access to things like sequence numbers and queues and timestamping of events but it got real ugly real fast and ended up putting it on hold due to time constraints and because I'm not really sure that it gained us much... I won't really have time to revisit the replacement until the fall when we'll have yet another macOS version to contend with...

Here is a modified version of hs.eventtap.event.newKeyEventSequence which posts the event sequence rather then returning it as an uposted set of events in an array... it might work for what you need right now and we should probably add it to hs.eventtap.event at some point:

postKeyEventSequence = function(modifiers, character)
    local keycodes = require("hs.keycodes")
    local event    = require("hs.eventtap").event
    local fnutils  = require("hs.fnutils")

    local allKeys = table.pack(table.unpack(modifiers)) -- create a shallow copy
    allKeys.n = nil -- added by table.pack
    table.insert(allKeys, character)
    local codes = fnutils.map(allKeys, function(s)
        local n
        if type(s)=='number' then n=s
        elseif type(s)~='string' then error('key must be a string or a number',3)
        elseif (s:sub(1, 1) == '#') then n=tonumber(s:sub(2))
        else n=keycodes.map[string.lower(s)] end
        if not n then error('Invalid key: '..s..' - this may mean that the key requested does not exist in your keymap (particularly if you switch keyboard layouts frequently)',3) end
        return n
    end)

    local n = #codes
    for i = 1, n, 1  do event.newKeyEvent(codes[i], true):post()  end
    for i = n, 1, -1 do event.newKeyEvent(codes[i], false):post() end
end

It seems to work as expected for the following: postKeyEventSequence({"shift","alt"}, "a") ; postKeyEventSequence({"shift"}, "left")

edit -- removed debugging output line that relies on locally defined helper function

lilyball commented 4 years ago

I just ran into this problem when returning a table of events from an event tap, and what I eventually determined was the keyUp/keyDown events for the modifiers don't matter. What matters is the flags on the event for the character itself. newKeyEventSequence() isn't setting those flags. So using newKeyEventSequence({"shift"}, "left") didn't work at all (even if I was holding shift when I triggered this code), but constructing the events myself as {newKeyEvent({"shift"}, "left", true), newKeyEvent({"shift"}, "left", false)} worked just fine.

FWIW this was on macOS 10.15.4 (19E287)

asmagill commented 4 years ago

I'll add this back to my list of things to make a point of reviewing, again, later this week...

The reason for adding the code to create "keystrokes" for the modifiers is because under some circumstances, it does matter. And it follows Apple's own examples in the documentation for the API.

But as you've discovered, in other circumstances, it doesn't matter, or in fact doesn't work at all.

Figuring out which is which, and which seems to be the most common in a given macOS release, seems annoyingly inconstant.

lilyball commented 4 years ago

I'm thinking that perhaps the best approach is to keep the additional keyUp/keyDown events for each modifier, but also add in the flags containing all the modifiers to the key we're actually trying to invoke.

Ideally if we're doing shift+alt+ctrl+q we'd do {({}, "shift", true), ({"shift"}, "alt", true), ({"shift", "alt"}, "ctrl", true), ({"shift", "alt", "ctrl"}, "q", true), ({"shift", "alt", "ctrl"}, "q", false), ({"shift", "alt", "ctrl", false), ({"shift"}, "alt", false), ({}, "shift", false)} though the flags on the modifier events probably don't really matter.

gpanders commented 2 years ago

I just ran into this problem when returning a table of events from an event tap, and what I eventually determined was the keyUp/keyDown events for the modifiers don't matter. What matters is the flags on the event for the character itself. newKeyEventSequence() isn't setting those flags. So using newKeyEventSequence({"shift"}, "left") didn't work at all (even if I was holding shift when I triggered this code), but constructing the events myself as {newKeyEvent({"shift"}, "left", true), newKeyEvent({"shift"}, "left", false)} worked just fine.

I just ran into this too. Do I understand from this issue that this is expected behavior? If so, it might be a good idea to document that as it is a bit unexpected.