summaryrefslogtreecommitdiff
path: root/src/utils
diff options
context:
space:
mode:
Diffstat (limited to 'src/utils')
-rw-r--r--src/utils/steamLaunchOptionParser.ts501
-rw-r--r--src/utils/steamLaunchOptions.ts512
-rw-r--r--src/utils/toastUtils.ts79
3 files changed, 409 insertions, 683 deletions
diff --git a/src/utils/steamLaunchOptionParser.ts b/src/utils/steamLaunchOptionParser.ts
deleted file mode 100644
index 5b8156c..0000000
--- a/src/utils/steamLaunchOptionParser.ts
+++ /dev/null
@@ -1,501 +0,0 @@
-export interface WorkaroundState {
- dxvkFrameRate: number;
- disableGamescopeWsi: boolean;
- disableSteamdeckMode: boolean;
- disableVkbasalt: boolean;
- enableZink: boolean;
-}
-
-export type WorkaroundField = keyof WorkaroundState;
-
-export interface ParsedWorkaroundOptions {
- state: WorkaroundState;
- issues: string[];
-}
-
-interface LaunchToken {
- raw: string;
- value: string;
-}
-
-interface EnvironmentEntry {
- value: string;
- count: number;
-}
-
-type BooleanWorkaroundField = Exclude<WorkaroundField, "dxvkFrameRate">;
-type EnvironmentSpec = readonly [key: string, value: string];
-type DxvkFrameRateKey = "dxvk.maxFrameRate" | "dxgi.maxFrameRate" | "d3d9.maxFrameRate";
-
-interface WorkaroundDefinition {
- spec: EnvironmentSpec;
- clear: readonly string[];
- label?: string;
-}
-
-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",
- "d3d9.maxFrameRate",
-];
-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"],
- },
- disableSteamdeckMode: {
- spec: ["SteamDeck", "0"],
- clear: ["SteamDeck"],
- label: "Steam Deck mode",
- },
- disableVkbasalt: {
- spec: ["DISABLE_VKBASALT", "1"],
- clear: ["DISABLE_VKBASALT"],
- label: "Disable vkBasalt",
- },
- enableZink: {
- spec: ["MESA_LOADER_DRIVER_OVERRIDE", "zink"],
- clear: ["__GLX_VENDOR_LIBRARY_NAME", "MESA_LOADER_DRIVER_OVERRIDE", "GALLIUM_DRIVER"],
- },
-} as const satisfies Record<BooleanWorkaroundField, WorkaroundDefinition>;
-const BOOLEAN_WORKAROUND_FIELDS: readonly BooleanWorkaroundField[] = [
- "disableGamescopeWsi",
- "disableSteamdeckMode",
- "disableVkbasalt",
- "enableZink",
-];
-const WSI_DISABLE_KEY = "DISABLE_GAMESCOPE_WSI";
-const WSI_ENABLE_KEY = "ENABLE_GAMESCOPE_WSI";
-const MANAGED_ENV_KEYS = new Set([
- ...DXVK_MANAGED_KEYS,
- ...BOOLEAN_WORKAROUND_FIELDS.flatMap((field) => WORKAROUND_DEFINITIONS[field].clear),
-]);
-
-const DEFAULT_WORKAROUND_STATE: WorkaroundState = {
- dxvkFrameRate: 0,
- disableGamescopeWsi: false,
- disableSteamdeckMode: false,
- disableVkbasalt: false,
- enableZink: false,
-};
-
-export function getDefaultWorkaroundState(): WorkaroundState {
- return { ...DEFAULT_WORKAROUND_STATE };
-}
-
-function decodeToken(raw: string): string {
- let value = "";
- let quote: "'" | '"' | null = null;
-
- for (let index = 0; index < raw.length; index += 1) {
- const character = raw[index];
- if (character === "\\" && quote !== "'" && index + 1 < raw.length) {
- value += raw[index + 1];
- index += 1;
- } else if (quote !== null) {
- if (character === quote) quote = null;
- else value += character;
- } else if (character === "'" || character === '"') {
- quote = character;
- } else {
- value += character;
- }
- }
-
- 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;
- let quote: "'" | '"' | null = null;
- let escaped = false;
-
- for (let index = 0; index < options.length; index += 1) {
- const character = options[index];
- if (start < 0) {
- if (/\s/.test(character)) continue;
- start = index;
- }
-
- if (escaped) {
- escaped = false;
- } else if (character === "\\" && quote !== "'") {
- escaped = true;
- } else if (quote !== null) {
- if (character === quote) quote = null;
- } else if (character === "'" || character === '"') {
- quote = character;
- } else if (/\s/.test(character)) {
- const raw = options.slice(start, index);
- tokens.push({ raw, value: decodeToken(raw) });
- start = -1;
- }
- }
-
- if (start >= 0) {
- const raw = options.slice(start);
- tokens.push({ raw, value: decodeToken(raw) });
- }
- return tokens;
-}
-
-function serialize(tokens: readonly LaunchToken[]): string {
- if (tokens.length === 1 && tokens[0].raw.toLowerCase() === COMMAND_TOKEN) return "";
- return tokens.map((token) => token.raw).join(" ");
-}
-
-export function normalizeLaunchOptions(options: string): string {
- return serialize(tokenize(options));
-}
-
-function parseEnvironmentToken(token: LaunchToken): [string, string] | null {
- const separator = token.value.indexOf("=");
- if (separator < 1) return null;
- const key = token.value.slice(0, separator);
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) return null;
- return [key, token.value.slice(separator + 1)];
-}
-
-function findCommandIndex(tokens: readonly LaunchToken[]): number {
- return tokens.findIndex((token) => token.raw.toLowerCase() === COMMAND_TOKEN);
-}
-
-function leadingEnvironmentCount(tokens: readonly LaunchToken[]): number {
- let count = 0;
- while (count < tokens.length && parseEnvironmentToken(tokens[count]) !== null) count += 1;
- return count;
-}
-
-function effectivePrefixLimit(tokens: readonly LaunchToken[]): number {
- return leadingEnvironmentCount(tokens);
-}
-
-function effectiveEnvironmentEntries(tokens: readonly LaunchToken[]): Map<string, EnvironmentEntry> {
- const entries = new Map<string, EnvironmentEntry>();
- for (let index = 0; index < effectivePrefixLimit(tokens); index += 1) {
- const parsed = parseEnvironmentToken(tokens[index]);
- if (!parsed) continue;
- const [key, value] = parsed;
- const previous = entries.get(key);
- entries.set(key, { value, count: (previous?.count || 0) + 1 });
- }
- return entries;
-}
-
-function removePrefixAssignments(tokens: LaunchToken[], predicate: (token: LaunchToken) => boolean): boolean {
- const limit = effectivePrefixLimit(tokens);
- const retained = tokens.filter((token, index) => index >= limit || !predicate(token));
- if (retained.length === tokens.length) return false;
- tokens.splice(0, tokens.length, ...retained);
- return true;
-}
-
-function removeAllAssignments(tokens: LaunchToken[], keys: ReadonlySet<string>): boolean {
- return removePrefixAssignments(tokens, (token) => {
- const parsed = parseEnvironmentToken(token);
- return parsed !== null && keys.has(parsed[0]);
- });
-}
-
-function encodeEnvironmentValue(value: string): string {
- if (/^[A-Za-z0-9_./:+,%=-]+$/.test(value)) return value;
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
-}
-
-function insertEnvironmentSpecs(tokens: LaunchToken[], specs: readonly EnvironmentSpec[]): void {
- tokens.unshift(...specs.map(([key, value]) => ({
- raw: `${key}=${encodeEnvironmentValue(value)}`,
- value: `${key}=${value}`,
- })));
-}
-
-function ensureCommandToken(tokens: LaunchToken[]): void {
- if (findCommandIndex(tokens) >= 0) return;
- tokens.splice(leadingEnvironmentCount(tokens), 0, { raw: COMMAND_TOKEN, value: COMMAND_TOKEN });
-}
-
-export function isLegacyWrapperToken(value: string): boolean {
- const path = decodeToken(value);
- return LEGACY_WRAPPER_TOKENS.has(path);
-}
-
-function removeLegacyWrapperFromTokens(tokens: LaunchToken[]): boolean {
- const commandIndex = findCommandIndex(tokens);
- const prefixEnd = commandIndex >= 0 ? commandIndex : tokens.length;
- 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;
-}
-
-interface DxvkConfigAssignment {
- values: string[];
- malformed: number;
-}
-
-interface ParsedDxvkConfig {
- segments: string[];
- assignments: Map<DxvkFrameRateKey, DxvkConfigAssignment>;
-}
-
-function splitDxvkConfig(value: string): string[] {
- const segments: string[] = [];
- let start = 0;
- let quote: "'" | '"' | null = null;
- let escaped = false;
-
- for (let index = 0; index < value.length; index += 1) {
- const character = value[index];
- if (escaped) escaped = false;
- else if (character === "\\" && quote !== "'") escaped = true;
- else if (quote !== null) {
- if (character === quote) quote = null;
- } else if (character === "'" || character === '"') quote = character;
- else if (character === ";") {
- segments.push(value.slice(start, index));
- start = index + 1;
- }
- }
-
- segments.push(value.slice(start));
- return segments;
-}
-
-function knownDxvkKey(value: string): DxvkFrameRateKey | null {
- const key = value.match(/^([A-Za-z][A-Za-z0-9.]*)/)?.[1];
- return key && DXVK_FRAME_RATE_KEYS.includes(key as DxvkFrameRateKey)
- ? key as DxvkFrameRateKey
- : null;
-}
-
-function parseDxvkConfig(value: string): ParsedDxvkConfig {
- const segments = splitDxvkConfig(value);
- const assignments = new Map<DxvkFrameRateKey, DxvkConfigAssignment>();
- for (const segment of segments) {
- const trimmed = segment.trim();
- const key = knownDxvkKey(trimmed);
- if (!key) continue;
- const match = trimmed.match(/^[A-Za-z][A-Za-z0-9.]*\s*=\s*(.*?)\s*$/);
- const entry = assignments.get(key) || { values: [], malformed: 0 };
- if (match) entry.values.push(match[1]);
- else entry.malformed += 1;
- assignments.set(key, entry);
- }
- return { segments, assignments };
-}
-
-function isDxvkFrameRateSegment(segment: string): boolean {
- return knownDxvkKey(segment.trim()) !== null;
-}
-
-function parseSupportedFrameRate(value: string): number | null {
- if (!/^\d+$/.test(value)) return null;
- const numericValue = Number(value);
- return Number.isSafeInteger(numericValue) && numericValue <= 60 ? numericValue : null;
-}
-
-function rewriteDxvkFrameRate(tokens: LaunchToken[], frameRate: number): void {
- const config = effectiveEnvironmentEntries(tokens).get("DXVK_CONFIG");
- const parsed = parseDxvkConfig(config?.value || "");
- const retained = parsed.segments
- .filter((segment) => !isDxvkFrameRateSegment(segment))
- .filter((segment) => segment.trim().length > 0)
- .join(";");
- const nextConfig = frameRate > 0
- ? [`dxvk.maxFrameRate = ${frameRate}`, ...(retained ? [retained] : [])].join(";")
- : retained;
-
- removeAllAssignments(tokens, DXVK_MANAGED_KEYS);
- if (nextConfig) {
- ensureCommandToken(tokens);
- insertEnvironmentSpecs(tokens, [["DXVK_CONFIG", nextConfig]]);
- }
-}
-
-function environmentSpecsForState(state: WorkaroundState): EnvironmentSpec[] {
- return BOOLEAN_WORKAROUND_FIELDS
- .filter((field) => state[field])
- .map((field) => WORKAROUND_DEFINITIONS[field].spec);
-}
-
-function validateFrameRate(frameRate: number): void {
- if (!Number.isInteger(frameRate) || frameRate < 0 || frameRate > 60) {
- throw new Error("Base FPS Cap must be an integer from 0 to 60");
- }
-}
-
-function readBoolean(
- entries: Map<string, EnvironmentEntry>,
- key: string,
- label: string,
- trueValue: string,
- issues: string[],
-): boolean {
- const entry = entries.get(key);
- if (!entry) return false;
- const falseValue = trueValue === "1" ? "0" : "1";
- if (entry.value === trueValue) return true;
- if (entry.value === falseValue) return false;
- issues.push(`${label} has an unsupported value.`);
- return false;
-}
-
-export function parseWorkaroundOptions(options: string): ParsedWorkaroundOptions {
- const tokens = tokenize(options);
- const entries = effectiveEnvironmentEntries(tokens);
- const state = getDefaultWorkaroundState();
- const issues: string[] = [];
-
- for (const [key, entry] of entries) {
- if (MANAGED_ENV_KEYS.has(key) && entry.count > 1) {
- issues.push(`${key} appears more than once; Steam uses the last value.`);
- }
- }
-
- const dxvkConfig = parseDxvkConfig(entries.get("DXVK_CONFIG")?.value || "");
- const effectiveDxvkValues = new Map<DxvkFrameRateKey, number | null>();
- for (const key of DXVK_FRAME_RATE_KEYS) {
- const assignment = dxvkConfig.assignments.get(key);
- if (!assignment) continue;
- if (assignment.malformed > 0) issues.push(`${key} in DXVK_CONFIG is malformed.`);
- if (assignment.values.length > 1) {
- issues.push(`${key} appears more than once in DXVK_CONFIG; DXVK uses the last value.`);
- }
- if (assignment.values.length === 0) continue;
- const value = parseSupportedFrameRate(assignment.values[assignment.values.length - 1]);
- effectiveDxvkValues.set(key, value);
- if (value === null) issues.push(`${key} in DXVK_CONFIG is outside the supported 0-60 range.`);
- }
-
- const unifiedFrameRate = effectiveDxvkValues.get("dxvk.maxFrameRate");
- const dxgiFrameRate = effectiveDxvkValues.get("dxgi.maxFrameRate");
- const d3d9FrameRate = effectiveDxvkValues.get("d3d9.maxFrameRate");
- if (unifiedFrameRate !== undefined) {
- if (unifiedFrameRate !== null) state.dxvkFrameRate = unifiedFrameRate;
- } else if (dxgiFrameRate !== undefined && d3d9FrameRate !== undefined) {
- if (dxgiFrameRate !== null && dxgiFrameRate === d3d9FrameRate) state.dxvkFrameRate = dxgiFrameRate;
- else issues.push("DXVK_CONFIG has conflicting or invalid DirectX frame caps.");
- } else if (dxgiFrameRate !== undefined || d3d9FrameRate !== undefined) {
- const partial = dxgiFrameRate ?? d3d9FrameRate;
- if (partial !== null && partial !== undefined) state.dxvkFrameRate = partial;
- issues.push("DXVK_CONFIG only caps one DirectX API; adjust the cap to normalize it.");
- }
-
- if (entries.has("DXVK_FRAME_RATE")) {
- issues.push("DXVK_FRAME_RATE is obsolete on current DXVK; adjust the cap to migrate it.");
- }
-
- const wsiSignals: boolean[] = [];
- const wsiDisable = entries.get(WSI_DISABLE_KEY);
- if (wsiDisable) {
- if (wsiDisable.value !== "0" && wsiDisable.value !== "1") issues.push("Disable Gamescope WSI has an unsupported value.");
- else wsiSignals.push(wsiDisable.value === "1");
- }
- const wsiEnable = entries.get(WSI_ENABLE_KEY);
- if (wsiEnable) {
- if (wsiEnable.value !== "0" && wsiEnable.value !== "1") issues.push("Enable Gamescope WSI has an unsupported value.");
- else wsiSignals.push(wsiEnable.value === "0");
- }
- if (wsiSignals.length > 0) {
- if (wsiSignals.length === 2 && wsiSignals[0] !== wsiSignals[1]) {
- issues.push("Gamescope WSI has conflicting enable and disable assignments.");
- }
- state.disableGamescopeWsi = wsiSignals.some(Boolean);
- }
-
- for (const field of ["disableSteamdeckMode", "disableVkbasalt"] as const) {
- const { spec, label } = WORKAROUND_DEFINITIONS[field];
- state[field] = readBoolean(entries, spec[0], label || field, spec[1], issues);
- }
-
- const vkBasaltEnable = entries.get("ENABLE_VKBASALT");
- const vkBasaltDisable = entries.get("DISABLE_VKBASALT");
- if (vkBasaltEnable?.value === "1" && vkBasaltDisable?.value === "1") {
- issues.push("vkBasalt has conflicting enable and disable assignments.");
- }
-
- const zink = entries.get(WORKAROUND_DEFINITIONS.enableZink.spec[0]);
- const glxVendor = entries.get("__GLX_VENDOR_LIBRARY_NAME");
- const galliumDriver = entries.get("GALLIUM_DRIVER");
- const hasLegacyZink = glxVendor !== undefined || galliumDriver !== undefined;
- if (zink || hasLegacyZink) {
- state.enableZink = zink?.value === WORKAROUND_DEFINITIONS.enableZink.spec[1];
- if (hasLegacyZink && (
- glxVendor?.value !== "mesa" ||
- zink?.value !== WORKAROUND_DEFINITIONS.enableZink.spec[1] ||
- galliumDriver?.value !== "zink"
- )) {
- issues.push("Zink workaround is only partially configured.");
- } else if (!state.enableZink) {
- issues.push("Zink workaround has an unsupported driver value.");
- }
- }
-
- return { state, issues };
-}
-
-export function applyWorkaroundState(options: string, state: WorkaroundState): string {
- validateFrameRate(state.dxvkFrameRate);
- const tokens = tokenize(options);
- removeLegacyWrapperFromTokens(tokens);
- rewriteDxvkFrameRate(tokens, state.dxvkFrameRate);
- const keysToClear = new Set<string>(
- BOOLEAN_WORKAROUND_FIELDS.flatMap((field) => WORKAROUND_DEFINITIONS[field].clear),
- );
- if (state.disableVkbasalt) keysToClear.add("ENABLE_VKBASALT");
- removeAllAssignments(tokens, keysToClear);
- const specs = environmentSpecsForState(state);
- if (specs.length > 0) {
- ensureCommandToken(tokens);
- insertEnvironmentSpecs(tokens, specs);
- }
- return serialize(tokens);
-}
-
-export function applyWorkaroundChange(options: string, field: WorkaroundField, value: boolean | number): string {
- const tokens = tokenize(options);
- removeLegacyWrapperFromTokens(tokens);
-
- if (field === "dxvkFrameRate") {
- if (typeof value !== "number") throw new Error("Base FPS Cap must be an integer from 0 to 60");
- validateFrameRate(value);
- rewriteDxvkFrameRate(tokens, value);
- return serialize(tokens);
- }
-
- if (typeof value !== "boolean") throw new Error(`${field} must be a boolean`);
- const definition = WORKAROUND_DEFINITIONS[field];
- const keysToClear = new Set<string>(definition.clear);
- if (value && field === "disableVkbasalt") keysToClear.add("ENABLE_VKBASALT");
- removeAllAssignments(tokens, keysToClear);
- if (value) {
- ensureCommandToken(tokens);
- insertEnvironmentSpecs(tokens, [definition.spec]);
- }
- return serialize(tokens);
-}
-
-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 39125bd..959ce9e 100644
--- a/src/utils/steamLaunchOptions.ts
+++ b/src/utils/steamLaunchOptions.ts
@@ -1,109 +1,104 @@
-// @ts-expect-error Node's built-in TypeScript loader requires explicit source extensions in tests.
-import { cleanupLegacyLaunchOptions, isLegacyWrapperToken, normalizeLaunchOptions } from "./steamLaunchOptionParser.ts";
+import type { TargetTransport } from "../api/lsfgApi";
-// @ts-expect-error Node's built-in TypeScript loader requires explicit source extensions in tests.
-export * from "./steamLaunchOptionParser.ts";
+const DEFAULT_WRAPPER_PATH = "~/.lsfg";
+const COMMAND_TOKEN = "%command%";
+export const LEGACY_WRAPPER_TOKENS = new Set([
+ "~/lsfg",
+ "~/.local/bin/lsfg",
+ "~/.local/bin/lsfg-vk-experimental",
+ "~/.local/bin/mako-run",
+ "mako-run",
+ "~/.local/bin/mako-launch",
+ "mako-launch",
+]);
+
+const LEGACY_ABSOLUTE_WRAPPER = /^\/(?:home|Users)\/[^/]+\/(?:lsfg|\.local\/bin\/(?:lsfg|lsfg-vk-experimental|mako-run|mako-launch))$/;
+const MANAGED_ENV_KEYS = new Set([
+ "ENABLE_GAMESCOPE_WSI", "DISABLE_GAMESCOPE_WSI", "DXVK_HDR", "SteamDeck",
+ "DISABLE_VKBASALT", "ENABLE_VKBASALT", "MESA_LOADER_DRIVER_OVERRIDE",
+ "__GLX_VENDOR_LIBRARY_NAME", "GALLIUM_DRIVER", "DXVK_FRAME_RATE",
+]);
+const DXVK_FRAME_RATE_SEGMENT = /^(?:dxvk\.maxFrameRate|dxgi\.maxFrameRate|d3d9\.maxFrameRate)\s*=/i;
+
+interface LaunchToken { raw: string; value: string; }
export interface SteamLaunchOptionsSnapshot {
appId: number;
nonSteam: boolean;
options: string;
+ target: string;
details: SteamAppDetails;
}
+export interface WrapperIntegrationResult {
+ snapshot: SteamLaunchOptionsSnapshot;
+ originalExecutable?: string;
+ commandTokenAdded: boolean;
+ changed: boolean;
+}
+
+function asError(error: unknown): Error {
+ return error instanceof Error ? error : new Error(String(error));
+}
+
+function apps(): Partial<SteamApps> | undefined {
+ return (globalThis as typeof globalThis & { SteamClient?: { Apps?: Partial<SteamApps> } }).SteamClient?.Apps;
+}
function validateAppId(appId: number): void {
if (!Number.isSafeInteger(appId) || appId <= 0) throw new Error("Invalid Steam App ID");
}
-function getSteamApps(): Partial<SteamApps> | undefined {
- return (globalThis as typeof globalThis & {
- SteamClient?: { Apps?: Partial<SteamApps> };
- }).SteamClient?.Apps;
+function timer() {
+ const host = typeof window !== "undefined" ? window : globalThis;
+ return {
+ set: (handler: () => void, ms: number) => host.setTimeout(handler, ms) as unknown as number,
+ clear: (id: number) => host.clearTimeout(id),
+ };
}
-function snapshotFromDetails(appId: number, nonSteam: boolean, details: SteamAppDetails): SteamLaunchOptionsSnapshot {
- if (nonSteam && isLegacyWrapperToken(details.strShortcutExe || "")) {
- throw new Error("The shortcut Target still points to a legacy frame-generation wrapper; restore its original executable first");
- }
+function snapshot(appId: number, nonSteam: boolean, details: SteamAppDetails): SteamLaunchOptionsSnapshot {
return {
appId,
nonSteam,
options: nonSteam ? details.strShortcutLaunchOptions || "" : details.strLaunchOptions || "",
+ target: nonSteam ? details.strShortcutExe || "" : "",
details,
};
}
-function asError(error: unknown): Error {
- return error instanceof Error ? error : new Error(String(error));
-}
-
-function registerSteamAppDetails(
- appId: number,
- onDetails: (details: SteamAppDetails) => boolean | void,
-): () => void {
+function registerDetails(appId: number, onDetails: (details: SteamAppDetails) => boolean | void): () => void {
validateAppId(appId);
- const apps = getSteamApps();
- const registerForAppDetails = apps?.RegisterForAppDetails;
- if (!registerForAppDetails) throw new Error("Steam launch options API is unavailable");
-
+ const register = apps()?.RegisterForAppDetails;
+ if (!register) throw new Error("Steam app-details API is unavailable");
let active = true;
- let unregisterPending = false;
let registration: SteamAppDetailsRegistration | undefined;
const unsubscribe = () => {
active = false;
- if (!registration) {
- unregisterPending = true;
- return;
- }
- try {
- registration.unregister();
- } catch {
- // Steam may invalidate registrations during a details refresh.
- }
+ try { registration?.unregister(); } catch {}
};
-
- try {
- registration = registerForAppDetails.call(apps, appId, (details) => {
- if (!active) return;
- if (onDetails(details || {}) === false && active) unsubscribe();
- });
- if (unregisterPending) {
- try {
- registration.unregister();
- } catch {
- // The registration can be invalidated before a synchronous callback returns.
- }
- }
- } catch (error) {
- throw asError(error);
- }
+ registration = register.call(apps(), appId, (details) => {
+ if (active && onDetails(details || {}) === false) unsubscribe();
+ });
+ if (!active) unsubscribe();
return unsubscribe;
}
export async function readSteamLaunchOptions(appId: number, nonSteam: boolean): Promise<SteamLaunchOptionsSnapshot> {
return new Promise((resolve, reject) => {
- let settled = false;
- let timeout = 0;
+ let done = false;
let unsubscribe = () => {};
+ const clock = timer();
+ const timeout = clock.set(() => finish(new Error("Timed out reading Steam app details")), 5000);
const finish = (error?: unknown, details?: SteamAppDetails) => {
- if (settled) return;
- settled = true;
- window.clearTimeout(timeout);
+ if (done) return;
+ done = true;
+ clock.clear(timeout);
unsubscribe();
- if (error) {
- reject(asError(error));
- return;
- }
- try {
- resolve(snapshotFromDetails(appId, nonSteam, details || {}));
- } catch (snapshotError) {
- reject(asError(snapshotError));
- }
+ if (error) reject(asError(error));
+ else resolve(snapshot(appId, nonSteam, details || {}));
};
-
- timeout = window.setTimeout(() => finish(new Error("Timed out reading Steam launch options")), 5000);
try {
- unsubscribe = registerSteamAppDetails(appId, (details) => {
+ unsubscribe = registerDetails(appId, (details) => {
finish(undefined, details);
return false;
});
@@ -116,71 +111,249 @@ export async function readSteamLaunchOptions(appId: number, nonSteam: boolean):
export function subscribeSteamLaunchOptions(
appId: number,
nonSteam: boolean,
- onSnapshot: (snapshot: SteamLaunchOptionsSnapshot) => void,
+ onSnapshot: (value: SteamLaunchOptionsSnapshot) => void,
onError: (error: Error) => void,
): () => void {
- return registerSteamAppDetails(appId, (details) => {
- try {
- onSnapshot(snapshotFromDetails(appId, nonSteam, details));
- } catch (error) {
- onError(asError(error));
- }
+ return registerDetails(appId, (details) => {
+ try { onSnapshot(snapshot(appId, nonSteam, details)); }
+ catch (error) { onError(asError(error)); }
});
}
-async function setSteamLaunchOptions(appId: number, nonSteam: boolean, options: string): Promise<void> {
- const apps = getSteamApps();
- const setter = nonSteam ? apps?.SetShortcutLaunchOptions : apps?.SetAppLaunchOptions;
- if (!setter) throw new Error(`Steam ${nonSteam ? "shortcut " : ""}launch options API is unavailable`);
- await Promise.resolve(setter.call(apps, appId, options));
+function decodeToken(raw: string): string {
+ let value = "";
+ let quote: "'" | '"' | null = null;
+ for (let i = 0; i < raw.length; i++) {
+ const c = raw[i];
+ if (c === "\\" && quote !== "'" && i + 1 < raw.length) value += raw[++i];
+ else if (quote) { if (c === quote) quote = null; else value += c; }
+ else if (c === "'" || c === '"') quote = c;
+ else value += c;
+ }
+ return value;
+}
+
+function tokenize(options: string): LaunchToken[] {
+ const tokens: LaunchToken[] = [];
+ let start = -1;
+ let quote: "'" | '"' | null = null;
+ let escaped = false;
+ const push = (end: number) => {
+ if (start < 0) return;
+ const raw = options.slice(start, end);
+ tokens.push({ raw, value: decodeToken(raw) });
+ start = -1;
+ };
+ for (let i = 0; i < options.length; i++) {
+ const c = options[i];
+ if (start < 0) { if (/\s/.test(c)) continue; start = i; }
+ if (escaped) escaped = false;
+ else if (c === "\\" && quote !== "'") escaped = true;
+ else if (quote) { if (c === quote) quote = null; }
+ else if (c === "'" || c === '"') quote = c;
+ else if (/\s/.test(c)) push(i);
+ }
+ push(options.length);
+ return tokens;
+}
+
+const serialize = (tokens: readonly LaunchToken[]) => tokens.map(({ raw }) => raw).join(" ");
+const commandIndex = (tokens: readonly LaunchToken[]) => tokens.findIndex((token) => token.raw.toLowerCase() === COMMAND_TOKEN);
+const isAssignment = (token: LaunchToken) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(token.value);
+const isLegacyToken = (value: string) => LEGACY_WRAPPER_TOKENS.has(value) || LEGACY_ABSOLUTE_WRAPPER.test(value);
+const isWrapperToken = (value: string, wrapperPath: string) => decodeToken(value) === wrapperPath || isLegacyWrapperToken(value);
+const usesShortcutTarget = (nonSteam: boolean, transport: TargetTransport) => nonSteam && transport.kind === "flatpak";
+
+function selectFlatpakExecutable(transport: TargetTransport, candidate?: string | null): string | undefined {
+ if (transport.kind !== "flatpak") return undefined;
+ const value = candidate?.trim();
+ return value ? (value.startsWith("/") ? value : "/usr/bin/flatpak") : undefined;
+}
+
+export const normalizeLaunchOptions = (options: string) => serialize(tokenize(options));
+export const isLegacyWrapperToken = (value: string) => isLegacyToken(decodeToken(value));
+
+function removeMatchingWrappers(tokens: LaunchToken[], predicate: (value: string) => boolean): boolean {
+ const command = commandIndex(tokens);
+ const prefixEnd = command >= 0 ? command : tokens.length;
+ const kept = tokens.filter((token, i) => i >= prefixEnd || !predicate(token.value));
+ if (kept.length === tokens.length) return false;
+ tokens.splice(0, tokens.length, ...kept);
+ return true;
+}
+
+function installLaunchOption(
+ options: string,
+ wrapperPath = DEFAULT_WRAPPER_PATH,
+ shortcutLaunchOptions = false,
+) {
+ const tokens = tokenize(options);
+ removeMatchingWrappers(tokens, isLegacyToken);
+ let command = commandIndex(tokens);
+ if (command >= 0) {
+ if (tokens[command - 1]?.value === wrapperPath) return { options: serialize(tokens), commandTokenAdded: false };
+ removeMatchingWrappers(tokens, (value) => decodeToken(value) === wrapperPath);
+ command = commandIndex(tokens);
+ tokens.splice(command, 0, { raw: wrapperPath, value: wrapperPath });
+ return { options: serialize(tokens), commandTokenAdded: false };
+ }
+ let insertion = 0;
+ while (insertion < tokens.length && isAssignment(tokens[insertion])) insertion++;
+ if (!shortcutLaunchOptions && insertion < tokens.length && !tokens[insertion].value.startsWith("-")) {
+ throw new Error("Launch options do not contain %command%; refusing to guess a launcher command");
+ }
+ tokens.splice(insertion, 0,
+ { raw: wrapperPath, value: wrapperPath },
+ { raw: COMMAND_TOKEN, value: COMMAND_TOKEN },
+ );
+ return { options: serialize(tokens), commandTokenAdded: true };
+}
+
+export function installWrapperLaunchOption(options: string, wrapperPath = DEFAULT_WRAPPER_PATH) {
+ return installLaunchOption(options, wrapperPath);
+}
+
+export function removeWrapperLaunchOption(
+ options: string,
+ wrapperPath = DEFAULT_WRAPPER_PATH,
+ commandTokenAdded = false,
+): string {
+ const tokens = tokenize(options);
+ if (removeMatchingWrappers(tokens, (value) => isWrapperToken(value, wrapperPath)) && commandTokenAdded) {
+ const command = commandIndex(tokens);
+ if (command >= 0) tokens.splice(command, 1);
+ }
+ return serialize(tokens);
+}
+
+function encodeAssignmentValue(value: string): string {
+ return /^[A-Za-z0-9_./:+,%=-]+$/.test(value)
+ ? value
+ : `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
+}
+
+export function cleanupPluginAssignments(options: string): string {
+ const tokens = tokenize(options);
+ const command = commandIndex(tokens);
+ const prefixEnd = command >= 0 ? command : tokens.length;
+ return serialize(tokens.flatMap((token, i) => {
+ if (i >= prefixEnd || !isAssignment(token)) return [token];
+ const split = token.value.indexOf("=");
+ const key = token.value.slice(0, split);
+ if (key === "DXVK_CONFIG") {
+ const value = token.value.slice(split + 1).split(";").map((part) => part.trim())
+ .filter((part) => part && !DXVK_FRAME_RATE_SEGMENT.test(part)).join("; ");
+ return value ? [{ raw: `DXVK_CONFIG=${encodeAssignmentValue(value)}`, value: `DXVK_CONFIG=${value}` }] : [];
+ }
+ return MANAGED_ENV_KEYS.has(key) ? [] : [token];
+ }));
+}
+
+export function cleanupLegacyLaunchOptions(options: string): string {
+ const tokens = tokenize(options);
+ removeMatchingWrappers(tokens, isLegacyToken);
+ return serialize(tokens);
+}
+export const cleanupPluginLaunchOptions = (options: string, wrapperPath = DEFAULT_WRAPPER_PATH) =>
+ cleanupPluginAssignments(removeWrapperLaunchOption(options, wrapperPath));
+export const cleanupLegacyWrapper = cleanupPluginLaunchOptions;
+export function hasWrapperLaunchIntegration(options: string, wrapperPath = DEFAULT_WRAPPER_PATH): boolean {
+ const tokens = tokenize(options);
+ const command = commandIndex(tokens);
+ return command > 0 && tokens[command - 1].value === wrapperPath;
+}
+
+export function isWrapperIntegrationInstalled(
+ steam: SteamLaunchOptionsSnapshot,
+ nonSteam: boolean,
+ transport: TargetTransport,
+ wrapperPath = DEFAULT_WRAPPER_PATH,
+): boolean {
+ return usesShortcutTarget(nonSteam, transport)
+ ? steam.target === wrapperPath
+ : hasWrapperLaunchIntegration(steam.options, wrapperPath);
}
-function delay(milliseconds: number): Promise<void> {
- return new Promise((resolve) => window.setTimeout(resolve, milliseconds));
+export function assertKnownShortcutTarget(
+ steam: SteamLaunchOptionsSnapshot,
+ nonSteam: boolean,
+ transport: TargetTransport,
+ wrapperPath: string,
+ originalExecutable?: string | null,
+): void {
+ if (usesShortcutTarget(nonSteam, transport) && steam.target === wrapperPath && !originalExecutable) {
+ throw new Error("Managed shortcut Target has no saved original executable");
+ }
+}
+
+const queues = new Map<string, Promise<unknown>>();
+function queued<T>(appId: number, nonSteam: boolean, operation: () => Promise<T>): Promise<T> {
+ const key = `${nonSteam ? "shortcut" : "app"}:${appId}`;
+ const previous = queues.get(key) || Promise.resolve();
+ const current = previous.catch(() => undefined).then(operation);
+ const cleanup = current.then(
+ () => { if (queues.get(key) === cleanup) queues.delete(key); },
+ () => { if (queues.get(key) === cleanup) queues.delete(key); },
+ );
+ queues.set(key, cleanup);
+ return current;
}
-async function waitForLaunchOptions(
+async function waitFor(
appId: number,
nonSteam: boolean,
- expected: string,
+ matches: (value: SteamLaunchOptionsSnapshot) => boolean,
+ message: string,
): Promise<SteamLaunchOptionsSnapshot> {
const deadline = Date.now() + 5000;
let lastError: Error | null = null;
while (Date.now() <= deadline) {
try {
- const snapshot = await readSteamLaunchOptions(appId, nonSteam);
- if (normalizeLaunchOptions(snapshot.options) === normalizeLaunchOptions(expected)) return snapshot;
- } catch (error) {
- lastError = asError(error);
+ const value = await readSteamLaunchOptions(appId, nonSteam);
+ if (matches(value)) return value;
+ } catch (error) { lastError = asError(error); }
+ if (Date.now() < deadline) await new Promise((resolve) => timer().set(resolve as () => void, 100));
+ }
+ throw lastError ? new Error(`${message}: ${lastError.message}`) : new Error(`${message} before the readback timeout`);
+}
+
+async function writeVerified(
+ appId: number,
+ nonSteam: boolean,
+ previous: string,
+ next: string,
+ write: (value: string) => Promise<void>,
+ read: (value: SteamLaunchOptionsSnapshot) => string,
+ message: string,
+): Promise<SteamLaunchOptionsSnapshot> {
+ const normalized = read === readOptions ? normalizeLaunchOptions : (value: string) => value;
+ try {
+ await write(next);
+ return await waitFor(appId, nonSteam, (value) => normalized(read(value)) === normalized(next), message);
+ } catch (error) {
+ const failure = asError(error);
+ try {
+ await write(previous);
+ await waitFor(appId, nonSteam, (value) => normalized(read(value)) === normalized(previous), `Steam did not restore the previous ${read === readOptions ? "launch options" : "shortcut Target"}`);
+ } catch (rollback) {
+ throw new Error(`${failure.message}; rollback also failed: ${asError(rollback).message}`);
}
- if (Date.now() >= deadline) break;
- await delay(100);
+ throw failure;
}
- if (lastError) throw new Error(`Steam did not accept the launch options: ${lastError.message}`);
- throw new Error("Steam did not accept the launch options before the readback timeout");
}
-const operationQueues = new Map<string, Promise<void>>();
+const readOptions = (value: SteamLaunchOptionsSnapshot) => value.options;
+const readTarget = (value: SteamLaunchOptionsSnapshot) => value.target;
-function queueKey(appId: number, nonSteam: boolean): string {
- return `${nonSteam ? "shortcut" : "app"}:${appId}`;
+function writeOptions(appId: number, nonSteam: boolean, value: string): Promise<void> {
+ const setter = nonSteam ? apps()?.SetShortcutLaunchOptions : apps()?.SetAppLaunchOptions;
+ if (!setter) return Promise.reject(new Error(`Steam ${nonSteam ? "shortcut " : ""}launch options API is unavailable`));
+ return Promise.resolve(setter.call(apps(), appId, value));
}
-
-function queueSteamAppOperation<T>(appId: number, nonSteam: boolean, operation: () => Promise<T>): Promise<T> {
- const key = queueKey(appId, nonSteam);
- const previous = operationQueues.get(key) || Promise.resolve();
- const queued = previous.catch(() => undefined).then(operation);
- let cleanup: Promise<void>;
- cleanup = queued.then(
- () => {
- if (operationQueues.get(key) === cleanup) operationQueues.delete(key);
- },
- () => {
- if (operationQueues.get(key) === cleanup) operationQueues.delete(key);
- },
- );
- operationQueues.set(key, cleanup);
- return queued;
+function writeTarget(appId: number, value: string): Promise<void> {
+ const setter = apps()?.SetShortcutExe;
+ if (!setter) return Promise.reject(new Error("Steam shortcut Target API is unavailable"));
+ return Promise.resolve(setter.call(apps(), appId, value));
}
export function updateSteamLaunchOptions(
@@ -188,24 +361,121 @@ export function updateSteamLaunchOptions(
nonSteam: boolean,
transform: (options: string) => string,
): Promise<SteamLaunchOptionsSnapshot> {
- return queueSteamAppOperation(appId, nonSteam, async () => {
+ return queued(appId, nonSteam, async () => {
const current = await readSteamLaunchOptions(appId, nonSteam);
const next = transform(current.options);
- if (next === current.options) return current;
- await setSteamLaunchOptions(appId, nonSteam, next);
- return waitForLaunchOptions(appId, nonSteam, next);
+ return next === current.options ? current : writeVerified(
+ appId, nonSteam, current.options, next,
+ (value) => writeOptions(appId, nonSteam, value), readOptions,
+ "Steam did not accept the launch options",
+ );
+ });
+}
+
+export function installWrapperIntegration(
+ appId: number,
+ nonSteam: boolean,
+ wrapperPath: string,
+ commandTokenAdded = false,
+ transport: TargetTransport = { kind: "host" },
+ originalExecutable?: string,
+): Promise<WrapperIntegrationResult> {
+ return queued(appId, nonSteam, async () => {
+ let current = await readSteamLaunchOptions(appId, nonSteam);
+ if (usesShortcutTarget(nonSteam, transport)) {
+ if (!current.target) throw new Error("Steam shortcut Target is empty; refusing to replace it");
+ if (current.target !== wrapperPath && isWrapperToken(current.target, wrapperPath)) {
+ throw new Error("The shortcut Target points to a legacy frame-generation wrapper; restore it first");
+ }
+ const cleaned = cleanupPluginLaunchOptions(current.options, wrapperPath);
+ const launchOptionsChanged = cleaned !== current.options;
+ if (launchOptionsChanged) {
+ current = await writeVerified(
+ appId, true, current.options, cleaned,
+ (value) => writeOptions(appId, true, value), readOptions,
+ "Steam did not accept shortcut launch options",
+ );
+ }
+ const savedOriginal = selectFlatpakExecutable(transport, originalExecutable);
+ if (current.target === wrapperPath) {
+ if (!savedOriginal) throw new Error("Managed shortcut Target has no saved original executable");
+ return { snapshot: current, originalExecutable: savedOriginal, commandTokenAdded: false, changed: launchOptionsChanged };
+ }
+ if (savedOriginal && selectFlatpakExecutable(transport, current.target) !== savedOriginal) {
+ throw new Error("Shortcut Target changed externally; refusing to replace it");
+ }
+ const currentOriginal = selectFlatpakExecutable(transport, current.target);
+ const value = await writeVerified(
+ appId, true, current.target, wrapperPath,
+ (target) => writeTarget(appId, target), readTarget,
+ "Steam did not accept the shortcut Target",
+ );
+ return { snapshot: value, originalExecutable: currentOriginal, commandTokenAdded: false, changed: true };
+ }
+
+ const cleaned = cleanupPluginAssignments(cleanupLegacyLaunchOptions(current.options));
+ const alreadyInstalled = hasWrapperLaunchIntegration(current.options, wrapperPath);
+ const rewrite = installLaunchOption(cleaned, wrapperPath, nonSteam);
+ if (rewrite.options === current.options) return { snapshot: current, commandTokenAdded, changed: false };
+ const value = await writeVerified(
+ appId, nonSteam, current.options, rewrite.options,
+ (options) => writeOptions(appId, nonSteam, options), readOptions,
+ "Steam did not accept the launch options",
+ );
+ return { snapshot: value, commandTokenAdded: alreadyInstalled ? commandTokenAdded : rewrite.commandTokenAdded, changed: true };
});
}
-export function cleanupSteamLaunchOptions(
+export function removeWrapperIntegration(
appId: number,
nonSteam: boolean,
+ wrapperPath: string,
+ originalExecutable?: string,
+ commandTokenAdded = false,
+ transport: TargetTransport = { kind: "host" },
): Promise<SteamLaunchOptionsSnapshot> {
- return queueSteamAppOperation(appId, nonSteam, async () => {
- const current = await readSteamLaunchOptions(appId, nonSteam);
- const next = cleanupLegacyLaunchOptions(current.options);
- if (next === current.options) return current;
- await setSteamLaunchOptions(appId, nonSteam, next);
- return waitForLaunchOptions(appId, nonSteam, next);
+ return queued(appId, nonSteam, async () => {
+ let current = await readSteamLaunchOptions(appId, nonSteam);
+ if (usesShortcutTarget(nonSteam, transport)) {
+ const cleaned = cleanupPluginLaunchOptions(current.options, wrapperPath);
+ if (cleaned !== current.options) {
+ current = await writeVerified(
+ appId, true, current.options, cleaned,
+ (value) => writeOptions(appId, true, value), readOptions,
+ "Steam did not clean shortcut launch options",
+ );
+ }
+ if (current.target !== wrapperPath) {
+ if (isWrapperToken(current.target, wrapperPath)) {
+ throw new Error("Original shortcut Target is unavailable; refusing to overwrite the current Target");
+ }
+ if (originalExecutable && selectFlatpakExecutable(transport, current.target) !== selectFlatpakExecutable(transport, originalExecutable)) {
+ throw new Error("Shortcut Target changed externally; refusing to restore it");
+ }
+ return current;
+ }
+ if (!originalExecutable || isWrapperToken(originalExecutable, wrapperPath)) {
+ throw new Error("Original shortcut Target is unavailable; refusing to overwrite the current Target");
+ }
+ return writeVerified(
+ appId, true, wrapperPath, originalExecutable,
+ (target) => writeTarget(appId, target), readTarget,
+ "Steam did not restore the shortcut Target",
+ );
+ }
+ const next = cleanupPluginAssignments(removeWrapperLaunchOption(current.options, wrapperPath, commandTokenAdded));
+ return next === current.options ? current : writeVerified(
+ appId, nonSteam, current.options, next,
+ (options) => writeOptions(appId, nonSteam, options), readOptions,
+ "Steam did not clean the launch options",
+ );
});
}
+
+export const cleanupLegacySteamLaunchOptions = (
+ appId: number,
+ nonSteam: boolean,
+ wrapperPath = DEFAULT_WRAPPER_PATH,
+) => updateSteamLaunchOptions(appId, nonSteam, (options) => cleanupPluginLaunchOptions(options, wrapperPath));
+
+export const getDefaultWrapperPath = () => DEFAULT_WRAPPER_PATH;
diff --git a/src/utils/toastUtils.ts b/src/utils/toastUtils.ts
index cbbbc55..c41f4c0 100644
--- a/src/utils/toastUtils.ts
+++ b/src/utils/toastUtils.ts
@@ -1,8 +1,3 @@
-/**
- * Centralized toast notification utilities
- * Provides consistent success/error messaging patterns
- */
-
import { toaster } from "@decky/api";
export interface ToastOptions {
@@ -10,84 +5,46 @@ export interface ToastOptions {
body: string;
}
-/**
- * Show a success toast notification
- */
-export function showSuccessToast(title: string, body: string): void {
- toaster.toast({
- title,
- body
- });
-}
+const showToast = (title: string, body: string): void => {
+ toaster.toast({ title, body });
+};
+export const showSuccessToast = showToast;
+export const showErrorToast = showToast;
-/**
- * Show an error toast notification
- */
-export function showErrorToast(title: string, body: string): void {
- toaster.toast({
- title,
- body
- });
-}
-
-/**
- * Standard success messages for common operations
- */
export const ToastMessages = {
INSTALL_SUCCESS: {
title: "Installation Complete",
- body: "lsfg-vk has been installed successfully"
+ body: "lsfg-vk has been installed successfully",
},
INSTALL_ERROR: {
title: "Installation Failed",
- body: "Unknown error occurred"
+ body: "Unknown error occurred",
},
UNINSTALL_SUCCESS: {
- title: "Uninstallation Complete",
- body: "lsfg-vk has been uninstalled successfully"
+ title: "Uninstallation Complete",
+ body: "lsfg-vk has been uninstalled successfully",
},
UNINSTALL_ERROR: {
title: "Uninstallation Failed",
- body: "Unknown error occurred"
+ body: "Unknown error occurred",
},
CONFIG_UPDATE_ERROR: {
title: "Update Failed",
- body: "Failed to update configuration"
- }
+ body: "Failed to update configuration",
+ },
} as const;
-/**
- * Show a toast with dynamic error message
- */
-export function showErrorToastWithMessage(title: string, error: unknown): void {
- const errorMessage = error instanceof Error ? error.message : String(error);
- showErrorToast(title, errorMessage);
-}
+export const showErrorToastWithMessage = (title: string, error: unknown): void =>
+ showErrorToast(title, error instanceof Error ? error.message : String(error));
-/**
- * Show installation success toast
- */
-export function showInstallSuccessToast(): void {
+export const showInstallSuccessToast = (): void =>
showSuccessToast(ToastMessages.INSTALL_SUCCESS.title, ToastMessages.INSTALL_SUCCESS.body);
-}
-/**
- * Show installation error toast
- */
-export function showInstallErrorToast(error?: string): void {
+export const showInstallErrorToast = (error?: string): void =>
showErrorToast(ToastMessages.INSTALL_ERROR.title, error || ToastMessages.INSTALL_ERROR.body);
-}
-/**
- * Show uninstallation success toast
- */
-export function showUninstallSuccessToast(): void {
+export const showUninstallSuccessToast = (): void =>
showSuccessToast(ToastMessages.UNINSTALL_SUCCESS.title, ToastMessages.UNINSTALL_SUCCESS.body);
-}
-/**
- * Show uninstallation error toast
- */
-export function showUninstallErrorToast(error?: string): void {
+export const showUninstallErrorToast = (error?: string): void =>
showErrorToast(ToastMessages.UNINSTALL_ERROR.title, error || ToastMessages.UNINSTALL_ERROR.body);
-}