mirror of
https://github.com/litruv/picoGraph.git
synced 2026-07-24 02:36:04 +10:00
- Created test_cart.p8 with basic drawing functionality and a print statement. - Created test_unix.p8 with a simple initialization function.
330 lines
8.6 KiB
JavaScript
330 lines
8.6 KiB
JavaScript
// Enable hot-reload during development
|
|
try {
|
|
require('electron-reloader')(module, {
|
|
debug: false,
|
|
watchRenderer: true,
|
|
ignore: [
|
|
/node_modules/,
|
|
/\.git/,
|
|
/dist/,
|
|
/build/,
|
|
/pico8-temp-player\.html$/
|
|
]
|
|
});
|
|
} catch (_) {
|
|
// Ignore errors in production (when electron-reloader isn't installed)
|
|
}
|
|
|
|
const { app, BrowserWindow, ipcMain, Menu, shell, dialog } = require("electron");
|
|
const fs = require("node:fs/promises");
|
|
const path = require("node:path");
|
|
const { spawn } = require("node:child_process");
|
|
|
|
const OUTPUT_FILENAME = "compiled.lua";
|
|
|
|
/**
|
|
* Resolves the absolute directory where compiled Lua output is stored.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
function getOutputDirectory() {
|
|
const applicationData = app.getPath("appData");
|
|
return path.join(applicationData, "pico-8", "carts", "picograph");
|
|
}
|
|
|
|
/**
|
|
* Resolves the absolute path to the generated Lua file.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
function getOutputPath() {
|
|
return path.join(getOutputDirectory(), OUTPUT_FILENAME);
|
|
}
|
|
|
|
/**
|
|
* Resolves the absolute directory where tileset images are stored.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
function getTilesetsDirectory() {
|
|
const applicationData = app.getPath("appData");
|
|
return path.join(applicationData, "pico-8", "carts", "picograph", "tilesets");
|
|
}
|
|
|
|
/** @type {import("electron").BrowserWindow | null} */
|
|
let mainWindow = null;
|
|
|
|
/**
|
|
* Creates the primary application window and loads the web UI.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
function createMainWindow() {
|
|
const window = new BrowserWindow({
|
|
width: 1440,
|
|
height: 900,
|
|
minWidth: 960,
|
|
minHeight: 640,
|
|
frame: false,
|
|
webPreferences: {
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
preload: path.join(__dirname, "preload.js"),
|
|
},
|
|
show: false,
|
|
});
|
|
|
|
window.once("ready-to-show", () => {
|
|
window.show();
|
|
});
|
|
|
|
window.loadFile(path.join(__dirname, "index.html"));
|
|
|
|
// Open DevTools in development
|
|
window.webContents.openDevTools();
|
|
|
|
// Register F12 to toggle DevTools
|
|
window.webContents.on('before-input-event', (event, input) => {
|
|
if (input.key === 'F12') {
|
|
window.webContents.toggleDevTools();
|
|
}
|
|
});
|
|
|
|
const emitMaximizedState = () => {
|
|
window.webContents.send("window:maximized-changed", window.isMaximized());
|
|
};
|
|
|
|
window.on("maximize", emitMaximizedState);
|
|
window.on("unmaximize", emitMaximizedState);
|
|
|
|
window.on("closed", () => {
|
|
mainWindow = null;
|
|
});
|
|
|
|
mainWindow = window;
|
|
}
|
|
|
|
/**
|
|
* Registers IPC handlers used by renderer processes.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
function registerIpcHandlers() {
|
|
ipcMain.handle("lua:compile", async (_event, source) => {
|
|
if (typeof source !== "string") {
|
|
throw new TypeError("Lua source must be a string");
|
|
}
|
|
|
|
const outputPath = getOutputPath();
|
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
await fs.writeFile(outputPath, source, "utf8");
|
|
|
|
return { filePath: outputPath };
|
|
});
|
|
|
|
ipcMain.handle("cart:preview", async (_event, cartContent) => {
|
|
if (typeof cartContent !== "string") {
|
|
throw new TypeError("Cart content must be a string");
|
|
}
|
|
|
|
const tempCartPath = path.join(app.getPath("temp"), `preview_${Date.now()}.p8`);
|
|
await fs.writeFile(tempCartPath, cartContent, "utf8");
|
|
|
|
return { filePath: tempCartPath };
|
|
});
|
|
|
|
ipcMain.handle("player:writeHtml", async (_event, htmlContent) => {
|
|
if (typeof htmlContent !== "string") {
|
|
throw new TypeError("HTML content must be a string");
|
|
}
|
|
|
|
const playerPath = path.join(__dirname, "public", "pico8-temp-player.html");
|
|
await fs.writeFile(playerPath, htmlContent, "utf8");
|
|
|
|
return { filePath: playerPath };
|
|
});
|
|
|
|
ipcMain.handle("player:writeCart", async (_event, cartContent) => {
|
|
if (typeof cartContent !== "string") {
|
|
throw new TypeError("Cart content must be a string");
|
|
}
|
|
|
|
const cartPath = path.join(__dirname, "public", "pico8-cart.p8");
|
|
await fs.writeFile(cartPath, cartContent, "utf8");
|
|
|
|
return { filePath: cartPath };
|
|
});
|
|
|
|
ipcMain.handle("window:minimize", () => {
|
|
mainWindow?.minimize();
|
|
});
|
|
|
|
ipcMain.handle("window:toggle-maximize", () => {
|
|
if (!mainWindow) {
|
|
return false;
|
|
}
|
|
|
|
if (mainWindow.isMaximized()) {
|
|
mainWindow.unmaximize();
|
|
} else {
|
|
mainWindow.maximize();
|
|
}
|
|
|
|
return mainWindow.isMaximized();
|
|
});
|
|
|
|
ipcMain.handle("window:close", () => {
|
|
mainWindow?.close();
|
|
});
|
|
|
|
ipcMain.handle("window:is-maximized", () => {
|
|
return mainWindow?.isMaximized() ?? false;
|
|
});
|
|
|
|
ipcMain.handle("tileset:save", async (_event, fileBuffer, suggestedName) => {
|
|
if (!(fileBuffer instanceof Uint8Array)) {
|
|
throw new TypeError("File buffer must be a Uint8Array");
|
|
}
|
|
if (typeof suggestedName !== "string") {
|
|
throw new TypeError("Suggested name must be a string");
|
|
}
|
|
|
|
const tilesetsDir = getTilesetsDirectory();
|
|
await fs.mkdir(tilesetsDir, { recursive: true });
|
|
|
|
// Ensure unique filename
|
|
let fileName = suggestedName.endsWith(".png") ? suggestedName : `${suggestedName}.png`;
|
|
let filePath = path.join(tilesetsDir, fileName);
|
|
let counter = 1;
|
|
|
|
while (await fs.access(filePath).then(() => true).catch(() => false)) {
|
|
const nameWithoutExt = suggestedName.replace(/\.png$/i, "");
|
|
fileName = `${nameWithoutExt}_${counter}.png`;
|
|
filePath = path.join(tilesetsDir, fileName);
|
|
counter++;
|
|
}
|
|
|
|
await fs.writeFile(filePath, Buffer.from(fileBuffer));
|
|
|
|
return { filePath, fileName };
|
|
});
|
|
|
|
ipcMain.handle("tileset:show-in-folder", async (_event, filePath) => {
|
|
if (typeof filePath !== "string") {
|
|
throw new TypeError("File path must be a string");
|
|
}
|
|
|
|
shell.showItemInFolder(filePath);
|
|
});
|
|
|
|
ipcMain.handle("tileset:reload", async (_event, filePath) => {
|
|
if (typeof filePath !== "string") {
|
|
throw new TypeError("File path must be a string");
|
|
}
|
|
|
|
const buffer = await fs.readFile(filePath);
|
|
return { buffer: Array.from(buffer) };
|
|
});
|
|
|
|
ipcMain.handle("cart:get-stats", async (_event, cartContent) => {
|
|
if (typeof cartContent !== "string") {
|
|
throw new TypeError("Cart content must be a string");
|
|
}
|
|
|
|
// Write cart to a temporary file
|
|
const tempDir = app.getPath("temp");
|
|
const tempCartPath = path.join(tempDir, `stats_${Date.now()}.p8`);
|
|
|
|
try {
|
|
await fs.writeFile(tempCartPath, cartContent, "utf8");
|
|
|
|
// Run p8tool stats command
|
|
const picotoolPath = path.join(__dirname, "picotool");
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const p8tool = spawn("p8tool", ["stats", tempCartPath], {
|
|
cwd: __dirname,
|
|
shell: true
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
|
|
p8tool.stdout.on("data", (data) => {
|
|
stdout += data.toString();
|
|
});
|
|
|
|
p8tool.stderr.on("data", (data) => {
|
|
stderr += data.toString();
|
|
});
|
|
|
|
p8tool.on("close", async (code) => {
|
|
// Clean up temp file
|
|
try {
|
|
await fs.unlink(tempCartPath);
|
|
} catch (cleanupError) {
|
|
console.warn("Failed to delete temp cart:", cleanupError);
|
|
}
|
|
|
|
if (code !== 0) {
|
|
reject(new Error(`p8tool stats exited with code ${code}: ${stderr}`));
|
|
return;
|
|
}
|
|
|
|
// Parse the output to extract token count
|
|
// Expected format: "version: 0 lines: 48 chars: 419 tokens: 134"
|
|
const tokenMatch = stdout.match(/tokens:\s*(\d+)/);
|
|
const linesMatch = stdout.match(/lines:\s*(\d+)/);
|
|
const charsMatch = stdout.match(/chars:\s*(\d+)/);
|
|
|
|
resolve({
|
|
tokens: tokenMatch ? parseInt(tokenMatch[1], 10) : 0,
|
|
lines: linesMatch ? parseInt(linesMatch[1], 10) : 0,
|
|
chars: charsMatch ? parseInt(charsMatch[1], 10) : 0,
|
|
rawOutput: stdout
|
|
});
|
|
});
|
|
|
|
p8tool.on("error", (error) => {
|
|
reject(new Error(`Failed to run p8tool: ${error.message}`));
|
|
});
|
|
});
|
|
} catch (error) {
|
|
// Clean up temp file if it exists
|
|
try {
|
|
await fs.unlink(tempCartPath);
|
|
} catch (_) {
|
|
// Ignore cleanup errors
|
|
}
|
|
throw error;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wires core Electron lifecycle events for the application.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
function registerAppLifecycle() {
|
|
app.whenReady().then(() => {
|
|
Menu.setApplicationMenu(null);
|
|
createMainWindow();
|
|
registerIpcHandlers();
|
|
|
|
app.on("activate", () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
createMainWindow();
|
|
}
|
|
});
|
|
});
|
|
|
|
app.on("window-all-closed", () => {
|
|
if (process.platform !== "darwin") {
|
|
app.quit();
|
|
}
|
|
});
|
|
}
|
|
|
|
registerAppLifecycle();
|