Skip to content

Add Nocturne Glucose Sparkline on Taskbar mod#4601

Open
ryceg wants to merge 3 commits into
ramensoftware:mainfrom
ryceg:add-nocturne-glucose-sparkline
Open

Add Nocturne Glucose Sparkline on Taskbar mod#4601
ryceg wants to merge 3 commits into
ramensoftware:mainfrom
ryceg:add-nocturne-glucose-sparkline

Conversation

@ryceg

@ryceg ryceg commented Jun 28, 2026

Copy link
Copy Markdown

New mod: Nocturne Glucose Sparkline on Taskbar (nocturne-glucose-sparkline), version 0.1.0.

Renders a glucose sparkline directly on the Windows 11 taskbar — recent CGM readings as a solid line, the predicted-glucose continuation as a dashed line, plus the current value, trend arrow, an optional loop-status line (IOB/COB), and a pulsing border on active server alarms. The tile auto-positions just left of the centred app icons and slides as apps open/close.

It reads a local V4SummaryResponse JSON written by the Nocturne desktop companion / packaged widget (default %LOCALAPPDATA%\Nocturne\glucose.json). There is no network access from explorer.exe — the mod only ever reads that file.

  • Windows 11 only; hooks TaskbarFrame::OnTaskbarLayoutChildBoundsChanged in Taskbar.View.dll.
  • Positioning approach adapted from m417z's taskbar mods (taskbar-labels / taskbar-icon-size).
  • License: AGPL-3.0-or-later.
  • @github matches the PR author (https://github.com/ryceg).

Rhys Gray added 2 commits June 28, 2026 21:05
Version 0.1.0.

Renders a glucose sparkline with predicted glucose, loop status (IOB/COB)
and server alerts on the Windows 11 taskbar, auto-positioned to track the
centred app icons. Reads a local V4 summary JSON written by the Nocturne
desktop companion.
- Settings: remove a colon-space from the dataPath $description so the
  settings block parses as valid YAML.
- Symbol hooks: rename the hook array to taskbarViewHooks and add a target
  module comment (Taskbar.View.dll, ExplorerExtensions.dll).
@m417z

m417z commented Jun 30, 2026

Copy link
Copy Markdown
Member

Submission review

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

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


1. The global std::thread g_readerThread can abort explorer at process shutdown. Wh_ModUninit is not called when the host process terminates (explorer restart, sign-out, reboot) — there the OS just detaches the DLL and the CRT runs global destructors during DLL_PROCESS_DETACH. At that point g_readerThread was never joined, so it is still joinable(), and ~std::thread() on a joinable thread calls std::terminate() → the host aborts. Use a heap pointer with a trivial destructor and keep the real teardown in Wh_ModUninit (by DLL_PROCESS_DETACH the worker thread is already gone, so there's nothing to clean up):

std::thread* g_readerThread = nullptr;            // trivial dtor; intentionally leaked at shutdown
// StartReader:  g_readerThread = new std::thread(ReaderLoop);
// StopReader:   g_running.store(false);
//               if (g_readerThread) { if (g_readerThread->joinable()) g_readerThread->join();
//                                     delete g_readerThread; g_readerThread = nullptr; }

The same shutdown path also runs the destructors of the other globals with non-trivial destructors (g_dispatcher, g_borderStoryboard) — releasing XAML/COM objects after their apartment may already be gone. The thread is the concrete crash; the winrt globals are worth being aware of (the real release happens in Wh_ModUninit anyway, so leaving them to leak at process exit is the safer outcome).

2. The tile is never removed when the mod is disabled (reversibility). A core Windhawk principle is that a mod's effects disappear when it is disabled. Here the tile is intentionally left behind (per the Wh_ModUninit comment), so after a plain disable a stale glucose widget sits on the taskbar until the next explorer restart. The cited use-after-free is real only if you fire-and-forget the removal onto the UI thread; during a normal disable the XAML thread is alive and pumping, so dispatch the removal and wait for it before returning — no race. You already mutate the tree from the UI thread safely in InsertWidgetIfNeeded; teardown just needs the same dispatch made synchronous. taskbar-labels does its revert in Wh_ModBeforeUninit while the hooks/UI are still live — the natural place to call TaskbarHost::RemoveTile (which already drops the host when it becomes empty).

3. fontSize, lineThickness and the sparkline width/height don't take effect on a settings change. These are baked into the XAML once in BuildWidgetXaml (FontSize="%d", StrokeThickness="%d", Canvas Width/Height), and ApplyWidget never updates them — only colors/text/visibility/points refresh. Wh_ModSettingsChanged doesn't rebuild the tile, so changing any of those settings does nothing visible until an explorer restart (and the Canvas keeps its old size while the points are mapped to the new dimensions, so they can mismatch). Simplest fix: switch to the reload variant so the tile is rebuilt with the new geometry —

BOOL Wh_ModSettingsChanged(BOOL* bReload) {
    *bReload = TRUE;   // rebuild the tile (and re-read geometry) via the reload path
    return TRUE;
}

— the reload re-runs Wh_ModInit, and InsertWidgetIfNeeded already clears the stale NocturneGlucoseTile and rebuilds it. (Reloading is justified here precisely because some settings only apply at construction.) Alternatively, rebuild the tile on the UI thread inside the existing void handler.

4. Add a screenshot/GIF to the README. The mod has an obvious visual effect (a taskbar tile), but the README has no image. Adding one (i.imgur.com or raw.githubusercontent.com) makes it much clearer what users get.

5. Minimal JSON parser optional suggestion: Consider using WinRT API instead of parsing manually. You can use the taskbar-ai-quota mod as a reference.

- Reader thread: move from a global std::thread (value) to a heap pointer
  with a trivial destructor, so DLL_PROCESS_DETACH (where Wh_ModUninit is not
  called) can't hit ~std::thread() on a still-joinable thread -> std::terminate.
  The real join+delete stays in Wh_ModUninit.
- winrt globals (g_dispatcher, g_borderStoryboard): heap-leak via reference so
  their COM destructors never run at process shutdown; release them on the UI
  thread during normal teardown instead.
- Reversibility: add Wh_ModBeforeUninit that synchronously removes the tile
  (dispatch to the UI thread and wait) while the apartment is still alive, so a
  plain disable leaves no stale widget. Capture rootGrid for the removal.
- Settings: switch to Wh_ModSettingsChanged(BOOL* bReload) with *bReload = TRUE
  so geometry-at-construction settings (fontSize, lineThickness, sparkline
  width/height) take effect by rebuilding the tile.
@ryceg

ryceg commented Jun 30, 2026

Copy link
Copy Markdown
Author

Thanks for the detailed review. Addressed the required items in 484554b:

1. Shutdown crash from the global std::thread. Moved g_readerThread to a heap pointer with a trivial destructor, exactly as suggested — at DLL_PROCESS_DETACH the pointer just leaks instead of running ~std::thread() on a still-joinable thread. The real join+delete stays in Wh_ModUninit. I also took the winrt-globals note: g_dispatcher and g_borderStoryboard are now heap-leaked (held by reference) so their COM destructors never run at process shutdown, and they're released on the UI/apartment thread during normal teardown.

2. Reversibility. Added Wh_ModBeforeUninit, which dispatches TaskbarHost::RemoveTile to the UI thread and waits (bounded) for it while the apartment is still live — so a plain disable removes the tile (and the now-empty host) with no fire-and-forget race. Captured rootGrid as a weak ref for that removal.

3. Geometry settings not applying. Switched to Wh_ModSettingsChanged(BOOL* bReload) with *bReload = TRUE, so the tile is rebuilt with the new fontSize / lineThickness / sparkline dimensions via the reload path.

On the optional items: I'll look at adding a screenshot to the README. The manual JSON parser I'll keep for now (the summary file is small and the parse path is explorer.exe-local), but I'll consider the WinRT JsonObject approach for a future version.

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