initial path override ui and be

This commit is contained in:
xXJSONDeruloXx
2025-09-26 11:32:03 -04:00
parent 6810b97dbf
commit cbed25162a
7 changed files with 337 additions and 4 deletions
+15
View File
@@ -0,0 +1,15 @@
runtimes:
- dart@3.7.2
- go@1.22.3
- java@17.0.10
- node@22.2.0
- python@3.11.11
tools:
- dartanalyzer@3.7.2
- eslint@8.57.0
- lizard@1.17.31
- pmd@7.11.0
- pylint@3.3.6
- revive@1.7.0
- semgrep@1.78.0
- trivy@0.65.0
+1
Submodule DeckyFileServer added at ef58c4fccd
+8 -2
View File
@@ -1,16 +1,22 @@
import { SmartClipboardButton } from "./SmartClipboardButton";
import type { CustomOverrideConfig } from "../types/index";
interface ClipboardCommandsProps {
pathExists: boolean | null;
overrideConfig?: CustomOverrideConfig | null;
}
export function ClipboardCommands({ pathExists }: ClipboardCommandsProps) {
export function ClipboardCommands({ pathExists, overrideConfig }: ClipboardCommandsProps) {
if (pathExists !== true) return null;
const patchCommand = overrideConfig
? `${overrideConfig.envAssignment} ~/fgmod/fgmod %command%`
: "~/fgmod/fgmod %command%";
return (
<>
<SmartClipboardButton
command="~/fgmod/fgmod %command%"
command={patchCommand}
buttonText="Copy Patch Command"
/>
+284
View File
@@ -0,0 +1,284 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { ButtonItem, Field, PanelSectionRow, ToggleField } from "@decky/ui";
import { FileSelectionType, openFilePicker } from "@decky/api";
import type { CustomOverrideConfig } from "../types/index";
interface CustomPathOverrideProps {
onOverrideChange: (override: CustomOverrideConfig | null) => void;
}
const DEFAULT_START_PATH = "/home";
const normalizePath = (path: string) => path.replace(/\\/g, "/");
const escapeForDoubleQuotes = (value: string) =>
value.replace(/[`"\\$]/g, (match) => `\\${match}`);
const escapeForPattern = (value: string) =>
value
.replace(/\\/g, "\\\\")
.replace(/\//g, "\\/")
.replace(/\[/g, "\\[")
.replace(/\]/g, "\\]")
.replace(/\*/g, "\\*")
.replace(/\?/g, "\\?");
const escapeForReplacement = (value: string) =>
value
.replace(/\\/g, "\\\\")
.replace(/\//g, "\\/")
.replace(/\$/g, "\\$");
const quoteForShell = (value: string) => `'${value.replace(/'/g, "'\\''")}'`;
const longestCommonPrefix = (left: string[], right: string[]) => {
const length = Math.min(left.length, right.length);
let idx = 0;
while (idx < length && left[idx] === right[idx]) {
idx++;
}
return idx;
};
interface ComputedOverride {
config: CustomOverrideConfig | null;
error: string | null;
}
const buildOverride = (
rawDefault: string | null,
rawOverride: string | null
): ComputedOverride => {
if (!rawDefault || !rawOverride) {
return { config: null, error: null };
}
const defaultPath = normalizePath(rawDefault.trim());
const overridePath = normalizePath(rawOverride.trim());
if (defaultPath === overridePath) {
return {
config: null,
error: "Paths are identical. Choose a different target executable.",
};
}
const defaultParts = defaultPath.split("/").filter(Boolean);
const overrideParts = overridePath.split("/").filter(Boolean);
if (!defaultParts.length || !overrideParts.length) {
return {
config: null,
error: "Unable to parse selected paths. Pick them again.",
};
}
const prefixLength = longestCommonPrefix(defaultParts, overrideParts);
if (prefixLength < 2) {
return {
config: null,
error: "Selections do not share a common game folder.",
};
}
const searchSuffixParts = defaultParts.slice(prefixLength);
const replaceSuffixParts = overrideParts.slice(prefixLength);
if (!searchSuffixParts.length || !replaceSuffixParts.length) {
return {
config: null,
error: "Could not determine differing portion of the paths.",
};
}
const searchSuffix = searchSuffixParts.join("/");
const replaceSuffix = replaceSuffixParts.join("/");
const pattern = defaultParts[prefixLength - 1] ?? defaultParts[defaultParts.length - 1];
if (!pattern) {
return {
config: null,
error: "Unable to infer game identifier from path.",
};
}
const escapedPattern = escapeForDoubleQuotes(pattern);
const escapedSearch = escapeForPattern(searchSuffix);
const escapedReplace = escapeForReplacement(replaceSuffix);
const expression = `[[ "$arg" == *"${escapedPattern}"* ]] && arg=\${arg//${escapedSearch}/${escapedReplace}}`;
const snippet = `[[ "$arg" == *"${pattern}"* ]] && arg=\${arg//${searchSuffix}/${replaceSuffix}}`;
const envAssignment = `FGMOD_OVERRIDE_EXPRESSION=${quoteForShell(expression)}`;
const config: CustomOverrideConfig = {
defaultPath,
overridePath,
pattern,
searchSuffix,
replaceSuffix,
expression,
snippet,
envAssignment,
};
return { config, error: null };
};
export const CustomPathOverride = ({ onOverrideChange }: CustomPathOverrideProps) => {
const [launcherPath, setLauncherPath] = useState<string | null>(null);
const [overridePath, setOverridePath] = useState<string | null>(null);
const [isEnabled, setEnabled] = useState(false);
const { config, error } = useMemo(
() => buildOverride(launcherPath, overridePath),
[launcherPath, overridePath]
);
useEffect(() => {
if (isEnabled && config) {
onOverrideChange(config);
} else {
onOverrideChange(null);
}
}, [config, isEnabled, onOverrideChange]);
const openPicker = useCallback(
async (existing: string | null, setter: (value: string) => void) => {
try {
const startPath = existing ? normalizePath(existing) : DEFAULT_START_PATH;
const result = await openFilePicker(
FileSelectionType.FILE,
startPath,
true,
false,
undefined,
undefined,
true
);
if (result?.path) {
setter(normalizePath(result.path));
}
} catch (err) {
console.error("CustomPathOverride -> openPicker", err);
}
},
[]
);
const handleToggle = (value: boolean) => {
setEnabled(value);
if (!value) {
setLauncherPath(null);
setOverridePath(null);
onOverrideChange(null);
} else if (config) {
onOverrideChange(config);
}
};
return (
<>
<PanelSectionRow>
<ToggleField
label="Custom Launcher Override"
description="Select launcher and target executables from the Deck's file browser."
checked={isEnabled}
onChange={handleToggle}
/>
</PanelSectionRow>
{isEnabled && (
<>
<PanelSectionRow>
<ButtonItem
layout="below"
onClick={() => openPicker(launcherPath, setLauncherPath)}
description={launcherPath || "Pick the EXE Steam currently uses."}
>
Select Steam-provided EXE
</ButtonItem>
</PanelSectionRow>
<PanelSectionRow>
<ButtonItem
layout="below"
onClick={() => openPicker(overridePath, setOverridePath)}
description={overridePath || "Pick the executable that should run instead."}
>
Select Override EXE
</ButtonItem>
</PanelSectionRow>
{(launcherPath || overridePath) && (
<PanelSectionRow>
<ButtonItem
layout="below"
onClick={() => {
setLauncherPath(null);
setOverridePath(null);
}}
>
Clear selections
</ButtonItem>
</PanelSectionRow>
)}
{error && (
<PanelSectionRow>
<Field
label="Override status"
description={error}
>
⚠️
</Field>
</PanelSectionRow>
)}
{!error && config && (
<>
<PanelSectionRow>
<Field
label="Detected game"
description="Based on the shared folder between both selections."
>
{config.pattern}
</Field>
</PanelSectionRow>
<PanelSectionRow>
<Field
label="Code snippet"
description="This is added to fgmod before resolving the install path."
bottomSeparator="none"
>
<div
style={{
backgroundColor: "rgba(255,255,255,0.05)",
borderRadius: "6px",
padding: "12px",
fontFamily: "monospace",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
>
{config.snippet}
</div>
</Field>
</PanelSectionRow>
<PanelSectionRow>
<Field
label="Environment preview"
description="Automatically appended to the Patch command."
>
{config.envAssignment}
</Field>
</PanelSectionRow>
</>
)}
</>
)}
</>
);
};
+14 -1
View File
@@ -10,6 +10,8 @@ import { ClipboardCommands } from "./ClipboardCommands";
import { InstructionCard } from "./InstructionCard";
import { OptiScalerWiki } from "./OptiScalerWiki";
import { UninstallButton } from "./UninstallButton";
import { CustomPathOverride } from "./CustomPathOverride";
import type { CustomOverrideConfig } from "../types/index";
interface OptiScalerControlsProps {
pathExists: boolean | null;
@@ -21,6 +23,13 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont
const [uninstalling, setUninstalling] = useState(false);
const [installResult, setInstallResult] = useState<OperationResult | null>(null);
const [uninstallResult, setUninstallResult] = useState<OperationResult | null>(null);
const [overrideConfig, setOverrideConfig] = useState<CustomOverrideConfig | null>(null);
useEffect(() => {
if (pathExists !== true && overrideConfig) {
setOverrideConfig(null);
}
}, [pathExists, overrideConfig]);
useEffect(() => {
if (installResult) {
@@ -76,7 +85,11 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont
<OptiScalerHeader pathExists={pathExists} />
<ClipboardCommands pathExists={pathExists} />
{pathExists === true && (
<CustomPathOverride onOverrideChange={setOverrideConfig} />
)}
<ClipboardCommands pathExists={pathExists} overrideConfig={overrideConfig} />
<InstructionCard pathExists={pathExists} />
+4 -1
View File
@@ -26,7 +26,10 @@ function MainContent() {
return (
<>
<OptiScalerControls pathExists={pathExists} setPathExists={setPathExists} />
<OptiScalerControls
pathExists={pathExists}
setPathExists={setPathExists}
/>
{pathExists === true ? (
<>
{/* <InstalledGamesSection /> */}
+11
View File
@@ -24,3 +24,14 @@ export interface ModInstallationConfig {
bin: string;
};
}
export interface CustomOverrideConfig {
defaultPath: string;
overridePath: string;
pattern: string;
searchSuffix: string;
replaceSuffix: string;
expression: string;
snippet: string;
envAssignment: string;
}