web-a2e 1.0.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 (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,176 @@
1
+ /*
2
+ * gamepad-handler.js - Physical game controller support via Gamepad API
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ const STORAGE_KEY_ENABLED = "gamepad-enabled";
9
+ const STORAGE_KEY_DEADZONE = "gamepad-deadzone";
10
+ const DEFAULT_DEADZONE = 0.1;
11
+
12
+ export class GamepadHandler {
13
+ constructor(wasmModule, joystickWindow) {
14
+ this.wasmModule = wasmModule;
15
+ this.joystickWindow = joystickWindow;
16
+ this.enabled = localStorage.getItem(STORAGE_KEY_ENABLED) !== "false";
17
+ this.deadzone = parseFloat(localStorage.getItem(STORAGE_KEY_DEADZONE)) || DEFAULT_DEADZONE;
18
+ this.gamepadIndex = null;
19
+ this.rafId = null;
20
+
21
+ // Track previous button state to detect edges
22
+ this.prevButtons = [false, false];
23
+
24
+ this._onConnected = this._onConnected.bind(this);
25
+ this._onDisconnected = this._onDisconnected.bind(this);
26
+ this._poll = this._poll.bind(this);
27
+
28
+ window.addEventListener("gamepadconnected", this._onConnected);
29
+ window.addEventListener("gamepaddisconnected", this._onDisconnected);
30
+
31
+ // Check if a gamepad is already connected
32
+ const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
33
+ for (let i = 0; i < gamepads.length; i++) {
34
+ if (gamepads[i]) {
35
+ this.gamepadIndex = gamepads[i].index;
36
+ this._notifyWindow();
37
+ if (this.enabled) {
38
+ this._startPolling();
39
+ }
40
+ break;
41
+ }
42
+ }
43
+ }
44
+
45
+ _onConnected(e) {
46
+ this.gamepadIndex = e.gamepad.index;
47
+ this._notifyWindow();
48
+ if (this.enabled) {
49
+ this._startPolling();
50
+ }
51
+ }
52
+
53
+ _onDisconnected(e) {
54
+ if (this.gamepadIndex === e.gamepad.index) {
55
+ this.gamepadIndex = null;
56
+ this._notifyWindow();
57
+ }
58
+ }
59
+
60
+ _notifyWindow() {
61
+ if (this.joystickWindow && this.joystickWindow.updateGamepadStatus) {
62
+ const gp = this._getGamepad();
63
+ this.joystickWindow.updateGamepadStatus(gp ? gp.id : null, this.enabled);
64
+ }
65
+ }
66
+
67
+ _getGamepad() {
68
+ if (this.gamepadIndex === null) return null;
69
+ const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
70
+ return gamepads[this.gamepadIndex] || null;
71
+ }
72
+
73
+ _applyDeadzone(value) {
74
+ if (Math.abs(value) < this.deadzone) return 0;
75
+ // Rescale so the range beyond deadzone maps to 0..1
76
+ const sign = value > 0 ? 1 : -1;
77
+ return sign * (Math.abs(value) - this.deadzone) / (1 - this.deadzone);
78
+ }
79
+
80
+ _poll() {
81
+ const gp = this._getGamepad();
82
+ if (!gp) {
83
+ this.rafId = null;
84
+ return;
85
+ }
86
+ if (this.enabled) {
87
+ // Left stick axes (0 = X, 1 = Y), range -1..1
88
+ const rawX = gp.axes[0] || 0;
89
+ const rawY = gp.axes[1] || 0;
90
+ const adjX = this._applyDeadzone(rawX);
91
+ const adjY = this._applyDeadzone(rawY);
92
+
93
+ // Map -1..1 to 0..1
94
+ const normX = (adjX + 1) / 2;
95
+ const normY = (adjY + 1) / 2;
96
+
97
+ // Map to 0-255 for paddle values
98
+ const paddleX = Math.round(normX * 255);
99
+ const paddleY = Math.round(normY * 255);
100
+
101
+ if (this.wasmModule._setPaddleValue) {
102
+ this.wasmModule._setPaddleValue(0, paddleX);
103
+ this.wasmModule._setPaddleValue(1, paddleY);
104
+ }
105
+
106
+ // Update the joystick window knob to reflect controller position
107
+ if (this.joystickWindow) {
108
+ this.joystickWindow.setExternalPosition(normX, normY);
109
+ }
110
+
111
+ // Buttons 0 and 1 (A/B on standard controllers)
112
+ for (let i = 0; i < 2; i++) {
113
+ const pressed = gp.buttons[i] ? gp.buttons[i].pressed : false;
114
+ if (pressed !== this.prevButtons[i]) {
115
+ this.prevButtons[i] = pressed;
116
+ if (this.wasmModule._setButton) {
117
+ this.wasmModule._setButton(i, pressed);
118
+ }
119
+ if (this.joystickWindow) {
120
+ this.joystickWindow.setExternalButton(i, pressed);
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ this.rafId = requestAnimationFrame(this._poll);
127
+ }
128
+
129
+ _startPolling() {
130
+ if (this.rafId !== null) return;
131
+ this.rafId = requestAnimationFrame(this._poll);
132
+ }
133
+
134
+ _stopPolling() {
135
+ if (this.rafId !== null) {
136
+ cancelAnimationFrame(this.rafId);
137
+ this.rafId = null;
138
+ }
139
+ }
140
+
141
+ start() {
142
+ if (this.enabled && this.gamepadIndex !== null) {
143
+ this._startPolling();
144
+ }
145
+ }
146
+
147
+ stop() {
148
+ this._stopPolling();
149
+ }
150
+
151
+ setEnabled(enabled) {
152
+ this.enabled = enabled;
153
+ localStorage.setItem(STORAGE_KEY_ENABLED, enabled);
154
+ this._notifyWindow();
155
+ if (enabled && this.gamepadIndex !== null) {
156
+ this._startPolling();
157
+ } else if (!enabled) {
158
+ this._stopPolling();
159
+ }
160
+ }
161
+
162
+ setDeadzone(value) {
163
+ this.deadzone = Math.max(0, Math.min(0.5, value));
164
+ localStorage.setItem(STORAGE_KEY_DEADZONE, this.deadzone);
165
+ }
166
+
167
+ isConnected() {
168
+ return this._getGamepad() !== null;
169
+ }
170
+
171
+ destroy() {
172
+ this.stop();
173
+ window.removeEventListener("gamepadconnected", this._onConnected);
174
+ window.removeEventListener("gamepaddisconnected", this._onDisconnected);
175
+ }
176
+ }
@@ -0,0 +1,12 @@
1
+ /*
2
+ * index.js - Input subsystem initialization and exports
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ export { InputHandler } from "./input-handler.js";
9
+ export { TextSelection } from "./text-selection.js";
10
+ export { JoystickWindow } from "./joystick-window.js";
11
+ export { MouseHandler } from "./mouse-handler.js";
12
+ export { GamepadHandler } from "./gamepad-handler.js";
@@ -0,0 +1,396 @@
1
+ /*
2
+ * input-handler.js - Keyboard input handling for the emulator
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ export class InputHandler {
9
+ constructor(wasmModule) {
10
+ this.wasmModule = wasmModule;
11
+
12
+ // Canvas element for focus management
13
+ this.canvas = null;
14
+
15
+ // Hidden input for mobile keyboard
16
+ this.mobileInput = null;
17
+ this.isMobile = false;
18
+
19
+ // Paste queue for typing pasted text
20
+ this.pasteQueue = [];
21
+ this.pasteTimer = null;
22
+ this.pasteSpeedUp = false; // whether we've set a speed multiplier for paste
23
+ this.savedSpeedMultiplier = 1; // speed before paste started
24
+
25
+ // MessageChannel for zero-delay batch scheduling (avoids setTimeout's ~4ms minimum)
26
+ this.pasteChannel = new MessageChannel();
27
+ this.pasteChannel.port1.onmessage = () => this.processPasteQueue();
28
+ }
29
+
30
+ init() {
31
+ // Detect mobile/touch devices
32
+ this.isMobile = this.detectMobile();
33
+
34
+ // Get canvas and make it focusable
35
+ this.canvas = document.getElementById("screen");
36
+ this.canvas.tabIndex = 1; // Make canvas focusable
37
+
38
+ // Create hidden input for mobile keyboard
39
+ if (this.isMobile) {
40
+ this.createMobileInput();
41
+ }
42
+
43
+ // Focus canvas on click (or mobile input on mobile)
44
+ this.canvas.addEventListener("click", () => {
45
+ if (this.isMobile && this.mobileInput) {
46
+ this.mobileInput.focus();
47
+ } else {
48
+ this.canvas.focus();
49
+ }
50
+ });
51
+
52
+ // Also handle touch events for mobile
53
+ this.canvas.addEventListener("touchend", (e) => {
54
+ if (this.isMobile && this.mobileInput) {
55
+ // Small delay to ensure touch event completes
56
+ setTimeout(() => {
57
+ this.mobileInput.focus();
58
+ }, 50);
59
+ }
60
+ });
61
+
62
+ // Focus canvas initially (not on mobile - wait for user tap)
63
+ if (!this.isMobile) {
64
+ setTimeout(() => this.canvas.focus(), 100);
65
+ }
66
+
67
+ // Keyboard event listeners - attach to canvas for better focus control
68
+ this.canvas.addEventListener("keydown", (e) => this.handleKeyDown(e));
69
+ this.canvas.addEventListener("keyup", (e) => this.handleKeyUp(e));
70
+
71
+ // Also listen on document but only process if canvas has focus or no other element does
72
+ document.addEventListener("keydown", (e) => {
73
+ // Only handle if canvas has focus or the active element is body
74
+ if (
75
+ document.activeElement === this.canvas ||
76
+ document.activeElement === document.body
77
+ ) {
78
+ this.handleKeyDown(e);
79
+ }
80
+ });
81
+
82
+ document.addEventListener("keyup", (e) => {
83
+ if (
84
+ document.activeElement === this.canvas ||
85
+ document.activeElement === document.body
86
+ ) {
87
+ this.handleKeyUp(e);
88
+ }
89
+ });
90
+
91
+ // Paste event listener
92
+ document.addEventListener("paste", (e) => {
93
+ if (
94
+ document.activeElement === this.canvas ||
95
+ document.activeElement === document.body
96
+ ) {
97
+ this.handlePaste(e);
98
+ }
99
+ });
100
+ }
101
+
102
+ handleKeyDown(event) {
103
+ const keyCode = event.keyCode || event.which;
104
+
105
+ // Get modifier states
106
+ const shift = event.shiftKey;
107
+ const ctrl = event.ctrlKey;
108
+ const alt = event.altKey;
109
+ const meta = event.metaKey;
110
+ const capsLock = event.getModifierState && event.getModifierState('CapsLock');
111
+
112
+ // Don't interfere with browser shortcuts
113
+ if (ctrl && keyCode === 82) {
114
+ // Ctrl+R for refresh
115
+ return;
116
+ }
117
+
118
+ // Prevent default for these keys when not using modifiers
119
+ if (this.shouldPreventDefault(event)) {
120
+ event.preventDefault();
121
+ }
122
+
123
+ // Always prevent default for printable characters when canvas has focus
124
+ // to stop them from triggering button shortcuts
125
+ if (document.activeElement === this.canvas) {
126
+ event.preventDefault();
127
+ }
128
+
129
+ // Send raw keycode to WASM - C++ handles the translation
130
+ this.wasmModule._handleRawKeyDown(keyCode, shift, ctrl, alt, meta, capsLock);
131
+ }
132
+
133
+ handleKeyUp(event) {
134
+ const keyCode = event.keyCode || event.which;
135
+
136
+ // Get modifier states
137
+ const shift = event.shiftKey;
138
+ const ctrl = event.ctrlKey;
139
+ const alt = event.altKey;
140
+ const meta = event.metaKey;
141
+
142
+ // Send raw keycode to WASM
143
+ this.wasmModule._handleRawKeyUp(keyCode, shift, ctrl, alt, meta);
144
+ }
145
+
146
+ shouldPreventDefault(event) {
147
+ const keyCode = event.keyCode || event.which;
148
+
149
+ // Prevent default for these keys when not using modifiers
150
+ const preventKeys = [
151
+ 8, // Backspace
152
+ 9, // Tab
153
+ 27, // Escape
154
+ 32, // Space (prevent page scroll)
155
+ 37, 38, 39, 40, // Arrow keys
156
+ ];
157
+
158
+ if (preventKeys.includes(keyCode) && !event.ctrlKey && !event.metaKey) {
159
+ return true;
160
+ }
161
+
162
+ return false;
163
+ }
164
+
165
+ // Handle paste event - queues text for input at accelerated speed
166
+ handlePaste(event) {
167
+ event.preventDefault();
168
+
169
+ const text = (event.clipboardData || window.clipboardData).getData("text");
170
+ if (!text) return;
171
+
172
+ this.queueTextInput(text, { speedMultiplier: 8 });
173
+ }
174
+
175
+ // Convert character to Apple II key code (for paste only)
176
+ charToAppleKey(char) {
177
+ const result = this.wasmModule._charToAppleKey(char.charCodeAt(0));
178
+ return result >= 0 ? result : null;
179
+ }
180
+
181
+ // Process paste queue in batches, yielding to the browser periodically
182
+ // to keep the UI responsive and let the audio worklet drive emulation.
183
+ // Speed multiplier (set via WASM) makes the audio-driven emulation run
184
+ // faster, while small boost cycle batches handle immediate key processing.
185
+ processPasteQueue() {
186
+ if (this.pasteQueue.length === 0) {
187
+ this.restorePasteSpeed();
188
+ this.pasteTimer = null;
189
+ return;
190
+ }
191
+
192
+ const BOOST_BATCH = 500; // small cycle batch for immediate key processing
193
+ const TIME_BUDGET_MS = 30;
194
+ const batchEnd = performance.now() + TIME_BUDGET_MS;
195
+
196
+ while (this.pasteQueue.length > 0 && performance.now() < batchEnd) {
197
+ // Wait for keyboard ready, running small boost cycles until it is
198
+ if (this.wasmModule._isKeyboardReady) {
199
+ while (!this.wasmModule._isKeyboardReady()) {
200
+ this.wasmModule._runCycles(BOOST_BATCH);
201
+ if (performance.now() >= batchEnd) break;
202
+ }
203
+ if (!this.wasmModule._isKeyboardReady()) {
204
+ break;
205
+ }
206
+ }
207
+
208
+ const appleKey = this.pasteQueue.shift();
209
+ this.wasmModule._keyDown(appleKey);
210
+
211
+ // Run a small burst to start processing the keystroke
212
+ this.wasmModule._runCycles(BOOST_BATCH);
213
+ }
214
+
215
+ // Schedule next batch if more characters remain
216
+ if (this.pasteQueue.length > 0) {
217
+ this.pasteTimer = true;
218
+ this.pasteChannel.port2.postMessage(null);
219
+ } else {
220
+ this.restorePasteSpeed();
221
+ this.pasteTimer = null;
222
+ if (this.pasteOnComplete) {
223
+ this.pasteOnComplete(false); // false = completed normally
224
+ this.pasteOnComplete = null;
225
+ }
226
+ }
227
+ }
228
+
229
+ // Set emulation speed multiplier for fast paste/input
230
+ setPasteSpeed(multiplier) {
231
+ if (!this.pasteSpeedUp && this.wasmModule._setSpeedMultiplier) {
232
+ this.savedSpeedMultiplier = this.wasmModule._getSpeedMultiplier
233
+ ? this.wasmModule._getSpeedMultiplier()
234
+ : 1;
235
+ this.wasmModule._setSpeedMultiplier(multiplier);
236
+ this.pasteSpeedUp = true;
237
+ }
238
+ }
239
+
240
+ // Restore emulation speed after paste completes
241
+ restorePasteSpeed() {
242
+ if (this.pasteSpeedUp && this.wasmModule._setSpeedMultiplier) {
243
+ this.wasmModule._setSpeedMultiplier(this.savedSpeedMultiplier);
244
+ this.pasteSpeedUp = false;
245
+ }
246
+ }
247
+
248
+ // Cancel any pending paste operation
249
+ cancelPaste() {
250
+ this.pasteTimer = null;
251
+ this.pasteQueue = [];
252
+ this.restorePasteSpeed();
253
+ if (this.pasteOnComplete) {
254
+ this.pasteOnComplete(true); // true = cancelled
255
+ this.pasteOnComplete = null;
256
+ }
257
+ }
258
+
259
+ // Queue text for programmatic input (used by BasicProgramWindow)
260
+ // speedMultiplier: emulation speed during input (1=normal, 8=8x)
261
+ // onStart: callback when pasting begins
262
+ // onComplete: callback when pasting finishes (or is cancelled)
263
+ queueTextInput(text, { speedMultiplier = 8, onStart = null, onComplete = null } = {}) {
264
+ this.pasteOnComplete = onComplete;
265
+ this.setPasteSpeed(speedMultiplier);
266
+
267
+ for (const char of text) {
268
+ const appleKey = this.charToAppleKey(char);
269
+ if (appleKey !== null) {
270
+ this.pasteQueue.push(appleKey);
271
+ }
272
+ }
273
+
274
+ // Start processing queue if not already running
275
+ if (!this.pasteTimer && this.pasteQueue.length > 0) {
276
+ if (onStart) onStart();
277
+ this.processPasteQueue();
278
+ }
279
+ }
280
+
281
+ // Check if a paste operation is in progress
282
+ isPasting() {
283
+ return this.pasteQueue.length > 0 || this.pasteTimer !== null;
284
+ }
285
+
286
+ // Detect if we're on a mobile/touch device
287
+ detectMobile() {
288
+ // Check for touch capability and mobile user agent
289
+ const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
290
+ const isMobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
291
+ // Also check for small screen width as a fallback
292
+ const isSmallScreen = window.innerWidth <= 800;
293
+
294
+ return hasTouch && (isMobileUA || isSmallScreen);
295
+ }
296
+
297
+ // Create hidden input element for mobile keyboard
298
+ createMobileInput() {
299
+ this.mobileInput = document.createElement('input');
300
+ this.mobileInput.type = 'text';
301
+ this.mobileInput.id = 'mobile-keyboard-input';
302
+ this.mobileInput.autocomplete = 'off';
303
+ this.mobileInput.autocapitalize = 'none';
304
+ this.mobileInput.autocorrect = 'off';
305
+ this.mobileInput.spellcheck = false;
306
+
307
+ // Style to be invisible but still functional
308
+ Object.assign(this.mobileInput.style, {
309
+ position: 'absolute',
310
+ left: '-9999px',
311
+ top: '0',
312
+ width: '1px',
313
+ height: '1px',
314
+ opacity: '0',
315
+ pointerEvents: 'none',
316
+ zIndex: '-1'
317
+ });
318
+
319
+ document.body.appendChild(this.mobileInput);
320
+
321
+ // Handle input events from mobile keyboard
322
+ this.mobileInput.addEventListener('input', (e) => {
323
+ const data = e.data;
324
+ if (data) {
325
+ // Process each character typed
326
+ for (const char of data) {
327
+ this.sendCharToEmulator(char);
328
+ }
329
+ }
330
+ // Clear the input to be ready for next character
331
+ this.mobileInput.value = '';
332
+ });
333
+
334
+ // Handle special keys via keydown
335
+ this.mobileInput.addEventListener('keydown', (e) => {
336
+ const keyCode = e.keyCode || e.which;
337
+
338
+ // Handle special keys that don't generate input events
339
+ switch (keyCode) {
340
+ case 8: // Backspace
341
+ case 13: // Enter
342
+ case 27: // Escape
343
+ case 9: // Tab
344
+ e.preventDefault();
345
+ this.handleKeyDown(e);
346
+ break;
347
+ }
348
+ });
349
+
350
+ this.mobileInput.addEventListener('keyup', (e) => {
351
+ const keyCode = e.keyCode || e.which;
352
+
353
+ // Handle special key releases
354
+ switch (keyCode) {
355
+ case 8: // Backspace
356
+ case 13: // Enter
357
+ case 27: // Escape
358
+ case 9: // Tab
359
+ this.handleKeyUp(e);
360
+ break;
361
+ }
362
+ });
363
+
364
+ // Handle blur - show visual feedback that keyboard is hidden
365
+ this.mobileInput.addEventListener('blur', () => {
366
+ this.canvas.classList.remove('keyboard-active');
367
+ });
368
+
369
+ // Handle focus - show visual feedback that keyboard is active
370
+ this.mobileInput.addEventListener('focus', () => {
371
+ this.canvas.classList.add('keyboard-active');
372
+ });
373
+ }
374
+
375
+ // Send a character to the emulator (for mobile input)
376
+ sendCharToEmulator(char) {
377
+ const appleKey = this.charToAppleKey(char);
378
+ if (appleKey !== null) {
379
+ this.wasmModule._keyDown(appleKey);
380
+ }
381
+ }
382
+
383
+ // Show mobile keyboard programmatically
384
+ showMobileKeyboard() {
385
+ if (this.isMobile && this.mobileInput) {
386
+ this.mobileInput.focus();
387
+ }
388
+ }
389
+
390
+ // Hide mobile keyboard
391
+ hideMobileKeyboard() {
392
+ if (this.isMobile && this.mobileInput) {
393
+ this.mobileInput.blur();
394
+ }
395
+ }
396
+ }