jsbeeb 1.10.0 → 1.11.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.
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "name": "jsbeeb",
8
8
  "description": "Emulate a BBC Micro",
9
9
  "repository": "git@github.com:mattgodbolt/jsbeeb.git",
10
- "version": "1.10.0",
10
+ "version": "1.11.0",
11
11
  "//": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
12
12
  "engines": {
13
13
  "node": ">=22.12.0"
@@ -31,12 +31,8 @@
31
31
  "bootstrap": "^5.3.8",
32
32
  "bootswatch": "^5.3.8",
33
33
  "event-emitter-es6": "^1.1.5",
34
- "fflate": "^0.8.2",
35
- "jquery": "^4.0.0",
36
- "pako": "^2.1.0",
37
34
  "sharp": "^0.34.5",
38
- "smoothie": "^1.36.1",
39
- "underscore": "^1.13.7"
35
+ "smoothie": "^1.36.1"
40
36
  },
41
37
  "devDependencies": {
42
38
  "@eslint/js": "^10.0.1",
package/src/6502.js CHANGED
@@ -1106,7 +1106,7 @@ export class Cpu6502 extends Base6502 {
1106
1106
  const ramRomOs = this.ramRomOs;
1107
1107
  let data = await utils.loadData(name);
1108
1108
  if (/\.zip/i.test(name)) {
1109
- data = utils.unzipRomImage(data).data;
1109
+ data = (await utils.unzipRomImage(data)).data;
1110
1110
  }
1111
1111
  ramRomOs.set(data, offset);
1112
1112
  }
@@ -3,8 +3,6 @@
3
3
  // Electron integration for jsbeeb desktop application.
4
4
  // Handles IPC communication for loading disc/tape images and showing modals from Electron's main process.
5
5
 
6
- export let initialise = function () {};
7
-
8
6
  function init(args) {
9
7
  const { loadDiscImage, loadTapeImage, loadStateFile, processor, modals, actions, config } = args;
10
8
  const api = window.electronAPI;
@@ -66,6 +64,8 @@ function init(args) {
66
64
  }
67
65
  }
68
66
 
69
- if (typeof window.electronAPI !== "undefined") {
70
- initialise = init;
67
+ export function initialise(args) {
68
+ if (typeof window.electronAPI !== "undefined") {
69
+ init(args);
70
+ }
71
71
  }
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ import { decompress } from "./utils.js";
2
3
 
3
4
  // B-em snapshot format parser (versions 1 and 3).
4
5
  // v1 (BEMSNAP1): Fixed-size 327,885 byte packed struct. Reference: beebjit state.c
@@ -434,39 +435,6 @@ function readString(data, pos) {
434
435
  return str;
435
436
  }
436
437
 
437
- async function inflateRaw(compressedData) {
438
- // Use DecompressionStream (available in modern browsers and Node 18+)
439
- if (typeof DecompressionStream !== "undefined") {
440
- const ds = new DecompressionStream("deflate");
441
- const writer = ds.writable.getWriter();
442
- const reader = ds.readable.getReader();
443
- const chunks = [];
444
-
445
- // Start reading before writing to avoid backpressure deadlock
446
- const readPromise = (async () => {
447
- for (;;) {
448
- const { done, value } = await reader.read();
449
- if (done) break;
450
- chunks.push(value);
451
- }
452
- })();
453
-
454
- await writer.write(compressedData);
455
- await writer.close();
456
- await readPromise;
457
-
458
- const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
459
- const result = new Uint8Array(totalLength);
460
- let offset = 0;
461
- for (const chunk of chunks) {
462
- result.set(chunk, offset);
463
- offset += chunk.length;
464
- }
465
- return result;
466
- }
467
- throw new Error("Zlib decompression not available (need DecompressionStream API)");
468
- }
469
-
470
438
  function parseBemV3(buffer) {
471
439
  const bytes = new Uint8Array(buffer);
472
440
  let offset = 8; // Skip "BEMSNAP3" signature
@@ -523,7 +491,7 @@ function parseBemV3(buffer) {
523
491
  if (memSection) {
524
492
  // Memory is zlib-compressed; decompression is async.
525
493
  // Decompressed layout: 2 bytes (fe30, fe34) + 64KB RAM + 256KB ROM
526
- return inflateRaw(memSection.data).then((memData) => {
494
+ return decompress(memSection.data, "deflate").then((memData) => {
527
495
  cpuState.fe30 = memData[0];
528
496
  cpuState.fe34 = memData[1];
529
497
  const ramStart = 2;
package/src/config.js CHANGED
@@ -1,5 +1,4 @@
1
1
  "use strict";
2
- import $ from "jquery";
3
2
  import EventEmitter from "event-emitter-es6";
4
3
  import { findModel } from "./models.js";
5
4
  import { getFilterForMode } from "./canvas.js";
@@ -12,8 +11,8 @@ export class Config extends EventEmitter {
12
11
  this.changed = {};
13
12
  this.model = null;
14
13
  this.coProcessor = null;
15
- const $configuration = document.getElementById("configuration");
16
- $configuration.addEventListener("show.bs.modal", () => {
14
+ const configuration = document.getElementById("configuration");
15
+ configuration.addEventListener("show.bs.modal", () => {
17
16
  this.changed = {};
18
17
  this.setDropdownText(this.model.name);
19
18
  this.set65c02(this.model.tube);
@@ -23,116 +22,122 @@ export class Config extends EventEmitter {
23
22
  this.setEconet(this.model.hasEconet);
24
23
  });
25
24
 
26
- $configuration.addEventListener("hide.bs.modal", () => {
25
+ configuration.addEventListener("hide.bs.modal", () => {
27
26
  this.onClose(this.changed);
28
27
  if (Object.keys(this.changed).length > 0) {
29
28
  this.emit("settings-changed", this.changed);
30
29
  }
31
30
  });
32
31
 
33
- $(".model-menu a").on("click", (e) => {
34
- this.changed.model = $(e.target).attr("data-target");
35
- this.setDropdownText($(e.target).text());
36
- });
32
+ for (const link of document.querySelectorAll(".model-menu a")) {
33
+ link.addEventListener("click", (e) => {
34
+ this.changed.model = e.target.dataset.target;
35
+ this.setDropdownText(e.target.textContent);
36
+ });
37
+ }
37
38
 
38
- $("#65c02").on("click", () => {
39
- this.changed.coProcessor = $("#65c02").prop("checked");
40
- $("#tubeCpuMultiplier").prop("disabled", !$("#65c02").prop("checked"));
39
+ document.getElementById("65c02").addEventListener("click", () => {
40
+ this.changed.coProcessor = document.getElementById("65c02").checked;
41
+ document.getElementById("tubeCpuMultiplier").disabled = !document.getElementById("65c02").checked;
41
42
  });
42
43
 
43
- $("#tubeCpuMultiplier").on("input", () => {
44
- const val = parseInt($("#tubeCpuMultiplier").val(), 10);
45
- $("#tubeCpuMultiplierValue").text(val);
44
+ document.getElementById("tubeCpuMultiplier").addEventListener("input", () => {
45
+ const val = parseInt(document.getElementById("tubeCpuMultiplier").value, 10);
46
+ document.getElementById("tubeCpuMultiplierValue").textContent = val;
46
47
  this.changed.tubeCpuMultiplier = val;
47
48
  });
48
49
 
49
- $("#hasTeletextAdaptor").on("click", () => {
50
- this.changed.hasTeletextAdaptor = $("#hasTeletextAdaptor").prop("checked");
50
+ document.getElementById("hasTeletextAdaptor").addEventListener("click", () => {
51
+ this.changed.hasTeletextAdaptor = document.getElementById("hasTeletextAdaptor").checked;
51
52
  });
52
53
 
53
- $("#hasEconet").on("click", () => {
54
- this.changed.hasEconet = $("#hasEconet").prop("checked");
54
+ document.getElementById("hasEconet").addEventListener("click", () => {
55
+ this.changed.hasEconet = document.getElementById("hasEconet").checked;
55
56
  });
56
57
 
57
- $("#hasMusic5000").on("click", () => {
58
- this.changed.hasMusic5000 = $("#hasMusic5000").prop("checked");
58
+ document.getElementById("hasMusic5000").addEventListener("click", () => {
59
+ this.changed.hasMusic5000 = document.getElementById("hasMusic5000").checked;
59
60
  });
60
61
 
61
- $(".keyboard-menu a").on("click", (e) => {
62
- const keyLayout = $(e.target).attr("data-target");
63
- this.changed.keyLayout = keyLayout;
64
- this.setKeyLayout(keyLayout);
65
- });
62
+ for (const link of document.querySelectorAll(".keyboard-menu a")) {
63
+ link.addEventListener("click", (e) => {
64
+ const keyLayout = e.target.dataset.target;
65
+ this.changed.keyLayout = keyLayout;
66
+ this.setKeyLayout(keyLayout);
67
+ });
68
+ }
66
69
 
67
- $(".mic-channel-option").on("click", (e) => {
68
- const channelString = $(e.target).data("channel");
69
- const channel = channelString === "" ? undefined : parseInt($(e.target).data("channel"), 10);
70
- this.changed.microphoneChannel = channel;
71
- this.setMicrophoneChannel(channel);
72
- });
70
+ for (const option of document.querySelectorAll(".mic-channel-option")) {
71
+ option.addEventListener("click", (e) => {
72
+ const channelString = e.target.dataset.channel;
73
+ const channel = channelString === "" ? undefined : parseInt(channelString, 10);
74
+ this.changed.microphoneChannel = channel;
75
+ this.setMicrophoneChannel(channel);
76
+ });
77
+ }
73
78
 
74
- $("#mouseJoystickEnabled").on("click", () => {
75
- this.changed.mouseJoystickEnabled = $("#mouseJoystickEnabled").prop("checked");
79
+ document.getElementById("mouseJoystickEnabled").addEventListener("click", () => {
80
+ this.changed.mouseJoystickEnabled = document.getElementById("mouseJoystickEnabled").checked;
76
81
  });
77
82
 
78
- $("#speechOutput").on("click", () => {
79
- this.changed.speechOutput = $("#speechOutput").prop("checked");
83
+ document.getElementById("speechOutput").addEventListener("click", () => {
84
+ this.changed.speechOutput = document.getElementById("speechOutput").checked;
80
85
  });
81
86
 
82
- $(".display-mode-option").on("click", (e) => {
83
- const mode = $(e.target).data("mode");
84
- this.changed.displayMode = mode;
85
- this.setDisplayMode(mode);
86
- this.onChange({ displayMode: mode });
87
- });
87
+ for (const option of document.querySelectorAll(".display-mode-option")) {
88
+ option.addEventListener("click", (e) => {
89
+ const mode = e.target.dataset.mode;
90
+ this.changed.displayMode = mode;
91
+ this.setDisplayMode(mode);
92
+ this.onChange({ displayMode: mode });
93
+ });
94
+ }
88
95
  }
89
96
 
90
97
  setMicrophoneChannel(channel) {
91
- if (channel !== undefined) {
92
- $(".mic-channel-text").text(`Channel ${channel}`);
93
- } else {
94
- $(".mic-channel-text").text("Disabled");
95
- }
98
+ const text = channel !== undefined ? `Channel ${channel}` : "Disabled";
99
+ for (const el of document.querySelectorAll(".mic-channel-text")) el.textContent = text;
96
100
  }
97
101
 
98
102
  setMouseJoystickEnabled(enabled) {
99
- $("#mouseJoystickEnabled").prop("checked", !!enabled);
103
+ document.getElementById("mouseJoystickEnabled").checked = !!enabled;
100
104
  }
101
105
 
102
106
  setSpeechOutput(enabled) {
103
- $("#speechOutput").prop("checked", !!enabled);
107
+ document.getElementById("speechOutput").checked = !!enabled;
104
108
  }
105
109
 
106
110
  setDisplayMode(mode) {
107
111
  const config = getFilterForMode(mode).getDisplayConfig();
108
- $(".display-mode-text").text(config.name);
112
+ for (const el of document.querySelectorAll(".display-mode-text")) el.textContent = config.name;
109
113
  }
110
114
 
111
115
  setModel(modelName) {
112
116
  this.model = findModel(modelName);
113
- $(".bbc-model").text(this.model.name);
117
+ for (const el of document.querySelectorAll(".bbc-model")) el.textContent = this.model.name;
114
118
  }
115
119
 
116
120
  setKeyLayout(keyLayout) {
117
- $(".keyboard-layout").text(keyLayout[0].toUpperCase() + keyLayout.substring(1));
121
+ const text = keyLayout[0].toUpperCase() + keyLayout.substring(1);
122
+ for (const el of document.querySelectorAll(".keyboard-layout")) el.textContent = text;
118
123
  }
119
124
 
120
125
  set65c02(enabled) {
121
126
  enabled = !!enabled;
122
- $("#65c02").prop("checked", enabled);
127
+ document.getElementById("65c02").checked = enabled;
123
128
  this.model.tube = enabled ? findModel("Tube65c02") : null;
124
- $("#tubeCpuMultiplier").prop("disabled", !enabled);
129
+ document.getElementById("tubeCpuMultiplier").disabled = !enabled;
125
130
  }
126
131
 
127
132
  setTubeCpuMultiplier(value) {
128
133
  this.tubeCpuMultiplier = value;
129
- $("#tubeCpuMultiplier").val(value);
130
- $("#tubeCpuMultiplierValue").text(value);
134
+ document.getElementById("tubeCpuMultiplier").value = value;
135
+ document.getElementById("tubeCpuMultiplierValue").textContent = value;
131
136
  }
132
137
 
133
138
  setEconet(enabled) {
134
139
  enabled = !!enabled;
135
- $("#hasEconet").prop("checked", enabled);
140
+ document.getElementById("hasEconet").checked = enabled;
136
141
  this.model.hasEconet = enabled;
137
142
 
138
143
  if (enabled && this.model.isMaster) {
@@ -142,20 +147,21 @@ export class Config extends EventEmitter {
142
147
 
143
148
  setMusic5000(enabled) {
144
149
  enabled = !!enabled;
145
- $("#hasMusic5000").prop("checked", enabled);
150
+ document.getElementById("hasMusic5000").checked = enabled;
146
151
  this.model.hasMusic5000 = enabled;
147
152
  this.addRemoveROM("ample.rom", enabled);
148
153
  }
149
154
 
150
155
  setTeletext(enabled) {
151
156
  enabled = !!enabled;
152
- $("#hasTeletextAdaptor").prop("checked", enabled);
157
+ document.getElementById("hasTeletextAdaptor").checked = enabled;
153
158
  this.model.hasTeletextAdaptor = enabled;
154
159
  this.addRemoveROM("ats-3.0.rom", enabled);
155
160
  }
156
161
 
157
162
  setDropdownText(modelName) {
158
- $("#bbc-model-dropdown .bbc-model").text(modelName);
163
+ const el = document.querySelector("#bbc-model-dropdown .bbc-model");
164
+ if (el) el.textContent = modelName;
159
165
  }
160
166
 
161
167
  addRemoveROM(romName, required) {
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+
3
+ // Minimal DOM helpers to replace jQuery usage.
4
+
5
+ export function show(el) {
6
+ el.style.display = "";
7
+ }
8
+
9
+ export function hide(el) {
10
+ el.style.display = "none";
11
+ }
12
+
13
+ export function toggle(el, visible) {
14
+ el.style.display = visible ? "" : "none";
15
+ }
16
+
17
+ export function fadeIn(el, duration = 400) {
18
+ el.style.display = "";
19
+ el.style.transition = `opacity ${duration}ms`;
20
+ el.style.opacity = "0";
21
+ // Force reflow so the transition triggers.
22
+ void el.offsetHeight;
23
+ el.style.opacity = "1";
24
+ }
25
+
26
+ export function fadeOut(el, duration = 400) {
27
+ el.style.transition = `opacity ${duration}ms`;
28
+ el.style.opacity = "0";
29
+ setTimeout(() => {
30
+ if (el.style.opacity === "0") el.style.display = "none";
31
+ }, duration);
32
+ }
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
 
3
- import _ from "underscore";
4
- import * as utils from "./utils.js";
3
+ import { debounce, uint8ArrayToString } from "./utils.js";
5
4
  import { discFor } from "./fdc.js";
6
5
 
7
6
  const MIME_TYPE = "application/vnd.jsbeeb.disc-image";
@@ -112,7 +111,7 @@ export class GoogleDriveLoader {
112
111
  const metadata = { name, mimeType: MIME_TYPE };
113
112
  if (!idOrNone) metadata.parents = [this.parentFolderId];
114
113
 
115
- const base64Data = btoa(utils.uint8ArrayToString(data));
114
+ const base64Data = btoa(uint8ArrayToString(data));
116
115
  const multipartRequestBody =
117
116
  `${delimiter}Content-Type: application/json\r\n\r\n` +
118
117
  `${JSON.stringify(metadata)}${delimiter}` +
@@ -141,7 +140,7 @@ export class GoogleDriveLoader {
141
140
  const id = meta.id;
142
141
  if (meta.capabilities.canEdit) {
143
142
  console.log("Making editable disc");
144
- flusher = _.debounce(async (changedData) => {
143
+ flusher = debounce(async (changedData) => {
145
144
  console.log("Data changed...");
146
145
  await this.saveFile(name, changedData, id);
147
146
  console.log("Saved ok");