feat: massive front-end overhaul and initial github release

This commit is contained in:
Simeon Radivoev 2026-02-08 21:18:10 +02:00
parent a2b40e38bf
commit d5a0e70580
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
303 changed files with 19840 additions and 676 deletions

38
src/bun/api/clients.ts Normal file
View file

@ -0,0 +1,38 @@
import { config } from "./settings";
import Elysia from "elysia";
export const romm = new Elysia({ prefix: "/romm" })
.all("/*", async ({ request, params, set }) =>
{
if (!config.has('rommAddress') && !config.get('rommAddress'))
{
return new Response("Romm Address Not Found", { status: 404 });
}
const rommUrl = new URL(config.get('rommAddress')!);
const url = new URL(request.url);
url.pathname = url.pathname.replace(/^\/api\/romm/, '');
url.host = rommUrl.host;
url.port = rommUrl.port;
url.protocol = rommUrl.protocol;
// Forward headers (optional: remove host if needed)
const headers = new Headers(request.headers);
headers.delete('host');
headers.set("accept-encoding", "identity");
const rommResponse = await fetch(url, {
method: request.method,
headers,
body: await request.arrayBuffer(),
redirect: 'manual', // avoid ROMM redirects
});
set.status = rommResponse.status;
rommResponse.headers.forEach((value, key) =>
{
set.headers[key] = value;
});
return new Response(rommResponse.body, { status: rommResponse.status });
});

42
src/bun/api/rpc.ts Normal file
View file

@ -0,0 +1,42 @@
import { RPC_PORT } from "../../shared/constants";
import { settings } from "./settings";
import { romm } from "./clients";
import Elysia from "elysia";
import { cors } from "@elysiajs/cors";
import { host } from "../utils";
const api = new Elysia({ prefix: "/api", serve: {} })
.use(cors())
.use(romm)
.use(settings);
export type AppType = typeof api;
export function RunAPIServer ()
{
console.log("Launching API Server on port ", RPC_PORT);
return {
apiServer: api.listen({
port: RPC_PORT,
hostname: host,
development: process.env.NODE_ENV === 'development',
fetch (req, server)
{
if (server.upgrade(req, {
data: undefined
}))
{
return;
}
return api.fetch(req);
},
websocket: {
message (ws, message)
{
},
}
})
};
}

29
src/bun/api/settings.ts Normal file
View file

@ -0,0 +1,29 @@
import z from "zod";
import { SettingsSchema, SettingsType } from "../../shared/constants";
import Conf from "conf";
import projectPackage from '../../../package.json';
import Elysia from "elysia";
export const config = new Conf<SettingsType>({
projectName: projectPackage.name,
projectSuffix: 'bun',
schema: Object.fromEntries(Object.entries(SettingsSchema.shape).map(([key, schema]) => [key, schema.toJSONSchema() as any])) as any,
defaults: SettingsSchema.parse({}),
});
console.log("Config Path Located At: ", config.path);
export const settings = new Elysia({ prefix: '/settings' })
.get("/:id", async ({ params: { id } }) =>
{
const value = config.get(id);
return { value: value };
}, {
params: z.object({ id: z.keyof(SettingsSchema) }),
}).post('/:id',
async ({ params: { id }, body: { value }, }) =>
{
config.set(id, value);
}, {
params: z.object({ id: z.keyof(SettingsSchema) }),
body: z.object({ value: z.any() })
});

View file

@ -1,39 +1,55 @@
import { BrowserWindow, Updater } from "electrobun/bun";
import { RunBunServer } from './server';
import { RunAPIServer } from './api/rpc';
import { spawnBrowser } from './utils/browser-spawner';
import { BuildParams } from './utils/browser-params';
const DEV_SERVER_PORT = 5173;
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}/Dashboard`;
const api = RunAPIServer();
let bunServer: { stop: () => void; url: URL; } | undefined;
// Check if Vite dev server is running for HMR
async function getMainViewUrl(): Promise<string> {
const channel = await Updater.localInfo.channel();
if (channel === "dev") {
try {
await fetch(DEV_SERVER_URL, { method: "HEAD" });
console.log(`HMR enabled: Using Vite dev server at ${DEV_SERVER_URL}`);
return DEV_SERVER_URL;
} catch {
console.log("Vite dev server not running. Run 'bun run dev:hmr' for HMR support.");
}
}
return "views://mainview/index.html";
if (!Bun.env.PUBLIC_ACCESS)
{
bunServer = RunBunServer();
}
// Create the main application window
const url = await getMainViewUrl();
function cleanup ()
{
bunServer?.stop();
api.apiServer.stop();
process.exit(0);
}
const mainWindow = new BrowserWindow({
title: "GameFlow",
url,
renderer: 'cef',
styleMask: {
Borderless: true,
},
frame: {
width: 1280,
height: 800,
x: 200,
y: 200,
},
});
try
{
const webviewWorker = new Worker(process.env.IS_BINARY ? "./webview-worker.ts" : new URL("./webview-worker", import.meta.url).href, {
smol: true,
});
webviewWorker.addEventListener('error', console.error);
await new Promise(resolve => webviewWorker.addEventListener('close', resolve));
cleanup();
}
catch (error)
{
console.error(error);
console.log("React Tailwind Vite app started!");
const browserParams = await BuildParams();
if (!browserParams)
{
console.error("Could not find valid browser");
process.exit();
}
const browser = spawnBrowser({
browser: browserParams.browser.type,
args: browserParams.args,
env: browserParams.env,
detached: true,
execPath: browserParams.browser.path,
source: browserParams.browser.source,
ipc (message)
{
console.log(message);
},
onExit: cleanup
});
}

22
src/bun/server.ts Normal file
View file

@ -0,0 +1,22 @@
import { SERVER_PORT } from "../shared/constants";
import path from 'node:path';
import { host } from "./utils";
export function RunBunServer ()
{
console.log("Launching Server on port ", SERVER_PORT);
return Bun.serve({
port: SERVER_PORT,
hostname: host,
routes: {
"/": Bun.file("./dist/index.html"),
// Serve a file by lazily loading it into memory
"/favicon.ico": Bun.file("./dist/favicon.ico"),
},
fetch: async (req) =>
{
const url = new URL(req.url);
return new Response(Bun.file(`./${path.join('dist', url.pathname)}`));
},
});
}

19
src/bun/types.d.ts vendored Normal file
View file

@ -0,0 +1,19 @@
declare const IS_BINARY: string;
declare module 'download-chromium' {
export default function download ({
platform,
revision = '499413',
log = false,
onProgress = undefined,
installPath = '{__dirname}/.local-chromium' }: {
platform?: 'linux' | 'mac' | 'win32' | 'win64',
revision?: string,
log?: boolean,
installPath?: string,
onProgress?: (percent: number, transferred: number, total: number) => void;
}): Promise<string>
{
};
}

19
src/bun/utils.ts Normal file
View file

@ -0,0 +1,19 @@
import { networkInterfaces } from 'node:os';
const localIp = Object.values(networkInterfaces())
.flat()
.find((iface) => iface?.family === 'IPv4' && !iface.internal)?.address || 'localhost';
export const host = process.env.PUBLIC_ACCESS ? localIp : 'localhost';
export function checkRunning (pid: number)
{
try
{
return process.kill(pid, 0);
} catch (error: any)
{
return error.code === 'EPERM';
}
}

View file

@ -0,0 +1,91 @@
import { SERVER_URL } from "../../shared/constants";
import os from 'node:os';
import path, { dirname } from 'node:path';
import { getBrowserPath } from "./get-browser";
import { config } from "../api/settings";
import { host } from "../utils";
export async function BuildParams ()
{
const validBrowser = await getBrowserPath({
browserOrder: ['chrome', 'chromium']
});
if (!validBrowser)
{
return undefined;
}
const args: string[] = [];
const browserEnv = {
GOOGLE_API_KEY: 'no',
GOOGLE_DEFAULT_CLIENT_ID: 'no',
GOOGLE_DEFAULT_CLIENT_SECRET: 'no',
};
if (validBrowser.type === 'chrome' || validBrowser.type === 'chromium')
{
const isEdge = validBrowser.path.toLowerCase().includes('edge') || validBrowser.path.toLowerCase().includes('msedge');
console.log(`[Browser] Detected: ${validBrowser.type} from ${validBrowser.source} - ${isEdge ? 'Edge' : 'Chrome/Chromium'}`);
args.push(`--app=${SERVER_URL(host)}`);
args.push(`--app-id=gameflow`);
args.push(`--force-app-mode`);
args.push('--no-default-browser-check');
args.push('--no-first-run');
args.push('--disable-infobars');
args.push("--disable-extensions");
args.push("--disable-plugins");
args.push(`--user-data-dir=${path.join(dirname(config.path), 'browser-data')}`);
args.push('--disable-sync'); //Disable syncing to a Google account
args.push('--disable-sync-preferences');
args.push('--disable-component-update');
args.push('--allow-insecure-localhost');
args.push('--auto-accept-camera-and-microphone-capture');
args.push(`--window-size=${config.get('windowSize.width')},${config.get('windowSize.height')}`);
args.push('--password-store=basic');
args.push('--block-new-web-contents');
args.push('--bwsi');
args.push('--ash-no-nudges');
args.push('--autoplay-policy=no-user-gesture-required'); // allow autoplay of videos
args.push('--disabled-features=WindowControlsOverlay,navigationControls,Translate,msUndersideButton');
args.push(`--profile-directory=Default`);
if (config.has('windowPosition'))
{
args.push(`--window-position=${config.get('windowPosition.x')},${config.get('windowPosition.y')}`);
}
if (isEdge)
{
// Disable Edge sync and cloud features
args.push('--disable-sync');
args.push('--disable-background-networking');
args.push('--disable-client-side-phishing-detection');
args.push('--disable-component-extensions-with-background-pages');
args.push('--disable-default-apps');
args.push('--disable-extensions-except=');
args.push('--disable-feature=TranslateUI');
args.push('--disable-background-timer-throttling');
args.push('--disable-backgrounding-occluded-windows');
args.push('--disable-breakpad');
args.push('--disable-client-side-phishing-detection');
args.push('--disable-component-update');
args.push('--disable-hang-monitor');
args.push('--disable-ipc-flooding-protection');
args.push('--disable-popup-blocking');
args.push('--disable-prompt-on-repost');
args.push('--disable-renderer-backgrounding');
args.push('--metrics-recording-only');
args.push('--no-service-autorun');
}
if (os.platform() === 'linux')
{
args.push("--disable-web-security");
args.push("--no-sandbox");
}
}
return { env: browserEnv, args, browser: validBrowser };
}

View file

@ -0,0 +1,166 @@
import { type Subprocess } from "bun";
export type RunBrowserType = "chrome" | "chromium" | "firefox" | "edge";
export type RunBrowserSource = "running" | "system" | "flatpak";
/**
* Options for spawning a browser process.
*
* @property browser - The browser type to spawn
* @property args - Optional command-line arguments to pass to the browser
* @property env - Optional environment variables to set for the browser process
* @property detached - If true, the browser process runs independently of the parent
* @property execPath - Full path to the browser executable (required)
* @property source - How the browser was discovered (running, system, or flatpak)
*/
interface SpawnBrowserOptions
{
browser: RunBrowserType;
args?: string[];
env?: Record<string, string>;
detached?: boolean;
execPath: string; // Required: browser executable path from get-browser.ts
source: RunBrowserSource; // How the browser was discovered (running, system, or flatpak)
onExit?: () => void; // Called when the browser exists duh
ipc?: (message: string) => void;
}
/**
* Spawns a browser process with proper handling for different installation types.
*
* Behavior depends on the browser source:
* - "running": Browser is already running, spawns additional instance
* - "system": Native system installation, spawned directly with execPath
* - "flatpak": Flatpak containerized browser, spawned via `flatpak run` with proper arguments
*
* For Flatpak browsers, uses Steam-style argument ordering:
* `flatpak run [OPTIONS] [APP_ID] @@u @@ [USER_ARGS]`
*
* @param options - Spawn options including browser type, path, source, and arguments
* @returns A Bun Subprocess instance
* @throws Error if execPath is not provided or if browser configuration is invalid
*
* @example
* const browser = await getBrowserPath();
* if (browser) {
* const proc = spawnBrowser({
* browser: browser.type,
* args: ["--no-sandbox", "https://example.com"],
* source: browser.source,
* execPath: browser.path,
* detached: true
* });
* }
*/
export function spawnBrowser ({
browser,
args = [],
env = {},
detached = false,
execPath,
source,
onExit,
ipc
}: SpawnBrowserOptions): Subprocess
{
// Configuration for both Flatpak and Native
// Contains Flatpak app IDs, internal container paths, and fallback binary names
const config: Record<RunBrowserType, { id: string; internalCmd: string; bin: string[]; }> = {
chrome: {
id: "com.google.Chrome",
internalCmd: "/app/bin/chrome", // Explicit command inside container
bin: ["google-chrome", "google-chrome-stable", "chrome"]
},
chromium: {
id: "org.chromium.Chromium",
internalCmd: "/app/bin/chromium",
bin: ["chromium", "chromium-browser"]
},
firefox: {
id: "org.mozilla.firefox",
internalCmd: "/app/bin/firefox",
bin: ["firefox"]
},
edge: {
id: "com.microsoft.Edge",
internalCmd: "/app/bin/edge", // Varies, but usually standard for Edge
bin: ["microsoft-edge", "microsoft-edge-stable"]
}
};
const target = config[browser];
const useFlatpak = source === "flatpak";
let cmd: string[];
let finalEnv: Record<string, string> | undefined;
if (useFlatpak)
{
// --- Flatpak Mode (Steam Style) ---
// Structure: flatpak run [ENV] [FLATPAK_OPTS] [APP_ID] @@u @@ [USER_ARGS]
// The @@u @@ syntax enables file forwarding for URL arguments
const envFlags = Object.entries(env).map(([k, v]) => `--env=${k}=${v}`);
// We explicitly set the command to ensure we don't rely on the default entrypoint failing
const flatpakOpts = [
"run",
"--branch=stable",
`--arch=${process.arch === "x64" ? "x86_64" : process.arch}`, // map node arch to flatpak arch
`--command=${target.internalCmd}`,
"--file-forwarding",
...envFlags // Inject env vars here
];
// Combine: flatpak run ... com.google.Chrome @@u @@ [USER_ARGS]
cmd = [
"flatpak",
...flatpakOpts,
target.id,
"@@u",
"@@",
...args
];
// Clear env for the spawner so it doesn't pollute the flatpak command wrapper
finalEnv = undefined;
console.log(`[Browser] Launching Flatpak: ${cmd.join(" ")}`);
} else
{
// --- Native Mode ---
// Use the provided execPath directly
cmd = [execPath, ...args];
finalEnv = { ...process.env, ...env } as Record<string, string>;
console.log(`[Browser] Launching Native: ${execPath}`);
}
const processSub = Bun.spawn(cmd, {
env: finalEnv,
stdin: "ignore",
stdout: "inherit",
stderr: "inherit",
ipc,
onExit (_proc, exitCode)
{
if (exitCode !== 0 && exitCode !== null)
{
console.error(`[Browser] Exited with code: ${exitCode}`);
}
onExit?.();
},
});
if (detached) processSub.unref();
return processSub;
}
// --- Test Run ---
// spawnBrowser({
// browser: "chrome",
// args: ["--window-size=1024,640", "--force-device-scale-factor=1.25"],
// detached: true
// });

View file

@ -0,0 +1,611 @@
import { spawnSync } from "bun";
import { platform } from "node:os";
import { RunBrowserType } from "./browser-spawner";
export type GetBrowserType = "chrome" | "chromium" | "firefox";
export type GetBrowserSource = "running" | "system" | "flatpak";
/**
* Browser discovery priority configuration
*/
interface BrowserPriorityConfig
{
/** Include currently running browser processes in search */
includeRunning?: boolean;
/** Browser types to search for, in priority order */
browserOrder?: GetBrowserType[];
/** Include system default browser on Windows */
includeSystemDefault?: boolean;
/** Include Flatpak browsers on Linux */
includeFlatpak?: boolean;
}
/**
* Browser discovery result containing the executable path, browser type, and discovery source.
*/
interface BrowserResult
{
/** Full path to the browser executable */
path: string;
/** Type of browser (chrome, chromium, or firefox) */
type: GetBrowserType;
/** Source of discovery (running process, system installation, or flatpak) */
source: GetBrowserSource;
}
/**
* Main function to find a valid browser executable.
*
* Searches for an available browser based on customizable priority configuration.
* Default priority order:
* 1. Currently running Chrome process (fastest return)
* 2. Windows: Default system browser (if on Windows)
* 3. Standard System Paths (Firefox > Chrome > Chromium by default)
* 4. Flatpak (Linux only)
*
* @param config - Optional priority configuration to customize search behavior
* @returns A promise that resolves to a BrowserResult containing the path, type, and source
* of the discovered browser, or null if no suitable browser is found.
*
* @example
* // Use default priority
* const browser = await getBrowserPath();
*
* @example
* // Prefer Chrome over Firefox, skip running processes
* const browser = await getBrowserPath({
* includeRunning: false,
* browserOrder: ['chrome', 'firefox', 'chromium']
* });
*/
export async function getBrowserPath (config?: BrowserPriorityConfig): Promise<BrowserResult | null>
{
// Default configuration
const {
includeRunning = true,
browserOrder = ["firefox", "chrome", "chromium"],
includeSystemDefault = true,
includeFlatpak = true
} = config || {};
const currentPlatform = platform();
// 1. Check for currently running browser process
if (includeRunning)
{
const runningBrowser = await getRunningBrowserPath(browserOrder, currentPlatform);
if (runningBrowser)
{
console.log(`[Found] Running ${runningBrowser.type} process: ${runningBrowser.path}`);
return { ...runningBrowser, source: "running" };
}
}
// 2. Windows: Check default system browser
if (includeSystemDefault && currentPlatform === "win32")
{
const defaultBrowser = await getWindowsDefaultBrowser(browserOrder);
if (defaultBrowser && browserOrder.includes(defaultBrowser.type))
{
console.log(`[Found] Windows default browser: ${defaultBrowser.path} (${defaultBrowser.type})`);
return { ...defaultBrowser, source: "system" };
}
}
// 3. Check standard install paths with custom priority
for (const browser of browserOrder)
{
const path = await findSystemBrowser(browser, currentPlatform);
if (path)
{
console.log(`[Found] Installed ${browser}: ${path}`);
return { path, type: browser, source: "system" };
}
}
// 4. Check Flatpaks (Linux only)
if (includeFlatpak && currentPlatform === "linux")
{
for (const browser of browserOrder)
{
const path = await findFlatpakBrowser(browser);
if (path)
{
console.log(`[Found] Flatpak ${browser}: ${path}`);
return { path, type: browser, source: "flatpak" };
}
}
}
console.error("No suitable browser found.");
return null;
}
// --- Helper: Find Running Process ---
/**
* Attempts to find the path of a currently running browser (Chrome, Chromium, or Firefox).
*
* Platform-specific implementations:
* - Windows: Uses PowerShell to query running processes
* - Linux: Uses pgrep to find the process and resolves /proc/[pid]/exe
* - macOS: Uses ps command to find Chrome or Firefox in process list
*
* @param os - The operating system ("win32", "linux", or "darwin")
* @returns An object with path and type of the running browser, or null if not found
*/
async function getRunningBrowserPath (browserOrder: GetBrowserType[], os: string): Promise<{ path: string, type: GetBrowserType; } | null>
{
try
{
if (os === "win32")
{
// PowerShell is most reliable for getting full paths on Windows
// Check for Firefox first, then Chrome
for (const processName of browserOrder)
{
const cmd = spawnSync([
"powershell",
"-NoProfile",
"-Command",
`(Get-Process ${processName} -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Path)`
]);
const path = cmd.stdout.toString().trim();
if (path && await Bun.file(path).exists())
{
const browserType: GetBrowserType = processName === 'firefox' ? 'firefox' : 'chrome';
console.log(`[Browser] Found running ${browserType}: ${path}`);
return { path, type: browserType };
}
}
return null;
}
if (os === "linux")
{
const names: Record<RunBrowserType, string[]> = {
chrome: ['chrome', 'google-chrome', 'google-chrome-stable'],
chromium: ['chromium'],
firefox: ['firefox'],
edge: ['edge']
};
// Find PID of firefox or chrome, then resolve the symlink in /proc
for (const processName of browserOrder.flatMap(b => names[b]))
{
const pgrep = spawnSync(["pgrep", "-o", processName]); // -o = oldest (parent)
const pid = pgrep.stdout.toString().trim();
if (!pid) continue;
// Read the symlink for the executable path using readlink
const linkPath = `/proc/${pid}/exe`;
// Use shell readlink to resolve the symlink
const readLink = spawnSync(["readlink", "-f", linkPath]);
const finalPath = readLink.stdout.toString().trim();
if (finalPath && await Bun.file(finalPath).exists())
{
const browserType: GetBrowserType = processName === 'firefox' ? 'firefox' : 'chrome';
console.log(`[Browser] Found running ${browserType}: ${finalPath}`);
return { path: finalPath, type: browserType };
}
}
return null;
}
if (os === "darwin")
{
// macOS: ps command to list process paths
const cmd = spawnSync(["ps", "-A", "-o", "comm"]);
const output = cmd.stdout.toString();
// Check for Firefox first
const firefoxMatch = output.split('\n').find(line => line.includes("Firefox.app/Contents/MacOS/firefox"));
if (firefoxMatch)
{
console.log(`[Browser] Found running firefox: ${firefoxMatch.trim()}`);
return { path: firefoxMatch.trim(), type: 'firefox' };
}
// Check for Chrome
const chromeMatch = output.split('\n').find(line => line.includes("Google Chrome.app/Contents/MacOS/Google Chrome"));
if (chromeMatch)
{
console.log(`[Browser] Found running chrome: ${chromeMatch.trim()}`);
return { path: chromeMatch.trim(), type: 'chrome' };
}
return null;
}
} catch (e)
{
// Ignore errors checking running processes
return null;
}
return null;
}
// --- Helper: Get Windows Default Browser ---
/**
* Detects the default browser set in Windows via registry queries.
*
* Queries multiple registry locations for Windows 11+ and Windows 10 compatibility:
* - URL associations (Windows 11+)
* - File extension associations (Windows 10)
* - Classic ProgID associations
*
* Falls back through multiple methods if the primary registry keys are unavailable.
*
* @returns An object with the default browser's path and type, or null if detection fails
*/
async function getWindowsDefaultBrowser (allowed: GetBrowserType[]): Promise<{ path: string, type: GetBrowserType; } | null>
{
try
{
// Query the registry for the default browser
// Windows 10/11 store default browser association in multiple places
const registryKeys = [
// Windows 11+ (Preferred)
'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice',
// Windows 10 fallback
'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.html\\UserChoice',
// Classic method - looks at .html file association
'HKEY_CLASSES_ROOT\\.html'
];
for (const regKey of registryKeys)
{
try
{
const cmd = spawnSync(["reg", "query", regKey]);
if (cmd.success)
{
const output = cmd.stdout.toString().toLowerCase();
// Check which browser is the default
if ((output.includes('chrome') || output.includes('google')) && allowed.includes('chrome'))
{
const chromePath = await findSystemBrowser("chrome", "win32");
if (chromePath) return { path: chromePath, type: "chrome" };
}
if ((output.includes('msedge') || output.includes('edge')) && allowed.includes('chromium'))
{
const edgePath = await findSystemBrowser("chromium", "win32");
if (edgePath && edgePath.includes('msedge')) return { path: edgePath, type: "chromium" };
}
if (output.includes('firefox') && allowed.includes('firefox'))
{
const firefoxPath = await findSystemBrowser("firefox", "win32");
if (firefoxPath) return { path: firefoxPath, type: "firefox" };
}
}
} catch (e)
{
// Try next registry key
}
}
// Fallback: Try to get progId for .html files
try
{
const progIdCmd = spawnSync(["reg", "query", "HKEY_CLASSES_ROOT\\.html", "/ve"]);
if (progIdCmd.success)
{
const progId = progIdCmd.stdout.toString().match(/REG_SZ\s+(.+?)(?:\r?\n|$)/)?.[1]?.trim();
if (progId)
{
// Query the ProgID's shell\\open\\command to get the browser path
const cmdKey = `HKEY_CLASSES_ROOT\\${progId}\\shell\\open\\command`;
const openCmd = spawnSync(["reg", "query", cmdKey, "/ve"]);
if (openCmd.success)
{
const execPath = openCmd.stdout.toString().match(/REG_SZ\s+"?([^"\r\n]+\.exe)/i)?.[1];
if (execPath && await Bun.file(execPath).exists())
{
// Determine browser type
if (execPath.toLowerCase().includes('chrome') && allowed.includes('chrome'))
{
return { path: execPath, type: "chrome" };
} else if ((execPath.toLowerCase().includes('edge') || execPath.toLowerCase().includes('msedge')) && allowed.includes('chromium'))
{
return { path: execPath, type: "chromium" };
} else if (execPath.toLowerCase().includes('firefox') && allowed.includes('firefox'))
{
return { path: execPath, type: "firefox" };
}
}
}
}
}
} catch (e)
{
// Fallback failed
}
} catch (e)
{
// Default browser detection failed
}
return null;
}
// --- Helper: Find System Installed Browser ---
/**
* Searches for a browser installation in standard system locations.
*
* Platform-specific behavior:
* - Windows: Checks registry and common installation directories (Program Files, AppData, etc.)
* - macOS: Checks Applications folder
* - Linux: Uses `which` command to find binary in $PATH
*
* @param browser - The browser type to search for
* @param os - The operating system ("win32", "linux", or "darwin")
* @returns The full path to the browser executable, or null if not found
*/
async function findSystemBrowser (browser: GetBrowserType, os: string): Promise<string | null>
{
if (os === "win32")
{
// First, try registry lookup (most reliable on Windows)
const registryPath = await findBrowserViaRegistry(browser);
if (registryPath) return registryPath;
// Fallback to standard install paths
const standardPaths = getStandardWindowsPaths(browser);
for (const fullPath of standardPaths)
{
if (await Bun.file(fullPath).exists()) return fullPath;
}
return null;
}
if (os === "linux" || os === "darwin")
{
// Common binary names
const binMap: Record<string, string[]> = {
chrome: ["google-chrome", "google-chrome-stable", "chrome"],
chromium: ["chromium", "chromium-browser"],
firefox: ["firefox"]
};
if (os === "darwin")
{
// macOS standard paths
const macPaths = {
chrome: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
chromium: "/Applications/Chromium.app/Contents/MacOS/Chromium",
firefox: "/Applications/Firefox.app/Contents/MacOS/firefox"
};
if (await Bun.file(macPaths[browser]).exists()) return macPaths[browser];
return null;
}
// Linux: use `which` to find in $PATH
for (const bin of binMap[browser])
{
const cmd = spawnSync(["which", bin]);
if (cmd.success)
{
const path = cmd.stdout.toString().trim();
if (path && await Bun.file(path).exists()) return path;
}
}
}
return null;
}
// --- Helper: Windows Registry Lookup ---
/**
* Queries Windows registry for browser installation paths.
*
* Checks App Paths registry hives which are populated by browser installers:
* - HKEY_LOCAL_MACHINE (system-wide installations)
* - HKEY_LOCAL_MACHINE\WOW6432Node (32-bit applications on 64-bit systems)
* - HKEY_CURRENT_USER (user-specific installations)
*
* This is more reliable than hardcoded paths as it dynamically finds where
* the browser installer registered itself.
*
* @param browser - The browser type to search for
* @returns The full path to the browser executable from registry, or null if not found
*/
async function findBrowserViaRegistry (browser: GetBrowserType): Promise<string | null>
{
try
{
// Registry paths for browser installations
const registryPaths: Record<GetBrowserType, string[]> = {
chrome: [
// Standard Chrome registry paths
'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe',
'HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe',
// User-specific Chrome registry
'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe'
],
chromium: [
'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chromium.exe',
'HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chromium.exe'
],
firefox: [
'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\firefox.exe',
'HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\App Paths\\firefox.exe',
// Check Mozilla Firefox registry for install location
'HKEY_LOCAL_MACHINE\\SOFTWARE\\Mozilla\\Mozilla Firefox'
]
};
for (const regPath of registryPaths[browser])
{
try
{
const cmd = spawnSync([
"reg",
"query",
regPath,
"/ve"
]);
if (cmd.success)
{
const output = cmd.stdout.toString();
// Extract path from registry output (format: " (Default) REG_SZ C:\path\to\exe")
const match = output.match(/REG_SZ\s+(.+?)(?:\r?\n|$)/);
if (match && match[1])
{
const path = match[1].trim();
if (path && await Bun.file(path).exists())
{
return path;
}
}
}
} catch (e)
{
// Continue to next registry path
}
}
} catch (e)
{
// Registry lookup failed, will fallback to standard paths
}
return null;
}
// --- Helper: Standard Windows Browser Paths ---
/**
* Generates a list of common Windows browser installation paths to check.
*
* Includes:
* - Program Files locations (64-bit and 32-bit)
* - LocalAppData (user-specific installations)
* - Microsoft Edge paths (treated as chromium)
* - Portable installations and custom locations
*
* @param browser - The browser type to generate paths for
* @returns An array of potential browser executable paths
*/
function getStandardWindowsPaths (browser: GetBrowserType): string[]
{
const paths: string[] = [];
const prefixes = [
process.env.LOCALAPPDATA,
process.env.PROGRAMFILES,
process.env["PROGRAMFILES(X86)"]
].filter(Boolean) as string[];
// Standard installation patterns
const browserPatterns: Record<GetBrowserType, string[]> = {
chrome: [
"\\Google\\Chrome\\Application\\chrome.exe",
"\\Google\\Chrome\\chrome.exe"
],
chromium: [
"\\Chromium\\Application\\chrome.exe",
"\\Chromium\\chromium.exe"
],
firefox: [
"\\Mozilla Firefox\\firefox.exe",
"\\Mozilla\\Firefox\\firefox.exe",
"\\Firefox\\firefox.exe"
]
};
// Add standard paths
for (const prefix of prefixes)
{
for (const pattern of browserPatterns[browser])
{
paths.push(`${prefix}${pattern}`);
}
}
// Add common user-specific paths (especially for Chrome Portable or custom installations)
const userProfile = process.env.USERPROFILE;
if (userProfile)
{
if (browser === "chrome")
{
paths.push(`${userProfile}\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe`);
paths.push(`${userProfile}\\AppData\\Roaming\\Google\\Chrome\\Application\\chrome.exe`);
} else if (browser === "firefox")
{
paths.push(`${userProfile}\\AppData\\Local\\Mozilla Firefox\\firefox.exe`);
paths.push(`${userProfile}\\AppData\\Roaming\\Mozilla Firefox\\firefox.exe`);
// Also check for Program Files under user profile (some custom installs)
paths.push(`${userProfile}\\AppData\\Local\\Programs\\Firefox\\firefox.exe`);
}
}
// Add alternative common locations for Edge (treated as chromium)
if (browser === "chromium")
{
const edgePaths = [
`${process.env.PROGRAMFILES}\\Microsoft\\Edge\\Application\\msedge.exe`,
`${process.env["PROGRAMFILES(X86)"]}\\Microsoft\\Edge\\Application\\msedge.exe`,
`${userProfile}\\AppData\\Local\\Microsoft\\Edge\\Application\\msedge.exe`
].filter(p => p);
paths.push(...edgePaths);
}
return paths;
}
// --- Helper: Find Flatpak (Linux Only) ---
/**
* Searches for a Flatpak browser installation on Linux.
*
* Checks if a Flatpak is installed by querying the flatpak command,
* then looks for the exported binary in standard Flatpak export directories.
*
* Flatpak paths checked:
* - /var/lib/flatpak/exports/bin/ (system-wide)
* - ~/.local/share/flatpak/exports/bin/ (user-specific)
*
* @param browser - The browser type to search for
* @returns The path to the Flatpak browser binary, or null if not found
*/
async function findFlatpakBrowser (browser: GetBrowserType): Promise<string | null>
{
// Check if flatpak is installed first
if (spawnSync(["which", "flatpak"]).exitCode !== 0) return null;
const flatpakIds = {
chrome: "com.google.Chrome",
chromium: "org.chromium.Chromium",
firefox: "org.mozilla.firefox"
};
const appId = flatpakIds[browser];
// Check if specific flatpak is installed
const checkCmd = spawnSync(["flatpak", "info", appId]);
if (checkCmd.success)
{
// We return the flatpak run command wrapper or the path?
// Usually tools expect an executable. For flatpak, we might need a wrapper script
// or just return "flatpak" with arguments.
// However, usually tools want a single path.
// We will return the internal path if accessible, or the flatpak binary path usually isn't enough.
// OPTION A: Return the standard export path if it exists
const exportPath = `/var/lib/flatpak/exports/bin/${appId}`;
if (await Bun.file(exportPath).exists()) return exportPath;
const userExportPath = `${process.env.HOME}/.local/share/flatpak/exports/bin/${appId}`;
if (await Bun.file(userExportPath).exists()) return userExportPath;
}
return null;
}

View file

@ -0,0 +1,9 @@
import Webview from "@rcompat/webview";
import platform from "@rcompat/webview/windows-x64";
import { SERVER_URL } from "../shared/constants";
import { host } from "./utils";
console.log("Launching Webview");
const webview = new Webview({ debug: import.meta.env.NODE_ENV === 'development', platform });
webview.navigate(SERVER_URL(host));
webview.run();