Skip to content

GNOME-like dynamic virtual desktops 1.0#4517

Open
Giggigx wants to merge 1 commit into
ramensoftware:mainfrom
Giggigx:main
Open

GNOME-like dynamic virtual desktops 1.0#4517
Giggigx wants to merge 1 commit into
ramensoftware:mainfrom
Giggigx:main

Conversation

@Giggigx

@Giggigx Giggigx commented Jun 21, 2026

Copy link
Copy Markdown

transforms windows' virtual desktop system to be more like GNOME

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.

transforms windows' virtual desktop system to be more like GNOME
@m417z

m417z commented Jun 22, 2026

Copy link
Copy Markdown
Member

In addition to the review below, the primary mod description is "behave like the GNOME desktop environment". Add an explanation for those who are not familiar with GNOME. Even those who do might want to know the specific GNOME aspects that the mod implements.

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.


A few things to address before this can be merged — most importantly, this should run as a tool mod rather than be injected into explorer.exe.

This should be a "mod as a tool", not injected into explorer.exe. The mod installs no function hooks at all — it only uses SetWinEventHook, top-level window enumeration (GetTopWindow/GetNextWindow/GetWindowDesktopId), and the ImmersiveShell virtual-desktop COM interfaces, all of which work fine from any process. It also operates on the desktop as a whole and ignores anything specific to its host process. Injecting into explorer.exe has two real downsides:

  • A bug or crash in the mod destabilizes the shell.
  • It runs in every explorer.exe instance simultaneously, so multiple background threads each race to create/delete the session-global virtual desktops.

The clean fit is the dedicated-process pattern: @include windhawk.exe, rename your lifecycle callbacks to WhTool_ModInit / WhTool_ModSettingsChanged / WhTool_ModUninit, and paste the launcher boilerplate verbatim. The framework's launcher also gives you single-instance behavior for free. See the wiki (Mods as tools) and, very relevantly, virtual-desktop-helper — it does virtual-desktop management through the same COM interfaces as a tool mod (@include windhawk.exe). The launcher snippet is at the bottom of explorer-folder-hover-menu.

The background thread isn't joined on unload → crash. Wh_ModUninit posts WM_QUIT and returns immediately, but BackgroundEventThread is still running. As soon as Wh_ModUninit returns, Windhawk unloads the mod DLL, and the thread continues executing code in the now-unmapped image (the unhook/Release/CoUninitialize cleanup block) → use-after-unload crash. You need to wait for the thread to finish:

HANDLE g_hThread = nullptr;   // store the handle
// in Wh_ModInit:
g_hThread = CreateThread(NULL, 0, BackgroundEventThread, NULL, 0, &g_threadId);
// in Wh_ModUninit:
if (g_threadId) {
    PostThreadMessage(g_threadId, WM_QUIT, 0, 0);
    WaitForSingleObject(g_hThread, INFINITE);
}
if (g_hThread) CloseHandle(g_hThread);

This also fixes the currently-leaked thread handle (the CreateThread return value is discarded). (If you move to the tool-mod pattern, this becomes less critical since the dedicated process exits on unload, but it's still the correct shape.)

COM reference leak in the "no populated desktops" branch. In the Rule 1 loop, desktops.pop_back() removes the pointer from the vector without Release()-ing it, and the Cleanup: loop only releases pointers still in the vector — so every desktop removed here leaks its IVirtualDesktop:

while (totalDesktops > g_settings.initial_desktops) {
    g_pDesktopManager->RemoveDesktop(desktops.back(), desktops.front());
    desktops.back()->Release();   // <-- add this
    totalDesktops--;
    desktops.pop_back();
}

This runs whenever all desktops are empty and there's an excess to trim, which is a normal path (e.g. closing everything), so the leak accumulates over a session.

Optional improvements

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

  • Italian code comments (LoadSettings: "Legge il valore del toggle…", "Se è acceso imposta 2 desktop…"). Repo convention is English for code as well as UI; worth translating.
  • Unused #include <atomic> — there's no std::atomic in the code.
  • Dead WM_QUIT branch in the message loop. GetMessage returns 0 (FALSE) when it retrieves WM_QUIT, so while (GetMessage(...)) already exits; the else if (msg.message == WM_QUIT) break; is unreachable.
  • Log prefixes [Config]/[Logic]/[Thread] — Windhawk already prefixes log lines with the mod name; the category tags are optional noise.
  • README has no visual. The effect (desktops auto-created/removed as you fill/empty them) is inherently visual — a short GIF would help users understand it at a glance. Allowed image hosts are i.imgur.com and raw.githubusercontent.com.
  • InitializeCOM partial-failure path. If one of the two interfaces is obtained but the other fails, the guard if (g_pDesktopManager && g_pPublicDesktopManager) is false, so the next call re-CoCreateInstances and overwrites the already-held pointer without releasing it. Minor, but worth releasing/null-checking before re-acquiring.

Functionality notes

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

  • Hardcoded GUIDs + vtable layout are build-specific. IVirtualDesktopManagerInternal is undocumented and both its IID and method ordering (the Dummy* slots) change across Windows builds. The mod will silently stop working (or worse) on a build whose layout differs. This is inherent to using these private interfaces — just an FYI, and note that the code comment says "Windows 11 25H2" while the README says "build 26100+"; align those so users know exactly what was tested.
  • CountWindowsInDesktop counts every visible top-level window. It only filters on IsWindowVisible, so tool windows, owned popups, and cloaked/system windows can make a desktop look "populated" when it holds nothing the user would consider a real window. Consider the usual alt-tab-style filtering (skip WS_EX_TOOLWINDOW, owned windows, and DWMWA_CLOAKED windows) so empty-desktop detection is accurate.
  • System-wide EVENT_OBJECT_CREATE/EVENT_OBJECT_DESTROY hooks. With WINEVENT_OUTOFCONTEXT and a 0 process filter, these fire for window objects across every process on the desktop, each marshalled to your thread. The 150ms debounce keeps re-evaluation cheap, but the event volume itself is high; if you find it heavy you could lean on EVENT_OBJECT_SHOW/EVENT_OBJECT_HIDE (fewer events) plus foreground, since those track what actually changes a desktop's apparent occupancy.
  • Pinned / shown-on-all-desktops windows. GetWindowDesktopId reports these specially; worth verifying they don't keep a desktop perpetually "non-empty" or otherwise confuse the trailing-empty-desktop logic.

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