109 Commits

Author SHA1 Message Date
Felix Rieseberg
6bf7678079 build: Use ascProvider 2019-12-02 15:54:29 -08:00
Felix Rieseberg
5396cae0f0 build: Notarize the app 2019-12-02 13:23:53 -08:00
Felix Rieseberg
c5a24643fd build: Tell me what's going on 2019-12-01 19:19:53 -08:00
Felix Rieseberg
59a651a205 build: Oops, actually code-sign this thing 2019-12-01 17:49:57 -08:00
Felix Rieseberg
f5cb94776a 2.2.1 2019-11-30 13:09:58 -08:00
Felix Rieseberg
982c866899 chore: Bump dependencies 2019-11-30 12:44:43 -08:00
Felix Rieseberg
9e8cef8da7 chore: Update Electron 2019-11-22 18:13:59 -08:00
Felix Rieseberg
3b76a39060 fix: Ensure that links show up 2019-11-22 18:06:58 -08:00
Felix Rieseberg
e7d515de84 docs: Update Readme 2019-08-24 19:02:48 +02:00
Felix Rieseberg
a4092f105a build: Fix Travis build 2019-08-24 18:39:23 +02:00
Felix Rieseberg
71a11cfbe3 2.2.0 2019-08-24 18:10:24 +02:00
Felix Rieseberg
f3c8f3409a Merge pull request #141 from felixrieseberg/new-style
New style
2019-08-24 18:09:51 +02:00
Felix Rieseberg
8d8fc949cd build: Put images in right location 2019-08-24 17:39:04 +02:00
Felix Rieseberg
0c2149b756 fix: Show image in explorer 2019-08-24 17:26:18 +02:00
Felix Rieseberg
51d0011ed0 fix: Hide info if requested 2019-08-24 17:14:22 +02:00
Felix Rieseberg
658fed75da docs: Update the docs 2019-08-24 17:03:56 +02:00
Felix Rieseberg
186a2a8ba9 chore: Improve build 2019-08-24 16:58:21 +02:00
Felix Rieseberg
7653d7294c feat: Icons, better UI 2019-08-23 22:58:56 +02:00
Felix Rieseberg
33ef8abcc8 feat: Zoom in/out/reset 2019-08-21 19:43:21 +02:00
Felix Rieseberg
ea134d046e feat: More keyboard shortcuts 2019-08-21 11:34:55 +02:00
Felix Rieseberg
c7f765df03 feat: More menu options 2019-08-21 11:26:56 +02:00
Felix Rieseberg
dcc3e72bcf feat: Allow stopping the emulator 2019-08-21 10:37:04 +02:00
Felix Rieseberg
241606d097 feat: Move to TypeScript 2019-08-21 09:48:49 +02:00
Felix Rieseberg
b7aa6a760d build: Move to less for styles 2019-08-19 17:37:35 -04:00
Felix Rieseberg
6b7bb0f460 chore: Move bios 2019-08-19 17:33:18 -04:00
Felix Rieseberg
90a97a11bf feat: Use TypeScript & Parcel 2019-08-19 17:31:33 -04:00
Felix Rieseberg
3598ceb97c chore: gitignore more stuff 2019-08-19 17:30:58 -04:00
Felix Rieseberg
dbcefb4b7b 🎨 Improved style 2019-08-19 08:41:14 -07:00
Felix Rieseberg
42719bb1d7 wip 2019-08-04 08:45:18 -07:00
Felix Rieseberg
153002403b wip 2019-07-24 08:57:23 -07:00
Felix Rieseberg
889c53857e chore: Update dependencies 2019-07-24 07:31:24 -07:00
Felix Rieseberg
5b8f3e12bc Merge pull request #139 from felixrieseberg/dependabot/npm_and_yarn/js-yaml-3.13.1
build(deps): bump js-yaml from 3.12.1 to 3.13.1
2019-06-23 18:12:53 -07:00
dependabot[bot]
59299a2c48 build(deps): bump js-yaml from 3.12.1 to 3.13.1
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.12.1 to 3.13.1.
- [Release notes](https://github.com/nodeca/js-yaml/releases)
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.12.1...3.13.1)

Signed-off-by: dependabot[bot] <support@github.com>
2019-06-23 16:48:36 +00:00
Felix Rieseberg
885af7f786 docs: Update download links 2019-05-23 11:54:39 -07:00
Felix Rieseberg
55e88cd5b5 fix: Update version number 2019-05-23 10:56:38 -07:00
Felix Rieseberg
e0ad866256 build: Fix Apple Code Signing 2019-05-23 10:18:57 -07:00
Felix Rieseberg
cf3acd4182 📝 Update readme links 2019-05-17 16:53:51 +09:00
Felix Rieseberg
a28aef8cf0 build: Oh, and don't install tree 2019-05-17 15:37:29 +09:00
Felix Rieseberg
d2b8d9dd35 build: Don't even tree 2019-05-17 15:31:48 +09:00
Felix Rieseberg
3802734ef0 build: Install tree on Linux 2019-05-17 15:31:13 +09:00
Felix Rieseberg
1f478676f1 build: Download images on Travis 2019-05-17 15:25:30 +09:00
Felix Rieseberg
d19bbeee8f build: Make standalone Windows builds 2019-05-17 15:25:21 +09:00
Felix Rieseberg
1e130b6140 build: Install x64 Node 2019-05-17 14:22:37 +09:00
Felix Rieseberg
e1c5992ff9 build: Add GitHub publisher 2019-05-17 13:56:02 +09:00
Felix Rieseberg
e879760678 build: Cut the flatpack 2019-05-17 13:43:39 +09:00
Felix Rieseberg
2a11633171 chore: Update version number to 2.1.0 2019-05-17 13:33:11 +09:00
Felix Rieseberg
b68d54ae62 build: Fix travis build 2019-05-17 13:32:28 +09:00
Felix Rieseberg
9600630340 build: Build on AppVeyor, too 2019-05-17 13:28:37 +09:00
Felix Rieseberg
bae1909793 build: What if we just don't asar? 2019-05-17 13:18:14 +09:00
Felix Rieseberg
ee9e138034 build: Enable verbose debugging 2019-05-17 13:01:56 +09:00
Felix Rieseberg
5558671688 build: Add a filename 2019-05-17 12:53:42 +09:00
Felix Rieseberg
9a46ed5080 build: Step by step, I guess 2019-05-17 12:43:54 +09:00
Felix Rieseberg
2c160d0f7f build: Add an AppVeyor file 2019-05-17 12:33:17 +09:00
Felix Rieseberg
aafab62707 chore: Update all dependencies 2019-05-17 12:30:03 +09:00
Felix Rieseberg
78126a57cb docs: Fix a typo 2019-02-22 11:41:46 +00:00
Felix Rieseberg
f5256ec7a2 docs: Add gitpod instructions 2019-02-22 11:40:56 +00:00
Felix Rieseberg
6c1687c9a5 Merge pull request #124 from jankeromnes/master
Add an online live demo
2019-02-22 11:39:07 +00:00
Jan Keromnes
2c041115d0 chore: Add online live demo 2019-02-13 15:31:23 +00:00
Felix Rieseberg
987dc57309 Merge pull request #117 from kant/patch-1
Formatting proposals
2019-02-12 00:05:42 -08:00
Darío Hereñú
614b18969d Formatting proposals 2019-02-08 13:58:24 -03:00
Felix Rieseberg
264ef7d069 Update README.md 2019-02-04 10:45:32 -08:00
Felix Rieseberg
e85cf4f1b2 docs: Add FrontPage credentials 2019-02-03 17:14:33 -08:00
Felix Rieseberg
e987da5460 docs: Update links 2019-02-03 17:13:39 -08:00
Felix Rieseberg
a542639bc3 docs: Update the readme with some answers 2019-02-03 16:49:19 -08:00
Felix Rieseberg
5d1928beb2 docs: Add instructions on how to mount the disk image 2019-02-03 16:45:58 -08:00
Felix Rieseberg
f1b657a53b docs: Remove link to CovalenceConf 2019-02-03 16:38:54 -08:00
Felix Rieseberg
6aa39e66ec 2.0.0 2019-02-03 15:40:31 -08:00
Felix Rieseberg
ed42ea8e0e fix: Ensure smooth migration 2019-02-03 15:38:06 -08:00
Felix Rieseberg
0779f18071 fix: Handle missing state 2019-02-03 15:24:29 -08:00
Felix Rieseberg
a9c4e38386 feat: Allow resetting the machien 2019-02-03 15:23:20 -08:00
Felix Rieseberg
62b0909cb4 fix: Correctly capture the mouse 2019-02-03 14:08:09 -08:00
Felix Rieseberg
873cb75241 fix: the various things I just broke 2019-02-03 14:02:30 -08:00
Felix Rieseberg
6467acb0c8 fix: Cleanup 2019-02-03 13:49:09 -08:00
Felix Rieseberg
ed1bd0a1e0 chore: Custom menu 2019-02-03 13:44:57 -08:00
Felix Rieseberg
ac84f4164e fix: Don't clear the cache on each start 2019-02-03 12:36:06 -08:00
Felix Rieseberg
77569d4ce6 infra: Update dependencies 2019-02-03 12:33:11 -08:00
Felix Rieseberg
68b7c181ad Merge pull request #109 from samuell/patch-1
Update README.md: Fix broken download links
2018-12-30 09:25:24 +01:00
Felix Rieseberg
293491477b Merge pull request #111 from malept/forge-arch-squirrel
chore: automatically add arch to squirrel installer filename
2018-12-30 09:24:56 +01:00
Felix Rieseberg
7eb750752b infra: Update to Electron 4.0 2018-12-30 09:22:32 +01:00
Mark Lee
f1488cedc2 Make it look more like a function 2018-12-26 22:41:56 -08:00
Mark Lee
9f366063eb chore: automatically add arch to squirrel installer filename 2018-12-26 22:40:08 -08:00
Samuel Lampa
55135f052e Update README.md: Fix download links
Fixes download links for Windows and the .deb Linux installer
2018-12-21 15:08:18 +01:00
Felix Rieseberg
95fd8e4925 chore: Update the readme with new download links 2018-12-20 11:25:33 -08:00
Felix Rieseberg
b794954da4 infra: Update dependencies 2018-12-20 10:32:00 -08:00
Felix Rieseberg
18a73c45a0 🚀 Mention CovalenceConf 2018-10-16 16:24:28 -07:00
Felix Rieseberg
b83914060f 📝 Update the Readme a little 2018-10-16 14:11:43 -07:00
Felix Rieseberg
93955564d9 Merge pull request #96 from jacobq/patch-1
Clarify support & purpose
2018-09-25 09:05:52 -07:00
Felix Rieseberg
d31920aaf4 📝 Spell out the actual platforms 2018-09-25 09:05:38 -07:00
Jacob
cdfe47d92b Clarify support & purpose
Close #90
2018-09-19 12:00:24 -05:00
Felix Rieseberg
b8259784e7 Merge pull request #89 from toolboc/dockerdocs
fix docker-instructions
2018-09-09 01:48:14 -07:00
Paul DeCarlo
b70b9fabd5 update docker-instructions.md 2018-09-08 12:20:51 -05:00
Felix Rieseberg
f2c1fc4142 Merge pull request #87 from argan/patch-1
also run on MacOS with XQuartz
2018-09-08 08:56:44 -07:00
Argan Wang
aeba364a7a also run on MacOS with XQuartz 2018-09-07 16:13:47 +08:00
Felix Rieseberg
a34ce54b56 📝 Fix name typo 2018-08-27 10:46:11 -07:00
Felix Rieseberg
e1477bfc05 📝 MS-DOS fixed 2018-08-27 10:29:15 -07:00
Felix Rieseberg
71d6f16318 📝 Make the downloads super-obvious 2018-08-27 08:53:27 -07:00
Felix Rieseberg
4bdbff6a4b 1.3.0 2018-08-27 08:29:52 -07:00
Felix Rieseberg
e062548c81 🔧 Add menu, credits 2018-08-27 08:26:34 -07:00
Felix Rieseberg
609668c581 📝 Add copy of v86 license 2018-08-27 08:13:42 -07:00
Felix Rieseberg
bec9577409 🔧 Update Readme 2018-08-27 08:12:38 -07:00
Felix Rieseberg
dd9ff5a319 Merge pull request #52 from toolboc/master
Add Dockerfile and instructions to run Win95 as a container
2018-08-27 08:11:19 -07:00
Felix Rieseberg
f5125219fd Merge pull request #66 from malept/add-flatpak-maker
🔧 add flatpak maker
2018-08-27 08:09:49 -07:00
Mark Lee
b4fd81b364 🔧 add flatpak maker 2018-08-26 11:06:40 -07:00
Paul DeCarlo
e62a8cbed6 Add detailed docker instructions to docs 2018-08-25 17:45:36 -05:00
Felix Rieseberg
2038ce9c31 📝 Update readme 2018-08-25 17:05:47 -05:00
toolboc
6c12063353 Add note on xhost for allowing connections to X11 2018-08-25 11:45:18 -05:00
toolboc
91783e7d26 Add libcanberra-gtk3-module 2018-08-25 11:25:13 -05:00
toolboc
9f8040bb22 Update Dockerfile 2018-08-25 10:35:09 -05:00
Paul DeCarlo
10d2e37e9d Add Dockerfile and instructions to run Win95 as a container 2018-08-24 17:00:31 -05:00
88 changed files with 11550 additions and 2793 deletions

49
.appveyor.yml Normal file
View File

@@ -0,0 +1,49 @@
environment:
matrix:
- nodejs_version: "10"
init:
- git config --global core.symlinks true
install:
# Setup the code signing certificate
- ps: >-
if (Test-Path Env:\WINDOWS_CERTIFICATE_P12) {
$workingDirectory = Convert-Path (Resolve-Path -path ".")
$filename = "$workingDirectory\cert.p12"
$bytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE_P12)
[IO.File]::WriteAllBytes($filename, $bytes)
$env:WINDOWS_CERTIFICATE_FILE = $filename
$sec = ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText
$cert = Get-PfxData -Password $sec $filename
Write-Host $cert.EndEntityCertificates
}
- ps: Install-Product node $env:nodejs_version x64
- node --version
- npm ci
- ps: mkdir images
- ps: cd images
- ps: Start-FileDownload 'https://1drv.ws/u/s!AkfaAw_EaahOkulh8rA41x2phgfYXQ' -FileName images.zip -Timeout 600000
- ps: 7z x images.zip -y -aoa
- ps: Remove-Item images.zip
- ps: Remove-Item __MACOSX -Recurse -ErrorAction Ignore
- ps: cd ..
- ps: Tree ./src /F
- ps: Tree ./images /F
cache:
- '%APPDATA%\npm-cache -> appveyor.yml'
test_script:
- node --version
- npm --version
- npm run lint
artifacts:
- path: 'out\make\squirrel.windows\**\*.exe'
build_script:
- if %APPVEYOR_REPO_TAG% EQU false npm run make
- if %APPVEYOR_REPO_TAG% EQU true npm run publish
- if %APPVEYOR_REPO_TAG% EQU true npm run publish -- --arch=ia32
- ps: Tree ./out/make /F

2
.gitignore vendored
View File

@@ -2,3 +2,5 @@ node_modules
out
src/images
.DS_Store
images
dist

64
.travis.yml Normal file
View File

@@ -0,0 +1,64 @@
language: node_js
node_js: "12"
os:
- linux
- osx
dist: trusty
osx_image: xcode10
sudo: false
cache:
directories:
- node_modules
- $HOME/.cache/electron
addons:
apt:
packages:
- fakeroot
- rpm
branches:
only:
- master
- /^v\d+\.\d+\.\d+/
install:
- npm install
- mkdir -p ./images
- cd ./images
- wget -O images.zip https://1drv.ws/u/s!AkfaAw_EaahOkulh8rA41x2phgfYXQ
- unzip -o images.zip
- rm images.zip
- rm -r __MACOSX
- cd ..
- ls src
- ls images
- |
if [[ "$TRAVIS_OS_NAME" == "osx" && "$TRAVIS_SECURE_ENV_VARS" == "true" ]]; then
export CERTIFICATE_P12=cert.p12;
echo $MACOS_CERT_P12 | base64 --decode > $CERTIFICATE_P12;
export KEYCHAIN=build.keychain;
# Create the keychain with a password
security create-keychain -p travis $KEYCHAIN;
# Make the custom keychain default, so xcodebuild will use it for signing
security default-keychain -s $KEYCHAIN;
# Unlock the keychain
security unlock-keychain -p travis $KEYCHAIN;
# Add certificates to keychain and allow codesign to access them
# Apple Worldwide Developer Relations Certification Authority
security import ./assets/certs/apple.cer -k ~/Library/Keychains/$KEYCHAIN -T /usr/bin/codesign
# Developer Authentication Certification Authority
security import ./assets/certs/dac.cer -k ~/Library/Keychains/$KEYCHAIN -T /usr/bin/codesign
# Developer ID Felix
security import $CERTIFICATE_P12 -k $KEYCHAIN -P $MACOS_CERT_PASSWORD -T /usr/bin/codesign 2>&1 >/dev/null;
rm $CERTIFICATE_P12;
security set-key-partition-list -S apple-tool:,apple: -s -k travis $KEYCHAIN
# Echo the identity
security find-identity -v -p codesigning
fi
script:
- npm run lint
- if test -z "$TRAVIS_TAG"; then npm run make; fi
after_success: if test -n "$TRAVIS_TAG"; then npm run publish; fi

59
CREDITS.md Normal file
View 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
View 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"]

34
HELP.md Normal file
View File

@@ -0,0 +1,34 @@
# Help & Commonly Asked Questions
## MS-DOS seems to brick the screen
Hit `Alt + Enter` to make the command screen "full screen" (as far as Windows 95 is
concerned). This should restore the display from the garbled mess you see and allow
you to access the command prompt. Press Alt-Enter again to leave full screen and go
back to a window mode. (Thanks to @DisplacedGamer for that wisdom)
## Windows 95 is stuck in a bad state
On the app's home screen, select "Settings" in the lower menu. Then, delete your
machine's state before starting it again - this time hopefully without issues.
## 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

View File

@@ -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.

View File

@@ -1,13 +1,17 @@
# 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.
## 💿⏬ [Download it here](https://github.com/felixrieseberg/windows95/releases).
## Downloads
| | Windows | macOS | Linux |
|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Standalone Download | 📦[Standalone, 32-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.0/windows95-2.2.0-win32-standalone-ia32.zip) <br /> 📦[Standalone, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.0/windows95-2.2.0-win32-standalone-x64.zip) | 📦[Standalone](https://github.com/felixrieseberg/windows95/releases/download/v2.2.0/windows95-macos-2.2.0.zip) | |
| Installer | 💽[Setup, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.0/windows95-2.2.0-setup-win32-x64.exe) <br /> 💽[Setup, 32-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.0/windows95-2.2.0-setup-win32-ia32.exe) | | 💽[deb, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.0/windows95-linux-2.2.0_amd64.deb) <br /> 💽[rpm, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.0/windows95-linux-2.2.0.x86_64.rpm) |
![Screenshot](https://user-images.githubusercontent.com/1426799/44532591-4ceb3680-a6a8-11e8-8c2c-bc29f3bfdef7.png)
## Does it work?
Yes! Quite well, actually.
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.
@@ -17,27 +21,37 @@ You'll likely be better off with an actual virtualization app, but the short ans
@DisplacedGamers](https://youtu.be/xDXqmdFxofM) I can recommend that you switch to a resolution of
640x480 @ 256 colors before starting DOS games - just like in the good ol' days.
## How's the code?
This only works well by accident and was mostly a joke. The code quality is accordingly.
## 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 and state images. They're not part of the repo,
but [you can download them here](https://mega.nz/#!euxygQBT!i03vtE4kYTgrZ1rjZa1gT2F8hvhcwIAgGBsY4ECjs0w).
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/renderer` folder, creating this layout:
Unpack the `images` folder into the `src` folder, creating this layout:
```
./src/images/default-state.bin
./src/images/windows95.img
- /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
This project is provided for educational purposes only. It is not affiliated with and has

BIN
assets/certs/apple.cer Normal file

Binary file not shown.

BIN
assets/certs/dac.cer Normal file

Binary file not shown.

View 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
```

View 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?
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/felixrieseberg/windows95)

View File

@@ -2,40 +2,61 @@ const path = require('path');
const package = require('./package.json');
module.exports = {
hooks: {
generateAssets: require('./tools/generateAssets'),
postPackage: require('./tools/notarize')
},
packagerConfig: {
asar: {
unpack: '**/images/*.img'
},
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': 'static/entitlements.plist',
'entitlements-inherit': 'static/entitlements.plist',
'signature-flags': 'library'
},
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: 'windows95',
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_CERTIFICATE_FILE,
certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD
}
}
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin']
platforms: ['darwin', 'win32']
},
{
name: '@electron-forge/maker-deb',
@@ -45,5 +66,18 @@ module.exports = {
name: '@electron-forge/maker-rpm',
platforms: ['linux']
}
],
publishers: [
{
name: '@electron-forge/publisher-github',
config: {
repository: {
owner: 'felixrieseberg',
name: 'windows95'
},
draft: true,
prerelease: true
}
}
]
};

11265
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,16 @@
{
"name": "windows95",
"productName": "windows95",
"version": "1.2.0",
"version": "2.2.1",
"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,29 +18,34 @@
"config": {
"forge": "./forge.config.js"
},
"standard": {
"globals": [
"appState",
"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",
"update-electron-app": "^1.3.0"
"fs-extra": "^8.1.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"tslib": "^1.10.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.45",
"@electron-forge/maker-deb": "^6.0.0-beta.45",
"@electron-forge/maker-flatpak": "^6.0.0-beta.45",
"@electron-forge/maker-rpm": "^6.0.0-beta.45",
"@electron-forge/maker-squirrel": "^6.0.0-beta.45",
"@electron-forge/maker-zip": "^6.0.0-beta.45",
"@electron-forge/publisher-github": "^6.0.0-beta.45",
"@types/fs-extra": "^8.0.1",
"@types/node": "^12.12.14",
"@types/react": "^16.9.13",
"@types/react-dom": "^16.9.4",
"electron": "7.1.2",
"electron-notarize": "^0.2.1",
"less": "^3.10.3",
"node-abi": "^2.13.0",
"parcel-bundler": "^1.12.4",
"prettier": "^1.19.1",
"rimraf": "^3.0.0",
"standard": "^14.3.1",
"typescript": "^3.7.2"
}
}

View File

@@ -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
}

25
src/cache.ts Normal file
View File

@@ -0,0 +1,25 @@
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
View 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'
}

View File

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

View File

@@ -1,57 +0,0 @@
const { app, BrowserWindow } = require('electron')
const path = require('path')
const { clearCaches } = require('./cache')
const { createMenu } = require('./menu')
const { setupProtocol } = require('./es6')
if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
app.quit()
}
if (app.isPackaged) {
require('update-electron-app')({
repo: 'felixrieseberg/windows95',
updateInterval: '1 hour'
})
}
let mainWindow
const createWindow = () => {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1280,
height: 800,
useContentSize: true,
webPreferences: {
nodeIntegration: false,
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadURL(`file://${__dirname}/renderer/index.html`)
mainWindow.on('closed', () => {
mainWindow = null
})
}
app.on('ready', async () => {
await setupProtocol()
await createMenu()
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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

1
src/less/vendor/95css.css vendored Normal file

File diff suppressed because one or more lines are too long

21
src/less/vendor/LICENSE vendored Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

BIN
src/less/vendor/dropdown.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
src/less/vendor/windows.woff vendored Normal file

Binary file not shown.

BIN
src/less/vendor/windows.woff2 vendored Normal file

Binary file not shown.

28
src/main/about-panel.ts Normal file
View 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
View 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();

260
src/main/menu.ts Normal file
View File

@@ -0,0 +1,260 @@
import { app, shell, Menu, BrowserWindow, ipcMain, webFrame } from "electron";
import { clearCaches } from "../cache";
import { IPC_COMMANDS } from "../constants";
import { isDevMode } from "../utils/devmode";
import { getOrCreateWindow } from "./windows";
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
View File

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

10
src/main/update.ts Normal file
View 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
View 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;
}

View File

@@ -1,53 +0,0 @@
const { app, shell, Menu, BrowserWindow } = require('electron')
const defaultMenu = require('electron-default-menu')
function send (cmd) {
const windows = BrowserWindow.getAllWindows()
if (windows[0]) {
windows[0].webContents.send(cmd)
}
}
async function createMenu () {
const menu = defaultMenu(app, shell)
.map((item) => {
if (item.label === 'View') {
item.submenu = item.submenu.filter((subItem) => {
return subItem.label !== 'Reload'
})
}
return item
})
.filter((item) => {
return item.label !== 'Edit'
})
menu.splice(1, 0, {
label: 'Machine',
submenu: [
{
label: 'Send Ctrl+Alt+Del',
click: () => send('ctrlaltdel')
},
{
label: 'Restart',
click: () => send('restart')
},
{
type: 'separator'
},
{
label: 'Go to Disk Image',
click: () => send('disk-image')
}
]
})
Menu.setApplicationMenu(Menu.buildFromTemplate(menu))
}
module.exports = {
createMenu
}

View File

@@ -1,45 +0,0 @@
const { remote, shell, ipcRenderer } = require('electron')
const path = require('path')
const { STATE_PATH, resetState, restoreState, saveState } = require('./state')
window.windows95 = {
STATE_PATH,
restoreState,
resetState,
saveState,
showDiskImage () {
const imagePath = path.join(__dirname, 'images/windows95.img')
.replace('app.asar', 'app.asar.unpacked')
shell.showItemInFolder(imagePath)
},
quit: () => remote.app.quit()
}
ipcRenderer.on('ctrlaltdel', () => {
if (!window.emulator || !window.emulator.is_running) return
window.emulator.keyboard_send_scancodes([
0x1D, // ctrl
0x38, // alt
0x53, // delete
// break codes
0x1D | 0x80,
0x38 | 0x80,
0x53 | 0x80
])
})
ipcRenderer.on('restart', () => {
if (!window.emulator || !window.emulator.is_running) return
window.emulator.restart()
})
ipcRenderer.on('disk-image', () => {
windows95.showDiskImage()
})

View File

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

34
src/renderer/app.tsx Normal file
View 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();

View File

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

Binary file not shown.

Binary file not shown.

View File

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

106
src/renderer/card-drive.tsx Normal file
View File

@@ -0,0 +1,106 @@
import * as React from "react";
import { shell } from "electron";
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>
);
}
}

View 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 });
}
}

View 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>
);
}
}

View 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
View 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
View File

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

View File

@@ -1,55 +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="status">
Disk: <span id="disk-status">Idle</span>
| CPU Speed: <span id="cpu-status">0</span>
| <a onclick="document.querySelector('#status').style.display='none'">Hide</a>
</div>
<div id="buttons">
<div id="start-buttons">
<!-- <div class="btn" id="win98">Windows 98</div> -->
<div class="btn btn-start" id="win95">
Start Windows 95
<br />
<small>Hit ESC to lock or unlock your mouse</small>
</div>
<!-- <div class="btn" id="win1">Windows 1</div> -->
</div>
<div id="other-buttons">
<div class="btn" id="reset">Reset Machine & Delete State</div>
<div class="btn" id="floppy">Insert Floppy Disk</div>
<div class="btn" id="discard-state">Discard State & Boot From Scratch</div>
<div class="btn" id="show-disk-image">Show Disk Image</div>
<input id="file-input" type='file'>
</div>
<div id="information">
<p id="floppy-path"></p>
<p>You can insert a floppy disk image with the ".img" format.</p>
<p>
Boot the machine from scratch if you've inserted a new floppy disk
or if you've changed the disk image.
</p>
</div>
</div>
<div id="emulator" style="height: 100vh; width: 100vw">
<div style="white-space: pre; font: 14px monospace; line-height: 14px"></div>
<canvas style="display: none"></canvas>
</div>
<script type="module">
import("es6://renderer.js")
</script>
</body>
</html>

View File

@@ -1,36 +0,0 @@
const $ = document.querySelector.bind(document)
export function setupInfo () {
const diskStatus = $('#disk-status')
const cpuStatus = $('#cpu-status')
let lastCounter = 0
let lastTick = 0
window.emulator.add_listener('ide-read-start', () => {
diskStatus.innerHTML = 'Read'
})
window.emulator.add_listener('ide-read-end', () => {
diskStatus.innerHTML = 'Idle'
})
window.emulator.add_listener('ide-write-end', () => {
diskStatus.innerHTML = 'Idle'
})
window.emulator.add_listener('screen-set-size-graphical', (...args) => {
console.log(...args)
})
setInterval(() => {
const now = Date.now()
const instructionCounter = window.emulator.get_instruction_counter()
const ips = instructionCounter - lastCounter
const deltaTime = now - lastTick
lastTick = now
lastCounter = instructionCounter
cpuStatus.innerHTML = Math.round(ips / deltaTime)
}, 500)
}

View 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.

View File

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

View File

@@ -1,66 +0,0 @@
import { setupState } from 'es6://app-state.js'
import { setupClickListener, setupEscListener, setupCloseListener } from 'es6://listeners.js'
import { toggleButtons, setupButtons } from 'es6://buttons.js'
import { setupInfo } from 'es6://info.js'
setupState()
/**
* The main method executing the VM.
*/
async function main () {
// New v86 instance
window.emulator = new V86Starter({
memory_size: 64 * 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: 242049024
},
fda: {
buffer: window.appState.floppyFile || undefined
},
boot_order: 0x132
})
// High DPI support
if (navigator.userAgent.includes('Windows')) {
const scale = window.devicePixelRatio
window.emulator.screen_adapter.set_scale(scale, scale)
}
// Restore state. We can't do this right away
// and randomly chose 500ms as the appropriate
// wait time (lol)
setTimeout(async () => {
if (!window.appState.bootFresh) {
windows95.restoreState()
}
setupInfo()
window.appState.cursorCaptured = true
window.emulator.lock_mouse()
window.emulator.run()
}, 500)
}
function start () {
document.body.className = ''
toggleButtons(false)
setupClickListener()
main()
}
setupEscListener()
setupCloseListener()
setupButtons(start)

View 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
View File

View File

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

View File

@@ -1,76 +0,0 @@
const fs = require('fs-extra')
const path = require('path')
const { remote } = require('electron')
const DEFAULT_PATH = path.join(__dirname, 'images/default-state.bin')
const STATE_PATH = path.join(remote.app.getPath('userData'), 'state.bin')
/**
* Returns the current machine's state - either what
* we have saved or alternatively the default state.
*
* @returns {ArrayBuffer}
*/
function getState () {
const statePath = fs.existsSync(STATE_PATH)
? STATE_PATH
: DEFAULT_PATH
return fs.readFileSync(statePath).buffer
}
/**
* Resets a saved state by simply deleting it.
*
* @returns {Promise<void>}
*/
async function resetState () {
if (fs.existsSync(STATE_PATH)) {
return fs.remove(STATE_PATH)
}
}
/**
* Saves the current VM's state.
*
* @returns {Promise<void>}
*/
async function saveState () {
return new Promise((resolve) => {
if (!window.emulator || !window.emulator.save_state) {
return resolve()
}
window.emulator.save_state(async (error, newState) => {
if (error) {
console.log(error)
return
}
await fs.outputFile(STATE_PATH, Buffer.from(newState))
console.log(`Saved state to ${STATE_PATH}`)
resolve()
})
})
}
/**
* Restores the VM's state.
*/
function restoreState () {
try {
window.emulator.restore_state(getState())
} catch (error) {
console.log(`Could not read state file. Maybe none exists?`, error)
}
}
module.exports = {
STATE_PATH,
saveState,
restoreState,
resetState,
getState
}

8
src/utils/devmode.ts Normal file
View File

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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
static/drive.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

10
static/entitlements.plist Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

16
static/index.html Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
static/reset.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
static/run.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
static/select-floppy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
static/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
static/show-disk-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
static/start.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

7
tools/generateAssets.js Normal file
View File

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

30
tools/notarize.js Normal file
View File

@@ -0,0 +1,30 @@
const { notarize } = require('electron-notarize');
const path = require('path');
const buildOutput = path.resolve(
__dirname,
'..',
'out',
'windows95-darwin-x64',
'windows95.app'
);
module.exports = function () {
if (process.platform !== 'darwin') {
console.log('Not a Mac; skipping notarization');
return;
}
console.log('Notarizing...');
return notarize({
appBundleId: 'com.felixrieseberg.windows95',
appPath: buildOutput,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
ascProvider: 'LT94ZKYDCJ'
}).catch((e) => {
console.error(e);
throw e;
});
}

47
tools/parcel-build.js Normal file
View 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
View 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
View 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
View 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
View 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
}
}