diff --git a/electron-main.js b/electron-main.js index fff6849..e6cee67 100644 --- a/electron-main.js +++ b/electron-main.js @@ -15,9 +15,10 @@ try { // Ignore errors in production (when electron-reloader isn't installed) } -const { app, BrowserWindow, ipcMain, Menu } = require("electron"); +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"; @@ -40,6 +41,16 @@ 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; @@ -54,6 +65,7 @@ function createMainWindow() { height: 900, minWidth: 960, minHeight: 640, + frame: false, webPreferences: { contextIsolation: true, nodeIntegration: false, @@ -78,6 +90,13 @@ function createMainWindow() { } }); + const emitMaximizedState = () => { + window.webContents.send("window:maximized-changed", window.isMaximized()); + }; + + window.on("maximize", emitMaximizedState); + window.on("unmaximize", emitMaximizedState); + window.on("closed", () => { mainWindow = null; }); @@ -135,6 +154,151 @@ function registerIpcHandlers() { 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; + } + }); } /** diff --git a/index.html b/index.html index d72eb37..be86b9b 100644 --- a/index.html +++ b/index.html @@ -3,18 +3,23 @@
- +