Skip to content

refactor: break up main.js into feature modules #638

@mattgodbolt

Description

@mattgodbolt

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

  1. Extract one module at a time, keeping main.js working at each step
  2. Each module gets its own PR for easy review/rollback
  3. Start with the most self-contained extraction (probably modal-manager.js)
  4. Add unit tests for extracted modules (previously impossible due to global state)
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureA feature requestjavascriptPull requests that update javascript codeuiIssues affecting the web user interface or browser experience

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions