Typescriptify ui/apple2.js (#74)

This is mostly a mechanical change; there are still lots of things
about `ui/apple2` that could be improved.  The change also converts a
few dependencies of `ui/apple2`, like `applesoft/compiler`.

Besides the straight conversions, some other packages have changes to
make all of the typing work out.

Lastly, `@types/micromodal` has been added as a development
dependency.
This commit is contained in:
Ian Flanigan 2021-03-31 02:27:44 +02:00 committed by GitHub
parent e3bbd2d640
commit 207bed3d27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1571 additions and 1406 deletions

View File

@ -37,6 +37,11 @@ export interface Apple2Options {
tick: () => void, tick: () => void,
} }
export interface Stats {
frames: number,
renderedFrames: number,
}
interface State { interface State {
cpu: CpuState, cpu: CpuState,
vm: VideoModesState, vm: VideoModesState,
@ -66,7 +71,7 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
private tick: () => void; private tick: () => void;
private stats = { private stats: Stats = {
frames: 0, frames: 0,
renderedFrames: 0 renderedFrames: 0
}; };
@ -215,7 +220,7 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
this.cpu.reset(); this.cpu.reset();
} }
getStats() { getStats(): Stats {
return this.stats; return this.stats;
} }

View File

@ -1,287 +0,0 @@
export default function ApplesoftCompiler(mem)
{
var _mem = mem;
var LOMEM = 0x69;
var ARRAY_START = 0x6B;
var ARRAY_END = 0x6D;
var PROGRAM_START = 0x801;
var TOKENS = {
'END': 0x80,
'FOR': 0x81,
'NEXT': 0x82,
'DATA': 0x83,
'INPUT': 0x84,
'DEL': 0x85,
'DIM': 0x86,
'READ': 0x87,
'GR': 0x88,
'TEXT': 0x89,
'PR#': 0x8a,
'IN#': 0x8b,
'CALL': 0x8c,
'PLOT': 0x8d,
'HLIN': 0x8e,
'VLIN': 0x8f,
'HGR2': 0x90,
'HGR': 0x91,
'HCOLOR=': 0x92,
'HPLOT': 0x93,
'DRAW': 0x94,
'XDRAW': 0x95,
'HTAB': 0x96,
'HOME': 0x97,
'ROT=': 0x98,
'SCALE=': 0x99,
'SHLOAD': 0x9a,
'TRACE': 0x9b,
'NOTRACE': 0x9c,
'NORMAL': 0x9d,
'INVERSE': 0x9e,
'FLASH': 0x9f,
'COLOR=': 0xa0,
'POP=': 0xa1,
'VTAB': 0xa2,
'HIMEM:': 0xa3,
'LOMEM:': 0xa4,
'ONERR': 0xa5,
'RESUME': 0xa6,
'RECALL': 0xa7,
'STORE': 0xa8,
'SPEED=': 0xa9,
'LET': 0xaa,
'GOTO': 0xab,
'RUN': 0xac,
'IF': 0xad,
'RESTORE': 0xae,
'&': 0xaf,
'GOSUB': 0xb0,
'RETURN': 0xb1,
'REM': 0xb2,
'STOP': 0xb3,
'ON': 0xb4,
'WAIT': 0xb5,
'LOAD': 0xb6,
'SAVE': 0xb7,
'DEF': 0xb8,
'POKE': 0xb9,
'PRINT': 0xba,
'CONT': 0xbb,
'LIST': 0xbc,
'CLEAR': 0xbd,
'GET': 0xbe,
'NEW': 0xbf,
'TAB(': 0xc0,
'TO': 0xc1,
'FN': 0xc2,
'SPC(': 0xc3,
'THEN': 0xc4,
'AT': 0xc5,
'NOT': 0xc6,
'STEP': 0xc7,
'+': 0xc8,
'-': 0xc9,
'*': 0xca,
'/': 0xcb,
'^': 0xcc,
'AND': 0xcd,
'OR': 0xce,
'>': 0xcf,
'=': 0xd0,
'<': 0xd1,
'SGN': 0xd2,
'INT': 0xd3,
'ABS': 0xd4,
'USR': 0xd5,
'FRE': 0xd6,
'SCRN(': 0xd7,
'PDL': 0xd8,
'POS': 0xd9,
'SQR': 0xda,
'RND': 0xdb,
'LOG': 0xdc,
'EXP': 0xdd,
'COS': 0xde,
'SIN': 0xdf,
'TAN': 0xe0,
'ATN': 0xe1,
'PEEK': 0xe2,
'LEN': 0xe3,
'STR$': 0xe4,
'VAL': 0xe5,
'ASC': 0xe6,
'CHR$': 0xe7,
'LEFT$': 0xe8,
'RIGHT$': 0xe9,
'MID$': 0xea
};
var STATES = {
NORMAL: 0,
STRING: 1,
COMMENT: 2,
DATA: 3
};
function writeByte(addr, val) {
var page = addr >> 8,
off = addr & 0xff;
return _mem.write(page, off, val);
}
function writeWord(addr, val) {
var lsb = val & 0xff;
var msb = val >> 8;
writeByte(addr, lsb);
writeByte(addr + 1, msb);
}
return {
compile: function(program) {
var lineNos = {};
function compileLine(line, offset) {
if (!line) {
return [];
}
var state = STATES.NORMAL;
var result = [0, 0, 0, 0];
var curChar = 0;
var character;
var lineNoStr = '';
while (line.length) {
character = line.charAt(curChar);
if (/\d/.test(character)) {
lineNoStr += character;
curChar++;
} else {
break;
}
}
while (curChar < line.length) {
character = line.charAt(curChar).toUpperCase();
switch (state) {
case STATES.NORMAL:
if (character !== ' ') {
if (character === '"') {
result.push(character.charCodeAt(0));
state = STATES.STRING;
curChar++;
} else {
var foundToken = '';
for (var possibleToken in TOKENS) {
if (possibleToken.charAt(0) == character) {
var tokenIdx = curChar + 1;
var idx = 1;
while (idx < possibleToken.length) {
if (line.charAt(tokenIdx) !== ' ') {
if (line.charAt(tokenIdx).toUpperCase() !== possibleToken.charAt(idx)) {
break;
}
idx++;
}
tokenIdx++;
}
if (idx === possibleToken.length) {
// Found a token
if (possibleToken === 'AT') {
var lookAhead = line.charAt(tokenIdx + 1).toUpperCase();
// ATN takes precedence over AT
if (lookAhead === 'N') {
foundToken = 'ATN';
tokenIdx++;
}
// TO takes precedence over AT
if (lookAhead === 'O') {
result.push(lookAhead.charCodeAt(0));
foundToken = 'TO';
tokenIdx++;
}
}
foundToken = possibleToken;
}
}
if (foundToken) {
break;
}
}
if (foundToken) {
result.push(TOKENS[foundToken]);
curChar = tokenIdx;
if (foundToken === 'REM') {
state = STATES.COMMENT;
}
} else {
result.push(character.charCodeAt(0));
curChar++;
}
}
} else {
curChar++;
}
break;
case STATES.COMMENT:
result.push(character.charCodeAt(0));
curChar++;
break;
case STATES.STRING:
result.push(character.charCodeAt(0));
if (character == '"') {
state = STATES.NORMAL;
}
curChar++;
break;
}
}
if (lineNoStr.length) {
var lineNo = parseInt(lineNoStr, 10);
if (lineNo < 0 || lineNo > 65535) {
throw new Error('Line number out of range');
}
if (lineNos[lineNoStr]) {
throw new Error('Duplicate line number');
}
lineNos[lineNoStr] = result;
// Next line pointer
result.push(0);
var nextLine = offset + result.length;
result[0] = nextLine & 0xff;
result[1] = nextLine >> 8;
// Line number
result[2] = lineNo & 0xff;
result[3] = lineNo >> 8;
} else {
throw new Error('Missing line number');
}
return result;
}
var compiled = [];
var lines = program.split(/[\r\n]+/g);
while (lines.length) {
var line = lines.shift();
var compiledLine = compileLine(line, PROGRAM_START + compiled.length);
compiled = compiled.concat(compiledLine);
}
compiled.push(0, 0);
for (var idx = 0; idx < compiled.length; idx++) {
writeByte(PROGRAM_START + idx, compiled[idx]);
}
writeWord(LOMEM, PROGRAM_START + compiled.length);
writeWord(ARRAY_START, PROGRAM_START + compiled.length);
writeWord(ARRAY_END, PROGRAM_START + compiled.length);
}
};
}

288
js/applesoft/compiler.ts Normal file
View File

@ -0,0 +1,288 @@
import { byte, KnownKeys, KnownValues, Memory, word } from '../types';
/** Map from keyword to token. */
const TOKENS = {
'END': 0x80,
'FOR': 0x81,
'NEXT': 0x82,
'DATA': 0x83,
'INPUT': 0x84,
'DEL': 0x85,
'DIM': 0x86,
'READ': 0x87,
'GR': 0x88,
'TEXT': 0x89,
'PR#': 0x8a,
'IN#': 0x8b,
'CALL': 0x8c,
'PLOT': 0x8d,
'HLIN': 0x8e,
'VLIN': 0x8f,
'HGR2': 0x90,
'HGR': 0x91,
'HCOLOR=': 0x92,
'HPLOT': 0x93,
'DRAW': 0x94,
'XDRAW': 0x95,
'HTAB': 0x96,
'HOME': 0x97,
'ROT=': 0x98,
'SCALE=': 0x99,
'SHLOAD': 0x9a,
'TRACE': 0x9b,
'NOTRACE': 0x9c,
'NORMAL': 0x9d,
'INVERSE': 0x9e,
'FLASH': 0x9f,
'COLOR=': 0xa0,
'POP=': 0xa1,
'VTAB': 0xa2,
'HIMEM:': 0xa3,
'LOMEM:': 0xa4,
'ONERR': 0xa5,
'RESUME': 0xa6,
'RECALL': 0xa7,
'STORE': 0xa8,
'SPEED=': 0xa9,
'LET': 0xaa,
'GOTO': 0xab,
'RUN': 0xac,
'IF': 0xad,
'RESTORE': 0xae,
'&': 0xaf,
'GOSUB': 0xb0,
'RETURN': 0xb1,
'REM': 0xb2,
'STOP': 0xb3,
'ON': 0xb4,
'WAIT': 0xb5,
'LOAD': 0xb6,
'SAVE': 0xb7,
'DEF': 0xb8,
'POKE': 0xb9,
'PRINT': 0xba,
'CONT': 0xbb,
'LIST': 0xbc,
'CLEAR': 0xbd,
'GET': 0xbe,
'NEW': 0xbf,
'TAB(': 0xc0,
'TO': 0xc1,
'FN': 0xc2,
'SPC(': 0xc3,
'THEN': 0xc4,
'AT': 0xc5,
'NOT': 0xc6,
'STEP': 0xc7,
'+': 0xc8,
'-': 0xc9,
'*': 0xca,
'/': 0xcb,
'^': 0xcc,
'AND': 0xcd,
'OR': 0xce,
'>': 0xcf,
'=': 0xd0,
'<': 0xd1,
'SGN': 0xd2,
'INT': 0xd3,
'ABS': 0xd4,
'USR': 0xd5,
'FRE': 0xd6,
'SCRN(': 0xd7,
'PDL': 0xd8,
'POS': 0xd9,
'SQR': 0xda,
'RND': 0xdb,
'LOG': 0xdc,
'EXP': 0xdd,
'COS': 0xde,
'SIN': 0xdf,
'TAN': 0xe0,
'ATN': 0xe1,
'PEEK': 0xe2,
'LEN': 0xe3,
'STR$': 0xe4,
'VAL': 0xe5,
'ASC': 0xe6,
'CHR$': 0xe7,
'LEFT$': 0xe8,
'RIGHT$': 0xe9,
'MID$': 0xea
} as const;
const LOMEM = 0x69;
const ARRAY_START = 0x6B;
const ARRAY_END = 0x6D;
const PROGRAM_START = 0x801;
const STATES = {
NORMAL: 0,
STRING: 1,
COMMENT: 2,
DATA: 3
} as const;
export default class ApplesoftCompiler {
constructor(private mem: Memory) { }
private writeByte(addr: word, val: byte) {
const page = addr >> 8;
const off = addr & 0xff;
return this.mem.write(page, off, val);
}
private writeWord(addr: word, val: byte) {
const lsb = val & 0xff;
const msb = val >> 8;
this.writeByte(addr, lsb);
this.writeByte(addr + 1, msb);
}
compile(program: string) {
const lineNos: { [line: string]: byte[]} = {};
function compileLine(line: string | null | undefined, offset: number) {
if (!line) {
return [];
}
let state: KnownValues<typeof STATES> = STATES.NORMAL;
const result = [0, 0, 0, 0];
let curChar = 0;
let character;
let lineNoStr = '';
while (line.length) {
character = line.charAt(curChar);
if (/\d/.test(character)) {
lineNoStr += character;
curChar++;
} else {
break;
}
}
while (curChar < line.length) {
character = line.charAt(curChar).toUpperCase();
switch (state) {
case STATES.NORMAL:
if (character !== ' ') {
if (character === '"') {
result.push(character.charCodeAt(0));
state = STATES.STRING;
curChar++;
} else {
let foundToken = '';
let tokenIdx = -1;
for (const possibleToken in TOKENS) {
if (possibleToken.charAt(0) == character) {
tokenIdx = curChar + 1;
let idx = 1;
while (idx < possibleToken.length) {
if (line.charAt(tokenIdx) !== ' ') {
if (line.charAt(tokenIdx).toUpperCase() !== possibleToken.charAt(idx)) {
break;
}
idx++;
}
tokenIdx++;
}
if (idx === possibleToken.length) {
// Found a token
if (possibleToken === 'AT') {
const lookAhead = line.charAt(tokenIdx + 1).toUpperCase();
// ATN takes precedence over AT
if (lookAhead === 'N') {
foundToken = 'ATN';
tokenIdx++;
}
// TO takes precedence over AT
if (lookAhead === 'O') {
result.push(lookAhead.charCodeAt(0));
foundToken = 'TO';
tokenIdx++;
}
}
foundToken = possibleToken;
}
}
if (foundToken) {
break;
}
}
if (foundToken) {
result.push(TOKENS[foundToken as KnownKeys<typeof TOKENS>]);
curChar = tokenIdx;
if (foundToken === 'REM') {
state = STATES.COMMENT;
}
} else {
result.push(character.charCodeAt(0));
curChar++;
}
}
} else {
curChar++;
}
break;
case STATES.COMMENT:
result.push(character.charCodeAt(0));
curChar++;
break;
case STATES.STRING:
result.push(character.charCodeAt(0));
if (character == '"') {
state = STATES.NORMAL;
}
curChar++;
break;
}
}
if (lineNoStr.length) {
const lineNo = parseInt(lineNoStr, 10);
if (lineNo < 0 || lineNo > 65535) {
throw new Error('Line number out of range');
}
if (lineNos[lineNoStr]) {
throw new Error('Duplicate line number');
}
lineNos[lineNoStr] = result;
// Next line pointer
result.push(0);
const nextLine = offset + result.length;
result[0] = nextLine & 0xff;
result[1] = nextLine >> 8;
// Line number
result[2] = lineNo & 0xff;
result[3] = lineNo >> 8;
} else {
throw new Error('Missing line number');
}
return result;
}
let compiled: number[] = [];
const lines = program.split(/[\r\n]+/g);
while (lines.length) {
const line = lines.shift();
const compiledLine = compileLine(line, PROGRAM_START + compiled.length);
compiled = compiled.concat(compiledLine);
}
compiled.push(0, 0);
for (let idx = 0; idx < compiled.length; idx++) {
this.writeByte(PROGRAM_START + idx, compiled[idx]);
}
this.writeWord(LOMEM, PROGRAM_START + compiled.length);
this.writeWord(ARRAY_START, PROGRAM_START + compiled.length);
this.writeWord(ARRAY_END, PROGRAM_START + compiled.length);
}
}

View File

@ -1,173 +0,0 @@
export default function ApplesoftDump(mem)
{
var _mem = mem;
var LETTERS =
' ' +
' !"#$%&\'()*+,-./0123456789:;<=>?' +
'@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_' +
'`abcdefghijklmnopqrstuvwxyz{|}~ ';
var TOKENS = {
0x80: 'END',
0x81: 'FOR',
0x82: 'NEXT',
0x83: 'DATA',
0x84: 'INPUT',
0x85: 'DEL',
0x86: 'DIM',
0x87: 'READ',
0x88: 'GR',
0x89: 'TEXT',
0x8a: 'PR#',
0x8b: 'IN#',
0x8c: 'CALL',
0x8d: 'PLOT',
0x8e: 'HLIN',
0x8f: 'VLIN',
0x90: 'HGR2',
0x91: 'HGR',
0x92: 'HCOLOR=',
0x93: 'HPLOT',
0x94: 'DRAW',
0x95: 'XDRAW',
0x96: 'HTAB',
0x97: 'HOME',
0x98: 'ROT=',
0x99: 'SCALE=',
0x9a: 'SHLOAD',
0x9b: 'TRACE',
0x9c: 'NOTRACE',
0x9d: 'NORMAL',
0x9e: 'INVERSE',
0x9f: 'FLASH',
0xa0: 'COLOR=',
0xa1: 'POP=',
0xa2: 'VTAB',
0xa3: 'HIMEM:',
0xa4: 'LOMEM:',
0xa5: 'ONERR',
0xa6: 'RESUME',
0xa7: 'RECALL',
0xa8: 'STORE',
0xa9: 'SPEED=',
0xaa: 'LET',
0xab: 'GOTO',
0xac: 'RUN',
0xad: 'IF',
0xae: 'RESTORE',
0xaf: '&',
0xb0: 'GOSUB',
0xb1: 'RETURN',
0xb2: 'REM',
0xb3: 'STOP',
0xb4: 'ON',
0xb5: 'WAIT',
0xb6: 'LOAD',
0xb7: 'SAVE',
0xb8: 'DEF',
0xb9: 'POKE',
0xba: 'PRINT',
0xbb: 'CONT',
0xbc: 'LIST',
0xbd: 'CLEAR',
0xbe: 'GET',
0xbf: 'NEW',
0xc0: 'TAB(',
0xc1: 'TO',
0xc2: 'FN',
0xc3: 'SPC(',
0xc4: 'THEN',
0xc5: 'AT',
0xc6: 'NOT',
0xc7: 'STEP',
0xc8: '+',
0xc9: '-',
0xca: '*',
0xcb: '/',
0xcc: '^',
0xcd: 'AND',
0xce: 'OR',
0xcf: '>',
0xd0: '=',
0xd1: '<',
0xd2: 'SGN',
0xd3: 'INT',
0xd4: 'ABS',
0xd5: 'USR',
0xd6: 'FRE',
0xd7: 'SCRN(',
0xd8: 'PDL',
0xd9: 'POS',
0xda: 'SQR',
0xdb: 'RND',
0xdc: 'LOG',
0xdd: 'EXP',
0xde: 'COS',
0xdf: 'SIN',
0xe0: 'TAN',
0xe1: 'ATN',
0xe2: 'PEEK',
0xe3: 'LEN',
0xe4: 'STR$',
0xe5: 'VAL',
0xe6: 'ASC',
0xe7: 'CHR$',
0xe8: 'LEFT$',
0xe9: 'RIGHT$',
0xea: 'MID$'
};
function readByte(addr) {
var page = addr >> 8,
off = addr & 0xff;
return _mem.read(page, off);
}
function readWord(addr) {
var lsb, msb;
lsb = readByte(addr);
msb = readByte(addr + 1);
return (msb << 8) | lsb;
}
return {
toString: function () {
var str = '';
var start = readWord(0x67); // Start
var end = readWord(0xaf); // End of program
var addr = start;
do {
var line = '';
var next = readWord(addr);
addr += 2;
var lineno = readWord(addr);
addr += 2;
line += lineno;
line += ' ';
var val = false;
do {
if (addr < start || addr > end)
return str;
val = readByte(addr++);
if (val >= 0x80) {
line += ' ';
line += TOKENS[val];
line += ' ';
}
else
line += LETTERS[val];
} while (val);
line += '\n';
str += line;
addr = next;
} while (addr && addr >= start && addr < end);
return str;
}
};
}

171
js/applesoft/decompiler.ts Normal file
View File

@ -0,0 +1,171 @@
import { byte, KnownKeys, Memory, word } from '../types';
const LETTERS =
' ' +
' !"#$%&\'()*+,-./0123456789:;<=>?' +
'@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_' +
'`abcdefghijklmnopqrstuvwxyz{|}~ ';
const TOKENS = {
0x80: 'END',
0x81: 'FOR',
0x82: 'NEXT',
0x83: 'DATA',
0x84: 'INPUT',
0x85: 'DEL',
0x86: 'DIM',
0x87: 'READ',
0x88: 'GR',
0x89: 'TEXT',
0x8a: 'PR#',
0x8b: 'IN#',
0x8c: 'CALL',
0x8d: 'PLOT',
0x8e: 'HLIN',
0x8f: 'VLIN',
0x90: 'HGR2',
0x91: 'HGR',
0x92: 'HCOLOR=',
0x93: 'HPLOT',
0x94: 'DRAW',
0x95: 'XDRAW',
0x96: 'HTAB',
0x97: 'HOME',
0x98: 'ROT=',
0x99: 'SCALE=',
0x9a: 'SHLOAD',
0x9b: 'TRACE',
0x9c: 'NOTRACE',
0x9d: 'NORMAL',
0x9e: 'INVERSE',
0x9f: 'FLASH',
0xa0: 'COLOR=',
0xa1: 'POP=',
0xa2: 'VTAB',
0xa3: 'HIMEM:',
0xa4: 'LOMEM:',
0xa5: 'ONERR',
0xa6: 'RESUME',
0xa7: 'RECALL',
0xa8: 'STORE',
0xa9: 'SPEED=',
0xaa: 'LET',
0xab: 'GOTO',
0xac: 'RUN',
0xad: 'IF',
0xae: 'RESTORE',
0xaf: '&',
0xb0: 'GOSUB',
0xb1: 'RETURN',
0xb2: 'REM',
0xb3: 'STOP',
0xb4: 'ON',
0xb5: 'WAIT',
0xb6: 'LOAD',
0xb7: 'SAVE',
0xb8: 'DEF',
0xb9: 'POKE',
0xba: 'PRINT',
0xbb: 'CONT',
0xbc: 'LIST',
0xbd: 'CLEAR',
0xbe: 'GET',
0xbf: 'NEW',
0xc0: 'TAB(',
0xc1: 'TO',
0xc2: 'FN',
0xc3: 'SPC(',
0xc4: 'THEN',
0xc5: 'AT',
0xc6: 'NOT',
0xc7: 'STEP',
0xc8: '+',
0xc9: '-',
0xca: '*',
0xcb: '/',
0xcc: '^',
0xcd: 'AND',
0xce: 'OR',
0xcf: '>',
0xd0: '=',
0xd1: '<',
0xd2: 'SGN',
0xd3: 'INT',
0xd4: 'ABS',
0xd5: 'USR',
0xd6: 'FRE',
0xd7: 'SCRN(',
0xd8: 'PDL',
0xd9: 'POS',
0xda: 'SQR',
0xdb: 'RND',
0xdc: 'LOG',
0xdd: 'EXP',
0xde: 'COS',
0xdf: 'SIN',
0xe0: 'TAN',
0xe1: 'ATN',
0xe2: 'PEEK',
0xe3: 'LEN',
0xe4: 'STR$',
0xe5: 'VAL',
0xe6: 'ASC',
0xe7: 'CHR$',
0xe8: 'LEFT$',
0xe9: 'RIGHT$',
0xea: 'MID$'
} as const;
export default class ApplesoftDump {
constructor(private mem: Memory) { }
private readByte(addr: word): byte {
const page = addr >> 8;
const off = addr & 0xff;
return this.mem.read(page, off);
}
private readWord(addr: word): word {
const lsb = this.readByte(addr);
const msb = this.readByte(addr + 1);
return (msb << 8) | lsb;
}
toString() {
let str = '';
const start = this.readWord(0x67); // Start
const end = this.readWord(0xaf); // End of program
let addr = start;
do {
let line = '';
const next = this.readWord(addr);
addr += 2;
const lineno = this.readWord(addr);
addr += 2;
line += lineno;
line += ' ';
let val = 0;
do {
if (addr < start || addr > end)
return str;
val = this.readByte(addr++);
if (val >= 0x80) {
line += ' ';
line += TOKENS[val as KnownKeys<typeof TOKENS>];
line += ' ';
}
else
line += LETTERS[val];
} while (val);
line += '\n';
str += line;
addr = next;
} while (addr && addr >= start && addr < end);
return str;
}
}

View File

@ -787,7 +787,7 @@ export default class DiskII implements Card {
this.callbacks.label(drive, name); this.callbacks.label(drive, name);
} }
getJSON(drive: DriveNumber, pretty: boolean) { getJSON(drive: DriveNumber, pretty: boolean = false) {
const cur = this.drives[drive - 1]; const cur = this.drives[drive - 1];
if (!isNibbleDrive(cur)) { if (!isNibbleDrive(cur)) {
throw new Error('Can\'t save WOZ disks to JSON'); throw new Error('Can\'t save WOZ disks to JSON');
@ -801,7 +801,7 @@ export default class DiskII implements Card {
return true; return true;
} }
setBinary(drive: DriveNumber, name: string, fmt: DiskFormat, rawData: memory) { setBinary(drive: DriveNumber, name: string, fmt: DiskFormat, rawData: ArrayBuffer) {
let disk; let disk;
const cur = this.drives[drive - 1]; const cur = this.drives[drive - 1];
const readOnly = false; const readOnly = false;

View File

@ -456,6 +456,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
for (let idx = 0; idx < data.byteLength; idx += 512) { for (let idx = 0; idx < data.byteLength; idx += 512) {
this.disks[drive].push(new Uint8Array(data.slice(idx, idx + 512))); this.disks[drive].push(new Uint8Array(data.slice(idx, idx + 512)));
} }
return true;
} }
setDisk(drive: number, json: BlockDevice) { setDisk(drive: number, json: BlockDevice) {

View File

@ -12,6 +12,7 @@
import { byte, DiskFormat, memory } from '../types'; import { byte, DiskFormat, memory } from '../types';
import { base64_decode, base64_encode } from '../base64'; import { base64_decode, base64_encode } from '../base64';
import { bytify, debug, toHex } from '../util'; import { bytify, debug, toHex } from '../util';
import { GamepadConfiguration } from '../ui/gamepad';
export interface Disk { export interface Disk {
format: DiskFormat format: DiskFormat
@ -27,8 +28,12 @@ export interface Disk {
export class JSONDiskBase { export class JSONDiskBase {
type: DiskFormat type: DiskFormat
name: string name: string
disk?: number
category?: string
writeProtected?: boolean
volume: byte volume: byte
readOnly: boolean readOnly: boolean
gamepad?: GamepadConfiguration
} }
/** /**

View File

@ -668,7 +668,7 @@ export class VideoModesGL implements VideoModes {
private _displayConfig: screenEmu.DisplayConfiguration; private _displayConfig: screenEmu.DisplayConfiguration;
private _monoMode: boolean = false; private _monoMode: boolean = false;
ready: Promise<void> public ready: Promise<void>
constructor( constructor(
gr: LoresPage, gr: LoresPage,

View File

@ -31,6 +31,25 @@ export type KnownKeys<T> = {
[K in keyof T]: string extends K ? never : number extends K ? never : K [K in keyof T]: string extends K ? never : number extends K ? never : K
} extends { [_ in keyof T]: infer U } ? U : never; } extends { [_ in keyof T]: infer U } ? U : never;
/**
* Extracts the declared values of a constant object.
*/
export type KnownValues<T> = T extends {
[_ in keyof T]: infer U } ? U : never;
/**
* Replacement for `includes` on constant types that is also a type assertion.
*
* @example
* const SOME_VALUES = [1, 2, 'a'] as const;
* let n: number = 1;
* let r = includes(SOME_VALUES, n); // r === true, n is 1 | 2 | 'a'
* n = 5;
* r = includes(SOME_VALUES, n); // r === false, n is number
*/
export function includes<S extends T, T>(a: ReadonlyArray<S>, v: T): v is S {
return (a as ReadonlyArray<T>).includes(v);
}
/** A bit. */ /** A bit. */
export type bit = 0 | 1; export type bit = 0 | 1;

View File

@ -1,937 +0,0 @@
import MicroModal from 'micromodal';
import { base64_json_parse, base64_json_stringify } from '../base64';
import Audio from './audio';
import DriveLights from './drive_lights';
import { DISK_FORMATS } from '../types';
import { gamepad, configGamepad, initGamepad } from './gamepad';
import KeyBoard from './keyboard';
import Tape, { TAPE_TYPES } from './tape';
import ApplesoftDump from '../applesoft/decompiler';
import ApplesoftCompiler from '../applesoft/compiler';
import { debug, gup, hup } from '../util';
import Prefs from '../prefs';
var paused = false;
var focused = false;
var startTime = Date.now();
var lastCycles = 0;
var lastFrames = 0;
var lastRenderedFrames = 0;
var hashtag = document.location.hash;
var disk_categories = {'Local Saves': []};
var disk_sets = {};
var disk_cur_name = [];
var disk_cur_cat = [];
var _apple2;
var cpu;
var stats;
var vm;
var tape;
var _disk2;
var _smartPort;
var _printer;
var audio;
var keyboard;
var io;
var _currentDrive = 1;
export const driveLights = new DriveLights();
export function dumpAppleSoftProgram() {
var dumper = new ApplesoftDump(cpu);
debug(dumper.toString());
}
export function compileAppleSoftProgram(program) {
var compiler = new ApplesoftCompiler(cpu);
compiler.compile(program);
}
export function openLoad(drive, event) {
_currentDrive = parseInt(drive, 10);
if (event.metaKey) {
openLoadHTTP(drive);
} else {
if (disk_cur_cat[drive]) {
document.querySelector('#category_select').value = disk_cur_cat[drive];
selectCategory();
}
MicroModal.show('load-modal');
}
}
export function openSave(drive, event) {
_currentDrive = parseInt(drive, 10);
var mimeType = 'application/octet-stream';
var data = _disk2.getBinary(drive);
var a = document.querySelector('#local_save_link');
var blob = new Blob([data], { 'type': mimeType });
a.href = window.URL.createObjectURL(blob);
a.download = driveLights.label(drive) + '.dsk';
if (event.metaKey) {
dumpDisk(drive);
} else {
document.querySelector('#save_name').value = driveLights.label(drive);
MicroModal.show('save-modal');
}
}
export function openAlert(msg) {
var el = document.querySelector('#alert-modal .message');
el.innerText = msg;
MicroModal.show('alert-modal');
}
/********************************************************************
*
* Drag and Drop
*/
export function handleDragOver(drive, event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
}
export function handleDragEnd(drive, event) {
var dt = event.dataTransfer;
if (dt.items) {
for (var i = 0; i < dt.items.length; i++) {
dt.items.remove(i);
}
} else {
event.dataTransfer.clearData();
}
}
export function handleDrop(drive, event) {
event.preventDefault();
event.stopPropagation();
if (drive < 1) {
if (!_disk2.getMetadata(1)) {
drive = 1;
} else if (!_disk2.getMetadata(2)) {
drive = 2;
} else {
drive = 1;
}
}
var dt = event.dataTransfer;
if (dt.files.length == 1) {
var runOnLoad = event.shiftKey;
doLoadLocal(drive, dt.files[0], { runOnLoad });
} else if (dt.files.length == 2) {
doLoadLocal(1, dt.files[0]);
doLoadLocal(2, dt.files[1]);
} else {
for (var idx = 0; idx < dt.items.length; idx++) {
if (dt.items[idx].type === 'text/uri-list') {
dt.items[idx].getAsString(function(url) {
var parts = document.location.hash.split('|');
parts[drive - 1] = url;
document.location.hash = parts.join('|');
});
}
}
}
}
function loadingStart () {
var meter = document.querySelector('#loading-modal .meter');
meter.style.display = 'none';
MicroModal.show('loading-modal');
}
function loadingProgress (current, total) {
if (total) {
var meter = document.querySelector('#loading-modal .meter');
var progress = document.querySelector('#loading-modal .progress');
meter.style.display = 'block';
progress.style.width = current / total * meter.clientWidth + 'px';
}
}
function loadingStop () {
MicroModal.close('loading-modal');
if (!paused) {
vm.ready.then(() => {
_apple2.run();
});
}
}
export function loadAjax(drive, url) {
loadingStart();
fetch(url).then(function(response) {
if (response.ok) {
return response.json();
} else {
throw new Error('Error loading: ' + response.statusText);
}
}).then(function(data) {
if (data.type == 'binary') {
loadBinary(drive, data);
} else if (DISK_FORMATS.indexOf(data.type) > -1) {
loadDisk(drive, data);
}
initGamepad(data.gamepad);
loadingStop();
}).catch(function(error) {
loadingStop();
openAlert(error.message);
});
}
export function doLoad() {
MicroModal.close('load-modal');
var urls = document.querySelector('#disk_select').value, url;
if (urls && urls.length) {
if (typeof(urls) == 'string') {
url = urls;
} else {
url = urls[0];
}
}
var files = document.querySelector('#local_file').files;
if (files.length == 1) {
doLoadLocal(_currentDrive, files[0]);
} else if (url) {
var filename;
MicroModal.close('load-modal');
if (url.substr(0,6) == 'local:') {
filename = url.substr(6);
if (filename == '__manage') {
openManage();
} else {
loadLocalStorage(_currentDrive, filename);
}
} else {
var r1 = /json\/disks\/(.*).json$/.exec(url);
if (r1) {
filename = r1[1];
} else {
filename = url;
}
var parts = document.location.hash.split('|');
parts[_currentDrive - 1] = filename;
document.location.hash = parts.join('|');
}
}
}
export function doSave() {
var name = document.querySelector('#save_name').value;
saveLocalStorage(_currentDrive, name);
MicroModal.close('save-modal');
window.setTimeout(() => openAlert('Saved'), 0);
}
export function doDelete(name) {
if (window.confirm('Delete ' + name + '?')) {
deleteLocalStorage(name);
}
}
const CIDERPRESS_EXTENSION = /#([0-9a-f]{2})([0-9a-f]{4})$/i;
const BIN_TYPES = ['bin'];
function doLoadLocal(drive, file, options = {}) {
var parts = file.name.split('.');
var ext = parts[parts.length - 1].toLowerCase();
var matches = file.name.match(CIDERPRESS_EXTENSION);
var type, aux;
if (matches && matches.length === 3) {
[, type, aux] = matches;
}
if (DISK_FORMATS.includes(ext)) {
doLoadLocalDisk(drive, file);
} else if (TAPE_TYPES.includes(ext)) {
tape.doLoadLocalTape(file);
} else if (BIN_TYPES.includes(ext) || type === '06' || options.address) {
doLoadBinary(file, { address: parseInt(aux || '2000', 16), ...options });
} else {
openAlert('Unknown file type: ' + ext);
}
}
function doLoadBinary(file, options) {
loadingStart();
var fileReader = new FileReader();
fileReader.onload = function() {
let { address } = options;
const bytes = new Uint8Array(this.result);
for (let idx = 0; idx < this.result.byteLength; idx++) {
cpu.write(address >> 8, address & 0xff, bytes[idx]);
address++;
}
if (options.runOnLoad) {
cpu.reset();
cpu.setPC(options.address);
}
loadingStop();
};
fileReader.readAsArrayBuffer(file);
}
function doLoadLocalDisk(drive, file) {
loadingStart();
var fileReader = new FileReader();
fileReader.onload = function() {
var parts = file.name.split('.');
var ext = parts.pop().toLowerCase();
var name = parts.join('.');
// Remove any json file reference
var files = document.location.hash.split('|');
files[drive - 1] = '';
document.location.hash = files.join('|');
if (this.result.byteLength >= 800 * 1024) {
if (_smartPort.setBinary(drive, name, ext, this.result)) {
focused = false;
initGamepad();
}
} else {
if (_disk2.setBinary(drive, name, ext, this.result)) {
focused = false;
initGamepad();
}
}
loadingStop();
};
fileReader.readAsArrayBuffer(file);
}
export function doLoadHTTP(drive, _url) {
if (!_url) {
MicroModal.close('http-modal');
}
loadingStart();
var url = _url || document.querySelector('#http_url').value;
if (url) {
fetch(url).then(function(response) {
if (response.ok) {
var reader = response.body.getReader();
var received = 0;
var chunks = [];
var contentLength = parseInt(response.headers.get('content-length'), 10);
return reader.read().then(function readChunk(result) {
if (result.done) {
var data = new Uint8Array(received);
var offset = 0;
for (var idx = 0; idx < chunks.length; idx++) {
data.set(chunks[idx], offset);
offset += chunks[idx].length;
}
return data.buffer;
}
received += result.value.length;
if (contentLength) {
loadingProgress(received, contentLength);
}
chunks.push(result.value);
return reader.read().then(readChunk);
});
} else {
throw new Error('Error loading: ' + response.statusText);
}
}).then(function(data) {
var urlParts = url.split('/');
var file = urlParts.pop();
var fileParts = file.split('.');
var ext = fileParts.pop().toLowerCase();
var name = decodeURIComponent(fileParts.join('.'));
if (data.byteLength >= 800 * 1024) {
if (_smartPort.setBinary(drive, name, ext, data)) {
initGamepad();
}
} else {
if (_disk2.setBinary(drive, name, ext, data)) {
initGamepad();
}
}
loadingStop();
}).catch(function(error) {
loadingStop();
openAlert(error.message);
});
}
}
function openLoadHTTP(drive) {
_currentDrive = parseInt(drive, 10);
MicroModal.show('http-modal');
}
function openManage() {
MicroModal.show('manage-modal');
}
var prefs = new Prefs();
var showStats = 0;
export function updateKHz() {
var now = Date.now();
var ms = now - startTime;
var cycles = cpu.getCycles();
var delta;
var fps;
var khz;
switch (showStats) {
case 0: {
delta = cycles - lastCycles;
khz = parseInt(delta/ms);
document.querySelector('#khz').innerText = khz + ' kHz';
break;
}
case 1: {
delta = stats.renderedFrames - lastRenderedFrames;
fps = parseInt(delta/(ms/1000), 10);
document.querySelector('#khz').innerText = fps + ' rps';
break;
}
default: {
delta = stats.frames - lastFrames;
fps = parseInt(delta/(ms/1000), 10);
document.querySelector('#khz').innerText = fps + ' fps';
}
}
startTime = now;
lastCycles = cycles;
lastRenderedFrames = stats.renderedFrames;
lastFrames = stats.frames;
}
export function toggleShowFPS() {
showStats = ++showStats % 3;
}
export function updateSound() {
var on = document.querySelector('#enable_sound').checked;
var label = document.querySelector('#toggle-sound i');
audio.enable(on);
if (on) {
label.classList.remove('fa-volume-off');
label.classList.add('fa-volume-up');
} else {
label.classList.remove('fa-volume-up');
label.classList.add('fa-volume-off');
}
}
function dumpDisk(drive) {
var wind = window.open('', '_blank');
wind.document.title = driveLights.label(drive);
wind.document.write('<pre>');
wind.document.write(_disk2.getJSON(drive, true));
wind.document.write('</pre>');
wind.document.close();
}
export function reset() {
_apple2.reset();
}
function loadBinary(bin) {
for (var idx = 0; idx < bin.length; idx++) {
var pos = bin.start + idx;
cpu.write(pos >> 8, pos & 0xff, bin.data[idx]);
}
cpu.reset();
cpu.setPC(bin.start);
}
export function selectCategory() {
document.querySelector('#disk_select').innerHTML = '';
var cat = disk_categories[document.querySelector('#category_select').value];
if (cat) {
for (var idx = 0; idx < cat.length; idx++) {
var file = cat[idx], name = file.name;
if (file.disk) {
name += ' - ' + file.disk;
}
var option = document.createElement('option');
option.value = file.filename;
option.innerText = name;
document.querySelector('#disk_select').append(option);
if (disk_cur_name[_currentDrive] == name) {
option.selected = true;
}
}
}
}
export function selectDisk() {
document.querySelector('#local_file').value = '';
}
export function clickDisk() {
doLoad();
}
function loadDisk(drive, disk) {
var name = disk.name;
var category = disk.category;
if (disk.disk) {
name += ' - ' + disk.disk;
}
disk_cur_cat[drive] = category;
disk_cur_name[drive] = name;
_disk2.setDisk(drive, disk);
initGamepad(disk.gamepad);
}
/*
* LocalStorage Disk Storage
*/
function updateLocalStorage() {
var diskIndex = JSON.parse(window.localStorage.diskIndex || '{}');
var names = Object.keys(diskIndex), cat;
cat = disk_categories['Local Saves'] = [];
document.querySelector('#manage-modal-content').innerHTML = '';
names.forEach(function(name) {
cat.push({
'category': 'Local Saves',
'name': name,
'filename': 'local:' + name
});
document.querySelector('#manage-modal-content').innerHTML =
'<span class="local_save">' +
name +
' <a href="#" onclick="Apple2.doDelete(\'' +
name +
'\')">Delete</a><br /></span>';
});
cat.push({
'category': 'Local Saves',
'name': 'Manage Saves...',
'filename': 'local:__manage'
});
}
function saveLocalStorage(drive, name) {
var diskIndex = JSON.parse(window.localStorage.diskIndex || '{}');
var json = _disk2.getJSON(drive);
diskIndex[name] = json;
window.localStorage.diskIndex = JSON.stringify(diskIndex);
driveLights.label(drive, name);
driveLights.dirty(drive, false);
updateLocalStorage();
}
function deleteLocalStorage(name) {
var diskIndex = JSON.parse(window.localStorage.diskIndex || '{}');
if (diskIndex[name]) {
delete diskIndex[name];
openAlert('Deleted');
}
window.localStorage.diskIndex = JSON.stringify(diskIndex);
updateLocalStorage();
}
function loadLocalStorage(drive, name) {
var diskIndex = JSON.parse(window.localStorage.diskIndex || '{}');
if (diskIndex[name]) {
_disk2.setJSON(drive, diskIndex[name]);
driveLights.label(drive, name);
driveLights.dirty(drive, false);
}
}
if (window.localStorage !== undefined) {
document.querySelectorAll('.disksave').forEach(function (el) { el.style.display = 'inline-block';});
}
var oldcat = '';
var option;
for (var idx = 0; idx < window.disk_index.length; idx++) {
var file = window.disk_index[idx];
var cat = file.category;
var name = file.name, disk = file.disk;
if (file.e && !window.e) {
continue;
}
if (cat != oldcat) {
option = document.createElement('option');
option.value = cat;
option.innerText = cat;
document.querySelector('#category_select').append(option);
disk_categories[cat] = [];
oldcat = cat;
}
disk_categories[cat].push(file);
if (disk) {
if (!disk_sets[name]) {
disk_sets[name] = [];
}
disk_sets[name].push(file);
}
}
option = document.createElement('option');
option.innerText = 'Local Saves';
document.querySelector('#category_select').append(option);
updateLocalStorage();
function processHash(hash) {
var files = hash.split('|');
for (var idx = 0; idx < files.length; idx++) {
var file = files[idx];
if (file.indexOf('://') > 0) {
var parts = file.split('.');
var ext = parts[parts.length - 1].toLowerCase();
if (ext == 'json') {
loadAjax(idx + 1, file);
} else {
doLoadHTTP(idx + 1, file);
}
} else if (file) {
loadAjax(idx + 1, 'json/disks/' + file + '.json');
}
}
}
/*
* Keyboard/Gamepad routines
*/
function _keydown(evt) {
if (!focused && (!evt.metaKey || evt.ctrlKey || window.e)) {
evt.preventDefault();
var key = keyboard.mapKeyEvent(evt);
if (key !== 0xff) {
io.keyDown(key);
}
}
if (evt.keyCode === 112) { // F1 - Reset
cpu.reset();
evt.preventDefault(); // prevent launching help
} else if (evt.keyCode === 113) { // F2 - Full Screen
var elem = document.getElementById('screen');
if (evt.shiftKey) { // Full window, but not full screen
document.body.classList.toggle('full-page');
} else if (document.webkitCancelFullScreen) {
if (document.webkitIsFullScreen) {
document.webkitCancelFullScreen();
} else {
if (Element.ALLOW_KEYBOARD_INPUT) {
elem.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
} else {
elem.webkitRequestFullScreen();
}
}
} else if (document.mozCancelFullScreen) {
if (document.mozIsFullScreen) {
document.mozCancelFullScreen();
} else {
elem.mozRequestFullScreen();
}
}
} else if (evt.keyCode === 114) { // F3
io.keyDown(0x1b);
} else if (evt.keyCode === 117) { // F6 Quick Save
window.localStorage.state = base64_json_stringify(_apple2.getState());
} else if (evt.keyCode === 120) { // F9 Quick Restore
if (window.localStorage.state) {
_apple2.setState(base64_json_parse(window.localStorage.state));
}
} else if (evt.keyCode == 16) { // Shift
keyboard.shiftKey(true);
} else if (evt.keyCode == 20) { // Caps lock
keyboard.capslockKey();
} else if (evt.keyCode == 17) { // Control
keyboard.controlKey(true);
} else if (evt.keyCode == 91 || evt.keyCode == 93) { // Command
keyboard.commandKey(true);
} else if (evt.keyCode == 18) { // Alt
if (evt.location == 1) {
keyboard.commandKey(true);
} else {
keyboard.optionKey(true);
}
}
}
function _keyup(evt) {
if (!focused)
io.keyUp();
if (evt.keyCode == 16) { // Shift
keyboard.shiftKey(false);
} else if (evt.keyCode == 17) { // Control
keyboard.controlKey(false);
} else if (evt.keyCode == 91 || evt.keyCode == 93) { // Command
keyboard.commandKey(false);
} else if (evt.keyCode == 18) { // Alt
if (evt.location == 1) {
keyboard.commandKey(false);
} else {
keyboard.optionKey(false);
}
}
}
export function updateScreen() {
var mono = document.querySelector('#mono_screen').checked;
var scanlines = document.querySelector('#show_scanlines').checked;
var gl = document.querySelector('#gl_canvas').checked;
var screen = document.querySelector('#screen');
var overscan = document.querySelector('.overscan');
if (scanlines && !gl) {
overscan.classList.add('scanlines');
} else {
overscan.classList.remove('scanlines');
}
if (mono && !gl) {
screen.classList.add('mono');
} else {
screen.classList.remove('mono');
}
vm.mono(mono);
}
export function updateCPU() {
var accelerated = document.querySelector('#accelerator_toggle').checked;
var kHz = accelerated ? 4092 : 1023;
io.updateKHz(kHz);
}
export function updateUI() {
if (document.location.hash != hashtag) {
hashtag = document.location.hash;
var hash = hup();
if (hash) {
processHash(hash);
}
}
}
var disableMouseJoystick = false;
var flipX = false;
var flipY = false;
var swapXY = false;
export function updateJoystick() {
disableMouseJoystick = document.querySelector('#disable_mouse').checked;
flipX = document.querySelector('#flip_x').checked;
flipY = document.querySelector('#flip_y').checked;
swapXY = document.querySelector('#swap_x_y').checked;
configGamepad(flipX, flipY);
if (disableMouseJoystick) {
io.paddle(0, 0.5);
io.paddle(1, 0.5);
return;
}
}
function _mousemove(evt) {
if (gamepad || disableMouseJoystick) {
return;
}
var s = document.querySelector('#screen');
var offset = s.getBoundingClientRect();
var x = (evt.pageX - offset.left) / s.clientWidth,
y = (evt.pageY - offset.top) / s.clientHeight,
z = x;
if (swapXY) {
x = y;
y = z;
}
io.paddle(0, flipX ? 1 - x : x);
io.paddle(1, flipY ? 1 - y : y);
}
export function pauseRun() {
var label = document.querySelector('#pause-run i');
if (paused) {
vm.ready.then(() => {
_apple2.run();
});
label.classList.remove('fa-play');
label.classList.add('fa-pause');
} else {
_apple2.stop();
label.classList.remove('fa-pause');
label.classList.add('fa-play');
}
paused = !paused;
}
export function toggleSound() {
var enableSound = document.querySelector('#enable_sound');
enableSound.checked = !enableSound.checked;
updateSound();
}
export function openOptions() {
MicroModal.show('options-modal');
}
export function openPrinterModal() {
const mimeType = 'application/octet-stream';
const data = _printer.getRawOutput();
const a = document.querySelector('#raw_printer_output');
const blob = new Blob([data], { 'type': mimeType});
a.href = window.URL.createObjectURL(blob);
a.download = 'raw_printer_output.bin';
MicroModal.show('printer-modal');
}
export function clearPrinterPaper() {
_printer.clear();
}
function onLoaded(apple2, disk2, smartPort, printer, e) {
_apple2 = apple2;
cpu = _apple2.getCPU();
io = _apple2.getIO();
stats = apple2.getStats();
vm = apple2.getVideoModes();
tape = new Tape(io);
_disk2 = disk2;
_smartPort = smartPort;
_printer = printer;
keyboard = new KeyBoard(cpu, io, e);
keyboard.create('#keyboard');
audio = new Audio(io);
MicroModal.init();
/*
* Input Handling
*/
window.addEventListener('keydown', _keydown);
window.addEventListener('keyup', _keyup);
window.addEventListener('keydown', audio.autoStart);
if (window.ontouchstart !== undefined) {
window.addEventListener('touchstart', audio.autoStart);
}
window.addEventListener('mousedown', audio.autoStart);
window.addEventListener('paste', (event) => {
var paste = (event.clipboardData || window.clipboardData).getData('text');
io.setKeyBuffer(paste);
event.preventDefault();
});
window.addEventListener('copy', (event) => {
event.clipboardData.setData('text/plain', vm.getText());
event.preventDefault();
});
document.querySelectorAll('canvas').forEach(function(canvas) {
canvas.addEventListener('mousedown', function(evt) {
if (!gamepad) {
io.buttonDown(evt.which == 1 ? 0 : 1);
}
evt.preventDefault();
});
canvas.addEventListener('mouseup', function(evt) {
if (!gamepad) {
io.buttonUp(evt.which == 1 ? 0 : 1);
}
});
canvas.addEventListener('contextmenu', function(evt) {
evt.preventDefault();
});
});
document.body.addEventListener('mousemove', _mousemove);
document.querySelectorAll('input,textarea').forEach(function(input) {
input.addEventListener('focus', function() { focused = true; });
input.addEventListener('blur', function() { focused = false; });
});
if (prefs.havePrefs()) {
document.querySelectorAll('#options-modal input[type=checkbox]').forEach(function(el) {
var val = prefs.readPref(el.id);
if (val) {
el.checked = JSON.parse(val);
}
el.addEventListener('change', function() {
prefs.writePref(el.id, JSON.stringify(el.checked));
});
});
document.querySelectorAll('#options-modal select').forEach(function(el) {
var val = prefs.readPref(el.id);
if (val) {
el.value = val;
}
el.addEventListener('change', function() {
prefs.writePref(el.id, el.value);
});
});
}
if (navigator.standalone) {
document.body.classList.add('standalone');
}
cpu.reset();
setInterval(updateKHz, 1000);
updateSound();
updateScreen();
updateCPU();
initGamepad();
// Check for disks in hashtag
var hash = gup('disk') || hup();
if (hash) {
_apple2.stop();
processHash(hash);
} else {
vm.ready.then(() => {
_apple2.run();
});
}
}
export function initUI(apple2, disk2, smartPort, printer, e) {
window.addEventListener('load', () => {
onLoaded(apple2, disk2, smartPort, printer, e);
});
}

1053
js/ui/apple2.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,11 +9,11 @@ export default class DriveLights implements Callbacks {
'url(css/red-off-16.png)'; 'url(css/red-off-16.png)';
} }
public dirty() { public dirty(_drive: DriveNumber, _dirty: boolean) {
// document.querySelector('#disksave' + drive).disabled = !dirty; // document.querySelector('#disksave' + drive).disabled = !dirty;
} }
public label(drive: DriveNumber, label: string) { public label(drive: DriveNumber, label?: string) {
const labelElement = const labelElement =
document.querySelector('#disk-label' + drive)! as HTMLElement; document.querySelector('#disk-label' + drive)! as HTMLElement;
if (label) { if (label) {

View File

@ -19,7 +19,7 @@ export const TAPE_TYPES = ['wav', 'aiff', 'aif', 'mp3', 'm4a'] as const;
export default class Tape { export default class Tape {
constructor(private readonly io: Apple2IO) {} constructor(private readonly io: Apple2IO) {}
public doLoadLocalTape(file: File, done: () => void) { public doLoadLocalTape(file: File, done?: () => void) {
const kHz = this.io.getKHz(); const kHz = this.io.getKHz();
// Audio Buffer Source // Audio Buffer Source
@ -28,7 +28,7 @@ export default class Tape {
context = new AudioContext(); context = new AudioContext();
} else { } else {
window.alert('Not supported by your browser'); window.alert('Not supported by your browser');
done(); done && done();
return; return;
} }

View File

@ -69,4 +69,10 @@ export interface VideoModes extends Restorable<VideoModesState> {
isMixed(): boolean isMixed(): boolean
isPage2(): boolean isPage2(): boolean
isText(): boolean isText(): boolean
mono(on: boolean): void
getText(): string
ready: Promise<void>
} }

13
package-lock.json generated
View File

@ -15,6 +15,7 @@
"@babel/core": "^7.9.0", "@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.0", "@babel/preset-env": "^7.9.0",
"@types/jest": "^26.0.14", "@types/jest": "^26.0.14",
"@types/micromodal": "^0.3.2",
"@typescript-eslint/eslint-plugin": "^4.6.1", "@typescript-eslint/eslint-plugin": "^4.6.1",
"@typescript-eslint/parser": "^4.6.1", "@typescript-eslint/parser": "^4.6.1",
"ajv": "^6.12.0", "ajv": "^6.12.0",
@ -2163,6 +2164,12 @@
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
"dev": true "dev": true
}, },
"node_modules/@types/micromodal": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@types/micromodal/-/micromodal-0.3.2.tgz",
"integrity": "sha512-rQAprHsGUqtbngygYKWKpgbmWCbdR6injIeLELVjapsI7xXCjKlbaet62haRG+CLYOacXzlUlS0me8H+3RG1UQ==",
"dev": true
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "14.14.35", "version": "14.14.35",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz",
@ -14938,6 +14945,12 @@
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
"dev": true "dev": true
}, },
"@types/micromodal": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@types/micromodal/-/micromodal-0.3.2.tgz",
"integrity": "sha512-rQAprHsGUqtbngygYKWKpgbmWCbdR6injIeLELVjapsI7xXCjKlbaet62haRG+CLYOacXzlUlS0me8H+3RG1UQ==",
"dev": true
},
"@types/node": { "@types/node": {
"version": "14.14.35", "version": "14.14.35",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz",

View File

@ -27,6 +27,7 @@
"@babel/core": "^7.9.0", "@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.0", "@babel/preset-env": "^7.9.0",
"@types/jest": "^26.0.14", "@types/jest": "^26.0.14",
"@types/micromodal": "^0.3.2",
"@typescript-eslint/eslint-plugin": "^4.6.1", "@typescript-eslint/eslint-plugin": "^4.6.1",
"@typescript-eslint/parser": "^4.6.1", "@typescript-eslint/parser": "^4.6.1",
"ajv": "^6.12.0", "ajv": "^6.12.0",