Skip to content

Repository files navigation

MarkCodesOS — a personal website as a mini operating system

A personal site that boots inside a browser tab. The whole thing is written in freestanding C, compiled to WebAssembly, and painted into a single <canvas> one pixel at a time.

There is no HTML for the UI. No React. No CSS layout inside. Every window, titlebar, icon, and character of text is drawn by C code into a software framebuffer.

  • Kernel + apps: ~13,000 lines of freestanding C
  • Binary: ~59 KB gzipped os.wasm (-Oz -flto + wasm-opt)
  • JS shim: ~430 lines — loads the module, forwards input (keyboard + mouse + touch), blits the framebuffer once per frame, and provides imports (clock, fetch, image decode, keyed localStorage, Web Audio, clipboard)
  • Runtime deps: none (no libc, no runtime)

Screenshots

Everything below — desktop, dock, menu bar, windows, text, the game board — is drawn by C into a software framebuffer. No DOM, no <div>s.

Desktop Terminal running neofetch
Desktop — icons, dock, menu-bar clock Terminalneofetch in the built-in shell
2048 Booting bare-metal in QEMU
2048 — a windowed app over the terminal Same OS, bare metal — booted from os.elf in QEMU (i686)

The last shot is the identical OS core booting on bare-metal x86 (no browser, no JS) — the platform ABI (src/include/host.h) is the only thing that differs. Render a still headlessly with the snippet under "Snapshots" below, or drive it in a real browser for the interactive shell.

Try it

npm run dev     # builds os.wasm, assembles dist/, serves it
open http://localhost:8000

dist/ is the only servable tree (the web sources live in src/platform/web/, and main.js loads os.wasm as a sibling). make serve does the same thing.

Then:

  • Double-click a desktop icon, or click one in the dock, to launch an app.
  • Spotlight: press ⌘K / Ctrl+K (or click the menu-bar magnifier) to search apps + documents; type to filter, ↑/↓ to move, Enter to launch, Esc to close.
  • Menus: click the MarkCodesOS brand for the Apple menu (About / running windows / Restart / Shut Down). Right-click the desktop for a context menu, or right-click a dock icon for Show / Quit / Open.
  • Windows: drag a titlebar; grab the bottom-right corner to resize. Double-click a titlebar to zoom. Drag a window to a screen edge to snap (top = maximize, left/right = half). The three dots are close (red) / minimise (yellow) / zoom (green); minimise flies to the dock.
  • Change Desktop (right-click menu) cycles the wallpaper tint. Your wallpaper + open windows are saved and restored across reloads.
  • Type help in Terminal for the command list. Try ls, cd apps, cat about.txt, open snake, cd ../games, open 2048.
  • Games: Snake (arrow keys/WASD, Enter to start, P to pause), 2048, Minesweeper (left-click reveal, right-click flag), Tetris, Pong, Breakout, Tic-tac-toe. Browse them in the Files app under games/ or search via Spotlight.
  • Apps: Settings (wallpaper + sound toggle), Calculator, Pixel-paint (64×64 canvas, auto-saves, copy to clipboard).

Directory layout

new-website/
├── Makefile                 # wasm build (os.wasm)
├── Makefile.x86             # bare-metal i686 build (os.elf, boots in QEMU)
├── sources.mk               # shared core+apps source list (both builds include it)
├── os.wasm                  # wasm build output (~59 KB gzipped)
├── posts.json               # blog posts (fetched at runtime)
├── videos.json              # video list (fetched at runtime)
├── toolchain/wasi-sdk/      # local WASI SDK 33 (clang 22 + wasm-ld)
└── src/
    ├── include/
    │   ├── os.h             # shared types, palette, RGB() macro, api
    │   ├── host.h           # the platform ABI contract (host_* fns) — one source of truth
    │   └── font_data.h      # 6x8 bitmap font (printable ASCII + icon glyphs)
    ├── platform/            # the two ports; each supplies the host_* ABI
    │   ├── web/             # web port: index.html + main.js shim + style.css
    │   └── x86/             # bare-metal port: boot.S, kernel.c, host.c, linker.ld
    ├── main.c               # wasm exports + boot + power (shutdown/restart) + app registry
    ├── runtime.c            # memcpy/memset/strlen/... + xorshift RNG + sin/cos LUT
    ├── memory.c             # 16 MiB bump arena
    ├── framebuffer.c        # put_pixel / rect / hline / circle / rounded / clip stack
    ├── font.c               # text_draw, text_draw_big
    ├── theme.c              # panels, titlebars, buttons, desktop bg + wallpaper presets
    ├── input.c              # global mouse state
    ├── wm.c                 # window manager (drag / resize / snap / zoom / animations)
    ├── vfs.c                # in-memory filesystem tree (apps/ + games/ + logs/)
    ├── shell.c              # desktop icons + menu bar + dock + menus + wall-clock
    ├── spotlight.c          # ⌘K search overlay (filter + launch apps/docs)
    ├── session.c            # persist wallpaper + open windows to localStorage
    ├── store.c              # keyed localStorage wrapper (notes, high scores, prefs)
    ├── snd.c                # Web Audio tone/noise effects (gated by g_sound_on)
    ├── clip.c               # clipboard write via the host ABI
    ├── net.c                # async fetch via the host ABI
    ├── img.c                # image decode (host) + scale/blit
    ├── json.c               # JSON parser (blog posts + videos)
    └── apps/
        ├── text_body.c      # shared word-wrapped reader (about/projects/contact/resume/readme)
        ├── terminal.c       # scrollback + cursor + shell parser
        ├── files.c          # icon grid file explorer
        ├── clock.c          # analog + digital (real wall-clock)
        ├── notes.c          # editable sticky note (auto-saved)
        ├── minigame.c       # snake (hi-score + pause)
        ├── blog.c           # blog index + reader (posts.json)
        ├── videos.c         # talks/videos with thumbnails (videos.json)
        ├── settings.c       # wallpaper presets + sound toggle
        ├── calc.c           # integer calculator
        ├── paint.c          # 64x64 pixel editor (auto-save + clipboard export)
        ├── g2048.c          # 2048
        ├── mines.c          # minesweeper
        ├── tetris.c         # tetris
        ├── pong.c           # pong
        ├── breakout.c       # breakout
        └── ttt.c            # tic-tac-toe

Architecture

The platform ABI (src/include/host.h)

The OS core is platform-agnostic. Everything it needs from the outside world it gets through a small host_* ABI declared in host.h. Each port supplies it:

  • web (src/platform/web/main.js): the functions are WASM imports in env.
  • x86 (src/platform/x86/host.c): ordinary C functions of the same name, so the core links unchanged on bare metal.

Web port (src/platform/web/main.js)

The entire JS surface is:

  1. Allocate a WebAssembly.Memory({ initial: 512, maximum: 512 }) = 32 MiB.
  2. Instantiate os.wasm with that memory imported into env.memory.
  3. Call init(width, height) once. Call on_resize(w, h) on window resize.
  4. Read the exported fb_ptr() and wrap it as an ImageData backed directly by WASM memory (zero-copy per frame).
  5. Each requestAnimationFrame: call frame(delta_ms), then ctx.putImageData(imageData, 0, 0).
  6. Forward mousemove/down/up/wheelon_mouse(x, y, buttons, wheel, kind).
  7. Forward keydown/keyupon_key(keycode, ch, mods, down).

No app logic, no drawing, no state. main.js also implements the host_* ABI (src/include/host.h) as WASM imports (all resolved via -Wl,--allow-undefined, so the core just includes host.h):

host_wall()                              # packed local wall-clock (see menu-bar clock)
host_fetch(urlPtr,len,reqId)             # async fetch -> on_fetch_done()
host_open_url(urlPtr,len)                # open external link in a new tab
host_load_image(urlPtr,len,reqId,w,h)    # decode image -> on_image_done()
host_load(keyPtr,keyLen,ptr,cap) -> len  # read localStorage[key] into WASM
host_save(keyPtr,keyLen,ptr,len)         # write localStorage[key]
host_del_key(keyPtr,keyLen)              # remove localStorage[key]
host_tone(freq,durMs,wave,vol)           # Web Audio tone (wave: 0-3, vol: 0-255)
host_noise(durMs,vol)                    # Web Audio white-noise burst
host_clip_write(ptr,len)                 # navigator.clipboard.writeText
host_clip_request_read()                 # async clipboard read -> on_clip_paste()

Because these are hard imports, any headless host (see "Snapshots") must provide stubs for at least host_wall, host_load, and host_save — they are called every frame / at boot, and a missing import is a LinkError.

WASM exports

init(w, h)
on_resize(w, h)
frame(dt_ms)
on_mouse(x, y, buttons, wheel, kind)   # kind: 0 move 1 down 2 up 3 wheel
on_key(keycode, ch, mods, down)        # mods bits: 1 shift 2 ctrl 4 alt 8 meta
fb_ptr() -> u32                        # pointer to RGBA framebuffer

Framebuffer format

u32* array of W*H pixels. Each u32 is little-endian [R, G, B, A] byte order — which is what ImageData reads directly. Use the RGB(r,g,b) macro in os.h to build colors so the byte order stays correct.

Max resolution is 1920 × 1200 (s_fb_storage in framebuffer.c); JS caps canvas.width/height accordingly, then CSS scales it up.

Window manager

  • Windows are Window structs in a fixed pool of 12.
  • s_order[] holds back-to-front slot order; the last entry is focused.
  • Drawing walks s_order[] back to front, sets a clip rect to each window's content region, and calls the app's on_draw(w).
  • Input hit-tests top-down; the topmost hit gets focus + the event. Titlebar buttons and resize grip are checked first.
  • Apps register a static const App with on_init/on_draw/on_event/on_tick.

Transitions. Each Window carries an anim (AnimKind: open / close / minimize / restore) and anim_ms. While animating, wm_draw paints only the chrome, scaled around a moving centre (anim_rect) — content is suppressed and the window is non-interactive. Minimise/restore fly to the app's real dock icon (shell_dock_icon_pos), else the dock centre. wm_close is deferred until the close animation finishes; wm_open_rect restores a saved window with no animation (used by session restore).

Zoom & snapping. Double-clicking a titlebar or the green light toggles toggle_zoom (fills the work area, remembers the prior rect). Dragging a titlebar into a screen edge shows a translucent snap preview and, on release, tiles the window: top = maximize, left/right = half. Dragging a snapped window restores its pre-snap size under the cursor. Pre-zoom/pre-snap rects live in s_saved[], keyed by slot.

Desktop environment (shell.c, spotlight.c)

The shell owns everything outside app windows: the menu bar, dock, desktop icons, menus, notifications, and the wall-clock.

Input routing (in main.c) is layered — overlays capture input by returning early, before the window manager sees it:

on_mouse DOWN:  spotlight → shell_event_pre (menus / menu-bar / dock right-click)
                → wm_event (windows) → shell_event (icons / dock)
on_key:         ⌘K toggles Spotlight → Spotlight (if open) → Esc closes a menu
                → wm_event (focused window)

shell_draw paints back-to-front: desktop bg → icons → wm_draw (+ snap preview) → menu bar → dock → menus → Spotlight.

Menus. One reusable dropdown (MenuItem[] + draw_menu + menu_run) backs three menus: the Apple menu (brand mark → About / live running-window switcher / Restart / Shut Down), the desktop context menu (right-click → New Note / Open Terminal / Change Desktop / Clean Up / About), and the dock menu (right-click an icon → Show + Quit if running, else Open). Menu actions are either an ACT_* command or a launch/quit-by-app_id.

Spotlight (spotlight.c) is a modal search overlay: ⌘K / Ctrl+K or the menu-bar magnifier. It substring-matches APP_THEMES labels/ids, dims the background, and launches via shell_launch. Note: ⌘Space is reserved by the OS/browser, so K is the shortcut; main.js preventDefaults it.

Menu-bar clock shows local time + date. The host packs it into one u32 via the host_wall ABI: secs-of-day (17b) | weekday 0=Sun (3b) | day 1..31 (5b) | month 0..11 (4b); C unpacks and formats with its own weekday/month tables. (On web this reads new Date(); on bare metal, the CMOS RTC.)

Wallpaper is a runtime tint (g_desk_top/g_desk_bot in theme.c) with 5 presets; "Change Desktop" cycles them (theme_cycle_desktop).

Power (main.c)

The Apple menu's Shut Down / Restart run a fade-to-black (dim_screen multiplies the framebuffer down over ~480 ms), then either a dark "safe to close this tab" screen (click to power back on) or a reboot() that resets the WM/shell and replays the boot animation.

Session persistence (session.c)

Wallpaper + open windows are serialized to a compact string and stored in localStorage["mc_session"] via the host ABI (host_save / host_load):

1|d<deskIndex>|w<id>,<x>,<y>,<w>,<h>,<min>|w...

State changes call session_mark_dirty(); frame flushes at most every ~400 ms (and force-flushes on shutdown/restart). On boot, session_restore() rebuilds the desktop; if there's no saved session it opens README + Terminal. Restored windows use wm_open_rect so they appear instantly without the open animation.

Virtual filesystem

Static tree — every "file" is a pointer to a C string literal in vfs.c:

/
├── readme.txt      about.txt      projects.txt      contact.txt
├── resume.txt
├── apps/
│   ├── terminal    files    clock    notes    snake
│   ├── about       blog     videos   resume
│   ├── settings    calc     paint
├── games/
│   ├── 2048        mines    tetris   pong     breakout   ttt
└── logs/
    └── boot.log

The Terminal and Files apps share this tree. open <app> and double-clicking a VFS_APP node both go through shell_launch().

Look & feel

Warm-amber CRT accent (#ffb84a) on muted teal desktop (#4a6a6a#2a4a4a gradient), off-white chrome, near-black ink.

Window chrome is mac-inspired: light unified titlebars with a subtle vertical gradient, three round traffic lights on the left in red / yellow / green order (vivid on focused windows, greyed out on unfocused), and rounded outer corners. Corner rounding in a software framebuffer is done by saving the underlying pixels before drawing chrome and writing them back afterwards through a per-corner mask — see corner_save/corner_restore in wm.c (~30 lines).

Dock is a floating rounded island centered near the bottom, not a full-width bar. Each app has its own colored icon square with a large glyph; hovering an icon magnifies it above the panel and shows a tooltip pill with a downward notch. A small dark dot appears under each running app.

Colors are defined once in os.h using RGB(r, g, b) and referenced by name — no hex literals scattered in app code.

Build details

Toolchain: WASI SDK 33 (bundled under toolchain/wasi-sdk/). Only clang and wasm-ld are used — no libc, no WASI imports.

clang --target=wasm32 -std=gnu11 -Oz -flto \
      -nostdlib -ffreestanding -fno-builtin \
      -fvisibility=hidden -Isrc/include \
      -Wl,--no-entry -Wl,--import-memory \
      -Wl,--initial-memory=33554432 -Wl,--max-memory=33554432 \
      -Wl,--stack-first -Wl,-z,stack-size=1048576 \
      -Wl,--export=init -Wl,--export=frame \
      -Wl,--export=on_mouse -Wl,--export=on_key \
      -Wl,--export=on_resize -Wl,--export=fb_ptr \
      src/*.c src/apps/*.c -o os.wasm

Adding a new app:

  1. Write src/apps/foo.c with foo_init/foo_draw/foo_event/foo_tick and a const App APP_FOO = { "foo", "foo", w, h, sizeof(FooState), ... };
  2. Add extern const App APP_FOO; to os.h.
  3. Register it in the APP_THEMES[] table in main.c — the single source of truth for display metadata: {"foo", "foo", ICON_STAR, RGB(bg), RGB(fg), &APP_FOO, is_doc}. app_by_id() / app_theme() look it up here; Spotlight and Files pick it up automatically.
  4. Add the source path to CORE_SRCS in sources.mk (both builds pick it up).
  5. Placement: the first NUM_ICONS (currently 14) APP_THEMES entries appear on the desktop grid (shell.c); the DOCK_ORDER[] array in shell.c controls which apps appear in the dock and in what order. Apps placed after the readme entry are excluded from the desktop grid but still reachable via Spotlight and the Files app (this is how the 6 games are surfaced).

Editing content

The About / Projects / Contact / README text lives as C string literals at the top of src/vfs.c. Edit those strings and rebuild.

Snapshots (headless render)

The WASM module can be driven from Node for CI screenshots or smoke tests:

import fs from "node:fs/promises";
import { PNG } from "pngjs";

const bytes = await fs.readFile("os.wasm");
const memory = new WebAssembly.Memory({ initial: 512, maximum: 512 });
// Stub every import the module declares, or instantiate throws LinkError.
// host_wall (every frame) + host_load (boot) + host_save must at least be present.
const env = {
  memory,
  host_wall: () => 0,               // 00:00, Sun, day 0, Jan — fine for stills
  host_load: () => 0,               // no saved session (keyed: keyPtr,keyLen,ptr,cap)
  host_save: () => {},              // keyed: keyPtr,keyLen,ptr,len
  host_del_key: () => {},
  host_fetch: () => {}, host_open_url: () => {}, host_load_image: () => {},
  host_tone: () => {}, host_noise: () => {}, host_clip_write: () => {},
  host_clip_request_read: () => {},
};
const { instance } = await WebAssembly.instantiate(bytes, { env });
const w = instance.exports;
const [W, H] = [1200, 800];
w.init(W, H);
for (let i = 0; i < 150; i++) w.frame(16);   // past boot animation

const buf = new Uint8Array(memory.buffer, w.fb_ptr(), W * H * 4);
const png = new PNG({ width: W, height: H });
png.data = Buffer.from(buf);
await fs.writeFile("out.png", PNG.sync.write(png));

License

Personal project. Font in src/include/font_data.h is a hand-drawn 6×8 bitmap, free to reuse.

About

My portfolio as a tiny OS — freestanding C → WebAssembly, no DOM, no framework. Also boots on bare-metal x86.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages