Commit Graph
22 Commits
Author SHA1 Message Date
Stefan ArentzandClaude Opus 4.8 73a38e19fa Mouse plan M5: docs + as-built notes/MOUSE.md
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:42 -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 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 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 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 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 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 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 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 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