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,409 @@
1
+ /*
2
+ * state-manager.js - Emulator state serialization and management
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ /**
9
+ * StateManager - Manages emulator state save/restore and UI
10
+ * Handles auto-save, manual save/restore, slot saves, and state popup UI
11
+ */
12
+
13
+ import {
14
+ saveStateToStorage,
15
+ loadStateFromStorage,
16
+ hasSavedState,
17
+ getSavedStateTimestamp,
18
+ saveStateToSlot,
19
+ loadStateFromSlot,
20
+ } from "./state-persistence.js";
21
+
22
+ // Constants
23
+ const AUTO_SAVE_INTERVAL_MS = 5000;
24
+ const THUMBNAIL_WIDTH = 140;
25
+ const THUMBNAIL_HEIGHT = 96;
26
+ const PREVIEW_WIDTH = 560;
27
+ const PREVIEW_HEIGHT = 384;
28
+
29
+ /**
30
+ * @typedef {Object} StateManagerDeps
31
+ * @property {Object} emulator - The emulator instance
32
+ * @property {Object} wasmModule - The WASM module
33
+ * @property {Object} uiController - UI controller for notifications
34
+ * @property {Object} diskManager - Disk manager for state sync
35
+ * @property {Object} reminderController - Reminder controller
36
+ * @property {Object} [cpuDebuggerWindow] - CPU debugger window (for resync after import)
37
+ */
38
+
39
+ export class StateManager {
40
+ /**
41
+ * @param {StateManagerDeps} deps - Dependencies
42
+ */
43
+ constructor(deps) {
44
+ this.emulator = deps.emulator;
45
+ this.wasmModule = deps.wasmModule;
46
+ this.uiController = deps.uiController;
47
+ this.diskManager = deps.diskManager;
48
+ this.reminderController = deps.reminderController;
49
+ this.cpuDebuggerWindow = deps.cpuDebuggerWindow || null;
50
+ this.basicProgramWindow = deps.basicProgramWindow || null;
51
+
52
+ this.autoSaveEnabled = false;
53
+ this.autoSaveInterval = null;
54
+
55
+ /** @type {function|null} Called after each autosave completes */
56
+ this.onAutosave = null;
57
+ }
58
+
59
+ /**
60
+ * Initialize state management
61
+ */
62
+ init() {
63
+ this.setupAutoSave();
64
+ this.setupStatePopup();
65
+ }
66
+
67
+ /**
68
+ * Set up auto-save functionality
69
+ */
70
+ setupAutoSave() {
71
+ // Load saved auto-save setting (default to enabled)
72
+ const savedAutosave = localStorage.getItem("a2e-autosave-state");
73
+ this.autoSaveEnabled = savedAutosave === "true";
74
+
75
+ // Save window states and emulator state when page is closed
76
+ window.addEventListener("beforeunload", () => {
77
+ if (this.autoSaveEnabled) {
78
+ this.saveState();
79
+ }
80
+ });
81
+
82
+ // Save state when page becomes hidden (tab switch, minimize, mobile)
83
+ document.addEventListener("visibilitychange", () => {
84
+ if (document.hidden && this.emulator.isRunning() && this.autoSaveEnabled) {
85
+ this.saveState();
86
+ }
87
+ });
88
+
89
+ // Periodic auto-save while running, deferred to idle time to avoid
90
+ // stalling the audio/render loop (which causes stutters in Safari/Brave)
91
+ this.autoSavePending = false;
92
+ this.autoSaveIdleHandle = null;
93
+ this.autoSaveInterval = setInterval(() => {
94
+ if (this.emulator.isRunning() && !document.hidden && this.autoSaveEnabled && !this.autoSavePending) {
95
+ this.autoSavePending = true;
96
+ const doSave = () => {
97
+ this.autoSavePending = false;
98
+ this.saveState();
99
+ };
100
+ if (typeof requestIdleCallback === "function") {
101
+ this.autoSaveIdleHandle = requestIdleCallback(doSave, { timeout: AUTO_SAVE_INTERVAL_MS });
102
+ } else {
103
+ // Safari <16.4 fallback — setTimeout(0) yields to the render loop
104
+ this.autoSaveIdleHandle = setTimeout(doSave, 0);
105
+ }
106
+ }
107
+ }, AUTO_SAVE_INTERVAL_MS);
108
+ }
109
+
110
+ /**
111
+ * Set up state controls within the System menu
112
+ */
113
+ setupStatePopup() {
114
+ const autosaveToggle = document.getElementById("autosave-toggle");
115
+
116
+ // Initialize toggle state
117
+ if (autosaveToggle) {
118
+ autosaveToggle.checked = this.autoSaveEnabled;
119
+ }
120
+
121
+ // Update last saved time when system menu opens
122
+ const systemMenuContainer = document.getElementById("file-menu-container");
123
+ if (systemMenuContainer) {
124
+ const observer = new MutationObserver(() => {
125
+ if (systemMenuContainer.classList.contains("open")) {
126
+ this.updateLastSavedTime();
127
+ }
128
+ });
129
+ observer.observe(systemMenuContainer, { attributes: true, attributeFilter: ["class"] });
130
+ }
131
+
132
+ // Auto-save toggle
133
+ if (autosaveToggle) {
134
+ autosaveToggle.addEventListener("change", () => {
135
+ this.autoSaveEnabled = autosaveToggle.checked;
136
+ localStorage.setItem("a2e-autosave-state", this.autoSaveEnabled);
137
+ });
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Update the "last saved" time display
143
+ */
144
+ async updateLastSavedTime() {
145
+ const lastSavedEl = document.getElementById("state-last-saved");
146
+ if (!lastSavedEl) return;
147
+
148
+ const timestamp = await getSavedStateTimestamp();
149
+ if (timestamp) {
150
+ const date = new Date(timestamp);
151
+ const now = new Date();
152
+ const diffMs = now - date;
153
+ const diffMins = Math.floor(diffMs / 60000);
154
+ const diffHours = Math.floor(diffMs / 3600000);
155
+
156
+ let timeStr;
157
+ if (diffMins < 1) {
158
+ timeStr = "just now";
159
+ } else if (diffMins < 60) {
160
+ timeStr = `${diffMins} minute${diffMins === 1 ? "" : "s"} ago`;
161
+ } else if (diffHours < 24) {
162
+ timeStr = `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`;
163
+ } else {
164
+ timeStr = date.toLocaleDateString();
165
+ }
166
+ lastSavedEl.textContent = `Last saved: ${timeStr}`;
167
+ } else {
168
+ lastSavedEl.textContent = "No saved state";
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Capture current emulator state as a Uint8Array
174
+ * @returns {Uint8Array|null}
175
+ */
176
+ captureStateData() {
177
+ if (!this.emulator.isRunning() || !this.wasmModule) {
178
+ return null;
179
+ }
180
+
181
+ const sizePtr = this.wasmModule._malloc(4);
182
+ try {
183
+ const statePtr = this.wasmModule._exportState(sizePtr);
184
+
185
+ if (statePtr && sizePtr) {
186
+ const heapU32 = new Uint32Array(this.wasmModule.HEAPU8.buffer);
187
+ const size = heapU32[sizePtr / 4];
188
+
189
+ if (size > 0) {
190
+ const stateData = new Uint8Array(size);
191
+ stateData.set(
192
+ new Uint8Array(this.wasmModule.HEAPU8.buffer, statePtr, size)
193
+ );
194
+ return stateData;
195
+ }
196
+ }
197
+ return null;
198
+ } finally {
199
+ this.wasmModule._free(sizePtr);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Import raw state data into the emulator (power cycles first)
205
+ * @param {Uint8Array} stateData
206
+ * @returns {boolean} True if state was imported successfully
207
+ */
208
+ importStateData(stateData) {
209
+ if (!this.wasmModule || !stateData) {
210
+ return false;
211
+ }
212
+
213
+ // Power cycle: stop and restart the emulator for a clean slate
214
+ const wasRunning = this.emulator.isRunning();
215
+ if (wasRunning) {
216
+ this.emulator.stop();
217
+ }
218
+
219
+ // Start fresh
220
+ this.emulator.start();
221
+
222
+ // Copy state data to WASM memory
223
+ const statePtr = this.wasmModule._malloc(stateData.length);
224
+ this.wasmModule.HEAPU8.set(stateData, statePtr);
225
+
226
+ // Import state
227
+ const success = this.wasmModule._importState(statePtr, stateData.length);
228
+
229
+ this.wasmModule._free(statePtr);
230
+
231
+ if (success) {
232
+ if (this.reminderController) {
233
+ this.reminderController.dismissPowerReminder();
234
+ this.reminderController.showBasicReminder(false);
235
+ }
236
+ if (this.diskManager) {
237
+ this.diskManager.syncWithEmulatorState();
238
+ }
239
+ // Re-push JS-side breakpoints/watchpoints/beam breakpoints to C++
240
+ // since importState() calls reset() which clears them on the WASM side
241
+ if (this.cpuDebuggerWindow) {
242
+ this.cpuDebuggerWindow.bpManager.resyncToWasm();
243
+ this.cpuDebuggerWindow.resyncBeamToWasm();
244
+ }
245
+ // Re-sync BASIC breakpoints
246
+ if (this.basicProgramWindow) {
247
+ this.basicProgramWindow.getBreakpointManager().resyncToWasm();
248
+ }
249
+ return true;
250
+ } else {
251
+ this.emulator.stop();
252
+ return false;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Capture a thumbnail screenshot of the current emulator display
258
+ * @returns {string|null} Data URL of the thumbnail, or null
259
+ */
260
+ captureScreenshot() {
261
+ const canvas = document.getElementById("screen");
262
+ if (!canvas) return null;
263
+
264
+ try {
265
+ const offscreen = document.createElement("canvas");
266
+ offscreen.width = THUMBNAIL_WIDTH;
267
+ offscreen.height = THUMBNAIL_HEIGHT;
268
+ const ctx = offscreen.getContext("2d");
269
+ ctx.drawImage(canvas, 0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
270
+ return offscreen.toDataURL("image/jpeg", 0.85);
271
+ } catch (error) {
272
+ console.error("Failed to capture screenshot:", error);
273
+ return null;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Capture a high-resolution preview of the current emulator display
279
+ * @returns {string|null} Data URL of the preview, or null
280
+ */
281
+ capturePreview() {
282
+ const canvas = document.getElementById("screen");
283
+ if (!canvas) return null;
284
+
285
+ try {
286
+ const offscreen = document.createElement("canvas");
287
+ offscreen.width = PREVIEW_WIDTH;
288
+ offscreen.height = PREVIEW_HEIGHT;
289
+ const ctx = offscreen.getContext("2d");
290
+ ctx.drawImage(canvas, 0, 0, PREVIEW_WIDTH, PREVIEW_HEIGHT);
291
+ return offscreen.toDataURL("image/jpeg", 0.85);
292
+ } catch (error) {
293
+ console.error("Failed to capture preview:", error);
294
+ return null;
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Save the current emulator state to IndexedDB (auto-save)
300
+ * @returns {Promise<void>}
301
+ */
302
+ async saveState() {
303
+ if (!this.emulator.isRunning() || !this.wasmModule) {
304
+ return;
305
+ }
306
+
307
+ try {
308
+ this.uiController.flashStateButton();
309
+ const stateData = this.captureStateData();
310
+ if (stateData) {
311
+ const thumbnail = this.captureScreenshot();
312
+ const preview = this.capturePreview();
313
+ await saveStateToStorage(stateData, thumbnail, preview);
314
+ if (this.onAutosave) this.onAutosave();
315
+ }
316
+ } catch (error) {
317
+ console.error("Failed to save emulator state:", error);
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Restore emulator state from IndexedDB (auto-save)
323
+ * @returns {Promise<boolean>} True if state was restored
324
+ */
325
+ async restoreState() {
326
+ if (!this.wasmModule) {
327
+ return false;
328
+ }
329
+
330
+ try {
331
+ const stateData = await loadStateFromStorage();
332
+ if (!stateData) {
333
+ return false;
334
+ }
335
+
336
+ const success = this.importStateData(stateData);
337
+ if (success) {
338
+ console.log("Restored emulator state from storage");
339
+ return true;
340
+ }
341
+ } catch (error) {
342
+ console.error("Failed to restore emulator state:", error);
343
+ }
344
+
345
+ return false;
346
+ }
347
+
348
+ /**
349
+ * Save current state to a numbered slot with screenshot
350
+ * @param {number} slotNumber - Slot number (1-5)
351
+ * @returns {Promise<boolean>}
352
+ */
353
+ async saveToSlot(slotNumber) {
354
+ const stateData = this.captureStateData();
355
+ if (!stateData) return false;
356
+
357
+ const thumbnail = this.captureScreenshot();
358
+ const preview = this.capturePreview();
359
+ await saveStateToSlot(slotNumber, stateData, thumbnail, preview);
360
+ return true;
361
+ }
362
+
363
+ /**
364
+ * Restore state from a numbered slot
365
+ * @param {number} slotNumber - Slot number (1-5)
366
+ * @returns {Promise<boolean>}
367
+ */
368
+ async restoreFromSlot(slotNumber) {
369
+ const slot = await loadStateFromSlot(slotNumber);
370
+ if (!slot) return false;
371
+
372
+ return this.importStateData(slot.data);
373
+ }
374
+
375
+ /**
376
+ * Restore state from raw file data (e.g. uploaded .a2state file)
377
+ * @param {Uint8Array} stateData
378
+ * @returns {boolean}
379
+ */
380
+ restoreFromFileData(stateData) {
381
+ return this.importStateData(stateData);
382
+ }
383
+
384
+ /**
385
+ * Check if there's a saved state available
386
+ * @returns {Promise<boolean>}
387
+ */
388
+ async hasSavedState() {
389
+ return hasSavedState();
390
+ }
391
+
392
+ /**
393
+ * Check if auto-save is enabled
394
+ * @returns {boolean}
395
+ */
396
+ isAutoSaveEnabled() {
397
+ return this.autoSaveEnabled;
398
+ }
399
+
400
+ /**
401
+ * Clean up resources
402
+ */
403
+ destroy() {
404
+ if (this.autoSaveInterval) {
405
+ clearInterval(this.autoSaveInterval);
406
+ this.autoSaveInterval = null;
407
+ }
408
+ }
409
+ }
@@ -0,0 +1,218 @@
1
+ /*
2
+ * state-persistence.js - State persistence to IndexedDB
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ import { createDatabaseManager } from "../utils/indexeddb-helper.js";
9
+
10
+ const DB_NAME = "a2e-state-persistence";
11
+ const DB_VERSION = 1;
12
+ const STORE_NAME = "emulatorState";
13
+
14
+ const db = createDatabaseManager({
15
+ dbName: DB_NAME,
16
+ version: DB_VERSION,
17
+ onUpgrade: (event) => {
18
+ const database = event.target.result;
19
+ if (!database.objectStoreNames.contains(STORE_NAME)) {
20
+ database.createObjectStore(STORE_NAME, { keyPath: "id" });
21
+ }
22
+ },
23
+ });
24
+
25
+ /**
26
+ * Save emulator state to IndexedDB
27
+ * @param {Uint8Array} stateData - The serialized emulator state
28
+ * @param {string|null} [thumbnail] - Optional data URL of screenshot thumbnail
29
+ * @param {string|null} [preview] - Optional data URL of high-res preview
30
+ * @returns {Promise<void>}
31
+ */
32
+ export async function saveStateToStorage(stateData, thumbnail, preview) {
33
+ try {
34
+ const stateRecord = {
35
+ id: "autosave",
36
+ data: stateData,
37
+ savedAt: Date.now(),
38
+ thumbnail: thumbnail || null,
39
+ preview: preview || null,
40
+ };
41
+
42
+ await db.put(STORE_NAME, stateRecord);
43
+ } catch (error) {
44
+ console.error("Error saving emulator state:", error);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Load emulator state from IndexedDB
50
+ * @returns {Promise<Uint8Array | null>}
51
+ */
52
+ export async function loadStateFromStorage() {
53
+ try {
54
+ const result = await db.get(STORE_NAME, "autosave");
55
+ if (result) {
56
+ console.log("Loaded emulator state from storage");
57
+ return new Uint8Array(result.data);
58
+ }
59
+ return null;
60
+ } catch (error) {
61
+ console.error("Error loading emulator state:", error);
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Clear saved emulator state from IndexedDB
68
+ * @returns {Promise<void>}
69
+ */
70
+ export async function clearStateFromStorage() {
71
+ try {
72
+ await db.remove(STORE_NAME, "autosave");
73
+ console.log("Cleared emulator state from storage");
74
+ } catch (error) {
75
+ console.error("Error clearing emulator state:", error);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Check if there is a saved emulator state
81
+ * @returns {Promise<boolean>}
82
+ */
83
+ export async function hasSavedState() {
84
+ try {
85
+ const result = await db.get(STORE_NAME, "autosave");
86
+ return result != null;
87
+ } catch (error) {
88
+ console.error("Error checking for saved state:", error);
89
+ return false;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Get the timestamp of the last saved state
95
+ * @returns {Promise<number | null>} Timestamp in milliseconds, or null if no saved state
96
+ */
97
+ export async function getSavedStateTimestamp() {
98
+ try {
99
+ const result = await db.get(STORE_NAME, "autosave");
100
+ return result ? result.savedAt : null;
101
+ } catch (error) {
102
+ console.error("Error getting saved state timestamp:", error);
103
+ return null;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Get autosave summary info (without loading full state data)
109
+ * @returns {Promise<{savedAt: number, thumbnail: string|null}|null>}
110
+ */
111
+ export async function getAutosaveInfo() {
112
+ try {
113
+ const result = await db.get(STORE_NAME, "autosave");
114
+ if (result) {
115
+ return {
116
+ savedAt: result.savedAt,
117
+ thumbnail: result.thumbnail || null,
118
+ preview: result.preview || null,
119
+ };
120
+ }
121
+ return null;
122
+ } catch (error) {
123
+ console.error("Error getting autosave info:", error);
124
+ return null;
125
+ }
126
+ }
127
+
128
+ // --- Save State Slots ---
129
+
130
+ const SLOT_COUNT = 5;
131
+
132
+ function slotKey(slotNumber) {
133
+ return `slot-${slotNumber}`;
134
+ }
135
+
136
+ /**
137
+ * Save emulator state to a numbered slot
138
+ * @param {number} slotNumber - Slot number (1-5)
139
+ * @param {Uint8Array} stateData - The serialized emulator state
140
+ * @param {string|null} thumbnail - Data URL of screenshot thumbnail
141
+ * @param {string|null} [preview] - Optional data URL of high-res preview
142
+ * @returns {Promise<void>}
143
+ */
144
+ export async function saveStateToSlot(slotNumber, stateData, thumbnail, preview) {
145
+ try {
146
+ const record = {
147
+ id: slotKey(slotNumber),
148
+ data: new Uint8Array(stateData),
149
+ savedAt: Date.now(),
150
+ thumbnail: thumbnail || null,
151
+ preview: preview || null,
152
+ };
153
+ await db.put(STORE_NAME, record);
154
+ } catch (error) {
155
+ console.error(`Error saving state to slot ${slotNumber}:`, error);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Load emulator state from a numbered slot
161
+ * @param {number} slotNumber - Slot number (1-5)
162
+ * @returns {Promise<{data: Uint8Array, savedAt: number, thumbnail: string|null}|null>}
163
+ */
164
+ export async function loadStateFromSlot(slotNumber) {
165
+ try {
166
+ const result = await db.get(STORE_NAME, slotKey(slotNumber));
167
+ if (result) {
168
+ return {
169
+ data: new Uint8Array(result.data),
170
+ savedAt: result.savedAt,
171
+ thumbnail: result.thumbnail || null,
172
+ };
173
+ }
174
+ return null;
175
+ } catch (error) {
176
+ console.error(`Error loading state from slot ${slotNumber}:`, error);
177
+ return null;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Clear a numbered slot
183
+ * @param {number} slotNumber - Slot number (1-5)
184
+ * @returns {Promise<void>}
185
+ */
186
+ export async function clearSlot(slotNumber) {
187
+ try {
188
+ await db.remove(STORE_NAME, slotKey(slotNumber));
189
+ } catch (error) {
190
+ console.error(`Error clearing slot ${slotNumber}:`, error);
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Get summary info for all 5 slots (without loading full state data)
196
+ * @returns {Promise<Array<{slotNumber: number, savedAt: number, thumbnail: string|null}|null>>}
197
+ */
198
+ export async function getAllSlotInfo() {
199
+ const slots = [];
200
+ for (let i = 1; i <= SLOT_COUNT; i++) {
201
+ try {
202
+ const result = await db.get(STORE_NAME, slotKey(i));
203
+ if (result) {
204
+ slots.push({
205
+ slotNumber: i,
206
+ savedAt: result.savedAt,
207
+ thumbnail: result.thumbnail || null,
208
+ preview: result.preview || null,
209
+ });
210
+ } else {
211
+ slots.push(null);
212
+ }
213
+ } catch (error) {
214
+ slots.push(null);
215
+ }
216
+ }
217
+ return slots;
218
+ }
@@ -0,0 +1,43 @@
1
+ /*
2
+ * confirm.js - Custom confirm dialog using app modal styles
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ /**
9
+ * Show a confirm dialog using the app's modal styling.
10
+ * @param {string} message - The confirmation message
11
+ * @param {string} [confirmLabel='OK'] - Label for the confirm button
12
+ * @returns {Promise<boolean>} Resolves true if confirmed, false if cancelled
13
+ */
14
+ export function showConfirm(message, confirmLabel = "OK") {
15
+ return new Promise((resolve) => {
16
+ const dialog = document.createElement("dialog");
17
+ dialog.className = "modal";
18
+ dialog.innerHTML = `
19
+ <div class="modal-content">
20
+ <div class="modal-body">
21
+ <p>${message}</p>
22
+ </div>
23
+ <div class="modal-footer">
24
+ <button class="modal-btn modal-btn-secondary confirm-cancel">Cancel</button>
25
+ <button class="modal-btn modal-btn-primary confirm-ok">${confirmLabel}</button>
26
+ </div>
27
+ </div>
28
+ `;
29
+
30
+ document.body.appendChild(dialog);
31
+ dialog.showModal();
32
+
33
+ const cleanup = (result) => {
34
+ dialog.close();
35
+ dialog.remove();
36
+ resolve(result);
37
+ };
38
+
39
+ dialog.querySelector(".confirm-ok").addEventListener("click", () => cleanup(true));
40
+ dialog.querySelector(".confirm-cancel").addEventListener("click", () => cleanup(false));
41
+ dialog.addEventListener("cancel", () => cleanup(false));
42
+ });
43
+ }