1 Commits

Author SHA1 Message Date
Felix Rieseberg
2af7992e94 1.5.0 2019-02-03 15:38:27 -08:00
103 changed files with 7602 additions and 11688 deletions

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
text eol=lf

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,121 +0,0 @@
name: Build & Release
on:
push:
branches:
- master
tags:
- v*
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 18.x
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install
run: yarn --frozen-lockfile
- name: lint
run: yarn lint
build:
needs: lint
name: Build (${{ matrix.os }} - ${{ matrix.arch }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
# Build for supported platforms
# https://github.com/electron/electron-packager/blob/ebcbd439ff3e0f6f92fa880ff28a8670a9bcf2ab/src/targets.js#L9
# 32-bit Linux unsupported as of 2019: https://www.electronjs.org/blog/linux-32bit-support
os: [ macOS-latest, ubuntu-latest, windows-latest ]
arch: [ x64, arm64 ]
include:
- os: windows-latest
arch: ia32
- os: ubuntu-latest
arch: armv7l
# Publishing artifacts for multiple Windows architectures has
# a bug which can cause the wrong architecture to be downloaded
# for an update, so until that is fixed, only build Windows x64
exclude:
- os: windows-latest
arch: arm64
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 18.x
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
if: matrix.os != 'macOS-latest'
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Set MacOS signing certs
if: matrix.os == 'macOS-latest'
run: chmod +x tools/add-macos-cert.sh && ./tools/add-macos-cert.sh
env:
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
- name: Set Windows signing certificate
if: matrix.os == 'windows-latest'
continue-on-error: true
id: write_file
uses: timheuer/base64-to-file@v1
with:
fileName: 'win-certificate.pfx'
encodedString: ${{ secrets.WINDOWS_CODESIGN_P12 }}
- name: Download disk image (ps1)
run: tools/download-disk.ps1
if: matrix.os == 'windows-latest' && startsWith(github.ref, 'refs/tags/')
env:
DISK_URL: ${{ secrets.DISK_URL }}
- name: Download disk image (sh)
run: chmod +x tools/download-disk.sh && ./tools/download-disk.sh
if: matrix.os != 'windows-latest' && startsWith(github.ref, 'refs/tags/')
env:
DISK_URL: ${{ secrets.DISK_URL }}
- name: Install
run: yarn
- name: Make
if: startsWith(github.ref, 'refs/tags/')
run: yarn make --arch=${{ matrix.arch }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WINDOWS_CODESIGN_FILE: ${{ steps.write_file.outputs.filePath }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
draft: true
files: |
out/**/*.deb
out/**/*.dmg
out/**/*setup*.exe
out/**/*.rpm
out/**/*.zip

6
.gitignore vendored
View File

@@ -2,9 +2,3 @@ node_modules
out
src/images
.DS_Store
images
images_new
dist
!.github/images
*.code-workspace
*.pfx

35
HELP.md
View File

@@ -1,35 +1,24 @@
# Help & Commonly Asked Questions
## MS-DOS seems to mess up the screen
Hit `Alt + Enter` to make the command screen "Full Screen" (as far as Windows 95 is
## MS-DOS seems to brick the screen
Hit `Alt + Enter` to make the command screen "full screen" (as far as Windows 95 is
concerned). This should restore the display from the garbled mess you see and allow
you to access the Command Prompt. Press Alt-Enter again to leave Full Screen and go
back to Window Mode. (Thanks to @DisplacedGamers for that wisdom)
you to access the command prompt. Press Alt-Enter again to leave full screen and go
back to a window mode. (Thanks to @DisplacedGamer for that wisdom)
## Windows 95 is stuck in a bad state
On the app's home screen, select "Settings" in the lower menu. Then, delete your
machine's state before starting it again - this time hopefully without issues.
Restart the application and click on the "Reset machine & delete state" button.
You can find it in the lower left of the screen. Then, hit the "Start Windows 95"
button to start your virtual machine again.
## I want to install additional apps or games
If you are running macOS, or Linux, you can probably "mount" the
virtual hard drive used by `windows95` to add files. Hit the "Modify C: Drive"
button, which will take you to the disk image.
On macOS, double-click the disk image to open it.
On Windows 10, Windows will _think_ that it can open up the image, but will
actually fail to do so. Use a tool [like OSFMount][osfmount] to mount your
disk image.
If you are running Windows 10, macOS, or Linux, you can probably "mount" the
virtual hard drive used by `windows95` to add files. Hit the "Show Disk Image"
button in the lower right of the app, which will take you to the disk image.
On both Windows 10 and macOS, double-click the disk image to open it.
On Linux, search the Internet for instructions on how to mount an `img` disk
image on your distribution. It's likely that you'll be able to run `mount`
with the image as input.
image on your distribution.
[osfmount]: https://www.osforensics.com/tools/mount-disk-images.html
## What's the FrontPage Username and Password?
Username: windows95
Password: password

View File

@@ -1,4 +1,4 @@
Copyright 2019 Felix Rieseberg
Copyright 2018 Felix Rieseberg
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

117
README.md
View File

@@ -2,101 +2,18 @@
This is Windows 95, running in an [Electron](https://electronjs.org/) app. Yes, it's the full thing. I'm sorry.
Interested in Electron? Join as at [CovalenceConf](http://covalenceconf.com) in San Francisco!
## Downloads
<table class="is-fullwidth">
</thead>
<tbody>
</tbody>
<tr>
<td>
<img src="./.github/images/windows.png" width="24"><br />
Windows
</td>
<td>
<span>32-bit</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-3.1.1-setup-ia32.exe">
💿 Installer
</a> |
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-win32-ia32-3.1.1.zip">
📦 Standalone Zip
</a>
<br />
<span>64-bit</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-3.1.1-setup-x64.exe">
💿 Installer
</a> |
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-win32-x64-3.1.1.zip">
📦 Standalone Zip
</a><br />
<span>ARM64</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-3.1.1-setup-arm64.exe">
💿 Installer
</a> |
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-win32-arm64-3.1.1.zip">
📦 Standalone Zip
</a><br />
<span>
❓ Don't know what kind of chip you have? Hit start, enter "processor" for info.
</span>
</td>
</tr>
<tr>
<td>
<img src="./.github/images/macos.png" width="24"><br />
macOS
</td>
<td>
<span>Intel Processor</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-darwin-x64-3.1.1.zip">
📦 Standalone Zip
</a><br />
<span>Apple M1 Processor</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-darwin-arm64-3.1.1.zip">
📦 Standalone Zip
</a><br />
<span>
❓ Don't know what kind of chip you have? Learn more at <a href="https://support.apple.com/en-us/HT211814">apple.com</a>.
</span>
</td>
</tr>
<tr>
<td>
<img src="./.github/images/linux.png" width="24"><br />
Linux
</td>
<td>
<span>64-bit</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-3.1.1-1.x86_64.rpm">
💿 rpm
</a> |
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95_3.1.1_amd64.deb">
💿 deb
</a><br />
<span>ARM64</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-3.1.1-1.arm64.rpm">
💿 rpm
</a> |
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95_3.1.1_arm64.deb">
💿 deb
</a><br />
<span>ARMv7 (armhf)</span>
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-3.1.1-1.armv7hl.rpm">
💿 rpm
</a> |
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95_3.1.1_armhf.deb">
💿 deb
</a>
</td>
</tr>
</table>
<hr />
| | Windows | macOS | Linux |
|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Standalone Download | 📦[Standalone, 32-bit](https://github.com/felixrieseberg/windows95/releases/download/v1.4.0/windows95-1.4.0-win32-standalone-ia32.zip) <br /> 📦[Standalone, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v1.4.0/windows95-1.4.0-win32-standalone-x64.zip) | 📦[Standalone](https://github.com/felixrieseberg/windows95/releases/download/v1.4.0/windows95-macos-1.4.0.zip) | |
| Installer | 💽[Setup, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v1.4.0/windows95-1.4.0-setup-win32-x64.exe) <br /> 💽[Setup, 32-bit](https://github.com/felixrieseberg/windows95/releases/download/v1.4.0/windows95-1.4.0-setup-win32-ia32.exe) | | 💽[deb, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v1.4.0/windows95-linux-1.4.0_amd64.deb) <br /> 💽[rpm, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v1.4.0/windows95-linux-1.4.0.x86_64.rpm) |
![Screenshot](https://user-images.githubusercontent.com/1426799/44532591-4ceb3680-a6a8-11e8-8c2c-bc29f3bfdef7.png)
## Does it work?
Yes! Quite well, actually - on macOS, Windows, and Linux. Bear in mind that this is written entirely in JavaScript, so please adjust your expectations.
Yes! Quite well, actually - on macOS, Windows, and Linux.
## Should this have been a native app?
Absolutely.
@@ -106,38 +23,30 @@ You'll likely be better off with an actual virtualization app, but the short ans
@DisplacedGamers](https://youtu.be/xDXqmdFxofM) I can recommend that you switch to a resolution of
640x480 @ 256 colors before starting DOS games - just like in the good ol' days.
## How's the code?
This only works well by accident and was mostly a joke. The code quality is accordingly. Thus it should not be used for anything other than personal amusement.
## Credits
99% of the work was done over at [v86](https://github.com/copy/v86/) by Copy aka Fabian Hemmer and his contributors.
99.999% of the work was done over at [v86](https://github.com/copy/v86/) by Copy.
## Contributing
Before you can run this from source, you'll need the disk image. It's not part of the
repository, but you can grab it using the `Show Disk Image` button from the packaged
release, which does include the disk image. You can find that button in the
`Modify C: Drive` section.
release, which does include the disk image.
Unpack the `images` folder into the `src` folder, creating this layout:
```
- /images/windows95.img
- /images/default-state.bin
- /assets/...
- /bios/...
- /docs/...
./src/images/windows95.img
```
Once you've done so, run `npm install` and `npm start` to run your local build.
If you want to tinker with the image or make a new one, check out the [QEMU docs](./docs/qemu.md).
## Other Questions
* [MS-DOS seems to brick the screen](./HELP.md#ms-dos-seems-to-brick-the-screen)
* [Windows 95 is stuck in a bad state](./HELP.md#windows-95-is-stuck-in-a-bad-state)
* [I want to install additional apps or games](./HELP.md#i-want-to-install-additional-apps-or-games)
* [Running in Docker](./docs/docker-instructions.md)
* [Running in an online VM with Kubernetes and Gitpod](./docs/docker-kubernetes-gitpod.md)
## License

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
<key>com.apple.security.automation.apple-events</key>
<true/>
</dict>
</plist>

Binary file not shown.

Binary file not shown.

View File

@@ -27,13 +27,7 @@ Note: You may need to run `xhost +` on your system to allow connections to the X
* [XQuartz](https://www.xquartz.org/)
* [Docker](http://docker.io)
1. Start XQuartz, go to `Preferences` -> `Security`, and check the box `Allow connections from network clients`
2. Restart XQuartz
3. In the terminal, run
```
xhost +
```
4. run
```
docker run -it -e DISPLAY=host.docker.internal:0 toolboc/windows95
```
1. Start XQuartz ,go to "Preferences -> Security " ,and check the box "allow connections from network clients"
2. restart XQuartz
3. In the terminal ,run "xhost +"
4. run "docker run -it -e DISPLAY=host.docker.internal:1 toolboc/windows95"

View File

@@ -1,4 +0,0 @@
## Running an online version of windows95
You can also run windows95 in Electron, in a virtual X server, in a JavaScript VNC client, in a Kubernetes workspace. What could go wrong?
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/felixrieseberg/windows95)

View File

@@ -1,28 +0,0 @@
# QEMU Instructions
The image built here was made with QEMU. In this doc, I'm keeping instructions
around.
Disk image creation
```sh
qemu-img create -f qcow2 win95.qcow2 1G
```
Installation
```sh
qemu-system-i386 -netdev user,id=mynet0 -device ne2k_isa,netdev=mynet0 -hda win95.qcow2 -soundhw sb16 -m 128 -cpu pentium -device cirrus-vga,vgamem_mb=64 -fda boot_floppy.img -cdrom Win95_OSR25.iso -boot a -soundhw pcspk
```
Running
With `ne2k_isa`
```sh
qemu-system-i386 -netdev user,id=mynet0 -device ne2k_isa,netdev=mynet0 -drive file=win95.img,format=raw,index=0,media=disk -soundhw sb16 -m 128 -cpu pentium -device cirrus-vga,vgamem_mb=16 -soundhw pcspk -cdrom Win95_OSR25.iso
```
With `ne2k_pci`
```sh
qemu-system-i386 -net nic,model=ne2k_pci -net user -drive file=win95_ne2k_pci.img,format=raw,index=0,media=disk -soundhw sb16 -m 128 -cpu pentium -device cirrus-vga,vgamem_mb=16 -soundhw pcspk -cdrom Win95_OSR25.iso --enable-kvm
```

View File

@@ -1,55 +1,21 @@
const path = require('path');
const fs = require('fs');
const package = require('./package.json');
if (process.env['WINDOWS_CODESIGN_FILE']) {
const certPath = path.join(__dirname, 'win-certificate.pfx');
const certExists = fs.existsSync(certPath);
if (certExists) {
process.env['WINDOWS_CODESIGN_FILE'] = certPath;
}
}
module.exports = {
hooks: {
generateAssets: require('./tools/generateAssets'),
},
packagerConfig: {
asar: false,
asar: {
unpack: '**/images/*.img'
},
icon: path.resolve(__dirname, 'assets', 'icon'),
appBundleId: 'com.felixrieseberg.windows95',
appCategoryType: 'public.app-category.developer-tools',
win32metadata: {
CompanyName: 'Felix Rieseberg',
OriginalFilename: 'windows95'
OriginalFilename: 'windows95',
},
osxSign: {
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)',
'hardened-runtime': true,
'gatekeeper-assess': false,
'entitlements': 'assets/entitlements.plist',
'entitlements-inherit': 'assets/entitlements.plist',
'signature-flags': 'library'
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)'
},
osxNotarize: {
appBundleId: 'com.felixrieseberg.macintoshjs',
appleId: process.env['APPLE_ID'],
appleIdPassword: process.env['APPLE_ID_PASSWORD'],
ascProvider: 'LT94ZKYDCJ'
},
ignore: [
/\/assets(\/?)/,
/\/docs(\/?)/,
/\/tools(\/?)/,
/\/src\/.*\.ts/,
/package-lock\.json/,
/README\.md/,
/tsconfig\.json/,
/Dockerfile/,
/issue_template\.md/,
/HELP\.md/,
]
},
makers: [
{
@@ -62,18 +28,16 @@ module.exports = {
exe: 'windows95.exe',
noMsi: true,
remoteReleases: '',
iconUrl: 'https://raw.githubusercontent.com/felixrieseberg/windows95/master/assets/icon.ico',
loadingGif: './assets/boot.gif',
setupExe: `windows95-${package.version}-setup-${arch}.exe`,
setupIcon: path.resolve(__dirname, 'assets', 'icon.ico'),
certificateFile: process.env['WINDOWS_CODESIGN_FILE'],
certificatePassword: process.env['WINDOWS_CODESIGN_PASSWORD'],
certificateFile: process.env.WINDOWS_CERTIFICATE_FILE,
certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD
}
}
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin', 'win32']
platforms: ['darwin']
},
{
name: '@electron-forge/maker-deb',
@@ -82,6 +46,10 @@ module.exports = {
{
name: '@electron-forge/maker-rpm',
platforms: ['linux']
},
{
name: '@electron-forge/maker-flatpak',
platforms: ['linux']
}
]
};

5548
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,15 @@
{
"name": "windows95",
"productName": "windows95",
"version": "3.1.2",
"version": "1.5.0",
"description": "Windows 95, in an app. I'm sorry.",
"main": "./dist/src/main/main.js",
"main": "src/index.js",
"scripts": {
"start": "rimraf ./dist && electron-forge start",
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "prettier --write src/**/*.{ts,tsx} && npm run check-links",
"less": "node ./tools/lessc.js",
"tsc": "tsc -p tsconfig.json --noEmit",
"check-links": "node tools/check-links.js"
"lint": "standard \"src/**/*.js\""
},
"keywords": [],
"author": "Felix Rieseberg, felix@felixrieseberg.com",
@@ -20,32 +17,30 @@
"config": {
"forge": "./forge.config.js"
},
"standard": {
"globals": [
"appState",
"V86Starter",
"windows95"
],
"ignore": [
"/src/renderer/lib/*.js"
]
},
"dependencies": {
"electron-squirrel-startup": "^1.0.0",
"fs-extra": "^10.1.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"tslib": "^2.4.0",
"update-electron-app": "^2.0.1"
"fs-extra": "^7.0.1",
"update-electron-app": "^1.3.0"
},
"devDependencies": {
"@electron-forge/cli": "6.2.1",
"@electron-forge/maker-deb": "6.2.1",
"@electron-forge/maker-flatpak": "^6.2.1",
"@electron-forge/maker-rpm": "^6.2.1",
"@electron-forge/maker-squirrel": "^6.2.1",
"@electron-forge/maker-zip": "^6.2.1",
"@electron-forge/publisher-github": "^6.2.1",
"@types/fs-extra": "^9.0.13",
"@types/node": "^12.19.9",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"electron": "25.3.0",
"less": "^3.13.0",
"node-abi": "^3.45.0",
"parcel-bundler": "^1.12.5",
"prettier": "^3.0.0",
"rimraf": "^5.0.1",
"typescript": "^5.1.6"
"@electron-forge/cli": "^6.0.0-beta.32",
"@electron-forge/maker-deb": "^6.0.0-beta.32",
"@electron-forge/maker-flatpak": "^6.0.0-beta.32",
"@electron-forge/maker-rpm": "^6.0.0-beta.32",
"@electron-forge/maker-squirrel": "^6.0.0-beta.32",
"@electron-forge/maker-zip": "^6.0.0-beta.32",
"electron": "4.0.4",
"node-abi": "^2.6.0",
"standard": "^12.0.1"
}
}

25
src/cache.js Normal file
View File

@@ -0,0 +1,25 @@
const { session } = require('electron')
const clearCaches = async () => {
await clearCache()
await clearStorageData()
}
const clearCache = () => {
return new Promise((resolve) => {
session.defaultSession.clearCache(resolve)
})
}
const clearStorageData = () => {
return new Promise((resolve) => {
session.defaultSession.clearStorageData({
storages: 'appcache, cookies, filesystem, indexdb, localstorage, shadercache, websql, serviceworkers',
quotas: 'temporary, persistent, syncable'
}, resolve)
})
}
module.exports = {
clearCaches
}

View File

@@ -1,32 +0,0 @@
import { session } from "electron";
export async function clearCaches() {
await clearCache();
await clearStorageData();
}
export async function clearCache() {
if (session.defaultSession) {
await session.defaultSession.clearCache();
}
}
export async function clearStorageData() {
if (!session.defaultSession) {
return;
}
await session.defaultSession.clearStorageData({
storages: [
"appcache",
"cookies",
"filesystem",
"indexdb",
"localstorage",
"shadercache",
"websql",
"serviceworkers",
],
quotas: ["temporary", "persistent", "syncable"],
});
}

24
src/constants.js Normal file
View File

@@ -0,0 +1,24 @@
const { remote, app } = require('electron')
const path = require('path')
const _app = app || remote.app
const CONSTANTS = {
IMAGE_PATH: path.join(__dirname, 'images/windows95.img'),
IMAGE_DEFAULT_SIZE: 1073741824, // 1GB
DEFAULT_STATE_PATH: path.join(__dirname, 'images/default-state.bin'),
STATE_PATH: path.join(_app.getPath('userData'), 'state-v2.bin')
}
const IPC_COMMANDS = {
TOGGLE_INFO: 'TOGGLE_INFO',
MACHINE_RESTART: 'MACHINE_RESTART',
MACHINE_RESET: 'MACHINE_RESET',
MACHINE_CTRL_ALT_DEL: 'MACHINE_CTRL_ALT_DEL',
SHOW_DISK_IMAGE: 'SHOW_DISK_IMAGE'
}
module.exports = {
CONSTANTS,
IPC_COMMANDS
}

View File

@@ -1,30 +0,0 @@
import * as path from "path";
export const CONSTANTS = {
IMAGE_PATH: path.join(__dirname, "../../images/windows95.img"),
IMAGE_DEFAULT_SIZE: 1073741824, // 1GB
DEFAULT_STATE_PATH: path.join(__dirname, "../../images/default-state.bin"),
};
export const IPC_COMMANDS = {
TOGGLE_INFO: "TOGGLE_INFO",
SHOW_DISK_IMAGE: "SHOW_DISK_IMAGE",
ZOOM_IN: "ZOOM_IN",
ZOOM_OUT: "ZOOM_OUT",
ZOOM_RESET: "ZOOM_RESET",
// Machine instructions
MACHINE_START: "MACHINE_START",
MACHINE_RESTART: "MACHINE_RESTART",
MACHINE_STOP: "MACHINE_STOP",
MACHINE_RESET: "MACHINE_RESET",
MACHINE_ALT_F4: "MACHINE_ALT_F4",
MACHINE_ESC: "MACHINE_ESC",
MACHINE_ALT_ENTER: "MACHINE_ALT_ENTER",
MACHINE_CTRL_ALT_DEL: "MACHINE_CTRL_ALT_DEL",
// Machine events
MACHINE_STARTED: "MACHINE_STARTED",
MACHINE_STOPPED: "MACHINE_STOPPED",
// Else
APP_QUIT: "APP_QUIT",
GET_STATE_PATH: "GET_STATE_PATH",
};

29
src/es6.js Normal file
View File

@@ -0,0 +1,29 @@
const { protocol } = require('electron')
const fs = require('fs-extra')
const path = require('path')
const ES6_PATH = path.join(__dirname, 'renderer')
protocol.registerStandardSchemes(['es6'])
async function setupProtocol () {
protocol.registerBufferProtocol('es6', async (req, cb) => {
console.log(req)
try {
const filePath = path.join(ES6_PATH, req.url.replace('es6://', ''))
.replace('.js/', '.js')
.replace('.js\\', '.js')
const fileContent = await fs.readFile(filePath)
cb({ mimeType: 'text/javascript', data: fileContent }) // eslint-disable-line
} catch (error) {
console.warn(error)
}
})
}
module.exports = {
setupProtocol
}

55
src/index.js Normal file
View File

@@ -0,0 +1,55 @@
const { app, BrowserWindow } = require('electron')
const path = require('path')
const { createMenu } = require('./menu')
const { setupProtocol } = require('./es6')
if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
app.quit()
}
if (app.isPackaged) {
require('update-electron-app')({
repo: 'felixrieseberg/windows95',
updateInterval: '1 hour'
})
}
let mainWindow
const createWindow = () => {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1024,
height: 768,
useContentSize: true,
webPreferences: {
nodeIntegration: false,
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadURL(`file://${__dirname}/renderer/index.html`)
mainWindow.on('closed', () => {
mainWindow = null
})
}
app.on('ready', async () => {
await setupProtocol()
await createMenu()
createWindow()
})
// Quit when all windows are closed.
app.on('window-all-closed', () => {
app.quit()
})
app.on('activate', () => {
if (mainWindow === null) {
createWindow()
}
})

View File

@@ -1,24 +0,0 @@
#emulator {
height: 100vh;
width: 100vw;
display: flex;
> div {
white-space: pre;
font: 14px monospace;
line-height: 14px
}
> canvas {
display: none;
margin: auto;
}
}
.paused {
canvas {
opacity: 0.2;
filter: blur(2px);
z-index: -100;
}
}

View File

@@ -1,7 +0,0 @@
#information {
text-align: center;
position: absolute;
width: 100vw;
bottom: 50px;
font-size: 18px;
}

View File

@@ -1,117 +0,0 @@
@import "./status.less";
@import "./emulator.less";
@import "./info.less";
@import "./settings.less";
@import "./start.less";
/* GENERAL RESETS */
html, body {
margin: 0;
padding: 0;
}
body {
background: #000;
}
body.paused > #emulator {
display: none;
}
body.paused {
background: #008080;
font-family: Courier;
}
#buttons {
user-select: none;
}
section {
display: flex;
position: absolute;
width: 100vw;
height: 100vh;
align-items: center;
justify-content: center;
}
.card {
width: 75%;
max-width: 700px;
min-width: 400px;
.card-title {
img {
margin-right: 5px;
}
}
}
.nav-link > img,
.btn > img {
height: 24px;
margin-right: 4px;
}
.windows95 {
* {
user-select: none;
}
*:focus {
outline: none;
}
nav .nav-link,
nav .nav-logo {
height: 37px;
display: flex;
}
nav .nav-logo img {
margin-left: 2px;
max-height: 20px;
}
nav .nav-logo > span {
position: absolute;
top: 9px;
left: 37px;
font-weight: bold;
}
.btn {
height: 40px;
padding-top: 3px;
}
.btn:focus {
border-color: #fff #000 #000 #fff;
outline: 5px auto -webkit-focus-ring-color;
}
.btn.active:before,
.btn:focus:before,
button.active:before,
button:focus:before,
input[type=submit].active:before,
input[type=submit]:focus:before {
border-color: #dedede grey grey #dedede;
}
.card {
// Fix link colors
.link, .link:active, .link:link, .link:visited, a, a:active, a:link, a:visited {
color: #008080;
text-decoration: underline;
cursor: pointer;
}
// Ensure a-elements in fieldsets receive click events
fieldset:before {
pointer-events: none;
}
}
}

View File

@@ -1,21 +0,0 @@
#floppy-path {
font-size: .6rem;
width: 100%;
height: 30px;
padding-left: 8px;
border-color: #000 #fff #fff #000;
border-style: solid;
border-width: 2px;
background-color: #c3c3c3;
line-height: 27px;
}
#file-input {
display: none;
}
.settings {
legend > img {
margin-right: 5px;
}
}

View File

@@ -1,9 +0,0 @@
#section-start {
display: flex;
flex-direction: column;
> small {
margin-top: 25px;
font-size: .8rem;
}
}

View File

@@ -1,16 +0,0 @@
#status {
user-select: none;
position: absolute;
z-index: 100;
left: calc(50vw - 110px);
background: white;
font-size: 10px;
padding-bottom: 3px;
border-bottom-left-radius: 15px;
border-bottom-right-radius: 15px;
overflow: hidden;
padding-left: 10px;
padding-right: 10px;
max-height: 18px;
top: 0;
}

BIN
src/less/vendor/95.ttf vendored

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 Yoshi Mannaert
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -1,28 +0,0 @@
import { AboutPanelOptionsOptions, app } from "electron";
/**
* Sets Fiddle's About panel options on Linux and macOS
*
* @returns
*/
export function setupAboutPanel(): void {
if (process.platform === "win32") return;
const options: AboutPanelOptionsOptions = {
applicationName: "windows95",
applicationVersion: app.getVersion(),
version: process.versions.electron,
copyright: "Felix Rieseberg",
};
switch (process.platform) {
case "linux":
options.website = "https://github.com/felixrieseberg/windows95";
case "darwin":
options.credits = "https://github.com/felixrieseberg/windows95";
default:
// fallthrough
}
app.setAboutPanelOptions(options);
}

View File

@@ -1,14 +0,0 @@
import { ipcMain, app } from "electron";
import * as path from "path";
import { IPC_COMMANDS } from "../constants";
export function setupIpcListeners() {
ipcMain.handle(IPC_COMMANDS.GET_STATE_PATH, () => {
return path.join(app.getPath("userData"), "state-v3.bin");
});
ipcMain.handle(IPC_COMMANDS.APP_QUIT, () => {
app.quit();
});
}

View File

@@ -1,69 +0,0 @@
import { app } from "electron";
import { isDevMode } from "../utils/devmode";
import { setupAboutPanel } from "./about-panel";
import { shouldQuit } from "./squirrel";
import { setupUpdates } from "./update";
import { getOrCreateWindow } from "./windows";
import { setupMenu } from "./menu";
import { setupIpcListeners } from "./ipc";
/**
* Handle the app's "ready" event. This is essentially
* the method that takes care of booting the application.
*/
export async function onReady() {
if (!isDevMode()) process.env.NODE_ENV = "production";
setupIpcListeners();
getOrCreateWindow();
setupAboutPanel();
setupMenu();
setupUpdates();
}
/**
* Handle the "before-quit" event
*
* @export
*/
export function onBeforeQuit() {
(global as any).isQuitting = true;
}
/**
* All windows have been closed, quit on anything but
* macOS.
*/
export function onWindowsAllClosed() {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== "darwin") {
app.quit();
}
}
/**
* The main method - and the first function to run
* when Fiddle is launched.
*
* Exported for testing purposes.
*/
export function main() {
// Handle creating/removing shortcuts on Windows when
// installing/uninstalling.
if (shouldQuit()) {
app.quit();
return;
}
// Set the app's name
app.setName("windows95");
// Launch
app.on("ready", onReady);
app.on("before-quit", onBeforeQuit);
app.on("window-all-closed", onWindowsAllClosed);
}
main();

View File

@@ -1,259 +0,0 @@
import { app, shell, Menu, BrowserWindow, ipcMain } from "electron";
import { clearCaches } from "../cache";
import { IPC_COMMANDS } from "../constants";
import { isDevMode } from "../utils/devmode";
const LINKS = {
homepage: "https://www.twitter.com/felixrieseberg",
repo: "https://github.com/felixrieseberg/windows95",
credits: "https://github.com/felixrieseberg/windows95/blob/master/CREDITS.md",
help: "https://github.com/felixrieseberg/windows95/blob/master/HELP.md",
};
export async function setupMenu() {
await createMenu();
ipcMain.on(IPC_COMMANDS.MACHINE_STARTED, () =>
createMenu({ isRunning: true })
);
ipcMain.on(IPC_COMMANDS.MACHINE_STOPPED, () =>
createMenu({ isRunning: false })
);
}
function send(cmd: string) {
const windows = BrowserWindow.getAllWindows();
if (windows[0]) {
console.log(`Sending "${cmd}"`);
windows[0].webContents.send(cmd);
} else {
console.log(`Tried to send "${cmd}", but could not find window`);
}
}
async function createMenu({ isRunning } = { isRunning: false }) {
const template: Array<Electron.MenuItemConstructorOptions> = [
{
label: "View",
submenu: [
{
label: "Toggle Full Screen",
accelerator: (function () {
if (process.platform === "darwin") {
return "Ctrl+Command+F";
} else {
return "F11";
}
})(),
click: function (_item, focusedWindow) {
if (focusedWindow) {
focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
}
},
},
{
label: "Toggle Developer Tools",
accelerator: (function () {
if (process.platform === "darwin") {
return "Alt+Command+I";
} else {
return "Ctrl+Shift+I";
}
})(),
click: function (_item, focusedWindow) {
if (focusedWindow) {
focusedWindow.webContents.toggleDevTools();
}
},
},
{
type: "separator",
},
{
label: "Toggle Emulator Info",
click: () => send(IPC_COMMANDS.TOGGLE_INFO),
},
{
type: "separator",
},
{
role: "reload",
},
],
},
{
role: "editMenu",
visible: isDevMode(),
},
{
label: "Window",
role: "window",
submenu: [
{
label: "Minimize",
accelerator: "CmdOrCtrl+M",
role: "minimize",
},
{
label: "Close",
accelerator: "CmdOrCtrl+W",
role: "close",
},
{
type: "separator",
},
{
label: "Zoom in",
click: () => send(IPC_COMMANDS.ZOOM_IN),
enabled: isRunning,
},
{
label: "Zoom out",
click: () => send(IPC_COMMANDS.ZOOM_OUT),
enabled: isRunning,
},
{
label: "Reset zoom",
click: () => send(IPC_COMMANDS.ZOOM_RESET),
enabled: isRunning,
},
],
},
{
label: "Machine",
submenu: [
{
label: "Send Ctrl+Alt+Del",
click: () => send(IPC_COMMANDS.MACHINE_CTRL_ALT_DEL),
enabled: isRunning,
},
{
label: "Send Alt+F4",
click: () => send(IPC_COMMANDS.MACHINE_ALT_F4),
enabled: isRunning,
},
{
label: "Send Alt+Enter",
click: () => send(IPC_COMMANDS.MACHINE_ALT_ENTER),
enabled: isRunning,
},
{
label: "Send Esc",
click: () => send(IPC_COMMANDS.MACHINE_ESC),
enabled: isRunning,
},
{
type: "separator",
},
isRunning
? {
label: "Stop",
click: () => send(IPC_COMMANDS.MACHINE_STOP),
}
: {
label: "Start",
click: () => send(IPC_COMMANDS.MACHINE_START),
},
{
label: "Restart",
click: () => send(IPC_COMMANDS.MACHINE_RESTART),
enabled: isRunning,
},
{
label: "Reset",
click: () => send(IPC_COMMANDS.MACHINE_RESET),
enabled: isRunning,
},
{
type: "separator",
},
{
label: "Go to Disk Image",
click: () => send(IPC_COMMANDS.SHOW_DISK_IMAGE),
},
],
},
{
label: "Help",
role: "help",
submenu: [
{
label: "Author",
click: () => shell.openExternal(LINKS.homepage),
},
{
label: "windows95 on GitHub",
click: () => shell.openExternal(LINKS.repo),
},
{
label: "Help",
click: () => shell.openExternal(LINKS.help),
},
{
type: "separator",
},
{
label: "Troubleshooting",
submenu: [
{
label: "Clear Cache and Restart",
async click() {
await clearCaches();
app.relaunch();
app.quit();
},
},
],
},
],
},
];
if (process.platform === "darwin") {
template.unshift({
label: "windows95",
submenu: [
{
role: "about",
},
{
type: "separator",
},
{
role: "services",
},
{
type: "separator",
},
{
label: "Hide windows95",
accelerator: "Command+H",
role: "hide",
},
{
label: "Hide Others",
accelerator: "Command+Shift+H",
role: "hideothers",
},
{
role: "unhide",
},
{
type: "separator",
},
{
label: "Quit",
accelerator: "Command+Q",
click() {
app.quit();
},
},
],
} as any);
}
Menu.setApplicationMenu(Menu.buildFromTemplate(template as any));
}

View File

@@ -1,3 +0,0 @@
export function shouldQuit() {
return require("electron-squirrel-startup");
}

View File

@@ -1,10 +0,0 @@
import { app } from "electron";
export function setupUpdates() {
if (app.isPackaged) {
require("update-electron-app")({
repo: "felixrieseberg/windows95",
updateInterval: "1 hour",
});
}
}

View File

@@ -1,43 +0,0 @@
import { BrowserWindow, shell } from "electron";
let mainWindow: BrowserWindow | null = null;
export function getOrCreateWindow(): BrowserWindow {
if (mainWindow) return mainWindow;
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1024,
height: 768,
useContentSize: true,
webPreferences: {
nodeIntegration: true,
sandbox: false,
webviewTag: false,
contextIsolation: false,
},
});
// mainWindow.webContents.toggleDevTools();
mainWindow.loadFile("./dist/static/index.html");
mainWindow.webContents.on("will-navigate", (event, url) =>
handleNavigation(event, url)
);
mainWindow.webContents.on("new-window", (event, url) =>
handleNavigation(event, url)
);
mainWindow.on("closed", () => {
mainWindow = null;
});
return mainWindow;
}
function handleNavigation(event: Electron.Event, url: string) {
if (url.startsWith("http")) {
event.preventDefault();
shell.openExternal(url);
}
}

182
src/menu.js Normal file
View File

@@ -0,0 +1,182 @@
const { app, shell, Menu, BrowserWindow } = require('electron')
const { clearCaches } = require('./cache')
const { IPC_COMMANDS } = require('./constants')
const LINKS = {
homepage: 'https://www.twitter.com/felixrieseberg',
repo: 'https://github.com/felixrieseberg/windows95',
credits: 'https://github.com/felixrieseberg/windows95/blob/master/CREDITS.md',
help: 'https://github.com/felixrieseberg/windows95/blob/master/HELP.md'
}
function send (cmd) {
const windows = BrowserWindow.getAllWindows()
if (windows[0]) {
windows[0].webContents.send(cmd)
}
}
async function createMenu () {
const template = [
{
label: 'View',
submenu: [
{
label: 'Toggle Full Screen',
accelerator: (function () {
if (process.platform === 'darwin') { return 'Ctrl+Command+F' } else { return 'F11' }
})(),
click: function (_item, focusedWindow) {
if (focusedWindow) { focusedWindow.setFullScreen(!focusedWindow.isFullScreen()) }
}
},
{
label: 'Toggle Developer Tools',
accelerator: (function () {
if (process.platform === 'darwin') { return 'Alt+Command+I' } else { return 'Ctrl+Shift+I' }
})(),
click: function (_item, focusedWindow) {
if (focusedWindow) { focusedWindow.toggleDevTools() }
}
},
{
type: 'separator'
},
{
label: 'Toggle Emulator Info',
click: () => send(IPC_COMMANDS.TOGGLE_INFO)
}
]
},
{
label: 'Window',
role: 'window',
submenu: [
{
label: 'Minimize',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
},
{
label: 'Close',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}
]
},
{
label: 'Machine',
submenu: [
{
label: 'Send Ctrl+Alt+Del',
click: () => send(IPC_COMMANDS.MACHINE_CTRL_ALT_DEL)
},
{
label: 'Restart',
click: () => send(IPC_COMMANDS.MACHINE_RESTART)
},
{
label: 'Reset',
click: () => send(IPC_COMMANDS.MACHINE_RESET)
},
{
type: 'separator'
},
{
label: 'Go to Disk Image',
click: () => send(IPC_COMMANDS.SHOW_DISK_IMAGE)
}
]
},
{
label: 'Help',
role: 'help',
submenu: [
{
label: 'Author',
click: () => shell.openExternal(LINKS.homepage)
},
{
label: 'windows95 on GitHub',
click: () => shell.openExternal(LINKS.repo)
},
{
label: 'Help',
click: () => shell.openExternal(LINKS.help)
},
{
type: 'separator'
},
{
label: 'Troubleshooting',
submenu: [
{
label: 'Clear Cache and Restart',
async click () {
await clearCaches()
app.relaunch()
app.quit()
}
}
]
}
]
}
]
if (process.platform === 'darwin') {
template.unshift({
label: 'windows95',
submenu: [
{
label: 'About windows95',
role: 'about'
},
{
type: 'separator'
},
{
label: 'Services',
role: 'services',
submenu: []
},
{
type: 'separator'
},
{
label: 'Hide windows95',
accelerator: 'Command+H',
role: 'hide'
},
{
label: 'Hide Others',
accelerator: 'Command+Shift+H',
role: 'hideothers'
},
{
label: 'Show All',
role: 'unhide'
},
{
type: 'separator'
},
{
label: 'Quit',
accelerator: 'Command+Q',
click () {
app.quit()
}
}
]
})
}
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
}
module.exports = {
createMenu
}

42
src/preload.js Normal file
View File

@@ -0,0 +1,42 @@
const { remote, shell, ipcRenderer } = require('electron')
const path = require('path')
const EventEmitter = require('events')
const { resetState, restoreState, saveState } = require('./state')
const { getDiskImageSize } = require('./utils/disk-image-size')
const { IPC_COMMANDS, CONSTANTS } = require('./constants')
class Windows95 extends EventEmitter {
constructor () {
super()
// Constants
this.CONSTANTS = CONSTANTS
this.IPC_COMMANDS = IPC_COMMANDS
// Methods
this.getDiskImageSize = getDiskImageSize
this.restoreState = restoreState
this.resetState = resetState
this.saveState = saveState
Object.keys(IPC_COMMANDS).forEach((command) => {
ipcRenderer.on(command, (...args) => {
this.emit(command, args)
})
})
}
showDiskImage () {
const imagePath = path.join(__dirname, 'images/windows95.img')
.replace('app.asar', 'app.asar.unpacked')
shell.showItemInFolder(imagePath)
}
quit () {
remote.app.quit()
}
}
window.windows95 = new Windows95()

View File

@@ -0,0 +1,9 @@
export function setupState () {
window.appState = {
isResetting: false,
isQuitting: false,
cursorCaptured: false,
floppyFile: null,
bootFresh: false
}
}

View File

@@ -1,34 +0,0 @@
/**
* The top-level class controlling the whole app. This is *not* a React component,
* but it does eventually render all components.
*
* @class App
*/
export class App {
/**
* Initial setup call, loading Monaco and kicking off the React
* render process.
*/
public async setup(): Promise<void | Element> {
const React = await import("react");
const { render } = await import("react-dom");
const { Emulator } = await import("./emulator");
const className = `${process.platform}`;
const app = (
<div className={className}>
<Emulator />
</div>
);
const rendered = render(app, document.getElementById("app"));
return rendered;
}
}
window["win95"] = window["win95"] || {
app: new App(),
};
window["win95"].app.setup();

1
src/renderer/bios/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.gz

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

44
src/renderer/buttons.js Normal file
View File

@@ -0,0 +1,44 @@
const $ = document.querySelector.bind(document)
export function setupButtons (start) {
// Start
$('.btn-start').addEventListener('click', () => start())
// Disk Image
$('#show-disk-image').addEventListener('click', () => windows95.showDiskImage())
// Reset
$('#reset').addEventListener('click', () => windows95.resetState())
$('#discard-state').addEventListener('click', () => {
window.appState.bootFresh = true
start()
})
// Floppy
$('#floppy').addEventListener('click', () => {
$('#file-input').click()
})
// Floppy (Hidden Input)
$('#file-input').addEventListener('change', (event) => {
window.appState.floppyFile = event.target.files && event.target.files.length > 0
? event.target.files[0]
: null
if (window.appState.floppyFile) {
$('#floppy-path').innerHTML = `Inserted Floppy Disk: ${window.appState.floppyFile.path}`
}
})
}
export function toggleButtons (forceTo) {
const buttonElements = $('#buttons')
if (buttonElements.style.display !== 'none' || forceTo === false) {
buttonElements.style.display = 'none'
} else {
buttonElements.style.display = undefined
}
}

View File

@@ -1,106 +0,0 @@
import * as React from "react";
interface CardDriveProps {
showDiskImage: () => void;
}
interface CardDriveState {}
export class CardDrive extends React.Component<CardDriveProps, CardDriveState> {
constructor(props: CardDriveProps) {
super(props);
this.state = {};
}
public render() {
let advice: JSX.Element | null = null;
if (process.platform === "win32") {
advice = this.renderAdviceWindows();
} else if (process.platform === "darwin") {
advice = this.renderAdviceMac();
} else {
advice = this.renderAdviceLinux();
}
return (
<section>
<div className="card settings">
<div className="card-header">
<h2 className="card-title">
<img src="../../static/drive.png" />
Modify C: Drive
</h2>
</div>
<div className="card-body">
<p>
windows95 (this app) uses a raw disk image. Windows 95 (the
operating system) is fragile, so adding or removing files is
risky.
</p>
{advice}
</div>
</div>
</section>
);
}
public renderAdviceWindows(): JSX.Element {
return (
<fieldset>
<legend>Changing the disk on Windows</legend>
<p>
Windows 10 cannot mount raw disk images (ironically, macOS and Linux
can). However, tools exist that let you mount this drive, like the
freeware tool{" "}
<a
target="_blank"
href="https://www.osforensics.com/tools/mount-disk-images.html"
>
OSFMount
</a>
. I am not affiliated with it, so please use it at your own risk.
</p>
{this.renderMountButton("Windows Explorer")}
</fieldset>
);
}
public renderAdviceMac(): JSX.Element {
return (
<fieldset>
<legend>Changing the disk on macOS</legend>
<p>
macOS can mount the disk image directly. Click the button below to see
the disk image in Finder. Then, double-click the image to mount it.
</p>
{this.renderMountButton("Finder")}
</fieldset>
);
}
public renderAdviceLinux(): JSX.Element {
return (
<fieldset>
<legend>Changing the disk on Linux</legend>
<p>
There are plenty of tools that enable Linux users to mount and modify
disk images. The disk image used by windows95 is a raw "img" disk
image and can probably be mounted using the <code>mount</code> tool,
which is likely installed on your machine.
</p>
{this.renderMountButton("file viewer")}
</fieldset>
);
}
public renderMountButton(explorer: string) {
return (
<button className="btn" onClick={this.props.showDiskImage}>
<img src="../../static/show-disk-image.png" />
<span>Show disk image in {explorer}</span>
</button>
);
}
}

View File

@@ -1,224 +0,0 @@
import * as React from "react";
import * as fs from "fs-extra";
import { getStatePath } from "./utils/get-state-path";
interface CardSettingsProps {
bootFromScratch: () => void;
setFloppy: (file: File) => void;
setCdrom: (cdrom: File) => void;
floppy?: File;
cdrom?: File;
}
interface CardSettingsState {
isStateReset: boolean;
}
export class CardSettings extends React.Component<
CardSettingsProps,
CardSettingsState
> {
constructor(props: CardSettingsProps) {
super(props);
this.onChangeFloppy = this.onChangeFloppy.bind(this);
this.onChangeCdrom = this.onChangeCdrom.bind(this);
this.onResetState = this.onResetState.bind(this);
this.state = {
isStateReset: false,
};
}
public render() {
return (
<section>
<div className="card settings">
<div className="card-header">
<h2 className="card-title">
<img src="../../static/settings.png" />
Settings
</h2>
</div>
<div className="card-body">
{this.renderCdrom()}
<hr />
{this.renderFloppy()}
<hr />
{this.renderState()}
</div>
</div>
</section>
);
}
public renderCdrom() {
// CD is currently not working, so.. let's return nothing.
return null;
const { cdrom } = this.props;
return (
<fieldset>
<legend>
<img src="../../static/cdrom.png" />
CD-ROM
</legend>
<input
id="cdrom-input"
type="file"
onChange={this.onChangeCdrom}
style={{ display: "none" }}
/>
<p>
windows95 comes with a virtual CD drive. It can mount images in the
"iso" format.
</p>
<p id="floppy-path">
{cdrom ? `Inserted CD: ${cdrom?.path}` : `No CD mounted`}
</p>
<button
className="btn"
onClick={() =>
(document.querySelector("#cdrom-input") as any).click()
}
>
<img src="../../static/select-cdrom.png" />
<span>Mount CD</span>
</button>
</fieldset>
);
}
public renderFloppy() {
const { floppy } = this.props;
return (
<fieldset>
<legend>
<img src="../../static/floppy.png" />
Floppy
</legend>
<input
id="floppy-input"
type="file"
onChange={this.onChangeFloppy}
style={{ display: "none" }}
/>
<p>
windows95 comes with a virtual floppy drive. It can mount floppy disk
images in the "img" format.
</p>
<p>
Back in the 90s and before CD-ROMs became a popular, software was
typically distributed on floppy disks. Some developers have since
released their apps or games for free, usually on virtual floppy disks
using the "img" format.
</p>
<p>
Once you've mounted a disk image, you might have to boot your virtual
windows95 machine from scratch.
</p>
<p id="floppy-path">
{floppy
? `Inserted Floppy Disk: ${floppy.path}`
: `No floppy mounted`}
</p>
<button
className="btn"
onClick={() =>
(document.querySelector("#floppy-input") as any).click()
}
>
<img src="../../static/select-floppy.png" />
<span>Mount floppy disk</span>
</button>
</fieldset>
);
}
public renderState() {
const { isStateReset } = this.state;
const { bootFromScratch } = this.props;
return (
<fieldset>
<legend>
<img src="../../static/reset.png" />
Reset machine state
</legend>
<div>
<p>
windows95 stores changes to your machine (like saved files) in a
state file. If you encounter any trouble, you can reset your state
or boot Windows 95 from scratch.{" "}
<strong>All your changes will be lost.</strong>
</p>
<button
className="btn"
onClick={this.onResetState}
disabled={isStateReset}
style={{ marginRight: "5px" }}
>
<img src="../../static/reset-state.png" />
{isStateReset ? "State reset" : "Reset state"}
</button>
<button className="btn" onClick={bootFromScratch}>
<img src="../../static/boot-fresh.png" />
Boot from scratch
</button>
</div>
</fieldset>
);
}
/**
* Handle a change in the floppy input
*
* @param event
*/
private onChangeFloppy(event: React.ChangeEvent<HTMLInputElement>) {
const floppyFile =
event.target.files && event.target.files.length > 0
? event.target.files[0]
: null;
if (floppyFile) {
this.props.setFloppy(floppyFile);
} else {
console.log(`Floppy: Input changed but no file selected`);
}
}
/**
* Handle a change in the cdrom input
*
* @param event
*/
private onChangeCdrom(event: React.ChangeEvent<HTMLInputElement>) {
const CdromFile =
event.target.files && event.target.files.length > 0
? event.target.files[0]
: null;
if (CdromFile) {
this.props.setCdrom(CdromFile);
} else {
console.log(`Cdrom: Input changed but no file selected`);
}
}
/**
* Handle the state reset
*/
private async onResetState() {
const statePath = await getStatePath();
if (fs.existsSync(statePath)) {
await fs.remove(statePath);
}
this.setState({ isStateReset: true });
}
}

View File

@@ -1,19 +0,0 @@
import * as React from "react";
export interface CardStartProps {
startEmulator: () => void;
}
export class CardStart extends React.Component<CardStartProps, {}> {
public render() {
return (
<section id="section-start">
<button className="btn" id="win95" onClick={this.props.startEmulator}>
<img src="../../static/run.png" />
<span>Start Windows 95</span>
</button>
<small>Hit ESC to lock or unlock your mouse</small>
</section>
);
}
}

View File

@@ -1,166 +0,0 @@
import * as React from "react";
interface EmulatorInfoProps {
toggleInfo: () => void;
emulator: any;
}
interface EmulatorInfoState {
cpu: number;
disk: string;
lastCounter: number;
lastTick: number;
}
export class EmulatorInfo extends React.Component<
EmulatorInfoProps,
EmulatorInfoState
> {
private cpuInterval = -1;
constructor(props: EmulatorInfoProps) {
super(props);
this.cpuCount = this.cpuCount.bind(this);
this.onIDEReadStart = this.onIDEReadStart.bind(this);
this.onIDEReadWriteEnd = this.onIDEReadWriteEnd.bind(this);
this.state = {
cpu: 0,
disk: "Idle",
lastCounter: 0,
lastTick: 0,
};
}
public render() {
const { cpu, disk } = this.state;
return (
<div id="status">
Disk: <span>{disk}</span> | CPU Speed: <span>{cpu}</span> |{" "}
<a href="#" onClick={this.props.toggleInfo}>
Hide
</a>
</div>
);
}
public componentWillUnmount() {
this.uninstallListeners();
}
/**
* The emulator starts whenever, so install or uninstall listeners
* at the right time
*
* @param newProps
*/
public componentDidUpdate(prevProps: EmulatorInfoProps) {
if (prevProps.emulator !== this.props.emulator) {
if (this.props.emulator) {
this.installListeners();
} else {
this.uninstallListeners();
}
}
}
/**
* Let's start listening to what the emulator is up to.
*/
private installListeners() {
const { emulator } = this.props;
if (!emulator) {
console.log(
`Emulator info: Tried to install listeners, but emulator not defined yet.`
);
return;
}
// CPU
if (this.cpuInterval > -1) {
clearInterval(this.cpuInterval);
}
// TypeScript think's we're using a Node.js setInterval. We're not.
this.cpuInterval = setInterval(this.cpuCount, 500) as unknown as number;
// Disk
emulator.add_listener("ide-read-start", this.onIDEReadStart);
emulator.add_listener("ide-read-end", this.onIDEReadWriteEnd);
emulator.add_listener("ide-write-end", this.onIDEReadWriteEnd);
// Screen
emulator.add_listener("screen-set-size-graphical", console.log);
}
/**
* Stop listening to the emulator.
*/
private uninstallListeners() {
const { emulator } = this.props;
if (!emulator) {
console.log(
`Emulator info: Tried to uninstall listeners, but emulator not defined yet.`
);
return;
}
// CPU
if (this.cpuInterval > -1) {
clearInterval(this.cpuInterval);
}
// Disk
emulator.remove_listener("ide-read-start", this.onIDEReadStart);
emulator.remove_listener("ide-read-end", this.onIDEReadWriteEnd);
emulator.remove_listener("ide-write-end", this.onIDEReadWriteEnd);
// Screen
emulator.remove_listener("screen-set-size-graphical", console.log);
}
/**
* The virtual IDE is handling read (start).
*/
private onIDEReadStart() {
this.requestIdle(() => this.setState({ disk: "Read" }));
}
/**
* The virtual IDE is handling read/write (end).
*/
private onIDEReadWriteEnd() {
this.requestIdle(() => this.setState({ disk: "Idle" }));
}
/**
* Request an idle callback with a 3s timeout.
*
* @param fn
*/
private requestIdle(fn: () => void) {
(window as any).requestIdleCallback(fn, { timeout: 3000 });
}
/**
* Calculates what's up with the virtual cpu.
*/
private cpuCount() {
const { lastCounter, lastTick } = this.state;
const now = Date.now();
const instructionCounter = this.props.emulator.get_instruction_counter();
const ips = instructionCounter - lastCounter;
const deltaTime = now - lastTick;
this.setState({
lastTick: now,
lastCounter: instructionCounter,
cpu: Math.round(ips / deltaTime),
});
}
}

View File

@@ -1,504 +0,0 @@
import * as React from "react";
import * as fs from "fs-extra";
import * as path from "path";
import { ipcRenderer, shell } from "electron";
import { CONSTANTS, IPC_COMMANDS } from "../constants";
import { getDiskImageSize } from "../utils/disk-image-size";
import { CardStart } from "./card-start";
import { StartMenu } from "./start-menu";
import { CardSettings } from "./card-settings";
import { EmulatorInfo } from "./emulator-info";
import { CardDrive } from "./card-drive";
import { getStatePath } from "./utils/get-state-path";
export interface EmulatorState {
currentUiCard: string;
emulator?: any;
scale: number;
floppyFile?: File;
cdromFile?: File;
isBootingFresh: boolean;
isCursorCaptured: boolean;
isInfoDisplayed: boolean;
isRunning: boolean;
}
export class Emulator extends React.Component<{}, EmulatorState> {
private isQuitting = false;
private isResetting = false;
constructor(props: {}) {
super(props);
this.startEmulator = this.startEmulator.bind(this);
this.stopEmulator = this.stopEmulator.bind(this);
this.restartEmulator = this.restartEmulator.bind(this);
this.resetEmulator = this.resetEmulator.bind(this);
this.bootFromScratch = this.bootFromScratch.bind(this);
this.state = {
isBootingFresh: false,
isCursorCaptured: false,
isRunning: false,
currentUiCard: "start",
isInfoDisplayed: true,
// We can start pretty large
// If it's too large, it'll just grow until it hits borders
scale: 2,
};
this.setupInputListeners();
this.setupIpcListeners();
this.setupUnloadListeners();
if (document.location.hash.includes("AUTO_START")) {
this.startEmulator();
}
}
/**
* We want to capture and release the mouse at appropriate times.
*/
public setupInputListeners() {
// ESC
document.onkeydown = (evt) => {
const { isCursorCaptured } = this.state;
evt = evt || window.event;
if (evt.keyCode === 27) {
if (isCursorCaptured) {
this.unlockMouse();
} else {
this.lockMouse();
}
evt.stopPropagation();
}
};
// Click
document.addEventListener("click", () => {
const { isRunning } = this.state;
if (isRunning) {
this.lockMouse();
}
});
}
/**
* Save the emulator's state to disk during exit.
*/
public setupUnloadListeners() {
const handleClose = async () => {
await this.saveState();
console.log(`Unload: Now done, quitting again.`);
this.isQuitting = true;
setImmediate(() => {
ipcRenderer.invoke(IPC_COMMANDS.APP_QUIT);
});
};
window.onbeforeunload = (event: Event) => {
if (this.isQuitting || this.isResetting) {
console.log(`Unload: Not preventing`);
return;
}
console.log(`Unload: Preventing to first save state`);
handleClose();
event.preventDefault();
event.returnValue = false;
};
}
/**
* Setup the various IPC messages sent to the renderer
* from the main process
*/
public setupIpcListeners() {
ipcRenderer.on(IPC_COMMANDS.MACHINE_CTRL_ALT_DEL, () => {
this.sendKeys([
0x1d, // ctrl
0x38, // alt
0x53, // delete
]);
});
ipcRenderer.on(IPC_COMMANDS.MACHINE_ALT_F4, () => {
this.sendKeys([
0x38, // alt
0x3e, // f4
]);
});
ipcRenderer.on(IPC_COMMANDS.MACHINE_ALT_ENTER, () => {
this.sendKeys([
0x38, // alt
0, // enter
]);
});
ipcRenderer.on(IPC_COMMANDS.MACHINE_ESC, () => {
this.sendKeys([
0x18, // alt
]);
});
ipcRenderer.on(IPC_COMMANDS.MACHINE_STOP, this.stopEmulator);
ipcRenderer.on(IPC_COMMANDS.MACHINE_RESET, this.resetEmulator);
ipcRenderer.on(IPC_COMMANDS.MACHINE_START, this.startEmulator);
ipcRenderer.on(IPC_COMMANDS.MACHINE_RESTART, this.restartEmulator);
ipcRenderer.on(IPC_COMMANDS.TOGGLE_INFO, () => {
this.setState({ isInfoDisplayed: !this.state.isInfoDisplayed });
});
ipcRenderer.on(IPC_COMMANDS.SHOW_DISK_IMAGE, () => {
this.showDiskImage();
});
ipcRenderer.on(IPC_COMMANDS.ZOOM_IN, () => {
this.setScale(this.state.scale * 1.2);
});
ipcRenderer.on(IPC_COMMANDS.ZOOM_OUT, () => {
this.setScale(this.state.scale * 0.8);
});
ipcRenderer.on(IPC_COMMANDS.ZOOM_RESET, () => {
this.setScale(1);
});
}
/**
* If the emulator isn't running, this is rendering the, erm, UI.
*
* 🤡
*/
public renderUI() {
const { isRunning, currentUiCard, floppyFile, cdromFile } = this.state;
if (isRunning) {
return null;
}
let card;
if (currentUiCard === "settings") {
card = (
<CardSettings
setFloppy={(floppyFile) => this.setState({ floppyFile })}
setCdrom={(cdromFile) => this.setState({ cdromFile })}
bootFromScratch={this.bootFromScratch}
floppy={floppyFile}
cdrom={cdromFile}
/>
);
} else if (currentUiCard === "drive") {
card = <CardDrive showDiskImage={this.showDiskImage} />;
} else {
card = <CardStart startEmulator={this.startEmulator} />;
}
return (
<>
{card}
<StartMenu
navigate={(target) => this.setState({ currentUiCard: target })}
/>
</>
);
}
/**
* Yaknow, render things and stuff.
*/
public render() {
return (
<>
{this.renderInfo()}
{this.renderUI()}
<div id="emulator">
<div></div>
<canvas></canvas>
</div>
</>
);
}
/**
* Render the little info thingy
*/
public renderInfo() {
if (!this.state.isInfoDisplayed) {
return null;
}
return (
<EmulatorInfo
emulator={this.state.emulator}
toggleInfo={() => {
this.setState({ isInfoDisplayed: !this.state.isInfoDisplayed });
}}
/>
);
}
/**
* Boot the emulator without restoring state
*/
public bootFromScratch() {
this.setState({ isBootingFresh: true });
this.startEmulator();
}
/**
* Show the disk image on disk
*/
public showDiskImage() {
// Contents/Resources/app/dist/static
const imagePath = path.join(__dirname, "../../images/windows95.img");
console.log(`Showing disk image in ${imagePath}`);
shell.showItemInFolder(imagePath);
}
/**
* Start the actual emulator
*/
private async startEmulator() {
document.body.classList.remove("paused");
const cdrom: any = {};
const cdromFile: any = this.state.cdromFile;
if (cdromFile?.path) {
cdrom.url = cdromFile.path;
cdrom.async = true;
cdrom.size = await getDiskImageSize(cdromFile.path);
}
const options = {
wasm_path: path.join(__dirname, "build/v86.wasm"),
memory_size: 128 * 1024 * 1024,
vga_memory_size: 32 * 1024 * 1024,
screen_container: document.getElementById("emulator"),
bios: {
url: path.join(__dirname, "../../bios/seabios.bin"),
},
vga_bios: {
url: path.join(__dirname, "../../bios/vgabios.bin"),
},
hda: {
url: CONSTANTS.IMAGE_PATH,
async: true,
size: await getDiskImageSize(CONSTANTS.IMAGE_PATH),
},
fda: {
buffer: this.state.floppyFile,
},
cdrom: cdrom,
boot_order: 0x132,
// One day, maybe!
// network_relay_url: "ws://localhost:8080/"
};
console.log(`🚜 Starting emulator with options`, options);
window["emulator"] = new V86Starter(options);
// New v86 instance
this.setState({
emulator: window["emulator"],
isRunning: true,
});
ipcRenderer.send(IPC_COMMANDS.MACHINE_STARTED);
// Restore state. We can't do this right away
// and randomly chose 500ms as the appropriate
// wait time (lol)
setTimeout(async () => {
if (!this.state.isBootingFresh) {
this.restoreState();
}
this.lockMouse();
this.state.emulator.run();
this.state.emulator.screen_set_scale(this.state.scale);
}, 500);
}
/**
* Restart emulator
*/
private restartEmulator() {
if (this.state.emulator && this.state.isRunning) {
console.log(`🚜 Restarting emulator`);
this.state.emulator.restart();
} else {
console.log(`🚜 Restarting emulator failed: Emulator not running`);
}
}
/**
* Stop the emulator
*/
private async stopEmulator() {
const { emulator, isRunning } = this.state;
if (!emulator || !isRunning) {
return;
}
console.log(`🚜 Stopping emulator`);
await this.saveState();
this.unlockMouse();
await emulator.stop();
this.setState({ isRunning: false });
document.body.classList.add("paused");
ipcRenderer.send(IPC_COMMANDS.MACHINE_STOPPED);
}
/**
* Reset the emulator by reloading the whole page (lol)
*/
private async resetEmulator() {
this.isResetting = true;
document.location.hash = `#AUTO_START`;
document.location.reload();
}
/**
* Take the emulators state and write it to disk. This is possibly
* a fairly big file.
*/
private async saveState(): Promise<void> {
const { emulator } = this.state;
const statePath = await getStatePath();
if (!emulator || !emulator.save_state) {
console.log(`restoreState: No emulator present`);
return;
}
try {
const newState = await emulator.save_state();
await fs.outputFile(statePath, Buffer.from(newState));
} catch (error) {
console.warn(`saveState: Could not save state`, error);
}
}
/**
* Restores state to the emulator.
*/
private async restoreState() {
const { emulator } = this.state;
const state = await this.getState();
// Nothing to do with if we don't have a state
if (!state) {
console.log(`restoreState: No state present, not restoring.`);
}
if (!emulator) {
console.log(`restoreState: No emulator present`);
}
try {
await this.state.emulator.restore_state(state);
} catch (error) {
console.log(
`State: Could not read state file. Maybe none exists?`,
error
);
}
}
/**
* Returns the current machine's state - either what
* we have saved or alternatively the default state.
*
* @returns {ArrayBuffer}
*/
private async getState(): Promise<ArrayBuffer | null> {
const expectedStatePath = await getStatePath();
const statePath = fs.existsSync(expectedStatePath)
? expectedStatePath
: CONSTANTS.DEFAULT_STATE_PATH;
if (fs.existsSync(statePath)) {
return fs.readFileSync(statePath).buffer;
}
return null;
}
private unlockMouse() {
const { emulator } = this.state;
this.setState({ isCursorCaptured: false });
if (emulator) {
emulator.mouse_set_status(false);
}
document.exitPointerLock();
}
private lockMouse() {
const { emulator } = this.state;
if (emulator) {
this.setState({ isCursorCaptured: true });
emulator.mouse_set_status(true);
emulator.lock_mouse();
} else {
console.warn(
`Emulator: Tried to lock mouse, but no emulator or not running`
);
}
}
/**
* Set the emulator's scale
*
* @param target
*/
private setScale(target: number) {
const { emulator, isRunning } = this.state;
if (emulator && isRunning) {
emulator.screen_set_scale(target);
this.setState({ scale: target });
}
}
/**
* Send keys to the emulator (including the key-up),
* if it's running
*
* @param {Array<number>} codes
*/
private sendKeys(codes: Array<number>) {
if (this.state.emulator && this.state.isRunning) {
const scancodes = codes;
// Push break codes (key-up)
for (const scancode of scancodes) {
scancodes.push(scancode | 0x80);
}
this.state.emulator.keyboard_send_scancodes(scancodes);
}
}
}

View File

@@ -1,2 +0,0 @@
declare const V86Starter: any;
declare const win95: any;

55
src/renderer/index.html Normal file
View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Windows</title>
<script src="./lib/libv86.js"></script>
<link rel="stylesheet" href="style/style.css">
</head>
<body class="paused">
<div id="status">
Disk: <span id="disk-status">Idle</span>
| CPU Speed: <span id="cpu-status">0</span>
| <a href="#" id="toggle-status">Hide</a>
</div>
<div id="buttons">
<div id="start-buttons">
<!-- <div class="btn" id="win98">Windows 98</div> -->
<div class="btn btn-start" id="win95">
Start Windows 95
<br />
<small>Hit ESC to lock or unlock your mouse</small>
</div>
<!-- <div class="btn" id="win1">Windows 1</div> -->
</div>
<div id="other-buttons">
<div class="btn" id="reset">Reset Machine & Delete State</div>
<div class="btn" id="floppy">Insert Floppy Disk</div>
<div class="btn" id="discard-state">Discard State & Boot From Scratch</div>
<div class="btn" id="show-disk-image">Show Disk Image</div>
<input id="file-input" type='file'>
</div>
<div id="information">
<p id="floppy-path"></p>
<p>You can insert a floppy disk image with the ".img" format.</p>
<p>
Boot the machine from scratch if you've inserted a new floppy disk
or if you've changed the disk image.
</p>
</div>
</div>
<div id="emulator" style="height: 100vh; width: 100vw">
<div style="white-space: pre; font: 14px monospace; line-height: 14px"></div>
<canvas style="display: none"></canvas>
</div>
<script type="module">
import("es6://renderer.js")
</script>
</body>
</html>

94
src/renderer/info.js Normal file
View File

@@ -0,0 +1,94 @@
const $ = document.querySelector.bind(document)
const status = $('#status')
const diskStatus = $('#disk-status')
const cpuStatus = $('#cpu-status')
const toggleStatus = $('#toggle-status')
let lastCounter = 0
let lastTick = 0
let infoInterval = null
const onIDEReadStart = () => {
diskStatus.innerHTML = 'Read'
}
const onIDEReadWriteEnd = () => {
diskStatus.innerHTML = 'Idle'
}
toggleStatus.onclick = toggleInfo
/**
* Toggle the information display
*/
export function toggleInfo () {
if (status.style.display !== 'none') {
disableInfo()
} else {
enableInfo()
}
}
/**
* Start information gathering, but only if the panel is visible
*/
export function startInfoMaybe () {
if (status.style.display !== 'none') {
enableInfo()
}
}
/**
* Enable the gathering of information (and hide the little information tab)
*/
export function enableInfo () {
// Show the info thingy
status.style.display = 'block'
// We can only do the rest with an emulator
if (!window.emulator.add_listener) {
return
}
// Set listeners
window.emulator.add_listener('ide-read-start', onIDEReadStart)
window.emulator.add_listener('ide-read-end', onIDEReadWriteEnd)
window.emulator.add_listener('ide-write-end', onIDEReadWriteEnd)
window.emulator.add_listener('screen-set-size-graphical', console.log)
// Set an interval
infoInterval = setInterval(() => {
const now = Date.now()
const instructionCounter = window.emulator.get_instruction_counter()
const ips = instructionCounter - lastCounter
const deltaTime = now - lastTick
lastTick = now
lastCounter = instructionCounter
cpuStatus.innerHTML = Math.round(ips / deltaTime)
}, 500)
}
/**
* Disable the gathering of information (and hide the little information tab)
*/
export function disableInfo () {
// Hide the info thingy
status.style.display = 'none'
// Clear the interval
clearInterval(infoInterval)
infoInterval = null
// We can only do the rest with an emulator
if (!window.emulator.remove_listener) {
return
}
// Unset the listeners
window.emulator.remove_listener('ide-read-start', onIDEReadStart)
window.emulator.remove_listener('ide-read-end', onIDEReadWriteEnd)
window.emulator.remove_listener('ide-write-end', onIDEReadWriteEnd)
window.emulator.remove_listener('screen-set-size-graphical', console.log)
}

44
src/renderer/ipc.js Normal file
View File

@@ -0,0 +1,44 @@
import { toggleInfo } from 'es6://info.js'
export function setupIpcListeners (start) {
const { windows95 } = window
windows95.addListener(windows95.IPC_COMMANDS.TOGGLE_INFO, () => {
toggleInfo()
})
windows95.addListener(windows95.IPC_COMMANDS.MACHINE_RESTART, () => {
console.log(`Restarting machine`)
if (!window.emulator || !window.emulator.is_running) return
window.emulator.restart()
})
windows95.addListener(windows95.IPC_COMMANDS.MACHINE_RESET, () => {
console.log(`Resetting machine`)
window.appState.isResetting = true
document.location.hash = `#AUTO_START`
document.location.reload()
})
windows95.addListener(windows95.IPC_COMMANDS.MACHINE_CTRL_ALT_DEL, () => {
if (!window.emulator || !window.emulator.is_running) return
window.emulator.keyboard_send_scancodes([
0x1D, // ctrl
0x38, // alt
0x53, // delete
// break codes
0x1D | 0x80,
0x38 | 0x80,
0x53 | 0x80
])
})
windows95.addListener(windows95.IPC_COMMANDS.SHOW_DISK_IMAGE, () => {
windows95.showDiskImage()
})
}

View File

@@ -1,4 +1,4 @@
Copyright (c) 2012, The v86 contributors
Copyright (c) 2012-2018, Fabian Hemmer
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -19,4 +19,8 @@ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those
of the authors and should not be interpreted as representing official policies,
either expressed or implied, of the FreeBSD Project.

Binary file not shown.

File diff suppressed because it is too large Load Diff

47
src/renderer/listeners.js Normal file
View File

@@ -0,0 +1,47 @@
export function setupCloseListener () {
window.appState.isQuitting = false
const handleClose = async () => {
await windows95.saveState()
window.appState.isQuitting = true
windows95.quit()
}
window.onbeforeunload = (event) => {
if (window.appState.isQuitting) return
if (window.appState.isResetting) return
handleClose()
event.preventDefault()
event.returnValue = false
}
}
export function setupEscListener () {
document.onkeydown = function (evt) {
evt = evt || window.event
if (evt.keyCode === 27) {
if (window.appState.cursorCaptured) {
window.appState.cursorCaptured = false
window.emulator.mouse_set_status(false)
document.exitPointerLock()
} else {
window.appState.cursorCaptured = true
window.emulator.lock_mouse()
}
}
}
}
function onDocumentClick () {
if (!window.appState.cursorCaptured) {
window.appState.cursorCaptured = true
window.emulator.mouse_set_status(true)
window.emulator.lock_mouse()
}
}
export function setupClickListener () {
document.removeEventListener('click', onDocumentClick)
document.addEventListener('click', onDocumentClick)
}

72
src/renderer/renderer.js Normal file
View File

@@ -0,0 +1,72 @@
/* We're using modern esm imports here */
import { setupState } from 'es6://app-state.js'
import { setupClickListener, setupEscListener, setupCloseListener } from 'es6://listeners.js'
import { toggleButtons, setupButtons } from 'es6://buttons.js'
import { startInfoMaybe } from 'es6://info.js'
import { setupIpcListeners } from 'es6://ipc.js'
setupState()
/**
* The main method executing the VM.
*/
async function main () {
const imageSize = await window.windows95.getDiskImageSize()
const options = {
memory_size: 128 * 1024 * 1024,
video_memory_size: 32 * 1024 * 1024,
screen_container: document.getElementById('emulator'),
bios: {
url: './bios/seabios.bin'
},
vga_bios: {
url: './bios/vgabios.bin'
},
hda: {
url: '../images/windows95.img',
async: true,
size: imageSize
},
fda: {
buffer: window.appState.floppyFile || undefined
},
boot_order: 0x132
}
console.log(`Starting emulator with options`, options)
// New v86 instance
window.emulator = new V86Starter(options)
// Restore state. We can't do this right away
// and randomly chose 500ms as the appropriate
// wait time (lol)
setTimeout(async () => {
if (!window.appState.bootFresh) {
windows95.restoreState()
}
startInfoMaybe()
window.appState.cursorCaptured = true
window.emulator.lock_mouse()
window.emulator.run()
}, 500)
}
function start () {
document.body.className = ''
toggleButtons(false)
setupClickListener()
main()
}
setupIpcListeners(start)
setupEscListener()
setupCloseListener()
setupButtons(start)
if (document.location.hash.includes('AUTO_START')) {
start()
}

View File

@@ -1,43 +0,0 @@
import * as React from "react";
export interface StartMenuProps {
navigate: (to: string) => void;
}
export class StartMenu extends React.Component<StartMenuProps, {}> {
constructor(props: StartMenuProps) {
super(props);
this.navigate = this.navigate.bind(this);
}
public render() {
return (
<nav className="nav nav-bottom">
<a onClick={this.navigate} href="#" id="start" className="nav-link">
<img src="../../static/start.png" alt="Start" />
<span>Start</span>
</a>
<div className="nav-menu">
<a
onClick={this.navigate}
href="#"
id="settings"
className="nav-link"
>
<img src="../../static/settings.png" />
<span>Settings</span>
</a>
<a onClick={this.navigate} href="#" id="drive" className="nav-link">
<img src="../../static/drive.png" />
<span>Modify C: Drive</span>
</a>
</div>
</nav>
);
}
private navigate(event: React.SyntheticEvent<HTMLAnchorElement>) {
this.props.navigate(event.currentTarget.id);
}
}

View File

@@ -0,0 +1,99 @@
html, body {
margin: 0;
padding: 0;
}
body {
background: #000;
}
body.paused > #emulator {
display: none;
}
body.paused {
background: #008080;
font-family: Courier;
}
#buttons {
user-select: none;
}
#status {
user-select: none;
position: absolute;
z-index: 100;
left: calc(50vw - 110px);
background: white;
text-align: center;
font-family: Courier;
font-size: 10px;
padding-bottom: 3px;
border-bottom-left-radius: 15px;
border-bottom-right-radius: 15px;
overflow: hidden;
padding-left: 10px;
padding-right: 10px;
max-height: 18px;
}
#floppy-path {
background: beige;
padding: 5px;
}
#information {
text-align: center;
position: absolute;
width: 100vw;
bottom: 50px;
font-size: 18px;
}
#emulator {
display: flex
}
#emulator canvas {
margin: auto;
}
#file-input {
display: none;
}
#other-buttons {
position: absolute;
width: 100vw;
height: 100px;
display: flex;
align-items: flex-end;
bottom: 0;
justify-content: center;
}
#start-buttons {
position: absolute;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.btn {
font-family: Courier;
cursor: pointer;
background: #ffd2fd;
margin: 10px;
padding: 5px;
text-align: center;
}
.btn:hover {
cursor: pointer;
background: #ff95fa;
margin: 10px;
padding: 5px;
}

View File

@@ -1,13 +0,0 @@
import { ipcRenderer } from "electron";
import { IPC_COMMANDS } from "../../constants";
let _statePath = "";
export async function getStatePath(): Promise<string> {
if (_statePath) {
return _statePath;
}
const statePath = await ipcRenderer.invoke(IPC_COMMANDS.GET_STATE_PATH);
return (_statePath = statePath);
}

81
src/state.js Normal file
View File

@@ -0,0 +1,81 @@
const fs = require('fs-extra')
const { CONSTANTS } = require('./constants')
/**
* Returns the current machine's state - either what
* we have saved or alternatively the default state.
*
* @returns {ArrayBuffer}
*/
function getState () {
const statePath = fs.existsSync(CONSTANTS.STATE_PATH)
? CONSTANTS.STATE_PATH
: CONSTANTS.DEFAULT_STATE_PATH
if (fs.existsSync(statePath)) {
return fs.readFileSync(statePath).buffer
}
}
/**
* Resets a saved state by simply deleting it.
*
* @returns {Promise<void>}
*/
async function resetState () {
if (fs.existsSync(CONSTANTS.STATE_PATH)) {
return fs.remove(CONSTANTS.STATE_PATH)
}
}
/**
* Saves the current VM's state.
*
* @returns {Promise<void>}
*/
async function saveState () {
return new Promise((resolve) => {
if (!window.emulator || !window.emulator.save_state) {
return resolve()
}
window.emulator.save_state(async (error, newState) => {
if (error) {
console.warn(`State: Could not save state`, error)
return
}
await fs.outputFile(CONSTANTS.STATE_PATH, Buffer.from(newState))
console.log(`State: Saved state to ${CONSTANTS.STATE_PATH}`)
resolve()
})
})
}
/**
* Restores the VM's state.
*/
function restoreState () {
const state = getState()
// Nothing to do with if we don't have a state
if (!state) {
console.log(`State: No state present, not restoring.`)
}
try {
window.emulator.restore_state(state)
} catch (error) {
console.log(`State: Could not read state file. Maybe none exists?`, error)
}
}
module.exports = {
saveState,
restoreState,
resetState,
getState
}

View File

@@ -1,8 +0,0 @@
/**
* Are we currently running in development mode?
*
* @returns {boolean}
*/
export function isDevMode() {
return !!process.defaultApp;
}

View File

@@ -0,0 +1,26 @@
const fs = require('fs-extra')
const { CONSTANTS } = require('../constants')
/**
* Get the size of the disk image
*
* @returns {number}
*/
async function getDiskImageSize () {
try {
const stats = await fs.stat(CONSTANTS.IMAGE_PATH)
if (stats) {
return stats.size
}
} catch (error) {
console.warn(`Could not determine image size`, error)
}
return CONSTANTS.IMAGE_DEFAULT_SIZE
}
module.exports = {
getDiskImageSize
}

View File

@@ -1,22 +0,0 @@
import * as fs from "fs-extra";
import { CONSTANTS } from "../constants";
/**
* Get the size of the disk image
*
* @returns {number}
*/
export async function getDiskImageSize(path: string) {
try {
const stats = await fs.stat(path);
if (stats) {
return stats.size;
}
} catch (error) {
console.warn(`Could not determine image size`, error);
}
return CONSTANTS.IMAGE_DEFAULT_SIZE;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>windows95</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../src/less/vendor/95css.css">
<link rel="stylesheet" href="../src/less/root.less">
<!-- libv86 -->
</head>
<body class="paused windows95">
<div id="app"></div>
<script src="../src/renderer/app.tsx"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -1,23 +0,0 @@
#!/usr/bin/env sh
KEY_CHAIN=build.keychain
MACOS_CERT_P12_FILE=certificate.p12
# Recreate the certificate from the secure environment variable
echo $MACOS_CERT_P12 | base64 --decode > $MACOS_CERT_P12_FILE
#create a keychain
security create-keychain -p actions $KEY_CHAIN
# Make the keychain the default so identities are found
security default-keychain -s $KEY_CHAIN
# Unlock the keychain
security unlock-keychain -p actions $KEY_CHAIN
security import $MACOS_CERT_P12_FILE -k $KEY_CHAIN -P $MACOS_CERT_PASSWORD -T /usr/bin/codesign;
security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN
# remove certs
rm -fr *.p12

View File

@@ -1,38 +0,0 @@
const fs = require('fs/promises')
const path = require('path')
const fetch = require('node-fetch')
const LINK_RGX = /(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?/g;
async function main() {
const readmePath = path.join(__dirname, '../README.md')
const readme = await fs.readFile(readmePath, 'utf-8')
const links = readme.match(LINK_RGX)
let failed = false
for (const link of links) {
try {
const response = await fetch(link, { method: 'HEAD' })
if (!response.ok) {
// If we're inside GitHub's release asset server, we just ran into AWS not allowing
// HEAD requests, which is different from a 404.
if (!response.url.startsWith('https://github-production-release-asset')) {
throw new Error (`HTTP Error Response: ${response.status} ${response.statusText}`)
}
}
console.log(`${link}`);
} catch (error) {
failed = true
console.log(`${link}\n${error}`)
}
}
if (failed) {
process.exit(-1);
}
}
main()

View File

@@ -1,11 +0,0 @@
mkdir images
cd images
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($env:DISK_URL, "$(Resolve-Path .)\images.zip")
7z x images.zip -y -aoa
Remove-Item images.zip
Remove-Item __MACOSX -Recurse -ErrorAction Ignore
cd ..
Tree ./ /F

View File

@@ -1,10 +0,0 @@
#!/usr/bin/env sh
mkdir -p ./images
cd ./images
wget -O images.zip $DISK_URL
unzip -o images.zip
rm images.zip
rm -r __MACOSX
cd -
ls images

View File

@@ -1,7 +0,0 @@
/* tslint:disable */
const { compileParcel } = require('./parcel-build')
module.exports = async () => {
await Promise.all([compileParcel()])
}

View File

@@ -1,72 +0,0 @@
/* tslint:disable */
const Bundler = require('parcel-bundler')
const path = require('path')
const fs = require('fs-extra')
async function copyLib() {
const target = path.join(__dirname, '../dist/static')
const lib = path.join(__dirname, '../src/renderer/lib')
const index = path.join(target, 'index.html')
// Copy in lib
await fs.copy(lib, target)
// Patch so that fs.read is used
const libv86path = path.join(target, 'libv86.js')
const libv86 = fs.readFileSync(libv86path, 'utf-8')
const patchedLibv86 = libv86.replace('v86util.load_file="undefined"===typeof XMLHttpRequest', 'v86util.load_file="undefined"!==typeof XMLHttpRequest')
fs.writeFileSync(libv86path, patchedLibv86)
// Overwrite
const indexContents = fs.readFileSync(index, 'utf-8');
const replacedContents = indexContents.replace('<!-- libv86 -->', '<script src="libv86.js"></script>')
fs.writeFileSync(index, replacedContents)
}
async function compileParcel (options = {}) {
const entryFiles = [
path.join(__dirname, '../static/index.html'),
path.join(__dirname, '../src/main/main.ts')
]
const bundlerOptions = {
outDir: './dist', // The out directory to put the build files in, defaults to dist
outFile: undefined, // The name of the outputFile
publicUrl: '../', // The url to server on, defaults to dist
watch: false, // whether to watch the files and rebuild them on change, defaults to process.env.NODE_ENV !== 'production'
cache: false, // Enabled or disables caching, defaults to true
cacheDir: '.cache', // The directory cache gets put in, defaults to .cache
contentHash: false, // Disable content hash from being included on the filename
minify: false, // Minify files, enabled if process.env.NODE_ENV === 'production'
scopeHoist: false, // turn on experimental scope hoisting/tree shaking flag, for smaller production bundles
target: 'electron', // browser/node/electron, defaults to browser
// https: { // Define a custom {key, cert} pair, use true to generate one or false to use http
// cert: './ssl/c.crt', // path to custom certificate
// key: './ssl/k.key' // path to custom key
// },
logLevel: 3, // 3 = log everything, 2 = log warnings & errors, 1 = log errors
hmr: false, // Enable or disable HMR while watching
hmrPort: 0, // The port the HMR socket runs on, defaults to a random free port (0 in node.js resolves to a random free port)
sourceMaps: true, // Enable or disable sourcemaps, defaults to enabled (minified builds currently always create sourcemaps)
hmrHostname: '', // A hostname for hot module reload, default to ''
detailedReport: false, // Prints a detailed report of the bundles, assets, filesizes and times, defaults to false, reports are only printed if watch is disabled,
...options
}
const bundler = new Bundler(entryFiles, bundlerOptions)
// Run the bundler, this returns the main bundle
// Use the events if you're using watch mode as this promise will only trigger once and not for every rebuild
await bundler.bundle()
await copyLib();
}
module.exports = {
compileParcel
}
if (require.main === module) compileParcel()

View File

@@ -1,11 +0,0 @@
const { compileParcel } = require('./parcel-build')
async function watchParcel () {
return compileParcel({ watch: true })
}
module.exports = {
watchParcel
}
if (require.main === module) watchParcel()

View File

@@ -1,30 +0,0 @@
/* tslint:disable */
const childProcess = require('child_process')
const path = require('path')
async function run (name, bin, args = []) {
await new Promise((resolve, reject) => {
console.info(`Running ${name}`)
const cmd = process.platform === 'win32' ? `${bin}.cmd` : bin
const child = childProcess.spawn(
path.resolve(__dirname, '..', 'node_modules', '.bin', cmd),
args,
{
cwd: path.resolve(__dirname, '..'),
stdio: 'inherit'
}
)
child.on('exit', (code) => {
console.log('')
if (code === 0) return resolve()
reject(new Error(`${name} failed`))
})
})
};
module.exports = {
run
}

Some files were not shown because too many files have changed in this diff Show More