91 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
84 changed files with 17020 additions and 10780 deletions

View File

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

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

16
.gitignore vendored
View File

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

View File

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

28
HELP.md
View File

@@ -1,34 +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
On the app's home screen, select "Settings" in the lower menu. Then, delete your
machine's state before starting it again - this time hopefully without issues.
## I want to install additional apps or games
If you are running macOS, or Linux, you can probably "mount" the
virtual hard drive used by `windows95` to add files. Hit the "Modify C: Drive"
button, which will take you to the disk image.
On macOS, double-click the disk image to open it.
On Windows 10, Windows will _think_ that it can open up the image, but will
actually fail to do so. Use a tool [like OSFMount][osfmount] to mount your
disk image.
On Linux, search the Internet for instructions on how to mount an `img` disk
image on your distribution. It's likely that you'll be able to run `mount`
with the image as input.
[osfmount]: https://www.osforensics.com/tools/mount-disk-images.html
## What's the FrontPage Username and Password?
Username: windows95
Password: password

View File

@@ -3,10 +3,95 @@
This is Windows 95, running in an [Electron](https://electronjs.org/) app. Yes, it's the full thing. I'm sorry.
## Downloads
| | Windows | macOS | Linux |
|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Standalone Download | 📦[Standalone, 32-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.1.1/windows95-2.1.1-win32-standalone-ia32.zip) <br /> 📦[Standalone, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.1.1/windows95-2.1.1-win32-standalone-x64.zip) | 📦[Standalone](https://github.com/felixrieseberg/windows95/releases/download/v2.1.1/windows95-macos-2.1.1.zip) | |
| Installer | 💽[Setup, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.1.1/windows95-2.1.1-setup-win32-x64.exe) <br /> 💽[Setup, 32-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.1.1/windows95-2.1.1-setup-win32-ia32.exe) | | 💽[deb, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.1.1/windows95-linux-2.1.1_amd64.deb) <br /> 💽[rpm, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.1.1/windows95-linux-2.1.1.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)
@@ -23,7 +108,7 @@ You'll likely be better off with an actual virtualization app, but the short ans
## Credits
99% 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
@@ -44,6 +129,8 @@ Unpack the `images` folder into the `src` folder, creating this layout:
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)

BIN
assets/boot.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Binary file not shown.

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>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -35,5 +35,5 @@ xhost +
```
4. run
```
docker run -it -e DISPLAY=host.docker.internal:1 toolboc/windows95
docker run -it -e DISPLAY=host.docker.internal:0 toolboc/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,9 +1,38 @@
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')
generateAssets: require('./tools/generateAssets'),
},
packagerConfig: {
asar: false,
@@ -12,22 +41,42 @@ module.exports = {
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: [
@@ -41,10 +90,11 @@ 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
}
}
},
@@ -60,18 +110,5 @@ module.exports = {
name: '@electron-forge/maker-rpm',
platforms: ['linux']
}
],
publishers: [
{
name: '@electron-forge/publisher-github',
config: {
repository: {
owner: 'felixrieseberg',
name: 'windows95'
},
draft: true,
prerelease: true
}
}
]
};

23684
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,19 @@
{
"name": "windows95",
"productName": "windows95",
"version": "2.2.0",
"version": "4.0.0",
"description": "Windows 95, in an app. I'm sorry.",
"main": "./dist/src/main/main",
"main": "./dist/src/main/main.js",
"scripts": {
"start": "rimraf ./dist && electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "prettier --write src/**/*.{ts,tsx}",
"less": "node ./tools/lessc.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",
@@ -18,43 +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": "^8.1.0",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"tslib": "^1.10.0",
"update-electron-app": "^1.5.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.44",
"@electron-forge/maker-deb": "^6.0.0-beta.44",
"@electron-forge/maker-flatpak": "^6.0.0-beta.44",
"@electron-forge/maker-rpm": "^6.0.0-beta.44",
"@electron-forge/maker-squirrel": "^6.0.0-beta.44",
"@electron-forge/maker-zip": "^6.0.0-beta.44",
"@electron-forge/publisher-github": "^6.0.0-beta.44",
"@types/fs-extra": "^8.0.0",
"@types/node": "^12.7.2",
"@types/react": "^16.9.2",
"@types/react-dom": "^16.8.5",
"electron": "6.0.2",
"less": "^3.10.1",
"node-abi": "^2.11.0",
"parcel-bundler": "^1.12.3",
"prettier": "^1.18.2",
"rimraf": "^3.0.0",
"standard": "^13.1.0",
"typescript": "^3.5.3"
"@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,8 +1,8 @@
import { session } from 'electron';
import { session } from "electron";
export async function clearCaches() {
await clearCache()
await clearStorageData()
await clearCache();
await clearStorageData();
}
export async function clearCache() {
@@ -11,15 +11,22 @@ export async function clearCache() {
}
}
export function clearStorageData() {
return new Promise((resolve) => {
if (!session.defaultSession) {
return resolve();
}
export async function clearStorageData() {
if (!session.defaultSession) {
return;
}
session.defaultSession.clearStorageData({
storages: [ 'appcache', 'cookies', 'filesystem', 'indexdb', 'localstorage', 'shadercache', 'websql', 'serviceworkers' ],
quotas: [ 'temporary', 'persistent', 'syncable' ]
}, resolve)
})
await session.defaultSession.clearStorageData({
storages: [
"appcache",
"cookies",
"filesystem",
"indexdb",
"localstorage",
"shadercache",
"websql",
"serviceworkers",
],
quotas: ["temporary", "persistent", "syncable"],
});
}

View File

@@ -1,31 +1,33 @@
import { remote, app } from 'electron';
import * as path from 'path';
import * as path from "path";
const _app = app || remote.app
const IMAGES_PATH = path.join(__dirname, "../../images");
export const CONSTANTS = {
IMAGE_PATH: path.join(__dirname, '../../images/windows95.img'),
IMAGES_PATH,
IMAGE_PATH: path.join(IMAGES_PATH, "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')
}
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',
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_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'
}
MACHINE_STARTED: "MACHINE_STARTED",
MACHINE_STOPPED: "MACHINE_STOPPED",
// Else
APP_QUIT: "APP_QUIT",
GET_STATE_PATH: "GET_STATE_PATH",
};

View File

@@ -21,4 +21,9 @@
filter: blur(2px);
z-index: -100;
}
#emulator-text-screen {
display: none;
visibility: hidden;
}
}

View File

@@ -100,4 +100,18 @@ section {
input[type=submit]:focus:before {
border-color: #dedede grey grey #dedede;
}
.card {
// Fix link colors
.link, .link:active, .link:link, .link:visited, a, a:active, a:link, a:visited {
color: #008080;
text-decoration: underline;
cursor: pointer;
}
// Ensure a-elements in fieldsets receive click events
fieldset:before {
pointer-events: none;
}
}
}

View File

@@ -12,7 +12,7 @@ export function setupAboutPanel(): void {
applicationName: "windows95",
applicationVersion: app.getVersion(),
version: process.versions.electron,
copyright: "Felix Rieseberg"
copyright: "Felix Rieseberg",
};
switch (process.platform) {

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

View File

@@ -6,6 +6,9 @@ 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
@@ -14,10 +17,13 @@ import { setupMenu } from "./menu";
export async function onReady() {
if (!isDevMode()) process.env.NODE_ENV = "production";
setupSession();
setupIpcListeners();
getOrCreateWindow();
setupAboutPanel();
setupMenu();
setupUpdates();
setupFileServer();
}
/**

View File

@@ -1,25 +1,25 @@
import { app, shell, Menu, BrowserWindow, ipcMain, webFrame } from "electron";
import { app, shell, Menu, BrowserWindow, ipcMain, dialog } from "electron";
import { clearCaches } from "../cache";
import { IPC_COMMANDS } from "../constants";
import { isDevMode } from "../utils/devmode";
import { getOrCreateWindow } from "./windows";
import { log } from "./logging";
const LINKS = {
homepage: "https://www.twitter.com/felixrieseberg",
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"
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 })
createMenu({ isRunning: true }),
);
ipcMain.on(IPC_COMMANDS.MACHINE_STOPPED, () =>
createMenu({ isRunning: false })
createMenu({ isRunning: false }),
);
}
@@ -27,10 +27,10 @@ function send(cmd: string) {
const windows = BrowserWindow.getAllWindows();
if (windows[0]) {
console.log(`Sending "${cmd}"`);
log(`Sending "${cmd}"`);
windows[0].webContents.send(cmd);
} else {
console.log(`Tried to send "${cmd}", but could not find window`);
log(`Tried to send "${cmd}", but could not find window`);
}
}
@@ -41,52 +41,52 @@ async function createMenu({ isRunning } = { isRunning: false }) {
submenu: [
{
label: "Toggle Full Screen",
accelerator: (function() {
accelerator: (function () {
if (process.platform === "darwin") {
return "Ctrl+Command+F";
} else {
return "F11";
}
})(),
click: function(_item, focusedWindow) {
click: function (_item, focusedWindow) {
if (focusedWindow) {
focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
}
}
},
},
{
label: "Toggle Developer Tools",
accelerator: (function() {
accelerator: (function () {
if (process.platform === "darwin") {
return "Alt+Command+I";
} else {
return "Ctrl+Shift+I";
}
})(),
click: function(_item, focusedWindow) {
click: function (_item, focusedWindow) {
if (focusedWindow) {
focusedWindow.webContents.toggleDevTools();
}
}
},
},
{
type: "separator"
type: "separator",
},
{
label: "Toggle Emulator Info",
click: () => send(IPC_COMMANDS.TOGGLE_INFO)
click: () => send(IPC_COMMANDS.TOGGLE_INFO),
},
{
type: "separator"
type: "separator",
},
{
role: "reload"
}
]
role: "reload",
},
],
},
{
role: "editMenu",
visible: isDevMode()
visible: isDevMode(),
},
{
label: "Window",
@@ -95,32 +95,32 @@ async function createMenu({ isRunning } = { isRunning: false }) {
{
label: "Minimize",
accelerator: "CmdOrCtrl+M",
role: "minimize"
role: "minimize",
},
{
label: "Close",
accelerator: "CmdOrCtrl+W",
role: "close"
role: "close",
},
{
type: "separator"
type: "separator",
},
{
label: "Zoom in",
click: () => send(IPC_COMMANDS.ZOOM_IN),
enabled: isRunning
enabled: isRunning,
},
{
label: "Zoom out",
click: () => send(IPC_COMMANDS.ZOOM_OUT),
enabled: isRunning
enabled: isRunning,
},
{
label: "Reset zoom",
click: () => send(IPC_COMMANDS.ZOOM_RESET),
enabled: isRunning
}
]
enabled: isRunning,
},
],
},
{
label: "Machine",
@@ -128,53 +128,66 @@ async function createMenu({ isRunning } = { isRunning: false }) {
{
label: "Send Ctrl+Alt+Del",
click: () => send(IPC_COMMANDS.MACHINE_CTRL_ALT_DEL),
enabled: isRunning
enabled: isRunning,
},
{
label: "Send Alt+F4",
click: () => send(IPC_COMMANDS.MACHINE_ALT_F4),
enabled: isRunning
enabled: isRunning,
},
{
label: "Send Alt+Enter",
click: () => send(IPC_COMMANDS.MACHINE_ALT_ENTER),
enabled: isRunning
enabled: isRunning,
},
{
label: "Send Esc",
click: () => send(IPC_COMMANDS.MACHINE_ESC),
enabled: isRunning
enabled: isRunning,
},
{
type: "separator"
type: "separator",
},
isRunning
? {
label: "Stop",
click: () => send(IPC_COMMANDS.MACHINE_STOP)
click: () => send(IPC_COMMANDS.MACHINE_STOP),
}
: {
label: "Start",
click: () => send(IPC_COMMANDS.MACHINE_START)
click: () => send(IPC_COMMANDS.MACHINE_START),
},
{
label: "Restart",
click: () => send(IPC_COMMANDS.MACHINE_RESTART),
enabled: isRunning
enabled: isRunning,
},
{
label: "Reset",
click: () => send(IPC_COMMANDS.MACHINE_RESET),
enabled: isRunning
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"
type: "separator",
},
{
label: "Go to Disk Image",
click: () => send(IPC_COMMANDS.SHOW_DISK_IMAGE)
}
]
click: () => send(IPC_COMMANDS.SHOW_DISK_IMAGE),
},
],
},
{
label: "Help",
@@ -182,18 +195,18 @@ async function createMenu({ isRunning } = { isRunning: false }) {
submenu: [
{
label: "Author",
click: () => shell.openExternal(LINKS.homepage)
click: () => shell.openExternal(LINKS.homepage),
},
{
label: "windows95 on GitHub",
click: () => shell.openExternal(LINKS.repo)
click: () => shell.openExternal(LINKS.repo),
},
{
label: "Help",
click: () => shell.openExternal(LINKS.help)
click: () => shell.openExternal(LINKS.help),
},
{
type: "separator"
type: "separator",
},
{
label: "Troubleshooting",
@@ -205,12 +218,12 @@ async function createMenu({ isRunning } = { isRunning: false }) {
app.relaunch();
app.quit();
}
}
]
}
]
}
},
},
],
},
],
},
];
if (process.platform === "darwin") {
@@ -218,41 +231,41 @@ async function createMenu({ isRunning } = { isRunning: false }) {
label: "windows95",
submenu: [
{
role: "about"
role: "about",
},
{
type: "separator"
type: "separator",
},
{
role: "services"
role: "services",
},
{
type: "separator"
type: "separator",
},
{
label: "Hide windows95",
accelerator: "Command+H",
role: "hide"
role: "hide",
},
{
label: "Hide Others",
accelerator: "Command+Shift+H",
role: "hideothers"
role: "hideothers",
},
{
role: "unhide"
role: "unhide",
},
{
type: "separator"
type: "separator",
},
{
label: "Quit",
accelerator: "Command+Q",
click() {
app.quit();
}
}
]
},
},
],
} 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();

View File

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

View File

@@ -1,4 +1,4 @@
import { BrowserWindow } from "electron";
import { BrowserWindow, shell } from "electron";
let mainWindow: BrowserWindow | null = null;
@@ -13,15 +13,28 @@ export function getOrCreateWindow(): BrowserWindow {
webPreferences: {
nodeIntegration: true,
sandbox: false,
webviewTag: 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,3 +1,12 @@
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.
@@ -27,8 +36,8 @@ export class App {
}
}
window["win95"] = window["win95"] || {
app: new App()
window.win95 = window.win95 || {
app: new App(),
};
window["win95"].app.setup();
window.win95.app.setup();

View File

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

View File

@@ -1,12 +1,13 @@
import * as React from "react";
import * as fs from "fs-extra";
import { CONSTANTS } from "../constants";
import { resetState } from "./utils/reset-state";
interface CardSettingsProps {
bootFromScratch: () => void;
setFloppy: (file: File) => void;
setCdrom: (cdrom: File) => void;
floppy?: File;
cdrom?: File;
}
interface CardSettingsState {
@@ -21,10 +22,11 @@ export class CardSettings extends React.Component<
super(props);
this.onChangeFloppy = this.onChangeFloppy.bind(this);
this.onChangeCdrom = this.onChangeCdrom.bind(this);
this.onResetState = this.onResetState.bind(this);
this.state = {
isStateReset: false
isStateReset: false,
};
}
@@ -39,6 +41,8 @@ export class CardSettings extends React.Component<
</h2>
</div>
<div className="card-body">
{this.renderCdrom()}
<hr />
{this.renderFloppy()}
<hr />
{this.renderState()}
@@ -48,6 +52,44 @@ export class CardSettings extends React.Component<
);
}
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;
@@ -79,7 +121,7 @@ export class CardSettings extends React.Component<
</p>
<p id="floppy-path">
{floppy
? `Inserted Floppy Disk: ${floppy.path}`
? `Inserted Floppy Disk: ${floppy.name}`
: `No floppy mounted`}
</p>
<button
@@ -148,14 +190,29 @@ export class CardSettings extends React.Component<
}
}
/**
* 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() {
if (fs.existsSync(CONSTANTS.STATE_PATH)) {
await fs.remove(CONSTANTS.STATE_PATH);
}
await resetState();
this.setState({ isStateReset: true });
}
}

View File

@@ -29,7 +29,7 @@ export class EmulatorInfo extends React.Component<
cpu: 0,
disk: "Idle",
lastCounter: 0,
lastTick: 0
lastTick: 0,
};
}
@@ -74,7 +74,7 @@ export class EmulatorInfo extends React.Component<
if (!emulator) {
console.log(
`Emulator info: Tried to install listeners, but emulator not defined yet.`
`Emulator info: Tried to install listeners, but emulator not defined yet.`,
);
return;
}
@@ -85,7 +85,7 @@ export class EmulatorInfo extends React.Component<
}
// TypeScript think's we're using a Node.js setInterval. We're not.
this.cpuInterval = (setInterval(this.cpuCount, 500) as unknown) as number;
this.cpuInterval = setInterval(this.cpuCount, 500) as unknown as number;
// Disk
emulator.add_listener("ide-read-start", this.onIDEReadStart);
@@ -104,7 +104,7 @@ export class EmulatorInfo extends React.Component<
if (!emulator) {
console.log(
`Emulator info: Tried to uninstall listeners, but emulator not defined yet.`
`Emulator info: Tried to uninstall listeners, but emulator not defined yet.`,
);
return;
}
@@ -160,7 +160,7 @@ export class EmulatorInfo extends React.Component<
this.setState({
lastTick: now,
lastCounter: instructionCounter,
cpu: Math.round(ips / deltaTime)
cpu: Math.round(ips / deltaTime),
});
}
}

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import * as fs from "fs-extra";
import * as fs from "fs";
import * as path from "path";
import { ipcRenderer, remote, shell } from "electron";
import { ipcRenderer, shell, webUtils } from "electron";
import { CONSTANTS, IPC_COMMANDS } from "../constants";
import { getDiskImageSize } from "../utils/disk-image-size";
@@ -9,13 +9,18 @@ import { CardStart } from "./card-start";
import { StartMenu } from "./start-menu";
import { CardSettings } from "./card-settings";
import { EmulatorInfo } from "./emulator-info";
import { CardDrive } from "./card-drive";
import { getStatePath } from "./utils/get-state-path";
import { Win95Window } from "./app";
import { resetState } from "./utils/reset-state";
declare let window: Win95Window;
export interface EmulatorState {
currentUiCard: string;
currentUiCard: "start" | "settings";
emulator?: any;
scale: number;
floppyFile?: File;
cdromFile?: File;
isBootingFresh: boolean;
isCursorCaptured: boolean;
isInfoDisplayed: boolean;
@@ -41,16 +46,14 @@ export class Emulator extends React.Component<{}, EmulatorState> {
isRunning: false,
currentUiCard: "start",
isInfoDisplayed: true,
scale: 1
// We can start pretty large
// If it's too large, it'll just grow until it hits borders
scale: 2,
};
this.setupInputListeners();
this.setupIpcListeners();
this.setupUnloadListeners();
if (document.location.hash.includes("AUTO_START")) {
this.startEmulator();
}
}
/**
@@ -58,7 +61,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
*/
public setupInputListeners() {
// ESC
document.onkeydown = evt => {
document.onkeydown = (evt) => {
const { isCursorCaptured } = this.state;
evt = evt || window.event;
@@ -95,11 +98,11 @@ export class Emulator extends React.Component<{}, EmulatorState> {
this.isQuitting = true;
setImmediate(() => {
remote.app.quit();
ipcRenderer.invoke(IPC_COMMANDS.APP_QUIT);
});
};
window.onbeforeunload = event => {
window.onbeforeunload = (event: Event) => {
if (this.isQuitting || this.isResetting) {
console.log(`Unload: Not preventing`);
return;
@@ -122,27 +125,27 @@ export class Emulator extends React.Component<{}, EmulatorState> {
this.sendKeys([
0x1d, // ctrl
0x38, // alt
0x53 // delete
0x53, // delete
]);
});
ipcRenderer.on(IPC_COMMANDS.MACHINE_ALT_F4, () => {
this.sendKeys([
0x38, // alt
0x3e // f4
0x3e, // f4
]);
});
ipcRenderer.on(IPC_COMMANDS.MACHINE_ALT_ENTER, () => {
this.sendKeys([
0x38, // alt
0 // enter
0, // enter
]);
});
ipcRenderer.on(IPC_COMMANDS.MACHINE_ESC, () => {
this.sendKeys([
0x18 // alt
0x18, // alt
]);
});
@@ -178,7 +181,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
* 🤡
*/
public renderUI() {
const { isRunning, currentUiCard, floppyFile } = this.state;
const { isRunning, currentUiCard, floppyFile, cdromFile } = this.state;
if (isRunning) {
return null;
@@ -189,13 +192,13 @@ export class Emulator extends React.Component<{}, EmulatorState> {
if (currentUiCard === "settings") {
card = (
<CardSettings
setFloppy={floppyFile => this.setState({ floppyFile })}
setFloppy={(floppyFile) => this.setState({ floppyFile })}
setCdrom={(cdromFile) => this.setState({ cdromFile })}
bootFromScratch={this.bootFromScratch}
floppy={floppyFile}
cdrom={cdromFile}
/>
);
} else if (currentUiCard === "drive") {
card = <CardDrive showDiskImage={this.showDiskImage} />;
} else {
card = <CardStart startEmulator={this.startEmulator} />;
}
@@ -204,7 +207,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
<>
{card}
<StartMenu
navigate={target => this.setState({ currentUiCard: target })}
navigate={(target) => this.setState({ currentUiCard: target as "start" | "settings" })}
/>
</>
);
@@ -219,8 +222,8 @@ export class Emulator extends React.Component<{}, EmulatorState> {
{this.renderInfo()}
{this.renderUI()}
<div id="emulator">
<div></div>
<canvas></canvas>
<div id="emulator-text-screen"></div>
<canvas id="emulator-canvas"></canvas>
</div>
</>
);
@@ -257,12 +260,9 @@ export class Emulator extends React.Component<{}, EmulatorState> {
*/
public showDiskImage() {
// Contents/Resources/app/dist/static
const imagePath = path
.join(__dirname, "../../images/windows95.img");
console.log(`Showing disk image in ${CONSTANTS.IMAGE_PATH}`);
console.log(`Showing disk image in ${imagePath}`);``
shell.showItemInFolder(imagePath);
shell.showItemInFolder(CONSTANTS.IMAGE_PATH);
}
/**
@@ -271,36 +271,57 @@ export class Emulator extends React.Component<{}, EmulatorState> {
private async startEmulator() {
document.body.classList.remove("paused");
const imageSize = await getDiskImageSize();
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,
video_memory_size: 32 * 1024 * 1024,
screen_container: document.getElementById("emulator"),
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: "../../bios/seabios.bin"
url: path.join(__dirname, "../../bios/seabios.bin"),
},
vga_bios: {
url: "../../bios/vgabios.bin"
url: path.join(__dirname, "../../bios/vgabios.bin"),
},
hda: {
url: "../../images/windows95.img",
url: CONSTANTS.IMAGE_PATH,
async: true,
size: imageSize
size: await getDiskImageSize(CONSTANTS.IMAGE_PATH),
},
fda: {
buffer: this.state.floppyFile
},
boot_order: 0x132
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 V86Starter(options);
window["emulator"] = new V86(options);
// New v86 instance
this.setState({
emulator: window["emulator"],
isRunning: true
isRunning: true,
});
ipcRenderer.send(IPC_COMMANDS.MACHINE_STARTED);
@@ -315,6 +336,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
this.lockMouse();
this.state.emulator.run();
this.state.emulator.screen_set_scale(this.state.scale);
}, 500);
}
@@ -344,19 +366,23 @@ export class Emulator extends React.Component<{}, EmulatorState> {
await this.saveState();
this.unlockMouse();
emulator.stop();
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 (lol)
* Reset the emulator by reloading the whole page
*/
private async resetEmulator() {
this.isResetting = true;
document.location.hash = `#AUTO_START`;
await this.stopEmulator();
await resetState();
document.location.reload();
}
@@ -366,50 +392,47 @@ export class Emulator extends React.Component<{}, EmulatorState> {
*/
private async saveState(): Promise<void> {
const { emulator } = this.state;
const statePath = await getStatePath();
return new Promise(resolve => {
if (!emulator || !emulator.save_state) {
console.log(`restoreState: No emulator present`);
return resolve();
}
if (!emulator || !emulator.save_state) {
console.log(`restoreState: No emulator present`);
return;
}
emulator.save_state(async (error: Error, newState: ArrayBuffer) => {
if (error) {
console.warn(`saveState: Could not save state`, error);
return resolve();
}
await fs.outputFile(CONSTANTS.STATE_PATH, Buffer.from(newState));
console.log(`saveState: Saved state to ${CONSTANTS.STATE_PATH}`);
resolve();
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 restoreState() {
const { emulator } = this.state;
const state = this.getState();
private async restoreState() {
const { emulator, isBootingFresh } = this.state;
const state = await this.getState();
// Nothing to do with if we don't have a state
if (!state) {
if (isBootingFresh) {
console.log(`restoreState: Booting fresh, not restoring.`);
return;
} else if (!state) {
console.log(`restoreState: No state present, not restoring.`);
}
if (!emulator) {
return;
} else if (!emulator) {
console.log(`restoreState: No emulator present`);
return;
}
try {
this.state.emulator.restore_state(state);
await this.state.emulator.restore_state(state);
} catch (error) {
console.log(
`State: Could not read state file. Maybe none exists?`,
error
`restoreState: Could not read state file. Maybe none exists?`,
error,
);
}
}
@@ -420,13 +443,16 @@ export class Emulator extends React.Component<{}, EmulatorState> {
*
* @returns {ArrayBuffer}
*/
private getState(): ArrayBuffer | null {
const statePath = fs.existsSync(CONSTANTS.STATE_PATH)
? CONSTANTS.STATE_PATH
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;
@@ -453,7 +479,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
emulator.lock_mouse();
} else {
console.warn(
`Emulator: Tried to lock mouse, but no emulator or not running`
`Emulator: Tried to lock mouse, but no emulator or not running`,
);
}
}
@@ -490,4 +516,16 @@ export class Emulator extends React.Component<{}, EmulatorState> {
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);
}
}
}

View File

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

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

@@ -28,10 +28,6 @@ export class StartMenu extends React.Component<StartMenuProps, {}> {
<img src="../../static/settings.png" />
<span>Settings</span>
</a>
<a onClick={this.navigate} href="#" id="drive" className="nav-link">
<img src="../../static/drive.png" />
<span>Modify C: Drive</span>
</a>
</div>
</nav>
);

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,4 +1,4 @@
import * as fs from "fs-extra";
import * as fs from "fs";
import { CONSTANTS } from "../constants";
@@ -7,9 +7,9 @@ import { CONSTANTS } from "../constants";
*
* @returns {number}
*/
export async function getDiskImageSize() {
export async function getDiskImageSize(path: string) {
try {
const stats = await fs.stat(CONSTANTS.IMAGE_PATH);
const stats = await fs.promises.stat(path);
if (stats) {
return stats.size;

BIN
static/cdrom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

10
static/entitlements.plist Normal file
View File

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

View File

@@ -7,7 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../src/less/vendor/95css.css">
<link rel="stylesheet" href="../src/less/root.less">
<script src="../src/renderer/lib/libv86.js"></script>
<!-- libv86 -->
</head>
<body class="paused windows95">
<div id="app"></div>

BIN
static/select-cdrom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

20
static/www/apps.htm Normal file
View File

@@ -0,0 +1,20 @@
<html>
<head>
<title>windows95 Help</title>
</head>
<body bgcolor="#C0C0C0">
<table width="100%" cellpadding="10" cellspacing="0">
<tr>
<td>
<font face="Arial" color="#000000">
<font size="5"><b>windows95 Apps & Games</b></font>
<hr>
<p>I've installed a few apps and games for you to try out. Check out the Games folder on the desktop!</p>
<p>If you want to try other games, I recommend trying to find them on the Internet Archive. On your host computer, visit https://archive.org, then find the "Classic PC Games" category. Once downloaded, you can import them into windows95 from <a href="http://my-computer">your host's Download folder</a>.</p>
</font>
</td>
</tr>
</table>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
static/www/buttons/msie.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

31
static/www/credits.htm Normal file
View File

@@ -0,0 +1,31 @@
<html>
<head>
<title>Windows 95 Credits</title>
</head>
<body bgcolor="#C0C0C0">
<table width="100%" cellpadding="10" cellspacing="0">
<tr>
<td>
<font face="Arial" color="#000000">
<font size="5"><b>windows95 Credits</b></font>
<hr>
<h3>Emulation Engine</h3>
<p>
None of this would be possible without the people working on v86, in particular Fabian Hemmer aka copy.
You can visit his website at <a href="http://copy.sh" target="_blank">copy.sh</a>. It also wouldn't be
possible without the QEMU project. If you enjoy running old systems, you probably want QEMU - windows95
is merely a toy, QEMU lets you actually run old systems in a stable manner.
</p>
<h3>Electron</h3>
<p>
Electron is a framework for building desktop applications using web technologies. It's what powers windows95.
You can visit the project's website at electronjs.org (in a modern browser, not in windows95).
</p>
</font>
</td>
</tr>
</table>
</body>
</html>

34
static/www/help.htm Normal file
View File

@@ -0,0 +1,34 @@
<html>
<head>
<title>windows95 Help</title>
</head>
<body bgcolor="#C0C0C0">
<table width="100%" cellpadding="10" cellspacing="0">
<tr>
<td>
<font face="Arial" color="#000000">
<font size="5"><b>windows95 Help</b></font>
<hr>
<h3>MS-DOS Display Issues</h3>
<p>If MS-DOS seems to mess up the screen:</p>
<ol>
<li>Hit <code>Alt + Enter</code> to make the command screen "Full Screen" (as far as Windows 95 is concerned)</li>
<li>This should restore the display from the garbled mess you see and allow you to access the Command Prompt</li>
<li>Press Alt-Enter again to leave Full Screen and go back to Window Mode</li>
</ol>
<p><i>(Thanks to @DisplacedGamers for that wisdom)</i></p>
<h3>windows95 Stuck in Bad State</h3>
<p>If windows95 becomes unresponsive or stuck:</p>
<ol>
<li>On the app's home screen, select "Settings" in the lower menu</li>
<li>Delete your machine's state before starting it again</li>
<li>This should resolve the issue when you restart</li>
</ol>
</font>
</td>
</tr>
</table>
</body>
</html>

37
static/www/home.htm Normal file
View File

@@ -0,0 +1,37 @@
<html>
<head>
<title>Welcome to Windows 95!</title>
</head>
<body bgcolor="#C0C0C0">
<table width="100%" cellpadding="10" cellspacing="0">
<tr>
<td>
<center>
<marquee scrollamount="3">
<font face="Arial" size="6" color="#000000">
<blink>Welcome to Windows 95!</blink>
</font>
</marquee>
</center>
<font face="Arial" color="#000000">
<p>Hi, I'm Felix, the maker behind windows95. I hope you're having fun!</p>
<p>Reach out to me in a modern browser (as in: not in windows95) on <font color="#0000FF">felixrieseberg.com</font> or find me on Bluesky at <font color="#0000FF">@felixrieseberg</font>.</p>
<hr width="75%">
<a name="internet"></a>
<font size="5" color="#000000"><img src="images/ie.gif" width="16" height="16" border="0" align="absmiddle"> <b>The Internet!</b></font>
<hr width="75%">
<p>In a major update since the last version, windows95 now has working Internet! That said, most modern websites will not work, so brace yourself. I recommend using <a href="http://theoldnet.com/" target="_blank">The Old Net</a> to travel back in time.</p>
</font>
<center>
<font size="2" color="#000000">Last updated: 2025</font>
</center>
</td>
</tr>
</table>
</body>
</html>

BIN
static/www/images/bg.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

BIN
static/www/images/doc.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
static/www/images/help.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

BIN
static/www/images/ie.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

21
static/www/index.htm Normal file
View File

@@ -0,0 +1,21 @@
<html>
<head>
<title>Welcome to Windows 95!</title>
</head>
<frameset cols="200,*" border="0" framespacing="0" frameborder="NO">
<frame src="navigation.htm" name="nav" scrolling="auto" noresize>
<frame src="home.htm" name="main" scrolling="auto" noresize>
<noframes>
<body bgcolor="#000080">
<font face="Arial" color="#FFFFFF">
<h2>Frame Alert!</h2>
<p>This page uses frames, but your browser doesn't support them.</p>
<p>Please upgrade to Netscape Navigator 2.0 or Internet Explorer 3.0!</p>
</font>
</body>
</noframes>
</frameset>
</html>

46
static/www/navigation.htm Normal file
View File

@@ -0,0 +1,46 @@
<html>
<head>
<title>Navigation</title>
</head>
<body bgcolor="#C0C0C0" background="images/bg.gif">
<table width="100%" cellpadding="4" cellspacing="1" bgcolor="#000000">
<tr><td bgcolor="#C0C0C0">
<font face="Arial" size="2" color="#000000">
<img src="images/folder.gif" width="16" height="16" border="0" align="absmiddle"> <b>Navigation</b>
</font>
</td></tr>
<tr><td bgcolor="#C0C0C0">
<font face="Arial" size="2" color="#000000">
<img src="images/desktop.gif" width="16" height="16" border="0" align="absmiddle"> <a href="home.htm" target="main">Home</a>
</font>
</td></tr>
<tr><td bgcolor="#C0C0C0">
<font face="Arial" size="2" color="#000000">
<img src="images/programs.gif" width="16" height="16" border="0" align="absmiddle"> <a href="apps.htm" target="main">Apps & Games</a>
</font>
</td></tr>
<tr><td bgcolor="#C0C0C0">
<font face="Arial" size="2" color="#000000">
<img src="images/help.gif" width="16" height="16" border="0" align="absmiddle"> <a href="help.htm" target="main">Help</a>
</font>
</td></tr>
<tr><td bgcolor="#C0C0C0">
<font face="Arial" size="2" color="#000000">
<img src="images/doc.gif" width="16" height="16" border="0" align="absmiddle"> <a href="credits.htm" target="main">Credits</a>
</font>
</td></tr>
</table>
<br>
<center>
<p>
<font face="Arial" size="1" color="#000000">
Best viewed with<br>
Internet Explorer 5.5 and windows95
</font>
</p>
<img src="buttons/madewithelectron.gif" width="88" height="31" border="0" align="absmiddle">
<img src="buttons/macos.gif" width="88" height="31" border="0" align="absmiddle">
<img src="buttons/msie.gif" width="88" height="31" border="0" align="absmiddle">
</center>
</body>
</html>

23
tools/add-macos-cert.sh Normal file
View File

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

38
tools/check-links.js Normal file
View File

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

11
tools/download-disk.ps1 Normal file
View File

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

10
tools/download-disk.sh Normal file
View File

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

View File

@@ -2,6 +2,31 @@
const Bundler = require('parcel-bundler')
const path = require('path')
const fs = require('fs')
async function copyLib() {
const target = path.join(__dirname, '../dist/static')
const lib = path.join(__dirname, '../src/renderer/lib')
const index = path.join(target, 'index.html')
// Copy in lib
await fs.promises.cp(lib, target, { recursive: true });
// Patch so that fs.read is used
const libv86path = path.join(target, 'libv86.js')
const libv86 = fs.readFileSync(libv86path, 'utf-8')
let patchedLibv86 = libv86.replace('k.load_file="undefined"===typeof XMLHttpRequest?pa:qa', 'k.load_file=pa')
patchedLibv86 = patchedLibv86.replace('H.exportSymbol=function(a,b){"undefined"!==typeof module&&"undefined"!==typeof module.exports?module.exports[a]=b:"undefined"!==typeof window?window[a]=b:"function"===typeof importScripts&&(self[a]=b)}', 'H.exportSymbol=function(a,b){"undefined"!==typeof window?window[a]=b:"undefined"!==typeof module&&"undefined"!==typeof module.exports?module.exports[a]=b:"function"===typeof importScripts&&(self[a]=b)}')
patchedLibv86 = patchedLibv86.replace('this.fetch=fetch;', 'this.fetch=(...args)=>fetch(...args);')
fs.writeFileSync(libv86path, patchedLibv86)
// Overwrite
const indexContents = fs.readFileSync(index, 'utf-8');
const replacedContents = indexContents.replace('<!-- libv86 -->', '<script src="libv86.js"></script>')
fs.writeFileSync(index, replacedContents)
}
async function compileParcel (options = {}) {
const entryFiles = [
@@ -27,7 +52,7 @@ async function compileParcel (options = {}) {
logLevel: 3, // 3 = log everything, 2 = log warnings & errors, 1 = log errors
hmr: false, // Enable or disable HMR while watching
hmrPort: 0, // The port the HMR socket runs on, defaults to a random free port (0 in node.js resolves to a random free port)
sourceMaps: true, // Enable or disable sourcemaps, defaults to enabled (minified builds currently always create sourcemaps)
sourceMaps: false, // Enable or disable sourcemaps, defaults to enabled (minified builds currently always create sourcemaps)
hmrHostname: '', // A hostname for hot module reload, default to ''
detailedReport: false, // Prints a detailed report of the bundles, assets, filesizes and times, defaults to false, reports are only printed if watch is disabled,
...options
@@ -38,6 +63,8 @@ async function compileParcel (options = {}) {
// Run the bundler, this returns the main bundle
// Use the events if you're using watch mode as this promise will only trigger once and not for every rebuild
await bundler.bundle()
await copyLib();
}
module.exports = {

27
tools/resedit.js Normal file
View File

@@ -0,0 +1,27 @@
const path = require('path');
const resedit = require('../node_modules/@electron/packager/dist/resedit.js')
const package = require('../package.json');
const exePath = process.argv[process.argv.length - 1]
console.log(exePath)
async function main() {
await resedit.resedit(exePath, {
"productVersion": package.version,
"fileVersion": package.version,
"productName": package.productName,
"icon": path.join(__dirname, "../assets/icon.ico"),
"win32Metadata": {
"FileDescription": package.productName,
"InternalName": package.name,
"OriginalFilename": `${package.name}.exe`,
"ProductName": package.productName,
"CompanyName": package.author
}
});
}
main();

View File

@@ -8,22 +8,20 @@
"preserveConstEnums": true,
"sourceMap": true,
"lib": [
"es2017",
"es2023",
"dom"
],
"noImplicitAny": true,
"noImplicitReturns": true,
"suppressImplicitAnyIndexErrors": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noImplicitThis": true,
"noUnusedParameters": true,
"importHelpers": true,
"noEmitHelpers": false,
"module": "commonjs",
"moduleResolution": "node",
"pretty": true,
"target": "es2017",
"target": "es2023",
"jsx": "react",
"typeRoots": [
"./node_modules/@types"