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,462 @@
1
+ /*
2
+ * text-selection.js - Screen text selection and copy support
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ /**
9
+ * TextSelection - Enable text selection and copying from the Apple II screen
10
+ *
11
+ * Handles mouse selection on the canvas and converts screen memory to text.
12
+ * Character encoding and screen memory reading are handled by C++ core.
13
+ */
14
+
15
+ export class TextSelection {
16
+ constructor(canvas, wasmModule, renderer) {
17
+ this.canvas = canvas;
18
+ this.wasmModule = wasmModule;
19
+ this.renderer = renderer || null;
20
+
21
+ // Selection state
22
+ this.isSelecting = false;
23
+ this.selectionStart = null; // {row, col}
24
+ this.selectionEnd = null; // {row, col}
25
+
26
+ // Overlay canvas for selection highlight
27
+ this.overlay = null;
28
+ this.overlayCtx = null;
29
+
30
+ // Screen dimensions
31
+ this.charWidth40 = 14; // 40-col: 14 pixels per char (7 * 2)
32
+ this.charWidth80 = 7; // 80-col: 7 pixels per char
33
+ this.charHeight = 16; // 16 pixels per char (8 * 2)
34
+ this.rows = 24;
35
+
36
+ // Bind event handlers for proper cleanup
37
+ this.boundOnMouseDown = this.onMouseDown.bind(this);
38
+ this.boundOnMouseMove = this.onMouseMove.bind(this);
39
+ this.boundOnMouseUp = this.onMouseUp.bind(this);
40
+ this.boundOnKeyDown = this.onKeyDown.bind(this);
41
+ this.boundOnContextMenu = this.onContextMenu.bind(this);
42
+
43
+ this.setupOverlay();
44
+ this.setupEventListeners();
45
+ }
46
+
47
+ setupOverlay() {
48
+ this.overlay = document.createElement('canvas');
49
+ this.overlay.width = 560;
50
+ this.overlay.height = 384;
51
+ this.overlayCtx = this.overlay.getContext('2d');
52
+ }
53
+
54
+ setupEventListeners() {
55
+ // mousedown on canvas starts selection
56
+ this.canvas.addEventListener('mousedown', this.boundOnMouseDown);
57
+ // keydown in capture phase so we can intercept before input handler
58
+ document.addEventListener('keydown', this.boundOnKeyDown, true);
59
+ this.canvas.addEventListener('contextmenu', this.boundOnContextMenu);
60
+ }
61
+
62
+ /**
63
+ * Get soft switch state once and extract all mode flags
64
+ * Soft switch bits from emulator (see emulator.cpp getSoftSwitchState):
65
+ * Bit 0: TEXT mode
66
+ * Bit 1: MIXED mode
67
+ * Bit 2: PAGE2
68
+ * Bit 3: HIRES
69
+ * Bit 4: 80COL
70
+ * Bit 5: ALTCHARSET
71
+ * @returns {{textMode: boolean, col80: boolean, page2: boolean}}
72
+ */
73
+ getDisplayMode() {
74
+ if (!this.wasmModule._getSoftSwitchState) {
75
+ return { textMode: false, col80: false, page2: false };
76
+ }
77
+
78
+ const state = this.wasmModule._getSoftSwitchState();
79
+ return {
80
+ textMode: (state & 0x01) !== 0, // Bit 0: TEXT mode
81
+ col80: (state & 0x10) !== 0, // Bit 4: 80COL mode
82
+ page2: (state & 0x04) !== 0 // Bit 2: PAGE2 (NOT bit 3 which is HIRES!)
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Check if the emulator is in text mode
88
+ */
89
+ isTextMode() {
90
+ return this.getDisplayMode().textMode;
91
+ }
92
+
93
+ /**
94
+ * Check if 80-column mode is active
95
+ */
96
+ is80ColumnMode() {
97
+ return this.getDisplayMode().col80;
98
+ }
99
+
100
+ /**
101
+ * Convert canvas pixel coordinates to character position.
102
+ * Mirrors the shader transform pipeline (curveUV → applyOverscan → applyScreenMargin)
103
+ * so the mouse mapping matches the curved on-screen content.
104
+ */
105
+ pixelToChar(x, y) {
106
+ const rect = this.canvas.getBoundingClientRect();
107
+ const params = this.renderer && this.renderer.crtParams || {};
108
+
109
+ // Convert mouse position to normalised UV (0–1)
110
+ let u = (x - rect.left) / rect.width;
111
+ let v = (y - rect.top) / rect.height;
112
+
113
+ // Apply CRT curvature (matches curveUV in crt.glsl)
114
+ const curvature = params.curvature || 0;
115
+ if (curvature > 0.001) {
116
+ const cx = u - 0.5;
117
+ const cy = v - 0.5;
118
+ const dist = cx * cx + cy * cy;
119
+ const distortion = dist * curvature * 0.5;
120
+ u += cx * distortion;
121
+ v += cy * distortion;
122
+ }
123
+
124
+ // Apply overscan (matches applyOverscan in crt.glsl)
125
+ const overscan = params.overscan || 0;
126
+ if (overscan > 0.001) {
127
+ const borderSize = overscan * 0.1;
128
+ const scale = 1.0 - borderSize * 2.0;
129
+ u = (u - 0.5) / scale + 0.5;
130
+ v = (v - 0.5) / scale + 0.5;
131
+ }
132
+
133
+ // Apply screen margin (matches applyScreenMargin in crt.glsl)
134
+ const margin = params.screenMargin || 0;
135
+ if (margin > 0.001) {
136
+ const scale = 1.0 / (1.0 - margin * 2.0);
137
+ u = (u - 0.5) * scale + 0.5;
138
+ v = (v - 0.5) * scale + 0.5;
139
+ }
140
+
141
+ // u,v are now contentUV — map to 560×384 texture space
142
+ const contentX = u * 560;
143
+ const contentY = v * 384;
144
+
145
+ const mode = this.getDisplayMode();
146
+ const cols = mode.col80 ? 80 : 40;
147
+ const charWidth = mode.col80 ? this.charWidth80 : this.charWidth40;
148
+
149
+ const col = Math.floor(contentX / charWidth);
150
+ const row = Math.floor(contentY / this.charHeight);
151
+
152
+ return {
153
+ row: Math.max(0, Math.min(this.rows - 1, row)),
154
+ col: Math.max(0, Math.min(cols - 1, col))
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Normalize selection so start is always before end
160
+ * @returns {{startRow, startCol, endRow, endCol}} Normalized coordinates
161
+ */
162
+ normalizeSelection() {
163
+ if (!this.selectionStart || !this.selectionEnd) {
164
+ return null;
165
+ }
166
+
167
+ let startRow = this.selectionStart.row;
168
+ let startCol = this.selectionStart.col;
169
+ let endRow = this.selectionEnd.row;
170
+ let endCol = this.selectionEnd.col;
171
+
172
+ if (startRow > endRow || (startRow === endRow && startCol > endCol)) {
173
+ [startRow, endRow] = [endRow, startRow];
174
+ [startCol, endCol] = [endCol, startCol];
175
+ }
176
+
177
+ return { startRow, startCol, endRow, endCol };
178
+ }
179
+
180
+ /**
181
+ * Get selected text as a string (delegates to C++ for screen memory reading and decoding)
182
+ */
183
+ getSelectedText() {
184
+ const sel = this.normalizeSelection();
185
+ if (!sel) return '';
186
+
187
+ const resultPtr = this.wasmModule._readScreenText(
188
+ sel.startRow, sel.startCol, sel.endRow, sel.endCol
189
+ );
190
+ return resultPtr ? this.wasmModule.UTF8ToString(resultPtr) : '';
191
+ }
192
+
193
+ /**
194
+ * Copy selected text to clipboard
195
+ */
196
+ async copyToClipboard() {
197
+ const text = this.getSelectedText();
198
+ if (!text) return false;
199
+
200
+ try {
201
+ await navigator.clipboard.writeText(text);
202
+ this.showCopyFeedback();
203
+ return true;
204
+ } catch (err) {
205
+ console.error('Failed to copy text:', err);
206
+ return false;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Show visual feedback when text is copied
212
+ */
213
+ showCopyFeedback() {
214
+ const ctx = this.overlayCtx;
215
+ const copyFlashColor = getComputedStyle(document.documentElement)
216
+ .getPropertyValue('--selection-copy-flash').trim() || 'rgba(63, 185, 80, 0.5)';
217
+ ctx.fillStyle = copyFlashColor;
218
+ this.drawSelectionHighlight();
219
+
220
+ setTimeout(() => {
221
+ this.drawSelectionHighlight();
222
+ }, 150);
223
+ }
224
+
225
+ /**
226
+ * Draw the selection highlight on the overlay
227
+ */
228
+ drawSelectionHighlight() {
229
+ const ctx = this.overlayCtx;
230
+ ctx.clearRect(0, 0, 560, 384);
231
+
232
+ const mode = this.getDisplayMode();
233
+ if (!mode.textMode) { this.uploadOverlay(); return; }
234
+
235
+ const sel = this.normalizeSelection();
236
+ if (!sel) { this.uploadOverlay(); return; }
237
+
238
+ const cols = mode.col80 ? 80 : 40;
239
+ const charWidth = mode.col80 ? this.charWidth80 : this.charWidth40;
240
+
241
+ const highlightColor = getComputedStyle(document.documentElement)
242
+ .getPropertyValue('--selection-highlight').trim() || 'rgba(88, 166, 255, 0.35)';
243
+ ctx.fillStyle = highlightColor;
244
+
245
+ for (let row = sel.startRow; row <= sel.endRow; row++) {
246
+ const colStart = (row === sel.startRow) ? sel.startCol : 0;
247
+ const colEnd = (row === sel.endRow) ? sel.endCol : cols - 1;
248
+
249
+ const x = colStart * charWidth;
250
+ const y = row * this.charHeight;
251
+ const width = (colEnd - colStart + 1) * charWidth;
252
+ const height = this.charHeight;
253
+
254
+ ctx.fillRect(x, y, width, height);
255
+ }
256
+
257
+ this.uploadOverlay();
258
+ }
259
+
260
+ uploadOverlay() {
261
+ if (this.renderer && this.renderer.updateSelectionTexture) {
262
+ this.renderer.updateSelectionTexture(this.overlay);
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Move overlay to the canvas's current parent and re-measure.
268
+ * Called when the canvas is reparented (e.g. for full-page mode).
269
+ */
270
+ reattach() {
271
+ // Overlay is offscreen; nothing to reparent
272
+ }
273
+
274
+ /**
275
+ * Clear the selection
276
+ */
277
+ clearSelection() {
278
+ this.selectionStart = null;
279
+ this.selectionEnd = null;
280
+ this.isSelecting = false;
281
+
282
+ if (this.overlayCtx) {
283
+ this.overlayCtx.clearRect(0, 0, 560, 384);
284
+ this.uploadOverlay();
285
+ }
286
+ }
287
+
288
+ // Event handlers
289
+
290
+ onMouseDown(e) {
291
+ if (!this.isTextMode()) return;
292
+ if (e.button !== 0) return;
293
+
294
+ const pos = this.pixelToChar(e.clientX, e.clientY);
295
+ this.selectionStart = pos;
296
+ this.selectionEnd = pos;
297
+ this.isSelecting = true;
298
+
299
+ // Add document-level listeners to track selection even outside canvas
300
+ document.addEventListener('mousemove', this.boundOnMouseMove);
301
+ document.addEventListener('mouseup', this.boundOnMouseUp);
302
+
303
+ this.drawSelectionHighlight();
304
+ e.preventDefault();
305
+ }
306
+
307
+ onMouseMove(e) {
308
+ if (!this.isSelecting) return;
309
+ if (!this.isTextMode()) {
310
+ this.clearSelection();
311
+ return;
312
+ }
313
+
314
+ const pos = this.pixelToChar(e.clientX, e.clientY);
315
+ this.selectionEnd = pos;
316
+
317
+ this.drawSelectionHighlight();
318
+ }
319
+
320
+ onMouseUp(e) {
321
+ if (!this.isSelecting) return;
322
+
323
+ this.isSelecting = false;
324
+
325
+ // Remove document-level listeners
326
+ document.removeEventListener('mousemove', this.boundOnMouseMove);
327
+ document.removeEventListener('mouseup', this.boundOnMouseUp);
328
+
329
+ // Single click (no drag) - clear selection
330
+ if (this.selectionStart && this.selectionEnd &&
331
+ this.selectionStart.row === this.selectionEnd.row &&
332
+ this.selectionStart.col === this.selectionEnd.col) {
333
+ this.clearSelection();
334
+ }
335
+ }
336
+
337
+ onKeyDown(e) {
338
+ // Ctrl+C / Cmd+C to copy selection
339
+ // Use e.code for reliable detection across platforms
340
+ const isCopyKey = e.code === 'KeyC' || e.key === 'c' || e.key === 'C';
341
+ if ((e.ctrlKey || e.metaKey) && isCopyKey) {
342
+ if (this.selectionStart && this.selectionEnd) {
343
+ this.copyToClipboard();
344
+ e.preventDefault();
345
+ e.stopPropagation(); // Prevent key from reaching emulator
346
+ return;
347
+ }
348
+ }
349
+
350
+ // Escape to clear selection
351
+ if (e.key === 'Escape' && this.selectionStart) {
352
+ this.clearSelection();
353
+ e.stopPropagation(); // Prevent key from reaching emulator
354
+ }
355
+ }
356
+
357
+ onContextMenu(e) {
358
+ if (!this.isTextMode()) return;
359
+
360
+ if (this.selectionStart && this.selectionEnd) {
361
+ e.preventDefault();
362
+ this.showContextMenu(e.clientX, e.clientY);
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Show a simple context menu with copy option
368
+ */
369
+ showContextMenu(x, y) {
370
+ const existing = document.querySelector('.text-select-context-menu');
371
+ if (existing) existing.remove();
372
+
373
+ const menu = document.createElement('div');
374
+ menu.className = 'text-select-context-menu';
375
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
376
+ const copyShortcut = isMac ? '⌘C' : 'Ctrl+C';
377
+ menu.innerHTML = `
378
+ <button class="context-menu-item" data-action="copy">
379
+ <span>Copy</span>
380
+ <span class="shortcut">${copyShortcut}</span>
381
+ </button>
382
+ <button class="context-menu-item" data-action="select-all">
383
+ <span>Select All</span>
384
+ </button>
385
+ `;
386
+
387
+ // Position menu, ensuring it stays within viewport
388
+ const menuWidth = 150;
389
+ const menuHeight = 70;
390
+ const left = Math.min(x, window.innerWidth - menuWidth - 10);
391
+ const top = Math.min(y, window.innerHeight - menuHeight - 10);
392
+
393
+ menu.style.left = left + 'px';
394
+ menu.style.top = top + 'px';
395
+
396
+ document.body.appendChild(menu);
397
+
398
+ const handleClick = (e) => {
399
+ const item = e.target.closest('.context-menu-item');
400
+ if (!item) return;
401
+
402
+ const action = item.dataset.action;
403
+ if (action === 'copy') {
404
+ this.copyToClipboard();
405
+ } else if (action === 'select-all') {
406
+ this.selectAll();
407
+ }
408
+
409
+ menu.remove();
410
+ };
411
+
412
+ menu.addEventListener('click', handleClick);
413
+
414
+ const closeMenu = (e) => {
415
+ if (!menu.contains(e.target)) {
416
+ menu.remove();
417
+ document.removeEventListener('mousedown', closeMenu);
418
+ }
419
+ };
420
+
421
+ setTimeout(() => {
422
+ document.addEventListener('mousedown', closeMenu);
423
+ }, 0);
424
+ }
425
+
426
+ /**
427
+ * Select all text on screen
428
+ */
429
+ selectAll() {
430
+ if (!this.isTextMode()) return;
431
+
432
+ const cols = this.is80ColumnMode() ? 80 : 40;
433
+
434
+ this.selectionStart = { row: 0, col: 0 };
435
+ this.selectionEnd = { row: this.rows - 1, col: cols - 1 };
436
+
437
+ this.drawSelectionHighlight();
438
+ }
439
+
440
+ /**
441
+ * Update overlay size and position when canvas resizes
442
+ */
443
+ resize() {
444
+ // Overlay is offscreen; no DOM positioning needed
445
+ }
446
+
447
+ /**
448
+ * Clean up resources and remove event listeners
449
+ */
450
+ destroy() {
451
+ // Remove event listeners
452
+ this.canvas.removeEventListener('mousedown', this.boundOnMouseDown);
453
+ document.removeEventListener('mousemove', this.boundOnMouseMove);
454
+ document.removeEventListener('mouseup', this.boundOnMouseUp);
455
+ document.removeEventListener('keydown', this.boundOnKeyDown, true); // capture phase
456
+ this.canvas.removeEventListener('contextmenu', this.boundOnContextMenu);
457
+
458
+ // Remove any open context menu
459
+ const menu = document.querySelector('.text-select-context-menu');
460
+ if (menu) menu.remove();
461
+ }
462
+ }