From 790132668c4421c68c32bdc8fc9792b0d6028f97 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 8 Sep 2026 22:18:13 -0400 Subject: I really dont want to but here you go little guy --- src/components/ConfigurationTab.tsx | 6 ++++ src/components/GameConfigurationSelector.tsx | 29 ++++++++++++++++-- src/components/ProfileDetails.tsx | 4 +-- src/components/WorkaroundsSection.tsx | 21 ++++++++----- src/hooks/useGameConfiguration.ts | 46 ++++++++++++++++++++++------ src/i18n/languages.json | 12 ++++++-- src/utils/steamLaunchOptionParser.ts | 40 +++++++++++++++++++----- src/utils/steamLaunchOptions.ts | 15 ++++++++- 8 files changed, 140 insertions(+), 33 deletions(-) (limited to 'src') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 2bb0f26..c41c941 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -33,6 +33,7 @@ export function ConfigurationTab({ const [detailAppId, setDetailAppId] = useState(null); const [focusFpsMultiplier, setFocusFpsMultiplier] = useState(false); const [focusDetailAction, setFocusDetailAction] = useState<"enable" | "fps" | null>(null); + const [focusConfiguredToggle, setFocusConfiguredToggle] = useState(false); const enableRef = useRef(null); const promptedRunningAppId = useRef(null); const closeDetails = useCallback(() => { @@ -41,6 +42,7 @@ export function ConfigurationTab({ setDetailAppId(null); }, []); const clearFpsFocusRequest = useCallback(() => setFocusFpsMultiplier(false), []); + const clearConfiguredToggleFocusRequest = useCallback(() => setFocusConfiguredToggle(false), []); useEffect(() => { if (!focusDetailAction) return; @@ -77,12 +79,15 @@ export function ConfigurationTab({ targets={targets} runningGame={runningGame} onSelect={(appid) => { + setFocusConfiguredToggle(false); setFocusDetailAction(targets.find((target) => target.appid === appid)?.configured ? "fps" : "enable"); onSelect(appid); setDetailAppId(appid); }} onEnableAll={onEnableAll} onResetAll={onResetAll} + focusConfiguredToggle={focusConfiguredToggle} + onConfiguredToggleFocused={clearConfiguredToggleFocusRequest} /> ); @@ -96,6 +101,7 @@ export function ConfigurationTab({ if (selectedTarget?.configured) { promptedRunningAppId.current = detailAppId; await onReset(); + setFocusConfiguredToggle(true); closeDetails(); } else if (detailAppId && await onEnable(detailAppId)) { setFocusFpsMultiplier(true); diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index 5f23b91..50b2cf8 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -1,5 +1,5 @@ import { ButtonItem, ConfirmModal, Field, PanelSectionRow, showModal } from "@decky/ui"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState, type RefObject } from "react"; import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; import { GameTarget } from "../hooks/useGameConfiguration"; @@ -9,6 +9,8 @@ interface Props { onSelect: (appid: string) => void; onEnableAll: () => Promise; onResetAll: () => Promise; + focusConfiguredToggle?: boolean; + onConfiguredToggleFocused?: () => void; } const CONFIGURED_COLLAPSED_KEY = "lsfg-configured-games-collapsed-v3"; @@ -40,12 +42,14 @@ function GameGroup({ collapsed, onToggle, onSelect, + toggleRef, }: { title: string; games: GameTarget[]; collapsed: boolean; onToggle: () => void; onSelect: (appid: string) => void; + toggleRef?: RefObject; }) { if (games.length === 0) return null; @@ -56,6 +60,7 @@ function GameGroup({
@@ -86,7 +91,15 @@ function GameGroup({ ); } -export function GameConfigurationSelector({ targets, runningGame, onSelect, onEnableAll, onResetAll }: Props) { +export function GameConfigurationSelector({ + targets, + runningGame, + onSelect, + onEnableAll, + onResetAll, + focusConfiguredToggle = false, + onConfiguredToggleFocused, +}: Props) { const sortGames = (games: GameTarget[]) => [...games].sort((a, b) => { if (a.appid === runningGame?.appid) return -1; if (b.appid === runningGame?.appid) return 1; @@ -102,6 +115,17 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onEn const [configuredNonSteamCollapsed, toggleConfiguredNonSteam] = usePersistentCollapsed(`${CONFIGURED_COLLAPSED_KEY}-non-steam`); const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(AVAILABLE_COLLAPSED_KEY); const [availableNonSteamCollapsed, toggleAvailableNonSteam] = usePersistentCollapsed(`${AVAILABLE_COLLAPSED_KEY}-non-steam`); + const configuredToggleRef = useRef(null); + + useEffect(() => { + if (!focusConfiguredToggle) return; + const frame = requestAnimationFrame(() => { + configuredToggleRef.current?.querySelector('[role="button"], button')?.focus(); + onConfiguredToggleFocused?.(); + }); + return () => cancelAnimationFrame(frame); + }, [configuredGames.length, focusConfiguredToggle, onConfiguredToggleFocused]); + const confirmResetAll = () => { showModal( setExpanded((value) => !value)} > - {expanded ? : } Details + {expanded ? : } Game Details {expanded && ( - + )} diff --git a/src/components/WorkaroundsSection.tsx b/src/components/WorkaroundsSection.tsx index e990784..d783620 100644 --- a/src/components/WorkaroundsSection.tsx +++ b/src/components/WorkaroundsSection.tsx @@ -10,7 +10,7 @@ interface WorkaroundsSectionProps { nonSteam: boolean; } -const WORKAROUNDS_COLLAPSED_KEY = "lsfg-workarounds-collapsed"; +const WORKAROUNDS_COLLAPSED_KEY = "lsfg-workarounds-collapsed-v2"; type ToggleWorkaroundField = Exclude; const TOGGLE_ROWS: readonly { @@ -20,19 +20,26 @@ const TOGGLE_ROWS: readonly { descriptionKey: string; description: string; }[] = [ + { + field: "disableSteamdeckMode", + labelKey: "CONFIG_DISABLE_STEAMDECK_MODE", + label: "Disable Steam Deck Mode", + descriptionKey: "CONFIG_DISABLE_STEAMDECK_MODE_DESC", + description: "Disables a game-specific Steam Deck compatibility switch. Requires game restart to apply.", + }, { field: "disableGamescopeWsi", labelKey: "CONFIG_DISABLE_GAMESCOPE_WSI", label: "Disable Gamescope WSI", descriptionKey: "CONFIG_DISABLE_GAMESCOPE_WSI_DESC", - description: "Adds ENABLE_GAMESCOPE_WSI=0 without changing HDR. Requires game restart to apply.", + description: "Adds ENABLE_GAMESCOPE_WSI=0. Requires game restart to apply.", }, { - field: "disableSteamdeckMode", - labelKey: "CONFIG_DISABLE_STEAMDECK_MODE", - label: "Disable Steam Deck Mode", - descriptionKey: "CONFIG_DISABLE_STEAMDECK_MODE_DESC", - description: "Disables a game-specific Steam Deck compatibility switch. Requires game restart to apply.", + field: "disableHdr", + labelKey: "CONFIG_DISABLE_HDR", + label: "Disable HDR", + descriptionKey: "CONFIG_DISABLE_HDR_DESC", + description: "Prevents DXVK from exposing HDR to the game. Requires game restart to apply.", }, { field: "disableVkbasalt", diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index 46607cb..6d1fe6a 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -3,7 +3,7 @@ import { useQuickAccessVisible } from "@decky/api"; import { Router } from "@decky/ui"; import { getGameConfigs, getInstalledGames, updateGameConfig, resetGameConfig, resetAllGameConfigs, type GameConfigEntry, type GlobalConfig, type InstalledGame } from "../api/lsfgApi"; import { ConfigurationData, getDefaults } from "../config/configSchema"; -import { cleanupSteamLaunchOptions } from "../utils/steamLaunchOptions"; +import { applyWorkaroundState, cleanupLegacySteamLaunchOptions, cleanupSteamLaunchOptions, getDefaultWorkaroundState, updateSteamLaunchOptions } from "../utils/steamLaunchOptions"; import { showErrorToast } from "../utils/toastUtils"; export interface GameTarget extends InstalledGame { configured: boolean; } @@ -98,7 +98,7 @@ export function useGameConfiguration() { const cleanupTargetLaunchOptions = useCallback(async (target: GameTarget): Promise => { if (!installedGames.some((game) => game.appid === target.appid)) return true; try { - await cleanupSteamLaunchOptions(Number(target.appid), target.nonSteam); + await cleanupLegacySteamLaunchOptions(Number(target.appid), target.nonSteam); return true; } catch (error) { showErrorToast("Could not update Steam launch options", error instanceof Error ? error.message : String(error)); @@ -106,6 +106,32 @@ export function useGameConfiguration() { } }, [installedGames]); + const removeTargetLaunchOptions = useCallback(async (target: GameTarget): Promise => { + if (!installedGames.some((game) => game.appid === target.appid)) return true; + try { + await cleanupSteamLaunchOptions(Number(target.appid), target.nonSteam); + return true; + } catch (error) { + showErrorToast("Could not clean up Steam launch options", error instanceof Error ? error.message : String(error)); + return false; + } + }, [installedGames]); + + const initializeTargetLaunchOptions = useCallback(async (target: GameTarget): Promise => { + if (!installedGames.some((game) => game.appid === target.appid)) return true; + try { + await updateSteamLaunchOptions( + Number(target.appid), + target.nonSteam, + (options) => applyWorkaroundState(options, getDefaultWorkaroundState()), + ); + return true; + } catch (error) { + showErrorToast("Could not initialize Steam launch options", error instanceof Error ? error.message : String(error)); + return false; + } + }, [installedGames]); + const save = useCallback(async (next: ConfigurationData, cleanupLaunchOptions = false) => { const selectedTarget = targets.find((target) => target.appid === selectedAppId); if (!selectedTarget?.name) return; @@ -117,16 +143,16 @@ export function useGameConfiguration() { const enable = useCallback(async (appid: string) => { const target = targets.find((item) => item.appid === appid); if (!target?.name) return false; - if (!(await cleanupTargetLaunchOptions(target))) return false; + if (!(await initializeTargetLaunchOptions(target))) return false; const result = await updateGameConfig(appid, target.name, template); if (result.success) await load(); return result.success; - }, [cleanupTargetLaunchOptions, load, targets, template]); + }, [initializeTargetLaunchOptions, load, targets, template]); const enableAll = useCallback(async (): Promise => { const available = targets.filter((target) => !target.configured && target.name); if (available.length === 0) return; for (const target of available) { - if (!(await cleanupTargetLaunchOptions(target))) return; + if (!(await initializeTargetLaunchOptions(target))) return; const result = await updateGameConfig(target.appid, target.name, template); if (!result.success) { showErrorToast("Could not enable all games", result.error || "A game profile could not be created"); @@ -134,12 +160,12 @@ export function useGameConfiguration() { } } await load(); - }, [cleanupTargetLaunchOptions, load, targets, template]); + }, [initializeTargetLaunchOptions, load, targets, template]); const resetSelected = useCallback(async () => { if (selectedAppId) { const selectedTarget = targets.find((target) => target.appid === selectedAppId); - if (selectedTarget && !(await cleanupTargetLaunchOptions(selectedTarget))) return; + if (selectedTarget && !(await removeTargetLaunchOptions(selectedTarget))) return; const result = await resetGameConfig(selectedAppId); if (result.success) { setRunningGame((current) => current?.appid === selectedAppId ? { ...current, configured: false } : current); @@ -147,10 +173,10 @@ export function useGameConfiguration() { await load(); } } - }, [cleanupTargetLaunchOptions, load, selectedAppId, targets]); + }, [load, removeTargetLaunchOptions, selectedAppId, targets]); const resetAll = useCallback(async () => { for (const target of targets.filter((item) => item.configured)) { - if (!(await cleanupTargetLaunchOptions(target))) return; + if (!(await removeTargetLaunchOptions(target))) return; } const result = await resetAllGameConfigs(); if (result.success) { @@ -158,7 +184,7 @@ export function useGameConfiguration() { setSelectedAppId(""); await load(); } - }, [cleanupTargetLaunchOptions, load, targets]); + }, [load, removeTargetLaunchOptions, targets]); return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, enableAll, resetSelected, resetAll, reload: load }; } diff --git a/src/i18n/languages.json b/src/i18n/languages.json index 3f1992c..7e5676b 100644 --- a/src/i18n/languages.json +++ b/src/i18n/languages.json @@ -22,7 +22,9 @@ "CONFIG_ENABLE_WSI": "WSIを有効化", "CONFIG_ENABLE_WSI_DESC": "Gamescope WSIレイヤーを再有効化します。ゲームの再起動が必要。", "CONFIG_DISABLE_GAMESCOPE_WSI": "Gamescope WSIを無効化", - "CONFIG_DISABLE_GAMESCOPE_WSI_DESC": "HDRを変更せずENABLE_GAMESCOPE_WSI=0を追加します。ゲームの再起動が必要です。", + "CONFIG_DISABLE_GAMESCOPE_WSI_DESC": "ENABLE_GAMESCOPE_WSI=0を追加します。ゲームの再起動が必要です。", + "CONFIG_DISABLE_HDR": "HDRを無効化", + "CONFIG_DISABLE_HDR_DESC": "DXVKがゲームにHDRを公開しないようにします。ゲームの再起動が必要です。", "CONFIG_DISABLE_STEAMDECK_MODE": "Steam Deckモードを無効化", "CONFIG_DISABLE_STEAMDECK_MODE_DESC": "ゲーム固有のSteam Deck互換スイッチを無効化します。ゲームの再起動が必要です。", "CONFIG_DISABLE_VKBASALT": "vkBasaltを無効化", @@ -109,7 +111,9 @@ "CONFIG_ENABLE_WSI": "WSI 활성화", "CONFIG_ENABLE_WSI_DESC": "Gamescope WSI 레이어를 다시 활성화합니다. 게임 재시작 필요.", "CONFIG_DISABLE_GAMESCOPE_WSI": "Gamescope WSI 비활성화", - "CONFIG_DISABLE_GAMESCOPE_WSI_DESC": "HDR을 변경하지 않고 ENABLE_GAMESCOPE_WSI=0을 추가합니다. 게임 재시작 필요.", + "CONFIG_DISABLE_GAMESCOPE_WSI_DESC": "ENABLE_GAMESCOPE_WSI=0을 추가합니다. 게임 재시작 필요.", + "CONFIG_DISABLE_HDR": "HDR 비활성화", + "CONFIG_DISABLE_HDR_DESC": "DXVK가 게임에 HDR을 노출하지 않도록 합니다. 게임 재시작 필요.", "CONFIG_DISABLE_STEAMDECK_MODE": "Steam Deck 모드 비활성화", "CONFIG_DISABLE_STEAMDECK_MODE_DESC": "게임별 Steam Deck 호환 스위치를 비활성화합니다. 게임 재시작 필요.", "CONFIG_DISABLE_VKBASALT": "vkBasalt 비활성화", @@ -224,7 +228,9 @@ "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 without changing HDR. Requires game restart to apply.", + "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_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", diff --git a/src/utils/steamLaunchOptionParser.ts b/src/utils/steamLaunchOptionParser.ts index 5b8156c..9449b33 100644 --- a/src/utils/steamLaunchOptionParser.ts +++ b/src/utils/steamLaunchOptionParser.ts @@ -1,6 +1,7 @@ export interface WorkaroundState { dxvkFrameRate: number; disableGamescopeWsi: boolean; + disableHdr: boolean; disableSteamdeckMode: boolean; disableVkbasalt: boolean; enableZink: boolean; @@ -41,8 +42,10 @@ const LEGACY_WRAPPER_TOKENS = new Set([ "/home/deck/.local/bin/lsfg-vk-experimental", "~/.local/bin/mako-run", "/home/deck/.local/bin/mako-run", + "mako-run", "~/.local/bin/mako-launch", "/home/deck/.local/bin/mako-launch", + "mako-launch", ]); const DXVK_FRAME_RATE_KEYS: readonly DxvkFrameRateKey[] = [ "dxvk.maxFrameRate", @@ -53,7 +56,12 @@ const DXVK_MANAGED_KEYS = new Set(["DXVK_CONFIG", "DXVK_FRAME_RATE"]); const WORKAROUND_DEFINITIONS = { disableGamescopeWsi: { spec: ["ENABLE_GAMESCOPE_WSI", "0"], - clear: ["DISABLE_GAMESCOPE_WSI", "ENABLE_GAMESCOPE_WSI", "DXVK_HDR"], + clear: ["DISABLE_GAMESCOPE_WSI", "ENABLE_GAMESCOPE_WSI"], + }, + disableHdr: { + spec: ["DXVK_HDR", "0"], + clear: ["DXVK_HDR"], + label: "Disable HDR", }, disableSteamdeckMode: { spec: ["SteamDeck", "0"], @@ -72,24 +80,34 @@ const WORKAROUND_DEFINITIONS = { } as const satisfies Record; const BOOLEAN_WORKAROUND_FIELDS: readonly BooleanWorkaroundField[] = [ "disableGamescopeWsi", + "disableHdr", "disableSteamdeckMode", "disableVkbasalt", "enableZink", ]; const WSI_DISABLE_KEY = "DISABLE_GAMESCOPE_WSI"; const WSI_ENABLE_KEY = "ENABLE_GAMESCOPE_WSI"; +const WORKAROUND_ENV_KEYS = new Set( + BOOLEAN_WORKAROUND_FIELDS.flatMap((field) => WORKAROUND_DEFINITIONS[field].clear), +); const MANAGED_ENV_KEYS = new Set([ ...DXVK_MANAGED_KEYS, - ...BOOLEAN_WORKAROUND_FIELDS.flatMap((field) => WORKAROUND_DEFINITIONS[field].clear), + ...WORKAROUND_ENV_KEYS, ]); -const DEFAULT_WORKAROUND_STATE: WorkaroundState = { +const EMPTY_WORKAROUND_STATE: WorkaroundState = { dxvkFrameRate: 0, disableGamescopeWsi: false, + disableHdr: false, disableSteamdeckMode: false, disableVkbasalt: false, enableZink: false, }; +const DEFAULT_WORKAROUND_STATE: WorkaroundState = { + ...EMPTY_WORKAROUND_STATE, + disableGamescopeWsi: true, + disableHdr: true, +}; export function getDefaultWorkaroundState(): WorkaroundState { return { ...DEFAULT_WORKAROUND_STATE }; @@ -117,8 +135,6 @@ function decodeToken(raw: string): string { return value; } -// Steam stores one shell-like line. Keep each token's raw spelling beside its -// decoded value so managed edits leave unrelated quoting and arguments alone. function tokenize(options: string): LaunchToken[] { const tokens: LaunchToken[] = []; let start = -1; @@ -358,7 +374,7 @@ function readBoolean( export function parseWorkaroundOptions(options: string): ParsedWorkaroundOptions { const tokens = tokenize(options); const entries = effectiveEnvironmentEntries(tokens); - const state = getDefaultWorkaroundState(); + const state = { ...EMPTY_WORKAROUND_STATE }; const issues: string[] = []; for (const [key, entry] of entries) { @@ -418,7 +434,7 @@ export function parseWorkaroundOptions(options: string): ParsedWorkaroundOptions state.disableGamescopeWsi = wsiSignals.some(Boolean); } - for (const field of ["disableSteamdeckMode", "disableVkbasalt"] as const) { + for (const field of ["disableHdr", "disableSteamdeckMode", "disableVkbasalt"] as const) { const { spec, label } = WORKAROUND_DEFINITIONS[field]; state[field] = readBoolean(entries, spec[0], label || field, spec[1], issues); } @@ -455,7 +471,7 @@ export function applyWorkaroundState(options: string, state: WorkaroundState): s removeLegacyWrapperFromTokens(tokens); rewriteDxvkFrameRate(tokens, state.dxvkFrameRate); const keysToClear = new Set( - BOOLEAN_WORKAROUND_FIELDS.flatMap((field) => WORKAROUND_DEFINITIONS[field].clear), + WORKAROUND_ENV_KEYS, ); if (state.disableVkbasalt) keysToClear.add("ENABLE_VKBASALT"); removeAllAssignments(tokens, keysToClear); @@ -496,6 +512,14 @@ export function cleanupLegacyLaunchOptions(options: string): string { return serialize(tokens); } +export function cleanupPluginLaunchOptions(options: string): string { + const tokens = tokenize(options); + removeLegacyWrapperFromTokens(tokens); + rewriteDxvkFrameRate(tokens, 0); + removeAllAssignments(tokens, WORKAROUND_ENV_KEYS); + 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 39125bd..82ff94b 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 { cleanupLegacyLaunchOptions, isLegacyWrapperToken, normalizeLaunchOptions } from "./steamLaunchOptionParser.ts"; +import { cleanupLegacyLaunchOptions, cleanupPluginLaunchOptions, isLegacyWrapperToken, normalizeLaunchOptions } from "./steamLaunchOptionParser.ts"; // @ts-expect-error Node's built-in TypeScript loader requires explicit source extensions in tests. export * from "./steamLaunchOptionParser.ts"; @@ -200,6 +200,19 @@ export function updateSteamLaunchOptions( export function cleanupSteamLaunchOptions( appId: number, nonSteam: boolean, +): Promise { + return queueSteamAppOperation(appId, nonSteam, async () => { + const current = await readSteamLaunchOptions(appId, nonSteam); + const next = cleanupPluginLaunchOptions(current.options); + if (next === current.options) return current; + await setSteamLaunchOptions(appId, nonSteam, next); + return waitForLaunchOptions(appId, nonSteam, next); + }); +} + +export function cleanupLegacySteamLaunchOptions( + appId: number, + nonSteam: boolean, ): Promise { return queueSteamAppOperation(appId, nonSteam, async () => { const current = await readSteamLaunchOptions(appId, nonSteam); -- cgit v1.2.3