Skip to content

Add Explorer Info Bar+ - #5229

Merged
m417z merged 12 commits into
ramensoftware:mainfrom
digart11:explorer-info-bar
Sep 1, 2026
Merged

Add Explorer Info Bar+#5229
m417z merged 12 commits into
ramensoftware:mainfrom
digart11:explorer-info-bar

Conversation

@digart11

@digart11 digart11 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Adds a customizable information bar to Windows 11 File Explorer with drive free space, folder/file totals, selection details, and optional single-file metadata.

Explorer Info Bar+ is designed as a broader, modern information bar for the native Windows 11 Explorer bottom area rather than as a restoration of the classic status bar or a metadata-only extension.

It combines several information groups in one configurable overlay:

  • Drive free space
  • Current folder totals and direct file size
  • Selection counts folder,files and selected file size
  • Literal file extension, preserving the actual extension as shown on disk
  • Basic file metadata such as image/video dimensions and media duration when available

The bar can also be customized with section visibility and order, multiple visual styles, automatic or custom colors, and separate styling for each information group.

This differs from Classic Explorer Status Bar, which restores a classic-style status bar and focuses on traditional status information, and Explorer Status Bar Metadata, which focuses primarily on single-file metadata. Explorer Info Bar+ combines status information, selection information, real file-extension display, and basic metadata into one customizable modern info bar.

Includes few styles, fully customizable: Simple, Flat panes, and Soft cards

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.

@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 the waiting-for-author The author's turn: request an AI review, or respond to one that was posted. label Aug 26, 2026
@digart11

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 26, 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 feature idea is nice and the mod is well-structured internally (per-window state, snapshot-under-lock discipline, DPI scaling, no persistent system changes). The problems are concentrated in how the mod finds the status row, how much work it does per poll, and its unload path.

1. The mod only works on English Windows. ContainsItemToken (line 614) is the sole trigger for the whole mod: nothing happens until Explorer draws a string containing the literal ASCII substring item. On a German ("45 Elemente"), French ("45 éléments"), Russian, Chinese, … Explorer, that never matches, so the DirectUIHWND is never subclassed and the mod silently does nothing at all. The maintainer has rejected this exact pattern before (string-matching the status-bar text and then trying to cover every language) as a workaround rather than a fix.

The same heuristic also produces false positives in the other direction: any DrawTextW call whose format is exactly DT_NOPREFIX|DT_SINGLELINE|DT_VCENTER and whose text contains "item" sets g_statusSourceDc/g_statusRowRect — e.g. a file named item.txt rendered in the list view. PaintFinalInfoBar then takes that rect as the status row; the only sanity check (lines 3363-3368) rejects a rect below the client area, so a rect in the middle of the window passes and the bar is painted in the wrong place.

For a language-independent hook point, see mods/explorer-status-metadata.wh.cpp — it hooks PSFormatForDisplayAlloc in propsys.dll (the function Explorer calls to format the status-bar size) precisely because the previous DrawTextW text-matching approach was language-dependent and false-positive-prone. Whatever you pick, please also validate the captured rect actually sits at the bottom of the client area before using it.

2. Reconsider the global DrawTextW + BitBlt hooks. Both are hooked process-wide in explorer.exe (lines 4593, 4616), so every GDI text draw and every blit in the shell — taskbar, desktop, every Explorer window — runs through mod code, and every DrawTextW call with count < 0 pays a wcslen. You don't actually need BitBlt to find the window: the target is structurally reachable as CabinetWClassShellTabWindowClassDUIViewWndClassNameDirectUIHWND, which is essentially what ActivateExistingExplorerProc already walks (line 4442). Discover it that way at init, and pick up new windows with SetWinEventHook(EVENT_OBJECT_CREATE) or a CreateWindowExW hook, and both global GDI hooks can go away.

3. Unloading the mod can hang Explorer indefinitely. The teardown loop in Wh_ModBeforeUninit (lines 4769-4893) has no overall bound: if SendMessageTimeoutW keeps failing (SMTO_ABORTIFHUNG returns immediately for a hung UI thread) or the handler keeps returning 0, the outer while (true) spins on Sleep(25) forever, and so does the while (IsWindow(hwnd)) barrier loop. Since Wh_ModBeforeUninit blocks the unload, that hangs the mod (and Windhawk's mod management) permanently.

This machinery is a re-implementation of WindhawkUtils::RemoveWindowSubclassFromAnyThread. Please use WindhawkUtils::SetWindowSubclassFromAnyThread / RemoveWindowSubclassFromAnyThread instead and delete the custom removal message, the timeout retries and the WM_NULL barrier — the standard pattern is a snapshot under the lock, then removal outside it, e.g. mods/click-on-empty-explorer.wh.cpp#L979-L998.

Related: Wh_ModUninit does WaitForSingleObject(g_workerThread, INFINITE) (line 4914). The worker can be blocked inside a marshaled COM call to an Explorer UI thread at that moment; CoCancelCall only cancels a call already in flight, so if the cancel lands between calls the wait can still block for as long as that UI thread does.

4. Every 500 ms poll re-enumerates the whole selection through cross-apartment COM. The IShellBrowser is registered into the GIT from Explorer's UI apartment, so what the worker holds is a proxy — every call marshals back to that UI thread. Per poll, per window, that is QueryActiveShellView + QueryInterface + GetFolder + 2× GetDisplayName + ItemCount + GetSelection + GetCount, and then GetItemAt + GetDisplayName for every selected item (lines 2629-2712) plus a local GetFileAttributesExW each. Nothing about the selection is cached. Select a few thousand files (Ctrl+A in a large folder) and the mod issues thousands of blocking round-trips onto Explorer's UI thread twice per second, plus thousands of GetFileAttributesExW calls — network round-trips on a UNC share.

Please gate the expensive path: cache the selection result and only re-enumerate when the selection actually changed, cap (or skip) per-item enumeration above some count, and ideally drive the update from an event (SetWinEventHook(EVENT_OBJECT_SELECTIONCHANGE) on the view, or the shell view's own change notifications) rather than a fixed 500 ms timer. The PKEY_Image_* / PKEY_Video_* / PKEY_Media_Duration reads in BuildSingleFileDetails go through the same proxy, so the property handler runs on Explorer's UI thread too — explorer-status-metadata puts that behind an explicit network-drive opt-in for exactly this reason.

5. A full directory scan of the current folder runs every 30 s, per window, forever. kContentSafetyRescanMs (line 163, used at lines 2507-2510) forces a fresh FindFirstFileEx walk of the whole folder even when nothing changed. On a large folder or a network share that's continuous background I/O for as long as the window is open. The item-count comparison already catches additions and removals; either drop the unconditional rescan or make it much rarer, and consider SHChangeNotifyRegister to invalidate the cache on change instead. Skipping the scan on non-fixed drives (or making it opt-in there) would also be worth doing.

6. Please describe how this differs from the existing status-bar mods. Classic Explorer Status Bar already shows free disk space, item/selection counts and total selected size, and Explorer Status Bar Metadata already shows single-file metadata (dimensions, duration, type) in the same status bar. The presentation here is genuinely different — a styled overlay on the native Windows 11 info bar rather than a classic status-bar control — but the "Show Single File Details" section in particular overlaps closely with the latter. Please state the difference in the PR description, and consider whether that section is better left to the existing mod (or contributed there as an option).

Optional improvements

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

  • Use WindhawkUtils::SetFunctionHook() instead of raw Wh_SetFunctionHook with reinterpret_cast<void*> (lines 4593, 4616) — it's type-safe and catches prototype mismatches at compile time. Requires #include <windhawk_utils.h>.
  • GetStringSetting (line 351) can be replaced with WindhawkUtils::StringSetting, which frees the string via RAII. Also, Wh_GetStringSetting never returns NULL (it returns L"" on error/unset), so the raw ? raw : L"" / if (raw) guards are dead code.
  • The try { … } catch (...) { … } blocks around std::vector operations (lines 1847, 1902, 2882, 4011) only catch bad_alloc; if the process is out of memory the mod isn't the thing that needs to survive. Removing them would cut a fair amount of noise.
  • The worker calls CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED) (line 2807) but never pumps messages. A worker with no message loop should be COINIT_MULTITHREADED.
  • -lshell32 in @compilerOptions doesn't appear to be used — the shell types used here are interfaces (no imports), and the property helpers come from propsys/ole32. Worth trimming the list (and #include <shlobj.h>) to what's actually needed.
  • The partial-invalidation machinery (InfoBarLayoutGeometry, CacheChangeFlags, the layout-match check in RefreshInfoBarWindow, ~250 lines total) exists to avoid repainting a 24 px strip. Unless you measured a real win, invalidating the whole row would be a lot less code to maintain.
  • ActivateExistingExplorerContext wraps a single DWORD that is already available as the global g_pid — the struct and the LPARAM round-trip can go.
  • driveTotalBytes is computed, threaded through UpdateCache's parameter list, and then only used as a > 0 validity flag (line 2010). Either display it or drop it.
  • Consider returning FALSE from Wh_ModInit when all four display sections are disabled — Windhawk reloads the mod after a settings change, so there's no reason to keep the hooks and subclasses resident when nothing will be drawn.
  • The one-argument-per-line formatting inflates the file to ~5000 lines for what is closer to ~2000 lines of code. Running clang-format with the repo's usual style would make the mod considerably easier to review and maintain.

Functionality notes

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

  • Background color sampling is fragile. PickBackgroundColor (line 3072) samples GetPixel at fixed x offsets 420/520/620/720 and takes the first hit. Two problems: (a) those coordinates are exactly where Explorer's own status text often is, so the sample can land on a glyph and the entire row gets filled with the text color; (b) if the window is narrower than ~640 logical px, every sample is past row.right, no stable color has been learned yet, and the fallback is the hardcoded RGB(32, 32, 32) — a dark bar on a light theme. Sampling several points and taking the most common value, or sampling just left of row.right (which is reserved and normally empty), would be more robust.
  • A folder that can't be enumerated shows "Content: Loading..." forever. ScanFilesystemDirectory returns false on e.g. ERROR_ACCESS_DENIED, so contentCache.valid never becomes true and line 2568 keeps setting Content: Loading... on every poll. Worth distinguishing "scan failed permanently" from "not scanned yet" and falling back to Explorer's own item count.
  • Counts can disagree with what Explorer shows. The displayed folder/file counts come from a raw FindFirstFileEx walk, which includes hidden and system entries, while Explorer's own item count respects the "Hidden items" setting. With hidden files present, "Content: 15 folders / 25 files" won't match the native count of the same folder.
  • No drive info on network locations. The drive section requires currentPath[1] == L':' (line 2431), so UNC paths get nothing. GetDiskFreeSpaceExW accepts UNC paths, so this could work for shares too.
  • Hardcoded font. CreateFontW(..., L"Segoe UI") at line 3496 won't match Explorer on systems where the shell font differs (CJK locales use Yu Gothic UI / Microsoft JhengHei UI, and users can change the UI font). SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, ...)lfStatusFont/lfMessageFont, or the theme font, would track the system.
  • Visible repaint of the native text. In the WM_PAINT path the subclass lets DefSubclassProc complete the full buffered paint (which blits Explorer's own status text to the screen) and only then acquires a fresh GetDC and paints over it, so each repaint briefly shows the native text. Painting into the same frame — e.g. always doing the overlay in the blit path rather than only for the first frame — would avoid the flash.
  • Hardcoded layout constants. kStatusRowHeight = 24 and the 220 px right-side reserve (line 3382) are guesses about Explorer's layout that will drift with future builds and with different Explorer configurations (e.g. the details/preview pane toggles). Deriving the reserved width from the actual right-hand controls, where possible, would age better.
  • On first paint the bar shows Content: Loading... for at least kInitialRefreshDelayMs (1 s) while covering Explorer's real status text. Leaving the native row alone until the first successful read would look better.


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 26, 2026
@digart11

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 27, 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.


Good progress since the last round: the language-dependent "item" match is gone, the custom subclass-removal machinery is replaced with WindhawkUtils::RemoveWindowSubclassFromAnyThread, the unconditional 30 s rescan is gone, per-item selection enumeration is capped, the worker is COINIT_MULTITHREADED, and the PR description now explains the relationship to the existing status-bar mods. Two of the replacements introduced new problems, though, and the polling cost and the two process-wide GDI hooks are unchanged.

1. TerminateThread on the worker is not a safe fallback (lines 4757-4762). This is the most serious item here. The 5 s timeout is reachable in normal use: the worker's longest blocking call isn't COM, it's ScanFilesystemDirectoryFindFirstFileExW / FindNextFileW (lines 2147, 2217) on a slow or unreachable network share. Neither the stop event nor CoCancelCall interrupts a blocking directory enumeration, and an SMB timeout can easily exceed 5 s — so the kill path is the expected path on a dead share, not a theoretical one.

Killing the thread there is much worse than waiting. The thread is terminated at an arbitrary instruction, so it can own g_cacheLock (entered at line 2066) — after which DeleteCriticalSection(&g_cacheLock) at line 4790 operates on a section that is still owned — or the CRT/OS heap lock, which permanently damages the whole Explorer process. Its CoInitializeEx is never balanced, so the apartment, the GIT reference and the IShellBrowser proxies leak; the find handle leaks; the thread's stack is never freed. TerminateThread is on Windhawk's never-do-this list for exactly this reason.

Please drop it and make the wait actually terminable instead:

SetEvent(g_stopEvent);
CancelSynchronousIo(g_workerThread);   // aborts a blocking FindNextFileW
WaitForSingleObject(g_workerThread, INFINITE);
CloseHandle(g_workerThread);

If you ever do need a bail-out, leak the thread handle rather than kill the thread — mods/explorer-nav-dragover-fix.wh.cpp#L2453-L2465 spells out the same trade-off.

Related, in the same area: CoCancelCall(g_workerThreadId, 0) at line 4745 runs before the join, and the stop event was already set back in Wh_ModBeforeUninit, so by then the worker has usually exited and its thread id may have been recycled by another Explorer thread — you'd be cancelling an unrelated component's outbound COM call. The HRESULT isn't checked either, and on the arbitrary thread Wh_ModUninit runs on, COM may not be initialized at all, in which case the cancel does nothing. Cancel once in Wh_ModBeforeUninit and don't treat it as the only unblocking mechanism.

2. The painter uses g_statusRowRect even when the BitBlt validation rejected it. DrawTextW_Hook (line 4188) now records the rect of every DrawTextW call in explorer.exe whose format is exactly DT_NOPREFIX|DT_SINGLELINE|DT_VCENTER (kNativeStatusTextFormat, line 153). That combination is extremely common and not specific to the status row, which is fine by itself — the comment says the decisive check is the mapping in BitBlt_Hook (lines 4262-4282). But when that check fails, the hook simply returns and leaves g_statusRowRect holding the rejected rect, and PaintFinalInfoBar reads it unconditionally (line 3351). Its only sanity check (lines 3354-3368) rejects a rect whose bottom is below the client area — so any rejected rect that happens to sit inside the client area is accepted as the status row, and the FillRect at line 3464 plus the text land in the middle of the file list.

The failure is concrete: during one WM_PAINT, DefSubclassProc lets DirectUI paint its whole tree into the buffer DC, and every text element drawn with that flag combination overwrites the marker (g_insideFinalPaint is only set later, around your own painting). Only the last rect before the blit gets validated. If that last element isn't the status text, validation fails, and the bar is drawn at that element's y position instead.

Fix: never let the painter read an unvalidated rect. Store the mapped rect only after it passes the check, together with the window it was validated for — e.g. next to stableRowBackground in TrackedDirectUiState — and have PaintFinalInfoBar use that, falling back to the geometric row you already compute at lines 3361-3367. Keying it per window also fixes a second problem: g_statusRowRect is thread_local, and on Windows 11 all tabs of one Explorer window share a thread, so one tab's rect can be applied to another tab's DirectUIHWND.

3. user32!DrawTextW and gdi32!BitBlt are still hooked process-wide. Every GDI text draw and every blit in explorer.exe — taskbar, desktop, every Explorer window — goes through mod code. The per-call cost is much lower than before, so this is now an architecture point rather than a hot-path one, but both hooks exist only to (a) learn the row rect and (b) reach the DirectUIHWND on its own UI thread. The target is structurally reachable — CabinetWClassShellTabWindowClassDUIViewWndClassNameDirectUIHWND — which is exactly what IsExplorerDirectUiTarget (line 2960) and ActivateExistingExplorerProc (line 4380) already walk. Discover it there, pick up new windows with a CreateWindowExW hook or SetWinEventHook(EVENT_OBJECT_CREATE), and derive the row geometrically. Both global hooks go away, and so does the entire class of bug in the previous item. If you want a text-anchored rect after all, mods/explorer-status-metadata.wh.cpp#L106 hooks PSFormatForDisplayAlloc in propsys.dll, which is specific to the status bar rather than to a flag combination.

4. Selection and folder state are still re-read from scratch twice a second. With nothing selected, each 500 ms poll makes roughly eight blocking cross-apartment calls per window (QueryActiveShellView, QueryInterface, GetFolder, two GetDisplayName, ItemCount, GetSelection, GetCount) that all marshal onto that window's UI thread. With a selection it adds GetItemAt + GetDisplayName per item (lines 2604-2624) — up to the 256 cap, so up to ~1000 marshaled round-trips per second per window — plus a GetFileAttributesExW per item (line 2635), which on a UNC path is a network round-trip per selected file, twice a second, for as long as the window is open.

Nothing is cached between polls, so an unchanged selection is fully re-enumerated every time. Please (a) skip the work when nothing changed, and (b) drive the update from an event rather than a fixed timer — SetWinEventHook(EVENT_OBJECT_SELECTIONCHANGE, ..., g_pid, 0, WINEVENT_OUTOFCONTEXT) gives you the signal and you already have g_workerWakeEvent to hand the work to the worker. Note that the count alone can't serve as a change token (arrow-keying between two files keeps it at 1). explorer-status-metadata manages the single-file case with zero polling, which is worth looking at for comparison.

5. The README lost its screenshot. The preview image was removed in the second commit. That wasn't necessary: raw.githubusercontent.com is an allowed image host, and a branch-pinned URL like the one you had is fine — Windhawk mirrors the image after merge. For a mod whose entire point is a visual change, a screenshot (ideally one per style: Simple, Flat panes, Soft cards) is the most useful thing the README can carry. Please restore it.

Optional improvements

Minor polish — none of this affects users, so it's your call. Several of these carry over from the previous round.

  • The README examples lost their ```text fences, so the sample lines will now render as one run-together paragraph on windhawk.net. The × character was also replaced with &times;, which isn't needed — CI only requires the file to be valid UTF-8 without a BOM, and literal non-ASCII is used throughout the repo. Restoring both would render better.
  • Use WindhawkUtils::SetFunctionHook() instead of raw Wh_SetFunctionHook with reinterpret_cast<void*> (lines 4524, 4547) — it's type-safe and catches prototype mismatches at compile time.
  • GetStringSetting (line 341) can be replaced with WindhawkUtils::StringSetting, which frees the string via RAII. Wh_GetStringSetting never returns NULL (it returns L"" on error/unset), so the raw ? raw : L"" / if (raw) guards are dead code.
  • The try { … } catch (...) { … } blocks around std::vector operations (lines 1859, 2841, 4001, 4701) only catch bad_alloc. Worth noting that the one in Wh_ModBeforeUninit (line 4707) is actively harmful: on an allocation failure it clears the list and skips subclass removal entirely, leaving subclass procs pointing into the unmapped image — a guaranteed crash later. Letting it throw would be better; dropping the blocks altogether would be better still.
  • -lshell32 in @compilerOptions and #include <shlobj.h> don't appear to be used — the shell types here come from shobjidl.h (interfaces, no imports) and the property helpers from propsys/ole32.
  • The partial-invalidation machinery (InfoBarLayoutGeometry, CacheChangeFlags, the layout-match check in RefreshInfoBarWindow, ~250 lines) exists to avoid repainting a 24 px strip. Unless you measured a real win, invalidating the whole row would be a lot less code to maintain.
  • ActivateExistingExplorerContext (line 4328) wraps a single DWORD that's already available as the global g_pid — the struct and the LPARAM round-trip can go.
  • driveTotalBytes is computed, threaded through UpdateCache's parameter list, and then only used as a > 0 validity flag (line 1967). Either display it or replace it with an explicit bool driveInfoValid.
  • Consider returning FALSE from Wh_ModInit when all four display sections are disabled — Windhawk reloads the mod after a settings change, so there's no reason to keep the hooks and subclasses resident when nothing will be drawn.
  • The one-argument-per-line formatting still inflates the file to ~4800 lines for what is closer to ~2000 lines of code. Running clang-format with the repo's usual style would make it considerably easier to review and maintain.

Functionality notes

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

  • A folder that can't be enumerated shows "Content: Loading..." forever, and retries every 2 s. ScanFilesystemDirectory returns false on e.g. ERROR_ACCESS_DENIED, so contentCache.valid never becomes true, line 2519 keeps setting Content: Loading..., and because sameFolder stays false the scan is re-attempted every kContentFailedRetryMs for as long as the window is open — continuous retry I/O on an unreachable share. Worth distinguishing "scan failed permanently" from "not scanned yet" and falling back to Explorer's own item count.
  • Counts can disagree with what Explorer shows. The folder/file counts come from a raw FindFirstFileEx walk, which includes hidden and system entries, while Explorer's own count respects the "Hidden items" setting. With hidden files present, "Content: 15 folders / 25 files" won't match the native count for the same folder.
  • Above 256 selected items the size total silently disappears. The cap is a good call, but the fallback text (line 2686) drops to "Selected: N items" with no byte total, which is the case where users most want it. IShellItemArray's per-item enumeration is the expensive part, not the sizes — if you keep a per-folder size map from the directory scan you already do, you could sum locally without any marshaling.
  • No drive info on network locations. The drive section requires currentPath[1] == L':' (line 2389), so UNC paths get nothing. GetDiskFreeSpaceExW accepts UNC paths, so this could work for shares too.
  • Background color sampling is fragile. PickBackgroundColor (line 3063) samples GetPixel at fixed x offsets 420/520/620/720 and takes the first hit. Those coordinates are roughly where Explorer's own status text sits, so a sample can land on a glyph and fill the whole row with the text color; and on a window narrower than ~640 logical px every sample is past row.right, so with no stable color learned yet the fallback is the hardcoded RGB(32, 32, 32) — a dark bar on a light theme. Sampling several points and taking the most common value, or sampling just left of row.right (normally empty), would be more robust.
  • Hardcoded font. CreateFontW(..., L"Segoe UI") at line 3502 won't match Explorer where the shell font differs (CJK locales use Yu Gothic UI / Microsoft JhengHei UI, and users can change the UI font). SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, ...)lfStatusFont/lfMessageFont would track the system.
  • Visible repaint of the native text. In the WM_PAINT path the subclass lets DefSubclassProc complete the full buffered paint — which blits Explorer's own status text to the screen — and only then acquires a fresh GetDC and paints over it, so each repaint briefly shows the native text. Painting into the same frame would avoid the flash.
  • Hardcoded layout constants. kStatusRowHeight = 24 and the 220 px right-side reserve (line 3374) are guesses about Explorer's layout that will drift with future builds and with different Explorer configurations (details/preview pane toggles). Deriving the reserved width from the actual right-hand controls would age better.
  • On first paint the bar shows Content: Loading... for at least kInitialRefreshDelayMs (1 s) while covering Explorer's real status text. Leaving the native row alone until the first successful read would look better.
  • The dividerColor setting is named "Divider / border color" but the Simple style's separator uses automaticDividerColor (lines 3648, 3675) and ignores the override — only Soft Cards borders honour it. The $description says so, but applying it to the Simple separator too would match the name.


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 27, 2026
@digart11 digart11 changed the title Add Explorer Info Bar mod Add Explorer Info Bar+ Aug 27, 2026
@digart11

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 27, 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.


Good round: TerminateThread is gone and replaced with CancelSynchronousIo + an unbounded join, the status-row rect is now validated per window before the painter can use it, the selection enumeration is cached behind a change token, the polling interval is 10 s instead of 500 ms, Wh_ModSettingsChanged is the void form, the harmful catch in Wh_ModBeforeUninit is gone, and the README screenshot and code fences are back. Two things remain: the new SetWinEventHook is installed on the wrong thread, and the two process-wide GDI hooks are unchanged.

1. The selection SetWinEventHook is installed and removed on the wrong thread (lines 4779-4788, 4889). SetWinEventHook is called from Wh_ModInit and UnhookWinEvent from Wh_ModBeforeUninit. Per the mod lifetime, Wh_ModInit runs on the target's main thread only when the mod is loaded before the process starts; everything from Wh_ModAfterInit onward — including Wh_ModBeforeUninit — runs on the Windhawk engine thread. That splits into two failure modes, and both are reachable in normal use:

  • Mod enabled/updated on a running ExplorerWh_ModInit runs on the engine thread. WINEVENT_OUTOFCONTEXT delivers callbacks to the thread that installed the hook, and only while that thread pumps messages — the engine thread doesn't, so SelectionWinEventProc (line 1952) never fires and g_selectionGeneration never advances.
  • Mod loaded at Explorer startup → the hook is installed by Explorer's main thread and works, but UnhookWinEvent is then called from the engine thread. MSDN lists "UnhookWinEvent is called from a thread that is different from the original call to SetWinEventHook" as one of the three documented failure causes, so the hook survives the unload and the next selection event in explorer.exe dispatches into the unmapped mod image — an Explorer crash on mod disable/update.

The dead-hook case also silently breaks a headline feature. selectionDirty (lines 2716-2719) is generation != cached || selected != cached.selected || folderChanged. With the generation frozen, arrow-keying from one file to another keeps the count at 1 and the folder unchanged, so selectionDirty stays false, the selection is never re-enumerated, and the info bar keeps showing the previous file's extension, size and metadata for as long as the window is open. (WakeWorkerFromPaint wakes the worker, but the worker then decides there's nothing to do.)

Fix: host the hook on a dedicated thread that owns it end to end — create the thread in Wh_ModInit, call SetWinEventHook on it, run a message loop, and UnhookWinEvent on that same thread before it exits; signal it and join it in Wh_ModUninit. Two in-repo examples: mods/explorer-nav-dragover-fix.wh.cpp#L1921-L2000 (event-based MsgWaitForMultipleObjectsEx loop, with the reasoning spelled out in the comments) and mods/explorer-treeitem-tweaker.wh.cpp#L2808-L2850 (simpler GetMessageW loop). Independently of the hook, please make the selection change token not rely solely on the count — comparing the focused/first selected item's path, or just re-enumerating when selectionCount == 1, would keep single-file details correct even if the hook fails to install.

2. user32!DrawTextW and gdi32!BitBlt are still hooked process-wide in explorer.exe (lines 4691-4722). Every GDI text draw and every blit in the shell — taskbar, desktop, every Explorer window — goes through mod code. The per-call cost is now genuinely small (a TLS read and a flag compare), so this is an architecture and blast-radius point rather than a hot-path one: a fault or a wrong assumption in either hook affects all of Explorer's rendering, not just the info bar.

Both hooks exist only to (a) reach the DirectUIHWND on its own UI thread and (b) learn the row rect. Neither actually requires them:

  • WindhawkUtils::SetWindowSubclassFromAnyThread is explicitly cross-thread (it marshals the install via a WH_CALLWNDPROC hook), so the "must be on the UI thread" restriction in EnsureDirectUiSubclass (lines 4096-4109) is self-imposed. EnsureShellBrowserRegistration does need the owning thread, but it already runs from WM_PAINT (line 4278), which is on that thread.
  • The target is structurally reachable — CabinetWClassShellTabWindowClassDUIViewWndClassNameDirectUIHWND — which is what IsExplorerDirectUiTarget (line 3116) and ActivateExistingExplorerProc (line 4547) already walk. New windows can be picked up with SetWinEventHook(EVENT_OBJECT_CREATE) on the helper thread from item 1, or a CreateWindowExW hook.
  • The row rect already has a geometric fallback (lines 3527-3534) that would become the only path.

If you'd rather keep a text-anchored rect, mods/explorer-status-metadata.wh.cpp#L106 hooks PSFormatForDisplayAlloc in propsys.dll — one function that is specific to the status bar, rather than a DrawTextW flag combination shared by the whole shell.

Optional improvements

Minor polish — none of this affects users, so it's your call. Several of these carry over from the previous rounds.

  • Wh_ModUninit's WaitForSingleObject(g_workerThread, INFINITE) (line 4976) is the right call now, but CoCancelCall is only issued once, back in Wh_ModBeforeUninit (line 4894). If the worker was between COM calls at that moment and then enters a new marshaled call to a wedged Explorer UI thread, nothing unblocks it. Also, CoCancelCall needs COM initialized on the calling thread, and the engine thread may not have it. Re-issuing the cancel from Wh_ModUninit right before the wait (and checking the HRESULT) would close both gaps cheaply.
  • Use WindhawkUtils::SetFunctionHook() instead of raw Wh_SetFunctionHook with reinterpret_cast<void*> (lines 4691, 4714) — cast the GetProcAddress result to DrawTextW_t / BitBlt_t and the helper gives you compile-time prototype checking.
  • GetStringSetting (line 360) can be replaced with WindhawkUtils::StringSetting, which frees the string via RAII. Wh_GetStringSetting never returns NULL (it returns L"" on error/unset), so the raw ? raw : L"" / if (raw) guards are dead code.
  • The remaining try { … } catch (...) { … } blocks around std::vector operations (lines 1823, 1989, 2997, 4167) only catch bad_alloc; if the process is out of memory the info bar isn't the thing that needs to survive. Dropping them would cut a fair amount of noise.
  • -lshell32 in @compilerOptions (line 11) and #include <shlobj.h> (line 143) don't appear to be used — the interfaces here come from shobjidl.h/objidl.h (no imports) and the property helpers from propsys/ole32.
  • The partial-invalidation machinery (InfoBarLayoutGeometry, CacheChangeFlags, the layout-match check in RefreshInfoBarWindow, ~250 lines) exists to avoid repainting a 24 px strip. Unless you measured a real win, invalidating the whole row would be a lot less code to maintain.
  • ActivateExistingExplorerContext (line 4495) wraps a single DWORD that's already available as the global g_pid — the struct and the LPARAM round-trip can go.
  • driveTotalBytes is computed, threaded through UpdateCache's parameter list, and then only used as a > 0 validity flag (line 2098). Either display it or replace it with an explicit bool driveInfoValid.
  • PaintFinalInfoBar creates and destroys an HFONT on every paint (lines 3654-3669). Caching it per DPI and recreating only on WM_DPICHANGED/settings change would be simpler than the partial-invalidation machinery it sits next to.
  • The DrawBox lambda's textColor parameter (line 3876) shadows the enclosing const COLORREF textColor; it's always passed the same value, so the parameter can just be dropped.
  • Consider returning FALSE from Wh_ModInit when all four display sections are disabled — Windhawk reloads the mod after a settings change, so there's no reason to keep the hooks and subclasses resident when nothing will be drawn.
  • The one-argument-per-line formatting still inflates the file to ~5000 lines for what is closer to ~2000 lines of code. Running clang-format with the repo's usual style would make it considerably easier to review and maintain.

Functionality notes

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

  • A folder that can't be enumerated shows "Content: Loading..." forever. ScanFilesystemDirectory returns false on e.g. ERROR_ACCESS_DENIED, so contentCache.valid never becomes true and line 2654 keeps setting Content: Loading..., retrying on every worker pass for as long as the window is open. Worth distinguishing "scan failed permanently" from "not scanned yet" and falling back to Explorer's own item count.
  • The metadata retry never fires. GetSingleFileDetailsCached sets retryAfterTick = now + kMetadataRetryMs on a transient property-read failure (line 2412), but it's only reached from inside the enumerateSelection branch, which requires selectionDirty. If the read fails while the selection is otherwise unchanged, the retry is unreachable and the details stay missing until the user selects something else.
  • Counts can disagree with what Explorer shows. The folder/file counts come from a raw FindFirstFileEx walk, which includes hidden and system entries, while Explorer's own count respects the "Hidden items" setting. With hidden files present, "Content: 15 folders / 25 files" won't match the native count for the same folder.
  • Above 256 selected items the size total silently disappears. The cap (line 2711) is a good call, but the fallback text (line 2846) drops to "Selected: N items" with no byte total, which is the case where users most want it. IShellItemArray's per-item enumeration is the expensive part, not the sizes — a per-folder size map from the directory scan you already do would let you sum locally without marshaling.
  • No drive info on network locations. The drive section requires currentPath[1] == L':' (line 2524), so UNC paths get nothing. GetDiskFreeSpaceExW accepts UNC paths, so this could work for shares too.
  • Background color sampling is fragile. PickBackgroundColor (line 3219) samples GetPixel at fixed x offsets 420/520/620/720 and takes the first hit. Those coordinates are roughly where Explorer's own status text sits, so a sample can land on a glyph and fill the whole row with the text color; and on a window narrower than ~640 logical px every sample is past row.right, so with no stable color learned yet the fallback is the hardcoded RGB(32, 32, 32) — a dark bar on a light theme. Sampling several points and taking the most common value, or sampling just left of row.right (normally empty), would be more robust.
  • Hardcoded font. CreateFontW(..., L"Segoe UI") at line 3668 won't match Explorer where the shell font differs (CJK locales use Yu Gothic UI / Microsoft JhengHei UI, and users can change the UI font). SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, ...)lfStatusFont/lfMessageFont would track the system.
  • Visible repaint of the native text. In the WM_PAINT path the subclass lets DefSubclassProc complete the full buffered paint — which blits Explorer's own status text to the screen — and only then acquires a fresh GetDC and paints over it, so each repaint briefly shows the native text. Painting into the same frame would avoid the flash.
  • Hardcoded layout constants. kStatusRowHeight = 24 (line 164) and the 220 px right-side reserve (line 3540) are guesses about Explorer's layout that will drift with future builds and with different Explorer configurations (details/preview pane toggles). Deriving the reserved width from the actual right-hand controls would age better.
  • At 10 s (kRefreshIntervalMs, line 167) the drive free-space and folder-content figures can lag noticeably behind a copy or delete happening in the same window. SHChangeNotifyRegister on the current folder would let you keep the long timer while still reacting promptly to real changes.
  • On first paint the bar shows Content: Loading... for at least kInitialRefreshDelayMs (1 s) while covering Explorer's real status text. Leaving the native row alone until the first successful read would look better.


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 27, 2026
@digart11

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 27, 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-27 18:56 UTC (in 5 hours) to get another one.

@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 27, 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-30 00:54 UTC (in 25 minutes) 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 30, 2026
@digart11

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 30, 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 mod is carefully written in a lot of places — the lock discipline (never holding g_subclassLock across SetWindowSubclassFromAnyThread), the GIT-marshalled IShellBrowser so the property lookups run off the UI thread, the per-window teardown ordering in Wh_ModBeforeUninit, and joining both worker threads in Wh_ModUninit are all correct. The findings below are mostly about cost on Explorer's UI thread and about how the mod positions itself against the existing status-bar mods.

1. The README's compatibility claim about Explorer Status Bar Metadata is wrong.

The README says (line 60) that the overlap with Explorer Status Bar Metadata is functional only and "no broader incompatibility is implied". But PaintFinalInfoBar unconditionally fills coverRow — the whole status row from client.left to client.right - 64dpi — whenever the mod has anything to show:

coverRow.right = std::max(coverRow.left, client.right - ScaleForDpi(dpi, 64));
...
FillRect(hdc, &coverRow, brush);

Explorer Status Bar Metadata works by extending Explorer's native status string (it hooks PSFormatForDisplayAlloc), so its output lives inside exactly the pixels this mod paints over. With both enabled and default settings, its metadata is invisible. That's the same hard conflict the README already documents for Classic Explorer Status Bar / PreVista Explorer Status Bar, and it should be documented the same way.

2. Overlap with the existing status-bar mods.

Related to the above: the maintainer's consistent preference is to extend an existing mod (or PR the original author's repo) rather than merge a mod that substantially overlaps one. Right now:

The genuinely new part is the combined, styled overlay on Win11's native bar (sections, ordering, panes/cards, colors), which none of the others do — that's a reasonable differentiator, but please be ready to make the case explicitly in the PR. One thing that would help a lot: default singleFileDetails to false so the mod doesn't silently take over a feature another mod already owns, and say in the README that it can be turned on if you don't use the metadata mod.

3. GetPixel background sampling re-runs on every full-row repaint.

PickBackgroundColor returns the cached color only when the repaint is partial:

if (theme.hasSampledNativeRowBackground && !fullRowRepaint)
    return theme.rowBackground;

Any repaint whose update region covers the row — window activation, resize, scroll, the g_refreshDirectUiMessage refresh, WM_WINDOWPOSCHANGED — sets fullRowRepaint and re-runs the sampling loop, up to 6 GetPixel calls. GetPixel on a DWM-redirected window DC is a read-back and costs on the order of hundreds of microseconds to milliseconds each, and this runs synchronously on the Explorer UI thread inside WM_PAINT. During a live window resize that's per frame.

The cached value is already invalidated at exactly the right moments (InvalidateAutomaticTheme on WM_THEMECHANGED / WM_SETTINGCHANGE / WM_SYSCOLORCHANGE), so re-sampling on full repaints buys nothing:

if (theme.hasSampledNativeRowBackground)
    return theme.rowBackground;

4. Per-message work in the paint and resize paths.

Three things run on the Explorer UI thread more often than they need to:

  • WM_PAINT calls EnsureShellBrowserRegistration(hwnd) (line 5194). Once registration succeeds this is a cheap lookup, but until it does — a tab whose ShellTabWindowClass isn't ready yet, or a window that's been untracked — every single paint does FindAncestorByClass + SendMessage(CWM_GETISHELLBROWSER) + CoCreateInstance(CLSID_StdGlobalInterfaceTable) + RegisterInterfaceInGlobal (+ RevokeInterfaceFromGlobal when the store fails). COM calls inside a WM_PAINT handler are best avoided entirely; do the registration from the g_refreshDirectUiMessage handler (which is already posted at attach) and retry it from the worker's wake path instead.
  • WM_PAINT also calls EnsureWindowDataCache(hwnd) on every paint; the cache is created at attach and on WM_NCDESTROY, so this is a redundant critical-section + linear scan per paint.
  • WM_WINDOWPOSCHANGED calls RefreshValidatedStatusRow(hwnd) unconditionally (line 5167), which walks the child list with FindWindowExW twice per child plus GetWindowRect/MapWindowPoints. WM_WINDOWPOSCHANGED fires continuously during a drag/resize. IsStatusRowRevalidationDue() already exists for exactly this — gate this call with it (the geometry is re-validated inside PaintFinalInfoBar anyway).

5. The fixed 10 s poll is mostly redundant.

kRefreshIntervalMs = 10000 makes the worker re-read every visible tab forever, and each pass is a batch of cross-apartment calls (QueryActiveShellView, QueryInterface, GetFolder, two GetDisplayName, ItemCount, GetSelection, GetCount, and for a one-item selection GetItemAt + GetDisplayName) that are marshalled to — and therefore executed on — the Explorer UI thread. That's the pattern the maintainer regularly pushes back on.

Selection is already event-driven via the WinEvent hook, so the timer is really only there for free space and for folder-content changes. Both have event sources: SHChangeNotifyRegister with SHCNE_UPDATEDIR/SHCNE_CREATE/SHCNE_DELETE/SHCNE_RENAMEITEM for the current folder, and SHCNE_FREESPACE/SHCNE_MEDIAINSERTED for drive free space. At minimum, please raise the fixed interval substantially (a free-space readout doesn't need 10 s granularity) and skip the whole pass when neither the selection generation nor the paint-wake flag changed since the last one.

Optional improvements

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

  • Dead code. g_insideFinalPaint (line 177) is set and cleared around PaintFinalInfoBar but never read anywhere — remove it or the reentrancy guard it was meant for. ContentRefreshCache::lastFullScanTick (line 327) is written at line 3415 and never read.
  • WindhawkUtils::StringSetting instead of the manual wrapper. GetStringSetting (line 361) is a hand-rolled Wh_GetStringSetting + Wh_FreeStringSetting pair; WindhawkUtils::StringSetting::make(L"style") does the same with RAII. Also, Wh_GetStringSetting never returns NULL (it returns L"" on error/unset), so raw ? raw : L"" and the if (raw) guard are dead.
  • CoCancelCall on a possibly-recycled thread ID. Wh_ModBeforeUninit (line 5580) and the retry loop in Wh_ModUninit (line 5666) call CoCancelCall(g_workerThreadId, 0) without first checking whether the worker has already exited. If it has and the OS recycled the TID, this cancels an unrelated Explorer COM call. A WaitForSingleObject(g_workerThread, 0) != WAIT_OBJECT_0 check before each cancel closes that.
  • Unbounded WaitForSingleObject(..., INFINITE) in Wh_ModInit (line 5487) waiting for the WinEvent helper to signal readiness. Wh_ModInit runs before the target process starts executing; a bounded wait with a log on timeout is safer, and the helper thread's failure path is already handled gracefully.
  • try/catch (...) around vector::reserve/push_back (lines 2255, 2391, 2724, 3824, 4955, 5015). These only fire on bad_alloc, at which point Explorer is already in trouble, and they add a lot of visual weight to otherwise straightforward code. Most mods in the repo don't guard allocations this way.
  • Narrow the WinEvent range. SetWinEventHook(EVENT_OBJECT_FOCUS, EVENT_OBJECT_SELECTIONWITHIN, ...) fires on every focus change anywhere in explorer.exe, including the taskbar and tray, and each one walks a parent chain in AdvanceSelectionGenerationForWinEvent. Filtering on idObject == OBJID_CLIENT (and bailing out before FindAncestorByClass when the window's class isn't one Explorer's view uses) would cut most of that.
  • dividerColor naming. The $name is "Divider / border color (Soft Cards style only)", but the Simple style's · separators always use automaticDividerColor and Flat panes never draw a border, so the setting only ever affects Soft Cards. "Card border color" would describe it exactly.

Functionality notes

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

  • Overpainting after DefSubclassProc has inherent limits. Drawing to a GetDC after DirectUI's own buffered WM_PAINT means there's a window where the native row is on the surface and the overlay isn't — usually swallowed by DWM composition, but visible as flicker during resize/scroll. It also means the row isn't covered in WM_PRINTCLIENT captures (Task View / Alt+Tab thumbnails will show the native text), and UIA/screen readers still report Explorer's own status text, not yours. There's no clean alternative for the Flat panes / Soft cards styles, so this is an FYI rather than a change request — but for the Simple style specifically, extending the native status string (the PSFormatForDisplayAlloc approach that explorer-status-metadata uses) would inherit Explorer's font, DPI, theme and accessibility for free.
  • Hard-coded Segoe UI. CreateInfoBarFont (line 1442) always asks for Segoe UI at 12 device pixels. On non-Latin UI languages Explorer uses a different shell font (e.g. Yu Gothic UI on ja-JP, Malgun Gothic on ko-KR), so the bar will fall back rather than match, and it ignores the user's text-size setting. SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, ..., dpi, 0)NONCLIENTMETRICS::lfMessageFont gives you the right face and size for the window's DPI in one call.
  • Hard-coded right-edge reservations. client.right - 64dpi for the cover and client.right - 220dpi for the content are guesses about where Explorer's view-mode buttons sit. 220 px is a large amount of unused bar on a wide window, and both numbers break silently if that area ever changes. If the buttons are reachable as child windows or DirectUI elements it'd be worth measuring them; otherwise consider trimming 220 to something closer to the actual control strip.
  • Selection details cap out at 256 items. Above kMaxDetailedSelectionItems (line 3525) the bar falls back to "Selected: N items" with no folder/file split and no size. That's a sensible cap, but it isn't mentioned in the README and the change in output is likely to look like a bug to users selecting a large folder.
  • The content counts come from the mod's own FindFirstFileEx scan, which reflects the global "Hidden items"/"Protected OS files" state, while Explorer's list can be filtered differently (search results, a filtered view, per-folder settings). The two numbers can disagree in those views. Worth a line in the README.
  • GetPixel sampling is fragile in more than just cost. If DirectUI's element-level dirty tracking ever means it doesn't repaint the sampled pixel during a "full row" update, the sample reads back the mod's own previously-painted pixels. In the panes/cards styles the sample points (40–90% of the usable width) can land on a panel that was blended 6% toward the text color, so the learned background would drift a little on each pass. Deriving the color from the theme (GetThemeColor on the status part, or the AppsUseLightTheme fallback you already have) would sidestep this 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 30, 2026
@digart11

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 30, 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-30 18:15 UTC (in 16 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 30, 2026
@digart11

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 30, 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.


All five items from the previous round landed: the README now documents the hard conflict with Explorer Status Bar Metadata, singleFileDetails defaults to false, GetPixel sampling is throttled, EnsureShellBrowserRegistration / EnsureWindowDataCache are out of WM_PAINT and RefreshValidatedStatusRow is gated on WM_WINDOWPOSCHANGED, and the poll dropped to 30 s with free-space, content and single-selection reads all cached behind change tokens. g_insideFinalPaint and the unused lastFullScanTick are gone too. Two things came out of that rework.

1. Nothing retries the IShellBrowser registration, and until it succeeds the bar covers Explorer's status text with Loading....

Moving the registration out of WM_PAINT also removed the only thing that retried it. EnsureShellBrowserRegistration is now reached from exactly two places: EnsureDirectUiSubclass (line 5214), where it returns early at lines 1336-1342 because the caller is the engine or WinEvent-helper thread rather than the window's owner, and the g_refreshDirectUiMessage handler (line 5259), which is posted once per window at attach. So there is effectively a single real attempt. If it fails — FindAncestorByClass misses, CWM_GETISHELLBROWSER returns null because the tab isn't fully wired up yet, or RegisterInterfaceInGlobal fails — shellBrowserCookie stays 0, the worker's target snapshot (line 3992 requires state.shellBrowserCookie) skips that window for good, and nothing ever posts the message again.

The failure isn't silent, it's disfiguring. EnsureWindowDataCache still creates the cache with contentGroup = L"Loading..." (line 364), hasVisibleContent (line 4579) is therefore true with the default settings, and PaintFinalInfoBar fills coverRow — the whole status row up to client.right - 64dpi (line 4643) — and draws Loading.... A window that fails registration permanently hides Explorer's own status text behind a Loading... bar, with no way back short of closing the window.

Two small fixes, worth doing both:

  • Retry. In the worker pass, for any tracked window whose cookie is still 0, PostMessageW(hwnd, g_refreshDirectUiMessage, 0, 0) — the handler already performs the registration on the correct thread, so this needs no new machinery.
  • Don't paint over the native row before there's anything to show: start WindowDataCache::contentGroup empty (and use an empty fallback in GetCachedGroups, line 2840) instead of L"Loading...". hasVisibleContent then stays false until the first successful read, so a failed window leaves Explorer's row intact — and this also removes the Loading... flash every newly opened window and tab currently shows for the first second.

2. CoCancelCall in the unload path almost certainly does nothing, which leaves the worker join with no escape hatch.

Wh_ModBeforeUninit (line 5746) and the retry loop in Wh_ModUninit (line 5832) call CoCancelCall(g_workerThreadId, 0) from the Windhawk engine thread, which generally has no COM apartment — so the call returns CO_E_NOTINITIALIZED and requests no cancellation at all. The HRESULT isn't checked, so it fails invisibly. That matters because the while (true) loop in Wh_ModUninit has no exit other than the worker finishing on its own: if the worker is blocked in a cross-apartment call marshalled to a stuck Explorer UI thread, the intended safety valve never fires and the unload spins on Sleep(500) indefinitely, hanging Windhawk's mod management.

RevokeShellBrowserCookie (lines 1239-1285) already has the right pattern for this — reuse it around the cancel, and log the result:

const HRESULT initHr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
const bool shouldUninitialize = SUCCEEDED(initHr);

const HRESULT cancelHr = CoCancelCall(g_workerThreadId, 0);
if (FAILED(cancelHr) && cancelHr != RPC_E_CALL_COMPLETE) {
    Wh_Log(L"CoCancelCall failed HRESULT=0x%08X", (unsigned)cancelHr);
}

if (shouldUninitialize)
    CoUninitialize();

While you're there: guard the cancel with WaitForSingleObject(g_workerThread, 0) != WAIT_OBJECT_0 so that once the worker has exited and the OS has recycled its thread id, the loop can't cancel an unrelated Explorer component's outbound COM call.

3. Overlap with the existing status-bar mods — flagged for the human reviewer, not a request for more work. The README's "Compatibility / Why this mod is separate" section now states the conflicts plainly, including the one with explorer-status-metadata, and singleFileDetails defaulting to false means the mod no longer takes over that feature silently. The remaining functional overlap with classic-explorer-statusbar and prevista-explorer-statusbar (free space, selection size) is real but the styled overlay on Win11's native bar is a genuinely different product. The maintainer's standing preference is to extend an existing mod rather than merge an overlapping one, so expect that to be weighed — nothing more for you to change here.

Optional improvements

Minor polish — none of this affects users, so it's your call. Several carry over from previous rounds.

  • WindhawkUtils::StringSetting instead of the hand-rolled wrapper. GetStringSetting (line 379) reimplements Wh_GetStringSetting + Wh_FreeStringSetting; WindhawkUtils::StringSetting::make(L"style") does it with RAII. Wh_GetStringSetting also never returns NULL (it returns L"" on error/unset), so raw ? raw : L"" and the if (raw) guard are dead.
  • try/catch (...) around vector::reserve/push_back (lines 2290, 2444, 2771, 4002, 4027, 5125, 5188). These only fire on bad_alloc, at which point Explorer is already in trouble; most mods in the repo don't guard allocations this way, and dropping them would cut a lot of visual weight.
  • The Wh_ModBeforeUninit snapshot has no such guard (lines 5757-5761), and there it actually matters: a throw leaves g_subclassLock held shared and escapes the callback, so no subclass is ever removed and the mod unloads with live subclass procs pointing into the unmapped image. Hoisting the reserve above AcquireSRWLockShared removes the only throwing call from the locked region.
  • Wh_ModUninit's join can spin without progress (lines 5845-5864): in the waitResult != WAIT_TIMEOUT branch, if GetExitCodeThread itself fails the code logs, sleeps 500 ms and loops with no exit condition. Practically unreachable on a valid handle, but breaking out on a GetExitCodeThread failure closes it.
  • WM_DPICHANGED is dead on a child window (lines 5316, 5329). Top-level windows get WM_DPICHANGED; children in a per-monitor-v2 process get WM_DPICHANGED_BEFOREPARENT / WM_DPICHANGED_AFTERPARENT. The DPI refresh is actually carried by the WM_WINDOWPOSCHANGED arm right next to it, so this case never runs.
  • FindAncestorByClass walks with GetParent (line 1155), which returns the owner once it reaches a top-level window and keeps going up an unrelated chain. GetAncestor(current, GA_PARENT) stops at the top-level window. It matters most in AdvanceSelectionGenerationForWinEvent, which runs this walk on arbitrary explorer.exe windows (taskbar, tray, desktop).
  • Narrow the WinEvent filtering. SetWinEventHook(EVENT_OBJECT_FOCUS, EVENT_OBJECT_SELECTIONWITHIN, ...) (line 2627) fires for every focus and selection change anywhere in explorer.exe, and SelectionWinEventProc ignores idObject entirely, so each one pays a full parent-chain walk. Filtering on idObject == OBJID_CLIENT and bailing before FindAncestorByClass when the window's class isn't one Explorer's view uses would cut most of that.
  • The selection is read even when both selection sections are off. folderView->GetSelection + GetCount (lines 3585-3600) run unconditionally so PickBackgroundColor can skip sampling on a tinted row. That's two cross-apartment calls per window per pass for a purely cosmetic guard — the painter could read selected from the cache it already has instead.
  • EnsureWindowDataCache(hwnd) in the g_refreshDirectUiMessage handler (line 5258) is redundant; EnsureDirectUiSubclass already created the cache at line 5213 before posting the message.
  • MeasureGapWidth's fallback return of 26 (line 4222) isn't DPI-scaled, unlike every other constant in the paint path.
  • DrawFinalPiece (Simple style, line 4384) hard-clips at row.right with no DT_END_ELLIPSIS, while DrawBox uses it — in a narrow window Simple clips mid-glyph while the other two styles ellipsize.
  • ParseColorOverride (line 397) silently falls back to auto for a malformed value (#GGGGGG, rgb(1,2,3)). A Wh_Log on the reject path would save someone a confusing debugging session.
  • driveTotalBytes is computed, threaded through UpdateCache's parameter list and then only used as a > 0 validity flag (line 2876). Either display it or replace it with an explicit bool driveInfoValid.
  • goto nextEntry in ScanFilesystemDirectory (line 3147) is only there because the hidden/system filter sits inside the ./.. check. Hoisting both into a single "skip this entry" condition reads better.
  • The one-argument-per-line formatting still inflates the file to ~5900 lines for what is closer to ~2000 lines of code. Running clang-format with the repo's usual style would make it considerably easier to review and maintain.

Functionality notes

Non-critical observations and ideas about the feature behavior itself. Most carry over unchanged.

  • Content figures can lag by up to 30 s now. The scan is gated on itemCountChanged or periodicFullScanDue (lines 3441-3451), and with the paint wake also gated on kRefreshIntervalMs (lines 2339-2353) the only sub-30 s trigger left is the selection WinEvent. A file copied in by another app, or a file growing in place, won't be reflected until the next timer pass. SHChangeNotifyRegister on the current folder (SHCNE_CREATE/SHCNE_DELETE/SHCNE_RENAMEITEM/SHCNE_UPDATEDIR) would let you keep — or even lengthen — the 30 s timer while staying responsive.
  • GetPixel sampling can learn the mod's own pixels. Sampling now repeats every 2 s on a full-row repaint, at 40–90% of the usable width (lines 4303-4310) — which in Flat panes / Soft cards is exactly where the mod's panels are drawn. If DirectUI's element-level dirty tracking ever skips repainting that area, the sample reads back a panel that was blended 6% toward the text color, and repeated passes drift the learned background. Deriving the color from the theme (GetThemeColor on the status part, or the AppsUseLightTheme fallback you already have at line 4225) would sidestep this entirely.
  • Overpainting after DefSubclassProc has inherent limits. Drawing to a GetDC once DirectUI's buffered WM_PAINT has returned leaves a window where the native row is on the surface and the overlay isn't — usually swallowed by DWM, visible as flicker during resize/scroll. It also means the row isn't covered in WM_PRINTCLIENT captures (Task View / Alt+Tab thumbnails show the native text), and UIA/screen readers still report Explorer's own status text. No clean alternative exists for the panes/cards styles, so this is an FYI — but for Simple specifically, extending the native status string (the PSFormatForDisplayAlloc approach explorer-status-metadata uses) would inherit Explorer's font, DPI, theme and accessibility for free.
  • Hard-coded Segoe UI. CreateInfoBarFont (line 1460) always asks for Segoe UI at 12 device pixels. On non-Latin UI languages Explorer uses a different shell font (Yu Gothic UI on ja-JP, Malgun Gothic on ko-KR), and it ignores the user's text-size setting. SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, ..., dpi, 0)NONCLIENTMETRICS::lfMessageFont gives the right face and size for the window's DPI in one call.
  • Hard-coded right-edge reservations. client.right - 64dpi for the cover (line 4538) and client.right - 220dpi for the content (line 4551) are guesses about where Explorer's view-mode buttons sit. 220 px is a lot of unused bar on a wide window, and both numbers break silently if that area changes.
  • Selection details cap out at 256 items. Above kMaxDetailedSelectionItems (line 3609) the bar falls back to Selected: N items with no folder/file split and no size. A sensible cap, but it isn't in the README and the change in output will look like a bug to someone selecting a large folder.
  • Content counts come from the mod's own FindFirstFileEx walk. It honours fShowAllObjects / fShowSuperHidden, but Explorer's list can be filtered differently (search results, a filtered view, per-folder settings), so the two numbers can disagree in those views. Worth a line in the README.
  • Content: 0 folders / 0 files (0B) on a failure path. In ReadCurrentView, if currentPath is empty and ItemCount fails, no override text is produced (the Loading... branch at line 3548 requires a non-empty path), so UpdateCache formats the zeroed counts and the bar confidently reports an empty folder. Suppressing the Content section when there's no usable data would be safer than showing zeros.
  • No drive info on network locations. The Drive section requires currentPath[1] == L':' (line 3349), so UNC paths get nothing even though GetDiskFreeSpaceExW accepts them. If that's deliberate (avoiding a blocking call on a slow share), a comment saying so would help.
  • One slow folder stalls every other window. The worker walks its targets sequentially (lines 4033-4071) and ScanFilesystemDirectory blocks in FindFirstFileExW / FindNextFileW. On an unreachable share that freezes the info bar in every other Explorer window until the SMB timeout expires. kContentFailedRetryMs bounds the retries, so this is an FYI rather than a redesign — but skipping the scan on non-fixed drives, or giving each target a time budget, would contain it.
  • Worth re-verifying the row geometry in a few layouts: View ▸ Show ▸ Status bar unchecked, the Details and Preview panes open, and a window with several tabs. RefreshValidatedStatusRow takes the full client width below SHELLDLL_DefView's bottom, so anything else sitting below DefView inside the DirectUIHWND would end up under the bar.


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 30, 2026
@digart11

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 30, 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 threading and lifecycle work here is genuinely careful — locks are consistently released before the SendMessage-based subclass helpers, the shell browser is marshalled through the GIT instead of being stashed as a raw pointer, both helper threads are signalled and joined in Wh_ModUninit, and the overlay is repainted away in Wh_ModBeforeUninit so disabling the mod restores Explorer's own row. The items below are mostly about the rendering approach and the mod's position in the catalog.

1. Overlap with the three existing Explorer status-bar mods. The maintainer's standing preference is to extend an existing mod (or PR the original author's repo) rather than merge a near-duplicate, so this will be the first question asked. Concretely:

The genuinely new part is presentation: rendering into the native Windows 11 row with configurable section order, panel styles and colors. That's a defensible difference, but the README currently frames it as "don't run these together", which is the fragmentation the maintainer is trying to avoid — three mods fighting over the same 24 pixels. Worth stating explicitly in the PR why this can't be an option on one of the existing mods (e.g. a "modern style" option for classic-explorer-statusbar), and consider dropping the single-file metadata section, which is the piece with the most direct duplication.

2. The overlay paints on top of Explorer's own status row instead of owning it. In the WM_PAINT handler (line 5410) the mod lets DirectUI paint the row, then grabs GetDC(hwnd) and paints over the result. Everything awkward in this file follows from that one decision:

  • The background color has to be guessed by reading back pixels Explorer just drew (GetPixel, line 4366), with a 2-second resample throttle, a "don't sample while something is selected" heuristic, and a hardcoded white/dark-gray fallback.
  • Explorer draws its native text first and the mod covers it afterwards, so the native string can show through depending on paint timing.
  • Any repaint that doesn't route through the subclassed WM_PAINT leaves the native text visible until the next one, which is why the row needs revalidating every 500 ms and why the worker has a paint-wake path.
  • The mod has to blank the whole row up to client.right - 64dpi, which is what makes it incompatible with the other three mods.

Both existing status-bar mods take the other route: hook CreateWindowExW, and when Explorer creates its window, create a real child control that owns its own pixels — see classic-explorer-statusbar.wh.cpp#L391-L410 and prevista-explorer-statusbar.wh.cpp#L1201-L1220. A child window (custom class, or STATUSCLASSNAME with owner-draw) positioned over the row would give you the same three styles and colors with none of the above: no pixel sampling, no repaint races, no covering. It also replaces the EVENT_OBJECT_CREATE WinEvent hook, since window creation is exactly what the CreateWindowExW hook tells you about. If you go that way, remember that a class registered with RegisterClass is not released when the mod unloads — it must be UnregisterClass'd in Wh_ModUninit with the mod's own hInstance, or the next enable/update either fails to register or reuses a class whose lpfnWndProc points into the unmapped image.

If you'd rather keep the overpaint approach, please say so and why — it's a judgement call, but it should be a deliberate one rather than the default.

3. Trim the defensive scaffolding. At 6013 lines this is one of the largest mods in the repo, and a large share of it is machinery that doesn't do anything for users. It matters for the catalog: whoever has to fix a bug here in a year has to read all of it. Specific things that can go:

  • ~7 try { ... } catch (...) blocks wrapping std::vector::reserve / push_back with logging and fallback paths (lines 2285, 2434, 2767, 3990, 4059, 5165, 5225). If pushing a handful of HWNDs throws bad_alloc, Explorer is already dead — no other mod in the repo guards these, and each one adds a branch and a "failed" flag to carry around.
  • The stop condition is expressed two ways: IsWorkerStopRequested() (line 310) and the same g_unloading || WaitForSingleObject(g_stopEvent, 0) expression written out inline at lines 3112, 3660, 3841, 4032 and 4079. Use the helper everywhere.
  • CancelWorkerComCall() re-initializes and uninitializes COM on every call, and Wh_ModUninit calls it in an unbounded while (true) retry loop alongside CancelSynchronousIo. Setting the stop event plus a single CoCancelCall before one WaitForSingleObject(..., INFINITE) covers the realistic cases; the loop mostly exists to compensate for the unbounded shell work discussed in the notes below.
  • The caching layers stack up: contentRefresh (with a parallel failedFolderIdentity/failedItemCount/failedScanTick shadow set), singleSelectionRefresh, metadata with its own retry tick, lastWorkerRefreshTick, lastPaintWakeTick, lastStatusRowValidationTick, lastNativeRowBackgroundSampleTick, lastShellBrowserRegistrationRetryTick. Several of these exist to make the overpaint approach behave; a child window would remove the need for most of them.
Optional improvements

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

  • GetStringSetting (line 381) can be replaced by WindhawkUtils::StringSetting, which is the RAII wrapper for exactly this. Also, Wh_GetStringSetting never returns NULL — it returns L"" when unset or on error — so the raw ? raw : L"" check is dead.
  • WM_DPICHANGED (line 5360) is only sent to top-level windows; a DirectUIHWND child never receives it. The DPI refresh works today only because of the WM_WINDOWPOSCHANGED path next to it. WM_DPICHANGED_AFTERPARENT is the child-window equivalent.
  • dividerColor is labelled "Divider / border color" but only the Soft Cards border reads it (line 4778); the Simple style's separator dot always uses the derived automaticDividerColor (lines 4849, 4874). Either apply the override there too or rename the setting to "Card border color" so the name matches.
  • CreateInfoBarFont (line 1462) hardcodes Segoe UI. SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, ...) and ncm.lfMessageFont would follow the system font, which matters on UI languages that don't use Segoe UI — see classic-min-max-animations.wh.cpp for the idiom.
  • Latent GDI hazard: GetPaintResources (line 1557) hands the raw HFONT out from under the shared lock, and RefreshTrackedDpiAndFont (line 1551) can DeleteObject it from another thread. It's safe today only because the sole cross-thread caller is the just-installed path, where the font is still nullptr. Selecting the font under the lock, or only touching it on the window's owning thread, would make that guarantee explicit rather than accidental.
  • SelectionWinEventProc (line 2572) walks the parent chain for every focus/selection event in the whole explorer.exe process — taskbar, desktop, tray included. An early idObject != OBJID_CLIENT return would drop most of them before the first GetClassNameW.
  • The Simple style hard-clips text at row.right (DrawFinalPiece, line 4386) while the pane/card styles use DT_END_ELLIPSIS (line 5013). Worth making consistent.
  • MeasureGapWidth's fallback of 26 (line 4266) and the + 4 padding in DrawFinalPiece (line 4417) aren't DPI-scaled, unlike every other constant in the painter.

Functionality notes

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

  • Fallback background color. GetAppThemeFallbackBackground (line 4269) returns pure RGB(255,255,255) / RGB(32,32,32). Windows 11's Explorer status area is neither — light mode is closer to #F3F3F3 and both are affected by Mica. Until the first full-row repaint lets the sampler run, users see a band that doesn't match the surrounding chrome.
  • High contrast themes. Colors are derived by luminance (GetContrastingTextColor, BlendColor), which ignores the high-contrast palette entirely. SystemParametersInfo(SPI_GETHIGHCONTRAST) plus GetSysColor(COLOR_WINDOW) / COLOR_WINDOWTEXT when it's active would keep the row readable for users who depend on it.
  • Non-English Windows. The mod covers Explorer's localized status text with hardcoded English strings ("Content:", "Selected:", "Loading...", "no extension"). English-by-default is correct for Windhawk, but on a localized Explorer this is a visible regression rather than an addition — worth mentioning in the README at minimum.
  • Network locations get no drive section. The drive branch requires currentPath[1] == L':' (line 3351), so UNC paths are skipped. GetDiskFreeSpaceExW accepts a UNC share root, so \\server\share could be supported with a small extra branch.
  • Selection enumeration cost. For a selection up to kMaxDetailedSelectionItems (256), the worker makes a GetItemAt + GetDisplayName round trip per item through a GIT-marshalled proxy — every one of those is serviced by the Explorer UI thread. That's ~512 cross-apartment calls per selection change (debounced to 200 ms). IShellItemArray::GetAttributes(SIATTRIBFLAGS_AND, SFGAO_FOLDER, ...) for the folder/file split, and IShellItemArray::GetPropertyStore for an aggregate size, would collapse most of that into a handful of calls if they behave as expected in your testing.
  • Unbounded shell work on the worker. BuildSingleFileDetails (line 918) reads PKEY_Video_FrameWidth / PKEY_Media_Duration, which invokes arbitrary in-process property handlers, and ScanFilesystemDirectory enumerates the live folder — both can block for a long time on a slow or offline network share. Running them off the UI thread is the right call, but the stall propagates into Wh_ModUninit's retry loop, which is presumably why that loop exists. GPS_FASTPROPERTIESONLY, or skipping network/offline items the way explorer-status-metadata does with its explicit drive-type settings, would bound it.
  • README preview. Nice that a screenshot is already there. Since the three styles are a headline feature, one image per style (Simple / Flat panes / Soft cards) would sell the mod better than a single shot.


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 30, 2026
@digart11

Copy link
Copy Markdown
Contributor Author

/ready-for-reviewer

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