keymanapp / keyman

Keyman cross platform input methods system running on Android, iOS, Linux, macOS, Windows and mobile and desktop web
https://keyman.com/
Other
386 stars 107 forks source link

feat(windows): Get Keyman working together with AutoHotkey #4805

Open mcdurdin opened 3 years ago

mcdurdin commented 3 years ago

Is your feature request related to a problem? Please describe.

Keyman and AutoHotkey don't work well together. This is particularly a problem if the serialised input queue is in use.

Describe the solution you'd like

Per https://community.software.sil.org/t/using-autohotkeys-with-keyman/3620/3:

This then, is my request, to make Keyman work with AutoHotkey.

Describe alternatives you've considered

Turning serialised input off had other issues apparently: #4804.


Keyman for Windows/macOS/Linux:

rfpng commented 2 months ago

In Jul 2020 Bruce wrote that AutoHotkey hotstrings don't work when Keyman is running (see https://community.software.sil.org/t/using-autohotkeys-with-keyman/3620/2).

In Mar 2021 I confirmed that I had exactly the same problem and that Marc's solution to disable the serialised input queue unfortunately didn't work.

Now I found out that the topic has been closed on Jan 16 (which year?), but the problem still persists.

I just checked with keyman-17.0.326, with both a AutoHotkey version 1 and a version 2 script. When a Keyman keyboard is enabled, the AutoHotkey Hotstrings is disabled (e.g. ::btw::by the way). It doesn't produce anything.

Unfortunately this is a serious problem to me. I simply can't use Keyman, as I have hundreds of such hotstrings in AutoHotkey. I'm therefore still using InKey but would like to make the switch to Keyman sometime.

Is there any solution for this problem in sight?

Thank you very much.

mcdurdin commented 2 months ago

We did some spelunking this morning with AutoHotKey 2.0 and Keyman 17. It's a very tangled problem -- not easy to solve properly. A few questions:

  1. Which keystroke hook gets precedence? Windows does not define an order in which hooks run.
  2. If a Keyman keyboard and a AHK hotstring could both result in output, which app is responsible for processing it?
  3. Does AHK work on the output of a Keyman keyboard, or does it work independently?
  4. How do we deal with base keyboard language? If a Keyman keyboard is installed with a non-US system keyboard, then AHK gets its character translation from that base keyboard. This is not always stable; for example und-Latn may be installed with US-English, or Greek, or something else (this is a commonly used bcp 47 code for IPA).
  5. We observed some instability with emitted strings from AHK in some cases when testing this with Keyman active -- some hotstrings were missing characters as if the backspacing was happening out of order. This may have been related to debugging, because after restart of Windows it did not recur.

We made a POC that showed that it may be possible to get this working, but I think we'll need to engage with the AHK team (@Lexikos) for a complete solution.

In a custom local build of AHK, we added a temporary stub to ignore Keyman-generated serialized input events in hook.cpp:186 (LowLevelKeybdProc):

    else if(event.vkCode == VK_PACKET || event.dwExtraInfo == 0x4B4D0000)
        return CallNextHookEx(g_KeybdHook, aCode, wParam, lParam);

Why ignoring all VK_PACKET? VK_PACKET generated from Keyman does not currently have a dwExtraInfo that we set; we could consider setting it in Keyman to e.g. 0x4B4B0001 but it would need careful review.

On the Keyman side, we added a test to ignore output coming from AHK in k32_lowlevelkeyboardhook.cpp (line 174, _kmnLowLevelKeyboardProc):

  if (hs->dwExtraInfo == 0xffc3d44d) {
    SendDebugMessage("Ignoring AHK");
    return CallNextHookEx(Globals::get_hhookLowLevelKeyboardProc(), nCode, wParam, lParam);
  }

While this seemed to "work" reasonably well, it's certainly incomplete!


As a workaround, hotstrings could also be added to a Keyman keyboard, albeit probably not as simply as with AHK. @rfpng: would this be an adequate stopgap?

rfpng commented 2 months ago

Thank you very much, Marc and everyone involved, for looking into this. Currently I run InKey 2 and AutoHotkey 1 about 99.9% of the time the system is on.

  1. I'm not sure which one of them takes precedence, but sometimes my AutoHotkey hotstrings don't work anymore. When that happens I just restart my AutoHotkey script and then things are normal again. So it could be that the one that has been started the latest takes precedence.
  2. I don't know.
  3. With InKey running, AHK works independently; i.e. it does not take the output of InKey as its input. (I have a physical Swiss German keyboard, but use US as the base keyboard and emulate the Swiss German keyboard in InKey (plus many other additional characters). Since on the Swiss German keyboard Y and Z are interchanged, I have, for example the AHK Hotstring ::zz::yaqyaq-a, which allows me to type ‘yy’ resulting in ‘yaqyaq-a’. This shows that AHK hotstrings work independently from InKey.)
  4. I currently always use US as the base keyboard. I haven't had any success using a different keyboard as the base. It would be nice if it worked, but this wouldn't be as crucial as being able to run Keyman and AHK together.
  5. Don't know about that.

About your stopgap question: I can't quite see how the full functionality of AHK's hotstrings could be implemented in Keyman. Some of my hotstrings actually invoke functions consisting of 50 lines of AHK code.

Let me know if you would like me to test anything. (Unfortunately though my AHK scripts that I mostly use (one 3000 lines and the other 1000) are still in version 1. I haven't found time yet to convert them to version 2.)

Thanks for everything! Roland

mcdurdin commented 2 months ago

These were mostly questions for the Keyman dev team to consider, sorry @rfpng. But the extra info you provide is helpful. I am doubtful that we would attempt to interop with v1 as well as v2 of AHK -- we are already spread awfully thin as it is.

About your stopgap question: I can't quite see how the full functionality of AHK's hotstrings could be implemented in Keyman. Some of my hotstrings actually invoke functions consisting of 50 lines of AHK code.

I don't really know what you are using hotstrings for so it is hard to me to know where to go with this. Just wanted to suggest it as a possible idea.

Lexikos commented 3 weeks ago

I know nothing about Keyman, but I am familiar with Windows keyboard hooks. Given the talk of keyboard hooks and VK_PACKET, I assume that Keyman uses a keyboard hook to conditionally intercept event(s) and generate text.

Which keystroke hook gets precedence?

The most recently installed hook gets precedence. Any previously installed hooks are only called if later hooks call CallNextHookEx.

If a Keyman keyboard and a AHK hotstring could both result in output, which app is responsible for processing it?

So, given an event which completes a pattern recognized by both Keyman and AutoHotkey, you're asking which app should process it?

The approach AutoHotkey takes for multiple scripts, or multiple competing hotstrings and hotkeys, is essentially to just go with the flow.

When an event occurs, it is passed to the most recently installed hook first. If that instance of the hook is interested in the event, it responds and decides whether or not to suppress the event. If the event is not suppressed, it is passed on to the next keyboard hook.

Hotstrings with auto-backspacing always suppress the last event, to reduce the number of backspace events needed. If only the final event is considered and AutoHotkey has installed its hook after Keyman, the hotstring would take precedence. Keyman will not see the final event of the hotstring. However, because only the last event is suppressed, it is possible that the previous events were already processed in some way by Keyman.

If the hotstring has auto-backspacing disabled, AutoHotkey will respond to the hotstring, but Keyman may also respond to the hotstring. There is likely no practical way for Keyman to determine that it has already been handled.

If Keyman is intended to essentially extend the keyboard layout, perhaps it "should" take precedence over hotstrings. For that, it would need to install its hook after AutoHotkey's.

AHK gets its character translation from that base keyboard.

The hotstring recognizer translates keys to characters with ToUnicodeEx and the keyboard layout of the focused window. I guess you would call that the "base keyboard", if you are extending the keyboard layout by intercepting certain patterns and simulating input.

The keyboard layout is retrieved from the focused control each time the hook is called. This can differ from the layout returned by the active/foreground window if they are owned by different threads (as it is for UWP apps).

Although there would be no way for AutoHotkey to be aware of patterns which only Keyman recognizes, if Keyman recognizes a pattern and then generates VK_PACKET events in response, AutoHotkey will see those VK_PACKET events.

Why ignoring all VK_PACKET?

It is not uncommon for a script to generate VK_PACKET events which should not be ignored by the hotstring recognizer. For instance, a remapping like !e::è will usually produce VK_PACKET events if è has no mapping on the current keyboard layout. In combination with a non-zero SendLevel, such a remapping can trigger a hotstring which contains è.

Other software can also generate input which should be considered by the hotstring recognizer. That might include VK_PACKET events generated by Keyman.

In other words, AutoHotkey cannot ignore all VK_PACKET events.

I am doubtful that we would attempt to interop with v1 as well as v2 of AHK

Between v1 and v2, there are only minor differences in the way input is generated or processed, such as affecting specific hotkey combinations. The default SendMode differs, but the actual behaviour of the different modes is basically the same.

sometimes my AutoHotkey hotstrings don't work anymore.

If a process installs a keyboard hook and does not call CallNextHookEx, any previously installed keyboard hooks will no longer be notified of events. Restarting the script causes it to reinstall its hook (or more accurately, the new instance of the script installs a new hook), which allows it to take precedence.

However, there are also conditions under which the OS will simply stop calling a hook. Generally it is a result of a process being unresponsive at the wrong moment. (The machanism for this is documented by Microsoft, but not very well.) With more recent OS versions, AutoHotkey users have observed that 64-bit processes aren't affected by this issue.

I don't really know what you are using hotstrings for so it is hard to me to know where to go with this.

I think the point of AutoHotkey combining hotkeys/hotstrings with scripting is that the program doesn't limit what the user can do with it. The user can define both the trigger (hotstring) and an arbitrary subroutine. AutoHotkey allows the subroutine to be defined in script, but you could allow similar flexibility by letting the user assign a command line, inter-process message, or key combination.

Programs like Logitech SetPoint are often used in combination with AutoHotkey by mapping a button to a key combination. Sometimes keys which don't exist on the user's physical keyboard are used to avoid conflict - e.g. different combinations of modifier keys with F13-F24.

Rather than communicating via the keyboard hook, other methods of inter-process communication can be more reliable. For instance, Win32 window messages are easy to handle with AutoHotkey, and easy to send (with the Win32 functions FindWindow and SendMessage). An arbitrary string can be sent between processes by using WM_COPYDATA (example script).

mcdurdin commented 3 weeks ago

@Lexikos thank you for your detailed response!

You may find the design of the Keyman input pipeline (serialized keyboard input) relevant and interesting -- as we essentially filter all key events through a Keyman sub-queue in order to guarantee keystroke order: https://blog.keyman.com/2018/10/the-keyman-keyboard-input-pipeline/

I guess the questions I asked for my team are somewhat philosophical in nature. My guess is that the intersection of AHK's user base and Keyman's user base tends to be quite technical users, who are going to be willing to navigate some of the uncertainties.

I think placing Keyman before AHK in terms of keystroke processing makes the most philosophical sense; as you say, Keyman is acting as a keyboard layout.

So given that, here's my proposal:

  1. Keyman adds an experiment flag ('interop with autohotkey'?). When this flag is active, Keyman adds the dwExtraInfo flag value 0x4B4D0001 to Keyman's synthesized events.

  2. AHK does something like this, e.g. in LowLevelKeybdProc:

// Keyman key events; https://github.com/keymanapp/keyman/issues/4805
else if(event.dwExtraInfo == 0x4B4D0001) {
  // Keyman 18.0+ keyboard event; WM_*KEY*, incl. VK_modifier, VK_PACKET
  if(event.vkCode == 0x0E) {
    // Keyman's _VK_PREFIX_DEFAULT should be ignored (aka VK_ZAP)
    return CallNextHookEx(g_KeybdHook, aCode, wParam, lParam);
  }  
  ... process hotstrings on VK_PACKET, process other hotkeys as desired, 
  ... note that 'character generating' WM_KEY* events are masked by Keyman 
  ... so spurious WM_CHAR is not generated by TranslateMessage
}
else if(event.dwExtraInfo == 0x4B4D0000) {
  // Keyman serialized input event, always ignore
  // See https://blog.keyman.com/2018/10/the-keyman-keyboard-input-pipeline/
  return CallNextHookEx(g_KeybdHook, aCode, wParam, lParam);
}
  1. Keyman documents for users who want to try this that they should enable the experiment flag, and start Keyman after AHK.

@rc-swag does this sound feasible to you? (Unknown: does VK_PACKET inherit dwExtraInfo when KEYEVENTF_UNICODE bit is set?)

@Lexikos hopefully this would be a minimal change to AHK -- dwExtraInfo is a bit of a free-for-all of course. I defer to you on how to actually implement 😆, but basic principle I think would be as shown in the code snippet above.