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,212 @@
1
+ /*
2
+ * disk-drives-window.js - Disk drives window UI
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ import { BaseWindow } from "../windows/base-window.js";
9
+
10
+ export class DiskDrivesWindow extends BaseWindow {
11
+ constructor() {
12
+ super({
13
+ id: "disk-drives",
14
+ title: "Disk Drives",
15
+ minWidth: 600,
16
+ minHeight: 100,
17
+ maxWidth: 600,
18
+ defaultWidth: 600,
19
+ defaultHeight: 310,
20
+ resizeDirections: [],
21
+ });
22
+
23
+ this._detailsOpen = false;
24
+ this._graphicsHidden = false;
25
+ }
26
+
27
+ _driveHTML(num) {
28
+ const prefix = `dd-d${num}`;
29
+ return `
30
+ <div class="disk-drive" id="disk${num}">
31
+ <div class="drive-image-container">
32
+ <canvas class="disk-surface" width="560" height="480"></canvas>
33
+ <span class="drive-label">D${num}</span>
34
+ </div>
35
+ <div class="drive-info">
36
+ <span class="disk-name">No Disk</span>
37
+ <span class="disk-track" title="Current Track">T--</span>
38
+ </div>
39
+ <div class="drive-controls">
40
+ <input type="file" id="disk${num}-input" accept=".dsk,.do,.po,.woz,.nib" hidden />
41
+ <button class="disk-insert" title="Insert Disk from File">Insert</button>
42
+ <div class="recent-container">
43
+ <button class="disk-recent" title="Recent Disks">Recent</button>
44
+ <div class="recent-dropdown"></div>
45
+ </div>
46
+ <button class="disk-blank" title="Insert Blank Disk">Blank</button>
47
+ <button class="disk-eject" disabled title="Eject Disk">Eject</button>
48
+ <button class="disk-browse" disabled title="Browse Files">Browse</button>
49
+ </div>
50
+ <div class="drive-detail-panel">
51
+ <div class="drive-detail-grid">
52
+ <span class="dd-label">QTrack</span><span class="dd-val" id="${prefix}-qt">0</span>
53
+ <span class="dd-label">Phase</span><span class="dd-val" id="${prefix}-phase">0</span>
54
+ <span class="dd-label">Nibble</span><span class="dd-val" id="${prefix}-nibble">0</span>
55
+ <span class="dd-label">Motor</span><span class="dd-val" id="${prefix}-motor">OFF</span>
56
+ <span class="dd-label">Mode</span><span class="dd-val" id="${prefix}-mode">Read</span>
57
+ <span class="dd-label">Byte</span><span class="dd-val" id="${prefix}-byte">00</span>
58
+ </div>
59
+ </div>
60
+ </div>`;
61
+ }
62
+
63
+ renderContent() {
64
+ return `
65
+ <div class="disk-drives-row">
66
+ ${this._driveHTML(1)}
67
+ ${this._driveHTML(2)}
68
+ </div>
69
+ `;
70
+ }
71
+
72
+ onContentRendered() {
73
+ const closeBtn = this.headerElement.querySelector(
74
+ `.${this.cssClasses.close}`,
75
+ );
76
+
77
+ this._graphicsBtn = document.createElement("button");
78
+ this._graphicsBtn.className = "drive-graphics-btn active";
79
+ this._graphicsBtn.title = "Toggle disk graphics";
80
+ this._graphicsBtn.innerHTML = `
81
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
82
+ <path d="M8 3C4.5 3 1.6 5.3.6 8c1 2.7 3.9 5 7.4 5s6.4-2.3 7.4-5c-1-2.7-3.9-5-7.4-5zm0 8.5A3.5 3.5 0 1 1 8 4.5a3.5 3.5 0 0 1 0 7zm0-5.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/>
83
+ </svg>
84
+ `;
85
+ this.headerElement.insertBefore(this._graphicsBtn, closeBtn);
86
+ this._graphicsBtn.addEventListener("mousedown", (e) => e.stopPropagation());
87
+ this._graphicsBtn.addEventListener("click", () => this._toggleGraphics());
88
+
89
+ this._detailBtn = document.createElement("button");
90
+ this._detailBtn.className = "drive-detail-btn";
91
+ this._detailBtn.title = "Toggle details";
92
+ this._detailBtn.innerHTML = `
93
+ <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
94
+ <path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm0 2.5a1 1 0 1 1 0 2 1 1 0 0 1 0-2zM6.5 7h1.75v4.5H10v1H6v-1h1.25V8H6.5V7z"/>
95
+ </svg>
96
+ `;
97
+ this.headerElement.insertBefore(this._detailBtn, closeBtn);
98
+ this._detailBtn.addEventListener("mousedown", (e) => e.stopPropagation());
99
+ this._detailBtn.addEventListener("click", () => this._toggleDetails());
100
+
101
+ // Set initial size from content so defaultHeight doesn't need to be kept in sync
102
+ this._fitToContent();
103
+ }
104
+
105
+ show() {
106
+ super.show();
107
+ this._fitToContent();
108
+ }
109
+
110
+ _toggleGraphics() {
111
+ this._graphicsHidden = !this._graphicsHidden;
112
+ this.contentElement.classList.toggle("hide-graphics", this._graphicsHidden);
113
+ if (this._graphicsBtn) {
114
+ this._graphicsBtn.classList.toggle("active", !this._graphicsHidden);
115
+ }
116
+ this._fitToContent();
117
+ if (this.onStateChange) this.onStateChange();
118
+ }
119
+
120
+ _toggleDetails() {
121
+ this._detailsOpen = !this._detailsOpen;
122
+ this.contentElement.classList.toggle("show-details", this._detailsOpen);
123
+ if (this._detailBtn) {
124
+ this._detailBtn.classList.toggle("active", this._detailsOpen);
125
+ }
126
+ this._fitToContent();
127
+ if (this.onStateChange) this.onStateChange();
128
+ }
129
+
130
+ _fitToContent() {
131
+ if (!this.element) return;
132
+ // Temporarily set auto height to measure natural size
133
+ this.element.style.height = "auto";
134
+ const newHeight = this.element.offsetHeight;
135
+ this.element.style.height = `${newHeight}px`;
136
+ this.currentHeight = newHeight;
137
+ this.minHeight = newHeight;
138
+ this.maxHeight = newHeight;
139
+ // Recalculate edge distances before constraining so the window
140
+ // isn't repositioned based on stale distances from a different height
141
+ this.updateEdgeDistances();
142
+ this.constrainToViewport();
143
+ }
144
+
145
+ /**
146
+ * Update technical details from WASM state (called by WindowManager.updateAll)
147
+ */
148
+ update(wasmModule) {
149
+ if (!this._detailsOpen) return;
150
+
151
+ const selectedDrive = wasmModule._getSelectedDrive();
152
+ const lastByte = wasmModule._getLastDiskByte();
153
+
154
+ for (let d = 0; d < 2; d++) {
155
+ const prefix = `dd-d${d + 1}`;
156
+ const el = (id) => this.contentElement.querySelector(`#${prefix}-${id}`);
157
+
158
+ const qt = el("qt");
159
+ if (qt) qt.textContent = wasmModule._getDiskHeadPosition(d);
160
+
161
+ const phase = el("phase");
162
+ if (phase) phase.textContent = wasmModule._getDiskPhase(d);
163
+
164
+ const nibble = el("nibble");
165
+ if (nibble) nibble.textContent = wasmModule._getCurrentNibblePosition(d);
166
+
167
+ const motor = el("motor");
168
+ if (motor) {
169
+ const on = wasmModule._getDiskMotorOn(d);
170
+ motor.textContent = on ? "ON" : "OFF";
171
+ motor.classList.toggle("on", on);
172
+ }
173
+
174
+ const mode = el("mode");
175
+ if (mode) {
176
+ const w = wasmModule._getDiskWriteMode(d);
177
+ mode.textContent = w ? "Write" : "Read";
178
+ mode.classList.toggle("write", w);
179
+ }
180
+
181
+ const byte = el("byte");
182
+ if (byte) {
183
+ byte.textContent =
184
+ d === selectedDrive
185
+ ? lastByte.toString(16).toUpperCase().padStart(2, "0")
186
+ : "--";
187
+ }
188
+ }
189
+ }
190
+
191
+ getState() {
192
+ const base = super.getState();
193
+ base.graphicsHidden = this._graphicsHidden;
194
+ base.detailsOpen = this._detailsOpen;
195
+ return base;
196
+ }
197
+
198
+ restoreState(state) {
199
+ if (state.graphicsHidden) {
200
+ this._graphicsHidden = true;
201
+ this.contentElement.classList.add("hide-graphics");
202
+ if (this._graphicsBtn) this._graphicsBtn.classList.remove("active");
203
+ }
204
+ if (state.detailsOpen) {
205
+ this._detailsOpen = true;
206
+ this.contentElement.classList.add("show-details");
207
+ if (this._detailBtn) this._detailBtn.classList.add("active");
208
+ }
209
+
210
+ super.restoreState(state);
211
+ }
212
+ }
@@ -0,0 +1,284 @@
1
+ /*
2
+ * disk-operations.js - Disk image loading and management operations
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ import {
9
+ saveDiskToStorage,
10
+ clearDiskFromStorage,
11
+ addToRecentDisks,
12
+ } from "./disk-persistence.js";
13
+
14
+ /**
15
+ * Helper to insert a disk into WASM with proper memory management
16
+ * @param {Object} wasmModule - The WASM module
17
+ * @param {number} driveNum - Drive number (0 or 1)
18
+ * @param {Uint8Array} data - The disk image data
19
+ * @param {string} filename - The disk filename
20
+ * @returns {boolean} True if successful
21
+ */
22
+ function insertDiskToWasm(wasmModule, driveNum, data, filename) {
23
+ // Allocate memory for disk data
24
+ const dataPtr = wasmModule._malloc(data.length);
25
+ wasmModule.HEAPU8.set(data, dataPtr);
26
+
27
+ // Allocate string for filename
28
+ const filenamePtr = wasmModule._malloc(filename.length + 1);
29
+ wasmModule.stringToUTF8(filename, filenamePtr, filename.length + 1);
30
+
31
+ // Insert disk
32
+ const success = wasmModule._insertDisk(driveNum, dataPtr, data.length, filenamePtr);
33
+
34
+ // Free memory
35
+ wasmModule._free(dataPtr);
36
+ wasmModule._free(filenamePtr);
37
+
38
+ return success;
39
+ }
40
+
41
+ /**
42
+ * Load a disk image from a file into a drive
43
+ * @param {Object} options
44
+ * @param {Object} options.wasmModule - The WASM module
45
+ * @param {Object} options.drive - The drive state object
46
+ * @param {number} options.driveNum - Drive number (0 or 1)
47
+ * @param {File} options.file - The file to load
48
+ * @param {Function} [options.onSuccess] - Callback on successful load
49
+ * @param {Function} [options.onError] - Callback on error
50
+ */
51
+ export async function loadDisk({ wasmModule, drive, driveNum, file, onSuccess, onError }) {
52
+ try {
53
+ const arrayBuffer = await file.arrayBuffer();
54
+ const data = new Uint8Array(arrayBuffer);
55
+
56
+ const success = insertDiskToWasm(wasmModule, driveNum, data, file.name);
57
+
58
+ if (success) {
59
+ drive.filename = file.name;
60
+ if (drive.ejectBtn) drive.ejectBtn.disabled = false;
61
+ if (drive.browseBtn) drive.browseBtn.disabled = false;
62
+ console.log(`Inserted disk in drive ${driveNum + 1}: ${file.name}`);
63
+
64
+ // Save to IndexedDB for persistence across sessions
65
+ await saveDiskToStorage(driveNum, file.name, data);
66
+
67
+ // Add to recent disks list for this drive
68
+ await addToRecentDisks(driveNum, file.name, data);
69
+
70
+ if (onSuccess) onSuccess(file.name);
71
+ } else {
72
+ const msg = `Failed to load disk image: ${file.name}`;
73
+ console.error(msg);
74
+ if (onError) onError(msg);
75
+ }
76
+ } catch (error) {
77
+ console.error("Error loading disk:", error);
78
+ if (onError) onError("Error loading disk: " + error.message);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Load a disk image from raw data (used for restoring from persistence)
84
+ * @param {Object} options
85
+ * @param {Object} options.wasmModule - The WASM module
86
+ * @param {Object} options.drive - The drive state object
87
+ * @param {number} options.driveNum - Drive number (0 or 1)
88
+ * @param {string} options.filename - The disk filename
89
+ * @param {Uint8Array} options.data - The disk image data
90
+ * @param {Function} [options.onSuccess] - Callback on successful load
91
+ * @param {Function} [options.onError] - Callback on error
92
+ */
93
+ export function loadDiskFromData({ wasmModule, drive, driveNum, filename, data, onSuccess, onError }) {
94
+ try {
95
+ const success = insertDiskToWasm(wasmModule, driveNum, data, filename);
96
+
97
+ if (success) {
98
+ drive.filename = filename;
99
+ if (drive.ejectBtn) drive.ejectBtn.disabled = false;
100
+ if (drive.browseBtn) drive.browseBtn.disabled = false;
101
+ console.log(`Restored disk in drive ${driveNum + 1}: ${filename}`);
102
+ if (onSuccess) onSuccess(filename);
103
+ } else {
104
+ const msg = `Failed to restore disk image: ${filename}`;
105
+ console.error(msg);
106
+ if (onError) onError(msg);
107
+ }
108
+ } catch (error) {
109
+ console.error("Error restoring disk:", error);
110
+ if (onError) onError("Error restoring disk: " + error.message);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Insert a blank WOZ disk into a drive
116
+ * @param {Object} options
117
+ * @param {Object} options.wasmModule - The WASM module
118
+ * @param {Object} options.drive - The drive state object
119
+ * @param {number} options.driveNum - Drive number (0 or 1)
120
+ * @param {Function} [options.onSuccess] - Callback on successful insert
121
+ * @param {Function} [options.onError] - Callback on error
122
+ */
123
+ export function insertBlankDisk({ wasmModule, drive, driveNum, onSuccess, onError }) {
124
+ const filename = "Blank Disk.woz";
125
+
126
+ // Use the WASM function to create and insert a blank disk
127
+ const success = wasmModule._insertBlankDisk(driveNum);
128
+
129
+ if (success) {
130
+ drive.filename = filename;
131
+ if (drive.ejectBtn) drive.ejectBtn.disabled = false;
132
+ console.log(`Inserted blank disk in drive ${driveNum + 1}`);
133
+ if (onSuccess) onSuccess(filename);
134
+ } else {
135
+ const msg = "Failed to insert blank disk";
136
+ console.error(msg);
137
+ if (onError) onError(msg);
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Perform the actual disk ejection
143
+ * @param {Object} options
144
+ * @param {Object} options.wasmModule - The WASM module
145
+ * @param {Object} options.drive - The drive state object
146
+ * @param {number} options.driveNum - Drive number (0 or 1)
147
+ * @param {Function} [options.onEject] - Callback after ejection
148
+ */
149
+ export function performEject({ wasmModule, drive, driveNum, onEject }) {
150
+ wasmModule._ejectDisk(driveNum);
151
+
152
+ drive.filename = null;
153
+ if (drive.ejectBtn) drive.ejectBtn.disabled = true;
154
+ if (drive.browseBtn) drive.browseBtn.disabled = true;
155
+ if (drive.input) drive.input.value = "";
156
+
157
+ // Clear from IndexedDB
158
+ clearDiskFromStorage(driveNum);
159
+
160
+ console.log(`Ejected disk from drive ${driveNum + 1}`);
161
+ if (onEject) onEject();
162
+ }
163
+
164
+ /**
165
+ * Eject a disk, prompting to save if modified
166
+ * @param {Object} options
167
+ * @param {Object} options.wasmModule - The WASM module
168
+ * @param {Object} options.drive - The drive state object
169
+ * @param {number} options.driveNum - Drive number (0 or 1)
170
+ * @param {Function} [options.onEject] - Callback after ejection
171
+ */
172
+ export async function ejectDisk({ wasmModule, drive, driveNum, onEject }) {
173
+ // Check if disk is modified
174
+ const hasModifiedCheck = typeof wasmModule._isDiskModified === "function";
175
+ const isModified = hasModifiedCheck && wasmModule._isDiskModified(driveNum);
176
+
177
+ if (isModified) {
178
+ // Generate suggested filename
179
+ let suggestedName = drive.filename || `disk${driveNum + 1}.woz`;
180
+ // Ensure WOZ extension for blank disks
181
+ if (suggestedName === "Blank Disk.woz" || !suggestedName.includes(".")) {
182
+ suggestedName = suggestedName.replace(/\.[^.]*$/, "") + ".woz";
183
+ }
184
+
185
+ // Go directly to OS save picker
186
+ await saveDiskWithPicker(wasmModule, driveNum, suggestedName);
187
+ }
188
+
189
+ // Always eject after save attempt
190
+ performEject({ wasmModule, drive, driveNum, onEject });
191
+ }
192
+
193
+ /**
194
+ * Save disk data using the File System Access API
195
+ * @param {Object} wasmModule - The WASM module
196
+ * @param {number} driveNum - Drive number (0 or 1)
197
+ * @param {string} suggestedName - Suggested filename
198
+ * @returns {Promise<boolean>} True if saved successfully
199
+ */
200
+ export async function saveDiskWithPicker(wasmModule, driveNum, suggestedName) {
201
+ const sizePtr = wasmModule._malloc(4);
202
+ if (!sizePtr) {
203
+ console.error("saveDiskWithPicker: failed to allocate size pointer");
204
+ return false;
205
+ }
206
+
207
+ const dataPtr = wasmModule._getDiskData(driveNum, sizePtr);
208
+
209
+ if (!dataPtr) {
210
+ console.error("saveDiskWithPicker: _getDiskData returned null");
211
+ wasmModule._free(sizePtr);
212
+ return false;
213
+ }
214
+
215
+ // Read size from WASM memory (little-endian 32-bit value)
216
+ const heap = wasmModule.HEAPU8;
217
+ const size =
218
+ heap[sizePtr] |
219
+ (heap[sizePtr + 1] << 8) |
220
+ (heap[sizePtr + 2] << 16) |
221
+ (heap[sizePtr + 3] << 24);
222
+
223
+ if (size <= 0 || size > 10000000) {
224
+ console.error(`saveDiskWithPicker: invalid size ${size}`);
225
+ wasmModule._free(sizePtr);
226
+ return false;
227
+ }
228
+
229
+ const data = new Uint8Array(wasmModule.HEAPU8.buffer, dataPtr, size);
230
+
231
+ // Create a copy of the data since the WASM buffer may become invalid
232
+ const dataCopy = new Uint8Array(data);
233
+
234
+ wasmModule._free(sizePtr);
235
+
236
+ // Try to use File System Access API (modern browsers)
237
+ if ("showSaveFilePicker" in window) {
238
+ try {
239
+ const handle = await window.showSaveFilePicker({
240
+ suggestedName: suggestedName,
241
+ types: [
242
+ {
243
+ description: "Disk Images",
244
+ accept: {
245
+ "application/octet-stream": [".dsk", ".do", ".po", ".woz", ".nib"],
246
+ },
247
+ },
248
+ ],
249
+ });
250
+
251
+ const writable = await handle.createWritable();
252
+ await writable.write(dataCopy);
253
+ await writable.close();
254
+
255
+ console.log(`Saved disk from drive ${driveNum + 1} to: ${handle.name}`);
256
+ return true;
257
+ } catch (err) {
258
+ // User cancelled the picker or other error
259
+ if (err.name !== "AbortError") {
260
+ console.error("Error saving disk:", err);
261
+ }
262
+ return false;
263
+ }
264
+ } else {
265
+ // Fallback for browsers without File System Access API
266
+ downloadFile(dataCopy, suggestedName);
267
+ return true;
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Download file using traditional blob/anchor approach
273
+ * @param {Uint8Array} data - The file data
274
+ * @param {string} filename - The filename
275
+ */
276
+ export function downloadFile(data, filename) {
277
+ const blob = new Blob([data], { type: "application/octet-stream" });
278
+ const url = URL.createObjectURL(blob);
279
+ const a = document.createElement("a");
280
+ a.href = url;
281
+ a.download = filename;
282
+ a.click();
283
+ URL.revokeObjectURL(url);
284
+ }