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,301 @@
1
+ /*
2
+ * disk-persistence.js - Disk image 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-disk-persistence";
11
+ const DB_VERSION = 3;
12
+ const STORE_NAME = "disks";
13
+ const RECENT_STORE_NAME = "recentDisks";
14
+ const MAX_RECENT_DISKS = 10;
15
+
16
+ const db = createDatabaseManager({
17
+ dbName: DB_NAME,
18
+ version: DB_VERSION,
19
+ onUpgrade: (event) => {
20
+ const database = event.target.result;
21
+ const oldVersion = event.oldVersion;
22
+
23
+ if (!database.objectStoreNames.contains(STORE_NAME)) {
24
+ database.createObjectStore(STORE_NAME, { keyPath: "driveNum" });
25
+ }
26
+
27
+ // v2: Add recent disks store
28
+ if (!database.objectStoreNames.contains(RECENT_STORE_NAME)) {
29
+ const recentStore = database.createObjectStore(RECENT_STORE_NAME, {
30
+ keyPath: "id",
31
+ autoIncrement: true,
32
+ });
33
+ recentStore.createIndex("filename", "filename", { unique: false });
34
+ recentStore.createIndex("accessedAt", "accessedAt", { unique: false });
35
+ recentStore.createIndex("driveNum", "driveNum", { unique: false });
36
+ } else if (oldVersion < 3) {
37
+ // v3: Add driveNum index to existing store
38
+ const transaction = event.target.transaction;
39
+ const recentStore = transaction.objectStore(RECENT_STORE_NAME);
40
+ if (!recentStore.indexNames.contains("driveNum")) {
41
+ recentStore.createIndex("driveNum", "driveNum", { unique: false });
42
+ }
43
+ }
44
+ },
45
+ });
46
+
47
+ /**
48
+ * Save a disk image to IndexedDB
49
+ * @param {number} driveNum - Drive number (0 or 1)
50
+ * @param {string} filename - The disk filename
51
+ * @param {Uint8Array} data - The disk image data
52
+ * @returns {Promise<void>}
53
+ */
54
+ export async function saveDiskToStorage(driveNum, filename, data) {
55
+ try {
56
+ const diskRecord = {
57
+ driveNum,
58
+ filename,
59
+ data: new Uint8Array(data),
60
+ savedAt: Date.now(),
61
+ };
62
+
63
+ await db.put(STORE_NAME, diskRecord);
64
+ console.log(`Saved disk to storage: drive ${driveNum + 1}, ${filename}`);
65
+ } catch (error) {
66
+ console.error("Error saving disk to storage:", error);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Load a disk image from IndexedDB
72
+ * @param {number} driveNum - Drive number (0 or 1)
73
+ * @returns {Promise<{filename: string, data: Uint8Array} | null>}
74
+ */
75
+ export async function loadDiskFromStorage(driveNum) {
76
+ try {
77
+ const result = await db.get(STORE_NAME, driveNum);
78
+ if (result) {
79
+ console.log(`Loaded disk from storage: drive ${driveNum + 1}, ${result.filename}`);
80
+ return {
81
+ filename: result.filename,
82
+ data: new Uint8Array(result.data),
83
+ };
84
+ }
85
+ return null;
86
+ } catch (error) {
87
+ console.error("Error loading disk from storage:", error);
88
+ return null;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Remove a disk from IndexedDB storage
94
+ * @param {number} driveNum - Drive number (0 or 1)
95
+ * @returns {Promise<void>}
96
+ */
97
+ export async function clearDiskFromStorage(driveNum) {
98
+ try {
99
+ await db.remove(STORE_NAME, driveNum);
100
+ console.log(`Cleared disk from storage: drive ${driveNum + 1}`);
101
+ } catch (error) {
102
+ console.error("Error clearing disk from storage:", error);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Check if there are any persisted disks
108
+ * @returns {Promise<boolean>}
109
+ */
110
+ export async function hasPersistedDisks() {
111
+ try {
112
+ const count = await db.count(STORE_NAME);
113
+ return count > 0;
114
+ } catch (error) {
115
+ console.error("Error checking persisted disks:", error);
116
+ return false;
117
+ }
118
+ }
119
+
120
+ // ============================================================================
121
+ // Recent Disks Functions
122
+ // ============================================================================
123
+
124
+ /**
125
+ * Find a recent disk by filename for a specific drive
126
+ * @param {number} driveNum - Drive number (0 or 1)
127
+ * @param {string} filename
128
+ * @returns {Promise<number|null>} The record ID or null
129
+ */
130
+ async function findRecentDiskByFilename(driveNum, filename) {
131
+ let foundId = null;
132
+
133
+ await db.iterate(
134
+ RECENT_STORE_NAME,
135
+ { indexName: "filename", range: IDBKeyRange.only(filename) },
136
+ (value, cursor) => {
137
+ if (value.driveNum === driveNum) {
138
+ foundId = cursor.primaryKey;
139
+ return false; // Stop iteration
140
+ }
141
+ }
142
+ );
143
+
144
+ return foundId;
145
+ }
146
+
147
+ /**
148
+ * Trim recent disks to MAX_RECENT_DISKS entries for a specific drive
149
+ * @param {number} driveNum - Drive number (0 or 1)
150
+ * @returns {Promise<void>}
151
+ */
152
+ async function trimRecentDisks(driveNum) {
153
+ // Collect all records for this drive
154
+ const records = [];
155
+
156
+ await db.iterate(
157
+ RECENT_STORE_NAME,
158
+ { indexName: "accessedAt" },
159
+ (value, cursor) => {
160
+ if (value.driveNum === driveNum) {
161
+ records.push({ id: cursor.primaryKey, accessedAt: value.accessedAt });
162
+ }
163
+ }
164
+ );
165
+
166
+ // Delete excess entries (oldest first since sorted by accessedAt)
167
+ if (records.length > MAX_RECENT_DISKS) {
168
+ const deleteCount = records.length - MAX_RECENT_DISKS;
169
+ for (let i = 0; i < deleteCount; i++) {
170
+ await db.remove(RECENT_STORE_NAME, records[i].id);
171
+ }
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Add a disk to the recent disks list for a specific drive
177
+ * If the disk already exists (same filename for same drive), update its access time
178
+ * Maintains a maximum of MAX_RECENT_DISKS entries per drive
179
+ * @param {number} driveNum - Drive number (0 or 1)
180
+ * @param {string} filename - The disk filename
181
+ * @param {Uint8Array} data - The disk image data
182
+ * @returns {Promise<void>}
183
+ */
184
+ export async function addToRecentDisks(driveNum, filename, data) {
185
+ try {
186
+ // Check if this filename already exists for this drive and remove it
187
+ const existingId = await findRecentDiskByFilename(driveNum, filename);
188
+ if (existingId !== null) {
189
+ await db.remove(RECENT_STORE_NAME, existingId);
190
+ }
191
+
192
+ // Add the new entry
193
+ const diskRecord = {
194
+ driveNum,
195
+ filename,
196
+ data: new Uint8Array(data),
197
+ accessedAt: Date.now(),
198
+ };
199
+
200
+ await db.add(RECENT_STORE_NAME, diskRecord);
201
+
202
+ // Trim to MAX_RECENT_DISKS for this drive
203
+ await trimRecentDisks(driveNum);
204
+
205
+ console.log(`Added to recent disks (drive ${driveNum + 1}): ${filename}`);
206
+ } catch (error) {
207
+ console.error("Error adding to recent disks:", error);
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Get the list of recent disks for a specific drive, sorted by most recently accessed
213
+ * @param {number} driveNum - Drive number (0 or 1)
214
+ * @returns {Promise<Array<{id: number, filename: string, accessedAt: number}>>}
215
+ */
216
+ export async function getRecentDisks(driveNum) {
217
+ try {
218
+ const results = [];
219
+
220
+ await db.iterate(
221
+ RECENT_STORE_NAME,
222
+ { indexName: "accessedAt", direction: "prev" },
223
+ (value) => {
224
+ if (value.driveNum === driveNum) {
225
+ results.push({
226
+ id: value.id,
227
+ filename: value.filename,
228
+ accessedAt: value.accessedAt,
229
+ });
230
+ }
231
+ }
232
+ );
233
+
234
+ return results;
235
+ } catch (error) {
236
+ console.error("Error getting recent disks:", error);
237
+ return [];
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Load a recent disk by its ID
243
+ * @param {number} id - The record ID
244
+ * @returns {Promise<{filename: string, data: Uint8Array} | null>}
245
+ */
246
+ export async function loadRecentDisk(id) {
247
+ try {
248
+ const result = await db.get(RECENT_STORE_NAME, id);
249
+ if (result) {
250
+ return {
251
+ filename: result.filename,
252
+ data: new Uint8Array(result.data),
253
+ };
254
+ }
255
+ return null;
256
+ } catch (error) {
257
+ console.error("Error loading recent disk:", error);
258
+ return null;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Remove a disk from the recent list
264
+ * @param {number} id - The record ID
265
+ * @returns {Promise<void>}
266
+ */
267
+ export async function removeRecentDisk(id) {
268
+ try {
269
+ await db.remove(RECENT_STORE_NAME, id);
270
+ console.log(`Removed recent disk with id: ${id}`);
271
+ } catch (error) {
272
+ console.error("Error removing recent disk:", error);
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Clear all recent disks for a specific drive
278
+ * @param {number} driveNum - Drive number (0 or 1)
279
+ * @returns {Promise<void>}
280
+ */
281
+ export async function clearRecentDisks(driveNum) {
282
+ try {
283
+ // Collect IDs to delete
284
+ const idsToDelete = [];
285
+
286
+ await db.iterate(RECENT_STORE_NAME, {}, (value, cursor) => {
287
+ if (value.driveNum === driveNum) {
288
+ idsToDelete.push(cursor.primaryKey);
289
+ }
290
+ });
291
+
292
+ // Delete them
293
+ for (const id of idsToDelete) {
294
+ await db.remove(RECENT_STORE_NAME, id);
295
+ }
296
+
297
+ console.log(`Cleared recent disks for drive ${driveNum + 1}`);
298
+ } catch (error) {
299
+ console.error("Error clearing recent disks:", error);
300
+ }
301
+ }
@@ -0,0 +1,388 @@
1
+ /*
2
+ * disk-surface-renderer.js - Disk surface visualization renderer
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ const CANVAS_W = 280;
9
+ const CANVAS_H = 240;
10
+ const CENTER_X = 140;
11
+ const CENTER_Y = 115;
12
+ const OUTER_RADIUS = 105;
13
+ const HUB_HOLE_RADIUS = 28; // large center hole (~27% of disk)
14
+ const HUB_RING_INNER = 28; // white reinforcement ring starts at hole edge
15
+ const HUB_RING_OUTER = 34; // ring is ~6px wide
16
+ const TRACK_OUTER = OUTER_RADIUS - 3; // outermost track position
17
+ const TRACK_INNER = HUB_RING_OUTER + 4; // innermost track position (just outside hub ring)
18
+ const NUM_TRACKS = 35;
19
+ const NUM_SECTORS = 16;
20
+ const TRACK_RANGE = TRACK_OUTER - TRACK_INNER;
21
+ const INDEX_HOLE_RADIUS = 4;
22
+ const INDEX_HOLE_DIST = (HUB_RING_INNER + HUB_RING_OUTER) / 2; // centered in hub ring
23
+ const RPM_RAD_PER_MS = Math.PI / 100; // 300 RPM
24
+ const PX_RATIO = 2; // backing store scale for sharper rendering
25
+
26
+ export class DiskSurfaceRenderer {
27
+ constructor(canvas) {
28
+ this.canvas = canvas;
29
+
30
+ // Set high-res backing store; CSS sizes the element
31
+ canvas.width = CANVAS_W * PX_RATIO;
32
+ canvas.height = CANVAS_H * PX_RATIO;
33
+
34
+ this.ctx = canvas.getContext('2d');
35
+ this.ctx.scale(PX_RATIO, PX_RATIO);
36
+
37
+ // Rotation state
38
+ this.angle = 0;
39
+ this.lastTimestamp = 0;
40
+ this.motorOn = false;
41
+ this.angularVelocity = 0;
42
+ this.spinning = false;
43
+
44
+ // Previous state for dirty checking
45
+ this._prev = {};
46
+
47
+ // Theme colors (read from CSS variables)
48
+ this._lastTheme = null;
49
+ this._updateThemeColors();
50
+
51
+ this._drawEmpty();
52
+ }
53
+
54
+ _updateThemeColors() {
55
+ const s = getComputedStyle(document.documentElement);
56
+ const v = (name, fallback) => s.getPropertyValue(name).trim() || fallback;
57
+ this._colors = {
58
+ bg: v('--disk-bg', '#161b22'),
59
+ medium: v('--disk-medium', '#1a1308'),
60
+ sectorLine: v('--disk-sector-line', 'rgba(255,255,255,0.08)'),
61
+ ghostOutline: v('--disk-ghost-outline', 'rgba(255,255,255,0.06)'),
62
+ ghostSector: v('--disk-ghost-sector', 'rgba(255,255,255,0.03)'),
63
+ ghostHub: v('--disk-ghost-hub', 'rgba(210,208,200,0.08)'),
64
+ hubRing: v('--disk-hub-ring', 'rgba(210,208,200,0.85)'),
65
+ hubEdge: v('--disk-hub-edge', 'rgba(255,255,255,0.12)'),
66
+ hubEdgeInner: v('--disk-hub-edge-inner', 'rgba(255,255,255,0.08)'),
67
+ holeEdge: v('--disk-hole-edge', 'rgba(0,0,0,0.3)'),
68
+ diskEdge: v('--disk-edge', 'rgba(255,255,255,0.06)'),
69
+ };
70
+ }
71
+
72
+ update(state) {
73
+ const {
74
+ hasDisk, isActive, isWriteMode, quarterTrack, track,
75
+ trackAccessCounts, maxAccessCount, diskColor, timestamp
76
+ } = state;
77
+
78
+ // Check for theme changes
79
+ const currentTheme = document.documentElement.dataset.theme;
80
+ if (this._lastTheme !== currentTheme) {
81
+ this._lastTheme = currentTheme;
82
+ this._updateThemeColors();
83
+ this._prev = {}; // force full redraw
84
+ }
85
+
86
+ // Rotation physics
87
+ const dt = this.lastTimestamp > 0 ? timestamp - this.lastTimestamp : 0;
88
+ this.lastTimestamp = timestamp;
89
+
90
+ if (isActive && hasDisk) {
91
+ this.motorOn = true;
92
+ this.spinning = true;
93
+ this.angularVelocity = RPM_RAD_PER_MS;
94
+ } else if (this.motorOn) {
95
+ this.motorOn = false;
96
+ this._prev = {};
97
+ }
98
+
99
+ if (this.spinning && dt > 0) {
100
+ if (!this.motorOn) {
101
+ this.angularVelocity *= Math.pow(0.5, dt / 600);
102
+ if (this.angularVelocity < RPM_RAD_PER_MS * 0.005) {
103
+ this.angularVelocity = 0;
104
+ this.spinning = false;
105
+ }
106
+ }
107
+ this.angle += this.angularVelocity * dt;
108
+ }
109
+
110
+ if (!hasDisk) {
111
+ if (this._prev.hasDisk !== false) {
112
+ this._drawEmpty();
113
+ this._prev = { hasDisk: false };
114
+ }
115
+ return;
116
+ }
117
+
118
+ if (!this.spinning) {
119
+ if (
120
+ this._prev.hasDisk === true &&
121
+ this._prev.quarterTrack === quarterTrack &&
122
+ this._prev.track === track &&
123
+ this._prev.isActive === isActive &&
124
+ this._prev.isWriteMode === isWriteMode &&
125
+ this._prev.maxAccessCount === maxAccessCount &&
126
+ this._prev.diskColor === diskColor
127
+ ) {
128
+ return;
129
+ }
130
+ }
131
+
132
+ this._prev = {
133
+ hasDisk: true, quarterTrack, track, isActive,
134
+ isWriteMode, maxAccessCount, diskColor
135
+ };
136
+
137
+ this._drawDisk(state);
138
+ }
139
+
140
+ reset() {
141
+ this.angle = 0;
142
+ this.lastTimestamp = 0;
143
+ this.motorOn = false;
144
+ this.angularVelocity = 0;
145
+ this.spinning = false;
146
+ this._prev = {};
147
+ this._drawEmpty();
148
+ }
149
+
150
+ // ---- Drawing ----
151
+
152
+ _drawEmpty() {
153
+ const ctx = this.ctx;
154
+ const c = this._colors;
155
+ ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
156
+
157
+ ctx.fillStyle = c.bg;
158
+ ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
159
+
160
+ // Ghost disk — faint outline of the platter
161
+ ctx.beginPath();
162
+ ctx.arc(CENTER_X, CENTER_Y, OUTER_RADIUS, 0, Math.PI * 2);
163
+ ctx.strokeStyle = c.ghostOutline;
164
+ ctx.lineWidth = 1;
165
+ ctx.stroke();
166
+
167
+ // Ghost sector lines
168
+ const TWO_PI = Math.PI * 2;
169
+ ctx.save();
170
+ ctx.translate(CENTER_X, CENTER_Y);
171
+ for (let s = 0; s < NUM_SECTORS; s++) {
172
+ const a = (s / NUM_SECTORS) * TWO_PI;
173
+ ctx.strokeStyle = c.ghostSector;
174
+ ctx.lineWidth = 0.5;
175
+ ctx.beginPath();
176
+ ctx.moveTo(Math.cos(a) * TRACK_INNER, Math.sin(a) * TRACK_INNER);
177
+ ctx.lineTo(Math.cos(a) * TRACK_OUTER, Math.sin(a) * TRACK_OUTER);
178
+ ctx.stroke();
179
+ }
180
+ ctx.restore();
181
+
182
+ // Ghost hub ring
183
+ ctx.beginPath();
184
+ ctx.arc(CENTER_X, CENTER_Y, HUB_RING_OUTER, 0, Math.PI * 2);
185
+ ctx.arc(CENTER_X, CENTER_Y, HUB_RING_INNER, TWO_PI, 0, true);
186
+ ctx.fillStyle = c.ghostHub;
187
+ ctx.fill();
188
+
189
+ // Ghost center hole
190
+ ctx.beginPath();
191
+ ctx.arc(CENTER_X, CENTER_Y, HUB_HOLE_RADIUS, 0, Math.PI * 2);
192
+ ctx.strokeStyle = c.ghostOutline;
193
+ ctx.lineWidth = 0.5;
194
+ ctx.stroke();
195
+
196
+ }
197
+
198
+ _drawDisk(state) {
199
+ const {
200
+ isActive, isWriteMode, quarterTrack,
201
+ trackAccessCounts, maxAccessCount, diskColor
202
+ } = state;
203
+
204
+ const ctx = this.ctx;
205
+ const c = this._colors;
206
+ ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
207
+
208
+ // Background
209
+ ctx.fillStyle = c.bg;
210
+ ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
211
+
212
+ // 1. Magnetic medium — dark brown disk
213
+ ctx.beginPath();
214
+ ctx.arc(CENTER_X, CENTER_Y, OUTER_RADIUS, 0, Math.PI * 2);
215
+ ctx.fillStyle = c.medium;
216
+ ctx.fill();
217
+
218
+ // Subtle edge
219
+ ctx.strokeStyle = c.diskEdge;
220
+ ctx.lineWidth = 0.5;
221
+ ctx.stroke();
222
+
223
+ // 2. Accessed track rings only (rotated with disk)
224
+ ctx.save();
225
+ ctx.translate(CENTER_X, CENTER_Y);
226
+ ctx.rotate(this.angle);
227
+ this._drawAccessedTracks(ctx, trackAccessCounts, maxAccessCount);
228
+
229
+ // 3. Sector lines (rotated)
230
+ this._drawSectorLines(ctx);
231
+ ctx.restore();
232
+
233
+ // 5. Head and activity glow
234
+ this._drawHeadArm(ctx, quarterTrack, isActive, isWriteMode);
235
+
236
+ if (isActive) {
237
+ this._drawHeadGlow(ctx, quarterTrack, isWriteMode);
238
+ }
239
+
240
+ // 7. Hub ring + center hole
241
+ this._drawHub(ctx);
242
+
243
+ // 8. Index hole — punched through the hub ring, rotates with disk
244
+ this._drawIndexHole(ctx);
245
+ }
246
+
247
+ _drawAccessedTracks(ctx, trackAccessCounts, maxAccessCount) {
248
+ if (!trackAccessCounts || maxAccessCount === 0) return;
249
+
250
+ const logMax = Math.log(maxAccessCount + 1);
251
+
252
+ for (let t = 0; t < NUM_TRACKS; t++) {
253
+ const count = trackAccessCounts[t];
254
+ if (count === 0) continue;
255
+
256
+ // Track 0 = outermost, track 34 = innermost
257
+ const outerR = TRACK_OUTER - (t * TRACK_RANGE / NUM_TRACKS);
258
+ const innerR = TRACK_OUTER - ((t + 1) * TRACK_RANGE / NUM_TRACKS);
259
+
260
+ const intensity = Math.log(count + 1) / logMax;
261
+ const r = Math.round(40 + 215 * intensity);
262
+ const g = Math.round(60 + 80 * intensity - 40 * intensity * intensity);
263
+ const b = Math.round(100 - 80 * intensity);
264
+ const a = 0.3 + 0.55 * intensity;
265
+
266
+ ctx.fillStyle = `rgba(${r},${g},${b},${a})`;
267
+ ctx.beginPath();
268
+ ctx.arc(0, 0, outerR - 0.5, 0, Math.PI * 2);
269
+ ctx.arc(0, 0, innerR + 0.5, Math.PI * 2, 0, true);
270
+ ctx.fill();
271
+ }
272
+ }
273
+
274
+ _drawSectorLines(ctx) {
275
+ const TWO_PI = Math.PI * 2;
276
+
277
+ ctx.strokeStyle = this._colors.sectorLine;
278
+ ctx.lineWidth = 0.5;
279
+ for (let s = 0; s < NUM_SECTORS; s++) {
280
+ const a = (s / NUM_SECTORS) * TWO_PI;
281
+ ctx.beginPath();
282
+ ctx.moveTo(Math.cos(a) * TRACK_INNER, Math.sin(a) * TRACK_INNER);
283
+ ctx.lineTo(Math.cos(a) * TRACK_OUTER, Math.sin(a) * TRACK_OUTER);
284
+ ctx.stroke();
285
+ }
286
+ }
287
+
288
+ _drawIndexHole(ctx) {
289
+ // Red parallelogram across the hub ring, rotates with disk
290
+ const outerR = HUB_RING_OUTER;
291
+ const innerR = HUB_RING_INNER;
292
+ const outerHalf = 1.5; // shorter side (next to disk)
293
+ const innerHalf = 2.5; // slightly longer side (next to center hole)
294
+
295
+ ctx.save();
296
+ ctx.translate(CENTER_X, CENTER_Y);
297
+ ctx.rotate(this.angle);
298
+
299
+ ctx.beginPath();
300
+ ctx.moveTo(outerR, -outerHalf);
301
+ ctx.lineTo(outerR, outerHalf);
302
+ ctx.lineTo(innerR, innerHalf);
303
+ ctx.lineTo(innerR, -innerHalf);
304
+ ctx.closePath();
305
+
306
+ ctx.fillStyle = 'rgba(200,30,30,0.9)';
307
+ ctx.fill();
308
+ ctx.strokeStyle = 'rgba(120,10,10,0.6)';
309
+ ctx.lineWidth = 0.5;
310
+ ctx.stroke();
311
+
312
+ ctx.restore();
313
+ }
314
+
315
+ _drawHeadArm(ctx, quarterTrack, isActive, isWriteMode) {
316
+ const trackPos = quarterTrack / 4;
317
+ const headR = TRACK_OUTER - ((trackPos + 0.5) * TRACK_RANGE / NUM_TRACKS);
318
+
319
+ // Head color: green when reading, red when writing, grey when idle
320
+ if (isActive) {
321
+ ctx.fillStyle = isWriteMode
322
+ ? 'rgba(220,40,40,0.85)'
323
+ : 'rgba(40,200,60,0.85)';
324
+ } else {
325
+ ctx.fillStyle = 'rgba(160,155,150,0.7)';
326
+ }
327
+ ctx.fillRect(CENTER_X - 3, CENTER_Y - headR - 2, 6, 4);
328
+ }
329
+
330
+ _drawHeadGlow(ctx, quarterTrack, isWriteMode) {
331
+ const trackPos = quarterTrack / 4;
332
+ const headR = TRACK_OUTER - ((trackPos + 0.5) * TRACK_RANGE / NUM_TRACKS);
333
+
334
+ ctx.fillStyle = isWriteMode
335
+ ? 'rgba(220,40,40,0.4)'
336
+ : 'rgba(40,200,60,0.4)';
337
+ ctx.fillRect(CENTER_X - 4, CENTER_Y - headR - 3, 8, 6);
338
+ }
339
+
340
+ _drawHub(ctx) {
341
+ const c = this._colors;
342
+
343
+ // White reinforcement hub ring
344
+ ctx.beginPath();
345
+ ctx.arc(CENTER_X, CENTER_Y, HUB_RING_OUTER, 0, Math.PI * 2);
346
+ ctx.arc(CENTER_X, CENTER_Y, HUB_RING_INNER, Math.PI * 2, 0, true);
347
+ ctx.fillStyle = c.hubRing;
348
+ ctx.fill();
349
+
350
+ // Ring edge lines
351
+ ctx.beginPath();
352
+ ctx.arc(CENTER_X, CENTER_Y, HUB_RING_OUTER, 0, Math.PI * 2);
353
+ ctx.strokeStyle = c.hubEdge;
354
+ ctx.lineWidth = 0.5;
355
+ ctx.stroke();
356
+
357
+ ctx.beginPath();
358
+ ctx.arc(CENTER_X, CENTER_Y, HUB_RING_INNER, 0, Math.PI * 2);
359
+ ctx.strokeStyle = c.hubEdgeInner;
360
+ ctx.lineWidth = 0.5;
361
+ ctx.stroke();
362
+
363
+ // Center hole — see through to background
364
+ ctx.beginPath();
365
+ ctx.arc(CENTER_X, CENTER_Y, HUB_HOLE_RADIUS, 0, Math.PI * 2);
366
+ ctx.fillStyle = c.bg;
367
+ ctx.fill();
368
+
369
+ // Hole edge
370
+ ctx.strokeStyle = c.holeEdge;
371
+ ctx.lineWidth = 0.5;
372
+ ctx.stroke();
373
+ }
374
+
375
+ _tintColor(hex, strength) {
376
+ let r = 0, g = 0, b = 0;
377
+ if (hex.length === 7) {
378
+ r = parseInt(hex.slice(1, 3), 16);
379
+ g = parseInt(hex.slice(3, 5), 16);
380
+ b = parseInt(hex.slice(5, 7), 16);
381
+ }
382
+ const br = 0x88, bg = 0x85, bb = 0x80;
383
+ r = Math.round(r * strength + br * (1 - strength));
384
+ g = Math.round(g * strength + bg * (1 - strength));
385
+ b = Math.round(b * strength + bb * (1 - strength));
386
+ return `rgb(${r},${g},${b})`;
387
+ }
388
+ }