├── .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 | 32 | 33 | 40 | 41 | 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 |
33 |
34 | 35 |
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