wm 1.0.1, crash handler demo, basic bundling support

This commit is contained in:
Annie 2025-03-07 21:50:42 +03:00
parent 349a9ddc0f
commit ac2b338f13
10 changed files with 244 additions and 191 deletions

View file

@ -2,17 +2,30 @@
"name": "neofunkin-1",
"version": "1.0.0",
"description": "",
"main": "index.js",
"main": "dist/index.js",
"build": {
"appId": "asper.games.neofunkin",
"productName": "NeoFunkin [EARLY ALPHA]",
"files": [
"dist/**/*"
],
"win": {
"target": "portable"
},
"linux": {
"target": "AppImage"
}
},
"scripts": {
"build": "tsc && node scripts/finalizeBuild.js",
"electron": "electron dist"
"electron": "electron dist",
"bundle": "electron-builder"
},
"keywords": [],
"author": "",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.4.1",
"dependencies": {
"electron": "^35.0.0",
"express": "^4.21.2"
},
"pnpm": {
@ -21,8 +34,10 @@
]
},
"devDependencies": {
"@types/electron": "^1.6.12",
"electron": "^35.0.0",
"@types/express": "^5.0.0",
"@types/node": "^22.13.9",
"electron-builder": "^25.1.8",
"fs-extra": "^11.3.0",
"typescript": "^5.8.2"
}

View file

@ -1,60 +1,72 @@
import wm from './lib/windowManager';
import { easeInOutQuad } from './lib/animations';
import { wait } from './lib/utils';
import { app } from 'electron';
import displayCrashScreen from './lib/crash';
import express from 'express';
import path from 'path';
import { createServer } from 'http';
app.on('ready', () => {
declare global {
namespace NodeJS {
interface Process {
nfenv?: any;
}
}
}
process.nfenv = {
serverPort: 65034 as number, // todo: pick a random one instead, not for now bcuz easier to debug and use /eval endpoint
crash: function (reason?: string) {
console.error('[nfcrash] Simulating a crash with reason:', reason || '*no reason*');
displayCrashScreen();
}
}
async function startWebRoot() {
const app = express();
const server = createServer(app);
app.use(express.static(path.join(__dirname, 'webroot')));
app.get('/eval', (req: express.Request, res: express.Response): any => {
const code = req.headers['x-code'] as string;
if (!code) return res.status(400).send('No code provided');
try {
const result = (function(require) {
return eval(code);
})(require);
res.status(200).send(result as string);
} catch (error: any) {
res.status(500).send(error.message as string);
}
});
// to-do: add echo webhook endpoint and endpoints to create more webhook endpoints (and also delete those)
server.listen(process.nfenv.serverPort, 'localhost');
};
startWebRoot().then(() => {
app.on('ready', async () => {
try {
const mw = wm.create({
w: 10,
h: 10,
w: 20,
h: 20,
whp: true,
onCreate: (win: any) => {
win.loadURL('https://example.com');
win.loadURL(`http://localhost:${process.nfenv.serverPort}/screens/mainmenu/test.html`);
},
});
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
(async () => {
await wait(200);
wm.resize({ id: mw, w: 25, h: 25, smooth: true, p: true, fromCenter: true, ease: easeInOutQuad});
await wait(1000);
wm.resize({ id: mw, w: 50, h: 50, smooth: true, p: true, fromCenter: true, ease: easeInOutQuad});
await wait(650);
wm.move({ id: mw, x: 0, y: 50, p: true, fromCenter: true});
wm.resize({ id: mw, w: 33, h: 100, p: true });
wm.resize({ id: mw, w: 33, h: 10, smooth: true, p: true, ease: easeInOutQuad, anchor: 'bottom'});
wm.move({ id: mw, x: 50, y: 50, p: true, fromCenter: true })
await wait(500);
wm.move({ id: mw, x: 50, y: 50, p: true, fromCenter: true});
wm.resize({ id: mw, w: 33, h: 100, p: true });
wm.resize({ id: mw, w: 33, h: 10, smooth: true, p: true, ease: easeInOutQuad, anchor: 'bottom'});
await wait(500);
wm.move({ id: mw, x: 100, y: 50, p: true, fromCenter: true});
wm.resize({ id: mw, w: 33, h: 100, p: true });
wm.resize({ id: mw, w: 33, h: 10, smooth: true, p: true, ease: easeInOutQuad, anchor: 'bottom'});
await wait(500);
wm.resize({ id: mw, w: 100, h: 100, smooth: true, p: true, fromCenter: true, ease: easeInOutQuad});
await wait(550);
wm.resize({ id: mw, w: 100, h: 10, smooth: true, p: true, ease: easeInOutQuad, anchor: 'bottom'});
await wait(550);
wm.resize({ id: mw, w: 33, h: 10, smooth: true, p: true, fromCenter: true, ease: easeInOutQuad, anchor: 'bottom'});
await wait(500);
wm.move({ id: mw, x: 50, y: 50, p: true, fromCenter: true, ease: easeInOutQuad, smooth: true});
await wait(500);
wm.resize({ id: mw, w: 50, h: 50, smooth: true, p: true, fromCenter: true, ease: easeInOutQuad});
await wait(510);
wm.eval(mw, `document.querySelector('body').innerHTML = '<h1>NeoFunkin</h1>'`);
await wait(500);
wm.eval(mw, `document.querySelector('body').innerHTML = '<h1>MrpGimlom</h1>'`);
await wait(100);
wm.eval(mw, `document.querySelector('body').innerHTML = '<h1>NeoFunkin: Window manager</h1>'`);
await wait(500);
wm.eval(mw, `document.querySelector('body').innerHTML = '<h1>NeoFunkin: Window manager</h1><br>DEMO BY @TRUE1ANN, NEOFUNKIN IS PROPERTY OF ASPER'`);
await wait(1500);
wm.eval(mw, `document.querySelector('body').innerHTML = '<h1>NeoFunkin: Window manager</h1><br>DEMO BY @TRUE1ANN, NEOFUNKIN IS PROPERTY OF ASPER<br><small>this took so much to do omfg</small>'`);
})();
wm.resize({ id: mw, w: 40, h: 40, smooth: true, p: true});
} catch (e) {
console.error(e);
process.nfenv.crash();
}
});
});
})

34
src/lib/crash.ts Normal file
View file

@ -0,0 +1,34 @@
import { wm, wdata } from './windowManager';
import { easeInOutQuad } from './animations';
import { wait, toP } from './utils'
import { app, screen } from 'electron';
export default async function displayCrashScreen() {
const bwdata = { ...wdata };
const win = wm.create({
w: 40,
h: 40,
whp: true,
onCreate: (win: any) => {
win.loadURL(`http://localhost:${process.nfenv.serverPort}/screens/crash/index.html`);
},
});
const button = wm.create({
w: 10,
h: 5,
whp: true,
noBorder: true,
noBackground: true,
onCreate: (win: any) => {
win.loadURL(`http://localhost:${process.nfenv.serverPort}/screens/crash/exit.html`);
},
});
await wait(300);
console.log('[nfcrash] Deleted all windows');
Object.keys(bwdata).forEach(async (id) => {
bwdata[id].win.destroy(); // Note: We dont use wm.destroy(...) so ...onDestroy wont get called and cause trouble
});
const { width: sw, height: sh } = screen.getPrimaryDisplay().bounds;
wm.move({ id: button, y: 75, x: 50, p: true, fromCenter: true });
wm.follow.start(button, win);
};

4
src/lib/utils.ts Normal file
View file

@ -0,0 +1,4 @@
export const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
export function toP(value: number, percentage: number) {
return value * (percentage / 100) as number;
}

View file

@ -1,17 +1,16 @@
import { BrowserWindow, screen } from 'electron';
import { BrowserWindow, Rectangle, screen } from 'electron';
import { wm_create_conf, wm_move_conf, wm_resize_conf } from './../types/wm';
import { toP } from './utils';
interface WindowData {
win: BrowserWindow;
conf: wm_create_conf;
follows?: string;
followStuff?: any;
}
let wdata: { [key: string]: WindowData } = {};
function toP(value: number, percentage: number) {
return value * (percentage / 100) as number;
}
const wm = {
create: function (c: wm_create_conf) {
const wid = Array.from({ length: 8 }, () => '0123456789abcdef'[Math.floor(Math.random() * 16)]).join('');
@ -21,7 +20,7 @@ const wm = {
width: c.whp ? toP(sw, c.w!) : c.w,
height: c.whp ? toP(sh, c.h!) : c.h,
x: c.xyp ? toP(sw, c.x!) : c.x,
y: c.xyp ? toP(sh, c.y!) : c.x,
y: c.xyp ? toP(sh, c.y!) : c.y,
frame: !c.noBorder,
transparent: c.noBackground,
webPreferences: {
@ -30,7 +29,7 @@ const wm = {
},
});
wdata[wid] = { win: win, conf: c }; // for further pullin
wdata[wid] = { win: win, conf: c };
win.setMenuBarVisibility(false); // tell me WHY should i NOT make this the default
if (typeof c.onMinimize === 'function') win.on('minimize', () => c.onMinimize!(win));
@ -43,7 +42,7 @@ const wm = {
if (c.onCreate) c.onCreate(win);
return wid // just so you can reference it later on
},
destroy: async function (id: number) {
destroy: async function (id: string) {
console.log('[wm] destroy', id);
const win = wdata[id];
if (typeof win.conf.onDestroy === 'function') await win.conf.onDestroy!(win.win);
@ -108,86 +107,85 @@ const wm = {
resize: function (c: wm_resize_conf) {
console.log('[wm] resize', c.id);
const win = wdata[c.id];
let wb = win.win.getBounds();
if (!win || !win.win) {
console.error('Window data not found for id:', c.id);
return;
}
const wb = win.win.getBounds();
const sW = wb.width, sH = wb.height;
// Use the given dimensions if provided; otherwise fall back to the current window size
c.w = c.w === undefined ? sW : c.w;
c.h = c.h === undefined ? sH : c.h;
const duration = c.duration || 500;
const tick = c.tick || 16;
const totalSteps = Math.floor(duration / tick);
// Get primary display bounds make sure that screen is imported from Electron
const { width: sw, height: sh } = screen.getPrimaryDisplay().bounds;
let targetW = toP(c.p ? sw : 1, c.w);
let targetH = toP(c.p ? sh : 1, c.h);
// Calculate target width and height using percentage conversion if needed.
// Assuming toP converts a percentage value to pixels
const targetW = toP(c.p ? sw : 1, c.w);
const targetH = toP(c.p ? sh : 1, c.h);
// Compute window center for centering if needed
const centerX = wb.x + wb.width / 2;
const centerY = wb.y + wb.height / 2;
// Adjust position based on the chosen anchor.
const getAnchoredPosition = (curWidth: number, curHeight: number) => {
let newX = wb.x;
let newY = wb.y;
if (!c.fromCenter && c.anchor) {
switch (c.anchor) {
case 'top':
newX = wb.x;
// Align to top x remains same
newY = wb.y;
break;
case 'bottom':
newX = wb.x;
// Align to bottom
newY = wb.y + wb.height - curHeight;
break;
case 'left':
// Align to left y remains same
newX = wb.x;
newY = wb.y;
break;
case 'right':
// Align to right
newX = wb.x + wb.width - curWidth;
newY = wb.y;
break;
default:
newX = wb.x;
newY = wb.y;
}
// Default: center the resized window relative to the original bounds
newX = centerX - curWidth / 2;
newY = centerY - curHeight / 2;
}
return { newX, newY };
};
// Animation function to smooth the resize
const animate = () => {
let currentStep = 0;
const startW = wb.width;
const startH = wb.height;
const step = () => {
currentStep++;
const t = currentStep / totalSteps;
const t = Math.min(currentStep / totalSteps, 1);
const eT = c.ease ? c.ease(t) : t;
const newW = startW + (targetW - startW) * eT;
const newH = startH + (targetH - startH) * eT;
let newX: number, newY: number;
if (c.fromCenter) {
newX = Math.round(centerX - newW / 2);
newY = Math.round(centerY - newH / 2);
} else {
// Get new positions based on the current width and height
const pos = getAnchoredPosition(Math.round(newW), Math.round(newH));
newX = pos.newX;
newY = pos.newY;
}
win.win.setBounds({
x: newX,
y: newY,
x: pos.newX,
y: pos.newY,
width: Math.round(newW),
height: Math.round(newH)
});
if (currentStep < totalSteps) {
setTimeout(step, tick);
} else {
let finalX: number, finalY: number;
if (c.fromCenter) {
finalX = Math.round(centerX - targetW / 2);
finalY = Math.round(centerY - targetH / 2);
} else {
const pos = getAnchoredPosition(Math.round(targetW), Math.round(targetH));
finalX = pos.newX;
finalY = pos.newY;
}
// Make sure to set final bounds exactly
const finalPos = getAnchoredPosition(Math.round(targetW), Math.round(targetH));
win.win.setBounds({
x: finalX,
y: finalY,
x: finalPos.newX,
y: finalPos.newY,
width: Math.round(targetW),
height: Math.round(targetH)
});
@ -195,21 +193,15 @@ const wm = {
};
step();
};
// Use animate if smooth animation is requested, otherwise set bounds immediately.
if (c.smooth) {
animate();
} else {
let finalX: number, finalY: number;
if (c.fromCenter) {
finalX = Math.round(centerX - targetW / 2);
finalY = Math.round(centerY - targetH / 2);
} else {
const pos = getAnchoredPosition(Math.round(targetW), Math.round(targetH));
finalX = pos.newX;
finalY = pos.newY;
}
const finalPos = getAnchoredPosition(Math.round(targetW), Math.round(targetH));
win.win.setBounds({
x: finalX,
y: finalY,
x: finalPos.newX,
y: finalPos.newY,
width: Math.round(targetW),
height: Math.round(targetH)
});
@ -222,6 +214,42 @@ const wm = {
} catch (error) {
console.error('Error executing JavaScript:', error);
}
},
follow: {
INTERNAL_follow: function (id: string) {
const leadWin = wdata[wdata[id].follows!].win;
const followWin = wdata[id].win;
const leadBounds = leadWin.getBounds();
const followBounds = followWin.getBounds();
if (wdata[id].followStuff === undefined) {
wdata[id].followStuff = {};
}
if (wdata[id].followStuff.offsetX === undefined || wdata[id].followStuff.offsetY === undefined) {
wdata[id].followStuff.offsetX = followBounds.x - leadBounds.x;
wdata[id].followStuff.offsetY = followBounds.y - leadBounds.y;
}
const newX = leadBounds.x + wdata[id].followStuff.offsetX;
const newY = leadBounds.y + wdata[id].followStuff.offsetY;
followWin.setPosition(newX, newY);
followWin.show();
},
INTERNAL_followfocus: function (id: string) {
wdata[id].win.show();
},
start: function (id: string, follows: string) {
console.log('[wm]', id, 'follows', follows);
wdata[id].follows = follows;
wdata[follows].win.on('move', this.INTERNAL_follow.bind(this, id));
wdata[follows].win.on('focus', this.INTERNAL_followfocus.bind(this, id));
},
stop: function (id: string) {
wdata[wdata[id].follows!].win.removeListener('move', this.INTERNAL_follow.bind(this, id));
wdata[wdata[id].follows!].win.removeListener('focus', this.INTERNAL_followfocus.bind(this, id));
}
}
};

60
src/types/wm.d.ts vendored
View file

@ -1,60 +0,0 @@
import { BrowserWindow } from 'electron';
export type wm_create_conf = {
w?: number; // Width
h?: number; // Height
x?: number; // X start pos
y?: number; // Y start pos
whp?: boolean; // is w and h in percentages
xyp?: boolean; // is x and y in percentages
noBorder?: boolean; // Hide OS window decorations
noBackground?: boolean; // Hide window background
onCreate?: Function; // wm.create(...)
onMinimize?: Function; // USER pressed '_'
onMaximize?: Function; // USER pressed '[]'
onRestore?: Function; // USER pressed '[]]'
onUnfocus?: Function; // USER unfocused on the window
onFocus?: Function; // USER focused on the window
onClose?: Function; // USER pressed 'x'
onDestroy?: Function; // USER pressed 'x' OR wm.destroy(...)
};
export type wm_move_conf = {
id: string; // ID of the window to perform everythin on
x?: number; // New X
y?: number; // New Y
p?: boolean; // Use percentages
fromCenter?: boolean; // Should be performed from the center of the window?
smooth?: boolean; // Should it be smooth?
ease?: Function; // Calculates the easing (lib/animations.ts)
duration?: number; // (ms) The total duration of the animation
tick?: number; // (ms) Delay between window movenments
}
export type wm_resize_conf = {
id: string; // ID of the window to perform everythin on
w?: number; // New width
h?: number; // New height
p?: boolean; // Use percentages
fromCenter?: boolean; // Should be performed from the center of the window?
smooth?: boolean; // Should it be smooth?
ease?: Function; // Calculates the easing (lib/animations.ts)
duration?: number; // (ms) The total duration of the animation
tick?: number; // (ms) Delay between window resizings
}
interface WindowData {
win: BrowserWindow;
conf: wm_create_conf;
}
declare const wm: {
create: (c: wm_create_conf) => string;
destroy: (id: string) => Promise<void>;
move: (c: wm_move_conf) => void;
resize: (c: wm_resize_conf) => void;
eval: (id: string, code: Function) => Promise<any>;
};
export default wm;
export { WindowData, wm };

View file

@ -1,6 +1,6 @@
export type wm_create_conf = {
w?: number; // Width
h?: number; // Height
w: number; // Width
h: number; // Height
x?: number; // X start pos
y?: number; // Y start pos
whp?: boolean; // is w and h in percentages
@ -34,10 +34,9 @@ export type wm_resize_conf = {
w?: number; // New width
h?: number; // New height
p?: boolean; // Use percentages
fromCenter?: boolean; // Should be performed from the center of the window?
smooth?: boolean; // Should it be smooth?
ease?: Function; // Calculates the easing (lib/animations.ts)
duration?: number; // (ms) The total duration of the animation
tick?: number; // (ms) Delay between window resizings
anchor?: string; // bottom, top, left, right
anchor?: string; // bottom, top, left, right, center
}

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NeoFunkin</title>
<style>
body {
margin: 0;
padding: 0;
box-sizing: border-box;
}
</style>
</head>
<body>
<button style="width: 100vw; height: 100vh; margin: 0;" onclick="fetch('/eval', { headers: { 'x-code': 'process.exit(1)' } });">Exit</button>
</body>
</html>

View file

@ -5,5 +5,6 @@
</head>
<body>
<h1>Uh oh.</h1>
NeoFunkin crashed. Check logs for more details.
</body>
</html>

View file

@ -5,5 +5,7 @@
</head>
<body>
<h1>Hello, World!</h1>
<button onclick="fetch('/eval', { headers: { 'x-code': 'process.nfenv.crash(`funny`);' } });">Crash</button>
</body>
</html>