diff --git a/profile_manager.py b/profile_manager.py index 15c5a5e..f5aca36 100644 --- a/profile_manager.py +++ b/profile_manager.py @@ -199,7 +199,7 @@ def _looks_like_uuid(value: str) -> bool: def _load_json(path: Path) -> dict[str, Any]: try: - return json.loads(path.read_text()) + return json.loads(path.read_text(encoding="utf-8")) except FileNotFoundError as exc: raise ProfileManagerError(f"Missing file: {path}") from exc except json.JSONDecodeError as exc: @@ -209,7 +209,7 @@ def _load_json(path: Path) -> dict[str, Any]: def _write_json_atomic(path: Path, data: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) temp_path = path.with_suffix(f"{path.suffix}.tmp") - temp_path.write_text(json.dumps(data, indent=2, sort_keys=True)) + temp_path.write_text(json.dumps(data, indent=2, sort_keys=True), encoding="utf-8") temp_path.replace(path) diff --git a/tests/test_profile_manager.py b/tests/test_profile_manager.py index a32ec55..dc148d9 100644 --- a/tests/test_profile_manager.py +++ b/tests/test_profile_manager.py @@ -28,7 +28,7 @@ def _stub_stream_deck_app_not_running(): def _write_json(path: Path, payload: dict) -> None: path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(payload)) + path.write_text(json.dumps(payload), encoding="utf-8") def _make_page_manifest(actions: dict | None = None, *, name: str = "") -> dict: @@ -1366,6 +1366,59 @@ def test_list_profiles_marks_unknown_models_clearly(tmp_path: Path) -> None: assert profiles[0]["device"]["ModelName"] == "Unknown Stream Deck model (20GZ9999)" +def test_list_profiles_reads_utf8_page_manifests(tmp_path: Path) -> None: + profiles_dir = tmp_path / "ProfilesV3" + profile_dir = profiles_dir / "UTF8.sdProfile" + page_uuid = "83480d17-dfd9-4fd1-afbc-7e318f88d2b5" + _write_json( + profile_dir / "manifest.json", + { + "AppIdentifier": "*", + "Device": {"Model": "20GBA9901", "UUID": "@(1)"}, + "Name": "UTF-8 Profile", + "Pages": {"Current": page_uuid, "Default": None, "Pages": []}, + "Version": "3.0", + }, + ) + page_manifest_path = profile_dir / "Profiles" / page_uuid.upper() / "manifest.json" + page_manifest_path.parent.mkdir(parents=True, exist_ok=True) + page_manifest_path.write_text( + json.dumps( + _make_page_manifest( + { + "0,0": { + "ActionID": "action-open", + "Name": "Thumbs up", + "Settings": {"pastedText": "👍"}, + "State": 0, + "States": [{}], + "UUID": "com.elgato.streamdeck.system.text", + } + }, + name="Emoji", + ), + ensure_ascii=False, + ), + encoding="utf-8", + ) + + manager = ProfileManager( + profiles_dir=profiles_dir, + scripts_dir=tmp_path / "scripts", + generated_icons_dir=tmp_path / "icons", + ) + + profiles = manager.list_profiles() + + assert len(profiles) == 1 + assert profiles[0]["pages"][0]["name"] == "Emoji" + page = manager.read_page(profile_name="UTF-8 Profile", page_index=0) + pasted_text = page["raw_manifest"]["Controllers"][0]["Actions"]["0,0"]["Settings"][ + "pastedText" + ] + assert pasted_text == "👍" + + def test_create_icons_generates_batch_in_one_call(tmp_path: Path) -> None: """Batch icon creation — the primary optimization behind this round of changes. 32-key decks authored with one icon per MCP call timed out in