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,1261 @@
1
+ /*
2
+ * index.js - File explorer window for browsing Apple II disk images
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ /**
9
+ * FileExplorerWindow - Browse and view contents of Apple II disk images
10
+ * Extends BaseWindow to inherit drag/resize/show/hide functionality
11
+ */
12
+
13
+ import { BaseWindow } from "../windows/base-window.js";
14
+ import {
15
+ formatFileContents,
16
+ formatFileSize,
17
+ formatHexDump,
18
+ formatMerlinFile,
19
+ checkIsMerlinFile,
20
+ setFileViewerWasm,
21
+ } from "./file-viewer.js";
22
+ import { disassemble, setWasmModule } from "./disassembler.js";
23
+ import { escapeHtml } from "../utils/string-utils.js";
24
+
25
+ // File type description tables (UI display only - parsing logic is in C++)
26
+ const DOS33_FILE_DESCRIPTIONS = {
27
+ 0x00: "Text",
28
+ 0x01: "Integer BASIC",
29
+ 0x02: "Applesoft BASIC",
30
+ 0x04: "Binary",
31
+ 0x08: "Type S",
32
+ 0x10: "Relocatable",
33
+ 0x20: "Type a",
34
+ 0x40: "Type b",
35
+ };
36
+
37
+ const PASCAL_FILE_DESCRIPTIONS = {
38
+ 0: "Volume",
39
+ 1: "Bad Blocks",
40
+ 2: "Code",
41
+ 3: "Text",
42
+ 4: "Info",
43
+ 5: "Data",
44
+ 6: "Graphics",
45
+ 7: "Photo",
46
+ 8: "Secure Dir",
47
+ };
48
+
49
+ const PRODOS_FILE_DESCRIPTIONS = {
50
+ 0x00: "Unknown",
51
+ 0x01: "Bad Block",
52
+ 0x04: "Text",
53
+ 0x06: "Binary",
54
+ 0x0f: "Directory",
55
+ 0x19: "AppleWorks DB",
56
+ 0x1a: "AppleWorks WP",
57
+ 0x1b: "AppleWorks SS",
58
+ 0xb0: "Source Code",
59
+ 0xb3: "GS/OS App",
60
+ 0xbf: "Document",
61
+ 0xc0: "Packed HiRes",
62
+ 0xc1: "HiRes Picture",
63
+ 0xe0: "ShrinkIt Archive",
64
+ 0xef: "Pascal",
65
+ 0xf0: "Command",
66
+ 0xfa: "Integer BASIC",
67
+ 0xfb: "Integer Vars",
68
+ 0xfc: "Applesoft BASIC",
69
+ 0xfd: "Applesoft Vars",
70
+ 0xfe: "Relocatable",
71
+ 0xff: "System",
72
+ };
73
+
74
+ export class FileExplorerWindow extends BaseWindow {
75
+ constructor(wasmModule) {
76
+ // Configure BaseWindow with file explorer specific settings
77
+ super({
78
+ id: "file-explorer-window",
79
+ title: "File Explorer",
80
+ minWidth: 400,
81
+ minHeight: 300,
82
+ defaultWidth: 700,
83
+ defaultHeight: 500,
84
+ storageKey: "a2e-file-explorer",
85
+ });
86
+
87
+ this.wasmModule = wasmModule;
88
+
89
+ // Initialize the disassembler and file viewer with the WASM module
90
+ setWasmModule(wasmModule);
91
+ setFileViewerWasm(wasmModule);
92
+
93
+ // Content state
94
+ this.sourceType = "floppy"; // 'floppy' or 'hd'
95
+ this.selectedDrive = 0;
96
+ this.catalog = [];
97
+ this.selectedFile = null;
98
+ this.diskDataPtr = 0; // Pointer to disk data in WASM heap
99
+ this.diskDataSize = 0; // Size of disk data
100
+ this.diskFormat = null; // 'dos33' | 'prodos' | 'pascal' | null
101
+ this.currentPath = ""; // Current directory path for ProDOS navigation
102
+ this.directoryStack = []; // Stack of {path, startBlock} for HD navigation
103
+ this.binaryViewMode = "asm"; // 'asm', 'hex', or 'merlin'
104
+ this.textViewMode = "text"; // 'text' or 'merlin'
105
+ this.currentFileData = null; // Cache for current file data
106
+ this.basicLineNumToIndex = null; // For BASIC GOTO/GOSUB navigation
107
+ this.basicOriginalHtml = null; // Original unhighlighted BASIC content
108
+
109
+ // Hex view dynamic column state
110
+ this.hexDisplayState = null; // { data, baseAddress, maxBytes }
111
+ this.hexResizeObserver = null;
112
+ this.hexBytesPerRow = 16;
113
+
114
+ // Bind handlers
115
+ this.handleBasicLineClick = this.handleBasicLineClick.bind(this);
116
+ }
117
+
118
+ /**
119
+ * Override renderContent to provide file explorer specific content
120
+ */
121
+ renderContent() {
122
+ return `
123
+ <div class="fe-toolbar">
124
+ <div class="fe-source-selector hidden">
125
+ <button class="fe-source-btn active" data-source="floppy">Floppy</button>
126
+ <button class="fe-source-btn" data-source="hd">HD</button>
127
+ </div>
128
+ <div class="fe-drive-selector">
129
+ <label>Drive:</label>
130
+ <button class="fe-drive-btn active" data-drive="0">1</button>
131
+ <button class="fe-drive-btn" data-drive="1">2</button>
132
+ </div>
133
+ <button class="fe-refresh-btn" title="Refresh catalog">Refresh</button>
134
+ <span class="fe-disk-info"></span>
135
+ </div>
136
+ <div class="fe-content">
137
+ <div class="fe-catalog-panel">
138
+ <div class="fe-panel-header">Catalog</div>
139
+ <div class="fe-path-bar hidden"></div>
140
+ <div class="fe-catalog-list"></div>
141
+ </div>
142
+ <div class="fe-file-panel">
143
+ <div class="fe-panel-header">
144
+ <span class="fe-file-title">Select a file</span>
145
+ <span class="fe-file-info"></span>
146
+ <div class="fe-view-toggle hidden">
147
+ <button class="fe-view-btn active" data-view="asm" title="DISASSEMBLE">Disassemble</button>
148
+ <button class="fe-view-btn" data-view="hex" title="Hex dump">HEX</button>
149
+ <button class="fe-view-btn" data-view="merlin" title="Merlin source">MERLIN</button>
150
+ </div>
151
+ <div class="fe-text-view-toggle hidden">
152
+ <button class="fe-view-btn active" data-view="text" title="Plain text">TEXT</button>
153
+ <button class="fe-view-btn" data-view="merlin" title="Merlin source">MERLIN</button>
154
+ </div>
155
+ </div>
156
+ <div class="fe-asm-legend hidden">
157
+ <span class="dis-branch">Jump/Branch</span>
158
+ <span class="dis-load">Load/Store</span>
159
+ <span class="dis-math">Math/Logic</span>
160
+ <span class="dis-stack">Stack/Reg</span>
161
+ <span class="dis-address">Address</span>
162
+ <span class="dis-immediate">Immediate</span>
163
+ <span class="dis-data">Data</span>
164
+ </div>
165
+ <div class="fe-hex-legend hidden">
166
+ <span class="hex-legend-printable">Printable</span>
167
+ <span class="hex-legend-control">Control</span>
168
+ <span class="hex-legend-highbit">High Bit</span>
169
+ <span class="hex-legend-zero">Zero</span>
170
+ </div>
171
+ <div class="fe-file-content"></div>
172
+ </div>
173
+ </div>
174
+ `;
175
+ }
176
+
177
+ /**
178
+ * Called after the window is created - set up file explorer specific event listeners
179
+ */
180
+ onContentRendered() {
181
+ // Load saved settings
182
+ this.loadSettings();
183
+
184
+ // Source selector (Floppy / HD)
185
+ const sourceSelector = this.element.querySelector(".fe-source-selector");
186
+ const sourceBtns = this.element.querySelectorAll(".fe-source-btn");
187
+ sourceBtns.forEach((btn) => {
188
+ btn.addEventListener("click", () => {
189
+ const newSource = btn.dataset.source;
190
+ if (newSource === this.sourceType) return;
191
+ sourceBtns.forEach((b) => b.classList.remove("active"));
192
+ btn.classList.add("active");
193
+ this.sourceType = newSource;
194
+ this.selectedDrive = 0;
195
+ this.updateDriveButtons();
196
+ this.loadDisk();
197
+ });
198
+ });
199
+
200
+ // Drive selector
201
+ const driveBtns = this.element.querySelectorAll(".fe-drive-btn");
202
+ driveBtns.forEach((btn) => {
203
+ btn.addEventListener("click", () => {
204
+ driveBtns.forEach((b) => b.classList.remove("active"));
205
+ btn.classList.add("active");
206
+ this.selectedDrive = parseInt(btn.dataset.drive, 10);
207
+ this.loadDisk();
208
+ });
209
+ });
210
+
211
+ // Refresh button
212
+ const refreshBtn = this.element.querySelector(".fe-refresh-btn");
213
+ refreshBtn.addEventListener("click", () => this.loadDisk());
214
+
215
+ // Catalog item selection
216
+ const catalogList = this.element.querySelector(".fe-catalog-list");
217
+ catalogList.addEventListener("click", (e) => {
218
+ const item = e.target.closest(".fe-catalog-item");
219
+ if (item) {
220
+ // Check for parent directory navigation
221
+ if (item.dataset.action === "parent") {
222
+ const parts = this.currentPath.split("/");
223
+ parts.pop();
224
+ this.navigateToPath(parts.join("/"));
225
+ return;
226
+ }
227
+ const index = parseInt(item.dataset.index, 10);
228
+ if (!isNaN(index)) {
229
+ this.selectFile(index);
230
+ }
231
+ }
232
+ });
233
+
234
+ // Path bar breadcrumb navigation (ProDOS)
235
+ const pathBar = this.element.querySelector(".fe-path-bar");
236
+ pathBar.addEventListener("click", (e) => {
237
+ const pathItem = e.target.closest(".fe-path-item");
238
+ if (pathItem && !pathItem.matches(":last-child")) {
239
+ const path = pathItem.dataset.path || "";
240
+ this.navigateToPath(path);
241
+ }
242
+ });
243
+
244
+ // View toggle for binary files
245
+ const viewToggle = this.element.querySelector(".fe-view-toggle");
246
+ viewToggle.addEventListener("click", (e) => {
247
+ const btn = e.target.closest(".fe-view-btn");
248
+ if (btn) {
249
+ const view = btn.dataset.view;
250
+ if (view !== this.binaryViewMode) {
251
+ this._binaryViewManuallySet = true;
252
+ this.binaryViewMode = view;
253
+ viewToggle
254
+ .querySelectorAll(".fe-view-btn")
255
+ .forEach((b) => b.classList.remove("active"));
256
+ btn.classList.add("active");
257
+ this.showFileContents();
258
+ }
259
+ }
260
+ });
261
+
262
+ // View toggle for text files (TEXT/MERLIN)
263
+ const textViewToggle = this.element.querySelector(".fe-text-view-toggle");
264
+ textViewToggle.addEventListener("click", (e) => {
265
+ const btn = e.target.closest(".fe-view-btn");
266
+ if (btn) {
267
+ const view = btn.dataset.view;
268
+ if (view !== this.textViewMode) {
269
+ this._textViewManuallySet = true;
270
+ this.textViewMode = view;
271
+ textViewToggle
272
+ .querySelectorAll(".fe-view-btn")
273
+ .forEach((b) => b.classList.remove("active"));
274
+ btn.classList.add("active");
275
+ this.showFileContents();
276
+ }
277
+ }
278
+ });
279
+
280
+ // Resize observer for dynamic hex column count
281
+ this.setupHexResizeObserver();
282
+ }
283
+
284
+ /**
285
+ * Override show to also load the disk
286
+ */
287
+ show() {
288
+ super.show();
289
+ this.updateSourceSelector();
290
+ this.updateDriveButtons();
291
+ this.loadDisk();
292
+ }
293
+
294
+ /**
295
+ * Show the source selector only when SmartPort card is installed
296
+ */
297
+ updateSourceSelector() {
298
+ const sourceSelector = this.element.querySelector(".fe-source-selector");
299
+ const wasm = this.wasmModule;
300
+ const hasSmartPort = wasm._isSmartPortCardInstalled && wasm._isSmartPortCardInstalled();
301
+ sourceSelector.classList.toggle("hidden", !hasSmartPort);
302
+ // Sync button active state
303
+ sourceSelector.querySelectorAll(".fe-source-btn").forEach((b) => {
304
+ b.classList.toggle("active", b.dataset.source === this.sourceType);
305
+ });
306
+ }
307
+
308
+ /**
309
+ * Update drive button labels based on source type
310
+ */
311
+ updateDriveButtons() {
312
+ const driveBtns = this.element.querySelectorAll(".fe-drive-btn");
313
+ const label = this.element.querySelector(".fe-drive-selector label");
314
+ if (this.sourceType === "hd") {
315
+ label.textContent = "Device:";
316
+ } else {
317
+ label.textContent = "Drive:";
318
+ }
319
+ driveBtns.forEach((btn) => {
320
+ btn.classList.toggle("active", parseInt(btn.dataset.drive, 10) === this.selectedDrive);
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Open the file explorer showing a specific hard drive device
326
+ */
327
+ showHardDrive(deviceNum) {
328
+ this.sourceType = "hd";
329
+ this.selectedDrive = deviceNum;
330
+ this.show();
331
+ }
332
+
333
+ /**
334
+ * Open the file explorer showing a specific floppy drive
335
+ */
336
+ showFloppyDisk(driveNum) {
337
+ this.sourceType = "floppy";
338
+ this.selectedDrive = driveNum;
339
+ this.show();
340
+ }
341
+
342
+ loadDisk() {
343
+ if (this.sourceType === "hd") {
344
+ this.loadHardDrive();
345
+ return;
346
+ }
347
+ this.loadFloppyDisk();
348
+ }
349
+
350
+ loadHardDrive() {
351
+ const wasm = this.wasmModule;
352
+ const diskInfo = this.element.querySelector(".fe-disk-info");
353
+ const catalogList = this.element.querySelector(".fe-catalog-list");
354
+
355
+ // Check if HD image is inserted
356
+ if (!wasm._isSmartPortImageInserted(this.selectedDrive)) {
357
+ diskInfo.textContent = "No image inserted";
358
+ catalogList.innerHTML = '<div class="fe-empty">No image in device</div>';
359
+ this.catalog = [];
360
+ this.diskDataPtr = 0;
361
+ this.diskDataSize = 0;
362
+ this.diskFormat = null;
363
+ this.clearFileView();
364
+ return;
365
+ }
366
+
367
+ // Get block data pointer (raw ProDOS blocks, skipping any 2IMG header)
368
+ const sizePtr = wasm._malloc(4);
369
+ const dataPtr = wasm._getSmartPortBlockData(this.selectedDrive, sizePtr);
370
+ const size = new Uint32Array(wasm.HEAPU8.buffer, sizePtr, 1)[0];
371
+ wasm._free(sizePtr);
372
+
373
+ if (!dataPtr || size === 0) {
374
+ const filenamePtr = wasm._getSmartPortImageFilename(this.selectedDrive);
375
+ const filename = filenamePtr ? wasm.UTF8ToString(filenamePtr) : "Hard Drive";
376
+ diskInfo.textContent = filename;
377
+ catalogList.innerHTML =
378
+ '<div class="fe-empty">Cannot read block data</div>';
379
+ this.catalog = [];
380
+ this.diskDataPtr = 0;
381
+ this.diskDataSize = 0;
382
+ this.diskFormat = null;
383
+ this.clearFileView();
384
+ return;
385
+ }
386
+
387
+ this.diskDataPtr = dataPtr;
388
+ this.diskDataSize = size;
389
+
390
+ // Get filename
391
+ const filenamePtr = wasm._getSmartPortImageFilename(this.selectedDrive);
392
+ const filename = filenamePtr ? wasm.UTF8ToString(filenamePtr) : "Unknown";
393
+
394
+ // HD images - try ProDOS first, then Pascal
395
+ if (wasm._isProDOSFormat(dataPtr, size)) {
396
+ this.diskFormat = "prodos";
397
+ wasm._getProDOSVolumeInfo(dataPtr, size);
398
+ const volumeName = wasm.UTF8ToString(wasm._getProDOSVolumeName());
399
+ this.volumeInfo = { volumeName };
400
+ diskInfo.textContent = `${filename} (ProDOS: ${volumeName})`;
401
+
402
+ this.currentPath = "";
403
+ this.directoryStack = [];
404
+ this.loadProDOSDirectory(2, "");
405
+ } else if (wasm._isPascalFormat(dataPtr, size)) {
406
+ this.diskFormat = "pascal";
407
+ wasm._getPascalVolumeInfo(dataPtr, size);
408
+ const volumeName = wasm.UTF8ToString(wasm._getPascalVolumeName());
409
+ this.volumeInfo = { volumeName };
410
+ diskInfo.textContent = `${filename} (Pascal: ${volumeName})`;
411
+
412
+ this.loadPascalCatalog();
413
+ } else {
414
+ this.diskFormat = null;
415
+ diskInfo.textContent = `${filename} (Unknown format)`;
416
+ catalogList.innerHTML = '<div class="fe-empty">Not a ProDOS volume</div>';
417
+ this.catalog = [];
418
+ this.clearFileView();
419
+ return;
420
+ }
421
+
422
+ this.selectedFile = null;
423
+ this.clearFileView();
424
+ }
425
+
426
+ loadFloppyDisk() {
427
+ const wasm = this.wasmModule;
428
+ const diskInfo = this.element.querySelector(".fe-disk-info");
429
+ const catalogList = this.element.querySelector(".fe-catalog-list");
430
+
431
+ // Check if disk is inserted
432
+ if (!wasm._isDiskInserted(this.selectedDrive)) {
433
+ diskInfo.textContent = "No disk inserted";
434
+ catalogList.innerHTML = '<div class="fe-empty">No disk in drive</div>';
435
+ this.catalog = [];
436
+ this.diskDataPtr = 0;
437
+ this.diskDataSize = 0;
438
+ this.diskFormat = null;
439
+ this.clearFileView();
440
+ return;
441
+ }
442
+
443
+ // Get disk sector data pointer (stays in WASM heap)
444
+ const sizePtr = wasm._malloc(4);
445
+ const dataPtr = wasm._getDiskSectorData(this.selectedDrive, sizePtr);
446
+ const size = new Uint32Array(wasm.HEAPU8.buffer, sizePtr, 1)[0];
447
+ wasm._free(sizePtr);
448
+
449
+ if (!dataPtr || size === 0) {
450
+ const filenamePtr = wasm._getDiskFilename(this.selectedDrive);
451
+ const filename = filenamePtr ? wasm.UTF8ToString(filenamePtr) : "Disk";
452
+ diskInfo.textContent = filename;
453
+ catalogList.innerHTML =
454
+ '<div class="fe-empty">Cannot read sector data<br><small>Copy-protected or non-standard disk format</small></div>';
455
+ this.catalog = [];
456
+ this.diskDataPtr = 0;
457
+ this.diskDataSize = 0;
458
+ this.diskFormat = null;
459
+ this.clearFileView();
460
+ return;
461
+ }
462
+
463
+ this.diskDataPtr = dataPtr;
464
+ this.diskDataSize = size;
465
+
466
+ // Get filename
467
+ const filenamePtr = wasm._getDiskFilename(this.selectedDrive);
468
+ const filename = filenamePtr ? wasm.UTF8ToString(filenamePtr) : "Unknown";
469
+
470
+ // Check disk format using WASM - try ProDOS first, then DOS 3.3
471
+ if (wasm._isProDOSFormat(dataPtr, size)) {
472
+ this.diskFormat = "prodos";
473
+ wasm._getProDOSVolumeInfo(dataPtr, size);
474
+ const volumeName = wasm.UTF8ToString(wasm._getProDOSVolumeName());
475
+ this.volumeInfo = { volumeName };
476
+ diskInfo.textContent = `${filename} (ProDOS: ${volumeName})`;
477
+
478
+ // Read ProDOS catalog via WASM
479
+ const count = wasm._getProDOSCatalog(dataPtr, size);
480
+ this.catalog = [];
481
+ for (let i = 0; i < count; i++) {
482
+ this.catalog.push({
483
+ filename: wasm.UTF8ToString(wasm._getProDOSEntryFilename(i)),
484
+ path: wasm.UTF8ToString(wasm._getProDOSEntryPath(i)),
485
+ fileType: wasm._getProDOSEntryFileType(i),
486
+ fileTypeName: wasm.UTF8ToString(wasm._getProDOSEntryFileTypeName(i)),
487
+ fileTypeDescription:
488
+ PRODOS_FILE_DESCRIPTIONS[wasm._getProDOSEntryFileType(i)] ||
489
+ "Unknown",
490
+ storageType: wasm._getProDOSEntryStorageType(i),
491
+ eof: wasm._getProDOSEntryEOF(i),
492
+ auxType: wasm._getProDOSEntryAuxType(i),
493
+ blocksUsed: wasm._getProDOSEntryBlocksUsed(i),
494
+ isLocked: wasm._getProDOSEntryIsLocked(i),
495
+ isDirectory: wasm._getProDOSEntryIsDirectory(i),
496
+ _wasmIndex: i,
497
+ });
498
+ }
499
+
500
+ // Reset to root directory and render
501
+ this.currentPath = "";
502
+ this.renderProDOSCatalog();
503
+ } else if (wasm._isPascalFormat(dataPtr, size)) {
504
+ this.diskFormat = "pascal";
505
+ wasm._getPascalVolumeInfo(dataPtr, size);
506
+ const volumeName = wasm.UTF8ToString(wasm._getPascalVolumeName());
507
+ this.volumeInfo = { volumeName };
508
+ diskInfo.textContent = `${filename} (Pascal: ${volumeName})`;
509
+
510
+ this.loadPascalCatalog();
511
+ } else if (wasm._isDOS33Format(dataPtr, size)) {
512
+ this.diskFormat = "dos33";
513
+ diskInfo.textContent = `${filename} (DOS 3.3)`;
514
+
515
+ // Read DOS 3.3 catalog via WASM
516
+ const count = wasm._getDOS33Catalog(dataPtr, size);
517
+ this.catalog = [];
518
+ for (let i = 0; i < count; i++) {
519
+ this.catalog.push({
520
+ filename: wasm.UTF8ToString(wasm._getDOS33EntryFilename(i)),
521
+ fileType: wasm._getDOS33EntryFileType(i),
522
+ fileTypeName: wasm.UTF8ToString(wasm._getDOS33EntryFileTypeName(i)),
523
+ fileTypeDescription:
524
+ DOS33_FILE_DESCRIPTIONS[wasm._getDOS33EntryFileType(i)] ||
525
+ "Unknown",
526
+ isLocked: wasm._getDOS33EntryIsLocked(i),
527
+ sectorCount: wasm._getDOS33EntrySectorCount(i),
528
+ _wasmIndex: i,
529
+ });
530
+ }
531
+
532
+ // Render catalog
533
+ if (this.catalog.length === 0) {
534
+ catalogList.innerHTML = '<div class="fe-empty">Disk is empty</div>';
535
+ } else {
536
+ catalogList.innerHTML = this.catalog
537
+ .map(
538
+ (entry, index) => `
539
+ <div class="fe-catalog-item" data-index="${index}">
540
+ <span class="fe-file-type ${entry.isLocked ? "locked" : ""}">${entry.isLocked ? "*" : " "}${entry.fileTypeName}</span>
541
+ <span class="fe-file-name">${escapeHtml(entry.filename)}</span>
542
+ <span class="fe-file-sectors">${entry.sectorCount}</span>
543
+ </div>
544
+ `,
545
+ )
546
+ .join("");
547
+ }
548
+ } else {
549
+ this.diskFormat = null;
550
+ diskInfo.textContent = `${filename} (Unknown format)`;
551
+ catalogList.innerHTML = '<div class="fe-empty">Unknown disk format</div>';
552
+ this.catalog = [];
553
+ this.clearFileView();
554
+ return;
555
+ }
556
+
557
+ this.selectedFile = null;
558
+ this.clearFileView();
559
+ }
560
+
561
+ /**
562
+ * Load a single ProDOS directory on-demand (for HD mode).
563
+ * For floppies, the full catalog is small enough to load at once.
564
+ */
565
+ loadProDOSDirectory(startBlock, path) {
566
+ const wasm = this.wasmModule;
567
+ const catalogList = this.element.querySelector(".fe-catalog-list");
568
+
569
+ // Allocate path string in WASM heap
570
+ const pathBytes = new TextEncoder().encode(path);
571
+ const pathPtr = wasm._malloc(pathBytes.length + 1);
572
+ wasm.HEAPU8.set(pathBytes, pathPtr);
573
+ wasm.HEAPU8[pathPtr + pathBytes.length] = 0;
574
+
575
+ const count = wasm._getProDOSDirectory(
576
+ this.diskDataPtr,
577
+ this.diskDataSize,
578
+ startBlock,
579
+ pathPtr,
580
+ );
581
+ wasm._free(pathPtr);
582
+
583
+ this.catalog = [];
584
+ for (let i = 0; i < count; i++) {
585
+ this.catalog.push({
586
+ filename: wasm.UTF8ToString(wasm._getProDOSEntryFilename(i)),
587
+ path: wasm.UTF8ToString(wasm._getProDOSEntryPath(i)),
588
+ fileType: wasm._getProDOSEntryFileType(i),
589
+ fileTypeName: wasm.UTF8ToString(wasm._getProDOSEntryFileTypeName(i)),
590
+ fileTypeDescription:
591
+ PRODOS_FILE_DESCRIPTIONS[wasm._getProDOSEntryFileType(i)] ||
592
+ "Unknown",
593
+ storageType: wasm._getProDOSEntryStorageType(i),
594
+ eof: wasm._getProDOSEntryEOF(i),
595
+ auxType: wasm._getProDOSEntryAuxType(i),
596
+ blocksUsed: wasm._getProDOSEntryBlocksUsed(i),
597
+ isLocked: wasm._getProDOSEntryIsLocked(i),
598
+ isDirectory: wasm._getProDOSEntryIsDirectory(i),
599
+ keyPointer: wasm._getProDOSEntryKeyPointer(i),
600
+ _wasmIndex: i,
601
+ });
602
+ }
603
+
604
+ this.currentPath = path;
605
+ this.selectedFile = null;
606
+ this.clearFileView();
607
+ this.renderProDOSCatalog();
608
+ }
609
+
610
+ /**
611
+ * Load and render the Pascal catalog (flat directory, no subdirectories)
612
+ */
613
+ loadPascalCatalog() {
614
+ const wasm = this.wasmModule;
615
+ const catalogList = this.element.querySelector(".fe-catalog-list");
616
+ const pathBar = this.element.querySelector(".fe-path-bar");
617
+
618
+ // Hide path bar (Pascal has no subdirectories)
619
+ pathBar.classList.add("hidden");
620
+ pathBar.innerHTML = "";
621
+
622
+ const count = wasm._getPascalCatalog(this.diskDataPtr, this.diskDataSize);
623
+ this.catalog = [];
624
+ for (let i = 0; i < count; i++) {
625
+ this.catalog.push({
626
+ filename: wasm.UTF8ToString(wasm._getPascalEntryFilename(i)),
627
+ fileType: wasm._getPascalEntryFileType(i),
628
+ fileTypeName: wasm.UTF8ToString(wasm._getPascalEntryFileTypeName(i)),
629
+ fileTypeDescription:
630
+ PASCAL_FILE_DESCRIPTIONS[wasm._getPascalEntryFileType(i)] || "Unknown",
631
+ fileSize: wasm._getPascalEntryFileSize(i),
632
+ blocksUsed: wasm._getPascalEntryBlocksUsed(i),
633
+ _wasmIndex: i,
634
+ });
635
+ }
636
+
637
+ if (this.catalog.length === 0) {
638
+ catalogList.innerHTML = '<div class="fe-empty">Disk is empty</div>';
639
+ } else {
640
+ catalogList.innerHTML = this.catalog
641
+ .map(
642
+ (entry, index) => `
643
+ <div class="fe-catalog-item" data-index="${index}">
644
+ <span class="fe-file-type">${entry.fileTypeName}</span>
645
+ <span class="fe-file-name">${escapeHtml(entry.filename)}</span>
646
+ <span class="fe-file-sectors">${entry.blocksUsed}</span>
647
+ </div>
648
+ `,
649
+ )
650
+ .join("");
651
+ }
652
+ }
653
+
654
+ /**
655
+ * Render the ProDOS catalog for the current directory.
656
+ * For HD mode: catalog contains only current directory entries (loaded on-demand).
657
+ * For floppy mode: catalog contains full tree, filtered by currentPath.
658
+ */
659
+ renderProDOSCatalog() {
660
+ const catalogList = this.element.querySelector(".fe-catalog-list");
661
+ const pathBar = this.element.querySelector(".fe-path-bar");
662
+
663
+ // Show/hide path bar based on whether we're in a subdirectory
664
+ if (this.currentPath) {
665
+ pathBar.classList.remove("hidden");
666
+ const parts = this.currentPath.split("/");
667
+ let pathHtml = `<span class="fe-path-item" data-path="">/${this.volumeInfo.volumeName}</span>`;
668
+ let builtPath = "";
669
+ for (const part of parts) {
670
+ builtPath += (builtPath ? "/" : "") + part;
671
+ pathHtml += `/<span class="fe-path-item" data-path="${escapeHtml(builtPath)}">${escapeHtml(part)}</span>`;
672
+ }
673
+ pathBar.innerHTML = pathHtml;
674
+ } else {
675
+ pathBar.classList.add("hidden");
676
+ pathBar.innerHTML = "";
677
+ }
678
+
679
+ // For HD mode, catalog already contains just the current directory's entries.
680
+ // For floppy mode, we filter the full catalog by path.
681
+ let entriesInPath;
682
+ if (this.sourceType === "hd") {
683
+ entriesInPath = this.catalog;
684
+ } else {
685
+ entriesInPath = this.catalog.filter((entry) => {
686
+ if (this.currentPath === "") {
687
+ return !entry.path.includes("/");
688
+ } else {
689
+ const prefix = this.currentPath + "/";
690
+ if (!entry.path.startsWith(prefix)) return false;
691
+ const remainder = entry.path.slice(prefix.length);
692
+ return !remainder.includes("/");
693
+ }
694
+ });
695
+ }
696
+
697
+ if (entriesInPath.length === 0) {
698
+ catalogList.innerHTML = '<div class="fe-empty">Directory is empty</div>';
699
+ } else {
700
+ // Sort: directories first, then files, alphabetically
701
+ entriesInPath.sort((a, b) => {
702
+ if (a.isDirectory && !b.isDirectory) return -1;
703
+ if (!a.isDirectory && b.isDirectory) return 1;
704
+ return a.filename.localeCompare(b.filename);
705
+ });
706
+
707
+ // Add parent directory entry if in subdirectory
708
+ let html = "";
709
+ if (this.currentPath) {
710
+ html += `
711
+ <div class="fe-catalog-item fe-directory fe-parent-dir" data-action="parent">
712
+ <span class="fe-file-type">DIR</span>
713
+ <span class="fe-file-name">..</span>
714
+ <span class="fe-file-sectors"></span>
715
+ </div>
716
+ `;
717
+ }
718
+
719
+ html += entriesInPath
720
+ .map((entry, idx) => {
721
+ const catalogIndex = this.catalog.indexOf(entry);
722
+ const isDir = entry.isDirectory;
723
+ return `
724
+ <div class="fe-catalog-item ${isDir ? "fe-directory" : ""}" data-index="${catalogIndex}" ${isDir ? 'data-action="enter"' : ""}>
725
+ <span class="fe-file-type ${entry.isLocked ? "locked" : ""}">${entry.isLocked ? "*" : " "}${isDir ? "DIR" : entry.fileTypeName}</span>
726
+ <span class="fe-file-name">${escapeHtml(entry.filename)}${isDir ? "/" : ""}</span>
727
+ <span class="fe-file-sectors">${entry.blocksUsed}</span>
728
+ </div>
729
+ `;
730
+ })
731
+ .join("");
732
+
733
+ catalogList.innerHTML = html;
734
+ }
735
+ }
736
+
737
+ /**
738
+ * Navigate to a directory path (ProDOS)
739
+ */
740
+ navigateToPath(path) {
741
+ if (this.sourceType === "hd") {
742
+ // On-demand: find the startBlock for this path
743
+ if (path === "") {
744
+ // Going back to root
745
+ this.directoryStack = [];
746
+ this.loadProDOSDirectory(2, "");
747
+ } else if (path.split("/").length < this.currentPath.split("/").length) {
748
+ // Going up: find the matching stack entry
749
+ const depth = path === "" ? 0 : path.split("/").length;
750
+ this.directoryStack = this.directoryStack.slice(0, depth);
751
+ const stackEntry = this.directoryStack[depth - 1];
752
+ this.loadProDOSDirectory(stackEntry.startBlock, path);
753
+ }
754
+ // Going into a subdirectory is handled in selectFile()
755
+ return;
756
+ }
757
+
758
+ // Floppy mode: just re-render with path filter
759
+ this.currentPath = path;
760
+ this.selectedFile = null;
761
+ this.clearFileView();
762
+ this.renderProDOSCatalog();
763
+ }
764
+
765
+ selectFile(index) {
766
+ if (index < 0 || index >= this.catalog.length) return;
767
+
768
+ const entry = this.catalog[index];
769
+
770
+ // If it's a directory, navigate into it
771
+ if (entry.isDirectory) {
772
+ if (this.sourceType === "hd") {
773
+ // On-demand: push current state and load subdirectory
774
+ this.directoryStack.push({
775
+ path: entry.path,
776
+ startBlock: entry.keyPointer,
777
+ });
778
+ this.loadProDOSDirectory(entry.keyPointer, entry.path);
779
+ } else {
780
+ this.navigateToPath(entry.path);
781
+ }
782
+ return;
783
+ }
784
+
785
+ // Update selection UI - compare with data-index attribute since items may be filtered
786
+ const items = this.element.querySelectorAll(".fe-catalog-item");
787
+ items.forEach((item) => {
788
+ const itemIndex = parseInt(item.dataset.index, 10);
789
+ item.classList.toggle("selected", itemIndex === index);
790
+ });
791
+
792
+ this.selectedFile = entry;
793
+ this._binaryViewManuallySet = false;
794
+ this._textViewManuallySet = false;
795
+ this.binaryViewMode = "asm";
796
+ this.textViewMode = "text";
797
+ this.showFileContents();
798
+ }
799
+
800
+ showFileContents() {
801
+ const titleEl = this.element.querySelector(".fe-file-title");
802
+ const infoEl = this.element.querySelector(".fe-file-info");
803
+ const contentEl = this.element.querySelector(".fe-file-content");
804
+ const viewToggle = this.element.querySelector(".fe-view-toggle");
805
+ const textViewToggle = this.element.querySelector(".fe-text-view-toggle");
806
+ const asmLegend = this.element.querySelector(".fe-asm-legend");
807
+ const hexLegend = this.element.querySelector(".fe-hex-legend");
808
+
809
+ if (!this.selectedFile || !this.diskDataPtr) {
810
+ this.clearFileView();
811
+ return;
812
+ }
813
+
814
+ // Display filename (with path for ProDOS)
815
+ const displayName =
816
+ this.diskFormat === "prodos" && this.selectedFile.path
817
+ ? this.selectedFile.path
818
+ : this.selectedFile.filename;
819
+ titleEl.textContent = displayName;
820
+
821
+ // Format file size info based on disk format
822
+ let sizeInfo;
823
+ if (this.diskFormat === "pascal") {
824
+ sizeInfo = `${this.selectedFile.fileSize} bytes (${this.selectedFile.blocksUsed} blocks)`;
825
+ } else if (this.diskFormat === "prodos") {
826
+ sizeInfo = formatFileSize(this.selectedFile.blocksUsed * 2); // ProDOS uses 512-byte blocks
827
+ } else {
828
+ sizeInfo = formatFileSize(this.selectedFile.sectorCount);
829
+ }
830
+ infoEl.textContent = `${this.selectedFile.fileTypeDescription} - ${sizeInfo}`;
831
+
832
+ // Determine if this is a binary file based on disk format
833
+ // DOS 3.3: fileType 0x04 is Binary
834
+ // ProDOS: fileType 0x06 (BIN) or 0xFF (SYS) are binary
835
+ // Pascal: fileType 2 (CODE) is binary
836
+ let isBinary, isText;
837
+ if (this.diskFormat === "pascal") {
838
+ isBinary = this.selectedFile.fileType === 2; // CODE
839
+ isText = this.selectedFile.fileType === 3; // TEXT
840
+ } else if (this.diskFormat === "prodos") {
841
+ isBinary =
842
+ this.selectedFile.fileType === 0x06 ||
843
+ this.selectedFile.fileType === 0xff;
844
+ isText = this.selectedFile.fileType === 0x04;
845
+ } else {
846
+ isBinary = this.selectedFile.fileType === 0x04;
847
+ isText = this.selectedFile.fileType === 0x00;
848
+ }
849
+
850
+ // Show/hide view toggles based on file type
851
+ viewToggle.classList.toggle("hidden", !isBinary);
852
+ textViewToggle.classList.toggle("hidden", !isText);
853
+
854
+ // Sync toggle button active states with current view modes
855
+ viewToggle.querySelectorAll(".fe-view-btn").forEach((b) => {
856
+ b.classList.toggle("active", b.dataset.view === this.binaryViewMode);
857
+ });
858
+ textViewToggle.querySelectorAll(".fe-view-btn").forEach((b) => {
859
+ b.classList.toggle("active", b.dataset.view === this.textViewMode);
860
+ });
861
+
862
+ // Show/hide ASM legend based on binary file and view mode
863
+ const showAsmLegend = isBinary && this.binaryViewMode === "asm";
864
+ asmLegend.classList.toggle("hidden", !showAsmLegend);
865
+
866
+ // Show/hide hex legend (shown for binary hex mode; also set later for unmapped types)
867
+ const showHexLegend = isBinary && this.binaryViewMode === "hex";
868
+ hexLegend.classList.toggle("hidden", !showHexLegend);
869
+
870
+ // Read file data (cache it for view switching)
871
+ try {
872
+ const wasm = this.wasmModule;
873
+
874
+ // Only re-read if we don't have cached data or file changed
875
+ const cacheKey =
876
+ this.diskFormat === "prodos" && this.selectedFile.path
877
+ ? this.selectedFile.path
878
+ : this.selectedFile.filename;
879
+
880
+ if (!this.currentFileData || this.currentFileData.filename !== cacheKey) {
881
+ // Read file via WASM
882
+ const wasmIdx = this.selectedFile._wasmIndex;
883
+ let bytesRead;
884
+
885
+ if (this.diskFormat === "pascal") {
886
+ bytesRead = wasm._readPascalFile(
887
+ this.diskDataPtr,
888
+ this.diskDataSize,
889
+ wasmIdx,
890
+ );
891
+ const bufPtr = wasm._getPascalFileBuffer();
892
+ const fileData = new Uint8Array(bytesRead);
893
+ fileData.set(new Uint8Array(wasm.HEAPU8.buffer, bufPtr, bytesRead));
894
+ this.currentFileData = {
895
+ filename: cacheKey,
896
+ data: fileData,
897
+ fileType: this.selectedFile.fileType,
898
+ };
899
+ } else if (this.diskFormat === "prodos") {
900
+ bytesRead = wasm._readProDOSFile(
901
+ this.diskDataPtr,
902
+ this.diskDataSize,
903
+ wasmIdx,
904
+ );
905
+ const bufPtr = wasm._getProDOSFileBuffer();
906
+ const fileData = new Uint8Array(bytesRead);
907
+ fileData.set(new Uint8Array(wasm.HEAPU8.buffer, bufPtr, bytesRead));
908
+ this.currentFileData = {
909
+ filename: cacheKey,
910
+ data: fileData,
911
+ fileType: this.selectedFile.fileType,
912
+ };
913
+ } else {
914
+ bytesRead = wasm._readDOS33File(
915
+ this.diskDataPtr,
916
+ this.diskDataSize,
917
+ wasmIdx,
918
+ );
919
+ const bufPtr = wasm._getDOS33FileBuffer();
920
+ const fileData = new Uint8Array(bytesRead);
921
+ fileData.set(new Uint8Array(wasm.HEAPU8.buffer, bufPtr, bytesRead));
922
+ this.currentFileData = {
923
+ filename: cacheKey,
924
+ data: fileData,
925
+ fileType: this.selectedFile.fileType,
926
+ };
927
+ }
928
+ }
929
+
930
+ const fileData = this.currentFileData.data;
931
+
932
+ // Handle binary files with view toggle
933
+ if (isBinary) {
934
+ // Get binary info - ProDOS stores address in auxType, DOS 3.3 in file header
935
+ let info;
936
+ let displayData;
937
+
938
+ if (this.diskFormat === "pascal") {
939
+ info = {
940
+ address: 0,
941
+ length: this.selectedFile.fileSize,
942
+ };
943
+ displayData = fileData; // Pascal binary data doesn't have header
944
+ } else if (this.diskFormat === "prodos") {
945
+ info = {
946
+ address: this.selectedFile.auxType,
947
+ length: this.selectedFile.eof,
948
+ };
949
+ displayData = fileData; // ProDOS binary data doesn't have header
950
+ } else {
951
+ // DOS 3.3 binary files have a 4-byte header: 2 bytes address, 2 bytes length
952
+ if (fileData.length >= 4) {
953
+ info = {
954
+ address: fileData[0] | (fileData[1] << 8),
955
+ length: fileData[2] | (fileData[3] << 8),
956
+ };
957
+ }
958
+ displayData = info ? fileData.slice(4) : fileData; // DOS 3.3 has 4-byte header
959
+ }
960
+
961
+ // Clear BASIC navigation state for binary files
962
+ this.basicLineNumToIndex = null;
963
+ this.basicOriginalHtml = null;
964
+
965
+ // Auto-detect Merlin source for binary files on first load
966
+ if (!this._binaryViewManuallySet && checkIsMerlinFile(displayData)) {
967
+ this.binaryViewMode = "merlin";
968
+ viewToggle.querySelectorAll(".fe-view-btn").forEach((b) => {
969
+ b.classList.toggle("active", b.dataset.view === "merlin");
970
+ });
971
+ }
972
+
973
+ // Update legend visibility after possible auto-detection
974
+ asmLegend.classList.toggle(
975
+ "hidden",
976
+ !(isBinary && this.binaryViewMode === "asm"),
977
+ );
978
+ hexLegend.classList.toggle(
979
+ "hidden",
980
+ !(isBinary && this.binaryViewMode === "hex"),
981
+ );
982
+
983
+ if (this.binaryViewMode === "merlin") {
984
+ // Show Merlin source view
985
+ this.hexDisplayState = null;
986
+ const merlinResult = formatMerlinFile(displayData);
987
+ contentEl.className = "fe-file-content merlin";
988
+ contentEl.innerHTML = `<pre>${merlinResult.content}</pre>`;
989
+ } else if (this.binaryViewMode === "hex") {
990
+ // Show hex dump with dynamic column count
991
+ contentEl.className = "fe-file-content hex";
992
+ this.hexDisplayState = {
993
+ data: displayData,
994
+ baseAddress: info?.address || 0,
995
+ maxBytes: 0,
996
+ };
997
+ this.hexBytesPerRow = this.calculateBytesPerRow();
998
+ const hexContent = formatHexDump(
999
+ displayData,
1000
+ info?.address || 0,
1001
+ 0,
1002
+ this.hexBytesPerRow,
1003
+ );
1004
+ contentEl.innerHTML = `<pre>${hexContent}</pre>`;
1005
+ } else {
1006
+ // Show disassembly (async) - progressive rendering to avoid freezing
1007
+ this.hexDisplayState = null;
1008
+ contentEl.className = "fe-file-content asm";
1009
+ contentEl.innerHTML = "<pre>Disassembling...</pre>";
1010
+
1011
+ // Create compatible data format for the disassembler.
1012
+ // The disassembler expects DOS 3.3 binary format: 4-byte header + data
1013
+ // Header: bytes 0-1 = load address (little-endian), bytes 2-3 = length (little-endian)
1014
+ // DOS 3.3 binary files already have this header, but ProDOS BIN files store
1015
+ // address/length in the file's aux_type field, not in the file data itself.
1016
+ // We create a synthetic header for ProDOS files so the disassembler works uniformly.
1017
+ let dataForDisasm;
1018
+ if (this.diskFormat === "prodos" || this.diskFormat === "pascal") {
1019
+ // Create header: 2 bytes address + 2 bytes length + actual data
1020
+ dataForDisasm = new Uint8Array(4 + fileData.length);
1021
+ dataForDisasm[0] = info.address & 0xff;
1022
+ dataForDisasm[1] = (info.address >> 8) & 0xff;
1023
+ dataForDisasm[2] = info.length & 0xff;
1024
+ dataForDisasm[3] = (info.length >> 8) & 0xff;
1025
+ dataForDisasm.set(fileData, 4);
1026
+ } else {
1027
+ dataForDisasm = fileData;
1028
+ }
1029
+
1030
+ // Pass contentEl for progressive rendering
1031
+ disassemble(dataForDisasm, contentEl).catch((e) => {
1032
+ contentEl.className = "fe-file-content error";
1033
+ contentEl.innerHTML = `<div class="fe-error">Error disassembling: ${e.message}</div>`;
1034
+ });
1035
+ }
1036
+ } else {
1037
+ // Non-binary files - use formatFileContents with mapped file type
1038
+ let viewerFileType;
1039
+ if (this.diskFormat === "pascal") {
1040
+ viewerFileType = wasm._mapPascalFileType(this.selectedFile.fileType);
1041
+ } else if (this.diskFormat === "prodos") {
1042
+ viewerFileType = wasm._mapProDOSFileType(this.selectedFile.fileType);
1043
+ } else {
1044
+ viewerFileType = this.selectedFile.fileType;
1045
+ }
1046
+
1047
+ // If mapFileTypeForViewer returns -1, use hex dump
1048
+ if (viewerFileType === -1) {
1049
+ contentEl.className = "fe-file-content hex";
1050
+ hexLegend.classList.remove("hidden");
1051
+ textViewToggle.classList.add("hidden");
1052
+ this.hexDisplayState = {
1053
+ data: fileData,
1054
+ baseAddress: 0,
1055
+ maxBytes: 0,
1056
+ };
1057
+ this.hexBytesPerRow = this.calculateBytesPerRow();
1058
+ const hexContent = formatHexDump(fileData, 0, 0, this.hexBytesPerRow);
1059
+ contentEl.innerHTML = `<pre>${hexContent}</pre>`;
1060
+ this.basicLineNumToIndex = null;
1061
+ this.basicOriginalHtml = null;
1062
+ return;
1063
+ }
1064
+
1065
+ // For text files, auto-detect Merlin source on first load
1066
+ if (
1067
+ isText &&
1068
+ !this._textViewManuallySet &&
1069
+ checkIsMerlinFile(fileData)
1070
+ ) {
1071
+ this.textViewMode = "merlin";
1072
+ textViewToggle.querySelectorAll(".fe-view-btn").forEach((b) => {
1073
+ b.classList.toggle("active", b.dataset.view === "merlin");
1074
+ });
1075
+ }
1076
+
1077
+ // Handle text file Merlin view
1078
+ if (isText && this.textViewMode === "merlin") {
1079
+ this.hexDisplayState = null;
1080
+ this.basicLineNumToIndex = null;
1081
+ this.basicOriginalHtml = null;
1082
+ const merlinResult = formatMerlinFile(fileData);
1083
+ contentEl.className = "fe-file-content merlin";
1084
+ contentEl.innerHTML = `<pre>${merlinResult.content}</pre>`;
1085
+ return;
1086
+ }
1087
+
1088
+ // ProDOS and Pascal files don't have the 2-byte length header that DOS 3.3 files have
1089
+ this.hexDisplayState = null;
1090
+ const hasLengthHeader = this.diskFormat === "dos33";
1091
+ const formatted = formatFileContents(fileData, viewerFileType, {
1092
+ hasLengthHeader,
1093
+ });
1094
+ contentEl.className = `fe-file-content ${formatted.format}`;
1095
+ // BASIC files output HTML with syntax highlighting, others need escaping
1096
+ if (formatted.isHtml) {
1097
+ contentEl.innerHTML = `<pre>${formatted.content}</pre>`;
1098
+ // Set up BASIC line navigation if available
1099
+ if (formatted.lineNumToIndex) {
1100
+ this.basicLineNumToIndex = formatted.lineNumToIndex;
1101
+ this.basicOriginalHtml = formatted.content; // Store original for highlight restoration
1102
+ contentEl
1103
+ .querySelector("pre")
1104
+ .addEventListener("click", this.handleBasicLineClick);
1105
+ } else {
1106
+ this.basicOriginalHtml = null;
1107
+ }
1108
+ } else {
1109
+ contentEl.innerHTML = `<pre>${escapeHtml(formatted.content)}</pre>`;
1110
+ this.basicLineNumToIndex = null;
1111
+ this.basicOriginalHtml = null;
1112
+ }
1113
+ }
1114
+ } catch (e) {
1115
+ contentEl.className = "fe-file-content error";
1116
+ contentEl.innerHTML = `<div class="fe-error">Error reading file: ${e.message}</div>`;
1117
+ }
1118
+ }
1119
+
1120
+ handleBasicLineClick(event) {
1121
+ const target = event.target.closest(".bas-lineref");
1122
+ if (!target || !this.basicLineNumToIndex || !this.basicOriginalHtml) return;
1123
+
1124
+ const targetLineNum = parseInt(target.dataset.targetLine, 10);
1125
+ if (isNaN(targetLineNum)) return;
1126
+
1127
+ const lineIndex = this.basicLineNumToIndex.get(targetLineNum);
1128
+ if (lineIndex === undefined) return;
1129
+
1130
+ const contentEl = this.element.querySelector(".fe-file-content");
1131
+ const pre = contentEl.querySelector("pre");
1132
+ if (!pre) return;
1133
+
1134
+ // Always rebuild from original HTML to avoid corruption from previous highlights
1135
+ const lines = this.basicOriginalHtml.split("\n");
1136
+ if (lineIndex >= 0 && lineIndex < lines.length) {
1137
+ lines[lineIndex] =
1138
+ `<span class="bas-highlight">${lines[lineIndex]}</span>`;
1139
+ pre.innerHTML = lines.join("\n");
1140
+
1141
+ // Scroll to the target line
1142
+ const lineHeight = 18;
1143
+ const scrollTop = lineIndex * lineHeight;
1144
+ const viewportHeight = contentEl.clientHeight;
1145
+ const centeredScrollTop = Math.max(
1146
+ 0,
1147
+ scrollTop - viewportHeight / 2 + lineHeight / 2,
1148
+ );
1149
+ contentEl.scrollTop = centeredScrollTop;
1150
+ }
1151
+ }
1152
+
1153
+ /**
1154
+ * Set up ResizeObserver to dynamically adjust hex column count
1155
+ */
1156
+ setupHexResizeObserver() {
1157
+ const contentEl = this.element.querySelector(".fe-file-content");
1158
+ if (!contentEl) return;
1159
+
1160
+ this.hexResizeObserver = new ResizeObserver(() => {
1161
+ if (!this.hexDisplayState || !contentEl.classList.contains("hex")) return;
1162
+
1163
+ const bytesPerRow = this.calculateBytesPerRow();
1164
+ if (bytesPerRow !== this.hexBytesPerRow) {
1165
+ this.hexBytesPerRow = bytesPerRow;
1166
+ this.rerenderHex();
1167
+ }
1168
+ });
1169
+
1170
+ this.hexResizeObserver.observe(contentEl);
1171
+ }
1172
+
1173
+ /**
1174
+ * Calculate optimal bytes per row based on available width
1175
+ */
1176
+ calculateBytesPerRow() {
1177
+ const contentEl = this.element.querySelector(".fe-file-content");
1178
+ if (!contentEl) return 16;
1179
+
1180
+ // Create a test element to measure monospace character width
1181
+ const testPre = document.createElement("pre");
1182
+ testPre.style.cssText =
1183
+ "position:absolute;visibility:hidden;pointer-events:none;margin:0;padding:0;font-family:var(--font-mono);font-size:11px";
1184
+ testPre.textContent = "0000000000";
1185
+ contentEl.appendChild(testPre);
1186
+ const charWidth = testPre.getBoundingClientRect().width / 10;
1187
+ const emWidth = parseFloat(getComputedStyle(testPre).fontSize);
1188
+ contentEl.removeChild(testPre);
1189
+
1190
+ if (charWidth === 0) return 16;
1191
+
1192
+ // Available width inside the content element (minus padding and scrollbar)
1193
+ const style = getComputedStyle(contentEl);
1194
+ const padding =
1195
+ (parseFloat(style.paddingLeft) || 0) +
1196
+ (parseFloat(style.paddingRight) || 0);
1197
+ const availableWidth = contentEl.clientWidth - padding;
1198
+
1199
+ // Fixed parts per line (in pixels):
1200
+ // Address: 4 chars, separator ":": 1 char + 0.5em margin, space: 1 char
1201
+ // ASCII separators: 2 × (1 char + 0.4em margin on each side)
1202
+ const fixedPx = 6 * charWidth + 0.5 * emWidth + 1.6 * emWidth;
1203
+
1204
+ // Per byte: 3 chars hex ("XX ") + 1 char ASCII = 4 chars
1205
+ const perBytePx = 4 * charWidth;
1206
+
1207
+ // First estimate without group gaps
1208
+ let bytesPerRow = Math.floor((availableWidth - fixedPx) / perBytePx);
1209
+
1210
+ // Refine: account for group gaps (0.75em each, between every 8-byte group)
1211
+ const groupGapWidth = 0.75 * emWidth;
1212
+ const gapCount = Math.max(0, Math.floor((bytesPerRow - 1) / 8));
1213
+ bytesPerRow = Math.floor(
1214
+ (availableWidth - fixedPx - gapCount * groupGapWidth) / perBytePx,
1215
+ );
1216
+
1217
+ return Math.max(1, Math.min(64, bytesPerRow));
1218
+ }
1219
+
1220
+ /**
1221
+ * Re-render hex dump with current bytesPerRow
1222
+ */
1223
+ rerenderHex() {
1224
+ const contentEl = this.element.querySelector(".fe-file-content");
1225
+ if (!contentEl || !this.hexDisplayState) return;
1226
+
1227
+ const { data, baseAddress, maxBytes } = this.hexDisplayState;
1228
+ const hexContent = formatHexDump(
1229
+ data,
1230
+ baseAddress,
1231
+ maxBytes,
1232
+ this.hexBytesPerRow,
1233
+ );
1234
+ contentEl.innerHTML = `<pre>${hexContent}</pre>`;
1235
+ }
1236
+
1237
+ clearFileView() {
1238
+ const titleEl = this.element.querySelector(".fe-file-title");
1239
+ const infoEl = this.element.querySelector(".fe-file-info");
1240
+ const contentEl = this.element.querySelector(".fe-file-content");
1241
+ const viewToggle = this.element.querySelector(".fe-view-toggle");
1242
+ const textViewToggle = this.element.querySelector(".fe-text-view-toggle");
1243
+ const asmLegend = this.element.querySelector(".fe-asm-legend");
1244
+ const hexLegend = this.element.querySelector(".fe-hex-legend");
1245
+
1246
+ titleEl.textContent = "Select a file";
1247
+ infoEl.textContent = "";
1248
+ contentEl.innerHTML = "";
1249
+ contentEl.className = "fe-file-content";
1250
+ viewToggle.classList.add("hidden");
1251
+ textViewToggle.classList.add("hidden");
1252
+ asmLegend.classList.add("hidden");
1253
+ hexLegend.classList.add("hidden");
1254
+ this.currentFileData = null;
1255
+ this.basicLineNumToIndex = null;
1256
+ this.basicOriginalHtml = null;
1257
+ this.hexDisplayState = null;
1258
+ this.textViewMode = "text";
1259
+ this.binaryViewMode = "asm";
1260
+ }
1261
+ }