Skip to content

Add One Key Keyboard Layout Switcher mod#4536

Closed
Anixx wants to merge 7 commits into
ramensoftware:mainfrom
Anixx:patch-53988
Closed

Add One Key Keyboard Layout Switcher mod#4536
Anixx wants to merge 7 commits into
ramensoftware:mainfrom
Anixx:patch-53988

Conversation

@Anixx

@Anixx Anixx commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

This mod allows switching keyboard layouts with a single key press instead of a two-key combination, supporting various keys like Win, Alt, and Menu.

Changelog

If this pull request updates an existing mod, describe the changes below:

  • Changelog item 1...
  • Changelog item 2...

Mod authorship

If this pull request introduces a new mod, please complete the section below.

This mod was created by:

    • The submitter, without AI assistance
    • The submitter, with AI assistance
    • Claude
    • ChatGPT
    • Gemini
    • Another AI (please specify):
    • Other (please specify):

Please select the options that best apply. Your selection does not affect the acceptance criteria, but it helps reviewers understand the context of the code and provide relevant feedback.

Anixx added 4 commits June 23, 2026 23:29
This mod allows switching keyboard layouts with a single key press instead of a two-key combination, supporting various keys like Win, Alt, and Menu.
@m417z

m417z commented Jun 23, 2026

Copy link
Copy Markdown
Member

Similar feedback to #4144: how about a tool mod that targets dwm.exe? And can you remind me why you didn't do it at #4144? Was this the main concern?

Also, my concern is: having two dwm.exe processes may break those mods that inject DWM (for custom windows borders, etc).

@Anixx

Anixx commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Yes, there are mods that modify window frames, etc, that also target dwm.exe. I am afraid if there are multiple DWM processes, they can glitch. Even one can choose to use this mod together with KeyClick mod. So it will inject the both?

@Anixx

Anixx commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

P.S. Yes, just tried this, when this mod is made into a toolmod, the Key Click mod injects the original DWM and this toolmod, so the sounds are doubled. This is a clear incompatibility. With other mods targeting DWM, similar things can happen.

@m417z

m417z commented Jun 23, 2026

Copy link
Copy Markdown
Member

If both mods are tool mods, it's fine, other tool mod processes are ignored, so they will ignore each other.

Regular mods are injected into tool mods, which is not optimal. In the next Windhawk update, I plan on excluding them in this case as well. We can merge it as is for now, but eventually I'd prefer both mods to become tool mods.

@Anixx

Anixx commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

I agree. When tool mods target Windhawk.exe, there is no such problem, because there are no mods that target Windhawk itself. But for processes like DWM, this is an issue because there are multiple mods that are not toolmods that also target the same process.

@m417z

m417z commented Jun 23, 2026

Copy link
Copy Markdown
Member

Submission review

Note: This review was done by Claude, and then refined manually. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


This should almost certainly be a tool mod, and there's an existing mod that does nearly the same thing the right way. The biggest items below all disappear if you adopt that structure.

1. Don't inject into dwm.exe — make this a tool mod. Deferred.

2. No blocking work inside the WH_KEYBOARD_LL callback. LowLevelKeyboardProc calls SendLayoutSwitch() synchronously, and in "both" mode that does Sleep(100) (lines 122–124). A low-level keyboard hook runs on the installing thread and blocks all system input until it returns — a 100 ms sleep freezes the entire machine's keyboard/mouse for 100 ms on every switch, and can push the callback past LowLevelHooksTimeout, after which Windows silently removes your hook and the mod stops working with no error. Even the SendInput calls shouldn't run inline.

The fix is the standard one (and your worker thread already has a message loop): from the hook, PostThreadMessage(g_threadId, WM_APP, ...) and return 1 immediately; do the SendInput/Sleep work in the message loop, which runs after the hook returns and so doesn't block input. This is exactly what caps-ime-switcher does — it posts a kSwitchInputSourceMessage to the worker thread and handles it in the loop.

3. Prefer the root-cause API over synthesizing Ctrl+Shift / Alt+Shift. Sending the OS layout-switch hotkey via SendInput only works if the user actually has that exact combo configured as their Windows switch hotkey — if they've changed it (or set it per-window), the mod silently does nothing. The robust, deterministic way is to post WM_INPUTLANGCHANGEREQUEST (with INPUTLANGCHANGE_FORWARD) to the foreground window, as caps-ime-switcher does. That removes the dependency on the user's hotkey config, and lets you delete the switchMethod setting, the "both" mode, and the Sleep(100) entirely.

4. Default-enabled keys repurpose Right Alt (AltGr) and Right Win, blocking them completely. Defaults are enableRightAlt=true, enableRightWin=true, enableMenu=true, and ShouldIntercept makes the callback swallow both key-down and key-up (return 1), so the chosen key no longer functions as itself at all. For Right Alt this is a real problem: it's AltGr on many international layouts (German, Polish, etc.) and is needed to type special characters — which is precisely the audience that switches layouts. As shipped, enabling this mod silently breaks AltGr typing for those users. Consider a more conservative default (e.g. only the Menu/Apps key on), and document that whichever key is selected is fully consumed.

5. Unload can hang the host process. Wh_ModUninit does PostThreadMessage(g_threadId, WM_QUIT, ...) then WaitForSingleObject(g_thread, INFINITE). PostThreadMessage fails if the worker thread hasn't created its message queue yet (it's created lazily by the first GetMessage), and g_threadId is valid the instant CreateThread returns — so if the mod is disabled in the small window before the loop starts, the WM_QUIT is dropped and the INFINITE wait never returns, hanging dwm.exe on unload. Prime the queue before installing the hook, the way caps-ime-switcher does with PeekMessageW(..., PM_NOREMOVE), and don't PostThreadMessage until the thread has signaled it's ready. (Adopting the tool-mod structure in item 1 gets you this pattern for free.)

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • Wh_GetStringSetting never returns NULL (it returns L"" on error/unset), so the method ? method : L"ctrlshift" guard in LoadSettings is dead. You can also drop the fixed WCHAR[32] buffer + wcscpy_s + Wh_FreeStringSetting in favor of WindhawkUtils::StringSetting (RAII).
  • settings is written by LoadSettings() (called from Wh_ModSettingsChanged on an arbitrary thread) while it's read by the hook on the worker thread — a benign data race that can only occur during a settings change. Not worth a lock, but converting switchMethod to an enum loaded once would make the read atomic-ish and also avoid per-keypress wcscmps.

Functionality notes

Non-critical observations about the feature behavior itself.

  • The "both" mode is conceptually shaky: if the user has both Ctrl+Shift and Alt+Shift active as switch hotkeys, sending both switches the layout twice (net no-op); if only one is active, the other does nothing. It mostly exists to paper over not knowing the user's configured hotkey — which the WM_INPUTLANGCHANGEREQUEST approach (item 3) makes moot.
  • You switch on key-down; caps-ime-switcher switches on key-up after confirming it wasn't a long-press. Down-trigger means a quick tap can't be distinguished from holding the key for some other purpose, which is part of why the keys end up fully consumed (item 4). Up-trigger is generally friendlier for repurposing a modifier.

@Anixx

Anixx commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

All raised issues are addressed.

@Anixx Anixx marked this pull request as draft June 23, 2026 23:34
@Anixx Anixx closed this Jun 24, 2026
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

Successfully merging this pull request may close these issues.

2 participants