Getting Started
Install FileDOM and run your first game.
Requires macOS, Node.js 20+, and React 18.
Install
pnpm add filedom react@18.3.1 react-reconciler@0.29.2
pnpm add -D tsx
Check permissions
pnpm exec filedom doctor
Grant the Automation and Accessibility permissions it asks for under System Settings → Privacy & Security.
Render your first game
Save this as pong.tsx. Each paddle and the ball is a real file on your Desktop.
import path from 'node:path';
import { useState } from 'react';
import {
defineAssets, solid, startSession, Stage, StageProvider, Sprite, Wallpaper,
useDesktopGameShell, useFixedTick, useGameControls,
} from 'filedom';
const FIELD = '#1c1c1e';
// Icons are baked PNGs: build them once, before anything renders.
const assets = defineAssets({
iconDir: path.resolve('pong-icons'),
icons: {
ball: (c) => c.circle({ r: c.canvas * 0.3, fill: '#ffffff' }),
paddle: solid('#0a84ff'),
},
});
assets.build();
function Pong() {
const { metrics } = useDesktopGameShell({ deriveGeometry: (m) => m, showDesktop: true });
const [left, setLeft] = useState(400);
const [right, setRight] = useState(400);
const [ball, setBall] = useState({ x: 600, y: 400, vx: 24, vy: 16 });
useGameControls({
shortcuts: [
{ keys: ['w', 's'], label: 'LEFT', onPress: (k) => setLeft((y) => y + (k === 'w' ? -48 : 48)) },
{ keys: ['i', 'k'], label: 'RIGHT', onPress: (k) => setRight((y) => y + (k === 'i' ? -48 : 48)) },
],
onQuit: () => void session.then((s) => s.exit()),
});
useFixedTick(() => {
if (!metrics) return;
setBall(({ x, y, vx, vy }) => {
x += vx;
y += vy;
if (y < 0 || y > metrics.height) vy = -vy;
const paddleY = vx < 0 ? left : right;
const atEdge = vx < 0 ? x < metrics.iconSize : x > metrics.width - metrics.iconSize;
if (atEdge && Math.abs(y - paddleY) < metrics.iconSize) vx = -vx;
// Missed: serve again from the centre.
if (x < 0 || x > metrics.width) return { x: metrics.width / 2, y: metrics.height / 2, vx: -vx, vy };
return { x, y, vx, vy };
});
}, 50, Boolean(metrics));
if (!metrics) return null;
return (
<StageProvider metrics={metrics} park={{ x: metrics.width / 2, y: metrics.iconSize }} background={FIELD}>
<Wallpaper source={{ kind: 'color', value: FIELD }} />
<Stage prebake={[assets.icon('paddle')]}>
<Sprite icon={assets.icon('paddle')} at={{ x: metrics.iconSize / 2, y: left }} />
<Sprite icon={assets.icon('paddle')} at={{ x: metrics.width - metrics.iconSize / 2, y: right }} />
</Stage>
{/* Mounted last so the ball draws above every other file. */}
<Stage prebake={[assets.icon('ball')]}>
<Sprite icon={assets.icon('ball')} at={ball} />
</Stage>
</StageProvider>
);
}
const session = startSession(<Pong />);
pnpm exec tsx pong.tsx
W/S and I/K move the paddles. Q quits and shows your Desktop items again.
The full game, with scores and sound, is on the Pong page. If nothing appears, see Troubleshooting.