diff --git a/js/components/Debugger.tsx b/js/components/Debugger.tsx
index 2fbaca4..6c8790d 100644
--- a/js/components/Debugger.tsx
+++ b/js/components/Debugger.tsx
@@ -1,10 +1,15 @@
import { h, JSX } from 'preact';
+import cs from 'classnames';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { Apple2 as Apple2Impl } from '../apple2';
+import { ControlButton } from './ControlButton';
+import { FileChooser } from './FileChooser';
import { Inset } from './Inset';
+import { loadLocalBinaryFile } from './util/files';
import styles from './css/Debugger.module.css';
-import { ControlButton } from './ControlButton';
+import { spawn } from './util/promises';
+import { toHex } from 'js/util';
export interface DebuggerProps {
apple2: Apple2Impl | undefined;
@@ -20,10 +25,25 @@ interface DebugData {
zeroPage: string;
}
+const CIDERPRESS_EXTENSION = /#([0-9a-f]{2})([0-9a-f]{4})$/i;
+const VALID_PAGE = /^[0-9A-F]{1,2}$/i;
+const VALID_ADDRESS = /^[0-9A-F]{1,4}$/i;
+
+const ERROR_ICON = (
+
+
+
+);
+
export const Debugger = ({ apple2 }: DebuggerProps) => {
const debug = apple2?.getDebugger();
const [data, setData] = useState();
const [memoryPage, setMemoryPage] = useState('08');
+ const [loadAddress, setLoadAddress] = useState('0800');
+ const [run, setRun] = useState(true);
const animationRef = useRef(0);
const animate = useCallback(() => {
@@ -57,10 +77,38 @@ export const Debugger = ({ apple2 }: DebuggerProps) => {
debug?.step();
}, [debug]);
- const doMemoryPage = useCallback((event: JSX.TargetedMouseEvent) => {
+ const doLoadAddress = useCallback((event: JSX.TargetedEvent) => {
+ setLoadAddress(event.currentTarget.value);
+ }, []);
+ const doRunCheck = useCallback((event: JSX.TargetedEvent) => {
+ setRun(event.currentTarget.checked);
+ }, []);
+
+ const doMemoryPage = useCallback((event: JSX.TargetedEvent) => {
setMemoryPage(event.currentTarget.value);
}, []);
+ const doChooseFile = useCallback((handles: FileSystemFileHandle[]) => {
+ if (debug && handles.length === 1) {
+ spawn(async () => {
+ const file = await handles[0].getFile();
+ let atAddress = parseInt(loadAddress, 16) || 0x800;
+
+ const matches = file.name.match(CIDERPRESS_EXTENSION);
+ if (matches && matches.length === 3) {
+ const [, , aux] = matches;
+ atAddress = parseInt(aux, 16);
+ }
+
+ await loadLocalBinaryFile(file, atAddress, debug);
+ setLoadAddress(toHex(atAddress, 4));
+ if (run) {
+ debug?.runAt(atAddress);
+ }
+ });
+ }
+ }, [debug, loadAddress, run]);
+
if (!data) {
return null;
}
@@ -74,9 +122,12 @@ export const Debugger = ({ apple2 }: DebuggerProps) => {
zeroPage
} = data;
+ const memoryPageValid = VALID_PAGE.test(memoryPage);
+ const loadAddressValid = VALID_ADDRESS.test(loadAddress);
+
return (
-
+
Debugger
Controls
@@ -125,18 +176,36 @@ export const Debugger = ({ apple2 }: DebuggerProps) => {
-
Memory Page: $
+
+
Memory Page: $
+ {memoryPageValid ? null : ERROR_ICON}
{memory}
+
+
+
Load File: $
+
+ {loadAddressValid ? null : ERROR_ICON}
+ {' '}
+
Run
+
+
+
+
);
diff --git a/js/components/css/App.module.css b/js/components/css/App.module.css
index 957b13a..ac7c77d 100644
--- a/js/components/css/App.module.css
+++ b/js/components/css/App.module.css
@@ -49,3 +49,29 @@ input[type="file"]::file-selector-button {
display: flex;
flex-direction: column;
}
+
+input[type="checkbox"] {
+ appearance: none;
+ background-color: #65594d;
+ border: 1px inset #65594d;
+ padding: 7px;
+ top: 7px;
+ border-radius: 3px;
+ display: inline-block;
+ position: relative;
+}
+
+input[type="checkbox"]:checked {
+ background-color: #65594d;
+ border: 1px inset #65594d;
+ color: #0d0;
+}
+
+input[type="checkbox"]:checked::after {
+ content: "\2716";
+ font-size: 12px;
+ position: absolute;
+ top: 0;
+ left: 2px;
+ color: #0d0;
+}
diff --git a/js/components/css/Debugger.module.css b/js/components/css/Debugger.module.css
index 7cc849e..1f66a56 100644
--- a/js/components/css/Debugger.module.css
+++ b/js/components/css/Debugger.module.css
@@ -7,19 +7,31 @@
border: 1px inset;
}
-.debugger input {
- border: 1px inset;
+.debugger {
font-size: 12px;
}
+.debugger button,
+.debugger input {
+ font-size: 12px;
+}
+
+.debugger input[type="text"] {
+ border: 1px inset;
+}
+
+.debugger hr {
+ color: #c4c1a0;
+}
+
.inset {
- margin: 5px 10px;
+ margin: 5px 10px 0;
width: auto;
}
.heading {
font-weight: bold;
- font-size: 16px;
+ font-size: 18px;
margin-bottom: 10px;
}
@@ -54,3 +66,22 @@
.stack {
width: 10em;
}
+
+.fileChooser {
+ padding: 5px 0;
+}
+
+.invalid {
+ color: #f00;
+}
+
+div.invalid {
+ position: relative;
+ display: inline-block;
+}
+
+div.invalid i {
+ position: absolute;
+ top: -9px;
+ left: -16px;
+}
diff --git a/js/components/util/files.ts b/js/components/util/files.ts
index 3a3dbd3..f02d7bd 100644
--- a/js/components/util/files.ts
+++ b/js/components/util/files.ts
@@ -1,4 +1,4 @@
-import { includes } from 'js/types';
+import { includes, word } from 'js/types';
import { initGamepad } from 'js/ui/gamepad';
import {
BlockFormat,
@@ -12,6 +12,7 @@ import {
} from 'js/formats/types';
import Disk2 from 'js/cards/disk2';
import SmartPort from 'js/cards/smartport';
+import Debugger from 'js/debugger';
type ProgressCallback = (current: number, total: number) => void;
@@ -255,3 +256,24 @@ export class SmartStorageBroker implements MassStorage {
return null;
}
}
+
+/**
+ * Load binary file into memory.
+ *
+ * @param file File object to read into memory
+ * @param address Address at which to start load
+ * @param debug Debugger object
+ * @returns resolves to true if successful
+ */
+export const loadLocalBinaryFile = (file: File, address: word, debug: Debugger) => {
+ return new Promise((resolve, _reject) => {
+ const fileReader = new FileReader();
+ fileReader.onload = function () {
+ const result = this.result as ArrayBuffer;
+ const bytes = new Uint8Array(result);
+ debug.setMemory(address, bytes);
+ resolve(true);
+ };
+ fileReader.readAsArrayBuffer(file);
+ });
+};
diff --git a/js/debugger.ts b/js/debugger.ts
index eff60ad..e8a0926 100644
--- a/js/debugger.ts
+++ b/js/debugger.ts
@@ -72,6 +72,17 @@ export default class Debugger {
this.container.run();
};
+ /**
+ * Restart at a given memory address.
+ *
+ * @param address Address to start execution
+ */
+
+ runAt = (address: word) => {
+ this.cpu.reset();
+ this.cpu.setPC(address);
+ };
+
isRunning = () =>
this.container.isRunning();
@@ -212,6 +223,35 @@ export default class Debugger {
return result;
};
+ /**
+ * Reads a range of memory. Will wrap at memory limit.
+ *
+ * @param address Starting address to read memory
+ * @param length Length of memory to read.
+ * @returns Byte array containing memory
+ */
+ getMemory(address: word, length: word) {
+ const bytes = new Uint8Array(length);
+ for (let idx = 0; idx < length; idx++) {
+ address &= 0xffff;
+ bytes[idx] = this.cpu.read(address++);
+ }
+ return bytes;
+ }
+
+ /**
+ * Writes a range of memory. Will wrap at memory limit.
+ *
+ * @param address Starting address to write memory
+ * @param bytes Data to write
+ */
+ setMemory(address: word, bytes: Uint8Array) {
+ for (const byte of bytes) {
+ address &= 0xffff;
+ this.cpu.write(address++, byte);
+ }
+ }
+
list = (pc: word) => {
const results = [];
for (let idx = 0; idx < 20; idx++) {