Skip to content

Add Win-X Hot Corners 4.1.1 - #5001

Open
DhakadG wants to merge 5 commits into
ramensoftware:mainfrom
DhakadG:add-win-x-hotcorners
Open

Add Win-X Hot Corners 4.1.1#5001
DhakadG wants to merge 5 commits into
ramensoftware:mainfrom
DhakadG:add-win-x-hotcorners

Conversation

@DhakadG

@DhakadG DhakadG commented Aug 3, 2026

Copy link
Copy Markdown

Adds Win-X Hot Corners, a hot corners and screen edges mod for Windows 10 and 11.

Moving the cursor into a screen corner, or against a screen edge, triggers a configurable action. Each monitor gets twelve independent zones — four corners, four edges and a centre block on each edge — and there are 32 built-in actions plus arbitrary key combinations and commands.

Notes on the implementation

Detection does not use a low-level mouse hook. A WH_MOUSE_LL hook has to return within LowLevelHooksTimeout or Windows skips the callback and eventually removes the hook, which shows up as corners that intermittently stop working. It also puts the mod on the input path of every application on the system. This mod instead polls the cursor every 16 ms on a dedicated thread in its own tool-mod process, so it adds nothing to system-wide input handling and cannot be starved.

Monitors are identified by display name read via QueryDisplayConfig, not by position in an enumeration. Rearranging displays or changing which one is primary therefore does not silently repoint a configuration at a different screen. Identical models are disambiguated with a numeric suffix.

Per-monitor DPI aware. The detection thread pins DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, so zone rectangles and cursor coordinates agree on mixed-scaling setups.

Zone geometry is provably disjoint. Corner and edge sizes are clamped per monitor so the zones can never overlap, which matters because the hit test is first-match-wins. Each edge's thickness is capped at the smaller of the two corners it runs between, which is what keeps the twelve zones disjoint under arbitrary per-zone sizes.

Actions run on a separate worker thread, with a rate limit between them, so a slow launch cannot delay detection and rapid triggering cannot flood the shell.

There is no settings page; the mod is configured from its tray icon. This changed in 4.1.0 and is worth explaining, because the earlier review of this pull request suggested the opposite — dropping the tray dashboard and keeping the settings page.

The problem is that the two could never agree. Twelve zones on each of up to eight displays, each with a 36-entry action list, an argument field and six timing overrides, is a settings tree of roughly 700 lines of YAML that reviewers and users alike found unusable. But a mod cannot write its own settings from code, so anything changed in the mod's own UI could not be written back to the page — leaving two sources of truth that diverged the moment either was touched. Rather than keep both, the settings block was deleted outright and the mod's value store became the only place configuration lives.

Left-clicking the tray icon toggles the mod; right-clicking gives a short menu (suspend, the fullscreen and drag guards) and opens the dashboard, which has a clickable live preview of the zones on the selected display, grouped options with hover help, and follows the system light/dark theme. The mod logs a line at startup saying where to find it, and the readme leads with it.

Checklist

  • Single file: mods/win-x-hotcorners.wh.cpp
  • Mod id win-x-hotcorners matches the filename
  • @github is https://github.com/DhakadG, which is the author of this pull request
  • Licensed MIT, declared in the mod metadata
  • Compiles and runs on Windows 11 (26300)

Inspired by WinXCorners by @vhanla. This is an independent implementation rather than a port of that code.

Source repository: https://github.com/DhakadG/win-x-hotcorners

Changelog

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

  • Still a new mod submission, so there is nothing in the catalogue for it to change.
  • The submitted version has moved from 3.5.0 to 4.1.1 while this pull request has been open. The mod's own readme carries the full changelog. In short: per-zone overrides for size and timings (blank means inherit, so existing configurations are unaffected), a settings dashboard opened from a tray icon, alternating actions, a centre zone on each edge, and a knock-to-activate trigger.
  • Since the last review comment on this pull request: the Windhawk settings page was removed in favour of the tray dashboard (see above), verbose logging was dropped as a setting, the fullscreen guard now suppresses only the display the fullscreen app is on rather than every display, and an edge split around a centre zone now shares one alternation position between its two halves instead of keeping one per half.

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.

macOS-style hot corners and screen edges for Windows 10 and 11, with full
multi-monitor support.

Move the cursor into a screen corner or against an edge to trigger an action.
Each monitor has eight configurable zones - four corners and four edges - and
31 built-in actions including Task View, Show Desktop, Switch to Last Window,
Task Switcher, window snapping, virtual desktop navigation, lock, sleep, and
arbitrary key combinations or commands.

Monitors are identified by their display name read from EDID rather than by
position in a list, so rearranging displays or changing which one is primary
does not reshuffle the configuration. Detection is per-monitor DPI aware.

Detection runs on a dedicated thread in the mod's own process and polls the
cursor, rather than installing a global low-level mouse hook, so it adds
nothing to the system-wide input path.
@windhawk-reviewer windhawk-reviewer Bot added the waiting-for-author The author's turn: request an AI review, or respond to one that was posted. label Aug 3, 2026
@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.

@DhakadG

DhakadG commented Aug 3, 2026

Copy link
Copy Markdown
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 3, 2026
@DhakadG DhakadG closed this Aug 3, 2026
@DhakadG DhakadG reopened this Aug 3, 2026
@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 3, 2026
@redrag2105

Copy link
Copy Markdown
Contributor

Really cool mod with so many handy options. Love how fresh this feels on Windows. Great job! 🔥

Submitted version moves from 3.5.0 while this pull request is open. The mod's
own readme carries the full changelog; the substantive changes are:

- Per-zone settings: size, delay, pass-through guard, knock window, cooldown
  and required modifier can each be overridden for one zone, with blank
  meaning inherit, so existing configurations behave identically.
- A settings dashboard opened from the tray icon, with a live preview of the
  twelve zones, following the system light/dark theme.
- Alternating actions, a centre zone on each edge, and a knock-to-activate
  trigger.
- Monitor names are read once per rebuild and now survive a display layout
  that changes while it is being read; previously that dropped every name and
  left name-bound zones unmatched until the next display change.
@DhakadG DhakadG changed the title Add Win-X Hot Corners 3.5.0 Add Win-X Hot Corners 4.0.2 Aug 5, 2026
Fixes the 2.0.0-alpha.2 compile job. The "Require a modifier key" setting was a
number carrying $options, which Windhawk 2.0 rejects - and it rejects the whole
settings block, so the mod would not have loaded at all there. Windhawk 1.6 and
1.7 accept it, which is why it only surfaced in this matrix.

The setting now takes none/ctrl/alt/shift/win. A bare 0-4 is still accepted, so
a configuration saved by an earlier build keeps working.
@DhakadG DhakadG changed the title Add Win-X Hot Corners 4.0.2 Add Win-X Hot Corners 4.0.3 Aug 5, 2026
IDC_ZONE_ACTION is a DashId and ZONE_COUNT is a Zone; C++20 deprecates
arithmetic between two different enumeration types, which fails the
2.0.0-alpha.2 job because it treats any warning as an error. Cast one side.
No behaviour change, so the mod version stays at 4.0.3.
@DhakadG

DhakadG commented Aug 5, 2026

Copy link
Copy Markdown
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 5, 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.


The tool-mod structure, the snapshot-based detection loop and the zone geometry are all well thought through, and the reasoning left in the comments is genuinely useful. The findings below are mostly about the settings dashboard, which is where the mod stops using Windhawk's own mechanisms and starts replacing them.

1. The dashboard shadows Windhawk's settings page, so editing settings in Windhawk silently does nothing.

Once Save and Apply has been pressed, gui_active is 1 and ApplyTrayOverrides overwrites nearly everything LoadSettings just read — sizes, delays, knock, cooldown, centre %, modifier, lock-blank delay, taskbar, monitor names, the exclusion list, and monitorConfigs wholesale (ApplyDashboardZones). Both WhTool_ModInit and WhTool_ModSettingsChanged run LoadSettings() and then ApplyTrayOverrides(), so from that point on the Windhawk settings page has no effect at all until the user finds Reset to Windhawk settings in the tray menu. That is a hard thing for a user to diagnose — they will change a value on Windhawk's page, see nothing happen, and report the mod as broken.

Roughly 1,900 of the 5,528 lines are a second settings UI for settings Windhawk already renders (and renders with localization, validation and per-setting descriptions). The recommendation is to drop the dashboard and the tray's setting toggles entirely and leave Windhawk's settings page as the single source of truth — that also removes items 2 and the dashboard half of the optional findings below. If you want to keep it, Windhawk's page has to stay authoritative: a settings change should win over the stored overrides, not be discarded by them.

2. The dashboard binds zone configurations to the wrong monitor, and only ever saves one of them.

Two separate defects in the same path, both reachable in ordinary use:

  • DashLoadZones seeds from g_settings.monitorConfigs[sel] where sel is the combo box index (0 = All monitors ( * ), 1 = first display, …). That index has nothing to do with which monitor a config entry is bound to. If your Windhawk settings contain one entry bound to Dell U2720Q, it is monitorConfigs[0], so it is shown under All monitors ( * ) — and pressing Save writes it back with id = "*", silently promoting a single-display configuration to one that fires on every display. Seed by matching monitorId against the selected entry instead of by index.

  • Switching the Monitor combo runs DashLoadZones straight away (WM_COMMAND / CBN_SELCHANGE), which reloads all twelve action combos, args and s->tuning without capturing what was on screen. Every edit made for the previous display is discarded with no warning. DashSave then writes only s->cfgIndex, so a two-monitor setup cannot be configured in one session at all. Either capture the current slot into an in-memory array on CBN_SELCHANGE and write every slot on Save, or save the slot before switching.

3. SendMessageTimeoutW(HWND_BROADCAST, ...) can block the worker thread for seconds.

BroadcastSysCommand is used by Turn Off Monitors, Start Screen Saver and Lock and Turn Off Monitors. For a broadcast, the uTimeout value applies to each window separately, so a desktop with a few dozen top-level windows and one or two that are slow to pump messages can hold the worker thread for many seconds — during which queued actions do not run and the mod looks stuck. Existing mods post instead of sending:

Better still, you already own two top-level windows that run DefWindowProc (g_hDetectWnd, g_hTrayWnd). One window reaching DefWindowProc with SC_MONITORPOWER / SC_SCREENSAVE is all the power manager needs, so SendMessageW(g_hTrayWnd, WM_SYSCOMMAND, SC_MONITORPOWER, 2) does the job without running the handler in every process on the desktop.

4. Drop the VerboseLogging setting and the tray toggle.

The premise stated in the setting description and the README ("these logs go through OutputDebugString, which takes a system-wide lock, so logging on every trigger can stutter other Windhawk mods") does not hold: Wh_Log compiles down to a cheap if (g_logsOn) { ... } and mod logging is off by default, so a Wh_Log call on a hot path costs a predicated branch and never reaches OutputDebugString in production. Windhawk already provides the per-mod enable/disable switch and prefixes the mod name, so a mod-level log-level setting is duplicated machinery. Remove VerboseLogging, the IDM_VERBOSE tray item, the kOvrVerbose value, the g_verboseLog checks, and the paragraphs about it in the README/changelog, and just call Wh_Log unconditionally.

5. No screenshot in the README.

The mod is largely invisible by nature, but the dashboard with its live zone preview is exactly the kind of thing a screenshot sells, and a short GIF of a corner firing would make the concept obvious at a glance. Only i.imgur.com and raw.githubusercontent.com are allowed image hosts.

6. Overlap with an existing mod.

edge-hot-corner-desktop-switch does exactly one thing this mod also does — left/right edge hover switches virtual desktop — so this submission is a strict superset of it. That is not a duplicate in the problematic sense (the scope here is far wider, and the trigger model is different from hotcorner-hotkeys, which dispatches on a hotkey rather than on hover), but it is worth a line in the README so users know which one to pick.

Optional improvements

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

  • The dashboard thread is missing from the teardown accounting. WhTool_ModUninit carefully waits for the tray, detection and worker threads and leaks the sync objects if any of them refused to stop — but g_hDashThread is never signalled, never waited on, and never folded into allStopped. If the dashboard is open and the user hits Save and Apply while the mod is being unloaded, DeleteCriticalSection(&g_settingsLock) can run while DashSaveLoadSettings() is inside it. ExitProcess(0) follows immediately so the window is tiny, but the same "leak rather than free from under a live thread" rule you applied elsewhere should cover this thread too: post WM_CLOSE to g_hDashWnd (or WM_QUIT to the thread), wait, and clear allStopped on timeout.

  • OpenDashboard ignores the wait result. WaitForSingleObject(g_hDashThread, 0) is called and its return value discarded, then CloseHandle runs unconditionally. A dashboard thread that has destroyed its window but not yet returned loses its handle, and a second thread is started against it. Check for WAIT_OBJECT_0 before closing, and leave the handle alone otherwise.

  • Dead fields. ZoneSet::activationDelay, settleMs, knockWindowMs, requireModifier and cooldownMs are filled in by BuildZoneSet but never read anywhere — the detection loop uses the per-zone HitZone copies, which is the point of resolving them at build time. HitZone::size is likewise never assigned or read (sizeOf reads ZoneConfig::tuning.size directly). Removing them makes the snapshot's contract clearer.

  • kMaxGuiConfigs (8) vs. the monitor combo. The combo has one entry per display plus the wildcard, and DashSave writes GuiKey(sel, ...) for whatever index is selected, while ApplyDashboardZones only reads slots 0..7. With seven or more displays the dashboard silently writes values nothing ever reads.

  • The tool-mod boilerplate has drifted from the wiki snippet. It has been reformatted to the mod's brace style, Wh_Log(L">") was dropped from EntryPoint_Hook, the GetModuleFileName result check was rewritten, and the commandLine buffer is sized MAX_PATH * 2 instead of the computed size. Keeping it a verbatim copy of the wiki snippet (as explorer-folder-hover-menu.wh.cpp does) makes it diffable when the snippet changes. The extra CloseHandle(g_toolModProcessMutex) you added on the already-running path is a genuine improvement — worth raising upstream rather than carrying locally.

  • ThemeControl leaks a module reference per call. It falls back to LoadLibraryExW(L"uxtheme.dll", ...) and never calls FreeLibrary, unlike ApplyModernFrame right below it, and it is called ~40 times while building the dashboard. Resolve SetWindowTheme once into a static and reuse it.

  • MakeTrayIcon builds the AND mask from uninitialised memory. CreateBitmap(sz, sz, 1, 1, nullptr) leaves the bits undefined. The 32-bit colour bitmap's alpha channel is what actually drives transparency here, so it works in practice, but passing an explicitly zeroed std::vector<BYTE> costs nothing and removes the ambiguity.

  • Include what you use. _snwprintf_s / swprintf_s, _wtoi and memcmp are used directly but only reach the translation unit through <windows.h>; <cstdio>, <cstdlib> and <cstring> would make that explicit.

  • Unsynchronised scalar globals. g_verboseLog, g_showMonitorNames and g_lockBlankDelayMs are written from Windhawk's thread, the tray thread and the dashboard thread and read from the detection and worker threads. Benign on x86, but std::atomic<bool> / std::atomic<int> (as you already did for g_trayEnabled and g_suspendUntil) makes it well-defined for free.

Functionality notes

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

  • The 16 ms poll is a permanent ~62 wakeups/second. The rationale for not using WH_MOUSE_LL is sound, and raw input with RIDEV_INPUTSINK isn't a clean substitute either — it only fires on physical device input, so a cursor moved by SetCursorPos from another app would be missed, which polling catches. So there's no obviously better mechanism. It is worth considering an adaptive rate though: when the cursor is far from every zone rectangle (which is nearly all of the time), sampling at 50–100 ms costs nothing in responsiveness because a zone cannot be entered without first crossing that distance, and tightening back to 16 ms once it's within, say, 200 px of a zone would keep the feel identical while cutting the idle wakeup rate by 3–6×. That matters on battery.

  • The fullscreen guard is desktop-wide, not per-monitor. IsFullScreenAppActive looks only at the foreground window, so a fullscreen game on monitor 1 suppresses every zone on monitor 2 as well. Comparing MonitorFromWindow(hFgWnd, ...) against the monitor the triggering zone belongs to would let the other displays keep working; that would mean carrying the HMONITOR (or the monitor rect) on HitZone, which you already build per monitor.

  • Alternating actions flip independently on each half of a split edge. When an edge has a centre zone configured, addEdge calls add(edge, ...) twice, and for AlternateKeypress / AlternateCommand each add builds a fresh executor with its own useSecond flag. So a top edge with a centre assigned alternates separately on its left and right halves — first trigger on the left fires A, first trigger on the right also fires A. The two halves also get independent cooldown slots, and both log under the same label. Sharing one shared_ptr<bool> across the two add calls for a given edge would make the alternation behave as one zone.

  • The suppression gates run after the trigger is committed. DetectTick sets g_lastFireTick[idx], g_lastAnyFireTick and g_firedThisEntry before enqueueing, and the fullscreen / excluded-process checks run later on the worker thread. A suppressed trigger therefore still consumes the 250 ms global floor and the zone's cooldown. Harmless, just slightly surprising if you ever chase a "why did the next corner not fire" report.


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 5, 2026
@DhakadG DhakadG changed the title Add Win-X Hot Corners 4.0.3 Add Win-X Hot Corners 4.1.1 Aug 5, 2026
@DhakadG

DhakadG commented Aug 5, 2026

Copy link
Copy Markdown
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 5, 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.


The detection core is in good shape — the immutable ZoneSet snapshot, the single-transaction ReloadConfig, the disjoint-zone clamping rule and the per-monitor DPI pinning are all sound, and the reasoning left in the comments is genuinely useful. The findings below are almost entirely about the configuration story.

1. Removing the Windhawk settings page is the wrong trade, and the "700 lines of YAML" premise doesn't hold.

Your reasoning for one source of truth is correct — a mod cannot write its own settings, so a dashboard that edits settings and a settings page that also edits them will always diverge. But the conclusion only follows if the settings block really has to be a static 12-zones × 8-displays tree. It doesn't. The idiomatic shape for this is an array of groups: one entry per zone the user actually configures, with monitor, zone, action, args and the six per-zone overrides as fields, read as zones[%d].monitor, zones[%d].action, … The closest example is the mod your own readme names as related — hotcorner-hotkeys expresses region × action × args, with a 22-entry action dropdown, in 57 lines of YAML. Sketch:

- zones:
  - - monitor: "*"
      $name: Monitor
      $description: The display's name as printed in the log, or * for all displays.
    - zone: TOP_LEFT
      $name: Zone
      $options:
      - TOP_LEFT: Top-left corner
      # ... 11 more
    - action: ACTION_NOTHING
      $name: Action
      $options:
      - ACTION_NOTHING: Nothing
      # ... the same list kActionIds already holds
    - args: ""
      $name: Argument / command
    - size: -1
      $name: Size override (-1 = inherit)
    # ... the other five overrides
  $name: Zones
  $description: Add one entry per zone you want to use.

That is one screen of YAML, it scales to any number of displays (dropping kMaxGuiConfigs), and the user only sees the zones they created rather than 96 rows of Nothing.

What the current arrangement costs users:

  • Nothing about the mod is reachable from Windhawk's own UI, and the settings tab on windhawk.net will be empty — a user who installs this and doesn't read the readme has a mod with no visible way to configure it. The only entry point is a tray icon, which Windows puts in the hidden overflow area by default.
  • No localization for setting names and descriptions ($name:xx-XX / $description:xx-XX / $options:xx-XX), and no Windhawk settings backup/export of the configuration.
  • Roughly 1,900 of the 5,120 lines are a re-implementation of what Windhawk renders for free, including its own theming, DPI, tooltip and layout handling. The changelog in this PR is a fair record of what that costs to maintain: hard-coded dark theme, unreadable tooltips, a leaked icon per open, a hover card drawn under the controls, a crash when unloading with the window open, and finding 2 below.

Recommendation: restore ==WindhawkModSettings== in the array-of-groups form above as the only store, and drop the dashboard (the tray icon can stay for the enable/suspend toggles, which are runtime state rather than settings, and can open Windhawk's settings page). This is ultimately the human maintainer's call, so if you want to keep the dashboard, say so and let them weigh in — but please make the case against the array form specifically rather than against the flat form, since the flat form is not what would be written.

2. The dashboard binds each stored zone configuration to a combo-box position, not to a monitor — so changing your display setup silently moves configurations onto the wrong screen.

DashLoad fills the monitor combo from snap->monitorNames, which is g_monitors order (primary first, then left-to-right — RefreshMonitors, line 1181). DashFillSlotFromStore(s, index) then reads store keys g<index>.* using that combo index directly (line 3978), and DashSave writes slot sel back to g<sel>.*, stamping g<sel>.id with whatever name is at combo position sel (lines 4139-4140). Nothing ever checks that the stored id matches the display the slot is being shown as.

The runtime resolution (ResolveZone) is name-based, so the readme's claim that "rearranging your desktop or changing which display is primary never reshuffles your configuration" is true for detection — but the editor breaks it. Concretely, with displays Dell and BOE configured (store g1.id=Dell, g2.id=BOE):

  • Make BOE primary, or unplug Dell. The combo becomes [*, BOE, …], so combo index 1 is now BOE — and selecting it loads g1, i.e. Dell's zones, displayed as BOE's.
  • Press Save and Apply: g1.id is rewritten to BOE with Dell's zones. Dell's configuration is gone, BOE now fires Dell's actions, and g2 is left as a second entry also claiming BOE.

Plugging in a display that sorts before an existing one has the same effect, shifted the other way. This is the same class of bug the 4.1.0 changelog says was fixed — removing the settings-page seeding fixed the seed, but the slot↔store addressing is still positional.

If the dashboard stays, address slots by identity: when filling combo entry i, scan the store's g0..g7 for the slot whose id matches DashSlotMonitorId(s, i) (case-insensitively), remember that store index on the Slot, and have DashSave write each slot back to the index it was read from, allocating a free index only for genuinely new entries.

3. ShellExecuteEx runs on a thread with no COM apartment and no message loop.

ActionStartProcess (line 1779) and ActionTaskManager (line 1619) both call ShellExecuteEx from ActionWorkerThread, which never calls CoInitializeEx and has no message loop. Both matter:

  • ShellExecuteEx delegates to COM-activated shell extensions for many targets — URLs, .lnk/.url files, folders, protocol handlers — so Custom Command with https://example.com or a shortcut path can fail on an uninitialized thread, and the failure is silent apart from a log line.
  • Without a message loop, SEE_MASK_NOASYNC is required: the call can otherwise return before the operation completes, and the thread is torn down at uninit.
static DWORD WINAPI ActionWorkerThread(LPVOID) {
    CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
    // ...
    CoUninitialize();
    return 0;
}

and sei.fMask = SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC; in both callers.

4. The readme has no screenshot.

The mod's whole configuration surface is a window nobody can see before installing, and the tray icon is the only entry point. A screenshot of the dashboard (and ideally the tray menu) in the readme would do a lot of work here. Only i.imgur.com and raw.githubusercontent.com are allowed image hosts.

Optional improvements

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

  • #include <windhawk_utils.h> (line 533) is unused — nothing from WindhawkUtils:: is referenced, and the <windowsx.h> comment about needing SUBCLASSPROC for it is stale.
  • wcsrchr / wcschr / wcscpy_s come from <cwchar>, which isn't included — it currently works transitively through <windows.h>.
  • Dead code: MonitorZoneConfig::monitorIndex and the "legacy numeric ordinal" branch in ResolveZone (lines 2059-2067) can never match, because ReadDashboardZones always sets monitorIndex = 0 while mon.index is 1-based. The "(unused)" id it tests for (line 2879) is never written either.
  • DashState::hPanel is created and destroyed but never used — WM_CTLCOLORSTATIC returns hBg and WM_CTLCOLOREDIT returns hField; only the g_pal.panel colour is used, for the tooltip.
  • Three comments are prefixed ponytail: (lines 675, 1089, 3894), which looks like a leftover internal annotation.
  • The tool-mod boilerplate at the bottom has been reformatted and edited rather than pasted verbatim from the wiki snippet — the Wh_Log(L">") in EntryPoint_Hook is gone, the GetModuleFileName check is restructured, the command-line buffer is sized differently, and there's an extra CloseHandle. Behaviour is equivalent, but keeping it verbatim (as explorer-folder-hover-menu does) makes it diffable when the snippet changes.
  • The dashboard's screen preview is hard-coded dark — RGB(24, 24, 24) background and fixed zone colours (lines 3860-3875) — while the rest of the window follows the system theme.
  • Clicking the tray icon immediately after closing the dashboard silently does nothing: OpenDashboard (line 4689) bails out when the previous thread hasn't finished winding down yet, and there's no retry.
  • Out-of-range numbers typed into the dashboard are discarded without feedback — DashSave stores whatever _wtoi produced, and ReloadConfig's pull() range checks then reject it, so e.g. a corner size of 9999 reverts to 6 on the next open with nothing said.
  • ActionLockAndMonitorsOff uses Sleep() for up to 10 s on the worker thread (line 1660). WaitForSingleObject(g_hStopEvent, delay) would let it abandon the wait on unload instead of pushing WhTool_ModUninit into its 3-second timeout path.
  • UpdateTrayIcon is called from three threads (tray, dashboard via DashSave, and Windhawk's thread via WhTool_ModSettingsChanged) and mutates the non-atomic g_trayUseGuid.
  • ProcessNameOfWindow (line 1244) and the inline block in IsForegroundAppExcluded (lines 1396-1414) are the same OpenProcess + QueryFullProcessImageNameW code twice.
  • The zone Argument / command edits have no EM_SETLIMITTEXT, so anything past 511 characters is silently truncated by GetWindowTextW(buf, 512) in DashCaptureSlot.

Functionality notes

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

  • The 16 ms tick is closer to 31 ms in practice. WaitForSingleObject(g_hStopEvent, 16) expires on a timer tick, and the default resolution is 15.625 ms, so a 16 ms timeout normally wakes at ~31 ms — the mod samples at roughly 32 Hz, not the ~62 Hz the readme advertises. Worth measuring before you rely on the number. If you want event-driven detection without the WH_MOUSE_LL problems you correctly describe, raw input is the middle ground: a RIDEV_INPUTSINK mouse registration on the detection window delivers WM_INPUT off the input path, with no LowLevelHooksTimeout exposure and no wakeups at all while the mouse is still. The trade is a message per report on a high-polling-rate mouse, so it's a judgement call rather than an obvious win.
  • A cooldown consumes the entry. In DetectTick, both the per-zone cooldown (line 2521) and the 250 ms global floor (line 2531) set g_firedThisEntry = true. So walking into a corner shortly after any other zone fired, and then parking there, never fires — you have to leave and come back. Returning without setting the flag would let the dwell simply outlast the cooldown, which is probably what a user expects.
  • TopologyChanged only watches the primary monitor's work area. SPI_GETWORKAREA (line 2294) returns the primary display's work area, so with Keep zones off the taskbar enabled, moving or auto-hiding a taskbar on a secondary display won't trigger a rebuild and that display's zones stay at the old coordinates. Handling WM_SETTINGCHANGE with SPI_SETWORKAREA on the detection window would cover it.
  • WM_COMMAND 407 to Shell_TrayWnd (line 1559) is an undocumented private message being sent to a window the mod doesn't own; the value is whatever that class currently happens to use. Win+D through the existing SendKeys path, or IShellDispatch::ToggleDesktop through Shell.Application, would both survive a shell change that this won't.
  • Displays beyond the eighth can't be configured at all (kMaxGuiConfigs, line 2841) — the log line in DashSave is good, but the array-of-groups settings form in item 1 would remove the cap entirely.


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 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-for-author The author's turn: request an AI review, or respond to one that was posted.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants