From 3555cd2925cc7f84c1d6be7adbad2cf90a143681 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Wed, 1 May 2024 10:09:30 -0700 Subject: [PATCH 01/35] Add terminal profile option to Windows schema --- menuinst/_schema.py | 6 ++++++ menuinst/data/menuinst.default.json | 1 + menuinst/data/menuinst.schema.json | 13 +++++++++++++ 3 files changed, 20 insertions(+) diff --git a/menuinst/_schema.py b/menuinst/_schema.py index d5aef586..42a6a643 100644 --- a/menuinst/_schema.py +++ b/menuinst/_schema.py @@ -76,6 +76,12 @@ class Windows(BasePlatformSpecific): "Whether to create a desktop icon in addition to the Start Menu item." quicklaunch: Optional[bool] = True "Whether to create a quick launch icon in addition to the Start Menu item." + terminal_profile: Optional[Union[bool, constr(min_length=1)]] = False + """ + Whether to create a Windows Terminal profile. + If set to a string, that string will be used as the display name of the profile. + Otherwise, it will default to the name of the menu item. + """ url_protocols: Optional[List[constr(regex=r"\S+")]] = None "URL protocols that will be associated with this program." file_extensions: Optional[List[constr(regex=r"\.\S*")]] = None diff --git a/menuinst/data/menuinst.default.json b/menuinst/data/menuinst.default.json index b220ad39..85622cf7 100644 --- a/menuinst/data/menuinst.default.json +++ b/menuinst/data/menuinst.default.json @@ -56,6 +56,7 @@ "win": { "desktop": true, "quicklaunch": true, + "terminal_profile": false, "url_protocols": null, "file_extensions": null, "app_user_model_id": null diff --git a/menuinst/data/menuinst.schema.json b/menuinst/data/menuinst.schema.json index 1d4f5a5b..5eec07a5 100644 --- a/menuinst/data/menuinst.schema.json +++ b/menuinst/data/menuinst.schema.json @@ -566,6 +566,19 @@ "default": true, "type": "boolean" }, + "terminal_profile": { + "title": "Terminal Profile", + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "minLength": 1 + } + ] + }, "url_protocols": { "title": "Url Protocols", "type": "array", From 575e4e2ae3d6d16181be1114d8261ff7d34b0d5c Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Wed, 1 May 2024 16:08:58 -0700 Subject: [PATCH 02/35] Add Windows Terminal profile installer --- menuinst/platforms/win.py | 57 ++++++++++++++++++++ menuinst/platforms/win_utils/knownfolders.py | 1 + 2 files changed, 58 insertions(+) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index b7a1a0c4..c9b6529d 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -1,6 +1,7 @@ """ """ +import json import os import shutil import warnings @@ -64,6 +65,22 @@ def quick_launch_location(self) -> Path: def desktop_location(self) -> Path: return Path(windows_folder_path(self.mode, False, "desktop")) + @property + def terminal_profile_location(self) -> Path: + if self.mode == "system": + warnings.warn("Terminal profiles are not available for system level installs") + return + profile_path = ( + Path(windows_folder_path(self.mode, False, "localappdata")) + / "Packages" + / "Microsoft.WindowsTerminal_8wekyb3d8bbwe" + / "LocalState" + / "settings.json" + ) + if profile_path.exists(): + return profile_path + return + @property def placeholders(self) -> Dict[str, str]: placeholders = super().placeholders @@ -165,6 +182,7 @@ def create(self) -> Tuple[Path, ...]: self._app_user_model_id(), ) + self._add_remove_windows_terminal_profile(remove=False) self._register_file_extensions() self._register_url_protocols() @@ -173,6 +191,7 @@ def create(self) -> Tuple[Path, ...]: def remove(self) -> Tuple[Path, ...]: self._unregister_file_extensions() self._unregister_url_protocols() + self._add_remove_windows_terminal_profile(remove=True) paths = self._paths() for path in paths: @@ -293,6 +312,44 @@ def _process_command(self, with_arg1=False) -> Tuple[str]: command.append("%1") return WinLex.quote_args(command) + def _add_remove_windows_terminal_profile(self, remove=False): + if not self.metadata.get("terminal_profile") or not self.menu.terminal_profile_location: + return + if self.metadata["terminal_profile"] is True: + name = self.metadata["name"] + else: + name = self.metadata["terminal_profile"] + + commandline = " ".join(WinLex.quote_args(self.render_key("command"))) + settings = json.loads(self.menu.terminal_profile_location.read_text()) + index = -1 + for p, profile in enumerate(settings["profiles"]["list"]): + # Define a profile as equal if name and command are the same. + # Do not use the whole dictionary because Windows may have added + # other items such as a GUID. + if profile.get("name") == name and profile.get("commandline") == commandline: + index = p + break + if remove: + if index < 0: + warnings.warn(f"Could not find terminal profile for {name}.") + return + del settings["profiles"]["list"][index] + else: + profile_data = { + "commandline": commandline, + "name": name, + } + if self.metadata.get("icon"): + profile_data["icon"] = self.render_key("icon") + if self.metadata.get("working_dir"): + profile_data["startingDirectory"] = self.render_key("working_dir") + if index < 0: + settings["profiles"]["list"].append(profile_data) + else: + settings["profiles"]["list"][index] = profile_data + self.menu.terminal_profile_location.write_text(json.dumps(settings, indent=4)) + def _ftype_identifier(self, extension): identifier = self.render_key("name", slug=True) return f"{identifier}.AssocFile{extension}" diff --git a/menuinst/platforms/win_utils/knownfolders.py b/menuinst/platforms/win_utils/knownfolders.py index 3b1782a2..1bc872f3 100644 --- a/menuinst/platforms/win_utils/knownfolders.py +++ b/menuinst/platforms/win_utils/knownfolders.py @@ -258,6 +258,7 @@ def get_folder_path(folder_id, user=None): "quicklaunch": get_folder_path(FOLDERID.QuickLaunch), "documents": get_folder_path(FOLDERID.Documents), "profile": get_folder_path(FOLDERID.Profile), + "localappdata": get_folder_path(FOLDERID.LocalAppData), }, } From 92e57dc94e8c177ee233ccf328abd5644a6d4939 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 2 May 2024 07:43:07 -0700 Subject: [PATCH 03/35] Windows Terminal option is string only --- menuinst/_schema.py | 8 ++++---- menuinst/data/menuinst.default.json | 2 +- menuinst/data/menuinst.schema.json | 12 ++---------- menuinst/platforms/win.py | 20 ++++++++++---------- 4 files changed, 17 insertions(+), 25 deletions(-) diff --git a/menuinst/_schema.py b/menuinst/_schema.py index 42a6a643..255b0a06 100644 --- a/menuinst/_schema.py +++ b/menuinst/_schema.py @@ -76,11 +76,11 @@ class Windows(BasePlatformSpecific): "Whether to create a desktop icon in addition to the Start Menu item." quicklaunch: Optional[bool] = True "Whether to create a quick launch icon in addition to the Start Menu item." - terminal_profile: Optional[Union[bool, constr(min_length=1)]] = False + terminal_profile: constr(min_length=1) = None """ - Whether to create a Windows Terminal profile. - If set to a string, that string will be used as the display name of the profile. - Otherwise, it will default to the name of the menu item. + Name of the Windows Terminal profile to create. + This name must be unique across multiple installations because + menuinst will overwrite Terminal profiles with the same name. """ url_protocols: Optional[List[constr(regex=r"\S+")]] = None "URL protocols that will be associated with this program." diff --git a/menuinst/data/menuinst.default.json b/menuinst/data/menuinst.default.json index 85622cf7..6e7e5db9 100644 --- a/menuinst/data/menuinst.default.json +++ b/menuinst/data/menuinst.default.json @@ -56,7 +56,7 @@ "win": { "desktop": true, "quicklaunch": true, - "terminal_profile": false, + "terminal_profile": null, "url_protocols": null, "file_extensions": null, "app_user_model_id": null diff --git a/menuinst/data/menuinst.schema.json b/menuinst/data/menuinst.schema.json index 5eec07a5..e6452b00 100644 --- a/menuinst/data/menuinst.schema.json +++ b/menuinst/data/menuinst.schema.json @@ -568,16 +568,8 @@ }, "terminal_profile": { "title": "Terminal Profile", - "default": false, - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string", - "minLength": 1 - } - ] + "minLength": 1, + "type": "string" }, "url_protocols": { "title": "Url Protocols", diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index c9b6529d..1c7f6c8d 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -313,23 +313,23 @@ def _process_command(self, with_arg1=False) -> Tuple[str]: return WinLex.quote_args(command) def _add_remove_windows_terminal_profile(self, remove=False): + """Add/remove the Windows Terminal profile. + + Windows Terminal is using the name of the profile to create a GUID, + so the name will be used as the unique identifier to find existing profiles. + """ if not self.metadata.get("terminal_profile") or not self.menu.terminal_profile_location: return - if self.metadata["terminal_profile"] is True: - name = self.metadata["name"] - else: - name = self.metadata["terminal_profile"] + name = self.render_key("terminal_profile") - commandline = " ".join(WinLex.quote_args(self.render_key("command"))) settings = json.loads(self.menu.terminal_profile_location.read_text()) + index = -1 for p, profile in enumerate(settings["profiles"]["list"]): - # Define a profile as equal if name and command are the same. - # Do not use the whole dictionary because Windows may have added - # other items such as a GUID. - if profile.get("name") == name and profile.get("commandline") == commandline: + if profile.get("name") == name: index = p break + if remove: if index < 0: warnings.warn(f"Could not find terminal profile for {name}.") @@ -337,7 +337,7 @@ def _add_remove_windows_terminal_profile(self, remove=False): del settings["profiles"]["list"][index] else: profile_data = { - "commandline": commandline, + "commandline": " ".join(WinLex.quote_args(self.render_key("command"))), "name": name, } if self.metadata.get("icon"): From c4079b0407b3dea3953161aa1741addf7090804a Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 2 May 2024 10:11:42 -0700 Subject: [PATCH 04/35] Add tests --- tests/data/jsons/windows-terminal.json | 33 +++++++++++++++++++++++++ tests/test_api.py | 34 ++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tests/data/jsons/windows-terminal.json diff --git a/tests/data/jsons/windows-terminal.json b/tests/data/jsons/windows-terminal.json new file mode 100644 index 00000000..be1bc1ed --- /dev/null +++ b/tests/data/jsons/windows-terminal.json @@ -0,0 +1,33 @@ + +{ + "$schema": "https://json-schema.org/draft-07/schema", + "$id": "https://schemas.conda.io/menuinst-1.schema.json", + "menu_name": "Package", + "menu_items": [ + { + "name": "A", + "description": "Package A", + "icon": null, + "command": ["testcommand_a.exe"], + "platforms": { + "win": { + "desktop": false, + "quicklaunch": false, + "terminal_profile": "A Terminal" + } + } + }, + { + "name": "B", + "description": "Package B", + "icon": null, + "command": ["testcommand_b.exe"], + "platforms": { + "win": { + "desktop": false, + "quicklaunch": false + } + } + } + ] +} diff --git a/tests/test_api.py b/tests/test_api.py index b5576fed..fd6a0b5e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -17,6 +17,7 @@ from menuinst.api import install, remove from menuinst.platforms.osx import _lsregister +from menuinst.platforms.win_utils.knownfolders import folder_path from menuinst.utils import DEFAULT_PREFIX, logged_run @@ -294,3 +295,36 @@ def test_url_protocol_association(delete_files): url_to_open=url, expected_output=url, ) + + +@pytest.mark.skipif(PLATFORM != "win", reason="Windows only") +def test_windows_terminal_profiles(): + def _parse_terminal_profiles(settings_file: Path) -> dict: + settings = json.loads(settings_file.read_text()) + return { + profile.get("name", ""): profile.get("commandline", "") + for profile in settings["profiles"]["list"] + } + + settings_file = ( + Path(folder_path("user", False, "localappdata")) + / "Packages" + / "Microsoft.WindowsTerminal_8wekyb3d8bbwe" + / "LocalState" + / "settings.json" + ) + tmpdir = mkdtemp() + (Path(tmpdir) / ".nonadmin").touch() + metadata_file = DATA / "jsons" / "windows-terminal.json" + install(metadata_file, target_prefix=tmpdir, base_prefix=tmpdir) + try: + profiles = _parse_terminal_profiles(settings_file) + assert "A Terminal" in profiles and profiles["A Terminal"] == "testcommand_a.exe" + assert "B" not in profiles + except Exception as exc: + remove(metadata_file, target_prefix=tmpdir, base_prefix=tmpdir) + raise exc + else: + remove(metadata_file, target_prefix=tmpdir, base_prefix=tmpdir) + profiles = _parse_terminal_profiles(settings_file) + assert "A Terminal" not in profiles and "B" not in profiles From 4f0907b208ed7ef1462a7c4090fd1732422afecd Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 2 May 2024 10:16:22 -0700 Subject: [PATCH 05/35] Add news item --- news/200-windows-terminal-profile | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 news/200-windows-terminal-profile diff --git a/news/200-windows-terminal-profile b/news/200-windows-terminal-profile new file mode 100644 index 00000000..6bee81c7 --- /dev/null +++ b/news/200-windows-terminal-profile @@ -0,0 +1,19 @@ +### Enhancements + +* Add option to create a Windows Terminal profile. (#196 via #200) + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* + +### Other + +* From b509fde6e5b9c7d41483e1a27ad2cf2e15b6eda2 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 2 May 2024 10:26:18 -0700 Subject: [PATCH 06/35] Explicitly return None if profile location not found --- menuinst/platforms/win.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index 1c7f6c8d..599643fb 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -69,7 +69,7 @@ def desktop_location(self) -> Path: def terminal_profile_location(self) -> Path: if self.mode == "system": warnings.warn("Terminal profiles are not available for system level installs") - return + return None profile_path = ( Path(windows_folder_path(self.mode, False, "localappdata")) / "Packages" @@ -79,7 +79,7 @@ def terminal_profile_location(self) -> Path: ) if profile_path.exists(): return profile_path - return + return None @property def placeholders(self) -> Dict[str, str]: From 9bdd4f3b58bd3faa5b5d9f8f3065adf326fb2e0a Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 2 May 2024 10:27:18 -0700 Subject: [PATCH 07/35] Skip tests if profile settings file cannot found --- tests/test_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index fd6a0b5e..fe166385 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -313,6 +313,8 @@ def _parse_terminal_profiles(settings_file: Path) -> dict: / "LocalState" / "settings.json" ) + if not settings_file.exists(): + pytest.skip("Terminal profile settings file not found.") tmpdir = mkdtemp() (Path(tmpdir) / ".nonadmin").touch() metadata_file = DATA / "jsons" / "windows-terminal.json" From 6f9d6f041aaa9eee49a758f669efc83f04b34ad6 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 2 May 2024 10:50:28 -0700 Subject: [PATCH 08/35] Only import win_utils when platform is win --- tests/test_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_api.py b/tests/test_api.py index fe166385..4d396b18 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -17,9 +17,11 @@ from menuinst.api import install, remove from menuinst.platforms.osx import _lsregister -from menuinst.platforms.win_utils.knownfolders import folder_path from menuinst.utils import DEFAULT_PREFIX, logged_run +if PLATFORM == "win": + from menuinst.platforms.win_utils.knownfolders import folder_path + def _poll_for_file_contents(path, timeout=10): t0 = time() From b87298cfdff1908d22499b25f51067cc50d6966f Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 6 May 2024 09:17:40 -0700 Subject: [PATCH 09/35] Apply suggestions from code review Co-authored-by: jaimergp --- menuinst/platforms/win.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index 599643fb..04853319 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -68,14 +68,14 @@ def desktop_location(self) -> Path: @property def terminal_profile_location(self) -> Path: if self.mode == "system": - warnings.warn("Terminal profiles are not available for system level installs") + log.warn("Terminal profiles are not available for system level installs") return None - profile_path = ( - Path(windows_folder_path(self.mode, False, "localappdata")) - / "Packages" - / "Microsoft.WindowsTerminal_8wekyb3d8bbwe" - / "LocalState" - / "settings.json" + profile_path = Path( + windows_folder_path(self.mode, False, "localappdata"), + "Packages", + "Microsoft.WindowsTerminal_8wekyb3d8bbwe", + "LocalState", + "settings.json", ) if profile_path.exists(): return profile_path From ebe0c6f24db25bcc6182271b0985484721a4924a Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 6 May 2024 09:22:24 -0700 Subject: [PATCH 10/35] Use indent=2 for test data --- tests/data/jsons/windows-terminal.json | 65 ++++++++++++++------------ 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/tests/data/jsons/windows-terminal.json b/tests/data/jsons/windows-terminal.json index be1bc1ed..3491da16 100644 --- a/tests/data/jsons/windows-terminal.json +++ b/tests/data/jsons/windows-terminal.json @@ -1,33 +1,36 @@ - { - "$schema": "https://json-schema.org/draft-07/schema", - "$id": "https://schemas.conda.io/menuinst-1.schema.json", - "menu_name": "Package", - "menu_items": [ - { - "name": "A", - "description": "Package A", - "icon": null, - "command": ["testcommand_a.exe"], - "platforms": { - "win": { - "desktop": false, - "quicklaunch": false, - "terminal_profile": "A Terminal" - } - } - }, - { - "name": "B", - "description": "Package B", - "icon": null, - "command": ["testcommand_b.exe"], - "platforms": { - "win": { - "desktop": false, - "quicklaunch": false - } - } - } - ] + "$schema": "https://json-schema.org/draft-07/schema", + "$id": "https://schemas.conda.io/menuinst-1.schema.json", + "menu_name": "Package", + "menu_items": [ + { + "name": "A", + "description": "Package A", + "icon": null, + "command": [ + "testcommand_a.exe" + ], + "platforms": { + "win": { + "desktop": false, + "quicklaunch": false, + "terminal_profile": "A Terminal" + } + } + }, + { + "name": "B", + "description": "Package B", + "icon": null, + "command": [ + "testcommand_b.exe" + ], + "platforms": { + "win": { + "desktop": false, + "quicklaunch": false + } + } + } + ] } From b1944f198af52e6455cc626e284feceadfb4ab0d Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 7 May 2024 07:59:35 -0700 Subject: [PATCH 11/35] Add profile to all known Windows Terminal locations --- menuinst/platforms/win.py | 57 +++++++++++++++++++++++++++------------ tests/test_api.py | 51 +++++++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 31 deletions(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index 04853319..670a998e 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -66,20 +66,41 @@ def desktop_location(self) -> Path: return Path(windows_folder_path(self.mode, False, "desktop")) @property - def terminal_profile_location(self) -> Path: + def terminal_profile_locations(self) -> list[Path]: + """Location of the Windows terminal profiles. + + See the Microsoft documentation for details: + https://learn.microsoft.com/en-us/windows/terminal/install#settings-json-file + """ if self.mode == "system": log.warn("Terminal profiles are not available for system level installs") return None - profile_path = Path( - windows_folder_path(self.mode, False, "localappdata"), - "Packages", - "Microsoft.WindowsTerminal_8wekyb3d8bbwe", - "LocalState", - "settings.json", - ) - if profile_path.exists(): - return profile_path - return None + profile_locations = [ + # Stable + Path( + windows_folder_path(self.mode, False, "localappdata"), + "Packages", + "Microsoft.WindowsTerminal_8wekyb3d8bbwe", + "LocalState", + "settings.json", + ), + # Preview + Path( + windows_folder_path(self.mode, False, "localappdata"), + "Packages", + "Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe", + "LocalState", + "settings.json", + ), + # Unpackaged (Scoop, Chocolatey, etc.) + Path( + windows_folder_path(self.mode, False, "localappdata"), + "Microsoft", + "Windows Terminal", + "settings.json", + ), + ] + return [location for location in profile_locations if location.exists()] @property def placeholders(self) -> Dict[str, str]: @@ -182,7 +203,8 @@ def create(self) -> Tuple[Path, ...]: self._app_user_model_id(), ) - self._add_remove_windows_terminal_profile(remove=False) + 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() @@ -191,7 +213,8 @@ def create(self) -> Tuple[Path, ...]: def remove(self) -> Tuple[Path, ...]: self._unregister_file_extensions() self._unregister_url_protocols() - self._add_remove_windows_terminal_profile(remove=True) + for location in self.menu.terminal_profile_locations: + self._add_remove_windows_terminal_profile(location, remove=True) paths = self._paths() for path in paths: @@ -312,17 +335,17 @@ def _process_command(self, with_arg1=False) -> Tuple[str]: command.append("%1") return WinLex.quote_args(command) - def _add_remove_windows_terminal_profile(self, remove=False): + def _add_remove_windows_terminal_profile(self, location: Path, remove: bool = False): """Add/remove the Windows Terminal profile. Windows Terminal is using the name of the profile to create a GUID, so the name will be used as the unique identifier to find existing profiles. """ - if not self.metadata.get("terminal_profile") or not self.menu.terminal_profile_location: + if not self.metadata.get("terminal_profile") or not location.exists(): return name = self.render_key("terminal_profile") - settings = json.loads(self.menu.terminal_profile_location.read_text()) + settings = json.loads(location.read_text()) index = -1 for p, profile in enumerate(settings["profiles"]["list"]): @@ -348,7 +371,7 @@ def _add_remove_windows_terminal_profile(self, remove=False): settings["profiles"]["list"].append(profile_data) else: settings["profiles"]["list"][index] = profile_data - self.menu.terminal_profile_location.write_text(json.dumps(settings, indent=4)) + location.write_text(json.dumps(settings, indent=4)) def _ftype_identifier(self, extension): identifier = self.render_key("name", slug=True) diff --git a/tests/test_api.py b/tests/test_api.py index 4d396b18..2a524730 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -308,27 +308,50 @@ def _parse_terminal_profiles(settings_file: Path) -> dict: for profile in settings["profiles"]["list"] } - settings_file = ( - Path(folder_path("user", False, "localappdata")) - / "Packages" - / "Microsoft.WindowsTerminal_8wekyb3d8bbwe" - / "LocalState" - / "settings.json" - ) - if not settings_file.exists(): - pytest.skip("Terminal profile settings file not found.") + settings_files = [ + # Stable + Path( + folder_path("user", False, "localappdata"), + "Packages", + "Microsoft.WindowsTerminal_8wekyb3d8bbwe", + "LocalState", + "settings.json", + ), + # Preview + Path( + folder_path("user", False, "localappdata"), + "Packages", + "Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe", + "LocalState", + "settings.json", + ), + # Unpackaged (Scoop, Chocolatey, etc.) + Path( + folder_path("user", False, "localappdata"), + "Microsoft", + "Windows Terminal", + "settings.json", + ), + ] + settings_files = [file for file in settings_files if file.exists()] + if not settings_files: + pytest.skip("No terminal profile settings file found.") tmpdir = mkdtemp() (Path(tmpdir) / ".nonadmin").touch() metadata_file = DATA / "jsons" / "windows-terminal.json" install(metadata_file, target_prefix=tmpdir, base_prefix=tmpdir) + a_in_profiles = [] + b_in_profiles = [] try: - profiles = _parse_terminal_profiles(settings_file) - assert "A Terminal" in profiles and profiles["A Terminal"] == "testcommand_a.exe" - assert "B" not in profiles + for file in settings_files: + profiles = _parse_terminal_profiles(file) + if "A Terminal" in profiles and profiles["A Terminal"] == "testcommand_a.exe": + a_in_profiles.append(file) + if "B" in profiles: + b_in_profiles.append(file) + assert a_in_profiles == settings_files and b_in_profiles == [] except Exception as exc: remove(metadata_file, target_prefix=tmpdir, base_prefix=tmpdir) raise exc else: remove(metadata_file, target_prefix=tmpdir, base_prefix=tmpdir) - profiles = _parse_terminal_profiles(settings_file) - assert "A Terminal" not in profiles and "B" not in profiles From fda26f05ba123f9b0815327ec8dad5234cd2597e Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 7 May 2024 08:26:29 -0700 Subject: [PATCH 12/35] Use typing.List for type declaration --- menuinst/platforms/win.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index 670a998e..d4978cb0 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -9,7 +9,7 @@ from pathlib import Path from subprocess import CompletedProcess from tempfile import NamedTemporaryFile -from typing import Any, Dict, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple from ..utils import WinLex, logged_run, unlink from .base import Menu, MenuItem @@ -66,7 +66,7 @@ def desktop_location(self) -> Path: return Path(windows_folder_path(self.mode, False, "desktop")) @property - def terminal_profile_locations(self) -> list[Path]: + def terminal_profile_locations(self) -> List[Path]: """Location of the Windows terminal profiles. See the Microsoft documentation for details: From 7d19bb3863a68c43749b988c34506f014504d8e5 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 7 May 2024 08:31:08 -0700 Subject: [PATCH 13/35] Return empty list instead of None if profile locations are not found --- menuinst/platforms/win.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index d4978cb0..ed3b4cda 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -74,7 +74,7 @@ def terminal_profile_locations(self) -> List[Path]: """ if self.mode == "system": log.warn("Terminal profiles are not available for system level installs") - return None + return [] profile_locations = [ # Stable Path( From cf9ebc2d30fb6843c0b44b8f6d4c545be3fedd10 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Tue, 7 May 2024 08:57:49 -0700 Subject: [PATCH 14/35] Add overwrite warning --- menuinst/platforms/win.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index ed3b4cda..fb053276 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -355,7 +355,7 @@ def _add_remove_windows_terminal_profile(self, location: Path, remove: bool = Fa if remove: if index < 0: - warnings.warn(f"Could not find terminal profile for {name}.") + log.warn(f"Could not find terminal profile for {name}.") return del settings["profiles"]["list"][index] else: @@ -370,6 +370,7 @@ def _add_remove_windows_terminal_profile(self, location: Path, remove: bool = Fa if index < 0: settings["profiles"]["list"].append(profile_data) else: + log.warn(f"Overwriting terminal profile for {name}.") settings["profiles"]["list"][index] = profile_data location.write_text(json.dumps(settings, indent=4)) From 448fddf758077a618dc00ea5202ef8820abed24d Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Wed, 8 May 2024 09:09:22 -0700 Subject: [PATCH 15/35] Update menuinst/platforms/win.py Co-authored-by: jaimergp --- menuinst/platforms/win.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index fb053276..76e7cd18 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -348,7 +348,7 @@ def _add_remove_windows_terminal_profile(self, location: Path, remove: bool = Fa settings = json.loads(location.read_text()) index = -1 - for p, profile in enumerate(settings["profiles"]["list"]): + for p, profile in enumerate(settings.get("profiles", {}).get("list", []): if profile.get("name") == name: index = p break From 9d36e8b5744a2fff00f3edfadc54a3a4fc7cf979 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Wed, 8 May 2024 09:14:07 -0700 Subject: [PATCH 16/35] Use tmp_path fixture --- tests/test_api.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 2a524730..20620d7a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -300,7 +300,7 @@ def test_url_protocol_association(delete_files): @pytest.mark.skipif(PLATFORM != "win", reason="Windows only") -def test_windows_terminal_profiles(): +def test_windows_terminal_profiles(tmp_path): def _parse_terminal_profiles(settings_file: Path) -> dict: settings = json.loads(settings_file.read_text()) return { @@ -336,10 +336,9 @@ def _parse_terminal_profiles(settings_file: Path) -> dict: settings_files = [file for file in settings_files if file.exists()] if not settings_files: pytest.skip("No terminal profile settings file found.") - tmpdir = mkdtemp() - (Path(tmpdir) / ".nonadmin").touch() + (tmp_path / ".nonadmin").touch() metadata_file = DATA / "jsons" / "windows-terminal.json" - install(metadata_file, target_prefix=tmpdir, base_prefix=tmpdir) + install(metadata_file, target_prefix=tmp_path, base_prefix=tmp_path) a_in_profiles = [] b_in_profiles = [] try: @@ -351,7 +350,7 @@ def _parse_terminal_profiles(settings_file: Path) -> dict: b_in_profiles.append(file) assert a_in_profiles == settings_files and b_in_profiles == [] except Exception as exc: - remove(metadata_file, target_prefix=tmpdir, base_prefix=tmpdir) + remove(metadata_file, target_prefix=tmp_path, base_prefix=tmp_path) raise exc else: - remove(metadata_file, target_prefix=tmpdir, base_prefix=tmpdir) + remove(metadata_file, target_prefix=tmp_path, base_prefix=tmp_path) From 4f6d8494f25801a35155838baab3fa5833fad316 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Wed, 8 May 2024 09:20:27 -0700 Subject: [PATCH 17/35] Ensure that terminal settings file always contains profiles list --- menuinst/platforms/win.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index 76e7cd18..da902f00 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -348,7 +348,7 @@ def _add_remove_windows_terminal_profile(self, location: Path, remove: bool = Fa settings = json.loads(location.read_text()) index = -1 - for p, profile in enumerate(settings.get("profiles", {}).get("list", []): + for p, profile in enumerate(settings.get("profiles", {}).get("list", [])): if profile.get("name") == name: index = p break @@ -368,6 +368,10 @@ def _add_remove_windows_terminal_profile(self, location: Path, remove: bool = Fa if self.metadata.get("working_dir"): profile_data["startingDirectory"] = self.render_key("working_dir") if index < 0: + if "profiles" not in settings: + settings["profiles"] = {} + if "list" not in settings["profiles"]: + settings["profiles"]["list"] = [] settings["profiles"]["list"].append(profile_data) else: log.warn(f"Overwriting terminal profile for {name}.") From a3cd0cf50d4992700a1c833dfcb676a933bd835a Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Wed, 8 May 2024 09:25:34 -0700 Subject: [PATCH 18/35] Install Windows Terminal before running tests --- .github/workflows/tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 577f37dd..368b2004 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -54,6 +54,12 @@ jobs: echo "XDG_UTILS_DEBUG_LEVEL=2" >> $GITHUB_ENV echo "XDG_CURRENT_DESKTOP=GNOME" >> $GITHUB_ENV + - name: Add Windows dependencies + if: startswith(matrix.os, 'windows') + run: | + choco install -y microsoft-windows-terminal + shell: pwsh + - name: Install miniforge uses: conda-incubator/setup-miniconda@v2 with: From 016e16afa1ea68f9b82cd762159ab231243d004b Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Wed, 8 May 2024 11:04:33 -0700 Subject: [PATCH 19/35] Fix code formatting --- tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_api.py b/tests/test_api.py index 9c560ca3..86eda66d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -382,4 +382,4 @@ def test_name_dictionary(target_env_is_base): item_names = {item.stem for item in menu_items} assert item_names == expected finally: - remove(abs_json_path, target_prefix=tmp_target_path, base_prefix=tmp_base_path) \ No newline at end of file + remove(abs_json_path, target_prefix=tmp_target_path, base_prefix=tmp_base_path) From a37bb099dc42ee9200cd8d4a5754d353c83a1d3f Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 9 May 2024 08:56:22 -0700 Subject: [PATCH 20/35] Create a function for settings.json location to avoid duplication --- menuinst/platforms/win.py | 33 ++--------------- menuinst/platforms/win_utils/knownfolders.py | 38 ++++++++++++++++++++ tests/test_api.py | 29 ++------------- 3 files changed, 43 insertions(+), 57 deletions(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index a44b9c09..b211384d 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -14,6 +14,7 @@ from ..utils import WinLex, logged_run, unlink from .base import Menu, MenuItem 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 ( register_file_extension, register_url_protocol, @@ -67,39 +68,11 @@ def desktop_location(self) -> Path: @property def terminal_profile_locations(self) -> List[Path]: - """Location of the Windows terminal profiles. - - See the Microsoft documentation for details: - https://learn.microsoft.com/en-us/windows/terminal/install#settings-json-file - """ + """Location of the Windows terminal profiles.""" if self.mode == "system": log.warn("Terminal profiles are not available for system level installs") return [] - profile_locations = [ - # Stable - Path( - windows_folder_path(self.mode, False, "localappdata"), - "Packages", - "Microsoft.WindowsTerminal_8wekyb3d8bbwe", - "LocalState", - "settings.json", - ), - # Preview - Path( - windows_folder_path(self.mode, False, "localappdata"), - "Packages", - "Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe", - "LocalState", - "settings.json", - ), - # Unpackaged (Scoop, Chocolatey, etc.) - Path( - windows_folder_path(self.mode, False, "localappdata"), - "Microsoft", - "Windows Terminal", - "settings.json", - ), - ] + profile_locations = windows_terminal_settings_files(self.mode) return [location for location in profile_locations if location.exists()] @property diff --git a/menuinst/platforms/win_utils/knownfolders.py b/menuinst/platforms/win_utils/knownfolders.py index 1bc872f3..8e5c256f 100644 --- a/menuinst/platforms/win_utils/knownfolders.py +++ b/menuinst/platforms/win_utils/knownfolders.py @@ -29,6 +29,8 @@ import os from ctypes import windll, wintypes from logging import getLogger +from pathlib import Path +from typing import List from uuid import UUID logger = getLogger(__name__) @@ -314,3 +316,39 @@ def folder_path(preferred_mode, check_other_mode, key): ) return None return path + + +def windows_terminal_settings_files(mode: str) -> List[Path]: + """Return all possible locations of the settings.json files for the Windows Terminal. + + See the Microsoft documentation for details: + https://learn.microsoft.com/en-us/windows/terminal/install#settings-json-file + """ + if mode != "user": + return [] + profile_locations = [ + # Stable + Path( + folder_path(mode, False, "localappdata"), + "Packages", + "Microsoft.WindowsTerminal_8wekyb3d8bbwe", + "LocalState", + "settings.json", + ), + # Preview + Path( + folder_path(mode, False, "localappdata"), + "Packages", + "Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe", + "LocalState", + "settings.json", + ), + # Unpackaged (Scoop, Chocolatey, etc.) + Path( + folder_path(mode, False, "localappdata"), + "Microsoft", + "Windows Terminal", + "settings.json", + ), + ] + return profile_locations diff --git a/tests/test_api.py b/tests/test_api.py index 86eda66d..80c80c49 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -20,7 +20,7 @@ from menuinst.utils import DEFAULT_PREFIX, logged_run if PLATFORM == "win": - from menuinst.platforms.win_utils.knownfolders import folder_path + from menuinst.platforms.win_utils.knownfolders import windows_terminal_settings_files def _poll_for_file_contents(path, timeout=10): @@ -308,32 +308,7 @@ def _parse_terminal_profiles(settings_file: Path) -> dict: for profile in settings["profiles"]["list"] } - settings_files = [ - # Stable - Path( - folder_path("user", False, "localappdata"), - "Packages", - "Microsoft.WindowsTerminal_8wekyb3d8bbwe", - "LocalState", - "settings.json", - ), - # Preview - Path( - folder_path("user", False, "localappdata"), - "Packages", - "Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe", - "LocalState", - "settings.json", - ), - # Unpackaged (Scoop, Chocolatey, etc.) - Path( - folder_path("user", False, "localappdata"), - "Microsoft", - "Windows Terminal", - "settings.json", - ), - ] - settings_files = [file for file in settings_files if file.exists()] + settings_files = [file for file in windows_terminal_settings_files("user") if file.exists()] if not settings_files: pytest.skip("No terminal profile settings file found.") (tmp_path / ".nonadmin").touch() From f1983b034d2677c310070cfd2403dabb13469b6e Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 9 May 2024 13:07:26 -0700 Subject: [PATCH 21/35] Check for parent directory of settings file instead --- menuinst/platforms/win.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index b211384d..07e9a0a7 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -68,12 +68,17 @@ def desktop_location(self) -> Path: @property def terminal_profile_locations(self) -> List[Path]: - """Location of the Windows terminal profiles.""" + """Location of the Windows terminal profiles. + + The parent directory is used to check if Terminal is installed + because the settings file is generated when Terminal is opened, + not when it is installed. + """ if self.mode == "system": log.warn("Terminal profiles are not available for system level installs") return [] profile_locations = windows_terminal_settings_files(self.mode) - return [location for location in profile_locations if location.exists()] + return [location for location in profile_locations if location.parent.exists()] @property def placeholders(self) -> Dict[str, str]: @@ -312,12 +317,17 @@ def _add_remove_windows_terminal_profile(self, location: Path, remove: bool = Fa Windows Terminal is using the name of the profile to create a GUID, so the name will be used as the unique identifier to find existing profiles. + + If the Terminal app has never been opened, the settings file may not exist yet. + Writing a minimal profile file will not break the application - Terminal will + automatically generate the missing options and profiles without overwriting + the profiles menuinst has created. """ - if not self.metadata.get("terminal_profile") or not location.exists(): + if not self.metadata.get("terminal_profile") or not location.parent.exists(): return name = self.render_key("terminal_profile") - settings = json.loads(location.read_text()) + settings = json.loads(location.read_text()) if location.exists() else {} index = -1 for p, profile in enumerate(settings.get("profiles", {}).get("list", [])): From d57865fa23412ce065023268951d851720d72b25 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 9 May 2024 13:08:07 -0700 Subject: [PATCH 22/35] Consolidate calls to get LocalAppData directory --- menuinst/platforms/win_utils/knownfolders.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/menuinst/platforms/win_utils/knownfolders.py b/menuinst/platforms/win_utils/knownfolders.py index 8e5c256f..6559a1db 100644 --- a/menuinst/platforms/win_utils/knownfolders.py +++ b/menuinst/platforms/win_utils/knownfolders.py @@ -326,10 +326,11 @@ def windows_terminal_settings_files(mode: str) -> List[Path]: """ if mode != "user": return [] + localappdata = folder_path(mode, False, "localappdata") profile_locations = [ # Stable Path( - folder_path(mode, False, "localappdata"), + localappdata, "Packages", "Microsoft.WindowsTerminal_8wekyb3d8bbwe", "LocalState", @@ -337,7 +338,7 @@ def windows_terminal_settings_files(mode: str) -> List[Path]: ), # Preview Path( - folder_path(mode, False, "localappdata"), + localappdata, "Packages", "Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe", "LocalState", @@ -345,7 +346,7 @@ def windows_terminal_settings_files(mode: str) -> List[Path]: ), # Unpackaged (Scoop, Chocolatey, etc.) Path( - folder_path(mode, False, "localappdata"), + localappdata, "Microsoft", "Windows Terminal", "settings.json", From e8f128c3225c097b3d3cfc3fb9829a094f6f2ab3 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 9 May 2024 13:09:26 -0700 Subject: [PATCH 23/35] Do not mock locations on CI for terminal profile test --- tests/conftest.py | 10 +++++++++- tests/test_api.py | 7 ++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 49cbd2a9..38bbe27a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,12 @@ PLATFORM = platform_key() +def pytest_configure(config): + config.addinivalue_line( + "markers", "no_mocking_on_ci: Do not mock locations when running on the CI." + ) + + def base_prefix(): prefix = os.environ.get("CONDA_ROOT", os.environ.get("MAMBA_ROOT_PREFIX")) if not prefix: @@ -52,7 +58,9 @@ def tmpdir(tmpdir, request): @pytest.fixture(autouse=True) -def mock_locations(monkeypatch, tmp_path): +def mock_locations(monkeypatch, tmp_path, request): + if "no_mocking_on_ci" in request.keywords and "CI" in os.environ: + return from menuinst.platforms.linux import LinuxMenu from menuinst.platforms.osx import MacOSMenuItem diff --git a/tests/test_api.py b/tests/test_api.py index 80c80c49..eb53d091 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -300,6 +300,7 @@ def test_url_protocol_association(delete_files): @pytest.mark.skipif(PLATFORM != "win", reason="Windows only") +@pytest.mark.no_mocking_on_ci def test_windows_terminal_profiles(tmp_path): def _parse_terminal_profiles(settings_file: Path) -> dict: settings = json.loads(settings_file.read_text()) @@ -308,7 +309,11 @@ def _parse_terminal_profiles(settings_file: Path) -> dict: for profile in settings["profiles"]["list"] } - settings_files = [file for file in windows_terminal_settings_files("user") if file.exists()] + settings_files = windows_terminal_settings_files("user") + if "CI" not in os.environ: + for file in settings_files: + file.parent.mkdir(parents=True, exist_ok=True) + settings_files = [file for file in settings_files if file.parent.exists()] if not settings_files: pytest.skip("No terminal profile settings file found.") (tmp_path / ".nonadmin").touch() From 7fd05bff80508edff4a7704aa231741e5f38030a Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 9 May 2024 13:10:21 -0700 Subject: [PATCH 24/35] Replace deprecated warn with warning --- menuinst/platforms/win.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index 07e9a0a7..7cfd9b4e 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -337,7 +337,7 @@ def _add_remove_windows_terminal_profile(self, location: Path, remove: bool = Fa if remove: if index < 0: - log.warn(f"Could not find terminal profile for {name}.") + log.warning(f"Could not find terminal profile for {name}.") return del settings["profiles"]["list"][index] else: From 21a9de9cca2a680410848ff3b4a41558d095c6f6 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 9 May 2024 13:21:21 -0700 Subject: [PATCH 25/35] Debug: output packages directory after installing terminal --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e10d4428..c90e2368 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -58,6 +58,7 @@ jobs: if: startswith(matrix.os, 'windows') run: | choco install -y microsoft-windows-terminal + dir "${Env:LocalAppData}/Packages" shell: pwsh - name: Install miniforge From b5c0fb917477ee65cf31dd440d09f6e03c9baa8a Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 9 May 2024 13:24:18 -0700 Subject: [PATCH 26/35] Debug: output of microsoft directory after installing terminal --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c90e2368..2120e360 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -59,6 +59,7 @@ jobs: run: | choco install -y microsoft-windows-terminal dir "${Env:LocalAppData}/Packages" + dir "${Env:LocalAppData}/Microsoft" shell: pwsh - name: Install miniforge From 3f822c429cc887939b88cc623255b7cc7ecb5ef3 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 9 May 2024 13:55:24 -0700 Subject: [PATCH 27/35] Debug: output path of terminal and start it --- .github/workflows/tests.yml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2120e360..c9abd3c3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,15 +32,17 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-latest, ubuntu-latest, macos-13] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - exclude: - - os: macos-13 - python-version: "3.11" - - os: macos-13 - python-version: "3.10" - - os: macos-13 - python-version: "3.9" + os: [windows-latest] + python-version: "3.11" + #os: [windows-latest, ubuntu-latest, macos-13] + #python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + #exclude: + # - os: macos-13 + # python-version: "3.11" + # - os: macos-13 + # python-version: "3.10" + # - os: macos-13 + # python-version: "3.9" steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b #v4.1.4 @@ -58,6 +60,8 @@ jobs: if: startswith(matrix.os, 'windows') run: | choco install -y microsoft-windows-terminal + (Get-Command wt).Path + wt dir "${Env:LocalAppData}/Packages" dir "${Env:LocalAppData}/Microsoft" shell: pwsh From ce1065948f84c08174e12fc4769e35aadc429710 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 9 May 2024 13:56:47 -0700 Subject: [PATCH 28/35] Debug: fix typo --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c9abd3c3..a4aa0461 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,7 +33,7 @@ jobs: fail-fast: false matrix: os: [windows-latest] - python-version: "3.11" + python-version: ["3.11"] #os: [windows-latest, ubuntu-latest, macos-13] #python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] #exclude: From ab06c18d6c8db1dbf9347019e08bc8fd9672d7f5 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 9 May 2024 14:24:38 -0700 Subject: [PATCH 29/35] Revert debugging --- .github/workflows/tests.yml | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a4aa0461..e10d4428 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,17 +32,15 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-latest] - python-version: ["3.11"] - #os: [windows-latest, ubuntu-latest, macos-13] - #python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - #exclude: - # - os: macos-13 - # python-version: "3.11" - # - os: macos-13 - # python-version: "3.10" - # - os: macos-13 - # python-version: "3.9" + os: [windows-latest, ubuntu-latest, macos-13] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + exclude: + - os: macos-13 + python-version: "3.11" + - os: macos-13 + python-version: "3.10" + - os: macos-13 + python-version: "3.9" steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b #v4.1.4 @@ -60,10 +58,6 @@ jobs: if: startswith(matrix.os, 'windows') run: | choco install -y microsoft-windows-terminal - (Get-Command wt).Path - wt - dir "${Env:LocalAppData}/Packages" - dir "${Env:LocalAppData}/Microsoft" shell: pwsh - name: Install miniforge From 0d29a9b933c29aa71ca5d88299db9afffdc74452 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 9 May 2024 14:36:10 -0700 Subject: [PATCH 30/35] Harden against changes in the terminal directory hash --- menuinst/platforms/win_utils/knownfolders.py | 23 ++++++++------------ tests/test_api.py | 7 +++--- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/menuinst/platforms/win_utils/knownfolders.py b/menuinst/platforms/win_utils/knownfolders.py index 6559a1db..664e556a 100644 --- a/menuinst/platforms/win_utils/knownfolders.py +++ b/menuinst/platforms/win_utils/knownfolders.py @@ -327,23 +327,18 @@ def windows_terminal_settings_files(mode: str) -> List[Path]: if mode != "user": return [] localappdata = folder_path(mode, False, "localappdata") + packages = Path(localappdata) / "Packages" profile_locations = [ # Stable - Path( - localappdata, - "Packages", - "Microsoft.WindowsTerminal_8wekyb3d8bbwe", - "LocalState", - "settings.json", - ), + *[ + Path(terminal, "LocalState", "settings.json") + for terminal in packages.glob("Microsoft.WindowsTerminal_*") + ], # Preview - Path( - localappdata, - "Packages", - "Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe", - "LocalState", - "settings.json", - ), + *[ + Path(terminal, "LocalState", "settings.json") + for terminal in packages.glob("Microsoft.WindowsTerminalPreview_*") + ], # Unpackaged (Scoop, Chocolatey, etc.) Path( localappdata, diff --git a/tests/test_api.py b/tests/test_api.py index 6df8552c..2bb2f988 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -338,9 +338,10 @@ def _parse_terminal_profiles(settings_file: Path) -> dict: if "CI" not in os.environ: for file in settings_files: file.parent.mkdir(parents=True, exist_ok=True) - settings_files = [file for file in settings_files if file.parent.exists()] - if not settings_files: - pytest.skip("No terminal profile settings file found.") + else: + settings_files = [file for file in settings_files if file.parent.exists()] + if not settings_files: + pytest.skip("No terminal profile settings file found.") (tmp_path / ".nonadmin").touch() metadata_file = DATA / "jsons" / "windows-terminal.json" install(metadata_file, target_prefix=tmp_path, base_prefix=tmp_path) From e554194f33391c1810277c6dce3b3685f1594d35 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Thu, 9 May 2024 14:42:16 -0700 Subject: [PATCH 31/35] Replace deprecated warn with warning --- menuinst/platforms/win.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index 7cfd9b4e..5d4a4935 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -75,7 +75,7 @@ def terminal_profile_locations(self) -> List[Path]: not when it is installed. """ if self.mode == "system": - log.warn("Terminal profiles are not available for system level installs") + log.warning("Terminal profiles are not available for system level installs") return [] profile_locations = windows_terminal_settings_files(self.mode) return [location for location in profile_locations if location.parent.exists()] @@ -356,7 +356,7 @@ def _add_remove_windows_terminal_profile(self, location: Path, remove: bool = Fa settings["profiles"]["list"] = [] settings["profiles"]["list"].append(profile_data) else: - log.warn(f"Overwriting terminal profile for {name}.") + log.warning(f"Overwriting terminal profile for {name}.") settings["profiles"]["list"][index] = profile_data location.write_text(json.dumps(settings, indent=4)) From b5a4191e8aef8f97b184ab2e74cc2419275a3f11 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 13 May 2024 13:29:23 -0700 Subject: [PATCH 32/35] Ensure that settings parent directory exists --- menuinst/platforms/win.py | 3 +-- menuinst/platforms/win_utils/knownfolders.py | 11 ++++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index 5d4a4935..737e64e7 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -77,8 +77,7 @@ def terminal_profile_locations(self) -> List[Path]: if self.mode == "system": log.warning("Terminal profiles are not available for system level installs") return [] - profile_locations = windows_terminal_settings_files(self.mode) - return [location for location in profile_locations if location.parent.exists()] + return windows_terminal_settings_files(self.mode) @property def placeholders(self) -> Dict[str, str]: diff --git a/menuinst/platforms/win_utils/knownfolders.py b/menuinst/platforms/win_utils/knownfolders.py index 664e556a..8b17b36e 100644 --- a/menuinst/platforms/win_utils/knownfolders.py +++ b/menuinst/platforms/win_utils/knownfolders.py @@ -339,12 +339,9 @@ def windows_terminal_settings_files(mode: str) -> List[Path]: Path(terminal, "LocalState", "settings.json") for terminal in packages.glob("Microsoft.WindowsTerminalPreview_*") ], - # Unpackaged (Scoop, Chocolatey, etc.) - Path( - localappdata, - "Microsoft", - "Windows Terminal", - "settings.json", - ), ] + # Unpackaged (Scoop, Chocolatey, etc.) + unpackaged_path = Path(localappdata, "Microsoft", "Windows Terminal", "settings.json") + if unpackaged_path.parent.exists(): + profile_locations.append(unpackaged_path) return profile_locations From 80984e9e006b8548edf808b02ddf12ca38fd2e24 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 13 May 2024 13:45:29 -0700 Subject: [PATCH 33/35] Only use mocked location for testing --- .github/workflows/tests.yml | 6 ------ tests/conftest.py | 8 -------- tests/test_api.py | 39 +++++++++++-------------------------- 3 files changed, 11 insertions(+), 42 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e10d4428..ac2f3fd3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -54,12 +54,6 @@ jobs: echo "XDG_UTILS_DEBUG_LEVEL=2" >> $GITHUB_ENV echo "XDG_CURRENT_DESKTOP=GNOME" >> $GITHUB_ENV - - name: Add Windows dependencies - if: startswith(matrix.os, 'windows') - run: | - choco install -y microsoft-windows-terminal - shell: pwsh - - name: Install miniforge uses: conda-incubator/setup-miniconda@a4260408e20b96e80095f42ff7f1a15b27dd94ca #v3.0.4 with: diff --git a/tests/conftest.py b/tests/conftest.py index 38bbe27a..8e4f9b71 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,12 +18,6 @@ PLATFORM = platform_key() -def pytest_configure(config): - config.addinivalue_line( - "markers", "no_mocking_on_ci: Do not mock locations when running on the CI." - ) - - def base_prefix(): prefix = os.environ.get("CONDA_ROOT", os.environ.get("MAMBA_ROOT_PREFIX")) if not prefix: @@ -59,8 +53,6 @@ def tmpdir(tmpdir, request): @pytest.fixture(autouse=True) def mock_locations(monkeypatch, tmp_path, request): - if "no_mocking_on_ci" in request.keywords and "CI" in os.environ: - return from menuinst.platforms.linux import LinuxMenu from menuinst.platforms.osx import MacOSMenuItem diff --git a/tests/test_api.py b/tests/test_api.py index 2bb2f988..a16236cc 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -20,9 +20,6 @@ from menuinst.platforms.osx import _lsregister from menuinst.utils import DEFAULT_PREFIX, logged_run, slugify -if PLATFORM == "win": - from menuinst.platforms.win_utils.knownfolders import windows_terminal_settings_files - def _poll_for_file_contents(path, timeout=10): t0 = time() @@ -325,36 +322,22 @@ def test_url_protocol_association(delete_files): @pytest.mark.skipif(PLATFORM != "win", reason="Windows only") -@pytest.mark.no_mocking_on_ci def test_windows_terminal_profiles(tmp_path): - def _parse_terminal_profiles(settings_file: Path) -> dict: - settings = json.loads(settings_file.read_text()) - return { - profile.get("name", ""): profile.get("commandline", "") - for profile in settings["profiles"]["list"] - } - - settings_files = windows_terminal_settings_files("user") - if "CI" not in os.environ: - for file in settings_files: - file.parent.mkdir(parents=True, exist_ok=True) - else: - settings_files = [file for file in settings_files if file.parent.exists()] - if not settings_files: - pytest.skip("No terminal profile settings file found.") + settings_file = Path( + tmp_path, "localappdata", "Microsoft", "Windows Terminal", "settings.json" + ) + settings_file.parent.mkdir(parents=True) (tmp_path / ".nonadmin").touch() metadata_file = DATA / "jsons" / "windows-terminal.json" install(metadata_file, target_prefix=tmp_path, base_prefix=tmp_path) - a_in_profiles = [] - b_in_profiles = [] try: - for file in settings_files: - profiles = _parse_terminal_profiles(file) - if "A Terminal" in profiles and profiles["A Terminal"] == "testcommand_a.exe": - a_in_profiles.append(file) - if "B" in profiles: - b_in_profiles.append(file) - assert a_in_profiles == settings_files and b_in_profiles == [] + settings = json.loads(settings_file.read_text()) + profiles = { + profile.get("name", ""): profile.get("commandline", "") + for profile in settings.get("profiles", {}).get("list", []) + } + assert profiles.get("A Terminal") == "testcommand_a.exe" + assert "B" not in profiles except Exception as exc: remove(metadata_file, target_prefix=tmp_path, base_prefix=tmp_path) raise exc From 857ff4d04fe27e4e7d05ecdd0c77b1c8a2edb952 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Mon, 13 May 2024 14:52:30 -0700 Subject: [PATCH 34/35] Mock running as user --- tests/conftest.py | 7 +++++++ tests/test_api.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8e4f9b71..7920f2ac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -76,3 +76,10 @@ def osx_base_location(self): monkeypatch.setattr(LinuxMenu, "_system_data_directory", tmp_path / "data") monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config")) monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "data")) + + +@pytest.fixture() +def run_as_user(monkeypatch): + from menuinst import utils as menuinst_utils + + monkeypatch.setattr(menuinst_utils, "user_is_admin", lambda: False) diff --git a/tests/test_api.py b/tests/test_api.py index a16236cc..2595f095 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -322,7 +322,7 @@ def test_url_protocol_association(delete_files): @pytest.mark.skipif(PLATFORM != "win", reason="Windows only") -def test_windows_terminal_profiles(tmp_path): +def test_windows_terminal_profiles(tmp_path, run_as_user): settings_file = Path( tmp_path, "localappdata", "Microsoft", "Windows Terminal", "settings.json" ) From 981f2cda4dedc0891db4653d2394d8da65fe1b92 Mon Sep 17 00:00:00 2001 From: Marco Esters Date: Wed, 15 May 2024 06:52:27 -0700 Subject: [PATCH 35/35] Update tests/conftest.py Co-authored-by: jaimergp --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7920f2ac..3e312ef0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,7 +52,7 @@ def tmpdir(tmpdir, request): @pytest.fixture(autouse=True) -def mock_locations(monkeypatch, tmp_path, request): +def mock_locations(monkeypatch, tmp_path): from menuinst.platforms.linux import LinuxMenu from menuinst.platforms.osx import MacOSMenuItem