├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .github
└── workflows
│ └── nodejs.yml
├── .gitignore
├── .gitmodules
├── .idx
└── dev.nix
├── .prettierignore
├── .prettierrc
├── .stylelintignore
├── .stylelintrc.json
├── LICENSE
├── README.md
├── apple2js.html
├── apple2jse.html
├── asm
├── .gitignore
├── mouse.s
└── smartport.s
├── babel.config.js
├── bin
├── dsk2json
└── index
├── css
├── apple2.css
├── green-off-16.png
├── green-off-32.png
├── green-on-16.png
├── green-on-32.png
├── red-off-16.png
├── red-off-32.png
├── red-on-16.png
└── red-on-32.png
├── img
├── badge.png
├── badge2e.png
├── closed-apple24-green.png
├── closed-apple24.png
├── logoicon.png
├── open-apple24-green.png
├── open-apple24.png
├── webapp-ipad.png
└── webapp-iphone.png
├── index.html
├── jest.config.js
├── js
├── apple2.ts
├── apple2io.ts
├── applesoft
│ ├── compiler.ts
│ ├── decompiler.ts
│ ├── heap.ts
│ ├── tokens.ts
│ └── zeropage.ts
├── base64.ts
├── canvas.ts
├── cards
│ ├── cffa.ts
│ ├── disk2.ts
│ ├── drivers
│ │ ├── BaseDiskDriver.ts
│ │ ├── EmptyDriver.ts
│ │ ├── NibbleDiskDriver.ts
│ │ ├── WozDiskDriver.ts
│ │ └── types.ts
│ ├── langcard.ts
│ ├── mouse.ts
│ ├── nsc.ts
│ ├── parallel.ts
│ ├── ramfactor.ts
│ ├── smartport.ts
│ ├── thunderclock.ts
│ └── videoterm.ts
├── components
│ ├── App.tsx
│ ├── Apple2.tsx
│ ├── AudioControl.tsx
│ ├── BlockDisk.tsx
│ ├── BlockFileModal.tsx
│ ├── CPUMeter.tsx
│ ├── Cassette.tsx
│ ├── ClipboardCopy.tsx
│ ├── ClipboardPaste.tsx
│ ├── ControlButton.tsx
│ ├── ControlStrip.tsx
│ ├── DiskDragTarget.tsx
│ ├── DiskII.tsx
│ ├── DownloadModal.tsx
│ ├── Drives.tsx
│ ├── ErrorModal.tsx
│ ├── FileChooser.tsx
│ ├── FileModal.tsx
│ ├── Header.tsx
│ ├── Inset.tsx
│ ├── Keyboard.tsx
│ ├── LanguageCard.tsx
│ ├── Modal.tsx
│ ├── Mouse.tsx
│ ├── OptionsContext.tsx
│ ├── OptionsModal.tsx
│ ├── Printer.tsx
│ ├── ProgressModal.tsx
│ ├── Screen.tsx
│ ├── Slinky.tsx
│ ├── Tabs.tsx
│ ├── ThunderClock.tsx
│ ├── Videoterm.tsx
│ ├── css
│ │ ├── App.module.scss
│ │ ├── Apple2.module.scss
│ │ ├── BlockDisk.module.scss
│ │ ├── BlockFileModal.module.scss
│ │ ├── CPUMeter.module.scss
│ │ ├── Components.module.scss
│ │ ├── ControlButton.module.scss
│ │ ├── ControlStrip.module.scss
│ │ ├── DiskII.module.scss
│ │ ├── DownloadModal.module.scss
│ │ ├── Drives.module.scss
│ │ ├── ErrorModal.module.scss
│ │ ├── FileModal.module.scss
│ │ ├── Header.module.scss
│ │ ├── Inset.module.scss
│ │ ├── Keyboard.module.scss
│ │ ├── Modal.module.scss
│ │ ├── OptionsModal.module.scss
│ │ ├── Printer.module.scss
│ │ ├── ProgressModal.module.scss
│ │ ├── Screen.module.scss
│ │ └── Tabs.module.scss
│ ├── debugger
│ │ ├── Applesoft.tsx
│ │ ├── CPU.tsx
│ │ ├── Debugger.tsx
│ │ ├── Disks.tsx
│ │ ├── FileViewer.tsx
│ │ ├── Memory.tsx
│ │ ├── VideoModes.tsx
│ │ └── css
│ │ │ ├── Applesoft.module.scss
│ │ │ ├── CPU.module.scss
│ │ │ ├── Debugger.module.scss
│ │ │ ├── Disks.module.scss
│ │ │ ├── FileViewer.module.scss
│ │ │ ├── Memory.module.scss
│ │ │ └── VideoModes.module.scss
│ ├── hooks
│ │ ├── useHash.ts
│ │ ├── useHotKey.ts
│ │ └── usePrefs.ts
│ └── util
│ │ ├── files.ts
│ │ ├── keyboard.ts
│ │ ├── promises.ts
│ │ └── systems.ts
├── entry.tsx
├── entry2.ts
├── entry2e.ts
├── formats
│ ├── 2mg.ts
│ ├── block.ts
│ ├── create_disk.ts
│ ├── d13.ts
│ ├── do.ts
│ ├── dos
│ │ └── dos33.ts
│ ├── format_utils.ts
│ ├── http_block_disk.ts
│ ├── nib.ts
│ ├── po.ts
│ ├── prodos
│ │ ├── base_file.ts
│ │ ├── bit_map.ts
│ │ ├── constants.ts
│ │ ├── directory.ts
│ │ ├── file_entry.ts
│ │ ├── index.ts
│ │ ├── prodos_volume.ts
│ │ ├── sapling_file.ts
│ │ ├── seedling_file.ts
│ │ ├── tree_file.ts
│ │ ├── types.ts
│ │ ├── utils.ts
│ │ └── vdh.ts
│ ├── types.ts
│ └── woz.ts
├── gl.ts
├── intbasic
│ └── decompiler.ts
├── main2.ts
├── main2e.ts
├── mmu.ts
├── options.ts
├── prefs.ts
├── ram.ts
├── roms
│ ├── cards
│ │ ├── cffa.ts
│ │ ├── disk2.ts
│ │ ├── mouse.ts
│ │ ├── parallel.ts
│ │ ├── ramfactor.ts
│ │ ├── smartport.ts
│ │ ├── thunderclock.ts
│ │ └── videoterm.ts
│ ├── character
│ │ ├── apple2_char.ts
│ │ ├── apple2e_char.ts
│ │ ├── apple2enh_char.ts
│ │ ├── apple2j_char.ts
│ │ ├── apple2lc_char.ts
│ │ ├── pigfont_char.ts
│ │ ├── pravetz82_char.ts
│ │ └── rmfont_char.ts
│ ├── rom.ts
│ └── system
│ │ ├── apple2e.ts
│ │ ├── apple2enh.ts
│ │ ├── apple2ex.ts
│ │ ├── apple2j.ts
│ │ ├── fpbasic.ts
│ │ ├── intbasic.ts
│ │ ├── original.ts
│ │ └── pravetz82.ts
├── symbols.ts
├── types.ts
├── ui
│ ├── apple2.ts
│ ├── audio.ts
│ ├── audio_worker.ts
│ ├── drive_lights.ts
│ ├── gamepad.ts
│ ├── joystick.ts
│ ├── keyboard.ts
│ ├── mouse.ts
│ ├── options_modal.ts
│ ├── printer.ts
│ ├── screen.ts
│ ├── system.ts
│ ├── tape.ts
│ └── types.ts
├── util.ts
└── videomodes.ts
├── json
└── disks
│ ├── audit.json
│ ├── blank_dos33.json
│ ├── blank_prodos.json
│ ├── dos33master.json
│ ├── index.js
│ ├── index.json
│ └── prodos.json
├── package-lock.json
├── package.json
├── test
├── components
│ ├── ErrorModal.spec.tsx
│ ├── FileChooser.spec.tsx
│ ├── Modal.spec.tsx
│ └── util
│ │ └── promises.spec.ts
├── env
│ ├── jsdom-with-backdoors.d.ts
│ └── jsdom-with-backdoors.js
├── jest-setup.ts
├── js
│ ├── __image_snapshots__
│ │ ├── canvas-test-ts-canvas-hires-page-double-lores-renders-1-snap.png
│ │ ├── canvas-test-ts-canvas-hires-page-double-lores-renders-mono-1-snap.png
│ │ ├── canvas-test-ts-canvas-hires-page-hires-renders-1-snap.png
│ │ ├── canvas-test-ts-canvas-hires-page-hires-renders-mono-1-snap.png
│ │ ├── canvas-test-ts-canvas-lores-page-graphics-mode-double-lores-renders-1-snap.png
│ │ ├── canvas-test-ts-canvas-lores-page-graphics-mode-double-lores-renders-mixed-1-snap.png
│ │ ├── canvas-test-ts-canvas-lores-page-graphics-mode-double-lores-renders-mono-1-snap.png
│ │ ├── canvas-test-ts-canvas-lores-page-graphics-mode-lores-renders-1-snap.png
│ │ ├── canvas-test-ts-canvas-lores-page-graphics-mode-lores-renders-mixed-1-snap.png
│ │ ├── canvas-test-ts-canvas-lores-page-graphics-mode-lores-renders-mono-1-snap.png
│ │ ├── canvas-test-ts-canvas-lores-page-text-mode-40-column-renders-1-snap.png
│ │ ├── canvas-test-ts-canvas-lores-page-text-mode-40-column-renders-alt-chars-1-snap.png
│ │ ├── canvas-test-ts-canvas-lores-page-text-mode-80-column-renders-1-snap.png
│ │ ├── canvas-test-ts-canvas-lores-page-text-mode-80-column-renders-alt-chars-1-snap.png
│ │ ├── gl-test-ts-gl-hires-page-double-lores-renders-1-snap.png
│ │ ├── gl-test-ts-gl-hires-page-hires-renders-1-snap.png
│ │ ├── gl-test-ts-gl-lores-page-graphics-mode-double-lores-renders-1-snap.png
│ │ ├── gl-test-ts-gl-lores-page-graphics-mode-double-lores-renders-mixed-1-snap.png
│ │ ├── gl-test-ts-gl-lores-page-graphics-mode-lores-renders-1-snap.png
│ │ ├── gl-test-ts-gl-lores-page-graphics-mode-lores-renders-mixed-1-snap.png
│ │ ├── gl-test-ts-gl-lores-page-text-mode-40-column-renders-1-snap.png
│ │ ├── gl-test-ts-gl-lores-page-text-mode-40-column-renders-alt-chars-1-snap.png
│ │ ├── gl-test-ts-gl-lores-page-text-mode-80-column-renders-1-snap.png
│ │ └── gl-test-ts-gl-lores-page-text-mode-80-column-renders-alt-chars-1-snap.png
│ ├── __mocks__
│ │ └── apple2shader.js
│ ├── applesoft
│ │ ├── compiler.spec.ts
│ │ └── decompiler.spec.ts
│ ├── base64.test.ts
│ ├── canvas.test.ts
│ ├── cards
│ │ ├── data
│ │ │ └── DOS 3.3 System Master.woz
│ │ ├── disk2.spec.ts
│ │ └── langcard.spec.ts
│ ├── formats
│ │ ├── 2mg.spec.ts
│ │ ├── create_disk.spec.ts
│ │ ├── d13.spec.ts
│ │ ├── do.spec.ts
│ │ ├── format_utils.spec.ts
│ │ ├── po.spec.ts
│ │ ├── testdata
│ │ │ ├── 13sector.spec.ts
│ │ │ ├── 13sector.ts
│ │ │ ├── 16sector.spec.ts
│ │ │ ├── 16sector.ts
│ │ │ ├── block_volume.ts
│ │ │ ├── json.ts
│ │ │ └── woz.ts
│ │ ├── types.spec.ts
│ │ ├── util.spec.ts
│ │ ├── util.ts
│ │ └── woz.spec.ts
│ ├── gl.test.ts
│ ├── mmu.test.ts
│ ├── rom.test.ts
│ ├── ui
│ │ ├── __snapshots__
│ │ │ └── options_modal.spec.ts.snap
│ │ └── options_modal.spec.ts
│ └── util.test.ts
└── util
│ ├── asserts.ts
│ ├── bios.ts
│ ├── image.ts
│ └── memory.ts
├── tsconfig.json
├── types
└── styles.d.ts
├── webpack.config.js
└── workers
├── format.worker.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_style = space
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 |
10 | [*.js]
11 | indent_size = 4
12 |
13 | [*.md]
14 | trim_trailing_whitespace = true
15 |
16 | [*.s]
17 | indent_size = 8
18 | indent_style = tab
19 |
20 | [Makefile]
21 | indent_style = tab
22 | indent_size = 8
23 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | json/disks/index.js
3 | node_modules
4 | submodules
5 | tmp
6 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: Node CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | strategy:
10 | matrix:
11 | node-version: [18.x, 20.x]
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 | with:
16 | submodules: 'true'
17 | - name: Use Node.js ${{ matrix.node-version }}
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: ${{ matrix.node-version }}
21 | - name: npm install, build, and test
22 | run: |
23 | npm ci
24 | npm run build --if-present
25 | npm test
26 | env:
27 | CI: true
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .*~
2 | .checked-*.js
3 | .DS_Store
4 | .vscode
5 | /coverage
6 | /dist
7 | /node_modules
8 | /tmp
9 | __diff_output__
10 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "submodules/cpu6502"]
2 | path = submodules/cpu6502
3 | url = https://github.com/whscullin/cpu6502
4 | [submodule "submodules/apple2shader"]
5 | path = submodules/apple2shader
6 | url = https://github.com/whscullin/apple2shader
7 |
--------------------------------------------------------------------------------
/.idx/dev.nix:
--------------------------------------------------------------------------------
1 | {pkgs}: {
2 |
3 | # Which nixpkgs channel to use.
4 | channel = "stable-23.05"; # or "unstable"
5 |
6 | # Use https://search.nixos.org/packages to find packages
7 | packages = [
8 | pkgs.nodejs_18
9 | pkgs.cacert
10 | pkgs.openssh
11 | pkgs.fakeroot
12 | ];
13 |
14 | # search for the extension on https://open-vsx.org/ and use "publisher.id"
15 | idx.extensions = [
16 | "ms-vscode.js-debug"
17 | "dbaeumer.vscode-eslint"
18 | "esbenp.prettier-vscode"
19 | ];
20 |
21 | idx.previews = {
22 | enable = true;
23 | previews = [
24 | {
25 | command = [
26 | "npm"
27 | "start"
28 | "--"
29 | "--port"
30 | "$PORT"
31 | ];
32 | manager = "web";
33 | id = "web";
34 | }
35 | ];
36 | };
37 | }
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | js/roms
2 | submodules
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 4,
4 | "semi": true,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/.stylelintignore:
--------------------------------------------------------------------------------
1 | css/apple2.css
2 | coverage
3 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "stylelint-config-standard-scss",
4 | "stylelint-config-css-modules"
5 | ],
6 | "rules": {
7 | "selector-class-pattern": "^[a-z][a-zA-Z0-9_-]+$"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2010-2021 Will Scullin and contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/asm/.gitignore:
--------------------------------------------------------------------------------
1 | smartport
2 | _FileInformation.txt
3 |
--------------------------------------------------------------------------------
/asm/mouse.s:
--------------------------------------------------------------------------------
1 | ;
2 | ; Minimal mouse support. Only firmware routines are supported, no
3 | ; I/O hooks or softswitches. This is enough to work with titles
4 | ; that follow the documentation's recommendations to use the
5 | ; firmware routines like Dazzle Draw and Apple II DeskTop
6 | ;
7 | ORG $C700
8 |
9 | ; Constants for future reference
10 |
11 | CLAMP_X_LOW EQU $478
12 | CLAMP_Y_LOW EQU $4F8
13 | CLAMP_X_HIGH EQU $578
14 | CLAMP_Y_HIGH EQU $5F8
15 |
16 | X_LOW EQU $478
17 | Y_LOW EQU $4F8
18 | X_HIGH EQU $578
19 | Y_HIGH EQU $5F8
20 | RESERVED1 EQU $678
21 | RESERVED2 EQU $67F
22 | STATUS EQU $778
23 | MODE EQU $7F8
24 |
25 | STATUS_DOWN EQU $80
26 | STATUS_LAST EQU $40
27 | INT_SCREEN EQU $08
28 | INT_BUTTON EQU $04
29 | INT_MOUSE EQU $02
30 |
31 | ROMRTS EQU $FF58
32 |
33 | DFB $00 ; $00
34 | DFB $00 ; $01
35 | DFB $00 ; $02
36 | DFB $00 ; $03
37 | DFB $00 ; $04
38 | ; Cx05 - Pascal ID byte
39 | DFB $38 ; $05
40 | DFB $00 ; $06
41 | ; Cx07 - Pascal ID byte
42 | DFB $18 ; $07
43 | DFB $00 ; $08
44 | DFB $00 ; $09
45 | DFB $00 ; $0A
46 | ; Cx0B - Generic signature byte of firmware cards
47 | DFB $01 ; $0B
48 | ; Cx0C - 2 = X-Y pointing device; 0 = identification code
49 | ID1 DFB $20 ; $0C
50 | DFB $00 ; $0D
51 | DFB $00 ; $0E
52 | DFB $00 ; $0F
53 | DFB $00 ; $10
54 | DFB $00 ; $11
55 | ; The firmware routines point to individual RTS opcodes
56 | ; that are intercepted by the card implementation which
57 | ; manipulates memory and processor state directly
58 | DFB $20 ; $12 SETMOUSE
59 | DFB $21 ; $13 SERVEMOUSE
60 | DFB $22 ; $14 READMOUSE
61 | DFB $23 ; $15 CLEARMOUSE
62 | DFB $24 ; $16 POSMOUSE
63 | DFB $25 ; $17 CLAMPMOUSE
64 | DFB $26 ; $18 HOMEMOUSE
65 | DFB $27 ; $19 INITMOUSE
66 | DFB $00 ; $1A
67 | DFB $00 ; $1B
68 | DFB $00 ; $1C
69 | DFB $00 ; $1D
70 | DFB $00 ; $1E
71 | DFB $00 ; $1F
72 | RTS ; $20 SETMOUSE
73 | RTS ; $21 SERVEMOUSE
74 | RTS ; $22 READMOUSE
75 | RTS ; $23 CLEARMOUSE
76 | RTS ; $24 POSMOUSE
77 | RTS ; $25 CLAMPMOUSE
78 | RTS ; $26 HOMEMOUSE
79 | RTS ; $27 INITMOUSE
80 | PADDING DS $C7FB - PADDING
81 | ORG $C7FB
82 | ; CxFB - A mouse identification byte
83 | ID2 DFB $D6 ; $FB
84 | DFB $00 ; $FC
85 | DFB $00 ; $FD
86 | DFB $00 ; $FE
87 | DFB $00 ; $FF
88 | END
89 |
--------------------------------------------------------------------------------
/asm/smartport.s:
--------------------------------------------------------------------------------
1 | ORG $C700
2 |
3 | ; Slot scan ZP addresses
4 | SCAN_LO EQU $00
5 | SCAN_HI EQU $01
6 |
7 | ; ProDOS
8 | COMMAND EQU $42
9 | UNIT EQU $43
10 | ADDRESS_LO EQU $44
11 | ADDRESS_HI EQU $45
12 | BLOCK_LO EQU $46
13 | BLOCK_HI EQU $47
14 |
15 | MSLOT EQU $7F8
16 |
17 | ; Slot I/O addresses
18 | STATUS EQU $C080
19 | READY EQU $C081
20 | XREG EQU $C082
21 | YREG EQU $C083
22 | CARRY EQU $C084
23 |
24 | ; ROM addresses
25 | BASIC EQU $E000
26 | SLOOP EQU $FABA ; Resume boot scan
27 | ROMRTS EQU $FF58
28 | BOOT EQU $0801
29 |
30 | LDX #$20 ; $20 $00 $03 $00 - Smartport signature
31 | LDX #$00 ; $20 $00 $03 $3C - Vanilla disk signature
32 | LDX #$03
33 | LDX #$00 ; Override with $3C for DumbPort
34 | ; Determine our slot
35 | JSR ROMRTS
36 | TSX
37 | LDA $0100,X
38 | STA MSLOT ; $Cn
39 | ASL
40 | ASL
41 | ASL
42 | ASL
43 | TAY ; $n0
44 | ; Load the disk status bits
45 | LDA STATUS,Y
46 | LSR ; Check for Disk 1
47 | BCS DISKREADY ; Boot from Disk 1
48 | LDA SCAN_LO
49 | BNE GO_BASIC
50 | LDA SCAN_HI
51 | CMP MSLOT
52 | BNE GO_BASIC
53 | JMP SLOOP ; Go back to scanning
54 | GO_BASIC JMP BASIC ; Go to basic
55 | ; Boot routine
56 | DISKREADY LDX #$01 ; Read
57 | STX COMMAND
58 | DEX
59 | STX BLOCK_LO ; Block 0
60 | STX BLOCK_HI
61 | STX ADDRESS_LO ; Into $800
62 | LDX #$08
63 | STX ADDRESS_HI
64 | LDA MSLOT
65 | PHA ; Save slot address
66 | PHA ; RTS address hi byte
67 | LDA #REENTRY - 1
68 | PHA ; RTS address lo byte
69 | CLV
70 | BVC BLOCK_ENT
71 | REENTRY PLA ; Restore slot address
72 | ASL ; Make I/O register index
73 | ASL
74 | ASL
75 | ASL
76 | TAX
77 | JMP BOOT
78 | DS 2
79 | BLOCK_ENT JMP COMMON_ENT
80 | SMARTPORT_ENT JMP COMMON_ENT
81 | COMMON_ENT LDA $00 ; Save $00
82 | PHA
83 | LDA #$60 ; Create a known RTS because ROM may be unavailable
84 | STA $00
85 | JSR $0000
86 | TSX
87 | LDA $0100,X ; Load ROM high byte
88 | ASL ; Convert to index for I/O register
89 | ASL
90 | ASL
91 | ASL
92 | TAX
93 | PLA ; Restore $00
94 | STA $00
95 | BUSY_LOOP LDA READY,X ; STATUS will return $80 until ready
96 | BMI BUSY_LOOP
97 | PHA ; Save A
98 | LDA XREG,X ; Read X register
99 | PHA ; Save X
100 | LDA YREG,X ; Read Y register
101 | PHA ; Save Y
102 | LDA CARRY,X ; Get Carry status
103 | ROR A ; Set or clear carry
104 | PLY ; Restore Y
105 | PLX ; Restore X
106 | PLA ; Restore A
107 | RTS
108 | PADDING DS $C7FE - PADDING
109 | ORG $C7FE
110 | FLAGS DFB $D7
111 | ENTRY_LO DFB BLOCK_ENT
112 |
113 | END
114 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | // babel.config.js
2 | module.exports = {
3 | presets: [
4 | [
5 | '@babel/preset-env',
6 | {
7 | targets: {
8 | node: 'current',
9 | },
10 | },
11 | ],
12 | [
13 | '@babel/typescript',
14 | {
15 | jsxPragma: 'h',
16 | },
17 | ],
18 | ],
19 | plugins: [
20 | [
21 | '@babel/plugin-transform-react-jsx',
22 | {
23 | pragma: 'h',
24 | pragmaFrag: 'Fragment',
25 | },
26 | ],
27 | ],
28 | };
29 |
--------------------------------------------------------------------------------
/bin/index:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 |
6 | const diskPath = path.resolve('json/disks');
7 | const dir = fs.readdirSync(diskPath);
8 |
9 | const index = [];
10 |
11 | for (const fileName of dir.sort()) {
12 | if (/\.json$/.test(fileName)) {
13 | const json = fs.readFileSync(path.resolve(diskPath, fileName));
14 | const data = JSON.parse(json);
15 | if (data.private) {
16 | continue;
17 | }
18 | if (!data.name || !data.category) {
19 | continue;
20 | }
21 |
22 | const entry = {
23 | filename: `json/disks/${fileName}`,
24 | e: data['2e'],
25 | name: data.name,
26 | disk: data.disk,
27 | category: data.category,
28 | };
29 | index.push(entry);
30 | }
31 | }
32 |
33 | index.sort((x, y) => {
34 | const xc = x.category.toLowerCase();
35 | const yc = y.category.toLowerCase();
36 | const xn = x.name.toLowerCase();
37 | const yn = y.name.toLowerCase();
38 |
39 | if (xc < yc) {
40 | return -1;
41 | } else if (xc > yc) {
42 | return 1;
43 | } else if (xn < yn) {
44 | return -1;
45 | } else if (xn > yn) {
46 | return 1;
47 | } else {
48 | return 0;
49 | }
50 | });
51 |
52 | fs.writeFileSync(
53 | path.resolve(diskPath, 'index.js'),
54 | `disk_index = ${JSON.stringify(index, null, 2)};`
55 | );
56 |
57 | fs.writeFileSync(
58 | path.resolve(diskPath, 'index.json'),
59 | JSON.stringify(index, null, 2)
60 | );
61 |
--------------------------------------------------------------------------------
/css/green-off-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/css/green-off-16.png
--------------------------------------------------------------------------------
/css/green-off-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/css/green-off-32.png
--------------------------------------------------------------------------------
/css/green-on-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/css/green-on-16.png
--------------------------------------------------------------------------------
/css/green-on-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/css/green-on-32.png
--------------------------------------------------------------------------------
/css/red-off-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/css/red-off-16.png
--------------------------------------------------------------------------------
/css/red-off-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/css/red-off-32.png
--------------------------------------------------------------------------------
/css/red-on-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/css/red-on-16.png
--------------------------------------------------------------------------------
/css/red-on-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/css/red-on-32.png
--------------------------------------------------------------------------------
/img/badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/img/badge.png
--------------------------------------------------------------------------------
/img/badge2e.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/img/badge2e.png
--------------------------------------------------------------------------------
/img/closed-apple24-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/img/closed-apple24-green.png
--------------------------------------------------------------------------------
/img/closed-apple24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/img/closed-apple24.png
--------------------------------------------------------------------------------
/img/logoicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/img/logoicon.png
--------------------------------------------------------------------------------
/img/open-apple24-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/img/open-apple24-green.png
--------------------------------------------------------------------------------
/img/open-apple24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/img/open-apple24.png
--------------------------------------------------------------------------------
/img/webapp-ipad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/img/webapp-ipad.png
--------------------------------------------------------------------------------
/img/webapp-iphone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whscullin/apple2js/25c08a40fb3244888406897303430392f9955ee2/img/webapp-iphone.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | PreApple II
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
27 |
28 |
29 |
30 |
31 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleNameMapper: {
3 | '^js/(.*)': '/js/$1',
4 | '^test/(.*)': '/test/$1',
5 | '\\.css$': 'identity-obj-proxy',
6 | '\\.scss$': 'identity-obj-proxy',
7 | // For some reason the preact modules are not where they are
8 | // expected. This seems to have something to do with jest > v27.
9 | // https://github.com/preactjs/enzyme-adapter-preact-pure/issues/179#issuecomment-1201096897
10 | '^preact(/(.*)|$)': 'preact$1',
11 | },
12 | roots: ['js/', 'test/'],
13 | testMatch: ['**/?(*.)+(spec|test).+(ts|js|tsx)'],
14 | transform: {
15 | '^.+\\.js$': 'babel-jest',
16 | '^.+\\.ts$': 'ts-jest',
17 | '^.*\\.tsx$': 'ts-jest',
18 | },
19 | transformIgnorePatterns: [
20 | '/node_modules/(?!(@testing-library/preact/dist/esm)/)',
21 | ],
22 | setupFilesAfterEnv: ['/test/jest-setup.ts'],
23 | coveragePathIgnorePatterns: ['/node_modules/', '/js/roms/', '/test/'],
24 | preset: 'ts-jest',
25 | };
26 |
--------------------------------------------------------------------------------
/js/applesoft/zeropage.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Zero page locations used by Applesoft. The names come from
3 | * the commented decompilation produced by the Merlin Pro
4 | * assembler, revision 4/27/84. There is evidence from
5 | * https://www.pagetable.com/?p=774 that the original Microsoft
6 | * BASIC source code used these names as well.
7 | */
8 |
9 | /** Start of program (word) */
10 | export const TXTTAB = 0x67;
11 | /** Start of variables (word) */
12 | export const VARTAB = 0x69;
13 | /** Start of arrays (word) */
14 | export const ARYTAB = 0x6b;
15 | /** End of strings (word). (Strings are allocated down from HIMEM.) */
16 | export const STREND = 0x6d;
17 | /** Current line */
18 | export const CURLINE = 0x75;
19 | /** Floating Point accumulator (float) */
20 | export const FAC = 0x9d;
21 | /** Floating Point arguments (float) */
22 | export const ARG = 0xa5;
23 | /**
24 | * End of program (word). This is actually 1 or 2 bytes past the three
25 | * zero bytes that end the program.
26 | */
27 | export const PRGEND = 0xaf;
28 |
--------------------------------------------------------------------------------
/js/cards/drivers/BaseDiskDriver.ts:
--------------------------------------------------------------------------------
1 | import { DriveNumber, NibbleDisk, WozDisk } from '../../formats/types';
2 | import { ControllerState, DiskDriver, Drive, DriverState } from './types';
3 |
4 | /**
5 | * Common logic for both `NibbleDiskDriver` and `WozDiskDriver`.
6 | */
7 | export abstract class BaseDiskDriver implements DiskDriver {
8 | constructor(
9 | protected readonly driveNo: DriveNumber,
10 | protected readonly drive: Drive,
11 | protected readonly disk: NibbleDisk | WozDisk,
12 | protected readonly controller: ControllerState
13 | ) {}
14 |
15 | /** Called frequently to ensure the disk is spinning. */
16 | abstract tick(): void;
17 |
18 | /** Called when Q6 is set LOW. */
19 | abstract onQ6Low(): void;
20 |
21 | /** Called when Q6 is set HIGH. */
22 | abstract onQ6High(readMode: boolean): void;
23 |
24 | /**
25 | * Called when drive is turned on. This is guaranteed to be called
26 | * only when the associated drive is toggled from off to on. This
27 | * is also guaranteed to be called when a new disk is inserted when
28 | * the drive is already on.
29 | */
30 | abstract onDriveOn(): void;
31 |
32 | /**
33 | * Called when drive is turned off. This is guaranteed to be called
34 | * only when the associated drive is toggled from on to off.
35 | */
36 | abstract onDriveOff(): void;
37 |
38 | debug(..._args: unknown[]) {
39 | // debug(...args);
40 | }
41 |
42 | /**
43 | * Called every time the head moves to clamp the track to a valid
44 | * range.
45 | */
46 | abstract clampTrack(): void;
47 |
48 | /** Returns `true` if the controller is on and this drive is selected. */
49 | isOn(): boolean {
50 | return this.controller.on && this.controller.driveNo === this.driveNo;
51 | }
52 |
53 | /** Returns `true` if the drive's write protect switch is enabled. */
54 | isWriteProtected(): boolean {
55 | return this.drive.readOnly;
56 | }
57 |
58 | /** Returns the current state of the driver as a serializable object. */
59 | abstract getState(): DriverState;
60 |
61 | /** Sets the state of the driver from the given `state`. */
62 | abstract setState(state: DriverState): void;
63 | }
64 |
--------------------------------------------------------------------------------
/js/cards/drivers/EmptyDriver.ts:
--------------------------------------------------------------------------------
1 | import { DiskDriver, Drive, DriverState } from './types';
2 |
3 | /** Returned state for an empty drive. */
4 | export interface EmptyDriverState extends DriverState {}
5 |
6 | /**
7 | * Driver for empty drives. This implementation does nothing except keep
8 | * the head clamped between tracks 0 and 34.
9 | */
10 | export class EmptyDriver implements DiskDriver {
11 | constructor(private readonly drive: Drive) {}
12 |
13 | tick(): void {
14 | // do nothing
15 | }
16 |
17 | onQ6Low(): void {
18 | // do nothing
19 | }
20 |
21 | onQ6High(_readMode: boolean): void {
22 | // do nothing
23 | }
24 |
25 | onDriveOn(): void {
26 | // do nothing
27 | }
28 |
29 | onDriveOff(): void {
30 | // do nothing
31 | }
32 |
33 | clampTrack(): void {
34 | // For empty drives, the emulator clamps the track to 0 to 34,
35 | // but real Disk II drives can seek past track 34 by at least a
36 | // half track, usually a full track. Some 3rd party drives can
37 | // seek to track 39.
38 | if (this.drive.track < 0) {
39 | this.drive.track = 0;
40 | }
41 | if (this.drive.track > 34) {
42 | this.drive.track = 34;
43 | }
44 | }
45 |
46 | getState() {
47 | return {};
48 | }
49 |
50 | setState(_state: EmptyDriverState): void {
51 | // do nothing
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/js/cards/drivers/types.ts:
--------------------------------------------------------------------------------
1 | import { DriveNumber, SupportedSectors } from 'js/formats/types';
2 | import { byte, nibble } from 'js/types';
3 |
4 | export type LssClockCycle = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
5 | export type LssState = nibble;
6 |
7 | export type Phase = 0 | 1 | 2 | 3;
8 |
9 | /**
10 | * State of the controller.
11 | */
12 | export interface ControllerState {
13 | /** Sectors supported by the controller. */
14 | sectors: SupportedSectors;
15 |
16 | /** Is the active drive powered on? */
17 | on: boolean;
18 |
19 | /** The active drive. */
20 | driveNo: DriveNumber;
21 |
22 | /** The 8-cycle LSS clock. */
23 | clock: LssClockCycle;
24 |
25 | /** Current state of the Logic State Sequencer. */
26 | state: LssState;
27 |
28 | /** Q6 (Shift/Load) */
29 | q6: boolean;
30 | /** Q7 (Read/Write) */
31 | q7: boolean;
32 |
33 | /** Last data from the disk drive. */
34 | latch: byte;
35 | /** Last data written by the CPU to card softswitch 0x8D. */
36 | bus: byte;
37 | }
38 |
39 | /** Common information for Nibble and WOZ disks. */
40 | export interface Drive {
41 | /** Whether the drive write protect is on. */
42 | readOnly: boolean;
43 | /** Quarter track position of read/write head. */
44 | track: byte;
45 | /** Position of the head on the track. */
46 | head: byte;
47 | /** Current active coil in the head stepper motor. */
48 | phase: Phase;
49 | /** Whether the drive has been written to since it was loaded. */
50 | dirty: boolean;
51 | }
52 |
53 | /** Base interface for disk driver states. */
54 | export interface DriverState {}
55 |
56 | /** Interface for drivers for various disk types. */
57 | export interface DiskDriver {
58 | tick(): void;
59 | onQ6Low(): void;
60 | onQ6High(readMode: boolean): void;
61 | onDriveOn(): void;
62 | onDriveOff(): void;
63 | clampTrack(): void;
64 | getState(): DriverState;
65 | setState(state: DriverState): void;
66 | }
67 |
--------------------------------------------------------------------------------
/js/cards/nsc.ts:
--------------------------------------------------------------------------------
1 | import ROM from 'js/roms/rom';
2 | import { bit, byte } from 'js/types';
3 | import { debug } from '../util';
4 |
5 | const PATTERN = [0xc5, 0x3a, 0xa3, 0x5c, 0xc5, 0x3a, 0xa3, 0x5c];
6 |
7 | const A0 = 0x01;
8 | const A2 = 0x04;
9 |
10 | export default class NoSlotClock {
11 | bits: bit[] = [];
12 | pattern = new Array(64);
13 | patternIdx: number = 0;
14 |
15 | constructor(private rom: ROM) {
16 | debug('NoSlotClock');
17 | }
18 |
19 | private patternMatch() {
20 | for (let idx = 0; idx < 8; idx++) {
21 | let byte = 0;
22 | for (let jdx = 0; jdx < 8; jdx++) {
23 | byte >>= 1;
24 | byte |= this.pattern.shift() ? 0x80 : 0x00;
25 | }
26 | if (byte !== PATTERN[idx]) {
27 | return false;
28 | }
29 | }
30 | return true;
31 | }
32 |
33 | private calcBits() {
34 | const shift = (val: byte) => {
35 | for (let idx = 0; idx < 4; idx++) {
36 | this.bits.push(val & 0x08 ? 0x01 : 0x00);
37 | val <<= 1;
38 | }
39 | };
40 | const shiftBCD = (val: byte) => {
41 | shift(Math.floor(val / 10));
42 | shift(Math.floor(val % 10));
43 | };
44 |
45 | const now = new Date();
46 | const year = now.getFullYear() % 100;
47 | const day = now.getDate();
48 | const weekday = now.getDay() + 1;
49 | const month = now.getMonth() + 1;
50 | const hour = now.getHours();
51 | const minutes = now.getMinutes();
52 | const seconds = now.getSeconds();
53 | const hundredths = now.getMilliseconds() / 10;
54 |
55 | this.bits = [];
56 |
57 | shiftBCD(year);
58 | shiftBCD(month);
59 | shiftBCD(day);
60 | shiftBCD(weekday);
61 | shiftBCD(hour);
62 | shiftBCD(minutes);
63 | shiftBCD(seconds);
64 | shiftBCD(hundredths);
65 | }
66 |
67 | access(off: byte) {
68 | if (off & A2) {
69 | this.patternIdx = 0;
70 | } else {
71 | const bit = off & A0;
72 | this.pattern[this.patternIdx++] = bit;
73 | if (this.patternIdx === 64) {
74 | if (this.patternMatch()) {
75 | this.calcBits();
76 | }
77 | this.patternIdx = 0;
78 | }
79 | }
80 | }
81 |
82 | start() {
83 | return this.rom.start();
84 | }
85 |
86 | end() {
87 | return this.rom.end();
88 | }
89 |
90 | read(page: byte, off: byte) {
91 | if (this.bits.length > 0) {
92 | const bit = this.bits.pop();
93 | return bit;
94 | } else {
95 | this.access(off);
96 | }
97 | return this.rom.read(page, off);
98 | }
99 |
100 | write(_page: byte, off: byte, _val: byte) {
101 | this.access(off);
102 | this.rom.write();
103 | }
104 |
105 | getState() {
106 | return {};
107 | }
108 |
109 | setState(_: unknown) {
110 | // Setting the state makes no sense.
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/js/cards/parallel.ts:
--------------------------------------------------------------------------------
1 | import { debug } from '../util';
2 | import { Card, Restorable, byte } from '../types';
3 | import { rom } from '../roms/cards/parallel';
4 |
5 | const LOC = {
6 | IOREG: 0x80,
7 | } as const;
8 |
9 | export interface ParallelState {}
10 | export interface ParallelOptions {
11 | putChar: (val: byte) => void;
12 | }
13 |
14 | export default class Parallel implements Card, Restorable {
15 | constructor(private cbs: ParallelOptions) {
16 | debug('Parallel card');
17 | }
18 |
19 | private access(off: byte, val?: byte) {
20 | switch (off & 0x8f) {
21 | case LOC.IOREG:
22 | if (this.cbs.putChar && val) {
23 | this.cbs.putChar(val);
24 | }
25 | break;
26 | default:
27 | debug('Parallel card unknown softswitch', off);
28 | }
29 | return 0;
30 | }
31 |
32 | ioSwitch(off: byte, val?: byte) {
33 | return this.access(off, val);
34 | }
35 |
36 | read(_page: byte, off: byte) {
37 | return rom[off];
38 | }
39 |
40 | write() {
41 | // not writable
42 | }
43 |
44 | getState() {
45 | return {};
46 | }
47 |
48 | setState(_state: ParallelState) {
49 | // can't set the state
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/js/components/App.tsx:
--------------------------------------------------------------------------------
1 | import 'preact/debug';
2 | import cs from 'classnames';
3 | import { h } from 'preact';
4 | import { Header } from './Header';
5 | import { Apple2 } from './Apple2';
6 | import { usePrefs } from './hooks/usePrefs';
7 | import { SYSTEM_TYPE_APPLE2E } from '../ui/system';
8 | import { SCREEN_GL } from '../ui/screen';
9 | import { defaultSystem, systemTypes } from './util/systems';
10 |
11 | import styles from './css/App.module.scss';
12 | import componentStyles from './css/Components.module.scss';
13 | import 'bootstrap-icons/font/bootstrap-icons.css';
14 |
15 | /**
16 | * Top level application component, provides the parameters
17 | * needed by the Apple2 component to bootstrap itself.
18 | *
19 | * @returns Application component
20 | */
21 | export const App = () => {
22 | const prefs = usePrefs();
23 | const systemType = prefs.readPref(SYSTEM_TYPE_APPLE2E, 'apple2enh');
24 | const gl = prefs.readPref(SCREEN_GL, 'true') === 'true';
25 |
26 | const system = {
27 | ...defaultSystem,
28 | ...(systemTypes[systemType] || {}),
29 | };
30 |
31 | return (
32 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/js/components/AudioControl.tsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
3 | import { ControlButton } from './ControlButton';
4 | import { OptionsContext } from './OptionsContext';
5 | import { Audio, SOUND_ENABLED_OPTION } from '../ui/audio';
6 | import { Apple2 as Apple2Impl } from '../apple2';
7 |
8 | /**
9 | * AudioControl component properties.
10 | */
11 | export interface AudioControlProps {
12 | apple2: Apple2Impl | undefined;
13 | }
14 |
15 | /**
16 | * Control that instantiates the Audio object and provides
17 | * a control to mute and unmute audio.
18 | *
19 | * @param apple2 The Apple2 object
20 | * @returns AudioControl component
21 | */
22 | export const AudioControl = ({ apple2 }: AudioControlProps) => {
23 | const [audioEnabled, setAudioEnabled] = useState(false);
24 | const [audio, setAudio] = useState