Skip to content
filedom
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.

Was this page helpful?