# 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.md` Phase 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 ` 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_options` runs two passes — pass 1 loads `--config` files into `Options`, 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 `--config` flags apply in order; config and CLI `--memory` regions 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.json` byte-for-byte in sync; regenerate with `EWM_UPDATE_SCHEMA=1 cargo test -p ewm schema_matches_committed`. - **serde caveat resolved**: `deny_unknown_fields` on the internally tagged `SlotCard` enum *does* reject typo'd keys with current serde (the `unknown_slot_card_key_is_rejected` canary 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 `--memory` flag stays decimal-only. - `cpu.speed` and `input.controller` exist 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)`** is the table-driven constructor; `Two::new`/`new_with_aux` delegate 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 because `Hdd::new` needs 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_ROM` is 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, and `dsk()` 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 in `ewm/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/`0xFF` this 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) — `Two` keeps them in a slot-keyed map like the controllers. - **Absent `machine.slots` = the default layout**; a *present* `slots` object (even `{}`) is literal — absent keys inside it are empty slots. This keeps `{"machine": {"model": "apple2plus"}}` equal to bare `ewm two` while honoring "an absent slot key means empty". *(The `--drive1`/ `--drive2`/`--hdd` sugar 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 =`** 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`/`--hdd` sugar flags are **removed** (owner's decision); the boo launcher and docs speak `--set`. - **One config document, sources layered left-to-right**: `--config` files load through the typed path (per-file validation and relative-path resolution intact) and are serialized back to JSON (`serde::Serialize` on the config types — fronting Phase C); `--set` mutates the document directly. One typed conversion + validation at the end (`config::from_document`). Consequence: multiple `--config` files 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 by `plans/20260719-01-flag-retirement.md`; `--serve` is the one document-overriding flag left.)* - **Merge rules** (`config::merge_documents`): objects merge recursively; `null` and 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_set` mirrors the card rule: setting a different `card` resets the object's other fields. - **Slots materialization**: a `--set` entering `machine:slots` on a document without one materializes the default table first, so `--set machine:slots:6:drive1=x` on a bare command line extends the default machine exactly like the removed `--drive1` did. - **Value typing**: a `--set` value 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 `--hdd` replacement); anything else is a plain string. Escape hatch for values that accidentally parse as JSON (a file named `123`): quote them (`--set 'machine:slots:6:drive1="123"'`). `--set` path 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:`** loads one of the embedded copies of the `configs/` files (`include_str!`, a static table in `config.rs` — no build script). Names are the schema's model tokens, matching the file stems 1:1: `builtin:apple2plus` (`configs/apple2plus.json`) and `builtin:apple2enhanced` (`configs/apple2enhanced.json`; the files were renamed from `plus.json` / `enhanced.json`). `builtin:list` prints the names with descriptions and exits 0, like `--help`; an unknown name errors listing the available names. A literal file named `builtin:x` is 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_builtin` rejects file references at load time and the `builtins_load_and_are_self_contained` test pins the property (plus: every builtin carries a `description`, and the table stays sorted). - **Top-level `description`** joined the schema — a one-line human description shown by `builtin:list`, usable by any config file. - **Bare `ewm two` boots `builtin: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.machine` and `Machine.model` became `Option`, so any fragment — `{}`, a slots-only overlay — parses. `merge_documents` needed no change (absent options serialize to `null`, already a merge no-op). - **Validation split in `config.rs`**: `validate` is *structural* (per file, fragment-judgeable: unknown keys, enum values, slot rules, multiplicity, sizes/addresses/ports); `validate_complete` is *completeness* (final document only: `machine.model` present, 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 `--config` path) still require completeness per file — a partial file fails with `machine.model is required (is this an overlay? use --config-overlay)` — while `load_document` runs only the structural pass (with the file named in errors and relative paths resolved) and is the path overlays will load through. `from_document` runs both passes on the final layered document; its missing-model message is `machine.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 into `schema/ewm-config.schema.json` and commits the relaxed one as `schema/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 `** layers a partial config onto the document; repeatable, and all 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 (`load_overlay_document` → `load_document`): the overlay file is named in errors and its relative paths resolve against its own directory. The `builtin:` 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 `--config` max** (plan recommendation adopted): a second errors with `only one --config allowed; use --config-overlay for additional layers`. Mildly breaking — two `--config` files 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 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". 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 in `config.rs` tests; `--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:apple2enhanced` instead. ## Config sources — `--print-config` (C4, recorded as built) Phase C4 of `plans/20260718-02-config-sources.md`: - **`ewm two … --print-config`** prints 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 of `apply_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`, `--break` and the hidden `--screenshot` are 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_document` does the shaping — it drops `null`s, 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 `--config` it yields identical `Options` (pinned by e2e tests, including a composed base + overlay + `--set` + flags command line). Path fields are emitted as the run would use them, so `--set` paths 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**: `Model` grew `apple1` / `replica1` tokens with a derived **family** (`Family::Apple2` vs `Family::Apple1`); a config fully describes *which* machine it is — the property `.ewmachine` bundles 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`, and `debug.enabled` are **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. `remote` for 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 `two` refuses 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` — bare `ewm one` boots 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:replica1` are minimal (`description` + `machine.model`; the ROMs are baked into the machine); `builtin:list` shows 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 as `plans/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`) / `` / ``, with a `meta.json` holding 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-Since` from the stored validators) 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 downloaded machine still boots offline. - **A URL is a source, not a path**: `resolve_paths` leaves it alone (like `builtin:`), but `referenced_files` *does* 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* in `hdd.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 including `ring`, 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 =` (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 - `, 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 in `machine.slots`** (owner's decision — hardware-faithful, not a separate field). The ][+ language card was hardwired into `Two::new_2plus` before this; now it is a declared card. - **Card set restricted**: slot 0 takes only `"language"`, `"saturn128"` or `"empty"` — it has no `$Cn00` firmware 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 `slots` table omits `"0"` is a stock **48K machine** ($D000–$FFFF motherboard ROM on the bus, slot 0's DEVSEL range unmapped). `configs/apple2plus.json` declares the card explicitly. The default table (absent `slots`, and the `--set` materialization) gains `"0": {"card": "language"}`, so bare command lines stay the classic 64K build; `--set machine:slots:0:card=empty` is the 48K opt-out. - **Machine plumbing**: slot 0 never becomes a `SlotDevice` — `build_machine` consumes it as a `two::Slot0` (Language / Saturn128 / Empty) passed to `Two::new_with_slots`. `Two::slot0()` reports it and WozBug's `SLOTS` shows 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 what `alc.rs` implements). 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 the `hdd.rs` style — hand-assembled `$Cn00` firmware 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 at `data_offset + block*512`, header preserved. Any slot 1–7, no multiplicity limit; `configs/apple2enhanced.json` carries one in slot 5. - **SmartPort identity is real**: signature `$Cn07=$00`, ID type at `$CnFB`, ProDOS entry via `$CnFF` with 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), `$45` is not restored after transfers and `$42-$45` are borrowed by the dispatch (ProDOS rebuilds `$42-$47` per call), and ProDOS STATUS reports empty drives as 0 blocks rather than an error (reads fail with `$2F` where it matters). The boot gap below `$40` houses 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 `description`s throughout so editors show help): ```json { "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.model` required; everything else optional with the current defaults (an empty `{ "machine": { "model": "apple2plus" } }` is today's bare `ewm two`). - **`slots`**: object keyed `"1"`–`"7"`; each value is a card object discriminated by `"card"`: `"diskii"` (`drive1`/`drive2` image paths, both optional), `"harddrive"` (`image`), `"thunderclock"`, `"empty"`. When the whole `slots` object 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" }` — `size` only valid with `ramworksiii`; whole `aux` object 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: false` everywhere** — 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 `.ewmachine` bundles (MAC_APP Phase 3) portable. ## Implementation ### Dependencies (flagged for the owner) - **`serde` + `serde_json`** for parsing into typed structs with `deny_unknown_fields` — the ecosystem standard; this is EWM's first general-purpose third-party dependency beyond sdl3/fontdue/chrono. - **`schemars`** to *derive* the JSON Schema from the same serde structs — one source of truth. A unit test regenerates the schema and asserts it matches the committed `schema/ewm-config.schema.json` byte-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>` in `parse_options`: the file populates `Options` first; **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 7 `harddrive`, slot 1 `thunderclock`, 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.speed` wire 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 `--drive1` does; CLI-overrides-config precedence. - **Docs**: README `--config` section + an example; `MAC_APP.md` Phase 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**: `Dsk` instances 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/`$FF` originally 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 `$00` and 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 `.ewmachine` document (MAC_APP Phase 3). - `serde::Serialize` on the same structs; a round-trip test (save → load → identical machine). ## Risks & open questions - **`.po` in `slots`**: the floppy/hard-drive ambiguity does not exist here — the slot's card type says which is meant. (The drag-drop size heuristic in `media.rs` stays 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 `boot` hint 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's `machine.model` enum leaves room (`"apple1"`, `"replica1"`) if wanted later.