148 Commits

Author SHA1 Message Date
Felix Rieseberg
94021edb61 Fix missing icon 2025-02-20 21:42:49 -08:00
Felix Rieseberg
6f2025ffc0 Improve reset 2025-02-20 21:19:55 -08:00
Felix Rieseberg
3a7b37fff0 Fix a few smaller build issues 2025-02-20 17:27:00 -08:00
Felix Rieseberg
16eb63e13b Add bg image 2025-02-20 17:16:51 -08:00
Felix Rieseberg
54fe721f4f Better gitignore 2025-02-20 17:16:31 -08:00
Felix Rieseberg
6dee2f45a2 Hide text screen when paused 2025-02-20 14:31:10 -08:00
Felix Rieseberg
e7e047b0a0 Deal with resedit segfaults 2025-02-20 10:54:02 -08:00
Felix Rieseberg
5a334abb13 Use dotenv 2025-02-20 07:57:57 -08:00
Felix Rieseberg
aacfae7ada Update forge config 2025-02-19 13:23:21 -08:00
Felix Rieseberg
9b87b77570 Update links 2025-02-18 23:03:37 -08:00
Felix Rieseberg
e6a0d931af Move out helper images 2025-02-18 23:03:08 -08:00
Felix Rieseberg
973580d60b Ignore more files 2025-02-18 23:01:44 -08:00
Felix Rieseberg
8fcf5eaed3 Remove old forge settings 2025-02-18 22:54:30 -08:00
Felix Rieseberg
e15d918fb3 Update version number to 4.0.0 2025-02-18 22:50:32 -08:00
Felix Rieseberg
b442c6db08 Update UI 2025-02-18 22:49:52 -08:00
Felix Rieseberg
5c946bbca4 Now with working network 2025-02-18 22:39:47 -08:00
Felix Rieseberg
c9e45a9f39 Update qemu docs 2025-02-18 22:39:35 -08:00
Felix Rieseberg
bc42ce3231 Remove fs-extra 2025-02-15 09:45:22 -08:00
Felix Rieseberg
d91e72ccc5 Upgrade TypeScript 2025-02-15 09:38:41 -08:00
Felix Rieseberg
bd40f00f8d Remove node-abi 2025-02-15 09:38:04 -08:00
Felix Rieseberg
1cbfca7451 Upgrade rimraf, node-abi, electron-squirrel-startup 2025-02-15 09:37:38 -08:00
Felix Rieseberg
7710c4b7af Upgrade prettier 2025-02-15 09:36:37 -08:00
Felix Rieseberg
4cce1f0740 Upgrade electron & electron-forge 2025-02-15 09:35:50 -08:00
Felix Rieseberg
f8ae78f247 Update v86 2025-02-15 09:34:41 -08:00
Felix Rieseberg
62f8eb2696 v3.1.2 2023-07-14 12:40:08 -07:00
Felix Rieseberg
da4b0dd728 Update v86 2023-07-14 12:39:57 -07:00
Felix Rieseberg
6cc05fa042 Upgrade dependencies 2023-07-14 11:52:30 -07:00
Felix Rieseberg
dda3707a23 Merge pull request #292 from fjbecerra/fix_docker_doc
fix docker mac doc
2023-03-15 11:10:24 -07:00
Francis Becerra
a4bcd7fb61 fix docker mac doc 2023-02-11 22:08:41 +00:00
Felix Rieseberg
17a8139346 Update links 2022-10-17 09:48:30 -07:00
Felix Rieseberg
489c7312d0 v3.1.1 2022-10-08 13:12:13 +02:00
Felix Rieseberg
c3537ae330 Handle promises 2022-10-08 13:12:08 +02:00
Felix Rieseberg
c483871df9 v3.1.0 2022-10-04 17:20:52 +02:00
Felix Rieseberg
e66cbd70db Update dependencies (Electron 18 > 21, Forge 63 > 66) 2022-10-04 17:07:41 +02:00
Felix Rieseberg
19a1bbc002 Update v86 to 5d02960 2022-10-04 10:09:26 +02:00
Felix Rieseberg
ef57e3a7fe Update links 2022-06-26 15:23:34 -07:00
Felix Rieseberg
7eae250c2a Fix OSFMount link 2022-06-26 14:05:49 -07:00
Felix Rieseberg
33db389d59 Start with a larger zoom 2022-06-26 13:01:05 -07:00
Felix Rieseberg
61f3269a45 Add loading gif 2022-06-26 11:55:09 -07:00
Felix Rieseberg
e5d897c663 Change how we build, hoping it'll fix macOS 2022-06-25 22:29:26 -07:00
Felix Rieseberg
a7ae665adc Build things step by step 2022-05-06 18:39:22 -07:00
Felix Rieseberg
bea2267f42 Fix Linux arch 2022-05-05 10:25:14 -07:00
Felix Rieseberg
a55d08fafc Update dist 2022-05-05 09:00:01 -07:00
Felix Rieseberg
97702cb01b Update Node in GitHub Workflows 2022-05-04 15:11:00 -07:00
Felix Rieseberg
12160a1ac4 3.0.0 2022-05-04 14:58:37 -07:00
Felix Rieseberg
3dd50db272 Cleanup 2022-05-04 14:51:49 -07:00
Felix Rieseberg
7b92d33584 Update v86, patch to use fs.readFile 2022-04-26 12:20:59 -07:00
Felix Rieseberg
24a1c30502 Update dependencies 2022-04-21 15:07:15 -07:00
Felix Rieseberg
7ce0863ae8 Merge pull request #228 from barfin/master
fixing the armv7hf linux rpm link in readme
2022-04-21 14:51:34 -07:00
Felix Rieseberg
90ec67fb16 Merge pull request #242 from hmsjy2017/patch-1
Correct the wrong links
2022-04-21 14:51:04 -07:00
Tony
9cab8e46f6 Correct the wrong links
I found that several quoted links were wrong, which caused the downloaded files to not meet expectations.
2021-08-24 01:30:04 +08:00
barfin
03b39d76b5 Update README.md 2021-04-27 13:21:04 +04:30
Felix Rieseberg
d8b4a139ac Merge pull request #203 from maacarbo/patch-1
macOS download links inverted for Intel/M1
2021-02-07 12:52:49 -08:00
Felix Rieseberg
9f4771bf26 chore: Update Readme 2021-01-31 09:24:51 -08:00
Maarten
552b97eec5 macOS download links inverted for Intel/M1 2021-01-07 10:31:39 +01:00
Felix Rieseberg
6c0f00170c Update README.md 2021-01-05 10:53:35 -08:00
Felix Rieseberg
e3b9a839f5 build: Update Node version 2021-01-04 10:31:33 -08:00
Felix Rieseberg
238b07b7dd build: Add a check-links tool 2021-01-04 09:54:20 -08:00
Felix Rieseberg
9dc1e422ff chore: One more readme update 2021-01-03 19:30:55 -08:00
Felix Rieseberg
ebe7427385 chore: Include images 2021-01-03 19:27:11 -08:00
Felix Rieseberg
3e3bee2062 chore: Pretty readme 2021-01-03 19:26:06 -08:00
Felix Rieseberg
c93b6878a9 chore: Update the readme again 2021-01-03 19:18:38 -08:00
Felix Rieseberg
d2e26ef5d1 chore: Update readme 2021-01-03 18:52:39 -08:00
Felix Rieseberg
c41befae64 build: Update dependencies 2021-01-03 12:58:37 -08:00
Felix Rieseberg
8b720750db build: Try to build for all archs 2021-01-02 15:38:15 -08:00
Felix Rieseberg
ee317ec5aa v2.3.0 2020-12-13 16:37:37 -08:00
Felix Rieseberg
d7c657e671 build: Build on ARM 2020-12-13 16:32:44 -08:00
Felix Rieseberg
7a8a54c76b build: Update Electron & React 2020-12-13 16:14:18 -08:00
Felix Rieseberg
c29f98b6bc Merge pull request #183 from felixrieseberg/dependabot/npm_and_yarn/electron-9.3.1
build(deps-dev): bump electron from 9.1.2 to 9.3.1
2020-11-08 09:48:10 -08:00
dependabot[bot]
8d1847a8d1 build(deps-dev): bump electron from 9.1.2 to 9.3.1
Bumps [electron](https://github.com/electron/electron) from 9.1.2 to 9.3.1.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/master/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v9.1.2...v9.3.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-06 19:02:36 +00:00
Felix Rieseberg
194f4fabaf Merge pull request #169 from PeterVatistas/patch-1
Fix amd64.deb download link
2020-08-04 16:00:56 -07:00
Peter Vatistas
3f4a5e97fa Revised all download links
All 7 download links were fixed using the latest release.
2020-08-04 10:01:46 -07:00
Peter Vatistas
3eb789d055 Fix amd64.deb download link 2020-08-03 19:33:08 -07:00
Felix Rieseberg
8a8f064864 docs Update readme 2020-08-03 15:50:13 -07:00
Felix Rieseberg
58add05655 build: Build on push 2020-08-02 13:33:54 -07:00
Felix Rieseberg
0a400d915f build: Fix macOS cert 2020-08-02 13:27:05 -07:00
Felix Rieseberg
f615e7754c 2.2.2 2020-08-02 13:13:52 -07:00
Felix Rieseberg
92717c8047 chore: Updated prettier run 2020-08-02 13:11:20 -07:00
Felix Rieseberg
045b83f843 build: Move to GitHub Actions, upgrade dependencies 2020-08-02 13:09:13 -07:00
Felix Rieseberg
1dd3b76187 Merge pull request #153 from PF94/patch-1
Improve HELP.MD a little bit.
2020-01-15 15:38:04 -08:00
PF94
4b1dd6146c Improve HELP.MD a little bit.
* Add some caplization to "Command Prompt", "Window Mode" and "Full Screen" mode
* Changed "brick" to "mess up" as "bricked" means that the operating system is permanently damaged.
* Add another "enter" for the "What's the FrontPage Username and Password?" section, now people won't accidently connect as "windows95Password".
* Fix DisplacedGamers missing "s" in the end.
2020-01-11 22:18:39 -05:00
Felix Rieseberg
3601599ff1 docs: Update readme 2019-12-04 10:17:07 -08:00
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
128 changed files with 21235 additions and 7379 deletions

1
.gitattributes vendored Normal file
View File

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

BIN
.github/images/linux.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
.github/images/macos.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
.github/images/windows.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

121
.github/workflows/build.yml vendored Normal file
View File

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

14
.gitignore vendored
View File

@@ -1,4 +1,16 @@
node_modules
out
src/images
.DS_Store
/images*/
/helper-images/
dist
!.github/images
*.code-workspace
*.pfx
Microsoft.Trusted.Signing.Client*
trusted-signing-metadata.json
.env
electron-windows-sign.log

22
HELP.md
View File

@@ -1,24 +1,14 @@
# 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
## 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 a window mode. (Thanks to @DisplacedGamer for that wisdom)
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
Restart the application and click on the "Reset machine & delete state" button.
You can find it in the lower left of the screen. Then, hit the "Start Windows 95"
button to start your virtual machine again.
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 Windows 10, macOS, or Linux, you can probably "mount" the
virtual hard drive used by `windows95` to add files. Hit the "Show Disk Image"
button in the lower right of the app, which will take you to the disk image.
On both Windows 10 and macOS, double-click the disk image to open it.
On Linux, search the Internet for instructions on how to mount an `img` disk
image on your distribution.

View File

@@ -1,4 +1,4 @@
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:

117
README.md
View File

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

BIN
assets/boot.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

16
assets/entitlements.plist Normal file
View 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>

BIN
bios/seabios.bin Normal file

Binary file not shown.

BIN
bios/vgabios.bin Normal file

Binary file not shown.

View File

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

48
docs/qemu.md Normal file
View File

@@ -0,0 +1,48 @@
# QEMU Instructions
The image built here was made with QEMU. In this doc, I'm keeping instructions
around.
Disk image creation
```sh
qemu-img create -f raw windows95_v4.raw 1G
```
ISO CD image creation
```sh
hdiutil makehybrid -o output.iso /path/to/folder -iso -joliet
```
Installation
```sh
qemu-system-i386 \
-cdrom Win95_OSR25.iso \
-m 128 \
-hda windows95.img \
-device sb16 \
-nic user,model=ne2k_pci \
-fda Win95_boot.img \
-boot a \
-M pc,acpi=off \
-cpu pentium
```
- Boot from floppy
- Run `fdisk` and `format c:`
- Run `D:\setup.exe` with `24796-OEM-0014736-66386`
- After completing setup and restarting your computer, you might get an IOS Windows protection error
- Use `fix95cpu.ima` as a bootable floppy to fix
- Use `vga-driver.iso` to install different video driver
```sh
qemu-system-i386 \
-m 128 \
-hda images/windows95.img \
-device sb16 \
-M pc,acpi=off \
-cpu pentium \
-netdev user,id=mynet0 \
-device ne2k_isa,netdev=mynet0,irq=10
```

View File

@@ -1,21 +1,83 @@
const path = require('path');
const fs = require('fs');
const package = require('./package.json');
require('dotenv').config()
process.env.TEMP = process.env.TMP = `C:\\Users\\FelixRieseberg\\AppData\\Local\\Temp`
const FLAGS = {
SIGNTOOL_PATH: process.env.SIGNTOOL_PATH,
AZURE_CODE_SIGNING_DLIB: process.env.AZURE_CODE_SIGNING_DLIB || path.join(__dirname, 'Microsoft.Trusted.Signing.Client.1.0.60/bin/x64/Azure.CodeSigning.Dlib.dll'),
AZURE_METADATA_JSON: process.env.AZURE_METADATA_JSON || path.resolve(__dirname, 'trusted-signing-metadata.json'),
AZURE_TENANT_ID: process.env.AZURE_TENANT_ID,
AZURE_CLIENT_ID: process.env.AZURE_CLIENT_ID,
AZURE_CLIENT_SECRET: process.env.AZURE_CLIENT_SECRET,
APPLE_ID: process.env.APPLE_ID,
APPLE_ID_PASSWORD: process.env.APPLE_ID_PASSWORD,
}
fs.writeFileSync(FLAGS.AZURE_METADATA_JSON, JSON.stringify({
Endpoint: process.env.AZURE_CODE_SIGNING_ENDPOINT || "https://wcus.codesigning.azure.net",
CodeSigningAccountName: process.env.AZURE_CODE_SIGNING_ACCOUNT_NAME,
CertificateProfileName: process.env.AZURE_CODE_SIGNING_CERTIFICATE_PROFILE_NAME,
}, null, 2));
const windowsSign = {
signToolPath: FLAGS.SIGNTOOL_PATH,
signWithParams: `/v /dlib ${FLAGS.AZURE_CODE_SIGNING_DLIB} /dmdf ${FLAGS.AZURE_METADATA_JSON}`,
timestampServer: "http://timestamp.acs.microsoft.com",
hashes: ["sha256"],
}
module.exports = {
hooks: {
generateAssets: require('./tools/generateAssets'),
},
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)',
},
osxNotarize: {
appleId: FLAGS.APPLE_ID,
appleIdPassword: FLAGS.APPLE_ID_PASSWORD,
teamId: 'LT94ZKYDCJ'
},
windowsSign,
ignore: [
/\/assets(\/?)/,
/\/docs(\/?)/,
/\/tools(\/?)/,
/\/src\/.*\.ts/,
/\/test(\/?)/,
/\/@types(\/?)/,
/\/helper-images(\/?)/,
/package-lock\.json/,
/README\.md/,
/tsconfig\.json/,
/Dockerfile/,
/issue_template\.md/,
/HELP\.md/,
/forge\.config\.js/,
/\.github(\/?)/,
/\.circleci(\/?)/,
/\.vscode(\/?)/,
/\.gitignore/,
/\.gitattributes/,
/\.eslintignore/,
/\.eslintrc/,
/\.prettierrc/,
/\/Microsoft\.Trusted\.Signing\.Client.*/,
/\/trusted-signing-metadata/,
]
},
makers: [
{
@@ -28,16 +90,17 @@ module.exports = {
exe: 'windows95.exe',
noMsi: true,
remoteReleases: '',
iconUrl: 'https://raw.githubusercontent.com/felixrieseberg/windows95/master/assets/icon.ico',
loadingGif: './assets/boot.gif',
setupExe: `windows95-${package.version}-setup-${arch}.exe`,
setupIcon: path.resolve(__dirname, 'assets', 'icon.ico'),
certificateFile: process.env.WINDOWS_CERTIFICATE_FILE,
certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD
windowsSign
}
}
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin']
platforms: ['darwin', 'win32']
},
{
name: '@electron-forge/maker-deb',
@@ -46,10 +109,6 @@ module.exports = {
{
name: '@electron-forge/maker-rpm',
platforms: ['linux']
},
{
name: '@electron-forge/maker-flatpak',
platforms: ['linux']
}
]
};

22415
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,19 @@
{
"name": "windows95",
"productName": "windows95",
"version": "2.0.0",
"version": "4.0.0",
"description": "Windows 95, in an app. I'm sorry.",
"main": "src/index.js",
"main": "./dist/src/main/main.js",
"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} && npm run check-links",
"less": "node ./tools/lessc.js",
"tsc": "tsc -p tsconfig.json --noEmit",
"check-links": "node tools/check-links.js",
"postinstall": "patch-package"
},
"keywords": [],
"author": "Felix Rieseberg, felix@felixrieseberg.com",
@@ -17,30 +21,30 @@
"config": {
"forge": "./forge.config.js"
},
"standard": {
"globals": [
"appState",
"V86Starter",
"windows95"
],
"ignore": [
"/src/renderer/lib/*.js"
]
},
"dependencies": {
"electron-squirrel-startup": "^1.0.0",
"fs-extra": "^7.0.1",
"update-electron-app": "^1.3.0"
"electron-squirrel-startup": "^1.0.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"update-electron-app": "^2.0.1"
},
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.32",
"@electron-forge/maker-deb": "^6.0.0-beta.32",
"@electron-forge/maker-flatpak": "^6.0.0-beta.32",
"@electron-forge/maker-rpm": "^6.0.0-beta.32",
"@electron-forge/maker-squirrel": "^6.0.0-beta.32",
"@electron-forge/maker-zip": "^6.0.0-beta.32",
"electron": "4.0.4",
"node-abi": "^2.6.0",
"standard": "^12.0.1"
"@electron-forge/cli": "7.6.1",
"@electron-forge/maker-deb": "7.6.1",
"@electron-forge/maker-flatpak": "^7.6.1",
"@electron-forge/maker-rpm": "^7.6.1",
"@electron-forge/maker-squirrel": "^7.6.1",
"@electron-forge/maker-zip": "^7.6.1",
"@electron-forge/publisher-github": "^7.6.1",
"@types/node": "^20",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"dotenv": "^16.4.7",
"electron": "34.2.0",
"less": "^3.13.0",
"parcel-bundler": "^1.12.5",
"patch-package": "^8.0.0",
"prettier": "^3.5.1",
"rimraf": "^6.0.1",
"typescript": "^5.7.3"
}
}

View File

@@ -0,0 +1,32 @@
diff --git a/node_modules/@electron/packager/dist/win32.js b/node_modules/@electron/packager/dist/win32.js
index 5399b3e..f3b6e88 100644
--- a/node_modules/@electron/packager/dist/win32.js
+++ b/node_modules/@electron/packager/dist/win32.js
@@ -57,7 +57,26 @@ class WindowsApp extends platform_1.App {
resOpts.iconPath = icon;
}
(0, common_1.debug)(`Running resedit with the options ${JSON.stringify(resOpts)}`);
- await (0, resedit_1.resedit)(this.electronBinaryPath, resOpts);
+
+ // This causes segmentation faults for me on multiple machines
+ // It's unclear why exactly but this spawn hack fixes it
+ // await (0, resedit_1.resedit)(this.electronBinaryPath, resOpts);
+
+ const { spawnSync } = require('child_process');
+ const resEditProcess = spawnSync('node', [
+ 'C:\\Users\\FelixRieseberg\\Code\\windows95\\tools\\resedit.js',
+ this.electronBinaryPath
+ ], {
+ stdio: 'inherit'
+ });
+
+ if (resEditProcess.error) {
+ throw resEditProcess.error;
+ }
+
+ if (resEditProcess.status !== 0) {
+ throw new Error(`Resedit process exited with code ${resEditProcess.status}`);
+ }
}
async signAppIfSpecified() {
const windowsSignOpt = this.opts.windowsSign;

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
}

32
src/cache.ts Normal file
View File

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

View File

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

33
src/constants.ts Normal file
View File

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

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

29
src/less/emulator.less Normal file
View File

@@ -0,0 +1,29 @@
#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;
}
#emulator-text-screen {
display: none;
visibility: hidden;
}
}

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

View File

@@ -0,0 +1,15 @@
export function encode(text: string) {
// Convert to windows-1252 compatible string by removing unsupported chars
let result = text.replaceAll(/[^\x00-\xFF]/g, '');
// If result would be empty, return original
if (!result.trim()) {
return text;
}
return result;
}
export function getEncoding() {
return `<meta http-equiv="Content-Type" content="text/html; charset=windows-1252">`;
}

View File

@@ -0,0 +1,157 @@
import { protocol, net } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import { generateDirectoryListing } from './page-directory-listing';
import { generateErrorPage } from './page-error';
import { log } from '../logging';
export interface FileEntry {
name: string;
fullPath: string;
stats: fs.Stats;
}
export const APP_INTERCEPT = 'http://windows95/';
export const MY_COMPUTER_INTERCEPT = 'http://my-computer/';
const interceptedUrls = [
MY_COMPUTER_INTERCEPT,
APP_INTERCEPT
];
export function setupFileServer() {
// Register protocol handler for our custom schema
protocol.handle('http', async (request) => {
if (!interceptedUrls.some(url => request.url.startsWith(url))) {
return fetch(request.url, {
headers: request.headers,
method: request.method,
body: request.body,
});
}
try {
const { fullPath, decodedPath } = getFilePath(request.url);
log(`FileServer: Handling request for ${request.url}`, { fullPath, decodedPath });
// Check if path exists
if (!fs.existsSync(fullPath)) {
return new Response(generateErrorPage(
'File or Directory Not Found',
decodedPath
), {
status: 404,
headers: {
'Content-Type': 'text/html'
}
});
}
// Check if it's a directory
const stats = await fs.promises.stat(fullPath);
if (stats.isDirectory()) {
// If we're in an app-intercept, check if there's an index.htm file in the directory
if (request.url.startsWith(APP_INTERCEPT)) {
const indexHtmlPath = path.join(fullPath, 'index.htm');
if (fs.existsSync(indexHtmlPath)) {
return serveFile(indexHtmlPath);
}
}
// Generate directory listing
const files = await fs.promises.readdir(fullPath);
const listing = generateDirectoryListing(fullPath, files);
return new Response(listing, {
status: 200,
headers: {
'Content-Type': 'text/html'
}
});
} else {
try {
return await serveFile(fullPath);
} catch (error) {
// Handle specific file read errors
if (error.code === 'EACCES') {
return new Response(generateErrorPage(
'Access Denied',
'You do not have permission to access this file'
), {
status: 403,
headers: {
'Content-Type': 'text/html'
}
});
}
// Re-throw other errors to be caught by outer try-catch
throw error;
}
}
} catch (error) {
const errorPage = generateErrorPage(
'Internal Server Error',
`An error occurred while processing your request: ${error.message}`
);
return new Response(errorPage, {
status: 500,
headers: {
'Content-Type': 'text/html'
}
});
}
});
}
function getFilePath(url: string) {
let urlPath: string;
let fullPath: string;
let decodedPath: string;
if (url.startsWith(APP_INTERCEPT)) {
fullPath = path.resolve(__dirname, '../../../static/www', url.replace(APP_INTERCEPT, ''));
decodedPath = '.';
} else if (url.startsWith(MY_COMPUTER_INTERCEPT)) {
urlPath = url.replace(MY_COMPUTER_INTERCEPT, '');
decodedPath = decodeURIComponent(urlPath);
fullPath = path.join('/', decodedPath);
} else {
throw new Error('Invalid URL');
}
return { fullPath, decodedPath };
}
async function serveFile(fullPath: string): Promise<Response> {
const fileData = await fs.promises.readFile(fullPath);
// Determine content type based on file extension
const ext = path.extname(fullPath).toLowerCase();
let contentType = 'application/octet-stream';
// Common content types
const contentTypes: Record<string, string> = {
'.htm': 'text/html',
'.html': 'text/html',
'.txt': 'text/plain',
'.css': 'text/css',
'.js': 'text/javascript',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif'
};
if (ext in contentTypes) {
contentType = contentTypes[ext];
}
return new Response(fileData, {
status: 200,
headers: {
'Content-Type': contentType
}
});
}

View File

@@ -0,0 +1,72 @@
import { Stats } from "fs";
import { settings } from "../settings";
import { FileEntry } from "./fileserver";
const FILES_TO_HIDE_ON_DARWIN: string[] = [
'.DS_Store',
'.localized',
'.Trashes',
'.fseventsd',
'.Spotlight-V100',
'.file',
'.hotfiles.btree',
'.DocumentRevisions-V100',
'.TemporaryItems',
'.file (resource fork files)',
'.VolumeIcon.icns',
];
const FILES_TO_HIDE_ON_WINDOWS: string[] = [
'desktop.ini',
'Thumbs.db',
'ehthumbs.db',
'ehthumbs.db-shm',
'ehthumbs.db-wal',
];
const FILES_TO_HIDE_ON_LINUX: string[] = [];
export function shouldHideFile(file: FileEntry) {
if (isHiddenFile(file) && !settings.get('isFileServerShowingHiddenFiles')) {
return true;
}
if (isSystemHiddenFile(file) && !settings.get('isFileServerShowingSystemHiddenFiles')) {
return true;
}
return false;
}
export function isHiddenFile(file: FileEntry) {
if (process.platform === 'win32') {
return (file.stats.mode & 0x2) === 0x2;
} else {
return file.name.startsWith('.');
}
}
export function isSystemHiddenFile(file: FileEntry) {
return getFilesToHide().some(hiddenFile => file.name.endsWith(hiddenFile));
}
let _filesToHide: string[];
function getFilesToHide() {
if (_filesToHide) {
return _filesToHide;
}
if (process.platform === 'darwin') {
_filesToHide = FILES_TO_HIDE_ON_DARWIN;
} else if (process.platform === 'win32') {
_filesToHide = FILES_TO_HIDE_ON_WINDOWS;
} else {
_filesToHide = FILES_TO_HIDE_ON_LINUX;
}
return _filesToHide;
}

View File

@@ -0,0 +1,120 @@
import path from "path";
import fs from "fs";
import { APP_INTERCEPT, FileEntry, MY_COMPUTER_INTERCEPT } from "./fileserver";
import { shouldHideFile } from "./hide-files";
import { encode, getEncoding } from "./encoding";
import { log } from "console";
import { app } from "electron";
export function generateDirectoryListing(currentPath: string, files: string[]): string {
const parentPath = path.dirname(currentPath || '/');
const title = currentPath === '/' ? 'My Host Computer' : `Directory: ${encode(currentPath)}`;
// Get file info and sort (directories first, then alphabetically)
const items = files
.map(name => {
const fullPath = path.join(currentPath, name);
let stats: fs.Stats;
try {
stats = fs.statSync(fullPath);
} catch (error) {
log(`FileServer: Failed to get stats for ${fullPath}: ${error}`);
stats = new fs.Stats();
}
return {
name,
fullPath,
stats
} as FileEntry;
})
.filter(entry => entry.stats && !shouldHideFile(entry))
.sort((a, b) => {
if (a.stats.isDirectory() !== b.stats.isDirectory()) {
return a.stats.isDirectory() ? -1 : 1;
}
return a.name.localeCompare(b.name);
})
.map(getFileLiHtml)
.join('')
// Generate very simple HTML that works in IE 5.5
return `
<html>
<head>
${getEncoding()}
<title>${title}</title>
</head>
<body>
<h2>${title}</h2>
<p>${getParentFolderLinkHtml(parentPath)} | ${getDesktopLinkHtml()} | ${getDownloadsLinkHtml()}</p>
<p>
<ul>
${items}
</ul>
</body>
</html>
`;
}
function getParentFolderLinkHtml(parentPath: string) {
return `
${getIconHtml('folder.gif')}
<a href="${MY_COMPUTER_INTERCEPT}${encodeURI(parentPath)}">
[Parent Directory]
</a>
`;
}
function getDesktopLinkHtml() {
const desktopPath = app.getPath('desktop');
return `
${getIconHtml('desktop.gif')}
<a href="${MY_COMPUTER_INTERCEPT}${encodeURI(desktopPath)}">
Desktop
</a>
`;
}
function getDownloadsLinkHtml() {
const downloadsPath = app.getPath('downloads');
return `
${getIconHtml('network.gif')}
<a href="${MY_COMPUTER_INTERCEPT}${encodeURI(downloadsPath)}">
Downloads
</a>
`;
}
function getIconHtml(icon: string) {
return `<img src="${APP_INTERCEPT}images/${icon}" style="vertical-align: middle; margin-right: 5px;" width="16" height="16">`;
}
function getFileLiHtml(entry: FileEntry) {
const encodedPath = encodeURI(entry.fullPath);
const sizeDisplay = entry.stats.isDirectory() ? '' : ` (${formatFileSize(entry.stats.size)})`;
const icon = entry.stats.isDirectory() ? getIconHtml('folder.gif') : getIconHtml('doc.gif');
return `<li>
${icon}
<a href="${MY_COMPUTER_INTERCEPT}${encodedPath}">
${getDisplayName(entry)}
</a>
${sizeDisplay}
</li>`;
}
function getDisplayName(entry: FileEntry) {
return encode(entry.stats.isDirectory() ? `[${entry.name}]` : entry.name);
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

View File

@@ -0,0 +1,22 @@
import { getEncoding } from "./encoding";
import { MY_COMPUTER_INTERCEPT } from "./fileserver";
export function generateErrorPage(errorMessage: string, requestedPath: string): string {
return `
<html>
<head>
${getEncoding()}
<title>Error - File Not Found</title>
</head>
<body>
<h2>Error: ${errorMessage}</h2>
<p>windows95 failed to find the file or directory on your host computer: <code>${requestedPath}</code></p>
<p>Options:</p>
<ul>
<li><a href="${MY_COMPUTER_INTERCEPT}">Return to root directory</a></li>
<li><a href="javascript:history.back()">Go back to previous page</a></li>
</ul>
</body>
</html>
`;
}

14
src/main/ipc.ts Normal file
View File

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

3
src/main/logging.ts Normal file
View File

@@ -0,0 +1,3 @@
export function log(message: string, ...args: unknown[]) {
console.log(`[${new Date().toLocaleString()}] ${message}`, ...args);
}

73
src/main/main.ts Normal file
View File

@@ -0,0 +1,73 @@
import { app } from "electron";
import { isDevMode } from "../utils/devmode";
import { setupAboutPanel } from "./about-panel";
import { shouldQuit } from "./squirrel";
import { setupUpdates } from "./update";
import { getOrCreateWindow } from "./windows";
import { setupMenu } from "./menu";
import { setupIpcListeners } from "./ipc";
import { setupSession } from "./session";
import { setupFileServer } from './fileserver/fileserver';
/**
* 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";
setupSession();
setupIpcListeners();
getOrCreateWindow();
setupAboutPanel();
setupMenu();
setupUpdates();
setupFileServer();
}
/**
* 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();

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

@@ -0,0 +1,273 @@
import { app, shell, Menu, BrowserWindow, ipcMain, dialog } from "electron";
import { clearCaches } from "../cache";
import { IPC_COMMANDS } from "../constants";
import { isDevMode } from "../utils/devmode";
import { log } from "./logging";
const LINKS = {
homepage: "https://www.felixrieseberg.com",
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]) {
log(`Sending "${cmd}"`);
windows[0].webContents.send(cmd);
} else {
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: async () => {
const result = await dialog.showMessageBox({
type: 'warning',
buttons: ['Reset', 'Cancel'],
defaultId: 1,
title: 'Reset Machine',
message: 'Are you sure you want to reset the machine?',
detail: 'This will delete the machine state, including all changes you have made.',
});
if (result.response === 0) {
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));
}

20
src/main/session.ts Normal file
View File

@@ -0,0 +1,20 @@
import { session } from "electron";
export function setupSession() {
const s = session.defaultSession;
s.webRequest.onBeforeSendHeaders(
(details, callback) => {
callback({ requestHeaders: { Origin: '*', ...details.requestHeaders } });
},
);
s.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
'Access-Control-Allow-Origin': ['*'],
...details.responseHeaders,
},
});
});
}

72
src/main/settings.ts Normal file
View File

@@ -0,0 +1,72 @@
import * as fs from 'fs';
import * as path from 'path';
import { app } from 'electron';
export interface Settings {
isFileServerEnabled: boolean;
isFileServerShowingHiddenFiles: boolean;
isFileServerShowingSystemHiddenFiles: boolean;
}
const DEFAULT_SETTINGS: Settings = {
isFileServerEnabled: true,
isFileServerShowingHiddenFiles: false,
isFileServerShowingSystemHiddenFiles: false,
};
class SettingsManager {
private filePath: string;
private data: Settings;
constructor() {
this.filePath = path.join(app.getPath('userData'), 'settings.json');
this.data = this.load();
}
private load(): Settings {
try {
if (fs.existsSync(this.filePath)) {
const fileContent = fs.readFileSync(this.filePath, 'utf8');
const parsed = JSON.parse(fileContent);
return {
...DEFAULT_SETTINGS,
...parsed,
};
}
} catch (error) {
console.error('Error loading settings:', error);
}
return DEFAULT_SETTINGS;
}
private save(): void {
try {
fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
} catch (error) {
console.error('Error saving settings:', error);
}
}
get(key: keyof Settings): any {
return this.data[key];
}
set(key: keyof Settings, value: any): void {
this.data[key] = value;
this.save();
}
delete(key: keyof Settings): void {
delete this.data[key];
this.save();
}
clear(): void {
this.data = DEFAULT_SETTINGS;
this.save();
}
}
export const settings = new SettingsManager();

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",
});
}
}

40
src/main/windows.ts Normal file
View File

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

View File

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

View File

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

View File

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

43
src/renderer/app.tsx Normal file
View File

@@ -0,0 +1,43 @@
export interface Win95Window extends Window {
emulator: any;
win95: {
app: App;
};
}
declare let window: Win95Window;
/**
* 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.

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()
})
// Floppy
$('#floppy').addEventListener('click', () => {
$('#file-input').click()
})
// Floppy (Hidden Input)
$('#file-input').addEventListener('change', (event) => {
window.appState.floppyFile = event.target.files && event.target.files.length > 0
? event.target.files[0]
: null
if (window.appState.floppyFile) {
$('#floppy-path').innerHTML = `Inserted Floppy Disk: ${window.appState.floppyFile.path}`
}
})
}
export function toggleButtons (forceTo) {
const buttonElements = $('#buttons')
if (buttonElements.style.display !== 'none' || forceTo === false) {
buttonElements.style.display = 'none'
} else {
buttonElements.style.display = undefined
}
}

View File

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

531
src/renderer/emulator.tsx Normal file
View File

@@ -0,0 +1,531 @@
import * as React from "react";
import * as fs from "fs";
import * as path from "path";
import { ipcRenderer, shell, webUtils } 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 { getStatePath } from "./utils/get-state-path";
import { Win95Window } from "./app";
import { resetState } from "./utils/reset-state";
declare let window: Win95Window;
export interface EmulatorState {
currentUiCard: "start" | "settings";
emulator?: any;
scale: number;
floppyFile?: File;
cdromFile?: File;
isBootingFresh: boolean;
isCursorCaptured: boolean;
isInfoDisplayed: boolean;
isRunning: boolean;
}
export class Emulator extends React.Component<{}, EmulatorState> {
private isQuitting = false;
private isResetting = false;
constructor(props: {}) {
super(props);
this.startEmulator = this.startEmulator.bind(this);
this.stopEmulator = this.stopEmulator.bind(this);
this.restartEmulator = this.restartEmulator.bind(this);
this.resetEmulator = this.resetEmulator.bind(this);
this.bootFromScratch = this.bootFromScratch.bind(this);
this.state = {
isBootingFresh: false,
isCursorCaptured: false,
isRunning: false,
currentUiCard: "start",
isInfoDisplayed: true,
// We can start pretty large
// If it's too large, it'll just grow until it hits borders
scale: 2,
};
this.setupInputListeners();
this.setupIpcListeners();
this.setupUnloadListeners();
}
/**
* We want to capture and release the mouse at appropriate times.
*/
public setupInputListeners() {
// ESC
document.onkeydown = (evt) => {
const { isCursorCaptured } = this.state;
evt = evt || window.event;
if (evt.keyCode === 27) {
if (isCursorCaptured) {
this.unlockMouse();
} else {
this.lockMouse();
}
evt.stopPropagation();
}
};
// Click
document.addEventListener("click", () => {
const { isRunning } = this.state;
if (isRunning) {
this.lockMouse();
}
});
}
/**
* Save the emulator's state to disk during exit.
*/
public setupUnloadListeners() {
const handleClose = async () => {
await this.saveState();
console.log(`Unload: Now done, quitting again.`);
this.isQuitting = true;
setImmediate(() => {
ipcRenderer.invoke(IPC_COMMANDS.APP_QUIT);
});
};
window.onbeforeunload = (event: Event) => {
if (this.isQuitting || this.isResetting) {
console.log(`Unload: Not preventing`);
return;
}
console.log(`Unload: Preventing to first save state`);
handleClose();
event.preventDefault();
event.returnValue = false;
};
}
/**
* Setup the various IPC messages sent to the renderer
* from the main process
*/
public setupIpcListeners() {
ipcRenderer.on(IPC_COMMANDS.MACHINE_CTRL_ALT_DEL, () => {
this.sendKeys([
0x1d, // ctrl
0x38, // alt
0x53, // delete
]);
});
ipcRenderer.on(IPC_COMMANDS.MACHINE_ALT_F4, () => {
this.sendKeys([
0x38, // alt
0x3e, // f4
]);
});
ipcRenderer.on(IPC_COMMANDS.MACHINE_ALT_ENTER, () => {
this.sendKeys([
0x38, // alt
0, // enter
]);
});
ipcRenderer.on(IPC_COMMANDS.MACHINE_ESC, () => {
this.sendKeys([
0x18, // alt
]);
});
ipcRenderer.on(IPC_COMMANDS.MACHINE_STOP, this.stopEmulator);
ipcRenderer.on(IPC_COMMANDS.MACHINE_RESET, this.resetEmulator);
ipcRenderer.on(IPC_COMMANDS.MACHINE_START, this.startEmulator);
ipcRenderer.on(IPC_COMMANDS.MACHINE_RESTART, this.restartEmulator);
ipcRenderer.on(IPC_COMMANDS.TOGGLE_INFO, () => {
this.setState({ isInfoDisplayed: !this.state.isInfoDisplayed });
});
ipcRenderer.on(IPC_COMMANDS.SHOW_DISK_IMAGE, () => {
this.showDiskImage();
});
ipcRenderer.on(IPC_COMMANDS.ZOOM_IN, () => {
this.setScale(this.state.scale * 1.2);
});
ipcRenderer.on(IPC_COMMANDS.ZOOM_OUT, () => {
this.setScale(this.state.scale * 0.8);
});
ipcRenderer.on(IPC_COMMANDS.ZOOM_RESET, () => {
this.setScale(1);
});
}
/**
* If the emulator isn't running, this is rendering the, erm, UI.
*
* 🤡
*/
public renderUI() {
const { isRunning, currentUiCard, floppyFile, cdromFile } = this.state;
if (isRunning) {
return null;
}
let card;
if (currentUiCard === "settings") {
card = (
<CardSettings
setFloppy={(floppyFile) => this.setState({ floppyFile })}
setCdrom={(cdromFile) => this.setState({ cdromFile })}
bootFromScratch={this.bootFromScratch}
floppy={floppyFile}
cdrom={cdromFile}
/>
);
} else {
card = <CardStart startEmulator={this.startEmulator} />;
}
return (
<>
{card}
<StartMenu
navigate={(target) => this.setState({ currentUiCard: target as "start" | "settings" })}
/>
</>
);
}
/**
* Yaknow, render things and stuff.
*/
public render() {
return (
<>
{this.renderInfo()}
{this.renderUI()}
<div id="emulator">
<div id="emulator-text-screen"></div>
<canvas id="emulator-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
console.log(`Showing disk image in ${CONSTANTS.IMAGE_PATH}`);
shell.showItemInFolder(CONSTANTS.IMAGE_PATH);
}
/**
* Start the actual emulator
*/
private async startEmulator() {
document.body.classList.remove("paused");
const cdromPath = this.state.cdromFile
? webUtils.getPathForFile(this.state.cdromFile)
: null;
const options = {
wasm_path: path.join(__dirname, "build/v86.wasm"),
memory_size: 128 * 1024 * 1024,
vga_memory_size: 64 * 1024 * 1024,
screen: {
container: document.getElementById("emulator"),
scale: 0
},
preserve_mac_from_state_image: true,
net_device: {
relay_url: "fetch",
type: "ne2k",
},
bios: {
url: path.join(__dirname, "../../bios/seabios.bin"),
},
vga_bios: {
url: path.join(__dirname, "../../bios/vgabios.bin"),
},
hda: {
url: CONSTANTS.IMAGE_PATH,
async: true,
size: await getDiskImageSize(CONSTANTS.IMAGE_PATH),
},
fda: this.state.floppyFile
? {
buffer: this.state.floppyFile,
}
: undefined,
cdrom: cdromPath
? {
url: cdromPath,
async: true,
size: await getDiskImageSize(cdromPath),
}
: undefined,
boot_order: 0x132,
};
console.log(`🚜 Starting emulator with options`, options);
window["emulator"] = new V86(options);
// New v86 instance
this.setState({
emulator: window["emulator"],
isRunning: true,
});
ipcRenderer.send(IPC_COMMANDS.MACHINE_STARTED);
// Restore state. We can't do this right away
// and randomly chose 500ms as the appropriate
// wait time (lol)
setTimeout(async () => {
if (!this.state.isBootingFresh) {
this.restoreState();
}
this.lockMouse();
this.state.emulator.run();
this.state.emulator.screen_set_scale(this.state.scale);
}, 500);
}
/**
* Restart emulator
*/
private restartEmulator() {
if (this.state.emulator && this.state.isRunning) {
console.log(`🚜 Restarting emulator`);
this.state.emulator.restart();
} else {
console.log(`🚜 Restarting emulator failed: Emulator not running`);
}
}
/**
* Stop the emulator
*/
private async stopEmulator() {
const { emulator, isRunning } = this.state;
if (!emulator || !isRunning) {
return;
}
console.log(`🚜 Stopping emulator`);
await this.saveState();
this.unlockMouse();
await emulator.stop();
this.setState({ isRunning: false });
this.resetCanvas();
document.body.classList.add("paused");
ipcRenderer.send(IPC_COMMANDS.MACHINE_STOPPED);
}
/**
* Reset the emulator by reloading the whole page
*/
private async resetEmulator() {
this.isResetting = true;
await this.stopEmulator();
await resetState();
document.location.reload();
}
/**
* Take the emulators state and write it to disk. This is possibly
* a fairly big file.
*/
private async saveState(): Promise<void> {
const { emulator } = this.state;
const statePath = await getStatePath();
if (!emulator || !emulator.save_state) {
console.log(`restoreState: No emulator present`);
return;
}
try {
const newState = await emulator.save_state();
await fs.promises.writeFile(statePath, Buffer.from(newState), {
flush: true
});
} catch (error) {
console.warn(`saveState: Could not save state`, error);
}
}
/**
* Restores state to the emulator.
*/
private async restoreState() {
const { emulator, isBootingFresh } = this.state;
const state = await this.getState();
if (isBootingFresh) {
console.log(`restoreState: Booting fresh, not restoring.`);
return;
} else if (!state) {
console.log(`restoreState: No state present, not restoring.`);
return;
} else if (!emulator) {
console.log(`restoreState: No emulator present`);
return;
}
try {
await this.state.emulator.restore_state(state);
} catch (error) {
console.log(
`restoreState: Could not read state file. Maybe none exists?`,
error,
);
}
}
/**
* Returns the current machine's state - either what
* we have saved or alternatively the default state.
*
* @returns {ArrayBuffer}
*/
private async getState(): Promise<ArrayBuffer | null> {
const expectedStatePath = await getStatePath();
const statePath = fs.existsSync(expectedStatePath)
? expectedStatePath
: CONSTANTS.DEFAULT_STATE_PATH;
if (fs.existsSync(statePath)) {
return fs.readFileSync(statePath).buffer;
} else {
console.log(`getState: No state file found at ${statePath}`);
}
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);
}
}
/**
* Reset the canvas
*/
private resetCanvas() {
const canvas = document.getElementById("emulator-canvas");
if (canvas instanceof HTMLCanvasElement) {
const ctx = canvas.getContext('2d');
ctx?.clearRect(0, 0, canvas.width, canvas.height);
}
}
}

2
src/renderer/global.d.ts vendored Normal file
View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
Copyright (c) 2012-2018, Fabian Hemmer
Copyright (c) 2012, The v86 contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -19,8 +19,4 @@ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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.
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

BIN
src/renderer/lib/build/v86.wasm Executable file

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +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
if (window.appState.isResetting) return
handleClose()
event.preventDefault()
event.returnValue = false
}
}
export function setupEscListener () {
document.onkeydown = function (evt) {
evt = evt || window.event
if (evt.keyCode === 27) {
if (window.appState.cursorCaptured) {
window.appState.cursorCaptured = false
window.emulator.mouse_set_status(false)
document.exitPointerLock()
} else {
window.appState.cursorCaptured = true
window.emulator.lock_mouse()
}
}
}
}
function onDocumentClick () {
if (!window.appState.cursorCaptured) {
window.appState.cursorCaptured = true
window.emulator.mouse_set_status(true)
window.emulator.lock_mouse()
}
}
export function setupClickListener () {
document.removeEventListener('click', onDocumentClick)
document.addEventListener('click', onDocumentClick)
}

View File

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

View File

@@ -0,0 +1,39 @@
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>
</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

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

View File

@@ -0,0 +1,14 @@
import fs from "fs";
import { getStatePath } from "./get-state-path";
export async function resetState() {
const statePath = await getStatePath();
if (fs.existsSync(statePath)) {
try {
await fs.promises.unlink(statePath);
} catch (error) {
console.error(`Failed to delete state file: ${error}`);
}
}
}

View File

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

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

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

View File

@@ -0,0 +1,22 @@
import * as fs from "fs";
import { CONSTANTS } from "../constants";
/**
* Get the size of the disk image
*
* @returns {number}
*/
export async function getDiskImageSize(path: string) {
try {
const stats = await fs.promises.stat(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/cdrom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

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">
<!-- libv86 -->
</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-cdrom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

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

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