Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions src/windows_mcp/desktop/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ def get_apps_from_start_menu(self) -> dict[str, str]:
command = "Get-StartApps | ConvertTo-Csv -NoTypeInformation"
apps_info, status = PowerShellExecutor.execute_command(command)

apps: dict[str, str] = {}
if status == 0 and apps_info and apps_info.strip():
try:
reader = csv.DictReader(io.StringIO(apps_info.strip()))
Expand All @@ -337,14 +338,39 @@ def get_apps_from_start_menu(self) -> dict[str, str]:
for row in reader
if row.get("Name") and row.get("AppID")
}
if apps:
return apps
except Exception as e:
logger.warning(f"Error parsing Get-StartApps output: {e}")

# Fallback: scan Start Menu shortcut folders (works on all Windows versions)
logger.info("Get-StartApps unavailable, falling back to Start Menu folder scan")
return self._get_apps_from_shortcuts()
if not apps:
# Fallback: scan Start Menu shortcut folders (works on all Windows versions)
logger.info("Get-StartApps unavailable, falling back to Start Menu folder scan")
apps = self._get_apps_from_shortcuts()

# AppsFolder supplies the display name rendered for the current Windows locale.
for name, appid in self._get_apps_folder_display_names().items():
apps.setdefault(name, appid)
return apps

def _get_apps_folder_display_names(self) -> dict[str, str]:
"""Return localized Start Menu display names mapped to AppUserModelIDs."""
command = (
"(New-Object -ComObject Shell.Application)."
"NameSpace('shell:::{4234d49b-0245-4df3-b780-3893943456e1}').Items() "
"| Select-Object Name,@{n='AppID';e={$_.Path}} | ConvertTo-Csv -NoTypeInformation"
)
apps_info, status = PowerShellExecutor.execute_command(command)

if status == 0 and apps_info and apps_info.strip():
try:
reader = csv.DictReader(io.StringIO(apps_info.strip()))
return {
row.get("Name", "").lower(): row.get("AppID", "")
for row in reader
if row.get("Name") and row.get("AppID")
}
except Exception as e:
logger.warning(f"Error parsing AppsFolder display names: {e}")
return {}

def _get_apps_from_shortcuts(self) -> dict[str, str]:
"""Scan Start Menu folders for .lnk shortcuts as a fallback for Get-StartApps."""
Expand Down
51 changes: 51 additions & 0 deletions tests/test_app_launch_localized.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import sys

import pytest

if sys.platform == "win32":
from windows_mcp.desktop.service import Desktop
from windows_mcp.powershell import PowerShellExecutor
else:
pytestmark = pytest.mark.skip(reason="Desktop imports Windows-only pywin32 modules")


def test_start_menu_apps_include_localized_appsfolder_name(monkeypatch: pytest.MonkeyPatch) -> None:
app_id = "Microsoft.WindowsNotepad_8wekyb3d8bbwe!App"
commands: list[str] = []

def execute_command(command: str, *_: object) -> tuple[str, int]:
commands.append(command)
if "Get-StartApps" in command:
return f'"Name","AppID"\n"Notepad","{app_id}"\n', 0
if "4234d49b-0245-4df3-b780-3893943456e1" in command:
return f'"Name","AppID"\n"记事本","{app_id}"\n', 0
if command == f"Start-Process 'shell:AppsFolder\\{app_id}'":
return "", 0
pytest.fail(f"unexpected PowerShell command: {command}")

monkeypatch.setattr(PowerShellExecutor, "execute_command", staticmethod(execute_command))

desktop = Desktop.__new__(Desktop)
monkeypatch.setattr(desktop, "_check_app_exists", lambda _: True)

assert desktop.launch_app("Notepad") == ("", 0, 0)
assert desktop.launch_app("记事本") == ("", 0, 0)
assert commands[-1] == f"Start-Process 'shell:AppsFolder\\{app_id}'"


def test_start_menu_shortcut_fallback_is_preserved_when_appsfolder_is_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
shortcut_apps = {
"legacy app": r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Legacy.lnk"
}

monkeypatch.setattr(
PowerShellExecutor,
"execute_command",
staticmethod(lambda *_: ("", 1)),
)
desktop = Desktop.__new__(Desktop)
monkeypatch.setattr(desktop, "_get_apps_from_shortcuts", lambda: shortcut_apps)

assert desktop.get_apps_from_start_menu() == shortcut_apps