From be1feeee76cf4956bc5456e4b2740930750fbedf Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 14:59:56 -0400 Subject: fix: recognize split Steam Flatpak targets --- py_modules/lsfg_vk/steam_service.py | 26 +++++++++++++++++--- src/hooks/useGameConfiguration.ts | 12 ++++++---- src/utils/steamLaunchOptions.ts | 10 +++++++- tests/steamLaunchOptions.test.ts | 48 +++++++++++++++++++++++++++++++++++++ tests/test_steam_service.py | 19 +++++++++++++++ 5 files changed, 107 insertions(+), 8 deletions(-) diff --git a/py_modules/lsfg_vk/steam_service.py b/py_modules/lsfg_vk/steam_service.py index a7c0083..a1b58a9 100644 --- a/py_modules/lsfg_vk/steam_service.py +++ b/py_modules/lsfg_vk/steam_service.py @@ -26,13 +26,33 @@ def _split_command(value: Optional[str]) -> Optional[list[str]]: return None -def classify_shortcut_transport(executable: Optional[str], _launch_options: Optional[str] = None) -> Dict[str, object]: +def _is_usr_bin_start_dir(value: Optional[str]) -> bool: + return isinstance(value, str) and value.strip().rstrip("/") == "/usr/bin" + + +def _effective_shortcut_executable( + executable: Optional[str], + start_dir: Optional[str], +) -> Optional[str]: + if _split_command(executable) == ["flatpak"] and _is_usr_bin_start_dir(start_dir): + return "/usr/bin/flatpak" + return executable + + +def classify_shortcut_transport( + executable: Optional[str], + _launch_options: Optional[str] = None, + start_dir: Optional[str] = None, +) -> Dict[str, object]: """Recognize only the direct Flatpak executable Target. + Steam can serialize the direct target as ``Exe=flatpak`` with + ``StartDir=/usr/bin/``. Treat that split representation as the same + effective target without inspecting launch arguments or app IDs. Launch scripts and wrapper commands remain host targets. Their launch options are intentionally opaque to the plugin. """ - executable_tokens = _split_command(executable) + executable_tokens = _split_command(_effective_shortcut_executable(executable, start_dir)) return {"kind": "flatpak"} if tuple(executable_tokens or ()) in _DIRECT_FLATPAK_TARGETS else {"kind": "host"} @@ -137,7 +157,7 @@ class SteamService(BaseService): "appid": str(appid & 0xFFFFFFFF), "name": name, "nonSteam": True, - "transport": classify_shortcut_transport(executable, arguments), + "transport": classify_shortcut_transport(executable, arguments, start_dir), } for key, value in (("executable", executable), ("arguments", arguments), ("startDir", start_dir)): if value is not None: diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index a3976a0..b2867b3 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -3,13 +3,16 @@ import { useQuickAccessVisible } from "@decky/api"; import { Router } from "@decky/ui"; 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 { getDefaultWrapperPath, installWrapperIntegration, normalizeShortcutTarget, removeWrapperIntegration } from "../utils/steamLaunchOptions"; import { showErrorToast } from "../utils/toastUtils"; export interface GameTarget extends InstalledGame { configured: boolean; } -function shortcutTransport(executable: unknown): InstalledGame["transport"] { - const target = typeof executable === "string" ? executable.trim() : ""; +function shortcutTransport(executable: unknown, startDir: unknown): InstalledGame["transport"] { + const target = normalizeShortcutTarget( + typeof executable === "string" ? executable : undefined, + typeof startDir === "string" ? startDir : undefined, + ); return target === "/usr/bin/flatpak" || target === '"~/.lsfg" "/usr/bin/flatpak"' || target === "~/.lsfg /usr/bin/flatpak" @@ -29,11 +32,12 @@ async function getSteamShortcuts(): Promise { const name = shortcut?.data?.strAppName; if (!Number.isInteger(appid) || appid === 0 || typeof name !== "string" || !name) return []; const executable = shortcut?.data?.strShortcutExe ?? shortcut?.data?.strExe ?? shortcut?.data?.exe; + const startDir = shortcut?.data?.strShortcutStartDir ?? shortcut?.data?.strStartDir ?? shortcut?.data?.startDir; return [{ appid: String(appid >>> 0), name, nonSteam: true, - transport: shortcutTransport(executable), + transport: shortcutTransport(executable, startDir), }]; }); } catch { diff --git a/src/utils/steamLaunchOptions.ts b/src/utils/steamLaunchOptions.ts index bcd69d0..9f88a9f 100644 --- a/src/utils/steamLaunchOptions.ts +++ b/src/utils/steamLaunchOptions.ts @@ -57,12 +57,20 @@ function timer() { }; } +export function normalizeShortcutTarget(executable?: string | null, startDir?: string | null): string { + const target = typeof executable === "string" ? executable.trim() : ""; + const normalizedStartDir = typeof startDir === "string" ? startDir.trim().replace(/\/+$/, "") : ""; + return target === "flatpak" && normalizedStartDir === "/usr/bin" + ? DIRECT_FLATPAK_EXECUTABLE + : target; +} + function snapshot(appId: number, nonSteam: boolean, details: SteamAppDetails): SteamLaunchOptionsSnapshot { return { appId, nonSteam, options: nonSteam ? details.strShortcutLaunchOptions || "" : details.strLaunchOptions || "", - target: nonSteam ? details.strShortcutExe || "" : "", + target: nonSteam ? normalizeShortcutTarget(details.strShortcutExe, details.strShortcutStartDir) : "", details, }; } diff --git a/tests/steamLaunchOptions.test.ts b/tests/steamLaunchOptions.test.ts index 049f6be..1a6069f 100644 --- a/tests/steamLaunchOptions.test.ts +++ b/tests/steamLaunchOptions.test.ts @@ -8,6 +8,7 @@ import { installWrapperIntegration, installWrapperLaunchOption, isLegacyWrapperToken, + normalizeShortcutTarget, normalizeLaunchOptions, readSteamLaunchOptions, removeWrapperIntegration, @@ -16,6 +17,12 @@ import { const wrapper = "~/.lsfg"; +test("normalizes Steam's split direct Flatpak target representation", () => { + assert.equal(normalizeShortcutTarget("flatpak", "/usr/bin/"), "/usr/bin/flatpak"); + assert.equal(normalizeShortcutTarget("flatpak", "/usr/bin"), "/usr/bin/flatpak"); + assert.equal(normalizeShortcutTarget("flatpak", "/home/deck/"), "flatpak"); +}); + test("inserts one wrapper immediately before an existing command macro", () => { assert.deepEqual(installWrapperLaunchOption('gamemoderun %command% --profile "high quality"', wrapper), { options: 'gamemoderun ~/.lsfg %command% --profile "high quality"', @@ -144,6 +151,47 @@ test("reads the matching app-details field and installs/removes Steam integratio } }); +test("wraps a split direct Flatpak target while preserving its launch arguments", async () => { + const previousWindow = (globalThis as Record).window; + const previousSteamClient = (globalThis as Record).SteamClient; + let shortcutTarget = "flatpak"; + const shortcutOptions = '"run" "--branch=master" "io.github.banjorecomp.banjorecomp"'; + const targetWrites: string[] = []; + const apps = { + RegisterForAppDetails(_appId: number, callback: (details: SteamAppDetails) => void) { + callback({ + strShortcutExe: shortcutTarget, + strShortcutStartDir: "/usr/bin/", + strShortcutLaunchOptions: shortcutOptions, + }); + return { unregister() {} }; + }, + SetShortcutExe(_appId: number, executable: string) { + targetWrites.push(executable); + shortcutTarget = executable; + }, + SetShortcutLaunchOptions() {}, + }; + (globalThis as Record).window = { setTimeout, clearTimeout }; + (globalThis as Record).SteamClient = { Apps: apps }; + try { + const installed = await installWrapperIntegration(46, true, wrapper, false, { kind: "flatpak" }); + assert.equal(installed.originalExecutable, "/usr/bin/flatpak"); + assert.equal(installed.snapshot.target, '"~/.lsfg" "/usr/bin/flatpak"'); + assert.equal(installed.snapshot.options, shortcutOptions); + assert.deepEqual(targetWrites, ['"~/.lsfg" "/usr/bin/flatpak"']); + + const restored = await removeWrapperIntegration(46, true, wrapper, installed.originalExecutable, false, { kind: "flatpak" }); + assert.equal(restored.target, "/usr/bin/flatpak"); + assert.deepEqual(targetWrites, ['"~/.lsfg" "/usr/bin/flatpak"', "/usr/bin/flatpak"]); + } finally { + if (previousWindow === undefined) delete (globalThis as Record).window; + else (globalThis as Record).window = previousWindow; + if (previousSteamClient === undefined) delete (globalThis as Record).SteamClient; + else (globalThis as Record).SteamClient = previousSteamClient; + } +}); + test("uses shortcut launch options for a host shortcut without changing its Target", async () => { const previousWindow = (globalThis as Record).window; const previousSteamClient = (globalThis as Record).SteamClient; diff --git a/tests/test_steam_service.py b/tests/test_steam_service.py index 95bd2a2..4f61f3c 100644 --- a/tests/test_steam_service.py +++ b/tests/test_steam_service.py @@ -21,6 +21,10 @@ class SteamTransportTests(unittest.TestCase): classify_shortcut_transport("/usr/bin/flatpak", "run com.example.Game --fullscreen"), {"kind": "flatpak"}, ) + self.assertEqual( + classify_shortcut_transport("flatpak", "run com.example.Game --fullscreen", "/usr/bin/"), + {"kind": "flatpak"}, + ) for executable, options in ( ("flatpak", "run com.example.Game"), ("/usr/bin/flatpak run com.example.Game", "--fullscreen"), @@ -40,6 +44,21 @@ class SteamTransportTests(unittest.TestCase): ) self.assertEqual(classify_shortcut_transport("~/.lsfg", "run com.example.Game"), {"kind": "host"}) + def test_split_flatpak_target_is_recognized_without_script_or_app_detection(self): + game = SteamService._shortcut_game( + { + "appid": 123456, + "AppName": "Split Flatpak shortcut", + "Exe": "flatpak", + "StartDir": "/usr/bin/", + "LaunchOptions": "run io.example.Game", + } + ) + + self.assertEqual(game["transport"], {"kind": "flatpak"}) + self.assertEqual(game["executable"], "flatpak") + self.assertEqual(game["startDir"], "/usr/bin/") + def test_direct_flatpak_shortcut_keeps_arguments_without_an_app_id(self): game = SteamService._shortcut_game( { -- cgit v1.2.3