Add Taskbar Countdown Timer mod - #5381
Conversation
|
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 See the pull request review process for the full details. |
|
/ai-review |
Submission reviewNote: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding. Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it. Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them. Nice, self-contained idea, and the 1. The mod never loads after a reboot or an Explorer restart. HWND taskbar = FindCurrentProcessTaskbarWnd();
if (!taskbar) {
Wh_Log(L"ERROR: Taskbar not found");
return FALSE;
}
Move the UI work to void Wh_ModAfterInit() {
...
g_retryStopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
g_retryThread = CreateThread(nullptr, 0, [](void*) -> DWORD {
for (int i = 0; i < 5 && !g_unloading; i++) {
if (WaitForSingleObject(g_retryStopEvent, 2000) != WAIT_TIMEOUT) break;
if (g_buttonGrid || g_unloading) break;
ApplyAllSettingsOnWindowThread();
}
return 0;
}, nullptr, 0, nullptr);
}The same mechanism also covers the second half of the problem: the button is currently added exactly once, so it does not come back if the taskbar XAML tree is rebuilt during the session. 2. The window classes are never unregistered — the next load runs a dangling if (!RegisterClassW(&wc) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) { ... return; }
classRegistered = true;
Swallowing if (!RegisterClassExW(&cls)) {
if (GetLastError() != ERROR_CLASS_ALREADY_EXISTS ||
!UnregisterClassW(kClass, instance) || !RegisterClassExW(&cls)) {
return 0;
}
}
...
UnregisterClassW(kClass, instance); // on teardown3. The popup thread can outlive the mod image. if (g_timerPopup) { PostMessageW(g_timerPopup, WM_APP_TIMER_SHUTDOWN, 0, 0); }
if (g_timerPopupThread) { WaitForSingleObject(g_timerPopupThread, 2000); CloseHandle(...); }Windhawk unloads the mod with a single
Also, the GDI objects ( Make the shutdown deterministic and then wait 4. The global XAML objects need static Button g_timerButton{nullptr};
static TextBlock g_timerText{nullptr};
static DispatcherTimer g_countdownTimer{nullptr};
[[clang::no_destroy]] Button g_timerButton{nullptr};
[[clang::no_destroy]] TextBlock g_timerText{nullptr};
[[clang::no_destroy]] DispatcherTimer g_countdownTimer{nullptr};The existing explicit cleanup in 5. The popups are laid out in raw pixels, so they break at non-100% DPI. Window sizes ( Positioning has the same problem: 6. if (code[0] == 0x48 && ... ) { offset = code[7]; }
else { /* log + return nullptr */ }
size_t taskbarElementIUnknownOffset = 0x48;
#if defined(_M_X64)
... pattern match, else Wh_Log(L"Unsupported TaskbarHost::FrameHeight");
#elif defined(_M_ARM64)
// Just use the default offset which will hopefully work in most cases.
#endifNote the default there is Optional improvements
Minor polish — none of this affects users, so it's your call.
Functionality notes
Non-critical observations and ideas about the feature behavior itself.
Next steps:
See the review process for details. |
|
/ai-review |
Submission reviewNote: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding. Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it. Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them. Nice, self-contained idea, and no existing mod overlaps with it — the closest neighbours ( 1. The retry thread polls the taskbar UI thread forever, every 5 seconds. Bound the retry and drive the re-add from an event instead. g_retryThread = CreateThread(nullptr, 0, [](void*) -> DWORD {
for (int i = 0; i < 5 && !g_unloading; i++) {
if (WaitForSingleObject(g_retryStopEvent, 2000) != WAIT_TIMEOUT) break;
if (g_buttonGrid || g_unloading) break;
ApplyAllSettingsOnWindowThread();
}
return 0;
}, nullptr, 0, nullptr);For re-adding the button after the taskbar's XAML tree is rebuilt, hook a tray (re)construction point rather than polling — 2. The popup font is deleted while it is still selected into live controls.
3. On unload, the XAML button and HWND taskbar = FindCurrentProcessTaskbarWnd();
if (taskbar) {
RunFromWindowThread(taskbar, RemoveTimerButton, nullptr);
}If either fails ( Check the result, and don't give up silently — retry a few times, and log an error if removal genuinely couldn't happen. See https://github.com/ramensoftware/windhawk/wiki/Global-objects-and-process-shutdown (section #4 XAML and other UI-thread objects) for why the explicit release on the owning thread is the only correct teardown here. 4. #elif defined(_M_ARM64)
const DWORD* p = (const DWORD*)TaskbarHost_FrameHeight_Original;
if (p[0] == 0xD503237F && (p[1] & 0xFFC07FFF) == 0xA9807BFD &&
p[2] == 0x910003FD && (p[3] & 0xFFF00FE0) == 0xF8400C00) {
offset = (p[3] >> 12) & 0xFF;
} else {
Wh_Log(L"Unsupported TaskbarHost::FrameHeight");
}
#endif5. Don't continue with a guessed offset when the 6. Column bookkeeping breaks if another mod also inserts into 7. Cross-thread globals are unsynchronized. 8. Popup-thread handshake: closed-handle race and unbounded waits on the shell UI thread. Three related problems around
DWORD result;
do {
result = MsgWaitForMultipleObjects(1, &g_retryThread, FALSE, INFINITE, QS_SENDMESSAGE);
if (result == WAIT_OBJECT_0 + 1) {
MSG message;
PeekMessageW(&message, nullptr, 0, 0, PM_NOREMOVE);
}
} while (result == WAIT_OBJECT_0 + 1);Create the ready event once (in Optional improvements
Minor polish — none of this affects users, so it's your call.
Functionality notes
Non-critical observations and ideas about the feature behavior itself.
Next steps:
See the review process for details. |
|
/ai-review |
Submission reviewNote: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding. Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it. Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them. The overall structure is solid — the XAML injection follows the established pattern from mods/taskbar-vd-switcher.wh.cpp and mods/adaptive-microphone-icon-visibility.wh.cpp, teardown joins both worker threads and unregisters the window classes, and the 1. The custom reminder text never reaches the completion popup. This is the mod's headline feature, and it's broken on both paths:
2. The timer popup is sized and positioned once, at 96 DPI, on the primary monitor. When the popup is later shown, Do the sizing/positioning at show time, against the taskbar window: case WM_APP_TIMER_SHOW: {
HWND taskbar = g_taskbarWnd.load();
UINT dpi = GetPopupDpi(taskbar);
ResetPopupForNewTimer(hWnd, ...);
LayoutTimerPopup(hWnd, dpi);
PositionPopupNearTaskbar(hWnd, taskbar, 380, 235, dpi);
...
}Related: 3. if (g_systemTrayModuleHooked.load() || GetSystemTrayModuleHandle() != module) {
return;
}
if (HookSystemTraySymbols(module)) {
g_systemTrayModuleHooked.store(true);
Wh_ApplyHookOperations();
}
if (!g_systemTrayModuleHooked && GetSystemTrayModuleHandle() == module &&
!g_systemTrayModuleHooked.exchange(true)) {
if (HookSystemTraySymbols(module)) {
Wh_ApplyHookOperations();
}
}Also, the Optional improvements
Minor polish — none of this affects users, so it's your call.
Functionality notes
Non-critical observations and ideas about the feature behavior itself.
Next steps:
See the review process for details. |
|
/ai-review |
|
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 |
Submission reviewNote: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding. Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it. Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them. Nice mod overall — the taskbar integration is done with the established symbol/XAML approach, the teardown is thought through ( 1. A large part of the infrastructure here is a near-verbatim (reformatted) copy of two of m417z's mods, both published under GPL-3.0:
Please change 2. The completion-alert thread can outlive In Fix: signal and join the timer worker (and the retry thread) first, then deal with the alert thread; and make static bool ShowFinishedAlert(const wchar_t* reminder) {
if (g_unloading.load()) {
return false;
}
...
}Everything that can still run mod code must be joined before 3. Line 2702 compares a Compare COM identity explicitly, as taskbar-numberer.wh.cpp does: auto sameObject = [](auto const& a, auto const& b) {
return a && b &&
winrt::get_abi(a.template as<winrt::Windows::Foundation::IUnknown>()) ==
winrt::get_abi(b.template as<winrt::Windows::Foundation::IUnknown>());
};
if (existing && g_timerButton && sameObject(existing, g_timerButton)) { ... }4. The flyout's
Store the tokens next to 5. Alert window class: don't continue on Lines 1972-1982 swallow Register the class once in Optional improvements
Minor polish — none of this affects users, so it's your call.
Functionality notes
Non-critical observations and ideas about the feature behavior itself.
Next steps:
See the review process for details. |
|
/ai-review |
|
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 |
Submission reviewNote: 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. Overall this is a solid submission — the teardown ordering in particular is careful and correct (threads joined before the handles/GDI objects they use are closed, XAML released on the taskbar thread, 1. Injection and re-injection rely on polling plus an incidental Right now the button is injected once from
using TrayUI_StartTaskbar_t = void (WINAPI*)(void* pThis);
static TrayUI_StartTaskbar_t TrayUI_StartTaskbar_Original;
void WINAPI TrayUI_StartTaskbar_Hook(void* pThis) {
TrayUI_StartTaskbar_Original(pThis);
if (!g_unloading.load()) {
ApplyTimerButtonIfAvailable();
}
}
// ...
{
{LR"(public: virtual void __cdecl TrayUI::StartTaskbar(void))"},
&TrayUI_StartTaskbar_Original,
TrayUI_StartTaskbar_Hook,
},This is the pattern used by taskbar-folder-menus, taskbar-multi-tray and mutealert. With a deterministic signal in place, 2. Every tray icon load re-resolves the entire taskbar XAML root on Explorer's UI thread The g_buttonInjected.store(false);
ApplyTimerButtonIfAvailable();unconditionally, so An early bail-out at the top of if (g_timerButton && VisualTreeHelper::GetParent(g_timerButton)) {
// Still attached to a live tree, nothing to rebuild.
g_buttonInjected.store(true);
return;
}Optional improvements
Minor polish — none of this affects users, so it's your call.
Functionality notes
Non-critical observations and ideas about the feature behavior itself.
Next steps:
See the review process for details. |
|
/ai-review |
Submission reviewNote: 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. Overall this is a well-structured mod — the taskbar XAML plumbing is faithfully adapted from 1. The authoritative timer is started from the XAML injection path, which makes the timer depend on UI success and opens unload races.
All three go away with the same change: create the events, the waitable timer and the worker thread unconditionally in 2. if (!systemTrayModuleLoadedAtInit) { // line 3572
// ... install the kernelbase LoadLibraryExW hook
}The gate is "was some candidate module already loaded", but the thing that matters is "did we actually hook Fix — gate on the hook result, as taskbar-icon-size.wh.cpp#L3169 does: if (!systemTrayHookedAtInit) {
// ... install the kernelbase LoadLibraryExW hook
}Optional improvements
Minor polish — none of this affects users, so it's your call.
Functionality notes
Non-critical observations and ideas about the feature behavior itself.
Next steps:
See the review process for details. |
|
/ai-review |
Submission reviewNote: 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. Overall this is in good shape. The taskbar XAML integration is a faithful adaptation of 1. The timer button is never re-added after an in-process tray rebuild. // mods/taskbar-countdown-timer.wh.cpp:2718
if (g_timerButton &&
VisualTreeHelper::GetParent(
g_timerButton))
{
return;
}When the system tray rebuilds its XAML tree in-process — which is exactly the case the The code just below already handles "already present" correctly and safely — it looks up 2. Worth clarifying: did you actually observe stale buttons, or was this added defensively (the PR notes AI assistance, and this reads like a speculative safety net)? If it was defensive, I'd drop the path — leaving a foreign-instance element alone is strictly safer than touching it. If you did observe it, the real bug is the teardown that failed, and that's what needs fixing; removing the corpse afterwards can't be made safe from the new instance. Optional improvements
Minor polish — none of this affects users in normal operation, so it's your call.
Functionality notes
Non-critical observations and ideas about the feature behavior itself.
Next steps:
See the review process for details. |
|
/ai-review |
|
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 |
Adds a lightweight countdown timer directly to the Windows 11 taskbar.
Features:
Tested on Windows 11 25H2 build 26200.9168.
Changelog
Not applicable — this pull request introduces a new mod.
Mod authorship
If this pull request introduces a new mod, please complete the section below.
This mod was created by:
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.