jsbeeb 1.23.0 → 1.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +2 -2
  2. package/package.json +14 -10
  3. package/src/6502.js +39 -42
  4. package/src/6502.opcodes.js +7 -13
  5. package/src/6847.js +4 -5
  6. package/src/6847_fontdata.js +0 -2
  7. package/src/acia.js +2 -3
  8. package/src/app/app.js +0 -1
  9. package/src/app/args.js +0 -2
  10. package/src/app/electron.js +4 -6
  11. package/src/app-bench.js +1 -3
  12. package/src/archive.js +243 -0
  13. package/src/basic-loader.js +0 -7
  14. package/src/basic-tokenise.js +19 -6
  15. package/src/bbc-palette.js +0 -2
  16. package/src/bbcdiscs.js +0 -2
  17. package/src/bem-snapshot.js +1 -3
  18. package/src/binary.js +175 -0
  19. package/src/cmos.js +0 -2
  20. package/src/ddnoise.js +0 -1
  21. package/src/debounce.js +7 -0
  22. package/src/disc-surface.js +0 -2
  23. package/src/disc.js +10 -9
  24. package/src/fake6502.js +11 -24
  25. package/src/fdc.js +9 -51
  26. package/src/filestore.js +10 -17
  27. package/src/hex.js +38 -0
  28. package/src/index.js +11 -0
  29. package/src/intel-fdc.js +18 -20
  30. package/src/{utils_atom.js → keymap-atom.js} +2 -18
  31. package/src/{utils.js → keymap.js} +47 -610
  32. package/src/loader.js +63 -0
  33. package/src/machine-session.js +37 -177
  34. package/src/machine-spec.js +59 -0
  35. package/src/main.js +66 -180
  36. package/src/media-resolver.js +68 -0
  37. package/src/mmc.js +3 -2
  38. package/src/models.js +18 -3
  39. package/src/music5000.js +0 -2
  40. package/src/ppia.js +3 -4
  41. package/src/printer.js +7 -14
  42. package/src/relaynoise.js +0 -1
  43. package/src/rewind.js +0 -2
  44. package/src/sample-player.js +2 -3
  45. package/src/serial.js +0 -2
  46. package/src/snapshot-helpers.js +0 -2
  47. package/src/snapshot.js +0 -2
  48. package/src/soundchip.js +1 -1
  49. package/src/state-utils.js +0 -2
  50. package/src/sth.js +2 -4
  51. package/src/tapes.js +10 -4
  52. package/src/teletext.js +2 -3
  53. package/src/teletext_adaptor.js +2 -4
  54. package/src/teletext_data.js +0 -2
  55. package/src/test-machine.js +281 -0
  56. package/src/touchscreen.js +2 -4
  57. package/src/tube.js +5 -6
  58. package/src/typist.js +103 -0
  59. package/src/uef-snapshot.js +0 -2
  60. package/src/url-params.js +4 -5
  61. package/src/vdu-capture.js +113 -0
  62. package/src/via.js +3 -4
  63. package/src/video-filters/pal-composite.js +0 -2
  64. package/src/video-filters/passthrough-filter.js +0 -2
  65. package/src/video-filters/pixel-grid.js +0 -2
  66. package/src/video-filters/shader-program.js +0 -2
  67. package/src/video-filters/xbr-filter.js +0 -2
  68. package/src/video.js +38 -11
  69. package/src/wd-fdc.js +20 -20
  70. package/src/web/analogue-inputs.js +6 -8
  71. package/src/web/analytics.js +13 -0
  72. package/src/web/audio-handler.js +2 -2
  73. package/src/{audio-utils.js → web/audio-utils.js} +0 -2
  74. package/src/web/autoboot.js +10 -13
  75. package/src/{canvas.js → web/canvas.js} +4 -5
  76. package/src/web/config.js +41 -74
  77. package/src/web/console-surface.js +25 -0
  78. package/src/web/debug.js +6 -5
  79. package/src/web/disc-visualiser.js +0 -2
  80. package/src/web/display.js +21 -4
  81. package/src/{dom-utils.js → web/dom-utils.js} +1 -11
  82. package/src/web/drives.js +1 -1
  83. package/src/web/emulation-loop.js +54 -33
  84. package/src/web/front-panel.js +13 -12
  85. package/src/{gamepads.js → web/gamepads.js} +2 -6
  86. package/src/web/google-drive-picker.js +14 -9
  87. package/src/{google-drive.js → web/google-drive.js} +9 -10
  88. package/src/web/hfe-picker.js +5 -4
  89. package/src/web/keyboard-setup.js +18 -38
  90. package/src/{keyboard.js → web/keyboard.js} +18 -119
  91. package/src/web/layout.js +4 -0
  92. package/src/web/local-disc.js +44 -0
  93. package/src/web/machine.js +27 -20
  94. package/src/web/media-loader.js +38 -147
  95. package/src/{microphone-input.js → web/microphone-input.js} +1 -1
  96. package/src/web/modals.js +6 -9
  97. package/src/web/page-actions.js +88 -0
  98. package/src/web/quick-settings.js +16 -15
  99. package/src/web/reporting.js +0 -7
  100. package/src/{rewind-thumbnail.js → web/rewind-thumbnail.js} +0 -2
  101. package/src/web/rewind-ui.js +29 -29
  102. package/src/web/run-controls.js +39 -0
  103. package/src/web/settings.js +64 -135
  104. package/src/web/snapshot-ui.js +10 -16
  105. package/src/{speech-output.js → web/speech-output.js} +0 -2
  106. package/src/web/sth-picker.js +9 -7
  107. package/src/web/url-state.js +20 -6
  108. package/src/econet/L3FS.dat +0 -0
  109. package/src/econet/scsi.dat +0 -0
  110. package/tests/test-machine.js +0 -609
@@ -1,7 +1,6 @@
1
- "use strict";
2
-
3
- import { debounce, uint8ArrayToString } from "./utils.js";
4
- import { discFor } from "./fdc.js";
1
+ import { discFor } from "../fdc.js";
2
+ import { uint8ArrayToString } from "../binary.js";
3
+ import { debounce } from "../debounce.js";
5
4
 
6
5
  const MIME_TYPE = "application/vnd.jsbeeb.disc-image";
7
6
  const CLIENT_ID = "356883185894-bhim19837nroivv18p0j25gecora60r5.apps.googleusercontent.com";
@@ -138,14 +137,14 @@ export class GoogleDriveLoader {
138
137
  });
139
138
  }
140
139
 
141
- async create(fdc, name, data) {
140
+ async create(name, data) {
142
141
  console.log(`Google Drive: creating disc image: '${name}'`);
143
142
  const response = await this.saveFile(name, data);
144
143
  const meta = response.result;
145
- return { fileId: meta.id, disc: this.makeDisc(fdc, data, meta) };
144
+ return { fileId: meta.id, disc: this.makeDisc(data, meta) };
146
145
  }
147
146
 
148
- makeDisc(fdc, data, meta, layout) {
147
+ makeDisc(data, meta, layout) {
149
148
  let flusher = null;
150
149
  const name = meta.name;
151
150
  const id = meta.id;
@@ -159,12 +158,12 @@ export class GoogleDriveLoader {
159
158
  } else {
160
159
  console.log("Making read-only disc");
161
160
  }
162
- return discFor(fdc, name, data, flusher, layout);
161
+ return discFor(name, data, flusher, layout);
163
162
  }
164
163
 
165
- async load(fdc, fileId, layout) {
164
+ async load(fileId, layout) {
166
165
  const meta = (await this.driveClient.files.get({ fileId, fields: FILE_FIELDS })).result;
167
166
  const data = (await this.driveClient.files.get({ fileId, alt: "media" })).body;
168
- return this.makeDisc(fdc, data, meta, layout);
167
+ return this.makeDisc(data, meta, layout);
169
168
  }
170
169
  }
@@ -1,8 +1,8 @@
1
- import * as utils from "../utils.js";
2
1
  import * as bootstrap from "bootstrap";
3
2
  import { BbcDiscArchive, Provenance, describe as describeHfe, matches, provenancesIn } from "../bbcdiscs.js";
4
3
  import { errorText } from "./reporting.js";
5
4
  import { clearArchiveList, showArchiveMessage } from "./archive-list.js";
5
+ import { noteEvent } from "./analytics.js";
6
6
 
7
7
  const HfeProvenanceLabels = {
8
8
  [Provenance.Captured]: ["Captured", "Direct from disc"],
@@ -57,15 +57,16 @@ export class HfePicker {
57
57
  }
58
58
 
59
59
  async pick(file) {
60
- utils.noteEvent("hfe", "click", file.path);
61
- this.media.setDisc1Image("hfe:" + file.path);
60
+ noteEvent("hfe", "click", file.path);
61
+ const image = "hfe:" + file.path;
62
+ this.media.setDisc1Image(image);
62
63
  const needsAutoboot = this.urlState.params.autoboot !== undefined;
63
64
  if (needsAutoboot) this.processor.reset(true);
64
65
 
65
66
  const name = describeHfe(file).title;
66
67
  this.modals.popupLoading("Loading " + name);
67
68
  try {
68
- const loaded = await this.media.loadDiscImage(this.urlState.params.disc1, this.drives.layoutForDrive(0));
69
+ const loaded = await this.media.loadDiscImage(image, this.drives.layoutForDrive(0));
69
70
  this.drives.putDiscIn(0, loaded);
70
71
  this.modals.loadingFinished();
71
72
  if (needsAutoboot) this.autoboot(name);
@@ -1,11 +1,15 @@
1
- import * as utils from "../utils.js";
2
- import { Keyboard } from "../keyboard.js";
1
+ import { Keyboard } from "./keyboard.js";
3
2
  import { showNotice } from "./reporting.js";
3
+ import { noteEvent } from "./analytics.js";
4
+ import { keyCodes } from "../keymap.js";
4
5
 
5
6
  const PasteBoxId = "paste-text";
6
7
  const TypingTargets = 'input, textarea, select, [contenteditable]:not([contenteditable="false"])';
7
8
 
8
- /** The page's keyboard: the emulated one and the browser shortcuts around it. */
9
+ /**
10
+ * Builds the emulated keyboard and wires the browser's shortcuts around it,
11
+ * exposing it as `keyboard` for whoever needs the machine's keys.
12
+ */
9
13
  export class KeyboardSetup {
10
14
  /**
11
15
  * @param {object} opts
@@ -26,28 +30,28 @@ export class KeyboardSetup {
26
30
  keyboard.addEventListener("resume", () => actions.resume());
27
31
  keyboard.addEventListener("break", (e) => {
28
32
  // F12/Break: Reset processor
29
- if (e.detail) utils.noteEvent("keyboard", "press", "break");
33
+ if (e.detail) noteEvent("keyboard", "press", "break");
30
34
  });
31
35
 
32
36
  const onDown = (note, action) => (down) => {
33
37
  if (down) {
34
- if (note) utils.noteEvent("keyboard", "press", note);
38
+ if (note) noteEvent("keyboard", "press", note);
35
39
  action();
36
40
  }
37
41
  };
38
42
  const alt = { alt: true, ctrl: false };
39
43
  const ctrl = { alt: false, ctrl: true };
40
- keyboard.registerKeyHandler(utils.keyCodes.S, onDown("S", actions.enterDebugger), alt);
41
- keyboard.registerKeyHandler(utils.keyCodes.R, onDown(null, actions.reload), alt);
42
- keyboard.registerKeyHandler(utils.keyCodes.HOME, onDown("home", actions.enterDebugger), ctrl);
43
- keyboard.registerKeyHandler(utils.keyCodes.INSERT, onDown("insert", actions.toggleFast), ctrl);
44
+ keyboard.registerKeyHandler(keyCodes.S, onDown("S", actions.enterDebugger), alt);
45
+ keyboard.registerKeyHandler(keyCodes.R, onDown(null, actions.reload), alt);
46
+ keyboard.registerKeyHandler(keyCodes.HOME, onDown("home", actions.enterDebugger), ctrl);
47
+ keyboard.registerKeyHandler(keyCodes.INSERT, onDown("insert", actions.toggleFast), ctrl);
44
48
  keyboard.registerKeyHandler(
45
- utils.keyCodes.END,
49
+ keyCodes.END,
46
50
  onDown("end", () => keyboard.pauseEmulation()),
47
51
  ctrl,
48
52
  );
49
- keyboard.registerKeyHandler(utils.keyCodes.PAGEDOWN, onDown("pagedown", actions.openRewind), alt);
50
- keyboard.registerKeyHandler(utils.keyCodes.B, onDown(null, actions.openPrinter), ctrl);
53
+ keyboard.registerKeyHandler(keyCodes.PAGEDOWN, onDown("pagedown", actions.openRewind), alt);
54
+ keyboard.registerKeyHandler(keyCodes.B, onDown(null, actions.openPrinter), ctrl);
51
55
 
52
56
  // Alt+1-8 and Alt+F1-F8 trigger the accessibility switches. Using Alt means
53
57
  // the underlying key is never forwarded to the BBC Micro (keyboard.js bails
@@ -55,8 +59,8 @@ export class KeyboardSetup {
55
59
  // works normally.
56
60
  const handleSwitch = (index) => (down) => accessibilitySwitches.setSwitch(index, down);
57
61
  for (let i = 0; i < 8; i++) {
58
- keyboard.registerKeyHandler(utils.keyCodes.K1 + i, handleSwitch(i), alt);
59
- keyboard.registerKeyHandler(utils.keyCodes.F1 + i, handleSwitch(i), alt);
62
+ keyboard.registerKeyHandler(keyCodes.K1 + i, handleSwitch(i), alt);
63
+ keyboard.registerKeyHandler(keyCodes.F1 + i, handleSwitch(i), alt);
60
64
  }
61
65
 
62
66
  document.addEventListener("keydown", (evt) => {
@@ -72,28 +76,4 @@ export class KeyboardSetup {
72
76
  if (text) actions.paste(text);
73
77
  });
74
78
  }
75
-
76
- sendRawKeyboard(keysToSend, checkCapsAndShiftLocks) {
77
- this.keyboard.sendRawKeyboard(keysToSend, checkCapsAndShiftLocks);
78
- }
79
-
80
- clearKeys() {
81
- this.keyboard.clearKeys();
82
- }
83
-
84
- setKeyLayout(keyLayout) {
85
- this.keyboard.setKeyLayout(keyLayout);
86
- }
87
-
88
- resumeEmulation() {
89
- this.keyboard.resumeEmulation();
90
- }
91
-
92
- setRunning(running) {
93
- this.keyboard.setRunning(running);
94
- }
95
-
96
- postFrameShouldPause() {
97
- return this.keyboard.postFrameShouldPause();
98
- }
99
79
  }
@@ -1,6 +1,5 @@
1
- "use strict";
2
- import * as utils from "./utils.js";
3
- import { ATOM } from "./utils_atom.js";
1
+ import { keyCodes } from "../keymap.js";
2
+ import { Typist } from "../typist.js";
4
3
 
5
4
  const isMac = typeof window !== "undefined" && /^Mac/i.test(window.navigator?.platform || "");
6
5
 
@@ -29,13 +28,8 @@ export class Keyboard extends EventTarget {
29
28
  this.inputEnabledFunction = inputEnabledFunction;
30
29
  this.dbgr = dbgr;
31
30
 
32
- // Key interface: routes key events to SysVia (BBC) or PPIA (Atom).
33
- // Both provide keyDown, keyUp, keyToggleRaw, setKeyLayout,
34
- // clearKeys, disableKeyboard, enableKeyboard.
35
- this.keyInterface = processor.model.isAtom ? processor.atomppia : processor.sysvia;
36
- // The SHIFT key constant used by stringToMachineKeys in the paste key array.
37
- // Compared by reference in _deliverPasteKey to avoid toggling shift off.
38
- this._shiftKey = processor.model.isAtom ? ATOM.SHIFT : utils.BBC.SHIFT;
31
+ this.keyInterface = processor.keyboardInterface;
32
+ this.typist = new Typist(processor);
39
33
 
40
34
  // State
41
35
  this.emuKeyHandlers = {};
@@ -49,13 +43,6 @@ export class Keyboard extends EventTarget {
49
43
  this.lastShiftLocation = 1;
50
44
  this.lastCtrlLocation = 1;
51
45
  this.lastAltLocation = 1;
52
-
53
- // Paste state — uses a scheduler task instead of a debugInstruction
54
- // hook so the CPU can remain on the fast execution path during paste.
55
- this._pasteKeys = [];
56
- this._pasteLastChar = undefined;
57
- this._pasteClocksPerMs = 0;
58
- this._pasteTask = this.processor.scheduler.newTask(() => this._deliverPasteKey());
59
46
  }
60
47
 
61
48
  /**
@@ -65,7 +52,6 @@ export class Keyboard extends EventTarget {
65
52
  */
66
53
  keyCode(evt) {
67
54
  const ret = evt.which || evt.charCode || evt.keyCode;
68
- const keyCodes = utils.keyCodes;
69
55
 
70
56
  switch (evt.location) {
71
57
  default:
@@ -108,9 +94,9 @@ export class Keyboard extends EventTarget {
108
94
  case 3: // numpad
109
95
  switch (ret) {
110
96
  case keyCodes.ENTER:
111
- return utils.keyCodes.NUMPADENTER;
97
+ return keyCodes.NUMPADENTER;
112
98
  case keyCodes.DELETE:
113
- return utils.keyCodes.NUMPAD_DECIMAL_POINT;
99
+ return keyCodes.NUMPAD_DECIMAL_POINT;
114
100
  }
115
101
  break;
116
102
  }
@@ -143,7 +129,7 @@ export class Keyboard extends EventTarget {
143
129
  */
144
130
  setKeyLayout(layout) {
145
131
  this.keyLayout = layout;
146
- this.keyInterface.setKeyLayout(layout);
132
+ this.processor.setKeyLayout(layout);
147
133
  }
148
134
 
149
135
  /**
@@ -226,7 +212,7 @@ export class Keyboard extends EventTarget {
226
212
  const code = this.keyCode(evt);
227
213
  evt.preventDefault();
228
214
 
229
- if (this.isPasting && code === utils.keyCodes.ESCAPE) {
215
+ if (this.isPasting && code === keyCodes.ESCAPE) {
230
216
  this.cancelPaste();
231
217
  return;
232
218
  }
@@ -255,11 +241,11 @@ export class Keyboard extends EventTarget {
255
241
  * @private
256
242
  */
257
243
  _handleSpecialKeys(code) {
258
- if (code === utils.keyCodes.F12 || code === utils.keyCodes.BREAK) {
244
+ if (code === keyCodes.F12 || code === keyCodes.BREAK) {
259
245
  this.dispatchEvent(new CustomEvent("break", { detail: true }));
260
246
  this.processor.setReset(true);
261
247
  return true;
262
- } else if (isMac && code === utils.keyCodes.CAPSLOCK) {
248
+ } else if (isMac && code === keyCodes.CAPSLOCK) {
263
249
  // Special CapsLock handling for Mac
264
250
  this.handleMacCapsLock();
265
251
  return true;
@@ -286,11 +272,11 @@ export class Keyboard extends EventTarget {
286
272
  evt.preventDefault();
287
273
 
288
274
  // Handle special key cases
289
- if (code === utils.keyCodes.F12 || code === utils.keyCodes.BREAK) {
275
+ if (code === keyCodes.F12 || code === keyCodes.BREAK) {
290
276
  this.dispatchEvent(new CustomEvent("break", { detail: false }));
291
277
  this.processor.setReset(false);
292
278
  return;
293
- } else if (isMac && code === utils.keyCodes.CAPSLOCK) {
279
+ } else if (isMac && code === keyCodes.CAPSLOCK) {
294
280
  // Special CapsLock handling for Mac
295
281
  this.handleMacCapsLock();
296
282
  return;
@@ -311,10 +297,10 @@ export class Keyboard extends EventTarget {
311
297
 
312
298
  // Mac browsers seem to model caps lock as a physical key that's down when capslock is on, and up when it's off.
313
299
  // No event is generated when it is physically released on the keyboard. So, we simulate a "tap" here.
314
- this.keyInterface.keyDown(utils.keyCodes.CAPSLOCK);
300
+ this.keyInterface.keyDown(keyCodes.CAPSLOCK);
315
301
 
316
302
  // Simulate a key release after a short delay
317
- setTimeout(() => this.keyInterface.keyUp(utils.keyCodes.CAPSLOCK), CAPS_LOCK_DELAY);
303
+ setTimeout(() => this.keyInterface.keyUp(keyCodes.CAPSLOCK), CAPS_LOCK_DELAY);
318
304
 
319
305
  if (this.saidCapsLockIsTapped) return;
320
306
  this.saidCapsLockIsTapped = true;
@@ -331,104 +317,17 @@ export class Keyboard extends EventTarget {
331
317
  );
332
318
  }
333
319
 
334
- /**
335
- * Send raw keyboard input to the emulated machine (for paste/autotype).
336
- * @param {Array} keysToSend - Array of machine-specific key codes to send
337
- * @param {boolean} checkCapsAndShiftLocks - Whether to check caps and shift locks
338
- */
320
+ /** Sends raw keys, and millisecond delays, to the machine: paste and autoboot come through here. */
339
321
  sendRawKeyboard(keysToSend, checkCapsAndShiftLocks) {
340
- if (this.isPasting) this.cancelPaste();
341
-
342
- this.keyInterface.disableKeyboard();
343
- // The paste task lives on the processor's scheduler, which is polled with peripheral
344
- // cycles, so paste delays stay in real time whatever the CPU multiplier is.
345
- this._pasteClocksPerMs = this.processor.peripheralCyclesPerSecond / 1000;
346
-
347
- if (checkCapsAndShiftLocks) {
348
- let toggleKey = null;
349
- if (!this.keyInterface.capsLockLight) toggleKey = utils.BBC.CAPSLOCK;
350
- else if (this.keyInterface.shiftLockLight) toggleKey = utils.BBC.SHIFTLOCK;
351
- if (toggleKey) {
352
- keysToSend.unshift(toggleKey);
353
- keysToSend.push(toggleKey);
354
- }
355
- }
356
-
357
- this._pasteKeys = keysToSend;
358
- this._pasteLastChar = undefined;
359
- this._pasteTask.schedule(0);
322
+ this.typist.type(keysToSend, checkCapsAndShiftLocks);
360
323
  }
361
324
 
362
- /**
363
- * Scheduler callback that delivers one key per invocation, rescheduling
364
- * itself for the next key. Replaces the old debugInstruction hook so the
365
- * CPU stays on the fast execution path during paste.
366
- * @private
367
- */
368
- _deliverPasteKey() {
369
- if (this._pasteLastChar && this._pasteLastChar !== this._shiftKey) {
370
- this.keyInterface.keyToggleRaw(this._pasteLastChar);
371
- }
372
-
373
- if (this._pasteKeys.length === 0) {
374
- this._pasteLastChar = undefined;
375
- this.keyInterface.enableKeyboard();
376
- return;
377
- }
378
-
379
- // Atom's PPIA keyboard is polled, not interrupt-driven like the BBC's
380
- // SysVIA. Insert a debounce gap after every key release so the ROM
381
- // sees the key-up before the next key-down arrives.
382
- if (this._pasteLastChar && this._pasteLastChar !== this._shiftKey && this.processor.model.isAtom) {
383
- this._pasteLastChar = undefined;
384
- this._pasteTask.schedule(30 * this._pasteClocksPerMs);
385
- return;
386
- }
387
-
388
- const ch = this._pasteKeys[0];
389
- const debounce = this._pasteLastChar === ch;
390
- this._pasteLastChar = ch;
391
- if (debounce) {
392
- this._pasteLastChar = undefined;
393
- this._pasteTask.schedule(30 * this._pasteClocksPerMs);
394
- return;
395
- }
396
-
397
- // Atom ROM polls the keyboard once per VSync (~16ms at 60 Hz).
398
- // 80ms gives the ROM ~5 scan cycles to detect, debounce, and
399
- // process each keypress.
400
- let delayMs = this.processor.model.isAtom ? 80 : 50;
401
- if (typeof this._pasteLastChar === "number") {
402
- delayMs = this._pasteLastChar;
403
- this._pasteLastChar = undefined;
404
- } else {
405
- this.keyInterface.keyToggleRaw(this._pasteLastChar);
406
- }
407
-
408
- this._pasteKeys.shift();
409
- this._pasteTask.schedule(delayMs * this._pasteClocksPerMs);
410
- }
411
-
412
- /**
413
- * Cancel any in-progress paste operation.
414
- */
415
325
  cancelPaste() {
416
- if (!this.isPasting) return;
417
- this._pasteTask.cancel();
418
- if (this._pasteLastChar && this._pasteLastChar !== this._shiftKey) {
419
- this.keyInterface.keyToggleRaw(this._pasteLastChar);
420
- }
421
- this._pasteLastChar = undefined;
422
- this._pasteKeys = [];
423
- this.keyInterface.enableKeyboard();
326
+ this.typist.cancel();
424
327
  }
425
328
 
426
- /**
427
- * Whether a paste operation is currently in progress.
428
- * @returns {boolean}
429
- */
430
329
  get isPasting() {
431
- return this._pasteKeys.length > 0 || this._pasteTask.scheduled();
330
+ return this.typist.isTyping;
432
331
  }
433
332
 
434
333
  /**
package/src/web/layout.js CHANGED
@@ -78,6 +78,10 @@ export class Layout {
78
78
  this.cubMonitorPic = document.getElementById("cub-monitor-pic");
79
79
  this.borderReservedSize = embed ? 0 : 100;
80
80
  this.bottomReservedSize = embed ? 0 : 68;
81
+ if (embed) {
82
+ for (const el of document.querySelectorAll(".embed-hide")) el.style.display = "none";
83
+ document.body.style.backgroundColor = "transparent";
84
+ }
81
85
 
82
86
  window.addEventListener("resize", () => this.resize());
83
87
  window.setTimeout(() => this.resize(), 1);
@@ -0,0 +1,44 @@
1
+ import { DiscLayout } from "../disc.js";
2
+ import { discFor, guessDiscTypeFromName } from "../fdc.js";
3
+ import { stringToUint8Array, uint8ArrayToString } from "../binary.js";
4
+
5
+ /**
6
+ * Create or open a disc held in the browser's local storage.
7
+ * @param {string} name - The file name with extension
8
+ * @param {string} [layout] - one of DiscLayout; by default the image is asked what it is
9
+ * @param {function(*): void} [onSaveError] - called with whatever was thrown, the first time a write
10
+ * cannot be stored
11
+ * @returns {Disc} The loaded disc object
12
+ */
13
+ export function localDisc(name, layout = DiscLayout.auto, onSaveError = () => {}) {
14
+ const discName = "disc_" + name;
15
+ let data;
16
+ const dataString = window.localStorage[discName];
17
+ if (!dataString) {
18
+ console.log("Creating browser-local disc " + name);
19
+ const discType = guessDiscTypeFromName(name);
20
+ if (!discType.byteSize) {
21
+ throw new Error(`Cannot create blank disc of type ${discType.extension} - unknown size`);
22
+ }
23
+ data = new Uint8Array(discType.byteSize);
24
+ if (discType.supportsCatalogue) {
25
+ discType.setDiscName(data, name);
26
+ }
27
+ } else {
28
+ console.log("Loading browser-local disc " + name);
29
+ data = stringToUint8Array(dataString);
30
+ }
31
+ let reportedSaveError = false;
32
+ const onChange = (data) => {
33
+ try {
34
+ const str = uint8ArrayToString(data);
35
+ window.localStorage.setItem(discName, str);
36
+ } catch (e) {
37
+ console.log(`Unable to save browser-local disc ${name}: ${e}`);
38
+ if (reportedSaveError) return;
39
+ reportedSaveError = true;
40
+ onSaveError(e);
41
+ }
42
+ };
43
+ return discFor(name, data, onChange, layout);
44
+ }
@@ -1,28 +1,36 @@
1
- import { AtomCpu6502, Cpu6502 } from "../6502.js";
1
+ import { machineSpec } from "../machine-spec.js";
2
2
  import { Cmos, localStoragePersistence } from "../cmos.js";
3
3
  import { Econet } from "../econet.js";
4
4
  import { LoadSD } from "../mmc.js";
5
5
  import { tubeModelFor } from "../models.js";
6
- import * as utils from "../utils.js";
7
6
  import { toast } from "./toast.js";
8
7
  import { errorText, reportLoadFailure, showNotice } from "./reporting.js";
8
+ import { loadData } from "../loader.js";
9
9
 
10
10
  /**
11
11
  * The machine's fittings as the CPU wants them handed over. Pure, so the bank
12
12
  * and flag decisions are testable on their own.
13
13
  */
14
- export function buildEmulationConfig({ config, parsedQuery, keyLayout, cpuMultiplier, extraRoms, userPort, printer }) {
15
- return {
14
+ export function buildEmulationConfig({
15
+ settings,
16
+ parsedQuery,
17
+ keyLayout,
18
+ cpuMultiplier,
19
+ extraRoms,
20
+ userPort,
21
+ printer,
22
+ }) {
23
+ return machineSpec({
16
24
  keyLayout,
17
25
  cpuMultiplier,
18
- tubeCpuMultiplier: config.tubeCpuMultiplier,
26
+ tubeCpuMultiplier: settings.tubeCpuMultiplier,
19
27
  videoCyclesBatch: parsedQuery.videoCyclesBatch,
20
- tube: config.coProcessor ? tubeModelFor(config.model) : null,
21
- hasMusic5000: config.hasMusic5000,
22
- hasTeletextAdaptor: config.hasTeletextAdaptor,
28
+ tube: settings.coProcessor ? tubeModelFor(settings.model) : null,
29
+ hasMusic5000: settings.hasMusic5000,
30
+ hasTeletextAdaptor: settings.hasTeletextAdaptor,
23
31
  // ROM order determines sideways bank allocation, and the fittings' ROMs claim banks
24
32
  // before any the user asked for with ?rom=.
25
- extraRoms: [...config.extraRoms, ...extraRoms],
33
+ extraRoms: [...settings.extraRoms, ...extraRoms],
26
34
  userPort,
27
35
  printerPort: printer,
28
36
  getGamepads: function () {
@@ -33,14 +41,14 @@ export function buildEmulationConfig({ config, parsedQuery, keyLayout, cpuMultip
33
41
  logFdcCommands: parsedQuery.logFdcCommands !== undefined,
34
42
  logFdcStateChanges: parsedQuery.logFdcStateChanges !== undefined,
35
43
  },
36
- };
44
+ });
37
45
  }
38
46
 
39
47
  /** The emulated machine itself: the processor with everything bolted to it, and its start-up. */
40
48
  export class Machine {
41
49
  constructor({
42
50
  model,
43
- config,
51
+ settings,
44
52
  parsedQuery,
45
53
  keyLayout,
46
54
  cpuMultiplier,
@@ -52,14 +60,14 @@ export class Machine {
52
60
  video,
53
61
  audioHandler,
54
62
  dbgr,
55
- makeCpu = (CpuClass, ...args) => new CpuClass(...args),
63
+ build = (cpuModel, options) => new cpuModel.Cpu(cpuModel, options),
56
64
  }) {
57
65
  this.model = model;
58
66
  this.audioHandler = audioHandler;
59
67
  this.speechOutput = speechOutput;
60
68
 
61
69
  this.econet = null;
62
- if (config.hasEconet) {
70
+ if (settings.hasEconet) {
63
71
  this.econet = new Econet(stationId, model.cyclesPerSecond);
64
72
  } else {
65
73
  document.getElementById("fsmenuitem").style.display = "none";
@@ -78,8 +86,8 @@ export class Machine {
78
86
  this.econet,
79
87
  );
80
88
 
81
- this.emulationConfig = buildEmulationConfig({
82
- config,
89
+ this.spec = buildEmulationConfig({
90
+ settings,
83
91
  parsedQuery,
84
92
  keyLayout,
85
93
  cpuMultiplier,
@@ -88,17 +96,16 @@ export class Machine {
88
96
  printer,
89
97
  });
90
98
 
91
- const CpuClass = model.isAtom ? AtomCpu6502 : Cpu6502;
92
- this.processor = makeCpu(CpuClass, model, {
99
+ this.processor = build(model, {
93
100
  dbgr,
94
101
  video,
95
102
  soundChip: audioHandler.soundChip,
96
103
  ddNoise: audioHandler.ddNoise,
97
104
  relayNoise: audioHandler.relayNoise,
98
- music5000: config.hasMusic5000 ? audioHandler.music5000 : null,
105
+ music5000: settings.hasMusic5000 ? audioHandler.music5000 : null,
99
106
  cmos: this.cmos,
100
- config: this.emulationConfig,
101
107
  econet: this.econet,
108
+ config: this.spec,
102
109
  });
103
110
 
104
111
  printer.attach(this.processor.uservia);
@@ -187,7 +194,7 @@ export class Machine {
187
194
  await startImageLoad(`BASIC program ${loadBasic}`, () =>
188
195
  autoBoot.insertBasic(
189
196
  (async () => {
190
- const data = await utils.loadData(loadBasic);
197
+ const data = await loadData(loadBasic);
191
198
  return String.fromCharCode.apply(null, data);
192
199
  })(),
193
200
  basicNeedsRun,