NovaOS Shell
A Mac OS 9-inspired fantasy operating system that runs entirely in your browser. Full window manager, virtual filesystem, app framework, and a boot sequence that makes you feel like it’s 1999.
💻 What is NovaOS
NovaOS is a fully functional retro operating system that ships alongside the Nova64 console. It draws inspiration from Mac OS 9’s Platinum theme, PS1 system menus, and early-2000s dark desktop aesthetics.
It runs entirely in the browser with no server or install required. Backed by IndexedDB for file persistence, React 18 + Zustand for UI state, and a clean TypeScript API that any nova64 cart can call via the global novaContext object.
Core Features
- Draggable, resizable windows with Mac OS 9-style chrome (close, zoom, windowshade)
- Virtual POSIX-like filesystem with IndexedDB persistence across page reloads
- App framework with mount/unmount lifecycle and menu bar integration
- Pub/sub event bus for OS, apps, and cart code to communicate
- Control strip with volume, brightness, scanlines, FPS counter, and custom widgets
- Modal alert dialogs with icon variants and auto-dismissing toast notifications
- Persistent user preferences that survive browser restarts
- Mobile-optimised shell variant in
os9-shellMobile/
Technology Stack
| Layer | Technology | Purpose |
|---|---|---|
| UI | React 18 | Component-based window & desktop rendering |
| Types | TypeScript 5 (strict) | Zero type errors, full public interfaces |
| State | Zustand 4 | Separate stores: windows, apps, menus, ui, system |
| Storage | IndexedDB — idb-keyval | Filesystem & preference persistence |
| Build | Vite 5 | HMR dev server, optimised production bundle |
⚡ Quick Start
Install dependencies
Move into the shell folder and run pnpm install.
Start the dev server
Vite serves NovaOS at http://localhost:3000 with hot-module reload.
Watch the boot sequence
Gray splash → extension icons march → desktop fades in with icons and menu bar.
Access the live API
Press F12 and call await novaContext.launchApp('com.nova64.notes') in the console.
cd os9-shell pnpm install pnpm dev # → http://localhost:3000 (HMR) # Production build pnpm build # output: os9-shell/dist/ pnpm preview # serve dist/ on http://localhost:4173
novaContext object is your live handle to every API — all methods in this document can be called directly from DevTools during development.🔌 Boot Sequence
NovaOS boots through a scripted sequence mirroring classic Mac OS 9 startup:
- Gray splash screen — Nova64 logo centred on platinum gray
- Extensions parade — icon badges march across a bottom bar (like OS 9 extensions)
- Desktop reveal — fades in with Trash, disk icons, and app aliases visible
- Menu bar activates — clock starts ticking, app menus become interactive
- Control strip slides in — bottom-right panel collapses in from the viewport edge
Full boot takes ~3–4 seconds. On subsequent loads, IndexedDB restores all filesystem contents, window positions, and preferences exactly as they were left.
🏠 Desktop & Icons
The desktop is an icon-based canvas layer above the wallpaper. Icons support:
- Click — select icon (border highlight)
- Double-click — open associated app
- Aliases — created programmatically via
ctx.createAlias()
Special icons present at first boot: Trash (bottom-right) and Hard Disk (top-right, represents the virtual root volume).
⚙️ Control Strip
A collapsible panel anchored to the bottom-right. Click the arrow tab to roll it in/out.
| Widget | Function |
|---|---|
| Volume | Slider — adjusts system volume preference |
| Brightness | Slider — CSS filter brightness on desktop layer (functional) |
| Scanlines | Toggle — enables/disables the CRT scanline overlay |
| FPS Counter | Toggle — shows live FPS readout top-right |
| Collapse tab | Rolls the strip to a single indicator |
Register a custom widget
novaContext.registerControlStrip({
id: 'com.myapp.widget',
icon: '🎮',
label: 'My Widget',
onClick: () => novaContext.toast('Widget activated!'),
});
// Widget with live DOM render function
novaContext.registerControlStrip({
id: 'com.myapp.clock',
icon: '⏱️',
label: 'Clock',
render: container => {
const el = document.createElement('span');
el.style.cssText = 'font-family:monospace;font-size:12px;color:#0f0';
container.appendChild(el);
let s = 0;
setInterval(() => {
const mm = String(Math.floor(s / 60)).padStart(2, '0');
const ss = String(s++ % 60).padStart(2, '0');
el.textContent = mm + ':' + ss;
}, 1000);
},
});
🪟 Window Manager
All window state lives in the Zustand windows store. novaContext exposes it through three imperative methods.
User interactions
- Drag title bar — moves window, constrained to viewport
- Drag bottom-right corner — resizes window freely
- Close box (left button) — destroys window, triggers
app.unmount() - Zoom box (right button) — maximises or restores previous size
- Double-click title bar — windowshade: collapses to title bar; double-click again to restore
- Click inside window — brings to front (z-order update)
| Method | Parameters | Returns | Description |
|---|---|---|---|
| createWindow | Partial<WindowState> | string | Create a window. Returns the window ID. |
| closeWindow | windowId: string | void | Close & destroy a window (calls app.unmount). |
| focusWindow | windowId: string | void | Bring a window to the top of the z-stack. |
const id = novaContext.createWindow({
title: 'My Window', x: 200, y: 120, width: 500, height: 360,
content: '<div style="padding:20px"><h2>Hello NovaOS!</h2></div>',
});
novaContext.focusWindow(id);
novaContext.closeWindow(id);
WindowState interface
interface WindowState {
id: string;
title: string;
x: number; // desktop left offset (px)
y: number; // desktop top offset (px)
width: number;
height: number;
zIndex: number; // managed by OS automatically
active: boolean;
shaded: boolean; // windowshade collapsed
maximized: boolean;
content: string; // inner HTML (apps inject via mount())
appId: string | null;
}
📁 Virtual Filesystem
A POSIX-like VFS backed by IndexedDB via idb-keyval. All operations are async. Pre-seeded on first boot: /System, /Applications, /Users/Player/{Desktop,Documents,Pictures,Library/Preferences}, /Trash.
| Method | Signature | Description |
|---|---|---|
| read | (path) → Promise<string|ArrayBuffer> | Read a file. String for text, ArrayBuffer for binary. |
| write | (path, data) → Promise<void> | Write string or ArrayBuffer. Parent dirs auto-created. |
| mkdir | (path) → Promise<void> | Create directory tree recursively (like mkdir -p). |
| rm | (path, opts?) → Promise<void> | Delete file or dir. Pass { recursive: true } for trees. |
| stat | (path) → Promise<FileStat> | Returns: path, type, size, created, modified. |
| readdir | (path) → Promise<string[]> | List filenames in a directory. |
| exists | (path) → Promise<boolean> | Check whether a path exists. |
| createAlias | (target, alias) → Promise<void> | Create a symbolic link: alias → target. |
| resolveAlias | (path) → Promise<string> | Resolve an alias path to its real target. |
const ctx = window.novaContext;
await ctx.write('/Users/Player/Documents/hello.txt', 'Hello, NovaOS!');
const txt = await ctx.read('/Users/Player/Documents/hello.txt'); // → "Hello, NovaOS!"
const files = await ctx.readdir('/Users/Player/Documents'); // → ["hello.txt"]
const info = await ctx.stat('/Users/Player/Documents/hello.txt');
// → { path, type: 'file', size: 14, created: Date, modified: Date }
await ctx.createAlias('/Applications', '/Users/Player/Desktop/Apps');
await ctx.resolveAlias('/Users/Player/Desktop/Apps'); // → '/Applications'
await ctx.rm('/Users/Player/Documents/hello.txt');
await ctx.rm('/Users/Player/Projects', { recursive: true });
nova64-fs. Clearing site data in browser settings will wipe the virtual filesystem.🚀 App Framework
Register any object implementing Nova64App. Registered apps can be launched to open windows with full menu bar integration and lifecycle callbacks.
Nova64App interface
interface Nova64App {
id: string; // reverse-DNS, e.g. 'com.yourname.appname'
name: string; // display name shown in menu bar & window title
icon: string; // emoji or image URL for desktop icon
menus?: AppMenu[];
mount(el: HTMLElement, ctx: NovaContext): void | Promise<void>;
unmount(): void;
onEvent?(evt: NovaEvent): void;
}
const myApp = {
id: 'com.example.hello', name: 'Hello App', icon: '👋',
menus: [{
label: 'File',
submenu: [
{ id: 'new', label: 'New', accelerator: '⌘N' },
{ id: 'save', label: 'Save', accelerator: '⌘S' },
],
}],
mount(el, ctx) {
el.innerHTML = '<div style="padding:24px;text-align:center">👋 Hello!</div>';
el.querySelector('div').addEventListener('click', () => ctx.toast('Hi!'));
},
unmount() {},
};
novaContext.registerApp(myApp);
await novaContext.launchApp('com.example.hello');
const running = novaContext.getRunningApps();
novaContext.quitApp('com.example.hello');
launchApp() with an already-running ID focuses the existing window rather than opening a duplicate.📡 Event System
A lightweight pub/sub bus. Any component, app, or cart code can subscribe to and emit events using a string type.
| Event type | Payload | When fired |
|---|---|---|
| app:registered | { appId } | App registered with OS |
| app:launched | { appId } | App window opens |
| app:quit | { appId } | App window closes |
| fs:changed | { path, op } | Any filesystem write or delete |
| window:created | { windowId } | createWindow() returns |
| window:closed | { windowId } | Window is destroyed |
| window:focused | { windowId } | Window brought to front |
// Subscribe — returns unsubscribe fn
const off = novaContext.on('app:launched', evt => {
console.log('App launched:', evt.payload.appId);
});
off(); // unsubscribe
// Wildcard — all events
novaContext.on('*', evt => console.log(evt.type, evt.payload));
// Emit a custom event
novaContext.emit({ type: 'game:score', payload: { score: 9999 }, timestamp: Date.now() });
novaContext.on('game:score', ({ payload }) => updateScoreboard(payload.score));
💬 UI & Toast
Alert dialogs
ctx.alert() opens a modal and resolves with the label of the button clicked.
// Info
await ctx.alert({ title: 'File Saved', message: 'Saved to Documents.', buttons: ['OK'] });
// Destructive confirmation
const choice = await ctx.alert({
title: 'Delete file?', message: 'This cannot be undone.',
buttons: ['Cancel', 'Delete'], icon: 'warning',
});
if (choice === 'Delete') { /* proceed */ }
// Error
await ctx.alert({ title: 'Read Error', message: 'Disk may be damaged.', buttons: ['OK'], icon: 'error' });
Toast
ctx.toast('File saved!'); // auto-dismisses after ~3 s
ctx.toast('3 new messages');
ctx.toast('Connection restored');
💾 Preferences
JSON-serialisable values stored at /Users/Player/Library/Preferences/ via the VFS. Both methods are async.
| Method | Signature | Description |
|---|---|---|
| getPref | (key: string) → Promise<any> | Read a preference. Returns undefined if not set. |
| setPref | (key, value) → Promise<void> | Write a preference (JSON-serialisable values only). |
const vol = await ctx.getPref('volume');
await ctx.setPref('volume', 0.5);
await ctx.setPref('scanlines', false);
await ctx.setPref('brightness', 0.9);
// Namespace app keys to avoid collisions
await ctx.setPref('com.example.myapp.theme', 'dark');
await ctx.setPref('com.example.myapp.fontSize', 14);
⌨️ Keyboard Shortcuts
📦 Built-in Applications
Three apps ship with NovaOS. Launch from the browser console or future desktop double-click:
Notes com.nova64.notes
Plain-text editor. Auto-saves to /Users/Player/Documents/Notes.txt. File menu with Save and Clear actions.
Paint com.nova64.paint
Canvas drawing app. Colour picker, three brush sizes, eraser, and Clear Canvas. Classic Mac OS palette layout.
System Profiler com.nova64.profiler
Live browser info: user agent, platform, screen resolution, CPU cores, available JS heap memory.
await novaContext.launchApp('com.nova64.notes');
await novaContext.launchApp('com.nova64.paint');
await novaContext.launchApp('com.nova64.profiler');
🏗️ Build & Deploy
Standard Vite app. Output to os9-shell/dist/ — deploy to any CDN or static host.
cd os9-shell pnpm dev # HMR → http://localhost:3000 pnpm build # output: os9-shell/dist/ pnpm preview # serve dist/ on http://localhost:4173
Source tree
os9-shell/ ├── src/ │ ├── apps/ # Notes, Paint, System Profiler │ ├── components/ # Window, MenuBar, Desktop, ControlStrip, ... │ ├── os/ # filesystem, event bus, Zustand stores, context │ ├── theme/ # Platinum CSS design tokens │ ├── types/ # TypeScript interfaces & enums │ └── main.tsx # React root ├── public/ ├── dist/ # Production build output └── vite.config.ts
📱 Mobile Shell
Touch-optimised variant in os9-shellMobile/. Same tech stack and APIs, but menu-based navigation suited to small screens — no draggable desktop.
cd os9-shellMobile pnpm install && pnpm dev # http://localhost:3000 pnpm build # os9-shellMobile/dist/
Nova64 Fantasy Console © 2026 · MIT License