summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorxXJSONDeruloXx <danielhimebauch@gmail.com>2026-09-10 16:26:17 -0400
committerxXJSONDeruloXx <danielhimebauch@gmail.com>2026-09-10 16:26:17 -0400
commit485ebb748185391e28764f48c39322b3cbdac65c (patch)
tree9b03047ed842e14d9c800bcdfa16cad322733cf4
parentde74f0d2499159ed1cf8f628a166302146ae1f13 (diff)
downloaddecky-lsfg-vk-v2-flatpakless.tar.gz
decky-lsfg-vk-v2-flatpakless.zip
refactor: remove Flatpak supportv2-flatpakless
-rw-r--r--assets/flatpak-target.pngbin78195 -> 0 bytes
-rw-r--r--defaults/i18n/ja.json23
-rw-r--r--defaults/i18n/ko.json23
-rw-r--r--defaults/i18n/template.json23
-rw-r--r--package.json15
-rw-r--r--py_modules/lsfg_vk/constants.py3
-rw-r--r--py_modules/lsfg_vk/flatpak_service.py431
-rw-r--r--py_modules/lsfg_vk/plugin.py49
-rw-r--r--py_modules/lsfg_vk/steam_service.py47
-rw-r--r--py_modules/lsfg_vk/wrapper_service.py193
-rw-r--r--src/api/lsfgApi.ts44
-rw-r--r--src/components/ConfigurationTab.tsx22
-rw-r--r--src/components/Content.tsx1
-rw-r--r--src/components/GameConfigurationControls.tsx3
-rw-r--r--src/components/GameConfigurationSelector.tsx3
-rw-r--r--src/components/NowPlayingTab.tsx36
-rw-r--r--src/components/WorkaroundsSection.tsx6
-rw-r--r--src/hooks/useGameConfiguration.ts44
-rw-r--r--src/hooks/usePerAppWorkarounds.ts35
-rw-r--r--src/i18n/languages.json81
-rw-r--r--src/types.d.ts2
-rw-r--r--src/utils/steamLaunchOptions.ts121
-rw-r--r--tests/steamLaunchOptions.test.ts95
-rw-r--r--tests/test_flatpak_service.py299
-rw-r--r--tests/test_plugin_migration.py2
-rw-r--r--tests/test_steam_service.py101
-rw-r--r--tests/test_wrapper_service.py141
27 files changed, 104 insertions, 1739 deletions
diff --git a/assets/flatpak-target.png b/assets/flatpak-target.png
deleted file mode 100644
index 773e567..0000000
--- a/assets/flatpak-target.png
+++ /dev/null
Binary files differ
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")