summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorxXJSONDeruloXx <danielhimebauch@gmail.com>2026-09-08 16:56:57 -0400
committerxXJSONDeruloXx <danielhimebauch@gmail.com>2026-09-08 16:56:57 -0400
commitbec26fe025c97c00d398e7a4fb571195706b9e76 (patch)
treeabec710e369d4c1cf3a0d97501a6fe9667a63d93
parent8352ee74e8a437c1f4a4dadb75d63049f45b164b (diff)
downloaddecky-lsfg-vk-bec26fe025c97c00d398e7a4fb571195706b9e76.tar.gz
decky-lsfg-vk-bec26fe025c97c00d398e7a4fb571195706b9e76.zip
feat: more launch arg janitoring
-rw-r--r--src/components/Content.tsx7
-rw-r--r--src/hooks/useGameConfiguration.ts5
-rw-r--r--src/utils/steamLaunchOptionParser.ts28
-rw-r--r--src/utils/steamLaunchOptions.ts6
-rw-r--r--tests/steamLaunchOptions.test.ts23
5 files changed, 46 insertions, 23 deletions
diff --git a/src/components/Content.tsx b/src/components/Content.tsx
index f1c8c11..1d7d2d1 100644
--- a/src/components/Content.tsx
+++ b/src/components/Content.tsx
@@ -80,8 +80,9 @@ export function Content() {
const handleConfigChange = async (
fieldName: keyof ConfigurationData,
value: boolean | number | string | string[],
+ cleanupLaunchOptions = false,
) => {
- await save({ ...config, [fieldName]: value });
+ await save({ ...config, [fieldName]: value }, cleanupLaunchOptions);
};
const onInstall = () => {
@@ -115,7 +116,7 @@ export function Content() {
<NowPlayingTab
game={runningGame}
config={config}
- onConfigChange={handleConfigChange}
+ onConfigChange={(fieldName, value) => handleConfigChange(fieldName, value)}
/>
),
}] : []),
@@ -128,7 +129,7 @@ export function Content() {
targets={targets}
runningGame={runningGame}
onSelect={setSelectedAppId}
- onConfigChange={handleConfigChange}
+ onConfigChange={(fieldName, value) => handleConfigChange(fieldName, value, true)}
onEnable={enable}
onEnableAll={enableAll}
onReset={resetSelected}
diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts
index 7a572f9..46607cb 100644
--- a/src/hooks/useGameConfiguration.ts
+++ b/src/hooks/useGameConfiguration.ts
@@ -106,12 +106,13 @@ export function useGameConfiguration() {
}
}, [installedGames]);
- const save = useCallback(async (next: ConfigurationData) => {
+ const save = useCallback(async (next: ConfigurationData, cleanupLaunchOptions = false) => {
const selectedTarget = targets.find((target) => target.appid === selectedAppId);
if (!selectedTarget?.name) return;
+ if (cleanupLaunchOptions && !(await cleanupTargetLaunchOptions(selectedTarget))) return;
const result = await updateGameConfig(selectedAppId, selectedTarget.name, next);
if (result.success) await load();
- }, [load, selectedAppId, targets]);
+ }, [cleanupTargetLaunchOptions, load, selectedAppId, targets]);
const enable = useCallback(async (appid: string) => {
const target = targets.find((item) => item.appid === appid);
diff --git a/src/utils/steamLaunchOptionParser.ts b/src/utils/steamLaunchOptionParser.ts
index 2fd7b67..5b8156c 100644
--- a/src/utils/steamLaunchOptionParser.ts
+++ b/src/utils/steamLaunchOptionParser.ts
@@ -34,6 +34,16 @@ interface WorkaroundDefinition {
}
const COMMAND_TOKEN = "%command%";
+const LEGACY_WRAPPER_TOKENS = new Set([
+ "~/lsfg",
+ "/home/deck/lsfg",
+ "~/.local/bin/lsfg-vk-experimental",
+ "/home/deck/.local/bin/lsfg-vk-experimental",
+ "~/.local/bin/mako-run",
+ "/home/deck/.local/bin/mako-run",
+ "~/.local/bin/mako-launch",
+ "/home/deck/.local/bin/mako-launch",
+]);
const DXVK_FRAME_RATE_KEYS: readonly DxvkFrameRateKey[] = [
"dxvk.maxFrameRate",
"dxgi.maxFrameRate",
@@ -145,6 +155,7 @@ function tokenize(options: string): LaunchToken[] {
}
function serialize(tokens: readonly LaunchToken[]): string {
+ if (tokens.length === 1 && tokens[0].raw.toLowerCase() === COMMAND_TOKEN) return "";
return tokens.map((token) => token.raw).join(" ");
}
@@ -171,9 +182,6 @@ function leadingEnvironmentCount(tokens: readonly LaunchToken[]): number {
}
function effectivePrefixLimit(tokens: readonly LaunchToken[]): number {
- // Only assignment words before the first command affect the launched game.
- // Anything after a wrapper command is that command's argument, even when it
- // happens to look like KEY=value.
return leadingEnvironmentCount(tokens);
}
@@ -223,15 +231,15 @@ function ensureCommandToken(tokens: LaunchToken[]): void {
export function isLegacyWrapperToken(value: string): boolean {
const path = decodeToken(value);
- return path === "~/lsfg" || path === "/home/deck/lsfg";
+ return LEGACY_WRAPPER_TOKENS.has(path);
}
function removeLegacyWrapperFromTokens(tokens: LaunchToken[]): boolean {
const commandIndex = findCommandIndex(tokens);
const prefixEnd = commandIndex >= 0 ? commandIndex : tokens.length;
- const wrapperIndex = leadingEnvironmentCount(tokens);
- if (wrapperIndex >= prefixEnd || !isLegacyWrapperToken(tokens[wrapperIndex].raw)) return false;
- tokens.splice(wrapperIndex, 1);
+ const retained = tokens.filter((token, index) => index >= prefixEnd || !isLegacyWrapperToken(token.raw));
+ if (retained.length === tokens.length) return false;
+ tokens.splice(0, tokens.length, ...retained);
return true;
}
@@ -482,8 +490,12 @@ export function applyWorkaroundChange(options: string, field: WorkaroundField, v
return serialize(tokens);
}
-export function cleanupLegacyWrapper(options: string): string {
+export function cleanupLegacyLaunchOptions(options: string): string {
const tokens = tokenize(options);
removeLegacyWrapperFromTokens(tokens);
return serialize(tokens);
}
+
+export function cleanupLegacyWrapper(options: string): string {
+ return cleanupLegacyLaunchOptions(options);
+}
diff --git a/src/utils/steamLaunchOptions.ts b/src/utils/steamLaunchOptions.ts
index 9c7b5eb..39125bd 100644
--- a/src/utils/steamLaunchOptions.ts
+++ b/src/utils/steamLaunchOptions.ts
@@ -1,5 +1,5 @@
// @ts-expect-error Node's built-in TypeScript loader requires explicit source extensions in tests.
-import { cleanupLegacyWrapper, isLegacyWrapperToken, normalizeLaunchOptions } from "./steamLaunchOptionParser.ts";
+import { cleanupLegacyLaunchOptions, isLegacyWrapperToken, normalizeLaunchOptions } from "./steamLaunchOptionParser.ts";
// @ts-expect-error Node's built-in TypeScript loader requires explicit source extensions in tests.
export * from "./steamLaunchOptionParser.ts";
@@ -23,7 +23,7 @@ function getSteamApps(): Partial<SteamApps> | undefined {
function snapshotFromDetails(appId: number, nonSteam: boolean, details: SteamAppDetails): SteamLaunchOptionsSnapshot {
if (nonSteam && isLegacyWrapperToken(details.strShortcutExe || "")) {
- throw new Error("The shortcut Target still points to the legacy ~/lsfg wrapper; restore its original executable first");
+ throw new Error("The shortcut Target still points to a legacy frame-generation wrapper; restore its original executable first");
}
return {
appId,
@@ -203,7 +203,7 @@ export function cleanupSteamLaunchOptions(
): Promise<SteamLaunchOptionsSnapshot> {
return queueSteamAppOperation(appId, nonSteam, async () => {
const current = await readSteamLaunchOptions(appId, nonSteam);
- const next = cleanupLegacyWrapper(current.options);
+ const next = cleanupLegacyLaunchOptions(current.options);
if (next === current.options) return current;
await setSteamLaunchOptions(appId, nonSteam, next);
return waitForLaunchOptions(appId, nonSteam, next);
diff --git a/tests/steamLaunchOptions.test.ts b/tests/steamLaunchOptions.test.ts
index 8ecfa77..0662fbb 100644
--- a/tests/steamLaunchOptions.test.ts
+++ b/tests/steamLaunchOptions.test.ts
@@ -3,9 +3,11 @@ import test from "node:test";
import {
applyWorkaroundChange,
applyWorkaroundState,
+ cleanupLegacyLaunchOptions,
cleanupLegacyWrapper,
getDefaultWorkaroundState,
isLegacyWrapperToken,
+ normalizeLaunchOptions,
parseWorkaroundOptions,
readSteamLaunchOptions,
updateSteamLaunchOptions,
@@ -45,7 +47,7 @@ test("uses SteamDeck=0 before %command% without a wrapper", () => {
test("keeps WSI disable opt-in and does not add HDR assignments", () => {
const defaults = getDefaultWorkaroundState();
- assert.equal(applyWorkaroundState("%command%", defaults), "%command%");
+ assert.equal(applyWorkaroundState("%command%", defaults), "");
assert.equal(parseWorkaroundOptions("%command%").state.disableGamescopeWsi, false);
assert.equal(
applyWorkaroundChange("%command%", "disableGamescopeWsi", true),
@@ -53,7 +55,7 @@ test("keeps WSI disable opt-in and does not add HDR assignments", () => {
);
assert.equal(
applyWorkaroundChange("ENABLE_GAMESCOPE_WSI=0 %command%", "disableGamescopeWsi", false),
- "%command%",
+ "",
);
const legacy = parseWorkaroundOptions("ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 %command%");
@@ -61,7 +63,7 @@ test("keeps WSI disable opt-in and does not add HDR assignments", () => {
assert.deepEqual(legacy.issues, []);
assert.equal(
applyWorkaroundChange("ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 %command%", "disableGamescopeWsi", false),
- "%command%",
+ "",
);
const invalid = parseWorkaroundOptions("ENABLE_GAMESCOPE_WSI=maybe %command%");
@@ -123,7 +125,7 @@ test("uses DXVK_CONFIG for the base cap and preserves other DXVK settings", () =
);
assert.equal(
applyWorkaroundChange("DXVK_FRAME_RATE=30 %command%", "dxvkFrameRate", 0),
- "%command%",
+ "",
);
const apiSpecific = parseWorkaroundOptions(
@@ -164,7 +166,7 @@ test("keeps vkBasalt disable mutually exclusive while preserving the dropped ena
assert.equal(disabled, "DISABLE_VKBASALT=1 %command%");
assert.equal(
applyWorkaroundChange(disabled, "disableVkbasalt", false),
- "%command%",
+ "",
);
const conflict = parseWorkaroundOptions("ENABLE_VKBASALT=1 DISABLE_VKBASALT=1 %command%");
assert.equal(conflict.state.disableVkbasalt, true);
@@ -191,7 +193,7 @@ test("handles current and legacy Zink forms and reports partial state", () => {
"enableZink",
false,
),
- "%command%",
+ "",
);
});
@@ -200,7 +202,7 @@ test("cleans only the known legacy wrapper and preserves launch options", () =>
cleanupLegacyWrapper('FOO=bar ~/lsfg %command% --arg "~/lsfg"'),
'FOO=bar %command% --arg "~/lsfg"',
);
- assert.equal(cleanupLegacyWrapper("/home/deck/lsfg %command%"), "%command%");
+ assert.equal(cleanupLegacyWrapper("/home/deck/lsfg %command%"), "");
assert.equal(
cleanupLegacyWrapper("DXVK_FRAME_RATE=30 LSFG_PROCESS=decky-lsfg-vk %command%"),
"DXVK_FRAME_RATE=30 LSFG_PROCESS=decky-lsfg-vk %command%",
@@ -212,6 +214,13 @@ test("cleans only the known legacy wrapper and preserves launch options", () =>
assert.equal(isLegacyWrapperToken("/home/kurt/lsfg"), false);
});
+test("canonicalizes a bare command token without removing real arguments", () => {
+ assert.equal(normalizeLaunchOptions("%command%"), "");
+ assert.equal(normalizeLaunchOptions("%COMMAND%"), "");
+ assert.equal(normalizeLaunchOptions("FOO=bar %command%"), "FOO=bar %command%");
+ assert.equal(normalizeLaunchOptions("%command% --windowed"), "%command% --windowed");
+});
+
test("is idempotent", () => {
const first = applyWorkaroundChange("gamemoderun %command%", "enableZink", true);
assert.equal(applyWorkaroundState(first, parseWorkaroundOptions(first).state), first);