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,139 @@
1
+ /*
2
+ * drive-sounds.js - Disk drive sound effects
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ export class DriveSounds {
9
+ constructor() {
10
+ // Audio context (lazily created)
11
+ this.audioContext = null;
12
+
13
+ // Master volume (mirrors the main volume slider, 0.0-1.0)
14
+ this.masterVolume = 1.0;
15
+
16
+ // Seek sound settings
17
+ this.seekSoundEnabled = true;
18
+ this.seekVolume = 0.3;
19
+ this.seekPrimaryFreq = 2200;
20
+ this.seekSecondaryFreq = 3800;
21
+ this.seekBodyFreq = 1200;
22
+ this.seekDecay = 350;
23
+ this.seekClickDecay = 1200;
24
+ }
25
+
26
+ /**
27
+ * Initialize audio context (lazily created on first use)
28
+ */
29
+ initAudioContext() {
30
+ if (!this.audioContext) {
31
+ try {
32
+ this.audioContext = new (
33
+ window.AudioContext || window.webkitAudioContext
34
+ )();
35
+ } catch (e) {
36
+ console.warn("Could not create audio context for drive sounds:", e);
37
+ this.seekSoundEnabled = false;
38
+ }
39
+ }
40
+ return this.audioContext;
41
+ }
42
+
43
+ /**
44
+ * Play a synthesized disk drive seek/step sound
45
+ * Models the mechanical "thunk" of a Disk II stepper motor
46
+ */
47
+ playSeekSound() {
48
+ if (!this.seekSoundEnabled) return;
49
+
50
+ const ctx = this.initAudioContext();
51
+ if (!ctx || ctx.state === "suspended") return;
52
+
53
+ const now = ctx.currentTime;
54
+ const duration = 0.025; // 25ms for the full sound
55
+ const sampleRate = ctx.sampleRate;
56
+ const bufferSize = Math.ceil(sampleRate * duration);
57
+ const buffer = ctx.createBuffer(1, bufferSize, sampleRate);
58
+ const data = buffer.getChannelData(0);
59
+
60
+ // Use configurable parameters
61
+ const primaryFreq = this.seekPrimaryFreq;
62
+ const secondaryFreq = this.seekSecondaryFreq;
63
+ const bodyFreq = this.seekBodyFreq;
64
+ const decay = this.seekDecay;
65
+ const clickDecay = this.seekClickDecay;
66
+
67
+ for (let i = 0; i < bufferSize; i++) {
68
+ const t = i / sampleRate;
69
+
70
+ // Very fast exponential decay - metallic tick
71
+ const envelope = Math.exp(-t * decay);
72
+
73
+ // Sharp initial transient/click
74
+ const clickEnv = Math.exp(-t * clickDecay);
75
+ const click = clickEnv * (Math.random() * 2 - 1) * 0.4;
76
+
77
+ // Primary high-pitched tick
78
+ const tick = Math.sin(2 * Math.PI * primaryFreq * t) * 0.5;
79
+
80
+ // Higher harmonic for metallic character
81
+ const harmonic = Math.sin(2 * Math.PI * secondaryFreq * t) * 0.25;
82
+
83
+ // Lower body resonance (decays faster)
84
+ const bodyEnv = Math.exp(-t * (decay + 150));
85
+ const body = bodyEnv * Math.sin(2 * Math.PI * bodyFreq * t) * 0.3;
86
+
87
+ // Combine components
88
+ data[i] = envelope * (tick + harmonic) + body * envelope + click;
89
+ }
90
+
91
+ const source = ctx.createBufferSource();
92
+ source.buffer = buffer;
93
+
94
+ // Add a low-pass filter to tame the very highest frequencies
95
+ const filter = ctx.createBiquadFilter();
96
+ filter.type = "lowpass";
97
+ filter.frequency.value = 6000;
98
+ filter.Q.value = 0.5;
99
+
100
+ const gain = ctx.createGain();
101
+ gain.gain.value = this.seekVolume * 0.8 * this.masterVolume;
102
+
103
+ source.connect(filter);
104
+ filter.connect(gain);
105
+ gain.connect(ctx.destination);
106
+
107
+ source.start(now);
108
+ source.stop(now + duration);
109
+ }
110
+
111
+ /**
112
+ * Enable or disable seek sounds
113
+ */
114
+ setSeekSoundEnabled(enabled) {
115
+ this.seekSoundEnabled = enabled;
116
+ }
117
+
118
+ /**
119
+ * Set seek sound volume (0.0 - 1.0)
120
+ */
121
+ setSeekVolume(volume) {
122
+ this.seekVolume = Math.max(0, Math.min(1, volume));
123
+ }
124
+
125
+ // Motor sound stubs - kept for API compatibility
126
+ startMotorSound() {}
127
+ stopMotorSound() {}
128
+ setMotorSoundEnabled() {}
129
+ setMotorVolume() {}
130
+ updateMotorSoundParams() {}
131
+
132
+ /**
133
+ * Set the master volume level (0.0 - 1.0), mirroring the main volume slider.
134
+ * Scales all drive sounds proportionally.
135
+ */
136
+ setMasterVolume(volume) {
137
+ this.masterVolume = Math.max(0, Math.min(1, volume));
138
+ }
139
+ }
@@ -0,0 +1,481 @@
1
+ /*
2
+ * hard-drive-manager.js - Hard drive image management
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ import {
9
+ saveImageToStorage,
10
+ loadImageFromStorage,
11
+ clearImageFromStorage,
12
+ addToRecentImages,
13
+ getRecentImages,
14
+ loadRecentImage,
15
+ clearRecentImages,
16
+ } from "./hard-drive-persistence.js";
17
+ import { showToast } from "../ui/toast.js";
18
+ import { createDatabaseManager } from "../utils/indexeddb-helper.js";
19
+
20
+ const CACHE_STORE = "images";
21
+
22
+ const libraryCacheDb = createDatabaseManager({
23
+ dbName: "a2e-disk-library-cache",
24
+ version: 1,
25
+ onUpgrade: (event) => {
26
+ const database = event.target.result;
27
+ if (!database.objectStoreNames.contains(CACHE_STORE)) {
28
+ database.createObjectStore(CACHE_STORE, { keyPath: "id" });
29
+ }
30
+ },
31
+ });
32
+
33
+ function insertImageToWasm(wasmModule, deviceNum, data, filename) {
34
+ const dataPtr = wasmModule._malloc(data.length);
35
+ wasmModule.HEAPU8.set(data, dataPtr);
36
+
37
+ const filenamePtr = wasmModule._malloc(filename.length + 1);
38
+ wasmModule.stringToUTF8(filename, filenamePtr, filename.length + 1);
39
+
40
+ const success = wasmModule._insertSmartPortImage(deviceNum, dataPtr, data.length, filenamePtr);
41
+
42
+ wasmModule._free(dataPtr);
43
+ wasmModule._free(filenamePtr);
44
+
45
+ return success;
46
+ }
47
+
48
+ export class HardDriveManager {
49
+ constructor(wasmModule) {
50
+ this.wasmModule = wasmModule;
51
+ this.devices = [
52
+ { filename: null, ejectBtn: null, nameLabel: null, input: null, activityFrames: 0 },
53
+ { filename: null, ejectBtn: null, nameLabel: null, input: null, activityFrames: 0 },
54
+ ];
55
+ this.canvas = null;
56
+ this.activeDropdown = null;
57
+ this.fileExplorer = null;
58
+ }
59
+
60
+ init() {
61
+ this.canvas = document.getElementById("screen");
62
+
63
+ for (let i = 0; i < 2; i++) {
64
+ this.setupDevice(i);
65
+ }
66
+
67
+ document.addEventListener("click", (e) => {
68
+ if (this.activeDropdown && !e.target.closest(".hd-recent-container")) {
69
+ this.closeRecentDropdown();
70
+ }
71
+ });
72
+
73
+ this.restoreImages();
74
+ }
75
+
76
+ setupDevice(deviceNum) {
77
+ const container = document.getElementById(`hd-device${deviceNum}`);
78
+ if (!container) return;
79
+
80
+ const device = this.devices[deviceNum];
81
+ device.input = container.querySelector(`#hd-device${deviceNum}-input`);
82
+ device.insertBtn = container.querySelector(".hd-insert");
83
+ device.recentBtn = container.querySelector(".hd-recent");
84
+ device.recentDropdown = container.querySelector(".hd-recent-dropdown");
85
+ device.ejectBtn = container.querySelector(".hd-eject");
86
+ device.browseBtn = container.querySelector(".hd-browse");
87
+ device.nameLabel = container.querySelector(".hd-name");
88
+ device.ledEl = container.querySelector(".hd-led");
89
+ device.infoLabel = container.querySelector(".hd-info");
90
+
91
+ if (device.browseBtn) {
92
+ device.browseBtn.addEventListener("click", () => {
93
+ if (this.fileExplorer) {
94
+ this.fileExplorer.showHardDrive(deviceNum);
95
+ }
96
+ this.refocusCanvas();
97
+ });
98
+ }
99
+
100
+ if (device.insertBtn) {
101
+ device.insertBtn.addEventListener("click", () => {
102
+ if (!this.isSmartPortInstalled()) {
103
+ showToast("SmartPort card is not installed. Configure it in the Expansion Slots window before loading SmartPort images.", "warning");
104
+ return;
105
+ }
106
+ device.input?.click();
107
+ });
108
+ }
109
+
110
+ if (device.input) {
111
+ device.input.addEventListener("change", (e) => {
112
+ if (e.target.files.length > 0) {
113
+ this.loadImage(deviceNum, e.target.files[0]);
114
+ }
115
+ this.refocusCanvas();
116
+ });
117
+ }
118
+
119
+ if (device.recentBtn) {
120
+ device.recentBtn.addEventListener("click", (e) => {
121
+ e.stopPropagation();
122
+ if (!this.isSmartPortInstalled()) {
123
+ showToast("SmartPort card is not installed. Configure it in the Expansion Slots window before loading SmartPort images.", "warning");
124
+ return;
125
+ }
126
+ this.toggleRecentDropdown(deviceNum);
127
+ });
128
+ }
129
+
130
+ if (device.ejectBtn) {
131
+ device.ejectBtn.addEventListener("click", () => {
132
+ this.ejectImage(deviceNum);
133
+ this.refocusCanvas();
134
+ });
135
+ }
136
+ }
137
+
138
+ isSmartPortInstalled() {
139
+ return this.wasmModule._isSmartPortCardInstalled &&
140
+ this.wasmModule._isSmartPortCardInstalled();
141
+ }
142
+
143
+ refocusCanvas() {
144
+ if (this.canvas) setTimeout(() => this.canvas.focus(), 0);
145
+ }
146
+
147
+ async loadImage(deviceNum, file) {
148
+ try {
149
+ const arrayBuffer = await file.arrayBuffer();
150
+ const data = new Uint8Array(arrayBuffer);
151
+
152
+ const success = insertImageToWasm(this.wasmModule, deviceNum, data, file.name);
153
+
154
+ if (success) {
155
+ this.devices[deviceNum].filename = file.name;
156
+ this.updateDeviceUI(deviceNum);
157
+ console.log(`Inserted HD image in device ${deviceNum + 1}: ${file.name}`);
158
+ await saveImageToStorage(deviceNum, file.name, data);
159
+ await addToRecentImages(deviceNum, file.name, data);
160
+ } else {
161
+ console.error(`Failed to load HD image: ${file.name}`);
162
+ showToast(`Failed to load SmartPort image: ${file.name}`, "error");
163
+ }
164
+ } catch (error) {
165
+ console.error("Error loading HD image:", error);
166
+ showToast("Error loading hard drive image: " + error.message, "error");
167
+ }
168
+ }
169
+
170
+ loadImageFromData(deviceNum, filename, data) {
171
+ const success = insertImageToWasm(this.wasmModule, deviceNum, data, filename);
172
+ if (success) {
173
+ this.devices[deviceNum].filename = filename;
174
+ this.updateDeviceUI(deviceNum);
175
+ console.log(`Restored HD image in device ${deviceNum + 1}: ${filename}`);
176
+ }
177
+ return success;
178
+ }
179
+
180
+ async ejectImage(deviceNum) {
181
+ const device = this.devices[deviceNum];
182
+ if (!device.filename) return;
183
+
184
+ // If modified, show host save dialog directly
185
+ if (this.wasmModule._isSmartPortImageModified &&
186
+ this.wasmModule._isSmartPortImageModified(deviceNum)) {
187
+ let filename = device.filename || `harddrive${deviceNum + 1}.hdv`;
188
+ if (!filename.includes(".")) filename += ".hdv";
189
+
190
+ try {
191
+ const sizePtr = this.wasmModule._malloc(4);
192
+ const dataPtr = this.wasmModule._getSmartPortImageData(deviceNum, sizePtr);
193
+ const size = new DataView(this.wasmModule.HEAPU8.buffer).getUint32(sizePtr, true);
194
+ this.wasmModule._free(sizePtr);
195
+
196
+ if (dataPtr && size > 0) {
197
+ const data = new Uint8Array(this.wasmModule.HEAPU8.buffer, dataPtr, size);
198
+ const blob = new Blob([data], { type: "application/octet-stream" });
199
+
200
+ if (window.showSaveFilePicker) {
201
+ const handle = await window.showSaveFilePicker({
202
+ suggestedName: filename,
203
+ types: [{ description: "SmartPort Image", accept: { "application/octet-stream": [".hdv", ".po", ".2mg"] } }],
204
+ });
205
+ const writable = await handle.createWritable();
206
+ await writable.write(blob);
207
+ await writable.close();
208
+ } else {
209
+ const url = URL.createObjectURL(blob);
210
+ const a = document.createElement("a");
211
+ a.href = url;
212
+ a.download = filename;
213
+ a.click();
214
+ URL.revokeObjectURL(url);
215
+ }
216
+ }
217
+ } catch (error) {
218
+ if (error.name !== "AbortError") {
219
+ console.error("Error saving HD image:", error);
220
+ }
221
+ }
222
+ }
223
+
224
+ this.performEject(deviceNum);
225
+ }
226
+
227
+ performEject(deviceNum) {
228
+ if (this.wasmModule._ejectSmartPortImage) {
229
+ this.wasmModule._ejectSmartPortImage(deviceNum);
230
+ }
231
+ this.devices[deviceNum].filename = null;
232
+ this.updateDeviceUI(deviceNum);
233
+ clearImageFromStorage(deviceNum);
234
+ console.log(`Ejected HD image from device ${deviceNum + 1}`);
235
+ }
236
+
237
+ updateDeviceUI(deviceNum) {
238
+ const device = this.devices[deviceNum];
239
+ if (device.nameLabel) {
240
+ device.nameLabel.textContent = device.filename || "No Image";
241
+ }
242
+ if (device.ejectBtn) {
243
+ device.ejectBtn.disabled = !device.filename;
244
+ }
245
+ if (device.browseBtn) {
246
+ device.browseBtn.disabled = !device.filename;
247
+ }
248
+ this.updateDeviceInfo(deviceNum);
249
+ }
250
+
251
+ updateDeviceInfo(deviceNum) {
252
+ const device = this.devices[deviceNum];
253
+ if (!device.infoLabel) return;
254
+
255
+ if (!device.filename || !this.wasmModule._isSmartPortImageInserted) {
256
+ device.infoLabel.textContent = "";
257
+ return;
258
+ }
259
+
260
+ // We don't have a direct way to get block count from JS;
261
+ // it's shown as the filename for now
262
+ device.infoLabel.textContent = "";
263
+ }
264
+
265
+ updateLEDs() {
266
+ if (!this.wasmModule._getSmartPortActivity) return;
267
+
268
+ const hasActivity = this.wasmModule._getSmartPortActivity(0);
269
+ const isWrite = this.wasmModule._getSmartPortActivityWrite(0);
270
+
271
+ for (let i = 0; i < 2; i++) {
272
+ const device = this.devices[i];
273
+
274
+ if (hasActivity && device.filename) {
275
+ device.activityFrames = 3;
276
+ device.lastWrite = isWrite;
277
+ }
278
+
279
+ if (device.activityFrames > 0) {
280
+ if (device.ledEl) {
281
+ device.ledEl.classList.add("active");
282
+ }
283
+ if (!hasActivity) {
284
+ device.activityFrames--;
285
+ }
286
+ } else {
287
+ if (device.ledEl) {
288
+ device.ledEl.classList.remove("active", "write");
289
+ }
290
+ }
291
+ }
292
+
293
+ if (hasActivity) {
294
+ this.wasmModule._clearSmartPortActivity();
295
+ }
296
+ }
297
+
298
+ async restoreImages() {
299
+ for (let deviceNum = 0; deviceNum < 2; deviceNum++) {
300
+ try {
301
+ const imageData = await loadImageFromStorage(deviceNum);
302
+ if (imageData) {
303
+ this.loadImageFromData(deviceNum, imageData.filename, imageData.data);
304
+ }
305
+ } catch (error) {
306
+ console.error(`Error restoring HD image for device ${deviceNum + 1}:`, error);
307
+ }
308
+ }
309
+ }
310
+
311
+ syncWithEmulatorState() {
312
+ for (let deviceNum = 0; deviceNum < 2; deviceNum++) {
313
+ const hasDisk = this.wasmModule._isSmartPortImageInserted &&
314
+ this.wasmModule._isSmartPortImageInserted(deviceNum);
315
+ if (hasDisk) {
316
+ const filenamePtr = this.wasmModule._getSmartPortImageFilename(deviceNum);
317
+ let filename = "Restored Image";
318
+ if (filenamePtr) {
319
+ filename = this.wasmModule.UTF8ToString(filenamePtr);
320
+ }
321
+ this.devices[deviceNum].filename = filename;
322
+ } else {
323
+ this.devices[deviceNum].filename = null;
324
+ }
325
+ this.updateDeviceUI(deviceNum);
326
+ }
327
+ }
328
+
329
+ // Recent images dropdown
330
+
331
+ async toggleRecentDropdown(deviceNum) {
332
+ const device = this.devices[deviceNum];
333
+ if (!device.recentDropdown) return;
334
+
335
+ if (this.activeDropdown && this.activeDropdown !== device.recentDropdown) {
336
+ this.closeRecentDropdown();
337
+ }
338
+
339
+ if (device.recentDropdown.classList.contains("open")) {
340
+ this.closeRecentDropdown();
341
+ } else {
342
+ await this.populateRecentDropdown(deviceNum);
343
+ device.recentDropdown.classList.add("open");
344
+ this.activeDropdown = device.recentDropdown;
345
+ }
346
+ }
347
+
348
+ closeRecentDropdown() {
349
+ if (this.activeDropdown) {
350
+ this.activeDropdown.classList.remove("open");
351
+ this.activeDropdown = null;
352
+ }
353
+ this.refocusCanvas();
354
+ }
355
+
356
+ async populateRecentDropdown(deviceNum) {
357
+ const device = this.devices[deviceNum];
358
+ if (!device.recentDropdown) return;
359
+
360
+ const recent = await getRecentImages(deviceNum);
361
+ device.recentDropdown.innerHTML = "";
362
+
363
+ if (recent.length === 0) {
364
+ const emptyItem = document.createElement("div");
365
+ emptyItem.className = "recent-item empty";
366
+ emptyItem.textContent = "No recent images";
367
+ device.recentDropdown.appendChild(emptyItem);
368
+ } else {
369
+ for (const img of recent) {
370
+ const item = document.createElement("div");
371
+ item.className = "recent-item";
372
+ item.textContent = img.filename;
373
+ item.title = img.filename;
374
+ item.addEventListener("click", (e) => {
375
+ e.stopPropagation();
376
+ this.loadRecentImageInDevice(deviceNum, img.id);
377
+ });
378
+ device.recentDropdown.appendChild(item);
379
+ }
380
+
381
+ const separator = document.createElement("div");
382
+ separator.className = "recent-separator";
383
+ device.recentDropdown.appendChild(separator);
384
+
385
+ const clearItem = document.createElement("div");
386
+ clearItem.className = "recent-item recent-clear";
387
+ clearItem.textContent = "Clear Recent";
388
+ clearItem.addEventListener("click", async (e) => {
389
+ e.stopPropagation();
390
+ await clearRecentImages(deviceNum);
391
+ this.closeRecentDropdown();
392
+ });
393
+ device.recentDropdown.appendChild(clearItem);
394
+ }
395
+
396
+ // Append library section
397
+ await this._appendLibrarySection(device.recentDropdown, deviceNum);
398
+ }
399
+
400
+ async _getLibraryImageData(entry) {
401
+ try {
402
+ const cached = await libraryCacheDb.get(CACHE_STORE, entry.id);
403
+ if (cached) return new Uint8Array(cached.data);
404
+ } catch (err) {
405
+ console.warn("Library cache read failed:", err);
406
+ }
407
+
408
+ const resp = await fetch(`/disks/${entry.file}`);
409
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
410
+ const arrayBuffer = await resp.arrayBuffer();
411
+ const data = new Uint8Array(arrayBuffer);
412
+
413
+ try {
414
+ await libraryCacheDb.put(CACHE_STORE, {
415
+ id: entry.id,
416
+ file: entry.file,
417
+ data: arrayBuffer,
418
+ });
419
+ } catch (err) {
420
+ console.warn("Library cache write failed:", err);
421
+ }
422
+
423
+ return data;
424
+ }
425
+
426
+ async _appendLibrarySection(dropdown, deviceNum) {
427
+ try {
428
+ const resp = await fetch("/disks/library.json");
429
+ if (!resp.ok) return;
430
+ const library = await resp.json();
431
+ const entries = library.filter((e) => e.type === "hard-drive");
432
+ if (entries.length === 0) return;
433
+
434
+ const separator = document.createElement("div");
435
+ separator.className = "recent-separator";
436
+ dropdown.appendChild(separator);
437
+
438
+ const label = document.createElement("div");
439
+ label.className = "recent-section-label";
440
+ dropdown.appendChild(label);
441
+ label.textContent = "Library";
442
+
443
+ for (const entry of entries) {
444
+ const item = document.createElement("div");
445
+ item.className = "recent-item";
446
+ item.textContent = entry.name;
447
+ item.title = entry.description || entry.name;
448
+ item.addEventListener("click", async (e) => {
449
+ e.stopPropagation();
450
+ this.closeRecentDropdown();
451
+ try {
452
+ const data = await this._getLibraryImageData(entry);
453
+ this.loadImageFromData(deviceNum, entry.file, data);
454
+ await saveImageToStorage(deviceNum, entry.file, data);
455
+ await addToRecentImages(deviceNum, entry.file, data);
456
+ } catch (err) {
457
+ console.error(`Failed to load ${entry.file}:`, err);
458
+ showToast(`Failed to load ${entry.name}`, "error");
459
+ }
460
+ });
461
+ dropdown.appendChild(item);
462
+ }
463
+ } catch (err) {
464
+ console.error("Failed to load library:", err);
465
+ }
466
+ }
467
+
468
+ async loadRecentImageInDevice(deviceNum, imageId) {
469
+ this.closeRecentDropdown();
470
+
471
+ const imageData = await loadRecentImage(imageId);
472
+ if (!imageData) {
473
+ console.error("Failed to load recent HD image");
474
+ return;
475
+ }
476
+
477
+ this.loadImageFromData(deviceNum, imageData.filename, imageData.data);
478
+ await saveImageToStorage(deviceNum, imageData.filename, imageData.data);
479
+ await addToRecentImages(deviceNum, imageData.filename, imageData.data);
480
+ }
481
+ }