Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58add05655 | ||
|
|
0a400d915f | ||
|
|
f615e7754c | ||
|
|
92717c8047 | ||
|
|
045b83f843 | ||
|
|
1dd3b76187 | ||
|
|
4b1dd6146c | ||
|
|
3601599ff1 | ||
|
|
6bf7678079 | ||
|
|
5396cae0f0 | ||
|
|
c5a24643fd | ||
|
|
59a651a205 | ||
|
|
f5cb94776a | ||
|
|
982c866899 | ||
|
|
9e8cef8da7 | ||
|
|
3b76a39060 | ||
|
|
e7d515de84 | ||
|
|
a4092f105a | ||
|
|
71a11cfbe3 | ||
|
|
f3c8f3409a | ||
|
|
8d8fc949cd | ||
|
|
0c2149b756 | ||
|
|
51d0011ed0 | ||
|
|
658fed75da | ||
|
|
186a2a8ba9 | ||
|
|
7653d7294c | ||
|
|
33ef8abcc8 | ||
|
|
ea134d046e | ||
|
|
c7f765df03 | ||
|
|
dcc3e72bcf | ||
|
|
241606d097 | ||
|
|
b7aa6a760d | ||
|
|
6b7bb0f460 | ||
|
|
90a97a11bf | ||
|
|
3598ceb97c | ||
|
|
dbcefb4b7b | ||
|
|
42719bb1d7 | ||
|
|
153002403b | ||
|
|
889c53857e | ||
|
|
5b8f3e12bc | ||
|
|
59299a2c48 | ||
|
|
885af7f786 | ||
|
|
55e88cd5b5 | ||
|
|
e0ad866256 | ||
|
|
cf3acd4182 | ||
|
|
a28aef8cf0 | ||
|
|
d2b8d9dd35 | ||
|
|
3802734ef0 | ||
|
|
1f478676f1 | ||
|
|
d19bbeee8f | ||
|
|
1e130b6140 | ||
|
|
e1c5992ff9 | ||
|
|
e879760678 | ||
|
|
2a11633171 | ||
|
|
b68d54ae62 | ||
|
|
9600630340 | ||
|
|
bae1909793 | ||
|
|
ee9e138034 | ||
|
|
5558671688 | ||
|
|
9a46ed5080 | ||
|
|
2c160d0f7f | ||
|
|
aafab62707 | ||
|
|
78126a57cb | ||
|
|
f5256ec7a2 | ||
|
|
6c1687c9a5 | ||
|
|
2c041115d0 | ||
|
|
987dc57309 | ||
|
|
614b18969d | ||
|
|
264ef7d069 | ||
|
|
e85cf4f1b2 | ||
|
|
e987da5460 | ||
|
|
a542639bc3 | ||
|
|
5d1928beb2 | ||
|
|
f1b657a53b | ||
|
|
6aa39e66ec | ||
|
|
ed42ea8e0e | ||
|
|
0779f18071 | ||
|
|
a9c4e38386 | ||
|
|
62b0909cb4 | ||
|
|
873cb75241 | ||
|
|
6467acb0c8 | ||
|
|
ed1bd0a1e0 | ||
|
|
ac84f4164e | ||
|
|
77569d4ce6 | ||
|
|
68b7c181ad | ||
|
|
293491477b | ||
|
|
7eb750752b | ||
|
|
f1488cedc2 | ||
|
|
9f366063eb | ||
|
|
55135f052e | ||
|
|
95fd8e4925 | ||
|
|
b794954da4 | ||
|
|
18a73c45a0 | ||
|
|
b83914060f | ||
|
|
93955564d9 | ||
|
|
d31920aaf4 | ||
|
|
cdfe47d92b | ||
|
|
b8259784e7 | ||
|
|
b70b9fabd5 | ||
|
|
f2c1fc4142 | ||
|
|
aeba364a7a | ||
|
|
a34ce54b56 | ||
|
|
e1477bfc05 | ||
|
|
71d6f16318 | ||
|
|
4bdbff6a4b | ||
|
|
e062548c81 | ||
|
|
609668c581 | ||
|
|
bec9577409 | ||
|
|
dd9ff5a319 | ||
|
|
f5125219fd | ||
|
|
b4fd81b364 | ||
|
|
e62a8cbed6 | ||
|
|
2038ce9c31 | ||
|
|
6c12063353 | ||
|
|
91783e7d26 | ||
|
|
9f8040bb22 | ||
|
|
c261079b67 | ||
|
|
6f337ac986 | ||
|
|
3189f3a8a2 | ||
|
|
5c1af3ae86 | ||
|
|
1ae2e5d546 | ||
|
|
7f3b23c5bf | ||
|
|
aa62c10700 | ||
|
|
86674b6090 | ||
|
|
90182076e6 | ||
|
|
10d2e37e9d | ||
|
|
e03e6148db | ||
|
|
59ee32806e | ||
|
|
9b32bf58d9 | ||
|
|
5bceb2e448 | ||
|
|
6918c425d6 | ||
|
|
8c3f608621 | ||
|
|
8b3117689e | ||
|
|
334a52b238 | ||
|
|
c11ae3caea | ||
|
|
1fbfca70b2 | ||
|
|
9c1ba25119 | ||
|
|
69d8b0d2be | ||
|
|
4f34053a84 | ||
|
|
5be27d7bab | ||
|
|
5ffbf4a106 |
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
text eol=lf
|
||||
115
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
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: 12.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
|
||||
- name: lint
|
||||
run: yarn lint
|
||||
build:
|
||||
needs: lint
|
||||
name: Build (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macOS-latest, ubuntu-latest, windows-latest ]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.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
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
WINDOWS_CODESIGN_FILE: ${{ steps.write_file.outputs.filePath }}
|
||||
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
|
||||
- name: Make (ia32)
|
||||
if: matrix.os == 'windows-latest' && startsWith(github.ref, 'refs/tags/')
|
||||
run: yarn make -- --arch=ia32
|
||||
env:
|
||||
WINDOWS_CODESIGN_FILE: ${{ steps.write_file.outputs.filePath }}
|
||||
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
|
||||
# - name: Archive production artifacts
|
||||
# uses: actions/upload-artifact@v2
|
||||
# with:
|
||||
# name: ${{ matrix.os }}
|
||||
# path: out/make/**/*
|
||||
- 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
|
||||
4
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
node_modules
|
||||
out
|
||||
src/renderer/images
|
||||
src/images
|
||||
.DS_Store
|
||||
images
|
||||
dist
|
||||
59
CREDITS.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# windows95 Credits
|
||||
|
||||
This app was made possible by three major engineering efforts:
|
||||
|
||||
* [v86 by Fabian Hemmer](https://github.com/copy/v86)
|
||||
* [Electron by the Electron Maintainers](https://electronjs.org)
|
||||
* Windows 95 by Microsoft
|
||||
|
||||
# v86 License and Copyright Notice
|
||||
|
||||
Copyright (c) 2012-2018, Fabian Hemmer
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
# Electron License and Copyright Notice
|
||||
|
||||
Copyright (c) 2013-2018 GitHub Inc.
|
||||
|
||||
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.
|
||||
45
Dockerfile
Normal file
@@ -0,0 +1,45 @@
|
||||
# DESCRIPTION: Run Windows 95 in a container
|
||||
# AUTHOR: Paul DeCarlo <toolboc@gmail.com>
|
||||
#
|
||||
# Made possible through prior art by:
|
||||
# copy (v86 - x86 virtualization in JavaScript)
|
||||
# felixrieseberg (Windows95 running in electron)
|
||||
# Microsoft (Windows 95)
|
||||
#
|
||||
# ***Docker Run Command***
|
||||
#
|
||||
# docker run -it \
|
||||
# -v /tmp/.X11-unix:/tmp/.X11-unix \ # mount the X11 socket
|
||||
# -e DISPLAY=unix$DISPLAY \ # pass the display
|
||||
# --device /dev/snd \ # sound
|
||||
# --name windows95 \
|
||||
# toolboc/windows95
|
||||
#
|
||||
# ***TroubleShooting***
|
||||
# If you receive Gtk-WARNING **: cannot open display: unix:0
|
||||
# Run:
|
||||
# xhost +
|
||||
#
|
||||
|
||||
FROM node:10.9-stretch
|
||||
|
||||
LABEL maintainer "Paul DeCarlo <toolboc@gmail.com>"
|
||||
|
||||
RUN apt update && apt install -y \
|
||||
libgtk-3-0 \
|
||||
libcanberra-gtk3-module \
|
||||
libx11-xcb-dev \
|
||||
libgconf2-dev \
|
||||
libnss3 \
|
||||
libasound2 \
|
||||
libxtst-dev \
|
||||
libxss1 \
|
||||
git \
|
||||
--no-install-recommends && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm install
|
||||
|
||||
ENTRYPOINT [ "npm", "start"]
|
||||
35
HELP.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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
|
||||
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)
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
|
||||
[osfmount]: https://www.osforensics.com/tools/mount-disk-images.html
|
||||
|
||||
## What's the FrontPage Username and Password?
|
||||
Username: windows95
|
||||
|
||||
Password: password
|
||||
35
LICENSE.md
@@ -1,7 +1,38 @@
|
||||
Copyright 2018 Felix Rieseberg
|
||||
Copyright 2019 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:
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
____
|
||||
|
||||
v86 Source Code
|
||||
|
||||
Copyright (c) 2012-2018, Fabian Hemmer
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
52
README.md
@@ -1,10 +1,56 @@
|
||||
# Windows95
|
||||
# windows95
|
||||
|
||||
This is Windows 95, running in an Electron app. Yes, it's the full thing. I'm sorry.
|
||||
This is Windows 95, running in an [Electron](https://electronjs.org/) app. Yes, it's the full thing. I'm sorry.
|
||||
|
||||
## Downloads
|
||||
| | Windows | macOS | Linux |
|
||||
|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Standalone Download | 📦[Standalone, 32-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.1/windows95-2.2.1-win32-standalone-ia32.zip) <br /> 📦[Standalone, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.1/windows95-2.2.1-win32-standalone-x64.zip) | 📦[Standalone](https://github.com/felixrieseberg/windows95/releases/download/v2.2.1/windows95-macos-2.2.1.zip) | |
|
||||
| Installer | 💽[Setup, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.1/windows95-2.2.1-setup-win32-x64.exe) <br /> 💽[Setup, 32-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.1/windows95-2.2.1-setup-win32-ia32.exe) | | 💽[deb, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.1/windows95-linux-2.2.1_amd64.deb) <br /> 💽[rpm, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.1/windows95-linux-2.2.1.x86_64.rpm) |
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
## Should this have been a native app?
|
||||
Absolutely.
|
||||
|
||||
## Does it run Doom (or my other favorite game)?
|
||||
You'll likely be better off with an actual virtualization app, but the short answer is yes. [Thanks to
|
||||
@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.
|
||||
|
||||
## Credits
|
||||
|
||||
99.999% of the work was done over at [v86](https://github.com/copy/v86/) by Copy.
|
||||
99% 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.
|
||||
|
||||
Unpack the `images` folder into the `src` folder, creating this layout:
|
||||
|
||||
```
|
||||
- /images/windows95.img
|
||||
- /images/default-state.bin
|
||||
- /assets/...
|
||||
- /bios/...
|
||||
- /docs/...
|
||||
```
|
||||
|
||||
Once you've done so, run `npm install` and `npm start` to run your local build.
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
16
assets/entitlements.plist
Normal file
@@ -0,0 +1,16 @@
|
||||
<?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>
|
||||
39
docs/docker-instructions.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Running windows95 in Docker
|
||||
|
||||
## Display using a volume mount of the host X11 Unix Socket (Linux Only):
|
||||
|
||||
**Requirements:**
|
||||
* Linux OS with a running X-Server Display
|
||||
* [Docker](http://docker.io)
|
||||
|
||||
docker run -it -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$DISPLAY --device /dev/snd --name windows95 toolboc/windows95
|
||||
|
||||
|
||||
Note: You may need to run `xhost +` on your system to allow connections to the X server running on the host.
|
||||
|
||||
## Display using Xming X11 Server over tcp Socket (Windows and beyond):
|
||||
|
||||
**Requirements:**
|
||||
* [Xming](https://sourceforge.net/projects/xming/)
|
||||
* [Docker](http://docker.io)
|
||||
|
||||
1. Start the Xming X11 Server
|
||||
2. Run the command below:
|
||||
|
||||
docker run -e DISPLAY=host.docker.internal:0 --name windows95 toolboc/windows95
|
||||
|
||||
## Display using the host XQuartz Server (MacOS Only):
|
||||
**Requirements:**
|
||||
* [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:1 toolboc/windows95
|
||||
```
|
||||
4
docs/docker-kubernetes-gitpod.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## 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?
|
||||
|
||||
[](https://gitpod.io/#https://github.com/felixrieseberg/windows95)
|
||||
@@ -1,39 +1,77 @@
|
||||
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: true,
|
||||
asar: false,
|
||||
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)'
|
||||
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'
|
||||
},
|
||||
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: [
|
||||
{
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
platforms: ['win32'],
|
||||
config: {
|
||||
name: 'windows98',
|
||||
authors: 'Felix Rieseberg',
|
||||
exe: 'windows95.exe',
|
||||
noMsi: true,
|
||||
remoteReleases: '',
|
||||
setupExe: `windows95-${package.version}-setup-${process.arch}.exe`,
|
||||
setupIcon: path.resolve(__dirname, 'assets', 'icon.ico'),
|
||||
certificateFile: process.env.WINDOWS_CERTIFICATE_FILE,
|
||||
certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD
|
||||
config: (arch) => {
|
||||
return {
|
||||
name: 'windows95',
|
||||
authors: 'Felix Rieseberg',
|
||||
exe: 'windows95.exe',
|
||||
noMsi: true,
|
||||
remoteReleases: '',
|
||||
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'],
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-zip',
|
||||
platforms: ['darwin']
|
||||
platforms: ['darwin', 'win32']
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-deb',
|
||||
|
||||
7
issue_template.md
Normal file
@@ -0,0 +1,7 @@
|
||||
⚠️ Thank you for reporting an issue!
|
||||
|
||||
Before we go any further, understand that I probably won't be able to fullfil feature requests.
|
||||
Feel free to report what feature you'd love to see, just don't get angry when I don't have
|
||||
time to implement it 🙇♂️
|
||||
|
||||
I will however _gladly_ help you make a pull request if you're willing to play with Javascript!
|
||||
4872
package-lock.json
generated
51
package.json
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "windows95",
|
||||
"productName": "windows95",
|
||||
"version": "1.1.0",
|
||||
"version": "2.2.2",
|
||||
"description": "Windows 95, in an app. I'm sorry.",
|
||||
"main": "src/index.js",
|
||||
"main": "./dist/src/main/main",
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"start": "rimraf ./dist && electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"publish": "electron-forge publish",
|
||||
"lint": "standard \"src/**/*.js\""
|
||||
"lint": "prettier --write src/**/*.{ts,tsx}",
|
||||
"less": "node ./tools/lessc.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Felix Rieseberg, felix@felixrieseberg.com",
|
||||
@@ -17,27 +18,33 @@
|
||||
"config": {
|
||||
"forge": "./forge.config.js"
|
||||
},
|
||||
"standard": {
|
||||
"globals": [
|
||||
"V86Starter",
|
||||
"windows95"
|
||||
],
|
||||
"ignore": [
|
||||
"/src/renderer/lib/*.js"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-default-menu": "^1.0.1",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"fs-extra": "^7.0.0"
|
||||
"fs-extra": "^9.0.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"tslib": "^2.0.0",
|
||||
"update-electron-app": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^6.0.0-beta.22",
|
||||
"@electron-forge/maker-deb": "^6.0.0-beta.22",
|
||||
"@electron-forge/maker-rpm": "^6.0.0-beta.22",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.22",
|
||||
"@electron-forge/maker-zip": "^6.0.0-beta.22",
|
||||
"electron": "3.0.0-beta.6",
|
||||
"standard": "^11.0.1"
|
||||
"@electron-forge/cli": "^6.0.0-beta.52",
|
||||
"@electron-forge/maker-deb": "^6.0.0-beta.52",
|
||||
"@electron-forge/maker-flatpak": "^6.0.0-beta.52",
|
||||
"@electron-forge/maker-rpm": "^6.0.0-beta.52",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.52",
|
||||
"@electron-forge/maker-zip": "^6.0.0-beta.52",
|
||||
"@electron-forge/publisher-github": "^6.0.0-beta.52",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/node": "^12.12.14",
|
||||
"@types/react": "^16.9.44",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"electron": "9.1.2",
|
||||
"less": "^3.12.2",
|
||||
"node-abi": "^2.18.0",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"prettier": "^2.0.5",
|
||||
"rimraf": "^3.0.2",
|
||||
"standard": "^14.3.4",
|
||||
"typescript": "^3.9.7"
|
||||
}
|
||||
}
|
||||
|
||||
25
src/cache.js
@@ -1,25 +0,0 @@
|
||||
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
|
||||
}
|
||||
37
src/cache.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { session } from "electron";
|
||||
|
||||
export async function clearCaches() {
|
||||
await clearCache();
|
||||
await clearStorageData();
|
||||
}
|
||||
|
||||
export async function clearCache() {
|
||||
if (session.defaultSession) {
|
||||
await session.defaultSession.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
export function clearStorageData() {
|
||||
return new Promise((resolve) => {
|
||||
if (!session.defaultSession) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
session.defaultSession.clearStorageData(
|
||||
{
|
||||
storages: [
|
||||
"appcache",
|
||||
"cookies",
|
||||
"filesystem",
|
||||
"indexdb",
|
||||
"localstorage",
|
||||
"shadercache",
|
||||
"websql",
|
||||
"serviceworkers",
|
||||
],
|
||||
quotas: ["temporary", "persistent", "syncable"],
|
||||
},
|
||||
resolve
|
||||
);
|
||||
});
|
||||
}
|
||||
31
src/constants.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { remote, app } from "electron";
|
||||
import * as path from "path";
|
||||
|
||||
const _app = app || remote.app;
|
||||
|
||||
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"),
|
||||
STATE_PATH: path.join(_app.getPath("userData"), "state-v2.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",
|
||||
};
|
||||
47
src/index.js
@@ -1,47 +0,0 @@
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
const path = require('path')
|
||||
|
||||
const { clearCaches } = require('./cache')
|
||||
const { createMenu } = require('./menu')
|
||||
|
||||
if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
|
||||
app.quit()
|
||||
}
|
||||
|
||||
let mainWindow
|
||||
|
||||
const createWindow = () => {
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 800,
|
||||
useContentSize: true,
|
||||
nodeIntegration: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.loadURL(`file://${__dirname}/renderer/index.html?system=win98`)
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null
|
||||
})
|
||||
}
|
||||
|
||||
app.on('ready', async () => {
|
||||
await createMenu()
|
||||
await clearCaches()
|
||||
createWindow()
|
||||
})
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit()
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
if (mainWindow === null) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
24
src/less/emulator.less
Normal file
@@ -0,0 +1,24 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
7
src/less/info.less
Normal file
@@ -0,0 +1,7 @@
|
||||
#information {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
bottom: 50px;
|
||||
font-size: 18px;
|
||||
}
|
||||
117
src/less/root.less
Normal file
@@ -0,0 +1,117 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/less/settings.less
Normal file
@@ -0,0 +1,21 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
9
src/less/start.less
Normal file
@@ -0,0 +1,9 @@
|
||||
#section-start {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> small {
|
||||
margin-top: 25px;
|
||||
font-size: .8rem;
|
||||
}
|
||||
}
|
||||
16
src/less/status.less
Normal file
@@ -0,0 +1,16 @@
|
||||
#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
Normal file
1
src/less/vendor/95css.css
vendored
Normal file
21
src/less/vendor/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
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.
|
||||
BIN
src/less/vendor/bg-pattern.png
vendored
Normal file
|
After Width: | Height: | Size: 144 B |
BIN
src/less/vendor/dropdown.png
vendored
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/less/vendor/windows.woff
vendored
Normal file
BIN
src/less/vendor/windows.woff2
vendored
Normal file
28
src/main/about-panel.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
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);
|
||||
}
|
||||
67
src/main/main.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
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";
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
||||
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();
|
||||
259
src/main/menu.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
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));
|
||||
}
|
||||
3
src/main/squirrel.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function shouldQuit() {
|
||||
return require("electron-squirrel-startup");
|
||||
}
|
||||
10
src/main/update.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { app } from "electron";
|
||||
|
||||
export function setupUpdates() {
|
||||
if (app.isPackaged) {
|
||||
require("update-electron-app")({
|
||||
repo: "felixrieseberg/windows95",
|
||||
updateInterval: "1 hour",
|
||||
});
|
||||
}
|
||||
}
|
||||
27
src/main/windows.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { BrowserWindow } 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,
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.loadFile("./dist/static/index.html");
|
||||
|
||||
mainWindow.on("closed", () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
return mainWindow;
|
||||
}
|
||||
12
src/menu.js
@@ -1,12 +0,0 @@
|
||||
const { app, shell, Menu } = require('electron')
|
||||
const defaultMenu = require('electron-default-menu')
|
||||
|
||||
async function createMenu () {
|
||||
const menu = defaultMenu(app, shell)
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(menu))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createMenu
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
const { remote } = require('electron')
|
||||
const fs = require('fs-extra')
|
||||
|
||||
const { STATE_PATH, getState, resetState } = require('./state')
|
||||
|
||||
window.windows95 = {
|
||||
STATE_PATH,
|
||||
|
||||
resetState,
|
||||
|
||||
async saveState () {
|
||||
return new Promise((resolve) => {
|
||||
if (!window.emulator || !window.emulator.save_state) {
|
||||
return resolve()
|
||||
}
|
||||
|
||||
window.emulator.save_state(async (error, newState) => {
|
||||
if (error) {
|
||||
console.log(error)
|
||||
return
|
||||
}
|
||||
|
||||
await fs.outputFile(STATE_PATH, Buffer.from(newState))
|
||||
|
||||
console.log(`Saved state to ${STATE_PATH}`)
|
||||
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
async restoreState () {
|
||||
try {
|
||||
window.emulator.restore_state(getState())
|
||||
} catch (error) {
|
||||
console.log(`Could not read state file. Maybe none exists?`, error)
|
||||
}
|
||||
},
|
||||
|
||||
quit () {
|
||||
remote.app.quit()
|
||||
}
|
||||
}
|
||||
34
src/renderer/app.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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
@@ -1 +0,0 @@
|
||||
*.gz
|
||||
100
src/renderer/card-drive.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
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 href="https://google.com">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>
|
||||
);
|
||||
}
|
||||
}
|
||||
161
src/renderer/card-settings.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import * as React from "react";
|
||||
import * as fs from "fs-extra";
|
||||
|
||||
import { CONSTANTS } from "../constants";
|
||||
|
||||
interface CardSettingsProps {
|
||||
bootFromScratch: () => void;
|
||||
setFloppy: (file: File) => void;
|
||||
floppy?: 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.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.renderFloppy()}
|
||||
<hr />
|
||||
{this.renderState()}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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 the state reset
|
||||
*/
|
||||
private async onResetState() {
|
||||
if (fs.existsSync(CONSTANTS.STATE_PATH)) {
|
||||
await fs.remove(CONSTANTS.STATE_PATH);
|
||||
}
|
||||
|
||||
this.setState({ isStateReset: true });
|
||||
}
|
||||
}
|
||||
19
src/renderer/card-start.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
166
src/renderer/emulator-info.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
||||
493
src/renderer/emulator.tsx
Normal file
@@ -0,0 +1,493 @@
|
||||
import * as React from "react";
|
||||
import * as fs from "fs-extra";
|
||||
import * as path from "path";
|
||||
import { ipcRenderer, remote, 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";
|
||||
|
||||
export interface EmulatorState {
|
||||
currentUiCard: string;
|
||||
emulator?: any;
|
||||
scale: number;
|
||||
floppyFile?: 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,
|
||||
scale: 1,
|
||||
};
|
||||
|
||||
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(() => {
|
||||
remote.app.quit();
|
||||
});
|
||||
};
|
||||
|
||||
window.onbeforeunload = (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 } = this.state;
|
||||
|
||||
if (isRunning) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let card;
|
||||
|
||||
if (currentUiCard === "settings") {
|
||||
card = (
|
||||
<CardSettings
|
||||
setFloppy={(floppyFile) => this.setState({ floppyFile })}
|
||||
bootFromScratch={this.bootFromScratch}
|
||||
floppy={floppyFile}
|
||||
/>
|
||||
);
|
||||
} 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 imageSize = await 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: this.state.floppyFile,
|
||||
},
|
||||
boot_order: 0x132,
|
||||
};
|
||||
|
||||
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();
|
||||
}, 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();
|
||||
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;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (!emulator || !emulator.save_state) {
|
||||
console.log(`restoreState: No emulator present`);
|
||||
return resolve();
|
||||
}
|
||||
|
||||
emulator.save_state(async (error: Error, newState: ArrayBuffer) => {
|
||||
if (error) {
|
||||
console.warn(`saveState: Could not save state`, error);
|
||||
return resolve();
|
||||
}
|
||||
|
||||
await fs.outputFile(CONSTANTS.STATE_PATH, Buffer.from(newState));
|
||||
|
||||
console.log(`saveState: Saved state to ${CONSTANTS.STATE_PATH}`);
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores state to the emulator.
|
||||
*/
|
||||
private restoreState() {
|
||||
const { emulator } = this.state;
|
||||
const state = 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 {
|
||||
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 getState(): ArrayBuffer | null {
|
||||
const statePath = fs.existsSync(CONSTANTS.STATE_PATH)
|
||||
? CONSTANTS.STATE_PATH
|
||||
: 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
src/renderer/global.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
declare const V86Starter: any;
|
||||
declare const win95: any;
|
||||
@@ -1,49 +0,0 @@
|
||||
<!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="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>
|
||||
<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 to
|
||||
make sure that Windows can load it.
|
||||
</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>
|
||||
require('./renderer.js')
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
26
src/renderer/lib/LICENSE.md
Normal file
@@ -0,0 +1,26 @@
|
||||
Copyright (c) 2012-2018, Fabian Hemmer
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
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.
|
||||
|
||||
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.
|
||||
@@ -1,136 +0,0 @@
|
||||
const $ = document.querySelector.bind(document)
|
||||
const $$ = document.querySelectorAll.bind(document)
|
||||
|
||||
const BUTTONS = $('#buttons')
|
||||
|
||||
let cursorCaptured = false
|
||||
let floppyFile = null
|
||||
let bootFresh = false
|
||||
|
||||
const OPTIONS = {
|
||||
win95: {
|
||||
hda: {
|
||||
url: './images/windows95.img',
|
||||
async: true,
|
||||
size: 242049024
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main (id) {
|
||||
const opts = Object.assign({
|
||||
memory_size: 64 * 1024 * 1024,
|
||||
screen_container: document.getElementById('emulator'),
|
||||
bios: {
|
||||
url: './bios/seabios.bin'
|
||||
},
|
||||
vga_bios: {
|
||||
url: './bios/vgabios.bin'
|
||||
},
|
||||
fda: {
|
||||
buffer: floppyFile || undefined
|
||||
},
|
||||
boot_order: 0x132
|
||||
}, OPTIONS[id])
|
||||
|
||||
// New v86 instance
|
||||
window.emulator = new V86Starter(opts)
|
||||
|
||||
// Restore state. We can't do this right away.
|
||||
setTimeout(async () => {
|
||||
if (!bootFresh) {
|
||||
await windows95.restoreState()
|
||||
}
|
||||
|
||||
cursorCaptured = true
|
||||
window.emulator.lock_mouse()
|
||||
window.emulator.run()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function start (id) {
|
||||
BUTTONS.remove()
|
||||
document.body.className = ''
|
||||
main(id)
|
||||
}
|
||||
|
||||
function setupButtons () {
|
||||
// Start
|
||||
$$('.btn-start').forEach((btn) => {
|
||||
btn.addEventListener('click', () => start(btn.id))
|
||||
})
|
||||
|
||||
// Reset
|
||||
$('#reset').addEventListener('click', () => {
|
||||
if (window.emulator.stop) {
|
||||
window.emulator.stop()
|
||||
}
|
||||
|
||||
windows95.resetState()
|
||||
|
||||
if (window.emulator.run) {
|
||||
window.emulator.run()
|
||||
}
|
||||
|
||||
$('#reset').disabled = true
|
||||
})
|
||||
|
||||
$('#discard-state').addEventListener('click', () => {
|
||||
bootFresh = true
|
||||
|
||||
start('win95')
|
||||
})
|
||||
|
||||
// Floppy
|
||||
$('#floppy').addEventListener('click', () => {
|
||||
$('#file-input').click()
|
||||
})
|
||||
|
||||
// Floppy (Hidden Input)
|
||||
$('#file-input').addEventListener('change', (event) => {
|
||||
floppyFile = event.target.files && event.target.files.length > 0
|
||||
? event.target.files[0]
|
||||
: null
|
||||
|
||||
if (floppyFile) {
|
||||
$('#floppy-path').innerHTML = `Inserted Floppy Disk: ${floppyFile.path}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setupEscListener () {
|
||||
document.onkeydown = function (evt) {
|
||||
evt = evt || window.event
|
||||
if (evt.keyCode === 27) {
|
||||
if (cursorCaptured) {
|
||||
cursorCaptured = false
|
||||
document.exitPointerLock()
|
||||
} else {
|
||||
cursorCaptured = true
|
||||
window.emulator.lock_mouse()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupCloseListener () {
|
||||
let isQuitting = false
|
||||
|
||||
const handleClose = async () => {
|
||||
await windows95.saveState()
|
||||
isQuitting = true
|
||||
windows95.quit()
|
||||
}
|
||||
|
||||
window.onbeforeunload = (event) => {
|
||||
if (isQuitting) return
|
||||
|
||||
handleClose()
|
||||
event.preventDefault()
|
||||
event.returnValue = false
|
||||
}
|
||||
}
|
||||
|
||||
setupEscListener()
|
||||
setupCloseListener()
|
||||
setupButtons()
|
||||
43
src/renderer/start-menu.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
0
src/renderer/status.tsx
Normal file
@@ -1,77 +0,0 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
body.paused > #emulator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.paused {
|
||||
background: #008080;
|
||||
font-family: Courier;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
24
src/state.js
@@ -1,24 +0,0 @@
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const { remote } = require('electron')
|
||||
|
||||
const DEFAULT_PATH = path.join(__dirname, 'renderer/images/default-state.bin')
|
||||
const STATE_PATH = path.join(remote.app.getPath('userData'), 'state.bin')
|
||||
|
||||
function getState () {
|
||||
const statePath = fs.existsSync(STATE_PATH)
|
||||
? STATE_PATH
|
||||
: DEFAULT_PATH
|
||||
|
||||
return fs.readFileSync(statePath).buffer
|
||||
}
|
||||
|
||||
function resetState () {
|
||||
fs.removeSync(STATE_PATH)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
STATE_PATH,
|
||||
resetState,
|
||||
getState
|
||||
}
|
||||
8
src/utils/devmode.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Are we currently running in development mode?
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isDevMode() {
|
||||
return !!process.defaultApp;
|
||||
}
|
||||
22
src/utils/disk-image-size.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as fs from "fs-extra";
|
||||
|
||||
import { CONSTANTS } from "../constants";
|
||||
|
||||
/**
|
||||
* Get the size of the disk image
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
export 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;
|
||||
}
|
||||
BIN
static/boot-fresh.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
static/drive.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
10
static/entitlements.plist
Normal file
@@ -0,0 +1,10 @@
|
||||
<?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>
|
||||
BIN
static/floppy.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
16
static/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!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">
|
||||
<script src="../src/renderer/lib/libv86.js"></script>
|
||||
</head>
|
||||
<body class="paused windows95">
|
||||
<div id="app"></div>
|
||||
<script src="../src/renderer/app.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
static/reset-state.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
static/reset.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
static/run.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
static/select-floppy.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
static/settings.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
static/show-disk-image.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
static/start.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
23
tools/add-macos-cert.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/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
|
||||
11
tools/download-disk.ps1
Normal file
@@ -0,0 +1,11 @@
|
||||
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
|
||||
10
tools/download-disk.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/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
|
||||
7
tools/generateAssets.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/* tslint:disable */
|
||||
|
||||
const { compileParcel } = require('./parcel-build')
|
||||
|
||||
module.exports = async () => {
|
||||
await Promise.all([compileParcel()])
|
||||
}
|
||||
47
tools/parcel-build.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/* tslint:disable */
|
||||
|
||||
const Bundler = require('parcel-bundler')
|
||||
const path = require('path')
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
compileParcel
|
||||
}
|
||||
|
||||
if (require.main === module) compileParcel()
|
||||
11
tools/parcel-watch.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const { compileParcel } = require('./parcel-build')
|
||||
|
||||
async function watchParcel () {
|
||||
return compileParcel({ watch: true })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
watchParcel
|
||||
}
|
||||
|
||||
if (require.main === module) watchParcel()
|
||||
30
tools/run-bin.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/* 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
|
||||
}
|
||||
13
tools/tsc.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/* tslint:disable */
|
||||
|
||||
const { run } = require('./run-bin')
|
||||
|
||||
async function compileTypeScript () {
|
||||
await run('TypeScript', 'tsc', ['-p', 'tsconfig.json'])
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
compileTypeScript
|
||||
}
|
||||
|
||||
if (require.main === module) compileTypeScript()
|
||||
43
tsconfig.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"removeComments": false,
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": true,
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
],
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedParameters": true,
|
||||
"importHelpers": true,
|
||||
"noEmitHelpers": false,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"pretty": true,
|
||||
"target": "es2017",
|
||||
"jsx": "react",
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
],
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
"formatCodeOptions": {
|
||||
"indentSize": 2,
|
||||
"tabSize": 2
|
||||
}
|
||||
}
|
||||