92 Commits
Author SHA1 Message Date
Stefan ArentzandClaude Opus 4.8 619b4dd92a ROM catalog R6: built-in configs ship as .jsonc; user files may use .json or .jsonc (#344)
The six built-in configs carry comments now (R5), so ship them as
configs/*.jsonc — editors keyed off the extension treat them as JSONC instead
of flagging the comments. git mv keeps history (all renames R100), and the
BUILTINS include_str! paths + the file-reading tests move to .jsonc in
lock-step.

The loader reads config sources by path and never dispatches on the
extension, so a user's --config / --config-overlay file may use .json or
.jsonc freely. Add a commented .jsonc fixture and
jsonc_and_json_user_files_both_load to pin that (a .jsonc with comments and a
plain .json both load).

Verified: fmt/clippy clean; full suite green — including the builtin-config
load/round-trip tests against the renamed files.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:20:59 -04:00
Stefan ArentzandClaude Opus 4.8 4aa5364c3d Mouse PIA plan P5: docs (notes/MOUSE.md as-built) + cleanup (#336)
Rewrite notes/MOUSE.md as the real-hardware as-built: the 6520 PIA + 6805
controller + banked 342-0270-C ROM, the port-B handshake, the commands, the
X=$Cn/Y=$n0 calling convention and the screen-hole layout (Pos/Read
slot-indexed; ClampMouse's fixed slot-0 quirk), the interrupt model (][+
works, //e backlog), host input, the EWM integration, and the MousePaint
confirmation. Drop the retired synthetic-firmware description and its (now
resolved) "Known limitation".

Update README and notes/IDEAS.md to describe the mouse as the card's real
hardware, and mark plans/20260721-01 (the synthetic card) as superseded by
this plan.

Docs only; no code change.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 11:52:51 -04:00
Stefan ArentzandClaude Opus 4.8 91f3128f59 Plan (revised) + the 342-0270-C ROM: real AppleMouse hardware (#331)
The reference implementations turn the mouse-hardware plan from blind
reverse-engineering into a port of known-good code. Two sources:
oliverschmidt/mouse-interface (MIT) — PIA6520.c + MouseInterfaceCard.c, which
document the whole card in their headers — and freitz85/AppleIIMouse, whose
342-0270-C ROM + disassembly this commit lands.

Findings that reshape the plan:
- The card is a 6520 PIA + a 6805 microcontroller + a 2KB ROM.
- The ROM banks into the 256-byte $Cn00 slot area as 8 pages, selected by PIA
  port B bits 1-3 — NO $C800 expansion (a real simplification over the prior
  revision's guess).
- The 6502 firmware talks to the 6805 over the PIA: port A = data, port B =
  RDACK/WRREQUEST/RDREADY/WRACK handshake + ROM bank + sync. No timeouts, so a
  wrong handshake hangs it — exactly today's MousePaint symptom.
- The 6805 drives the slot IRQ directly (the PIA's IRQ is unused).

plans/20260721-03 rewritten to this reference-grounded design and five phases
(P1 PIA + banked ROM, P2 the 6805 + handshake, P3 host input + MousePaint
flagship, P4 interrupts, P5 retire the synthetic card + docs). The card works
on both ][+ and //e with its own ROM — no family split. notes/MOUSE.md points
at the reference + ROM.

- roms/Apple Mouse Interface Card ROM - 342-0270-C.bin (2KB, sha1
  3a9d881a8a8d30f55b9719aceebbcf717f829d6f), owner-directed fetch.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 09:14:23 -04:00
Stefan ArentzandClaude Opus 4.8 7feab74572 Plan: real AppleMouse hardware (6821 PIA) for the //e firmware path (#330)
MousePaint hangs on the //e: it runs the //e's internal mouse firmware
(INTCXROM=1), which drives the card as its real 6821 PIA ($C0n0-$C0n3), not
the synthetic card's $Cn00 entry points. Diagnosed by booting the real disk
headlessly (reaches the menu, RETURN ignored, the device touched twice). The
synthetic firmware (plans/20260721-01) only covered the card's own entry
points — the gap the "no committed MousePaint image" kickoff decision left.

- notes/MOUSE.md: a "Known limitation" section recording the diagnosis and
  pointing at the new plan.
- plans/20260721-03-mouse-pia-hardware.md: a six-phase plan to model the card
  as its real hardware — a 6821 PIA (reusing ewm/src/pia.rs, the Apple 1's)
  with the mouse mechanism (quadrature X/Y, button, IRQ lines) wired to its
  ports, so the //e internal firmware drives it. P1 reverse-engineers the
  wiring (the linchpin); P5 rewrites mouse_rom to drive the PIA for the ][+.
  Three kickoff decisions (][+ path, flagship gate, PR granularity).

Docs only.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 08:26:19 -04:00
Stefan ArentzandClaude Opus 4.8 a45b920e8f Mouse plan M5: docs + as-built notes/MOUSE.md (#329)
The closer for the AppleMouse II card (plans/20260721-01, now complete). No
code — the as-built record and the config surface's documentation.

- notes/MOUSE.md: the as-built — synthetic firmware and the identification /
  screen-hole map, the private DEVSEL protocol, the interrupt model and the
  reusable maskable-IRQ line (so Mockingboard inherits it), and the
  coordinate decision (absolute/mapped, revised from the plan's
  relative/captured).
- README: the "What's emulated" list gains the AppleMouse II; a runnable
  example (examples/mouse.json, a slot-4 mouse //e) checked by
  readme_examples_parse.
- notes/JSON_CONFIG.md: the slot-card inventory gains "mouse" (one, any slot
  1-7, both families); notes/STATE.md lists the Mou Persist fields;
  notes/IDEAS.md ticks the AppleMouse item and points Mockingboard at the
  shared IRQ line.

Gate: readme_examples_parse + standard gates (382 pass).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 01:27:48 -04:00
Stefan ArentzandClaude Opus 4.8 8232da812c Backlog: //e Ctrl-Reset should clear the MMU soft switches (#324)
Record the reset-fidelity gap found while testing the original //e: EWM's
Ctrl-Reset is only cpu.reset() (CPU registers), but a real //e's RESET line
also clears the memory soft switches (80STORE/RAMRD/RAMWRT/ALTZP/INTCXROM/
SLOTC3ROM) before the ROM handler runs — and the ROM relies on that. Measured:
after Ctrl-Reset from an all-switches-on state, RAMRD/RAMWRT/ALTZP/INTCXROM
persist on both //e, so a program that leaves aux memory active and resets
comes back still in aux memory (never happens on hardware).

New "Emulation accuracy" section in notes/IDEAS.md with the story, the fix
shape (a hardware-reset hook distinct from a program's soft reset through the
vector), what to pin against a reference before implementing, and the //e-only
scope. No code change here — backlog only.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 00:29:33 -04:00
Stefan ArentzandClaude Opus 4.8 9a27b68b2b Add builtin:apple2e config + docs (//e original-plan E4) (#321)
The closer for the original //e: a `builtin:apple2e` config so the machine
E3 added is reachable by name, and the docs to match. This completes
plans/20260720-02.

- configs/apple2e.json: model apple2e, aux ext80col, UniDisk 3.5 (Liron) in
  slot 5, Disk ][ in slot 6, RGB — the same layout as apple2enhanced, on the
  6502. Registered in BUILTINS (sorted apple2 < apple2e < apple2enhanced);
  the builtin list / title / model / self-containment tests extend.
- Boot gate: builtin_apple2e_boots_dos33 builds the machine through the
  config pipeline and boots DOS 3.3 to the Applesoft `]` prompt (the //e
  autostarts the slot 6 disk; DOS 3.3 runs on the 6502).
- Docs: README "What's emulated" folds the original and Enhanced //e into
  one entry and gains a builtin:apple2e profile; notes/JSON_CONFIG.md model
  inventory + the aux-validity note cover both //e models; IDEAS.md marks
  "NMOS //e" landed.

Gates: cargo fmt / clippy -D warnings / cargo test (365 pass, incl. the new
boot gate and readme_examples_parse with the new builtin:apple2e example).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 23:52:01 -04:00
Stefan ArentzandClaude Opus 4.8 869ba8175e Rename builtin:apple2e -> apple2enhanced (//e original-plan E1) (#316)
Our `apple2e` was really the Enhanced //e (65C02 + 342-0303/0304 system
ROMs + the 342-0265 MouseText video ROM). Rename the config-level token
to the honest `apple2enhanced`, freeing `apple2e` for the true original
(unenhanced) //e that E2-E4 add. Mechanical, mirroring the
`2plus`->`apple2plus` rename (#308); no ROM or machine changes.

What moved (the config surface only):
- `config::Model` serde token + `Model::token()`; `git mv
  configs/apple2e.json -> apple2enhanced.json` with model updated and
  title made honest ("Apple //e" -> "Enhanced Apple //e", reserving
  "Apple //e" for E4's original). BUILTINS re-sorts in place (apple2enhanced
  sorts before apple2plus, so no manual reorder).
- The `Persist` INFO model string mirrors the token by design
  (apple2/apple2plus/...), so it renames too, keeping save-states
  self-describing as `apple2enhanced`.
- Every `"apple2e"` config-token literal in tests/error strings; usage
  and `--config` help text; README, notes/JSON_CONFIG.md, notes/MAC_APP.md,
  examples/myiie.json. Both JSON schemas regenerated from source
  (EWM_UPDATE_SCHEMA=1), not hand-edited.
- `builtin:list` column widened 12->16 so the longer name still aligns.

Deliberately NOT touched (internal, renamed later by E3): the Rust
identifiers `TwoType::Apple2E` / `Model::TwoE`, the `two_e_*` test files,
and their "apple2e must construct" panic strings — they name the type,
which stays until E3 splits the two machines.

Verified: cargo fmt --all --check, cargo clippy --all-targets -D warnings,
and cargo test (359 pass, incl. the golden-BMP screenshots and
readme_examples_parse). Gate: `builtin:list` shows apple2enhanced; the old
`builtin:apple2e` errors "no built-in config ... (available: apple1,
apple2, apple2enhanced, apple2plus, replica1)".

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 22:58:41 -04:00
Stefan ArentzandClaude Opus 4.8 548040ff7c Plan: SKU-keyed ROM catalog (notes/ROMS.md + plan) (#315)
Capture the ROM-organisation idea as a living design note and a phased
plan. The note inventories today's roms/ folder — five naming styles,
three extensions, an exact duplicate (3410036.bin ≡ AppleII/…341-0036),
size-mismatched .rom/.rom.bin Krusader pairs, and two code paths for
embedding ROMs (two.rs/chr.rs statics vs config::ROM_BUILTINS) — and
proposes SKU-first filenames plus one lookup table SKU → (desc, bytes).

The plan sequences it after the original-//e ROMs land so the reorg
happens once: R1 folder rename → R2 catalog table → R3 builtin:SKU
resolver → R4 optional config migration. Docs only; no code.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 22:54:07 -04:00
Stefan ArentzandClaude Fable 5 2150f7e0a8 Bare ewm two boots builtin:apple2plus (no in-code default machine) (#313)
Owner's decision: the default machine *is* a config, not an in-code
layout. When parse_options collects no sources, it now loads
builtin:apple2plus, so `ewm two` and `ewm two --config builtin:apple2plus`
build the identical machine — code and configs describe the ][+ once,
the way `ewm one` already builds its default from the replica1 builtin.

Consequences: the old in-code default's Thunderclock in slot 1 is
retired (builtin:apple2plus is language@0 + diskii@6, no clock — a
stock ][+); and, combined with the title field, the bare-two window now
reads "EWM - Apple ][+" (the owner's first computer). The golden-BMP
screenshots are unaffected — the Thunderclock never rendered.

Two default-pinning tests updated to the new behavior:
bare_two_is_the_apple2plus_builtin asserts bare == the builtin, and the
print-config round trip checks the title and language card instead of
the retired clock. default_slot_cards stays as Two::new's constructor
default (the lib/test path, distinct from the CLI default). Docs: the
README profiles section and JSON_CONFIG note now say the default is
builtin:apple2plus; the config-sources backlog item is marked done.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 21:05:47 -04:00
Stefan ArentzandClaude Fable 5 2a36ca634f Window title from the config: EWM, or EWM - <title> (#312)
The window bar was picked by matching options.model, so an apple2
machine fell into the ][+ arm and showed "Apple ][+". Replace it with a
config-driven title: a new top-level `title` field, window reads
"EWM - <title>" when set and plain "EWM" otherwise
(config::window_title, shared by both frontends). The five built-in
configs set it to the machine name: Apple 1, Replica 1, Apple ][,
Apple ][+, Apple //e. The old "EWM v0.1 / …" strings are gone; the
bootloader window aligns to "EWM - Bootloader".

Both `two` and `one` carry the title in Options (from apply_config, and
round-tripped by options_to_config so --print-config shows it). It
comes from the document only: a bare `ewm two`/`ewm one` stays "EWM"
even though one's normalize fills the board from a builtin — so the
title tracks what you asked for, not the machine underneath.

Tests: window_title formatting, and every built-in carries its
machine-name title. Schemas regenerated for the new field.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 20:51:06 -04:00
Stefan ArentzandClaude Fable 5 c76edb68c3 builtin:apple2 config + PR#6 DOS 3.3 boot gate (A3) (#311)
Phase A3 of plans/20260720-01-original-apple2.md, closing the plan.
configs/apple2.json is the machine the request started from: a 48K
Apple ][ (no slot-0 language card), Integer BASIC, a Disk ][ in slot
6, green monitor — registered in BUILTINS (sorted: apple1, apple2,
apple2e, apple2plus, replica1). The self-containment sweep and the
builtin-name/list assertions extend to it.

Boot gate: builtin:apple2 with the DOS 3.3 System Master in drive 1
resets to the Monitor — asserting it does NOT autostart the disk (a
][+ would) — then C600G runs the slot 6 boot ROM and DOS 3.3 loads
(its cold-start banner is the marker). No `]` check: the original ][
has Integer BASIC, not Applesoft, so DOS drops to `>`.

Docs: the README What's-emulated list gains the Apple ][; the two
profiles section adds builtin:apple2 with its JSON and the C600G/PR#6
boot note; JSON_CONFIG's model inventory lists apple2. Plan complete.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 20:28:19 -04:00
Stefan ArentzandClaude Fable 5 64dc24f7a5 Rename model tokens: 2plus -> apple2plus, 2e -> apple2e (#308)
Spell out the two-family model tokens so every machine reads the same
way — apple1, apple2e, apple2plus, replica1 — ahead of adding the
original Apple ][ as `apple2` (plans/20260720-01). Pre-1.0, so the old
terse tokens go outright: `--set machine:model=2plus` and
`--config builtin:2plus` are now apple2plus / apple2e, and an old name
errors with the available list.

Mechanical and wide: the config::Model serde tokens and Model::token(),
the BUILTINS table (renamed, re-sorted, include_str paths), configs/
files (git mv 2plus.json/2e.json -> apple2plus.json/apple2e.json) and
their model values, the ewm/tests/configs fixtures, every "2plus"/"2e"
string literal in tests and error messages, the examples, both schemas,
and the live docs (README, JSON_CONFIG, IDEAS, MAC_APP). Guarded
word-boundary replacement so e2e, new_2e and hex escaped it. The
builtin:list column widened to fit the 10-char "apple2plus".

Verified: fmt, clippy -D warnings, the full suite (37 binaries incl.
golden-BMP and the README example check) pass; builtin:list and the
old-token error checked against the real binary.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 19:49:15 -04:00
Stefan ArentzandClaude Fable 5 c9c1a75fb6 Disk images over HTTP, cached with ETag revalidation (#306)
* --tty-telnet: proactive echo negotiation (fix telnet double-echo)

The telnet echo suppression was reactive — the server only announced
WILL ECHO / WILL SGA after the client sent an IAC first. But a classic
telnet client sits in line-mode-with-local-echo and waits for the
server to negotiate, so the announcement never went out, the client
kept echoing locally, and every line appeared twice (its echo plus the
machine's own authentic echo).

A telnet server that wants remote echo has to offer it proactively on
connect. The new --tty-telnet flag (implies --tty) does exactly that:
it writes WILL ECHO + WILL SGA before the banner and marks the filter
active so the reactive path doesn't double-announce. Bare --tty stays
byte-clean for local terminals and nc (kickoff option B); the reactive
fallback still helps a plain --tty client that does negotiate first.
The systemd @.service switches its ExecStart to --tty-telnet.

The growing session-behavior params (throttle, banner, telnet) fold
into a TtyConfig struct. Tests: proactive announce leads the stream
(before the banner) and doesn't double-announce when the client then
sends its own IAC; the flag implies --tty and parses. Verified live:
`ewm one --tty-telnet | xxd` shows FF FB 01 FF FB 03 then the prompt.

Docs: scripts/systemd/README.md and notes/APPLE1.md explain proactive
--tty-telnet vs byte-clean --tty, with troubleshooting for the
double-echo and the nc negotiation bytes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Disk images over HTTP, downloaded once into a cache

A floppy, hard-drive or 3.5" image path may now be an http(s):// URL:

  ewm two --set 'machine:slots:6:drive1=https://example.com/Frogger.dsk'

ewm/src/fetch.rs downloads it into $XDG_CACHE_HOME/ewm (else
~/.cache/ewm) / <sha1 of the URL> / <file name from the URL>, beside a
meta.json holding the ETag and Last-Modified. The digest keys the
directory because URLs are not filesystem-safe; the file keeps its own
name so the cache is browsable and errors read normally. The hash is
the crate's own SHA-1 (ws::sha1) — no dependency for that.

Freshness is one *conditional* GET (If-None-Match / If-Modified-Since)
rather than HEAD-then-GET: a 304 costs a single round trip. If the
request fails and a copy is cached, the cached copy is used, so a
machine assembled from URLs still boots offline; a cold cache with no
server is a loud error instead.

A URL is a source, not a path: resolve_paths leaves it alone (like
builtin:), while referenced_files still counts it, so a built-in
config may not carry one — builtins must run offline.

Downloaded hard drives mount memory-only. This needed a real change,
not just leaving the file handle unopened: in hdd.rs an unopenable
file means WRITE PROTECTED, which is a different (and still supported)
state for a local image. Hdd now carries an explicit `writeback` flag
— false for downloads, so ProDOS writes update the in-memory image and
never reach the cached file, and revalidating a download cannot
destroy them. Floppies already behaved this way.

Dependency: ureq (blocking + rustls), owner's decision — the one place
the tiny-dependency budget gives way, because TLS is not something to
hand-roll. It brings ~25 transitive crates including ring, so the
headless server build now compiles native crypto too.

Tests: the cache round trip against a real local HTTP server (cold
download, conditional revalidation with a 304, offline fallback, and a
cold cache with nothing listening failing loudly); URL naming and
digest stability including degenerate URLs; memory-only vs write-back
block writes at the register level; URLs surviving path resolution;
and the wiring gate — a machine builds with DOS 3.3 served over HTTP
into drive 1, which only passes if the mount path really fetched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 21:48:11 -04:00
Stefan Arentz 75bce7aec8 --tty-telnet: proactive echo negotiation (fix telnet double-echo) (#305) 2026-07-19 19:57:11 -04:00
Stefan ArentzandClaude Fable 5 25f59cc451 scripts/systemd/README.md: the telnet Apple 1 build & setup guide (#304)
The first real deployment surfaced everything a header comment
couldn't hold: SDL's Linux build dependencies (and the XTEST error a
partial set produces), the stale-CMake-cache gotcha after a failed
configure (cargo clean -p sdl3-sys), what sdl-static-headless actually
builds and its --tty-only caveat, the install paths the units expect,
how to watch sessions (systemctl list-units / journalctl), operating
knobs (machine, port, limits, banner), the security posture, and a
troubleshooting section written from the actual failures.

The guide lives next to the units as scripts/systemd/README.md; the
socket header shrinks to a pointer (one source of truth), and the main
README's Experimental bullet and notes/APPLE1.md link to it.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 18:33:23 -04:00
Stefan ArentzandClaude Fable 5 60a161ecee --tty-banner: greet telnet visitors with instructions (T3) (#302)
Phase T3 of plans/20260719-04-apple1-telnet.md (added after T2): a
--tty-banner <path> text file prints to the session before the machine
says anything — where the operator puts the instructions a bare Woz
Monitor prompt doesn't offer. Line endings are CRLF-normalized for raw
terminals; a missing file fails loud at startup (a systemd misconfig
should be visible in the journal, not silently banner-less).

The flag is tty-session tooling, not machine description — it lives
with --tty, outside the config document, like the debug flags do.
scripts/systemd/banner.txt ships as the example (monitor and BASIC
starters, the reset chord, how to leave telnet) and the service unit
mounts it from /usr/local/share/ewm — readable under
ProtectSystem=strict.

Test: the banner precedes the machine's prompt, CRLF-normalized, and
the flag without a value falls into usage. Verified live: piped
session shows banner then prompt; a bad path exits 1 with the error.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:05:51 -04:00
Stefan ArentzandClaude Fable 5 3e177a8bb6 Telnet negotiation, BREAK reset, systemd units (T2) (#301)
Phase T2, closing plans/20260719-04-apple1-telnet.md.

The tty session gains a hand-rolled RFC 854 subset: a state-machine
filter that stays dormant until the first inbound IAC — nc and local
terminals never see a protocol byte — then announces WILL ECHO + WILL
SGA (character-at-a-time, remote echo), refuses every other
negotiation, swallows subnegotiations, drops IAC IAC (a literal 0xFF
is not an Apple 1 key), and maps BRK/IP — telnet's `send brk`, the
serial-terminal attention — to the RESET button: immediate, flushing
typed-ahead, like the real thing.

scripts/systemd/ carries the inetd-style units: ewm-one.socket
(ListenStream=6502, Accept=yes, MaxConnectionsPerSource) and
ewm-one@.service (StandardInput=socket, DynamicUser, ProtectSystem=
strict, MemoryMax, RuntimeMaxSec) — the sandbox is tight because the
binary needs no filesystem; configs and ROMs are embedded. README's
Experimental section gains the entry; notes/APPLE1.md records the
tty/telnet design as built; the plan is complete.

Tests: the filter unit-tested byte for byte (activation volley, the
no-reply-to-DO-ECHO loop guard, WONT/DONT refusals, subnegotiation
swallowing, BRK, IAC IAC); a session-level test where a staged reader
delivers the volley, enters BASIC, then BREAKs back to the monitor —
staged because BREAK is deliberately out-of-band. Verified live:
piping an IAC volley plus a command through `ewm one --tty` yields
prompt, WILLs, echo, and the dump in one stream.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:01:50 -04:00
Stefan ArentzandClaude Fable 5 237955a54a Docs: the one profiles as schematics; as-built notes (R4) (#298)
Phase R4, closing plans/20260719-03-one-machine-components.md.

The README's one-profiles section now shows what the plan was for: the
profile JSON is the machine's schematic — CPU, every RAM bank with
size and address, every ROM image with its address — with the
mountable builtin: ROM names listed, user ROM paths mentioned, the
whole-board semantics and reset-vector rule explained, and the
faithful details told in English (BASIC preloaded in the $E000 RAM
bank as the cassette would have; the Replica 1's $E000-$FFFF being the
Krusader distribution byte for byte).

notes/APPLE1.md gains the as-built section (registry, byte-identity
gates, from_components, whole-board semantics, the profile decisions);
notes/JSON_CONFIG.md's schema inventory adds machine.cpu and the
generalized memory regions with the three-way family rules. Plan
marked complete.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:22:37 -04:00
Stefan ArentzandClaude Fable 5 c9a16fb616 Plan: one machine components — CPU, RAM, and ROMs in the document (#294)
The one-family profiles should describe the machine like the
schematics do: machine.cpu, RAM banks with sizes and addresses, ROM
images with addresses — embedded in the binary and mounted with
builtin:<name> in a memory region's path (or a user file path). The
only fixed hardware is the PIA at $D010-$D013.

notes/APPLE1.md records the research: the real Apple 1 and Replica 1
memory maps (applefritter ch. 7), and the byte-level answer to the
Krusader/BASIC question — they don't overlap; the 8KB Krusader images
are BASIC ($E000) + Krusader ($F000) + a per-build modified WozMon
($FF00) side by side. Our krusader.rom is the 6502 build, so today's
Replica 1 pairs a 65C02 CPU with the 6502 Krusader — the new profile
fixes that. On the real Apple 1, BASIC was cassette-loaded into a 4KB
RAM bank at $E000, never ROM — the profile preloads that bank.

Four phases (R1 roms/ + registry + builtin: resolution, R2 config keys
+ overlap/reset-vector validation, R3 One built from components with
byte-identity gates, R4 README profiles). Six kickoff decisions listed
with recommendations. Status: draft.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:46:01 -04:00
Stefan ArentzandClaude Fable 5 d672893b05 Docs: one profiles, JSON_CONFIG as-built, REMOTE Phase 7 note (O5) (#292)
Phase O5, the last phase of plans/20260719-02-one-config.md.

The README gains "The one machine profiles" alongside two's — both
builtins in English and JSON with their boot commands, a note that
bare `ewm one` boots the Replica 1, and the one-family key rules in a
sentence. The example check extends to `-- one` commands
(readme_examples_parse dispatches on the subcommand; one::parse_options
is pub(crate) for it) so one's examples are enforced like two's.

JSON_CONFIG.md records the as-built decisions: one document type with
a derived family, the cross-check table and its one-place relax point,
the shared collect_document parameterization, minimal builtins, boo
booting via builtin:, and one's flag retirement. The schema inventory
names which keys are apple2-family-only, and the status table gains
the one-family row. REMOTE.md's Phase 7 notes that the config surface
it wanted now exists — relaxing the remote-block family check is a
one-place change. Plan marked complete.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:37:05 -04:00
Stefan ArentzandClaude Fable 5 924557fc7e Retire one's flags: --model, --memory, --trace, --strict (O4) (#291)
Phase O4 of plans/20260719-02-one-config.md, same v0.0.1 ruling as
plan 20260719-01: the four flags are gone from `ewm one` — they were
duplicates of machine.model, machine.memory, debug.trace and
cpu.strict, all reachable through the sources O3 added. Retired flags
fall into the generic usage error; a test sweeps the list. one's
private parse_memory_option goes with them (MemoryOption stays — the
config path fills it), and the usage screen shrinks to the source
surface, mirroring two's.

The boo bootloader now boots the Apple 1 / Replica 1 via
`--config builtin:apple1` / `builtin:replica1` (kickoff decision 2 as
recommended — the builtins gain a user besides the CLI). README's one
examples and REMOTE.md's Phase 7 example speak the config spelling;
One::add_ram/add_rom doc comments reference machine.memory instead of
the dead flag.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:32:49 -04:00
Stefan ArentzandClaude Fable 5 de6abcd1ce Flag retirement: the CLI after config sources (F1–F3) (#286)
* Flag retirement F1: the quiet seven

Plan 20260719-01: --scanlines, --fps, --strict, --debug, --boot-delay,
--trace/--trace= and --state are gone from `ewm two` — they were
single-value duplicates of config keys (display.scanlines, display.fps,
cpu.strict, debug.enabled, boot.delay, debug.trace, state.path), all
reachable via --set or any config source. Retired flags fall into the
generic usage error (owner's ruling: v0.0.1, no transition ceremony);
a test sweeps the list and pins the --set spellings.

Scanlines::parse went with its only caller (the flag's optional-value
peek); MonitorStyle::parse follows in F2 with --color. Docs swept:
README's feature blurb, STATE.md and REMOTE.md speak state.path /
--set state:path now, IDEAS.md's trace item names debug.trace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Flag retirement F2: the muscle-memory trio

Plan 20260719-01: --model, --color and --aux are gone from `ewm two` —
machine.model, display.monitor and machine.aux are config keys, and
`--config builtin:2e` is as short as `--model 2e` was. The --model
alias spellings (2+, ][+, //e, iie) go with it, as does --color's
optional-value peek (bare --color meant rgb) and MonitorStyle::parse,
its last caller. The aux token survives internally (Options.aux,
rebuilt by apply_config); only the flag is gone.

Swept the consumers: main.rs's getting-started hint and subcommand
line, the README feature blurbs and quick-start (builtin-first now,
enforced by readme_two_examples_parse), JSON_CONFIG.md's schema
inventory (sources are config + --set for everything), and the
--color/--aux comment references in scr.rs, aux.rs, config.rs and
two.rs. `one` keeps its --model — it has no config surface (plan
non-goal). Historical notes (APPLE_IIE_ENHANCED, REWRITE, landed
IDEAS items) keep their period wording.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Flag retirement F3: --memory

Plan 20260719-01, last phase: --memory is gone from `ewm two`; memory
regions come from config or overlay files (machine.memory), the one
setting --set cannot reach (arrays) — apply_set's error text now says
so. two's parse_memory_option went with the flag (one keeps its own
copy and its --memory flag: no config surface there, plan non-goal).
The print-config round trip composes the region through an overlay
now, which is exactly the documented replacement.

With this, `ewm two`'s surface is the plan's goal shape: the four
sources (--config, --config-overlay, --set, --print-config), --serve,
and the debug tooling (--wozbug, --break, hidden --screenshot). Plan
marked complete with the kickoff decisions recorded as built.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* README: wrap the quick-start commands; document the three two profiles

The "start a machine directly" commands wrap with backslash
continuations (the readme_two_examples_parse test joins them, so they
stay checked). A new section explains that bare `ewm two` boots the
default machine — a ][+ with the 16K Language Card (the classic 64K
build), Thunderclock, and Disk II — and documents all three machine
profiles in English and JSON: the default (with its config-terms
equivalent, printable via --print-config), builtin:2plus, and
builtin:2e, each with its explicit `--config builtin:<name>` boot
command. The later ready-made-configs paragraph folded into it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* README: brief Experimental section for WozBug, VNC, and the Mac app

WozBug's walkthrough and the Native Mac app section fold into a new
Experimental section at the bottom — three short bullets, each linking
to its working document (notes/DEBUGGING_TOOLS.md, notes/REMOTE.md,
notes/MAC_APP.md). The remote console gets its first README mention
there; its --serve bullet leaves the source-kinds list, which is back
to the three document sources.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* README: drop the cc65 tests/ sentence

The C-era assembly test programs are being removed in #284; the
Testing section no longer mentions them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* README: complete Key commands section

The keys table becomes a "Key commands" section and gains the missing
entries: Cmd-K (the Command Palette, available on every machine) and
Cmd-Shift-R (reboot, power off/on), plus a Machines column instead of
per-row parentheticals. A short paragraph documents the palette's own
keys (type to filter, arrows, Return, Esc), that everything else —
including Ctrl-A..Z control characters — goes to the emulated machine,
and drag-and-drop disk swapping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 12:53:11 -04:00
Stefan ArentzandClaude Fable 5 a3c856aab3 Docs sweep: README around the four config sources (C5) (#282)
Phase C5, the last phase of plans/20260718-02-config-sources.md.

The README's config section is rewritten around the compositional
model — one document, four source kinds merged in command-line order
(--config base, --config-overlay layers, --set overrides, convenience
flags on top) — with the Total Replay overlay as the worked example
and --config builtin:2e in the quick-start. The example configs are
now committed under examples/ (myiie.json and
drive-with-total-replay.json, shown verbatim in the README), so every
example references real files. --print-config closes the section as
the "what machine did I just describe?" / linter answer.

The gate's scripted check is the readme_two_examples_parse test in
two.rs: it extracts every `cargo run --release -- two ...` example
from the README (quote-aware, continuation-joining) and runs it
through parse_options, which opens every --config/--config-overlay
source — so a renamed flag, a bad example path, or an example config
that stops validating fails the suite.

Notes brought up to date: JSON_CONFIG.md's status table gains the
config-sources row (C1-C5 done) and Phase C is marked as seeded by
options_to_config; IDEAS.md moves machine config files from "in
progress" to "landed" with the remaining palette work named;
MAC_APP.md's Phase 3 bullet records that .ewmachine bundles can be a
thin layer over the landed source surface. Also fixes the --fps usage
text (the default is 40, not 30 — drift spotted during C4).

Verified: the README's composed, complete-config, and overlay-only
examples run against the real binary from the repo root (exit 0,
expected documents); fmt, clippy -D warnings, and the full suite
(including the golden-BMP screenshot tests) pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 21:41:42 -04:00
Stefan ArentzandClaude Fable 5 5ee1546db8 --print-config: print the machine the command line describes (C4) (#281)
Phase C4 of plans/20260718-02-config-sources.md. With four layering
sources, "what machine did I just describe?" needs a first-class
answer:

  ewm two --config builtin:2e \
          --config-overlay drive-with-total-replay.json \
          --set display:monitor=amber --print-config

prints the final machine — sources *and* convenience flags applied,
i.e. exactly what a real run would build — as config JSON on stdout
and exits 0. Load and validation errors still exit nonzero first, so
the flag doubles as a config linter for scripts and CI.

options_to_config (two.rs) is the one Options→Config mapping, the
inverse of apply_config, kept as a single function so the palette's
"save current setup" (JSON_CONFIG Phase C) can reuse it. The plan's
full-mapping option worked out; the pre-convenience-flags fallback
wasn't needed. --wozbug, --break and the hidden --screenshot are debug
tooling, not machine configuration, and don't appear in the document.

The printed document is explicit where it matters and quiet where it
doesn't: model, slot table, display and cpu settings print even at
their defaults (stable against future default changes), while
off-by-default extras (strict, debug, boot delay, remote) appear only
when enabled. config::compact_document does the shaping — it drops
nulls, empty arrays, and sections that emptied out, but keeps a
genuinely bare "slots": {} table, which means "no cards", not
"default layout".

Verified: e2e tests pin the gate — a command line composed from every
source kind prints a document that, fed back via --config, yields
identical Options (Options/ServeOptions/MemoryOption grew PartialEq
for exactly this) — plus the default machine and bare-table round
trips and the exit codes; fmt, clippy -D warnings, and the full suite
(including the golden-BMP screenshot tests) pass; the real binary was
exercised by hand for the composed and linter cases.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 21:32:33 -04:00
Stefan ArentzandClaude Fable 5 37f5e788a5 --config-overlay: layer partial configs onto the base (C3) (#280)
Phase C3 of plans/20260718-02-config-sources.md, on top of C2's
partial parsing. The machine configuration surface is now
compositional:

  ewm two --config builtin:2plus \
          --config-overlay drive-with-total-replay.json \
          --set display:monitor=amber

--config-overlay <source> is repeatable, and all document sources —
the --config base, overlays, --set — apply strictly in command-line
order through the existing merge. Overlay files load through the
structural-only typed path: the overlay file is named in errors and
its relative paths resolve against its own directory, the same
portability property config files have. The builtin: scheme is shared
with --config (a complete config is a valid overlay), and overlay-only
command lines start from the default machine, like bare --set.

Slots materialization extends to overlays (merge_overlay_document): an
overlay carrying machine.slots onto a slotless document materializes
the default table first, so `--config-overlay hdd7.json` means "the
default machine plus a hard drive in slot 7" rather than a literal
one-slot machine. A base's explicit table stays literal — the plan's
asymmetry — and all four base x overlay slots combinations are pinned
in config.rs tests before the flag wiring, per the plan's hazard note.
--set's materialization is untouched, so the boo launcher's drag-drop
paths behave identically.

Kickoff decision, recorded as built: at most one --config, per the
plan's recommendation — a second errors with "only one --config
allowed; use --config-overlay for additional layers". Two complete
machines silently deep-merging read as an accident once partial layers
had their own flag.

Verified: fmt, clippy -D warnings, the full test suite (including the
golden-BMP screenshot tests), plus the real binary exercised by hand —
the one-config error, the partial-to---config refusal, the overlay
composition, and the known (accepted, documented) edge that
`--config-overlay builtin:2e` over the ][+ default fails the //e
slot-0 completeness check.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 21:24:06 -04:00
Stefan ArentzandClaude Fable 5 fc90fff5a8 Partial configs: relax the parse, split validation (C2) (#279)
Phase C2 of plans/20260718-02-config-sources.md — the enabling change
for --config-overlay (C3), landed separately so C3 stays small.

Config.machine and Machine.model become Option in the serde types, so
any partial fragment ({}, a slots-only overlay) parses. Validation
splits to match: validate() keeps every check a lone fragment can be
judged on (unknown keys, enum values, slot rules, multiplicity), while
the new validate_complete() runs only on the final layered document —
machine.model present, plus the model cross-checks (aux is //e-only,
the //e has no slot 0) that an overlay can't answer until the merged
document names the model.

The loader contract keeps configs and overlays distinct concepts:
load/load_source_document (the --config path) still require
completeness per file — a partial file fails early with "machine.model
is required (is this an overlay? use --config-overlay)" — while
load_document now runs only the structural pass (file named in errors,
relative paths resolved) and is the path overlays will load through.
from_document runs both passes on the merged document and points a
missing model at the fix: "start from --config, e.g. --config
builtin:2plus".

Schema: option (2) from the plan. The generated schema is now
overlay-shaped, so the golden test post-processes the requiredness
(machine, machine.model) back into schema/ewm-config.schema.json and
commits the relaxed variant as schema/ewm-config-overlay.schema.json
with its own title/description, giving overlay files editor support.
Round-tripping the generated schema through serde_json::Value would
lose schemars' canonical keyword ordering, so the test converts back
to schemars::Schema before serializing.

Verified: fmt, clippy -D warnings, and the full test suite (including
the golden-BMP screenshot tests) pass; new tests pin the fragment
round-trip through load_document, the per-path completeness messages,
and the model cross-checks moving to the final-document pass. Behavior
of every existing command line is unchanged.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 21:13:44 -04:00
Stefan ArentzandClaude aae19c97b8 Built-in machine configs: --config builtin:2plus / builtin:2e (C1) (#278)
Phase C1 of plans/20260718-02-config-sources.md. The configs/ files are
embedded at compile time (include_str!, a static table in config.rs), so
--config builtin:<name> works from any installed binary; names are the
schema's model tokens, matching the renamed files 1:1 (configs/2plus.json,
configs/2e.json). builtin:list prints names with descriptions and exits
like --help; an unknown name errors listing the available names.

Built-ins are self-contained by rule: load_builtin rejects any file
reference (there is no directory to resolve relative paths against) and
a test pins the property, along with every builtin carrying the new
top-level description field (schema regenerated) and the table staying
sorted.

Verified: builtin:2plus/2e produce the same machine as their committed
files (unit + integration tests), built-ins layer with --set like any
source, builtin:list and unknown-name exits exercised against the real
binary, and the full suite (306 tests, golden BMPs included) is green
with fmt and clippy clean.


Claude-Session: https://claude.ai/code/session_014tBgXL3aSDMgV2CoxTmBQ6

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-18 20:55:38 -04:00
Stefan ArentzandClaude Fable 5 5171ff69de Remote console Phase 1: shared overlay compositor (pause box over VNC) (#275)
The prototype ran a parallel headless serve() loop that published the bare
scr.frame(), while the SDL loop composited its overlays (drive lights, the
pause box) with SDL canvas calls the VNC path never saw. So a machine paused
from the web button looked frozen-but-unmarked in a browser, and the drive
lights were window-only. This is the deferred Phase 1 debt coming due.

Fixed the way the plan discussion concluded: not a Two-owning Driver (both
frontends already build the machine identically via power_on_machine, so
wrapping ownership buys little), but a shared *pure Vec<u32> compositor* -
the piece that was actually duplicated and missing remotely.

- overlay.rs (new): Compositor::compose(screen, w, h, &Overlays) -> &[u32]
  stamps the passive overlays onto the machine's screen frame, SDL-free and
  unit-tested. Drive lights reuse led.rs; the pause / restored-state box
  reuses tty.rs, horizontally nearest-scaled to the frame width (identity on
  the ][+, 2x on the //e). Darkening is the pixel-space stand-in for the SDL
  alpha-224 black blend. PixelLayout gains unpack_rgb for it.
- Both frontends now hold a Compositor: the SDL loop uploads its output to
  the single screen texture (the separate LED and pause textures, and
  status_tty, are gone - a net simplification); the serve loop publishes it.
- Interactive/window chrome stays per-frontend by design (REMOTE.md §15,
  now resolved): the command palette is SDL-only UI, the status bar is a
  strip below the screen, scanlines are an up-scaling effect.

Golden-BMP tests still pass - they capture the pre-overlay scr.frame(), so
the machine's rendered screen is byte-for-byte unchanged. Verified in a real
Chrome tab: pausing over the web now shows the PAUSED box over the darkened
DOS screen with the drive lights in the corner, and resuming clears it.
fmt/clippy/test green (37 groups; 5 new compositor unit tests).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:05:26 -04:00
Stefan ArentzandClaude Fable 5 66b338bd78 Web console: Reset, Reboot, Pause buttons (#274)
The web page gains machine-control buttons, the first of the operator
controls the owned-page architecture was chosen for (notes/VNC.md §2).
Reset/Reboot/Pause aren't RFB messages - RFB has no way to pause or
power-cycle - so the buttons POST to /control/<action> on the same web
port. rfb.rs routes /control/* to an InputEvent::Control on the input
channel the serve loop already drains each frame (204 on hit, 404
otherwise), so no new port, protocol, or noVNC change; view-only servers
drop control like all other input.

The serve loop acts on them: Reset is Ctrl-Reset; Reboot rebuilds from
the same options via power_on_machine (cold boot), carrying over the
trace sink and restarting the wave/keys, always coming up running;
Pause toggles a flag that freezes CPU stepping while the framebuffer
keeps publishing, so the frozen screen shows and - reusing the phase
gate - the FLASH cursor freezes too. The page tracks its own Pause/Resume
label best-effort; Reset/Reboot reset it since a fresh machine runs.

Verified in a real Chrome tab against the embedded console: Reboot
cold-booted DOS 3.3, Pause froze it at the prompt with a solid
(non-blinking) cursor, Resume continued and PRINT 3*4 printed 12; the
endpoints answer 204/404 and the reset/pause/reboot actions log
server-side. Unit tests cover /control routing and the view-only drop.
fmt/clippy/test green (37 groups).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:58:48 -04:00
Stefan ArentzandClaude Fable 5 c3d0e85b22 Cmd-P toggles pause (#272)
The pause toggle existed only as a command-palette row; Cmd-P now flips
it directly, alongside the other Cmd shortcuts (Cmd-R reset, Cmd-Return
fullscreen, Cmd-I status bar, Cmd-K palette). Same toggle the palette
uses, so the palette's Pause/Unpause label stays in sync, the pause box
(and the restored-from-state banner) appear exactly as before, and the
FLASH-freeze behavior applies. Comments and notes/STATE.md now name
Cmd-P as the way to set a restored machine going.

fmt/clippy/test green (37 groups). The binding itself lives in the SDL
event loop, so the eyes-on check: Cmd-P pauses, Cmd-P resumes.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:42:40 -04:00
Stefan ArentzandClaude Fable 5 b3aa295f5e Start paused when restored from a saved state, naming the save (#270)
A machine restored via --state now starts paused in the SDL frontend, so
it does not run off without you: the pause screen shows RESTORED FROM
SAVE STATE and the save's local time (YYYY-MM-DD HH:MM:SS), taken from
the state file's mtime - which the atomic write-then-rename stamps at
the moment of saving, so no format change or version bump. The first
unpause (command palette, which already reads Unpause) dismisses the
banner; later manual pauses show the plain PAUSED box. Headless serve
has no pause UI, so a restored machine runs immediately there - noted
in notes/STATE.md §6 alongside the behavior.

The banner timestamp line is centered in the box's 26 inner columns and
unit-tested to be exactly the TTY's 40 columns, like every other
pause-box line. fmt/clippy/test green (150 lib tests, 37 groups).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:25:11 -04:00
Stefan ArentzandClaude Fable 5 973e06f187 state S5: the determinism gate; plan and notes brought up to date
The state analogue of the golden-BMP tests (notes/STATE.md §8), run at
the harshest moment: save mid-boot - motor on, arm seeking, DOS half
loaded - restore into a fresh twin, then run BOTH machines millions of
further cycles comparing text screens, cycle counters and PC along the
way, finishing with pixel-level equality through the pure renderer. Any
component that forgot a field diverges the trajectories and fails this
loudly. It passes, and both machines finish booting to identical screens.

Docs: the plan's phase table is all Done; notes/STATE.md gains the
as-built notes (S0's stub never existed - the CLI landed inside S4; the
INFO model seatbelt shipped early; Clk/Pia save slightly more than
planned; the gate saves mid-boot); notes/REMOTE.md points out that
--state gives the headless VM fleet suspend/resume for free.

Completes plans/20260718-01-machine-state.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 14:57:35 -04:00
Stefan ArentzandClaude Fable 5 fc48f10444 Plan: machine state save/restore (--state), and the plan-driven workflow (#268)
* Add notes/STATE.md: the machine-state save/restore plan (--state)

Plans suspend/resume: --state=/path restores at startup and saves at quit.
Core design: a Persist trait made a supertrait of Device and AuxCard, so
every component keeps its store/load code local and the compiler forces
new cards to answer the persistence question; the existing ownership tree
(Two -> Cpu -> Memory -> devices) is the serialization tree; config builds
the machine, state overlays it (same-config precondition, mismatch
detection in the backlog); a hand-rolled LE chunk container in the house
style. Includes the component inventory (what saves, what pointedly does
not - floppy media is in, block-device media is not), cycle-stamp and
quiescence rules, the determinism gate, and a five-phase plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Adopt plan-driven workflow: plans/ directory and project CLAUDE.md

Work now flows idea -> notes/ design doc -> phased plan in
plans/YYYYMMDD-NN-slug.md -> pull requests (per phase or whole plan, the
user's call per plan). CLAUDE.md records the methodology, the standing
gates, and the house style; plans/20260718-01-machine-state.md is the
first plan, phasing the notes/STATE.md design into S0-S5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 14:28:12 -04:00
Stefan ArentzandClaude Fable 5 979e536117 Remote console: speaker audio in the browser (WebAudio side-channel, notes/VNC.md A1+A2) (#266)
* Add notes/VNC.md: the browser console as built + the WebAudio plan

Describes the embedded noVNC setup (Phases 4-5) and plans speaker audio in
the browser as a WebAudio side-channel: a /audio route on the existing
WebSocket port streaming Wave's 44.1 kHz mono i16 PCM, played by an
AudioWorklet in EWM's own console page. No noVNC fork needed - the engine
is consumed as a library (core/rfb.js) inside a page we own, so buttons
and audio live beside the RFB canvas, never inside noVNC. REMOTE.md §9 and
Track B2 now point here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Remote console: speaker audio in the browser (WebAudio side-channel)

RFB has no audio channel, so the speaker was dropped for every remote
client. Implements notes/VNC.md phases A1+A2: the WebSocket port gains an
/audio route that streams the speaker's PCM, and the embedded console page
plays it — the beep is back, in the browser, with no noVNC changes (the
engine stays a vendored library; audio lives beside it in our page).

Server (A1):
- snd.rs: Wave (the SDL-free toggles->PCM synthesizer) goes pub, with
  set_cpu_frequency moved onto it so the headless path pitches up under
  acceleration exactly like the SDL path.
- audio.rs (new): a broadcast Hub in the frame-Publisher shape. The serve
  loop renders each frame's toggles through Wave and publishes; every
  /audio client has a small bounded queue (~200 ms) drained by its own
  writer thread, with drop-oldest backpressure so a slow tab loses stale
  chunks instead of accumulating latency. Wire format: one JSON text frame
  ({"format":"s16le","rate":44100,"channels":1}), then binary frames of raw
  little-endian mono i16 (~88 KB/s per listener, no compression, no deps).
- rfb.rs: a WebSocket upgrade whose path is /audio attaches to the hub;
  everything else is unchanged (RFB on the default path, console page on
  plain GET). ws.rs gains WsWriter::write_text for the header.

Page (A2):
- audio.js + audio-worklet.js (EWM's own files, embedded like the rest):
  /audio socket -> Int16->Float32 -> AudioWorklet ring buffer -> speakers,
  44.1 kHz context. Browsers refuse audio before a user gesture, so the
  first click/keypress (the one that focuses the screen anyway) arms the
  pipeline; a speaker button in the status bar toggles mute. Underrun plays
  silence, which the decay-to-silence speaker model was converging to
  anyway. window.ewmAudio exposes live counters for debugging.

Verified: a scripted /audio client heard PRINT CHR$(7) at peak amplitude
8000 (the full speaker rail) among 194 streamed chunks; in a real Chrome
tab the console page's pipeline armed on gesture, streamed continuously,
and the in-page stats flipped heardSignal on a fresh bell. New tests:
hub framing + drop-oldest, attach round-trip over loopback, and the rfb
route test (audio + RFB + console on one port). fmt/clippy/test green
(149 lib tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:31:58 -04:00
Stefan ArentzandClaude Fable 5 859309fdd9 Remote console Phase 5: embedded web console (vendored noVNC, optional web server) (#265)
* Vendor the noVNC engine v1.6.0 (core/ + vendor/pako) for the web console

The pinned noVNC client engine and its one dependency, copied unmodified
from the v1.6.0 release (MPL-2.0; LICENSE.txt vendored alongside), plus
EWM's own minimal console page (index.html) that loads core/rfb.js and
connects back to the host and port it was served from. The full app/ UI
chrome (~800 KB of settings panels, images, and locales) is deliberately
not vendored. See ewm/novnc/README.md for the update procedure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Remote console Phase 5: embedded web console served from the binary

http://host:PORT/ is now a live Apple II in a browser tab with no external
tooling: a small built-in HTTP handler serves the vendored noVNC console on
the WebSocket port, and the page connects back to the same host:port with the
RFB WebSocket upgrade — one port carries both, the Proxmox shape.

Strictly opt-in tiers, per the design note: --serve vnc://... is native VNC
only; ?ws=PORT adds the raw RFB-over-WebSocket listener (bring-your-own
noVNC; plain HTTP still answers 426); ?web=PORT additionally serves the
embedded console (web=PORT is sugar for web&ws=PORT; bare ?web defaults the
port to 5701). Config equivalent: remote.websocket + remote.web. Bind stays
the single remote.bind for all listeners.

- build.rs: embeds the novnc/ tree as a generated (path, bytes) table of
  include_bytes! entries — no embed crate, no runtime filesystem access.
- web.rs: static-table asset lookup (literal keys only, so no traversal;
  query strings ignored; "/" -> index.html) and a minimal one-shot HTTP
  responder (200 with content-type/length, 404, Connection: close).
- ws.rs: upgrade() split into read_http_request / accept_upgrade /
  refuse_plain_http so the connection handler can branch.
- rfb.rs: the WebSocket listener's Transport carries the web flag; plain
  HTTP requests route to the console when enabled, 426 otherwise.

Verified in a real Chrome tab: http://127.0.0.1:5701/ served the console
from the binary, booted the DOS 3.3 System Master, and CATALOG typed at
browser speed listed the disk. fmt/clippy/test green (145 lib tests, up
from 140: 3 web unit tests + 2 console integration tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:45:54 -04:00
Stefan ArentzandClaude Fable 5 b48f635d85 Re-land Phase 4 (WebSocket transport + keyboard pacing) onto remote-console (#264)
* Remote console Phase 4: embedded WebSocket transport, noVNC connects directly

Browsers cannot open raw TCP, so noVNC speaks RFB-over-WebSocket. This adds an
optional second listener that serves the identical RFB byte stream inside
WebSocket binary frames — no websockify sidecar, no new dependencies.

- ws.rs: a hand-rolled WebSocket (RFC 6455) server transport in the des.rs
  spirit, instead of the plan's tungstenite (which would add ~9 transitive
  crates; kept as documented fallback). HTTP upgrade with from-scratch SHA-1 +
  base64 (RFC-vector-tested, incl. the RFC 6455 accept-key example), binary
  frame encode/decode across all three length encodings, client-mask
  enforcement, ping->pong, close echo, subprotocol echo for older noVNC.
  Exposed as io::Read/io::Write adapters (WsReader/WsWriter).
- rfb.rs: the connection handling is now generic over Read/Write, so the RFB
  state machine (handshake, VNC auth, input, framebuffer) is shared verbatim
  between plain TCP and WebSocket; Server::start takes an Options struct and
  binds both listeners, feeding one machine. noVNC's SetPixelFormat request
  (little-endian, shifts 0/8/16) is byte-identical on the wire to our
  advertised big-endian 24/16/8, so keeping our format remains safe.
- config/CLI: remote.websocket config field and ?ws=5701 on the --serve URL
  (bare ?ws defaults to 5701); validation rejects port 0 and a ws port equal
  to the RFB port; schema regenerated.

Verified: a from-scratch Python WebSocket client (accept key checked against
hashlib's reference SHA-1) booted DOS 3.3 through the WS port and computed
PRINT 640*1.6 -> 1024; plain TCP verified in parallel on the same machine.
New tests: 6 ws unit tests, a full WS-transport RFB session test, and
parse_serve URL tests. fmt/clippy/test green (139 lib tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Remote console: pace remote key input through the one-byte keyboard latch

The Phase 4 browser gate (stock noVNC in Chrome) caught a real bug: a browser
delivers a whole typed word within a single frame, and the serve loop latched
every pending key back-to-back with no CPU cycles in between. The Apple II
keyboard is a one-byte latch, so each byte overwrote the previous before the
ROM could read it — typing CATALOG arrived as just "G". Scripted clients had
masked this by sleeping between keystrokes.

Fix: translated key bytes now land in a queue (RemoteKeys), and the frame loop
pumps at most one byte per frame, only after the ROM has consumed the previous
one (keyboard strobe cleared via $C010). Reset and pointer buttons still apply
immediately; the queue is bounded against flooding. Free side effect: correct
type-ahead while the machine is busy booting.

Verified in a real Chrome tab via noVNC over the WebSocket transport: booted
DOS 3.3, typed PRINT "HELLO FROM CHROME" at browser speed, every character
arrived; a zero-delay scripted burst (PRINT 6*7 -> 42) passed as a second
concurrent viewer of the same machine. New unit test drives the latch/strobe
pacing against a real machine. fmt/clippy/test green (140 lib tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:09:04 -04:00
Stefan ArentzandClaude Fable 5 f2544dde19 Remote console: headless VNC server for the Apple ][+ / //e (prototype) (#262)
* Remote console prototype: headless VNC server for `two` (notes/REMOTE.md)

A first working prototype of the remote-console plan, collapsing Phases 0/2/3
so an EWM machine can be booted headless and reached with a native VNC client.

- config: new `remote` block (protocol/bind/port/view_only) with validation
  (rejects reserved "rdp" and port 0); schema regenerated.
- rfb.rs: hand-rolled RFB 3.8/3.7/3.3 server on std::net in the wozbug shape —
  security None, Raw encoding, one handler thread per client (several viewers
  of one machine), mpsc input back to the emulator, big-endian-RGBA frames via
  u32::to_be_bytes (no per-pixel conversion). Unit-tested: full handshake +
  first FramebufferUpdate decode, key/pointer delivery, view-only.
- two.rs: `--serve vnc://host:port` parsing and a headless `serve()` loop — the
  SDL frame loop's shape without SDL, reusing build_machine() and the pure Scr
  renderer. Keysym->byte translation mirrors the SDL keyboard table (printable,
  arrows, Return, ESC, Tab/Del, Ctrl-letter, Ctrl+F12 reset).

The SDL path is left byte-for-byte unchanged (a parallel headless path rather
than Phase 1's full Driver refactor, which is deferred — see the prototype note
in notes/REMOTE.md). Verified end-to-end: booted DOS 3.3 and bare Applesoft
over vnc://, typed PRINT 2+2 and saw 4. fmt/clippy/test all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Remote console: VNC authentication (DES challenge) so macOS clients connect

macOS Screen Sharing refuses the RFB "None" security type and always prompts
for a password, so the None-only prototype could not be used from the built-in
client. Add standard VNC authentication (the RFB DES challenge, RFC 6143
§7.2.2), gated on a password.

- des.rs: a small from-scratch DES (FIPS 46-3, ECB, encrypt-only) — the project
  hand-rolls its primitives and keeps deps tiny. Tested against the textbook and
  all-zero vectors, and cross-checked end-to-end against OpenSSL's DES (an
  OpenSSL-computed auth response is accepted by the server).
- rfb.rs: offer security type VNC (2) with the DES challenge when a password is
  set, else None (1) as before; SecurityResult handling per 3.3/3.7/3.8. New
  tests cover auth accept and reject.
- config/CLI: `remote.password` config field and a `?password=` (and
  `?view_only=`) query on the --serve URL; --serve now merges onto config-
  provided serve options so a config password survives an explicit --serve.

Verified: macOS-style VNC auth handshake accepted with the right password and
rejected with the wrong one, then booted Applesoft and typed PRINT 2+2 over an
authenticated session. fmt/clippy/test (129 lib tests) all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* REMOTE.md: record the actual branch layout (remote-console integration branch)

The integration branch is 'remote-console' (cut from main); work branches
like 'claude/remote-console' PR into it, and one final PR promotes it to
main. PR #262 is the first such sub-phase PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 11:04:13 -04:00
Stefan ArentzandClaude 309b371b46 Add the remote-console (VNC/RDP) plan (notes/REMOTE.md) (#260)
A working document, in the style of APPLE_IIE_ENHANCED.md and JSON_CONFIG.md,
for booting EWM machines headless as network-reachable "VMs" — one machine per
process, each on its own port, connectable from a native VNC client and from a
web page like the Proxmox console.

The plan picks RFB (VNC) over RDP as the v1 protocol and explains why: a
minimal Raw-encoding RFB server is a few hundred lines of blocking std::net
(the wozbug.rs embedded-server pattern), the pure Scr/Tty renderers already
hand back exactly the Vec<u32> framebuffer such a server needs, and noVNC gives
the browser console for free. It documents the accelerated-renderer constraint
that rules out the Xvfb + x11vnc route, a frontend-agnostic driver refactor
that keeps the SDL path byte-for-byte unchanged, RFB keysym -> two.key() input
mapping, an embedded WebSocket transport so noVNC connects with no websockify,
multi-VM orchestration, security posture, and RDP (ironrdp) as an optional
later track. Sliced into PR-sized phases with per-phase verification gates.


Claude-Session: https://claude.ai/code/session_01W9SGzucxQrUm3mPrYCHziz

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-13 23:10:26 -04:00
Stefan ArentzandClaude Fable 5 185f64895f Emulate the UniDisk 3.5 Controller (Liron) with .2mg images (liron.rs) (#259)
The SmartPort card that put 800K 3.5" drives on the Apple II, as
virtual hardware in the hdd.rs style: hand-assembled $Cn00 firmware
over magic DEVSEL ports, no IWM emulation. Two drives, .2mg images of
exactly 400K or 800K, ProDOS order; the 2IMG locked flag mounts
read-only and writes land back in the file at data_offset, header
preserved.

The firmware carries a real SmartPort identity - signature $Cn07=$00,
ID type at $CnFB, ProDOS entry via $CnFF with the SmartPort dispatch
at entry+3 - and implements both protocols: the ProDOS block driver
with the drive bit in $43, and a SmartPort dispatch (the full
JSR-with-inline-bytes convention, return address fixed up past the
command and parameter-list bytes) serving STATUS, READ_BLOCK and
WRITE_BLOCK, with block calls translated onto the ProDOS pump.
Squeezing two protocols into one 256-byte page took a tiny two-pass
assembler (Asm) instead of hdd_rom's hand-counted offsets, plus a few
documented economies: the third SmartPort block byte is ignored (800K
is 1600 blocks), $45 is not preserved across transfers, and empty
drives report 0 blocks from ProDOS STATUS.

The boot test settled the plan's open question: the Enhanced //e
Autostart scan accepts the SmartPort signature, so ProDOS 2.4.3 boots
to Bitsy Bye from an 800K .2mg in slot 5 through this firmware.

Config: {"card": "liron", "drive1": ..., "drive2": ...}, any slot 1-7;
configs/enhanced.json now carries one in slot 5. WozBug SLOTS shows
the drives: S5: UniDisk 3.5 (D1 800K, D2 -).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 00:20:57 -04:00
Stefan ArentzandClaude Fable 5 9fad92a5ed Emulate the Saturn Systems 128K RAM Board in slot 0 (saturn.rs) (#257)
The classic third-party alternative to the Apple Language Card, per
the Saturn Operations Manual ch. 9: 128K as eight 16K banks that all
live at $D000-$FFFF, each bank two 4K banks (A/B) at $D000 plus its
own 8K. Bank 1 speaks the exact Language Card protocol, which is the
card's compatibility story - DOS 3.3, Pascal and VisiCalc treat it as
a 16K card and never notice the other seven banks.

The $C08x decode: A2=0 is the Language Card state machine (A3 now the
4K bank, A0/A1 read/write enables, copied from alc.rs including the
read-twice write-enable - the manual's "PEEK or POKE" prose is looser,
but the LC-compatible semantics are what software relies on); A2=1
selects the 16K bank ($C084-7 are banks 1-4, $C08C-F banks 5-8), with
the board state persisting across switches. Power-up: ROM read, write
protect, bank 1.

The slot-0 plumbing grows from a language_card bool into two::Slot0
(Language / Saturn128 / Empty), mirroring how SlotDevice mirrors the
config cards. The config gains {"card": "saturn128"}, slot 0 only,
][+ only, same rules as the language card; WozBug's SLOTS shows
S0: Saturn 128K (bank n).

Tested: all sixteen $C08x offsets against the manual's table, eight
banks holding independent contents on the bus with the A/B split at
$D000 and the shared per-bank 8K, and the compatibility claim end to
end - DOS 3.3 boots, prints (LOADING INTEGER INTO LANGUAGE CARD),
and INT/FP swap between Integer BASIC in Saturn bank 1 and Applesoft
in ROM.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:21:09 -04:00
Stefan ArentzandClaude Fable 5 7ccd4b369d Model slot 0: the ][+ Language Card becomes a configurable card (#256)
The Alc was hardwired into Two::new_2plus, so every ][+ was a 64K
machine and configs/plus.json could not even mention the card it was
supposed to demonstrate. Slot 0 now lives in the machine.slots table
(owner's decision — hardware-faithful, keyed "0"), restricted to
"language" or "empty": the socket has no $Cn00 firmware space, so
nothing else can work there, and the //e rejects "0" outright — its
language card is soldered onto the motherboard.

The literal-table rule now covers slot 0, a deliberate breaking change:
a ][+ config whose slots table omits "0" is a stock 48K machine, with
the motherboard ROM directly at $D000-$FFFF and slot 0's DEVSEL range
as unmapped as any other empty slot's. The default table (and the
--set materialization) gains "0": {"card": "language"}, so bare
command lines keep the classic 64K build and

  --set machine:slots:0:card=empty

is the 48K opt-out. configs/plus.json now declares the card.

Slot 0 never becomes a SlotDevice: build_machine consumes it as a
language_card flag on Two::new_with_slots, and new_2plus either wires
the Alc as before or maps the ROM straight onto the bus.
Two::language_card() reports the socket and WozBug's SLOTS shows an
S0 line on the ][+. A guard catches the --set plus --model 2e path
that bypasses config validation.

Tested: the $C083/$C083 write-enable sequence changes $D000 with the
card and bounces off ROM without it, and DOS 3.3 boots to the prompt
on the 48K machine — it just skips loading Integer BASIC.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 21:30:10 -04:00
Stefan ArentzandClaude Fable 5 f29d2e9e13 WozBug: a resident Woz-Monitor-style debugger with breakpoints, watchpoints, and a TCP line server (#254)
* Add the debugging-tools plan (notes/DEBUGGING_TOOLS.md)

WozBug: a MicroBug-inspired (TN1136), Woz-Monitor-dialect minimal
debugger — one command core with test, TCP-line-server, and
breakpoint-pause frontends — plus CPU breakpoints, device-state
commands, and a built-in symbol table. Grounded in what the PR #253
RWTS hunt actually required.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* WozBug phase 1: CPU breakpoints and the monitor command core

Cpu gains PC breakpoints (add/remove/stopped/stop/resume): a hit makes
step() return 0 without executing until resume(), which skips the
breakpoint at the current PC once so stepping off a hit makes progress.
The empty-breakpoint fast path is one always-false branch — the Dormann
suite times identically (0.19s release) before and after, so this lives
in the normal build with no debug split.

ewm/src/wozbug.rs is the command core: MicroBug in role (TN1136), Woz
Monitor in dialect. 280.29F dumps (a bare Return continues from the dot
address), 300:A9 20 deposits, R / PC=BD00 / A=FF registers, S [n]
stepping with trace lines, G / 300G, B breakpoints — plus the
machine-aware commands DSK, SW, TEXT, and SLOTS, and a built-in symbol
table for Monitor/DOS/ProDOS landmarks (B RWTS works; R prints
PC=BD25 (RWTS+$25)). Dumps read through the bus, soft-switch side
effects included, as on real hardware.

ewm/tests/wozbug.rs replays the PR #253 hunt with tools instead of
temporary code: boot DOS 3.3, B RWTS, CATALOG lands on the breakpoint,
R names the site, DSK shows the controller, Y/A -> IOB verified, S/G
resume cleanly to the catalog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* WozBug phase 2: the --wozbug line server and --break flag

`ewm two --wozbug [port]` (default 6502) serves WozBug on a local TCP
port, one nc-friendly client at a time; `--break addr[,addr]` arms
breakpoints at boot — hex or built-in symbols (--break RWTS) — and
implies the server. The server threads only move strings over channels:
the frame loop drains commands and executes them on the machine's own
thread between frames, so there is no locking and an idle server costs
one try_recv per loop. The frame burst breaks on a 0-cycle step, and a
rising stopped edge announces "stopped at ..." + registers to both the
client and stderr.

Verified live: --break RWTS with the DOS 3.3 disk, nc connect receives
the stop announcement, R shows the IOB pointer in Y/A, S 3 traces
RWTS's first instructions, G resumes the boot. README gains a
"Debugging with WozBug" section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* WozBug Phase 3: watchpoints, trace toggle, and disassembly

Watchpoints live in Memory as inclusive address ranges. When any are
set, reads and writes record the first watched access per instruction
(WatchHit: address, direction, value); the CPU checks after each
instruction completes and stops post-instruction, so registers show the
state right after the touching access. Opcode/operand fetches count as
reads; the trace formatter's peeks are excluded by clearing the hit
after the trace block. With no watchpoints armed the recording paths
are gated on an empty-vec check: the Dormann suite measures 0.19s
release either way, and 0.21s with a watchpoint armed, so the feature
stays in the normal build.

New WozBug commands:

  W             list watchpoints
  W 2000.3FFF   watch a range (W 400 for one byte)
  W-2000.3FFF   clear one, W- clears all
  T / T-        CPU trace to stderr on/off
  L [addr]      disassemble 16 instructions, plain L continues

The stop banner now names the access that fired:
watch: write 0427 = C1.

Dialect fix recorded as a regression test: B, W, and L only act as
commands when the argument is separated by a space or '-'. B and D are
hex digits, so BD00 must examine memory at $BD00, never set a
breakpoint at $D00.

ewm_core::fmt grows disassemble(cpu, addr) -> (line, next_addr),
reusing the trace formatter and instruction table.

Also adds a server test that sequential client connections are served
after a client disconnects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:45:43 -04:00
Stefan ArentzandClaude Fable 5 6a6fd790eb Make machine construction slot-table-driven (JSON_CONFIG Phase B) (#252)
* Make machine construction slot-table-driven (JSON_CONFIG Phase B)

The machine builders no longer hard-wire the card set — Two::new_*
constructs from a slot table, so a config can put any card in any slot:

- Dsk/Clk/Hdd decode only the DEVSEL low nibble, so one implementation
  serves any slot; the clock and hard-disk firmware are generated per
  slot (clk_rom/hdd_rom patch the unit byte, $Cn page, and port
  operands), with golden tests pinning the slot-1/slot-7 images
  byte-for-byte to the pre-generator statics. The IouE holds owned slot
  ROM copies so generated firmware can be installed.
- Two::new_with_slots builds up to three Disk ][ controllers (the
  classic maximum), one Thunderclock, any number of hard drives, and
  empty slots (reading $00, which the Autostart scan skips). new/
  new_with_aux delegate with the default table, so the whole existing
  suite runs unchanged. Boot stays the real Autostart ROM slot scan —
  highest populated slot wins, as on hardware; no config boot hint.
- The Phase A layout gate in config validation is replaced by
  multiplicity rules; machine.slots is now optional (absent = the
  default layout, present = literal). --drive1/--drive2/--hdd became
  slot 6/7 sugar merged into the table, preserving CLI-over-config
  precedence. Drive lights OR across controllers; drag-drop targets the
  boot controller.
- New ewm/tests/two_slots.rs: controller independence at the bus level,
  boot-scan order, empty slots falling through to BASIC, a clock in
  slot 2 surviving a full ProDOS 2.4.3 boot, and hard drives in two
  slots serving distinct images.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add ready-made machine configs for the classic setups

configs/plus.json is the Apple ][+ (language card built in): green
monitor, Disk ][ in slot 6, nothing else. configs/enhanced.json is the
Enhanced //e (65C02 inherent to the model): extended 80-column 64K card
in the aux slot, Disk ][ in slot 6, color monitor. Neither names a
disk — the --drive1/--drive2 flags merge into the config's slot 6 card.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Replace the drive flags with generic --set config overrides

--set <key>=<value> overrides any config value by colon-separated key
path (--set machine:slots:6:drive1=game.dsk, --set display:monitor=
amber); the --drive1/--drive2/--hdd sugar flags are removed.

Config sources now layer left-to-right into one JSON document:
--config files load through the typed path (per-file validation and
relative-path resolution intact) and serialize back to JSON — the
config types gained serde::Serialize, fronting Phase C — while --set
mutates the document directly; one typed conversion + validation runs
at the end. Deep-merge rules: objects recurse, null/empty-array
overlays are no-ops, and objects whose "card" discriminators differ
replace wholesale. A --set entering machine:slots on a slotless
document materializes the default table first, so a bare
--set machine:slots:6:drive1=x behaves exactly like the removed
--drive1. Values parse as JSON when they are JSON (numbers, booleans,
whole objects — the one-line --hdd replacement) and as plain strings
otherwise.

The boo-menu launcher speaks --set for dropped media; README and the
notes record the new syntax and the multi-file deep-merge semantic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix CI: point the full.json fixture at a committed disk image

The disks directory is gitignored with only four images force-added;
"Total Replay v6.0.1.hdv" exists only in local working trees, so the
two_config.rs existence assertion failed on CI (and has kept main red
since #251 merged). The fixture's slot 7 hard drive now uses the
committed ProDOS_2_4_3.po block image instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:15:32 -04:00
Stefan Arentz a37fba0a98 Merge pull request #251 from st3fan/claude/json-config-phase-a
Add JSON machine configuration via --config (JSON_CONFIG Phase A)
2026-07-11 15:27:21 -04:00
Stefan ArentzandClaude Fable 5 e83139331e Add JSON machine configuration via --config (JSON_CONFIG Phase A)
A whole machine can now be described in one JSON file and started with
`ewm two --config myemulator.json`:

- ewm/src/config.rs: serde types mirroring the schema; load() parses,
  validates semantically (Phase A accepts only today's buildable layout:
  slot 1 thunderclock, slot 6 diskii, slot 7 harddrive, any aux card),
  and resolves relative paths against the config file's directory.
- --config in parse_options via a two-pass parse: pass 1 seeds Options
  from the file, pass 2 is the unchanged flag loop — so explicitly given
  CLI flags override config values structurally.
- Config-only settings cpu.speed and input.controller wire to the
  existing palette mechanisms at startup (including the audio
  cycle-to-sample pairing and a preferred-gamepad name scan).
- schema/ewm-config.schema.json: draft 2020-12, derived from the config
  structs by a golden-file test (schemars is a dev-dependency only;
  regenerate with EWM_UPDATE_SCHEMA=1 cargo test -p ewm).
- Tests: schema lockstep, parse/validation/rejection messages,
  relative-path resolution, CLI-overrides-config precedence, and a boot
  gate (a config naming the DOS 3.3 disk boots to the ] prompt exactly
  like --drive1 does).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 15:16:03 -04:00
Stefan ArentzandClaude Fable 5 21243789ea Follow the default-branch rename: CI triggers on main
The push trigger in ci.yml still said master, so CI would silently stop
running on pushes after the GitHub-side rename to main. Also the one
living-document reference (MAC_APP.md branch strategy). Historical notes
keep their original wording.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 13:22:28 -04:00
Stefan ArentzandClaude Fable 5 6f7a79c9ad Add the JSON machine-configuration plan (notes/JSON_CONFIG.md)
Plan for `ewm two --config myemulator.json`, per the owner's decisions:
JSON over TOML (better supported); a committed JSON Schema
(schema/ewm-config.schema.json, derived from the serde structs via
schemars and locked by a round-trip test) covering everything the CLI
and Command Palette configure today -- model, drives, hdd, aux card,
monitor style, scanlines, boot delay, fps, memory regions, the palette's
accelerator speeds and controller pick; plus configurable slots 1-7
(diskii with drive images, harddrive, thunderclock, empty -- multiple
Disk ][ controllers legal in the schema). CLI options stay; only
--config is added, with explicit flags overriding config values, and
relative paths resolving against the config file's directory (the
property .ewmachine bundles need).

Three phases: A = --config + schema + today's layout expressible (other
layouts error clearly); B = table-driven machine construction for real
slot flexibility (multiple controllers at $C080+slot*16 with per-slot P5
ROMs, empty slots, boot-scan order); C = palette "save machine
configuration" round-tripping into the .ewmachine story.

MAC_APP.md Phase 3 rewritten TOML->JSON; the IDEAS.md bullet now points
here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:16:08 -04:00
Stefan ArentzandClaude Fable 5 3bb74324c0 Package EWM as a native Mac application (MAC_APP.md Phase 1)
scripts/make-app.sh assembles dist/EWM.app, a self-contained
double-clickable bundle:

- SDL3 is compiled from source and linked statically (a new sdl-static
  cargo feature; dev/CI builds keep the Homebrew pkg-config path as the
  default feature). Verified: otool -L shows only system frameworks.
- The icon is "][" rendered by the emulator's own Apple ][ character
  generator (examples/icon.rs), green phosphor on a dark bezel, drawn at
  integer scales per icon size and written as PNGs by a hand-rolled
  ~40-line PNG encoder (flate2 as a dev-dependency for the zlib stream);
  iconutil turns the set into EWM.icns at bundle time, so no binary icon
  is committed.
- Info.plist declares CFBundleDocumentTypes for .dsk/.do/.po/.nib/.woz
  and .hdv at Alternate rank, ad-hoc codesign for local use (real
  signing/notarization is Phase 2).

Opening a disk image from the Finder arrives as an SDL drop event, which
also buys drag-and-drop: the boo menu boots the ][+ with a dropped floppy
in drive 1 (or .hdv on slot 7), and dropping a floppy on a running
machine swaps drive 1. The new media module classifies images -- the one
subtlety is .po, which is a floppy at exactly 143,360 bytes and a ProDOS
hard-drive volume when larger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:24:56 -04:00
Stefan ArentzandClaude Fable 5 a42cbdd912 Add the native Mac application plan (notes/MAC_APP.md)
Four phases: a self-contained double-clickable EWM.app (static SDL3,
hand-rolled bundle script, ad-hoc signing, .dsk/.woz/.hdv file
associations delivered as SDL drop events, an icon rendered by the
emulator's own character generator); distribution polish (Developer ID +
notarization + DMG); .ewmachine machine documents built on the config-file
idea; and finally the VMware-Fusion-style library app that spawns each
machine as its own app instance. Option 1 first, as the owner decided --
it produces exactly the building blocks option 2 orchestrates. Grounded
against the tree: the binary currently links Homebrew SDL3 dynamically,
sdl3 0.18 offers build-from-source-static, and the rewrite already made
the binary Finder-launch-safe (include_bytes ROMs, no cwd dependence).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:06:08 -04:00