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.
- package/.clangd +5 -0
- package/.mcp.json +12 -0
- package/CLAUDE.md +362 -0
- package/CMakeLists.txt +774 -0
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/build-wasm/generated/roms.cpp +2447 -0
- package/docker-compose.staging.yml +9 -0
- package/docs/basic-rom-disassembly.md +6663 -0
- package/docs/softswitch-comparison.md +273 -0
- package/docs/thunderclock-debug.md +89 -0
- package/examples/cube.bas +72 -0
- package/examples/hello.s +55 -0
- package/examples/scroll.s +140 -0
- package/package.json +18 -0
- package/public/assets/apple-logo-old.png +0 -0
- package/public/assets/apple-logo.png +0 -0
- package/public/assets/drive-closed-light-on.png +0 -0
- package/public/assets/drive-closed.png +0 -0
- package/public/assets/drive-open-light-on.png +0 -0
- package/public/assets/drive-open.png +0 -0
- package/public/audio-worklet.js +82 -0
- package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
- package/public/disks/ProDOS 2.4.3.po +0 -0
- package/public/disks/h32mb.2mg +0 -0
- package/public/disks/library.json +26 -0
- package/public/docs/llms/llm-assembler.txt +90 -0
- package/public/docs/llms/llm-basic-program.txt +256 -0
- package/public/docs/llms/llm-disk-drives.txt +72 -0
- package/public/docs/llms/llm-file-explorer.txt +50 -0
- package/public/docs/llms/llm-hard-drives.txt +80 -0
- package/public/docs/llms/llm-main.txt +51 -0
- package/public/docs/llms/llm-slot-configuration.txt +66 -0
- package/public/icons/icon-192.svg +4 -0
- package/public/icons/icon-512.svg +4 -0
- package/public/index.html +661 -0
- package/public/llms.txt +49 -0
- package/public/manifest.json +29 -0
- package/public/shaders/burnin.glsl +22 -0
- package/public/shaders/crt.glsl +706 -0
- package/public/shaders/edge.glsl +109 -0
- package/public/shaders/vertex.glsl +8 -0
- package/public/sw.js +186 -0
- package/roms/341-0027.bin +0 -0
- package/roms/341-0160-A-US-UK.bin +0 -0
- package/roms/341-0160-A.bin +0 -0
- package/roms/342-0273-A-US-UK.bin +0 -0
- package/roms/342-0349-B-C0-FF.bin +0 -0
- package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
- package/roms/Thunderclock Plus ROM.bin +0 -0
- package/scripts/generate_roms.sh +69 -0
- package/src/bindings/wasm_interface.cpp +1940 -0
- package/src/core/assembler/assembler.cpp +1239 -0
- package/src/core/assembler/assembler.hpp +115 -0
- package/src/core/audio/audio.cpp +160 -0
- package/src/core/audio/audio.hpp +81 -0
- package/src/core/basic/basic_detokenizer.cpp +436 -0
- package/src/core/basic/basic_detokenizer.hpp +41 -0
- package/src/core/basic/basic_tokenizer.cpp +286 -0
- package/src/core/basic/basic_tokenizer.hpp +26 -0
- package/src/core/basic/basic_tokens.hpp +295 -0
- package/src/core/cards/disk2_card.cpp +568 -0
- package/src/core/cards/disk2_card.hpp +316 -0
- package/src/core/cards/expansion_card.hpp +185 -0
- package/src/core/cards/mockingboard/ay8910.cpp +616 -0
- package/src/core/cards/mockingboard/ay8910.hpp +159 -0
- package/src/core/cards/mockingboard/via6522.cpp +530 -0
- package/src/core/cards/mockingboard/via6522.hpp +163 -0
- package/src/core/cards/mockingboard_card.cpp +312 -0
- package/src/core/cards/mockingboard_card.hpp +159 -0
- package/src/core/cards/mouse_card.cpp +654 -0
- package/src/core/cards/mouse_card.hpp +190 -0
- package/src/core/cards/smartport/block_device.cpp +202 -0
- package/src/core/cards/smartport/block_device.hpp +60 -0
- package/src/core/cards/smartport/smartport_card.cpp +603 -0
- package/src/core/cards/smartport/smartport_card.hpp +120 -0
- package/src/core/cards/thunderclock_card.cpp +237 -0
- package/src/core/cards/thunderclock_card.hpp +122 -0
- package/src/core/cpu/cpu6502.cpp +1609 -0
- package/src/core/cpu/cpu6502.hpp +203 -0
- package/src/core/debug/condition_evaluator.cpp +470 -0
- package/src/core/debug/condition_evaluator.hpp +87 -0
- package/src/core/disassembler/disassembler.cpp +552 -0
- package/src/core/disassembler/disassembler.hpp +171 -0
- package/src/core/disk-image/disk_image.hpp +267 -0
- package/src/core/disk-image/dsk_disk_image.cpp +827 -0
- package/src/core/disk-image/dsk_disk_image.hpp +204 -0
- package/src/core/disk-image/gcr_encoding.cpp +147 -0
- package/src/core/disk-image/gcr_encoding.hpp +78 -0
- package/src/core/disk-image/woz_disk_image.cpp +1049 -0
- package/src/core/disk-image/woz_disk_image.hpp +343 -0
- package/src/core/emulator.cpp +2126 -0
- package/src/core/emulator.hpp +434 -0
- package/src/core/filesystem/dos33.cpp +178 -0
- package/src/core/filesystem/dos33.hpp +66 -0
- package/src/core/filesystem/pascal.cpp +262 -0
- package/src/core/filesystem/pascal.hpp +87 -0
- package/src/core/filesystem/prodos.cpp +369 -0
- package/src/core/filesystem/prodos.hpp +119 -0
- package/src/core/input/keyboard.cpp +227 -0
- package/src/core/input/keyboard.hpp +111 -0
- package/src/core/mmu/mmu.cpp +1387 -0
- package/src/core/mmu/mmu.hpp +236 -0
- package/src/core/types.hpp +196 -0
- package/src/core/video/video.cpp +680 -0
- package/src/core/video/video.hpp +156 -0
- package/src/css/assembler-editor.css +1617 -0
- package/src/css/base.css +470 -0
- package/src/css/basic-debugger.css +791 -0
- package/src/css/basic-editor.css +792 -0
- package/src/css/controls.css +783 -0
- package/src/css/cpu-debugger.css +1413 -0
- package/src/css/debug-base.css +160 -0
- package/src/css/debug-windows.css +6455 -0
- package/src/css/disk-drives.css +406 -0
- package/src/css/documentation.css +392 -0
- package/src/css/file-explorer.css +867 -0
- package/src/css/hard-drive.css +180 -0
- package/src/css/layout.css +217 -0
- package/src/css/memory-windows.css +798 -0
- package/src/css/modals.css +510 -0
- package/src/css/monitor.css +425 -0
- package/src/css/release-notes.css +101 -0
- package/src/css/responsive.css +400 -0
- package/src/css/rule-builder.css +340 -0
- package/src/css/save-states.css +201 -0
- package/src/css/settings-windows.css +1231 -0
- package/src/css/window-switcher.css +150 -0
- package/src/js/agent/agent-manager.js +643 -0
- package/src/js/agent/agent-tools.js +293 -0
- package/src/js/agent/agent-version-tools.js +131 -0
- package/src/js/agent/assembler-tools.js +357 -0
- package/src/js/agent/basic-program-tools.js +894 -0
- package/src/js/agent/disk-tools.js +417 -0
- package/src/js/agent/file-explorer-tools.js +269 -0
- package/src/js/agent/index.js +13 -0
- package/src/js/agent/main-tools.js +222 -0
- package/src/js/agent/slot-tools.js +303 -0
- package/src/js/agent/smartport-tools.js +257 -0
- package/src/js/agent/window-tools.js +80 -0
- package/src/js/audio/audio-driver.js +417 -0
- package/src/js/audio/audio-worklet.js +85 -0
- package/src/js/audio/index.js +8 -0
- package/src/js/config/default-layout.js +34 -0
- package/src/js/config/version.js +8 -0
- package/src/js/data/apple2-rom-routines.js +577 -0
- package/src/js/debug/assembler-editor-window.js +2993 -0
- package/src/js/debug/basic-breakpoint-manager.js +529 -0
- package/src/js/debug/basic-program-parser.js +436 -0
- package/src/js/debug/basic-program-window.js +2594 -0
- package/src/js/debug/basic-variable-inspector.js +447 -0
- package/src/js/debug/breakpoint-manager.js +472 -0
- package/src/js/debug/cpu-debugger-window.js +2396 -0
- package/src/js/debug/index.js +22 -0
- package/src/js/debug/label-manager.js +238 -0
- package/src/js/debug/memory-browser-window.js +416 -0
- package/src/js/debug/memory-heat-map-window.js +481 -0
- package/src/js/debug/memory-map-window.js +206 -0
- package/src/js/debug/mockingboard-window.js +882 -0
- package/src/js/debug/mouse-card-window.js +355 -0
- package/src/js/debug/rule-builder-window.js +648 -0
- package/src/js/debug/soft-switch-window.js +458 -0
- package/src/js/debug/stack-viewer-window.js +221 -0
- package/src/js/debug/symbols.js +416 -0
- package/src/js/debug/trace-panel.js +291 -0
- package/src/js/debug/zero-page-watch-window.js +297 -0
- package/src/js/disk-manager/disk-drives-window.js +212 -0
- package/src/js/disk-manager/disk-operations.js +284 -0
- package/src/js/disk-manager/disk-persistence.js +301 -0
- package/src/js/disk-manager/disk-surface-renderer.js +388 -0
- package/src/js/disk-manager/drive-sounds.js +139 -0
- package/src/js/disk-manager/hard-drive-manager.js +481 -0
- package/src/js/disk-manager/hard-drive-persistence.js +187 -0
- package/src/js/disk-manager/hard-drive-window.js +57 -0
- package/src/js/disk-manager/index.js +890 -0
- package/src/js/display/display-settings-window.js +383 -0
- package/src/js/display/index.js +10 -0
- package/src/js/display/screen-window.js +342 -0
- package/src/js/display/webgl-renderer.js +705 -0
- package/src/js/file-explorer/disassembler.js +574 -0
- package/src/js/file-explorer/dos33.js +266 -0
- package/src/js/file-explorer/file-viewer.js +359 -0
- package/src/js/file-explorer/index.js +1261 -0
- package/src/js/file-explorer/prodos.js +549 -0
- package/src/js/file-explorer/utils.js +67 -0
- package/src/js/help/documentation-window.js +1096 -0
- package/src/js/help/index.js +10 -0
- package/src/js/help/release-notes-window.js +85 -0
- package/src/js/help/release-notes.js +612 -0
- package/src/js/input/gamepad-handler.js +176 -0
- package/src/js/input/index.js +12 -0
- package/src/js/input/input-handler.js +396 -0
- package/src/js/input/joystick-window.js +404 -0
- package/src/js/input/mouse-handler.js +99 -0
- package/src/js/input/text-selection.js +462 -0
- package/src/js/main.js +653 -0
- package/src/js/state/index.js +15 -0
- package/src/js/state/save-states-window.js +393 -0
- package/src/js/state/state-manager.js +409 -0
- package/src/js/state/state-persistence.js +218 -0
- package/src/js/ui/confirm.js +43 -0
- package/src/js/ui/disk-drive-positioner.js +347 -0
- package/src/js/ui/reminder-controller.js +129 -0
- package/src/js/ui/slot-configuration-window.js +560 -0
- package/src/js/ui/theme-manager.js +61 -0
- package/src/js/ui/toast.js +44 -0
- package/src/js/ui/ui-controller.js +897 -0
- package/src/js/ui/window-switcher.js +275 -0
- package/src/js/utils/basic-autocomplete.js +832 -0
- package/src/js/utils/basic-highlighting.js +473 -0
- package/src/js/utils/basic-tokenizer.js +153 -0
- package/src/js/utils/basic-tokens.js +117 -0
- package/src/js/utils/constants.js +28 -0
- package/src/js/utils/indexeddb-helper.js +225 -0
- package/src/js/utils/merlin-editor-support.js +905 -0
- package/src/js/utils/merlin-highlighting.js +551 -0
- package/src/js/utils/storage.js +125 -0
- package/src/js/utils/string-utils.js +19 -0
- package/src/js/utils/wasm-memory.js +54 -0
- package/src/js/windows/base-window.js +690 -0
- package/src/js/windows/index.js +9 -0
- package/src/js/windows/window-manager.js +375 -0
- package/tests/catch2/catch.hpp +17976 -0
- package/tests/common/basic_program_builder.cpp +119 -0
- package/tests/common/basic_program_builder.hpp +209 -0
- package/tests/common/disk_image_builder.cpp +444 -0
- package/tests/common/disk_image_builder.hpp +141 -0
- package/tests/common/test_helpers.hpp +118 -0
- package/tests/gcr/gcr-test.cpp +142 -0
- package/tests/integration/check-rom.js +70 -0
- package/tests/integration/compare-boot.js +239 -0
- package/tests/integration/crash-trace.js +102 -0
- package/tests/integration/disk-boot-test.js +264 -0
- package/tests/integration/memory-crash.js +108 -0
- package/tests/integration/nibble-read-test.js +249 -0
- package/tests/integration/phase-test.js +159 -0
- package/tests/integration/test_emulator.cpp +291 -0
- package/tests/integration/test_emulator_basic.cpp +91 -0
- package/tests/integration/test_emulator_debug.cpp +344 -0
- package/tests/integration/test_emulator_disk.cpp +153 -0
- package/tests/integration/test_emulator_state.cpp +163 -0
- package/tests/klaus/6502_functional_test.bin +0 -0
- package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
- package/tests/klaus/klaus_6502_test.cpp +184 -0
- package/tests/klaus/klaus_65c02_test.cpp +197 -0
- package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
- package/tests/thunderclock/thunderclock_test.cpp +550 -0
- package/tests/unit/test_assembler.cpp +521 -0
- package/tests/unit/test_audio.cpp +196 -0
- package/tests/unit/test_ay8910.cpp +311 -0
- package/tests/unit/test_basic_detokenizer.cpp +265 -0
- package/tests/unit/test_basic_tokenizer.cpp +382 -0
- package/tests/unit/test_block_device.cpp +259 -0
- package/tests/unit/test_condition_evaluator.cpp +219 -0
- package/tests/unit/test_cpu6502.cpp +1301 -0
- package/tests/unit/test_cpu_addressing.cpp +361 -0
- package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
- package/tests/unit/test_cpu_decimal.cpp +166 -0
- package/tests/unit/test_cpu_interrupts.cpp +285 -0
- package/tests/unit/test_disassembler.cpp +323 -0
- package/tests/unit/test_disk2_card.cpp +330 -0
- package/tests/unit/test_dos33.cpp +273 -0
- package/tests/unit/test_dsk_disk_image.cpp +315 -0
- package/tests/unit/test_expansion_card.cpp +178 -0
- package/tests/unit/test_gcr_encoding.cpp +232 -0
- package/tests/unit/test_keyboard.cpp +262 -0
- package/tests/unit/test_mmu.cpp +555 -0
- package/tests/unit/test_mmu_slots.cpp +323 -0
- package/tests/unit/test_mockingboard.cpp +352 -0
- package/tests/unit/test_mouse_card.cpp +386 -0
- package/tests/unit/test_pascal.cpp +248 -0
- package/tests/unit/test_prodos.cpp +259 -0
- package/tests/unit/test_smartport_card.cpp +321 -0
- package/tests/unit/test_thunderclock.cpp +354 -0
- package/tests/unit/test_via6522.cpp +323 -0
- package/tests/unit/test_video.cpp +319 -0
- package/tests/unit/test_woz_disk_image.cpp +257 -0
- package/vite.config.js +96 -0
- package/wiki/AI-Agent.md +372 -0
- package/wiki/Architecture-Overview.md +303 -0
- package/wiki/Audio-System.md +449 -0
- package/wiki/CPU-Emulation.md +477 -0
- package/wiki/Debugger.md +516 -0
- package/wiki/Disk-Drives.md +161 -0
- package/wiki/Disk-System-Internals.md +547 -0
- package/wiki/Display-Settings.md +88 -0
- package/wiki/Expansion-Slots.md +187 -0
- package/wiki/File-Explorer.md +259 -0
- package/wiki/Getting-Started.md +156 -0
- package/wiki/Home.md +69 -0
- package/wiki/Input-Devices.md +183 -0
- package/wiki/Keyboard-Shortcuts.md +158 -0
- package/wiki/Memory-System.md +364 -0
- package/wiki/Save-States.md +172 -0
- 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
|
+
}
|