Skip to content

Add Taskbar Volume Percentage Indicator mod - #5389

Open
gilnett wants to merge 5 commits into
ramensoftware:mainfrom
gilnett:patch-1
Open

Add Taskbar Volume Percentage Indicator mod#5389
gilnett wants to merge 5 commits into
ramensoftware:mainfrom
gilnett:patch-1

Conversation

@gilnett

@gilnett gilnett commented Sep 7, 2026

Copy link
Copy Markdown

Displays the volume percentage directly in the Windows 11 system tray volume icon.

Includes configurable display formats (percentage, number only, custom prefix, icon + percentage, or default).

Closes #4162

Changelog

  • Initial release.

Mod authorship

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.

This mod displays the exact master volume percentage in the Windows 11 system tray volume icon with real-time synchronization. It includes various display styles for the volume indicator.
@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 Sep 7, 2026
@gilnett

gilnett commented Sep 7, 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 Sep 7, 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 is a reasonable fit for the catalog and there's no existing mod that does it, but the implementation writes into an internal WinRT object at a hard-coded offset from arbitrary threads, and forces refreshes by changing the machine's actual audio volume. Those need to be reworked before this can ship.

1. Hard-coded struct offset 0x90 into VolumeSystemTrayIconDataModel

ApplyCustomVolumeText and Wh_ModBeforeUninit do:

shared_hstring_header** ppHeader = reinterpret_cast<shared_hstring_header**>(
    reinterpret_cast<char*>(pThis) + 0x90);

There is no validation and no build gate. Symbol hooks tolerate Windows updates because the symbol name is stable; a member offset is not. The moment Microsoft adds/removes/reorders a field in that class, the mod overwrites an unrelated member and passes a non-hstring pointer to HeapFree — heap corruption and an explorer crash for every user on that build, with no way for the mod to notice.

Please drive this through the type's own code instead of its memory layout. Two patterns already in the repo:

  • Modify the returned/produced string rather than a member — this is what taskbar-clock-customization does: it hooks GetTimeToolTipString and rewrites the hstring the function returns (a value the caller owns), so no object layout is assumed. If VolumeSystemTrayIconDataModel has an accessor/produce<> thunk that yields the icon text, hooking that is the equivalent here.
  • Or drive the XAML element directly, as the same mod does for the clock text (DateTimeIconContent_OnApplyTemplate_Hook + FindChildByName). That also fixes item 4 below, since it naturally runs on the UI thread.

If neither is workable and the offset really is the only route, it must at minimum be gated on the module version and sanity-checked before use (e.g. flags/length plausible and ptr == buffer), and bail out otherwise.

2. TriggerImmediateVolumeSync changes the user's real system volume

float nudge = (vol > 0.01f) ? (vol - 0.0005f) : (vol + 0.0005f);
pEndpointVolume->SetMasterVolumeLevelScalar(nudge, nullptr);
pEndpointVolume->SetMasterVolumeLevelScalar(vol, nullptr);

This writes to the shared audio endpoint to provoke a redraw. It's a system-wide side effect: two endpoint-volume change notifications are broadcast to every app that listens (other volume UIs, OSDs, streaming/DJ tools, other Windhawk mods), the output level genuinely moves for a moment, and if the second call fails — or the process is killed between the two — the user's volume is left permanently off. It runs on every mod enable/reload (Wh_ModAfterInit), on every settings change with no cached data model, and from the LoadLibraryExW hook.

A mod must not modify state outside itself to refresh its own UI. Use the refresh path you already resolve (OnDataModelChanged, marshalled to the UI thread — see item 4), or simply let the next natural UpdateVolume repaint and accept a slightly delayed first render.

3. COM initialization inside the LoadLibraryExW hook

if (HookSystemTraySymbols(hModule)) {
    Wh_ApplyHookOperations();
    TriggerImmediateVolumeSync();   // CoInitializeEx + CoCreateInstance + Activate
}

LoadLibraryExW_Hook can run in a nested load (a DLL's DllMain calling LoadLibrary), in which case the loader lock is still held. Doing CoInitializeEx/CoCreateInstance/IMMDevice::Activate there can deadlock or hang explorer. The LoadLibraryExW hook should only install hooks and call Wh_ApplyHookOperations, nothing else.

4. The data model is written from arbitrary threads (use-after-free + XAML thread affinity)

Wh_ModSettingsChanged and Wh_ModBeforeUninit run on an arbitrary Windhawk thread, and both:

  • swap *ppHeader and then ReleaseSharedHString(oldHeader) while the taskbar UI thread may be concurrently reading that same field and taking a reference on it. g_dataModelMutex doesn't help — the UI thread doesn't take it. The result is a torn pointer read / lost refcount → use-after-free on the process heap.
  • call VolumeSystemTrayIconDataModel_OnDataModelChanged_Original(g_pVolumeDataModel, &propName), i.e. raise a XAML property-change notification off the UI thread. That is a thread-affinity violation that happens every time the mod is disabled or its settings are changed, not just occasionally.

Please marshal both operations onto the taskbar UI thread (post to the tray window's thread / use the element's dispatcher), or keep all writes inside the UpdateVolume hook, which already runs on the correct thread.

5. g_pVolumeDataModel can be a dangling pointer

The pThis captured in the ctor / UpdateVolume hooks is cached indefinitely and never cleared when the object is destroyed (default-device change, tray rebuild, taskbar reconstruction inside the same explorer process). Wh_ModSettingsChanged and Wh_ModBeforeUninit then write to freed + 0x90 and HeapFree whatever they read there. The guard

if (g_pVolumeDataModel && ((ULONG_PTR)g_pVolumeDataModel & (sizeof(void*) - 1)) == 0)

only checks alignment, which a freed pointer still satisfies. Either hook the destructor and clear the global there, or don't cache the pointer at all and only act from inside UpdateVolume.

6. The constructor hook's prototype doesn't match every symbol it can bind to

The ctor entry lists both overloads:

LR"(public: __cdecl ...::VolumeSystemTrayIconDataModel(struct winrt::SystemTray::IIconDataModel const &))",
...
LR"(public: __cdecl ...::VolumeSystemTrayIconDataModel(void))",

but the hook and original are typed void*(WINAPI*)(void* pThis) — one parameter. If the two-argument overload is the one that resolves, VolumeSystemTrayIconDataModel_ctor_Original(pThis) leaves rdx undefined and the real constructor dereferences garbage as IIconDataModel const& → crash while the taskbar is being built. Split it into two entries with matching prototypes, or drop the ctor hook entirely (item 5 makes it largely unnecessary).

Related: LR"(?0VolumeSystemTrayIconDataModel@implementation@SystemTray@winrt@@QEAA@AEBUIIconDataModel@23@@Z)" is missing a leading ? — a constructor mangles to ??0..., so that alternative can never match.

7. No reentrancy guard around OnDataModelChanged

VolumeSystemTrayIconDataModel_UpdateVolume_Hook unconditionally calls OnDataModelChanged(L"CurrentData") after the original. If that notification causes the model to refresh in a way that reaches UpdateVolume again, this recurses without bound → stack overflow in explorer. A thread_local bool guard around the notification (and an early return in the hook when set) is cheap insurance.

8. Taskbar.View.dll fallback has no version guard, and a hook failure is unrecoverable

GetSystemTrayModuleHandle falls back to Taskbar.View.dll unconditionally. On builds where the winrt::SystemTray types moved out of Taskbar.View.dll into SystemTray.dll, if SystemTray.dll isn't loaded yet at Wh_ModInit but Taskbar.View.dll is, the mod hooks the wrong module, the non-optional UpdateVolume symbol fails to resolve, and HookSystemTraySymbols returns false. Because Wh_ModInit only installs the LoadLibraryExW hook in the else branch, nothing is retried when SystemTray.dll does load — the mod silently does nothing for the rest of the session.

Copy the version check from taskbar-volume-control (also in adaptive-microphone-icon-visibility), and install the LoadLibraryExW hook whenever the symbols were not successfully hooked, not only when no module was found.

9. The symbol-hook array declares only one of the two modules it's used against

systemTrayDllHooks encodes SystemTray.dll in its name, but the same array is resolved against Taskbar.View.dll too. Declare both, e.g. rename it to symbolHooks and put a comment on the line above, as in taskbar-volume-control:

// SystemTray.dll, Taskbar.View.dll
WindhawkUtils::SYMBOL_HOOK symbolHooks[] = { ... };

10. OnDataModelChanged is optional, but the whole mod depends on it

It's declared with optional = true, and it is the only thing that makes the new text appear. If it ever stops resolving, HookSymbols succeeds, the mod logs "Successfully hooked SystemTray volume symbols", and the user sees absolutely nothing change with no error. Make it required, or add a fallback refresh and log loudly when it's missing.

11. Add a screenshot to the README

This is a purely visual mod and the README has no image. Please add a screenshot (or GIF) of the tray showing at least the default 50% style — it's also the quickest way to answer the sizing question in the functionality notes below.

Optional improvements

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

  • Wh_GetStringSetting never returns NULL (it returns L"" when unset or on error), so the if (displayStyleStr) / else and if (prefixStr) / else branches in LoadSettings are dead. While you're there, WindhawkUtils::StringSetting is the RAII form and removes the manual Wh_FreeStringSetting calls:

    WindhawkUtils::StringSetting displayStyle =
        WindhawkUtils::StringSetting::make(L"displayStyle");
  • LoadSettings accepts a pile of option values that don't exist in the settings block — default, vanillaOnly, numberOnly, volPrefix, emojiAndPercent, iconAndPercent. For an initial release there is no legacy to be compatible with; this looks like an AI artifact. Dropping them leaves five clean comparisons matching the five $options.

  • g_settings.customPrefix (a std::wstring) is rewritten by LoadSettings() in Wh_ModSettingsChanged outside g_dataModelMutex, while FormatVolumeText reads it from the taskbar thread. Only reachable on a settings change, but it's a real data race on a non-trivial object — take the lock around LoadSettings(), or snapshot the settings into a value the hook reads atomically.

  • Unused dependencies: #include <shlwapi.h>, -lshlwapi and -loleaut32 aren't used by anything in the file (-lole32 is needed for the Co* calls).

  • Consider adding @architecture x86-64, as the other Win11 taskbar mods do. Windows 11's explorer.exe is 64-bit only, so the x86 variant Windhawk builds today is dead weight — and in it WINAPI (__stdcall) is the wrong calling convention for these __thiscall member functions.

  • @version 2.0.0 with a changelog of "Initial release." is a bit surprising; 1.0.0 is the usual starting point.

  • The README credits Windows 11 Taskbar Styler, but nothing here appears to come from it — the hstring_header / shared_hstring_header block is the one from taskbar-clock-customization. Worth correcting the attribution.

Functionality notes

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

  • The unload restore in Wh_ModBeforeUninit hard-codes GLYPH_VOL_3 (full volume) instead of the level-appropriate glyph, so disabling the mod at 10% volume leaves a full-volume icon until the next volume change. GetVanillaVolumeGlyph(std::lround(g_lastVolumeLevel * 100), g_lastIsMuted) is already there.

  • The ctor hook renders with the initial g_lastVolumeLevel = 0.50f, so a newly created data model briefly displays 50% regardless of the real volume until the first UpdateVolume arrives.

  • MUT is a fairly cryptic abbreviation for the non-vanilla styles. The mute glyph (0xE74F), Muted, or 0% would read better — and MUT under the Number only style is inconsistent with what that style promises.

  • The Windows Default style re-implements the shell's own glyph selection with its own thresholds (<=33 / <=66) and only five states — it can drift from what Windows actually shows, and "make the mod do nothing" is already achievable by disabling the mod. Consider dropping it, or at least confirm the thresholds match the shell's.

  • Sizing/font: the tray icon slot is a fixed-width TextBlock using Segoe Fluent Icons, which has no Latin digits or emoji — those fall back to another font. Does Vol 100% fit without clipping, and does 🔊 50% render as expected? The screenshot requested above would settle both.


Next steps:

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

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Sep 7, 2026
- Add jitter prevention (zero and space padding modes).
- Add 7 customizable mute display styles with custom text support.
- Add dynamic XAML memory offset detection to prevent crashes.
- Support Windows 11 24H2 and recent builds (>= 2604).
- Ensure thread-safe UI marshaling on the taskbar thread.
- Hook destructor for clean resource disposal.
- Query system volume passively without artificial volume nudging.
- Update license to GPL-3.0.
- Add safety tags to custom strings to prevent memory corruption
- Remove artificial text padding options
- Modernize settings management with Windhawk helpers
- Improve string and offset detection for Windows 11
- Simplify initial audio volume query on startup
@gilnett

gilnett commented Sep 9, 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 Sep 9, 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 idea is good and doesn't overlap with anything in the catalog (taskbar-volume-control handles scroll/middle-click, not the icon text). The problem is the mechanism: instead of using the winrt::hstring that UpdateVolume already hands you, the mod hunts for a private member by scanning raw offsets and then swaps in a hand-forged HSTRING header. That's the source of most of the findings below, and it can be removed entirely.

1. GetIconTextOffset blind-scans the object and dereferences arbitrary values as pointers — this will crash Explorer.

for (size_t offset = 0x40; offset <= 0x120; offset += sizeof(void*)) {
    shared_hstring_header* candidate = *reinterpret_cast<shared_hstring_header**>(
        reinterpret_cast<char*>(pThis) + offset);
    if (isValidHeader(candidate)) { ... }

Three separate problems:

  • The object's size is unknown, so offsets up to 0x120 may read past the allocation.
  • isValidHeader only checks non-null + alignment before dereferencing h->flags, h->length, h->ptr, and later candidate->buffer[0]. Any aligned non-pointer value in that range — a small integer, a packed bool/float pair, a stale pointer to a freed page — becomes an access violation. There is no try/__try around it either.
  • Even a "successful" match is a coincidence: flags <= 1, length <= 256, ptr != nullptr is satisfied by a great many unrelated members. Writing over the wrong slot corrupts an unrelated field of a live shell object. The fast path at 0xB8 doesn't even apply the first-character check the loop does, so it's the weakest of the two.

The fix is to stop touching the object at all and substitute the hstringIcon argument, which is exactly the string being displayed. On x64, a by-value winrt::hstring parameter is passed as a hidden pointer, which is why your prototype already receives it as void* — so you can pass the address of your own winrt::hstring:

#include <winrt/base.h>

void __cdecl VolumeSystemTrayIconDataModel_UpdateVolume_Hook(
    void* pThis, float volumeLevel, bool isMuted, void* hstringIcon) {
    if (g_settings.displayStyle == DisplayStyle::Vanilla) {
        VolumeSystemTrayIconDataModel_UpdateVolume_Original(
            pThis, volumeLevel, isMuted, hstringIcon);
        return;
    }

    int percentage = std::clamp(
        static_cast<int>(std::lround(volumeLevel * 100.0f)), 0, 100);
    winrt::hstring text{FormatVolumeText(percentage, isMuted)};

    VolumeSystemTrayIconDataModel_UpdateVolume_Original(
        pThis, volumeLevel, isMuted, &text);
}

Note this passes your own local, it does not modify the caller's temporary — the original will take its own reference and text releases itself on scope exit. This removes GetIconTextOffset, CreateSharedHString, ReleaseSharedHString, the MOD_HSTRING_MAGIC tag, the manual OnDataModelChanged notification (the original already raises it), and findings 2 and 3 below.

Please verify first that UpdateVolume really is what stores the displayed icon string — your current code overwrites the member after calling the original, which suggests you may have found it doesn't. If the icon is derived somewhere else, hook that function/setter instead; an offset scan is not an acceptable substitute in either case. For reference, taskbar-clock-customization does do HSTRING header surgery, but only on a value it receives from a hooked function at a known location, never by scanning an object: taskbar-clock-customization.wh.cpp#L4047-L4108.

2. The original HSTRING is leaked on every volume change.

static void ReleaseSharedHString(shared_hstring_header* header) {
    if (header->flags != 0 || header->padding1 != MOD_HSTRING_MAGIC) {
        return;   // <-- OS-created strings are never released
    }

UpdateVolume_Original stores a Windows-allocated hstring into the member; ApplyCustomVolumeText then overwrites the slot and calls ReleaseSharedHString on it, which bails out because the magic tag doesn't match. That reference is now unowned and never dropped — one leaked hstring per volume change, per settings change, and once more on unload, for the lifetime of the Explorer process. The guard is the right instinct (you must not HeapFree a string you didn't allocate), but the correct release for a foreign string is WindowsDeleteString((HSTRING)oldHeader).

Also note that using padding1 as an ownership tag relies on those header fields being permanently unused by combase — undocumented, and a Windows-created string with a nonzero padding1 would silently be treated as yours and freed with the wrong path. The parameter substitution in item 1 makes all of this moot.

3. Deadlock on settings change: OnDataModelChanged is invoked while holding g_dataModelMutex.

static void SettingsChangedOnUIThread(void*) {
    std::lock_guard<std::mutex> lock(g_dataModelMutex);
    ...
        VolumeSystemTrayIconDataModel_OnDataModelChanged_Original(g_pVolumeDataModel, &propName);

You added the s_inHook guard in UpdateVolume_Hook precisely because OnDataModelChanged can re-enter UpdateVolume synchronously. That guard is thread_local and is not set here, and g_unloading is false on this path — so the re-entrant UpdateVolume_Hook reaches std::lock_guard<std::mutex> lock(g_dataModelMutex) on a non-recursive mutex already held by this same thread. That's undefined behaviour, and in practice a hang of the taskbar UI thread — i.e. a frozen taskbar on a settings change. More generally, raising a XAML property-change notification while holding a lock is reentrancy-prone regardless.

Fix: snapshot what you need, release the lock, then notify:

void* dataModel = nullptr;
{
    std::lock_guard<std::mutex> lock(g_dataModelMutex);
    if (g_pVolumeDataModel && !g_unloading) {
        ApplyCustomVolumeText(g_pVolumeDataModel, g_lastVolumeLevel, g_lastIsMuted);
        dataModel = g_pVolumeDataModel;
    }
}
if (dataModel && VolumeSystemTrayIconDataModel_OnDataModelChanged_Original) {
    std::wstring_view propName = L"CurrentData";
    VolumeSystemTrayIconDataModel_OnDataModelChanged_Original(dataModel, &propName);
}

and set the recursion guard around it as you do in UpdateVolume_Hook.

4. The destructor hook is optional, but g_pVolumeDataModel is only safe if it resolves.

reinterpret_cast<void*>(VolumeSystemTrayIconDataModel_dtor_Hook),
true,   // optional

~VolumeSystemTrayIconDataModel is the only thing that clears the cached pThis. If the symbol doesn't resolve on some build (which is exactly what optional permits), HookSymbols still returns true, the mod keeps a raw pointer to a destroyed object, and SettingsChangedOnUIThread / BeforeUninitOnUIThread write a forged hstring pointer into freed memory. Make that hook required, or drop the cached pointer altogether — with the approach in item 1 you no longer need it for the update path (only, optionally, to refresh on a settings change).

5. The "Windows Default" style isn't the Windows default.

$options advertises vanilla as "exact vanilla Windows speaker icon", but FormatVolumeText throws away the string Windows computed and substitutes its own glyph from its own thresholds (0 / < 33 / < 66), which don't match Windows' (1-33 / 34-66 / 67-100) and don't cover the other states Windows shows there (no audio endpoint, etc.). It also means the offset poking and the leak in item 2 happen even in the mode that's supposed to change nothing. Make vanilla a plain pass-through to the original, as in the snippet in item 1.

6. The README has no screenshot.

This is a purely visual mod, and the README's feature list describes five display styles that a user can't picture from text. Please add a screenshot or GIF (i.imgur.com or raw.githubusercontent.com are the allowed hosts) showing the indicator in the tray — ideally covering a couple of the styles and the muted state.

Optional improvements

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

  • QueryInitialSystemVolume is dead code. CoCreateInstance is called without CoInitializeEx on the mod-init thread, so it returns CO_E_NOTINITIALIZED and the function always falls back to the 0.5f / false defaults. And even if it succeeded, the values are never used: SettingsChangedOnUIThread and BeforeUninitOnUIThread both require g_pVolumeDataModel != nullptr, which is only ever set by UpdateVolume_Hook, which overwrites g_lastVolumeLevel/g_lastIsMuted in the same breath. Removing it also lets you drop -lole32, <mmdeviceapi.h> and <endpointvolume.h>.
  • -lshlwapi and #include <shlwapi.h> are unused — the mod only uses CRT string functions (wcscmp, _wcsicmp, wcsrchr).
  • g_settings is mutated without synchronization. LoadSettings() runs on the settings thread and reassigns the std::wstring members while the taskbar thread may be reading them in FormatVolumeText (under g_dataModelMutex, which LoadSettings doesn't take). A concurrent read/write on std::wstring is UB. Only reachable on a settings change, hence optional, but it's a cheap fix — take g_dataModelMutex in LoadSettings, or build a new Settings and swap it under the lock.
  • Dead alias values in LoadSettings. default, vanillaOnly, numberOnly, volPrefix, emojiAndPercent, iconAndPercent are accepted but don't exist in $options, so they're unreachable. Looks like leftovers from an earlier iteration.
  • Wh_GetStringSetting never returns NULL — it returns L"" on error or when unset. All the if (displayStyleStr) / if (prefixSetting.get()) branches (and their else fallbacks) are dead; check for an empty string if you want a fallback.
  • s_inHook isn't exception-safe. If OnDataModelChanged_Original throws, s_inHook stays true for that thread and the mod silently stops updating. A small RAII guard (or std::exchange + scope guard) fixes it.
  • RunFromWindowThread deviates from the standard snippet. Use RegisterWindowMessage(L"Windhawk_RunFromWindowThread_" WH_MOD_ID) inside a function-local static const rather than a hand-written id string in a lazily-initialized global (which is also an unsynchronized write from arbitrary threads). See adaptive-microphone-icon-visibility.wh.cpp#L497-L546 for the canonical form.
  • The reinterpret_casts in the SYMBOL_HOOK initializers defeat its type checking. SYMBOL_HOOK's constructor is templated on the function prototype specifically so the hook and the original are verified to match; casting both to void**/void* collapses that to Prototype = void. Pass &VolumeSystemTrayIconDataModel_UpdateVolume_Original and VolumeSystemTrayIconDataModel_UpdateVolume_Hook directly.
  • HookSymbols can run twice for the same module. If the call in Wh_ModInit fails, g_systemTrayModuleHooked stays false and Wh_ModAfterInit calls HookSystemTraySymbols again on the same handle. A second call invalidates the symbol cache and forces a re-resolution; set a separate "attempted" flag so each module is resolved at most once.

Functionality notes

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

  • The unload restore uses a hardcoded glyph. BeforeUninitOnUIThread writes GLYPH_VOL_3 (three bars) regardless of the actual level, so after disabling the mod at 10% volume the tray shows a full-volume icon until the next volume change. You already have g_lastVolumeLevel — reuse the same glyph selection as the Vanilla style. (With the parameter-substitution approach the restore becomes unnecessary for the update path, though you'd still want one for the currently-displayed string.)
  • Font and width. The tray icon element renders with Segoe Fluent Icons, which has no Latin glyphs — worth confirming that 50%, Vol 50% and the emoji styles actually fall back to a text font rather than showing tofu, and that the wider strings aren't clipped by the fixed icon slot (especially Prefix and Percentage with a long custom prefix). If they are, consider capping the prefix length or documenting the limit.
  • Custom prefix / mute text aren't validated. Nothing bounds their length, so an over-long value will either be clipped or push the tray layout around. A short $description note or a length cap would help.
  • README credits. "Inspired by the taskbar visual customization concepts from m417z" is vague — if you did adapt code (the hstring_header / shared_hstring_header layout and its reference comment look like they came from taskbar-clock-customization), it's better to say so explicitly and name the mod.


Next steps:

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

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Sep 9, 2026
- Hook IconView OnViewModelChanged and UpdateHostedContent
- Lock container minimum width using IFrameworkElement::put_MinWidth
- Add fixedContainerWidth setting with automatic optimal width per style
- Remove legacy MMDeviceEnumerator startup overhead
- Lock icon width during layout (MeasureOverride), not just after
- Track the icon element so width stays correct on settings change/unload
- Remove padStyle setting — no longer needed
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.

Volume percentage Indicator on the taskbar (like the battery percentage indicator)

1 participant