From cbed25162a1058e67180aafb8fbd424bf2573e95 Mon Sep 17 00:00:00 2001
From: xXJSONDeruloXx <danielhimebauch@gmail.com>
Date: Fri, 26 Sep 2025 11:32:03 -0400
Subject: initial path override ui and be

---
 src/components/CustomPathOverride.tsx | 284 ++++++++++++++++++++++++++++++++++
 1 file changed, 284 insertions(+)
 create mode 100644 src/components/CustomPathOverride.tsx

(limited to 'src/components/CustomPathOverride.tsx')

diff --git a/src/components/CustomPathOverride.tsx b/src/components/CustomPathOverride.tsx
new file mode 100644
index 0000000..9e7d34b
--- /dev/null
+++ b/src/components/CustomPathOverride.tsx
@@ -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>
+            </>
+          )}
+        </>
+      )}
+    </>
+  );
+};
\ No newline at end of file
-- 
cgit v1.2.3


From 7d2322e637faae5ccfab58c54f7a13e6a5f7ea88 Mon Sep 17 00:00:00 2001
From: xXJSONDeruloXx <danielhimebauch@gmail.com>
Date: Fri, 26 Sep 2025 12:16:22 -0400
Subject: feat: first arg steam path start, second mirror first

---
 src/components/CustomPathOverride.tsx | 146 +++++++++++++++++++++++++++++-----
 1 file changed, 126 insertions(+), 20 deletions(-)

(limited to 'src/components/CustomPathOverride.tsx')

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>
-- 
cgit v1.2.3


From 1cc6a781fe7d6c053b2eb59aa79766a859c66209 Mon Sep 17 00:00:00 2001
From: xXJSONDeruloXx <danielhimebauch@gmail.com>
Date: Fri, 26 Sep 2025 20:14:44 -0400
Subject: revised approach, pick dir then patch in plugin ui itself

---
 src/components/CustomPathOverride.tsx | 445 +++++++++++++---------------------
 1 file changed, 164 insertions(+), 281 deletions(-)

(limited to 'src/components/CustomPathOverride.tsx')

diff --git a/src/components/CustomPathOverride.tsx b/src/components/CustomPathOverride.tsx
index 9915efc..b28a8de 100644
--- a/src/components/CustomPathOverride.tsx
+++ b/src/components/CustomPathOverride.tsx
@@ -1,388 +1,271 @@
 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 {
-  onOverrideChange: (override: CustomOverrideConfig | null) => void;
-}
-
-const DEFAULT_START_PATH = "/home";
-const DEFAULT_STEAM_LIBRARY_PATH = "/home/deck/.local/share/Steam/steamapps/common";
+import { getPathDefaults, runManualPatch, runManualUnpatch } from "../api";
+import type { ApiResponse } from "../types/index";
 
 interface PathDefaults {
   home: string;
   steamCommon: string;
 }
 
-const INITIAL_PATH_DEFAULTS: PathDefaults = {
-  home: DEFAULT_START_PATH,
-  steamCommon: DEFAULT_STEAM_LIBRARY_PATH,
+const DEFAULT_HOME = "/home";
+const DEFAULT_STEAM_COMMON = "/home/deck/.local/share/Steam/steamapps/common";
+
+const INITIAL_DEFAULTS: PathDefaults = {
+  home: DEFAULT_HOME,
+  steamCommon: DEFAULT_STEAM_COMMON,
 };
 
-const normalizePath = (path: string) => path.replace(/\\/g, "/");
+const normalizePath = (value: string) => value.replace(/\\/g, "/");
 
 const stripTrailingSlash = (value: string) =>
-  value.endsWith("/") ? value.slice(0, -1) : value;
-
-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 dirname = (path: string) => {
-  const normalized = normalizePath(path);
-  const parts = normalized.split("/");
-  parts.pop();
-  const dir = parts.join("/");
-  return dir.length > 0 ? dir : "/";
-};
+  value.length > 1 && value.endsWith("/") ? value.slice(0, -1) : value;
 
-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++;
+const ensureDirectory = (value: string) => {
+  const normalized = normalizePath(value);
+  const lastSegment = normalized.substring(normalized.lastIndexOf("/") + 1);
+  if (!lastSegment || !lastSegment.includes(".")) {
+    return stripTrailingSlash(normalized);
   }
-  return idx;
+  const parent = normalized.slice(0, normalized.lastIndexOf("/"));
+  return parent || "/";
 };
 
-interface ComputedOverride {
-  config: CustomOverrideConfig | null;
-  error: string | null;
+interface ManualPatchControlsProps {
+  isAvailable: boolean;
 }
 
-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.",
-    };
-  }
+interface PickerState {
+  selectedPath: string | null;
+  lastError: string | null;
+}
 
-  const searchSuffix = searchSuffixParts.join("/");
-  const replaceSuffix = replaceSuffixParts.join("/");
-  const pattern = defaultParts[prefixLength - 1] ?? defaultParts[defaultParts.length - 1];
+const INITIAL_PICKER_STATE: PickerState = {
+  selectedPath: null,
+  lastError: null,
+};
 
-  if (!pattern) {
-    return {
-      config: null,
-      error: "Unable to infer game identifier from path.",
-    };
+const formatResultMessage = (result: ApiResponse | null) => {
+  if (!result) return null;
+  if (result.status === "success") {
+    return result.message || result.output || "Operation completed successfully.";
   }
-
-  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 };
+  return result.message || result.output || "Operation failed.";
 };
 
-export const CustomPathOverride = ({ onOverrideChange }: CustomPathOverrideProps) => {
-  const [launcherPath, setLauncherPath] = useState<string | null>(null);
-  const [overridePath, setOverridePath] = useState<string | null>(null);
+export const ManualPatchControls = ({ isAvailable }: ManualPatchControlsProps) => {
   const [isEnabled, setEnabled] = useState(false);
-  const [pathDefaults, setPathDefaults] = useState<PathDefaults>(INITIAL_PATH_DEFAULTS);
+  const [defaults, setDefaults] = useState<PathDefaults>(INITIAL_DEFAULTS);
+  const [pickerState, setPickerState] = useState<PickerState>(INITIAL_PICKER_STATE);
+  const [isPatching, setIsPatching] = useState(false);
+  const [isUnpatching, setIsUnpatching] = useState(false);
+  const [operationResult, setOperationResult] = useState<ApiResponse | null>(null);
 
   useEffect(() => {
     let cancelled = false;
 
-    const fetchDefaults = async () => {
+    (async () => {
       try {
-        const result = await getPathDefaults();
-        if (!result) {
-          return;
-        }
+        const response = await getPathDefaults();
+        if (!response || cancelled) return;
 
-        const home = result.home ? normalizePath(result.home) : INITIAL_PATH_DEFAULTS.home;
-        const steamCommonSource = result.steam_common
-          ? normalizePath(result.steam_common)
+        const home = response.home ? normalizePath(response.home) : DEFAULT_HOME;
+        const steamCommon = response.steam_common
+          ? normalizePath(response.steam_common)
           : normalizePath(`${stripTrailingSlash(home)}/.local/share/Steam/steamapps/common`);
 
-        if (!cancelled) {
-          setPathDefaults({
-            home,
-            steamCommon: steamCommonSource || INITIAL_PATH_DEFAULTS.steamCommon,
-          });
-        }
+        setDefaults({
+          home,
+          steamCommon: steamCommon || DEFAULT_STEAM_COMMON,
+        });
       } catch (err) {
-        console.error("CustomPathOverride -> getPathDefaults", err);
+        console.error("ManualPatchControls -> getPathDefaults", err);
       }
-    };
-
-    fetchDefaults();
+    })();
 
     return () => {
       cancelled = true;
     };
   }, []);
 
-  const { config, error } = useMemo(
-    () => buildOverride(launcherPath, overridePath),
-    [launcherPath, overridePath]
-  );
-
   useEffect(() => {
-    if (isEnabled && config) {
-      onOverrideChange(config);
-    } else {
-      onOverrideChange(null);
+    if (!isAvailable) {
+      setEnabled(false);
+      setPickerState(INITIAL_PICKER_STATE);
+      setOperationResult(null);
     }
-  }, [config, isEnabled, onOverrideChange]);
+  }, [isAvailable]);
 
-  interface PickerArgs {
-    existing: string | null;
-    setter: (value: string) => void;
-    fallbackStart?: string | null;
-  }
+  const canInteract = isAvailable && isEnabled;
+  const selectedPath = pickerState.selectedPath;
+  const statusMessage = useMemo(() => formatResultMessage(operationResult), [operationResult]);
+  const wasSuccessful = operationResult?.status === "success";
 
-  const openPicker = useCallback(
-    async ({ existing, setter, fallbackStart }: PickerArgs) => {
-      const candidates = new Set<string>();
+  const openDirectoryPicker = useCallback(async () => {
+    const candidates = [
+      selectedPath,
+      defaults.steamCommon,
+      defaults.home,
+    ];
 
-      if (existing) {
-        candidates.add(normalizePath(existing));
-      } else {
-        if (fallbackStart) {
-          candidates.add(normalizePath(fallbackStart));
-        }
-        candidates.add(pathDefaults.steamCommon);
-        candidates.add(pathDefaults.home);
-      }
+    let lastError: string | null = null;
 
-      let lastError: unknown = null;
+    for (const candidate of candidates) {
+      if (!candidate) continue;
 
-      for (const candidate of candidates) {
-        if (!candidate) {
-          continue;
-        }
+      const startPath = ensureDirectory(candidate);
 
-        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;
+      try {
+        const result = await openFilePicker(
+          FileSelectionType.FOLDER,
+          startPath,
+          true,
+          true,
+          undefined,
+          undefined,
+          true
+        );
+
+        if (result?.path) {
+          setPickerState({ selectedPath: normalizePath(result.path), lastError: null });
+          setOperationResult(null);
+          return;
         }
+      } catch (err) {
+        console.error("ManualPatchControls -> openDirectoryPicker", err);
+        lastError = err instanceof Error ? err.message : String(err);
       }
+    }
 
-      if (lastError) {
-        console.error("CustomPathOverride -> openPicker", lastError);
+    setPickerState((prev) => ({ ...prev, lastError }));
+  }, [defaults.home, defaults.steamCommon, selectedPath]);
+
+  const runOperation = useCallback(
+    async (action: "patch" | "unpatch") => {
+      if (!selectedPath) return;
+
+      const setBusy = action === "patch" ? setIsPatching : setIsUnpatching;
+      setBusy(true);
+      setOperationResult(null);
+
+      try {
+        const response =
+          action === "patch"
+            ? await runManualPatch(selectedPath)
+            : await runManualUnpatch(selectedPath);
+        setOperationResult(response ?? { status: "error", message: "No response from backend." });
+      } catch (err) {
+        setOperationResult({
+          status: "error",
+          message: err instanceof Error ? err.message : String(err),
+        });
+      } finally {
+        setBusy(false);
       }
     },
-    [pathDefaults]
+    [selectedPath]
   );
 
   const handleToggle = (value: boolean) => {
+    if (!isAvailable) {
+      setEnabled(false);
+      return;
+    }
+
     setEnabled(value);
     if (!value) {
-      setLauncherPath(null);
-      setOverridePath(null);
-      onOverrideChange(null);
-    } else if (config) {
-      onOverrideChange(config);
+      setPickerState(INITIAL_PICKER_STATE);
+      setOperationResult(null);
     }
   };
 
+  const busy = isPatching || isUnpatching;
+
   return (
     <>
       <PanelSectionRow>
         <ToggleField
-          label="Custom Launcher Override"
-          description="Select launcher and target executables from the Deck's file browser."
-          checked={isEnabled}
+          label="Manual Patch Controls"
+          description={
+            isAvailable
+              ? "Manually apply OptiScaler to a specific game directory."
+              : "Install OptiScaler first to enable manual patching."
+          }
+          checked={isEnabled && isAvailable}
+          disabled={!isAvailable}
           onChange={handleToggle}
         />
       </PanelSectionRow>
 
-      {isEnabled && (
+      {canInteract && (
         <>
           <PanelSectionRow>
             <ButtonItem
               layout="below"
-              onClick={() =>
-                openPicker({
-                  existing: launcherPath,
-                  setter: setLauncherPath,
-                  fallbackStart: pathDefaults.steamCommon,
-                })
-              }
-              description={launcherPath || "Pick the EXE Steam currently uses."}
-            >
-              Select Steam-provided EXE
-            </ButtonItem>
-          </PanelSectionRow>
-
-          <PanelSectionRow>
-            <ButtonItem
-              layout="below"
-              disabled={!launcherPath}
-              onClick={() =>
-                launcherPath &&
-                openPicker({
-                  existing: overridePath,
-                  setter: setOverridePath,
-                  fallbackStart: launcherPath ? dirname(launcherPath) : pathDefaults.steamCommon,
-                })
-              }
+              onClick={openDirectoryPicker}
               description={
-                launcherPath
-                  ? overridePath || "Pick the executable that should run instead."
-                  : "Select the Steam-provided executable first."
+                selectedPath || "Choose the game's installation directory (where the EXE lives)."
               }
             >
-              Select Override EXE
+              Select directory
             </ButtonItem>
           </PanelSectionRow>
 
-          {(launcherPath || overridePath) && (
-            <PanelSectionRow>
-              <ButtonItem
-                layout="below"
-                onClick={() => {
-                  setLauncherPath(null);
-                  setOverridePath(null);
-                }}
-              >
-                Clear selections
-              </ButtonItem>
-            </PanelSectionRow>
-          )}
-
-          {error && (
+          {pickerState.lastError && (
             <PanelSectionRow>
               <Field
-                label="Override status"
-                description={error}
+                label="Picker error"
+                description={pickerState.lastError}
               >
                 ⚠️
               </Field>
             </PanelSectionRow>
           )}
 
-          {!error && config && (
+          {selectedPath && (
             <>
               <PanelSectionRow>
                 <Field
-                  label="Detected game"
-                  description="Based on the shared folder between both selections."
+                  label="Target directory"
+                  description="OptiScaler files will be copied here."
                 >
-                  {config.pattern}
+                  {selectedPath}
                 </Field>
               </PanelSectionRow>
 
               <PanelSectionRow>
-                <Field
-                  label="Code snippet"
-                  description="This is added to fgmod before resolving the install path."
-                  bottomSeparator="none"
+                <ButtonItem
+                  layout="below"
+                  disabled={busy}
+                  onClick={() => runOperation("patch")}
                 >
-                  <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>
+                  {isPatching ? "Patching..." : "Patch directory"}
+                </ButtonItem>
               </PanelSectionRow>
 
               <PanelSectionRow>
-                <Field
-                  label="Environment preview"
-                  description="Automatically appended to the Patch command."
+                <ButtonItem
+                  layout="below"
+                  disabled={busy}
+                  onClick={() => runOperation("unpatch")}
                 >
-                  {config.envAssignment}
-                </Field>
+                  {isUnpatching ? "Reverting..." : "Unpatch directory"}
+                </ButtonItem>
               </PanelSectionRow>
             </>
           )}
+
+          {operationResult && (
+            <PanelSectionRow>
+              <Field
+                label={wasSuccessful ? "Last action succeeded" : "Last action failed"}
+                description={wasSuccessful ? undefined : operationResult.output?.slice(0, 200)}
+              >
+                {statusMessage}
+              </Field>
+            </PanelSectionRow>
+          )}
         </>
       )}
     </>
-- 
cgit v1.2.3


From c22c11714150d53701016efe2daeb5cd02ae02a3 Mon Sep 17 00:00:00 2001
From: xXJSONDeruloXx <danielhimebauch@gmail.com>
Date: Fri, 26 Sep 2025 20:38:49 -0400
Subject: ui cleanup

---
 src/components/CustomPathOverride.tsx | 36 ++++++++++++++++++++++++++++-------
 1 file changed, 29 insertions(+), 7 deletions(-)

(limited to 'src/components/CustomPathOverride.tsx')

diff --git a/src/components/CustomPathOverride.tsx b/src/components/CustomPathOverride.tsx
index b28a8de..26e5c8f 100644
--- a/src/components/CustomPathOverride.tsx
+++ b/src/components/CustomPathOverride.tsx
@@ -61,6 +61,7 @@ export const ManualPatchControls = ({ isAvailable }: ManualPatchControlsProps) =
   const [isPatching, setIsPatching] = useState(false);
   const [isUnpatching, setIsUnpatching] = useState(false);
   const [operationResult, setOperationResult] = useState<ApiResponse | null>(null);
+  const [lastOperation, setLastOperation] = useState<"patch" | "unpatch" | null>(null);
 
   useEffect(() => {
     let cancelled = false;
@@ -94,6 +95,7 @@ export const ManualPatchControls = ({ isAvailable }: ManualPatchControlsProps) =
       setEnabled(false);
       setPickerState(INITIAL_PICKER_STATE);
       setOperationResult(null);
+      setLastOperation(null);
     }
   }, [isAvailable]);
 
@@ -101,6 +103,13 @@ export const ManualPatchControls = ({ isAvailable }: ManualPatchControlsProps) =
   const selectedPath = pickerState.selectedPath;
   const statusMessage = useMemo(() => formatResultMessage(operationResult), [operationResult]);
   const wasSuccessful = operationResult?.status === "success";
+  const statusLabel = useMemo(() => {
+    if (!operationResult || !lastOperation) return null;
+    if (operationResult.status === "success") {
+      return lastOperation === "patch" ? "Game patched" : "Game unpatched";
+    }
+    return lastOperation === "patch" ? "Patch failed" : "Unpatch failed";
+  }, [lastOperation, operationResult]);
 
   const openDirectoryPicker = useCallback(async () => {
     const candidates = [
@@ -146,6 +155,7 @@ export const ManualPatchControls = ({ isAvailable }: ManualPatchControlsProps) =
       if (!selectedPath) return;
 
       const setBusy = action === "patch" ? setIsPatching : setIsUnpatching;
+  setLastOperation(action);
       setBusy(true);
       setOperationResult(null);
 
@@ -177,6 +187,7 @@ export const ManualPatchControls = ({ isAvailable }: ManualPatchControlsProps) =
     if (!value) {
       setPickerState(INITIAL_PICKER_STATE);
       setOperationResult(null);
+      setLastOperation(null);
     }
   };
 
@@ -204,9 +215,7 @@ export const ManualPatchControls = ({ isAvailable }: ManualPatchControlsProps) =
             <ButtonItem
               layout="below"
               onClick={openDirectoryPicker}
-              description={
-                selectedPath || "Choose the game's installation directory (where the EXE lives)."
-              }
+              description="Choose the game's installation directory (where the EXE lives)."
             >
               Select directory
             </ButtonItem>
@@ -230,7 +239,21 @@ export const ManualPatchControls = ({ isAvailable }: ManualPatchControlsProps) =
                   label="Target directory"
                   description="OptiScaler files will be copied here."
                 >
-                  {selectedPath}
+                  <div
+                    style={{
+                      fontFamily: "monospace",
+                      backgroundColor: "rgba(255, 255, 255, 0.05)",
+                      border: "1px solid rgba(255, 255, 255, 0.1)",
+                      borderRadius: "4px",
+                      padding: "6px 8px",
+                      width: "100%",
+                      boxSizing: "border-box",
+                      whiteSpace: "pre-wrap",
+                      wordBreak: "break-word",
+                    }}
+                  >
+                    {selectedPath}
+                  </div>
                 </Field>
               </PanelSectionRow>
 
@@ -259,10 +282,9 @@ export const ManualPatchControls = ({ isAvailable }: ManualPatchControlsProps) =
           {operationResult && (
             <PanelSectionRow>
               <Field
-                label={wasSuccessful ? "Last action succeeded" : "Last action failed"}
-                description={wasSuccessful ? undefined : operationResult.output?.slice(0, 200)}
+                label={statusLabel ?? (wasSuccessful ? "Last action succeeded" : "Last action failed")}
               >
-                {statusMessage}
+                {!wasSuccessful && statusMessage ? statusMessage : null}
               </Field>
             </PanelSectionRow>
           )}
-- 
cgit v1.2.3


From 1ff60fe98f2adeb4f19a5db5ae8b10ebc77a2749 Mon Sep 17 00:00:00 2001
From: xXJSONDeruloXx <danielhimebauch@gmail.com>
Date: Fri, 26 Sep 2025 20:59:29 -0400
Subject: ui cleanup and hide on mode change to manual

---
 src/components/CustomPathOverride.tsx | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

(limited to 'src/components/CustomPathOverride.tsx')

diff --git a/src/components/CustomPathOverride.tsx b/src/components/CustomPathOverride.tsx
index 26e5c8f..efae9de 100644
--- a/src/components/CustomPathOverride.tsx
+++ b/src/components/CustomPathOverride.tsx
@@ -3,6 +3,7 @@ import { ButtonItem, Field, PanelSectionRow, ToggleField } from "@decky/ui";
 import { FileSelectionType, openFilePicker } from "@decky/api";
 import { getPathDefaults, runManualPatch, runManualUnpatch } from "../api";
 import type { ApiResponse } from "../types/index";
+import { SmartClipboardButton } from "./SmartClipboardButton";
 
 interface PathDefaults {
   home: string;
@@ -34,6 +35,7 @@ const ensureDirectory = (value: string) => {
 
 interface ManualPatchControlsProps {
   isAvailable: boolean;
+  onManualModeChange?: (enabled: boolean) => void;
 }
 
 interface PickerState {
@@ -54,7 +56,7 @@ const formatResultMessage = (result: ApiResponse | null) => {
   return result.message || result.output || "Operation failed.";
 };
 
-export const ManualPatchControls = ({ isAvailable }: ManualPatchControlsProps) => {
+export const ManualPatchControls = ({ isAvailable, onManualModeChange }: ManualPatchControlsProps) => {
   const [isEnabled, setEnabled] = useState(false);
   const [defaults, setDefaults] = useState<PathDefaults>(INITIAL_DEFAULTS);
   const [pickerState, setPickerState] = useState<PickerState>(INITIAL_PICKER_STATE);
@@ -96,8 +98,9 @@ export const ManualPatchControls = ({ isAvailable }: ManualPatchControlsProps) =
       setPickerState(INITIAL_PICKER_STATE);
       setOperationResult(null);
       setLastOperation(null);
+      onManualModeChange?.(false);
     }
-  }, [isAvailable]);
+  }, [isAvailable, onManualModeChange]);
 
   const canInteract = isAvailable && isEnabled;
   const selectedPath = pickerState.selectedPath;
@@ -184,6 +187,7 @@ export const ManualPatchControls = ({ isAvailable }: ManualPatchControlsProps) =
     }
 
     setEnabled(value);
+    onManualModeChange?.(value);
     if (!value) {
       setPickerState(INITIAL_PICKER_STATE);
       setOperationResult(null);
@@ -211,6 +215,10 @@ export const ManualPatchControls = ({ isAvailable }: ManualPatchControlsProps) =
 
       {canInteract && (
         <>
+          <SmartClipboardButton
+            command='WINEDLLOVERRIDES="dxgi=n,b" SteamDeck=0 %command%'
+            buttonText="Manual launch cmd"
+          />
           <PanelSectionRow>
             <ButtonItem
               layout="below"
-- 
cgit v1.2.3


From cf29301fc9270a574549ead06982431551fe43ac Mon Sep 17 00:00:00 2001
From: xXJSONDeruloXx <danielhimebauch@gmail.com>
Date: Fri, 26 Sep 2025 21:44:17 -0400
Subject: desc tweak

---
 src/components/CustomPathOverride.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/components/CustomPathOverride.tsx')

diff --git a/src/components/CustomPathOverride.tsx b/src/components/CustomPathOverride.tsx
index efae9de..ffc4b1f 100644
--- a/src/components/CustomPathOverride.tsx
+++ b/src/components/CustomPathOverride.tsx
@@ -201,7 +201,7 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange }: ManualP
     <>
       <PanelSectionRow>
         <ToggleField
-          label="Manual Patch Controls"
+          label="Advanced Mode"
           description={
             isAvailable
               ? "Manually apply OptiScaler to a specific game directory."
-- 
cgit v1.2.3

