diff options
| author | xXJSONDeruloXx <danielhimebauch@gmail.com> | 2026-09-10 16:26:17 -0400 |
|---|---|---|
| committer | xXJSONDeruloXx <danielhimebauch@gmail.com> | 2026-09-10 16:26:17 -0400 |
| commit | 485ebb748185391e28764f48c39322b3cbdac65c (patch) | |
| tree | 9b03047ed842e14d9c800bcdfa16cad322733cf4 | |
| parent | de74f0d2499159ed1cf8f628a166302146ae1f13 (diff) | |
| download | decky-lsfg-vk-v2-flatpakless.tar.gz decky-lsfg-vk-v2-flatpakless.zip | |
refactor: remove Flatpak supportv2-flatpakless
27 files changed, 104 insertions, 1739 deletions
diff --git a/assets/flatpak-target.png b/assets/flatpak-target.png Binary files differdeleted file mode 100644 index 773e567..0000000 --- a/assets/flatpak-target.png +++ /dev/null diff --git a/defaults/i18n/ja.json b/defaults/i18n/ja.json index fb4a3ad..6bd56b0 100644 --- a/defaults/i18n/ja.json +++ b/defaults/i18n/ja.json @@ -1,7 +1,6 @@ { "CONTENT_FPS_MULTIPLIER": "FPS倍率", "CONTENT_NERD_STUFF": "詳細情報", - "CONTENT_FLATPAK_SETUP": "Flatpak設定", "MULTIPLIER_OFF": "オフ", "CONFIG_SECTION_TITLE": "設定", "CONFIG_WORKAROUNDS_TITLE": "互換性設定", @@ -32,28 +31,6 @@ "INSTALL_UNINSTALLING": "アンインストール中...", "INSTALL_UNINSTALL_BTN": "LSFG-VKをアンインストール", "INSTALL_INSTALL_BTN": "LSFG-VKをインストール", - "FLATPAK_MODAL_TITLE": "Flatpak拡張", - "FLATPAK_RUNTIME_INSTALLER": "ランタイム拡張インストーラー", - "FLATPAK_RUNTIME_23": "ランタイム 23.08", - "FLATPAK_RUNTIME_24": "ランタイム 24.08", - "FLATPAK_RUNTIME_25": "ランタイム 25.08", - "FLATPAK_INSTALLED": "インストール済み", - "FLATPAK_NOT_INSTALLED": "未インストール", - "FLATPAK_UNINSTALL_TITLE": "ランタイム拡張をアンインストール", - "FLATPAK_UNINSTALL_CONFIRM_PREFIX": "本当に", - "FLATPAK_UNINSTALL_CONFIRM_SUFFIX": "ランタイム拡張をアンインストールしますか?", - "FLATPAK_UNINSTALL_BTN": "アンインストール", - "FLATPAK_INSTALL_BTN": "インストール", - "FLATPAK_APPS_TITLE": "Flatpakアプリケーション", - "FLATPAK_NO_APPS": "Flatpakアプリなし", - "FLATPAK_NO_APPS_DESC": "現在インストールされているFlatpakアプリケーションはありません", - "FLATPAK_STATUS_CONFIGURED": "設定済み", - "FLATPAK_STATUS_PARTIAL": "部分設定", - "FLATPAK_STATUS_NO_OVERRIDES": "オーバーライドなし", - "FLATPAK_ERROR": "エラー", - "FLATPAK_ERROR_STATUS": "拡張ステータスの確認に失敗しました", - "FLATPAK_ERROR_APPS": "Flatpakアプリケーションの読み込みに失敗しました", - "FLATPAK_CLOSE": "閉じる", "NERD_LOADING": "情報を読み込み中...", "NERD_DLL_PATH": "DLLパス", "NERD_NOT_AVAILABLE": "利用不可", diff --git a/defaults/i18n/ko.json b/defaults/i18n/ko.json index 29783c4..8301a92 100644 --- a/defaults/i18n/ko.json +++ b/defaults/i18n/ko.json @@ -1,7 +1,6 @@ { "CONTENT_FPS_MULTIPLIER": "FPS 배율", "CONTENT_NERD_STUFF": "상세 정보", - "CONTENT_FLATPAK_SETUP": "Flatpak 설정", "MULTIPLIER_OFF": "끄기", "CONFIG_SECTION_TITLE": "설정", "CONFIG_WORKAROUNDS_TITLE": "호환성 설정", @@ -32,28 +31,6 @@ "INSTALL_UNINSTALLING": "제거 중...", "INSTALL_UNINSTALL_BTN": "LSFG-VK 제거", "INSTALL_INSTALL_BTN": "LSFG-VK 설치", - "FLATPAK_MODAL_TITLE": "Flatpak 확장", - "FLATPAK_RUNTIME_INSTALLER": "런타임 확장 설치", - "FLATPAK_RUNTIME_23": "런타임 23.08", - "FLATPAK_RUNTIME_24": "런타임 24.08", - "FLATPAK_RUNTIME_25": "런타임 25.08", - "FLATPAK_INSTALLED": "설치됨", - "FLATPAK_NOT_INSTALLED": "설치 안 됨", - "FLATPAK_UNINSTALL_TITLE": "런타임 확장 제거", - "FLATPAK_UNINSTALL_CONFIRM_PREFIX": "정말로", - "FLATPAK_UNINSTALL_CONFIRM_SUFFIX": "런타임 확장을 제거하시겠습니까?", - "FLATPAK_UNINSTALL_BTN": "제거", - "FLATPAK_INSTALL_BTN": "설치", - "FLATPAK_APPS_TITLE": "Flatpak 애플리케이션", - "FLATPAK_NO_APPS": "Flatpak 앱 없음", - "FLATPAK_NO_APPS_DESC": "현재 설치된 Flatpak 애플리케이션이 없습니다", - "FLATPAK_STATUS_CONFIGURED": "설정됨", - "FLATPAK_STATUS_PARTIAL": "부분 설정", - "FLATPAK_STATUS_NO_OVERRIDES": "오버라이드 없음", - "FLATPAK_ERROR": "오류", - "FLATPAK_ERROR_STATUS": "확장 상태 확인 실패", - "FLATPAK_ERROR_APPS": "Flatpak 애플리케이션 로드 실패", - "FLATPAK_CLOSE": "닫기", "NERD_LOADING": "정보 불러오는 중...", "NERD_DLL_PATH": "DLL 경로", "NERD_NOT_AVAILABLE": "사용 불가", diff --git a/defaults/i18n/template.json b/defaults/i18n/template.json index 2a5b06e..6f1e18b 100644 --- a/defaults/i18n/template.json +++ b/defaults/i18n/template.json @@ -1,7 +1,6 @@ { "CONTENT_FPS_MULTIPLIER": "FPS Multiplier", "CONTENT_NERD_STUFF": "Nerd Stuff", - "CONTENT_FLATPAK_SETUP": "Flatpak Setup", "MULTIPLIER_OFF": "OFF", "CONFIG_SECTION_TITLE": "Config", "CONFIG_WORKAROUNDS_TITLE": "Workarounds", @@ -32,28 +31,6 @@ "INSTALL_UNINSTALLING": "Uninstalling...", "INSTALL_UNINSTALL_BTN": "Uninstall LSFG-VK", "INSTALL_INSTALL_BTN": "Install LSFG-VK", - "FLATPAK_MODAL_TITLE": "Flatpak Extensions", - "FLATPAK_RUNTIME_INSTALLER": "Runtime Extension Installer", - "FLATPAK_RUNTIME_23": "Runtime 23.08", - "FLATPAK_RUNTIME_24": "Runtime 24.08", - "FLATPAK_RUNTIME_25": "Runtime 25.08", - "FLATPAK_INSTALLED": "Installed", - "FLATPAK_NOT_INSTALLED": "Not installed", - "FLATPAK_UNINSTALL_TITLE": "Uninstall Runtime Extension", - "FLATPAK_UNINSTALL_CONFIRM_PREFIX": "Are you sure you want to uninstall the", - "FLATPAK_UNINSTALL_CONFIRM_SUFFIX": "runtime extension?", - "FLATPAK_UNINSTALL_BTN": "Uninstall", - "FLATPAK_INSTALL_BTN": "Install", - "FLATPAK_APPS_TITLE": "Flatpak Applications", - "FLATPAK_NO_APPS": "No Flatpak Apps Found", - "FLATPAK_NO_APPS_DESC": "No Flatpak applications are currently installed", - "FLATPAK_STATUS_CONFIGURED": "Configured", - "FLATPAK_STATUS_PARTIAL": "Partial", - "FLATPAK_STATUS_NO_OVERRIDES": "No overrides", - "FLATPAK_ERROR": "Error", - "FLATPAK_ERROR_STATUS": "Failed to check extension status", - "FLATPAK_ERROR_APPS": "Failed to load Flatpak applications", - "FLATPAK_CLOSE": "Close", "NERD_LOADING": "Loading information...", "NERD_DLL_PATH": "DLL Path", "NERD_NOT_AVAILABLE": "Not available", diff --git a/package.json b/package.json index b5e716c..4a1780a 100644 --- a/package.json +++ b/package.json @@ -52,21 +52,6 @@ "name": "lsfg-vk-2.0.0.tar.xz", "url": "https://builds.lsfg-vk.dev/lsfg-vk-2.0.0.tar.xz", "sha256hash": "08bdbdf373a111022df87dac7aa87e3b564bb841f961552e3ca85fea12b5aa74" - }, - { - "name": "org.freedesktop.Platform.VulkanLayer.lsfg_vk_23.08.flatpak", - "url": "https://github.com/xXJSONDeruloXx/lsfg-vk/releases/download/v2.0.0-decky.2/org.freedesktop.Platform.VulkanLayer.lsfg_vk_23.08.flatpak", - "sha256hash": "7eff81f96b278fde6fe395c88461c55e611547bf5991d179b9145ec6f5a778b1" - }, - { - "name": "org.freedesktop.Platform.VulkanLayer.lsfg_vk_24.08.flatpak", - "url": "https://github.com/xXJSONDeruloXx/lsfg-vk/releases/download/v2.0.0-decky.2/org.freedesktop.Platform.VulkanLayer.lsfg_vk_24.08.flatpak", - "sha256hash": "cbd663b9021c355dddec61ec51fd4388c12705f16637cadd4b45aead0e5dc427" - }, - { - "name": "org.freedesktop.Platform.VulkanLayer.lsfg_vk_25.08.flatpak", - "url": "https://github.com/xXJSONDeruloXx/lsfg-vk/releases/download/v2.0.0-decky.2/org.freedesktop.Platform.VulkanLayer.lsfg_vk_25.08.flatpak", - "sha256hash": "615e87c03b18368ed1cca97e036f1bf54fff95fc6126aac697bb368f33281cbf" } ], "pnpm": { diff --git a/py_modules/lsfg_vk/constants.py b/py_modules/lsfg_vk/constants.py index 960d230..45ac4c5 100644 --- a/py_modules/lsfg_vk/constants.py +++ b/py_modules/lsfg_vk/constants.py @@ -16,9 +16,6 @@ CLI_FILENAME = "lsfg-vk-cli" UI_FILENAME = "lsfg-vk-ui" UI_DESKTOP_FILENAME = "gay.pancake.lsfg-vk-ui.desktop" UI_ICON_FILENAME = "gay.pancake.lsfg-vk-ui.png" -FLATPAK_23_08_FILENAME = "org.freedesktop.Platform.VulkanLayer.lsfg_vk_23.08.flatpak" -FLATPAK_24_08_FILENAME = "org.freedesktop.Platform.VulkanLayer.lsfg_vk_24.08.flatpak" -FLATPAK_25_08_FILENAME = "org.freedesktop.Platform.VulkanLayer.lsfg_vk_25.08.flatpak" STEAM_LOSSLESS_SCALING_APP_ID = "993090" STEAM_LOSSLESS_SCALING_BRANCH = "lsfg-vk" diff --git a/py_modules/lsfg_vk/flatpak_service.py b/py_modules/lsfg_vk/flatpak_service.py deleted file mode 100644 index f486b74..0000000 --- a/py_modules/lsfg_vk/flatpak_service.py +++ /dev/null @@ -1,431 +0,0 @@ -"""Flatpak runtime support for classified Steam targets.""" - -from __future__ import annotations - -import json -import os -import pwd -import re -import shutil -import subprocess -import threading -from pathlib import Path -from typing import Dict, Optional, Set - -from .base_service import BaseService -from .constants import ( - BIN_DIR, - FLATPAK_23_08_FILENAME, - FLATPAK_24_08_FILENAME, - FLATPAK_25_08_FILENAME, -) - - -class FlatpakService(BaseService): - EXTENSION_ID = "org.freedesktop.Platform.VulkanLayer.lsfgvk" - SUPPORTED_RUNTIMES = ("23.08", "24.08", "25.08") - DERIVED_RUNTIME_IDS = {"org.gnome.Platform", "org.kde.Platform"} - RUNTIME_METADATA_SECTION = "Extension org.freedesktop.Platform.GL" - OWNERSHIP_FILENAME = "flatpak_extensions.json" - OWNERSHIP_VERSION = 1 - APP_ID_PATTERN = re.compile( - r"^[A-Za-z0-9][A-Za-z0-9-]*(?:\.[A-Za-z0-9][A-Za-z0-9-]*)+$" - ) - - def __init__(self, logger=None): - super().__init__(logger) - self.flatpak_command: Optional[str] = None - self._lock = threading.RLock() - - @property - def ownership_path(self) -> Path: - return self.config_dir / self.OWNERSHIP_FILENAME - - def _clean_env(self) -> Dict[str, str]: - env = os.environ.copy() - env.pop("LD_LIBRARY_PATH", None) - env["HOME"] = str(self.user_home) - path = [entry for entry in env.get("PATH", "").split(":") if entry] - for entry in ("/usr/bin", "/usr/local/bin", "/bin"): - if entry not in path: - path.insert(0, entry) - env["PATH"] = ":".join(path) - return env - - def check_flatpak_available(self) -> bool: - env = self._clean_env() - self.flatpak_command = shutil.which("flatpak", path=env["PATH"]) - return self.flatpak_command is not None - - def _run_flatpak_command(self, args, **kwargs): - if self.flatpak_command is None and not self.check_flatpak_available(): - raise FileNotFoundError("Flatpak command not available") - env = self._clean_env() - command = [self.flatpak_command, *args] - try: - user = pwd.getpwuid(self.user_home.stat().st_uid) - except (KeyError, OSError) as error: - raise RuntimeError(f"Unable to resolve Flatpak user for {self.user_home}") from error - if os.geteuid() != user.pw_uid: - runuser = shutil.which("runuser", path=env["PATH"]) - if runuser is None: - raise FileNotFoundError("runuser command not available") - command = [runuser, "--user", user.pw_name, "--", *command] - return subprocess.run(command, env=env, **kwargs) - - @classmethod - def _validate_app_id(cls, app_id: str) -> str: - if not isinstance(app_id, str) or not cls.APP_ID_PATTERN.fullmatch(app_id): - raise ValueError("Invalid Flatpak application ID") - return app_id - - @classmethod - def _validate_runtime(cls, branch: str) -> str: - if branch not in cls.SUPPORTED_RUNTIMES: - raise ValueError( - f"Unsupported Flatpak runtime branch {branch}; supported branches are " - + ", ".join(cls.SUPPORTED_RUNTIMES) - ) - return branch - - @classmethod - def runtime_branch_from_ref(cls, runtime_ref: str) -> str: - parts = runtime_ref.strip().split("/") if isinstance(runtime_ref, str) else [] - if len(parts) != 3 or parts[0] != "org.freedesktop.Platform": - raise ValueError(f"Unsupported Flatpak runtime reference: {runtime_ref}") - return cls._validate_runtime(parts[2]) - - @classmethod - def runtime_branch_from_metadata(cls, metadata: str) -> str: - section = None - versions = [] - for raw_line in metadata.splitlines() if isinstance(metadata, str) else []: - line = raw_line.strip() - if line.startswith("[") and line.endswith("]"): - section = line[1:-1].strip() - continue - if section != cls.RUNTIME_METADATA_SECTION: - continue - key, separator, value = line.partition("=") - if separator and key.strip() == "versions": - versions.extend(part.strip() for part in value.split(";")) - for value in versions: - for branch in cls.SUPPORTED_RUNTIMES: - if value == branch or value.startswith(f"{branch}-"): - return branch - raise ValueError("Could not determine a supported Freedesktop base runtime from Flatpak metadata") - - @classmethod - def _extension_ref(cls, branch: str) -> str: - return f"{cls.EXTENSION_ID}/x86_64/{cls._validate_runtime(branch)}" - - def _bundled_extension_path(self, branch: str) -> Path: - filename = { - "23.08": FLATPAK_23_08_FILENAME, - "24.08": FLATPAK_24_08_FILENAME, - "25.08": FLATPAK_25_08_FILENAME, - }[self._validate_runtime(branch)] - return Path(__file__).resolve().parent.parent.parent / BIN_DIR / filename - - def _installed_extension_branches(self, scope: Optional[str] = None) -> Set[str]: - scopes = ("user", "system") if scope is None else (scope,) - if any(item not in ("user", "system") for item in scopes): - raise ValueError("Flatpak installation scope must be user or system") - installed = set() - for item in scopes: - result = self._run_flatpak_command( - ["list", f"--{item}", "--runtime", "--columns=application,arch,branch"], - capture_output=True, - text=True, - check=True, - ) - for line in result.stdout.splitlines(): - fields = line.split("\t") if "\t" in line else line.split() - if len(fields) >= 3 and fields[0] == self.EXTENSION_ID and fields[1] == "x86_64": - installed.add(fields[2]) - return installed - - def _owned_branches(self) -> Set[str]: - path = self.ownership_path - if not path.exists() and not path.is_symlink(): - return set() - if path.is_symlink() or not path.is_file(): - raise RuntimeError("Flatpak ownership metadata is not a regular file") - try: - data = json.loads(path.read_text(encoding="utf-8")) - branches = data.get("plugin_owned_branches") - if data.get("version") != self.OWNERSHIP_VERSION or not isinstance(branches, list): - raise ValueError("invalid ownership metadata") - owned = {self._validate_runtime(branch) for branch in branches} - if len(owned) != len(branches): - raise ValueError("invalid ownership metadata") - return owned - except (OSError, json.JSONDecodeError, TypeError, ValueError) as error: - raise RuntimeError(f"Could not trust Flatpak ownership metadata: {error}") from error - - def _write_owned_branches(self, branches: Set[str]) -> None: - if not branches: - self.ownership_path.unlink(missing_ok=True) - return - self._write_file( - self.ownership_path, - json.dumps( - { - "version": self.OWNERSHIP_VERSION, - "plugin_owned_branches": sorted(branches), - }, - indent=2, - ) + "\n", - ) - - def get_extension_status(self): - try: - available = self.check_flatpak_available() - installed = self._installed_extension_branches() if available else set() - return self._success_response( - dict, - "Flatpak runtime extension status retrieved" if available else "Flatpak is not available", - available=available, - extension_id=self.EXTENSION_ID, - supported_branches=list(self.SUPPORTED_RUNTIMES), - installed_branches=sorted(installed), - ) - except Exception as error: - return self._error_response( - dict, - str(error), - available=False, - extension_id=self.EXTENSION_ID, - supported_branches=list(self.SUPPORTED_RUNTIMES), - installed_branches=[], - ) - - get_flatpak_support_status = get_extension_status - - def _resolve_runtime(self, app_id: str): - self._validate_app_id(app_id) - if not self.check_flatpak_available(): - raise FileNotFoundError("Flatpak is not available on this system") - result = self._run_flatpak_command( - ["info", "--show-runtime", app_id], - capture_output=True, - text=True, - ) - if result.returncode != 0: - raise OSError(result.stderr.strip() or f"Could not inspect Flatpak app {app_id}") - runtime = result.stdout.strip().splitlines()[0] if result.stdout.strip() else "" - parts = runtime.split("/") - if len(parts) != 3: - raise ValueError(f"Unsupported Flatpak runtime reference: {runtime}") - if parts[0] == "org.freedesktop.Platform": - branch = self._validate_runtime(parts[2]) - elif parts[0] in self.DERIVED_RUNTIME_IDS: - metadata_result = self._run_flatpak_command( - ["info", "--show-metadata", runtime], - capture_output=True, - text=True, - ) - if metadata_result.returncode != 0: - raise OSError( - metadata_result.stderr.strip() - or f"Could not inspect Flatpak runtime {runtime}" - ) - branch = self.runtime_branch_from_metadata(metadata_result.stdout) - else: - raise ValueError(f"Unsupported Flatpak runtime reference: {runtime}") - return runtime, branch - - def resolve_app_support(self, app_id: str): - try: - app_id = self._validate_app_id(app_id) - runtime, branch = self._resolve_runtime(app_id) - installed = self._installed_extension_branches() - ready = branch in installed - return self._success_response( - dict, - f"lsfg-vk support is ready for {app_id}" if ready - else f"lsfg-vk runtime extension {branch} is required for {app_id}", - flatpak_app_id=app_id, - runtime=runtime, - runtime_branch=branch, - support_status="ready" if ready else "needs-runtime", - extension_installed=ready, - installed_branches=sorted(installed), - ) - except ValueError as error: - return self._success_response( - dict, - str(error), - flatpak_app_id=app_id, - runtime=None, - runtime_branch=None, - support_status="unsupported", - extension_installed=False, - installed_branches=[], - error=str(error), - ) - except Exception as error: - return self._error_response( - dict, - str(error), - flatpak_app_id=app_id, - runtime=None, - runtime_branch=None, - support_status="error", - extension_installed=False, - installed_branches=[], - ) - - def install_extension(self, branch: str): - try: - branch = self._validate_runtime(branch) - if not self.check_flatpak_available(): - raise FileNotFoundError("Flatpak is not available on this system") - with self._lock: - if branch in self._installed_extension_branches(): - return self._extension_result(branch, True, False, "already installed") - bundle = self._bundled_extension_path(branch) - if not bundle.is_file(): - raise FileNotFoundError(f"Bundled Flatpak extension not found at {bundle}; reinstall the plugin") - result = self._run_flatpak_command( - ["install", "--user", "--noninteractive", "--or-update", str(bundle)], - capture_output=True, - text=True, - ) - if result.returncode != 0: - raise OSError(result.stderr.strip() or "Flatpak installation failed") - if branch not in self._installed_extension_branches("user"): - raise RuntimeError(f"Flatpak install completed but {self._extension_ref(branch)} was not visible afterwards") - owned = self._owned_branches() - owned.add(branch) - self._write_owned_branches(owned) - return self._extension_result(branch, True, False, "installed") - except Exception as error: - return self._error_response(dict, str(error), runtime_branch=branch, installed=False, enabled=False) - - def _remove_extension(self, branch: str) -> bool: - if branch not in self._installed_extension_branches("user"): - return False - result = self._run_flatpak_command( - ["uninstall", "--user", "--noninteractive", self._extension_ref(branch)], - capture_output=True, - text=True, - ) - if result.returncode != 0: - raise OSError(result.stderr.strip() or "Flatpak uninstall failed") - if branch in self._installed_extension_branches("user"): - raise RuntimeError(f"Flatpak uninstall completed but {self._extension_ref(branch)} is still installed") - return True - - def _extension_result(self, branch: str, installed: bool, removed: bool, verb: str): - return self._success_response( - dict, - f"lsfg-vk {branch} runtime extension {verb}", - runtime_branch=branch, - installed=installed, - enabled=installed, - removed=removed, - ) - - def uninstall_extension(self, branch: str): - try: - branch = self._validate_runtime(branch) - if not self.check_flatpak_available(): - raise FileNotFoundError("Flatpak is not available on this system") - with self._lock: - owned = self._owned_branches() - if branch not in owned: - installed = branch in self._installed_extension_branches() - return self._extension_result(branch, installed, False, "preserved (not plugin-owned)") - removed = self._remove_extension(branch) - owned.remove(branch) - self._write_owned_branches(owned) - installed = branch in self._installed_extension_branches() - return self._extension_result(branch, installed, removed, "uninstalled") - except Exception as error: - return self._error_response( - dict, - str(error), - runtime_branch=branch, - removed=False, - installed=False, - enabled=False, - ) - - def ensure_extension(self, branch: str): - try: - branch = self._validate_runtime(branch) - if branch in self._installed_extension_branches(): - return self._extension_result(branch, True, False, "is ready") - except Exception as error: - return self._error_response(dict, str(error), runtime_branch=branch, support_status="error") - return self.install_extension(branch) - - def ensure_app_support(self, app_id: str): - resolved = self.resolve_app_support(app_id) - if not resolved.get("success") or resolved.get("support_status") != "needs-runtime": - return resolved - result = self.ensure_extension(resolved["runtime_branch"]) - if not result.get("success"): - return self._error_response( - dict, - result.get("error") or "Could not install the required Flatpak runtime extension", - flatpak_app_id=app_id, - runtime=resolved.get("runtime"), - runtime_branch=resolved.get("runtime_branch"), - support_status="error", - extension_installed=False, - ) - return self.resolve_app_support(app_id) - - def set_extension_enabled(self, branch: str, enabled: bool): - if type(enabled) is not bool: - return self._error_response(dict, "enabled must be a boolean", runtime_branch=branch, installed=False, enabled=False) - return self.install_extension(branch) if enabled else self.uninstall_extension(branch) - - def remove_plugin_owned_extensions(self): - try: - with self._lock: - owned = self._owned_branches() - if not owned: - return self._success_response( - dict, - "No plugin-owned Flatpak extensions to remove", - removed_branches=[], - preserved_branches=[], - ownership_uncertain=False, - ) - if not self.check_flatpak_available(): - raise RuntimeError("Flatpak is not available; plugin-owned extension metadata was preserved") - removed, failures = [], [] - for branch in sorted(owned): - try: - self._remove_extension(branch) - removed.append(branch) - except Exception as error: - failures.append(f"{branch}: {error}") - remaining = owned - set(removed) - self._write_owned_branches(remaining) - if failures: - return self._error_response( - dict, - "; ".join(failures), - removed_branches=removed, - preserved_branches=sorted(remaining), - ownership_uncertain=False, - ) - return self._success_response( - dict, - "Plugin-owned Flatpak extensions removed", - removed_branches=removed, - preserved_branches=[], - ownership_uncertain=False, - ) - except Exception as error: - return self._error_response( - dict, - str(error), - removed_branches=[], - preserved_branches=[], - ownership_uncertain=True, - ) diff --git a/py_modules/lsfg_vk/plugin.py b/py_modules/lsfg_vk/plugin.py index 13df7a4..b010db6 100644 --- a/py_modules/lsfg_vk/plugin.py +++ b/py_modules/lsfg_vk/plugin.py @@ -1,10 +1,9 @@ import os -from typing import Any, Dict, Optional +from typing import Any, Dict import decky from .configuration import ConfigurationService -from .flatpak_service import FlatpakService from .installation import InstallationService from .runtime_service import RuntimeService from .steam_service import SteamService @@ -20,7 +19,6 @@ class Plugin: steam_service=self.steam_service, ) self.configuration_service = ConfigurationService(runtime_service=self.runtime_service) - self.flatpak_service = FlatpakService() self.wrapper_service = WrapperService() async def install_lsfg_vk(self): @@ -36,21 +34,7 @@ class Plugin: return self.configuration_service.get_game_configs() async def get_installed_games(self): - result = self.steam_service.get_installed_games() - if not result.get("success"): - return result - cache: Dict[str, Dict[str, Any]] = {} - for game in result.get("games", []): - transport = game.get("transport", {}) - if transport.get("kind") != "flatpak": - continue - app_id = transport.get("flatpakAppId") - if not app_id: - continue - if app_id not in cache: - cache[app_id] = self.flatpak_service.resolve_app_support(app_id) - game["flatpakSupport"] = cache[app_id] - return result + return self.steam_service.get_installed_games() async def update_game_config(self, appid: str, game_name: str, config: Dict[str, Any]): return self.configuration_service.update_game_config(appid, game_name, config) @@ -68,17 +52,9 @@ class Plugin: self, appid: str, state: Dict[str, Any], - shortcut_exe: Optional[str] = None, command_token_added: bool = False, - transport: Optional[Dict[str, Any]] = None, ): - return self.wrapper_service.set( - appid, - state, - shortcut_exe, - command_token_added, - transport, - ) + return self.wrapper_service.set(appid, state, command_token_added) async def remove_workaround_state(self, appid: str): return self.wrapper_service.remove(appid) @@ -112,7 +88,6 @@ class Plugin: ("config", "LSFG-VK configuration", self.configuration_service.config_file_path), ("workarounds", "Per-app workarounds", self.wrapper_service.sidecar_path), ("wrapper", "Generated launch wrapper", self.wrapper_service.wrapper_path), - ("flatpak_extensions", "Flatpak extension ownership", self.flatpak_service.ownership_path), ) contents = [] for file_id, label, path in files: @@ -147,18 +122,6 @@ class Plugin: async def get_lossless_scaling_branch_status(self): return self.steam_service.get_branch_status() - async def get_flatpak_support_status(self): - return self.flatpak_service.get_flatpak_support_status() - - async def ensure_flatpak_support(self, flatpak_app_id: str): - return self.flatpak_service.ensure_app_support(flatpak_app_id) - - async def repair_flatpak_support(self, flatpak_app_id: str): - return self.flatpak_service.ensure_app_support(flatpak_app_id) - - async def set_flatpak_extension_enabled(self, version: str, enabled: bool): - return self.flatpak_service.set_extension_enabled(version, enabled) - async def _main(self): repair = self.wrapper_service.repair() if not repair.get("success"): @@ -171,12 +134,6 @@ class Plugin: async def _uninstall(self): decky.logger.info("decky-lsfg-vk plugin being uninstalled") self.installation_service.cleanup_on_uninstall() - try: - result = self.flatpak_service.remove_plugin_owned_extensions() - if not result.get("success"): - decky.logger.warning(result.get("error")) - except Exception as error: - decky.logger.error(f"Error during Flatpak cleanup: {error}") decky.logger.info("decky-lsfg-vk plugin uninstall cleanup completed") async def _migration(self): diff --git a/py_modules/lsfg_vk/steam_service.py b/py_modules/lsfg_vk/steam_service.py index a952fcb..53748de 100644 --- a/py_modules/lsfg_vk/steam_service.py +++ b/py_modules/lsfg_vk/steam_service.py @@ -1,5 +1,4 @@ import re -import shlex from pathlib import Path from typing import Dict, Optional, Tuple @@ -7,51 +6,8 @@ from .base_service import BaseService from .constants import ( STEAM_LOSSLESS_SCALING_APP_ID, STEAM_LOSSLESS_SCALING_BRANCH, - WRAPPER_FILENAME, ) -_FLATPAK_APP_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]*(?:\.[A-Za-z0-9][A-Za-z0-9-]*)+$") -_WRAPPER_TOKEN = f"~/{WRAPPER_FILENAME}" - - -def _split_command(value: Optional[str]) -> Optional[list[str]]: - if not isinstance(value, str) or not value.strip(): - return [] - try: - return shlex.split(value, posix=True) - except ValueError: - return None - - -def _is_managed_wrapper(value: str) -> bool: - if value in {_WRAPPER_TOKEN, f"$HOME/{WRAPPER_FILENAME}"}: - return True - path = Path(value) - return path.is_absolute() and path.name == WRAPPER_FILENAME - - -def classify_shortcut_transport(executable: Optional[str], launch_options: Optional[str] = None) -> Dict[str, object]: - executable_tokens = _split_command(executable) - option_tokens = _split_command(launch_options) - if executable_tokens is None or option_tokens is None or not executable_tokens: - return {"kind": "host"} - direct_flatpak = executable_tokens[0] in {"flatpak", "/usr/bin/flatpak"} - managed_wrapper = len(executable_tokens) == 1 and _is_managed_wrapper(executable_tokens[0]) - if not direct_flatpak and not managed_wrapper: - return {"kind": "host"} - arguments = [*executable_tokens[1:], *option_tokens] - if not arguments or arguments[0] != "run": - return {"kind": "host"} - for argument in arguments[1:]: - if argument == "--" or argument.startswith("-"): - continue - return ( - {"kind": "flatpak", "flatpakAppId": argument} - if _FLATPAK_APP_ID.fullmatch(argument) - else {"kind": "host"} - ) - return {"kind": "host"} - def _first_string(values: Dict[str, object], *keys: str) -> Optional[str]: return next((values[key] for key in keys if isinstance(values.get(key), str)), None) @@ -74,7 +30,6 @@ class SteamService(BaseService): self.user_home / ".local/share/Steam", self.user_home / ".steam/steam", self.user_home / ".steam/root", - self.user_home / ".var/app/com.valvesoftware.Steam/.local/share/Steam", ): yield from self._unique_existing_root(candidate, seen) @@ -154,7 +109,6 @@ class SteamService(BaseService): "appid": str(appid & 0xFFFFFFFF), "name": name, "nonSteam": True, - "transport": classify_shortcut_transport(executable, arguments), } for key, value in (("executable", executable), ("arguments", arguments), ("startDir", start_dir)): if value is not None: @@ -309,7 +263,6 @@ class SteamService(BaseService): "appid": appid, "name": self._section_value(content, "AppState", "name") or f"App {appid}", "nonSteam": False, - "transport": {"kind": "host"}, } for game in self._shortcut_games(): games.setdefault(str(game["appid"]), game) diff --git a/py_modules/lsfg_vk/wrapper_service.py b/py_modules/lsfg_vk/wrapper_service.py index 980f7ae..69a3600 100644 --- a/py_modules/lsfg_vk/wrapper_service.py +++ b/py_modules/lsfg_vk/wrapper_service.py @@ -4,9 +4,7 @@ from __future__ import annotations import json import re -import shlex import threading -from pathlib import Path from typing import Any, Dict, Optional, Tuple from .base_service import BaseService @@ -86,51 +84,18 @@ class WrapperService(BaseService): return state @classmethod - def _validate_transport(cls, raw: Any) -> Dict[str, Any]: - if raw is None: - return {"kind": "host"} - if not isinstance(raw, dict): - raise ValueError("Workaround transport must be an object") - kind = raw.get("kind") - if kind == "host": - return {"kind": "host"} - if kind == "flatpak": - app_id = raw.get("flatpakAppId") - if ( - not isinstance(app_id, str) - or not re.fullmatch( - r"^[A-Za-z0-9][A-Za-z0-9-]*(?:\.[A-Za-z0-9][A-Za-z0-9-]*)+$", - app_id, - ) - ): - raise ValueError("Flatpak transport requires a valid application ID") - return {"kind": "flatpak", "flatpakAppId": app_id} - raise ValueError("Workaround transport must be host or flatpak") - - @classmethod def _validate_entry(cls, raw: Any) -> Dict[str, Any]: if not isinstance(raw, dict): raise ValueError("Workaround AppID entry must be an object") + unsupported = set(raw) - {"state", "command_token_added"} + if unsupported: + raise ValueError("Unsupported workaround entry fields: " + ", ".join(sorted(unsupported))) entry = { "state": cls._validate_state(raw.get("state")), "command_token_added": raw.get("command_token_added", False), - # Version 1 entries had no transport field. They are preserved as - # host entries until the shortcut is explicitly repaired with the - # backend's classified transport. - "transport": cls._validate_transport(raw.get("transport")), } if type(entry["command_token_added"]) is not bool: raise ValueError("command_token_added must be a boolean") - if entry["transport"]["kind"] == "flatpak" and "shortcut_exe" in raw and raw["shortcut_exe"] is not None: - shortcut_exe = raw["shortcut_exe"] - if ( - not isinstance(shortcut_exe, str) - or not shortcut_exe.startswith("/") - or "\x00" in shortcut_exe - or not shortcut_exe.strip() - ): - raise ValueError("shortcut_exe must be an absolute executable path") - entry["shortcut_exe"] = shortcut_exe return entry @classmethod @@ -186,23 +151,8 @@ class WrapperService(BaseService): ) return True - @staticmethod - def _shell(value: str) -> str: - return shlex.quote(value) - - @staticmethod - def _direct_flatpak_tokens(value: str) -> Optional[list[str]]: - """Parse the supported full executable form: /usr/bin/flatpak run APP.""" - try: - tokens = shlex.split(value, posix=True) - except ValueError: - return None - if len(tokens) >= 3 and Path(tokens[0]).name == "flatpak" and tokens[1] == "run": - return tokens - return None - @classmethod - def _state_lines(cls, state: Dict[str, Any], shortcut_exe: Optional[str]) -> list[str]: + def _state_lines(cls, state: Dict[str, Any]) -> list[str]: lines = [" unset " + " ".join(cls.MANAGED_ENV_KEYS)] if state["disableGamescopeWsi"]: lines.extend([" ENABLE_GAMESCOPE_WSI=0", " export ENABLE_GAMESCOPE_WSI"]) @@ -235,68 +185,8 @@ class WrapperService(BaseService): " fi", " export DXVK_CONFIG", ]) - lines.append(f" shortcut_exe={cls._shell(shortcut_exe or '')}") return lines - def _dll_directory(self) -> Path: - if self.config_file_path.exists(): - try: - content = self.config_file_path.read_text(encoding="utf-8") - match = re.search( - r'(?m)^[ \t]*dll[ \t]*=[ \t]*"((?:\\.|[^"\\])*)"', - content, - ) - if match: - configured_dll = json.loads('"' + match.group(1) + '"') - if configured_dll: - return Path(configured_dll).parent - except Exception: - pass - return self.user_home / ".local/share/Steam/steamapps/common/Lossless Scaling" - - def _flatpak_args(self, state: Dict[str, Any]) -> list[str]: - config_dir = str(self.config_dir) - config_file = str(self.config_file_path) - dll_dir = str(self._dll_directory()) - args = [ - self._shell(f"--filesystem={config_dir}:rw"), - self._shell(f"--filesystem={dll_dir}:ro"), - self._shell(f"--env=LSFGVK_CONFIG={config_file}"), - '"--env=LSFGVK_FLATPAK=1"', - '"--env=SteamAppId=$appid"', - '"--unset-env=DISABLE_LSFGVK" "--unset-env=DISABLE_LSFG"', - '"--unset-env=DISABLE_GAMESCOPE_WSI"', - '"--unset-env=ENABLE_GAMESCOPE_WSI"' if not state["disableGamescopeWsi"] else - '"--env=ENABLE_GAMESCOPE_WSI=0"', - '"--unset-env=DXVK_HDR"' if not state["disableHdr"] else - '"--env=DXVK_HDR=0"', - '"--unset-env=SteamDeck"' if not state["disableSteamdeckMode"] else - '"--env=SteamDeck=0"', - '"--unset-env=DISABLE_VKBASALT" "--unset-env=ENABLE_VKBASALT"', - ] - if state["disableVkbasalt"]: - args.append('"--env=DISABLE_VKBASALT=1"') - args.extend([ - '"--unset-env=MESA_LOADER_DRIVER_OVERRIDE" "--unset-env=__GLX_VENDOR_LIBRARY_NAME" "--unset-env=GALLIUM_DRIVER"', - ]) - if state["enableZink"]: - args.extend([ - '"--env=__GLX_VENDOR_LIBRARY_NAME=mesa"', - '"--env=MESA_LOADER_DRIVER_OVERRIDE=zink"', - '"--env=GALLIUM_DRIVER=zink"', - ]) - args.extend([ - '"--unset-env=DXVK_FRAME_RATE"', - ]) - static_args = " ".join(args) - return [ - ' if [ -n "${DXVK_CONFIG+x}" ]; then', - f' set -- "$flatpak_command" {static_args} "--env=DXVK_CONFIG=$DXVK_CONFIG" "$@"', - " else", - f' set -- "$flatpak_command" {static_args} "$@"', - " fi", - ] - def _render_wrapper(self, document: Dict[str, Any]) -> str: lines = [ "#!/bin/sh", @@ -320,69 +210,16 @@ class WrapperService(BaseService): ' *) appid="${STEAM_COMPAT_APP_ID}" ;;', " esac", "fi", - "shortcut_exe=", 'case "$appid" in', ] for appid in sorted(document["apps"], key=lambda value: int(value)): entry = document["apps"][appid] lines.append(f" {appid})") - lines.extend(self._state_lines(entry["state"], entry.get("shortcut_exe"))) + lines.extend(self._state_lines(entry["state"])) lines.append(" ;;") lines.extend([ "esac", "", - 'if [ -n "$shortcut_exe" ]; then', - ]) - # The arguments are emitted per branch below so the values are static and - # the wrapper never needs a JSON parser or another helper executable. - lines.append(' case "$appid" in') - for appid in sorted(document["apps"], key=lambda value: int(value)): - entry = document["apps"][appid] - transport = entry.get("transport", {"kind": "host"}) - if transport.get("kind") != "flatpak": - continue - shortcut_exe = entry.get("shortcut_exe", "") - direct_flatpak_tokens = self._direct_flatpak_tokens(shortcut_exe) - if direct_flatpak_tokens is None and Path(shortcut_exe).name != "flatpak": - raise ValueError( - f"Flatpak target {appid} does not use a direct flatpak executable" - ) - lines.append(f" {appid})") - lines.extend([ - *( - [ - f" shortcut_exe={self._shell(direct_flatpak_tokens[0])}", - " set -- " - + " ".join(self._shell(token) for token in direct_flatpak_tokens[1:]) - + ' "$@"', - ] - if direct_flatpak_tokens - else [] - ), - ' if [ "${1-}" != "run" ]; then', - ' echo "lsfg-vk: Flatpak shortcut must use direct flatpak run transport" >&2', - " exit 64", - " fi", - ' flatpak_command="$1"', - " shift", - " flatpak_target=", - ' for flatpak_arg in "$@"; do', - ' case "$flatpak_arg" in', - ' -*) ;;', - ' *) flatpak_target="$flatpak_arg"; break ;;', - " esac", - " done", - f' if [ "$flatpak_target" != {self._shell(transport["flatpakAppId"])} ]; then', - ' echo "lsfg-vk: Flatpak shortcut application ID changed externally" >&2', - " exit 64", - " fi", - ]) - lines.extend(self._flatpak_args(entry["state"])) - lines.append(" ;;") - lines.extend([ - " esac", - ' exec "$shortcut_exe" "$@"', - "fi", 'exec "$@"', "", ]) @@ -424,9 +261,7 @@ class WrapperService(BaseService): "state": dict(entry["state"]) if entry else None, "wrapper_path": self.WRAPPER_TOKEN, "wrapper_owned": self._wrapper_marker() if document["apps"] else False, - "shortcut_exe": entry.get("shortcut_exe") if entry else None, "command_token_added": entry.get("command_token_added", False) if entry else False, - "transport": dict(entry.get("transport", {"kind": "host"})) if entry else None, } def get(self, appid: str) -> Dict[str, Any]: @@ -451,9 +286,7 @@ class WrapperService(BaseService): self, appid: str, state: Dict[str, Any], - shortcut_exe: Optional[str] = None, command_token_added: bool = False, - transport: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: try: normalized = self._valid_appid(appid) @@ -463,26 +296,10 @@ class WrapperService(BaseService): with self._lock: self._assert_wrapper_owned_or_absent() document, _, _ = self._read_document() - previous_entry = document["apps"].get(normalized) - selected_transport = self._validate_transport( - transport - if transport is not None - else ( - previous_entry.get("transport") - if previous_entry - else None - ) - ) entry: Dict[str, Any] = { "state": validated_state, "command_token_added": bool(command_token_added), - "transport": selected_transport, } - if selected_transport["kind"] == "flatpak": - if shortcut_exe is not None: - entry = self._validate_entry({**entry, "shortcut_exe": shortcut_exe}) - elif previous_entry and "shortcut_exe" in previous_entry: - entry["shortcut_exe"] = previous_entry["shortcut_exe"] document["apps"][normalized] = entry self._write_pair(document) return self._response(document, normalized) diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts index 567e1ae..f8ef765 100644 --- a/src/api/lsfgApi.ts +++ b/src/api/lsfgApi.ts @@ -30,10 +30,6 @@ export interface SteamBranchStatus extends ApiResult { } export type LsfgConfig = ConfigurationData; -export type TargetTransport = - | { kind: "host" } - | { kind: "flatpak"; flatpakAppId: string }; -export type FlatpakTargetSupportStatus = "ready" | "needs-runtime" | "unsupported" | "error"; export interface GameConfigEntry { appid: string; @@ -45,11 +41,9 @@ export interface InstalledGame { appid: string; name: string; nonSteam: boolean; - transport: TargetTransport; executable?: string; arguments?: string; startDir?: string; - flatpakSupport?: FlatpakTargetSupport; } export interface GlobalConfig { @@ -57,15 +51,6 @@ export interface GlobalConfig { no_fp16: boolean; } -export interface FlatpakTargetSupport extends ApiResult { - flatpak_app_id?: string; - runtime?: string | null; - runtime_branch?: string | null; - support_status: FlatpakTargetSupportStatus; - extension_installed: boolean; - installed_branches: string[]; -} - export interface WorkaroundState { dxvkFrameRate: number; disableGamescopeWsi: boolean; @@ -80,9 +65,7 @@ export interface WorkaroundStateResult extends ApiResult { state?: WorkaroundState | null; wrapper_path?: string; wrapper_owned?: boolean; - shortcut_exe?: string | null; command_token_added?: boolean; - transport?: TargetTransport | null; } export interface GameConfigsResult extends ApiResult { @@ -118,42 +101,17 @@ export interface DebugFileContentsResult extends ApiResult { files?: DebugFileContent[]; } -export interface FlatpakExtensionStatus extends ApiResult { - message: string; - available: boolean; - extension_id: string; - supported_branches: string[]; - installed_branches: string[]; -} - -export interface FlatpakExtensionToggleResult extends ApiResult { - message: string; - runtime_branch: string; - enabled: boolean; - installed: boolean; -} - export const installLsfgVk = callable<[], InstallationResult>("install_lsfg_vk"); export const uninstallLsfgVk = callable<[], InstallationResult>("uninstall_lsfg_vk"); export const checkLsfgVkInstalled = callable<[], InstallationStatus>("check_lsfg_vk_installed"); export const getLosslessScalingBranchStatus = callable<[], SteamBranchStatus>("get_lossless_scaling_branch_status"); export const getConfigFileContent = callable<[], FileContentResult>("get_config_file_content"); -export const getFlatpakSupportStatus = callable<[], FlatpakExtensionStatus>("get_flatpak_support_status"); -export const ensureFlatpakSupport = callable<[string], FlatpakTargetSupport>("ensure_flatpak_support"); -export const repairFlatpakSupport = callable<[string], FlatpakTargetSupport>("repair_flatpak_support"); -export const setFlatpakExtensionEnabled = callable<[string, boolean], FlatpakExtensionToggleResult>("set_flatpak_extension_enabled"); export const getGameConfigs = callable<[], GameConfigsResult>("get_game_configs"); export const getInstalledGames = callable<[], InstalledGamesResult>("get_installed_games"); export const updateGameConfig = callable<[string, string, LsfgConfig], GameConfigResult>("update_game_config"); export const resetGameConfig = callable<[string], GameConfigResult>("reset_game_config"); export const resetAllGameConfigs = callable<[], GameConfigsResult>("reset_all_game_configs"); export const getWorkaroundState = callable<[string], WorkaroundStateResult>("get_workaround_state"); -export const setWorkaroundState = callable<[ - string, - WorkaroundState, - string | null | undefined, - boolean, - TargetTransport | null | undefined, -], WorkaroundStateResult>("set_workaround_state"); +export const setWorkaroundState = callable<[string, WorkaroundState, boolean?], WorkaroundStateResult>("set_workaround_state"); export const removeWorkaroundState = callable<[string], WorkaroundStateResult>("remove_workaround_state"); export const getDebugFileContents = callable<[], DebugFileContentsResult>("get_debug_file_contents"); diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 4e2d93d..957f697 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -99,13 +99,11 @@ export function ConfigurationTab({ } const profileLabel = selectedTarget?.name || "Game profile"; - const profileTransport = selectedTarget - ? selectedTarget.transport.kind === "flatpak" - ? "Non-Steam · Flatpak" - : selectedTarget.nonSteam ? "Non-Steam" : "Steam" + const profileType = selectedTarget + ? selectedTarget.nonSteam ? "Non-Steam" : "Steam" : "Game"; const profileDescription = selectedTarget - ? `${profileTransport} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? "LSFG-VK Enabled" : "LSFG-VK not enabled"}` + ? `${profileType} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? "LSFG-VK Enabled" : "LSFG-VK not enabled"}` : "Game is no longer available"; const enableProfile = async (appid: string, quitRunningGame = false) => { if (!(await onEnable(appid))) return; @@ -120,9 +118,7 @@ export function ConfigurationTab({ } else if (detailAppId) { const isRunningUnconfigured = runningGame?.appid === detailAppId && runningGame.nonSteam === false - && runningGame.transport.kind === "host" && selectedTarget?.nonSteam === false - && selectedTarget?.transport.kind === "host" && !runningGame.configured; if (isRunningUnconfigured) { showModal( @@ -182,18 +178,6 @@ export function ConfigurationTab({ </PanelSectionRow> )} </PanelSection> - {selectedTarget?.configured && selectedTarget.transport.kind === "flatpak" && selectedTarget.flatpakSupport?.support_status !== "ready" && ( - <PanelSection> - <PanelSectionRow> - <ButtonItem - layout="below" - onClick={() => void onRepair(selectedTarget.appid)} - > - Repair Flatpak support - </ButtonItem> - </PanelSectionRow> - </PanelSection> - )} {selectedTarget?.configured && ( <GameConfigurationControls config={config} diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 43720c0..480a3e3 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -132,7 +132,6 @@ export function Content() { game={runningGame} config={config} onConfigChange={(field, value) => handleConfigChange(field, value)} - onRepair={repair} /> ), }] : []), diff --git a/src/components/GameConfigurationControls.tsx b/src/components/GameConfigurationControls.tsx index 6bea2e0..7025f78 100644 --- a/src/components/GameConfigurationControls.tsx +++ b/src/components/GameConfigurationControls.tsx @@ -10,7 +10,7 @@ interface Props { autoFocusFpsMultiplier?: boolean; onFpsMultiplierFocused?: () => void; showWorkarounds?: boolean; - workaroundTarget?: Pick<GameTarget, "appid" | "nonSteam" | "transport">; + workaroundTarget?: Pick<GameTarget, "appid" | "nonSteam">; onRepairWorkaround?: () => Promise<boolean>; } @@ -36,7 +36,6 @@ export function GameConfigurationControls({ <WorkaroundsSection appId={workaroundTarget.appid} nonSteam={workaroundTarget.nonSteam} - transport={workaroundTarget.transport} onRepair={onRepairWorkaround} /> )} diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index 92eacba..e407919 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -37,7 +37,6 @@ function usePersistentCollapsed(key: string) { } function targetDescription(game: GameTarget): string { - if (game.transport.kind === "flatpak") return "Non-Steam · Flatpak"; return game.nonSteam ? "Non-Steam" : "Steam"; } @@ -137,7 +136,7 @@ export function GameConfigurationSelector({ showModal( <ConfirmModal strTitle="Enable all available games?" - strDescription="Create individual LSFG-VK profiles for every available game using the plugin defaults. Flatpak targets will be provisioned as needed." + strDescription="Create individual LSFG-VK profiles for every available game using the plugin defaults." strOKButtonText="Enable all" strCancelButtonText="Cancel" onOK={() => void onEnableAll()} diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx index 57858db..c067dc0 100644 --- a/src/components/NowPlayingTab.tsx +++ b/src/components/NowPlayingTab.tsx @@ -1,5 +1,4 @@ -import { ButtonItem, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; -import { useState } from "react"; +import { Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; import { ConfigurationData } from "../config/configSchema"; import { GameTarget } from "../hooks/useGameConfiguration"; import { GameConfigurationControls } from "./GameConfigurationControls"; @@ -11,11 +10,9 @@ interface Props { fieldName: keyof ConfigurationData, value: boolean | number | string | string[], ) => Promise<void>; - onRepair: (appid: string) => Promise<boolean>; } function targetDescription(game: GameTarget): string { - if (game.transport.kind === "flatpak") return "Non-Steam · Flatpak"; return game.nonSteam ? "Non-Steam" : "Steam"; } @@ -23,23 +20,7 @@ export function NowPlayingTab({ game, config, onConfigChange, - onRepair, }: Props) { - const [busy, setBusy] = useState(false); - const supportNeedsRepair = - game.transport.kind === "flatpak" && - game.flatpakSupport?.support_status !== "ready"; - - const handleRepair = async () => { - if (busy) return; - setBusy(true); - try { - await onRepair(game.appid); - } finally { - setBusy(false); - } - }; - return ( <Focusable> <PanelSection title="Now Playing"> @@ -47,21 +28,6 @@ export function NowPlayingTab({ <Field label={game.name} description={targetDescription(game)} /> </PanelSectionRow> </PanelSection> - {supportNeedsRepair && ( - <PanelSection> - <PanelSectionRow> - <Field - label="Flatpak support needs repair" - description={game.flatpakSupport?.error || "The target runtime extension is not ready."} - /> - </PanelSectionRow> - <PanelSectionRow> - <ButtonItem layout="below" disabled={busy} onClick={() => void handleRepair()}> - {busy ? "Repairing..." : "Repair Flatpak support"} - </ButtonItem> - </PanelSectionRow> - </PanelSection> - )} <GameConfigurationControls config={config} onConfigChange={onConfigChange} diff --git a/src/components/WorkaroundsSection.tsx b/src/components/WorkaroundsSection.tsx index 5587392..dbdd6a3 100644 --- a/src/components/WorkaroundsSection.tsx +++ b/src/components/WorkaroundsSection.tsx @@ -1,7 +1,6 @@ import { ButtonItem, Field, PanelSectionRow, SliderField, ToggleField } from "@decky/ui"; import { useEffect, useState } from "react"; import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; -import type { TargetTransport } from "../api/lsfgApi"; import { usePerAppWorkarounds } from "../hooks/usePerAppWorkarounds"; import t from "../i18n/i18n"; import type { WorkaroundField } from "../hooks/usePerAppWorkarounds"; @@ -9,7 +8,6 @@ import type { WorkaroundField } from "../hooks/usePerAppWorkarounds"; interface WorkaroundsSectionProps { appId: string; nonSteam: boolean; - transport: TargetTransport; onRepair?: () => Promise<boolean>; } @@ -81,9 +79,9 @@ function usePersistentCollapsed() { return [collapsed, () => setCollapsed((value) => !value)] as const; } -export function WorkaroundsSection({ appId, nonSteam, transport, onRepair }: WorkaroundsSectionProps) { +export function WorkaroundsSection({ appId, nonSteam, onRepair }: WorkaroundsSectionProps) { const [collapsed, toggleCollapsed] = usePersistentCollapsed(); - const { status, snapshot, refresh, update, error } = usePerAppWorkarounds(appId, nonSteam, transport); + const { status, snapshot, refresh, update, error } = usePerAppWorkarounds(appId, nonSteam); const [repairing, setRepairing] = useState(false); const state = snapshot?.state; const controlsDisabled = status !== "ready" || state === undefined || snapshot?.wrapperOwned !== true || snapshot.integrationInstalled !== true; diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index 3260019..e94ce42 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useQuickAccessVisible } from "@decky/api"; import { Router } from "@decky/ui"; -import { ensureFlatpakSupport, getGameConfigs, getInstalledGames, getWorkaroundState, removeWorkaroundState, repairFlatpakSupport, resetGameConfig, resetAllGameConfigs, setWorkaroundState, updateGameConfig, type GameConfigEntry, type GlobalConfig, type InstalledGame, type WorkaroundState } from "../api/lsfgApi"; +import { getGameConfigs, getInstalledGames, getWorkaroundState, removeWorkaroundState, resetGameConfig, resetAllGameConfigs, setWorkaroundState, updateGameConfig, type GameConfigEntry, type GlobalConfig, type InstalledGame, type WorkaroundState } from "../api/lsfgApi"; import { ConfigurationData, getDefaults } from "../config/configSchema"; import { getDefaultWrapperPath, installWrapperIntegration, removeWrapperIntegration } from "../utils/steamLaunchOptions"; import { showErrorToast } from "../utils/toastUtils"; @@ -23,7 +23,6 @@ async function getSteamShortcuts(): Promise<InstalledGame[]> { appid: String(appid >>> 0), name, nonSteam: true, - transport: { kind: "host" }, }]; }); } catch { @@ -90,7 +89,7 @@ export function useGameConfiguration() { const name = app.display_name || installed?.name; if (!name) return setRunningGame(null); setRunningGame((current) => current?.appid === appid ? current : { - ...(installed || { appid, name, nonSteam: false, transport: { kind: "host" } }), + ...(installed || { appid, name, nonSteam: false }), name, configured: games.some((game) => game.appid === appid), }); @@ -109,26 +108,13 @@ export function useGameConfiguration() { const targets = useMemo<GameTarget[]>(() => { const configured = installedGames.map((game) => ({ ...game, configured: games.some((item) => item.appid === game.appid) })); - for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: game.profile, nonSteam: false, transport: { kind: "host" }, configured: true }); + for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: game.profile, nonSteam: false, configured: true }); if (runningGame && !configured.some((game) => game.appid === runningGame.appid)) configured.unshift(runningGame); return configured; }, [games, installedGames, runningGame]); const template = useMemo(() => ({ ...getDefaults(), ...globalConfig }), [globalConfig]); const config = games.find((game) => game.appid === selectedAppId)?.config || template; - const ensureTargetFlatpakSupport = useCallback(async (target: GameTarget): Promise<boolean> => { - if (target.transport.kind !== "flatpak") return true; - const result = await ensureFlatpakSupport(target.transport.flatpakAppId); - if (!result.success || result.support_status !== "ready") { - showErrorToast( - "Flatpak support unavailable", - result.error || result.message || "The required Flatpak runtime extension is not ready", - ); - return false; - } - return true; - }, []); - const ensureTargetWorkarounds = useCallback(async (target: GameTarget): Promise<boolean> => { if (!installedGames.some((game) => game.appid === target.appid)) return true; const appId = Number(target.appid); @@ -148,16 +134,12 @@ export function useGameConfiguration() { target.nonSteam, wrapperPath, commandTokenAdded, - target.transport, - target.transport.kind === "flatpak" ? existing.shortcut_exe || undefined : undefined, ); stateWriteAttempted = true; const saved = await setWorkaroundState( target.appid, state, - integration.originalExecutable ?? null, integration.commandTokenAdded, - target.transport, ); if (!saved.success) throw new Error(saved.error || "Could not save workaround state"); return true; @@ -169,9 +151,7 @@ export function useGameConfiguration() { appId, target.nonSteam, wrapperPath, - integration.originalExecutable, integration.commandTokenAdded, - target.transport, ); } catch (rollbackError) { showErrorToast("Workaround rollback failed", asError(rollbackError).message); @@ -201,9 +181,7 @@ export function useGameConfiguration() { appId, target.nonSteam, wrapperPath, - target.transport.kind === "flatpak" ? existing.shortcut_exe || undefined : undefined, existing.command_token_added === true, - target.transport, ); const removed = await removeWorkaroundState(target.appid); if (!removed.success) throw new Error(removed.error || "Could not remove workaround state"); @@ -228,19 +206,17 @@ export function useGameConfiguration() { const enable = useCallback(async (appid: string) => { const target = targets.find((item) => item.appid === appid); if (!target?.name) return false; - if (!(await ensureTargetFlatpakSupport(target))) return false; if (!(await ensureTargetWorkarounds(target))) return false; const result = await updateGameConfig(appid, target.name, template); if (result.success) await load(); else await removeTargetWorkarounds(target); return result.success; - }, [ensureTargetFlatpakSupport, ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); + }, [ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); const enableAll = useCallback(async (): Promise<void> => { const available = targets.filter((target) => !target.configured && target.name); if (available.length === 0) return; for (const target of available) { - if (!(await ensureTargetFlatpakSupport(target))) return; if (!(await ensureTargetWorkarounds(target))) return; const result = await updateGameConfig(target.appid, target.name, template); if (!result.success) { @@ -253,20 +229,10 @@ export function useGameConfiguration() { } } await load(); - }, [ensureTargetFlatpakSupport, ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); + }, [ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); const repair = useCallback(async (appid: string): Promise<boolean> => { const target = targets.find((item) => item.appid === appid); if (!target) return false; - if (target.transport.kind === "flatpak") { - const support = await repairFlatpakSupport(target.transport.flatpakAppId); - if (!support.success || support.support_status !== "ready") { - showErrorToast( - "Flatpak support unavailable", - support.error || support.message || "The required Flatpak runtime extension is not ready", - ); - return false; - } - } const success = await ensureTargetWorkarounds(target); if (success) await load(); return success; diff --git a/src/hooks/usePerAppWorkarounds.ts b/src/hooks/usePerAppWorkarounds.ts index 29e1181..63ebdb9 100644 --- a/src/hooks/usePerAppWorkarounds.ts +++ b/src/hooks/usePerAppWorkarounds.ts @@ -3,11 +3,9 @@ import { getWorkaroundState, removeWorkaroundState, setWorkaroundState, - type TargetTransport, type WorkaroundState, } from "../api/lsfgApi"; import { - assertKnownShortcutTarget, getDefaultWrapperPath, installWrapperIntegration, isWrapperIntegrationInstalled, @@ -45,8 +43,6 @@ export interface WorkaroundSnapshot { wrapperOwned: boolean; integrationInstalled: boolean; commandTokenAdded: boolean; - shortcutExe?: string | null; - transport: TargetTransport; } interface PerAppWorkarounds { @@ -64,30 +60,22 @@ function asError(error: unknown): Error { function makeSnapshot( steam: SteamLaunchOptionsSnapshot, result: Awaited<ReturnType<typeof getWorkaroundState>>, - nonSteam: boolean, - transport: TargetTransport, ): WorkaroundSnapshot { if (!result.state) throw new Error("Workaround state is not initialized for this profile"); const wrapperPath = result.wrapper_path || getDefaultWrapperPath(); - const selectedTransport = result.transport || transport; - const shortcutExe = selectedTransport.kind === "flatpak" ? result.shortcut_exe : undefined; - assertKnownShortcutTarget(steam, nonSteam, selectedTransport, wrapperPath, shortcutExe); return { steam, state: result.state, wrapperPath, wrapperOwned: result.wrapper_owned === true, - integrationInstalled: isWrapperIntegrationInstalled(steam, nonSteam, selectedTransport, wrapperPath), + integrationInstalled: isWrapperIntegrationInstalled(steam, wrapperPath), commandTokenAdded: result.command_token_added === true, - shortcutExe, - transport: selectedTransport, }; } async function adoptWorkaroundState( appId: string, nonSteam: boolean, - transport: TargetTransport, wrapperPath: string, ): Promise<WorkaroundSnapshot> { let integration: Awaited<ReturnType<typeof installWrapperIntegration>> | null = null; @@ -97,17 +85,14 @@ async function adoptWorkaroundState( nonSteam, wrapperPath, false, - transport, ); const finalized = await setWorkaroundState( appId, DEFAULT_WORKAROUND_STATE, - integration.originalExecutable ?? null, integration.commandTokenAdded, - transport, ); if (!finalized.success) throw new Error(finalized.error || "Could not finalize workaround state"); - return makeSnapshot(integration.snapshot, finalized, nonSteam, transport); + return makeSnapshot(integration.snapshot, finalized); } catch (error) { let rollbackSucceeded = true; if (integration?.changed) { @@ -116,9 +101,7 @@ async function adoptWorkaroundState( Number(appId), nonSteam, wrapperPath, - integration.originalExecutable, integration.commandTokenAdded, - transport, ); } catch { // Leave the owned integration in place rather than guessing at cleanup. @@ -136,7 +119,6 @@ async function adoptWorkaroundState( export function usePerAppWorkarounds( appId: string, nonSteam: boolean, - transport: TargetTransport = { kind: "host" }, ): PerAppWorkarounds { const [status, setStatus] = useState<WorkaroundLoadStatus>("loading"); const [snapshot, setSnapshot] = useState<WorkaroundSnapshot | null>(null); @@ -154,12 +136,11 @@ export function usePerAppWorkarounds( return adoptWorkaroundState( appId, nonSteam, - transport, result.wrapper_path || getDefaultWrapperPath(), ); } - return makeSnapshot(steam, result, nonSteam, transport); - }, [appId, nonSteam, numericAppId, transport]); + return makeSnapshot(steam, result); + }, [appId, nonSteam, numericAppId]); const applySnapshot = useCallback((next: WorkaroundSnapshot) => { setSnapshot(next); @@ -194,7 +175,7 @@ export function usePerAppWorkarounds( setSnapshot((current) => current ? { ...current, steam, - integrationInstalled: isWrapperIntegrationInstalled(steam, nonSteam, current.transport, current.wrapperPath), + integrationInstalled: isWrapperIntegrationInstalled(steam, current.wrapperPath), } : current); }, (subscriptionError) => { @@ -229,24 +210,18 @@ export function usePerAppWorkarounds( setError(null); const nextState = { ...current.state, [field]: value } as WorkaroundState; try { - const shortcutExe = current.transport.kind === "flatpak" ? current.shortcutExe ?? null : null; const result = await setWorkaroundState( appId, nextState, - shortcutExe, current.commandTokenAdded, - current.transport, ); if (!result.success || !result.state) throw new Error(result.error || "Could not save workaround state"); - const selectedTransport = result.transport || current.transport; applySnapshot({ ...current, state: result.state, wrapperPath: result.wrapper_path || current.wrapperPath, wrapperOwned: result.wrapper_owned === true, - shortcutExe: selectedTransport.kind === "flatpak" ? result.shortcut_exe : undefined, commandTokenAdded: result.command_token_added === true, - transport: selectedTransport, }); return true; } catch (updateError) { diff --git a/src/i18n/languages.json b/src/i18n/languages.json index 7e5676b..0ee539a 100644 --- a/src/i18n/languages.json +++ b/src/i18n/languages.json @@ -2,7 +2,6 @@ "ja": { "CONTENT_FPS_MULTIPLIER": "FPS倍率", "CONTENT_NERD_STUFF": "詳細情報", - "CONTENT_FLATPAK_SETUP": "Flatpak設定", "MULTIPLIER_OFF": "オフ", "CONFIG_SECTION_TITLE": "設定", "CONFIG_WORKAROUNDS_TITLE": "互換性設定", @@ -22,9 +21,7 @@ "CONFIG_ENABLE_WSI": "WSIを有効化", "CONFIG_ENABLE_WSI_DESC": "Gamescope WSIレイヤーを再有効化します。ゲームの再起動が必要。", "CONFIG_DISABLE_GAMESCOPE_WSI": "Gamescope WSIを無効化", - "CONFIG_DISABLE_GAMESCOPE_WSI_DESC": "ENABLE_GAMESCOPE_WSI=0を追加します。ゲームの再起動が必要です。", - "CONFIG_DISABLE_HDR": "HDRを無効化", - "CONFIG_DISABLE_HDR_DESC": "DXVKがゲームにHDRを公開しないようにします。ゲームの再起動が必要です。", + "CONFIG_DISABLE_GAMESCOPE_WSI_DESC": "HDRを変更せずENABLE_GAMESCOPE_WSI=0を追加します。ゲームの再起動が必要です。", "CONFIG_DISABLE_STEAMDECK_MODE": "Steam Deckモードを無効化", "CONFIG_DISABLE_STEAMDECK_MODE_DESC": "ゲーム固有のSteam Deck互換スイッチを無効化します。ゲームの再起動が必要です。", "CONFIG_DISABLE_VKBASALT": "vkBasaltを無効化", @@ -35,28 +32,6 @@ "INSTALL_UNINSTALLING": "アンインストール中...", "INSTALL_UNINSTALL_BTN": "LSFG-VKをアンインストール", "INSTALL_INSTALL_BTN": "LSFG-VKをインストール", - "FLATPAK_MODAL_TITLE": "Flatpak拡張", - "FLATPAK_RUNTIME_INSTALLER": "ランタイム拡張インストーラー", - "FLATPAK_RUNTIME_23": "ランタイム 23.08", - "FLATPAK_RUNTIME_24": "ランタイム 24.08", - "FLATPAK_RUNTIME_25": "ランタイム 25.08", - "FLATPAK_INSTALLED": "インストール済み", - "FLATPAK_NOT_INSTALLED": "未インストール", - "FLATPAK_UNINSTALL_TITLE": "ランタイム拡張をアンインストール", - "FLATPAK_UNINSTALL_CONFIRM_PREFIX": "本当に", - "FLATPAK_UNINSTALL_CONFIRM_SUFFIX": "ランタイム拡張をアンインストールしますか?", - "FLATPAK_UNINSTALL_BTN": "アンインストール", - "FLATPAK_INSTALL_BTN": "インストール", - "FLATPAK_APPS_TITLE": "Flatpakアプリケーション", - "FLATPAK_NO_APPS": "Flatpakアプリなし", - "FLATPAK_NO_APPS_DESC": "現在インストールされているFlatpakアプリケーションはありません", - "FLATPAK_STATUS_CONFIGURED": "設定済み", - "FLATPAK_STATUS_PARTIAL": "部分設定", - "FLATPAK_STATUS_NO_OVERRIDES": "オーバーライドなし", - "FLATPAK_ERROR": "エラー", - "FLATPAK_ERROR_STATUS": "拡張ステータスの確認に失敗しました", - "FLATPAK_ERROR_APPS": "Flatpakアプリケーションの読み込みに失敗しました", - "FLATPAK_CLOSE": "閉じる", "NERD_LOADING": "情報を読み込み中...", "NERD_DLL_PATH": "DLLパス", "NERD_NOT_AVAILABLE": "利用不可", @@ -91,7 +66,6 @@ "ko": { "CONTENT_FPS_MULTIPLIER": "FPS 배율", "CONTENT_NERD_STUFF": "상세 정보", - "CONTENT_FLATPAK_SETUP": "Flatpak 설정", "MULTIPLIER_OFF": "끄기", "CONFIG_SECTION_TITLE": "설정", "CONFIG_WORKAROUNDS_TITLE": "호환성 설정", @@ -111,9 +85,7 @@ "CONFIG_ENABLE_WSI": "WSI 활성화", "CONFIG_ENABLE_WSI_DESC": "Gamescope WSI 레이어를 다시 활성화합니다. 게임 재시작 필요.", "CONFIG_DISABLE_GAMESCOPE_WSI": "Gamescope WSI 비활성화", - "CONFIG_DISABLE_GAMESCOPE_WSI_DESC": "ENABLE_GAMESCOPE_WSI=0을 추가합니다. 게임 재시작 필요.", - "CONFIG_DISABLE_HDR": "HDR 비활성화", - "CONFIG_DISABLE_HDR_DESC": "DXVK가 게임에 HDR을 노출하지 않도록 합니다. 게임 재시작 필요.", + "CONFIG_DISABLE_GAMESCOPE_WSI_DESC": "HDR을 변경하지 않고 ENABLE_GAMESCOPE_WSI=0을 추가합니다. 게임 재시작 필요.", "CONFIG_DISABLE_STEAMDECK_MODE": "Steam Deck 모드 비활성화", "CONFIG_DISABLE_STEAMDECK_MODE_DESC": "게임별 Steam Deck 호환 스위치를 비활성화합니다. 게임 재시작 필요.", "CONFIG_DISABLE_VKBASALT": "vkBasalt 비활성화", @@ -124,28 +96,6 @@ "INSTALL_UNINSTALLING": "제거 중...", "INSTALL_UNINSTALL_BTN": "LSFG-VK 제거", "INSTALL_INSTALL_BTN": "LSFG-VK 설치", - "FLATPAK_MODAL_TITLE": "Flatpak 확장", - "FLATPAK_RUNTIME_INSTALLER": "런타임 확장 설치", - "FLATPAK_RUNTIME_23": "런타임 23.08", - "FLATPAK_RUNTIME_24": "런타임 24.08", - "FLATPAK_RUNTIME_25": "런타임 25.08", - "FLATPAK_INSTALLED": "설치됨", - "FLATPAK_NOT_INSTALLED": "설치 안 됨", - "FLATPAK_UNINSTALL_TITLE": "런타임 확장 제거", - "FLATPAK_UNINSTALL_CONFIRM_PREFIX": "정말로", - "FLATPAK_UNINSTALL_CONFIRM_SUFFIX": "런타임 확장을 제거하시겠습니까?", - "FLATPAK_UNINSTALL_BTN": "제거", - "FLATPAK_INSTALL_BTN": "설치", - "FLATPAK_APPS_TITLE": "Flatpak 애플리케이션", - "FLATPAK_NO_APPS": "Flatpak 앱 없음", - "FLATPAK_NO_APPS_DESC": "현재 설치된 Flatpak 애플리케이션이 없습니다", - "FLATPAK_STATUS_CONFIGURED": "설정됨", - "FLATPAK_STATUS_PARTIAL": "부분 설정", - "FLATPAK_STATUS_NO_OVERRIDES": "오버라이드 없음", - "FLATPAK_ERROR": "오류", - "FLATPAK_ERROR_STATUS": "확장 상태 확인 실패", - "FLATPAK_ERROR_APPS": "Flatpak 애플리케이션 로드 실패", - "FLATPAK_CLOSE": "닫기", "NERD_LOADING": "정보 불러오는 중...", "NERD_DLL_PATH": "DLL 경로", "NERD_NOT_AVAILABLE": "사용 불가", @@ -208,7 +158,6 @@ "template": { "CONTENT_FPS_MULTIPLIER": "FPS Multiplier", "CONTENT_NERD_STUFF": "Nerd Stuff", - "CONTENT_FLATPAK_SETUP": "Flatpak Setup", "MULTIPLIER_OFF": "OFF", "CONFIG_SECTION_TITLE": "Config", "CONFIG_WORKAROUNDS_TITLE": "Workarounds", @@ -228,9 +177,7 @@ "CONFIG_ENABLE_WSI": "Enable WSI", "CONFIG_ENABLE_WSI_DESC": "Re-Enable Gamescope WSI Layer. Requires game restart to apply.", "CONFIG_DISABLE_GAMESCOPE_WSI": "Disable Gamescope WSI", - "CONFIG_DISABLE_GAMESCOPE_WSI_DESC": "Adds ENABLE_GAMESCOPE_WSI=0. Requires game restart to apply.", - "CONFIG_DISABLE_HDR": "Disable HDR", - "CONFIG_DISABLE_HDR_DESC": "Prevents DXVK from exposing HDR to the game. Requires game restart to apply.", + "CONFIG_DISABLE_GAMESCOPE_WSI_DESC": "Adds ENABLE_GAMESCOPE_WSI=0 without changing HDR. Requires game restart to apply.", "CONFIG_DISABLE_STEAMDECK_MODE": "Disable Steam Deck Mode", "CONFIG_DISABLE_STEAMDECK_MODE_DESC": "Disables a game-specific Steam Deck compatibility switch. Requires game restart to apply.", "CONFIG_DISABLE_VKBASALT": "Disable vkBasalt", @@ -241,28 +188,6 @@ "INSTALL_UNINSTALLING": "Uninstalling...", "INSTALL_UNINSTALL_BTN": "Uninstall LSFG-VK", "INSTALL_INSTALL_BTN": "Install LSFG-VK", - "FLATPAK_MODAL_TITLE": "Flatpak Extensions", - "FLATPAK_RUNTIME_INSTALLER": "Runtime Extension Installer", - "FLATPAK_RUNTIME_23": "Runtime 23.08", - "FLATPAK_RUNTIME_24": "Runtime 24.08", - "FLATPAK_RUNTIME_25": "Runtime 25.08", - "FLATPAK_INSTALLED": "Installed", - "FLATPAK_NOT_INSTALLED": "Not installed", - "FLATPAK_UNINSTALL_TITLE": "Uninstall Runtime Extension", - "FLATPAK_UNINSTALL_CONFIRM_PREFIX": "Are you sure you want to uninstall the", - "FLATPAK_UNINSTALL_CONFIRM_SUFFIX": "runtime extension?", - "FLATPAK_UNINSTALL_BTN": "Uninstall", - "FLATPAK_INSTALL_BTN": "Install", - "FLATPAK_APPS_TITLE": "Flatpak Applications", - "FLATPAK_NO_APPS": "No Flatpak Apps Found", - "FLATPAK_NO_APPS_DESC": "No Flatpak applications are currently installed", - "FLATPAK_STATUS_CONFIGURED": "Configured", - "FLATPAK_STATUS_PARTIAL": "Partial", - "FLATPAK_STATUS_NO_OVERRIDES": "No overrides", - "FLATPAK_ERROR": "Error", - "FLATPAK_ERROR_STATUS": "Failed to check extension status", - "FLATPAK_ERROR_APPS": "Failed to load Flatpak applications", - "FLATPAK_CLOSE": "Close", "NERD_LOADING": "Loading information...", "NERD_DLL_PATH": "DLL Path", "NERD_NOT_AVAILABLE": "Not available", diff --git a/src/types.d.ts b/src/types.d.ts index e5db40d..ebcf32c 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -16,7 +16,6 @@ declare module "*.jpg" { interface SteamAppDetails { strLaunchOptions?: string; strShortcutLaunchOptions?: string; - strShortcutExe?: string; strShortcutStartDir?: string; } @@ -31,7 +30,6 @@ interface SteamApps { ): SteamAppDetailsRegistration; SetAppLaunchOptions(appId: number, options: string): void | Promise<void>; SetShortcutLaunchOptions(appId: number, options: string): void | Promise<void>; - SetShortcutExe(appId: number, executable: string): void | Promise<void>; TerminateApp(appId: string, param1: boolean): void; GetAllShortcuts?(): Promise<unknown[]>; } diff --git a/src/utils/steamLaunchOptions.ts b/src/utils/steamLaunchOptions.ts index 82cf116..662f060 100644 --- a/src/utils/steamLaunchOptions.ts +++ b/src/utils/steamLaunchOptions.ts @@ -1,5 +1,3 @@ -import type { TargetTransport } from "../api/lsfgApi"; - const DEFAULT_WRAPPER_PATH = "~/.lsfg"; const COMMAND_TOKEN = "%command%"; @@ -26,12 +24,10 @@ export interface SteamLaunchOptionsSnapshot { appId: number; nonSteam: boolean; options: string; - target: string; details: SteamAppDetails; } export interface WrapperIntegrationResult { snapshot: SteamLaunchOptionsSnapshot; - originalExecutable?: string; commandTokenAdded: boolean; changed: boolean; } @@ -61,7 +57,6 @@ function snapshot(appId: number, nonSteam: boolean, details: SteamAppDetails): S appId, nonSteam, options: nonSteam ? details.strShortcutLaunchOptions || "" : details.strLaunchOptions || "", - target: nonSteam ? details.strShortcutExe || "" : "", details, }; } @@ -162,15 +157,6 @@ const commandIndex = (tokens: readonly LaunchToken[]) => tokens.findIndex((token const isAssignment = (token: LaunchToken) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(token.value); const isLegacyToken = (value: string) => LEGACY_WRAPPER_TOKENS.has(value) || LEGACY_ABSOLUTE_WRAPPER.test(value); const isWrapperToken = (value: string, wrapperPath: string) => decodeToken(value) === wrapperPath || isLegacyWrapperToken(value); -const usesShortcutTarget = (nonSteam: boolean, transport: TargetTransport) => nonSteam && transport.kind === "flatpak"; - -function selectFlatpakExecutable(transport: TargetTransport, candidate?: string | null): string | undefined { - if (transport.kind !== "flatpak") return undefined; - const value = candidate?.trim() ? decodeToken(candidate.trim()) : ""; - if (value === "flatpak") return "/usr/bin/flatpak"; - if (value === "/usr/bin/flatpak") return value; - return undefined; -} export const normalizeLaunchOptions = (options: string) => serialize(tokenize(options)); export const isLegacyWrapperToken = (value: string) => isLegacyToken(decodeToken(value)); @@ -267,25 +253,9 @@ export function hasWrapperLaunchIntegration(options: string, wrapperPath = DEFAU export function isWrapperIntegrationInstalled( steam: SteamLaunchOptionsSnapshot, - nonSteam: boolean, - transport: TargetTransport, wrapperPath = DEFAULT_WRAPPER_PATH, ): boolean { - return usesShortcutTarget(nonSteam, transport) - ? steam.target === wrapperPath - : hasWrapperLaunchIntegration(steam.options, wrapperPath); -} - -export function assertKnownShortcutTarget( - steam: SteamLaunchOptionsSnapshot, - nonSteam: boolean, - transport: TargetTransport, - wrapperPath: string, - originalExecutable?: string | null, -): void { - if (usesShortcutTarget(nonSteam, transport) && steam.target === wrapperPath && !originalExecutable) { - throw new Error("Managed shortcut Target has no saved original executable"); - } + return hasWrapperLaunchIntegration(steam.options, wrapperPath); } const queues = new Map<string, Promise<unknown>>(); @@ -325,18 +295,16 @@ async function writeVerified( previous: string, next: string, write: (value: string) => Promise<void>, - read: (value: SteamLaunchOptionsSnapshot) => string, message: string, ): Promise<SteamLaunchOptionsSnapshot> { - const normalized = read === readOptions ? normalizeLaunchOptions : (value: string) => value; try { await write(next); - return await waitFor(appId, nonSteam, (value) => normalized(read(value)) === normalized(next), message); + return await waitFor(appId, nonSteam, (value) => normalizeLaunchOptions(value.options) === normalizeLaunchOptions(next), message); } catch (error) { const failure = asError(error); try { await write(previous); - await waitFor(appId, nonSteam, (value) => normalized(read(value)) === normalized(previous), `Steam did not restore the previous ${read === readOptions ? "launch options" : "shortcut Target"}`); + await waitFor(appId, nonSteam, (value) => normalizeLaunchOptions(value.options) === normalizeLaunchOptions(previous), "Steam did not restore the previous launch options"); } catch (rollback) { throw new Error(`${failure.message}; rollback also failed: ${asError(rollback).message}`); } @@ -344,19 +312,11 @@ async function writeVerified( } } -const readOptions = (value: SteamLaunchOptionsSnapshot) => value.options; -const readTarget = (value: SteamLaunchOptionsSnapshot) => value.target; - function writeOptions(appId: number, nonSteam: boolean, value: string): Promise<void> { const setter = nonSteam ? apps()?.SetShortcutLaunchOptions : apps()?.SetAppLaunchOptions; if (!setter) return Promise.reject(new Error(`Steam ${nonSteam ? "shortcut " : ""}launch options API is unavailable`)); return Promise.resolve(setter.call(apps(), appId, value)); } -function writeTarget(appId: number, value: string): Promise<void> { - const setter = apps()?.SetShortcutExe; - if (!setter) return Promise.reject(new Error("Steam shortcut Target API is unavailable")); - return Promise.resolve(setter.call(apps(), appId, value)); -} export function updateSteamLaunchOptions( appId: number, @@ -368,7 +328,7 @@ export function updateSteamLaunchOptions( const next = transform(current.options); return next === current.options ? current : writeVerified( appId, nonSteam, current.options, next, - (value) => writeOptions(appId, nonSteam, value), readOptions, + (value) => writeOptions(appId, nonSteam, value), "Steam did not accept the launch options", ); }); @@ -379,52 +339,16 @@ export function installWrapperIntegration( nonSteam: boolean, wrapperPath: string, commandTokenAdded = false, - transport: TargetTransport = { kind: "host" }, - originalExecutable?: string, ): Promise<WrapperIntegrationResult> { return queued(appId, nonSteam, async () => { - let current = await readSteamLaunchOptions(appId, nonSteam); - if (usesShortcutTarget(nonSteam, transport)) { - if (!current.target) throw new Error("Steam shortcut Target is empty; refusing to replace it"); - if (current.target !== wrapperPath && isWrapperToken(current.target, wrapperPath)) { - throw new Error("The shortcut Target points to a legacy frame-generation wrapper; restore it first"); - } - const cleaned = cleanupPluginLaunchOptions(current.options, wrapperPath); - const launchOptionsChanged = cleaned !== current.options; - if (launchOptionsChanged) { - current = await writeVerified( - appId, true, current.options, cleaned, - (value) => writeOptions(appId, true, value), readOptions, - "Steam did not accept shortcut launch options", - ); - } - const savedOriginal = selectFlatpakExecutable(transport, originalExecutable); - if (current.target === wrapperPath) { - if (!savedOriginal) throw new Error("Managed shortcut Target has no saved original executable"); - return { snapshot: current, originalExecutable: savedOriginal, commandTokenAdded: false, changed: launchOptionsChanged }; - } - if (savedOriginal && selectFlatpakExecutable(transport, current.target) !== savedOriginal) { - throw new Error("Shortcut Target changed externally; refusing to replace it"); - } - const currentOriginal = selectFlatpakExecutable(transport, current.target); - if (!currentOriginal || (originalExecutable && !savedOriginal)) { - throw new Error("Flatpak shortcut Target is not a supported executable"); - } - const value = await writeVerified( - appId, true, current.target, wrapperPath, - (target) => writeTarget(appId, target), readTarget, - "Steam did not accept the shortcut Target", - ); - return { snapshot: value, originalExecutable: currentOriginal, commandTokenAdded: false, changed: true }; - } - + const current = await readSteamLaunchOptions(appId, nonSteam); const cleaned = cleanupPluginAssignments(cleanupLegacyLaunchOptions(current.options)); const alreadyInstalled = hasWrapperLaunchIntegration(current.options, wrapperPath); const rewrite = installLaunchOption(cleaned, wrapperPath, nonSteam); if (rewrite.options === current.options) return { snapshot: current, commandTokenAdded, changed: false }; const value = await writeVerified( appId, nonSteam, current.options, rewrite.options, - (options) => writeOptions(appId, nonSteam, options), readOptions, + (options) => writeOptions(appId, nonSteam, options), "Steam did not accept the launch options", ); return { snapshot: value, commandTokenAdded: alreadyInstalled ? commandTokenAdded : rewrite.commandTokenAdded, changed: true }; @@ -435,43 +359,14 @@ export function removeWrapperIntegration( appId: number, nonSteam: boolean, wrapperPath: string, - originalExecutable?: string, commandTokenAdded = false, - transport: TargetTransport = { kind: "host" }, ): Promise<SteamLaunchOptionsSnapshot> { return queued(appId, nonSteam, async () => { - let current = await readSteamLaunchOptions(appId, nonSteam); - if (usesShortcutTarget(nonSteam, transport)) { - const cleaned = cleanupPluginLaunchOptions(current.options, wrapperPath); - if (cleaned !== current.options) { - current = await writeVerified( - appId, true, current.options, cleaned, - (value) => writeOptions(appId, true, value), readOptions, - "Steam did not clean shortcut launch options", - ); - } - if (current.target !== wrapperPath) { - if (isWrapperToken(current.target, wrapperPath)) { - throw new Error("Original shortcut Target is unavailable; refusing to overwrite the current Target"); - } - if (originalExecutable && selectFlatpakExecutable(transport, current.target) !== selectFlatpakExecutable(transport, originalExecutable)) { - throw new Error("Shortcut Target changed externally; refusing to restore it"); - } - return current; - } - if (!originalExecutable || isWrapperToken(originalExecutable, wrapperPath)) { - throw new Error("Original shortcut Target is unavailable; refusing to overwrite the current Target"); - } - return writeVerified( - appId, true, wrapperPath, originalExecutable, - (target) => writeTarget(appId, target), readTarget, - "Steam did not restore the shortcut Target", - ); - } + const current = await readSteamLaunchOptions(appId, nonSteam); const next = cleanupPluginAssignments(removeWrapperLaunchOption(current.options, wrapperPath, commandTokenAdded)); return next === current.options ? current : writeVerified( appId, nonSteam, current.options, next, - (options) => writeOptions(appId, nonSteam, options), readOptions, + (options) => writeOptions(appId, nonSteam, options), "Steam did not clean the launch options", ); }); diff --git a/tests/steamLaunchOptions.test.ts b/tests/steamLaunchOptions.test.ts index c799907..1bb9526 100644 --- a/tests/steamLaunchOptions.test.ts +++ b/tests/steamLaunchOptions.test.ts @@ -78,21 +78,19 @@ test("removes only old plugin assignments and preserves DXVK settings", () => { ); }); -test("reads the matching app-details field and installs/removes Steam integration", async () => { +test("reads the matching app-details field and installs/removes host integration", async () => { const previousWindow = (globalThis as Record<string, unknown>).window; const previousSteamClient = (globalThis as Record<string, unknown>).SteamClient; let appOptions = "FOO=bar %command%"; let shortcutOptions = "--windowed"; - let shortcutTarget = '"flatpak"'; const appWrites: string[] = []; const shortcutWrites: string[] = []; - const targetWrites: string[] = []; const unregisters: number[] = []; const apps = { RegisterForAppDetails(appId: number, callback: (details: SteamAppDetails) => void) { callback(appId === 42 ? { strLaunchOptions: appOptions, strShortcutLaunchOptions: "wrong-field" } - : { strShortcutExe: shortcutTarget, strShortcutLaunchOptions: shortcutOptions, strLaunchOptions: "wrong-field" }); + : { strShortcutLaunchOptions: shortcutOptions, strLaunchOptions: "wrong-field" }); return { unregister: () => unregisters.push(appId) }; }, SetAppLaunchOptions(appId: number, options: string) { @@ -105,11 +103,6 @@ test("reads the matching app-details field and installs/removes Steam integratio shortcutWrites.push(options); shortcutOptions = options; }, - SetShortcutExe(appId: number, executable: string) { - assert.equal(appId, 43); - targetWrites.push(executable); - shortcutTarget = executable; - }, }; (globalThis as Record<string, unknown>).window = { setTimeout, clearTimeout }; (globalThis as Record<string, unknown>).SteamClient = { Apps: apps }; @@ -122,16 +115,15 @@ test("reads the matching app-details field and installs/removes Steam integratio assert.equal(appWrites.length, 1); assert.equal(shortcutWrites.length, 0); - const shortcut = await installWrapperIntegration(43, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }); - assert.equal(shortcut.originalExecutable, "/usr/bin/flatpak"); - assert.equal(shortcut.snapshot.target, wrapper); - assert.deepEqual(targetWrites, [wrapper]); - const restored = await removeWrapperIntegration(43, true, wrapper, shortcut.originalExecutable, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }); - assert.equal(restored.target, "/usr/bin/flatpak"); - assert.deepEqual(targetWrites, [wrapper, "/usr/bin/flatpak"]); - assert.equal(shortcutWrites.length, 0); + const shortcut = await installWrapperIntegration(43, true, wrapper); + assert.equal(shortcut.snapshot.options, `${wrapper} %command% --windowed`); + assert.equal(shortcut.commandTokenAdded, true); + assert.deepEqual(shortcutWrites, [shortcut.snapshot.options]); + const restored = await removeWrapperIntegration(43, true, wrapper, shortcut.commandTokenAdded); + assert.equal(restored.options, "--windowed"); + assert.deepEqual(shortcutWrites, [shortcut.snapshot.options, "--windowed"]); - const cleaned = await removeWrapperIntegration(42, false, wrapper, undefined, installed.commandTokenAdded); + const cleaned = await removeWrapperIntegration(42, false, wrapper, installed.commandTokenAdded); assert.equal(cleaned.options, "FOO=bar %command%".replaceAll(" ", " ")); assert.ok(unregisters.includes(42)); assert.ok(unregisters.includes(43)); @@ -143,46 +135,35 @@ test("reads the matching app-details field and installs/removes Steam integratio } }); -test("uses shortcut launch options for a host shortcut without changing its Target", async () => { +test("uses shortcut launch options for a non-Steam shortcut", async () => { const previousWindow = (globalThis as Record<string, unknown>).window; const previousSteamClient = (globalThis as Record<string, unknown>).SteamClient; const originalOptions = 'DESKTOPINTEGRATION=1 "/home/deck/AppImages/dusk.appimage"'; let shortcutOptions = originalOptions; - let shortcutTarget = "env"; const shortcutWrites: string[] = []; - const targetWrites: string[] = []; const apps = { RegisterForAppDetails(_appId: number, callback: (details: SteamAppDetails) => void) { - callback({ strShortcutExe: shortcutTarget, strShortcutLaunchOptions: shortcutOptions }); + callback({ strShortcutLaunchOptions: shortcutOptions }); return { unregister() {} }; }, SetShortcutLaunchOptions(_appId: number, options: string) { shortcutWrites.push(options); shortcutOptions = options; }, - SetShortcutExe(_appId: number, executable: string) { - targetWrites.push(executable); - shortcutTarget = executable; - }, }; (globalThis as Record<string, unknown>).window = { setTimeout, clearTimeout }; (globalThis as Record<string, unknown>).SteamClient = { Apps: apps }; try { - const installed = await installWrapperIntegration(44, true, wrapper, false, { kind: "host" }); - assert.equal(installed.originalExecutable, undefined); - assert.equal(installed.snapshot.target, "env"); + const installed = await installWrapperIntegration(44, true, wrapper); assert.equal(installed.snapshot.options, 'DESKTOPINTEGRATION=1 ~/.lsfg %command% "/home/deck/AppImages/dusk.appimage"'); - assert.deepEqual(targetWrites, []); assert.deepEqual(shortcutWrites, [installed.snapshot.options]); - const secondInstall = await installWrapperIntegration(44, true, wrapper, false, { kind: "host" }); + const secondInstall = await installWrapperIntegration(44, true, wrapper); assert.equal(secondInstall.snapshot.options, installed.snapshot.options); assert.deepEqual(shortcutWrites, [installed.snapshot.options]); - const restored = await removeWrapperIntegration(44, true, wrapper, undefined, installed.commandTokenAdded, { kind: "host" }); - assert.equal(restored.target, "env"); + const restored = await removeWrapperIntegration(44, true, wrapper, installed.commandTokenAdded); assert.equal(restored.options, originalOptions); - assert.deepEqual(targetWrites, []); } finally { if (previousWindow === undefined) delete (globalThis as Record<string, unknown>).window; else (globalThis as Record<string, unknown>).window = previousWindow; @@ -191,42 +172,18 @@ test("uses shortcut launch options for a host shortcut without changing its Targ } }); -test("fails closed when shortcut Target ownership or setters are unavailable", async () => { - const previousWindow = (globalThis as Record<string, unknown>).window; - const previousSteamClient = (globalThis as Record<string, unknown>).SteamClient; - (globalThis as Record<string, unknown>).window = { setTimeout, clearTimeout }; - (globalThis as Record<string, unknown>).SteamClient = { - Apps: { - RegisterForAppDetails(_appId: number, callback: (details: SteamAppDetails) => void) { - callback({ strShortcutExe: _appId === 99 ? "/usr/bin/flatpak" : "garbage", strShortcutLaunchOptions: "" }); - return { unregister() {} }; - }, - }, - }; - try { - await assert.rejects(installWrapperIntegration(98, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /supported executable/); - await assert.rejects(installWrapperIntegration(99, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /Target API is unavailable/); - await assert.rejects(removeWrapperIntegration(99, true, wrapper, "/usr/bin/original", false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /Target changed externally/); - } finally { - if (previousWindow === undefined) delete (globalThis as Record<string, unknown>).window; - else (globalThis as Record<string, unknown>).window = previousWindow; - if (previousSteamClient === undefined) delete (globalThis as Record<string, unknown>).SteamClient; - else (globalThis as Record<string, unknown>).SteamClient = previousSteamClient; - } -}); - -test("restores launch options and shortcut Target when a setter fails after changing them", async () => { +test("restores launch options when a setter fails after changing them", async () => { const previousWindow = (globalThis as Record<string, unknown>).window; const previousSteamClient = (globalThis as Record<string, unknown>).SteamClient; let appOptions = "FOO=bar %command%"; - let shortcutTarget = "/usr/bin/flatpak"; + let shortcutOptions = "--windowed"; const appWrites: string[] = []; - const targetWrites: string[] = []; + const shortcutWrites: string[] = []; const apps = { RegisterForAppDetails(appId: number, callback: (details: SteamAppDetails) => void) { callback(appId === 42 ? { strLaunchOptions: appOptions } - : { strShortcutExe: shortcutTarget, strShortcutLaunchOptions: "" }); + : { strShortcutLaunchOptions: shortcutOptions }); return { unregister() {} }; }, SetAppLaunchOptions(_appId: number, options: string) { @@ -234,10 +191,10 @@ test("restores launch options and shortcut Target when a setter fails after chan appOptions = options; if (options.includes(wrapper)) throw new Error("simulated launch-option write failure"); }, - SetShortcutExe(_appId: number, executable: string) { - targetWrites.push(executable); - shortcutTarget = executable; - if (executable === wrapper) throw new Error("simulated Target write failure"); + SetShortcutLaunchOptions(_appId: number, options: string) { + shortcutWrites.push(options); + shortcutOptions = options; + if (options.includes(wrapper)) throw new Error("simulated shortcut launch-option write failure"); }, }; (globalThis as Record<string, unknown>).window = { setTimeout, clearTimeout }; @@ -247,9 +204,9 @@ test("restores launch options and shortcut Target when a setter fails after chan assert.equal(appOptions, "FOO=bar %command%"); assert.deepEqual(appWrites, [`FOO=bar ${wrapper} %command%`, "FOO=bar %command%"]); - await assert.rejects(installWrapperIntegration(43, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /simulated Target write failure/); - assert.equal(shortcutTarget, "/usr/bin/flatpak"); - assert.deepEqual(targetWrites, [wrapper, "/usr/bin/flatpak"]); + await assert.rejects(installWrapperIntegration(43, true, wrapper), /simulated shortcut launch-option write failure/); + assert.equal(shortcutOptions, "--windowed"); + assert.deepEqual(shortcutWrites, [`${wrapper} %command% --windowed`, "--windowed"]); } finally { if (previousWindow === undefined) delete (globalThis as Record<string, unknown>).window; else (globalThis as Record<string, unknown>).window = previousWindow; diff --git a/tests/test_flatpak_service.py b/tests/test_flatpak_service.py deleted file mode 100644 index d5baf61..0000000 --- a/tests/test_flatpak_service.py +++ /dev/null @@ -1,299 +0,0 @@ -import json -import sys -import tempfile -import types -import unittest -from pathlib import Path -from unittest.mock import Mock - - -sys.modules.setdefault( - "decky", - types.SimpleNamespace(DECKY_USER_HOME="/home/deck", logger=Mock()), -) -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules")) - -from lsfg_vk.flatpak_service import FlatpakService - - -class FlatpakServiceTests(unittest.TestCase): - def setUp(self): - self.tempdir = tempfile.TemporaryDirectory() - self.home = Path(self.tempdir.name) / "home" / "deck" - self.home.mkdir(parents=True) - self.service = FlatpakService() - self.service.user_home = self.home - self.service.config_dir = self.home / ".config/lsfg-vk" - self.service.config_file_path = self.service.config_dir / "conf.toml" - self.service.check_flatpak_available = Mock(return_value=True) - self.service._run_flatpak_command = Mock(side_effect=self._run_flatpak_command) - self.runtime_ref = "org.freedesktop.Platform/x86_64/24.08" - self.runtime_metadata = "" - self.user_branches = set() - self.system_branches = set() - self.install_branch = "24.08" - self.bundle = self.home / "lsfg-vk-24.08.flatpak" - self.bundle.write_bytes(b"bundle") - self.service._bundled_extension_path = Mock(return_value=self.bundle) - - def tearDown(self): - self.tempdir.cleanup() - - @staticmethod - def _result(stdout="", returncode=0, stderr=""): - return types.SimpleNamespace(stdout=stdout, returncode=returncode, stderr=stderr) - - @staticmethod - def _extension_line(branch): - return f"org.freedesktop.Platform.VulkanLayer.lsfgvk\tx86_64\t{branch}\n" - - def _run_flatpak_command(self, args, **_kwargs): - if args[0] == "info" and args[1] == "--show-runtime": - return self._result(self.runtime_ref + "\n") - if args[0] == "info" and args[1] == "--show-metadata": - return self._result(self.runtime_metadata) - if args[0] == "list": - branches = self.user_branches if "--user" in args else self.system_branches - return self._result("".join(self._extension_line(branch) for branch in sorted(branches))) - if args[0] == "install": - self.user_branches.add(self.install_branch) - return self._result() - if args[0] == "uninstall": - self.user_branches.discard(args[-1].rsplit("/", 1)[-1]) - return self._result() - raise AssertionError(f"Unexpected Flatpak command: {args}") - - def test_runtime_branch_mapping_is_strict_and_branch_specific(self): - self.assertEqual( - FlatpakService.runtime_branch_from_ref( - "org.freedesktop.Platform/x86_64/24.08" - ), - "24.08", - ) - self.assertEqual( - FlatpakService.runtime_branch_from_ref( - "org.freedesktop.Platform//25.08" - ), - "25.08", - ) - with self.assertRaises(ValueError): - FlatpakService.runtime_branch_from_ref("org.gnome.Sdk/x86_64/46") - with self.assertRaises(ValueError): - FlatpakService.runtime_branch_from_ref( - "org.freedesktop.Platform/x86_64/26.08" - ) - - def test_runtime_branch_mapping_reads_documented_gl_metadata(self): - metadata = """ -[Extension org.freedesktop.Platform.GL] -versions=25.08;25.08-extra;1.4 -version=1.4 -""" - self.assertEqual(FlatpakService.runtime_branch_from_metadata(metadata), "25.08") - with self.assertRaises(ValueError): - FlatpakService.runtime_branch_from_metadata( - "[Extension org.freedesktop.Platform.GL]\nversions=26.08;26.08-extra;1.4\n" - ) - - def test_resolve_reads_required_runtime_instead_of_any_installed_branch(self): - self.user_branches = {"23.08"} - - response = self.service.resolve_app_support("com.example.Game") - - self.assertTrue(response["success"]) - self.assertEqual(response["runtime_branch"], "24.08") - self.assertEqual(response["support_status"], "needs-runtime") - self.assertFalse(response["extension_installed"]) - self.assertEqual( - self.service._run_flatpak_command.call_args_list[0].args[0], - ["info", "--show-runtime", "com.example.Game"], - ) - self.assertEqual( - self.service._run_flatpak_command.call_args_list[1].args[0], - ["list", "--user", "--runtime", "--columns=application,arch,branch"], - ) - self.assertEqual( - self.service._run_flatpak_command.call_args_list[2].args[0], - ["list", "--system", "--runtime", "--columns=application,arch,branch"], - ) - - def test_resolve_maps_kde_and_gnome_runtimes_from_gl_metadata(self): - metadata = "[Extension org.freedesktop.Platform.GL]\nversions=25.08;25.08-extra;1.4\n" - for runtime in ("org.kde.Platform/x86_64/6.10", "org.gnome.Platform/x86_64/49"): - with self.subTest(runtime=runtime): - self.service._run_flatpak_command.reset_mock() - self.runtime_ref = runtime - self.runtime_metadata = metadata - response = self.service.resolve_app_support("com.example.Game") - self.assertEqual(response["runtime_branch"], "25.08") - self.assertEqual(response["support_status"], "needs-runtime") - self.assertEqual( - self.service._run_flatpak_command.call_args_list[1].args[0], - ["info", "--show-metadata", runtime], - ) - - def test_system_extension_is_ready_without_installing_a_user_copy(self): - self.system_branches = {"24.08"} - - response = self.service.ensure_app_support("com.example.Game") - - self.assertTrue(response["success"]) - self.assertEqual(response["support_status"], "ready") - self.assertEqual( - [call.args[0][0] for call in self.service._run_flatpak_command.call_args_list], - ["info", "list", "list"], - ) - self.assertFalse(any(call.args[0][0] == "install" for call in self.service._run_flatpak_command.call_args_list)) - - def test_install_records_only_a_new_user_owned_branch(self): - response = self.service.install_extension("24.08") - - self.assertTrue(response["success"]) - self.assertTrue(response["enabled"]) - self.assertTrue(response["installed"]) - install_args = self.service._run_flatpak_command.call_args_list[2].args[0] - self.assertEqual(install_args[:4], ["install", "--user", "--noninteractive", "--or-update"]) - self.assertEqual( - json.loads(self.service.ownership_path.read_text(encoding="utf-8")), - {"version": 1, "plugin_owned_branches": ["24.08"]}, - ) - - def test_preexisting_branch_is_not_claimed_or_removed(self): - self.user_branches = {"24.08"} - - install_response = self.service.install_extension("24.08") - cleanup_response = self.service.remove_plugin_owned_extensions() - - self.assertTrue(install_response["success"]) - self.assertTrue(install_response["enabled"]) - self.assertTrue(install_response["installed"]) - self.assertFalse(self.service.ownership_path.exists()) - self.assertTrue(cleanup_response["success"]) - self.assertEqual(self.service._run_flatpak_command.call_count, 2) - - def test_extension_toggle_preserves_preexisting_branch(self): - self.user_branches = {"24.08"} - - enable_response = self.service.set_extension_enabled("24.08", True) - disable_response = self.service.set_extension_enabled("24.08", False) - - self.assertTrue(enable_response["success"]) - self.assertTrue(enable_response["enabled"]) - self.assertTrue(enable_response["installed"]) - self.assertTrue(disable_response["success"]) - self.assertTrue(disable_response["enabled"]) - self.assertTrue(disable_response["installed"]) - self.assertFalse(disable_response["removed"]) - self.assertEqual(self.user_branches, {"24.08"}) - self.assertEqual( - [call.args[0][0] for call in self.service._run_flatpak_command.call_args_list], - ["list", "list", "list", "list"], - ) - - def test_extension_toggle_removes_owned_user_branch_but_preserves_system_branch(self): - self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True) - self.service.ownership_path.write_text( - json.dumps({"version": 1, "plugin_owned_branches": ["24.08"]}), - encoding="utf-8", - ) - self.user_branches = {"24.08"} - self.system_branches = {"24.08"} - - disable_response = self.service.set_extension_enabled("24.08", False) - repeat_response = self.service.set_extension_enabled("24.08", False) - - self.assertTrue(disable_response["success"]) - self.assertTrue(disable_response["enabled"]) - self.assertTrue(disable_response["removed"]) - self.assertTrue(repeat_response["success"]) - self.assertTrue(repeat_response["enabled"]) - self.assertTrue(repeat_response["installed"]) - self.assertEqual(self.user_branches, set()) - self.assertEqual(self.system_branches, {"24.08"}) - self.assertFalse(self.service.ownership_path.exists()) - uninstall_commands = [ - call.args[0] - for call in self.service._run_flatpak_command.call_args_list - if call.args[0][0] == "uninstall" - ] - self.assertEqual(len(uninstall_commands), 1) - - def test_corrupt_ownership_metadata_fails_closed(self): - self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True) - self.service.ownership_path.write_text("{not-json", encoding="utf-8") - - response = self.service.remove_plugin_owned_extensions() - - self.assertFalse(response["success"]) - self.assertTrue(response["ownership_uncertain"]) - self.assertEqual(self.service._run_flatpak_command.call_count, 0) - - def test_dangling_ownership_symlink_fails_closed(self): - self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True) - self.service.ownership_path.symlink_to(self.home / "missing-metadata") - - response = self.service.remove_plugin_owned_extensions() - - self.assertFalse(response["success"]) - self.assertTrue(response["ownership_uncertain"]) - self.assertEqual(self.service._run_flatpak_command.call_count, 0) - - def test_ensure_app_support_installs_only_the_app_runtime_branch(self): - response = self.service.ensure_app_support("com.example.Game") - - self.assertTrue(response["success"]) - self.assertEqual(response["support_status"], "ready") - self.assertEqual(response["runtime_branch"], "24.08") - install_args = next( - call.args[0] - for call in self.service._run_flatpak_command.call_args_list - if call.args[0][0] == "install" - ) - self.assertEqual(install_args[0], "install") - self.assertIn("--user", install_args) - self.assertNotIn("23.08", install_args) - self.assertEqual( - json.loads(self.service.ownership_path.read_text(encoding="utf-8")), - {"version": 1, "plugin_owned_branches": ["24.08"]}, - ) - - def test_two_shortcuts_using_one_flatpak_share_one_extension_branch(self): - first = self.service.ensure_app_support("net.pcsx2.PCSX2") - second = self.service.ensure_app_support("net.pcsx2.PCSX2.Dev") - - self.assertEqual(first["support_status"], "ready") - self.assertEqual(second["support_status"], "ready") - install_commands = [ - call.args[0] - for call in self.service._run_flatpak_command.call_args_list - if call.args[0][0] == "install" - ] - self.assertEqual(len(install_commands), 1) - self.assertEqual( - json.loads(self.service.ownership_path.read_text(encoding="utf-8")), - {"version": 1, "plugin_owned_branches": ["24.08"]}, - ) - - def test_cleanup_removes_all_owned_branches_without_reusing_stale_metadata(self): - self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True) - self.service.ownership_path.write_text( - json.dumps({"version": 1, "plugin_owned_branches": ["23.08", "24.08"]}), - encoding="utf-8", - ) - self.user_branches = {"23.08", "24.08"} - - response = self.service.remove_plugin_owned_extensions() - - self.assertTrue(response["success"]) - self.assertEqual(response["removed_branches"], ["23.08", "24.08"]) - self.assertFalse(self.service.ownership_path.exists()) - uninstall_commands = [ - call.args[0] - for call in self.service._run_flatpak_command.call_args_list - if call.args[0][0] == "uninstall" - ] - self.assertEqual(len(uninstall_commands), 2) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_plugin_migration.py b/tests/test_plugin_migration.py index a392f85..54e8f83 100644 --- a/tests/test_plugin_migration.py +++ b/tests/test_plugin_migration.py @@ -25,7 +25,6 @@ class PluginMigrationTests(unittest.TestCase): plugin = Plugin.__new__(Plugin) plugin.installation_service = Mock() - plugin.flatpak_service = Mock() asyncio.run(plugin._migration()) @@ -33,7 +32,6 @@ class PluginMigrationTests(unittest.TestCase): decky.migrate_settings.assert_called_once() decky.migrate_runtime.assert_called_once() plugin.installation_service.install.assert_not_called() - plugin.flatpak_service.migrate_v2.assert_not_called() finally: sys.path.remove("py_modules") if previous_decky is None: diff --git a/tests/test_steam_service.py b/tests/test_steam_service.py index 636c87f..dd78f10 100644 --- a/tests/test_steam_service.py +++ b/tests/test_steam_service.py @@ -11,105 +11,42 @@ sys.modules.setdefault( ) sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules")) -from lsfg_vk.steam_service import SteamService, classify_shortcut_transport +from lsfg_vk.steam_service import SteamService -class SteamTransportTests(unittest.TestCase): - def test_only_direct_canonical_flatpak_forms_are_classified(self): - self.assertEqual( - classify_shortcut_transport( - "/usr/bin/flatpak", - "run com.example.PCSX2 --fullscreen", - ), - {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, - ) - self.assertEqual( - classify_shortcut_transport( - "flatpak", - "run com.example.PCSX2 --fullscreen", - ), - {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, - ) - self.assertEqual( - classify_shortcut_transport( - "/usr/bin/flatpak run com.example.PCSX2", - "--fullscreen", - ), - {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, - ) - self.assertEqual( - classify_shortcut_transport( - "/usr/bin/bash", - "~/launch-game.sh --fullscreen", - ), - {"kind": "host"}, - ) - self.assertEqual( - classify_shortcut_transport( - "/usr/bin/flatpak", - "--user run com.example.PCSX2", - ), - {"kind": "host"}, - ) - self.assertEqual( - classify_shortcut_transport( - "/usr/bin/flatpak", - "run bash ~/launch-game.sh", - ), - {"kind": "host"}, - ) - self.assertEqual( - classify_shortcut_transport( - "~/.lsfg", - "run --branch=stable --arch=x86_64 com.example.PCSX2", - ), - {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, - ) - self.assertEqual( - classify_shortcut_transport( - "/home/deck/.lsfg", - "run com.example.PCSX2", - ), - {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, - ) - self.assertEqual( - classify_shortcut_transport("~/.lsfg", "--profile high"), - {"kind": "host"}, - ) - - def test_shortcut_data_preserves_transport_inputs(self): +class SteamServiceTests(unittest.TestCase): + def test_shortcut_data_is_host_only_and_preserves_launch_fields(self): game = SteamService._shortcut_game( { "appid": 123456, - "AppName": "PCSX2 shortcut", - "Exe": "/usr/bin/flatpak", - "LaunchOptions": "run net.pcsx2.PCSX2 --fullscreen", + "AppName": "Native shortcut", + "Exe": "/usr/bin/game", + "LaunchOptions": "--fullscreen", "StartDir": "/home/deck/Games", } ) - self.assertEqual(game["appid"], "123456") - self.assertEqual(game["transport"], { - "kind": "flatpak", - "flatpakAppId": "net.pcsx2.PCSX2", + self.assertEqual(game, { + "appid": "123456", + "name": "Native shortcut", + "nonSteam": True, + "executable": "/usr/bin/game", + "arguments": "--fullscreen", + "startDir": "/home/deck/Games", }) - self.assertEqual(game["executable"], "/usr/bin/flatpak") - self.assertEqual(game["arguments"], "run net.pcsx2.PCSX2 --fullscreen") - self.assertEqual(game["startDir"], "/home/deck/Games") - def test_wrapped_flatpak_shortcut_remains_a_flatpak_target(self): + def test_shortcut_data_accepts_missing_optional_launch_fields(self): game = SteamService._shortcut_game( { "appid": 987654, - "AppName": "Wrapped Flatpak", - "Exe": "~/.lsfg", - "LaunchOptions": "run --branch=stable --arch=x86_64 com.example.Game", + "AppName": "Game shortcut", } ) - self.assertEqual(game["transport"], { - "kind": "flatpak", - "flatpakAppId": "com.example.Game", + self.assertEqual(game, { + "appid": "987654", + "name": "Game shortcut", + "nonSteam": True, }) diff --git a/tests/test_wrapper_service.py b/tests/test_wrapper_service.py index 0632d93..592ffb3 100644 --- a/tests/test_wrapper_service.py +++ b/tests/test_wrapper_service.py @@ -1,4 +1,4 @@ -import os +import json import subprocess import sys import tempfile @@ -114,125 +114,6 @@ class WrapperServiceTests(unittest.TestCase): self.assertEqual(passthrough_values["KEEP"], "yes") self.assertEqual(passthrough_values["DXVK_HDR"], "1") - def test_flatpak_shortcut_receives_env_arguments_and_original_target(self): - fake_flatpak = self.home / ".local/bin/flatpak" - fake_flatpak.parent.mkdir(parents=True, exist_ok=True) - fake_flatpak.write_text( - "#!/bin/sh\n" - "printf 'ARG:%s\\n' \"$@\"\n", - encoding="utf-8", - ) - fake_flatpak.chmod(0o755) - self.service.set( - "123", - self._state(dxvkFrameRate=20, enableZink=True), - str(fake_flatpak), - False, - {"kind": "flatpak", "flatpakAppId": "com.example.Game"}, - ) - result = self._run(123, "run", "com.example.Game", "--windowed", env={"DXVK_CONFIG": "foo=1"}) - args = result.stdout.splitlines() - self.assertEqual(args[0], "ARG:run") - self.assertIn("ARG:--filesystem=" + str(self.service.config_dir) + ":rw", args) - self.assertIn("ARG:--filesystem=" + str(self.home / ".local/share/Steam/steamapps/common/Lossless Scaling") + ":ro", args) - self.assertIn("ARG:--env=LSFGVK_CONFIG=" + str(self.service.config_file_path), args) - self.assertIn("ARG:--env=LSFGVK_FLATPAK=1", args) - self.assertIn("ARG:--env=SteamAppId=123", args) - self.assertIn("ARG:--env=ENABLE_GAMESCOPE_WSI=0", args) - self.assertIn("ARG:--env=DXVK_HDR=0", args) - self.assertIn("ARG:--env=__GLX_VENDOR_LIBRARY_NAME=mesa", args) - self.assertIn("ARG:--env=MESA_LOADER_DRIVER_OVERRIDE=zink", args) - self.assertIn("ARG:--env=GALLIUM_DRIVER=zink", args) - self.assertIn("ARG:--env=DXVK_CONFIG=foo=1; dxvk.maxFrameRate = 20", args) - self.assertIn("ARG:com.example.Game", args) - self.assertIn("ARG:--windowed", args) - - def test_flatpak_full_executable_form_is_preserved(self): - fake_flatpak = self.home / ".local/bin/flatpak" - fake_flatpak.parent.mkdir(parents=True, exist_ok=True) - fake_flatpak.write_text( - "#!/bin/sh\n" - "printf 'ARG:%s\\n' \"$@\"\n", - encoding="utf-8", - ) - fake_flatpak.chmod(0o755) - response = self.service.set( - "123", - self._state(), - f"{fake_flatpak} run com.example.Game", - False, - {"kind": "flatpak", "flatpakAppId": "com.example.Game"}, - ) - self.assertTrue(response["success"]) - result = self._run(123, "--windowed") - args = result.stdout.splitlines() - self.assertEqual(args[0], "ARG:run") - self.assertIn("ARG:com.example.Game", args) - self.assertIn("ARG:--windowed", args) - - def test_host_transport_does_not_store_shortcut_target(self): - self.service.set( - "123", - self._state(), - "/usr/bin/flatpak", - False, - {"kind": "flatpak", "flatpakAppId": "com.example.Game"}, - ) - response = self.service.set( - "123", - self._state(), - "/usr/bin/ignored", - False, - {"kind": "host"}, - ) - self.assertTrue(response["success"]) - self.assertIsNone(response["shortcut_exe"]) - self.assertIsNone(self.service.get("123")["shortcut_exe"]) - - def test_flatpak_transport_rejects_non_run_invocation(self): - fake_flatpak = self.home / ".local/bin/flatpak" - fake_flatpak.parent.mkdir(parents=True, exist_ok=True) - fake_flatpak.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") - fake_flatpak.chmod(0o755) - response = self.service.set( - "123", - self._state(), - str(fake_flatpak), - False, - {"kind": "flatpak", "flatpakAppId": "com.example.Game"}, - ) - self.assertTrue(response["success"]) - result = subprocess.run( - [str(self.service.wrapper_path), "bash", "launch-game.sh"], - env={"PATH": "/usr/bin:/bin", "SteamAppId": "123"}, - capture_output=True, - text=True, - ) - self.assertEqual(result.returncode, 64) - self.assertIn("direct flatpak run", result.stderr) - - def test_flatpak_transport_rejects_external_app_id_change(self): - fake_flatpak = self.home / ".local/bin/flatpak" - fake_flatpak.parent.mkdir(parents=True, exist_ok=True) - fake_flatpak.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") - fake_flatpak.chmod(0o755) - response = self.service.set( - "123", - self._state(), - str(fake_flatpak), - False, - {"kind": "flatpak", "flatpakAppId": "com.example.Game"}, - ) - self.assertTrue(response["success"]) - result = subprocess.run( - [str(self.service.wrapper_path), "run", "com.other.Game"], - env={"PATH": "/usr/bin:/bin", "SteamAppId": "123"}, - capture_output=True, - text=True, - ) - self.assertEqual(result.returncode, 64) - self.assertIn("application ID changed externally", result.stderr) - def test_invalid_state_and_foreign_wrapper_fail_closed(self): invalid = self.service.set("0", self.service.default_state()) self.assertFalse(invalid["success"]) @@ -246,6 +127,26 @@ class WrapperServiceTests(unittest.TestCase): self.assertIn("unowned", response["error"]) self.assertEqual(self.service.wrapper_path.read_text(encoding="utf-8"), "#!/bin/sh\necho foreign\n") + def test_rejects_unsupported_persisted_fields_without_rewriting(self): + self.service.config_dir.mkdir(parents=True, exist_ok=True) + original = json.dumps({ + "version": self.service.FORMAT_VERSION, + "apps": { + "123": { + "state": self._state(), + "command_token_added": False, + "legacy": "unsupported", + }, + }, + }) + self.service.sidecar_path.write_text(original, encoding="utf-8") + + response = self.service.get("123") + + self.assertFalse(response["success"]) + self.assertIn("Unsupported workaround entry fields", response["error"]) + self.assertEqual(self.service.sidecar_path.read_text(encoding="utf-8"), original) + def test_remove_keeps_a_safe_owned_passthrough_wrapper(self): self.service.set("123", self.service.default_state()) response = self.service.remove("123") |
