From f705497b80ba86a8d63ee1cba6b026a1c5d98cec Mon Sep 17 00:00:00 2001 From: Tommy Carpenter <9419125+tommyjcarpenter@users.noreply.github.com> Date: Fri, 26 Jun 2026 06:37:33 -0400 Subject: [PATCH] Refresh Explorer association cache after registering file associations `install_file_associations` wrote each per-user class registration but never told Explorer to refresh its cached file-type defaults, so a newly registered association only took effect after the next sign-in or an Explorer restart. For an extension Explorer had already resolved to another app (e.g. `.json` -> Visual Studio) the stale default kept winning on double-click even though the new type showed up in "Open with". Extensions nothing else had claimed (`.md`, `.yaml`, `.toml`) had no cached default, so they resolved fresh to the new handler and masked the gap. Changes: - Add `_notify_assoc_changed()`, which calls `SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, ...)` via `ctypes` to invalidate Explorer's association cache. It uses a local `import ctypes` to match the module's existing pattern for Windows-only APIs (`winreg`), so the module still imports cleanly on the Linux CI runner. - Call it once in `install_file_associations` after the registration loop, so the broadcast fires a single time regardless of how many extensions were registered. No unit test added: the helper is a thin `shell32` call with no return value to assert on; the existing import smoke test already covers it loading. --- bootstrap/os_specific/windows.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/bootstrap/os_specific/windows.py b/bootstrap/os_specific/windows.py index 94dd314..96f3af5 100644 --- a/bootstrap/os_specific/windows.py +++ b/bootstrap/os_specific/windows.py @@ -146,8 +146,30 @@ def set_file_assoc(ext, progid, cmd, name=None, icon=None, label=None): pass +def _notify_assoc_changed(): + """Broadcast SHCNE_ASSOCCHANGED so Explorer drops its cached file-type defaults. + + Writing HKCU\\Software\\Classes alone isn't enough: Explorer caches the resolved + default per extension, so a freshly written association doesn't take effect until the + next sign-in or an Explorer restart. For an extension Explorer already resolved to a + different app (e.g. .json -> Visual Studio) the stale default keeps winning until the + cache is invalidated — which is why a newly registered type appears in "Open with" yet + double-click still opens the old app. SHChangeNotify is the documented, admin-free way + to invalidate that cache; it lives in shell32 and returns nothing. + """ + import ctypes + + SHCNE_ASSOCCHANGED = 0x08000000 + SHCNF_IDLIST = 0x0000 + ctypes.windll.shell32.SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, None, None) + + def install_file_associations(config): - """Top-level entry: reads config['file_associations']['windows'] and registers each.""" + """Top-level entry: reads config['file_associations']['windows'] and registers each. + + After registering every entry, broadcasts a single SHCNE_ASSOCCHANGED so Explorer + picks up the new defaults immediately instead of on the next sign-in. + """ if "file_associations" not in config: log.skip("No file_associations in config") return @@ -168,3 +190,5 @@ def install_file_associations(config): icon=assoc.get("icon"), label=assoc.get("label"), ) + log.action("refreshing Explorer association cache (SHCNE_ASSOCCHANGED)") + _notify_assoc_changed()