Context
main.js is ~2100 lines of mostly imperative top-level code with ~25 mutable globals. This makes it untestable, hard to navigate, and tightly couples unrelated concerns (UI, emulation setup, file I/O, layout, modal management).
Proposed module extraction
1. media-loader.js — disc/tape/ROM loading logic
Extract from main.js:
loadDiscImage(), loadTapeImage(), loadHTMLFile(), loadSCSIFile()
setDisc1Image(), setDisc2Image(), setTapeImage() (URL state management)
- STH catalogue wiring (
makeOnCat, discSthClick, tapeSthClick, sthClearList, sthStartLoad, sthOnError, filter logic)
- Google Drive UI logic (
gdAuth, gdLoad, drive file list population)
- Disc list population (built-in images)
Interface: Takes a processor, fdc, acia, and config; exposes methods to load/set media.
2. layout.js — screen resize and CRT monitor layout
Extract from main.js:
- The
resizeTv IIFE and window.addEventListener("resize", ...)
setCrtPic() and display mode switching layout
- Sidebar image binding (
sbBind)
- CRT mouse event handler (
onCubMouseEvent)
Interface: Takes the screen canvas and CRT monitor elements; manages layout.
3. snapshot-ui.js — save/load state UI wiring
Extract from main.js:
- Save state click handler (compress → download)
- Load state file picker handler (decompress → restore)
loadStateFromFile(), reloadSnapshotMedia()
isSnapshotFile() detection
- Electron save-state action
Interface: Takes a processor and snapshot functions; manages save/load UI.
4. modal-manager.js — modal lifecycle
Extract from main.js:
anyModalsVisible(), pause-on-modal logic
popupLoading(), loadingFinished()
areYouSure() confirmation dialog
showError() error dialog
- Bootstrap Modal instance creation (currently scattered)
Interface: Centralised modal show/hide with pause/resume coordination.
5. emulation-controls.js — go/stop/step/debug
Extract from main.js:
go(), stop(), benchmarkCpu(), profileCpu(), benchmarkVideo(), profileVideo()
VirtualSpeedUpdater class
- Debug pause/play button wiring
updateDebugButtons()
Interface: Takes a processor and debugger; manages run state.
Synergy with machine-session.js
machine-session.js already wraps the emulator core (CPU, video, sound, disc loading) with a clean lifecycle API (initialise(), boot(), type(), screenshot()). It's used by the jsbeeb-mcp project for headless operation.
The web UI could migrate toward using MachineSession (or a shared base) instead of directly wiring Cpu6502, Video, FDC, ACIA, etc. This would:
- Eliminate the duplicated setup logic between main.js and machine-session.js
- Make the web UI testable (mock or inject a session)
- Allow the same emulator core to be driven by the web UI, Electron, MCP, or tests
The key challenge is that MachineSession currently uses TestMachine (which uses FakeVideo), while the web UI needs real Video with a canvas. A shared EmulatorCore base that both extend could bridge this.
Approach
- Extract one module at a time, keeping main.js working at each step
- Each module gets its own PR for easy review/rollback
- Start with the most self-contained extraction (probably
modal-manager.js)
- Add unit tests for extracted modules (previously impossible due to global state)
- Consider
MachineSession unification as the final step
Globals to eliminate
These main.js globals should become module-scoped or injected:
processor, video → owned by an emulator session
running, frames, frameSkip → owned by emulation controls
parsedQuery → passed to modules that need URL state
discSth, tapeSth → owned by media loader
keyboard, gamepad → owned by input module
🤖 Generated with Claude Code
Context
main.jsis ~2100 lines of mostly imperative top-level code with ~25 mutable globals. This makes it untestable, hard to navigate, and tightly couples unrelated concerns (UI, emulation setup, file I/O, layout, modal management).Proposed module extraction
1.
media-loader.js— disc/tape/ROM loading logicExtract from main.js:
loadDiscImage(),loadTapeImage(),loadHTMLFile(),loadSCSIFile()setDisc1Image(),setDisc2Image(),setTapeImage()(URL state management)makeOnCat,discSthClick,tapeSthClick,sthClearList,sthStartLoad,sthOnError, filter logic)gdAuth,gdLoad, drive file list population)Interface: Takes a processor, fdc, acia, and config; exposes methods to load/set media.
2.
layout.js— screen resize and CRT monitor layoutExtract from main.js:
resizeTvIIFE andwindow.addEventListener("resize", ...)setCrtPic()and display mode switching layoutsbBind)onCubMouseEvent)Interface: Takes the screen canvas and CRT monitor elements; manages layout.
3.
snapshot-ui.js— save/load state UI wiringExtract from main.js:
loadStateFromFile(),reloadSnapshotMedia()isSnapshotFile()detectionInterface: Takes a processor and snapshot functions; manages save/load UI.
4.
modal-manager.js— modal lifecycleExtract from main.js:
anyModalsVisible(), pause-on-modal logicpopupLoading(),loadingFinished()areYouSure()confirmation dialogshowError()error dialogInterface: Centralised modal show/hide with pause/resume coordination.
5.
emulation-controls.js— go/stop/step/debugExtract from main.js:
go(),stop(),benchmarkCpu(),profileCpu(),benchmarkVideo(),profileVideo()VirtualSpeedUpdaterclassupdateDebugButtons()Interface: Takes a processor and debugger; manages run state.
Synergy with
machine-session.jsmachine-session.jsalready wraps the emulator core (CPU, video, sound, disc loading) with a clean lifecycle API (initialise(),boot(),type(),screenshot()). It's used by thejsbeeb-mcpproject for headless operation.The web UI could migrate toward using
MachineSession(or a shared base) instead of directly wiringCpu6502,Video,FDC,ACIA, etc. This would:The key challenge is that
MachineSessioncurrently usesTestMachine(which usesFakeVideo), while the web UI needs realVideowith a canvas. A sharedEmulatorCorebase that both extend could bridge this.Approach
modal-manager.js)MachineSessionunification as the final stepGlobals to eliminate
These main.js globals should become module-scoped or injected:
processor,video→ owned by an emulator sessionrunning,frames,frameSkip→ owned by emulation controlsparsedQuery→ passed to modules that need URL statediscSth,tapeSth→ owned by media loaderkeyboard,gamepad→ owned by input module🤖 Generated with Claude Code