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,690 @@
1
+ /*
2
+ * base-window.js - Base window class for all windows
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ export class BaseWindow {
9
+ constructor(config) {
10
+ this.id = config.id;
11
+ this.title = config.title;
12
+ this.minWidth = config.minWidth || 280;
13
+ this.minHeight = config.minHeight || 200;
14
+ this.maxWidth = config.maxWidth || Infinity;
15
+ this.maxHeight = config.maxHeight || Infinity;
16
+ this.defaultWidth = config.defaultWidth || 400;
17
+ this.defaultHeight = config.defaultHeight || 300;
18
+ this.defaultPosition = config.defaultPosition || null;
19
+ this.closable = config.closable !== false;
20
+ this.focusCanvas = config.focusCanvas || false;
21
+
22
+ // Customizable CSS class names (defaults to debug-window style)
23
+ this.cssClasses = {
24
+ window: config.cssClasses?.window || "debug-window",
25
+ header: config.cssClasses?.header || "debug-window-header",
26
+ title: config.cssClasses?.title || "debug-window-title",
27
+ close: config.cssClasses?.close || "debug-window-close",
28
+ content: config.cssClasses?.content || "debug-window-content",
29
+ resizeHandle: config.cssClasses?.resizeHandle || "debug-resize-handle",
30
+ };
31
+
32
+ // Resize directions to create handles for
33
+ this.resizeDirections = config.resizeDirections || [
34
+ "n",
35
+ "e",
36
+ "s",
37
+ "w",
38
+ "ne",
39
+ "nw",
40
+ "se",
41
+ "sw",
42
+ ];
43
+
44
+ // Optional localStorage key for persisting window state
45
+ this.storageKey = config.storageKey || null;
46
+
47
+ this.element = null;
48
+ this.headerElement = null;
49
+ this.contentElement = null;
50
+ this.zIndex = 1000;
51
+ this.isVisible = false;
52
+ this.isDragging = false;
53
+ this.isResizing = false;
54
+ this.dragOffset = { x: 0, y: 0 };
55
+ this.resizeStart = { x: 0, y: 0, width: 0, height: 0, left: 0, top: 0 };
56
+ this.resizeDirection = null;
57
+
58
+ // Track current position/size (needed because getBoundingClientRect returns zeros for hidden elements)
59
+ this.currentX = config.defaultPosition?.x ?? 0;
60
+ this.currentY = config.defaultPosition?.y ?? 0;
61
+ this.currentWidth = config.defaultWidth || 400;
62
+ this.currentHeight = config.defaultHeight || 300;
63
+
64
+ // Track distance from right/bottom edges for maintaining position on resize
65
+ this.distanceFromRight = null;
66
+ this.distanceFromBottom = null;
67
+ this.lastViewportWidth = window.innerWidth;
68
+ this.lastViewportHeight = window.innerHeight;
69
+
70
+ // Bind event handlers
71
+ this.handleMouseDown = this.handleMouseDown.bind(this);
72
+ this.handleMouseMove = this.handleMouseMove.bind(this);
73
+ this.handleMouseUp = this.handleMouseUp.bind(this);
74
+ }
75
+
76
+ /**
77
+ * Create the window DOM structure
78
+ */
79
+ create() {
80
+ // Calculate centered position if no explicit default was provided
81
+ if (!this.defaultPosition) {
82
+ const header = document.querySelector("header");
83
+ const footer = document.querySelector("footer");
84
+ const minTop = header ? header.offsetHeight : 0;
85
+ const footerHeight = footer ? footer.offsetHeight : 0;
86
+ const availHeight = window.innerHeight - minTop - footerHeight;
87
+ this.defaultPosition = {
88
+ x: Math.max(0, Math.round((window.innerWidth - this.defaultWidth) / 2)),
89
+ y: Math.max(minTop, Math.round(minTop + (availHeight - this.defaultHeight) / 2)),
90
+ };
91
+ this.currentX = this.defaultPosition.x;
92
+ this.currentY = this.defaultPosition.y;
93
+ }
94
+
95
+ // Create main window element
96
+ this.element = document.createElement("div");
97
+ this.element.id = this.id;
98
+ this.element.className = `${this.cssClasses.window} hidden`;
99
+ this.element.style.width = `${this.defaultWidth}px`;
100
+ this.element.style.height = `${this.defaultHeight}px`;
101
+ this.element.style.left = `${this.defaultPosition.x}px`;
102
+ this.element.style.top = `${this.defaultPosition.y}px`;
103
+
104
+ // Header (draggable area)
105
+ this.headerElement = document.createElement("div");
106
+ this.headerElement.className = this.cssClasses.header;
107
+ this.headerElement.innerHTML = `
108
+ <span class="${this.cssClasses.title}">${this.title}</span>
109
+ ${this.closable ? `<button class="${this.cssClasses.close}" title="Close">&times;</button>` : ""}
110
+ `;
111
+
112
+ // Content area
113
+ this.contentElement = document.createElement("div");
114
+ this.contentElement.className = this.cssClasses.content;
115
+ this.contentElement.innerHTML = this.renderContent();
116
+
117
+ // Resize handles
118
+ this.resizeDirections.forEach((dir) => {
119
+ const handle = document.createElement("div");
120
+ handle.className = `${this.cssClasses.resizeHandle} ${dir}`;
121
+ handle.dataset.direction = dir;
122
+ this.element.appendChild(handle);
123
+ });
124
+
125
+ // Assemble
126
+ this.element.appendChild(this.headerElement);
127
+ this.element.appendChild(this.contentElement);
128
+ document.body.appendChild(this.element);
129
+
130
+ // Set up event listeners
131
+ this.setupEventListeners();
132
+
133
+ // Call hook for subclasses to set up after content is rendered
134
+ if (typeof this.onContentRendered === "function") {
135
+ this.onContentRendered();
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Set up drag, resize, and close event listeners
141
+ */
142
+ setupEventListeners() {
143
+ // Close button
144
+ const closeBtn = this.headerElement.querySelector(
145
+ `.${this.cssClasses.close}`,
146
+ );
147
+ if (closeBtn) {
148
+ closeBtn.addEventListener("click", () => this.hide());
149
+ }
150
+
151
+ // Drag start on header
152
+ this.headerElement.addEventListener("mousedown", (e) => {
153
+ if (e.target.classList.contains(this.cssClasses.close)) return;
154
+ this.startDrag(e);
155
+ });
156
+
157
+ // Resize start on handles
158
+ this.element
159
+ .querySelectorAll(`.${this.cssClasses.resizeHandle}`)
160
+ .forEach((handle) => {
161
+ handle.addEventListener("mousedown", (e) => {
162
+ this.startResize(e, handle.dataset.direction);
163
+ });
164
+ });
165
+
166
+ // Bring to front on click — if the window isn't focused, consume the
167
+ // first click so it only brings the window to front without interacting
168
+ // with content like text areas. Buttons and interactive controls are
169
+ // allowed through so they respond on the first click.
170
+ this._consumeNextClick = false;
171
+ this.element.addEventListener("mousedown", (e) => {
172
+ const wasFocused = this.element.classList.contains("focused");
173
+ if (this.onFocus) this.onFocus(this.id);
174
+ if (this.focusCanvas && !this.headerElement.contains(e.target)) {
175
+ // Focus the emulator canvas so keystrokes go to the emulator.
176
+ // Blur first to release any focused element (e.g. BASIC textarea),
177
+ // then focus the canvas on next frame after browser focus handling.
178
+ if (document.activeElement && document.activeElement !== document.body) {
179
+ document.activeElement.blur();
180
+ }
181
+ e.preventDefault();
182
+ const canvas = document.getElementById("screen");
183
+ if (canvas) setTimeout(() => canvas.focus(), 0);
184
+ return;
185
+ }
186
+ if (!wasFocused && !this.headerElement.contains(e.target)) {
187
+ const clickable = e.target.closest("button, input, select, label, a, .toggle-switch");
188
+ if (!clickable) {
189
+ this._consumeNextClick = true;
190
+ e.preventDefault();
191
+ e.stopPropagation();
192
+ }
193
+ }
194
+ }, true);
195
+ this.element.addEventListener("click", (e) => {
196
+ if (this._consumeNextClick) {
197
+ this._consumeNextClick = false;
198
+ e.preventDefault();
199
+ e.stopPropagation();
200
+ }
201
+ }, true);
202
+
203
+ // Global mouse events for drag/resize
204
+ document.addEventListener("mousemove", this.handleMouseMove);
205
+ document.addEventListener("mouseup", this.handleMouseUp);
206
+ }
207
+
208
+ /**
209
+ * Handle mouse down for drag/resize detection
210
+ */
211
+ handleMouseDown(e) {
212
+ // Handled by specific listeners
213
+ }
214
+
215
+ /**
216
+ * Handle mouse move for dragging and resizing
217
+ */
218
+ handleMouseMove(e) {
219
+ if (this.isDragging) {
220
+ this.drag(e);
221
+ } else if (this.isResizing) {
222
+ this.resize(e);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Handle mouse up to end drag/resize
228
+ */
229
+ handleMouseUp(e) {
230
+ if (this.isDragging || this.isResizing) {
231
+ this.isDragging = false;
232
+ this.isResizing = false;
233
+ this.element.classList.remove("dragging", "resizing");
234
+ if (this.onStateChange) this.onStateChange();
235
+ this.saveSettings();
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Start dragging the window
241
+ */
242
+ startDrag(e) {
243
+ this.isDragging = true;
244
+ this.element.classList.add("dragging");
245
+ const rect = this.element.getBoundingClientRect();
246
+ this.dragOffset = {
247
+ x: e.clientX - rect.left,
248
+ y: e.clientY - rect.top,
249
+ };
250
+ e.preventDefault();
251
+ }
252
+
253
+ /**
254
+ * Handle drag movement
255
+ */
256
+ drag(e) {
257
+ let x = e.clientX - this.dragOffset.x;
258
+ let y = e.clientY - this.dragOffset.y;
259
+
260
+ // Get header and footer heights to prevent dragging under/over them
261
+ const header = document.querySelector("header");
262
+ const footer = document.querySelector("footer");
263
+ const minY = header ? header.offsetHeight : 0;
264
+ const footerHeight = footer ? footer.offsetHeight : 0;
265
+
266
+ // Keep window on screen, below header, and above footer
267
+ const maxX = window.innerWidth - this.element.offsetWidth;
268
+ const maxY = window.innerHeight - footerHeight - this.element.offsetHeight;
269
+ x = Math.max(0, Math.min(x, maxX));
270
+ y = Math.max(minY, Math.min(y, maxY));
271
+
272
+ this.element.style.left = `${x}px`;
273
+ this.element.style.top = `${y}px`;
274
+ this.currentX = x;
275
+ this.currentY = y;
276
+
277
+ // Update edge distances after drag
278
+ this.updateEdgeDistances();
279
+ }
280
+
281
+ /**
282
+ * Update tracked distances from right and bottom edges
283
+ */
284
+ updateEdgeDistances() {
285
+ const viewportWidth = window.innerWidth;
286
+ const viewportHeight = window.innerHeight;
287
+ const footer = document.querySelector("footer");
288
+ const footerHeight = footer ? footer.offsetHeight : 0;
289
+ const maxBottom = viewportHeight - footerHeight;
290
+ const centerX = this.currentX + this.currentWidth / 2;
291
+ const centerY = this.currentY + this.currentHeight / 2;
292
+
293
+ // Track distance from right edge if window is on the right half
294
+ if (centerX > viewportWidth / 2) {
295
+ this.distanceFromRight =
296
+ viewportWidth - (this.currentX + this.currentWidth);
297
+ } else {
298
+ this.distanceFromRight = null;
299
+ }
300
+
301
+ // Track distance from bottom edge (above footer) if window is on the bottom half
302
+ if (centerY > viewportHeight / 2) {
303
+ this.distanceFromBottom =
304
+ maxBottom - (this.currentY + this.currentHeight);
305
+ } else {
306
+ this.distanceFromBottom = null;
307
+ }
308
+
309
+ this.lastViewportWidth = viewportWidth;
310
+ this.lastViewportHeight = viewportHeight;
311
+ }
312
+
313
+ /**
314
+ * Start resizing the window
315
+ */
316
+ startResize(e, direction) {
317
+ if (this.onFocus) this.onFocus(this.id);
318
+ this.isResizing = true;
319
+ this.resizeDirection = direction;
320
+ this.element.classList.add("resizing");
321
+ const rect = this.element.getBoundingClientRect();
322
+ this.resizeStart = {
323
+ x: e.clientX,
324
+ y: e.clientY,
325
+ width: rect.width,
326
+ height: rect.height,
327
+ left: rect.left,
328
+ top: rect.top,
329
+ };
330
+ e.preventDefault();
331
+ e.stopPropagation();
332
+ }
333
+
334
+ /**
335
+ * Handle resize movement
336
+ */
337
+ resize(e) {
338
+ const dx = e.clientX - this.resizeStart.x;
339
+ const dy = e.clientY - this.resizeStart.y;
340
+ const dir = this.resizeDirection;
341
+
342
+ // Get header and footer heights for bounds checking
343
+ const header = document.querySelector("header");
344
+ const footer = document.querySelector("footer");
345
+ const minTop = header ? header.offsetHeight : 0;
346
+ const footerHeight = footer ? footer.offsetHeight : 0;
347
+ const maxBottom = window.innerHeight - footerHeight;
348
+
349
+ let newWidth = this.resizeStart.width;
350
+ let newHeight = this.resizeStart.height;
351
+ let newLeft = this.resizeStart.left;
352
+ let newTop = this.resizeStart.top;
353
+
354
+ // Calculate new dimensions based on direction
355
+ if (dir.includes("e")) {
356
+ newWidth = Math.min(this.maxWidth, Math.max(this.minWidth, this.resizeStart.width + dx));
357
+ }
358
+ if (dir.includes("w")) {
359
+ const proposedWidth = Math.min(this.maxWidth, this.resizeStart.width - dx);
360
+ if (proposedWidth >= this.minWidth) {
361
+ newWidth = proposedWidth;
362
+ newLeft = this.resizeStart.left + (this.resizeStart.width - proposedWidth);
363
+ }
364
+ }
365
+ if (dir.includes("s")) {
366
+ newHeight = Math.min(this.maxHeight, Math.max(this.minHeight, this.resizeStart.height + dy));
367
+ }
368
+ if (dir.includes("n")) {
369
+ const proposedHeight = Math.min(this.maxHeight, this.resizeStart.height - dy);
370
+ if (proposedHeight >= this.minHeight) {
371
+ newHeight = proposedHeight;
372
+ newTop = this.resizeStart.top + (this.resizeStart.height - proposedHeight);
373
+ }
374
+ }
375
+
376
+ // Keep on screen (respect header and footer)
377
+ newLeft = Math.max(0, newLeft);
378
+ newTop = Math.max(minTop, newTop);
379
+ if (newLeft + newWidth > window.innerWidth) {
380
+ newWidth = window.innerWidth - newLeft;
381
+ }
382
+ if (newTop + newHeight > maxBottom) {
383
+ newHeight = maxBottom - newTop;
384
+ }
385
+
386
+ // Re-apply minimum constraints after viewport clamping, shifting
387
+ // position if needed so the window stays on screen at its min size
388
+ if (newWidth < this.minWidth) {
389
+ newWidth = this.minWidth;
390
+ newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - newWidth));
391
+ }
392
+ if (newHeight < this.minHeight) {
393
+ newHeight = this.minHeight;
394
+ newTop = Math.max(minTop, Math.min(newTop, maxBottom - newHeight));
395
+ }
396
+
397
+ this.element.style.width = `${newWidth}px`;
398
+ this.element.style.height = `${newHeight}px`;
399
+ this.element.style.left = `${newLeft}px`;
400
+ this.element.style.top = `${newTop}px`;
401
+ this.currentWidth = newWidth;
402
+ this.currentHeight = newHeight;
403
+ this.currentX = newLeft;
404
+ this.currentY = newTop;
405
+ }
406
+
407
+ /**
408
+ * Show the window
409
+ */
410
+ show() {
411
+ this.element.classList.remove("hidden");
412
+ this.isVisible = true;
413
+ // Ensure window is within viewport when shown
414
+ this.constrainToViewport();
415
+ if (this.onFocus) this.onFocus(this.id);
416
+ }
417
+
418
+ /**
419
+ * Hide the window
420
+ */
421
+ hide() {
422
+ // Set visibility flag first so getState() returns correct value
423
+ this.isVisible = false;
424
+ // Save state BEFORE adding hidden class, since getBoundingClientRect returns zeros for display:none
425
+ if (this.onStateChange) this.onStateChange();
426
+ this.saveSettings();
427
+ this.element.classList.add("hidden");
428
+ // Refocus canvas for keyboard input
429
+ const canvas = document.getElementById("screen");
430
+ if (canvas) {
431
+ setTimeout(() => canvas.focus(), 0);
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Toggle window visibility
437
+ */
438
+ toggle() {
439
+ if (this.isVisible) {
440
+ this.hide();
441
+ } else {
442
+ this.show();
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Set window z-index
448
+ */
449
+ setZIndex(z) {
450
+ this.zIndex = z;
451
+ this.element.style.zIndex = z;
452
+ }
453
+
454
+ /**
455
+ * Whether this window supports resizing
456
+ */
457
+ get isResizable() {
458
+ return this.resizeDirections.length > 0;
459
+ }
460
+
461
+ /**
462
+ * Get window state for persistence
463
+ */
464
+ getState() {
465
+ // Use tracked values instead of getBoundingClientRect which returns zeros for hidden elements
466
+ const state = {
467
+ x: this.currentX,
468
+ y: this.currentY,
469
+ visible: this.isVisible,
470
+ zIndex: this.zIndex,
471
+ };
472
+ // Only persist size for resizable windows; fixed-size windows always use their defaults
473
+ if (this.isResizable) {
474
+ state.width = this.currentWidth;
475
+ state.height = this.currentHeight;
476
+ }
477
+ return state;
478
+ }
479
+
480
+ /**
481
+ * Restore window state from persistence
482
+ */
483
+ restoreState(state) {
484
+ if (state.x !== undefined) {
485
+ this.element.style.left = `${state.x}px`;
486
+ this.currentX = state.x;
487
+ }
488
+ if (state.y !== undefined) {
489
+ this.element.style.top = `${state.y}px`;
490
+ this.currentY = state.y;
491
+ }
492
+ // Only restore size for resizable windows; fixed-size windows keep their defaults
493
+ if (this.isResizable) {
494
+ if (state.width !== undefined) {
495
+ const width = Math.min(this.maxWidth, Math.max(state.width, this.minWidth));
496
+ this.element.style.width = `${width}px`;
497
+ this.currentWidth = width;
498
+ }
499
+ if (state.height !== undefined) {
500
+ const height = Math.min(this.maxHeight, Math.max(state.height, this.minHeight));
501
+ this.element.style.height = `${height}px`;
502
+ this.currentHeight = height;
503
+ }
504
+ }
505
+
506
+ // Ensure window is within current viewport bounds
507
+ this.constrainToViewport();
508
+
509
+ // Calculate edge distances based on restored position
510
+ this.updateEdgeDistances();
511
+
512
+ if (state.visible) {
513
+ this.show();
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Constrain window position to keep it within the visible viewport
519
+ * Maintains distance from right/bottom edges for windows on those sides
520
+ */
521
+ constrainToViewport() {
522
+ if (!this.element) return;
523
+
524
+ const viewportWidth = window.innerWidth;
525
+ const viewportHeight = window.innerHeight;
526
+ const width = this.currentWidth;
527
+ const height = this.currentHeight;
528
+
529
+ // Get header and footer heights to prevent windows going under/over them
530
+ const header = document.querySelector("header");
531
+ const footer = document.querySelector("footer");
532
+ const minTop = header ? header.offsetHeight : 0;
533
+ const footerHeight = footer ? footer.offsetHeight : 0;
534
+ const maxBottom = viewportHeight - footerHeight;
535
+
536
+ let newLeft = this.currentX;
537
+ let newTop = this.currentY;
538
+ let changed = false;
539
+
540
+ // If window was on the right side, maintain distance from right edge
541
+ if (this.distanceFromRight !== null) {
542
+ const targetLeft = viewportWidth - width - this.distanceFromRight;
543
+ if (targetLeft !== newLeft) {
544
+ newLeft = targetLeft;
545
+ changed = true;
546
+ }
547
+ }
548
+
549
+ // If window was on the bottom side, maintain distance from bottom edge
550
+ if (this.distanceFromBottom !== null) {
551
+ const targetTop = maxBottom - height - this.distanceFromBottom;
552
+ if (targetTop !== newTop) {
553
+ newTop = targetTop;
554
+ changed = true;
555
+ }
556
+ }
557
+
558
+ // Ensure window stays within viewport bounds
559
+ if (width >= viewportWidth) {
560
+ newLeft = 0;
561
+ changed = true;
562
+ } else if (newLeft + width > viewportWidth) {
563
+ newLeft = viewportWidth - width;
564
+ changed = true;
565
+ } else if (newLeft < 0) {
566
+ newLeft = 0;
567
+ changed = true;
568
+ }
569
+
570
+ if (height >= maxBottom - minTop) {
571
+ newTop = minTop;
572
+ changed = true;
573
+ } else if (newTop + height > maxBottom) {
574
+ newTop = maxBottom - height;
575
+ changed = true;
576
+ } else if (newTop < minTop) {
577
+ newTop = minTop;
578
+ changed = true;
579
+ }
580
+
581
+ if (changed) {
582
+ this.element.style.left = `${newLeft}px`;
583
+ this.element.style.top = `${newTop}px`;
584
+ this.currentX = newLeft;
585
+ this.currentY = newTop;
586
+ }
587
+
588
+ // Update viewport tracking
589
+ this.lastViewportWidth = viewportWidth;
590
+ this.lastViewportHeight = viewportHeight;
591
+ }
592
+
593
+ /**
594
+ * Override in subclasses to provide window content HTML
595
+ */
596
+ renderContent() {
597
+ return "<p>Override renderContent() in subclass</p>";
598
+ }
599
+
600
+ /**
601
+ * Override in subclasses to update window content.
602
+ * Only called when the window is visible.
603
+ */
604
+ update(wasmModule) {
605
+ // Override in subclasses
606
+ }
607
+
608
+ /**
609
+ * Override in subclasses to set up additional event listeners
610
+ */
611
+ setupContentEventListeners() {
612
+ // Override in subclasses
613
+ }
614
+
615
+ /**
616
+ * Helper to format a hex byte
617
+ */
618
+ formatHex(value, digits = 2) {
619
+ return value.toString(16).toUpperCase().padStart(digits, "0");
620
+ }
621
+
622
+ /**
623
+ * Helper to format a hex address
624
+ */
625
+ formatAddr(value) {
626
+ return "$" + this.formatHex(value, 4);
627
+ }
628
+
629
+ /**
630
+ * Save window state to localStorage (if storageKey is configured)
631
+ */
632
+ saveSettings() {
633
+ if (!this.storageKey) return;
634
+ try {
635
+ const settings = {
636
+ x: this.currentX,
637
+ y: this.currentY,
638
+ visible: this.isVisible,
639
+ };
640
+ if (this.isResizable) {
641
+ settings.width = this.currentWidth;
642
+ settings.height = this.currentHeight;
643
+ }
644
+ localStorage.setItem(this.storageKey, JSON.stringify(settings));
645
+ } catch (e) {
646
+ console.warn(`Failed to save ${this.storageKey} settings:`, e.message);
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Load window state from localStorage (if storageKey is configured)
652
+ */
653
+ loadSettings() {
654
+ if (!this.storageKey) return;
655
+ try {
656
+ const saved = localStorage.getItem(this.storageKey);
657
+ if (saved) {
658
+ const state = JSON.parse(saved);
659
+ if (state.x !== undefined) this.currentX = state.x;
660
+ if (state.y !== undefined) this.currentY = state.y;
661
+
662
+ this.element.style.left = `${this.currentX}px`;
663
+ this.element.style.top = `${this.currentY}px`;
664
+
665
+ // Only restore size for resizable windows; fixed-size windows keep their defaults
666
+ if (this.isResizable) {
667
+ if (state.width !== undefined)
668
+ this.currentWidth = Math.min(this.maxWidth, Math.max(state.width, this.minWidth));
669
+ if (state.height !== undefined)
670
+ this.currentHeight = Math.min(this.maxHeight, Math.max(state.height, this.minHeight));
671
+ this.element.style.width = `${this.currentWidth}px`;
672
+ this.element.style.height = `${this.currentHeight}px`;
673
+ }
674
+ }
675
+ } catch (e) {
676
+ console.warn(`Failed to load ${this.storageKey} settings:`, e.message);
677
+ }
678
+ }
679
+
680
+ /**
681
+ * Clean up event listeners and remove element from DOM
682
+ */
683
+ destroy() {
684
+ document.removeEventListener("mousemove", this.handleMouseMove);
685
+ document.removeEventListener("mouseup", this.handleMouseUp);
686
+ if (this.element && this.element.parentNode) {
687
+ this.element.parentNode.removeChild(this.element);
688
+ }
689
+ }
690
+ }
@@ -0,0 +1,9 @@
1
+ /*
2
+ * index.js - Windows subsystem initialization and exports
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ export { BaseWindow } from "./base-window.js";
9
+ export { WindowManager } from "./window-manager.js";