summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/SetupTab.tsx128
-rw-r--r--src/hooks/useGameConfiguration.ts39
-rw-r--r--src/hooks/usePerAppWorkarounds.ts35
-rw-r--r--src/utils/steamLaunchOptions.ts24
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",
);
});