Help us shape the future of Go! Take the 2026 Go Developer Survey

wasmbox

command module
v0.0.0-...-34b843d Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 29, 2026 License: BSD-3-Clause Imports: 8 Imported by: 0

README

wasmdesk

wasmbox

A window manager, in your browser, written in Ruby.

A Wayland-inspired compositor with an Openbox-style window-manager policy — pure Ruby, running in WebAssembly, driving real external clients, no server.

part of wasmdesk built on go-embedded-ruby WebAssembly


wasmbox is a self-contained browser desktop: a Wayland-inspired compositor with an Openbox-style window-manager policy (Aqua-style + others selectable at boot), written entirely in pure Ruby and rendered to a <canvas> through the interpreter's interactive JS bridge. The Ruby program owns the desktop — it composites window surfaces, maintains the stacking order, applies focus and placement policy, draws decorations, runs a desktop-environment shell (dock, tray, notifications, applets, wallpaper, Spotlight, Exposé) and drives its own requestAnimationFrame render loop.

The compositor runs in one WebAssembly instance of the pure-Go (CGO=0) rbgo interpreter, with the Ruby source (split across compositor/*.rb) baked into the wasm binary via //go:embed — there is no server-side application logic and nothing of the compositor is fetched at runtime; the page is just a loader. It builds on rbgo's JS bridge (a JS module plus JS::Ref handles that let Ruby reach the DOM and the Canvas 2D context, register event listeners, and schedule animation frames), and renders its widgets with the pure-Go go-widgets/toolkit (AA-rendered, OpenType-shaped text) bound into Ruby.

Windows are no longer all the same program: external clients each run in their own Web Worker / WASM instance and paint into a SharedArrayBuffer, speaking a documented wire protocol to the compositor (see docs/protocol.md). The single-instance demo was "step A"; the code today is step C.1 — the compositor itself lives in a dedicated worker rendering into an OffscreenCanvas, and each client talks to it over a direct MessageChannel.

Live demo

https://wasmdesk.github.io/wasmbox/ — the desktop, served from GitHub Pages (cross-origin isolation is earned client-side via coi-serviceworker.js, so the SharedArrayBuffer surfaces work without special server headers).

Boot experience

The page shows a determinate boot splash from the very first paint: the wasmbox.wasm binary is fetched with a streaming reader that drives a real progress bar (bytes received vs Content-Length), then the bar completes as the Go runtime instantiates and the embedded Ruby compositor starts. The splash then fades and is removed from the DOM (no lingering timer/rAF). If boot fails, an error surface replaces it.

Desktop environment

Beyond bare window management, the compositor ships a small DE shell, all in Ruby:

  • Dock — the wasmdock client: bottom-anchored, magnifying, with running/active indicators; launches apps through the launch protocol message.
  • System tray / status strip, desktop notification toasts (icon + multi-line + multi-action, from the go-widgets v0.86 refinements), and a big clock.
  • Desktop applets behind the windows — live clock, a real Calendar applet, and monitor tiles.
  • Selectable wallpaper (gradients + a bundled image), persisted to localStorage.
  • Window snapping / half-tiling (Super+arrow, or drag to a screen edge).
  • Visual Alt-Tab switcher and Exposé (F3 "show all windows"), both with live per-window framebuffer thumbnails.
  • Spotlight launcher (⌘/Ctrl+Space) — a centered fuzzy-search command palette over the launchable apps.

Clients

Clients live under clients/ and are built for js/wasm. Most are pure-Go toolkit consumers; one is authored entirely in Ruby:

Client What it is
hello reference minimal external client (the protocol smoke test)
dock the wasmdock dock, built from the sibling repo
terminal, files placeholder app windows with distinct surfaces/titles
code a VS Code-styled editor shell
showcase a 7-tab Notebook exercising every toolkit widget family
calculator Entry + 5×4 Button grid — a small focused toolkit consumer
notepad multi-doc TextView editor
settings a WhiteSur (macOS Big Sur) preferences panel (Switch/Scale rows)
browser a WhiteSur/Safari-styled browser shell (see note below)
rubytk a Tip Calculator whose whole UI + logic are Ruby via require "widgets"
quake pure-Go Quake 1 from the sibling go-quake1 repo, streaming its assets from an OCI registry
doom pure-Go Doom from the sibling go-doom repo (software renderer, 4:3-locked window), streaming the Freedoom IWAD (BSD-3-Clause, sha256-pinned) from the same-origin /v2/doom-assets mirror

Doom

Applications → Doom in the root menu. The client (go-doom/engine cmd/doom-wasmbox, built by the Pages workflow or task build:doom) drives the protocol from Go, like Quake. It presents Doom's 320×200 frame at 640×480 with a 4:3 aspect lock and takes the classic keys: arrows to move, Ctrl to fire, Space to use, Shift to run, Alt or , . to strafe, Escape for the menu, Enter, and digits for weapons. The left mouse button fires and the right one uses. The client sets the new hello field inhibit_shortcuts (the analog of Wayland's keyboard-shortcuts inhibitor, see docs/protocol.md), so while Doom has the keyboard, Ctrl+Alt+arrow, Ctrl+Space, Ctrl/Alt+Tab and F3 go to the game instead of snapping the window or opening Spotlight, the switcher or Exposé. Click another window or the dock to leave.

The game data is Freedoom: Phase 1 0.13.0, which is never committed. The workflows download the official release, check the zip against the sha256 in Freedoom's signed CHECKSUM file, check the extracted freedoom1.wad against the digest the client pins, then pack it with packs/doom-assets.toml into /v2/doom-assets. The client fetches only from its own origin, recomputes the digest, and bounds the WAD's lump directory before the engine reads it. Sound is wired, but Chromium exposes no AudioContext to a worker, so Doom is silent there (DOOM: ... audio=unavailable in the console). test/probe-doom-smoke.mjs is the cross-browser check: it boots the client, opens the menu with Escape, starts E1M1 and walks, asserting on the client's DOOM: state=... log lines and on frames read inside the compositor worker.

Clients are spawned two ways: from a static worker URL (the compositor's own asset tree) or pulled at runtime as an OCI artifact via ociapps (?ociapps=1). The set of launchable ids is a compositor-owned trust boundary (LAUNCHABLE in compositor/04_window_manager.rb): a launch message carries only an opaque id, never a path or command line.

Streaming browser (in review). A new clients/browser variant renders real web pages by proxying them through the go-webengine/browserproxy (a WebSocket feeds rendered frames the client blits into its surface, behind a go-widgets loading skeleton). It is proposed in PR #93 and depends on the user hosting the proxy, so it is not yet on main; the bundled browser here is the offline WhiteSur/Safari shell (the page is COEP: require-corp, so it cannot embed live cross-origin sites directly — tiles open local placeholder pages).

Pick a window decoration

Wasmbox ships 16 registered decoration presets (2 layouts × 7 GTK palettes, plus the 2 plain layouts). Pick one via a URL query param:

http://localhost:8080/                              # OpenboxFrame (default)
http://localhost:8080/?frame=aqua                   # AquaFrame — subsumes wasmaqua
http://localhost:8080/?frame=aqua-whitesur-light    # Aqua + WhiteSur ≈ macOS Big Sur
http://localhost:8080/?frame=aqua-whitesur-dark     # ≈ Big Sur dark
http://localhost:8080/?frame=openbox-juno           # Openbox + Juno teal palette
http://localhost:8080/?frame=aqua-adwaita-{light,dark}
http://localhost:8080/?frame=openbox-solarized-{light,dark}

Or run wasmbox-serve -default-frame=NAME to make a whole instance default to one preset (bare / 307-redirects to /?frame=NAME). The registry lives in compositor/02_frame.rb — adding a new frame is one entry in FrameRegistry::TABLE. You can also switch at runtime from the root-menu Frame submenu.

The sibling wasmdesk/wasmaqua repo is archived as of 2026-06-30 — its macOS-Aqua chrome is now the AquaFrame preset here.

Architecture

  • Threading (step C / C.1). The compositor runs in a dedicated Web Worker, rendering into an OffscreenCanvas transferred from the page. The main thread is a thin shell — it owns the <canvas> element, relays DOM input to the compositor worker, and mirrors localStorage; it runs no Ruby/wasm. Each external client runs in its own worker and talks to the compositor over a direct MessageChannel (port2 handed to the client at spawn), not relayed through the main thread. See docs/protocol.md for the wire contract.
  • Compositor — holds the canvas, fits it to the viewport, installs input handlers, and drives a JS.raf render loop. A frame-level dirty gate skips the full-frame recomposite when nothing changed, so an idle desktop costs ~0 CPU (this fixed a 100%-CPU idle regression in Firefox). A small HUD shows the window count, a smoothed FPS reading and a frame counter.
  • WindowManager — owns the stacking order (last element topmost), a click-to-focus / raise-on-focus policy, cascade placement, workspaces, minimize-into-dock, the launch trust boundary, and the focus cycle.
  • Window — a client surface plus its decoration geometry, with roles (window / panel / popup), hit-testing helpers, and per-frame damage.

The window-management logic (geometry, hit-testing, stacking, placement, focus) is plain Ruby with no reference to JS, so it is unit-tested natively; only the rendering methods touch the bridge.

Wasm size

The compositor wasm is built with -trimpath -ldflags '-s -w', and the js/wasm build drops the network backends the browser cannot use anyway. That cut the binary from ~175 MB to ~86 MB (about 18 MB gzipped over the wire), which is what the streaming boot splash downloads.

Build & serve

Uses Task:

task          # list the available tasks
task build    # build wasmbox.wasm (embeds compositor/*.rb) + every client + wasm_exec.js
task serve    # build, then serve http://localhost:8080/ with COOP/COEP headers
task test     # native Go + Ruby unit tests (no browser)
task clean    # remove the build artifacts

wasmbox is a Go module that imports the interpreter, so task build is a plain go build for the js/wasm target (GOOS=js GOARCH=wasm CGO_ENABLED=0) — Go fetches the interpreter as a dependency; there is no separate checkout to clone. External clients built from sibling repos (the dock, Quake, Doom) resolve those repos via DOCK_DIR / ENGINE_DIR / DOOM_DIR (see the Taskfile).

The SharedArrayBuffer client surfaces require cross-origin isolation (COOP/COEP), which python3 -m http.server does not set — that is why the demo ships its own dev server, cmd/serve (task serve). For the GitHub-Pages model (no server headers), task serve:pages assembles the site and relies on coi-serviceworker.js to supply isolation, reproducing the real deploy locally. The ?ociapps=1 flow (task demo-wasmbox-ociapps) streams every client from a local OCI registry instead of the static tree.

Then open http://localhost:8080/. The page instantiates wasmbox.wasm, the Go runtime runs the embedded compositor, and the canvas takes over once ready. Any static server that serves .wasm as application/wasm with COOP/COEP works.

Controls

  • Drag a titlebar — move a window. Drag the bottom-right corner — resize.
  • Click a window — focus and raise it. × in the titlebar — close.
  • Right-click the desktop — root menu (Applications submenu, Frame submenu, wallpaper). Right-click a window — context menu.
  • Tab — cycle focus; Alt+Tab — visual switcher; Esc — dismiss.
  • Super+arrow / drag-to-edge — snap / half-tile. F3 — Exposé.
  • ⌘/Ctrl+Space — Spotlight launcher.

Validation note

The in-browser rendering is validated with a headless browser (Playwright driving Chrome): after boot the splash overlay is removed, the <canvas> is fully painted (desktop + cascaded windows + DE shell), external clients spawn and commit damage, an OCI-streamed client is pulled and rendered, and the console reports the compositor started. The GOOS=js GOARCH=wasm binary build is also checked in CI, along with the native Ruby/Go unit tests.

Part of wasmdesk

wasmdesk is a family for a WebAssembly desktop built on pure-Go Ruby. wasmbox is its compositor + window manager; wasmdock is the dock client; and ociapps streams client apps from any OCI registry.

Documentation

Rendered for js/wasm
The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
clients
browser command
calculator command
code command
code/internal/scene
Package scene is the VS Code Dark+-inspired editor scene used by the wasmbox "code" external client.
Package scene is the VS Code Dark+-inspired editor scene used by the wasmbox "code" external client.
dock command
dock/internal/scene
SPDX-License-Identifier: BSD-3-Clause
SPDX-License-Identifier: BSD-3-Clause
dock/internal/theme
SPDX-License-Identifier: BSD-3-Clause
SPDX-License-Identifier: BSD-3-Clause
files command
hello command
hello/internal/scene
Package scene paints the hello-world client's surface.
Package scene paints the hello-world client's surface.
mvvm-counter
Package mvvmcounter is a self-contained wasmbox demo of the MVVM data-binding stack: a `require "mvvm"` Observable drives a `require "widgets"` UI, run through the embedded go-embedded-ruby interpreter (rbgo).
Package mvvmcounter is a self-contained wasmbox demo of the MVVM data-binding stack: a `require "mvvm"` Observable drives a `require "widgets"` UI, run through the embedded go-embedded-ruby interpreter (rbgo).
notepad command
rubytk command
rubytk/internal/scene
SPDX-License-Identifier: BSD-3-Clause
SPDX-License-Identifier: BSD-3-Clause
settings command
sharedvfs
Package sharedvfs is the virtual filesystem shared by the wasmbox terminal and files clients.
Package sharedvfs is the virtual filesystem shared by the wasmbox terminal and files clients.
showcase command
showcase/internal/scene
SPDX-License-Identifier: BSD-3-Clause
SPDX-License-Identifier: BSD-3-Clause
terminal command
terminal/internal/complete
Package complete implements Tab autocompletion for the terminal client.
Package complete implements Tab autocompletion for the terminal client.
terminal/internal/scene
Package scene drives the terminal client: a Grid (character buffer + cursor), a Shell (line-editor + builtin dispatch wired to a clients/sharedvfs VFS), and a renderer that rasterises the Grid into the SAB pixel buffer the compositor blits.
Package scene drives the terminal client: a Grid (character buffer + cursor), a Shell (line-editor + builtin dispatch wired to a clients/sharedvfs VFS), and a renderer that rasterises the Grid into the SAB pixel buffer the compositor blits.
terminal/internal/shellparse
Package shellparse is the POSIX-ish shell front-end the terminal client uses to turn a typed command line into a runnable AST.
Package shellparse is the POSIX-ish shell front-end the terminal client uses to turn a typed command line into a runnable AST.
cmd
rbbench command
rbtest command
serve command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL