summaryrefslogtreecommitdiff
path: root/src/components/CustomPathOverride.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/CustomPathOverride.tsx')
-rw-r--r--src/components/CustomPathOverride.tsx146
1 files changed, 126 insertions, 20 deletions
diff --git a/src/components/CustomPathOverride.tsx b/src/components/CustomPathOverride.tsx
index 9e7d34b..9915efc 100644
--- a/src/components/CustomPathOverride.tsx
+++ b/src/components/CustomPathOverride.tsx
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { ButtonItem, Field, PanelSectionRow, ToggleField } from "@decky/ui";
import { FileSelectionType, openFilePicker } from "@decky/api";
+import { getPathDefaults } from "../api";
import type { CustomOverrideConfig } from "../types/index";
interface CustomPathOverrideProps {
@@ -8,9 +9,23 @@ interface CustomPathOverrideProps {
}
const DEFAULT_START_PATH = "/home";
+const DEFAULT_STEAM_LIBRARY_PATH = "/home/deck/.local/share/Steam/steamapps/common";
+
+interface PathDefaults {
+ home: string;
+ steamCommon: string;
+}
+
+const INITIAL_PATH_DEFAULTS: PathDefaults = {
+ home: DEFAULT_START_PATH,
+ steamCommon: DEFAULT_STEAM_LIBRARY_PATH,
+};
const normalizePath = (path: string) => path.replace(/\\/g, "/");
+const stripTrailingSlash = (value: string) =>
+ value.endsWith("/") ? value.slice(0, -1) : value;
+
const escapeForDoubleQuotes = (value: string) =>
value.replace(/[`"\\$]/g, (match) => `\\${match}`);
@@ -31,6 +46,14 @@ const escapeForReplacement = (value: string) =>
const quoteForShell = (value: string) => `'${value.replace(/'/g, "'\\''")}'`;
+const dirname = (path: string) => {
+ const normalized = normalizePath(path);
+ const parts = normalized.split("/");
+ parts.pop();
+ const dir = parts.join("/");
+ return dir.length > 0 ? dir : "/";
+};
+
const longestCommonPrefix = (left: string[], right: string[]) => {
const length = Math.min(left.length, right.length);
let idx = 0;
@@ -129,6 +152,40 @@ 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 [pathDefaults, setPathDefaults] = useState<PathDefaults>(INITIAL_PATH_DEFAULTS);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ const fetchDefaults = async () => {
+ try {
+ const result = await getPathDefaults();
+ if (!result) {
+ return;
+ }
+
+ const home = result.home ? normalizePath(result.home) : INITIAL_PATH_DEFAULTS.home;
+ const steamCommonSource = result.steam_common
+ ? normalizePath(result.steam_common)
+ : normalizePath(`${stripTrailingSlash(home)}/.local/share/Steam/steamapps/common`);
+
+ if (!cancelled) {
+ setPathDefaults({
+ home,
+ steamCommon: steamCommonSource || INITIAL_PATH_DEFAULTS.steamCommon,
+ });
+ }
+ } catch (err) {
+ console.error("CustomPathOverride -> getPathDefaults", err);
+ }
+ };
+
+ fetchDefaults();
+
+ return () => {
+ cancelled = true;
+ };
+ }, []);
const { config, error } = useMemo(
() => buildOverride(launcherPath, overridePath),
@@ -143,27 +200,58 @@ export const CustomPathOverride = ({ onOverrideChange }: CustomPathOverrideProps
}
}, [config, isEnabled, onOverrideChange]);
+ interface PickerArgs {
+ existing: string | null;
+ setter: (value: string) => void;
+ fallbackStart?: string | null;
+ }
+
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));
+ async ({ existing, setter, fallbackStart }: PickerArgs) => {
+ const candidates = new Set<string>();
+
+ if (existing) {
+ candidates.add(normalizePath(existing));
+ } else {
+ if (fallbackStart) {
+ candidates.add(normalizePath(fallbackStart));
}
- } catch (err) {
- console.error("CustomPathOverride -> openPicker", err);
+ candidates.add(pathDefaults.steamCommon);
+ candidates.add(pathDefaults.home);
+ }
+
+ let lastError: unknown = null;
+
+ for (const candidate of candidates) {
+ if (!candidate) {
+ continue;
+ }
+
+ try {
+ const result = await openFilePicker(
+ FileSelectionType.FILE,
+ candidate,
+ true,
+ true,
+ undefined,
+ undefined,
+ true
+ );
+
+ if (result?.path) {
+ setter(normalizePath(result.path));
+ return;
+ }
+ } catch (err) {
+ lastError = err;
+ }
+ }
+
+ if (lastError) {
+ console.error("CustomPathOverride -> openPicker", lastError);
}
},
- []
+ [pathDefaults]
);
const handleToggle = (value: boolean) => {
@@ -193,7 +281,13 @@ export const CustomPathOverride = ({ onOverrideChange }: CustomPathOverrideProps
<PanelSectionRow>
<ButtonItem
layout="below"
- onClick={() => openPicker(launcherPath, setLauncherPath)}
+ onClick={() =>
+ openPicker({
+ existing: launcherPath,
+ setter: setLauncherPath,
+ fallbackStart: pathDefaults.steamCommon,
+ })
+ }
description={launcherPath || "Pick the EXE Steam currently uses."}
>
Select Steam-provided EXE
@@ -203,8 +297,20 @@ export const CustomPathOverride = ({ onOverrideChange }: CustomPathOverrideProps
<PanelSectionRow>
<ButtonItem
layout="below"
- onClick={() => openPicker(overridePath, setOverridePath)}
- description={overridePath || "Pick the executable that should run instead."}
+ disabled={!launcherPath}
+ onClick={() =>
+ launcherPath &&
+ openPicker({
+ existing: overridePath,
+ setter: setOverridePath,
+ fallbackStart: launcherPath ? dirname(launcherPath) : pathDefaults.steamCommon,
+ })
+ }
+ description={
+ launcherPath
+ ? overridePath || "Pick the executable that should run instead."
+ : "Select the Steam-provided executable first."
+ }
>
Select Override EXE
</ButtonItem>