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>
31 KiB
JSON Machine Configuration (--config) — Implementation Plan
A working document for configuring a whole machine from one JSON file:
ewm two --config myemulator.json
In the house style: re-read at the start of every session, update as phases
land. Every phase passes the full gates (cargo fmt --check,
cargo clippy --all-targets -- -D warnings, cargo test).
Owner's decisions (recorded)
- JSON, not TOML — better supported (editors, JSON Schema tooling,
every language). This supersedes the TOML wording in
MAC_APP.mdPhase 3. - A JSON Schema captures everything configurable today via the command line and the Command Palette.
- Slots are configurable: no cards, three Disk ][ controllers, the different aux cards — the machine's physical layout lives in the file.
- CLI options stay — only
--config <path>is added for now.
Status
| Phase | Description | Size | Status |
|---|---|---|---|
| A | --config + schema + serde types; today's layout expressible |
M | Done |
| B | Real slot flexibility: any slot, multiple Disk ][ controllers, empty slots | M/L | Done |
| Sources | Built-ins, overlays, --print-config (plans/20260718-02-config-sources.md C1–C5) |
M | Done |
| One family | The same document for ewm one (plans/20260719-02-one-config.md O1–O5) |
M | Done |
| C | "Save current setup" from the palette; .ewmachine integration |
M | Not started — seeded by options_to_config (C4) |
Phase A decisions (recorded as built)
- Precedence is structural, not tracked:
parse_optionsruns two passes — pass 1 loads--configfiles intoOptions, pass 2 is the unchanged flag loop, which only assigns a field when its flag is present. CLI-overrides-config falls out without per-flag "was it given" state. Multiple--configflags apply in order; config and CLI--memoryregions are additive. - schemars is a dev-dependency only (owner's decision): the derive is
gated
#[cfg_attr(test, derive(schemars::JsonSchema))], keeping schemars out of release builds. Consequence: no#[schemars(...)]attributes on the config structs, ever — doc comments become the schema descriptions. Promote it to a plain dependency if an attribute is genuinely needed. - Schema regeneration: the golden-file test keeps
schema/ewm-config.schema.jsonbyte-for-byte in sync; regenerate withEWM_UPDATE_SCHEMA=1 cargo test -p ewm schema_matches_committed. - serde caveat resolved:
deny_unknown_fieldson the internally taggedSlotCardenum does reject typo'd keys with current serde (theunknown_slot_card_key_is_rejectedcanary test pins this). - Slot rulings: slot 7
"empty"is accepted (just no HDD attached);"empty"in slots 1 and 6 is rejected with the Phase B message — the machine builders hard-wire the Thunderclock and Disk II today, so "no card there" is real slot flexibility. (Superseded by Phase B: any card in any slot.) - Memory addresses are strings, hex (
"0xd000") or decimal ("53248"); the CLI--memoryflag stays decimal-only. cpu.speedandinput.controllerexist only in the config (no new CLI flags), wiring to the palette's speed constants and a preferred-name gamepad scan at startup.
Phase B decisions (recorded as built)
Two::new_with_slots(model, aux, &BTreeMap<u8, SlotDevice>)is the table-driven constructor;Two::new/new_with_auxdelegate with the default table (Thunderclock@1, Disk II@6), so the whole pre-Phase-B test suite runs unchanged. The machine table carries card kinds only — media inserts afterwards (load_disk_at(slot, drive, path),attach_hdd_at(slot, path); the hard drive attaches post-construction becauseHdd::newneeds its image up front).- Devices decode only the DEVSEL low nibble (
addr & 0x0f); each is registered over its slot's 16-byte range, so one implementation serves any slot.DSK_ROMis naturally slot-agnostic (the P5 ROM derives its slot from the return address); the clock and hard-disk firmware are generated per slot (clk_rom(slot),hdd_rom(slot)), with golden tests pinning the slot-1/slot-7 images byte-for-byte to the pre-generator statics. - Boot controller = highest slot with a Disk II;
load_disk, the drag-drop handler, anddsk()target it. No config boot hint — the real Autostart ROM scan (7→1) picks the boot device, exactly as on hardware (proved by the boot-scan tests inewm/tests/two_slots.rs). - Empty slots read
0x00(owner's decision), the pre-existing unmapped-read behavior on both models — it fails the Autostart boot signature just as safely as the floating/0xFFthis plan originally sketched. - Multiplicity: at most three Disk ][ controllers (the classic
maximum) and one Thunderclock (ProDOS installs a single clock driver);
hard drives are unlimited (owner's decision) —
Twokeeps them in a slot-keyed map like the controllers. - Absent
machine.slots= the default layout; a presentslotsobject (even{}) is literal — absent keys inside it are empty slots. This keeps{"machine": {"model": "apple2plus"}}equal to bareewm twowhile honoring "an absent slot key means empty". (The--drive1/--drive2/--hddsugar this section originally described was replaced by--set; see the CLI overrides decisions below.) - Drive lights are OR'ed across controllers — the status bar and LED strip keep their two-light layout; at any moment at most one controller spins, so the pair reads as the active controller's drives.
CLI overrides (--set) decisions (recorded as built)
--set <key>=<value>overrides one config value by colon-separated key path (--set machine:slots:6:drive1=game.dsk,--set display:monitor=amber). A separate flag (owner's decision — not overloaded onto--config), and the--drive1/--drive2/--hddsugar flags are removed (owner's decision); the boo launcher and docs speak--set.- One config document, sources layered left-to-right:
--configfiles load through the typed path (per-file validation and relative-path resolution intact) and are serialized back to JSON (serde::Serializeon the config types — fronting Phase C);--setmutates the document directly. One typed conversion + validation at the end (config::from_document). Consequence: multiple--configfiles now deep-merge instead of a later file's slots table replacing wholesale. The remaining convenience flags (--model,--color, …) still override the finished document, per the Phase A precedence design. (Since superseded: the per-setting convenience flags were retired byplans/20260719-01-flag-retirement.md;--serveis the one document-overriding flag left.) - Merge rules (
config::merge_documents): objects merge recursively;nulland empty-array overlays are no-ops (a source that doesn't set a field must not clear it); two objects whose"card"discriminators differ replace wholesale (merging a diskii's drives into an"empty"card would fail validation).apply_setmirrors the card rule: setting a differentcardresets the object's other fields. - Slots materialization: a
--setenteringmachine:slotson a document without one materializes the default table first, so--set machine:slots:6:drive1=xon a bare command line extends the default machine exactly like the removed--drive1did. - Value typing: a
--setvalue that parses as JSON is used as JSON (numbers, booleans, quoted strings, whole objects —--set 'machine:slots:7={"card":"harddrive","image":"tr.hdv"}'is the one-line--hddreplacement); anything else is a plain string. Escape hatch for values that accidentally parse as JSON (a file named123): quote them (--set 'machine:slots:6:drive1="123"').--setpath values stay as-given (CWD-relative), like the flags they replace; file paths keep resolving against their config's directory. - Array paths are rejected (
machine:memory:0:path) — memory regions come from--memory.
Config sources — built-ins (C1, recorded as built)
Phase C1 of plans/20260718-02-config-sources.md:
--config builtin:<name>loads one of the embedded copies of theconfigs/files (include_str!, a static table inconfig.rs— no build script). Names are the schema's model tokens, matching the file stems 1:1:builtin:apple2plus(configs/apple2plus.json) andbuiltin:apple2enhanced(configs/apple2enhanced.json; the files were renamed fromplus.json/enhanced.json).builtin:listprints the names with descriptions and exits 0, like--help; an unknown name errors listing the available names. A literal file namedbuiltin:xis reachable as./builtin:x(documented, not engineered around).- Built-ins are self-contained: no drive images, memory files, or
trace/state paths — there is no directory to resolve relative paths
against.
load_builtinrejects file references at load time and thebuiltins_load_and_are_self_containedtest pins the property (plus: every builtin carries adescription, and the table stays sorted). - Top-level
descriptionjoined the schema — a one-line human description shown bybuiltin:list, usable by any config file. - Bare
ewm twobootsbuiltin:apple2plus— the default machine is that built-in config, not an in-code layout (owner's decision; the earlier Thunderclock-in-slot-1 in-code default is retired), so code and configs describe the ][+ once.
Config sources — partial configs (C2, recorded as built)
Phase C2 of plans/20260718-02-config-sources.md, the enabling change
for --config-overlay (C3):
- The serde types are partial-friendly:
Config.machineandMachine.modelbecameOption, so any fragment —{}, a slots-only overlay — parses.merge_documentsneeded no change (absent options serialize tonull, already a merge no-op). - Validation split in
config.rs:validateis structural (per file, fragment-judgeable: unknown keys, enum values, slot rules, multiplicity, sizes/addresses/ports);validate_completeis completeness (final document only:machine.modelpresent, plus the model cross-checks — aux is //e-only, the //e has no slot 0 — which an overlay can't be judged on until the merged document names the model). - Loader contract:
load/load_source_document(the--configpath) still require completeness per file — a partial file fails withmachine.model is required (is this an overlay? use --config-overlay)— whileload_documentruns only the structural pass (with the file named in errors and relative paths resolved) and is the path overlays will load through.from_documentruns both passes on the final layered document; its missing-model message ismachine.model is required (start from --config, e.g. --config builtin:apple2plus). - Two schemas, one generator (plan option 2): the generated schema is
now overlay-shaped, so the golden test post-processes the requiredness
(
machine,machine.model) back intoschema/ewm-config.schema.jsonand commits the relaxed one asschema/ewm-config-overlay.schema.json(own title/description) for editor support of overlay files. One regeneration command updates both.
Config sources — overlays (C3, recorded as built)
Phase C3 of plans/20260718-02-config-sources.md:
--config-overlay <source>layers a partial config onto the document; repeatable, and all sources — the--configbase, overlays,--set— apply strictly in command-line order through the existing merge. Overlay files load through the structural-only typed path (load_overlay_document→load_document): the overlay file is named in errors and its relative paths resolve against its own directory. Thebuiltin:scheme is shared with--config(a complete config is a valid overlay); overlay-only command lines start from the default machine, like bare--set.- One
--configmax (plan recommendation adopted): a second errors withonly one --config allowed; use --config-overlay for additional layers. Mildly breaking — two--configfiles used to deep-merge — but that read as an accident once partial layers had their own flag. - Slots materialization extends to overlays
(
merge_overlay_document): an overlay carryingmachine.slotsonto a slotless document materializes the default table first, so--config-overlay hdd7.jsonmeans "the default machine plus a hard drive in slot 7". A base's explicit table (from--config) stays literal — materialization fills a missing table, never touches a present one. All four base × overlay combinations are pinned inconfig.rstests;--set's materialization is untouched (the boo launcher's drag-drop paths behave identically). - Known edge, accepted: overlaying a complete //e config onto the
slotless ][+ default (
ewm two --config-overlay builtin:apple2enhanced) fails completeness — materialization brings in the ][+ default table, whose slot 0 the //e rejects. Consistent with the rules (an overlay means "default machine plus this"); start from--config builtin:apple2enhancedinstead.
Config sources — --print-config (C4, recorded as built)
Phase C4 of plans/20260718-02-config-sources.md:
ewm two … --print-configprints the machine the command line describes — sources and convenience flags applied, i.e. the machine a real run would build — as config JSON on stdout and exits 0. Any load/validation error still exits 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 ofapply_config, kept as a single function so the palette's "save current setup" (Phase C) reuses it. The full mapping was implemented; the plan's fallback (print before convenience flags) wasn't needed.--wozbug,--breakand the hidden--screenshotare debug tooling, not machine configuration, and don't appear.- The document is explicit where it matters, quiet where it doesn't:
model, slot table, display and cpu settings print even at their
defaults (stable against future default changes); off-by-default
extras (strict, debug, boot delay, remote) appear only when enabled.
config::compact_documentdoes the shaping — it dropsnulls, empty arrays, and sections that emptied out, but keeps a genuinely bare"slots": {}(which means "no cards", not "default layout"). Keys print in sorted order (documents are sorted maps) — deterministic, semantics-free. - The printed document round-trips: fed back via
--configit yields identicalOptions(pinned by e2e tests, including a composed base + overlay +--set+ flags command line). Path fields are emitted as the run would use them, so--setpaths given relative to the CWD print relative and would re-resolve against the printed file's directory — save next to where you ran, or use absolute paths.
Config sources — the one family (recorded as built)
plans/20260719-02-one-config.md (O1–O5): ewm one speaks the same
configuration language.
- One document type, not a second schema:
Modelgrewapple1/replica1tokens with a derived family (Family::Apple2vsFamily::Apple1); a config fully describes which machine it is — the property.ewmachinebundles and a future unified launcher need. - Family cross-checks live in the completeness pass, next to the
//e-only rules: on one-family documents,
machine.slots,machine.aux,display.*,cpu.speed,input,boot,remote,state, anddebug.enabledare rejected, not ignored — each error names the offending key and model. Valid everywhere:machine.model,machine.memory,cpu.strict,debug.trace. Relaxing a key for the family (e.g.remotefor REMOTE.md Phase 7) is a one-place change. - The subcommands enforce the family both ways — a one-family
document is a valid config that
tworefuses to run (and vice versa), each error pointing at the right subcommand. - The shared source loop (
config::collect_document) carries pass 1 for both subcommands, parameterized by the seed model (apple2plus/replica1— bareewm oneboots the Replica 1, the C default) and the slots-materialization hook (two-only; a one-family document never grows the ][+ default table). - Built-ins
builtin:apple1/builtin:replica1are minimal (description+machine.model; the ROMs are baked into the machine);builtin:listshows all four names, and the boo bootloader boots the Apple 1 / Replica 1 via--config builtin:…. one's flags retired (--model,--memory,--trace,--strict) under the same v0.0.1 ruling asplans/20260719-01-flag-retirement.md; config memory regions are an upgrade (hex addresses, per-file relative-path resolution).
Disk images over HTTP (recorded as built)
A floppy or hard-drive image path may be an http(s):// URL
(ewm/src/fetch.rs):
- Cache:
$XDG_CACHE_HOME/ewm(else~/.cache/ewm) /<sha1 of the URL>/<file name from the URL>, with ameta.jsonholding the validators. 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. SHA-1 is the crate's own (ws::sha1) — no new dependency for hashing. - Freshness: one conditional GET (
If-None-Match/If-Modified-Sincefrom the stored validators) rather than HEAD then GET — a304costs a single round trip. If the request fails and a copy is cached, the cached copy is used, so a downloaded machine still boots offline. - A URL is a source, not a path:
resolve_pathsleaves it alone (likebuiltin:), butreferenced_filesdoes count it, so a built-in config still may not carry one — builtins must run offline. - Downloaded hard drives mount memory-only (
writeback = false): ProDOS writes update the in-memory image and never reach the cached file, so revalidating a download cannot destroy them. This needed a real change, not just leaving the file handle unopened — an unopenable file means WRITE PROTECTED inhdd.rs, which is a different (and still supported) state for a local image. - 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 includingring, so the headless server build now compiles native crypto too.
What is configurable today (the schema inventory)
Since plans/20260719-01-flag-retirement.md, the CLI column is
--set <key>=<value> (or any config source) for everything — the
per-setting convenience flags are gone. Family rules (the completeness
pass rejects the rest by name): machine.model, machine.memory,
cpu.strict, and debug.trace apply everywhere; machine.cpu and
RAM-bank (size) regions are apple1-family only; everything else
below is apple2-family only.
| Source | Setting | Values |
|---|---|---|
config + --set |
machine.model |
apple2, apple2e, apple2plus, apple2enhanced, apple1, replica1 |
config + --set |
title |
window bar reads EWM - <title>, else plain EWM; the built-ins set the machine name |
config + --set |
machine.cpu |
6502, 65C02 (apple1 family; absent = the model's CPU) |
config + --set |
machine.aux |
80col, ext80col, ramworksiii (+ size; //e only) |
config + --set + palette |
display.monitor |
green, amber, white, rgb |
config + --set + palette |
display.scanlines |
off, light, heavy |
config + --set |
boot.delay |
seconds |
config + --set |
display.fps |
display refresh |
| config / overlay files | machine.memory |
regions: type + address + one of path (a file or builtin:<rom>) or size (an empty RAM bank; apple1 family). On the apple1 family the regions describe the whole board. (--set cannot reach arrays) |
config + --set |
debug.trace, cpu.strict, debug.enabled |
debugging |
config + --set + palette |
cpu.speed |
1.023 MHz (normal), 3.58 MHz, 7.16 MHz — the classic accelerator steps |
config + --set + palette |
input.controller |
picked by name when several are present |
config + --set |
slots 0–7 | card per slot (below); slot 0 is the ][+ language-card socket |
Slot 0 decisions (recorded as built)
- Slot 0 is a
"0"key inmachine.slots(owner's decision — hardware-faithful, not a separate field). The ][+ language card was hardwired intoTwo::new_2plusbefore this; now it is a declared card. - Card set restricted: slot 0 takes only
"language","saturn128"or"empty"— it has no$Cn00firmware space, so firmware-bearing cards can't work there — and those cards fit nowhere else. The //e rejects"0"outright (its language card is soldered onto the motherboard). - The literal-table rule covers slot 0 — a deliberate breaking
change, accepted by the owner: a ][+ config whose
slotstable omits"0"is a stock 48K machine ($D000–$FFFF motherboard ROM on the bus, slot 0's DEVSEL range unmapped).configs/apple2plus.jsondeclares the card explicitly. The default table (absentslots, and the--setmaterialization) gains"0": {"card": "language"}, so bare command lines stay the classic 64K build;--set machine:slots:0:card=emptyis the 48K opt-out. - Machine plumbing: slot 0 never becomes a
SlotDevice—build_machineconsumes it as atwo::Slot0(Language / Saturn128 / Empty) passed toTwo::new_with_slots.Two::slot0()reports it and WozBug'sSLOTSshows the slot 0 line on the ][+ (with the selected bank for the Saturn). DOS 3.3 boots on the 48K machine (it just skips loading Integer BASIC) — regression-tested. "saturn128"— the Saturn Systems 128K RAM Board (ewm/src/saturn.rs, from the Saturn Operations Manual ch. 9): eight 16K banks at $D000–$FFFF, each two 4K banks (A/B) at $D000 plus its own 8K. The $C08x A2=0 column is the exact Language Card protocol (bank 1 is how DOS/Pascal/VisiCalc see a "16K card"); A2=1 selects the 16K bank ($C084–7 → 1–4, $C08C–F → 5–8), with read/write/4K state persisting across switches. Write-enable follows the LC's read-twice rule (the manual's "PEEK or POKE" prose is looser; the LC-compatible semantics are what software relies on and whatalc.rsimplements). Regression-tested: all eight banks hold independent contents on the bus, and DOS 3.3 loads Integer BASIC into bank 1 with INT/FP switching both ways.- Future: an Integer BASIC Firmware card would be a fourth slot 0 card kind; the Saturn 32K/64K siblings would be a size field or variants.
The Liron / UniDisk 3.5 decisions (recorded as built)
"liron"(ewm/src/liron.rs): the UniDisk 3.5 Controller as virtual hardware in thehdd.rsstyle — hand-assembled$Cn00firmware over magic DEVSEL ports, no IWM emulation. Two drives (drive1/drive2), .2mg only, ProDOS-order, exactly 800 (400K) or 1600 (800K) blocks; the 2IMG locked flag mounts read-only; write-back lands atdata_offset + block*512, header preserved. Any slot 1–7, no multiplicity limit;configs/apple2enhanced.jsoncarries one in slot 5.- SmartPort identity is real: signature
$Cn07=$00, ID type at$CnFB, ProDOS entry via$CnFFwith the SmartPort dispatch at entry+3. The dispatch implements STATUS (device count, per-unit status + block count, DIB), READ_BLOCK and WRITE_BLOCK (translated onto the ProDOS driver's pump); everything else returns $21. The Enhanced //e Autostart scan boots it — the boot test proved the scan accepts$Cn07=$00, so an 800K ProDOS .2mg in the highest populated slot boots exactly like a hard drive. - Space economies in the 256-byte firmware (all safe for this
media): the third SmartPort block byte is ignored (800K = 1600
blocks),
$45is not restored after transfers and$42-$45are borrowed by the dispatch (ProDOS rebuilds$42-$47per call), and ProDOS STATUS reports empty drives as 0 blocks rather than an error (reads fail with$2Fwhere it matters). The boot gap below$40houses the SmartPort block-call setup.
The schema (draft)
Draft 2020-12, committed as schema/ewm-config.schema.json. Sketch of
the shape (the committed schema is the authority; field-level descriptions
throughout so editors show help):
{
"machine": {
"model": "apple2enhanced",
"aux": { "card": "ramworksiii", "size": "1m" },
"slots": {
"1": { "card": "thunderclock" },
"5": { "card": "diskii", "drive1": "work.woz" },
"6": { "card": "diskii", "drive1": "dos33.dsk", "drive2": "blank.dsk" },
"7": { "card": "harddrive", "image": "Total Replay v6.0.1.hdv" }
},
"memory": [ { "type": "rom", "address": "0xd000", "path": "custom.bin" } ]
},
"display": { "monitor": "green", "scanlines": "off", "fps": 30 },
"cpu": { "speed": "normal", "strict": false },
"input": { "controller": "Xbox Wireless Controller" },
"boot": { "delay": 1.5 },
"debug": { "trace": "trace.txt", "enabled": false }
}
Schema rules:
machine.modelrequired; everything else optional with the current defaults (an empty{ "machine": { "model": "apple2plus" } }is today's bareewm two).slots: object keyed"1"–"7"; each value is a card object discriminated by"card":"diskii"(drive1/drive2image paths, both optional),"harddrive"(image),"thunderclock","empty". When the wholeslotsobject is absent the machine gets the default layout (clock in 1, Disk II in 6); when present it is literal — an absent slot key means empty, and"empty"says it explicitly. Up to three"diskii"entries, one"thunderclock", any number of"harddrive"cards (Phase B).machine.aux:{ "card": "80col" | "ext80col" | "ramworksiii", "size": "64k".."8m" }—sizeonly valid withramworksiii; wholeauxobject only valid with a //e model ("apple2e"/"apple2enhanced") (enforced in code; the schema documents it).cpu.speed:"normal" | "3.58mhz" | "7.16mhz"— exactly the palette's accelerator steps (SPEED_NORMAL/FAST/FASTER).input.controller: a preferred game-controller name; hot-plug still applies when absent or unmatched.additionalProperties: falseeverywhere — a typo'd key is an error, not a silent ignore.- Relative paths resolve relative to the config file's directory — the
property that makes
.ewmachinebundles (MAC_APP Phase 3) portable.
Implementation
Dependencies (flagged for the owner)
serde+serde_jsonfor parsing into typed structs withdeny_unknown_fields— the ecosystem standard; this is EWM's first general-purpose third-party dependency beyond sdl3/fontdue/chrono.schemarsto derive the JSON Schema from the same serde structs — one source of truth. A unit test regenerates the schema and asserts it matches the committedschema/ewm-config.schema.jsonbyte-for-byte (regenerate with a documented one-liner when the config grows).
Phase A — --config, schema, today's capabilities (M)
ewm/src/config.rs: the serde types mirroring the schema +pub fn load(path) -> Result<Config, String>(parse + semantic validation + path resolution against the config's directory).--config <path>inparse_options: the file populatesOptionsfirst; explicitly given CLI flags override config values (documented precedence — makes quick experiments cheap and keeps.ewmachine+ overrides possible later).- Phase A accepts only layouts the emulator can already build: slot 6
diskii, slot 7harddrive, slot 1thunderclock, any aux card — i.e. today's machine expressed in slot syntax. Any other layout parses fine but errors clearly: "slot 5 diskii: not supported yet (see notes/JSON_CONFIG.md Phase B)". input.controller+cpu.speedwire to the existing palette mechanisms at startup.- Tests: schema/struct agreement (the schemars round-trip); example
configs parse (a committed
ewm/tests/configs/set); unknown-key and bad-value rejections with useful messages; relative-path resolution; a boot gate — a config naming the DOS 3.3 disk boots exactly like--drive1does; CLI-overrides-config precedence. - Docs: README
--configsection + an example;MAC_APP.mdPhase 3 rewritten TOML→JSON pointing here; IDEAS.md config bullet → planned.
Phase B — real slot flexibility (M/L) — done
The machine builders used to hard-wire the card set; Two::new_* now
constructs from the slot table (decisions above):
- Multiple Disk ][ controllers:
Dskinstances at$C080 + slot*16, each with its own P5 boot ROM at$Cn00— up to three controllers / six drives (the classic maximum). The boot scan order follows the //e/][+ autostart convention (highest slot first). - Empty slots: no Thunderclock, no Disk II — the vacated soft-switch
and ROM ranges read
$00(not the floating/$FForiginally sketched; see the Phase B decisions). - Card moves (Disk II in slot 4, clock in slot 2, …) and multiple hard drives fall out of the same table-driven construction.
- Tests (
ewm/tests/two_slots.rs): two controllers with distinct state and disks both readable (bus-level probes at both slots' addresses); empty slot 6 reads$00and the machine falls through to BASIC; boot-scan order; a clock moved to slot 2 (through a full ProDOS boot); hard drives in two slots; the whole existing suite green with the default table, untouched.
Phase C — round-tripping (M)
- Palette: "Save machine configuration…" writes the current state
(monitor, scanlines, speed, mounted disks, aux card) as a valid config
file — the seed of the
.ewmachinedocument (MAC_APP Phase 3). serde::Serializeon the same structs; a round-trip test (save → load → identical machine).
Risks & open questions
.poinslots: the floppy/hard-drive ambiguity does not exist here — the slot's card type says which is meant. (The drag-drop size heuristic inmedia.rsstays for pathless opens.)- Boot order with multiple controllers (resolved in Phase B): the
Autostart scan (slot 7 → 1) picks the boot device — highest populated
slot wins, as on hardware. No
boothint in the config. - Palette state vs config: monitor/scanlines/speed change at runtime; the config sets the initial state only (Phase C's save captures the current one).
one(Apple 1 / Replica 1): out of scope for now — the schema'smachine.modelenum leaves room ("apple1","replica1") if wanted later.