Skip to content

carledwards/u64shell

Repository files navigation

u64shell

A DOS-style TUI that mirrors a Commodore 64 Ultimate screen live and remote-controls it — in your terminal or in a browser, from the same code.

It's a fourth face over the u64ctl device core (alongside u64ctl's CLI, MCP server, and WASM library), built on the foxpro-go FoxPro-for-DOS TUI framework.

 ┌ Text Screen ┐   ┌ Graphics ┐   ┌ Command Window ┐
 │ 40x25 mirror│   │ 320x200  │   │ RESET TYPE RUN │   foxpro-go TUI
 │ (cell text) │   │ pixels   │   │ POKE PEEK …    │   (terminal or canvas)
 └─────────────┘   └──────────┘   └────────────────┘
        └────────────── ops (u64ctl) ──────────────┘
                          │
                  u64 REST client ── HTTP /v1/... ── Ultimate 64

u64shell running in a terminal

The terminal face: overlapping DOS-style windows — the 40×25 Text Screen, the Command window (after HELP), a Sprite Inspector, and the live Screen mirror.

Running

Two faces, one binary set.

Terminal

go run .                         # start disconnected; File ▸ Connect…
go run . -host 192.168.1.64      # auto-connect (or set U64_HOST / U64_PASSWORD)
# or: make build  ->  bin/u64shell

Diamond Mine running in the terminal

Diamond Mine running, with System ▸ Ultimate 64 Info open and a sprite in the inspector. (The Text Screen shows scrambled cells because the game runs in bitmap mode — the Screen window renders it correctly.)

Browser

make run-serve builds the WASM module and a small server that hosts it and proxies device calls. Open the printed URL.

export U64_HOST=192.168.1.64
make run-serve                   # serves http://localhost:8064

The same TUI runs on an HTML canvas (via foxpro-go's WASM bridge); device calls go to /u64/... on the page's own origin, which the server forwards to the Ultimate — so there's no browser CORS problem.

u64shell in a web browser

The same app in a browser: the Screen drawn with real pixels on an HTML canvas (Diamond Mine), with device calls routed through the server/broker ("Connected: via server").

Using it

  • File ▸ Connect… / Disconnect — a modal dialog to point at a device; the status bar (right) shows Connected: <host> / Disconnected.
  • View — toggle windows. In the browser it's Screen + Sprite Inspector (Screen opens by default). In the terminal it's Text Screen (default) + Screen + Sprite Inspector.
  • F2 (or File ▸ Command Window) — the command window. Type a line, Enter to run; HELP lists commands, HELP TYPE shows detail.

Windows:

Window What it shows
Screen the live C64 display: the 320×200 window (text via the embedded char ROM, ECM-aware, hi-res/multicolor char + bitmap modes) plus a slice of the border (its $D020 color, and sprites parked in the border), with sprites composited on top. Real pixels on the browser canvas; Unicode half-blocks as the terminal fallback. Resizable, letterboxed to the C64 aspect. This is the C64 screen — in the browser it's the only display you need.
Text Screen (terminal only) the 40×25 text screen decoded to characters in C64 colors (ECM-aware), as a readable cell grid. Fixed size. Redundant in the browser, where Screen renders text too.
Sprite Inspector one hardware sprite zoomed (← → to change sprite).

Command-window verbs (over u64ctl/ops):

Command Action
RESET REBOOT MENU PAUSE RESUME machine control
STOP send RUN/STOP (break a BASIC program)
TYPE <text> type into the keyboard buffer — {return}/{enter}/{cr} = RETURN, {space} = a literal space
RUN type RUN + RETURN
PEEK <addr> / POKE <addr> <val> read / write one byte (hex needs $/0x)
CLEAR HELP QUIT command-window built-ins

What the screen mirror can (and can't) show

The Screen is reconstructed from device memory — screen/color RAM, the VIC registers, sprite data, and the character font — read about once a second. That faithfully mirrors normal RAM-based programs (BASIC, most .prgs, demos), including hi-res / Extended Background Color / multicolor character modes and hi-res / multicolor bitmap modes.

Two things it can't fully reproduce:

  • Cartridge graphics. A cartridge's font/graphics live in cartridge ROM, which the device's memory read doesn't expose (it reads C64 RAM + I/O only). For an Ultimax cart (many MAX-machine games, e.g. Kickman) the CPU has RAM only at $0000–$0FFF, so the font region reads back as $FF and every character renders as a solid colored block — you get the screen layout and colors, but not the glyph art.
  • Mid-frame raster tricks. A ~1 Hz memory snapshot can't capture per-scanline mode/color changes, sprite multiplexing, or animated border effects (raster bars, FLD, opened borders); it shows one consistent instant with a single $D020 border color. (Sprites parked in the border do show — that's static.)

The faithful fix for both is the Ultimate's VIC video stream (a future direction) — capturing the real VIC output instead of rebuilding it from memory.

The broker (cross-tool serialization)

The Ultimate's small HTTP server cannot handle concurrent requests — two overlapping calls wedge it. The browser server (cmd/serve) is therefore the single serialized broker: it forwards /u64/... to the device one request at a time and answers /healthz with the device it bridges. The u64ctl CLI and MCP server auto-detect a broker on the well-known port and route through it, so the web UI, CLI, and Claude can all drive one C64 without colliding. Run them direct with -direct.

Ultimate 64 HTTP quirks (learned the hard way)

The device's REST server is minimal; cmd/serve and u64ctl/u64 work around it:

  • No concurrency — serialized by a mutex (u64.Client) and the broker.
  • No request compression — Go's default Accept-Encoding: gzip makes the device gzip on its slow CPU (multi-second delays); compression is disabled.
  • POST …writemem (body) is unreliable — "could not read data from attachment". Writes use the body-less PUT …writemem?address=&data=<hex> form (chunked at 128 bytes), which the device's own web UI uses.
  • Sensitive to back-to-back connections — it replies Connection: close and drops packets under zero-gap bursts (→ ~2s TCP retransmits). The broker spaces requests with -min-gap (default 60ms).

Layout

main.go        terminal entry (//go:build !js)
wasm_main.go   browser entry (//go:build js && wasm) — SimulationScreen + foxpro-go/wasm
ui.go          buildUI(): windows, menu, commands, poller — shared by both entries
screen.go      Text Screen provider (cell text, ECM-aware)
graphics.go    Graphics provider (320x200 pixels / half-blocks) + Sprite Inspector
c64render.go   C64 palette + screen-code → Unicode glyph map
commands.go    command-window verbs
conn.go        connection state (host/password/client; pluggable dial for the proxy)
connectdialog.go  modal Connect dialog
cmd/serve/     the broker: serves the embedded web app + forwards /u64 → device

Build targets

make build       # terminal binary -> bin/u64shell
make wasm        # browser module  -> cmd/serve/web/u64shell.wasm (+ wasm_exec.js)
make serve       # build wasm, then the server -> bin/u64shell-serve
make run-serve   # build + run the server (HOST=<ip> or U64_HOST)

Local cross-module development resolves u64ctl / foxpro-go / go6asm through the parent go.work (and replace directives in go.mod).

About

A DOS-style TUI that mirrors a Commodore 64 Ultimate's screen live and remote-controls it — in your terminal or a browser, from one codebase. Built on u64ctl + foxpro-go.

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors