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,22 @@
1
+ /*
2
+ * index.js - Debug subsystem module exports
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ export { BaseWindow } from "../windows/base-window.js";
9
+ export { WindowManager } from "../windows/window-manager.js";
10
+ export { CPUDebuggerWindow } from "./cpu-debugger-window.js";
11
+ export { SoftSwitchWindow } from "./soft-switch-window.js";
12
+ export { MemoryBrowserWindow } from "./memory-browser-window.js";
13
+ export { MemoryHeatMapWindow } from "./memory-heat-map-window.js";
14
+ export { MemoryMapWindow } from "./memory-map-window.js";
15
+ export { StackViewerWindow } from "./stack-viewer-window.js";
16
+ export { ZeroPageWatchWindow } from "./zero-page-watch-window.js";
17
+ export { MockingboardWindow } from "./mockingboard-window.js";
18
+ export { MouseCardWindow } from "./mouse-card-window.js";
19
+ export { BasicProgramWindow } from "./basic-program-window.js";
20
+ export { RuleBuilderWindow } from "./rule-builder-window.js";
21
+ export { AssemblerEditorWindow } from "./assembler-editor-window.js";
22
+ export { TracePanelWindow } from "./trace-panel.js";
@@ -0,0 +1,238 @@
1
+ /*
2
+ * label-manager.js - User-defined label and imported symbol management
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ /**
9
+ * LabelManager - Manages user-defined labels, imported symbols, and inline comments.
10
+ * Provides a unified lookup layer on top of the built-in symbols.js data.
11
+ */
12
+ export class LabelManager {
13
+ constructor() {
14
+ this.userLabels = new Map(); // address -> {name, comment}
15
+ this.importedLabels = new Map(); // address -> {name, source}
16
+ this.listeners = [];
17
+
18
+ this.load();
19
+ }
20
+
21
+ static STORAGE_KEY = "a2e-user-labels";
22
+
23
+ onChange(fn) {
24
+ this.listeners.push(fn);
25
+ }
26
+
27
+ _notify() {
28
+ for (const fn of this.listeners) fn();
29
+ }
30
+
31
+ /**
32
+ * Add or update a user label
33
+ */
34
+ addLabel(address, name, comment = "") {
35
+ this.userLabels.set(address, { name, comment });
36
+ this.save();
37
+ this._notify();
38
+ }
39
+
40
+ /**
41
+ * Remove a user label
42
+ */
43
+ removeLabel(address) {
44
+ if (this.userLabels.delete(address)) {
45
+ this.save();
46
+ this._notify();
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Set inline comment for an address
52
+ */
53
+ setComment(address, comment) {
54
+ const existing = this.userLabels.get(address);
55
+ if (existing) {
56
+ existing.comment = comment;
57
+ } else {
58
+ this.userLabels.set(address, { name: "", comment });
59
+ }
60
+ this.save();
61
+ this._notify();
62
+ }
63
+
64
+ /**
65
+ * Get label info for an address. User labels take priority over imports.
66
+ * @returns {object|null} {name, comment, source} or null
67
+ */
68
+ getLabel(address) {
69
+ const user = this.userLabels.get(address);
70
+ if (user && user.name) {
71
+ return { name: user.name, comment: user.comment || "", source: "user" };
72
+ }
73
+
74
+ const imported = this.importedLabels.get(address);
75
+ if (imported) {
76
+ return { name: imported.name, comment: "", source: imported.source };
77
+ }
78
+
79
+ // Check for comment-only user entry
80
+ if (user && user.comment) {
81
+ return { name: "", comment: user.comment, source: "user" };
82
+ }
83
+
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * Resolve an address by label name (case-insensitive)
89
+ * @returns {number|null}
90
+ */
91
+ resolveByName(name) {
92
+ const upper = name.toUpperCase();
93
+
94
+ for (const [addr, info] of this.userLabels) {
95
+ if (info.name && info.name.toUpperCase() === upper) return addr;
96
+ }
97
+
98
+ for (const [addr, info] of this.importedLabels) {
99
+ if (info.name.toUpperCase() === upper) return addr;
100
+ }
101
+
102
+ return null;
103
+ }
104
+
105
+ /**
106
+ * Import a symbol file. Supports ca65 .dbg, Merlin, ACME, and generic formats.
107
+ * @param {string} text File content
108
+ * @param {string} source Descriptive source name (e.g. filename)
109
+ */
110
+ importSymbolFile(text, source = "import") {
111
+ const lines = text.split(/\r?\n/);
112
+ let count = 0;
113
+
114
+ for (const line of lines) {
115
+ const trimmed = line.trim();
116
+ if (!trimmed || trimmed.startsWith(";") || trimmed.startsWith("*")) continue;
117
+
118
+ let match;
119
+
120
+ // ca65 .dbg format: sym name="LABEL",addrsize=absolute,scope=0,def=123,ref=...,val=0xXXXX,type=lab
121
+ match = trimmed.match(/^sym\s.*name="([^"]+)".*val=(0x[0-9A-Fa-f]+)/);
122
+ if (match) {
123
+ const name = match[1];
124
+ const addr = parseInt(match[2], 16);
125
+ if (addr >= 0 && addr <= 0xffff) {
126
+ this.importedLabels.set(addr, { name, source });
127
+ count++;
128
+ }
129
+ continue;
130
+ }
131
+
132
+ // Merlin format: LABEL EQU $XXXX
133
+ match = trimmed.match(/^(\w+)\s+EQU\s+\$([0-9A-Fa-f]{1,4})/i);
134
+ if (match) {
135
+ const name = match[1];
136
+ const addr = parseInt(match[2], 16);
137
+ if (addr >= 0 && addr <= 0xffff) {
138
+ this.importedLabels.set(addr, { name, source });
139
+ count++;
140
+ }
141
+ continue;
142
+ }
143
+
144
+ // ACME format: LABEL = $XXXX or !addr LABEL = $XXXX
145
+ match = trimmed.match(/^(?:!addr\s+)?(\w+)\s*=\s*\$([0-9A-Fa-f]{1,4})/i);
146
+ if (match) {
147
+ const name = match[1];
148
+ const addr = parseInt(match[2], 16);
149
+ if (addr >= 0 && addr <= 0xffff) {
150
+ this.importedLabels.set(addr, { name, source });
151
+ count++;
152
+ }
153
+ continue;
154
+ }
155
+
156
+ // Generic: $XXXX LABEL or XXXX LABEL
157
+ match = trimmed.match(/^\$?([0-9A-Fa-f]{2,4})\s+(\w+)/);
158
+ if (match) {
159
+ const addr = parseInt(match[1], 16);
160
+ const name = match[2];
161
+ if (addr >= 0 && addr <= 0xffff && !/^[0-9A-Fa-f]+$/.test(name)) {
162
+ this.importedLabels.set(addr, { name, source });
163
+ count++;
164
+ }
165
+ continue;
166
+ }
167
+ }
168
+
169
+ this._notify();
170
+ return count;
171
+ }
172
+
173
+ /**
174
+ * Clear imported labels from a specific source
175
+ */
176
+ clearImportedBySource(source) {
177
+ for (const [addr, info] of this.importedLabels) {
178
+ if (info.source === source) {
179
+ this.importedLabels.delete(addr);
180
+ }
181
+ }
182
+ this._notify();
183
+ }
184
+
185
+ /**
186
+ * Clear all imported labels
187
+ */
188
+ clearImported() {
189
+ this.importedLabels.clear();
190
+ this._notify();
191
+ }
192
+
193
+ /**
194
+ * Export user labels as text (generic format)
195
+ */
196
+ exportLabels() {
197
+ const lines = [];
198
+ for (const [addr, info] of this.userLabels) {
199
+ if (!info.name) continue;
200
+ const hex = addr.toString(16).toUpperCase().padStart(4, "0");
201
+ let line = `$${hex} ${info.name}`;
202
+ if (info.comment) line += ` ; ${info.comment}`;
203
+ lines.push(line);
204
+ }
205
+ return lines.join("\n");
206
+ }
207
+
208
+ // ---- Persistence ----
209
+
210
+ save() {
211
+ try {
212
+ const data = [];
213
+ for (const [addr, info] of this.userLabels) {
214
+ data.push({ address: addr, name: info.name, comment: info.comment });
215
+ }
216
+ localStorage.setItem(LabelManager.STORAGE_KEY, JSON.stringify(data));
217
+ } catch (e) {
218
+ console.warn("Failed to save labels:", e);
219
+ }
220
+ }
221
+
222
+ load() {
223
+ try {
224
+ const saved = localStorage.getItem(LabelManager.STORAGE_KEY);
225
+ if (saved) {
226
+ const data = JSON.parse(saved);
227
+ for (const entry of data) {
228
+ this.userLabels.set(entry.address, {
229
+ name: entry.name || "",
230
+ comment: entry.comment || "",
231
+ });
232
+ }
233
+ }
234
+ } catch (e) {
235
+ console.warn("Failed to load labels:", e);
236
+ }
237
+ }
238
+ }
@@ -0,0 +1,416 @@
1
+ /*
2
+ * memory-browser-window.js - Scrollable hex/ASCII memory browser for 64KB address space
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ import { BaseWindow } from "../windows/base-window.js";
9
+
10
+ // Memory region definitions for Apple II
11
+ const MEMORY_REGIONS = [
12
+ { name: "Zero Page", start: 0x0000, end: 0x00ff },
13
+ { name: "Stack", start: 0x0100, end: 0x01ff },
14
+ { name: "Input Buffer", start: 0x0200, end: 0x02ff },
15
+ { name: "Vectors/Data", start: 0x0300, end: 0x03ff },
16
+ { name: "Text Page 1", start: 0x0400, end: 0x07ff },
17
+ { name: "Text Page 2", start: 0x0800, end: 0x0bff },
18
+ { name: "Free RAM", start: 0x0c00, end: 0x1fff },
19
+ { name: "HiRes Page 1", start: 0x2000, end: 0x3fff },
20
+ { name: "HiRes Page 2", start: 0x4000, end: 0x5fff },
21
+ { name: "Free RAM", start: 0x6000, end: 0x95ff },
22
+ { name: "DOS 3.3", start: 0x9600, end: 0xbfff },
23
+ { name: "I/O Space", start: 0xc000, end: 0xc0ff },
24
+ { name: "Slot ROMs", start: 0xc100, end: 0xcfff },
25
+ { name: "ROM/LC RAM", start: 0xd000, end: 0xffff },
26
+ ];
27
+
28
+ // Quick jump buttons
29
+ const QUICK_JUMPS = [
30
+ { label: "ZP", addr: 0x0000, title: "Zero Page ($0000)" },
31
+ { label: "Stack", addr: 0x0100, title: "Stack ($0100)" },
32
+ { label: "Text1", addr: 0x0400, title: "Text Page 1 ($0400)" },
33
+ { label: "Text2", addr: 0x0800, title: "Text Page 2 ($0800)" },
34
+ { label: "HiRes1", addr: 0x2000, title: "HiRes Page 1 ($2000)" },
35
+ { label: "HiRes2", addr: 0x4000, title: "HiRes Page 2 ($4000)" },
36
+ { label: "I/O", addr: 0xc000, title: "I/O Space ($C000)" },
37
+ { label: "ROM", addr: 0xd000, title: "ROM / Language Card ($D000)" },
38
+ { label: "Vectors", addr: 0xfff0, title: "Vectors ($FFF0)" },
39
+ { label: "DOS", addr: 0x9600, title: "DOS 3.3 ($9600)" },
40
+ ];
41
+
42
+ export class MemoryBrowserWindow extends BaseWindow {
43
+ constructor(wasmModule) {
44
+ super({
45
+ id: "memory-browser",
46
+ title: "Memory Browser",
47
+ defaultWidth: 600,
48
+ defaultHeight: 520,
49
+ minWidth: 600,
50
+ minHeight: 300,
51
+ maxWidth: 600,
52
+ });
53
+ this.wasmModule = wasmModule;
54
+ this.baseAddress = 0x0000;
55
+ this.bytesPerRow = 16;
56
+ this.visibleRows = 28;
57
+ this.previousMemory = new Uint8Array(65536);
58
+ this.changedBytes = new Set();
59
+ this.changeTimestamps = new Map();
60
+ }
61
+
62
+ renderContent() {
63
+ return `
64
+ <div class="mem-browser-toolbar">
65
+ <div class="mem-browser-jumps">
66
+ ${QUICK_JUMPS.map(
67
+ (j) =>
68
+ `<button class="mem-jump-btn" data-addr="${j.addr}" title="${j.title}">${j.label}</button>`,
69
+ ).join("")}
70
+ </div>
71
+ <div class="mem-browser-nav">
72
+ <input type="text" class="mem-addr-input" placeholder="Address" maxlength="4" />
73
+ <button class="mem-go-btn" title="Go to address">Go</button>
74
+ <input type="text" class="mem-search-input" placeholder="Search hex" maxlength="16" />
75
+ <button class="mem-search-btn" title="Search for bytes">Find</button>
76
+ <button class="mem-refresh-btn" title="Refresh memory view">Refresh</button>
77
+ </div>
78
+ </div>
79
+ <div class="mem-browser-region">Region: <span class="mem-region-name">Zero Page</span></div>
80
+ <div class="mem-browser-header">
81
+ <span class="mem-addr-col">Addr</span>
82
+ <span class="mem-hdr-separator">:</span>
83
+ <span class="mem-hdr-bytes">${Array.from({ length: 16 }, (_, i) => i.toString(16).toUpperCase().padStart(2, "0")).join(" ")} </span>
84
+ <span class="mem-ascii-sep">&nbsp;</span>
85
+ <span class="mem-ascii-hdr">ASCII</span>
86
+ <span class="mem-ascii-sep">&nbsp;</span>
87
+ </div>
88
+ <div class="mem-browser-scroll-container">
89
+ <div class="mem-browser-content"></div>
90
+ <div class="mem-browser-scrollbar">
91
+ <div class="mem-scrollbar-track">
92
+ <div class="mem-scrollbar-thumb"></div>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ `;
97
+ }
98
+
99
+ onContentRendered() {
100
+ this.contentDiv = this.contentElement.querySelector(".mem-browser-content");
101
+ this.scrollContainer = this.contentElement.querySelector(
102
+ ".mem-browser-scroll-container",
103
+ );
104
+ this.scrollbarThumb = this.contentElement.querySelector(
105
+ ".mem-scrollbar-thumb",
106
+ );
107
+ this.regionNameSpan = this.contentElement.querySelector(".mem-region-name");
108
+ this.addrInput = this.contentElement.querySelector(".mem-addr-input");
109
+ this.searchInput = this.contentElement.querySelector(".mem-search-input");
110
+
111
+ this.setupScrolling();
112
+ this.setupContentEventListeners();
113
+ }
114
+
115
+ setupScrolling() {
116
+ // Mouse wheel scrolling - explicitly non-passive since we need preventDefault()
117
+ this.scrollContainer.addEventListener(
118
+ "wheel",
119
+ (e) => {
120
+ e.preventDefault();
121
+ const delta = Math.sign(e.deltaY) * this.bytesPerRow * 4;
122
+ this.scrollToAddress(this.baseAddress + delta);
123
+ },
124
+ { passive: false },
125
+ );
126
+
127
+ // Scrollbar dragging
128
+ let isDragging = false;
129
+ let dragStartY = 0;
130
+ let dragStartAddr = 0;
131
+
132
+ this.scrollbarThumb.addEventListener("mousedown", (e) => {
133
+ isDragging = true;
134
+ dragStartY = e.clientY;
135
+ dragStartAddr = this.baseAddress;
136
+ e.preventDefault();
137
+ });
138
+
139
+ document.addEventListener("mousemove", (e) => {
140
+ if (!isDragging) return;
141
+ const trackHeight =
142
+ this.scrollbarThumb.parentElement.offsetHeight -
143
+ this.scrollbarThumb.offsetHeight;
144
+ const deltaY = e.clientY - dragStartY;
145
+ const maxAddress = 0x10000 - this.bytesPerRow * this.visibleRows;
146
+ const newAddr = Math.round(
147
+ dragStartAddr + (deltaY / trackHeight) * maxAddress,
148
+ );
149
+ this.scrollToAddress(newAddr);
150
+ });
151
+
152
+ document.addEventListener("mouseup", () => {
153
+ isDragging = false;
154
+ });
155
+
156
+ // Track click
157
+ this.scrollbarThumb.parentElement.addEventListener("click", (e) => {
158
+ if (e.target === this.scrollbarThumb) return;
159
+ const rect = this.scrollbarThumb.parentElement.getBoundingClientRect();
160
+ const clickY = e.clientY - rect.top;
161
+ const trackHeight = rect.height;
162
+ const maxAddress = 0x10000 - this.bytesPerRow * this.visibleRows;
163
+ const newAddr = Math.round((clickY / trackHeight) * maxAddress);
164
+ this.scrollToAddress(newAddr);
165
+ });
166
+ }
167
+
168
+ setupContentEventListeners() {
169
+ // Quick jump buttons
170
+ this.contentElement.querySelectorAll(".mem-jump-btn").forEach((btn) => {
171
+ btn.addEventListener("click", () => {
172
+ const addr = parseInt(btn.dataset.addr, 10);
173
+ this.scrollToAddress(addr);
174
+ });
175
+ });
176
+
177
+ // Go button
178
+ this.contentElement
179
+ .querySelector(".mem-go-btn")
180
+ .addEventListener("click", () => {
181
+ this.goToAddress();
182
+ });
183
+
184
+ // Address input enter key
185
+ this.addrInput.addEventListener("keydown", (e) => {
186
+ if (e.key === "Enter") {
187
+ this.goToAddress();
188
+ }
189
+ });
190
+
191
+ // Search button
192
+ this.contentElement
193
+ .querySelector(".mem-search-btn")
194
+ .addEventListener("click", () => {
195
+ this.searchBytes();
196
+ });
197
+
198
+ // Search input enter key
199
+ this.searchInput.addEventListener("keydown", (e) => {
200
+ if (e.key === "Enter") {
201
+ this.searchBytes();
202
+ }
203
+ });
204
+
205
+ // Refresh button
206
+ this.contentElement
207
+ .querySelector(".mem-refresh-btn")
208
+ .addEventListener("click", () => {
209
+ this.forceRefresh = true;
210
+ });
211
+
212
+ // Byte click to edit
213
+ this.contentDiv.addEventListener("click", (e) => {
214
+ const byteSpan = e.target.closest(".mem-byte");
215
+ if (byteSpan && byteSpan.dataset.addr) {
216
+ this.startEditByte(parseInt(byteSpan.dataset.addr, 10));
217
+ }
218
+ });
219
+ }
220
+
221
+ goToAddress() {
222
+ const input = this.addrInput.value.trim();
223
+ let addr = parseInt(input, 16);
224
+ if (isNaN(addr)) {
225
+ addr = parseInt(input, 10);
226
+ }
227
+ if (!isNaN(addr) && addr >= 0 && addr <= 0xffff) {
228
+ this.scrollToAddress(addr);
229
+ }
230
+ }
231
+
232
+ searchBytes() {
233
+ const input = this.searchInput.value.trim().replace(/\s/g, "");
234
+ if (input.length === 0 || input.length % 2 !== 0) return;
235
+
236
+ const bytes = [];
237
+ for (let i = 0; i < input.length; i += 2) {
238
+ const byte = parseInt(input.substr(i, 2), 16);
239
+ if (isNaN(byte)) return;
240
+ bytes.push(byte);
241
+ }
242
+
243
+ // Search from current position + 1
244
+ const startAddr = this.baseAddress + 1;
245
+ for (let i = 0; i < 65536; i++) {
246
+ const addr = (startAddr + i) & 0xffff;
247
+ let found = true;
248
+ for (let j = 0; j < bytes.length; j++) {
249
+ if (this.wasmModule._peekMemory((addr + j) & 0xffff) !== bytes[j]) {
250
+ found = false;
251
+ break;
252
+ }
253
+ }
254
+ if (found) {
255
+ this.scrollToAddress(addr);
256
+ return;
257
+ }
258
+ }
259
+ }
260
+
261
+ scrollToAddress(addr) {
262
+ // Align to row boundary and clamp
263
+ addr = Math.floor(addr / this.bytesPerRow) * this.bytesPerRow;
264
+ addr = Math.max(
265
+ 0,
266
+ Math.min(addr, 0x10000 - this.bytesPerRow * this.visibleRows),
267
+ );
268
+ if (addr !== this.baseAddress) {
269
+ this.baseAddress = addr;
270
+ this.forceRefresh = true;
271
+ if (this.onStateChange) this.onStateChange();
272
+ }
273
+ this.updateScrollbar();
274
+ this.updateRegionName();
275
+ }
276
+
277
+ updateScrollbar() {
278
+ const maxAddress = 0x10000 - this.bytesPerRow * this.visibleRows;
279
+ const thumbHeight = Math.max(
280
+ 20,
281
+ (this.visibleRows * this.bytesPerRow * 100) / 65536,
282
+ );
283
+ const thumbPosition = (this.baseAddress / maxAddress) * (100 - thumbHeight);
284
+ this.scrollbarThumb.style.height = `${thumbHeight}%`;
285
+ this.scrollbarThumb.style.top = `${thumbPosition}%`;
286
+ }
287
+
288
+ updateRegionName() {
289
+ const addr = this.baseAddress;
290
+ for (const region of MEMORY_REGIONS) {
291
+ if (addr >= region.start && addr <= region.end) {
292
+ this.regionNameSpan.textContent = region.name;
293
+ return;
294
+ }
295
+ }
296
+ this.regionNameSpan.textContent = "Unknown";
297
+ }
298
+
299
+ startEditByte(addr) {
300
+ const currentValue = this.wasmModule._peekMemory(addr);
301
+ const newValueStr = prompt(
302
+ `Edit $${this.formatHex(addr, 4)}\nCurrent value: $${this.formatHex(currentValue, 2)}`,
303
+ this.formatHex(currentValue, 2),
304
+ );
305
+ if (newValueStr !== null) {
306
+ const newValue = parseInt(newValueStr, 16);
307
+ if (!isNaN(newValue) && newValue >= 0 && newValue <= 255) {
308
+ this.wasmModule._writeMemory(addr, newValue);
309
+ }
310
+ }
311
+ }
312
+
313
+ getRegionForAddress(addr) {
314
+ for (const region of MEMORY_REGIONS) {
315
+ if (addr >= region.start && addr <= region.end) {
316
+ return region.name;
317
+ }
318
+ }
319
+ return "Unknown";
320
+ }
321
+
322
+ update(wasmModule) {
323
+ if (!this.isVisible || !this.contentDiv) return;
324
+ if (!wasmModule._isPaused() && !this.forceRefresh) return;
325
+ this.forceRefresh = false;
326
+
327
+ const now = Date.now();
328
+ const fadeTime = 1000;
329
+
330
+ let html = "";
331
+ for (let row = 0; row < this.visibleRows; row++) {
332
+ const rowAddr = this.baseAddress + row * this.bytesPerRow;
333
+ if (rowAddr >= 0x10000) break;
334
+
335
+ html += `<div class="mem-row"><span class="mem-addr">${this.formatAddr(rowAddr)}</span><span class="mem-separator">:</span>`;
336
+
337
+ // Hex bytes
338
+ for (let col = 0; col < this.bytesPerRow; col++) {
339
+ if (col === 8) html += " ";
340
+
341
+ const addr = rowAddr + col;
342
+ if (addr >= 0x10000) break;
343
+
344
+ const value = wasmModule._peekMemory(addr);
345
+ const prevValue = this.previousMemory[addr];
346
+
347
+ if (value !== prevValue) {
348
+ this.changedBytes.add(addr);
349
+ this.changeTimestamps.set(addr, now);
350
+ this.previousMemory[addr] = value;
351
+ }
352
+
353
+ let byteClass = "mem-byte";
354
+ if (value === 0x00) {
355
+ byteClass += " mem-zero";
356
+ } else if (value >= 0x20 && value < 0x7f) {
357
+ byteClass += " mem-printable";
358
+ } else if (value >= 0x80) {
359
+ byteClass += " mem-highbit";
360
+ }
361
+
362
+ if (this.changeTimestamps.has(addr)) {
363
+ const elapsed = now - this.changeTimestamps.get(addr);
364
+ if (elapsed < fadeTime) {
365
+ byteClass += " changed";
366
+ } else {
367
+ this.changeTimestamps.delete(addr);
368
+ }
369
+ }
370
+
371
+ html += `<span class="${byteClass}" data-addr="${addr}">${this.formatHex(value, 2)}</span>`;
372
+ }
373
+
374
+ // ASCII
375
+ html += '<span class="mem-ascii">';
376
+ for (let col = 0; col < this.bytesPerRow; col++) {
377
+ const addr = rowAddr + col;
378
+ if (addr >= 0x10000) break;
379
+ const value = wasmModule._peekMemory(addr);
380
+ const ch = value & 0x7f;
381
+ html += ch >= 0x20 && ch < 0x7f ? String.fromCharCode(ch) : ".";
382
+ }
383
+ html += "</span></div>";
384
+ }
385
+
386
+ this.contentDiv.innerHTML = html;
387
+ }
388
+
389
+ getState() {
390
+ const base = super.getState();
391
+ base.baseAddress = this.baseAddress;
392
+ return base;
393
+ }
394
+
395
+ restoreState(state) {
396
+ if (state.baseAddress !== undefined) {
397
+ this.baseAddress = state.baseAddress;
398
+ }
399
+ super.restoreState(state);
400
+ }
401
+
402
+ show() {
403
+ super.show();
404
+ // Apply restored base address to UI after content is rendered
405
+ if (this.contentDiv) {
406
+ this.updateScrollbar();
407
+ this.updateRegionName();
408
+ this.forceRefresh = true;
409
+ }
410
+ }
411
+
412
+ // Allow external code to jump to a specific address
413
+ jumpToAddress(addr) {
414
+ this.scrollToAddress(addr);
415
+ }
416
+ }