* Enabled prettier

* Update lint, fix issues

* Restore some array formatting
This commit is contained in:
Will Scullin 2023-11-24 06:45:55 -08:00 committed by GitHub
parent e7891114c6
commit 1e79d9d59d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
172 changed files with 35468 additions and 33785 deletions

View File

@ -1,4 +1,5 @@
dist
json/disks/index.js
node_modules
submodules
tmp

View File

@ -1,23 +1,17 @@
{
// Global
"root": true,
"plugins": [
"prettier"
],
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:jest/recommended"
],
"rules": {
"indent": [
"error",
4,
{
"SwitchCase": 1
}
],
"quotes": [
"error",
"single"
],
"prettier/prettier": "error",
"linebreak-style": [
"error",
"unix"
@ -80,12 +74,6 @@
"rules": {
// recommended is just "warn"
"@typescript-eslint/no-explicit-any": "error",
// enforce semicolons at ends of statements
"semi": "off",
"@typescript-eslint/semi": [
"error",
"always"
],
// enforce semicolons to separate members
"@typescript-eslint/member-delimiter-style": [
"error",

View File

@ -3,25 +3,25 @@ name: Node CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x]
strategy:
matrix:
node-version: [16.x, 18.x]
steps:
- uses: actions/checkout@v2
with:
submodules: "true"
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: npm install, build, and test
run: |
npm ci
npm run build --if-present
npm test
env:
CI: true
steps:
- uses: actions/checkout@v2
with:
submodules: 'true'
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: npm install, build, and test
run: |
npm ci
npm run build --if-present
npm test
env:
CI: true

2
.prettierignore Normal file
View File

@ -0,0 +1,2 @@
js/roms
submodules

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": true
}

View File

@ -1,10 +1,9 @@
{
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-css-modules"
],
"rules": {
"indentation": 4,
"selector-class-pattern": "^[a-z][a-zA-Z0-9_-]+$"
}
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-css-modules"
],
"rules": {
"selector-class-pattern": "^[a-z][a-zA-Z0-9_-]+$"
}
}

View File

@ -46,29 +46,29 @@ then
### 2019-03-02
* Behind the scenes
- Behind the scenes
A lot of not so visible changes behind the scenes. The website now runs directly off of github, and has a more modern toolchain (Goodbye make, Perl and closure compiler, hello Webpack!) to facilitate development.
* Performance
- Performance
In particular, the screen no longer redraws as quickly as possible.
* Drag and Drop
- Drag and Drop
Disk images can be dragged into the window to load them.
* Contributions
- Contributions
Thanks to [Ian Flanigan](https://github.com/iflan) for additions to improve ChromeBook behavior.
### 2017-10-08
* Better iOS support
- Better iOS support
Bluetooth keyboards now work better. Reset is Ctrl-Shift-Delete. iOS now allows loading disks from iCloud and services like Drop Box. Saving locally is still not supported by iOS. I now understand why sound doesn't work, and I'm working on a work-around.
* Source Maps
- Source Maps
Although the source code has always been available, by default I serve up minified Javascript for performance reasons. But now you can poke around more easily.
@ -76,102 +76,104 @@ then
(It's been a long time since I updated, so this is a rough list)
* Videx Videoterm Emulation (\]\[js)
- Videx Videoterm Emulation (\]\[js)
PR#3 now does something on the Apple \]\[!.
* AppleColor RGB Card Emulation (//jse)
- AppleColor RGB Card Emulation (//jse)
Now supports a bunch of the mostly non-standard video formats found on the AppleColor RGB card, including 16 color text, 16 color hires mode, and mixed black and white and color double hires
* Machine selection
- Machine selection
You can now select between original, autostart and plus Apple \]\[s, and unenhanced and enhanced //es.
## Updates (2013-07-04)
* RAMFactor Emulation (//jse)
- RAMFactor Emulation (//jse)
I now simulate having a 1 Megabyte RAMFactor card in slot 2.
* Thunderclock Emulation
- Thunderclock Emulation
There is cursory emulation of the Thunderclock card, enough to keep ProDOS applications from asking you to enter the date all the time. ProDOS attempts to guess the year from the month, the day and the day of the week, something that needs to be patched every 6 years. This means newer versions think it's 1996, older versions are stuck in the 80s.
* Firefox Nightly Joystick Support
- Firefox Nightly Joystick Support
Joystick support has yet to officially land, but the latest nightlies support the gamepad API.
## Updates (2013-03-20)
* Animation Frames
- Animation Frames
I've switched from using setInterval() to requestAnimationFrame() where supported. This, in conjunction with the graphics re-write, seems to smooth performance and provide a more stable CPU speed.
* Graphics Re-Write
- Graphics Re-Write
This (third) re-write of the graphics system should improve performance with graphics intensive programs. Rather than rendering each graphics update as it happens, updates are rendered each animation frame.
## Updates (2013-03-12)
* Apple //e
- Apple //e
After much flailing, and much staring at MMU emulation code in despair, I've finally published my Apple //e emulator. It's probably a little more rough than I'd hoped, but it has a lot of features that I really wanted to get into it, like basic double hires support, and it uses the enhanced Apple //e ROMs.
## Updates (2013-02-25)
* Joystick Support
- Joystick Support
Chrome only so far, the nascent gamepad API has finally allowed me to add basic joystick support. I can now re-live my glory days of Skyfox.
* Re-written CPU emulator
- Re-written CPU emulator
I finally got around to applying some of the many lessons I learned along the way writing my first CPU emulator in Javascript. The last re-working gave me about a 100% performance gain.
* Finally Fixed Oregon Trail
- Finally Fixed Oregon Trail
This seems to have been a major disappointment for many people. I was able to make it as a banker, but I'm embarassed to reveal my score.
* Competition
- Competition
Now in addition to [Gil Megidish's](http://www.megidish.net/apple2js/) Apple2JS, there's a couple of new kids on the block, including [David Caldwell's](http://porkrind.org/a2/) Apple II+ emulator where he's put a lot more thought into the graphics rendering than I have, and [appletoo](https://github.com/nicholasbs/appletoo), which I just stumbled across while looking for David's emulator and haven't had much time to look at.
## Requirements
* A Browser with HTML5 Support
- A Browser with HTML5 Support
The most recent versions of [Google Chrome](https://www.google.com/chrome/), [Safari](https://www.apple.com/safari/), [Firefox](https://www.firefox.com/), and [Opera](https//www.opera.com/) all seem to work reasonably well these days,although variations in HTML5 support pop up, and occasionally a major release will move things around out from under me. IE prior to 9 lacks canvas tag support and is unsupported. [IE 9+](https://windows.microsoft.com/ie9) renders nicely on a modern machine.
* Basic Knowledge of the Apple \]\[
- Basic Knowledge of the Apple \]\[
If you don't know how to use an Apple \]\[, this won't be much fun for you.
## Acknowledgements
* I'm using the following libraries:
- I'm using the following libraries:
* [jQuery](https://jquery.com) and [jQuery UI](https://jqueryui.com)
* Base64 Utilities via [KvZ](http://kevin.vanzonneveld.net/)
* LED graphics from [Modern Life](http://modernl.com/).
* [CFFA2 Firmware](http://dreher.net/?s=projects/CFforAppleII&c=projects/CFforAppleII/downloads1.php) by Chris Schumann, Rich Dreher and Dave Lyons
- [jQuery](https://jquery.com) and [jQuery UI](https://jqueryui.com)
- Base64 Utilities via [KvZ](http://kevin.vanzonneveld.net/)
- LED graphics from [Modern Life](http://modernl.com/).
- [CFFA2 Firmware](http://dreher.net/?s=projects/CFforAppleII&c=projects/CFforAppleII/downloads1.php) by Chris Schumann, Rich Dreher and Dave Lyons
* I heavily referenced:
- I heavily referenced:
* [_Beneath Apple DOS_](http://www.scribd.com/doc/200679/Beneath-Apple-DOS-By-Don-Worth-and-Pieter-Lechner) by Don Worth and Pieter Lechner
* _Inside the Apple //e_ by Gary B. Little
* [_DOS 3.3 Anatomy_](http://apple2.org.za/gswv/a2zine/GS.WorldView/Resources/DOS.3.3.ANATOMY/)
* [_Apple II Disk Drive Article_](http://www.doc.ic.ac.uk/~ih/doc/stepper/others/example3/diskii_specs.html) by Neil Parker
* [6502.org](http://6502.org/)
* The [comp.sys.apple2.programmer](http://www.faqs.org/faqs/apple2/programmerfaq/part1/) FAQ
* [Understanding the Apple \]\[](https://archive.org/details/understanding_the_apple_ii) and [Understanding the Apple //e](https://archive.org/details/Understanding_the_Apple_IIe) by Jim Sather.
* [Apple II Colors](https://mrob.com/pub/xapple2/colors.html) by Robert Munafo.
- [_Beneath Apple DOS_](http://www.scribd.com/doc/200679/Beneath-Apple-DOS-By-Don-Worth-and-Pieter-Lechner) by Don Worth and Pieter Lechner
- _Inside the Apple //e_ by Gary B. Little
- [_DOS 3.3 Anatomy_](http://apple2.org.za/gswv/a2zine/GS.WorldView/Resources/DOS.3.3.ANATOMY/)
- [_Apple II Disk Drive Article_](http://www.doc.ic.ac.uk/~ih/doc/stepper/others/example3/diskii_specs.html) by Neil Parker
- [6502.org](http://6502.org/)
- The [comp.sys.apple2.programmer](http://www.faqs.org/faqs/apple2/programmerfaq/part1/) FAQ
- [Understanding the Apple \]\[](https://archive.org/details/understanding_the_apple_ii) and [Understanding the Apple //e](https://archive.org/details/Understanding_the_Apple_IIe) by Jim Sather.
- [Apple II Colors](https://mrob.com/pub/xapple2/colors.html) by Robert Munafo.
* And special thanks to:
- And special thanks to:
* [ADTPro](http://adtpro.sourceforge.net/) for allowing me to pull some of my circa 1980 programming efforts off some ancient floppies.
* [KEGS](http://kegs.sourceforge.net/), because at some point I got so tired of futzing with ADC/SBC code I just ported the KEGS C code for those opcodes to Javascript so I could stop worrying about it.
* [Apple II History](http://apple2history.org/), for a lovely, informative site.
* [Gil Megidish](http://www.megidish.net/apple2js/), for the kick in the pants to finally post my version, once I realized there was, in fact, another apple2js in the world.
* [AppleWin](https://github.com/AppleWin/AppleWin/), whose source code is a goldmine of useful references.
* [Zellyn Hunter](https://github.com/zellyn/a2audit) and a2audit, for allowing me to get really nitpicky in my memory emulation.
- [ADTPro](http://adtpro.sourceforge.net/) for allowing me to pull some of my circa 1980 programming efforts off some ancient floppies.
- [KEGS](http://kegs.sourceforge.net/), because at some point I got so tired of futzing with ADC/SBC code I just ported the KEGS C code for those opcodes to Javascript so I could stop worrying about it.
- [Apple II History](http://apple2history.org/), for a lovely, informative site.
- [Gil Megidish](http://www.megidish.net/apple2js/), for the kick in the pants to finally post my version, once I realized there was, in fact, another apple2js in the world.
- [AppleWin](https://github.com/AppleWin/AppleWin/), whose source code is a goldmine of useful references.
- [Zellyn Hunter](https://github.com/zellyn/a2audit) and a2audit, for allowing me to get really nitpicky in my memory emulation.
* Contributors
* [Snapperfish](http://github.com/Snapperfish) Various fixes
- Contributors
- [Snapperfish](http://github.com/Snapperfish) Various fixes

View File

@ -12,16 +12,17 @@ module.exports = {
[
'@babel/typescript',
{
jsxPragma: 'h'
}
jsxPragma: 'h',
},
],
],
plugins: [
[
'@babel/plugin-transform-react-jsx', {
'@babel/plugin-transform-react-jsx',
{
pragma: 'h',
pragmaFrag: 'Fragment',
}
]
]
},
],
],
};

View File

@ -44,7 +44,7 @@ function read2MG(fileData) {
return {
diskData: fileData.slice(headerLength),
type: type,
readOnly: readOnly
readOnly: readOnly,
};
}
@ -53,11 +53,11 @@ function readTracks(type, diskData) {
if (type === 'nib') {
let start = 0;
let end = 0x1A00;
let end = 0x1a00;
while (start < diskData.length) {
const trackData = diskData.slice(start, end);
start += 0x1A00;
end += 0x1A00;
start += 0x1a00;
end += 0x1a00;
tracks.push(trackData.toString('base64'));
}
@ -82,7 +82,6 @@ function readTracks(type, diskData) {
const fileName = argv._[0];
let readOnly = argv.r || argv.readOnly ? true : undefined;
const name = argv.n || argv.name;
const category = argv.c || argv.category;
@ -110,7 +109,7 @@ fs.readFile(fileName, (err, fileData) => {
let diskData;
if (type === '2mg') {
({diskData, readOnly, type} = read2MG(fileData));
({ diskData, readOnly, type } = read2MG(fileData));
} else {
diskData = fileData;
}
@ -123,7 +122,7 @@ fs.readFile(fileName, (err, fileData) => {
readOnly,
disk,
'2e': e,
data: readTracks(type, diskData)
data: readTracks(type, diskData),
};
Object.keys(entry).forEach((key) => {

View File

@ -1,6 +1,5 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
@ -13,8 +12,12 @@ for (const fileName of dir.sort()) {
if (/\.json$/.test(fileName)) {
const json = fs.readFileSync(path.resolve(diskPath, fileName));
const data = JSON.parse(json);
if (data.private) { continue; }
if (!data.name || !data.category) { continue; }
if (data.private) {
continue;
}
if (!data.name || !data.category) {
continue;
}
const entry = {
filename: `json/disks/${fileName}`,

View File

@ -1,6 +1,6 @@
#header {
width: 580px;
margin: auto;
width: 580px;
margin: auto;
}
img {
@ -16,22 +16,22 @@ img {
margin: 0;
padding: 3px 0 0 10;
color: black;
font-family: "Adobe Garamond Pro",Garamond,Times;
font-family: 'Adobe Garamond Pro', Garamond, Times;
font-size: 13px;
font-weight: normal;
user-select: none;
}
.motter {
font-family: "Motter Tektura";
font-family: 'Motter Tektura';
font-size: 12px;
}
input[type="button"] {
input[type='button'] {
width: 75px;
}
input[type="text"] {
input[type='text'] {
width: 40px;
}
@ -73,7 +73,7 @@ body {
padding: 0;
border: 0;
position: fixed;
top:0;
top: 0;
bottom: 0;
left: 0;
right: 0;
@ -100,7 +100,9 @@ body {
height: 100%;
}
.full-page #header, .full-page .inset, .full-page #reset {
.full-page #header,
.full-page .inset,
.full-page #reset {
display: none;
}
@ -187,9 +189,9 @@ th {
}
canvas {
display: block;
float: left;
image-rendering: pixelated;
display: block;
float: left;
image-rendering: pixelated;
}
.mono {
@ -199,7 +201,13 @@ canvas {
.scanlines:after {
display: block;
pointer-events: none;
background-image: repeating-linear-gradient(to bottom, transparent 0, transparent 1px, rgba(0,0,0,0.5) 1px, rgba(0,0,0,0.5) 2px);
background-image: repeating-linear-gradient(
to bottom,
transparent 0,
transparent 1px,
rgba(0, 0, 0, 0.5) 1px,
rgba(0, 0, 0, 0.5) 2px
);
content: '';
position: absolute;
top: 0;
@ -209,7 +217,13 @@ canvas {
}
.full-page .scanlines:after {
background-image: repeating-linear-gradient(to bottom, transparent 0, transparent 0.25vh, rgba(0,0,0,0.5) 0.25vh, rgba(0,0,0,0.5) 0.5vh);
background-image: repeating-linear-gradient(
to bottom,
transparent 0,
transparent 0.25vh,
rgba(0, 0, 0, 0.5) 0.25vh,
rgba(0, 0, 0, 0.5) 0.5vh
);
}
#screen {
@ -238,7 +252,7 @@ canvas {
#about {
padding: 16px;
margin: 0;
font-family: "Adobe Garamond Pro",Garamond,Times;
font-family: 'Adobe Garamond Pro', Garamond, Times;
font-size: 14px;
background-color: #fff;
color: #000;
@ -287,7 +301,7 @@ canvas {
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.6);
background: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
@ -307,10 +321,10 @@ canvas {
font-size: 14px;
justify-content: space-between;
align-items: center;
background: #44372C;
background: #44372c;
color: #fff;
padding: 5px 11px;
border: 1px outset #66594E;
border: 1px outset #66594e;
border-radius: 3px;
user-select: none;
}
@ -325,13 +339,15 @@ canvas {
box-sizing: border-box;
}
.modal__close, .modal__close:active, .modal__close:hover {
.modal__close,
.modal__close:active,
.modal__close:hover {
background: transparent;
padding: 3px;
}
.modal__header .modal__close:before {
content: "\2715";
content: '\2715';
}
.modal__content {
@ -348,10 +364,10 @@ canvas {
button,
a.button {
background: #44372C;
background: #44372c;
color: #fff;
padding: 2px 8px;
border: 1px outset #66594E;
border: 1px outset #66594e;
border-radius: 3px;
font-size: 15px;
text-decoration: none;
@ -359,14 +375,14 @@ a.button {
button:hover,
a.button:hover {
background-color: #55473D;
border: 1px outset #66594E;
background-color: #55473d;
border: 1px outset #66594e;
}
button:active,
a.button:active {
background-color: #22150A;
border: 1px outset #44372C;
background-color: #22150a;
border: 1px outset #44372c;
}
button:focus,
@ -374,7 +390,8 @@ a.button:hover {
outline: none;
}
#keyboard, #reset {
#keyboard,
#reset {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
@ -435,23 +452,23 @@ a.button:hover {
padding: 0;
width: 32px;
height: 32px;
background-color: #44372C;
background-color: #44372c;
color: white;
text-align: center;
position: relative;
border-left: 5px solid #65594D;
border-top: 5px solid #65594D;
border-right: 5px solid #110E0D;
border-bottom: 5px solid #110E0D;
border-left: 5px solid #65594d;
border-top: 5px solid #65594d;
border-right: 5px solid #110e0d;
border-bottom: 5px solid #110e0d;
/* border: 5px outset #66594E; */
border-radius: 5px;
cursor: pointer;
}
#keyboard .pressed {
background-color: #22150A;
border-left: 5px solid #44372C;
border-top: 5px solid #44372C;
background-color: #22150a;
border-left: 5px solid #44372c;
border-top: 5px solid #44372c;
border-right: 5px solid #000000;
border-bottom: 5px solid #000000;
/* border: 5px outset #44372C; */
@ -602,11 +619,11 @@ a.button:hover {
}
#reset {
background: #44372C;
border-left: 3px solid #65594D;
border-top: 3px solid #65594D;
border-right: 3px solid #110E0D;
border-bottom: 3px solid #110E0D;
background: #44372c;
border-left: 3px solid #65594d;
border-top: 3px solid #65594d;
border-right: 3px solid #110e0d;
border-bottom: 3px solid #110e0d;
/* border: 5px outset #66594E; */
border-radius: 3px;
color: white;
@ -621,14 +638,14 @@ a.button:hover {
}
#reset:hover {
background: #44372C;
border: 3px outset #66594E;
background: #44372c;
border: 3px outset #66594e;
}
#reset:active {
background-color: #22150A;
border-left: 3px solid #44372C;
border-top: 3px solid #44372C;
background-color: #22150a;
border-left: 3px solid #44372c;
border-top: 3px solid #44372c;
border-right: 3px solid #000000;
border-bottom: 3px solid #000000;
}
@ -654,8 +671,8 @@ a.button:hover {
}
.standalone {
position: fixed;
width: 100%;
position: fixed;
width: 100%;
}
.standalone #header {

View File

@ -1,32 +1,38 @@
<!DOCTYPE html>
<!doctype html>
<head>
<title>PreApple II</title>
<meta name="viewport" content="width=640, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta charset="utf-8" />
<title>PreApple II</title>
<meta name="viewport" content="width=640, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta charset="utf-8" />
<link rel="apple-touch-icon" href="img/webapp-iphone.png" />
<link rel="apple-touch-icon" size="72x72" href="img/webapp-ipad.png" />
<link rel="shortcut icon" href="img/logoicon.png" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v6.1.1/css/all.css" />
<style>
body {
background-color: #c4c1a0;
margin: 16px 0;
}
</style>
<link rel="apple-touch-icon" href="img/webapp-iphone.png" />
<link rel="apple-touch-icon" size="72x72" href="img/webapp-ipad.png" />
<link rel="shortcut icon" href="img/logoicon.png" />
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v6.1.1/css/all.css"
/>
<style>
body {
background-color: #c4c1a0;
margin: 16px 0;
}
</style>
</head>
<body>
<div id="app"></div>
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
<filter id="green">
<feColorMatrix type="matrix" values="0.0 0.0 0.0 0.0 0
<div id="app"></div>
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
<filter id="green">
<feColorMatrix
type="matrix"
values="0.0 0.0 0.0 0.0 0
0.0 1.0 0.0 0.0 0
0.0 0.0 0.5 0.0 0
0.0 0.0 0.0 1.0 0" />
</filter>
</svg>
<script src="dist/preact.bundle.js"></script>
0.0 0.0 0.0 1.0 0"
/>
</filter>
</svg>
<script src="dist/preact.bundle.js"></script>
</body>

View File

@ -1,5 +1,5 @@
module.exports = {
'moduleNameMapper': {
moduleNameMapper: {
'^js/(.*)': '<rootDir>/js/$1',
'^test/(.*)': '<rootDir>/test/$1',
'\\.css$': 'identity-obj-proxy',
@ -9,28 +9,17 @@ module.exports = {
// https://github.com/preactjs/enzyme-adapter-preact-pure/issues/179#issuecomment-1201096897
'^preact(/(.*)|$)': 'preact$1',
},
'roots': [
'js/',
'test/',
],
'testMatch': [
'**/?(*.)+(spec|test).+(ts|js|tsx)'
],
'transform': {
roots: ['js/', 'test/'],
testMatch: ['**/?(*.)+(spec|test).+(ts|js|tsx)'],
transform: {
'^.+\\.js$': 'babel-jest',
'^.+\\.ts$': 'ts-jest',
'^.*\\.tsx$': 'ts-jest',
},
'transformIgnorePatterns': [
transformIgnorePatterns: [
'/node_modules/(?!(@testing-library/preact/dist/esm)/)',
],
'setupFilesAfterEnv': [
'<rootDir>/test/jest-setup.ts'
],
'coveragePathIgnorePatterns': [
'/node_modules/',
'/js/roms/',
'/test/',
],
'preset': 'ts-jest',
setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'],
coveragePathIgnorePatterns: ['/node_modules/', '/js/roms/', '/test/'],
preset: 'ts-jest',
};

View File

@ -5,16 +5,8 @@ import {
VideoModes,
VideoModesState,
} from './videomodes';
import {
HiresPage2D,
LoresPage2D,
VideoModes2D,
} from './canvas';
import {
HiresPageGL,
LoresPageGL,
VideoModesGL,
} from './gl';
import { HiresPage2D, LoresPage2D, VideoModes2D } from './canvas';
import { HiresPageGL, LoresPageGL, VideoModesGL } from './gl';
import ROM from './roms/rom';
import { Apple2IOState } from './apple2io';
import {
@ -82,7 +74,7 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
private stats: Stats = {
cycles: 0,
frames: 0,
renderedFrames: 0
renderedFrames: 0,
};
public ready: Promise<void>;
@ -92,23 +84,30 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
}
async init(options: Apple2Options) {
const romImportPromise = import(`./roms/system/${options.rom}`) as Promise<{ default: new () => ROM }>;
const characterRomImportPromise = import(`./roms/character/${options.characterRom}`) as Promise<{ default: ReadonlyUint8Array }>;
const romImportPromise = import(
`./roms/system/${options.rom}`
) as Promise<{
default: new () => ROM;
}>;
const characterRomImportPromise = import(
`./roms/character/${options.characterRom}`
) as Promise<{ default: ReadonlyUint8Array }>;
const LoresPage = options.gl ? LoresPageGL : LoresPage2D;
const HiresPage = options.gl ? HiresPageGL : HiresPage2D;
const VideoModes = options.gl ? VideoModesGL : VideoModes2D;
this.cpu = new CPU6502({
flavor: options.enhanced ? FLAVOR_ROCKWELL_65C02 : FLAVOR_6502
flavor: options.enhanced ? FLAVOR_ROCKWELL_65C02 : FLAVOR_6502,
});
this.vm = new VideoModes(options.canvas, options.e);
const [{ default: Apple2ROM }, { default: characterRom }] = await Promise.all([
romImportPromise,
characterRomImportPromise,
this.vm.ready,
]);
const [{ default: Apple2ROM }, { default: characterRom }] =
await Promise.all([
romImportPromise,
characterRomImportPromise,
this.vm.ready,
]);
this.rom = new Apple2ROM();
this.characterRom = characterRom;
@ -121,13 +120,22 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
this.tick = options.tick;
if (options.e) {
this.mmu = new MMU(this.cpu, this.vm, this.gr, this.gr2, this.hgr, this.hgr2, this.io, this.rom);
this.mmu = new MMU(
this.cpu,
this.vm,
this.gr,
this.gr2,
this.hgr,
this.hgr2,
this.io,
this.rom
);
this.cpu.addPageHandler(this.mmu);
} else {
this.ram = [
new RAM(0x00, 0x03),
new RAM(0x0C, 0x1F),
new RAM(0x60, 0xBF)
new RAM(0x0c, 0x1f),
new RAM(0x60, 0xbf),
];
this.cpu.addPageHandler(this.ram[0]);
@ -158,7 +166,8 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
const interval = 30;
let now, last = Date.now();
let now,
last = Date.now();
const runFn = () => {
const kHz = this.io.getKHz();
now = Date.now();
@ -228,7 +237,7 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
vm: this.vm.getState(),
io: this.io.getState(),
mmu: this.mmu?.getState(),
ram: this.ram?.map(bank => bank.getState()),
ram: this.ram?.map((bank) => bank.getState()),
};
return state;

View File

@ -12,7 +12,7 @@ type Annunciators = Record<annunciator, boolean>;
export interface Apple2IOState {
annunciators: Annunciators;
cards: Array<unknown | null>;
cards: Array<unknown>;
}
export type SampleListener = (sample: number[]) => void;
@ -33,12 +33,12 @@ const LOC = {
SETHIRES: 0x57, // select Hi-res
CLRAN0: 0x58, // Set annunciator-0 output to 0
SETAN0: 0x59, // Set annunciator-0 output to 1
CLRAN1: 0x5A, // Set annunciator-1 output to 0
SETAN1: 0x5B, // Set annunciator-1 output to 1
CLRAN2: 0x5C, // Set annunciator-2 output to 0
SETAN2: 0x5D, // Set annunciator-2 output to 1
CLRAN3: 0x5E, // Set annunciator-3 output to 0
SETAN3: 0x5F, // Set annunciator-3 output to 1
CLRAN1: 0x5a, // Set annunciator-1 output to 0
SETAN1: 0x5b, // Set annunciator-1 output to 1
CLRAN2: 0x5c, // Set annunciator-2 output to 0
SETAN2: 0x5d, // Set annunciator-2 output to 1
CLRAN3: 0x5e, // Set annunciator-3 output to 0
SETAN3: 0x5f, // Set annunciator-3 output to 1
TAPEIN: 0x60, // bit 7: data from cassette
PB0: 0x61, // game Pushbutton 0 / open apple (command) key data
PB1: 0x62, // game Pushbutton 1 / closed apple (option) key data
@ -51,7 +51,9 @@ const LOC = {
ACCEL: 0x74, // CPU Speed control
};
export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState> {
export default class Apple2IO
implements MemoryPages, Restorable<Apple2IOState>
{
private _slot: Array<Card | null> = new Array<Card | null>(7).fill(null);
private _auxRom: Memory | null = null;
@ -85,7 +87,10 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
private _tapeNext: number = 0;
private _tapeCurrent = false;
constructor(private readonly cpu: CPU6502, private readonly vm: VideoModes) {
constructor(
private readonly cpu: CPU6502,
private readonly vm: VideoModes
) {
this.init();
}
@ -99,8 +104,16 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
_tick() {
const now = this.cpu.getCycles();
const phase = this._didAudio ? (this._phase > 0 ? this._high : this._low) : 0.0;
for (; this._sampleTime < now; this._sampleTime += this._cycles_per_sample) {
const phase = this._didAudio
? this._phase > 0
? this._high
: this._low
: 0.0;
for (
;
this._sampleTime < now;
this._sampleTime += this._cycles_per_sample
) {
this._sample[this._sampleIdx++] = phase;
if (this._sampleIdx === this._sample_size) {
if (this._audioListener) {
@ -114,7 +127,7 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
}
_calcSampleRate() {
this._cycles_per_sample = this._khz * 1000 / this._rate;
this._cycles_per_sample = (this._khz * 1000) / this._rate;
}
_updateKHz(khz: number) {
@ -200,16 +213,16 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
result = this._button[2] ? 0x80 : 0;
break;
case LOC.PADDLE0:
result = (delta < (this._paddle[0] * 2756) ? 0x80 : 0x00);
result = delta < this._paddle[0] * 2756 ? 0x80 : 0x00;
break;
case LOC.PADDLE1:
result = (delta < (this._paddle[1] * 2756) ? 0x80 : 0x00);
result = delta < this._paddle[1] * 2756 ? 0x80 : 0x00;
break;
case LOC.PADDLE2:
result = (delta < (this._paddle[2] * 2756) ? 0x80 : 0x00);
result = delta < this._paddle[2] * 2756 ? 0x80 : 0x00;
break;
case LOC.PADDLE3:
result = (delta < (this._paddle[3] * 2756) ? 0x80 : 0x00);
result = delta < this._paddle[3] * 2756 ? 0x80 : 0x00;
break;
case LOC.ACCEL:
if (val !== undefined) {
@ -225,13 +238,12 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
if (this._tapeOffset < this._tape.length) {
this._tapeCurrent = this._tape[this._tapeOffset][1];
while (now >= this._tapeNext) {
if ((this._tapeOffset % 1000) === 0) {
if (this._tapeOffset % 1000 === 0) {
debug(`Read ${this._tapeOffset / 1000}`);
}
this._tapeCurrent = this._tape[this._tapeOffset][1];
this._tapeNext += this._tape[this._tapeOffset++][0];
}
}
result = this._tapeCurrent ? 0x80 : 0x00;
@ -344,7 +356,7 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
slot = page & 0x0f;
card = this._slot[slot];
if (this._auxRom !== card) {
// _debug('Setting auxRom to slot', slot);
// _debug('Setting auxRom to slot', slot);
this._auxRom = card;
}
if (card) {
@ -380,7 +392,7 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
slot = page & 0x0f;
card = this._slot[slot];
if (this._auxRom !== card) {
// _debug('Setting auxRom to slot', slot);
// _debug('Setting auxRom to slot', slot);
this._auxRom = card;
}
if (card) {
@ -399,13 +411,15 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
// TODO vet more potential state
return {
annunciators: this._annunciators,
cards: this._slot.map((card) => card ? card.getState() : null)
cards: this._slot.map((card) => (card ? card.getState() : null)),
};
}
setState(state: Apple2IOState) {
this._annunciators = state.annunciators;
state.cards.map((cardState, idx) => this._slot[idx]?.setState(cardState));
state.cards.map(
(cardState, idx) => this._slot[idx]?.setState(cardState)
);
}
setSlot(slot: slot, card: Card) {

View File

@ -51,7 +51,10 @@ function writeWord(mem: Memory, addr: word, val: byte) {
class LineBuffer implements IterableIterator<string> {
private prevChar: number = 0;
constructor(private readonly line: string, private curChar: number = 0) { }
constructor(
private readonly line: string,
private curChar: number = 0
) {}
[Symbol.iterator](): IterableIterator<string> {
return this;
@ -132,7 +135,11 @@ export default class ApplesoftCompiler {
* @param programStart Optional start address of the program. Defaults to
* standard Applesoft program address, 0x801.
*/
static compileToMemory(mem: Memory, program: string, programStart: word = PROGRAM_START) {
static compileToMemory(
mem: Memory,
program: string,
programStart: word = PROGRAM_START
) {
const compiler = new ApplesoftCompiler();
compiler.compile(program);
const compiledProgram: Uint8Array = compiler.program(programStart);
@ -305,8 +312,8 @@ export default class ApplesoftCompiler {
for (const lineNo of lineNumbers) {
const lineBytes = this.lines.get(lineNo) || [];
const nextLineAddr = programStart + result.length + 4
+ lineBytes.length + 1; // +1 for the zero at end of line
const nextLineAddr =
programStart + result.length + 4 + lineBytes.length + 1; // +1 for the zero at end of line
result.push(nextLineAddr & 0xff, nextLineAddr >> 8);
result.push(lineNo & 0xff, lineNo >> 8);
result.push(...lineBytes);

View File

@ -48,7 +48,6 @@ const DEFAULT_DECOMPILE_OPTIONS: DecompileOptions = {
const MAX_LINES = 32768;
export default class ApplesoftDecompiler {
/**
* Returns a decompiler for the program in the given memory.
*
@ -57,10 +56,16 @@ export default class ApplesoftDecompiler {
static decompilerFromMemory(ram: Memory): ApplesoftDecompiler {
const program: byte[] = [];
const start = ram.read(0x00, TXTTAB) + (ram.read(0x00, TXTTAB + 1) << 8);
const start =
ram.read(0x00, TXTTAB) + (ram.read(0x00, TXTTAB + 1) << 8);
const end = ram.read(0x00, PRGEND) + (ram.read(0x00, PRGEND + 1) << 8);
if (start >= 0xc000 || end >= 0xc000) {
throw new Error(`Program memory ${toHex(start, 4)}-${toHex(end, 4)} out of range`);
throw new Error(
`Program memory ${toHex(start, 4)}-${toHex(
end,
4
)} out of range`
);
}
for (let addr = start; addr <= end; addr++) {
program.push(ram.read(addr >> 8, addr & 0xff));
@ -117,8 +122,10 @@ export default class ApplesoftDecompiler {
* is the offset of the line number of the line; the tokens follow.
*/
private forEachLine(
from: number, to: number,
callback: (offset: word) => void): void {
from: number,
to: number,
callback: (offset: word) => void
): void {
let count = 0;
let offset = 0;
let nextLineAddr = this.wordAt(offset);
@ -223,12 +230,15 @@ export default class ApplesoftDecompiler {
* @param from The first line to print (default 0).
* @param to The last line to print (default end of program).
*/
list(options: Partial<ListOptions> = {},
from: number = 0, to: number = 65536): string {
list(
options: Partial<ListOptions> = {},
from: number = 0,
to: number = 65536
): string {
const allOptions = { ...DEFAULT_LIST_OPTIONS, ...options };
let result = '';
this.forEachLine(from, to, offset => {
this.forEachLine(from, to, (offset) => {
result += this.listLine(offset, allOptions);
});
return result;
@ -264,7 +274,8 @@ export default class ApplesoftDecompiler {
spaceIf = () => false;
if (token === STRING_TO_TOKEN['AT']) {
spaceIf = (nextToken) => nextToken.toUpperCase().startsWith('N');
spaceIf = (nextToken) =>
nextToken.toUpperCase().startsWith('N');
}
offset++;
@ -329,13 +340,20 @@ export default class ApplesoftDecompiler {
/**
* Decompiles the program based on the given options.
*/
decompile(options: Partial<DecompileOptions> = {},
from: number = 0, to: number = 65536): string {
decompile(
options: Partial<DecompileOptions> = {},
from: number = 0,
to: number = 65536
): string {
const allOptions = { ...DEFAULT_DECOMPILE_OPTIONS, ...options };
const results: string[] = [];
this.forEachLine(from, to, offset => {
results.push(allOptions.style === 'compact' ? this.compactLine(offset) : this.prettyLine(offset));
this.forEachLine(from, to, (offset) => {
results.push(
allOptions.style === 'compact'
? this.compactLine(offset)
: this.prettyLine(offset)
);
});
return results.join('\n');
}

View File

@ -1,23 +1,15 @@
import { byte, word, Memory } from 'js/types';
import { toHex } from 'js/util';
import {
CURLINE,
ARG,
FAC,
ARYTAB,
STREND,
TXTTAB,
VARTAB
} from './zeropage';
import { CURLINE, ARG, FAC, ARYTAB, STREND, TXTTAB, VARTAB } from './zeropage';
export type ApplesoftValue = word | number | string | ApplesoftArray;
export type ApplesoftValue = word | string | ApplesoftArray;
export type ApplesoftArray = Array<ApplesoftValue>;
export enum VariableType {
Float = 0,
String = 1,
Function = 2,
Integer = 3
Integer = 3,
}
export interface ApplesoftVariable {
@ -27,7 +19,6 @@ export interface ApplesoftVariable {
value: ApplesoftValue | undefined;
}
export class ApplesoftHeap {
constructor(private mem: Memory) {}
@ -61,7 +52,7 @@ export class ApplesoftHeap {
if (exponent === 0) {
return 0;
}
exponent = (exponent & 0x80 ? 1 : -1) * ((exponent & 0x7F) - 1);
exponent = (exponent & 0x80 ? 1 : -1) * ((exponent & 0x7f) - 1);
let msb = this.readByte(addr + 1);
const sb3 = this.readByte(addr + 2);
@ -74,7 +65,7 @@ export class ApplesoftHeap {
} else {
sign = msb & 0x80 ? -1 : 1;
}
msb &= 0x7F;
msb &= 0x7f;
const mantissa = (msb << 24) | (sb3 << 16) | (sb2 << 8) | lsb;
return sign * (1 + mantissa / 0x80000000) * Math.pow(2, exponent);
@ -83,7 +74,7 @@ export class ApplesoftHeap {
private readString(len: byte, addr: word): string {
let str = '';
for (let idx = 0; idx < len; idx++) {
str += String.fromCharCode(this.readByte(addr + idx) & 0x7F);
str += String.fromCharCode(this.readByte(addr + idx) & 0x7f);
}
return str;
}
@ -91,13 +82,13 @@ export class ApplesoftHeap {
private readVar(addr: word) {
const firstByte = this.readByte(addr);
const lastByte = this.readByte(addr + 1);
const firstLetter = firstByte & 0x7F;
const lastLetter = lastByte & 0x7F;
const firstLetter = firstByte & 0x7f;
const lastLetter = lastByte & 0x7f;
const name =
String.fromCharCode(firstLetter) +
(lastLetter ? String.fromCharCode(lastLetter) : '');
const type = (lastByte & 0x80) >> 7 | (firstByte & 0x80) >> 6;
const type = ((lastByte & 0x80) >> 7) | ((firstByte & 0x80) >> 6);
return { name, type };
}
@ -158,7 +149,7 @@ export class ApplesoftHeap {
for (addr = simpleVariableTable; addr < arrayVariableTable; addr += 7) {
const { name, type } = this.readVar(addr);
switch (type) {
switch (type as VariableType) {
case VariableType.Float:
value = this.readFloat(addr + 2);
break;

View File

@ -108,116 +108,116 @@ export const TOKEN_TO_STRING: Record<byte, string> = {
0xe7: 'CHR$',
0xe8: 'LEFT$',
0xe9: 'RIGHT$',
0xea: 'MID$'
0xea: 'MID$',
};
/** Map from keyword to token. */
export const STRING_TO_TOKEN: Record<string, byte> = {
'END': 0x80,
'FOR': 0x81,
'NEXT': 0x82,
'DATA': 0x83,
'INPUT': 0x84,
'DEL': 0x85,
'DIM': 0x86,
'READ': 0x87,
'GR': 0x88,
'TEXT': 0x89,
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,
CALL: 0x8c,
PLOT: 0x8d,
HLIN: 0x8e,
VLIN: 0x8f,
HGR2: 0x90,
HGR: 0x91,
'HCOLOR=': 0x92,
'HPLOT': 0x93,
'DRAW': 0x94,
'XDRAW': 0x95,
'HTAB': 0x96,
'HOME': 0x97,
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,
SHLOAD: 0x9a,
TRACE: 0x9b,
NOTRACE: 0x9c,
NORMAL: 0x9d,
INVERSE: 0x9e,
FLASH: 0x9f,
'COLOR=': 0xa0,
'POP=': 0xa1,
'VTAB': 0xa2,
VTAB: 0xa2,
'HIMEM:': 0xa3,
'LOMEM:': 0xa4,
'ONERR': 0xa5,
'RESUME': 0xa6,
'RECALL': 0xa7,
'STORE': 0xa8,
ONERR: 0xa5,
RESUME: 0xa6,
RECALL: 0xa7,
STORE: 0xa8,
'SPEED=': 0xa9,
'LET': 0xaa,
'GOTO': 0xab,
'RUN': 0xac,
'IF': 0xad,
'RESTORE': 0xae,
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,
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,
TO: 0xc1,
FN: 0xc2,
'SPC(': 0xc3,
'THEN': 0xc4,
'AT': 0xc5,
'NOT': 0xc6,
'STEP': 0xc7,
THEN: 0xc4,
AT: 0xc5,
NOT: 0xc6,
STEP: 0xc7,
'+': 0xc8,
'-': 0xc9,
'*': 0xca,
'/': 0xcb,
'^': 0xcc,
'AND': 0xcd,
'OR': 0xce,
AND: 0xcd,
OR: 0xce,
'>': 0xcf,
'=': 0xd0,
'<': 0xd1,
'SGN': 0xd2,
'INT': 0xd3,
'ABS': 0xd4,
'USR': 0xd5,
'FRE': 0xd6,
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
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,
};

View File

@ -11,17 +11,17 @@ export const TXTTAB = 0x67;
/** Start of variables (word) */
export const VARTAB = 0x69;
/** Start of arrays (word) */
export const ARYTAB = 0x6B;
export const ARYTAB = 0x6b;
/** End of strings (word). (Strings are allocated down from HIMEM.) */
export const STREND = 0x6D;
export const STREND = 0x6d;
/** Current line */
export const CURLINE = 0x75;
/** Floating Point accumulator (float) */
export const FAC = 0x9D;
export const FAC = 0x9d;
/** Floating Point arguments (float) */
export const ARG = 0xA5;
export const ARG = 0xa5;
/**
* End of program (word). This is actually 1 or 2 bytes past the three
* zero bytes that end the program.
*/
export const PRGEND = 0xAF;
export const PRGEND = 0xaf;

View File

@ -5,7 +5,9 @@ const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
/** Encode an array of bytes in base64. */
export function base64_encode(data: null | undefined): undefined;
export function base64_encode(data: memory): string;
export function base64_encode(data: memory | null | undefined): string | undefined {
export function base64_encode(
data: memory | null | undefined
): string | undefined {
// Twacked by Will Scullin to handle arrays of 'bytes'
// http://kevin.vanzonneveld.net
@ -25,28 +27,39 @@ export function base64_encode(data: memory | null | undefined): string | undefin
// return atob(data);
//}
let o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc='';
let o1,
o2,
o3,
h1,
h2,
h3,
h4,
bits,
i = 0,
ac = 0,
enc = '';
const tmp_arr = [];
if (!data) {
return undefined;
}
do { // pack three octets into four hexets
do {
// pack three octets into four hexets
o1 = data[i++];
o2 = data[i++];
o3 = data[i++];
bits = o1<<16 | o2<<8 | o3;
bits = (o1 << 16) | (o2 << 8) | o3;
h1 = bits>>18 & 0x3f;
h2 = bits>>12 & 0x3f;
h3 = bits>>6 & 0x3f;
h1 = (bits >> 18) & 0x3f;
h2 = (bits >> 12) & 0x3f;
h3 = (bits >> 6) & 0x3f;
h4 = bits & 0x3f;
// use hexets to index into b64, and append result to encoded string
tmp_arr[ac++] = B64.charAt(h1) + B64.charAt(h2) + B64.charAt(h3) + B64.charAt(h4);
tmp_arr[ac++] =
B64.charAt(h1) + B64.charAt(h2) + B64.charAt(h3) + B64.charAt(h4);
} while (i < data.length);
enc = tmp_arr.join('');
@ -68,7 +81,9 @@ export function base64_decode(data: null | undefined): undefined;
/** Returns an array of bytes from the given base64-encoded string. */
export function base64_decode(data: string): memory;
/** Returns an array of bytes from the given base64-encoded string. */
export function base64_decode(data: string | null | undefined): memory | undefined {
export function base64_decode(
data: string | null | undefined
): memory | undefined {
// Twacked by Will Scullin to handle arrays of 'bytes'
// http://kevin.vanzonneveld.net
@ -91,23 +106,33 @@ export function base64_decode(data: string | null | undefined): memory | undefin
// return btoa(data);
//}
let o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0;
let o1,
o2,
o3,
h1,
h2,
h3,
h4,
bits,
i = 0,
ac = 0;
const tmp_arr = [];
if (!data) {
return undefined;
}
do { // unpack four hexets into three octets using index points in B64
do {
// unpack four hexets into three octets using index points in B64
h1 = B64.indexOf(data.charAt(i++));
h2 = B64.indexOf(data.charAt(i++));
h3 = B64.indexOf(data.charAt(i++));
h4 = B64.indexOf(data.charAt(i++));
bits = h1<<18 | h2<<12 | h3<<6 | h4;
bits = (h1 << 18) | (h2 << 12) | (h3 << 6) | h4;
o1 = bits>>16 & 0xff;
o2 = bits>>8 & 0xff;
o1 = (bits >> 16) & 0xff;
o2 = (bits >> 8) & 0xff;
o3 = bits & 0xff;
tmp_arr[ac++] = o1;
@ -126,7 +151,7 @@ const DATA_URL_PREFIX = 'data:application/octet-stream;base64,';
export function base64_json_parse(json: string): unknown {
const reviver = (_key: string, value: unknown) => {
if (typeof value ==='string' && value.startsWith(DATA_URL_PREFIX)) {
if (typeof value === 'string' && value.startsWith(DATA_URL_PREFIX)) {
return base64_decode(value.slice(DATA_URL_PREFIX.length));
}
return value;

View File

@ -8,15 +8,11 @@ import {
VideoModes,
VideoModesState,
bank,
pageNo
pageNo,
} from './videomodes';
const dim = (c: Color): Color => {
return [
c[0] * 0.75 & 0xff,
c[1] * 0.75 & 0xff,
c[2] * 0.75 & 0xff
];
return [(c[0] * 0.75) & 0xff, (c[1] * 0.75) & 0xff, (c[2] * 0.75) & 0xff];
};
// hires colors
@ -49,25 +45,25 @@ const _colors: Color[] = [
//
const r4 = [
0, // Black
2, // Dark Blue
4, // Dark Green
6, // Medium Blue
0, // Black
2, // Dark Blue
4, // Dark Green
6, // Medium Blue
8, // Brown
5, // Gray 1
12, // Light Green
14, // Aqua
8, // Brown
5, // Gray 1
12, // Light Green
14, // Aqua
1, // Red
3, // Purple
10, // Gray 2
7, // Pink
1, // Red
3, // Purple
10, // Gray 2
7, // Pink
9, // Orange
11, // Light Blue
13, // Yellow
15 // White
9, // Orange
11, // Light Blue
13, // Yellow
15, // White
] as const;
const dcolors: Color[] = [
@ -93,7 +89,7 @@ const notDirty: Region = {
top: 193,
bottom: -1,
left: 561,
right: -1
right: -1,
} as const;
/****************************************************************************
@ -131,14 +127,18 @@ export class LoresPage2D implements LoresPage {
}
private _drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = data[off + 4] = c0;
data[off + 1] = data[off + 5] = c1;
data[off + 2] = data[off + 6] = c2;
}
private _drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = c0;
data[off + 1] = c1;
data[off + 2] = c2;
@ -148,10 +148,10 @@ export class LoresPage2D implements LoresPage {
let inverse = false;
if (this.e) {
if (!this.vm._80colMode && !this.vm.altCharMode) {
inverse = ((val & 0xc0) === 0x40) && this._blink;
inverse = (val & 0xc0) === 0x40 && this._blink;
}
} else {
inverse = !((val & 0x80) || (val & 0x40) && this._blink);
inverse = !(val & 0x80 || (val & 0x40 && this._blink));
}
return inverse;
}
@ -177,19 +177,22 @@ export class LoresPage2D implements LoresPage {
// These are used by both bank 0 and 1
private _start() {
return (0x04 * this.page);
return 0x04 * this.page;
}
private _end() { return (0x04 * this.page) + 0x03; }
private _end() {
return 0x04 * this.page + 0x03;
}
private _read(page: byte, off: byte, bank: bank) {
const addr = (page << 8) | off, base = addr & 0x3FF;
const addr = (page << 8) | off,
base = addr & 0x3ff;
return this._buffer[bank][base];
}
private _write(page: byte, off: byte, val: byte, bank: bank) {
const addr = (page << 8) | off;
const base = addr & 0x3FF;
const base = addr & 0x3ff;
let fore, back;
if (this._buffer[bank][base] === val && !this._refreshing) {
@ -201,23 +204,35 @@ export class LoresPage2D implements LoresPage {
const adj = off - col;
// 000001cd eabab000 -> 000abcde
const ab = (adj & 0x18);
const ab = adj & 0x18;
const cd = (page & 0x03) << 1;
const ee = adj >> 7;
const row = ab | cd | ee;
const data = this.imageData.data;
if ((row < 24) && (col < 40)) {
if (row < 24 && col < 40) {
let y = row << 3;
if (y < this.dirty.top) { this.dirty.top = y; }
if (y < this.dirty.top) {
this.dirty.top = y;
}
y += 8;
if (y > this.dirty.bottom) { this.dirty.bottom = y; }
if (y > this.dirty.bottom) {
this.dirty.bottom = y;
}
let x = col * 14;
if (x < this.dirty.left) { this.dirty.left = x; }
if (x < this.dirty.left) {
this.dirty.left = x;
}
x += 14;
if (x > this.dirty.right) { this.dirty.right = x; }
if (x > this.dirty.right) {
this.dirty.right = x;
}
if (this.vm.textMode || this.vm.hiresMode || (this.vm.mixedMode && row > 19)) {
if (
this.vm.textMode ||
this.vm.hiresMode ||
(this.vm.mixedMode && row > 19)
) {
if (this.vm._80colMode) {
const inverse = this._checkInverse(val);
@ -225,15 +240,16 @@ export class LoresPage2D implements LoresPage {
back = inverse ? whiteCol : blackCol;
if (!this.vm.altCharMode) {
val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
val = val >= 0x40 && val < 0x80 ? val - 0x40 : val;
}
let offset = (col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
let offset =
(col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
for (let jdx = 0; jdx < 8; jdx++) {
let b = this.charset[val * 8 + jdx];
for (let idx = 0; idx < 7; idx++) {
const color = (b & 0x01) ? back : fore;
const color = b & 0x01 ? back : fore;
this._drawHalfPixel(data, offset, color);
b >>= 1;
offset += 4;
@ -249,7 +265,7 @@ export class LoresPage2D implements LoresPage {
back = inverse ? whiteCol : blackCol;
if (!this.vm.altCharMode) {
val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
val = val >= 0x40 && val < 0x80 ? val - 0x40 : val;
}
let offset = (col * 14 + row * 560 * 8) * 4;
@ -263,7 +279,7 @@ export class LoresPage2D implements LoresPage {
for (let jdx = 0; jdx < 8; jdx++) {
let b = this.charset[val * 8 + jdx];
for (let idx = 0; idx < 7; idx++) {
const color = (b & 0x01) ? back : fore;
const color = b & 0x01 ? back : fore;
this._drawPixel(data, offset, color);
b >>= 1;
offset += 8;
@ -271,7 +287,10 @@ export class LoresPage2D implements LoresPage {
offset += 546 * 4;
}
} else {
const colorMode = this.vm.mixedMode && !this.vm.textMode && !this.vm.monoMode;
const colorMode =
this.vm.mixedMode &&
!this.vm.textMode &&
!this.vm.monoMode;
// var val0 = col > 0 ? _buffer[0][base - 1] : 0;
// var val2 = col < 39 ? _buffer[0][base + 1] : 0;
@ -281,7 +300,9 @@ export class LoresPage2D implements LoresPage {
if (colorMode) {
// var b0 = charset[val0 * 8 + jdx];
// var b2 = charset[val2 * 8 + jdx];
if (inverse) { b ^= 0x1ff; }
if (inverse) {
b ^= 0x1ff;
}
}
for (let idx = 0; idx < 7; idx++) {
@ -298,7 +319,7 @@ export class LoresPage2D implements LoresPage {
}
odd = !odd;
} else {
color = (b & 0x80) ? fore : back;
color = b & 0x80 ? fore : back;
}
this._drawPixel(data, offset, color);
b <<= 1;
@ -310,17 +331,18 @@ export class LoresPage2D implements LoresPage {
}
} else {
if (this.vm._80colMode && !this.vm.an3State) {
let offset = (col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
let offset =
(col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
if (this.vm.monoMode) {
for (let jdx = 0; jdx < 8; jdx++) {
let b = (jdx < 4) ? (val & 0x0f) : (val >> 4);
b |= (b << 4);
b |= (b << 8);
let b = jdx < 4 ? val & 0x0f : val >> 4;
b |= b << 4;
b |= b << 8;
if (col & 0x1) {
b >>= 2;
}
for (let idx = 0; idx < 7; idx++) {
const color = (b & 0x01) ? whiteCol : blackCol;
const color = b & 0x01 ? whiteCol : blackCol;
this._drawHalfPixel(data, offset, color);
b >>= 1;
offset += 4;
@ -332,8 +354,8 @@ export class LoresPage2D implements LoresPage {
val = ((val & 0x77) << 1) | ((val & 0x88) >> 3);
}
for (let jdx = 0; jdx < 8; jdx++) {
const color = _colors[(jdx < 4) ?
(val & 0x0f) : (val >> 4)];
const color =
_colors[jdx < 4 ? val & 0x0f : val >> 4];
for (let idx = 0; idx < 7; idx++) {
this._drawHalfPixel(data, offset, color);
offset += 4;
@ -346,14 +368,14 @@ export class LoresPage2D implements LoresPage {
if (this.vm.monoMode) {
for (let jdx = 0; jdx < 8; jdx++) {
let b = (jdx < 4) ? (val & 0x0f) : (val >> 4);
b |= (b << 4);
b |= (b << 8);
let b = jdx < 4 ? val & 0x0f : val >> 4;
b |= b << 4;
b |= b << 8;
if (col & 0x1) {
b >>= 2;
}
for (let idx = 0; idx < 14; idx++) {
const color = (b & 0x0001) ? whiteCol : blackCol;
const color = b & 0x0001 ? whiteCol : blackCol;
this._drawHalfPixel(data, offset, color);
b >>= 1;
offset += 4;
@ -362,7 +384,8 @@ export class LoresPage2D implements LoresPage {
}
} else {
for (let jdx = 0; jdx < 8; jdx++) {
const color = _colors[(jdx < 4) ? (val & 0x0f) : (val >> 4)];
const color =
_colors[jdx < 4 ? val & 0x0f : val >> 4];
for (let idx = 0; idx < 7; idx++) {
this._drawPixel(data, offset, color);
offset += 8;
@ -376,7 +399,8 @@ export class LoresPage2D implements LoresPage {
}
refresh() {
this.highColorTextMode = !this.vm.an3State && this.vm.textMode && !this.vm._80colMode;
this.highColorTextMode =
!this.vm.an3State && this.vm.textMode && !this.vm._80colMode;
let addr = 0x400 * this.page;
this._refreshing = true;
@ -395,7 +419,7 @@ export class LoresPage2D implements LoresPage {
this._blink = !this._blink;
for (let idx = 0; idx < 0x400; idx++, addr++) {
const b = this._buffer[0][idx];
if ((b & 0xC0) === 0x40) {
if ((b & 0xc0) === 0x40) {
this._write(addr >> 8, addr & 0xff, this._buffer[0][idx], 0);
}
}
@ -424,7 +448,7 @@ export class LoresPage2D implements LoresPage {
buffer: [
new Uint8Array(this._buffer[0]),
new Uint8Array(this._buffer[1]),
]
],
};
}
@ -443,25 +467,29 @@ export class LoresPage2D implements LoresPage {
}
private mapCharCode(charCode: byte) {
charCode &= 0x7F;
charCode &= 0x7f;
if (charCode < 0x20) {
charCode += 0x40;
}
if (!this.e && (charCode >= 0x60)) {
if (!this.e && charCode >= 0x60) {
charCode -= 0x40;
}
return charCode;
}
getText() {
let buffer = '', line, charCode;
let buffer = '',
line,
charCode;
let row, col, base;
for (row = 0; row < 24; row++) {
base = this.rowToBase(row);
line = '';
if (this.e && this.vm._80colMode) {
for (col = 0; col < 80; col++) {
charCode = this.mapCharCode(this._buffer[1 - col % 2][base + Math.floor(col / 2)]);
charCode = this.mapCharCode(
this._buffer[1 - (col % 2)][base + Math.floor(col / 2)]
);
line += String.fromCharCode(charCode);
}
} else {
@ -498,7 +526,7 @@ export class HiresPage2D implements HiresPage {
constructor(
private vm: VideoModes,
private page: pageNo,
private page: pageNo
) {
this.imageData = this.vm.context.createImageData(560, 192);
this.imageData.data.fill(0xff);
@ -509,7 +537,9 @@ export class HiresPage2D implements HiresPage {
}
private _drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = data[off + 4] = c0;
data[off + 1] = data[off + 5] = c1;
@ -517,7 +547,9 @@ export class HiresPage2D implements HiresPage {
}
private _drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = c0;
data[off + 1] = c1;
@ -529,7 +561,9 @@ export class HiresPage2D implements HiresPage {
//
private _draw3Pixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = data[off + 4] = data[off + 8] = c0;
data[off + 1] = data[off + 5] = data[off + 9] = c1;
@ -537,7 +571,9 @@ export class HiresPage2D implements HiresPage {
}
private _draw4Pixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
const c0 = color[0],
c1 = color[1],
c2 = color[2];
data[off + 0] = data[off + 4] = data[off + 8] = data[off + 12] = c0;
data[off + 1] = data[off + 5] = data[off + 9] = data[off + 13] = c1;
@ -562,18 +598,23 @@ export class HiresPage2D implements HiresPage {
};
}
private _start() { return (0x20 * this.page); }
private _start() {
return 0x20 * this.page;
}
private _end() { return (0x020 * this.page) + 0x1f; }
private _end() {
return 0x020 * this.page + 0x1f;
}
private _read(page: byte, off: byte, bank: bank) {
const addr = (page << 8) | off, base = addr & 0x1FFF;
const addr = (page << 8) | off,
base = addr & 0x1fff;
return this._buffer[bank][base];
}
private _write(page: byte, off: byte, val: byte, bank: bank) {
const addr = (page << 8) | off;
const base = addr & 0x1FFF;
const base = addr & 0x1fff;
if (this._buffer[bank][base] === val && !this._refreshing) {
return;
@ -584,7 +625,7 @@ export class HiresPage2D implements HiresPage {
const adj = off - col;
// 000001cd eabab000 -> 000abcde
const ab = (adj & 0x18);
const ab = adj & 0x18;
const cd = (page & 0x03) << 1;
const e = adj >> 7;
@ -593,17 +634,25 @@ export class HiresPage2D implements HiresPage {
const data = this.imageData.data;
let dx, dy;
if ((rowa < 24) && (col < 40) && (this.vm.hiresMode || this._refreshing)) {
let y = rowa << 4 | rowb << 1;
if (y < this.dirty.top) { this.dirty.top = y; }
if (rowa < 24 && col < 40 && (this.vm.hiresMode || this._refreshing)) {
let y = (rowa << 4) | (rowb << 1);
if (y < this.dirty.top) {
this.dirty.top = y;
}
y += 1;
if (y > this.dirty.bottom) { this.dirty.bottom = y; }
if (y > this.dirty.bottom) {
this.dirty.bottom = y;
}
let x = col * 14 - 2;
if (x < this.dirty.left) { this.dirty.left = x; }
if (x < this.dirty.left) {
this.dirty.left = x;
}
x += 18;
if (x > this.dirty.right) { this.dirty.right = x; }
if (x > this.dirty.right) {
this.dirty.right = x;
}
dy = rowa << 4 | rowb << 1;
dy = (rowa << 4) | (rowb << 1);
let bz, b0, b1, b2, b3, b4, c;
if (this.oneSixtyMode && !this.vm.monoMode) {
// 1 byte = two pixels, but 3:4 ratio
@ -626,7 +675,9 @@ export class HiresPage2D implements HiresPage {
// 76543210 76543210 76543210 76543210
// 1111222 2333344 4455556 6667777
const mod = col % 2, mcol = col - mod, baseOff = base - mod;
const mod = col % 2,
mcol = col - mod,
baseOff = base - mod;
bz = this._buffer[0][baseOff - 1];
b0 = this._buffer[1][baseOff];
b1 = this._buffer[0][baseOff];
@ -635,14 +686,14 @@ export class HiresPage2D implements HiresPage {
b4 = this._buffer[1][baseOff + 2];
c = [
0,
((b0 & 0x0f) >> 0), // 0
(b0 & 0x0f) >> 0, // 0
((b0 & 0x70) >> 4) | ((b1 & 0x01) << 3), // 1
((b1 & 0x1e) >> 1), // 2
(b1 & 0x1e) >> 1, // 2
((b1 & 0x60) >> 5) | ((b2 & 0x03) << 2), // 3
((b2 & 0x3c) >> 2), // 4
(b2 & 0x3c) >> 2, // 4
((b2 & 0x40) >> 6) | ((b3 & 0x07) << 1), // 5
((b3 & 0x78) >> 3), // 6
0
(b3 & 0x78) >> 3, // 6
0,
]; // 7
const hb = [
0,
@ -653,7 +704,7 @@ export class HiresPage2D implements HiresPage {
b2 & 0x80, // 4
b3 & 0x80, // 5
b3 & 0x80, // 6
0
0,
]; // 7
if (col > 0) {
c[0] = (bz & 0x78) >> 3;
@ -695,16 +746,17 @@ export class HiresPage2D implements HiresPage {
} else if (this.colorDHRMode) {
this._drawHalfPixel(data, offset, dcolor);
} else if (
((c[idx] !== c[idx - 1]) && (c[idx] !== c[idx + 1])) &&
(((bits & 0x1c) === 0x1c) ||
((bits & 0x70) === 0x70) ||
((bits & 0x38) === 0x38))
c[idx] !== c[idx - 1] &&
c[idx] !== c[idx + 1] &&
((bits & 0x1c) === 0x1c ||
(bits & 0x70) === 0x70 ||
(bits & 0x38) === 0x38)
) {
this._drawHalfPixel(data, offset, whiteCol);
} else if (
(bits & 0x38) ||
(c[idx] === c[idx + 1]) ||
(c[idx] === c[idx - 1])
bits & 0x38 ||
c[idx] === c[idx + 1] ||
c[idx] === c[idx - 1]
) {
this._drawHalfPixel(data, offset, dcolor);
} else if (bits & 0x28) {
@ -733,11 +785,13 @@ export class HiresPage2D implements HiresPage {
b0 = col > 0 ? this._buffer[0][base - 1] : 0;
b2 = col < 39 ? this._buffer[0][base + 1] : 0;
val |= (b2 & 0x3) << 7;
let v0 = b0 & 0x20, v1 = b0 & 0x40, v2 = val & 0x1,
let v0 = b0 & 0x20,
v1 = b0 & 0x40,
v2 = val & 0x1,
odd = !(col & 0x1),
color;
const oddCol = (hbs ? orangeCol : greenCol);
const evenCol = (hbs ? blueCol : violetCol);
const oddCol = hbs ? orangeCol : greenCol;
const evenCol = hbs ? blueCol : violetCol;
let offset = dx * 4 + dy * 280 * 4;
@ -785,7 +839,8 @@ export class HiresPage2D implements HiresPage {
}
refresh() {
this.highColorHGRMode = !this.vm.an3State && this.vm.hiresMode && !this.vm._80colMode;
this.highColorHGRMode =
!this.vm.an3State && this.vm.hiresMode && !this.vm._80colMode;
this.oneSixtyMode = this.vm.flag === 1 && this.vm.doubleHiresMode;
this.mixedDHRMode = this.vm.flag === 2 && this.vm.doubleHiresMode;
this.monoDHRMode = this.vm.flag === 3 && this.vm.doubleHiresMode;
@ -824,7 +879,7 @@ export class HiresPage2D implements HiresPage {
buffer: [
new Uint8Array(this._buffer[0]),
new Uint8Array(this._buffer[1]),
]
],
};
}
@ -884,7 +939,8 @@ export class VideoModes2D implements VideoModes {
}
_refresh() {
this.doubleHiresMode = !this.an3State && this.hiresMode && this._80colMode;
this.doubleHiresMode =
!this.an3State && this.hiresMode && this._80colMode;
this._refreshFlag = true;
}
@ -936,7 +992,9 @@ export class VideoModes2D implements VideoModes {
}
_80col(on: boolean) {
if (!this.e) { return; }
if (!this.e) {
return;
}
const old = this._80colMode;
this._80colMode = on;
@ -947,7 +1005,9 @@ export class VideoModes2D implements VideoModes {
}
altChar(on: boolean) {
if (!this.e) { return; }
if (!this.e) {
return;
}
const old = this.altCharMode;
this.altCharMode = on;
@ -969,13 +1029,16 @@ export class VideoModes2D implements VideoModes {
}
an3(on: boolean) {
if (!this.e) { return; }
if (!this.e) {
return;
}
const old = this.an3State;
this.an3State = on;
if (on) {
this.flag = ((this.flag << 1) | (this._80colMode ? 0x0 : 0x1)) & 0x3;
this.flag =
((this.flag << 1) | (this._80colMode ? 0x0 : 0x1)) & 0x3;
}
if (old !== on) {
@ -1056,8 +1119,14 @@ export class VideoModes2D implements VideoModes {
const imageData = this.buildScreen(mainData, mixData);
this._screenContext.drawImage(
imageData,
0, 0, 560, 192,
this._left, this._top, 560, 384
0,
0,
560,
192,
this._left,
this._top,
560,
384
);
blitted = true;
}
@ -1076,10 +1145,12 @@ export class VideoModes2D implements VideoModes {
}
if (altData) {
blitted = this.updateImage(
altData,
{ top: 0, left: 0, right: 560, bottom: 192 }
);
blitted = this.updateImage(altData, {
top: 0,
left: 0,
right: 560,
bottom: 192,
});
} else if (this.hiresMode && !this.textMode) {
blitted = this.updateImage(
hgr.imageData,
@ -1088,9 +1159,7 @@ export class VideoModes2D implements VideoModes {
this.mixedMode ? gr.dirty : null
);
} else {
blitted = this.updateImage(
gr.imageData, gr.dirty
);
blitted = this.updateImage(gr.imageData, gr.dirty);
}
hgr.dirty = { ...notDirty };
gr.dirty = { ...notDirty };
@ -1109,7 +1178,7 @@ export class VideoModes2D implements VideoModes {
_80colMode: this._80colMode,
altCharMode: this.altCharMode,
an3State: this.an3State,
flag: this.flag
flag: this.flag,
};
}

View File

@ -1,7 +1,11 @@
import type { byte, Card, Restorable } from '../types';
import { debug, toHex } from '../util';
import { rom as readOnlyRom } from '../roms/cards/cffa';
import { create2MGFromBlockDisk, HeaderData, read2MGHeader } from '../formats/2mg';
import {
create2MGFromBlockDisk,
HeaderData,
read2MGHeader,
} from '../formats/2mg';
import { ProDOSVolume } from '../formats/prodos';
import createBlockDisk from '../formats/block';
import {
@ -15,9 +19,9 @@ import {
const rom = new Uint8Array(readOnlyRom);
const COMMANDS = {
ATACRead: 0x20,
ATACWrite: 0x30,
ATAIdentify: 0xEC
ATACRead: 0x20,
ATACWrite: 0x30,
ATAIdentify: 0xec,
};
// CFFA Card Settings
@ -25,68 +29,69 @@ const COMMANDS = {
const SETTINGS = {
Max32MBPartitionsDev0: 0x800,
Max32MBPartitionsDev1: 0x801,
DefaultBootDevice: 0x802,
DefaultBootPartition: 0x803,
Reserved: 0x804, // 4 bytes
WriteProtectBits: 0x808,
MenuSnagMask: 0x809,
MenuSnagKey: 0x80A,
BootTimeDelayTenths: 0x80B,
BusResetSeconds: 0x80C,
CheckDeviceTenths: 0x80D,
ConfigOptionBits: 0x80E,
BlockOffsetDev0: 0x80F, // 3 bytes
BlockOffsetDev1: 0x812, // 3 bytes
Unused: 0x815
DefaultBootDevice: 0x802,
DefaultBootPartition: 0x803,
Reserved: 0x804, // 4 bytes
WriteProtectBits: 0x808,
MenuSnagMask: 0x809,
MenuSnagKey: 0x80a,
BootTimeDelayTenths: 0x80b,
BusResetSeconds: 0x80c,
CheckDeviceTenths: 0x80d,
ConfigOptionBits: 0x80e,
BlockOffsetDev0: 0x80f, // 3 bytes
BlockOffsetDev1: 0x812, // 3 bytes
Unused: 0x815,
};
// CFFA ATA Register Locations
const LOC = {
ATADataHigh: 0x80,
SetCSMask: 0x81,
ClearCSMask: 0x82,
WriteEEPROM: 0x83,
ATADataHigh: 0x80,
SetCSMask: 0x81,
ClearCSMask: 0x82,
WriteEEPROM: 0x83,
NoWriteEEPROM: 0x84,
ATADevCtrl: 0x86,
ATAAltStatus: 0x86,
ATADataLow: 0x88,
AError: 0x89,
ASectorCnt: 0x8a,
ASector: 0x8b,
ATACylinder: 0x8c,
ATACylinderH: 0x8d,
ATAHead: 0x8e,
ATACommand: 0x8f,
ATAStatus: 0x8f
ATADevCtrl: 0x86,
ATAAltStatus: 0x86,
ATADataLow: 0x88,
AError: 0x89,
ASectorCnt: 0x8a,
ASector: 0x8b,
ATACylinder: 0x8c,
ATACylinderH: 0x8d,
ATAHead: 0x8e,
ATACommand: 0x8f,
ATAStatus: 0x8f,
};
// ATA Status Bits
const STATUS = {
BSY: 0x80, // Busy
BSY: 0x80, // Busy
DRDY: 0x40, // Drive ready. 1 when ready
DWF: 0x20, // Drive write fault. 1 when fault
DSC: 0x10, // Disk seek complete. 1 when not seeking
DRQ: 0x08, // Data request. 1 when ready to write
DWF: 0x20, // Drive write fault. 1 when fault
DSC: 0x10, // Disk seek complete. 1 when not seeking
DRQ: 0x08, // Data request. 1 when ready to write
CORR: 0x04, // Correct data. 1 on correctable error
IDX: 0x02, // 1 once per revolution
ERR: 0x01 // Error. 1 on error
IDX: 0x02, // 1 once per revolution
ERR: 0x01, // Error. 1 on error
};
// ATA Identity Block Locations
const IDENTITY = {
SectorCountLow: 58,
SectorCountHigh: 57
SectorCountLow: 58,
SectorCountHigh: 57,
};
export interface CFFAState {
disks: Array<BlockDisk | null>;
}
export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<CFFAState> {
export default class CFFA
implements Card, MassStorage<BlockFormat>, Restorable<CFFAState>
{
// CFFA internal Flags
private _disableSignalling = false;
@ -122,22 +127,22 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
// Disk data
private _partitions: Array<ProDOSVolume|null> = [
private _partitions: Array<ProDOSVolume | null> = [
// Drive 1
null,
// Drive 2
null
null,
];
private _sectors: Uint16Array[][] = [
// Drive 1
[],
// Drive 2
[]
[],
];
private _name: string[] = [];
private _metadata: Array<HeaderData|null> = [];
private _metadata: Array<HeaderData | null> = [];
constructor() {
debug('CFFA');
@ -178,7 +183,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
const statusArray = [];
let flag: keyof typeof STATUS;
for (flag in STATUS) {
if(status & STATUS[flag]) {
if (status & STATUS[flag]) {
statusArray.push(flag);
}
}
@ -199,7 +204,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
const val = this._sectors[this._drive][sector][idx * 16 + jdx];
row.push(toHex(val, 4));
const low = val & 0x7f;
const hi = val >> 8 & 0x7f;
const hi = (val >> 8) & 0x7f;
charRow.push(low > 0x1f ? String.fromCharCode(low) : '.');
charRow.push(hi > 0x1f ? String.fromCharCode(hi) : '.');
}
@ -217,106 +222,119 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
if (readMode) {
retVal = 0;
switch (off & 0x8f) {
case LOC.ATADataHigh: // 0x00
case LOC.ATADataHigh: // 0x00
retVal = this._dataHigh;
break;
case LOC.SetCSMask: // 0x01
case LOC.SetCSMask: // 0x01
this._disableSignalling = true;
break;
case LOC.ClearCSMask: // 0x02
case LOC.ClearCSMask: // 0x02
this._disableSignalling = false;
break;
case LOC.WriteEEPROM: // 0x03
case LOC.WriteEEPROM: // 0x03
this._writeEEPROM = true;
break;
case LOC.NoWriteEEPROM: // 0x04
this._writeEEPROM = false;
break;
case LOC.ATAAltStatus: // 0x06
case LOC.ATAAltStatus: // 0x06
retVal = this._altStatus;
break;
case LOC.ATADataLow: // 0x08
case LOC.ATADataLow: // 0x08
this._dataHigh = this._curSector[this._curWord] >> 8;
retVal = this._curSector[this._curWord] & 0xff;
if (!this._disableSignalling) {
this._curWord++;
}
break;
case LOC.AError: // 0x09
case LOC.AError: // 0x09
retVal = this._error;
break;
case LOC.ASectorCnt: // 0x0A
case LOC.ASectorCnt: // 0x0A
retVal = this._sectorCnt;
break;
case LOC.ASector: // 0x0B
case LOC.ASector: // 0x0B
retVal = this._sector;
break;
case LOC.ATACylinder: // 0x0C
case LOC.ATACylinder: // 0x0C
retVal = this._cylinder;
break;
case LOC.ATACylinderH: // 0x0D
case LOC.ATACylinderH: // 0x0D
retVal = this._cylinderH;
break;
case LOC.ATAHead: // 0x0E
retVal = this._head | (this._lba ? 0x40 : 0) | (this._drive ? 0x10 : 0) | 0xA0;
case LOC.ATAHead: // 0x0E
retVal =
this._head |
(this._lba ? 0x40 : 0) |
(this._drive ? 0x10 : 0) |
0xa0;
break;
case LOC.ATAStatus: // 0x0F
retVal = this._sectors[this._drive].length > 0 ? STATUS.DRDY | STATUS.DSC : 0;
case LOC.ATAStatus: // 0x0F
retVal =
this._sectors[this._drive].length > 0
? STATUS.DRDY | STATUS.DSC
: 0;
this._debug('returning status', this._statusString(retVal));
break;
default:
debug('read unknown soft switch', toHex(off));
}
if (off & 0x7) { // Anything but data high/low
if (off & 0x7) {
// Anything but data high/low
this._debug('read soft switch', toHex(off), toHex(retVal));
}
} else {
if (off & 0x7) { // Anything but data high/low
if (off & 0x7) {
// Anything but data high/low
this._debug('write soft switch', toHex(off), toHex(val));
}
switch (off & 0x8f) {
case LOC.ATADataHigh: // 0x00
case LOC.ATADataHigh: // 0x00
this._dataHigh = val;
break;
case LOC.SetCSMask: // 0x01
case LOC.SetCSMask: // 0x01
this._disableSignalling = true;
break;
case LOC.ClearCSMask: // 0x02
case LOC.ClearCSMask: // 0x02
this._disableSignalling = false;
break;
case LOC.WriteEEPROM: // 0x03
case LOC.WriteEEPROM: // 0x03
this._writeEEPROM = true;
break;
case LOC.NoWriteEEPROM: // 0x04
this._writeEEPROM = false;
break;
case LOC.ATADevCtrl: // 0x06
case LOC.ATADevCtrl: // 0x06
this._debug('devCtrl:', toHex(val));
this._interruptsEnabled = (val & 0x04) ? true : false;
this._debug('Interrupts', this._interruptsEnabled ? 'enabled' : 'disabled');
this._interruptsEnabled = val & 0x04 ? true : false;
this._debug(
'Interrupts',
this._interruptsEnabled ? 'enabled' : 'disabled'
);
if (val & 0x02) {
this._reset();
}
break;
case LOC.ATADataLow: // 0x08
this._curSector[this._curWord] = this._dataHigh << 8 | val;
case LOC.ATADataLow: // 0x08
this._curSector[this._curWord] =
(this._dataHigh << 8) | val;
this._curWord++;
break;
case LOC.ASectorCnt: // 0x0a
case LOC.ASectorCnt: // 0x0a
this._debug('setting sector count', val);
this._sectorCnt = val;
break;
case LOC.ASector: // 0x0b
case LOC.ASector: // 0x0b
this._debug('setting sector', toHex(val));
this._sector = val;
break;
case LOC.ATACylinder: // 0x0c
case LOC.ATACylinder: // 0x0c
this._debug('setting cylinder', toHex(val));
this._cylinder = val;
break;
case LOC.ATACylinderH: // 0x0d
case LOC.ATACylinderH: // 0x0d
this._debug('setting cylinder high', toHex(val));
this._cylinderH = val;
break;
@ -324,14 +342,23 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
this._head = val & 0xf;
this._lba = val & 0x40 ? true : false;
this._drive = val & 0x10 ? 1 : 0;
this._debug('setting head', toHex(val & 0xf), 'drive', this._drive);
this._debug(
'setting head',
toHex(val & 0xf),
'drive',
this._drive
);
if (!this._lba) {
console.error('CHS mode not supported');
}
break;
case LOC.ATACommand: // 0x0f
case LOC.ATACommand: // 0x0f
this._debug('command:', toHex(val));
sector = this._head << 24 | this._cylinderH << 16 | this._cylinder << 8 | this._sector;
sector =
(this._head << 24) |
(this._cylinderH << 16) |
(this._cylinder << 8) |
this._sector;
this._dumpSector(sector);
switch (val) {
@ -341,13 +368,27 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
this._curWord = 0;
break;
case COMMANDS.ATACRead:
this._debug('ATA read sector', toHex(this._cylinderH), toHex(this._cylinder), toHex(this._sector), sector);
this._curSector = this._sectors[this._drive][sector];
this._debug(
'ATA read sector',
toHex(this._cylinderH),
toHex(this._cylinder),
toHex(this._sector),
sector
);
this._curSector =
this._sectors[this._drive][sector];
this._curWord = 0;
break;
case COMMANDS.ATACWrite:
this._debug('ATA write sector', toHex(this._cylinderH), toHex(this._cylinder), toHex(this._sector), sector);
this._curSector = this._sectors[this._drive][sector];
this._debug(
'ATA write sector',
toHex(this._cylinderH),
toHex(this._cylinder),
toHex(this._sector),
sector
);
this._curSector =
this._sectors[this._drive][sector];
this._curWord = 0;
break;
default:
@ -367,49 +408,45 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
}
read(page: byte, off: byte) {
return rom[(page - 0xc0) << 8 | off];
return rom[((page - 0xc0) << 8) | off];
}
write(page: byte, off: byte, val: byte) {
if (this._writeEEPROM) {
this._debug('writing', toHex(page << 8 | off), toHex(val));
rom[(page - 0xc0) << 8 | off] - val;
this._debug('writing', toHex((page << 8) | off), toHex(val));
rom[((page - 0xc0) << 8) | off] - val;
}
}
getState() {
return {
disks: this._partitions.map(
(partition) => {
let result: BlockDisk | null = null;
if (partition) {
const disk: BlockDisk = partition.disk();
result = {
blocks: disk.blocks.map(
(block) => new Uint8Array(block)
),
encoding: ENCODING_BLOCK,
format: disk.format,
readOnly: disk.readOnly,
metadata: { ...disk.metadata },
};
}
return result;
disks: this._partitions.map((partition) => {
let result: BlockDisk | null = null;
if (partition) {
const disk: BlockDisk = partition.disk();
result = {
blocks: disk.blocks.map(
(block) => new Uint8Array(block)
),
encoding: ENCODING_BLOCK,
format: disk.format,
readOnly: disk.readOnly,
metadata: { ...disk.metadata },
};
}
)
return result;
}),
};
}
setState(state: CFFAState) {
state.disks.forEach(
(disk, idx) => {
if (disk) {
this.setBlockVolume(idx + 1, disk);
} else {
this.resetBlockVolume(idx + 1);
}
state.disks.forEach((disk, idx) => {
if (disk) {
this.setBlockVolume(idx + 1, disk);
} else {
this.resetBlockVolume(idx + 1);
}
);
});
}
resetBlockVolume(drive: number) {
@ -433,12 +470,14 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
drive = drive - 1;
// Convert 512 byte blocks into 256 word sectors
this._sectors[drive] = disk.blocks.map(function(block) {
this._sectors[drive] = disk.blocks.map(function (block) {
return new Uint16Array(block.buffer);
});
this._identity[drive][IDENTITY.SectorCountHigh] = this._sectors[0].length & 0xffff;
this._identity[drive][IDENTITY.SectorCountLow] = this._sectors[0].length >> 16;
this._identity[drive][IDENTITY.SectorCountHigh] =
this._sectors[0].length & 0xffff;
this._identity[drive][IDENTITY.SectorCountLow] =
this._sectors[0].length >> 16;
const prodos = new ProDOSVolume(disk);
@ -455,7 +494,12 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
// Assign a raw disk image to a drive. Must be 2mg or raw PO image.
setBinary(drive: number, name: string, ext: BlockFormat, rawData: ArrayBuffer) {
setBinary(
drive: number,
name: string,
ext: BlockFormat,
rawData: ArrayBuffer
) {
const volume = 254;
const readOnly = false;
@ -471,7 +515,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
rawData,
name,
volume,
readOnly
readOnly,
};
const disk = createBlockDisk(ext, options);

View File

@ -1,23 +1,40 @@
import { base64_encode } from '../base64';
import type {
byte,
Card,
ReadonlyUint8Array
} from '../types';
import type { byte, Card, ReadonlyUint8Array } from '../types';
import {
DISK_PROCESSED, DriveNumber, DRIVE_NUMBERS, FloppyDisk,
FloppyFormat, FormatWorkerMessage,
FormatWorkerResponse, isNibbleDisk, isNoFloppyDisk, isWozDisk, JSONDisk, MassStorage,
MassStorageData, NibbleDisk, NibbleFormat, NO_DISK, PROCESS_BINARY, PROCESS_JSON, PROCESS_JSON_DISK, SupportedSectors, WozDisk
DISK_PROCESSED,
DriveNumber,
DRIVE_NUMBERS,
FloppyDisk,
FloppyFormat,
FormatWorkerMessage,
FormatWorkerResponse,
isNibbleDisk,
isNoFloppyDisk,
isWozDisk,
JSONDisk,
MassStorage,
MassStorageData,
NibbleDisk,
NibbleFormat,
NO_DISK,
PROCESS_BINARY,
PROCESS_JSON,
PROCESS_JSON_DISK,
SupportedSectors,
WozDisk,
} from '../formats/types';
import {
createDisk,
createDiskFromJsonDisk
} from '../formats/create_disk';
import { createDisk, createDiskFromJsonDisk } from '../formats/create_disk';
import { jsonDecode, jsonEncode, readSector, _D13O, _DO, _PO } from '../formats/format_utils';
import {
jsonDecode,
jsonEncode,
readSector,
_D13O,
_DO,
_PO,
} from '../formats/format_utils';
import Apple2IO from '../apple2io';
@ -25,30 +42,36 @@ import { BOOTSTRAP_ROM_13, BOOTSTRAP_ROM_16 } from '../roms/cards/disk2';
import { EmptyDriver } from './drivers/EmptyDriver';
import { NibbleDiskDriver } from './drivers/NibbleDiskDriver';
import { ControllerState, DiskDriver, Drive, DriverState, Phase } from './drivers/types';
import {
ControllerState,
DiskDriver,
Drive,
DriverState,
Phase,
} from './drivers/types';
import { WozDiskDriver } from './drivers/WozDiskDriver';
/** Softswitch locations */
const LOC = {
// Disk II Controller Commands
// See Understanding the Apple IIe, Table 9.1
PHASE0OFF: 0x80, // Q0L: Phase 0 OFF
PHASE0ON: 0x81, // Q0H: Phase 0 ON
PHASE1OFF: 0x82, // Q1L: Phase 1 OFF
PHASE1ON: 0x83, // Q1H: Phase 1 ON
PHASE2OFF: 0x84, // Q2L: Phase 2 OFF
PHASE2ON: 0x85, // Q2H: Phase 2 ON
PHASE3OFF: 0x86, // Q3L: Phase 3 OFF
PHASE3ON: 0x87, // Q3H: Phase 3 ON
PHASE0OFF: 0x80, // Q0L: Phase 0 OFF
PHASE0ON: 0x81, // Q0H: Phase 0 ON
PHASE1OFF: 0x82, // Q1L: Phase 1 OFF
PHASE1ON: 0x83, // Q1H: Phase 1 ON
PHASE2OFF: 0x84, // Q2L: Phase 2 OFF
PHASE2ON: 0x85, // Q2H: Phase 2 ON
PHASE3OFF: 0x86, // Q3L: Phase 3 OFF
PHASE3ON: 0x87, // Q3H: Phase 3 ON
DRIVEOFF: 0x88, // Q4L: Drives OFF
DRIVEON: 0x89, // Q4H: Selected drive ON
DRIVE1: 0x8A, // Q5L: Select drive 1
DRIVE2: 0x8B, // Q5H: Select drive 2
DRIVEREAD: 0x8C, // Q6L: Shift while writing; read data
DRIVEWRITE: 0x8D, // Q6H: Load while writing; read write protect
DRIVEREADMODE: 0x8E, // Q7L: Read
DRIVEWRITEMODE: 0x8F // Q7H: Write
DRIVEOFF: 0x88, // Q4L: Drives OFF
DRIVEON: 0x89, // Q4H: Selected drive ON
DRIVE1: 0x8a, // Q5L: Select drive 1
DRIVE2: 0x8b, // Q5H: Select drive 2
DRIVEREAD: 0x8c, // Q6L: Shift while writing; read data
DRIVEWRITE: 0x8d, // Q6H: Load while writing; read write protect
DRIVEREADMODE: 0x8e, // Q7L: Read
DRIVEWRITEMODE: 0x8f, // Q7H: Write
} as const;
/** Logic state sequencer ROM */
@ -62,6 +85,7 @@ const LOC = {
// B LOAD XXXXXXXX YYYYYYYY
// D SL1 ABCDEFGH BCDEFGH1
// prettier-ignore
const SEQUENCER_ROM_13 = [
// See Understanding the Apple IIe, Figure 9.10 The DOS 3.2 Logic State Sequencer
// Note that the column order here is NOT the same as in Figure 9.10 for Q7 H (Write).
@ -88,6 +112,7 @@ const SEQUENCER_ROM_13 = [
0xDD, 0x4D, 0xE0, 0xE0, 0x0A, 0x0A, 0x0A, 0x0A, 0x88, 0x88, 0x08, 0x08, 0x88, 0x88, 0x08, 0x08 // F
] as const;
// prettier-ignore
const SEQUENCER_ROM_16 = [
// See Understanding the Apple IIe, Figure 9.11 The DOS 3.3 Logic State Sequencer
// Note that the column order here is NOT the same as in Figure 9.11 for Q7 H (Write).
@ -152,7 +177,7 @@ const PHASE_DELTA = [
[0, 1, 2, -1],
[-1, 0, 1, 2],
[-2, -1, 0, 1],
[1, -2, -1, 0]
[1, -2, -1, 0],
] as const;
/** Callbacks triggered by events of the drive or controller. */
@ -211,7 +236,8 @@ function getDiskState(disk: FloppyDisk): FloppyDisk {
}
if (isWozDisk(disk)) {
const { format, encoding, metadata, readOnly, trackMap, rawTracks } = disk;
const { format, encoding, metadata, readOnly, trackMap, rawTracks } =
disk;
const result: WozDisk = {
format,
encoding,
@ -235,22 +261,23 @@ function getDiskState(disk: FloppyDisk): FloppyDisk {
* Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller.
*/
export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
private drives: Record<DriveNumber, Drive> = {
1: { // Drive 1
1: {
// Drive 1
track: 0,
head: 0,
phase: 0,
readOnly: false,
dirty: false,
},
2: { // Drive 2
2: {
// Drive 2
track: 0,
head: 0,
phase: 0,
readOnly: false,
dirty: false,
}
},
};
private disks: Record<DriveNumber, FloppyDisk> = {
@ -263,7 +290,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
encoding: NO_DISK,
readOnly: false,
metadata: { name: 'Disk 2' },
}
},
};
private driver: Record<DriveNumber, DiskDriver> = {
@ -283,7 +310,11 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
private worker: Worker;
/** Builds a new Disk ][ card. */
constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors: SupportedSectors = 16) {
constructor(
private io: Apple2IO,
private callbacks: Callbacks,
private sectors: SupportedSectors = 16
) {
this.debug('Disk ][');
this.state = {
@ -382,7 +413,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
case LOC.PHASE3OFF: // 0x06
this.setPhase(3, false);
break;
case LOC.PHASE3ON: // 0x07
case LOC.PHASE3ON: // 0x07
this.setPhase(3, true);
break;
@ -414,7 +445,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
}
break;
case LOC.DRIVE1: // 0x0a
case LOC.DRIVE1: // 0x0a
this.debug('Disk 1');
state.driveNo = 1;
this.updateActiveDrive();
@ -423,7 +454,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
this.callbacks.driveLight(1, true);
}
break;
case LOC.DRIVE2: // 0x0b
case LOC.DRIVE2: // 0x0b
this.debug('Disk 2');
state.driveNo = 2;
this.updateActiveDrive();
@ -443,7 +474,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
this.curDriver.onQ6High(readMode);
break;
case LOC.DRIVEREADMODE: // 0x0e (Q7L)
case LOC.DRIVEREADMODE: // 0x0e (Q7L)
this.debug('Read Mode');
state.q7 = false;
break;
@ -551,7 +582,6 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
this.driver[driveNo].setState(state.driver);
}
setState(state: State) {
this.state = { ...state.controllerState };
for (const d of DRIVE_NUMBERS) {
@ -580,7 +610,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
rwts(driveNo: DriveNumber, track: byte, sector: byte) {
const curDisk = this.disks[driveNo];
if (!isNibbleDisk(curDisk)) {
throw new Error('Can\'t read WOZ disks');
throw new Error("Can't read WOZ disks");
}
return readSector(curDisk, track, sector);
}
@ -593,7 +623,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
type: PROCESS_JSON_DISK,
payload: {
driveNo: driveNo,
jsonDisk
jsonDisk,
},
};
this.worker.postMessage(message);
@ -611,7 +641,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
getJSON(driveNo: DriveNumber, pretty: boolean = false) {
const curDisk = this.disks[driveNo];
if (!isNibbleDisk(curDisk)) {
throw new Error('Can\'t save WOZ disks to JSON');
throw new Error("Can't save WOZ disks to JSON");
}
return jsonEncode(curDisk, pretty);
}
@ -622,7 +652,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
type: PROCESS_JSON,
payload: {
driveNo: driveNo,
json
json,
},
};
this.worker.postMessage(message);
@ -633,7 +663,12 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
return true;
}
setBinary(driveNo: DriveNumber, name: string, fmt: FloppyFormat, rawData: ArrayBuffer) {
setBinary(
driveNo: DriveNumber,
name: string,
fmt: FloppyFormat,
rawData: ArrayBuffer
) {
const readOnly = false;
const volume = 254;
const options = {
@ -650,7 +685,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
driveNo: driveNo,
fmt,
options,
}
},
};
this.worker.postMessage(message);
@ -673,19 +708,22 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
try {
this.worker = new Worker('dist/format_worker.bundle.js');
this.worker.addEventListener('message', (message: MessageEvent<FormatWorkerResponse>) => {
const { data } = message;
switch (data.type) {
case DISK_PROCESSED:
{
const { driveNo: drive, disk } = data.payload;
if (disk) {
this.insertDisk(drive, disk);
this.worker.addEventListener(
'message',
(message: MessageEvent<FormatWorkerResponse>) => {
const { data } = message;
switch (data.type) {
case DISK_PROCESSED:
{
const { driveNo: drive, disk } = data.payload;
if (disk) {
this.insertDisk(drive, disk);
}
}
}
break;
break;
}
}
});
);
} catch (e: unknown) {
console.error(e);
}
@ -696,22 +734,22 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
if (isNoFloppyDisk(disk)) {
this.driver[driveNo] = new EmptyDriver(this.drives[driveNo]);
} else if (isNibbleDisk(disk)) {
this.driver[driveNo] =
new NibbleDiskDriver(
driveNo,
this.drives[driveNo],
disk,
this.state,
() => this.updateDirty(driveNo, true));
this.driver[driveNo] = new NibbleDiskDriver(
driveNo,
this.drives[driveNo],
disk,
this.state,
() => this.updateDirty(driveNo, true)
);
} else if (isWozDisk(disk)) {
this.driver[driveNo] =
new WozDiskDriver(
driveNo,
this.drives[driveNo],
disk,
this.state,
() => this.updateDirty(driveNo, true),
this.io);
this.driver[driveNo] = new WozDiskDriver(
driveNo,
this.drives[driveNo],
disk,
this.state,
() => this.updateDirty(driveNo, true),
this.io
);
} else {
throw new Error(`Unknown disk format ${disk.encoding}`);
}
@ -736,16 +774,20 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
* an error will be thrown. Using `ext == 'nib'` will always return
* an image.
*/
getBinary(driveNo: DriveNumber, ext?: Exclude<NibbleFormat, 'woz'>): MassStorageData | null {
getBinary(
driveNo: DriveNumber,
ext?: Exclude<NibbleFormat, 'woz'>
): MassStorageData | null {
const curDisk = this.disks[driveNo];
if (!isNibbleDisk(curDisk)) {
return null;
}
const { format, readOnly, tracks, volume } = curDisk;
const { name } = curDisk.metadata;
const len = format === 'nib' ?
tracks.reduce((acc, track) => acc + track.length, 0) :
this.sectors * tracks.length * 256;
const len =
format === 'nib'
? tracks.reduce((acc, track) => acc + track.length, 0)
: this.sectors * tracks.length * 256;
const data = new Uint8Array(len);
ext = ext ?? format;
@ -767,7 +809,11 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
for (let s = 0; s < maxSector; s++) {
const _s = sectorMap[s];
const sector = readSector({ ...curDisk, format: ext }, t, _s);
const sector = readSector(
{ ...curDisk, format: ext },
t,
_s
);
data.set(sector, idx);
idx += sector.length;
}

View File

@ -1,7 +1,6 @@
import { DriveNumber, NibbleDisk, WozDisk } from '../../formats/types';
import { ControllerState, DiskDriver, Drive, DriverState } from './types';
/**
* Common logic for both `NibbleDiskDriver` and `WozDiskDriver`.
*/
@ -10,7 +9,8 @@ export abstract class BaseDiskDriver implements DiskDriver {
protected readonly driveNo: DriveNumber,
protected readonly drive: Drive,
protected readonly disk: NibbleDisk | WozDisk,
protected readonly controller: ControllerState) { }
protected readonly controller: ControllerState
) {}
/** Called frequently to ensure the disk is spinning. */
abstract tick(): void;

View File

@ -1,14 +1,14 @@
import { DiskDriver, Drive, DriverState } from './types';
/** Returned state for an empty drive. */
export interface EmptyDriverState extends DriverState { }
export interface EmptyDriverState extends DriverState {}
/**
* Driver for empty drives. This implementation does nothing except keep
* the head clamped between tracks 0 and 34.
*/
export class EmptyDriver implements DiskDriver {
constructor(private readonly drive: Drive) { }
constructor(private readonly drive: Drive) {}
tick(): void {
// do nothing

View File

@ -22,7 +22,8 @@ export class NibbleDiskDriver extends BaseDiskDriver {
drive: Drive,
readonly disk: NibbleDisk,
controller: ControllerState,
private readonly onDirty: () => void) {
private readonly onDirty: () => void
) {
super(driveNo, drive, disk, controller);
}
@ -57,7 +58,7 @@ export class NibbleDiskDriver extends BaseDiskDriver {
} else {
this.controller.latch = 0;
}
this.skip = (++this.skip % 2);
this.skip = ++this.skip % 2;
}
onQ6High(readMode: boolean): void {

View File

@ -3,7 +3,13 @@ import { DriveNumber, WozDisk } from '../../formats/types';
import { toHex } from '../../util';
import { SEQUENCER_ROM } from '../disk2';
import { BaseDiskDriver } from './BaseDiskDriver';
import { ControllerState, Drive, DriverState, LssClockCycle, LssState } from './types';
import {
ControllerState,
Drive,
DriverState,
LssClockCycle,
LssState,
} from './types';
interface WozDiskDriverState extends DriverState {
clock: LssClockCycle;
@ -32,7 +38,8 @@ export class WozDiskDriver extends BaseDiskDriver {
readonly disk: WozDisk,
controller: ControllerState,
private readonly onDirty: () => void,
private readonly io: Apple2IO) {
private readonly io: Apple2IO
) {
super(driveNo, drive, disk, controller);
// From the example in UtA2e, p. 9-29, col. 1, para. 1., this is
@ -129,7 +136,13 @@ export class WozDiskDriver extends BaseDiskDriver {
const command = SEQUENCER_ROM[controller.sectors][idx];
this.debug(`clock: ${this.clock} state: ${toHex(this.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${controller.q6} latch: ${toHex(controller.latch)}`);
this.debug(
`clock: ${this.clock} state: ${toHex(
this.state
)} pulse: ${pulse} command: ${toHex(command)} q6: ${
controller.q6
} latch: ${toHex(controller.latch)}`
);
switch (command & 0xf) {
case 0x0: // CLR
@ -140,23 +153,23 @@ export class WozDiskDriver extends BaseDiskDriver {
case 0x9: // SL0
controller.latch = (controller.latch << 1) & 0xff;
break;
case 0xA: // SR
case 0xa: // SR
controller.latch >>= 1;
if (this.isWriteProtected()) {
controller.latch |= 0x80;
}
break;
case 0xB: // LD
case 0xb: // LD
controller.latch = controller.bus;
this.debug('Loading', toHex(controller.latch), 'from bus');
break;
case 0xD: // SL1
case 0xd: // SL1
controller.latch = ((controller.latch << 1) | 0x01) & 0xff;
break;
default:
this.debug(`unknown command: ${toHex(command & 0xf)}`);
}
this.state = (command >> 4 & 0xF) as LssState;
this.state = ((command >> 4) & 0xf) as LssState;
if (this.clock === 4) {
if (this.isOn()) {

View File

@ -13,7 +13,9 @@ export interface LanguageCardState {
prewrite: boolean;
}
export default class LanguageCard implements Card, Restorable<LanguageCardState> {
export default class LanguageCard
implements Card, Restorable<LanguageCardState>
{
private bank1: RAM;
private bank2: RAM;
private ram: RAM;
@ -88,7 +90,8 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
let bankStr;
let rwStr;
if (writeSwitch) { // 0xC081, 0xC083
if (writeSwitch) {
// 0xC081, 0xC083
if (readMode) {
if (this._prewrite) {
this._writebsr = true;
@ -96,30 +99,37 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
}
this._prewrite = readMode;
if (offSwitch) { // $C083, $C08B
if (offSwitch) {
// $C083, $C08B
this._readbsr = true;
rwStr = 'Read/Write';
} else { // $C081, $C089
} else {
// $C081, $C089
this._readbsr = false;
rwStr = 'Write';
}
} else { // $C080, $C082, $C088, $C08A
} else {
// $C080, $C082, $C088, $C08A
this._writebsr = false;
this._prewrite = false;
if (offSwitch) { // $C082, $C08A
if (offSwitch) {
// $C082, $C08A
this._readbsr = false;
rwStr = 'Off';
} else { // $C080, $C088
} else {
// $C080, $C088
this._readbsr = true;
rwStr = 'Read';
}
}
if (bank1Switch) { // C08[8-C]
if (bank1Switch) {
// C08[8-C]
this._bsr2 = false;
bankStr = 'Bank 1';
} else { // C08[0-3]
} else {
// C08[0-3]
this._bsr2 = true;
bankStr = 'Bank 2';
}
@ -180,7 +190,7 @@ export default class LanguageCard implements Card, Restorable<LanguageCardState>
prewrite: this._prewrite,
ram: this.ram.getState(),
bank1: this.bank1.getState(),
bank2: this.bank2.getState()
bank2: this.bank2.getState(),
};
}

View File

@ -4,16 +4,16 @@ import { debug } from '../util';
import { rom } from '../roms/cards/mouse';
const CLAMP_MIN_LOW = 0x478;
const CLAMP_MAX_LOW = 0x4F8;
const CLAMP_MAX_LOW = 0x4f8;
const CLAMP_MIN_HIGH = 0x578;
const CLAMP_MAX_HIGH = 0x5F8;
const CLAMP_MAX_HIGH = 0x5f8;
const X_LOW = 0x478;
const Y_LOW = 0x4F8;
const Y_LOW = 0x4f8;
const X_HIGH = 0x578;
const Y_HIGH = 0x5F8;
const Y_HIGH = 0x5f8;
const STATUS = 0x778;
const MODE = 0x7F8;
const MODE = 0x7f8;
const STATUS_DOWN = 0x80;
const STATUS_LAST = 0x40;
@ -38,7 +38,7 @@ const ENTRIES = {
POS_MOUSE: 0x16,
CLAMP_MOUSE: 0x17,
HOME_MOUSE: 0x18,
INIT_MOUSE: 0x19
INIT_MOUSE: 0x19,
};
interface MouseState {
@ -65,9 +65,9 @@ export default class Mouse implements Card, Restorable<MouseState> {
/** Lowest mouse Y */
private clampYMin: word = 0;
/** Highest mouse X */
private clampXMax: word = 0x3FF;
private clampXMax: word = 0x3ff;
/** Highest mouse Y */
private clampYMax: word = 0x3FF;
private clampYMax: word = 0x3ff;
/** Mouse X position */
private x: word = 0;
/** Mouse Y position */
@ -117,7 +117,7 @@ export default class Mouse implements Card, Restorable<MouseState> {
};
const clearCarry = (state: CpuState) => {
state.s &= 0xFE;
state.s &= 0xfe;
return state;
};
@ -145,7 +145,8 @@ export default class Mouse implements Card, Restorable<MouseState> {
break;
case rom[ENTRIES.READ_MOUSE]:
{
const moved = (this.lastX !== this.x) || (this.lastY !== this.y);
const moved =
this.lastX !== this.x || this.lastY !== this.y;
const status =
(this.down ? STATUS_DOWN : 0) |
(this.lastDown ? STATUS_LAST : 0) |
@ -183,13 +184,29 @@ export default class Mouse implements Card, Restorable<MouseState> {
{
const clampY = state.a;
if (clampY) {
this.clampYMin = holeRead(CLAMP_MIN_LOW) | (holeRead(CLAMP_MIN_HIGH) << 8);
this.clampYMax = holeRead(CLAMP_MAX_LOW) | (holeRead(CLAMP_MAX_HIGH) << 8);
debug('clampMouse Y', this.clampYMin, this.clampYMax);
this.clampYMin =
holeRead(CLAMP_MIN_LOW) |
(holeRead(CLAMP_MIN_HIGH) << 8);
this.clampYMax =
holeRead(CLAMP_MAX_LOW) |
(holeRead(CLAMP_MAX_HIGH) << 8);
debug(
'clampMouse Y',
this.clampYMin,
this.clampYMax
);
} else {
this.clampXMin = holeRead(CLAMP_MIN_LOW) | (holeRead(CLAMP_MIN_HIGH) << 8);
this.clampXMax = holeRead(CLAMP_MAX_LOW) | (holeRead(CLAMP_MAX_HIGH) << 8);
debug('clampMouse X', this.clampXMin, this.clampXMax);
this.clampXMin =
holeRead(CLAMP_MIN_LOW) |
(holeRead(CLAMP_MIN_HIGH) << 8);
this.clampXMax =
holeRead(CLAMP_MAX_LOW) |
(holeRead(CLAMP_MAX_HIGH) << 8);
debug(
'clampMouse X',
this.clampXMin,
this.clampXMax
);
}
state = clearCarry(state);
}
@ -229,10 +246,10 @@ export default class Mouse implements Card, Restorable<MouseState> {
if (this.mode & MODE_INT_VBL) {
this.serve |= INT_SCREEN;
}
if ((this.mode & MODE_INT_PRESS) && this.shouldIntPress) {
if (this.mode & MODE_INT_PRESS && this.shouldIntPress) {
this.serve |= INT_PRESS;
}
if ((this.mode & MODE_INT_MOVE) && this.shouldIntMove) {
if (this.mode & MODE_INT_MOVE && this.shouldIntMove) {
this.serve |= INT_MOVE;
}
if (this.serve) {
@ -255,8 +272,8 @@ export default class Mouse implements Card, Restorable<MouseState> {
setMouseXY(x: number, y: number, w: number, h: number) {
const rangeX = this.clampXMax - this.clampXMin;
const rangeY = this.clampYMax - this.clampYMin;
this.x = (x * rangeX / w + this.clampXMin) & 0xffff;
this.y = (y * rangeY / h + this.clampYMin) & 0xffff;
this.x = ((x * rangeX) / w + this.clampXMin) & 0xffff;
this.y = ((y * rangeY) / h + this.clampYMin) & 0xffff;
this.shouldIntMove = true;
}
@ -318,7 +335,7 @@ export default class Mouse implements Card, Restorable<MouseState> {
serve: this.serve,
shouldIntMove: this.shouldIntMove,
shouldIntPress: this.shouldIntPress,
slot: this.slot
slot: this.slot,
};
}
}

View File

@ -2,9 +2,7 @@ import ROM from 'js/roms/rom';
import { bit, byte } from 'js/types';
import { debug } from '../util';
const PATTERN = [
0xC5, 0x3A, 0xA3, 0x5C, 0xC5, 0x3A, 0xA3, 0x5C
];
const PATTERN = [0xc5, 0x3a, 0xa3, 0x5c, 0xc5, 0x3a, 0xa3, 0x5c];
const A0 = 0x01;
const A2 = 0x04;
@ -18,7 +16,6 @@ export default class NoSlotClock {
debug('NoSlotClock');
}
private patternMatch() {
for (let idx = 0; idx < 8; idx++) {
let byte = 0;
@ -53,7 +50,7 @@ export default class NoSlotClock {
const hour = now.getHours();
const minutes = now.getMinutes();
const seconds = now.getSeconds();
const hundredths = (now.getMilliseconds() / 10);
const hundredths = now.getMilliseconds() / 10;
this.bits = [];
@ -113,4 +110,3 @@ export default class NoSlotClock {
// Setting the state makes no sense.
}
}

View File

@ -3,7 +3,7 @@ import { Card, Restorable, byte } from '../types';
import { rom } from '../roms/cards/parallel';
const LOC = {
IOREG: 0x80
IOREG: 0x80,
} as const;
export interface ParallelState {}

View File

@ -12,7 +12,7 @@ const LOC = {
_RAMMID: 0x85,
_RAMHI: 0x86,
_RAMDATA: 0x87,
BANK: 0x8F
BANK: 0x8f,
} as const;
export class RAMFactorState {
@ -41,21 +41,21 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
}
private sethi(val: byte) {
this.ramhi = (val & 0xff);
this.ramhi = val & 0xff;
}
private setmid(val: byte) {
if (((this.rammid & 0x80) !== 0) && ((val & 0x80) === 0)) {
if ((this.rammid & 0x80) !== 0 && (val & 0x80) === 0) {
this.sethi(this.ramhi + 1);
}
this.rammid = (val & 0xff);
this.rammid = val & 0xff;
}
private setlo(val: byte) {
if (((this.ramlo & 0x80) !== 0) && ((val & 0x80) === 0)) {
if ((this.ramlo & 0x80) !== 0 && (val & 0x80) === 0) {
this.setmid(this.rammid + 1);
}
this.ramlo = (val & 0xff);
this.ramlo = val & 0xff;
}
private access(off: byte, val: byte) {
@ -105,7 +105,7 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
default:
break;
}
this.loc = (this.ramhi << 16) | (this.rammid << 8) | (this.ramlo);
this.loc = (this.ramhi << 16) | (this.rammid << 8) | this.ramlo;
/*
if (val === undefined) {
@ -123,7 +123,7 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
}
read(page: byte, off: byte) {
return rom[this.firmware << 12 | (page - 0xC0) << 8 | off];
return rom[(this.firmware << 12) | ((page - 0xc0) << 8) | off];
}
write() {
@ -138,7 +138,7 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
return {
loc: this.loc,
firmware: this.firmware,
mem: new Uint8Array(this.mem)
mem: new Uint8Array(this.mem),
};
}
@ -149,6 +149,6 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
this.ramhi = (this.loc >> 16) & 0xff;
this.rammid = (this.loc >> 8) & 0xff;
this.ramlo = (this.loc) & 0xff;
this.ramlo = this.loc & 0xff;
}
}

View File

@ -1,9 +1,20 @@
import { debug, toHex } from '../util';
import { rom as smartPortRom } from '../roms/cards/smartport';
import { Card, Restorable, byte, word, rom } from '../types';
import { MassStorage, BlockDisk, ENCODING_BLOCK, BlockFormat, MassStorageData, DiskFormat } from '../formats/types';
import {
MassStorage,
BlockDisk,
ENCODING_BLOCK,
BlockFormat,
MassStorageData,
DiskFormat,
} from '../formats/types';
import { CPU6502, CpuState, flags } from '@whscullin/cpu6502';
import { create2MGFromBlockDisk, HeaderData, read2MGHeader } from '../formats/2mg';
import {
create2MGFromBlockDisk,
HeaderData,
read2MGHeader,
} from '../formats/2mg';
import createBlockDisk from '../formats/block';
import { DriveNumber } from '../formats/types';
@ -27,7 +38,11 @@ class Address {
lo: byte;
hi: byte;
constructor(private cpu: CPU6502, a: byte | word, b?: byte) {
constructor(
private cpu: CPU6502,
a: byte,
b?: byte
) {
if (b === undefined) {
this.lo = a & 0xff;
this.hi = a >> 8;
@ -46,7 +61,10 @@ class Address {
}
inc(val: byte) {
return new Address(this.cpu, ((this.hi << 8 | this.lo) + val) & 0xffff);
return new Address(
this.cpu,
(((this.hi << 8) | this.lo) + val) & 0xffff
);
}
readByte() {
@ -57,7 +75,7 @@ class Address {
const readLo = this.readByte();
const readHi = this.inc(1).readByte();
return readHi << 8 | readLo;
return (readHi << 8) | readLo;
}
readAddress() {
@ -97,8 +115,8 @@ const BLOCK_LO = 0x46;
// const IO_ERROR = 0x27;
const NO_DEVICE_CONNECTED = 0x28;
const WRITE_PROTECTED = 0x2B;
const DEVICE_OFFLINE = 0x2F;
const WRITE_PROTECTED = 0x2b;
const DEVICE_OFFLINE = 0x2f;
// const VOLUME_DIRECTORY_NOT_FOUND = 0x45;
// const NOT_A_PRODOS_DISK = 0x52;
// const VOLUME_CONTROL_BLOCK_FULL = 0x55;
@ -123,8 +141,9 @@ const DEVICE_TYPE_SCSI_HD = 0x07;
// $0D: Printer
// $0E: Clock
// $0F: Modem
export default class SmartPort implements Card, MassStorage<BlockFormat>, Restorable<SmartPortState> {
export default class SmartPort
implements Card, MassStorage<BlockFormat>, Restorable<SmartPortState>
{
private rom: rom;
private disks: BlockDisk[] = [];
private busy: boolean[] = [];
@ -139,7 +158,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
) {
if (options?.block) {
const dumbPortRom = new Uint8Array(smartPortRom);
dumbPortRom[0x07] = 0x3C;
dumbPortRom[0x07] = 0x3c;
this.rom = dumbPortRom;
debug('DumbPort card');
} else {
@ -221,7 +240,12 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
* readBlock
*/
readBlock(state: CpuState, driveNo: DriveNumber, block: number, buffer: Address) {
readBlock(
state: CpuState,
driveNo: DriveNumber,
block: number,
buffer: Address
) {
this.debug(`read drive=${driveNo}`);
this.debug(`read buffer=${buffer.toString()}`);
this.debug(`read block=$${toHex(block)}`);
@ -249,7 +273,12 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
* writeBlock
*/
writeBlock(state: CpuState, driveNo: DriveNumber, block: number, buffer: Address) {
writeBlock(
state: CpuState,
driveNo: DriveNumber,
block: number,
buffer: Address
) {
this.debug(`write drive=${driveNo}`);
this.debug(`write buffer=${buffer.toString()}`);
this.debug(`write block=$${toHex(block)}`);
@ -347,13 +376,14 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
const blockOff = this.rom[0xff];
const smartOff = blockOff + 3;
if (off === blockOff && this.cpu.getSync()) { // Regular block device entry POINT
if (off === blockOff && this.cpu.getSync()) {
// Regular block device entry POINT
this.debug('block device entry');
cmd = this.cpu.read(0x00, COMMAND);
unit = this.cpu.read(0x00, UNIT);
const bufferAddr = new Address(this.cpu, ADDRESS_LO);
const blockAddr = new Address(this.cpu, BLOCK_LO);
const drive = (unit & 0x80) ? 2 : 1;
const drive = unit & 0x80 ? 2 : 1;
const driveSlot = (unit & 0x70) >> 4;
buffer = bufferAddr.readAddress();
@ -435,27 +465,42 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
default: // Unit 1
switch (status) {
case 0:
blocks = this.disks[unit]?.blocks.length ?? 0;
blocks =
this.disks[unit]?.blocks.length ?? 0;
buffer.writeByte(0xf0); // W/R Block device in drive
buffer.inc(1).writeByte(blocks & 0xff); // 1600 blocks
buffer.inc(2).writeByte((blocks & 0xff00) >> 8);
buffer.inc(3).writeByte((blocks & 0xff0000) >> 16);
buffer
.inc(2)
.writeByte((blocks & 0xff00) >> 8);
buffer
.inc(3)
.writeByte((blocks & 0xff0000) >> 16);
state.x = 4;
state.y = 0;
state.a = 0;
state.s &= ~flags.C;
break;
case 3:
blocks = this.disks[unit]?.blocks.length ?? 0;
blocks =
this.disks[unit]?.blocks.length ?? 0;
buffer.writeByte(0xf0); // W/R Block device in drive
buffer.inc(1).writeByte(blocks & 0xff); // Blocks low byte
buffer.inc(2).writeByte((blocks & 0xff00) >> 8); // Blocks middle byte
buffer.inc(3).writeByte((blocks & 0xff0000) >> 16); // Blocks high byte
buffer
.inc(2)
.writeByte((blocks & 0xff00) >> 8); // Blocks middle byte
buffer
.inc(3)
.writeByte((blocks & 0xff0000) >> 16); // Blocks high byte
buffer.inc(4).writeByte(ID.length); // Vendor ID length
for (let idx = 0; idx < ID.length; idx++) { // Vendor ID
buffer.inc(5 + idx).writeByte(ID.charCodeAt(idx));
for (let idx = 0; idx < ID.length; idx++) {
// Vendor ID
buffer
.inc(5 + idx)
.writeByte(ID.charCodeAt(idx));
}
buffer.inc(21).writeByte(DEVICE_TYPE_SCSI_HD); // Device Type
buffer
.inc(21)
.writeByte(DEVICE_TYPE_SCSI_HD); // Device Type
buffer.inc(22).writeByte(0x0); // Device Subtype
buffer.inc(23).writeWord(0x0101); // Version
state.x = 24;
@ -515,41 +560,38 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
getState() {
return {
disks: this.disks.map(
(disk) => {
const result: BlockDisk = {
blocks: disk.blocks.map(
(block) => new Uint8Array(block)
),
encoding: ENCODING_BLOCK,
format: disk.format,
readOnly: disk.readOnly,
metadata: { ...disk.metadata },
};
return result;
}
)
};
}
setState(state: SmartPortState) {
this.disks = state.disks.map(
(disk) => {
disks: this.disks.map((disk) => {
const result: BlockDisk = {
blocks: disk.blocks.map(
(block) => new Uint8Array(block)
),
blocks: disk.blocks.map((block) => new Uint8Array(block)),
encoding: ENCODING_BLOCK,
format: disk.format,
readOnly: disk.readOnly,
metadata: { ...disk.metadata },
};
return result;
}
);
}),
};
}
setBinary(driveNo: DriveNumber, name: string, fmt: BlockFormat, rawData: ArrayBuffer) {
setState(state: SmartPortState) {
this.disks = state.disks.map((disk) => {
const result: BlockDisk = {
blocks: disk.blocks.map((block) => new Uint8Array(block)),
encoding: ENCODING_BLOCK,
format: disk.format,
readOnly: disk.readOnly,
metadata: { ...disk.metadata },
};
return result;
});
}
setBinary(
driveNo: DriveNumber,
name: string,
fmt: BlockFormat,
rawData: ArrayBuffer
) {
let volume = 254;
let readOnly = false;
if (fmt === '2mg') {

View File

@ -4,25 +4,26 @@ import { rom } from '../roms/cards/thunderclock';
const LOC = {
CONTROL: 0x80,
AUX: 0x88
AUX: 0x88,
} as const;
const COMMANDS = {
MASK: 0x18,
REGHOLD: 0x00,
REGSHIFT: 0x08,
TIMED: 0x18
TIMED: 0x18,
} as const;
const FLAGS = {
DATA: 0x01,
CLOCK: 0x02,
STROBE: 0x04
STROBE: 0x04,
} as const;
export interface ThunderclockState {}
export default class Thunderclock implements Card, Restorable<ThunderclockState>
export default class Thunderclock
implements Card, Restorable<ThunderclockState>
{
constructor() {
debug('Thunderclock');
@ -82,7 +83,7 @@ export default class Thunderclock implements Card, Restorable<ThunderclockState>
}
private access(off: byte, val?: byte) {
switch (off & 0x8F) {
switch (off & 0x8f) {
case LOC.CONTROL:
if (val !== undefined) {
const strobe = val & FLAGS.STROBE ? true : false;
@ -105,7 +106,10 @@ export default class Thunderclock implements Card, Restorable<ThunderclockState>
this.shiftMode = false;
break;
default:
this.debug('Unknown command', toHex(this.command));
this.debug(
'Unknown command',
toHex(this.command)
);
}
}
}
@ -132,7 +136,7 @@ export default class Thunderclock implements Card, Restorable<ThunderclockState>
if (page < 0xc8) {
result = rom[off];
} else {
result = rom[(page - 0xc8) << 8 | off];
result = rom[((page - 0xc8) << 8) | off];
}
return result;
}

View File

@ -13,25 +13,25 @@ interface VideotermState {
const LOC = {
IOREG: 0x80,
IOVAL: 0x81
IOVAL: 0x81,
} as const;
const REGS = {
CURSOR_UPPER: 0x0A,
CURSOR_LOWER: 0x0B,
STARTPOS_HI: 0x0C,
STARTPOS_LO: 0x0D,
CURSOR_HI: 0x0E,
CURSOR_LO: 0x0F,
CURSOR_UPPER: 0x0a,
CURSOR_LOWER: 0x0b,
STARTPOS_HI: 0x0c,
STARTPOS_LO: 0x0d,
CURSOR_HI: 0x0e,
CURSOR_LO: 0x0f,
LIGHTPEN_HI: 0x10,
LIGHTPEN_LO: 0x11
LIGHTPEN_LO: 0x11,
} as const;
const CURSOR_MODES = {
SOLID: 0x00,
HIDDEN: 0x01,
BLINK: 0x10,
FAST_BLINK: 0x11
FAST_BLINK: 0x11,
} as const;
const BLACK: Color = [0x00, 0x00, 0x00];
@ -56,7 +56,7 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
0x00, // 0E - Cursor Hi
0x00, // 0F - Cursor Lo
0x00, // 10 - Lightpen Hi
0x00 // 11 - Lightpen Lo
0x00, // 11 - Lightpen Lo
];
private blink = false;
@ -123,7 +123,8 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
}
private refreshCursor(fromRegs: boolean) {
const addr = this.regs[REGS.CURSOR_HI] << 8 | this.regs[REGS.CURSOR_LO];
const addr =
(this.regs[REGS.CURSOR_HI] << 8) | this.regs[REGS.CURSOR_LO];
const saddr = (0x800 + addr - this.startPos) & 0x7ff;
const data = this.imageData.data;
const row = (saddr / 80) & 0xff;
@ -144,16 +145,20 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
if (blinkmode === CURSOR_MODES.HIDDEN) {
return;
}
if (this.blink || (blinkmode === CURSOR_MODES.SOLID)) {
if (this.blink || blinkmode === CURSOR_MODES.SOLID) {
this.dirty = true;
for (let idx = 0; idx < 8; idx++) {
const color = WHITE;
if (idx >= (this.regs[REGS.CURSOR_UPPER] & 0x1f) &&
idx <= (this.regs[REGS.CURSOR_LOWER] & 0x1f)) {
if (
idx >= (this.regs[REGS.CURSOR_UPPER] & 0x1f) &&
idx <= (this.regs[REGS.CURSOR_LOWER] & 0x1f)
) {
for (let jdx = 0; jdx < 7; jdx++) {
data[(y + idx) * 560 * 4 + (x + jdx) * 4] = color[0];
data[(y + idx) * 560 * 4 + (x + jdx) * 4 + 1] = color[1];
data[(y + idx) * 560 * 4 + (x + jdx) * 4 + 2] = color[2];
data[(y + idx) * 560 * 4 + (x + jdx) * 4 + 1] =
color[1];
data[(y + idx) * 560 * 4 + (x + jdx) * 4 + 2] =
color[2];
}
}
}
@ -162,8 +167,7 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
private updateStartPos() {
const startPos =
this.regs[REGS.STARTPOS_HI] << 8 |
this.regs[REGS.STARTPOS_LO];
(this.regs[REGS.STARTPOS_HI] << 8) | this.regs[REGS.STARTPOS_LO];
if (this.startPos !== startPos) {
this.startPos = startPos;
this.shouldRefresh = true;
@ -208,7 +212,7 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
}
break;
}
this.bank = (off & 0x0C) >> 2;
this.bank = (off & 0x0c) >> 2;
return result;
}
@ -218,9 +222,9 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
read(page: byte, off: byte) {
if (page < 0xcc) {
return ROM[(page & 0x03) << 8 | off];
} else if (page < 0xce){
const addr = ((page & 0x01) + (this.bank << 1)) << 8 | off;
return ROM[((page & 0x03) << 8) | off];
} else if (page < 0xce) {
const addr = (((page & 0x01) + (this.bank << 1)) << 8) | off;
return this.buffer[addr];
}
return 0;
@ -228,7 +232,7 @@ export default class Videoterm implements Card, Restorable<VideotermState> {
write(page: byte, off: byte, val: byte) {
if (page > 0xcb && page < 0xce) {
const addr = ((page & 0x01) + (this.bank << 1)) << 8 | off;
const addr = (((page & 0x01) + (this.bank << 1)) << 8) | off;
this.updateBuffer(addr, val);
}
}

View File

@ -24,16 +24,13 @@ export const App = () => {
const system = {
...defaultSystem,
...(systemTypes[systemType] || {})
...(systemTypes[systemType] || {}),
};
return (
<div className={cs(styles.container, componentStyles.components)}>
<Header e={system.e} />
<Apple2
gl={gl}
{...system}
/>
<Apple2 gl={gl} {...system} />
</div>
);
};

View File

@ -1,6 +1,12 @@
import { h } from 'preact';
import cs from 'classnames';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import { Apple2 as Apple2Impl } from '../apple2';
import { ControlStrip } from './ControlStrip';
import { Debugger } from './debugger/Debugger';
@ -60,40 +66,50 @@ export const Apple2 = (props: Apple2Props) => {
const vm = apple2?.getVideoModes();
const rom = apple2?.getROM();
const doPaste = useCallback((event: Event) => {
if (
(document.activeElement !== screenRef.current) &&
(document.activeElement !== document.body)
) {
return;
}
if (io) {
const paste = (event.clipboardData || window.clipboardData)?.getData('text');
if (paste) {
io.setKeyBuffer(paste);
const doPaste = useCallback(
(event: Event) => {
if (
document.activeElement !== screenRef.current &&
document.activeElement !== document.body
) {
return;
}
}
event.preventDefault();
}, [io]);
if (io) {
const paste = (
event.clipboardData || window.clipboardData
)?.getData('text');
if (paste) {
io.setKeyBuffer(paste);
}
}
event.preventDefault();
},
[io]
);
const doCopy = useCallback((event: Event) => {
if (
(document.activeElement !== screenRef.current) &&
(document.activeElement !== document.body)
) {
return;
}
if (vm) {
event.clipboardData?.setData('text/plain', vm.getText());
}
event.preventDefault();
}, [vm]);
const doCopy = useCallback(
(event: Event) => {
if (
document.activeElement !== screenRef.current &&
document.activeElement !== document.body
) {
return;
}
if (vm) {
event.clipboardData?.setData('text/plain', vm.getText());
}
event.preventDefault();
},
[vm]
);
useEffect(() => {
if (screenRef.current) {
const options = {
canvas: screenRef.current,
tick: () => { /* do nothing */ },
tick: () => {
/* do nothing */
},
...props,
};
const apple2 = new Apple2Impl(options);
@ -148,18 +164,33 @@ export const Apple2 = (props: Apple2Props) => {
return (
<div className={styles.container}>
<div
className={cs(styles.outer, { apple2e: e, [styles.ready]: ready })}
className={cs(styles.outer, {
apple2e: e,
[styles.ready]: ready,
})}
>
<Screen screenRef={screenRef} />
{!e ? <LanguageCard cpu={cpu} io={io} rom={rom} slot={0} /> : null}
{!e ? (
<LanguageCard cpu={cpu} io={io} rom={rom} slot={0} />
) : null}
<Slinky io={io} slot={2} />
{!e ? <Videoterm io={io} slot={3} /> : null}
<Mouse cpu={cpu} screenRef={screenRef} io={io} slot={4} />
<ThunderClock io={io} slot={5} />
<Inset>
<Drives cpu={cpu} io={io} sectors={sectors} enhanced={enhanced} ready={drivesReady} />
<Drives
cpu={cpu}
io={io}
sectors={sectors}
enhanced={enhanced}
ready={drivesReady}
/>
</Inset>
<ControlStrip apple2={apple2} e={e} toggleDebugger={toggleDebugger} />
<ControlStrip
apple2={apple2}
e={e}
toggleDebugger={toggleDebugger}
/>
<Inset>
<Keyboard apple2={apple2} e={e} />
</Inset>

View File

@ -85,12 +85,17 @@ export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => {
id={`disk${number}`}
className={cs(styles.diskLight, { [styles.on]: on })}
/>
<ControlButton title="Load Disk" onClick={onOpenModal} icon="folder-open" />
<ControlButton title="Save Disk" onClick={onOpenDownloadModal} icon="save" />
<div
id={`disk-label${number}`}
className={styles.diskLabel}
>
<ControlButton
title="Load Disk"
onClick={onOpenModal}
icon="folder-open"
/>
<ControlButton
title="Save Disk"
onClick={onOpenDownloadModal}
icon="save"
/>
<div id={`disk-label${number}`} className={styles.diskLabel}>
{name}
</div>
</DiskDragTarget>

View File

@ -14,8 +14,10 @@ import styles from './css/BlockFileModal.module.scss';
const DISK_TYPES: FilePickerAcceptType[] = [
{
description: 'Disk Images',
accept: { 'application/octet-stream': BLOCK_FORMATS.map(x => '.' + x) },
}
accept: {
'application/octet-stream': BLOCK_FORMATS.map((x) => '.' + x),
},
},
];
interface BlockFileModalProps {
@ -25,7 +27,12 @@ interface BlockFileModalProps {
onClose: (closeBox?: boolean) => void;
}
export const BlockFileModal = ({ smartPort, driveNo: number, onClose, isOpen }: BlockFileModalProps) => {
export const BlockFileModal = ({
smartPort,
driveNo: number,
onClose,
isOpen,
}: BlockFileModalProps) => {
const [handles, setHandles] = useState<FileSystemFileHandle[]>();
const [busy, setBusy] = useState<boolean>(false);
const [empty, setEmpty] = useState<boolean>(true);
@ -41,7 +48,11 @@ export const BlockFileModal = ({ smartPort, driveNo: number, onClose, isOpen }:
hashParts[number] = '';
setBusy(true);
try {
await loadLocalBlockFile(smartPort, number, await handles[0].getFile());
await loadLocalBlockFile(
smartPort,
number,
await handles[0].getFile()
);
} catch (error) {
setError(error);
} finally {
@ -68,7 +79,9 @@ export const BlockFileModal = ({ smartPort, driveNo: number, onClose, isOpen }:
</ModalContent>
<ModalFooter>
<button onClick={doCancel}>Cancel</button>
<button onClick={noAwait(doOpen)} disabled={busy || empty}>Open</button>
<button onClick={noAwait(doOpen)} disabled={busy || empty}>
Open
</button>
</ModalFooter>
</Modal>
<ErrorModal error={error} setError={setError} />

View File

@ -38,19 +38,11 @@ export const CPUMeter = ({ apple2 }: CPUMeterProps) => {
const time = Date.now();
const delta = time - lastTime.current;
if (stats) {
setKhz(
Math.floor(
(stats.cycles - cycles) / delta
)
);
setFps(
Math.floor(
(stats.frames - frames) / delta * 1000
)
);
setKhz(Math.floor((stats.cycles - cycles) / delta));
setFps(Math.floor(((stats.frames - frames) / delta) * 1000));
setRps(
Math.floor(
(stats.renderedFrames - renderedFrames) / delta * 1000
((stats.renderedFrames - renderedFrames) / delta) * 1000
)
);
lastStats.current = { ...stats };

View File

@ -22,8 +22,19 @@ export interface ControlButtonProps {
* @param onClick Click callback
* @returns Control Button component
*/
export const ControlButton = ({ active, icon, title, onClick, ...props }: ControlButtonProps) => (
<button className={styles.iconButton} onClick={onClick} title={title} {...props} >
export const ControlButton = ({
active,
icon,
title,
onClick,
...props
}: ControlButtonProps) => (
<button
className={styles.iconButton}
onClick={onClick}
title={title}
{...props}
>
<i className={cs('fas', `fa-${icon}`, { [styles.active]: active })}></i>
</button>
);

View File

@ -33,7 +33,11 @@ interface ControlStripProps {
* @param e Whether or not this is a //e
* @returns ControlStrip component
*/
export const ControlStrip = ({ apple2, e, toggleDebugger }: ControlStripProps) => {
export const ControlStrip = ({
apple2,
e,
toggleDebugger,
}: ControlStripProps) => {
const [showOptions, setShowOptions] = useState(false);
const [io, setIO] = useState<Apple2IO>();
const options = useContext(OptionsContext);
@ -55,23 +59,22 @@ export const ControlStrip = ({ apple2, e, toggleDebugger }: ControlStripProps) =
}
}, [apple2, e, options]);
const doReset = useCallback(() =>
apple2?.reset(), [apple2]);
const doReset = useCallback(() => apple2?.reset(), [apple2]);
const doReadme = useCallback(() =>
window.open(README, '_blank'), []);
const doReadme = useCallback(() => window.open(README, '_blank'), []);
const doShowOptions = useCallback(() =>
setShowOptions(true), []);
const doShowOptions = useCallback(() => setShowOptions(true), []);
const doCloseOptions = useCallback(() =>
setShowOptions(false), []);
const doCloseOptions = useCallback(() => setShowOptions(false), []);
const doToggleFullPage = useCallback(() =>
options.setOption(
SCREEN_FULL_PAGE,
!options.getOption(SCREEN_FULL_PAGE)
), [options]);
const doToggleFullPage = useCallback(
() =>
options.setOption(
SCREEN_FULL_PAGE,
!options.getOption(SCREEN_FULL_PAGE)
),
[options]
);
useHotKey('F2', doToggleFullPage);
useHotKey('F4', doShowOptions);
@ -82,12 +85,20 @@ export const ControlStrip = ({ apple2, e, toggleDebugger }: ControlStripProps) =
<OptionsModal isOpen={showOptions} onClose={doCloseOptions} />
<Inset>
<CPUMeter apple2={apple2} />
<ControlButton onClick={toggleDebugger} title="Toggle Debugger" icon="bug" />
<ControlButton
onClick={toggleDebugger}
title="Toggle Debugger"
icon="bug"
/>
<AudioControl apple2={apple2} />
<Printer io={io} slot={1} />
<div style={{ flexGrow: 1 }} />
<ControlButton onClick={doReadme} title="About" icon="info" />
<ControlButton onClick={doShowOptions} title="Options (F4)" icon="cog" />
<ControlButton
onClick={doShowOptions}
title="Options (F4)"
icon="cog"
/>
</Inset>
{e && (
<div className={styles.reset} onClick={doReset}>

View File

@ -1,15 +1,20 @@
import { BLOCK_FORMATS, DISK_FORMATS, DriveNumber, FLOPPY_FORMATS, MassStorage } from 'js/formats/types';
import {
BLOCK_FORMATS,
DISK_FORMATS,
DriveNumber,
FLOPPY_FORMATS,
MassStorage,
} from 'js/formats/types';
import { h, JSX, RefObject } from 'preact';
import { useEffect, useRef } from 'preact/hooks';
import { loadLocalFile } from './util/files';
import { loadLocalFile } from './util/files';
import { spawn } from './util/promises';
export interface DiskDragTargetProps<T> extends JSX.HTMLAttributes<HTMLDivElement> {
export interface DiskDragTargetProps<T>
extends JSX.HTMLAttributes<HTMLDivElement> {
storage: MassStorage<T> | undefined;
driveNo?: DriveNumber;
formats: typeof FLOPPY_FORMATS
| typeof BLOCK_FORMATS
| typeof DISK_FORMATS;
formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS;
dropRef?: RefObject<HTMLElement>;
onError: (error: unknown) => void;
}
@ -32,7 +37,11 @@ export const DiskDragTarget = ({
event.preventDefault();
const dt = event.dataTransfer;
if (dt) {
if (Array.from(dt.items).every((item) => item.kind === 'file')) {
if (
Array.from(dt.items).every(
(item) => item.kind === 'file'
)
) {
dt.dropEffect = 'copy';
} else {
dt.dropEffect = 'none';
@ -54,13 +63,18 @@ export const DiskDragTarget = ({
const onDrop = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
const targetDrive = driveNo ?? 1; //TODO(whscullin) Maybe pick available drive
const targetDrive = driveNo ?? 1; //TODO(whscullin) Maybe pick available drive
const dt = event.dataTransfer;
if (dt?.files.length === 1 && storage) {
spawn(async () => {
try {
await loadLocalFile(storage, formats, targetDrive, dt.files[0]);
await loadLocalFile(
storage,
formats,
targetDrive,
dt.files[0]
);
} catch (e) {
onError(e);
}
@ -68,8 +82,18 @@ export const DiskDragTarget = ({
} else if (dt?.files.length === 2 && storage) {
spawn(async () => {
try {
await loadLocalFile(storage, formats, 1, dt.files[0]);
await loadLocalFile(storage, formats, 2, dt.files[1]);
await loadLocalFile(
storage,
formats,
1,
dt.files[0]
);
await loadLocalFile(
storage,
formats,
2,
dt.files[1]
);
} catch (e) {
onError(e);
}

View File

@ -70,7 +70,12 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
formats={FLOPPY_FORMATS}
onError={setError}
>
<FileModal disk2={disk2} driveNo={number} onClose={doClose} isOpen={modalOpen} />
<FileModal
disk2={disk2}
driveNo={number}
onClose={doClose}
isOpen={modalOpen}
/>
<DownloadModal
driveNo={number}
massStorage={disk2}
@ -79,11 +84,17 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
/>
<ErrorModal error={error} setError={setError} />
<div className={cs(styles.diskLight, { [styles.on]: on })} />
<ControlButton title="Load Disk" onClick={onOpenModal} icon="folder-open" />
<ControlButton title="Save Disk" onClick={onOpenDownloadModal} icon="save" />
<div className={styles.diskLabel}>
{label}
</div>
<ControlButton
title="Load Disk"
onClick={onOpenModal}
icon="folder-open"
/>
<ControlButton
title="Save Disk"
onClick={onOpenDownloadModal}
icon="save"
/>
<div className={styles.diskLabel}>{label}</div>
</DiskDragTarget>
);
};

View File

@ -12,7 +12,12 @@ interface DownloadModalProps {
onClose: (closeBox?: boolean) => void;
}
export const DownloadModal = ({ massStorage, driveNo, onClose, isOpen }: DownloadModalProps) => {
export const DownloadModal = ({
massStorage,
driveNo,
onClose,
isOpen,
}: DownloadModalProps) => {
const [href, setHref] = useState('');
const [downloadName, setDownloadName] = useState('');
const doCancel = useCallback(() => onClose(true), [onClose]);
@ -24,10 +29,9 @@ export const DownloadModal = ({ massStorage, driveNo, onClose, isOpen }: Downloa
const { ext, data } = storageData;
const { name } = storageData.metadata;
if (data.byteLength) {
const blob = new Blob(
[data],
{ type: 'application/octet-stream' }
);
const blob = new Blob([data], {
type: 'application/octet-stream',
});
const href = window.URL.createObjectURL(blob);
setHref(href);
setDownloadName(`${name}.${ext}`);
@ -44,22 +48,20 @@ export const DownloadModal = ({ massStorage, driveNo, onClose, isOpen }: Downloa
<Modal title="Save File" isOpen={isOpen} onClose={onClose}>
<ModalContent>
<div className={styles.modalContent}>
{href
? (
<>
<span>Disk Name: {downloadName}</span>
<a
role="button"
href={href}
download={downloadName}
>
Download
</a>
</>
) : (
<span>No Download Available</span>
)
}
{href ? (
<>
<span>Disk Name: {downloadName}</span>
<a
role="button"
href={href}
download={downloadName}
>
Download
</a>
</>
) : (
<span>No Download Available</span>
)}
</div>
</ModalContent>
<ModalFooter>

View File

@ -8,9 +8,18 @@ import { CPU6502 } from '@whscullin/cpu6502';
import { BlockDisk } from './BlockDisk';
import { ErrorModal } from './ErrorModal';
import { ProgressModal } from './ProgressModal';
import { loadHttpUnknownFile, getHashParts, loadJSON, SmartStorageBroker } from './util/files';
import {
loadHttpUnknownFile,
getHashParts,
loadJSON,
SmartStorageBroker,
} from './util/files';
import { useHash } from './hooks/useHash';
import { DISK_FORMATS, DRIVE_NUMBERS, SupportedSectors } from 'js/formats/types';
import {
DISK_FORMATS,
DRIVE_NUMBERS,
SupportedSectors,
} from 'js/formats/types';
import { spawn, Ready } from './util/promises';
import styles from './css/Drives.module.scss';
@ -74,12 +83,12 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
const [smartData1, setSmartData1] = useState<DiskIIData>({
on: false,
number: 1,
name: 'HD 1'
name: 'HD 1',
});
const [smartData2, setSmartData2] = useState<DiskIIData>({
on: false,
number: 2,
name: 'HD 2'
name: 'HD 2',
});
const hash = useHash();
@ -97,25 +106,30 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
const isJson = hashPart.match(/\.json$/i);
if (isHttp && !isJson) {
loading++;
controllers.push(spawn(async (signal) => {
try {
await loadHttpUnknownFile(
smartStorageBroker,
driveNo,
hashPart,
signal,
onProgress);
} catch (e) {
setError(e);
}
if (--loading === 0) {
ready.onReady();
}
setCurrent(0);
setTotal(0);
}));
controllers.push(
spawn(async (signal) => {
try {
await loadHttpUnknownFile(
smartStorageBroker,
driveNo,
hashPart,
signal,
onProgress
);
} catch (e) {
setError(e);
}
if (--loading === 0) {
ready.onReady();
}
setCurrent(0);
setTotal(0);
})
);
} else {
const url = isHttp ? hashPart : `json/disks/${hashPart}.json`;
const url = isHttp
? hashPart
: `json/disks/${hashPart}.json`;
loadJSON(disk2, driveNo, url).catch((e) => setError(e));
}
}
@ -123,7 +137,8 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
if (!loading) {
ready.onReady();
}
return () => controllers.forEach((controller) => controller.abort());
return () =>
controllers.forEach((controller) => controller.abort());
}
}, [hash, onProgress, ready, storageDevices]);
@ -132,10 +147,10 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
const setSmartData = [setSmartData1, setSmartData2];
const callbacks: Callbacks = {
driveLight: (drive, on) => {
setData[drive - 1]?.(data => ({ ...data, on }));
setData[drive - 1]?.((data) => ({ ...data, on }));
},
label: (drive, name, side) => {
setData[drive - 1]?.(data => ({
setData[drive - 1]?.((data) => ({
...data,
name: name ?? `Disk ${drive}`,
side,
@ -143,27 +158,31 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
},
dirty: () => {
// do nothing
}
},
};
const smartPortCallbacks: Callbacks = {
driveLight: (drive, on) => {
setSmartData[drive - 1]?.(data => ({ ...data, on }));
setSmartData[drive - 1]?.((data) => ({ ...data, on }));
},
label: (drive, name, side) => {
setSmartData[drive - 1]?.(data => ({
setSmartData[drive - 1]?.((data) => ({
...data,
name: name ?? `HD ${drive}`,
side,
}));
},
dirty: () => {/* Unused */ }
dirty: () => {
/* Unused */
},
};
if (cpu && io) {
const disk2 = new Disk2(io, callbacks, sectors);
io.setSlot(6, disk2);
const smartPort = new SmartPort(cpu, smartPortCallbacks, { block: !enhanced });
const smartPort = new SmartPort(cpu, smartPortCallbacks, {
block: !enhanced,
});
io.setSlot(7, smartPort);
const smartStorageBroker = new SmartStorageBroker(disk2, smartPort);

View File

@ -5,7 +5,7 @@ import { Modal, ModalContent, ModalFooter } from './Modal';
import styles from './css/ErrorModal.module.scss';
export interface ErrorProps {
error: unknown | undefined;
error: unknown;
setError: (error: string | undefined) => void;
}
@ -32,9 +32,7 @@ export const ErrorModal = ({ error, setError }: ErrorProps) => {
onClose={onClose}
>
<ModalContent>
<div className={styles.errorModal}>
{errorStr}
</div>
<div className={styles.errorModal}>{errorStr}</div>
</ModalContent>
<ModalFooter>
<button onClick={onClose}>OK</button>

View File

@ -1,5 +1,11 @@
import { h, Fragment } from 'preact';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import { noAwait } from './util/promises';
export interface FilePickerAcceptType {
@ -34,7 +40,9 @@ interface ExtraProps {
const InputFileChooser = ({
disabled = false,
onChange = () => { /* do nothing */ },
onChange = () => {
/* do nothing */
},
accept = [],
}: InputFileChooserProps) => {
const inputRef = useRef<HTMLInputElement>(null);
@ -69,7 +77,9 @@ const InputFileChooser = ({
// the moment, not adding the MIME type is sufficient.
const newAccept: string[] = [];
for (const type of accept) {
for (let [/* typeString */, suffixes] of Object.entries(type.accept)) {
for (let [, /* typeString */ suffixes] of Object.entries(
type.accept
)) {
// newAccept.push(typeString);
if (!Array.isArray(suffixes)) {
suffixes = [suffixes];
@ -91,11 +101,15 @@ const InputFileChooser = ({
}, [accept]);
return (
<input type="file" role='button' aria-label='Open file'
<input
type="file"
role="button"
aria-label="Open file"
ref={inputRef}
onChange={onChangeInternal}
disabled={disabled}
{...extraProps} />
{...extraProps}
/>
);
};
@ -107,8 +121,10 @@ interface FilePickerChooserProps {
const FilePickerChooser = ({
disabled = false,
onChange = () => { /* do nothing */ },
accept = [ACCEPT_EVERYTHING_TYPE]
onChange = () => {
/* do nothing */
},
accept = [ACCEPT_EVERYTHING_TYPE],
}: FilePickerChooserProps) => {
const [busy, setBusy] = useState<boolean>(false);
const [selectedFilename, setSelectedFilename] = useState<string>();
@ -138,14 +154,16 @@ const FilePickerChooser = ({
useEffect(() => {
setSelectedFilename(
fileHandles?.length
? fileHandles[0].name
: 'No file selected');
fileHandles?.length ? fileHandles[0].name : 'No file selected'
);
}, [fileHandles]);
return (
<>
<button onClick={noAwait(onClickInternal)} disabled={disabled || busy}>
<button
onClick={noAwait(onClickInternal)}
disabled={disabled || busy}
>
Choose File
</button>
&nbsp;
@ -164,7 +182,7 @@ const FilePickerChooser = ({
* Using `window.showOpenFilePicker` has the advantage of allowing read/write
* access to the file, whereas the regular input element only gives read
* access.
*
*
* The FileChooser takes an optional `accept` parameter that specifies which
* types of files can be opened. The parameter is a map of MIME type to file
* extension. If the MIME type is the empty string, t
@ -174,38 +192,48 @@ export const FileChooser = ({
control = controlDefault,
...rest
}: FileChooserProps) => {
const onChangeForInput = useCallback((files: FileList) => {
const handles: FileSystemFileHandle[] = [];
for (let i = 0; i < files.length; i++) {
const file = files.item(i);
if (file === null) {
continue;
const onChangeForInput = useCallback(
(files: FileList) => {
const handles: FileSystemFileHandle[] = [];
for (let i = 0; i < files.length; i++) {
const file = files.item(i);
if (file === null) {
continue;
}
handles.push({
kind: 'file',
name: file.name,
getFile: () => Promise.resolve(file),
createWritable: (_options) =>
Promise.reject('File not writable.'),
queryPermission: (descriptor) =>
Promise.resolve(
descriptor?.mode === 'read' ? 'granted' : 'denied'
),
requestPermission: (descriptor) =>
Promise.resolve(
descriptor?.mode === 'read' ? 'granted' : 'denied'
),
isSameEntry: (_unused) => Promise.resolve(false),
isDirectory: false,
isFile: true,
});
}
handles.push({
kind: 'file',
name: file.name,
getFile: () => Promise.resolve(file),
createWritable: (_options) => Promise.reject('File not writable.'),
queryPermission: (descriptor) => Promise.resolve(descriptor?.mode === 'read' ? 'granted' : 'denied'),
requestPermission: (descriptor) => Promise.resolve(descriptor?.mode === 'read' ? 'granted' : 'denied'),
isSameEntry: (_unused) => Promise.resolve(false),
isDirectory: false,
isFile: true,
});
}
onChange(handles);
}, [onChange]);
onChange(handles);
},
[onChange]
);
const onChangeForPicker = useCallback((fileHandles: FileSystemFileHandle[]) => {
onChange(fileHandles);
}, [onChange]);
const onChangeForPicker = useCallback(
(fileHandles: FileSystemFileHandle[]) => {
onChange(fileHandles);
},
[onChange]
);
return control === 'picker'
? (
<FilePickerChooser onChange={onChangeForPicker} {...rest} />
)
: (
<InputFileChooser onChange={onChangeForInput} {...rest} />
);
return control === 'picker' ? (
<FilePickerChooser onChange={onChangeForPicker} {...rest} />
) : (
<InputFileChooser onChange={onChangeForInput} {...rest} />
);
};

View File

@ -1,8 +1,18 @@
import { h, Fragment, JSX } from 'preact';
import { useCallback, useEffect, useState } from 'preact/hooks';
import { DiskDescriptor, DriveNumber, FLOPPY_FORMATS, NibbleFormat } from '../formats/types';
import {
DiskDescriptor,
DriveNumber,
FLOPPY_FORMATS,
NibbleFormat,
} from '../formats/types';
import { Modal, ModalContent, ModalFooter } from './Modal';
import { loadLocalNibbleFile, loadJSON, getHashParts, setHashParts } from './util/files';
import {
loadLocalNibbleFile,
loadJSON,
getHashParts,
setHashParts,
} from './util/files';
import DiskII from '../cards/disk2';
import { ErrorModal } from './ErrorModal';
@ -15,8 +25,10 @@ import styles from './css/FileModal.module.scss';
const DISK_TYPES: FilePickerAcceptType[] = [
{
description: 'Disk Images',
accept: { 'application/octet-stream': FLOPPY_FORMATS.map(x => '.' + x) },
}
accept: {
'application/octet-stream': FLOPPY_FORMATS.map((x) => '.' + x),
},
},
];
export type NibbleFileCallback = (
@ -38,7 +50,12 @@ interface IndexEntry {
category: string;
}
export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) => {
export const FileModal = ({
disk2,
driveNo,
onClose,
isOpen,
}: FileModalProps) => {
const [busy, setBusy] = useState<boolean>(false);
const [empty, setEmpty] = useState<boolean>(true);
const [category, setCategory] = useState<string>();
@ -52,7 +69,7 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
spawn(async () => {
try {
const indexResponse = await fetch('json/disks/index.json');
const index = await indexResponse.json() as IndexEntry[];
const index = (await indexResponse.json()) as IndexEntry[];
setIndex(index);
} catch (error) {
setIndex([]);
@ -70,7 +87,11 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
try {
if (handles?.length === 1) {
hashParts[driveNo] = '';
await loadLocalNibbleFile(disk2, driveNo, await handles[0].getFile());
await loadLocalNibbleFile(
disk2,
driveNo,
await handles[0].getFile()
);
}
if (filename) {
const name = filename.match(/\/([^/]+).json$/) || ['', ''];
@ -95,15 +116,16 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
const doSelectCategory = useCallback(
(event: JSX.TargetedMouseEvent<HTMLSelectElement>) =>
setCategory(event.currentTarget.value)
, []
setCategory(event.currentTarget.value),
[]
);
const doSelectFilename = useCallback(
(event: JSX.TargetedMouseEvent<HTMLSelectElement>) => {
setEmpty(!event.currentTarget.value);
setFilename(event.currentTarget.value);
}, []
},
[]
);
if (!index) {
@ -111,10 +133,7 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
}
const categories = index.reduce<Record<string, DiskDescriptor[]>>(
(
acc: Record<string, DiskDescriptor[]>,
disk: DiskDescriptor
) => {
(acc: Record<string, DiskDescriptor[]>, disk: DiskDescriptor) => {
const category = disk.category || 'Misc';
acc[category] = [disk, ...(acc[category] || [])];
@ -138,7 +157,10 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
</select>
<select multiple onChange={doSelectFilename}>
{disks.map((disk) => (
<option key={disk.filename} value={disk.filename}>
<option
key={disk.filename}
value={disk.filename}
>
{disk.name}
{disk.disk ? ` - ${disk.disk}` : ''}
</option>
@ -149,7 +171,9 @@ export const FileModal = ({ disk2, driveNo, onClose, isOpen }: FileModalProps) =
</ModalContent>
<ModalFooter>
<button onClick={doCancel}>Cancel</button>
<button onClick={noAwait(doOpen)} disabled={busy || empty}>Open</button>
<button onClick={noAwait(doOpen)} disabled={busy || empty}>
Open
</button>
</ModalFooter>
</Modal>
<ErrorModal error={error} setError={setError} />

View File

@ -7,7 +7,7 @@ import {
keys2e,
mapMouseEvent,
keysAsTuples,
mapKeyboardEvent
mapKeyboardEvent,
} from './util/keyboard';
import styles from './css/Keyboard.module.scss';
@ -58,26 +58,21 @@ export const Key = ({
active,
pressed,
onMouseDown,
onMouseUp
onMouseUp,
}: KeyProps) => {
const keyName = lower.replace(/[&#;]/g, '');
const center =
lower === 'LOCK'
? styles.vCenter2
: (upper === lower && upper.length > 1)
? styles.vCenter
: '';
: upper === lower && upper.length > 1
? styles.vCenter
: '';
return (
<div
className={cs(
styles.key,
styles[`key-${keyName}`],
center,
{
[styles.pressed]: pressed,
[styles.active]: active,
},
)}
className={cs(styles.key, styles[`key-${keyName}`], center, {
[styles.pressed]: pressed,
[styles.active]: active,
})}
data-key1={lower}
data-key2={upper}
onMouseDown={onMouseDown}
@ -85,7 +80,12 @@ export const Key = ({
>
<div>
{buildLabel(upper)}
{upper !== lower && <><br />{buildLabel(lower)}</>}
{upper !== lower && (
<>
<br />
{buildLabel(lower)}
</>
)}
</div>
</div>
);
@ -119,15 +119,22 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
return;
}
if (document.activeElement && document.activeElement !== document.body) {
if (
document.activeElement &&
document.activeElement !== document.body
) {
return;
}
event.preventDefault();
const { key, keyCode, keyLabel } = mapKeyboardEvent(event, active.includes('LOCK'), active.includes('CTRL'));
setPressed(pressed => pressed.concat([keyLabel]));
setActive(active => active.concat([keyLabel]));
const { key, keyCode, keyLabel } = mapKeyboardEvent(
event,
active.includes('LOCK'),
active.includes('CTRL')
);
setPressed((pressed) => pressed.concat([keyLabel]));
setActive((active) => active.concat([keyLabel]));
if (key === 'RESET') {
apple2.reset();
@ -153,8 +160,8 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
return;
}
const { key, keyLabel } = mapKeyboardEvent(event);
setPressed(pressed => pressed.filter(k => k !== keyLabel));
setActive(active => active.filter(k => k !== keyLabel));
setPressed((pressed) => pressed.filter((k) => k !== keyLabel));
setActive((active) => active.filter((k) => k !== keyLabel));
const io = apple2.getIO();
if (key === 'OPEN_APPLE') {
@ -186,7 +193,7 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
setActive([...active, key]);
return true;
}
setActive(active.filter(x => x !== key));
setActive(active.filter((x) => x !== key));
return false;
};
@ -237,12 +244,12 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
true
);
apple2?.getIO().keyUp();
setPressed(pressed.filter(x => x !== keyLabel));
setPressed(pressed.filter((x) => x !== keyLabel));
},
[apple2, active, pressed]
);
const bindKey = ([lower, upper]: [string, string]) =>
const bindKey = ([lower, upper]: [string, string]) => (
<Key
lower={lower}
upper={upper}
@ -250,17 +257,14 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
pressed={pressed.includes(lower)}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
/>;
/>
);
const rows = keys.map((row, idx) =>
const rows = keys.map((row, idx) => (
<div key={idx} className={cs(styles.row, styles[`row${idx}`])}>
{row.map(bindKey)}
</div>
);
));
return (
<div className={styles.keyboard}>
{rows}
</div>
);
return <div className={styles.keyboard}>{rows}</div>;
};

View File

@ -15,11 +15,7 @@ import { ControlButton } from './ControlButton';
* @returns ModalOverlay component
*/
export const ModalOverlay = ({ children }: { children: ComponentChildren }) => {
return (
<div className={styles.modalOverlay}>
{children}
</div>
);
return <div className={styles.modalOverlay}>{children}</div>;
};
/**
@ -28,11 +24,7 @@ export const ModalOverlay = ({ children }: { children: ComponentChildren }) => {
* @returns ModalContent component
*/
export const ModalContent = ({ children }: { children: ComponentChildren }) => {
return (
<div className={styles.modalContent}>
{children}
</div>
);
return <div className={styles.modalContent}>{children}</div>;
};
/**
@ -66,9 +58,7 @@ export const ModalCloseButton = ({ onClose }: ModalCloseButtonProp) => {
const doClose = useCallback(() => onClose(true), [onClose]);
useHotKey('Escape', doClose);
return (
<ControlButton onClick={doClose} title="Close" icon="xmark" />
);
return <ControlButton onClick={doClose} title="Close" icon="xmark" />;
};
type OnCloseCallback = (closeBox?: boolean) => void;
@ -94,8 +84,7 @@ export const ModalHeader = ({ onClose, title, icon }: ModalHeaderProps) => {
return (
<div role="banner" className={styles.modalHeader}>
<span className={styles.modalTitle}>
{icon && <i className={`fa-solid fa-${icon}`} role="img" />}
{' '}
{icon && <i className={`fa-solid fa-${icon}`} role="img" />}{' '}
{title}
</span>
{onClose && <ModalCloseButton onClose={onClose} />}
@ -124,20 +113,19 @@ export interface ModalProps {
* @param onClose Close callback
* @returns Modal component
*/
export const Modal = ({
isOpen,
children,
title,
...props
}: ModalProps) => {
return (
isOpen ? createPortal((
<ModalOverlay>
<div className={cs(styles.modal, componentStyles.components)} role="dialog">
{title && <ModalHeader title={title} {...props} />}
{children}
</div>
</ModalOverlay>
), document.body) : null
);
export const Modal = ({ isOpen, children, title, ...props }: ModalProps) => {
return isOpen
? createPortal(
<ModalOverlay>
<div
className={cs(styles.modal, componentStyles.components)}
role="dialog"
>
{title && <ModalHeader title={title} {...props} />}
{children}
</div>
</ModalOverlay>,
document.body
)
: null;
};

View File

@ -33,17 +33,13 @@ const Boolean = ({ option, value, setValue }: BooleanProps) => {
const { label, name } = option;
const onChange = useCallback(
(event: JSX.TargetedMouseEvent<HTMLInputElement>) =>
setValue(name, event.currentTarget.checked)
, [name, setValue]
setValue(name, event.currentTarget.checked),
[name, setValue]
);
return (
<li>
<input
type="checkbox"
checked={value}
onChange={onChange}
/>
<input type="checkbox" checked={value} onChange={onChange} />
<label>{label}</label>
</li>
);
@ -84,9 +80,7 @@ const Select = ({ option, value, setValue }: SelectProps) => {
return (
<li>
<select onChange={onChange}>
{option.values.map(makeOption)}
</select>
<select onChange={onChange}>{option.values.map(makeOption)}</select>
<label>{label}</label>
</li>
);
@ -110,9 +104,12 @@ export interface OptionsModalProps {
export const OptionsModal = ({ isOpen, onClose }: OptionsModalProps) => {
const options = useContext(OptionsContext);
const sections = options.getSections();
const setValue = useCallback((name: string, value: string | boolean) => {
options.setOption(name, value);
}, [options]);
const setValue = useCallback(
(name: string, value: string | boolean) => {
options.setOption(name, value);
},
[options]
);
const makeOption = (option: Option) => {
const { name, type } = option;
@ -143,9 +140,7 @@ export const OptionsModal = ({ isOpen, onClose }: OptionsModalProps) => {
return (
<>
<h3>{section.name}</h3>
<ul>
{section.options.map(makeOption)}
</ul>
<ul>{section.options.map(makeOption)}</ul>
</>
);
};

View File

@ -1,5 +1,11 @@
import { h, Fragment } from 'preact';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import Apple2IO, { slot } from 'js/apple2io';
import Parallel, { ParallelOptions } from 'js/cards/parallel';
import { Modal, ModalContent, ModalFooter } from './Modal';
@ -20,32 +26,35 @@ export const Printer = ({ io, slot }: PrinterProps) => {
const rawLength = useRef(0);
const [href, setHref] = useState('');
const cbs = useMemo<ParallelOptions>(() => ({
putChar: (val: byte) => {
const ascii = val & 0x7f;
const visible = val >= 0x20;
const char = String.fromCharCode(ascii);
const cbs = useMemo<ParallelOptions>(
() => ({
putChar: (val: byte) => {
const ascii = val & 0x7f;
const visible = val >= 0x20;
const char = String.fromCharCode(ascii);
if (char === '\r') {
// Skip for once
} else if (char === '\t') {
// possibly not right due to tab stops
setContent((content) => content += ' ');
} else if (ascii === 0x04) {
setContent((content) => content = content.slice(0, -1));
return;
} else if (visible) {
setContent((content) => content += char);
}
if (char === '\r') {
// Skip for once
} else if (char === '\t') {
// possibly not right due to tab stops
setContent((content) => (content += ' '));
} else if (ascii === 0x04) {
setContent((content) => (content = content.slice(0, -1)));
return;
} else if (visible) {
setContent((content) => (content += char));
}
raw.current[rawLength.current++] = val;
if (rawLength.current > raw.current.length) {
const newRaw = new Uint8Array(raw.current.length * 2);
newRaw.set(raw.current);
raw.current = newRaw;
}
}
}), [rawLength]);
raw.current[rawLength.current++] = val;
if (rawLength.current > raw.current.length) {
const newRaw = new Uint8Array(raw.current.length * 2);
newRaw.set(raw.current);
raw.current = newRaw;
}
},
}),
[rawLength]
);
useEffect(() => {
if (io) {
@ -56,10 +65,9 @@ export const Printer = ({ io, slot }: PrinterProps) => {
useEffect(() => {
if (isOpen) {
const blob = new Blob(
[raw.current.slice(0, rawLength.current)],
{ type: 'application/octet-stream' }
);
const blob = new Blob([raw.current.slice(0, rawLength.current)], {
type: 'application/octet-stream',
});
const href = window.URL.createObjectURL(blob);
setHref(href);
}
@ -80,7 +88,7 @@ export const Printer = ({ io, slot }: PrinterProps) => {
<>
<Modal isOpen={isOpen} onClose={onClose} title="Printer">
<ModalContent>
<pre className={styles.printer} tabIndex={-1} >
<pre className={styles.printer} tabIndex={-1}>
{content}
</pre>
</ModalContent>

View File

@ -17,7 +17,9 @@ export const ProgressModal = ({ title, current, total }: ErrorProps) => {
<div className={styles.progressContainer}>
<div
className={styles.progressBar}
style={{ width: Math.floor(320 * (current / total)) }}
style={{
width: Math.floor(320 * (current / total)),
}}
/>
</div>
</ModalContent>

View File

@ -9,11 +9,7 @@ export interface TabProps {
}
export const Tab = ({ children }: TabProps) => {
return (
<div>
{children}
</div>
);
return <div>{children}</div>;
};
interface TabWrapperProps {
@ -24,7 +20,10 @@ interface TabWrapperProps {
const TabWrapper = ({ children, onClick, selected }: TabWrapperProps) => {
return (
<div onClick={onClick} className={cs(styles.tab, { [styles.selected]: selected })}>
<div
onClick={onClick}
className={cs(styles.tab, { [styles.selected]: selected })}
>
{children}
</div>
);
@ -38,10 +37,13 @@ export interface TabsProps {
export const Tabs = ({ children, setSelected }: TabsProps) => {
const [innerSelected, setInnerSelected] = useState(0);
const innerSetSelected = useCallback((idx: number) => {
setSelected(idx);
setInnerSelected(idx);
}, [setSelected]);
const innerSetSelected = useCallback(
(idx: number) => {
setSelected(idx);
setInnerSelected(idx);
},
[setSelected]
);
if (!Array.isArray(children)) {
return null;
@ -49,7 +51,7 @@ export const Tabs = ({ children, setSelected }: TabsProps) => {
return (
<div className={styles.tabs}>
{children.map((child, idx) =>
{children.map((child, idx) => (
<TabWrapper
key={idx}
onClick={() => innerSetSelected(idx)}
@ -57,7 +59,7 @@ export const Tabs = ({ children, setSelected }: TabsProps) => {
>
{child}
</TabWrapper>
)}
))}
</div>
);
};

View File

@ -9,7 +9,7 @@ export interface VideotermProps {
slot: slot;
}
export const Videoterm = ({ io, slot }: VideotermProps ) => {
export const Videoterm = ({ io, slot }: VideotermProps) => {
useEffect(() => {
if (io) {
const videoterm = new VideotermImpl();

View File

@ -6,7 +6,7 @@
.diskLight {
margin: 5px;
background-image: url("../../../css/green-off-16.png");
background-image: url('../../../css/green-off-16.png');
background-size: 16px 16px;
flex-shrink: 0;
width: 16px;
@ -14,7 +14,7 @@
}
.diskLight.on {
background-image: url("../../../css/green-on-16.png");
background-image: url('../../../css/green-on-16.png');
}
.diskLabel {
@ -29,12 +29,12 @@
user-select: none;
}
@media only screen and (min-resolution: 1.25dppx) {
@media only screen and (resolution >= 1.25dppx) {
.diskLight {
background-image: url("../../../css/green-off-32.png");
background-image: url('../../../css/green-off-32.png');
}
.diskLight.on {
background-image: url("../../../css/green-on-32.png");
background-image: url('../../../css/green-on-32.png');
}
}

View File

@ -2,13 +2,13 @@
font-size: 14px;
font-family: sans-serif;
a[role="button"] {
a[role='button'] {
text-decoration: none;
}
button,
a[role="button"],
input[type="file"]::file-selector-button {
a[role='button'],
input[type='file']::file-selector-button {
background: #44372c;
color: #fff;
padding: 2px 8px;
@ -24,26 +24,26 @@
}
button:hover,
a[role="button"]:hover,
input[type="file"]::file-selector-button {
a[role='button']:hover,
input[type='file']::file-selector-button {
background-color: #55473d;
border: 1px outset #66594e;
}
button:active,
a[role="button"]:active,
input[type="file"]::file-selector-button {
a[role='button']:active,
input[type='file']::file-selector-button {
background-color: #22150a;
border: 1px outset #44372c;
}
button:focus,
a[role="button"]:focus,
input[type="file"]::file-selector-button {
a[role='button']:focus,
input[type='file']::file-selector-button {
outline: none;
}
input[type="checkbox"] {
input[type='checkbox'] {
appearance: none;
background-color: #65594d;
border: 1px inset #65594d;
@ -54,14 +54,14 @@
position: relative;
}
input[type="checkbox"]:checked {
input[type='checkbox']:checked {
background-color: #65594d;
border: 1px inset #65594d;
color: #0d0;
}
input[type="checkbox"]:checked::after {
content: "\2716";
input[type='checkbox']:checked::after {
content: '\2716';
font-size: 12px;
position: absolute;
top: 0;

View File

@ -8,7 +8,9 @@
/* border: 5px outset #66594E; */
border-radius: 3px;
color: white;
font: 9px Helvetica, sans-serif;
font:
9px Helvetica,
sans-serif;
height: 36px;
padding: 0;
margin-left: 10px;

View File

@ -6,7 +6,7 @@
.diskLight {
margin: 5px;
background-image: url("../../../css/red-off-16.png");
background-image: url('../../../css/red-off-16.png');
background-size: 16px 16px;
flex-shrink: 0;
width: 16px;
@ -14,7 +14,7 @@
}
.diskLight.on {
background-image: url("../../../css/red-on-16.png");
background-image: url('../../../css/red-on-16.png');
}
.diskLabel {
@ -29,12 +29,12 @@
user-select: none;
}
@media only screen and (min-resolution: 1.25dppx) {
@media only screen and (resolution >= 1.25dppx) {
.diskLight {
background-image: url("../../../css/red-off-32.png");
background-image: url('../../../css/red-off-32.png');
}
.diskLight.on {
background-image: url("../../../css/red-on-32.png");
background-image: url('../../../css/red-on-32.png');
}
}

View File

@ -26,7 +26,7 @@
margin: 0;
padding: 3px 0 0 10;
color: black;
font-family: "Adobe Garamond Pro", Garamond, Times, serif;
font-family: 'Adobe Garamond Pro', Garamond, Times, serif;
font-size: 13px;
font-weight: normal;
user-select: none;

View File

@ -177,7 +177,7 @@
}
.key-OPEN_APPLE div {
background-image: url("../../../img/open-apple24.png");
background-image: url('../../../img/open-apple24.png');
width: 24px;
height: 24px;
bottom: 1px;
@ -185,7 +185,7 @@
}
.key-CLOSED_APPLE div {
background-image: url("../../../img/closed-apple24.png");
background-image: url('../../../img/closed-apple24.png');
width: 24px;
height: 24px;
bottom: 1px;
@ -201,9 +201,9 @@
}
.key-OPEN_APPLE.active div {
background-image: url("../../../img/open-apple24-green.png");
background-image: url('../../../img/open-apple24-green.png');
}
.key-CLOSED_APPLE.active div {
background-image: url("../../../img/closed-apple24-green.png");
background-image: url('../../../img/closed-apple24-green.png');
}

View File

@ -1,9 +1,6 @@
.modalOverlay {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
@ -63,7 +60,7 @@
text-align: right;
user-select: none;
a[role="button"],
a[role='button'],
button {
margin: 0 0 0 5px;
min-width: 75px;

View File

@ -1,5 +1,5 @@
:global(.mono) {
filter: url("#green");
filter: url('#green');
}
.display {
@ -14,10 +14,7 @@
padding: 0;
border: 0;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
inset: 0;
justify-content: center;
align-items: center;
margin: auto !important;
@ -48,31 +45,26 @@
:global(.scanlines)::after {
display: block;
pointer-events: none;
background-image:
repeating-linear-gradient(
to bottom,
transparent 0,
transparent 1px,
rgb(0 0 0 / 50%) 1px,
rgb(0 0 0 / 50%) 2px
);
content: "";
background-image: repeating-linear-gradient(
to bottom,
transparent 0,
transparent 1px,
rgb(0 0 0 / 50%) 1px,
rgb(0 0 0 / 50%) 2px
);
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
inset: 0;
}
:global(.full-page) :global(.scanlines)::after {
background-image:
repeating-linear-gradient(
to bottom,
transparent 0,
transparent 0.25vh,
rgb(0 0 0 / 50%) 0.25vh,
rgb(0 0 0 / 50%) 0.5vh
);
background-image: repeating-linear-gradient(
to bottom,
transparent 0,
transparent 0.25vh,
rgb(0 0 0 / 50%) 0.25vh,
rgb(0 0 0 / 50%) 0.5vh
);
}
.screen {

View File

@ -42,12 +42,23 @@ const formatArray = (value: unknown): string => {
const Variable = ({ variable }: { variable: ApplesoftVariable }) => {
const { name, type, sizes, value } = variable;
const isArray = !!sizes;
const arrayStr = isArray ? `(${sizes.map((size) => size - 1).join(',')})` : '';
const arrayStr = isArray
? `(${sizes.map((size) => size - 1).join(',')})`
: '';
return (
<tr>
<td>{name}{TYPE_SYMBOL[type]}{arrayStr}</td>
<td>{TYPE_NAME[type]}{isArray ? ' Array' : ''}</td>
<td><pre tabIndex={-1}>{isArray ? formatArray(value) : value}</pre></td>
<td>
{name}
{TYPE_SYMBOL[type]}
{arrayStr}
</td>
<td>
{TYPE_NAME[type]}
{isArray ? ' Array' : ''}
</td>
<td>
<pre tabIndex={-1}>{isArray ? formatArray(value) : value}</pre>
</td>
</tr>
);
};
@ -57,7 +68,7 @@ export const Applesoft = ({ apple2 }: ApplesoftProps) => {
const [data, setData] = useState<ApplesoftData>({
listing: '',
variables: [],
internals: {}
internals: {},
});
const [heap, setHeap] = useState<ApplesoftHeap>();
const cpu = apple2?.getCPU();
@ -72,18 +83,19 @@ export const Applesoft = ({ apple2 }: ApplesoftProps) => {
const animate = useCallback(() => {
if (cpu && heap) {
try {
const decompiler = ApplesoftDecompiler.decompilerFromMemory(cpu);
const decompiler =
ApplesoftDecompiler.decompilerFromMemory(cpu);
setData({
variables: heap.dumpVariables(),
internals: heap.dumpInternals(),
listing: decompiler.decompile()
listing: decompiler.decompile(),
});
} catch (error) {
if (error instanceof Error) {
setData({
variables: [],
internals: {},
listing: error.message
listing: error.message,
});
} else {
throw error;
@ -103,7 +115,9 @@ export const Applesoft = ({ apple2 }: ApplesoftProps) => {
return (
<div className={styles.column}>
<span className={debuggerStyles.subHeading}>Listing</span>
<pre className={styles.listing} tabIndex={-1}>{listing}</pre>
<pre className={styles.listing} tabIndex={-1}>
{listing}
</pre>
<span className={debuggerStyles.subHeading}>Variables</span>
<div className={styles.variables}>
<table>
@ -112,7 +126,9 @@ export const Applesoft = ({ apple2 }: ApplesoftProps) => {
<th>Type</th>
<th>Value</th>
</tr>
{variables.map((variable, idx) => <Variable key={idx} variable={variable} />)}
{variables.map((variable, idx) => (
<Variable key={idx} variable={variable} />
))}
</table>
</div>
<span className={debuggerStyles.subHeading}>Internals</span>

View File

@ -60,7 +60,7 @@ export const CPU = ({ apple2 }: CPUProps) => {
stack: debug.getStack(38),
trace: debug.getTrace(16),
zeroPage: debug.dumpPage(0),
memory: debug.dumpPage(parseInt(memoryPage, 16) || 0)
memory: debug.dumpPage(parseInt(memoryPage, 16) || 0),
});
}
animationRef.current = requestAnimationFrame(animate);
@ -83,46 +83,51 @@ export const CPU = ({ apple2 }: CPUProps) => {
debug?.step();
}, [debug]);
const doLoadAddress = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
setLoadAddress(event.currentTarget.value);
}, []);
const doRunCheck = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
setRun(event.currentTarget.checked);
}, []);
const doLoadAddress = useCallback(
(event: JSX.TargetedEvent<HTMLInputElement>) => {
setLoadAddress(event.currentTarget.value);
},
[]
);
const doRunCheck = useCallback(
(event: JSX.TargetedEvent<HTMLInputElement>) => {
setRun(event.currentTarget.checked);
},
[]
);
const doMemoryPage = useCallback((event: JSX.TargetedEvent<HTMLInputElement>) => {
setMemoryPage(event.currentTarget.value);
}, []);
const doMemoryPage = useCallback(
(event: JSX.TargetedEvent<HTMLInputElement>) => {
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 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);
}
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]);
await loadLocalBinaryFile(file, atAddress, debug);
setLoadAddress(toHex(atAddress, 4));
if (run) {
debug?.runAt(atAddress);
}
});
}
},
[debug, loadAddress, run]
);
const {
memory,
registers,
running,
stack,
trace,
zeroPage
} = data;
const { memory, registers, running, stack, trace, zeroPage } = data;
const memoryPageValid = VALID_PAGE.test(memoryPage);
const loadAddressValid = VALID_ADDRESS.test(loadAddress);
@ -156,9 +161,7 @@ export const CPU = ({ apple2 }: CPUProps) => {
<div className={debuggerStyles.row}>
<div className={debuggerStyles.column}>
<span className={debuggerStyles.subHeading}>Registers</span>
<pre tabIndex={-1}>
{registers}
</pre>
<pre tabIndex={-1}>{registers}</pre>
<span className={debuggerStyles.subHeading}>Trace</span>
<pre className={styles.trace} tabIndex={-1}>
{trace}
@ -177,7 +180,9 @@ export const CPU = ({ apple2 }: CPUProps) => {
</div>
<div>
<hr />
<span className={debuggerStyles.subHeading}>Memory Page: $ </span>
<span className={debuggerStyles.subHeading}>
Memory Page: ${' '}
</span>
<input
value={memoryPage}
onChange={doMemoryPage}
@ -199,9 +204,9 @@ export const CPU = ({ apple2 }: CPUProps) => {
onChange={doLoadAddress}
className={cs({ [styles.invalid]: !loadAddressValid })}
/>
{loadAddressValid ? null : ERROR_ICON}
{' '}
<input type="checkbox" checked={run} onChange={doRunCheck} />Run
{loadAddressValid ? null : ERROR_ICON}{' '}
<input type="checkbox" checked={run} onChange={doRunCheck} />
Run
<div className={styles.fileChooser}>
<FileChooser onChange={doChooseFile} />
</div>

View File

@ -2,7 +2,15 @@ import { h, Fragment } from 'preact';
import { useMemo } from 'preact/hooks';
import cs from 'classnames';
import { Apple2 as Apple2Impl } from 'js/apple2';
import { BlockDisk, DiskFormat, DriveNumber, FloppyDisk, isBlockDiskFormat, isNibbleDisk, MassStorage } from 'js/formats/types';
import {
BlockDisk,
DiskFormat,
DriveNumber,
FloppyDisk,
isBlockDiskFormat,
isNibbleDisk,
MassStorage,
} from 'js/formats/types';
import { slot } from 'js/apple2io';
import DiskII from 'js/cards/disk2';
import SmartPort from 'js/cards/smartport';
@ -18,7 +26,11 @@ import { toHex } from 'js/util';
import styles from './css/Disks.module.scss';
import debuggerStyles from './css/Debugger.module.scss';
import { useCallback, useState } from 'preact/hooks';
import { DOS33, FileEntry as DOSEntry, isMaybeDOS33 } from 'js/formats/dos/dos33';
import {
DOS33,
FileEntry as DOSEntry,
isMaybeDOS33,
} from 'js/formats/dos/dos33';
import createDiskFromDOS from 'js/formats/do';
import { FileData, FileViewer } from './FileViewer';
@ -29,7 +41,10 @@ import { FileData, FileViewer } from './FileViewer';
* @returns Short string date
*/
const formatDate = (date: Date) => {
return date.toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'short' });
return date.toLocaleString(undefined, {
dateStyle: 'short',
timeStyle: 'short',
});
};
/**
@ -39,7 +54,7 @@ const formatDate = (date: Date) => {
* @returns true if is BlockDisk
*/
function isBlockDisk(disk: FloppyDisk | BlockDisk): disk is BlockDisk {
return !!((disk as BlockDisk).blocks);
return !!(disk as BlockDisk).blocks;
}
/**
@ -80,14 +95,17 @@ const FileListing = ({ depth, fileEntry, setFileData }: FileListingProps) => {
onClick={doSetFileData}
>
{'| '.repeat(depth)}
{deleted ?
<i className="fas fa-file-circle-xmark" /> :
{deleted ? (
<i className="fas fa-file-circle-xmark" />
) : (
<i className="fas fa-file" />
}
{' '}
)}{' '}
{fileEntry.name}
</td>
<td>{FILE_TYPES[fileEntry.fileType] ?? `$${toHex(fileEntry.fileType)}`}</td>
<td>
{FILE_TYPES[fileEntry.fileType] ??
`$${toHex(fileEntry.fileType)}`}
</td>
<td>{`$${toHex(fileEntry.auxType, 4)}`}</td>
<td>{fileEntry.blocksUsed}</td>
<td>{formatDate(fileEntry.creation)}</td>
@ -115,7 +133,12 @@ interface DirectoryListingProps {
* @param dirEntry Current directory entry to display
* @returns DirectoryListing component
*/
const DirectoryListing = ({ volume, depth, dirEntry, setFileData }: DirectoryListingProps) => {
const DirectoryListing = ({
volume,
depth,
dirEntry,
setFileData,
}: DirectoryListingProps) => {
const [open, setOpen] = useState(depth === 0);
return (
<>
@ -126,8 +149,12 @@ const DirectoryListing = ({ volume, depth, dirEntry, setFileData }: DirectoryLis
title={dirEntry.name}
>
{'| '.repeat(depth)}
<i className={cs('fas', { 'fa-folder-open': open, 'fa-folder-closed': !open })} />
{' '}
<i
className={cs('fas', {
'fa-folder-open': open,
'fa-folder-closed': !open,
})}
/>{' '}
{dirEntry.name}
</td>
<td></td>
@ -136,26 +163,31 @@ const DirectoryListing = ({ volume, depth, dirEntry, setFileData }: DirectoryLis
<td>{formatDate(dirEntry.creation)}</td>
<td></td>
</tr>
{open && dirEntry.entries.map((fileEntry, idx) => {
if (fileEntry.storageType === STORAGE_TYPES.DIRECTORY) {
const dirEntry = new Directory(volume, fileEntry);
return <DirectoryListing
key={idx}
depth={depth + 1}
volume={volume}
dirEntry={dirEntry}
setFileData={setFileData}
/>;
} else {
return <FileListing
key={idx}
depth={depth + 1}
volume={volume}
fileEntry={fileEntry}
setFileData={setFileData}
/>;
}
})}
{open &&
dirEntry.entries.map((fileEntry, idx) => {
if (fileEntry.storageType === STORAGE_TYPES.DIRECTORY) {
const dirEntry = new Directory(volume, fileEntry);
return (
<DirectoryListing
key={idx}
depth={depth + 1}
volume={volume}
dirEntry={dirEntry}
setFileData={setFileData}
/>
);
} else {
return (
<FileListing
key={idx}
depth={depth + 1}
volume={volume}
fileEntry={fileEntry}
setFileData={setFileData}
/>
);
}
})}
</>
);
};
@ -187,9 +219,12 @@ const CatalogEntry = ({ dos, fileEntry, setFileData }: CatalogEntryProps) => {
return (
<tr onClick={doSetFileData}>
<td className={cs(styles.filename, { [styles.deleted]: fileEntry.deleted })}>
{fileEntry.locked && <i className="fas fa-lock" />}
{' '}
<td
className={cs(styles.filename, {
[styles.deleted]: fileEntry.deleted,
})}
>
{fileEntry.locked && <i className="fas fa-lock" />}{' '}
{fileEntry.name}
</td>
<td>{fileEntry.type}</td>
@ -303,12 +338,20 @@ const DiskInfo = ({ massStorage, driveNo, setFileData }: DiskInfoProps) => {
<table>
<thead>
<tr>
<th className={styles.filename}>Filename</th>
<th className={styles.filename}>
Filename
</th>
<th className={styles.type}>Type</th>
<th className={styles.aux}>Aux</th>
<th className={styles.blocks}>Blocks</th>
<th className={styles.created}>Created</th>
<th className={styles.modified}>Modified</th>
<th className={styles.blocks}>
Blocks
</th>
<th className={styles.created}>
Created
</th>
<th className={styles.modified}>
Modified
</th>
</tr>
</thead>
<tbody>
@ -321,9 +364,13 @@ const DiskInfo = ({ massStorage, driveNo, setFileData }: DiskInfoProps) => {
</tbody>
<tfoot>
<tr>
<td colSpan={1}>Blocks Free: {freeCount}</td>
<td colSpan={1}>
Blocks Free: {freeCount}
</td>
<td colSpan={3}>Used: {usedCount}</td>
<td colSpan={2}>Total: {totalBlocks}</td>
<td colSpan={2}>
Total: {totalBlocks}
</td>
</tr>
</tfoot>
</table>
@ -337,7 +384,9 @@ const DiskInfo = ({ massStorage, driveNo, setFileData }: DiskInfoProps) => {
<table>
<thead>
<tr>
<th className={styles.filename}>Filename</th>
<th className={styles.filename}>
Filename
</th>
<th className={styles.type}>Type</th>
<th className={styles.sectors}>Sectors</th>
<th></th>
@ -409,11 +458,19 @@ export const Disks = ({ apple2 }: DisksProps) => {
<div className={debuggerStyles.subHeading}>
{card.constructor.name} - 1
</div>
<DiskInfo massStorage={card} driveNo={1} setFileData={setFileData} />
<DiskInfo
massStorage={card}
driveNo={1}
setFileData={setFileData}
/>
<div className={debuggerStyles.subHeading}>
{card.constructor.name} - 2
</div>
<DiskInfo massStorage={card} driveNo={2} setFileData={setFileData} />
<DiskInfo
massStorage={card}
driveNo={2}
setFileData={setFileData}
/>
</div>
))}
<FileViewer fileData={fileData} onClose={onClose} />

View File

@ -56,7 +56,14 @@ const HiresPreview = ({ binary }: { binary: Uint8Array }) => {
vm.blit();
}
return <canvas ref={canvasRef} width={560} height={384} className={styles.hiresPreview} />;
return (
<canvas
ref={canvasRef}
width={560}
height={384}
className={styles.hiresPreview}
/>
);
};
/**
@ -96,7 +103,14 @@ const DoubleHiresPreview = ({ binary }: { binary: Uint8Array }) => {
vm.blit();
}
return <canvas ref={canvasRef} width={560} height={384} className={styles.hiresPreview} />;
return (
<canvas
ref={canvasRef}
width={560}
height={384}
className={styles.hiresPreview}
/>
);
};
/**
@ -114,16 +128,14 @@ export const FileViewer = ({ fileData, onClose }: FileViewerProps) => {
useEffect(() => {
if (fileData) {
const { binary, text } = fileData;
const binaryBlob = new Blob(
[binary],
{ type: 'application/octet-stream' }
);
const binaryBlob = new Blob([binary], {
type: 'application/octet-stream',
});
const binaryHref = window.URL.createObjectURL(binaryBlob);
setBinaryHref(binaryHref);
const textBlob = new Blob(
[text],
{ type: 'application/octet-stream' }
);
const textBlob = new Blob([text], {
type: 'application/octet-stream',
});
const textHref = window.URL.createObjectURL(textBlob);
setTextHref(textHref);
}
@ -142,7 +154,7 @@ export const FileViewer = ({ fileData, onClose }: FileViewerProps) => {
<div className={styles.fileViewer}>
<HiresPreview binary={binary} />
<DoubleHiresPreview binary={binary} />
<pre className={styles.textViewer} tabIndex={-1} >
<pre className={styles.textViewer} tabIndex={-1}>
{text}
</pre>
</div>

View File

@ -54,8 +54,8 @@ interface Banks {
* @returns LC read/write state
*/
const calcLC = (mmu: MMU, altzp: boolean) => {
const read = mmu.readbsr && (mmu.altzp === altzp);
const write = mmu.writebsr && (mmu.altzp === altzp);
const read = mmu.readbsr && mmu.altzp === altzp;
const write = mmu.writebsr && mmu.altzp === altzp;
return {
read,
write,
@ -170,7 +170,7 @@ const calcLanguageCard = (card: LanguageCard): LC => {
rom: {
read: !card.readbsr,
write: !card.writebsr,
}
},
};
};
@ -207,16 +207,14 @@ interface LanguageCardMapProps {
const LanguageCardMap = ({ lc, children }: LanguageCardMapProps) => {
return (
<div className={cs(styles.bank)}>
<div className={cs(styles.lc, rw(lc))}>
{children} LC
</div>
<div className={cs(styles.lc, rw(lc))}>{children} LC</div>
<div className={styles.lcbanks}>
<div className={cs(styles.lcbank, styles.lcbank0, rw(lc.bank0))}>
<div
className={cs(styles.lcbank, styles.lcbank0, rw(lc.bank0))}
>
Bank 0
</div>
<div className={cs(styles.lcbank, rw(lc.bank1))}>
Bank 1
</div>
<div className={cs(styles.lcbank, rw(lc.bank1))}>Bank 1</div>
</div>
</div>
);
@ -238,10 +236,14 @@ const Legend = () => {
<div className={cs(styles.write, styles.legend)}> </div> Write
</div>
<div>
<div className={cs(styles.write, styles.read, styles.legend)}> </div> Read/Write
<div className={cs(styles.write, styles.read, styles.legend)}>