Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ body:
attributes:
label: ProtonShift version
description: "Check the app's About dialog or `electron/package.json`."
placeholder: "0.8.6"
placeholder: "0.8.7"
validations:
required: true

Expand Down
1 change: 1 addition & 0 deletions assets/io.github.protonshift.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<launchable type="desktop-id">io.github.protonshift.desktop</launchable>
<url type="homepage">https://github.com/I4cTime/protonshift</url>
<releases>
<release version="0.8.7" date="2026-04-15"/>
<release version="0.8.6" date="2026-04-15"/>
<release version="0.8.5" date="2026-04-15"/>
</releases>
Expand Down
111 changes: 110 additions & 1 deletion electron/main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { app, BrowserWindow, ipcMain, session } from "electron";
import { app, BrowserWindow, ipcMain } from "electron";
import { ChildProcess, spawn } from "child_process";
import * as path from "path";
import * as http from "http";

let mainWindow: BrowserWindow | null = null;
let pythonProcess: ChildProcess | null = null;
let apiPort: number | null = null;
let staticServer: http.Server | null = null;
let staticRendererPort: number | null = null;

const isDev = !app.isPackaged;

Expand Down Expand Up @@ -126,6 +128,102 @@ function getIconPath(): string {
return path.join(process.resourcesPath, "assets", "256x256.png");
}

function mimeFor(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
const map: Record<string, string> = {
".html": "text/html; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".woff": "font/woff",
".woff2": "font/woff2",
".txt": "text/plain; charset=utf-8",
".map": "application/json; charset=utf-8",
};
return map[ext] ?? "application/octet-stream";
}

/** Serves Next static export over http://127.0.0.1 — root-relative /_next/... URLs do not work with file:// */
function startStaticRendererServer(rootDir: string): Promise<number> {
const root = path.resolve(rootDir);
return new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
try {
const rawPath = req.url?.split("?")[0] ?? "/";
let pathname: string;
try {
pathname = decodeURIComponent(rawPath);
} catch {
res.writeHead(400).end();
return;
}
if (pathname.includes("..")) {
res.writeHead(403).end();
return;
}
const rel = pathname.replace(/^\/+/, "");
const rootResolved = path.resolve(root);

const candidates: string[] = [];
if (rel === "" || rel === "/") {
candidates.push(path.join(rootResolved, "index.html"));
} else {
candidates.push(
path.join(rootResolved, rel),
path.join(rootResolved, `${rel}.html`),
path.join(rootResolved, rel, "index.html"),
);
}

let found: string | null = null;
for (const candidate of candidates) {
const normalized = path.resolve(candidate);
if (!normalized.startsWith(rootResolved + path.sep) && normalized !== rootResolved) {
continue;
}
if (fs.existsSync(normalized) && fs.statSync(normalized).isFile()) {
found = normalized;
break;
}
}

if (!found) {
res.writeHead(404).end("Not found");
return;
}

const body = fs.readFileSync(found);
res.writeHead(200, {
"Content-Type": mimeFor(found),
"Content-Length": String(body.length),
"Cache-Control": "no-store",
});
res.end(body);
} catch {
res.writeHead(500).end();
}
});

server.on("error", reject);
server.listen(0, "127.0.0.1", () => {
const addr = server.address();
staticServer = server;
if (addr && typeof addr === "object") {
staticRendererPort = addr.port;
resolve(addr.port);
} else {
reject(new Error("Static server failed to bind"));
}
});
});
}

function createWindow(): void {
mainWindow = new BrowserWindow({
width: 1200,
Expand All @@ -147,6 +245,8 @@ function createWindow(): void {
if (isDev) {
mainWindow.loadURL("http://localhost:3000");
mainWindow.webContents.openDevTools({ mode: "detach" });
} else if (staticRendererPort) {
mainWindow.loadURL(`http://127.0.0.1:${staticRendererPort}/`);
} else {
mainWindow.loadFile(path.join(__dirname, "..", "renderer", "out", "index.html"));
}
Expand Down Expand Up @@ -202,6 +302,10 @@ app.on("ready", async () => {
try {
apiPort = await startPython();
await waitForHealth(apiPort);
if (!isDev) {
const outDir = path.join(__dirname, "..", "renderer", "out");
await startStaticRendererServer(outDir);
}
} catch (err) {
console.error("Failed to start Python backend:", err);
app.quit();
Expand All @@ -219,6 +323,11 @@ app.on("before-quit", () => {
pythonProcess.kill("SIGTERM");
pythonProcess = null;
}
if (staticServer) {
staticServer.close();
staticServer = null;
staticRendererPort = null;
}
});

app.on("activate", () => {
Expand Down
2 changes: 1 addition & 1 deletion electron/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "protonshift",
"version": "0.8.6",
"version": "0.8.7",
"description": "Linux game configuration toolkit",
"main": "dist/main.js",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion electron/renderer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "protonshift-renderer",
"version": "0.8.6",
"version": "0.8.7",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "protonshift"
version = "0.8.6"
version = "0.8.7"
description = "Linux game configuration toolkit: GPU, launch options, Proton, env vars"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
2 changes: 1 addition & 1 deletion src/game_setup_hub/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""ProtonShift — game configuration for Pop!_OS, Ubuntu, and related distros."""

__version__ = "0.8.6"
__version__ = "0.8.7"
2 changes: 1 addition & 1 deletion src/game_setup_hub/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ class StatusResponse(BaseModel):
# App setup
# ---------------------------------------------------------------------------

app = FastAPI(title="ProtonShift API", version="0.8.6")
app = FastAPI(title="ProtonShift API", version="0.8.7")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])

# Locks for serializing writes to shared resources
Expand Down
2 changes: 1 addition & 1 deletion src/game_setup_hub/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1032,7 +1032,7 @@ def on_about(_action, _param):
about = Adw.AboutWindow(
transient_for=win,
application_name="ProtonShift",
version="0.8.6",
version="0.8.7",
developer_name="ProtonShift",
website="https://github.com/protonshift/protonshift",
application_icon="io.github.protonshift",
Expand Down
Loading