Files
Recorder-Tabber/electron/main.cjs
T
Cursor Agentandchen b354ea9f88
AppImage release / appimage (push) Successful in 53s
Replace sheet reading with a fingering editor.
Drag Baroque charts onto a line, reorder them, set a duration, or remove a note. The AppImage no longer packages oemer, ONNX, or the models. Version 0.2.0.

Co-authored-by: catpersonisme <catpersonisme@gmail.com>
2026-10-11 09:06:33 +00:00

125 lines
3.4 KiB
JavaScript

const { app, BrowserWindow, dialog } = require("electron");
const { spawn } = require("node:child_process");
const fs = require("node:fs");
const http = require("node:http");
const net = require("node:net");
const path = require("node:path");
app.commandLine.appendSwitch("no-sandbox");
app.commandLine.appendSwitch("disable-dev-shm-usage");
let server;
function freePort() {
return new Promise((resolve, reject) => {
const probe = net.createServer();
probe.listen(0, "127.0.0.1", () => {
const address = probe.address();
const port = address && typeof address === "object" ? address.port : 0;
probe.close(() => resolve(port));
});
probe.on("error", reject);
});
}
function waitFor(url, timeoutMs) {
const started = Date.now();
return new Promise((resolve, reject) => {
const tick = () => {
const request = http.get(url, (response) => {
response.resume();
if (response.statusCode && response.statusCode < 500) resolve();
else retry();
});
request.on("error", retry);
};
const retry = () => {
if (Date.now() - started > timeoutMs) reject(new Error(`Timed out waiting for ${url}`));
else setTimeout(tick, 250);
};
tick();
});
}
function stopServer() {
if (server && !server.killed) server.kill("SIGTERM");
}
async function open() {
const resources = process.resourcesPath;
const standalone = path.join(resources, "standalone");
const serverJs = path.join(standalone, "server.js");
if (!fs.existsSync(serverJs)) {
dialog.showErrorBox(
"Recorder fingering editor",
"The bundled app is missing from this AppImage.",
);
app.quit();
return;
}
const port = await freePort();
const logPath = path.join(app.getPath("temp"), "recorder-tab-converter.log");
const log = fs.createWriteStream(logPath, { flags: "a" });
server = spawn(process.execPath, [serverJs], {
cwd: standalone,
env: {
...process.env,
ELECTRON_RUN_AS_NODE: "1",
PORT: String(port),
HOSTNAME: "127.0.0.1",
NODE_ENV: "production",
},
stdio: ["ignore", "pipe", "pipe"],
});
server.stdout.pipe(log);
server.stderr.pipe(log);
server.on("exit", (code) => {
log.write(`\nserver exit ${code}\n`);
});
const url = `http://127.0.0.1:${port}/`;
try {
await waitFor(url, 45_000);
} catch (error) {
dialog.showErrorBox(
"Recorder fingering editor",
`${error instanceof Error ? error.message : "The app server did not start."}\nLog: ${logPath}`,
);
stopServer();
app.quit();
return;
}
const window = new BrowserWindow({
width: 1280,
height: 880,
minWidth: 390,
minHeight: 640,
show: false,
title: "Recorder fingering editor",
backgroundColor: "#f3ecdf",
autoHideMenuBar: true,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
// Renderer sandbox cannot create shared memory here, so the window
// fails with ERR_FAILED. The page is the local UI and has no Node access.
sandbox: false,
},
});
window.once("ready-to-show", () => window.show());
await window.loadURL(url);
}
app.whenReady().then(open).catch((error) => {
dialog.showErrorBox("Recorder fingering editor", error instanceof Error ? error.message : String(error));
app.quit();
});
app.on("before-quit", stopServer);
app.on("window-all-closed", () => {
stopServer();
app.quit();
});