← Back to Docs
Bonus Disc · System Software

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.

React 18TypeScript 5Zustand 4 IndexedDBVite 5176 KB · 56 KB gz

💻 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

LayerTechnologyPurpose
UIReact 18Component-based window & desktop rendering
TypesTypeScript 5 (strict)Zero type errors, full public interfaces
StateZustand 4Separate stores: windows, apps, menus, ui, system
StorageIndexedDB — idb-keyvalFilesystem & preference persistence
BuildVite 5HMR dev server, optimised production bundle

⚡ Quick Start

1

Install dependencies

Move into the shell folder and run pnpm install.

2

Start the dev server

Vite serves NovaOS at http://localhost:3000 with hot-module reload.

3

Watch the boot sequence

Gray splash → extension icons march → desktop fades in with icons and menu bar.

4

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
Pro tip: Keep the browser console open. The global 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.

WidgetFunction
VolumeSlider — adjusts system volume preference
BrightnessSlider — CSS filter brightness on desktop layer (functional)
ScanlinesToggle — enables/disables the CRT scanline overlay
FPS CounterToggle — shows live FPS readout top-right
Collapse tabRolls 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)
MethodParametersReturnsDescription
createWindowPartial<WindowState>stringCreate a window. Returns the window ID.
closeWindowwindowId: stringvoidClose & destroy a window (calls app.unmount).
focusWindowwindowId: stringvoidBring 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.

MethodSignatureDescription
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 });
Persistence: Data lives in IndexedDB under 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');
App IDs are unique. Calling 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 typePayloadWhen 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.

MethodSignatureDescription
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

Focus menu barF10
App switcher⌘ Tab
Close focused window⌘ W
Dismiss menu / dialogEsc
Windowshade toggledbl-click title
Quit active app⌘ Q
New (in active app)⌘ N
Save (in active app)⌘ S
Cut / Copy / Paste⌘ X / C / V

📦 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/
Beta: Core filesystem and app APIs are stable; navigation chrome and touch gestures are still iterating.

Nova64 Fantasy Console © 2026 · MIT License

← All Docs  ·  Launch NovaOS ↗  ·  GitHub ↗