jsbeeb 1.22.4 → 1.24.0

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 (112) hide show
  1. package/README.md +2 -2
  2. package/package.json +16 -11
  3. package/src/6502.js +39 -42
  4. package/src/6502.opcodes.js +198 -123
  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 +7 -23
  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/jsbeeb.css +33 -0
  31. package/src/{utils_atom.js → keymap-atom.js} +2 -18
  32. package/src/{utils.js → keymap.js} +47 -610
  33. package/src/loader.js +63 -0
  34. package/src/machine-session.js +37 -177
  35. package/src/machine-spec.js +59 -0
  36. package/src/main.js +69 -184
  37. package/src/media-resolver.js +68 -0
  38. package/src/mmc.js +3 -2
  39. package/src/models.js +40 -4
  40. package/src/music5000.js +0 -2
  41. package/src/ppia.js +3 -4
  42. package/src/printer.js +7 -14
  43. package/src/relaynoise.js +0 -1
  44. package/src/rewind.js +0 -2
  45. package/src/sample-player.js +2 -3
  46. package/src/serial.js +0 -2
  47. package/src/snapshot-helpers.js +0 -2
  48. package/src/snapshot.js +0 -2
  49. package/src/soundchip.js +1 -1
  50. package/src/state-utils.js +0 -2
  51. package/src/sth.js +2 -4
  52. package/src/tapes.js +10 -4
  53. package/src/teletext.js +2 -3
  54. package/src/teletext_adaptor.js +2 -4
  55. package/src/teletext_data.js +0 -2
  56. package/src/test-machine.js +307 -0
  57. package/src/touchscreen.js +2 -4
  58. package/src/tube.js +5 -6
  59. package/src/typist.js +103 -0
  60. package/src/uef-snapshot.js +0 -2
  61. package/src/url-params.js +4 -5
  62. package/src/vdu-capture.js +113 -0
  63. package/src/via.js +3 -4
  64. package/src/video-filters/pal-composite.js +0 -2
  65. package/src/video-filters/passthrough-filter.js +0 -2
  66. package/src/video-filters/pixel-grid.js +0 -2
  67. package/src/video-filters/shader-program.js +0 -2
  68. package/src/video-filters/xbr-filter.js +0 -2
  69. package/src/video.js +12 -11
  70. package/src/wd-fdc.js +20 -20
  71. package/src/web/analogue-inputs.js +6 -8
  72. package/src/web/analytics.js +13 -0
  73. package/src/web/audio-handler.js +2 -2
  74. package/src/{audio-utils.js → web/audio-utils.js} +0 -2
  75. package/src/web/autoboot.js +10 -13
  76. package/src/{canvas.js → web/canvas.js} +4 -5
  77. package/src/web/config.js +42 -72
  78. package/src/web/console-surface.js +25 -0
  79. package/src/web/debug.js +8 -49
  80. package/src/web/disc-visualiser.js +0 -2
  81. package/src/web/display.js +21 -4
  82. package/src/{dom-utils.js → web/dom-utils.js} +1 -11
  83. package/src/web/drives.js +1 -1
  84. package/src/web/emulation-loop.js +54 -33
  85. package/src/web/front-panel.js +13 -12
  86. package/src/{gamepads.js → web/gamepads.js} +2 -6
  87. package/src/web/google-drive-picker.js +14 -9
  88. package/src/{google-drive.js → web/google-drive.js} +9 -10
  89. package/src/web/hfe-picker.js +5 -4
  90. package/src/web/icons.js +42 -0
  91. package/src/web/keyboard-setup.js +29 -40
  92. package/src/{keyboard.js → web/keyboard.js} +18 -119
  93. package/src/web/layout.js +4 -0
  94. package/src/web/local-disc.js +44 -0
  95. package/src/web/machine.js +27 -20
  96. package/src/web/media-loader.js +38 -147
  97. package/src/{microphone-input.js → web/microphone-input.js} +1 -1
  98. package/src/web/modals.js +6 -9
  99. package/src/web/page-actions.js +88 -0
  100. package/src/web/quick-settings.js +16 -15
  101. package/src/web/reporting.js +0 -7
  102. package/src/{rewind-thumbnail.js → web/rewind-thumbnail.js} +0 -2
  103. package/src/web/rewind-ui.js +29 -29
  104. package/src/web/run-controls.js +39 -0
  105. package/src/web/settings.js +64 -135
  106. package/src/web/snapshot-ui.js +10 -16
  107. package/src/{speech-output.js → web/speech-output.js} +0 -2
  108. package/src/web/sth-picker.js +9 -7
  109. package/src/web/url-state.js +20 -6
  110. package/src/econet/L3FS.dat +0 -0
  111. package/src/econet/scsi.dat +0 -0
  112. package/tests/test-machine.js +0 -609
@@ -1,9 +1,10 @@
1
- import * as utils from "../utils.js";
2
1
  import * as bootstrap from "bootstrap";
3
2
  import * as disc from "../fdc.js";
4
- import { GoogleDriveLoader } from "../google-drive.js";
3
+ import { GoogleDriveLoader } from "./google-drive.js";
5
4
  import { toast } from "./toast.js";
6
5
  import { errorText } from "./reporting.js";
6
+ import { replaceOrAddExtension } from "../archive.js";
7
+ import { noteEvent } from "./analytics.js";
7
8
 
8
9
  /**
9
10
  * The Google Drive picker: signing in, listing the user's discs, loading one
@@ -32,8 +33,12 @@ export class GoogleDrivePicker {
32
33
  else this.authReject?.(new Error("Unable to authorize Google Drive"));
33
34
  });
34
35
 
35
- // Closing the loading dialog while the auth panel waits is a cancellation, not an error.
36
- document.getElementById("loading-dialog").addEventListener("hidden.bs.modal", () => this.authResolve?.(false));
36
+ // Closing the loading dialog puts the auth panel away; while the panel
37
+ // waits, that close is a cancellation, not an error.
38
+ document.getElementById("loading-dialog").addEventListener("hidden.bs.modal", () => {
39
+ this.authEl.style.display = "none";
40
+ this.authResolve?.(false);
41
+ });
37
42
 
38
43
  // Loading the Google client holds the main thread for ~100ms, so it waits for
39
44
  // someone to ask for Drive.
@@ -94,7 +99,7 @@ export class GoogleDrivePicker {
94
99
  }
95
100
  }
96
101
 
97
- const ssd = await this.googleDrive.load(this.processor.fdc, cat.id, layout);
102
+ const ssd = await this.googleDrive.load(cat.id, layout);
98
103
  console.log("Google Drive loading finished");
99
104
  this.modals.loadingFinished();
100
105
  if (!ssd.savesChanges) {
@@ -133,7 +138,7 @@ export class GoogleDrivePicker {
133
138
  dbList.appendChild(row);
134
139
  row.querySelector(".name").textContent = item.name;
135
140
  row.addEventListener("click", async () => {
136
- utils.noteEvent("google-drive", "click", item.name);
141
+ noteEvent("google-drive", "click", item.name);
137
142
  this.media.setDisc1Image(`gd:${item.id}/${item.name}`);
138
143
  this.modal.hide();
139
144
  try {
@@ -165,10 +170,10 @@ export class GoogleDrivePicker {
165
170
  this.modals.loadingFinished(`Unable to create ${name} on Google Drive: ${errorText(e)}`);
166
171
  return;
167
172
  }
168
- name = utils.replaceOrAddExtension(name, discType.extension);
173
+ name = replaceOrAddExtension(name, discType.extension);
169
174
  console.log(`Saving existing disc: ${name}`);
170
175
  } else {
171
- // TODO support HFE, I guess?
176
+ // TODO(#1070) blank HFE images have no fixed byteSize, so only SSD and DSD blanks can be made here.
172
177
  const discType = disc.guessDiscTypeFromName(name);
173
178
  if (!discType.byteSize) {
174
179
  this.modals.loadingFinished(
@@ -184,7 +189,7 @@ export class GoogleDrivePicker {
184
189
  }
185
190
 
186
191
  try {
187
- const result = await this.googleDrive.create(this.processor.fdc, name, data);
192
+ const result = await this.googleDrive.create(name, data);
188
193
  this.media.setDisc1Image("gd:" + result.fileId + "/" + name);
189
194
  this.drives.putDiscIn(0, result.disc);
190
195
  this.modals.loadingFinished();
@@ -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);
@@ -0,0 +1,42 @@
1
+ import arrowCounterclockwise from "bootstrap-icons/icons/arrow-counterclockwise.svg?raw";
2
+ import cassette from "bootstrap-icons/icons/cassette.svg?raw";
3
+ import clockHistory from "bootstrap-icons/icons/clock-history.svg?raw";
4
+ import display from "bootstrap-icons/icons/display.svg?raw";
5
+ import floppy from "bootstrap-icons/icons/floppy.svg?raw";
6
+ import gear from "bootstrap-icons/icons/gear.svg?raw";
7
+ import headphones from "bootstrap-icons/icons/headphones.svg?raw";
8
+ import pauseFill from "bootstrap-icons/icons/pause-fill.svg?raw";
9
+ import playFill from "bootstrap-icons/icons/play-fill.svg?raw";
10
+ import soundwave from "bootstrap-icons/icons/soundwave.svg?raw";
11
+ import stars from "bootstrap-icons/icons/stars.svg?raw";
12
+ import threeDots from "bootstrap-icons/icons/three-dots.svg?raw";
13
+ import tv from "bootstrap-icons/icons/tv.svg?raw";
14
+ import volumeUp from "bootstrap-icons/icons/volume-up.svg?raw";
15
+
16
+ /** The Bootstrap Icons the page uses, by their upstream names; nothing else of the set is bundled. */
17
+ const Icons = {
18
+ "arrow-counterclockwise": arrowCounterclockwise,
19
+ cassette,
20
+ "clock-history": clockHistory,
21
+ display,
22
+ floppy,
23
+ gear,
24
+ headphones,
25
+ "pause-fill": pauseFill,
26
+ "play-fill": playFill,
27
+ soundwave,
28
+ stars,
29
+ "three-dots": threeDots,
30
+ tv,
31
+ "volume-up": volumeUp,
32
+ };
33
+
34
+ export const IconNames = Object.freeze(Object.keys(Icons));
35
+
36
+ /** Puts the named SVG inside every `[data-icon]` element under `root`; an unknown name leaves them all untouched. */
37
+ export function installIcons(root = document) {
38
+ const placeholders = [...root.querySelectorAll("[data-icon]")];
39
+ const unknown = placeholders.find((el) => !(el.dataset.icon in Icons));
40
+ if (unknown) throw new Error(`No icon named ${unknown.dataset.icon}`);
41
+ for (const el of placeholders) el.innerHTML = Icons[el.dataset.icon];
42
+ }
@@ -1,20 +1,27 @@
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
- /** The page's keyboard: the emulated one and the browser shortcuts around it. */
6
+ const PasteBoxId = "paste-text";
7
+ const TypingTargets = 'input, textarea, select, [contenteditable]:not([contenteditable="false"])';
8
+
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
+ */
6
13
  export class KeyboardSetup {
7
14
  /**
8
15
  * @param {object} opts
9
16
  * @param {object} opts.actions what each shortcut does, supplied late-bound:
10
17
  * enterDebugger, reload, toggleFast, openRewind, openPrinter,
11
- * pause, resume, onAnyKeyDown
18
+ * pause, resume, paste, onAnyKeyDown
12
19
  * @param {import("./accessibility-switches.js").AccessibilitySwitches} opts.accessibilitySwitches
13
20
  */
14
21
  constructor({ actions, accessibilitySwitches, processor, dbgr, keyLayout }) {
15
22
  const keyboard = (this.keyboard = new Keyboard({
16
23
  processor,
17
- inputEnabledFunction: () => document.activeElement && document.activeElement.id === "paste-text",
24
+ inputEnabledFunction: () => document.activeElement && document.activeElement.id === PasteBoxId,
18
25
  keyLayout,
19
26
  dbgr,
20
27
  }));
@@ -23,28 +30,28 @@ export class KeyboardSetup {
23
30
  keyboard.addEventListener("resume", () => actions.resume());
24
31
  keyboard.addEventListener("break", (e) => {
25
32
  // F12/Break: Reset processor
26
- if (e.detail) utils.noteEvent("keyboard", "press", "break");
33
+ if (e.detail) noteEvent("keyboard", "press", "break");
27
34
  });
28
35
 
29
36
  const onDown = (note, action) => (down) => {
30
37
  if (down) {
31
- if (note) utils.noteEvent("keyboard", "press", note);
38
+ if (note) noteEvent("keyboard", "press", note);
32
39
  action();
33
40
  }
34
41
  };
35
42
  const alt = { alt: true, ctrl: false };
36
43
  const ctrl = { alt: false, ctrl: true };
37
- keyboard.registerKeyHandler(utils.keyCodes.S, onDown("S", actions.enterDebugger), alt);
38
- keyboard.registerKeyHandler(utils.keyCodes.R, onDown(null, actions.reload), alt);
39
- keyboard.registerKeyHandler(utils.keyCodes.HOME, onDown("home", actions.enterDebugger), ctrl);
40
- 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);
41
48
  keyboard.registerKeyHandler(
42
- utils.keyCodes.END,
49
+ keyCodes.END,
43
50
  onDown("end", () => keyboard.pauseEmulation()),
44
51
  ctrl,
45
52
  );
46
- keyboard.registerKeyHandler(utils.keyCodes.PAGEDOWN, onDown("pagedown", actions.openRewind), alt);
47
- 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);
48
55
 
49
56
  // Alt+1-8 and Alt+F1-F8 trigger the accessibility switches. Using Alt means
50
57
  // the underlying key is never forwarded to the BBC Micro (keyboard.js bails
@@ -52,8 +59,8 @@ export class KeyboardSetup {
52
59
  // works normally.
53
60
  const handleSwitch = (index) => (down) => accessibilitySwitches.setSwitch(index, down);
54
61
  for (let i = 0; i < 8; i++) {
55
- keyboard.registerKeyHandler(utils.keyCodes.K1 + i, handleSwitch(i), alt);
56
- 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);
57
64
  }
58
65
 
59
66
  document.addEventListener("keydown", (evt) => {
@@ -62,29 +69,11 @@ export class KeyboardSetup {
62
69
  });
63
70
  document.addEventListener("keypress", (evt) => keyboard.keyPress(evt));
64
71
  document.addEventListener("keyup", (evt) => keyboard.keyUp(evt));
65
- }
66
-
67
- sendRawKeyboard(keysToSend, checkCapsAndShiftLocks) {
68
- this.keyboard.sendRawKeyboard(keysToSend, checkCapsAndShiftLocks);
69
- }
70
-
71
- clearKeys() {
72
- this.keyboard.clearKeys();
73
- }
74
-
75
- setKeyLayout(keyLayout) {
76
- this.keyboard.setKeyLayout(keyLayout);
77
- }
78
-
79
- resumeEmulation() {
80
- this.keyboard.resumeEmulation();
81
- }
82
-
83
- setRunning(running) {
84
- this.keyboard.setRunning(running);
85
- }
86
-
87
- postFrameShouldPause() {
88
- return this.keyboard.postFrameShouldPause();
72
+ document.addEventListener("paste", (evt) => {
73
+ const target = document.activeElement;
74
+ if (target && target.id !== PasteBoxId && target.matches(TypingTargets)) return;
75
+ const text = evt.clipboardData?.getData("text/plain");
76
+ if (text) actions.paste(text);
77
+ });
89
78
  }
90
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
+ }