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
package/src/fake6502.js CHANGED
@@ -1,40 +1,24 @@
1
1
  // Fakes out various 6502s for testing purposes.
2
- "use strict";
3
2
 
4
3
  import { FakeVideo } from "./video.js";
5
4
  import { FakeSoundChip } from "./soundchip.js";
6
5
  import { TEST_6502, TEST_65C02, TEST_65C12, tubeModelFor } from "./models.js";
7
- import { FakeDdNoise } from "./ddnoise.js";
8
- import { FakeRelayNoise } from "./relaynoise.js";
9
- import { Cpu6502, AtomCpu6502 } from "./6502.js";
10
- import { Cmos } from "./cmos.js";
11
- import { FakeMusic5000 } from "./music5000.js";
6
+ import { machineSpec, nullIo } from "./machine-spec.js";
12
7
 
13
8
  const fakeVideo = new FakeVideo();
14
9
  const soundChip = new FakeSoundChip();
15
- const dbgr = {
16
- setCpu: () => {},
17
- };
18
10
 
19
- export function fake6502(model, opts) {
20
- opts = opts || {};
11
+ export function fake6502(model, opts = {}) {
21
12
  model = model || TEST_6502;
22
- const CpuClass = model.isAtom ? AtomCpu6502 : Cpu6502;
23
- return new CpuClass(model, {
24
- dbgr,
25
- video: opts.video || fakeVideo,
26
- soundChip: opts.soundChip || soundChip,
27
- ddNoise: new FakeDdNoise(),
28
- relayNoise: new FakeRelayNoise(),
29
- music5000: new FakeMusic5000(),
30
- cmos: new Cmos(),
31
- cycleAccurate: opts.cycleAccurate,
32
- config: {
13
+ return new model.Cpu(model, {
14
+ ...nullIo({ video: opts.video ?? fakeVideo, soundChip: opts.soundChip ?? soundChip }),
15
+ config: machineSpec({
33
16
  tube: opts.tube ? tubeModelFor(model) : null,
34
17
  tubeCpuMultiplier: opts.tubeCpuMultiplier,
35
18
  cpuMultiplier: opts.cpuMultiplier,
36
19
  hasTeletextAdaptor: opts.hasTeletextAdaptor,
37
- },
20
+ }),
21
+ cycleAccurate: opts.cycleAccurate,
38
22
  });
39
23
  }
40
24
 
package/src/fdc.js CHANGED
@@ -10,11 +10,12 @@ import {
10
10
  toSsdOrDsd,
11
11
  } from "./disc.js";
12
12
  import { loadHfe, sniffHfeLayout, toHfe } from "./disc-hfe.js";
13
- import * as utils from "./utils.js";
13
+ import { stringToUint8Array } from "./binary.js";
14
+ import { loadData } from "./loader.js";
14
15
 
15
16
  export function load(name) {
16
- console.log("Loading disc from " + name); // todo support zip files
17
- return utils.loadData(name);
17
+ console.log("Loading disc from " + name);
18
+ return loadData(name);
18
19
  }
19
20
 
20
21
  /**
@@ -185,7 +186,7 @@ const hfeDiscType = new DiscType({
185
186
  const adlDiscType = new DiscType({
186
187
  extension: ".adl",
187
188
  loader: (disc, data, _onChange) => {
188
- // TODO handle onChange
189
+ // TODO(#822) handle onChange
189
190
  return loadAdf(disc, data, true);
190
191
  },
191
192
  saver: (_data) => {
@@ -200,7 +201,7 @@ const adlDiscType = new DiscType({
200
201
  const adfDiscType = new DiscType({
201
202
  extension: ".adf",
202
203
  loader: (disc, data, _onChange) => {
203
- // TODO handle onChange
204
+ // TODO(#822) handle onChange
204
205
  return loadAdf(disc, data, false);
205
206
  },
206
207
  saver: (_data) => {
@@ -265,15 +266,14 @@ function is40TrackLayout(discType, data, name, layout) {
265
266
 
266
267
  /**
267
268
  * Create a disc object of the appropriate type based on the file name
268
- * @param {Object} fdc - The FDC controller object
269
269
  * @param {string} name - The file name with extension
270
270
  * @param {string|Uint8Array} stringData - The disc image data as string or Uint8Array
271
- * @param {function(Uint8Array): void} onChange - Optional callback when disc content changes
271
+ * @param {function(Uint8Array): void} [onChange] - called when disc content changes
272
272
  * @param {string} [layout] - one of DiscLayout; by default the image is asked what it is
273
273
  * @returns {Disc} The loaded disc object
274
274
  */
275
- export function discFor(fdc, name, stringData, onChange, layout = DiscLayout.auto) {
276
- const data = typeof stringData !== "string" ? stringData : utils.stringToUint8Array(stringData);
275
+ export function discFor(name, stringData, onChange, layout = DiscLayout.auto) {
276
+ const data = typeof stringData !== "string" ? stringData : stringToUint8Array(stringData);
277
277
  const discType = guessDiscTypeFromName(name);
278
278
  const config = new DiscConfig();
279
279
  config.expandTo80 = is40TrackLayout(discType, data, name, layout);
@@ -288,45 +288,3 @@ export function discFor(fdc, name, stringData, onChange, layout = DiscLayout.aut
288
288
  disc.setOriginalImageCrc32(data instanceof Uint8Array ? data : new Uint8Array(data));
289
289
  return disc;
290
290
  }
291
-
292
- /**
293
- * Create or open a disc held in the browser's local storage.
294
- * @param {Object} fdc - The FDC controller object
295
- * @param {string} name - The file name with extension
296
- * @param {string} [layout] - one of DiscLayout; by default the image is asked what it is
297
- * @param {function(*): void} [onSaveError] - called with whatever was thrown, the first time a write
298
- * cannot be stored
299
- * @returns {Disc} The loaded disc object
300
- */
301
- export function localDisc(fdc, name, layout = DiscLayout.auto, onSaveError = () => {}) {
302
- const discName = "disc_" + name;
303
- let data;
304
- const dataString = window.localStorage[discName];
305
- if (!dataString) {
306
- console.log("Creating browser-local disc " + name);
307
- const discType = guessDiscTypeFromName(name);
308
- if (!discType.byteSize) {
309
- throw new Error(`Cannot create blank disc of type ${discType.extension} - unknown size`);
310
- }
311
- data = new Uint8Array(discType.byteSize);
312
- if (discType.supportsCatalogue) {
313
- discType.setDiscName(data, name);
314
- }
315
- } else {
316
- console.log("Loading browser-local disc " + name);
317
- data = utils.stringToUint8Array(dataString);
318
- }
319
- let reportedSaveError = false;
320
- const onChange = (data) => {
321
- try {
322
- const str = utils.uint8ArrayToString(data);
323
- window.localStorage.setItem(discName, str);
324
- } catch (e) {
325
- console.log(`Unable to save browser-local disc ${name}: ${e}`);
326
- if (reportedSaveError) return;
327
- reportedSaveError = true;
328
- onSaveError(e);
329
- }
330
- };
331
- return discFor(fdc, name, data, onChange, layout);
332
- }
package/src/filestore.js CHANGED
@@ -1,10 +1,8 @@
1
1
  // Level-3 file server emulator ported from FSEM2
2
2
  // https://github.com/mmbeeb/FSEM
3
3
 
4
- "use strict";
5
-
6
4
  import { ReceiveBlock, EconetPacket } from "./econet.js";
7
- import * as utils from "./utils.js";
5
+ import { loadData } from "./loader.js";
8
6
 
9
7
  export class Filestore {
10
8
  constructor(cpu, econet) {
@@ -306,22 +304,17 @@ export class Filestore {
306
304
  }
307
305
  }
308
306
 
309
- reset() {
307
+ /** Resolves once the filestore code and its disc are loaded and in place. */
308
+ async reset() {
310
309
  console.log("Filestore: initialisation");
311
310
 
312
- const filestoreRef = this;
313
- utils.loadData("econet/L3FS.dat").then(function (data) {
314
- filestoreRef.l3fs = data;
315
- for (let i = 0; i < data.length; i++) {
316
- filestoreRef.ram[0x400 + i] = data[i];
317
- }
318
- filestoreRef.PC = 0x400;
319
- filestoreRef.SP = 0xff;
320
- filestoreRef.A = 1;
321
- });
322
- utils.loadData("econet/scsi.dat").then(function (data) {
323
- filestoreRef.scsi = data;
324
- });
311
+ const [code, disc] = await Promise.all([loadData("econet/L3FS.dat"), loadData("econet/scsi.dat")]);
312
+ this.l3fs = code;
313
+ this.ram.set(code, 0x400);
314
+ this.PC = 0x400;
315
+ this.SP = 0xff;
316
+ this.A = 1;
317
+ this.scsi = disc;
325
318
  }
326
319
 
327
320
  polltime(cycles) {
package/src/hex.js ADDED
@@ -0,0 +1,38 @@
1
+ export function parseAddr(s) {
2
+ if (s[0] === "$" || s[0] === "&") return parseInt(s.substring(1), 16);
3
+ if (s.indexOf("0x") === 0) return parseInt(s.substring(2), 16);
4
+ return parseInt(s, 16);
5
+ }
6
+
7
+ export function hexbyte(value) {
8
+ return ((value >>> 4) & 0xf).toString(16) + (value & 0xf).toString(16);
9
+ }
10
+
11
+ export function hexword(value) {
12
+ return hexbyte(value >>> 8) + hexbyte(value & 0xff);
13
+ }
14
+
15
+ export function hd(reader, start, end, opts) {
16
+ opts = opts || {};
17
+ const width = opts.width || 16;
18
+ const gap = opts.gap === undefined ? 8 : opts.gap;
19
+ const res = [];
20
+ let str = "";
21
+ let j = 0;
22
+ for (let i = start; i < end; ++i) {
23
+ str += " ";
24
+ str += hexbyte(reader(i));
25
+ if (++j === gap) str += " ";
26
+ if (j === width) {
27
+ res.push(str);
28
+ str = "";
29
+ j = 0;
30
+ }
31
+ }
32
+ if (str) res.push(str);
33
+ let joined = "";
34
+ for (let i = 0; i < res.length; ++i) {
35
+ joined += hexword(start + i * width) + " :" + res[i] + "\n";
36
+ }
37
+ return joined;
38
+ }
package/src/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * What jsbeeb offers a program that is not the web page: a machine to drive
3
+ * headless, the session the MCP server wraps around it, the models, and the
4
+ * pieces that describe and feed a machine. Electron starts from src/app/app.js
5
+ * through package.json's main; the page starts from src/main.js.
6
+ */
7
+ export { MachineSession } from "./machine-session.js";
8
+ export { TestMachine } from "./test-machine.js";
9
+ export { allModels, findModel } from "./models.js";
10
+ export { machineSpec, nullIo } from "./machine-spec.js";
11
+ export { MediaResolver } from "./media-resolver.js";
package/src/intel-fdc.js CHANGED
@@ -9,7 +9,7 @@ import { Disc, IbmDiscFormat } from "./disc.js";
9
9
  import { BaseDiscDrive, DiscDrive } from "./disc-drive.js";
10
10
  // eslint-disable-next-line no-unused-vars
11
11
  import { Scheduler } from "./scheduler.js";
12
- import * as utils from "./utils.js";
12
+ import { hexbyte, hexword } from "./hex.js";
13
13
 
14
14
  // TODOs remaining for intel-fdc and related functionality
15
15
  // - support loading other disc formats
@@ -343,7 +343,7 @@ export class IntelFdc {
343
343
  // aka. late DMA, which will abort the command while NMI is asserted. We
344
344
  // therefore need to de-assert NMI so that the NMI for command completion
345
345
  // isn't lost.
346
- // TODO(matt) - we don't model NMIs properly here, each device should have its own nmi line
346
+ // TODO(#1058) we don't model NMIs properly here, each device should have its own nmi line
347
347
  this._cpu.NMI(false);
348
348
  }
349
349
 
@@ -400,7 +400,7 @@ export class IntelFdc {
400
400
  case Address.unknown_read_3:
401
401
  return this._regs[Registers.internalCountLsb];
402
402
  default:
403
- throw new Error(`"Unexpected read of addr ${utils.hexword(addr)}"`);
403
+ throw new Error(`"Unexpected read of addr ${hexword(addr)}"`);
404
404
  }
405
405
  }
406
406
 
@@ -443,7 +443,7 @@ export class IntelFdc {
443
443
  break;
444
444
  case 3:
445
445
  default:
446
- this._log(`Not supported: ${utils.hexword(addr)}=${utils.hexbyte(val)}`);
446
+ this._log(`Not supported: ${hexword(addr)}=${hexbyte(val)}`);
447
447
  }
448
448
  }
449
449
 
@@ -516,7 +516,7 @@ export class IntelFdc {
516
516
  const data = this._mmioData;
517
517
  const pulses = IbmDiscFormat.fmTo2usPulses(clocks, data);
518
518
  if (clocks !== 0xff && clocks !== IbmDiscFormat.markClockPattern)
519
- this._log(`writing unusual clocks=${utils.hexbyte(clocks)} data=${utils.hexbyte(data)}`);
519
+ this._log(`writing unusual clocks=${hexbyte(clocks)} data=${hexbyte(data)}`);
520
520
  this._currentDrive.writePulses(pulses);
521
521
  }
522
522
 
@@ -1058,7 +1058,7 @@ export class IntelFdc {
1058
1058
  status |= StatusFlag.resultReady;
1059
1059
  }
1060
1060
 
1061
- // TODO: "command register full", bit value 0x40, isn't understood. In
1061
+ // TODO(#1060) "command register full", bit value 0x40, isn't understood. In
1062
1062
  // particular, the mode register (shared with the status register we
1063
1063
  // believe) is set to 0xC1 in typical operation. This would seem to raise
1064
1064
  // 0x40 after it has been lowered at command register acceptance. However,
@@ -1073,9 +1073,7 @@ export class IntelFdc {
1073
1073
  _commandWritten(command) {
1074
1074
  const status = this.internalStatus;
1075
1075
  if (status & StatusFlag.busy) {
1076
- this._log(
1077
- `command ${utils.hexbyte(command)} while busy with ${utils.hexbyte(this._regs[Registers.internalCommand])}`,
1078
- );
1076
+ this._log(`command ${hexbyte(command)} while busy with ${hexbyte(this._regs[Registers.internalCommand])}`);
1079
1077
  }
1080
1078
 
1081
1079
  // Set command.
@@ -1125,7 +1123,7 @@ export class IntelFdc {
1125
1123
  break;
1126
1124
  }
1127
1125
  case ParamAccept.specify: {
1128
- this._logCommand(`specify param ${utils.hexbyte(param)}`);
1126
+ this._logCommand(`specify param ${hexbyte(param)}`);
1129
1127
  this._writeRegister(this._regs[Registers.internalPointer], param);
1130
1128
  ++this._regs[Registers.internalPointer];
1131
1129
  if (--this._regs[Registers.internalParamCount] === 0) {
@@ -1155,7 +1153,7 @@ export class IntelFdc {
1155
1153
  case Registers.mmioData & 0x07:
1156
1154
  return this._mmioData;
1157
1155
  default:
1158
- this._log(`direct read from MMIO register ${utils.hexbyte(reg)}`);
1156
+ this._log(`direct read from MMIO register ${hexbyte(reg)}`);
1159
1157
  break;
1160
1158
  }
1161
1159
  return 0;
@@ -1185,7 +1183,7 @@ export class IntelFdc {
1185
1183
  this._mmioData = val;
1186
1184
  break;
1187
1185
  default:
1188
- this._log(`direct write to MMIO register ${utils.hexbyte(reg)}`);
1186
+ this._log(`direct write to MMIO register ${hexbyte(reg)}`);
1189
1187
  break;
1190
1188
  }
1191
1189
  }
@@ -1430,13 +1428,13 @@ export class IntelFdc {
1430
1428
  commandReg &= ~(DriveOut.selectFlags | 0x03);
1431
1429
  this._regs[Registers.internalCommand] = commandReg;
1432
1430
  this._logCommand(
1433
- `command ${utils.hexbyte(origCommand & 0x3f)} ` +
1434
- `sel ${utils.hexbyte(selectBits)} ` +
1435
- `params ${utils.hexbyte(this._regs[Registers.internalParam_1])} ` +
1436
- `${utils.hexbyte(this._regs[Registers.internalParam_2])} ` +
1437
- `${utils.hexbyte(this._regs[Registers.internalParam_3])} ` +
1438
- `${utils.hexbyte(this._regs[Registers.internalParam_4])} ` +
1439
- `${utils.hexbyte(this._regs[Registers.internalParam_5])} ` +
1431
+ `command ${hexbyte(origCommand & 0x3f)} ` +
1432
+ `sel ${hexbyte(selectBits)} ` +
1433
+ `params ${hexbyte(this._regs[Registers.internalParam_1])} ` +
1434
+ `${hexbyte(this._regs[Registers.internalParam_2])} ` +
1435
+ `${hexbyte(this._regs[Registers.internalParam_3])} ` +
1436
+ `${hexbyte(this._regs[Registers.internalParam_4])} ` +
1437
+ `${hexbyte(this._regs[Registers.internalParam_5])} ` +
1440
1438
  `ptrk ${this._currentDrive ? this._currentDrive.track : -1} ` +
1441
1439
  `hpos ${this._currentDrive ? this._currentDrive.headPosition : -1}`,
1442
1440
  );
@@ -1631,7 +1629,7 @@ export class IntelFdc {
1631
1629
 
1632
1630
  _lowerBusyAndLog() {
1633
1631
  this._statusLower(StatusFlag.busy);
1634
- this._logCommand(`status ${utils.hexbyte(this._status)} result ${utils.hexbyte(this._result)}`);
1632
+ this._logCommand(`status ${hexbyte(this._status)} result ${hexbyte(this._result)}`);
1635
1633
  }
1636
1634
 
1637
1635
  _spinDown() {
package/src/jsbeeb.css CHANGED
@@ -560,6 +560,39 @@ small {
560
560
  flex: none;
561
561
  }
562
562
 
563
+ #header-bar .navbar-brand img {
564
+ display: block;
565
+ }
566
+
567
+ #header-bar [data-icon] > svg {
568
+ width: 1.15em;
569
+ height: 1.15em;
570
+ vertical-align: -0.2em;
571
+ }
572
+
573
+ /* Shrunk, the menu list wraps onto a second row; the promo yields instead, by truncating. */
574
+ #header-bar .navbar-nav {
575
+ flex-shrink: 0;
576
+ }
577
+
578
+ #header-bar .navbar-text {
579
+ flex: 0 1 auto;
580
+ min-width: 0;
581
+ white-space: nowrap;
582
+ overflow: hidden;
583
+ text-overflow: ellipsis;
584
+ }
585
+
586
+ #paste-form {
587
+ flex: 1 0 6rem;
588
+ min-width: 0;
589
+ max-width: 14rem;
590
+ }
591
+
592
+ #paste-text {
593
+ min-width: 0;
594
+ }
595
+
563
596
  /* The only item in the header with no bound on its width, so it is the one that yields. */
564
597
  #disc-name {
565
598
  flex: 1;
@@ -1,21 +1,4 @@
1
- import { keyCodes, userKeymap } from "./utils.js";
2
-
3
- // Detect keyboard layout locally — mirrors the logic in utils.js but avoids
4
- // importing the non-exported isUKlayout. Should be replaced with an import
5
- // if utils.js ever exports it.
6
- function detectKeyboardLayout() {
7
- if (typeof navigator === "undefined") return "UK";
8
- if (typeof localStorage !== "undefined" && localStorage.keyboardLayout) {
9
- return localStorage.keyboardLayout === "US" ? "US" : "UK";
10
- }
11
- if (navigator.language) {
12
- if (navigator.language.toLowerCase() === "en-gb") return "UK";
13
- if (navigator.language.toLowerCase() === "en-us") return "US";
14
- }
15
- return "UK";
16
- }
17
-
18
- const isUKlayout = detectKeyboardLayout() === "UK";
1
+ import { detectKeyboardLayout, keyCodes, userKeymap } from "./keymap.js";
19
2
 
20
3
  // ATOM
21
4
 
@@ -228,6 +211,7 @@ export function stringToATOMKeys(str) {
228
211
  }
229
212
 
230
213
  export function getKeyMapAtom(keyLayout) {
214
+ const isUKlayout = detectKeyboardLayout() === "UK";
231
215
  const keys2 = [];
232
216
 
233
217
  // shift pressed