diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/SetupTab.tsx | 128 | ||||
| -rw-r--r-- | src/hooks/useGameConfiguration.ts | 39 | ||||
| -rw-r--r-- | src/hooks/usePerAppWorkarounds.ts | 35 | ||||
| -rw-r--r-- | src/utils/steamLaunchOptions.ts | 24 |
4 files changed, 87 insertions, 139 deletions
diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx index 624be54..d769200 100644 --- a/src/components/SetupTab.tsx +++ b/src/components/SetupTab.tsx @@ -1,13 +1,6 @@ -import { ButtonItem, Field, PanelSection, PanelSectionRow, ToggleField } from "@decky/ui"; -import { useEffect, useState } from "react"; -import { - getFlatpakSupportStatus, - setFlatpakExtensionEnabled, - type FlatpakExtensionStatus, - type SteamBranchStatus, -} from "../api/lsfgApi"; +import { ButtonItem, Field, PanelSection, PanelSectionRow } from "@decky/ui"; +import { type SteamBranchStatus } from "../api/lsfgApi"; import t from "../i18n/i18n"; -import { showErrorToast } from "../utils/toastUtils"; interface SetupTabProps { isInstalled: boolean; @@ -21,72 +14,6 @@ interface SetupTabProps { onUninstall: () => void; } -function FlatpakSupportDiagnostics() { - const [status, setStatus] = useState<FlatpakExtensionStatus | null>(null); - const [operation, setOperation] = useState<string | null>(null); - - const refresh = async () => { - try { - setStatus(await getFlatpakSupportStatus()); - } catch (error) { - setStatus({ - success: false, - message: "", - error: String(error), - available: false, - extension_id: "", - supported_branches: [], - installed_branches: [], - }); - } - }; - - useEffect(() => { - void refresh(); - }, []); - - if (!status?.available) return null; - - const setEnabled = async (branch: string, enabled: boolean) => { - setOperation(`${enabled ? "enable" : "disable"}-${branch}`); - try { - const result = await setFlatpakExtensionEnabled(branch, enabled); - if (!result.success) throw new Error(result.error || result.message || "Flatpak runtime update failed"); - await refresh(); - } catch (error) { - showErrorToast("Flatpak runtime update failed", String(error)); - } finally { - setOperation(null); - } - }; - - return ( - <PanelSection title="Flatpak runtimes"> - <PanelSectionRow> - <Field - label="LSFG-VK runtime extensions" - description={status.message || "Toggle a branch to install or uninstall it."} - /> - </PanelSectionRow> - {status.supported_branches.map((branch) => { - const installed = status.installed_branches.includes(branch); - const pending = operation?.endsWith(`-${branch}`); - return ( - <PanelSectionRow key={branch}> - <ToggleField - label={branch} - description={pending ? (operation?.startsWith("enable") ? "Installing..." : "Uninstalling...") : installed ? "Installed" : "Not installed"} - checked={installed} - onChange={(enabled) => void setEnabled(branch, enabled)} - disabled={operation !== null} - /> - </PanelSectionRow> - ); - })} - </PanelSection> - ); -} - export function SetupTab(props: SetupTabProps) { const { isInstalled, @@ -109,36 +36,33 @@ export function SetupTab(props: SetupTabProps) { : t("INSTALL_INSTALL_BTN", "Install LSFG-VK"); return ( - <> - <PanelSection title="Setup"> + <PanelSection title="Setup"> + <PanelSectionRow> + <Field + label="Lossless Scaling" + description={losslessScalingAppInstalled ? "Installed" : losslessScalingStatus || "Not installed"} + /> + </PanelSectionRow> + <PanelSectionRow> + <Field label="LSFG-VK" description={installationStatus} /> + </PanelSectionRow> + {steamBranchStatus?.installed && ( <PanelSectionRow> <Field - label="Lossless Scaling" - description={losslessScalingAppInstalled ? "Installed" : losslessScalingStatus || "Not installed"} + label="Steam branch" + description={`${steamBranchStatus.current_branch || "public"}${steamBranchStatus.needs_switch ? ` - ${steamBranchStatus.message}` : ""}`} /> </PanelSectionRow> - <PanelSectionRow> - <Field label="LSFG-VK" description={installationStatus} /> - </PanelSectionRow> - {steamBranchStatus?.installed && ( - <PanelSectionRow> - <Field - label="Steam branch" - description={`${steamBranchStatus.current_branch || "public"}${steamBranchStatus.needs_switch ? ` - ${steamBranchStatus.message}` : ""}`} - /> - </PanelSectionRow> - )} - <PanelSectionRow> - <ButtonItem - layout="below" - onClick={isInstalled ? onUninstall : onInstall} - disabled={isInstalling || isUninstalling} - > - {buttonLabel} - </ButtonItem> - </PanelSectionRow> - </PanelSection> - <FlatpakSupportDiagnostics /> - </> + )} + <PanelSectionRow> + <ButtonItem + layout="below" + onClick={isInstalled ? onUninstall : onInstall} + disabled={isInstalling || isUninstalling} + > + {buttonLabel} + </ButtonItem> + </PanelSectionRow> + </PanelSection> ); } diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index c70d589..e120426 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -48,11 +48,6 @@ function selectShortcutExecutable( .map((candidate) => candidate?.trim()) .find((candidate) => candidate && candidate.startsWith("/")); if (absolute) return absolute; - - // Steam's app-details API can report a Flatpak Target as just "flatpak" - // even when the shortcut's canonical VDF executable is /usr/bin/flatpak. - // Keep the stored original executable absolute so SetShortcutExe and the - // generated dispatcher agree on the same direct transport. if (target.transport.kind === "flatpak") return "/usr/bin/flatpak"; return candidates.map((candidate) => candidate?.trim()).find(Boolean); } @@ -158,25 +153,26 @@ export function useGameConfiguration() { const oldShortcutExe = existing.shortcut_exe || undefined; const oldCommandTokenAdded = existing.command_token_added === true; const oldTransport = existing.transport || target.transport; - if (target.nonSteam && oldState && current.target === wrapperPath && !oldShortcutExe) { + const usesShortcutTarget = target.nonSteam && target.transport.kind === "flatpak"; + if (usesShortcutTarget && oldState && current.target === wrapperPath && !oldShortcutExe) { throw new Error("Managed shortcut Target has no saved original executable"); } - if (target.nonSteam && oldState && current.target !== wrapperPath && current.target !== oldShortcutExe) { + if (usesShortcutTarget && oldState && current.target !== wrapperPath && current.target !== oldShortcutExe) { throw new Error("Shortcut Target changed externally; refusing to replace it"); } - if (target.nonSteam && !oldState && current.target === wrapperPath) { + if (usesShortcutTarget && !oldState && current.target === wrapperPath) { throw new Error("Shortcut Target is already the managed wrapper but its original Target is unknown"); } const state = oldState || { ...DEFAULT_WORKAROUND_STATE }; - const originalExecutable = target.nonSteam + const originalExecutable = usesShortcutTarget ? selectShortcutExecutable( target, oldShortcutExe, - target.transport.kind === "flatpak" ? target.executable : undefined, + target.executable, current.target, ) : undefined; - const initialIntegration = target.nonSteam + const initialIntegration = usesShortcutTarget ? current.target === wrapperPath : hasWrapperLaunchIntegration(current.options, wrapperPath); const initialStateResult = await setWorkaroundState( @@ -190,16 +186,22 @@ export function useGameConfiguration() { let integration: Awaited<ReturnType<typeof installWrapperIntegration>> | null = null; try { - integration = await installWrapperIntegration(appId, target.nonSteam, wrapperPath, oldCommandTokenAdded); + integration = await installWrapperIntegration( + appId, + target.nonSteam, + wrapperPath, + oldCommandTokenAdded, + target.transport.kind, + ); const finalStateResult = await setWorkaroundState( target.appid, state, - target.nonSteam + usesShortcutTarget ? (selectShortcutExecutable( target, integration.originalExecutable, originalExecutable, - target.transport.kind === "flatpak" ? target.executable : undefined, + target.executable, ) || null) : null, integration.commandTokenAdded, @@ -215,15 +217,16 @@ export function useGameConfiguration() { appId, target.nonSteam, wrapperPath, - target.nonSteam + usesShortcutTarget ? (selectShortcutExecutable( target, integration?.originalExecutable, originalExecutable, - target.transport.kind === "flatpak" ? target.executable : undefined, + target.executable, ) || undefined) : undefined, integration?.commandTokenAdded ?? oldCommandTokenAdded, + target.transport.kind, ); } catch (rollbackError) { showErrorToast("Workaround rollback failed", asError(rollbackError).message); @@ -257,6 +260,7 @@ export function useGameConfiguration() { const existing = await getWorkaroundState(target.appid); if (!existing.success) throw new Error(existing.error || "Could not read workaround state"); const wrapperPath = existing.wrapper_path || getDefaultWrapperPath(); + const usesShortcutTarget = target.nonSteam && target.transport.kind === "flatpak"; if (existing.state) { await removeWrapperIntegration( appId, @@ -264,10 +268,11 @@ export function useGameConfiguration() { wrapperPath, existing.shortcut_exe || undefined, existing.command_token_added === true, + target.transport.kind, ); } else { const current = await readSteamLaunchOptions(appId, target.nonSteam); - if (target.nonSteam && (current.target === wrapperPath || isLegacyWrapperToken(current.target))) { + if (usesShortcutTarget && (current.target === wrapperPath || isLegacyWrapperToken(current.target))) { throw new Error("Shortcut Target is a frame-generation wrapper but its original Target is unknown"); } await cleanupLegacySteamLaunchOptions(appId, target.nonSteam, wrapperPath); diff --git a/src/hooks/usePerAppWorkarounds.ts b/src/hooks/usePerAppWorkarounds.ts index e9e44e1..ebb12e2 100644 --- a/src/hooks/usePerAppWorkarounds.ts +++ b/src/hooks/usePerAppWorkarounds.ts @@ -73,22 +73,31 @@ function selectShortcutExecutable( return candidates.map((candidate) => candidate?.trim()).find(Boolean); } +function usesShortcutTarget(nonSteam: boolean, transport: TargetTransport): boolean { + return nonSteam && transport.kind === "flatpak"; +} + function integrationIsInstalled( steam: SteamLaunchOptionsSnapshot, nonSteam: boolean, + transport: TargetTransport, wrapperPath: string, ): boolean { - return nonSteam ? steam.target === wrapperPath : hasWrapperLaunchIntegration(steam.options, wrapperPath); + return usesShortcutTarget(nonSteam, transport) + ? steam.target === wrapperPath + : hasWrapperLaunchIntegration(steam.options, wrapperPath); } 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(); - if (nonSteam && steam.target === wrapperPath && !result.shortcut_exe) { + const selectedTransport = result.transport || transport; + if (usesShortcutTarget(nonSteam, selectedTransport) && steam.target === wrapperPath && !result.shortcut_exe) { throw new Error("Managed shortcut Target has no saved original executable"); } return { @@ -96,10 +105,10 @@ function makeSnapshot( state: result.state, wrapperPath, wrapperOwned: result.wrapper_owned === true, - integrationInstalled: integrationIsInstalled(steam, nonSteam, wrapperPath), + integrationInstalled: integrationIsInstalled(steam, nonSteam, selectedTransport, wrapperPath), commandTokenAdded: result.command_token_added === true, shortcutExe: result.shortcut_exe, - transport: result.transport || { kind: "host" }, + transport: selectedTransport, }; } @@ -110,10 +119,11 @@ async function adoptWorkaroundState( steam: SteamLaunchOptionsSnapshot, wrapperPath: string, ): Promise<WorkaroundSnapshot> { - if (nonSteam && (!steam.target || steam.target === wrapperPath || isLegacyWrapperToken(steam.target))) { + const shortcutTarget = usesShortcutTarget(nonSteam, transport); + if (shortcutTarget && (!steam.target || steam.target === wrapperPath || isLegacyWrapperToken(steam.target))) { throw new Error("Shortcut Target is a wrapper but its original Target is unknown"); } - const originalExecutable = nonSteam + const originalExecutable = shortcutTarget ? selectShortcutExecutable(transport, steam.target) : null; const initial = await setWorkaroundState( @@ -130,18 +140,20 @@ async function adoptWorkaroundState( Number(appId), nonSteam, wrapperPath, + false, + transport.kind, ); const finalized = await setWorkaroundState( appId, DEFAULT_WORKAROUND_STATE, - nonSteam + shortcutTarget ? (selectShortcutExecutable(transport, integration.originalExecutable, originalExecutable) || null) : null, integration.commandTokenAdded, transport, ); if (!finalized.success) throw new Error(finalized.error || "Could not finalize workaround state"); - return makeSnapshot(integration.snapshot, finalized, nonSteam); + return makeSnapshot(integration.snapshot, finalized, nonSteam, transport); } catch (error) { let rollbackSucceeded = true; if (integration) { @@ -150,10 +162,11 @@ async function adoptWorkaroundState( Number(appId), nonSteam, wrapperPath, - nonSteam + shortcutTarget ? (selectShortcutExecutable(transport, integration?.originalExecutable, originalExecutable) || undefined) : undefined, integration?.commandTokenAdded ?? false, + transport.kind, ); } catch { // Leave the owned integration in place rather than guessing at cleanup. @@ -194,7 +207,7 @@ export function usePerAppWorkarounds( result.wrapper_path || getDefaultWrapperPath(), ); } - return makeSnapshot(steam, result, nonSteam); + return makeSnapshot(steam, result, nonSteam, transport); }, [appId, nonSteam, numericAppId, transport]); const applySnapshot = useCallback((next: WorkaroundSnapshot) => { @@ -230,7 +243,7 @@ export function usePerAppWorkarounds( setSnapshot((current) => current ? { ...current, steam, - integrationInstalled: integrationIsInstalled(steam, nonSteam, current.wrapperPath), + integrationInstalled: integrationIsInstalled(steam, nonSteam, current.transport, current.wrapperPath), } : current); }, (subscriptionError) => { diff --git a/src/utils/steamLaunchOptions.ts b/src/utils/steamLaunchOptions.ts index 0af1bbf..65541d8 100644 --- a/src/utils/steamLaunchOptions.ts +++ b/src/utils/steamLaunchOptions.ts @@ -172,7 +172,11 @@ function removeMatchingWrappers(tokens: LaunchToken[], predicate: (value: string return true; } -export function installWrapperLaunchOption(options: string, wrapperPath = DEFAULT_WRAPPER_PATH) { +export function installWrapperLaunchOption( + options: string, + wrapperPath = DEFAULT_WRAPPER_PATH, + allowCommandArgs = false, +) { const tokens = tokenize(options); removeMatchingWrappers(tokens, isLegacyToken); let command = commandIndex(tokens); @@ -185,7 +189,7 @@ export function installWrapperLaunchOption(options: string, wrapperPath = DEFAUL } let insertion = 0; while (insertion < tokens.length && isAssignment(tokens[insertion])) insertion++; - if (insertion < tokens.length && !tokens[insertion].value.startsWith("-")) { + if (!allowCommandArgs && insertion < tokens.length && !tokens[insertion].value.startsWith("-")) { throw new Error("Launch options do not contain %command%; refusing to guess a launcher command"); } tokens.splice(insertion, 0, @@ -336,10 +340,11 @@ export function installWrapperIntegration( nonSteam: boolean, wrapperPath: string, commandTokenAdded = false, + transport: "host" | "flatpak" = "host", ): Promise<WrapperIntegrationResult> { return queued(appId, nonSteam, async () => { let current = await readSteamLaunchOptions(appId, nonSteam); - if (nonSteam) { + if (nonSteam && transport === "flatpak") { 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"); @@ -364,11 +369,11 @@ export function installWrapperIntegration( const cleaned = cleanupPluginAssignments(cleanupLegacyLaunchOptions(current.options)); const alreadyInstalled = hasWrapperLaunchIntegration(current.options, wrapperPath); - const rewrite = installWrapperLaunchOption(cleaned, wrapperPath); + const rewrite = installWrapperLaunchOption(cleaned, wrapperPath, nonSteam); if (rewrite.options === current.options) return { snapshot: current, commandTokenAdded }; const value = await writeVerified( - appId, false, current.options, rewrite.options, - (options) => writeOptions(appId, false, options), readOptions, + appId, nonSteam, current.options, rewrite.options, + (options) => writeOptions(appId, nonSteam, options), readOptions, "Steam did not accept the launch options", ); return { snapshot: value, commandTokenAdded: alreadyInstalled ? commandTokenAdded : rewrite.commandTokenAdded }; @@ -381,10 +386,11 @@ export function removeWrapperIntegration( wrapperPath: string, originalExecutable?: string, commandTokenAdded = false, + transport: "host" | "flatpak" = "host", ): Promise<SteamLaunchOptionsSnapshot> { return queued(appId, nonSteam, async () => { let current = await readSteamLaunchOptions(appId, nonSteam); - if (nonSteam) { + if (nonSteam && transport === "flatpak") { if (!originalExecutable || isWrapperToken(originalExecutable, wrapperPath)) { throw new Error("Original shortcut Target is unavailable; refusing to overwrite the current Target"); } @@ -408,8 +414,8 @@ export function removeWrapperIntegration( } const next = cleanupPluginAssignments(removeWrapperLaunchOption(current.options, wrapperPath, commandTokenAdded)); return next === current.options ? current : writeVerified( - appId, false, current.options, next, - (options) => writeOptions(appId, false, options), readOptions, + appId, nonSteam, current.options, next, + (options) => writeOptions(appId, nonSteam, options), readOptions, "Steam did not clean the launch options", ); }); |
