From e3c5e0c2a9c9465984a117e5893e5e9f16aa42d4 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 1 Jul 2024 19:51:33 +0200 Subject: [PATCH 01/12] Specify FriendlyTypeName on Windows file type association --- menuinst/platforms/win.py | 18 +++++++++++-- menuinst/platforms/win_utils/registry.py | 33 ++++++++++++++++-------- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index e001e7d9..870a1f53 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -444,7 +444,14 @@ def _register_file_extensions(self): exts = list(dict.fromkeys([ext.lower() for ext in extensions])) for ext in exts: identifier = self._ftype_identifier(ext) - register_file_extension(ext, identifier, command, icon=icon, mode=self.menu.mode) + register_file_extension( + ext, + identifier, + command, + icon=icon, + name=self.render_key("name"), + mode=self.menu.mode, + ) def _unregister_file_extensions(self): extensions = self.metadata["file_extensions"] @@ -465,7 +472,14 @@ def _register_url_protocols(self): icon = self.render_key("icon") for protocol in protocols: identifier = self._ftype_identifier(protocol) - register_url_protocol(protocol, command, identifier, icon=icon, mode=self.menu.mode) + register_url_protocol( + protocol, + command, + identifier, + icon=icon, + name=self.render_key("name"), + mode=self.menu.mode, + ) def _unregister_url_protocols(self): protocols = self.metadata["url_protocols"] diff --git a/menuinst/platforms/win_utils/registry.py b/menuinst/platforms/win_utils/registry.py index ac3d320b..68dfbfc2 100644 --- a/menuinst/platforms/win_utils/registry.py +++ b/menuinst/platforms/win_utils/registry.py @@ -26,7 +26,7 @@ def _reg_exe(*args, check=True): return logged_run(["reg.exe", *args, "/f"], check=True) -def register_file_extension(extension, identifier, command, icon=None, mode="user"): +def register_file_extension(extension, identifier, command, icon=None, name=None, mode="user"): """ We want to achieve this. Entries ending in / denote keys; no trailing / means named value. If the item has a value attached to it, it's written after the : symbol. @@ -44,12 +44,11 @@ def register_file_extension(extension, identifier, command, icon=None, mode="use open/ command/: "the command to be executed when opening a file with this extension" """ - with winreg.OpenKeyEx( - ( - winreg.HKEY_LOCAL_MACHINE if mode == "system" else winreg.HKEY_CURRENT_USER # HKLM - ), # HKCU - r"Software\Classes", - ) as key: + if mode == "system": + root_key = winreg.HKEY_LOCAL_MACHINE # HKLM + else: + root_key = winreg.HKEY_CURRENT_USER # HKCU + with winreg.OpenKeyEx(root_key, r"Software\Classes") as key: # First we associate an extension with a handler winreg.SetValueEx( winreg.CreateKey(key, fr"{extension}\OpenWithProgids"), @@ -72,9 +71,19 @@ def register_file_extension(extension, identifier, command, icon=None, mode="use log.debug("Created registry entry for command '%s'", command) if icon: - subkey = winreg.OpenKey(key, identifier, access=winreg.KEY_SET_VALUE) - winreg.SetValueEx(subkey, "DefaultIcon", 0, winreg.REG_SZ, icon) - log.debug("Created registry entry for icon '%s'", icon) + with winreg.OpenKey(key, identifier, access=winreg.KEY_SET_VALUE) as subkey: + winreg.SetValueEx(subkey, "DefaultIcon", 0, winreg.REG_SZ, icon) + log.debug("Created registry entry for icon '%s'", icon) + + if name: + # If not provided, Windows will take the name of the EXE in command + # This way we can override e.g. Python for a custom PyQt App name + with winreg.OpenKey(key, identifier, access=winreg.KEY_SET_VALUE) as subkey: + # NOTE: Windows <10 requires the string in a PE file, but that's too + # much work. We can just put the raw string here even if the docs say + # otherwise. + winreg.SetValueEx(subkey, "FriendlyTypeName", 0, winreg.REG_SZ, name) + log.debug("Created registry entry for friendly name '%s'", name) # TODO: We can add contextual menu items too # via f"{handler_key}\shell\\command" @@ -105,7 +114,7 @@ def unregister_file_extension(extension, identifier, mode="user"): raise -def register_url_protocol(protocol, command, identifier=None, icon=None, mode="user"): +def register_url_protocol(protocol, command, identifier=None, icon=None, name=None, mode="user"): if mode == "system": key = winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, protocol) else: @@ -118,6 +127,8 @@ def register_url_protocol(protocol, command, identifier=None, icon=None, mode="u winreg.SetValue(key, r"shell\open\command", winreg.REG_SZ, command) if icon: winreg.SetValueEx(key, "DefaultIcon", 0, winreg.REG_SZ, icon) + if name: + winreg.SetValueEx(key, "FriendlyTypeName", 0, winreg.REG_SZ, name) if identifier: # We add this one value for traceability; not required winreg.SetValueEx(key, "_menuinst", 0, winreg.REG_SZ, identifier) From 1ae9bd26a61debe3d5087d85fbe2185da921c84d Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 1 Jul 2024 20:07:29 +0200 Subject: [PATCH 02/12] allow errors on cleanup --- menuinst/platforms/win_utils/registry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/menuinst/platforms/win_utils/registry.py b/menuinst/platforms/win_utils/registry.py index 68dfbfc2..a71927fa 100644 --- a/menuinst/platforms/win_utils/registry.py +++ b/menuinst/platforms/win_utils/registry.py @@ -23,7 +23,7 @@ def _reg_exe(*args, check=True): - return logged_run(["reg.exe", *args, "/f"], check=True) + return logged_run(["reg.exe", *args, "/f"], check=check) def register_file_extension(extension, identifier, command, icon=None, name=None, mode="user"): @@ -95,7 +95,7 @@ def unregister_file_extension(extension, identifier, mode="user"): if mode == "system" else (winreg.HKEY_CURRENT_USER, "HKCU") ) - _reg_exe("delete", fr"{root_str}\Software\Classes\{identifier}") + _reg_exe("delete", fr"{root_str}\Software\Classes\{identifier}", check=False) try: with winreg.OpenKey( @@ -111,7 +111,7 @@ def unregister_file_extension(extension, identifier, mode="user"): winreg.DeleteValue(key, identifier) except Exception as exc: log.exception("Could not check key '%s' for deletion", extension, exc_info=exc) - raise + return def register_url_protocol(protocol, command, identifier=None, icon=None, name=None, mode="user"): From 6f8698b9dda8582bc411e8ab0bffb80ef8caf213 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 1 Jul 2024 20:14:21 +0200 Subject: [PATCH 03/12] try indexed icon --- menuinst/platforms/win_utils/registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/menuinst/platforms/win_utils/registry.py b/menuinst/platforms/win_utils/registry.py index a71927fa..daa0e179 100644 --- a/menuinst/platforms/win_utils/registry.py +++ b/menuinst/platforms/win_utils/registry.py @@ -72,7 +72,7 @@ def register_file_extension(extension, identifier, command, icon=None, name=None if icon: with winreg.OpenKey(key, identifier, access=winreg.KEY_SET_VALUE) as subkey: - winreg.SetValueEx(subkey, "DefaultIcon", 0, winreg.REG_SZ, icon) + winreg.SetValueEx(subkey, "DefaultIcon", 0, winreg.REG_SZ, f"@{icon},0") log.debug("Created registry entry for icon '%s'", icon) if name: @@ -126,7 +126,7 @@ def register_url_protocol(protocol, command, identifier=None, icon=None, name=No # SetValueEx creates a value with backslashes - we don't want that here winreg.SetValue(key, r"shell\open\command", winreg.REG_SZ, command) if icon: - winreg.SetValueEx(key, "DefaultIcon", 0, winreg.REG_SZ, icon) + winreg.SetValueEx(key, "DefaultIcon", 0, winreg.REG_SZ, f"@{icon},0") if name: winreg.SetValueEx(key, "FriendlyTypeName", 0, winreg.REG_SZ, name) if identifier: From 5a0ec6705806374bd9a49658fa0ec25b63e3388c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 18:15:09 +0000 Subject: [PATCH 04/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- menuinst/platforms/win_utils/registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/menuinst/platforms/win_utils/registry.py b/menuinst/platforms/win_utils/registry.py index daa0e179..69dfcee0 100644 --- a/menuinst/platforms/win_utils/registry.py +++ b/menuinst/platforms/win_utils/registry.py @@ -45,9 +45,9 @@ def register_file_extension(extension, identifier, command, icon=None, name=None command/: "the command to be executed when opening a file with this extension" """ if mode == "system": - root_key = winreg.HKEY_LOCAL_MACHINE # HKLM + root_key = winreg.HKEY_LOCAL_MACHINE # HKLM else: - root_key = winreg.HKEY_CURRENT_USER # HKCU + root_key = winreg.HKEY_CURRENT_USER # HKCU with winreg.OpenKeyEx(root_key, r"Software\Classes") as key: # First we associate an extension with a handler winreg.SetValueEx( From c037a7fd67ccb11089bdda4e01297bba5ced2409 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 2 Jul 2024 10:49:58 +0200 Subject: [PATCH 05/12] expose app_name and AUMI too --- menuinst/platforms/win.py | 6 +- menuinst/platforms/win_utils/registry.py | 82 +++++++++++++++++------- 2 files changed, 64 insertions(+), 24 deletions(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index 870a1f53..1fb05978 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -449,7 +449,8 @@ def _register_file_extensions(self): identifier, command, icon=icon, - name=self.render_key("name"), + app_name=self.render_key("name"), + app_user_model_id=self._app_user_model_id(), mode=self.menu.mode, ) @@ -477,7 +478,8 @@ def _register_url_protocols(self): command, identifier, icon=icon, - name=self.render_key("name"), + app_name=self.render_key("name"), + app_user_model_id=self._app_user_model_id(), mode=self.menu.mode, ) diff --git a/menuinst/platforms/win_utils/registry.py b/menuinst/platforms/win_utils/registry.py index 69dfcee0..aadf9e6d 100644 --- a/menuinst/platforms/win_utils/registry.py +++ b/menuinst/platforms/win_utils/registry.py @@ -26,7 +26,16 @@ def _reg_exe(*args, check=True): return logged_run(["reg.exe", *args, "/f"], check=check) -def register_file_extension(extension, identifier, command, icon=None, name=None, mode="user"): +def register_file_extension( + extension, + identifier, + command, + icon=None, + app_name=None, + friendly_type_name=None, + app_user_model_id=None, + mode="user", +): """ We want to achieve this. Entries ending in / denote keys; no trailing / means named value. If the item has a value attached to it, it's written after the : symbol. @@ -41,7 +50,8 @@ def register_file_extension(extension, identifier, command, icon=None, name=None /: "a description of the file being handled" DefaultIcon: "path to the app icon" shell/ - open/ + open/: "Name of the program" + icon: "path to the app icon" command/: "the command to be executed when opening a file with this extension" """ if mode == "system": @@ -51,7 +61,7 @@ def register_file_extension(extension, identifier, command, icon=None, name=None with winreg.OpenKeyEx(root_key, r"Software\Classes") as key: # First we associate an extension with a handler winreg.SetValueEx( - winreg.CreateKey(key, fr"{extension}\OpenWithProgids"), + winreg.CreateKey(key, rf"{extension}\OpenWithProgids"), identifier, 0, winreg.REG_SZ, @@ -68,22 +78,34 @@ def register_file_extension(extension, identifier, command, icon=None, name=None subkey = rf"{identifier}\shell\open\command" # Use SetValue to create subkeys as necessary winreg.SetValue(key, subkey, winreg.REG_SZ, command) - log.debug("Created registry entry for command '%s'", command) + if app_name: + with winreg.OpenKey( + key, rf"{identifier}\shell\open", access=winreg.KEY_SET_VALUE + ) as subkey: + winreg.SetValueEx(subkey, "", 0, winreg.REG_SZ, app_name) + log.debug("Created registry entry for command name '%s'", app_name) + + if app_user_model_id: + with winreg.OpenKey(key, identifier, access=winreg.KEY_SET_VALUE) as subkey: + winreg.SetValueEx(subkey, "AppUserModelID", 0, winreg.REG_SZ, app_user_model_id) + log.debug("Created registry entry for AUMI '%s'", icon) if icon: with winreg.OpenKey(key, identifier, access=winreg.KEY_SET_VALUE) as subkey: - winreg.SetValueEx(subkey, "DefaultIcon", 0, winreg.REG_SZ, f"@{icon},0") - log.debug("Created registry entry for icon '%s'", icon) - - if name: - # If not provided, Windows will take the name of the EXE in command - # This way we can override e.g. Python for a custom PyQt App name + winreg.SetValueEx(subkey, "DefaultIcon", 0, winreg.REG_SZ, icon) + with winreg.OpenKey( + key, rf"{identifier}\shell\open", access=winreg.KEY_SET_VALUE + ) as subkey: + winreg.SetValueEx(subkey, "Icon", 0, winreg.REG_SZ, icon) + log.debug("Created registry entries for icon '%s'", icon) + + if friendly_type_name: with winreg.OpenKey(key, identifier, access=winreg.KEY_SET_VALUE) as subkey: # NOTE: Windows <10 requires the string in a PE file, but that's too # much work. We can just put the raw string here even if the docs say # otherwise. - winreg.SetValueEx(subkey, "FriendlyTypeName", 0, winreg.REG_SZ, name) - log.debug("Created registry entry for friendly name '%s'", name) + winreg.SetValueEx(subkey, "FriendlyTypeName", 0, winreg.REG_SZ, friendly_type_name) + log.debug("Created registry entry for friendly type name '%s'", friendly_type_name) # TODO: We can add contextual menu items too # via f"{handler_key}\shell\\command" @@ -95,11 +117,11 @@ def unregister_file_extension(extension, identifier, mode="user"): if mode == "system" else (winreg.HKEY_CURRENT_USER, "HKCU") ) - _reg_exe("delete", fr"{root_str}\Software\Classes\{identifier}", check=False) + _reg_exe("delete", rf"{root_str}\Software\Classes\{identifier}", check=False) try: with winreg.OpenKey( - root, fr"Software\Classes\{extension}\OpenWithProgids", 0, winreg.KEY_ALL_ACCESS + root, rf"Software\Classes\{extension}\OpenWithProgids", 0, winreg.KEY_ALL_ACCESS ) as key: try: winreg.QueryValueEx(key, identifier) @@ -114,21 +136,37 @@ def unregister_file_extension(extension, identifier, mode="user"): return -def register_url_protocol(protocol, command, identifier=None, icon=None, name=None, mode="user"): +def register_url_protocol( + protocol, + command, + identifier=None, + icon=None, + app_name=None, + app_user_model_id=None, + mode="user", +): if mode == "system": key = winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, protocol) else: - key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, fr"Software\Classes\{protocol}") + key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, rf"Software\Classes\{protocol}") with key: winreg.SetValueEx(key, "", 0, winreg.REG_SZ, f"URL:{protocol.title()}") winreg.SetValueEx(key, "URL Protocol", 0, winreg.REG_SZ, "") # SetValue creates sub keys when slashes are present; # SetValueEx creates a value with backslashes - we don't want that here winreg.SetValue(key, r"shell\open\command", winreg.REG_SZ, command) + if app_name: + with winreg.OpenKey(key, r"shell\open", access=winreg.KEY_SET_VALUE) as subkey: + winreg.SetValueEx(subkey, "", 0, winreg.REG_SZ, app_name) + log.debug("Created registry entry for command name '%s'", app_name) if icon: - winreg.SetValueEx(key, "DefaultIcon", 0, winreg.REG_SZ, f"@{icon},0") - if name: - winreg.SetValueEx(key, "FriendlyTypeName", 0, winreg.REG_SZ, name) + winreg.SetValueEx(key, "DefaultIcon", 0, winreg.REG_SZ, icon) + with winreg.OpenKey(key, r"shell\open", access=winreg.KEY_SET_VALUE) as subkey: + winreg.SetValueEx(subkey, "Icon", 0, winreg.REG_SZ, icon) + log.debug("Created registry entries for command icon '%s'", icon) + if app_user_model_id: + winreg.SetValueEx(key, "AppUserModelID", 0, winreg.REG_SZ, app_user_model_id) + log.debug("Created registry entries for AUMI '%s'", app_user_model_id) if identifier: # We add this one value for traceability; not required winreg.SetValueEx(key, "_menuinst", 0, winreg.REG_SZ, identifier) @@ -137,10 +175,10 @@ def register_url_protocol(protocol, command, identifier=None, icon=None, name=No def unregister_url_protocol(protocol, identifier=None, mode="user"): if mode == "system": key_tuple = winreg.HKEY_CLASSES_ROOT, protocol - key_str = fr"HKCR\{protocol}" + key_str = rf"HKCR\{protocol}" else: - key_tuple = winreg.HKEY_CURRENT_USER, fr"Software\Classes\{protocol}" - key_str = fr"HKCU\Software\Classes\{protocol}" + key_tuple = winreg.HKEY_CURRENT_USER, rf"Software\Classes\{protocol}" + key_str = rf"HKCU\Software\Classes\{protocol}" try: with winreg.OpenKey(*key_tuple) as key: value, _ = winreg.QueryValueEx(key, "_menuinst") From 774ecfbd3c221820f0b004b48e421b1598d08f34 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 2 Jul 2024 11:57:08 +0200 Subject: [PATCH 06/12] Enable some code cleanups --- menuinst/platforms/win_utils/registry.py | 158 ++++++++++++----------- 1 file changed, 82 insertions(+), 76 deletions(-) diff --git a/menuinst/platforms/win_utils/registry.py b/menuinst/platforms/win_utils/registry.py index aadf9e6d..c9b8d1bb 100644 --- a/menuinst/platforms/win_utils/registry.py +++ b/menuinst/platforms/win_utils/registry.py @@ -55,60 +55,36 @@ def register_file_extension( command/: "the command to be executed when opening a file with this extension" """ if mode == "system": - root_key = winreg.HKEY_LOCAL_MACHINE # HKLM + key = "HKEY_LOCAL_MACHINE/Software/Classes" # HKLM else: - root_key = winreg.HKEY_CURRENT_USER # HKCU - with winreg.OpenKeyEx(root_key, r"Software\Classes") as key: - # First we associate an extension with a handler - winreg.SetValueEx( - winreg.CreateKey(key, rf"{extension}\OpenWithProgids"), - identifier, - 0, - winreg.REG_SZ, - "", # presence of the key is enough - ) - log.debug("Created registry entry for extension '%s'", extension) - - # Now we register the handler - handler_desc = f"{extension} {identifier} handler" - winreg.SetValue(key, identifier, winreg.REG_SZ, handler_desc) - log.debug("Created registry entry for handler '%s'", identifier) - - # and set the 'open' command - subkey = rf"{identifier}\shell\open\command" - # Use SetValue to create subkeys as necessary - winreg.SetValue(key, subkey, winreg.REG_SZ, command) - if app_name: - with winreg.OpenKey( - key, rf"{identifier}\shell\open", access=winreg.KEY_SET_VALUE - ) as subkey: - winreg.SetValueEx(subkey, "", 0, winreg.REG_SZ, app_name) - log.debug("Created registry entry for command name '%s'", app_name) - - if app_user_model_id: - with winreg.OpenKey(key, identifier, access=winreg.KEY_SET_VALUE) as subkey: - winreg.SetValueEx(subkey, "AppUserModelID", 0, winreg.REG_SZ, app_user_model_id) - log.debug("Created registry entry for AUMI '%s'", icon) - - if icon: - with winreg.OpenKey(key, identifier, access=winreg.KEY_SET_VALUE) as subkey: - winreg.SetValueEx(subkey, "DefaultIcon", 0, winreg.REG_SZ, icon) - with winreg.OpenKey( - key, rf"{identifier}\shell\open", access=winreg.KEY_SET_VALUE - ) as subkey: - winreg.SetValueEx(subkey, "Icon", 0, winreg.REG_SZ, icon) - log.debug("Created registry entries for icon '%s'", icon) - - if friendly_type_name: - with winreg.OpenKey(key, identifier, access=winreg.KEY_SET_VALUE) as subkey: - # NOTE: Windows <10 requires the string in a PE file, but that's too - # much work. We can just put the raw string here even if the docs say - # otherwise. - winreg.SetValueEx(subkey, "FriendlyTypeName", 0, winreg.REG_SZ, friendly_type_name) - log.debug("Created registry entry for friendly type name '%s'", friendly_type_name) - - # TODO: We can add contextual menu items too - # via f"{handler_key}\shell\\command" + key = "HKEY_CURRENT_USER/Software/Classes" # HKCU + + # First we associate an extension with a handler (presence of key is enough) + regvalue(f"{key}/{extension}/OpenWithProgids/{identifier}/@", "") + + # Now we register the handler + regvalue(f"{key}/{identifier}/@", f"{extension} {identifier} handler") + + # Set the 'open' command + regvalue(f"{key}/{identifier}/shell/open/command/@", command) + if app_name: + regvalue(f"{key}/{identifier}/shell/open/@", app_name) + + if app_user_model_id: + regvalue(f"{key}/{identifier}/AppUserModelID", app_user_model_id) + + if icon: + regvalue(f"{key}/{identifier}/DefaultIcon/@", icon) + regvalue(f"{key}/{identifier}/shell/open/Icon", icon) + + if friendly_type_name: + # NOTE: Windows <10 requires the string in a PE file, but that's too + # much work. We can just put the raw string here even if the docs say + # otherwise. + regvalue(f"{key}/{identifier}/FriendlyTypeName", friendly_type_name) + + # TODO: We can add contextual menu items too + # via f"{handler_key}\shell\\command" def unregister_file_extension(extension, identifier, mode="user"): @@ -146,30 +122,22 @@ def register_url_protocol( mode="user", ): if mode == "system": - key = winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, protocol) + key = f"HKEY_CLASSES_ROOT/{protocol}" else: - key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, rf"Software\Classes\{protocol}") - with key: - winreg.SetValueEx(key, "", 0, winreg.REG_SZ, f"URL:{protocol.title()}") - winreg.SetValueEx(key, "URL Protocol", 0, winreg.REG_SZ, "") - # SetValue creates sub keys when slashes are present; - # SetValueEx creates a value with backslashes - we don't want that here - winreg.SetValue(key, r"shell\open\command", winreg.REG_SZ, command) - if app_name: - with winreg.OpenKey(key, r"shell\open", access=winreg.KEY_SET_VALUE) as subkey: - winreg.SetValueEx(subkey, "", 0, winreg.REG_SZ, app_name) - log.debug("Created registry entry for command name '%s'", app_name) - if icon: - winreg.SetValueEx(key, "DefaultIcon", 0, winreg.REG_SZ, icon) - with winreg.OpenKey(key, r"shell\open", access=winreg.KEY_SET_VALUE) as subkey: - winreg.SetValueEx(subkey, "Icon", 0, winreg.REG_SZ, icon) - log.debug("Created registry entries for command icon '%s'", icon) - if app_user_model_id: - winreg.SetValueEx(key, "AppUserModelID", 0, winreg.REG_SZ, app_user_model_id) - log.debug("Created registry entries for AUMI '%s'", app_user_model_id) - if identifier: - # We add this one value for traceability; not required - winreg.SetValueEx(key, "_menuinst", 0, winreg.REG_SZ, identifier) + key = f"HKEY_CURRENT_USER/Software/Classes/{protocol}" + regvalue(f"{key}/@", f"URL:{protocol.title()}") + regvalue(f"{key}/URL Protocol", "") + regvalue(f"{key}/shell/open/command/@", command) + if app_name: + regvalue(f"{key}/shell/open/@", app_name) + if icon: + regvalue(f"{key}/DefaultIcon/@", icon) + regvalue(f"{key}/shell/open/Icon", icon) + if app_user_model_id: + regvalue(f"{key}/AppUserModelId", app_user_model_id) + if identifier: + # We add this one value for traceability; not required + regvalue(f"{key}/_menuinst", identifier) def unregister_url_protocol(protocol, identifier=None, mode="user"): @@ -189,3 +157,41 @@ def unregister_url_protocol(protocol, identifier=None, mode="user"): if delete: _reg_exe("delete", key_str, check=False) + + +def regvalue(key, value, value_type=winreg.REG_SZ, raise_on_errors=True): + """ + Convenience wrapper to set different types of registry values. + + For practical purposes we distinguish between three cases: + + - A key with no value (think of a directory with no contents). + Use value = "". + - A key with an unnamed value (think of a directory with a file 'index.html') + Use a key with '@' as the last component. + - A key with named values (think of non-index.html files in the directory) + + The first component of the key is the root, and must be one of the winreg.HKEY_* + variable _names_ (their actual value will be fetched from winreg). + + Key must be at least three components long. + """ + log.debug("Setting registry value %s = '%s'", key, value) + key = original_key = key.replace("\\", "/").strip("/") + root, *midkey, subkey, named_value = key.split("/") + rootkey = getattr(winreg, root) + access = winreg.KEY_SET_VALUE + try: + if named_value == "@": + if midkey: + winreg.CreateKey(rootkey, "\\".join(midkey)) # ensure it exists + with winreg.OpenKey(rootkey, "\\".join(midkey), access=access) as key: + winreg.SetValue(key, subkey, value_type, value) + else: + winreg.CreateKey(rootkey, "\\".join([*midkey, subkey])) # ensure it exists + with winreg.OpenKey(rootkey, "\\".join([*midkey, subkey]), access=access) as key: + winreg.SetValueEx(key, named_value, 0, value_type, value) + except OSError as exc: + if raise_on_errors: + raise + log.warning("Could not set %s to %s", original_key, value, exc_info=exc) From b9dfa8ef19fadf270b4d7e71a64600774a3a53b3 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 2 Jul 2024 13:12:28 +0200 Subject: [PATCH 07/12] Debug on Windows --- menuinst/platforms/win.py | 20 +++++++++++++----- menuinst/platforms/win_utils/registry.py | 27 ++++++++++++++++++------ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index 1fb05978..22863f80 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -16,6 +16,7 @@ from .win_utils.knownfolders import folder_path as windows_folder_path from .win_utils.knownfolders import windows_terminal_settings_files from .win_utils.registry import ( + notify_shell_changes, register_file_extension, register_url_protocol, unregister_file_extension, @@ -198,14 +199,19 @@ def create(self) -> Tuple[Path, ...]: for location in self.menu.terminal_profile_locations: self._add_remove_windows_terminal_profile(location, remove=False) - self._register_file_extensions() - self._register_url_protocols() + changed_extensions = self._register_file_extensions() + changed_protocols = self._register_url_protocols() + if changed_extensions or changed_protocols: + notify_shell_changes() return paths def remove(self) -> Tuple[Path, ...]: - self._unregister_file_extensions() - self._unregister_url_protocols() + changed_extensions = self._unregister_file_extensions() + changed_protocols = self._unregister_url_protocols() + if changed_extensions or changed_protocols: + notify_shell_changes() + for location in self.menu.terminal_profile_locations: self._add_remove_windows_terminal_profile(location, remove=True) @@ -433,7 +439,7 @@ def _cmd_ftype(identifier, command=None, query=False, remove=False) -> Completed arg = f"{identifier}=" return logged_run(["cmd", "/D", "/C", f"assoc {arg}"], check=True) - def _register_file_extensions(self): + def _register_file_extensions(self) -> bool: """WIP""" extensions = self.metadata["file_extensions"] if not extensions: @@ -453,6 +459,7 @@ def _register_file_extensions(self): app_user_model_id=self._app_user_model_id(), mode=self.menu.mode, ) + return True def _unregister_file_extensions(self): extensions = self.metadata["file_extensions"] @@ -463,6 +470,7 @@ def _unregister_file_extensions(self): for ext in exts: identifier = self._ftype_identifier(ext) unregister_file_extension(ext, identifier, mode=self.menu.mode) + return True def _register_url_protocols(self): "See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85)" # noqa @@ -482,6 +490,7 @@ def _register_url_protocols(self): app_user_model_id=self._app_user_model_id(), mode=self.menu.mode, ) + return True def _unregister_url_protocols(self): protocols = self.metadata["url_protocols"] @@ -490,6 +499,7 @@ def _unregister_url_protocols(self): for protocol in protocols: identifier = self._ftype_identifier(protocol) unregister_url_protocol(protocol, identifier, mode=self.menu.mode) + return True def _app_user_model_id(self): aumi = self.render_key("app_user_model_id") diff --git a/menuinst/platforms/win_utils/registry.py b/menuinst/platforms/win_utils/registry.py index c9b8d1bb..f3714d85 100644 --- a/menuinst/platforms/win_utils/registry.py +++ b/menuinst/platforms/win_utils/registry.py @@ -14,6 +14,7 @@ Mnemonic: SetValueEx for "excalars" (scalars, named values) """ +import ctypes import winreg from logging import getLogger @@ -60,22 +61,26 @@ def register_file_extension( key = "HKEY_CURRENT_USER/Software/Classes" # HKCU # First we associate an extension with a handler (presence of key is enough) - regvalue(f"{key}/{extension}/OpenWithProgids/{identifier}/@", "") + regvalue(f"{key}/{extension}/OpenWithProgids/{identifier}", "") # Now we register the handler - regvalue(f"{key}/{identifier}/@", f"{extension} {identifier} handler") + regvalue(f"{key}/{identifier}/@", f"{extension} {identifier} file") # Set the 'open' command regvalue(f"{key}/{identifier}/shell/open/command/@", command) if app_name: regvalue(f"{key}/{identifier}/shell/open/@", app_name) + regvalue(f"{key}/{identifier}/FriendlyAppName/@", app_name) + regvalue(f"{key}/{identifier}/shell/open/FriendlyAppName", app_name) if app_user_model_id: regvalue(f"{key}/{identifier}/AppUserModelID", app_user_model_id) if icon: - regvalue(f"{key}/{identifier}/DefaultIcon/@", icon) - regvalue(f"{key}/{identifier}/shell/open/Icon", icon) + # NOTE: This doesn't change the icon next in the Open With menu + # This defaults to whatever the command executable is shipping + regvalue(f"{key}/{identifier}/DefaultIcon/@", f'{icon},0') + regvalue(f"{key}/{identifier}/shell/open/Icon", f'{icon},0') if friendly_type_name: # NOTE: Windows <10 requires the string in a PE file, but that's too @@ -130,9 +135,11 @@ def register_url_protocol( regvalue(f"{key}/shell/open/command/@", command) if app_name: regvalue(f"{key}/shell/open/@", app_name) + regvalue(f"{key}/FriendlyAppName/@", app_name) + regvalue(f"{key}/shell/open/FriendlyAppName", app_name) if icon: - regvalue(f"{key}/DefaultIcon/@", icon) - regvalue(f"{key}/shell/open/Icon", icon) + regvalue(f"{key}/DefaultIcon/@", f'"{icon}"') + regvalue(f"{key}/shell/open/Icon", f'"{icon}"') if app_user_model_id: regvalue(f"{key}/AppUserModelId", app_user_model_id) if identifier: @@ -195,3 +202,11 @@ def regvalue(key, value, value_type=winreg.REG_SZ, raise_on_errors=True): if raise_on_errors: raise log.warning("Could not set %s to %s", original_key, value, exc_info=exc) + + +def notify_shell_changes(): + shell32 = ctypes.OleDLL('shell32') + shell32.SHChangeNotify.restype = None + event = 0x08000000 # SHCNE_ASSOCCHANGED + flags = 0x0000 # SHCNF_IDLIST + shell32.SHChangeNotify(event, flags, None, None) From 006c579f76c0f43f438c60392ded42dad24f4911 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Tue, 2 Jul 2024 13:23:46 +0200 Subject: [PATCH 08/12] do not quote icons --- menuinst/platforms/win_utils/registry.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/menuinst/platforms/win_utils/registry.py b/menuinst/platforms/win_utils/registry.py index f3714d85..fe04b598 100644 --- a/menuinst/platforms/win_utils/registry.py +++ b/menuinst/platforms/win_utils/registry.py @@ -79,8 +79,8 @@ def register_file_extension( if icon: # NOTE: This doesn't change the icon next in the Open With menu # This defaults to whatever the command executable is shipping - regvalue(f"{key}/{identifier}/DefaultIcon/@", f'{icon},0') - regvalue(f"{key}/{identifier}/shell/open/Icon", f'{icon},0') + regvalue(f"{key}/{identifier}/DefaultIcon/@", icon) + regvalue(f"{key}/{identifier}/shell/open/Icon", icon) if friendly_type_name: # NOTE: Windows <10 requires the string in a PE file, but that's too @@ -138,8 +138,8 @@ def register_url_protocol( regvalue(f"{key}/FriendlyAppName/@", app_name) regvalue(f"{key}/shell/open/FriendlyAppName", app_name) if icon: - regvalue(f"{key}/DefaultIcon/@", f'"{icon}"') - regvalue(f"{key}/shell/open/Icon", f'"{icon}"') + regvalue(f"{key}/DefaultIcon/@", icon) + regvalue(f"{key}/shell/open/Icon", icon) if app_user_model_id: regvalue(f"{key}/AppUserModelId", app_user_model_id) if identifier: From 869665af253a437c5b515c58eb6e1372cbd462c4 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 3 Jul 2024 11:33:02 +0200 Subject: [PATCH 09/12] pre-commit --- menuinst/platforms/win_utils/registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/menuinst/platforms/win_utils/registry.py b/menuinst/platforms/win_utils/registry.py index fe04b598..1bf0f221 100644 --- a/menuinst/platforms/win_utils/registry.py +++ b/menuinst/platforms/win_utils/registry.py @@ -207,6 +207,6 @@ def regvalue(key, value, value_type=winreg.REG_SZ, raise_on_errors=True): def notify_shell_changes(): shell32 = ctypes.OleDLL('shell32') shell32.SHChangeNotify.restype = None - event = 0x08000000 # SHCNE_ASSOCCHANGED - flags = 0x0000 # SHCNF_IDLIST + event = 0x08000000 # SHCNE_ASSOCCHANGED + flags = 0x0000 # SHCNF_IDLIST shell32.SHChangeNotify(event, flags, None, None) From 7a03b1d68154f9411da8b4a2c9b9c0d2d8d27988 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 3 Jul 2024 11:34:08 +0200 Subject: [PATCH 10/12] add news --- news/225-friendly-open-with | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 news/225-friendly-open-with diff --git a/news/225-friendly-open-with b/news/225-friendly-open-with new file mode 100644 index 00000000..14ef86c2 --- /dev/null +++ b/news/225-friendly-open-with @@ -0,0 +1,19 @@ +### Enhancements + +* + +### Bug fixes + +* Display shortcut name in Windows' "Open with" menu entries. (#225) + +### Deprecations + +* + +### Docs + +* + +### Other + +* From cdb3cf4b29c03805a496b44f95108df4eeb54ead Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 3 Jul 2024 11:43:48 +0200 Subject: [PATCH 11/12] add docstrings --- menuinst/platforms/win_utils/registry.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/menuinst/platforms/win_utils/registry.py b/menuinst/platforms/win_utils/registry.py index 1bf0f221..45bb9148 100644 --- a/menuinst/platforms/win_utils/registry.py +++ b/menuinst/platforms/win_utils/registry.py @@ -49,10 +49,13 @@ def register_file_extension( ... /: "a description of the file being handled" - DefaultIcon: "path to the app icon" + DefaultIcon/: "path to the app icon" + FriendlyAppName/: "Name of the program" + AppUserModelID: "AUMI string" shell/ open/: "Name of the program" icon: "path to the app icon" + FriendlyAppName: "name of the program" command/: "the command to be executed when opening a file with this extension" """ if mode == "system": @@ -181,7 +184,7 @@ def regvalue(key, value, value_type=winreg.REG_SZ, raise_on_errors=True): The first component of the key is the root, and must be one of the winreg.HKEY_* variable _names_ (their actual value will be fetched from winreg). - Key must be at least three components long. + Key must be at least three components long (root key, *key, @ or named value). """ log.debug("Setting registry value %s = '%s'", key, value) key = original_key = key.replace("\\", "/").strip("/") @@ -205,6 +208,11 @@ def regvalue(key, value, value_type=winreg.REG_SZ, raise_on_errors=True): def notify_shell_changes(): + """ + Needed to propagate registry changes without having to reboot. + + https://discuss.python.org/t/is-there-a-library-to-change-windows-10-default-program-icon/5846/2 + """ shell32 = ctypes.OleDLL('shell32') shell32.SHChangeNotify.restype = None event = 0x08000000 # SHCNE_ASSOCCHANGED From e31fb562c3913c565eb0d1aadcb696796837b718 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Wed, 3 Jul 2024 17:51:37 +0200 Subject: [PATCH 12/12] consistent types --- menuinst/platforms/win.py | 14 +++++++------- menuinst/platforms/win_utils/registry.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index 22863f80..c80f7076 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -443,7 +443,7 @@ def _register_file_extensions(self) -> bool: """WIP""" extensions = self.metadata["file_extensions"] if not extensions: - return + return False command = " ".join(self._process_command(with_arg1=True)) icon = self.render_key("icon") @@ -461,10 +461,10 @@ def _register_file_extensions(self) -> bool: ) return True - def _unregister_file_extensions(self): + def _unregister_file_extensions(self) -> bool: extensions = self.metadata["file_extensions"] if not extensions: - return + return False exts = list(dict.fromkeys([ext.lower() for ext in extensions])) for ext in exts: @@ -472,11 +472,11 @@ def _unregister_file_extensions(self): unregister_file_extension(ext, identifier, mode=self.menu.mode) return True - def _register_url_protocols(self): + def _register_url_protocols(self) -> bool: "See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85)" # noqa protocols = self.metadata["url_protocols"] if not protocols: - return + return False command = " ".join(self._process_command(with_arg1=True)) icon = self.render_key("icon") for protocol in protocols: @@ -492,10 +492,10 @@ def _register_url_protocols(self): ) return True - def _unregister_url_protocols(self): + def _unregister_url_protocols(self) -> bool: protocols = self.metadata["url_protocols"] if not protocols: - return + return False for protocol in protocols: identifier = self._ftype_identifier(protocol) unregister_url_protocol(protocol, identifier, mode=self.menu.mode) diff --git a/menuinst/platforms/win_utils/registry.py b/menuinst/platforms/win_utils/registry.py index 45bb9148..550ef9f5 100644 --- a/menuinst/platforms/win_utils/registry.py +++ b/menuinst/platforms/win_utils/registry.py @@ -117,7 +117,7 @@ def unregister_file_extension(extension, identifier, mode="user"): winreg.DeleteValue(key, identifier) except Exception as exc: log.exception("Could not check key '%s' for deletion", extension, exc_info=exc) - return + return False def register_url_protocol(