Skip to content

Publish the first version of the language restorer mod - #5214

Merged
m417z merged 9 commits into
ramensoftware:mainfrom
babamohammed2022:patch-34
Sep 1, 2026
Merged

Publish the first version of the language restorer mod#5214
m417z merged 9 commits into
ramensoftware:mainfrom
babamohammed2022:patch-34

Conversation

@babamohammed2022

@babamohammed2022 babamohammed2022 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

This is a Windhawk mod that restores the Windows 7/8.1 Language switcher on the taskbar on Windows 10 and Windows 11. Credits to ac for the contribution.

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.

@babamohammed2022

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer

Copy link
Copy Markdown

Thanks for the pull request! This repository uses a two-stage review: an AI review that you run yourself, followed by a human review.

To get started, comment /ai-review. Once you're happy with the result, comment /ready-for-reviewer to hand it over to a human reviewer.

See the pull request review process for the full details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 25, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

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

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

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


Nice idea, and there's no existing mod that covers it (caps-ime-switcher and one-key-layout-switcher only remap the switch key, they don't recreate the flyout), so no overlap concerns. The problems below are all in how the mod integrates with the Windhawk lifecycle and with Explorer's threads — interestingly, your own merged win7-action-center-recreation already solves most of them correctly, so a lot of this is "do what that mod does".

1. The flyout window can survive the unload, leaving a dangling WndProc → crash.

Wh_ModUninit runs on the Windhawk Engine thread, but g_hFlyoutWnd is created in ShowSwitcher on whichever thread happened to trigger it (the LL-hook thread, or the taskbar thread via InputIndicatorButtonSubclassProc). DestroyWindow cannot destroy a window created by another thread — it just fails with ERROR_ACCESS_DENIED. Then UnregisterClassW also fails (a window of the class still exists), and Windhawk FreeLibrarys the mod. The next broadcast message (WM_SETTINGCHANGE, WM_DISPLAYCHANGE, WM_DWMCOLORIZATIONCOLORCHANGED, …) reaches FlyoutWndProc at an address that is no longer mapped → Explorer crashes.

The fix is the one you already use in the Action Center mod: own the window from a single dedicated thread with its own message loop, ask that thread to destroy its windows, and join it in Wh_ModUninit — see win7-action-center-recreation.wh.cpp#L5282-L5306 ("Nessun DestroyWindow cross-thread"). The thread exit is the barrier that guarantees no mod code is running when the image is unmapped.

2. Deadlock on unload / settings change: a lock is held across a cross-thread SendMessage.

WindhawkUtils::SetWindowSubclassFromAnyThread and RemoveWindowSubclassFromAnyThread are implemented with SendMessage to the window's owning thread (windhawk_utils.h). Both call sites hold g_subclassedWindowsMutex across that send:

  • SubclassTaskbarWindow takes the lock, then calls SetWindowSubclassFromAnyThread.
  • Wh_ModBeforeUninit takes the lock, then calls RemoveWindowSubclassFromAnyThread in a loop.

Meanwhile InputIndicatorButtonSubclassProc's WM_NCDESTROY path acquires the same mutex on the taskbar thread. Classic inversion: the Engine thread holds the mutex and blocks in SendMessage; the taskbar thread is inside the subclass proc waiting for the mutex and therefore not dispatching messages. Explorer's UI freezes permanently.

Copy out, release, then call:

void Wh_ModBeforeUninit(void) {
    g_unloading.store(true, std::memory_order_release);
    HideFlyout();

    std::vector<HWND> hwnds;
    {
        std::lock_guard<std::mutex> lock(g_subclassedWindowsMutex);
        hwnds.assign(g_subclassedWindows.begin(), g_subclassedWindows.end());
        g_subclassedWindows.clear();
    }
    for (HWND hWnd : hwnds) {
        WindhawkUtils::RemoveWindowSubclassFromAnyThread(hWnd, InputIndicatorButtonSubclassProc);
    }
}

Do the same in SubclassTaskbarWindow (insert into the set under the lock for de-duplication, release the lock, then subclass, and erase again if it failed). Also note that the utils wrapper already removes the subclass on WM_NCDESTROY for you, so the explicit RemoveWindowSubclassFromAnyThread call inside your own WM_NCDESTROY handler is redundant — just erase from the set there.

3. g_mainThreadId is not Explorer's UI thread when the mod is loaded into a running process, which disables most of the mod.

Per the mod lifetime docs: "In case a mod is loaded into a process which is already running, all events are executed in the Windhawk Engine thread." That's the normal case — enabling the mod, updating it, or changing a setting while Explorer is running. Consequences:

  • g_mainThreadId becomes the Engine thread, so CreateWindowExW_Hook and ShowWindow_Hook take the early-out branch on every call made from Explorer's UI threads. Tray-click subclassing of newly created indicators and the interception of the modern Shell_InputSwitch* flyout silently never happen. The mod only behaves as documented after an Explorer restart with the mod already enabled — which likely explains why it looks fine in testing but will be reported as "doesn't work" by users.
  • The WH_KEYBOARD_LL / WH_MOUSE_LL hooks are installed from that thread. Low-level hooks are dispatched on the installing thread and only run while that thread pumps messages. If the Engine thread has no message loop, the hooks never fire, and every keystroke and mouse event system-wide stalls until LowLevelHooksTimeout expires before the system gives up on the hook.

Install the LL hooks from a dedicated thread that owns a message loop and is joined in Wh_ModUninit — see caps-ime-switcher.wh.cpp#L338 and taskbar-volume-control.wh.cpp#L2216. That thread can also own the flyout window (item 1). For the taskbar thread, derive it from the window instead of from Wh_ModInit: GetWindowThreadProcessId(FindWindowW(L"Shell_TrayWnd", nullptr), nullptr).

4. Heavy, blocking work runs inside the low-level keyboard hook.

LowLevelKeyboardProc calls RefreshKeyboardLayouts() (multiple RegOpenKeyEx/RegQueryValueEx per layout plus SHLoadIndirectString, which can page in MUI resources from disk) and then ShowSwitcher() (CreateWindowExW, three DwmSetWindowAttribute calls, SHAppBarMessage(ABM_GETTASKBARPOS) — a cross-thread SendMessage to Shell_TrayWndSetWindowPos, and UpdateWindow, which forces a synchronous WM_PAINT). LL hook callbacks block all system input until they return, and exceeding LowLevelHooksTimeout gets the hook silently dropped by the system. The SendMessage inside SHAppBarMessage can also stall indefinitely if the taskbar thread is busy.

Keep the hook proc to state inspection plus a PostMessage/PostThreadMessage to the mod's own thread, and do the layout enumeration and window work there. RefreshKeyboardLayouts also doesn't need to run per keystroke — cache it and refresh on WM_INPUTLANGCHANGE / WM_SETTINGCHANGE.

5. The Alt+Shift handler fires on unrelated chords and on auto-repeat.

if (g_enableAltShift && isKeyDown && isAltDown && (kbd->vkCode == VK_SHIFT || ...)) {

This matches any Shift key-down while Alt is held, so Alt+Shift+Tab (reverse Alt-Tab), Alt+Shift+Esc, and every Alt+Shift+letter menu accelerator switch the keyboard layout and pop the flyout. Modifier keys also auto-repeat, so holding Alt+Shift cycles layouts continuously (the 250 ms debounce in TriggerSwitcher only throttles the flyout; SwitchToLayout runs on every repeat). And since the handler falls through to CallNextHookEx without swallowing the keys, Windows' own Alt+Shift language hotkey fires too when the user has it enabled, producing a double switch.

Track the modifier sequence explicitly (Alt down → Shift down with no other key in between → act on release), ignore repeats via KBDLLHOOKSTRUCT state, and decide deliberately whether to swallow the chord.

Separately, the enableAltShift setting description promises "Alt+Shift (and Ctrl+Shift)", but Ctrl+Shift is never handled — isCtrlDown is only used by the Ctrl+Shift+L hotkey. Either implement it or fix the description and the README.

6. Win+Space swallows the Space but lets the Win key-up through, so the Start menu opens.

The hook returns 1 for the Space key-down, so win32k never sees it and still considers the Win key a bare press. Letting the subsequent Win key-up through (return 0) then triggers the Start menu. In the win7 style this is unconditional, because g_isWinSpaceCycling is never set on that path, so the key-up branch that applies the selection is unreachable there. Worth verifying on your machines; if it reproduces, the usual fix is to inject a neutral keystroke (e.g. a VK_CONTROL down/up via SendInput) to break the Win chord before releasing.

7. Shared state is read and written from several threads without synchronization.

g_switcherStyle, g_themeMode, g_uiLanguage and g_customPreferencesCmd are std::wstrings reassigned by LoadModSettings() from Wh_ModSettingsChanged (Engine thread) while PaintWin8Flyout / PaintWin7Menu / FlyoutWndProc / the hook procs read them concurrently on other threads — that's a data race with a real dangling-c_str() failure mode, not just a torn read. The same applies to the plain (non-atomic) g_hFlyoutWnd, g_targetWindow, g_isWinSpaceCycling, g_flyoutWasVisibleOnDown and g_lastTriggerTick.

Simplest fix once the mod has a single owning thread (items 1 and 3): keep a settings struct guarded by g_stateMutex, snapshot it into a local at the top of each consumer, and make the simple flags std::atomic<>. g_switcherStyle / g_themeMode would also be cheaper and safer as enums resolved once in LoadModSettings rather than string comparisons in the paint loop.

8. Globals with non-trivial destructors run at process shutdown, when Wh_ModUninit does not.

On Explorer restart / sign-out / reboot the OS terminates all other threads first and only then runs global destructors, alone under the loader lock. static ScopedGdiplus g_gdiplus; destructs into GdiplusShutdown, which signals and waits for GDI+'s background notification thread — exactly the "blocks on a thread that can no longer complete" case, and it can hang Explorer's shutdown. g_keyboardHook / g_mouseHook (ScopedHookUnhookWindowsHookEx) are in the same category.

Both types are handle-like (reset() fully releases and empties them), so the bare attribute is fine here:

[[clang::no_destroy]] static ScopedGdiplus g_gdiplus;
[[clang::no_destroy]] static ScopedHook g_keyboardHook;
[[clang::no_destroy]] static ScopedHook g_mouseHook;

Keep the existing explicit reset() calls in Wh_ModUninitno_destroy only suppresses the automatic destructor. See https://github.com/ramensoftware/windhawk/wiki/Global-objects-and-process-shutdown for the full picture. (g_layouts, g_subclassedWindows, g_stateMutex and the std::wstring settings are heap-only or no-op at teardown and need nothing.)

9. The window class is registered under Explorer's HINSTANCE and the result is ignored.

wc.hInstance = GetModuleHandleW(nullptr);   // explorer.exe, not the mod
RegisterClassExW(&wc);                       // return value discarded

lpfnWndProc points into the mod image, so the class must be owned by the mod's own module — you already have GetModInstance() in win7-action-center-recreation.wh.cpp#L1548, and audio-scroll-switcher.wh.cpp#L127 has the same helper with a comment explaining exactly this. Discarding the return value is the dangerous part: if a previous instance left the class registered (which item 1 makes likely), RegisterClassExW fails with ERROR_CLASS_ALREADY_EXISTS, ShowSwitcher still calls CreateWindowExW successfully, and the window ends up dispatching to the previous, unloaded image's FlyoutWndProc. Check the result, bail out on failure, and unregister with the same HINSTANCE as in win7-action-center-recreation.wh.cpp#L5338-L5344.

10. DarkContextMenu changes Explorer-wide state for a window that's entirely owner-drawn.

ShowSwitcher calls DarkContextMenu::Apply(isDark)SetPreferredAppMode(ForceDark | Default) + FlushMenuThemes() every time the switcher is shown. That's a process-wide uxtheme setting: it restyles every menu and themed control in Explorer, not just your flyout — and in light mode it forces AppMode::Default, which can undo AllowDark that another mod or Explorer itself set. Meanwhile the flyout is painted 100% by hand with GDI (FillRect/DrawTextW/Rectangle), so none of SetPreferredAppMode, FlushMenuThemes or AllowDarkModeForWindow affect its appearance at all. The whole DarkContextMenu namespace looks like it can be deleted; the DwmSetWindowAttribute dark-mode calls in ShowSwitcher are enough (and even those only matter for the non-client area, which a WS_POPUP window doesn't have).

11. ActivateKeyboardLayout(targetHkl, KLF_SETFORPROCESS) changes explorer.exe's own layout and isn't reverted.

KLF_SETFORPROCESS sets the input locale for every thread in the host process, i.e. Explorer — not the app the user is actually typing into. The PostMessageW(hTarget, WM_INPUTLANGCHANGEREQUEST, ...) above it is what does the real work. The ActivateKeyboardLayout call has no benefit for the target app and leaves Explorer on a layout the user never chose, which persists after the mod is disabled. I'd drop it (or at least drop KLF_SETFORPROCESS).

12. The README has no screenshot.

This is a purely visual mod with two distinct styles, five themes and a light/dark split — a screenshot (or a short GIF) of each style is what users will judge it on in the catalog. Only i.imgur.com and raw.githubusercontent.com are allowed as image hosts.

Optional improvements

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

  • ~250 lines of unused RAII wrappers. WinHandle, ScopedCoInit, ScopedMenu, ScopedTheme and ScopedCursor are defined but never instantiated anywhere in the file. Together with them, #include <msctf.h> and #include <objbase.h>, and -lole32 / -luxtheme in @compilerOptions, can go (uxtheme is loaded dynamically with LoadLibraryExW, so the static import isn't needed once ScopedTheme is gone).
  • The #if __has_include(<windhawk_api.h>) fallback block (lines 171-201) is dead in every real build, and its stand-in SetWindowSubclassFromAnyThread is a plain SetWindowSubclass, which would be wrong if it were ever used. Same for the WINVER / _WIN32_WINNT defines — Windhawk already sets those. Removing all of it makes the file easier to trust.
  • Wh_GetStringSetting never returns NULL — it returns L"" on error or when unset — so the if (styleSetting) / else fallback branches in LoadModSettings are unreachable. WindhawkUtils::StringSetting is the idiomatic RAII form and removes the manual Wh_FreeStringSetting calls:
    g_switcherStyle = WindhawkUtils::StringSetting::make(L"switcherStyle").get();
  • Drop the "Win78LangSwitcher: " prefix from every Wh_Log — Windhawk already prefixes log lines with the mod name. The emoji in the keyboard-hook failure log can go too.
  • Wh_ModSettingsChanged can use the simpler void form. The only path that sets *reload = TRUE is a catch (...) around code that can't throw, so the mod never actually requests a reload.
  • The blanket try { ... } catch (...) {} around Win32 code (in ParseHexColor, GetSystemAccentColor, IsDarkModeActive, GetWindowDpi, RegisterFlyoutClass, HideFlyout, EnumerateAndSubclassTaskbars, …) doesn't buy anything — none of those APIs throw — and it hides real failures. Worth keeping only where std::wstring/std::vector allocation is genuinely in play.
  • WM_MOUSEACTIVATE returns 0, which isn't one of the defined MA_* values. Return MA_NOACTIVATE (3) if the goal is to keep the indicator from stealing activation.
  • Wh_ModUninit calls DarkContextMenu::Restore() and then DarkContextMenu::Uninit(), which restores the app mode a second time. One of the two is enough.
  • RefreshKeyboardLayouts mixes two index spaces: foundActiveIndex = i is an index into hkls, but it's later compared against g_layouts.size() and stored as g_selectedIndex, which indexes newLayouts. The if (!hkl) continue; above means the two can diverge. Push into newLayouts first and record newLayouts.size() - 1.
  • Per-paint work: PaintWin8Flyout creates six fonts plus several brushes/pens on every WM_PAINT, and GetWindowDpi does a GetProcAddress(user32, "GetDpiForWindow") on every call (from paint, WM_MOUSEMOVE, WM_LBUTTONUP, …). GetDpiForWindow exists on Win10 1607+, which is below your stated minimum, so it can be called directly; the fonts can be cached and rebuilt only on DPI/settings change.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • KLID derivation is wrong for secondary layouts, so the layout name is wrong. In RefreshKeyboardLayouts, when (HIWORD(hkl) & 0xF000) != 0xF000 the code throws the device ID away and formats the KLID from langId alone. For a layout like United States-Dvorak (KLID 00010409) that produces 00000409, so GetLayoutDisplayName returns the plain "US" entry. The 0xF000 branch has the mirror problem: 0000F0xx isn't a registry key name — those HKLs encode a Layout Id that has to be matched against the Layout Id values under HKLM\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\*. Worth using HIWORD(hkl) when non-zero and doing the Layout Id lookup for the 0xFxxx case.
  • The keyboard navigation in FlyoutWndProc is unreachable. The window is created with WS_EX_NOACTIVATE, shown with SWP_NOACTIVATE, and never given focus, so it can't receive WM_KEYDOWN — the VK_UP / VK_DOWN / VK_HOME / VK_END / VK_RETURN / VK_ESCAPE handling and the WM_ACTIVATE case are dead code (Escape is actually handled by the LL keyboard hook). Either drop them or route the keys through the hook.
  • Hit-testing uses hard-coded rectangles that don't match the drawn text. The "Language preferences" link is drawn from paddingX to width - paddingX, but the click test is x >= paddingX && x <= ScaleForDpi(250, dpi) (Win8) / ScaleForDpi(220, dpi) (hover). Long localized strings (German, Greek, Hungarian) will extend past the clickable area. DrawTextW with DT_CALCRECT would give you the real extent to both draw and hit-test against.
  • Fixed flyout width + DT_END_ELLIPSIS (330 DIP for Win8.1, 270 for Win7) will truncate longer entries like "Portuguese (Brazil)" / "United States-International". Measuring the widest item and sizing to it (clamped to the work area) would match the native flyout more closely.
  • Multi-monitor / mixed-DPI positioning. ShowSwitcher computes dpi from GetWindowDpi(g_hFlyoutWnd) before moving the window to the target monitor, so on the first show (and on any move between monitors with different scaling) the metrics come from the wrong DPI. SHAppBarMessage(ABM_GETTASKBARPOS) also only reports the primary taskbar, so on a secondary monitor with a differently-placed taskbar the ABE_TOP/ABE_LEFT/ABE_RIGHT adjustments use coordinates from another display. GetDpiForMonitor(hMon, MDT_EFFECTIVE_DPI, ...) after picking the monitor, plus the secondary taskbar's own rect, would be more robust.
  • ShowWindow is hooked process-wide. Every ShowWindow call on the taskbar thread pays a GetClassNameW round trip, and Explorer calls it a lot (thumbnails, jump lists, hover previews). If it turns out to matter, SetWinEventHook(EVENT_OBJECT_SHOW) filtered to the process, or hooking only the specific Shell_InputSwitch* creation, would narrow it.
  • Returning TRUE from ShowWindow_Hook without calling the original tells the caller the window was already visible, and leaves the modern input-switch window in a state its owner doesn't expect. It seems to work in practice, but hiding the window (or blocking it at creation time) may be less surprising to the shell code that manages it.
  • The subAbbrev disambiguation heuristic is substring-based (upperLayout.find(L"US"), find(L"UK")), so any layout name containing those two letters anywhere — e.g. a Russian or Czech layout name that happens to contain "US" — gets tagged incorrectly. Matching on the KLID instead of the display name would be deterministic.
  • GDI+ is initialized process-wide just to draw one checkmark, and the file already contains a pure-GDI fallback (DrawWin7GdiFallbackCheck) that produces essentially the same glyph. Dropping GDI+ would remove GdiplusStartup/GdiplusShutdown, the -lgdiplus dependency and the shutdown hazard in item 8 altogether. DrawThemeBackground with MENU_POPUPCHECK would be an even closer match to the real Windows 7 glyph.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 25, 2026
@babamohammed2022

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 25, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Something went wrong while preparing the AI review, so there is nothing to post this time. This is a problem on the reviewer's side, not with this pull request.

Comment /ai-review to try again.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 25, 2026
@babamohammed2022

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 25, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Something went wrong while preparing the AI review, so there is nothing to post this time. This is a problem on the reviewer's side, not with this pull request.

Comment /ai-review to try again.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 25, 2026
@babamohammed2022

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 25, 2026
@windhawk-reviewer

Copy link
Copy Markdown

This pull request has already had 3 AI reviews in the last 24 hours, which is the limit, so no review was posted this time.

Comment /ai-review again after 2026-08-26 16:39 UTC (in 22 hours) to get another one.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 25, 2026
@babamohammed2022

Copy link
Copy Markdown
Contributor Author

Hi, @m417z. Sorry for bothering, but it seems like the AI reviewer has had a problem and it says that there have been 3 AI reviews when only one has been done.

@m417z m417z removed the waiting-for-author The author's turn: request an AI review, or respond to one that was posted. label Aug 25, 2026
@babamohammed2022

babamohammed2022 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. This is a 1.0.0 release and the mod is stable and functional. I'll address the performance optimizations (#1, #3, #4) in future releases, and fix Alt+Shift (#2) in the next version. If the maintainer considers any of these blocking for 1.0.0, let me know and I'll try to fix them.
/ready-for-reviewer

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-reviewer Ready for a human reviewer, and in the queue for one. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 30, 2026
@m417z

m417z commented Sep 1, 2026

Copy link
Copy Markdown
Member

You haven't answered my question:

I assume you only need to target the taskbar explorer.exe process, right? If so, add a check like you did with IsMainExplorerShell in your other PR.

If the answer is yes, please return false in Wh_ModInit for explorer.exe processes which are not the main shell to unload the mod in these processes.

@m417z m417z added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-reviewer Ready for a human reviewer, and in the queue for one. labels Sep 1, 2026
@babamohammed2022

Copy link
Copy Markdown
Contributor Author

You haven't answered my question:

I assume you only need to target the taskbar explorer.exe process, right? If so, add a check like you did with IsMainExplorerShell in your other PR.

If the answer is yes, please return false in Wh_ModInit for explorer.exe processes which are not the main shell to unload the mod in these processes.

Yes, the mod only needs to target the taskbar explorer.exe process. I'll fix this.

Updated version log from v1.0.1 to v1.0.0 and added unloading logic for secondary explorer processes.
@babamohammed2022

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Sep 1, 2026
@m417z

m417z commented Sep 1, 2026

Copy link
Copy Markdown
Member

Return FALSE to keep the mod unloaded.

Change return value from TRUE to FALSE when unloading.
@babamohammed2022

Copy link
Copy Markdown
Contributor Author

Return FALSE to keep the mod unloaded.

Is it correct now or are there any other adjustments to include?

@m417z

m417z commented Sep 1, 2026

Copy link
Copy Markdown
Member

This part is correct, yes.

@windhawk-reviewer

Copy link
Copy Markdown

Submission review

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

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

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


The overall structure (dedicated worker + hook threads, per-load window class with no ERROR_CLASS_ALREADY_EXISTS reuse, joins in SafeCleanup, no persistent system changes, subclass calls made outside the lock) is solid, and there's no meaningful overlap with the existing language-indicator mods. The items below are what I'd fix before merge.

1. Wh_ModInit deletes the critical section and then returns TRUE (lines 2861-2867)

if (!IsMainExplorerShell()) {
    g_Initialized = TRUE;
    DeleteCriticalSection(&g_Ctx.csLock);
    return TRUE;
}

Returning TRUE tells Windhawk the mod loaded successfully, so Wh_ModSettingsChanged and Wh_ModBeforeUninit/Wh_ModUninit will still be called in that process — and all three enter the deleted critical section:

  • Wh_ModSettingsChangedLoadSettings()EnterCriticalSection(&g_Ctx.csLock) (line 2737) — this runs on every settings change.
  • Wh_ModBeforeUninitSafeCleanup()RemoveTrayInterception()EnterCriticalSection(&g_Ctx.csLock) (line 2281).
  • Wh_ModUninit then calls DeleteCriticalSection(&g_Ctx.csLock) a second time (line 2961).

Entering a deleted CRITICAL_SECTION and double-deleting one are both undefined behavior, in a live explorer.exe. This is reachable in a normal session: with "Launch folder windows in a separate process" enabled (or any second explorer.exe), the tray owner check fails and this path is taken.

Return FALSE instead. Per the Windhawk lifetime contract, Wh_ModUninit is not called when Wh_ModInit returns FALSE, so deleting the critical section there is correct, and the mod gets reloaded after the next settings change. This is also the documented "nothing to do in this process" pattern — your own win7-network-flyout-recreation already does this correctly (undoing everything it installed, then return FALSE).

Worth double-checking the behaviour change this early return introduced, too: the comments at lines 2810-2815 and 2872-2876 still say the shell role is deliberately never baked in at init time and is re-evaluated by the worker's tick, but the new early return does bake it in. If the mod loads into a freshly started explorer.exe while a dying instance still owns Shell_TrayWnd/Progman (crash-restart), the new shell process now stays inert until the user touches a setting.

2. g_targetWindow can end up pointing at the taskbar instead of the user's window

ToolbarWndProc (line 2088) and InputIndicatorButtonProc (line 2130) return MA_ACTIVATE for WM_MOUSEACTIVATE, so by the time WM_LBUTTONUP arrives the taskbar has already become the foreground window. The capture then runs:

HWND hFore = GetForegroundWindow();       // -> Shell_TrayWnd
if (hFore != hWnd && hFore != hFlyout) {  // hWnd is the *child* toolbar, so this passes
    g_targetWindow.store(hFore, ...);
}

hWnd is the toolbar/indicator child window, never the top-level Shell_TrayWnd that GetForegroundWindow returns, so the guard never fires. RefreshKeyboardLayouts (line 800) and SwitchToLayout (line 954) then both use that HWND, so the flyout shows the taskbar thread's current layout as selected and PostMessageW(hTarget, WM_INPUTLANGCHANGEREQUEST, ...) switches the taskbar's layout rather than the app the user was typing in. The same applies to the capture in ShowWindow_Hook (lines 1976-1980), which runs after the click has already activated the taskbar.

Add an explicit exclusion in one shared helper used by all three sites — you already have FindAncestorTaskbar, so something like if (FindAncestorTaskbar(hFore) == hFore) return; — and/or capture the foreground window on WM_MOUSEACTIVATE / WM_LBUTTONDOWN, before activation changes.

3. Both low-level hooks are installed unconditionally, for the whole session

HookThreadProc (lines 2582-2592) installs WH_KEYBOARD_LL and WH_MOUSE_LL at load and keeps them until unload, regardless of settings and regardless of whether the flyout is on screen. Every keystroke and every mouse-down on the system then makes a round trip through this thread in explorer.exe. This is the single most common performance objection on mod submissions, and here most of it is avoidable:

  • The mouse hook exists only to dismiss the flyout on an outside click (HideFlyoutIfClickOutside returns immediately unless the flyout is visible). Install it when the flyout is shown and remove it when it's hidden, instead of filtering every system-wide click. As a minimum, check g_hFlyoutWnd in LowLevelMouseProc before posting.
  • The keyboard hook is pure overhead when enableWinSpace, enableAltShift and enableCustomHotkey are all off — skip installing it in that case, and re-evaluate in Wh_ModSettingsChanged.

4. The tray retry timer polls forever (line 2627)

UINT_PTR trayRetryTimer = SetTimer(NULL, 0, 1500, NULL);

EvaluateShellRole() runs every 1.5 s for the whole session, and each tick does GetModuleFileNameW, FindWindowW, a walk of the tray tree and a desktop-wide FindWindowExW(NULL, ..., L"Shell_SecondaryTrayWnd", ...) enumeration — even when nothing has changed since the last tick.

The comment above the function says TaskbarCreated "is never delivered to a thread's message queue", but your win7-network-flyout-recreation does exactly the right thing and it works there: arm the 1.5 s timer only while interception is not installed, KillTimer as soon as InstallTrayInterceptionInternal() succeeds, and re-arm it when the RegisterWindowMessageW(L"TaskbarCreated") message arrives. Reuse that pattern here — the subclass procs already clear G_hSubclassedToolbar / G_hSubclassedIndicator on WM_NCDESTROY, which gives you the other re-arm trigger for free.

Optional improvements

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

  • Dead compatibility shim (lines 154-183). Windhawk always provides windhawk_api.h / windhawk_utils.h, so the #else branch never compiles. It's also wrong if it ever did: its SetWindowSubclassFromAnyThread is a plain SetWindowSubclass, which cannot subclass a window owned by another thread. Same for the #ifndef UNICODE / WINVER / _WIN32_WINNT blocks (lines 129-138) — Windhawk already sets these.
  • IsExplorerProcess() is redundant. @include explorer.exe already restricts the mod to that process, so the checks at lines 2794, 2837, 2855 and 2942 can go.
  • LoadLibraryW by bare name (gdiplus.dll line 280, shcore.dll line 588) uses the default search order, which includes the executable's directory. Neither is a KnownDLL. The mod is scoped to explorer.exe, which lives in a protected directory, so this is a hardening nit rather than a real vector — but LoadLibraryExW(L"shcore.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) costs nothing. While you're there: GetMonitorDpi does LoadLibrary/GetProcAddress/FreeLibrary on every call and GetWindowDpi does a GetProcAddress on every call, both from paint/positioning paths — resolve both once at init.
  • Off-by-one in RefreshKeyboardLayouts (lines 811-824). foundActiveIndex = i is an index into hkls, but entries are skipped by if (!hkl) continue; (line 815), so it can be out of sync with newLayouts. Use foundActiveIndex = newLayouts.size(); immediately before the push_back.
  • String settings. WindhawkUtils::StringSetting (RAII) is preferred over Wh_GetStringSetting + manual Wh_FreeStringSetting in LoadSettings. Also, Wh_GetStringSetting never returns NULL (it returns L""), so the if (styleSetting) / if (langSetting) / … checks are dead.
  • PostThreadMessageW results are never checked while SafeCleanup waits INFINITE on both threads (lines 2763-2783). The comment says delivery is guaranteed because Wh_ModInit waited on the ready events — but those waits have a 5000 ms timeout (lines 2914, 2933) and their result is ignored, so the guarantee doesn't hold in the timeout case, and a lost WM_QUIT turns into a permanent Explorer hang on unload. Either wait INFINITE on the ready events, or loop while (WaitForSingleObject(hThread, 100) == WAIT_TIMEOUT) PostThreadMessageW(tid, WM_QUIT, 0, 0);.
  • Secondary taskbars aren't PID-filtered. Lines 2159-2163 explain very clearly why a foreign Shell_TrayWnd must be refused, but the Shell_SecondaryTrayWnd enumeration at line 2232 searches all top-level windows on the desktop with no such check, and pushes the HWND into G_hSubclassedSecToolbars before the subclass call, so a failed subclass is still recorded and later gets a cross-process RemoveWindowSubclassFromAnyThread. Apply the same GetWindowThreadProcessId check, and only record the HWND if SetWindowSubclassFromAnyThread succeeded.
  • ShowWindow hook cost. ShowWindow_Hook calls GetClassNameW on nearly every ShowWindow in Explorer. You could hook CreateWindowExW, remember the Shell_InputSwitchTopLevelWindow / Shell_InputSwitchDismissOverlay HWNDs there, and reduce the hook body to a pointer comparison — that's the approach windows-11-taskbar-styler uses for the same class.
  • Unused dependencies and dead state. -luxtheme, #include <uxtheme.h> and #include <msctf.h> aren't used by anything. g_Ctx.refCount (lines 1850, 1789) and g_Initialized are written but never read. PaintSwitcher's s_inPaint re-entrancy guard guards a path that can't re-enter.
  • try/catch (...) blocks. Under Clang/mingw these don't catch access violations, only C++ exceptions — and almost none of the wrapped code can throw anything but bad_alloc. They add noise without adding safety; dropping most of them would make the code easier to follow.
  • keybd_event (lines 2373-2374) is superseded by SendInput.
  • Magic DWM attribute numbers 19/20 at lines 1856-1857 — you named DWMWA_WINDOW_CORNER_PREFERENCE_LOCAL just below, so it'd be consistent to name DWMWA_USE_IMMERSIVE_DARK_MODE too.
  • Credit "ac" in the README. The PR body credits them; Windhawk keeps a single @author (the person responsible for the mod), and extra credits belong in the README so they're visible to users.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • Does the mod conflict with Windows' own layout hotkey? Windows' "Switch input language" hot key defaults to Left Alt+Shift, and the mod doesn't (and can't easily) swallow the modifier keys, so both the OS and the mod would fire on the same chord and the layout would advance twice. Did you test with the OS hotkey left at its default, or was it set to "Not Assigned" on your machine? Worth documenting in the README either way.
  • "Show the Language bar" doesn't show the language bar — it runs control.exe /name Microsoft.Language (line 1703), and that canonical Control Panel page was removed in Windows 10 1803; on current builds it just redirects to Settings. Either point it at the Settings page for the language bar or rename the item.
  • GetSystemAccentColor reads the wrong value. DwmGetColorizationColor returns DWM's colorization color, not the Windows accent color; they're usually related but not identical. HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\AccentAccentColorMenu, or winrt::Windows::UI::ViewManagement::UISettings::GetColorValue(UIColorType::Accent), gives the actual accent.
  • RTL languages. The Arabic and Hebrew strings are drawn with DT_LEFT into a non-RTL window, so they'll be left-aligned with LTR paragraph direction. WS_EX_LAYOUTRTL on the flyout (or DT_RTLREADING) when the selected UI language is RTL would fix the layout.
  • SetForegroundWindow from the worker thread (line 1869) will usually fail when Explorer isn't already the foreground process — e.g. the Win+Space path. The flyout still appears, but it won't have focus, so WM_KEYDOWN navigation and the WM_ACTIVATE-based auto-hide won't run there (the low-level hooks cover Escape and cycling, so this is mostly cosmetic — just noting it in case the arrow-key navigation appears not to work).
  • Is the ToolbarWindow32 interception actually reached? IsLanguageButton compares TB_GETBUTTONTEXT output against the language abbreviations, but the toolbar under SysPager is the notification-area icon toolbar, whose button text is the icon tooltip. On Windows 10 the input indicator is the separate TrayInputIndicatorWClass handled a few lines below, and on Windows 11 it goes through the ShowWindow hook. If this path never fires in practice it's worth removing; if it does, it'd be good to say which configuration exercises it.
  • Injecting a Ctrl press from inside the low-level keyboard hook (lines 2373-2374) to suppress the Start menu is a known trick, but the foreground app does see a real Ctrl down/up pair. Apps that react to a bare Ctrl (the "show pointer location on Ctrl" setting, some games/editors) will notice.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Sep 1, 2026
Added IsTaskbarWindow function to check if a window is a taskbar window. Updated logic to prevent taskbar windows from being set as the target window in various functions.
@babamohammed2022

Copy link
Copy Markdown
Contributor Author

/ai-review
I've tried to enhance the mod to make it more stable

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Sep 1, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

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

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

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


Wh_ModInit now returns FALSE in non-shell Explorer instances, which resolves the deleted-critical-section problem from the last round. The two commits after that one, however, introduced a new set of problems in SwitchToLayout and in the target-window capture — items 1-4 below are all in that new code.

1. ActivateKeyboardLayout(..., KLF_SETFORPROCESS) switches Explorer's own layout, not the target app's (lines 993 and 1003)

ActivateKeyboardLayout acts on the calling thread, and with KLF_SETFORPROCESS on the whole calling process — which here is explorer.exe, on the mod's worker thread. It can never change the layout of the app the user was typing in, so the actual switch is still done by the PostMessageW(hTarget, WM_INPUTLANGCHANGEREQUEST, ...) that was already there. What it does do is silently re-target Explorer's input language on every switch (desktop, Explorer windows, Start/search boxes) — a side effect on the host process that the mod never undoes.

The retry logic makes it fire twice per switch: PostMessageW is asynchronous, so the GetKeyboardLayout(dwTargetThreadId) != targetHkl check on line 1002 runs before the target thread has processed anything and is essentially always true, which means the KLF_SETFORPROCESS | KLF_RESET call on line 1003 also runs every time. KLF_RESET is not a "reset the layout" flag either — together with KLF_SHIFTLOCK it selects whether Caps Lock is cleared by Caps Lock or by Shift, so the comment above it doesn't describe what the flag does.

Please drop both ActivateKeyboardLayout calls and keep just the message-based switch that the previous version had:

if (hTarget && IsWindow(hTarget)) {
    PostMessageW(hTarget, WM_INPUTLANGCHANGEREQUEST,
                 0, reinterpret_cast<LPARAM>(targetHkl));
}

2. The SendMessageW fallback can hang Explorer on unload (line 999)

if (!PostMessageW(hTarget, WM_INPUTLANGCHANGEREQUEST, ...)) {
    SendMessageW(hTarget, WM_INPUTLANGCHANGEREQUEST, ...);
}

This is a blocking cross-process send from the worker thread to an arbitrary application's window. If that app is hung, the send never returns — and SafeCleanup waits INFINITE on the worker thread (line 2854), so disabling or updating the mod hangs explorer.exe permanently. PostMessageW to a foreign window essentially only fails when the target queue is full, i.e. exactly when the app is already in trouble. Remove the fallback (or, if you want to keep it, use SendMessageTimeoutW(..., SMTO_ABORTIFHUNG, 200, nullptr)).

3. RememberForegroundTarget can pick an arbitrary window from another process (lines 2377-2384)

When the taskbar is foreground and no previous target is remembered, the code walks the desktop's children in Z order and takes the first visible non-taskbar top-level window:

HWND hReal = GetWindow(GetDesktopWindow(), GW_CHILD);
while (hReal) {
    if (IsWindowVisible(hReal) && !IsTaskbarWindow(hReal)) {
        g_targetWindow.store(hReal, std::memory_order_release);
        break;
    }
    hReal = GetWindow(hReal, GW_HWNDNEXT);
}

That first entry is whatever happens to be topmost — very often a system helper window that is IsWindowVisible but not a real app window (Progman/WorkerW, a tooltip, an overflow/host window, a XAML island). The mod then stores it in g_targetWindow, so it stays sticky, and SwitchToLayout posts WM_INPUTLANGCHANGEREQUEST into an unrelated process. There's no way to make this heuristic reliable — please delete the walk and simply do nothing when there is no known non-taskbar target.

4. The taskbar filter misses taskbar children, so the click path still captures the wrong window

IsTaskbarWindow (line 626) only matches the two top-level classes, but the windows that reach the filter are frequently children of the taskbar:

  • SwitchToLayout's last-resort chain (lines 967-974) uses GetGUIThreadInfo(0, ...), which returns the foreground thread's info. While the taskbar is active, gti.hwndActive is Shell_TrayWnd (rejected) and it falls through to gti.hwndFocus, which is the tray ToolbarWindow32 — class name ToolbarWindow32, so the filter passes and the mod ends up trying to switch the layout of the tray toolbar.
  • InputIndicatorButtonProc has no filter at all (lines 2183-2185 and 2195-2197). Since it returns MA_ACTIVATE for WM_MOUSEACTIVATE, the taskbar is already foreground by the time WM_LBUTTONUP arrives, so g_targetWindow is set to Shell_TrayWnd on every click on the indicator — the original problem, unchanged on that path.

Two things fix this properly:

  • Use the ancestor test you already have instead of the class name: FindAncestorTaskbar(h) != h means h is the taskbar or anything inside it.
  • Capture the foreground window in the WM_MOUSEACTIVATE (or WM_LBUTTONDOWN) branch instead of WM_LBUTTONUP. WM_MOUSEACTIVATE is sent before activation happens, so GetForegroundWindow() there is still the user's app and no filtering guesswork is needed. Both subclass procs already handle that message.

5. The keyboard hook breaks the hook chain on Win key-up (line 2468)

s_inKbdHook = false;
return 0;

Returning without calling CallNextHookEx skips every WH_KEYBOARD_LL hook installed before this one, for that event. The key isn't swallowed (the return value is 0), but other hook consumers — PowerToys, AutoHotkey, other Windhawk mods — never see the VK_LWIN/VK_RWIN key-up and can be left believing Win is still held. This is the only path in the proc that does this; make it return CallNextHookEx(nullptr, nCode, wParam, lParam);.

6. Both low-level hooks are still installed unconditionally, for the whole session (lines 2657-2666)

Repeating this from the previous review since it's unchanged: WH_KEYBOARD_LL and WH_MOUSE_LL are installed at load and kept until unload, regardless of settings and regardless of whether the flyout is on screen, so every keystroke and every mouse-down in the system round-trips through this thread in explorer.exe. Most of it is avoidable: install the mouse hook when the flyout is shown and remove it when it's hidden (it does nothing else — HideFlyoutIfClickOutside returns immediately when the flyout isn't visible), and skip the keyboard hook entirely when enableWinSpace, enableAltShift and enableCustomHotkey are all off, re-evaluating in Wh_ModSettingsChanged.

7. The tray retry timer still polls forever (line 2702)

Also unchanged: EvaluateShellRole() runs every 1.5 s for the life of the process, and each tick does GetModuleFileNameW + FindWindowW + a walk of the tray tree + a desktop-wide FindWindowExW(NULL, ..., L"Shell_SecondaryTrayWnd", ...) enumeration, even when nothing changed. Your own win7-network-flyout-recreation arms the timer only while interception is not installed, kills it as soon as InstallTrayInterceptionInternal() succeeds, and re-arms it on RegisterWindowMessageW(L"TaskbarCreated"); the WM_NCDESTROY handlers in the subclass procs give you the other re-arm trigger for free. (Note that TaskbarCreated is delivered to a thread that owns a top-level window, which is how it works in that mod — so the comment at lines 2681-2684 isn't quite right.)

Optional improvements

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

  • Leftover scaffolding in the last two commits. The WM_LBUTTONUP block in ToolbarWndProc (lines 2129-2143) re-reads GetForegroundWindow() into hRealFore after already establishing that hFore is the taskbar, so IsTaskbarWindow(hRealFore) is necessarily true and the else branch is dead — the whole thing collapses to if (!IsTaskbarWindow(hFore)) g_targetWindow.store(hFore, ...);. The comments there are in Italian, and the indentation of that block and of lines 2021-2031 got mangled; both read like AI edit artifacts.
  • Cache the layout list. CycleSwitcher calls RefreshKeyboardLayouts on every Win+Space tap, and each call does, per installed layout, a Keyboard Layout\Substitutes query, a Keyboard Layouts\<klid> open + SHLoadIndirectString, and sometimes a full enumeration of HKLM\...\Keyboard Layouts (FindKlidByLayoutId). The list changes very rarely — build it once and refresh only when the layout set actually changes (e.g. on WM_INPUTLANGCHANGE / when showing the flyout).
  • Off-by-one in RefreshKeyboardLayouts (lines 819-830). foundActiveIndex = i indexes hkls, but entries can be skipped by if (!hkl) continue;, so it can be out of sync with newLayouts. Use foundActiveIndex = newLayouts.size(); immediately before the push_back.
  • [[clang::no_destroy]] on g_keyboardHook / g_mouseHook (lines 511-512) isn't needed. ScopedHookHolder's destructor is a plain UnhookWindowsHookEx, which is safe to run at process shutdown, and the hook thread already resets both holders before exiting — so the suppression is noise. See Global objects and process shutdown for when it's actually warranted.
  • Dead compatibility shim (lines 154-183). Windhawk always provides windhawk_api.h / windhawk_utils.h, so the #else branch never compiles — and it's wrong if it ever did: its SetWindowSubclassFromAnyThread is a plain SetWindowSubclass, which can't subclass a window owned by another thread. Same for the #ifndef UNICODE / WINVER / _WIN32_WINNT blocks (lines 129-138).
  • IsExplorerProcess() is redundant. @include explorer.exe already restricts the mod to that process, so the checks at lines 2912, 2930 and 3017 can go.
  • LoadLibraryW by bare name (gdiplus.dll line 280, shcore.dll line 588) uses the default search order, which includes the executable's directory; neither is a KnownDLL. The mod is scoped to explorer.exe, which lives in a protected directory, so this is a hardening nit rather than a real vector — but LoadLibraryExW(L"shcore.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) costs nothing. While you're there: GetMonitorDpi does LoadLibrary/GetProcAddress/FreeLibrary on every call and GetWindowDpi does a GetProcAddress on every call, both from paint/positioning paths — resolve both once at init.
  • String settings. WindhawkUtils::StringSetting (RAII) is preferred over Wh_GetStringSetting + manual Wh_FreeStringSetting in LoadSettings. Also, Wh_GetStringSetting never returns NULL (it returns L""), so the if (styleSetting) / if (langSetting) / … checks are dead.
  • The ready-event waits have a timeout but the result is ignored (lines 2989 and 3008), while SafeCleanup waits INFINITE on both threads. The comment at lines 2834-2837 argues the WM_QUIT posts can't be dropped because the queues are guaranteed to exist — that guarantee doesn't hold if either 5000 ms wait times out, and a lost WM_QUIT becomes a permanent Explorer hang. Either wait INFINITE on the ready events, or loop while (WaitForSingleObject(hThread, 100) == WAIT_TIMEOUT) PostThreadMessageW(tid, WM_QUIT, 0, 0);.
  • Secondary taskbars aren't PID-filtered. Lines 2212-2223 explain very clearly why a foreign Shell_TrayWnd must be refused, but the Shell_SecondaryTrayWnd enumeration at line 2292 searches all top-level windows on the desktop with no such check, and pushes the HWND into G_hSubclassedSecToolbars before the subclass call, so a failed subclass is still recorded and later gets a cross-process RemoveWindowSubclassFromAnyThread. Apply the same GetWindowThreadProcessId check, and only record the HWND if SetWindowSubclassFromAnyThread succeeded.
  • ShowWindow hook cost. ShowWindow_Hook calls GetClassNameW on nearly every ShowWindow in Explorer. You could hook CreateWindowExW, remember the Shell_InputSwitchTopLevelWindow / Shell_InputSwitchDismissOverlay HWNDs there, and reduce the hook body to a pointer comparison — that's the approach windows-11-taskbar-styler uses for the same class.
  • Unused dependencies and dead state. -luxtheme, #include <uxtheme.h> and #include <msctf.h> aren't used by anything. g_Ctx.refCount (lines 1838, 1899) and g_Initialized are written but never read. PaintSwitcher's s_inPaint guard protects a path that can't re-enter. Conversely, <cwctype> / <cstdlib> (for towupper, iswspace, wcstoul) are used but only included transitively.
  • try / catch (...) blocks. Under Clang/mingw these don't catch access violations, only C++ exceptions — and almost none of the wrapped code can throw anything but bad_alloc. They add noise without adding safety.
  • keybd_event (lines 2448-2449) is superseded by SendInput.
  • Magic DWM attribute numbers 19/20 at lines 1905-1906 — you named DWMWA_WINDOW_CORNER_PREFERENCE_LOCAL just below, so it'd be consistent to name DWMWA_USE_IMMERSIVE_DARK_MODE too.
  • Credit "ac" in the README. The PR body credits them; Windhawk keeps a single @author (the person responsible for the mod), and extra credits belong in the README so they're visible to users.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • Conflict with Windows' own layout hotkey. Windows' "Switch input language" hot key defaults to Left Alt+Shift, and the mod doesn't (and can't easily) swallow the modifier keys, so both the OS and the mod fire on the same chord and the layout advances twice. You mentioned fixing Alt+Shift in a later version — worth documenting the current behaviour in the README in the meantime.
  • "Show the Language bar" doesn't show the language bar — it runs control.exe /name Microsoft.Language (line 1752), and that canonical Control Panel page was removed in Windows 10 1803; on current builds it just redirects to Settings. Either point it at the Settings page for the language bar or rename the item.
  • GetSystemAccentColor reads the wrong value. DwmGetColorizationColor returns DWM's colorization color, not the Windows accent color; they're usually related but not identical. HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\AccentAccentColorMenu, or winrt::Windows::UI::ViewManagement::UISettings::GetColorValue(UIColorType::Accent), gives the actual accent.
  • RTL languages. The Arabic and Hebrew strings are drawn with DT_LEFT into a non-RTL window, so they'll be left-aligned with LTR paragraph direction. WS_EX_LAYOUTRTL on the flyout (or DT_RTLREADING) when the selected UI language is RTL would fix the layout.
  • SetForegroundWindow from the worker thread (line 1918) will usually fail when Explorer isn't already the foreground process — e.g. the Win+Space path. The flyout still appears but without focus, so WM_KEYDOWN navigation and the WM_ACTIVATE auto-hide won't run there (the low-level hooks cover Escape and cycling, so this is mostly cosmetic — noting it in case arrow-key navigation appears not to work).
  • Is the ToolbarWindow32 interception actually reached? IsLanguageButton compares TB_GETBUTTONTEXT output against the language abbreviations, but the toolbar under SysPager is the notification-area icon toolbar, whose button text is the icon tooltip. On Windows 10 the input indicator is the separate TrayInputIndicatorWClass handled a few lines below, and on Windows 11 it goes through the ShowWindow hook. If this path never fires in practice it's worth removing; if it does, it'd be good to say which configuration exercises it.
  • Injecting a Ctrl press from inside the low-level keyboard hook (lines 2448-2449) to suppress the Start menu is a known trick, but the foreground app does see a real Ctrl down/up pair. Apps that react to a bare Ctrl (the "show pointer location on Ctrl" setting, some games/editors) will notice.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Sep 1, 2026
@babamohammed2022

Copy link
Copy Markdown
Contributor Author

I've checked myself and none of the problems seems to occur, if the maintainer believes that these problems need to be addressed I'll try to do it.
Logs to confirm:
21:53:41.841 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [2923:Wh_ModInit]: === Win78LangSwitcher: Wh_ModInit v1.0.0 ===
21:53:41.846 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [2946:Wh_ModInit]: Win78LangSwitcher: pid=2444 trayOwner=2444 mainShell=1
21:53:41.846 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [2975:Wh_ModInit]: Win78LangSwitcher: ShowWindow hook installed successfully
21:53:41.852 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [2248:InstallTrayInterceptionInternal]: Win78LangSwitcher: Subclassed ToolbarWindow32 (0x00000000000100EC)
21:53:41.852 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [2275:InstallTrayInterceptionInternal]: Win78LangSwitcher: Subclassed Input Indicator TrayInputIndicatorWClass (0x00000000000100EE)

21:53:44.379 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [1008:SwitchToLayout]: SwitchToLayout: Changed to layout FFFFFFFFF0030410 for thread 5116 (window: 0000000000010372)
21:53:46.026 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [1008:SwitchToLayout]: SwitchToLayout: Changed to layout 0000000004100410 for thread 5116 (window: 0000000000010372)
21:53:48.413 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [1008:SwitchToLayout]: SwitchToLayout: Changed to layout FFFFFFFFF0030410 for thread 5116 (window: 0000000000010372)
21:53:49.605 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [1008:SwitchToLayout]: SwitchToLayout: Changed to layout 0000000004100410 for thread 5116 (window: 0000000000010372)
21:54:03.883 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [1008:SwitchToLayout]: SwitchToLayout: Changed to layout FFFFFFFFF0030410 for thread 10004 (window: 0000000000010332)
21:54:05.036 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [1008:SwitchToLayout]: SwitchToLayout: Changed to layout 0000000004100410 for thread 10004 (window: 0000000000010332)
21:55:44.155 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [1008:SwitchToLayout]: SwitchToLayout: Changed to layout FFFFFFFFF0030410 for thread 10004 (window: 0000000000010332)
21:55:46.404 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [1008:SwitchToLayout]: SwitchToLayout: Changed to layout 0000000004100410 for thread 10004 (window: 0000000000010332)

22:06:02.231 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [1008:SwitchToLayout]: SwitchToLayout: Changed to layout FFFFFFFFF0030410 for thread 10004 (window: 0000000000010332)
22:06:04.222 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [1008:SwitchToLayout]: SwitchToLayout: Changed to layout 0000000004100410 for thread 10004 (window: 0000000000010332)
22:19:16.850 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [1008:SwitchToLayout]: SwitchToLayout: Changed to layout FFFFFFFFF0030410 for thread 10004 (window: 0000000000010332)
22:19:18.415 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [1008:SwitchToLayout]: SwitchToLayout: Changed to layout 0000000004100410 for thread 10004 (window: 0000000000010332)
22:19:21.009 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [1008:SwitchToLayout]: SwitchToLayout: Changed to layout FFFFFFFFF0030410 for thread 5116 (window: 0000000000010372)
22:19:22.561 2444 explorer.exe [WH] [local@win7-language-switcher-restorer] [1008:SwitchToLayout]: SwitchToLayout: Changed to layout 0000000004100410 for thread 5116 (window: 0000000000010372)

/ready-for-reviewer

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-reviewer Ready for a human reviewer, and in the queue for one. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Sep 1, 2026
@m417z
m417z merged commit bdc548b into ramensoftware:main Sep 1, 2026
5 checks passed
@windhawk-reviewer windhawk-reviewer Bot removed the waiting-for-reviewer Ready for a human reviewer, and in the queue for one. label Sep 1, 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