The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .gitattributes
├── .github
    ├── images
    │   ├── linux.png
    │   ├── macos.png
    │   └── windows.png
    └── workflows
    │   └── build.yml
├── .gitignore
├── CREDITS.md
├── Dockerfile
├── HELP.md
├── LICENSE.md
├── README.md
├── assets
    ├── boot.gif
    ├── entitlements.plist
    ├── icon.icns
    └── icon.ico
├── bios
    ├── COPYING.LESSER
    ├── seabios.bin
    └── vgabios.bin
├── docs
    ├── docker-instructions.md
    ├── docker-kubernetes-gitpod.md
    └── qemu.md
├── forge.config.js
├── issue_template.md
├── package-lock.json
├── package.json
├── patches
    └── @electron+packager+18.3.6.patch
├── src
    ├── cache.ts
    ├── constants.ts
    ├── less
    │   ├── emulator.less
    │   ├── info.less
    │   ├── root.less
    │   ├── settings.less
    │   ├── start.less
    │   ├── status.less
    │   └── vendor
    │   │   ├── 95.ttf
    │   │   ├── 95css.css
    │   │   ├── LICENSE
    │   │   ├── bg-pattern.png
    │   │   ├── dropdown.png
    │   │   ├── windows.woff
    │   │   └── windows.woff2
    ├── main
    │   ├── about-panel.ts
    │   ├── fileserver
    │   │   ├── encoding.ts
    │   │   ├── fileserver.ts
    │   │   ├── hide-files.ts
    │   │   ├── page-directory-listing.ts
    │   │   └── page-error.ts
    │   ├── ipc.ts
    │   ├── logging.ts
    │   ├── main.ts
    │   ├── menu.ts
    │   ├── session.ts
    │   ├── settings.ts
    │   ├── squirrel.ts
    │   ├── update.ts
    │   └── windows.ts
    ├── renderer
    │   ├── app.tsx
    │   ├── card-settings.tsx
    │   ├── card-start.tsx
    │   ├── emulator-info.tsx
    │   ├── emulator.tsx
    │   ├── global.d.ts
    │   ├── lib
    │   │   ├── LICENSE.md
    │   │   ├── build
    │   │   │   └── v86.wasm
    │   │   └── libv86.js
    │   ├── start-menu.tsx
    │   ├── status.tsx
    │   └── utils
    │   │   ├── get-state-path.ts
    │   │   └── reset-state.ts
    └── utils
    │   ├── devmode.ts
    │   └── disk-image-size.ts
├── static
    ├── boot-fresh.png
    ├── cdrom.png
    ├── entitlements.plist
    ├── floppy.png
    ├── index.html
    ├── reset-state.png
    ├── reset.png
    ├── run.png
    ├── select-cdrom.png
    ├── select-floppy.png
    ├── settings.png
    ├── show-disk-image.png
    ├── start.png
    └── www
    │   ├── apps.htm
    │   ├── buttons
    │       ├── macos.gif
    │       ├── madewithelectron.gif
    │       └── msie.gif
    │   ├── credits.htm
    │   ├── help.htm
    │   ├── home.htm
    │   ├── images
    │       ├── bg.gif
    │       ├── desktop.gif
    │       ├── doc.gif
    │       ├── folder.gif
    │       ├── help.gif
    │       ├── ie.gif
    │       ├── network.gif
    │       └── programs.gif
    │   ├── index.htm
    │   └── navigation.htm
├── tools
    ├── add-macos-cert.sh
    ├── check-links.js
    ├── download-disk.ps1
    ├── download-disk.sh
    ├── generateAssets.js
    ├── parcel-build.js
    ├── parcel-watch.js
    ├── resedit.js
    ├── run-bin.js
    └── tsc.js
└── tsconfig.json


/.gitattributes:
--------------------------------------------------------------------------------
1 | text eol=lf
2 | 


--------------------------------------------------------------------------------
/.github/images/linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/.github/images/linux.png


--------------------------------------------------------------------------------
/.github/images/macos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/.github/images/macos.png


--------------------------------------------------------------------------------
/.github/images/windows.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/.github/images/windows.png


--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
  1 | name: Build & Release
  2 | 
  3 | on:
  4 |   push:
  5 |     branches:
  6 |       - master
  7 |     tags:
  8 |       - v*
  9 |   pull_request:
 10 | 
 11 | jobs:
 12 |   lint:
 13 |     runs-on: ubuntu-latest
 14 |     steps:
 15 |       - uses: actions/checkout@v2
 16 |       - name: Setup Node.js
 17 |         uses: actions/setup-node@v1
 18 |         with:
 19 |           node-version: 18.x
 20 |       - name: Get yarn cache directory path
 21 |         id: yarn-cache-dir-path
 22 |         run: echo "::set-output name=dir::$(yarn cache dir)"
 23 |       - uses: actions/cache@v1
 24 |         id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
 25 |         with:
 26 |           path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
 27 |           key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
 28 |           restore-keys: |
 29 |             ${{ runner.os }}-yarn-
 30 |       - name: Install
 31 |         run: yarn --frozen-lockfile
 32 |       - name: lint
 33 |         run: yarn lint
 34 |   build:
 35 |     needs: lint
 36 |     name: Build (${{ matrix.os }} - ${{ matrix.arch }})
 37 |     runs-on: ${{ matrix.os }}
 38 |     strategy:
 39 |       matrix:
 40 |         # Build for supported platforms
 41 |         # https://github.com/electron/electron-packager/blob/ebcbd439ff3e0f6f92fa880ff28a8670a9bcf2ab/src/targets.js#L9
 42 |         # 32-bit Linux unsupported as of 2019: https://www.electronjs.org/blog/linux-32bit-support
 43 |         os: [ macOS-latest, ubuntu-latest, windows-latest ]
 44 |         arch: [ x64, arm64 ]
 45 |         include:
 46 |         - os: windows-latest
 47 |           arch: ia32
 48 |         - os: ubuntu-latest
 49 |           arch: armv7l
 50 |         # Publishing artifacts for multiple Windows architectures has
 51 |         # a bug which can cause the wrong architecture to be downloaded
 52 |         # for an update, so until that is fixed, only build Windows x64
 53 |         exclude:
 54 |         - os: windows-latest
 55 |           arch: arm64
 56 | 
 57 |     steps:
 58 |       - uses: actions/checkout@v2
 59 |       - name: Setup Node.js
 60 |         uses: actions/setup-node@v1
 61 |         with:
 62 |           node-version: 18.x
 63 |       - name: Get yarn cache directory path
 64 |         id: yarn-cache-dir-path
 65 |         run: echo "::set-output name=dir::$(yarn cache dir)"
 66 |       - uses: actions/cache@v1
 67 |         if: matrix.os != 'macOS-latest'
 68 |         id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
 69 |         with:
 70 |           path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
 71 |           key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
 72 |           restore-keys: |
 73 |             ${{ runner.os }}-yarn-
 74 |       - name: Set MacOS signing certs
 75 |         if: matrix.os == 'macOS-latest'
 76 |         run: chmod +x tools/add-macos-cert.sh && ./tools/add-macos-cert.sh
 77 |         env:
 78 |           MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
 79 |           MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
 80 |       - name: Set Windows signing certificate
 81 |         if: matrix.os == 'windows-latest'
 82 |         continue-on-error: true
 83 |         id: write_file
 84 |         uses: timheuer/base64-to-file@v1
 85 |         with:
 86 |           fileName: 'win-certificate.pfx'
 87 |           encodedString: ${{ secrets.WINDOWS_CODESIGN_P12 }}
 88 |       - name: Download disk image (ps1)
 89 |         run: tools/download-disk.ps1
 90 |         if: matrix.os == 'windows-latest' && startsWith(github.ref, 'refs/tags/')
 91 |         env:
 92 |           DISK_URL: ${{ secrets.DISK_URL }}
 93 |       - name: Download disk image (sh)
 94 |         run: chmod +x tools/download-disk.sh && ./tools/download-disk.sh
 95 |         if: matrix.os != 'windows-latest' && startsWith(github.ref, 'refs/tags/')
 96 |         env:
 97 |           DISK_URL: ${{ secrets.DISK_URL }}
 98 |       - name: Install
 99 |         run: yarn
100 |       - name: Make
101 |         if: startsWith(github.ref, 'refs/tags/')
102 |         run: yarn make --arch=${{ matrix.arch }}
103 |         env:
104 |           APPLE_ID: ${{ secrets.APPLE_ID }}
105 |           APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
106 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
107 |           WINDOWS_CODESIGN_FILE: ${{ steps.write_file.outputs.filePath }}
108 |           WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
109 |       - name: Release
110 |         uses: softprops/action-gh-release@v1
111 |         if: startsWith(github.ref, 'refs/tags/')
112 |         env:
113 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
114 |         with:
115 |           draft: true
116 |           files: |
117 |             out/**/*.deb
118 |             out/**/*.dmg
119 |             out/**/*setup*.exe
120 |             out/**/*.rpm
121 |             out/**/*.zip


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | node_modules
 2 | out
 3 | .DS_Store
 4 | 
 5 | /images*/
 6 | /helper-images/
 7 | 
 8 | dist
 9 | !.github/images
10 | *.code-workspace
11 | *.pfx
12 | 
13 | Microsoft.Trusted.Signing.Client*
14 | trusted-signing-metadata.json
15 | .env
16 | electron-windows-sign.log
17 | 


--------------------------------------------------------------------------------
/CREDITS.md:
--------------------------------------------------------------------------------
 1 | # windows95 Credits
 2 | 
 3 | This app was made possible by three major engineering efforts:
 4 | 
 5 |  * [v86 by Fabian Hemmer](https://github.com/copy/v86)
 6 |  * [Electron by the Electron Maintainers](https://electronjs.org)
 7 |  * Windows 95 by Microsoft
 8 | 
 9 | # v86 License and Copyright Notice
10 | 
11 | Copyright (c) 2012-2018, Fabian Hemmer
12 | All rights reserved.
13 | 
14 | Redistribution and use in source and binary forms, with or without
15 | modification, are permitted provided that the following conditions are met:
16 | 
17 | 1. Redistributions of source code must retain the above copyright notice, this
18 |    list of conditions and the following disclaimer.
19 | 2. Redistributions in binary form must reproduce the above copyright notice,
20 |    this list of conditions and the following disclaimer in the documentation
21 |    and/or other materials provided with the distribution.
22 | 
23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
24 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
27 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
28 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
30 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
31 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
32 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 | 
34 | The views and conclusions contained in the software and documentation are those
35 | of the authors and should not be interpreted as representing official policies,
36 | either expressed or implied, of the FreeBSD Project.
37 | 
38 | # Electron License and Copyright Notice
39 | 
40 | Copyright (c) 2013-2018 GitHub Inc.
41 | 
42 | Permission is hereby granted, free of charge, to any person obtaining
43 | a copy of this software and associated documentation files (the
44 | "Software"), to deal in the Software without restriction, including
45 | without limitation the rights to use, copy, modify, merge, publish,
46 | distribute, sublicense, and/or sell copies of the Software, and to
47 | permit persons to whom the Software is furnished to do so, subject to
48 | the following conditions:
49 | 
50 | The above copyright notice and this permission notice shall be
51 | included in all copies or substantial portions of the Software.
52 | 
53 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
54 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
55 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
56 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
57 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
58 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
59 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
60 | 


--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
 1 | # DESCRIPTION:	  Run Windows 95 in a container
 2 | # AUTHOR:		  Paul DeCarlo <toolboc@gmail.com>
 3 | #
 4 | #   Made possible through prior art by:
 5 | #   copy (v86 - x86 virtualization in JavaScript) 
 6 | #   felixrieseberg (Windows95 running in electron) 
 7 | #   Microsoft (Windows 95)
 8 | #
 9 | #   ***Docker Run Command***
10 | #
11 | #   docker run -it \
12 | #    -v /tmp/.X11-unix:/tmp/.X11-unix \ # mount the X11 socket
13 | #    -e DISPLAY=unix$DISPLAY \ # pass the display
14 | #    --device /dev/snd \ # sound
15 | #    --name windows95 \
16 | #    toolboc/windows95
17 | #
18 | #   ***TroubleShooting***
19 | #   If you receive Gtk-WARNING **: cannot open display: unix:0
20 | #   Run:
21 | #       xhost +
22 | #
23 | 
24 | FROM node:10.9-stretch
25 | 
26 | LABEL maintainer "Paul DeCarlo <toolboc@gmail.com>"
27 | 
28 | RUN apt update && apt install -y \
29 |     libgtk-3-0 \
30 |     libcanberra-gtk3-module \
31 |     libx11-xcb-dev \
32 |     libgconf2-dev \
33 |     libnss3 \
34 |     libasound2 \
35 |     libxtst-dev \
36 |     libxss1 \
37 |     git \
38 |     --no-install-recommends && \
39 |     rm -rf /var/lib/apt/lists/*
40 | 
41 | COPY . .
42 | 
43 | RUN npm install 
44 | 
45 | ENTRYPOINT [ "npm", "start"]
46 | 


--------------------------------------------------------------------------------
/HELP.md:
--------------------------------------------------------------------------------
 1 | # Help & Commonly Asked Questions
 2 | 
 3 | ## MS-DOS seems to mess up the screen
 4 | Hit `Alt + Enter` to make the command screen "Full Screen" (as far as Windows 95 is
 5 | concerned). This should restore the display from the garbled mess you see and allow
 6 | you to access the Command Prompt. Press Alt-Enter again to leave Full Screen and go
 7 | back to Window Mode. (Thanks to @DisplacedGamers for that wisdom)
 8 | 
 9 | ## Windows 95 is stuck in a bad state
10 | 
11 | On the app's home screen, select "Settings" in the lower menu. Then, delete your
12 | machine's state before starting it again - this time hopefully without issues.
13 | 
14 | 
15 | 


--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
 1 | Copyright 2019 Felix Rieseberg
 2 | 
 3 | 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:
 4 | 
 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 6 | 
 7 | 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.
 8 | 
 9 | ____
10 | 
11 | v86 Source Code
12 | 
13 | Copyright (c) 2012-2018, Fabian Hemmer
14 | All rights reserved.
15 | 
16 | Redistribution and use in source and binary forms, with or without
17 | modification, are permitted provided that the following conditions are met:
18 | 
19 | 1. Redistributions of source code must retain the above copyright notice, this
20 |    list of conditions and the following disclaimer.
21 | 2. Redistributions in binary form must reproduce the above copyright notice,
22 |    this list of conditions and the following disclaimer in the documentation
23 |    and/or other materials provided with the distribution.
24 | 
25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
26 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
27 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
28 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
29 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
30 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
32 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
33 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
34 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35 | 
36 | The views and conclusions contained in the software and documentation are those
37 | of the authors and should not be interpreted as representing official policies,
38 | either expressed or implied, of the FreeBSD Project.
39 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # windows95
  2 | 
  3 | This is Windows 95, running in an [Electron](https://electronjs.org/) app. Yes, it's the full thing. I'm sorry.
  4 | 
  5 | ## Downloads
  6 | 
  7 | <table class="is-fullwidth">
  8 | </thead>
  9 | <tbody>
 10 | </tbody>
 11 |   <tr>
 12 |     <td>
 13 |       <img src="./.github/images/windows.png" width="24"><br />
 14 |       Windows
 15 |     </td>
 16 |     <td>
 17 |       <span>32-bit</span>
 18 |       <a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-4.0.0-setup-ia32.exe">
 19 |         💿 Installer
 20 |       </a> |
 21 |       <a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-win32-ia32-4.0.0.zip">
 22 |         📦 Standalone Zip
 23 |       </a>
 24 |       <br />
 25 |       <span>64-bit</span>
 26 |       <a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-4.0.0-setup-x64.exe">
 27 |         💿 Installer
 28 |       </a> |
 29 |       <a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-win32-x64-4.0.0.zip">
 30 |         📦 Standalone Zip
 31 |       </a><br />
 32 |       <span>ARM64</span>
 33 |       <a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-4.0.0-setup-arm64.exe">
 34 |         💿 Installer
 35 |       </a> |
 36 |       <a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-win32-arm64-4.0.0.zip">
 37 |         📦 Standalone Zip
 38 |       </a><br />
 39 |       <span>
 40 |         ❓ Don't know what kind of chip you have? It's probably `x64`. To confirm, on your computer, hit Start, enter "processor" for info.
 41 |       </span>
 42 |     </td>
 43 |   </tr>
 44 |   <tr>
 45 |     <td>
 46 |       <img src="./.github/images/macos.png" width="24"><br />
 47 |       macOS
 48 |     </td>
 49 |     <td>
 50 |       <span>Apple Silicon Processor</span>
 51 |       <a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-darwin-arm64-4.0.0.zip">
 52 |         📦 Standalone Zip
 53 |       </a><br />
 54 |       <span>Intel Processor</span>
 55 |       <a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-darwin-x64-4.0.0.zip">
 56 |         📦 Standalone Zip
 57 |       </a>
 58 |       <span>
 59 |         ❓ Don't know what kind of chip you have? If you bought your computer after 2020, select "Apple Silicon". Learn more at <a href="https://support.apple.com/en-us/HT211814">apple.com</a>.
 60 |       </span>
 61 |     </td>
 62 |   </tr>
 63 |   <tr>
 64 |     <td>
 65 |       <img src="./.github/images/linux.png" width="24"><br />
 66 |       Linux
 67 |     </td>
 68 |     <td>
 69 |       <span>64-bit</span>
 70 |       <a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95-4.0.0-1.x86_64.rpm">
 71 |         💿 rpm
 72 |       </a> |
 73 |       <a href="https://github.com/felixrieseberg/windows95/releases/download/v4.0.0/windows95_4.0.0_amd64.deb">
 74 |         💿 deb
 75 |       </a><br />
 76 |     </td>
 77 |   </tr>
 78 | </table>
 79 | 
 80 | <hr />
 81 | 
 82 | <table width="100%">
 83 |   <tr>
 84 |     <td width="50%">
 85 |       <img src="https://github.com/user-attachments/assets/43ab7126-765e-444b-ad14-27b1beadbc7c" width="100%" alt="Screenshot showing Windows 95">
 86 |     </td>
 87 |     <td width="50%">
 88 |       <img src="https://github.com/user-attachments/assets/7ac5dc36-cbd4-4455-a616-0e5cca314b34" width="100%" alt="Screenshot showing Windows 95">
 89 |     </td>
 90 |   </tr>
 91 | </table>
 92 | 
 93 | ## Does it work?
 94 | Yes! Quite well, actually - on macOS, Windows, and Linux. Bear in mind that this is written entirely in JavaScript, so please adjust your expectations.
 95 | 
 96 | ## Should this have been a native app?
 97 | Absolutely.
 98 | 
 99 | ## Does it run Doom (or my other favorite game)?
100 | You'll likely be better off with an actual virtualization app, but the short answer is yes. In fact, a few games are already preinstalled - and more can be found on the Internet, for instance at [archive.org](https://www.archive.org). [Thanks to
101 | @DisplacedGamers](https://youtu.be/xDXqmdFxofM) I can recommend that you switch to a resolution of
102 | 640x480 @ 256 colors before starting DOS games - just like in the good ol' days.
103 | 
104 | ## Credits
105 | 
106 | 99% of the work was done over at [v86](https://github.com/copy/v86/) by Copy aka Fabian Hemmer and his contributors.
107 | 
108 | ## Contributing
109 | 
110 | Before you can run this from source, you'll need the disk image. It's not part of the
111 | repository, but you can grab it using the `Show Disk Image` button from the packaged
112 | release, which does include the disk image. You can find that button in the
113 | `Modify C: Drive` section.
114 | 
115 | Unpack the `images` folder into the `src` folder, creating this layout:
116 | 
117 | ```
118 | - /images/windows95.img
119 | - /images/default-state.bin
120 | - /assets/...
121 | - /bios/...
122 | - /docs/...
123 | ```
124 | 
125 | Once you've done so, run `npm install` and `npm start` to run your local build.
126 | 
127 | If you want to tinker with the image or make a new one, check out the [QEMU docs](./docs/qemu.md).
128 | 
129 | ## Other Questions
130 | 
131 |  * [MS-DOS seems to brick the screen](./HELP.md#ms-dos-seems-to-brick-the-screen)
132 |  * [Windows 95 is stuck in a bad state](./HELP.md#windows-95-is-stuck-in-a-bad-state)
133 |  * [I want to install additional apps or games](./HELP.md#i-want-to-install-additional-apps-or-games)
134 |  * [Running in Docker](./docs/docker-instructions.md)
135 |  * [Running in an online VM with Kubernetes and Gitpod](./docs/docker-kubernetes-gitpod.md)
136 | 
137 | ## License
138 | 
139 | This project is provided for educational purposes only. It is not affiliated with and has
140 | not been approved by Microsoft.
141 | 


--------------------------------------------------------------------------------
/assets/boot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/assets/boot.gif


--------------------------------------------------------------------------------
/assets/entitlements.plist:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 3 | <plist version="1.0">
 4 |   <dict>
 5 |     <key>com.apple.security.cs.allow-jit</key>
 6 |     <true/>
 7 |     <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
 8 |     <true/>
 9 |     <key>com.apple.security.cs.disable-library-validation</key>
10 |     <true/>
11 |     <key>com.apple.security.cs.disable-executable-page-protection</key>
12 |     <true/>
13 |     <key>com.apple.security.automation.apple-events</key>
14 |     <true/>
15 |   </dict>
16 | </plist>
17 | 


--------------------------------------------------------------------------------
/assets/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/assets/icon.icns


--------------------------------------------------------------------------------
/assets/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/assets/icon.ico


--------------------------------------------------------------------------------
/bios/COPYING.LESSER:
--------------------------------------------------------------------------------
  1 | 		   GNU LESSER GENERAL PUBLIC LICENSE
  2 |                        Version 3, 29 June 2007
  3 | 
  4 |  Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
  5 |  Everyone is permitted to copy and distribute verbatim copies
  6 |  of this license document, but changing it is not allowed.
  7 | 
  8 | 
  9 |   This version of the GNU Lesser General Public License incorporates
 10 | the terms and conditions of version 3 of the GNU General Public
 11 | License, supplemented by the additional permissions listed below.
 12 | 
 13 |   0. Additional Definitions. 
 14 | 
 15 |   As used herein, "this License" refers to version 3 of the GNU Lesser
 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
 17 | General Public License.
 18 | 
 19 |   "The Library" refers to a covered work governed by this License,
 20 | other than an Application or a Combined Work as defined below.
 21 | 
 22 |   An "Application" is any work that makes use of an interface provided
 23 | by the Library, but which is not otherwise based on the Library.
 24 | Defining a subclass of a class defined by the Library is deemed a mode
 25 | of using an interface provided by the Library.
 26 | 
 27 |   A "Combined Work" is a work produced by combining or linking an
 28 | Application with the Library.  The particular version of the Library
 29 | with which the Combined Work was made is also called the "Linked
 30 | Version".
 31 | 
 32 |   The "Minimal Corresponding Source" for a Combined Work means the
 33 | Corresponding Source for the Combined Work, excluding any source code
 34 | for portions of the Combined Work that, considered in isolation, are
 35 | based on the Application, and not on the Linked Version.
 36 | 
 37 |   The "Corresponding Application Code" for a Combined Work means the
 38 | object code and/or source code for the Application, including any data
 39 | and utility programs needed for reproducing the Combined Work from the
 40 | Application, but excluding the System Libraries of the Combined Work.
 41 | 
 42 |   1. Exception to Section 3 of the GNU GPL.
 43 | 
 44 |   You may convey a covered work under sections 3 and 4 of this License
 45 | without being bound by section 3 of the GNU GPL.
 46 | 
 47 |   2. Conveying Modified Versions.
 48 | 
 49 |   If you modify a copy of the Library, and, in your modifications, a
 50 | facility refers to a function or data to be supplied by an Application
 51 | that uses the facility (other than as an argument passed when the
 52 | facility is invoked), then you may convey a copy of the modified
 53 | version:
 54 | 
 55 |    a) under this License, provided that you make a good faith effort to
 56 |    ensure that, in the event an Application does not supply the
 57 |    function or data, the facility still operates, and performs
 58 |    whatever part of its purpose remains meaningful, or
 59 | 
 60 |    b) under the GNU GPL, with none of the additional permissions of
 61 |    this License applicable to that copy.
 62 | 
 63 |   3. Object Code Incorporating Material from Library Header Files.
 64 | 
 65 |   The object code form of an Application may incorporate material from
 66 | a header file that is part of the Library.  You may convey such object
 67 | code under terms of your choice, provided that, if the incorporated
 68 | material is not limited to numerical parameters, data structure
 69 | layouts and accessors, or small macros, inline functions and templates
 70 | (ten or fewer lines in length), you do both of the following:
 71 | 
 72 |    a) Give prominent notice with each copy of the object code that the
 73 |    Library is used in it and that the Library and its use are
 74 |    covered by this License.
 75 | 
 76 |    b) Accompany the object code with a copy of the GNU GPL and this license
 77 |    document.
 78 | 
 79 |   4. Combined Works.
 80 | 
 81 |   You may convey a Combined Work under terms of your choice that,
 82 | taken together, effectively do not restrict modification of the
 83 | portions of the Library contained in the Combined Work and reverse
 84 | engineering for debugging such modifications, if you also do each of
 85 | the following:
 86 | 
 87 |    a) Give prominent notice with each copy of the Combined Work that
 88 |    the Library is used in it and that the Library and its use are
 89 |    covered by this License.
 90 | 
 91 |    b) Accompany the Combined Work with a copy of the GNU GPL and this license
 92 |    document.
 93 | 
 94 |    c) For a Combined Work that displays copyright notices during
 95 |    execution, include the copyright notice for the Library among
 96 |    these notices, as well as a reference directing the user to the
 97 |    copies of the GNU GPL and this license document.
 98 | 
 99 |    d) Do one of the following:
100 | 
101 |        0) Convey the Minimal Corresponding Source under the terms of this
102 |        License, and the Corresponding Application Code in a form
103 |        suitable for, and under terms that permit, the user to
104 |        recombine or relink the Application with a modified version of
105 |        the Linked Version to produce a modified Combined Work, in the
106 |        manner specified by section 6 of the GNU GPL for conveying
107 |        Corresponding Source.
108 | 
109 |        1) Use a suitable shared library mechanism for linking with the
110 |        Library.  A suitable mechanism is one that (a) uses at run time
111 |        a copy of the Library already present on the user's computer
112 |        system, and (b) will operate properly with a modified version
113 |        of the Library that is interface-compatible with the Linked
114 |        Version. 
115 | 
116 |    e) Provide Installation Information, but only if you would otherwise
117 |    be required to provide such information under section 6 of the
118 |    GNU GPL, and only to the extent that such information is
119 |    necessary to install and execute a modified version of the
120 |    Combined Work produced by recombining or relinking the
121 |    Application with a modified version of the Linked Version. (If
122 |    you use option 4d0, the Installation Information must accompany
123 |    the Minimal Corresponding Source and Corresponding Application
124 |    Code. If you use option 4d1, you must provide the Installation
125 |    Information in the manner specified by section 6 of the GNU GPL
126 |    for conveying Corresponding Source.)
127 | 
128 |   5. Combined Libraries.
129 | 
130 |   You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 | 
136 |    a) Accompany the combined library with a copy of the same work based
137 |    on the Library, uncombined with any other library facilities,
138 |    conveyed under the terms of this License.
139 | 
140 |    b) Give prominent notice with the combined library that part of it
141 |    is a work based on the Library, and explaining where to find the
142 |    accompanying uncombined form of the same work.
143 | 
144 |   6. Revised Versions of the GNU Lesser General Public License.
145 | 
146 |   The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 | 
151 |   Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 | 
161 |   If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 | 


--------------------------------------------------------------------------------
/bios/seabios.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/bios/seabios.bin


--------------------------------------------------------------------------------
/bios/vgabios.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/bios/vgabios.bin


--------------------------------------------------------------------------------
/docs/docker-instructions.md:
--------------------------------------------------------------------------------
 1 | # Running windows95 in Docker
 2 | 
 3 | ## Display using a volume mount of the host X11 Unix Socket (Linux Only):
 4 | 
 5 | **Requirements:**
 6 | * Linux OS with a running X-Server Display
 7 | * [Docker](http://docker.io) 
 8 | 
 9 |         docker run -it -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$DISPLAY --device /dev/snd --name windows95 toolboc/windows95
10 | 
11 | 
12 | Note: You may need to run `xhost +` on your system to allow connections to the X server running on the host.
13 | 
14 | ## Display using Xming X11 Server over tcp Socket (Windows and beyond):
15 | 
16 | **Requirements:**
17 | * [Xming](https://sourceforge.net/projects/xming/)
18 | * [Docker](http://docker.io) 
19 | 
20 | 1. Start the Xming X11 Server
21 | 2. Run the command below:
22 | 
23 |         docker run -e DISPLAY=host.docker.internal:0 --name windows95 toolboc/windows95
24 | 
25 | ## Display using the host XQuartz Server (MacOS Only):
26 | **Requirements:**
27 | * [XQuartz](https://www.xquartz.org/)
28 | * [Docker](http://docker.io) 
29 | 
30 | 1. Start XQuartz, go to `Preferences` -> `Security`, and check the box `Allow connections from network clients`
31 | 2. Restart XQuartz
32 | 3. In the terminal, run 
33 | ```
34 | xhost +
35 | ```
36 | 4. run 
37 | ```
38 | docker run -it -e DISPLAY=host.docker.internal:0 toolboc/windows95
39 | ```
40 | 


--------------------------------------------------------------------------------
/docs/docker-kubernetes-gitpod.md:
--------------------------------------------------------------------------------
1 | ## Running an online version of windows95
2 | 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?
3 | 
4 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/felixrieseberg/windows95)
5 | 


--------------------------------------------------------------------------------
/docs/qemu.md:
--------------------------------------------------------------------------------
 1 | # QEMU Instructions
 2 | 
 3 | The image built here was made with QEMU. In this doc,  I'm keeping instructions
 4 | around.
 5 | 
 6 | Disk image creation
 7 | 
 8 | ```sh
 9 | qemu-img create -f raw windows95_v4.raw 1G
10 | ```
11 | 
12 | ISO CD image creation
13 | 
14 | ```sh
15 | hdiutil makehybrid -o output.iso /path/to/folder -iso -joliet
16 | ```
17 | 
18 | Installation
19 | ```sh
20 | qemu-system-i386 \
21 |     -cdrom Win95_OSR25.iso \
22 |     -m 128 \
23 |     -hda windows95.img \
24 |     -device sb16 \
25 |     -nic user,model=ne2k_pci \
26 |     -fda Win95_boot.img \
27 |     -boot a \
28 |     -M pc,acpi=off \
29 |     -cpu pentium
30 | ```
31 | 
32 | - Boot from floppy
33 | - Run `fdisk` and `format c:`
34 | - Run `D:\setup.exe` with `24796-OEM-0014736-66386`
35 | - After completing setup and restarting your computer, you might get an IOS Windows protection error
36 | - Use `fix95cpu.ima` as a bootable floppy to fix
37 | - Use `vga-driver.iso` to install different video driver
38 | 
39 | ```sh
40 | qemu-system-i386 \
41 |     -m 128 \
42 |     -hda images/windows95.img \
43 |     -device sb16 \
44 |     -M pc,acpi=off \
45 |     -cpu pentium \
46 |     -netdev user,id=mynet0 \
47 |     -device ne2k_isa,netdev=mynet0,irq=10
48 | ```
49 | 


--------------------------------------------------------------------------------
/forge.config.js:
--------------------------------------------------------------------------------
  1 | const path = require('path');
  2 | const fs = require('fs');
  3 | const package = require('./package.json');
  4 | 
  5 | require('dotenv').config()
  6 | 
  7 | process.env.TEMP = process.env.TMP = `C:\\Users\\FelixRieseberg\\AppData\\Local\\Temp`
  8 | 
  9 | const FLAGS = {
 10 |   SIGNTOOL_PATH: process.env.SIGNTOOL_PATH,
 11 |   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'),
 12 |   AZURE_METADATA_JSON: process.env.AZURE_METADATA_JSON || path.resolve(__dirname, 'trusted-signing-metadata.json'),
 13 |   AZURE_TENANT_ID: process.env.AZURE_TENANT_ID,
 14 |   AZURE_CLIENT_ID: process.env.AZURE_CLIENT_ID,
 15 |   AZURE_CLIENT_SECRET: process.env.AZURE_CLIENT_SECRET,
 16 |   APPLE_ID: process.env.APPLE_ID,
 17 |   APPLE_ID_PASSWORD: process.env.APPLE_ID_PASSWORD,
 18 | }
 19 | 
 20 | fs.writeFileSync(FLAGS.AZURE_METADATA_JSON, JSON.stringify({
 21 |   Endpoint: process.env.AZURE_CODE_SIGNING_ENDPOINT || "https://wcus.codesigning.azure.net",
 22 |   CodeSigningAccountName: process.env.AZURE_CODE_SIGNING_ACCOUNT_NAME,
 23 |   CertificateProfileName: process.env.AZURE_CODE_SIGNING_CERTIFICATE_PROFILE_NAME,
 24 | }, null, 2));
 25 | 
 26 | const windowsSign = {
 27 |   signToolPath: FLAGS.SIGNTOOL_PATH,
 28 |   signWithParams: `/v /dlib ${FLAGS.AZURE_CODE_SIGNING_DLIB} /dmdf ${FLAGS.AZURE_METADATA_JSON}`,
 29 |   timestampServer: "http://timestamp.acs.microsoft.com",
 30 |   hashes: ["sha256"],
 31 | }
 32 | 
 33 | module.exports = {
 34 |   hooks: {
 35 |     generateAssets: require('./tools/generateAssets'),
 36 |   },
 37 |   packagerConfig: {
 38 |     asar: false,
 39 |     icon: path.resolve(__dirname, 'assets', 'icon'),
 40 |     appBundleId: 'com.felixrieseberg.windows95',
 41 |     appCategoryType: 'public.app-category.developer-tools',
 42 |     win32metadata: {
 43 |       CompanyName: 'Felix Rieseberg',
 44 |       OriginalFilename: 'windows95'
 45 |     },
 46 |     osxSign: {
 47 |       identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)',
 48 |     },
 49 |     osxNotarize: {
 50 |       appleId: FLAGS.APPLE_ID,
 51 |       appleIdPassword: FLAGS.APPLE_ID_PASSWORD,
 52 |       teamId: 'LT94ZKYDCJ'
 53 |     },
 54 |     windowsSign,
 55 |     ignore: [
 56 |       /\/assets(\/?)/,
 57 |       /\/docs(\/?)/,
 58 |       /\/tools(\/?)/,
 59 |       /\/src\/.*\.ts/,
 60 |       /\/test(\/?)/,
 61 |       /\/@types(\/?)/,
 62 |       /\/helper-images(\/?)/,
 63 |       /package-lock\.json/,
 64 |       /README\.md/,
 65 |       /tsconfig\.json/,
 66 |       /Dockerfile/,
 67 |       /issue_template\.md/,
 68 |       /HELP\.md/,
 69 |       /forge\.config\.js/,
 70 |       /\.github(\/?)/,
 71 |       /\.circleci(\/?)/,
 72 |       /\.vscode(\/?)/,
 73 |       /\.gitignore/,
 74 |       /\.gitattributes/,
 75 |       /\.eslintignore/,
 76 |       /\.eslintrc/,
 77 |       /\.prettierrc/,
 78 |       /\/Microsoft\.Trusted\.Signing\.Client.*/,
 79 |       /\/trusted-signing-metadata/,
 80 |     ]
 81 |   },
 82 |   makers: [
 83 |     {
 84 |       name: '@electron-forge/maker-squirrel',
 85 |       platforms: ['win32'],
 86 |       config: (arch) => {
 87 |         return {
 88 |           name: 'windows95',
 89 |           authors: 'Felix Rieseberg',
 90 |           exe: 'windows95.exe',
 91 |           noMsi: true,
 92 |           remoteReleases: '',
 93 |           iconUrl: 'https://raw.githubusercontent.com/felixrieseberg/windows95/master/assets/icon.ico',
 94 |           loadingGif: './assets/boot.gif',
 95 |           setupExe: `windows95-${package.version}-setup-${arch}.exe`,
 96 |           setupIcon: path.resolve(__dirname, 'assets', 'icon.ico'),
 97 |           windowsSign
 98 |         }
 99 |       }
100 |     },
101 |     {
102 |       name: '@electron-forge/maker-zip',
103 |       platforms: ['darwin', 'win32']
104 |     },
105 |     {
106 |       name: '@electron-forge/maker-deb',
107 |       platforms: ['linux']
108 |     },
109 |     {
110 |       name: '@electron-forge/maker-rpm',
111 |       platforms: ['linux']
112 |     }
113 |   ]
114 | };
115 | 


--------------------------------------------------------------------------------
/issue_template.md:
--------------------------------------------------------------------------------
1 | ⚠️ Thank you for reporting an issue!
2 | 
3 | Before we go any further, understand that I probably won't be able to fullfil feature requests.
4 | Feel free to report what feature you'd love to see, just don't get angry when I don't have
5 | time to implement it 🙇‍♂️
6 | 
7 | I will however _gladly_ help you make a pull request if you're willing to play with Javascript!
8 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "windows95",
 3 |   "productName": "windows95",
 4 |   "version": "4.0.0",
 5 |   "description": "Windows 95, in an app. I'm sorry.",
 6 |   "main": "./dist/src/main/main.js",
 7 |   "scripts": {
 8 |     "start": "rimraf ./dist && electron-forge start",
 9 |     "package": "electron-forge package",
10 |     "make": "electron-forge make",
11 |     "publish": "electron-forge publish",
12 |     "lint": "prettier --write src/**/*.{ts,tsx} && npm run check-links",
13 |     "less": "node ./tools/lessc.js",
14 |     "tsc": "tsc -p tsconfig.json --noEmit",
15 |     "check-links": "node tools/check-links.js",
16 |     "postinstall": "patch-package"
17 |   },
18 |   "keywords": [],
19 |   "author": "Felix Rieseberg, felix@felixrieseberg.com",
20 |   "license": "MIT",
21 |   "config": {
22 |     "forge": "./forge.config.js"
23 |   },
24 |   "dependencies": {
25 |     "electron-squirrel-startup": "^1.0.1",
26 |     "react": "^17.0.1",
27 |     "react-dom": "^17.0.1",
28 |     "update-electron-app": "^2.0.1"
29 |   },
30 |   "devDependencies": {
31 |     "@electron-forge/cli": "7.6.1",
32 |     "@electron-forge/maker-deb": "7.6.1",
33 |     "@electron-forge/maker-flatpak": "^7.6.1",
34 |     "@electron-forge/maker-rpm": "^7.6.1",
35 |     "@electron-forge/maker-squirrel": "^7.6.1",
36 |     "@electron-forge/maker-zip": "^7.6.1",
37 |     "@electron-forge/publisher-github": "^7.6.1",
38 |     "@types/node": "^20",
39 |     "@types/react": "^17.0.0",
40 |     "@types/react-dom": "^17.0.0",
41 |     "dotenv": "^16.4.7",
42 |     "electron": "34.2.0",
43 |     "less": "^3.13.0",
44 |     "parcel-bundler": "^1.12.5",
45 |     "patch-package": "^8.0.0",
46 |     "prettier": "^3.5.1",
47 |     "rimraf": "^6.0.1",
48 |     "typescript": "^5.7.3"
49 |   }
50 | }
51 | 


--------------------------------------------------------------------------------
/patches/@electron+packager+18.3.6.patch:
--------------------------------------------------------------------------------
 1 | diff --git a/node_modules/@electron/packager/dist/win32.js b/node_modules/@electron/packager/dist/win32.js
 2 | index 5399b3e..f3b6e88 100644
 3 | --- a/node_modules/@electron/packager/dist/win32.js
 4 | +++ b/node_modules/@electron/packager/dist/win32.js
 5 | @@ -57,7 +57,26 @@ class WindowsApp extends platform_1.App {
 6 |              resOpts.iconPath = icon;
 7 |          }
 8 |          (0, common_1.debug)(`Running resedit with the options ${JSON.stringify(resOpts)}`);
 9 | -        await (0, resedit_1.resedit)(this.electronBinaryPath, resOpts);
10 | +
11 | +        // This causes segmentation faults for me on multiple machines
12 | +        // It's unclear why exactly but this spawn hack fixes it
13 | +        // await (0, resedit_1.resedit)(this.electronBinaryPath, resOpts);
14 | +
15 | +        const { spawnSync } = require('child_process');
16 | +        const resEditProcess = spawnSync('node', [
17 | +        'C:\\Users\\FelixRieseberg\\Code\\windows95\\tools\\resedit.js',
18 | +        this.electronBinaryPath
19 | +        ], {
20 | +        stdio: 'inherit'
21 | +        });
22 | +
23 | +        if (resEditProcess.error) {
24 | +        throw resEditProcess.error;
25 | +        }
26 | +
27 | +        if (resEditProcess.status !== 0) {
28 | +        throw new Error(`Resedit process exited with code ${resEditProcess.status}`);
29 | +        }
30 |      }
31 |      async signAppIfSpecified() {
32 |          const windowsSignOpt = this.opts.windowsSign;
33 | 


--------------------------------------------------------------------------------
/src/cache.ts:
--------------------------------------------------------------------------------
 1 | import { session } from "electron";
 2 | 
 3 | export async function clearCaches() {
 4 |   await clearCache();
 5 |   await clearStorageData();
 6 | }
 7 | 
 8 | export async function clearCache() {
 9 |   if (session.defaultSession) {
10 |     await session.defaultSession.clearCache();
11 |   }
12 | }
13 | 
14 | export async function clearStorageData() {
15 |   if (!session.defaultSession) {
16 |     return;
17 |   }
18 | 
19 |   await session.defaultSession.clearStorageData({
20 |     storages: [
21 |       "appcache",
22 |       "cookies",
23 |       "filesystem",
24 |       "indexdb",
25 |       "localstorage",
26 |       "shadercache",
27 |       "websql",
28 |       "serviceworkers",
29 |     ],
30 |     quotas: ["temporary", "persistent", "syncable"],
31 |   });
32 | }
33 | 


--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
 1 | import * as path from "path";
 2 | 
 3 | const IMAGES_PATH = path.join(__dirname, "../../images");
 4 | 
 5 | export const CONSTANTS = {
 6 |   IMAGES_PATH,
 7 |   IMAGE_PATH: path.join(IMAGES_PATH, "windows95.img"),
 8 |   IMAGE_DEFAULT_SIZE: 1073741824, // 1GB
 9 |   DEFAULT_STATE_PATH: path.join(IMAGES_PATH, "default-state.bin"),
10 | };
11 | 
12 | export const IPC_COMMANDS = {
13 |   TOGGLE_INFO: "TOGGLE_INFO",
14 |   SHOW_DISK_IMAGE: "SHOW_DISK_IMAGE",
15 |   ZOOM_IN: "ZOOM_IN",
16 |   ZOOM_OUT: "ZOOM_OUT",
17 |   ZOOM_RESET: "ZOOM_RESET",
18 |   // Machine instructions
19 |   MACHINE_START: "MACHINE_START",
20 |   MACHINE_RESTART: "MACHINE_RESTART",
21 |   MACHINE_STOP: "MACHINE_STOP",
22 |   MACHINE_RESET: "MACHINE_RESET",
23 |   MACHINE_ALT_F4: "MACHINE_ALT_F4",
24 |   MACHINE_ESC: "MACHINE_ESC",
25 |   MACHINE_ALT_ENTER: "MACHINE_ALT_ENTER",
26 |   MACHINE_CTRL_ALT_DEL: "MACHINE_CTRL_ALT_DEL",
27 |   // Machine events
28 |   MACHINE_STARTED: "MACHINE_STARTED",
29 |   MACHINE_STOPPED: "MACHINE_STOPPED",
30 |   // Else
31 |   APP_QUIT: "APP_QUIT",
32 |   GET_STATE_PATH: "GET_STATE_PATH",
33 | };
34 | 


--------------------------------------------------------------------------------
/src/less/emulator.less:
--------------------------------------------------------------------------------
 1 | #emulator {
 2 |   height: 100vh;
 3 |   width: 100vw;
 4 |   display: flex;
 5 | 
 6 |   > div {
 7 |     white-space: pre;
 8 |     font: 14px monospace;
 9 |     line-height: 14px
10 |   }
11 | 
12 |   > canvas {
13 |     display: none;
14 |     margin: auto;
15 |   }
16 | }
17 | 
18 | .paused {
19 |   canvas {
20 |     opacity: 0.2;
21 |     filter: blur(2px);
22 |     z-index: -100;
23 |   }
24 | 
25 |   #emulator-text-screen {
26 |     display: none;
27 |     visibility: hidden;
28 |   }
29 | }
30 | 


--------------------------------------------------------------------------------
/src/less/info.less:
--------------------------------------------------------------------------------
1 | #information {
2 |   text-align: center;
3 |   position: absolute;
4 |   width: 100vw;
5 |   bottom: 50px;
6 |   font-size: 18px;
7 | }
8 | 


--------------------------------------------------------------------------------
/src/less/root.less:
--------------------------------------------------------------------------------
  1 | @import "./status.less";
  2 | @import "./emulator.less";
  3 | @import "./info.less";
  4 | @import "./settings.less";
  5 | @import "./start.less";
  6 | 
  7 | /* GENERAL RESETS */
  8 | 
  9 | html, body {
 10 |   margin: 0;
 11 |   padding: 0;
 12 | }
 13 | 
 14 | body {
 15 |   background: #000;
 16 | }
 17 | 
 18 | body.paused > #emulator {
 19 |   display: none;
 20 | }
 21 | 
 22 | body.paused {
 23 |   background: #008080;
 24 |   font-family: Courier;
 25 | }
 26 | 
 27 | #buttons {
 28 |   user-select: none;
 29 | }
 30 | 
 31 | section {
 32 |   display: flex;
 33 |   position: absolute;
 34 |   width: 100vw;
 35 |   height: 100vh;
 36 |   align-items: center;
 37 |   justify-content: center;
 38 | }
 39 | 
 40 | .card {
 41 |   width: 75%;
 42 |   max-width: 700px;
 43 |   min-width: 400px;
 44 | 
 45 |   .card-title {
 46 |     img {
 47 |       margin-right: 5px;
 48 |     }
 49 |   }
 50 | }
 51 | 
 52 | .nav-link > img,
 53 | .btn > img {
 54 |   height: 24px;
 55 |   margin-right: 4px;
 56 | }
 57 | 
 58 | .windows95 {
 59 |   * {
 60 |     user-select: none;
 61 |   }
 62 | 
 63 |   *:focus {
 64 |     outline: none;
 65 |   }
 66 | 
 67 |   nav .nav-link,
 68 |   nav .nav-logo {
 69 |     height: 37px;
 70 |     display: flex;
 71 |   }
 72 | 
 73 |   nav .nav-logo img {
 74 |     margin-left: 2px;
 75 |     max-height: 20px;
 76 |   }
 77 | 
 78 |   nav .nav-logo > span {
 79 |     position: absolute;
 80 |     top: 9px;
 81 |     left: 37px;
 82 |     font-weight: bold;
 83 |   }
 84 | 
 85 |   .btn {
 86 |     height: 40px;
 87 |     padding-top: 3px;
 88 |   }
 89 | 
 90 |   .btn:focus {
 91 |     border-color: #fff #000 #000 #fff;
 92 |     outline: 5px auto -webkit-focus-ring-color;
 93 |   }
 94 | 
 95 |   .btn.active:before,
 96 |   .btn:focus:before,
 97 |   button.active:before,
 98 |   button:focus:before,
 99 |   input[type=submit].active:before,
100 |   input[type=submit]:focus:before {
101 |     border-color: #dedede grey grey #dedede;
102 |   }
103 | 
104 |   .card {
105 |     // Fix link colors
106 |     .link, .link:active, .link:link, .link:visited, a, a:active, a:link, a:visited {
107 |       color: #008080;
108 |       text-decoration: underline;
109 |       cursor: pointer;
110 |     }
111 | 
112 |     // Ensure a-elements in fieldsets receive click events
113 |     fieldset:before {
114 |       pointer-events: none;
115 |     }
116 |   }
117 | }
118 | 


--------------------------------------------------------------------------------
/src/less/settings.less:
--------------------------------------------------------------------------------
 1 | #floppy-path {
 2 |   font-size: .6rem;
 3 |   width: 100%;
 4 |   height: 30px;
 5 |   padding-left: 8px;
 6 |   border-color: #000 #fff #fff #000;
 7 |   border-style: solid;
 8 |   border-width: 2px;
 9 |   background-color: #c3c3c3;
10 |   line-height: 27px;
11 | }
12 | 
13 | #file-input {
14 |   display: none;
15 | }
16 | 
17 | .settings {
18 |   legend > img {
19 |     margin-right: 5px;
20 |   }
21 | }
22 | 


--------------------------------------------------------------------------------
/src/less/start.less:
--------------------------------------------------------------------------------
 1 | #section-start {
 2 |   display: flex;
 3 |   flex-direction: column;
 4 | 
 5 |   > small {
 6 |     margin-top: 25px;
 7 |     font-size: .8rem;
 8 |   }
 9 | }
10 | 


--------------------------------------------------------------------------------
/src/less/status.less:
--------------------------------------------------------------------------------
 1 | #status {
 2 |   user-select: none;
 3 |   position: absolute;
 4 |   z-index: 100;
 5 |   left: calc(50vw - 110px);
 6 |   background: white;
 7 |   font-size: 10px;
 8 |   padding-bottom: 3px;
 9 |   border-bottom-left-radius: 15px;
10 |   border-bottom-right-radius: 15px;
11 |   overflow: hidden;
12 |   padding-left: 10px;
13 |   padding-right: 10px;
14 |   max-height: 18px;
15 |   top: 0;
16 | }
17 | 


--------------------------------------------------------------------------------
/src/less/vendor/95.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/src/less/vendor/95.ttf


--------------------------------------------------------------------------------
/src/less/vendor/95css.css:
--------------------------------------------------------------------------------
1 | *,:after,:before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{font-style:normal;line-height:inherit}address,dl,ol,ul{margin-bottom:1rem}dl,ol,ul{margin-top:0}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]),a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{border-style:none}img,svg{vertical-align:middle}svg{overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}html{box-sizing:border-box;-ms-overflow-style:scrollbar}*,:after,:before{box-sizing:inherit}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:flex;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12,.col-auto,.col-lg,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-auto,.col-md,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-md-auto,.col-sm,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{flex-basis:0;flex-grow:1;max-width:100%}.col-auto{flex:0 0 auto;width:auto;max-width:100%}.col-1{flex:0 0 8.333333%;max-width:8.333333%}.col-2{flex:0 0 16.666667%;max-width:16.666667%}.col-3{flex:0 0 25%;max-width:25%}.col-4{flex:0 0 33.333333%;max-width:33.333333%}.col-5{flex:0 0 41.666667%;max-width:41.666667%}.col-6{flex:0 0 50%;max-width:50%}.col-7{flex:0 0 58.333333%;max-width:58.333333%}.col-8{flex:0 0 66.666667%;max-width:66.666667%}.col-9{flex:0 0 75%;max-width:75%}.col-10{flex:0 0 83.333333%;max-width:83.333333%}.col-11{flex:0 0 91.666667%;max-width:91.666667%}.col-12{flex:0 0 100%;max-width:100%}.order-first{order:-1}.order-last{order:13}.order-0{order:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-9{order:9}.order-10{order:10}.order-11{order:11}.order-12{order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{flex-basis:0;flex-grow:1;max-width:100%}.col-sm-auto{flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{flex:0 0 25%;max-width:25%}.col-sm-4{flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{flex:0 0 50%;max-width:50%}.col-sm-7{flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{flex:0 0 75%;max-width:75%}.col-sm-10{flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{flex:0 0 100%;max-width:100%}.order-sm-first{order:-1}.order-sm-last{order:13}.order-sm-0{order:0}.order-sm-1{order:1}.order-sm-2{order:2}.order-sm-3{order:3}.order-sm-4{order:4}.order-sm-5{order:5}.order-sm-6{order:6}.order-sm-7{order:7}.order-sm-8{order:8}.order-sm-9{order:9}.order-sm-10{order:10}.order-sm-11{order:11}.order-sm-12{order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{flex-basis:0;flex-grow:1;max-width:100%}.col-md-auto{flex:0 0 auto;width:auto;max-width:100%}.col-md-1{flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{flex:0 0 25%;max-width:25%}.col-md-4{flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{flex:0 0 50%;max-width:50%}.col-md-7{flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{flex:0 0 75%;max-width:75%}.col-md-10{flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{flex:0 0 100%;max-width:100%}.order-md-first{order:-1}.order-md-last{order:13}.order-md-0{order:0}.order-md-1{order:1}.order-md-2{order:2}.order-md-3{order:3}.order-md-4{order:4}.order-md-5{order:5}.order-md-6{order:6}.order-md-7{order:7}.order-md-8{order:8}.order-md-9{order:9}.order-md-10{order:10}.order-md-11{order:11}.order-md-12{order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{flex-basis:0;flex-grow:1;max-width:100%}.col-lg-auto{flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{flex:0 0 25%;max-width:25%}.col-lg-4{flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{flex:0 0 50%;max-width:50%}.col-lg-7{flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{flex:0 0 75%;max-width:75%}.col-lg-10{flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{flex:0 0 100%;max-width:100%}.order-lg-first{order:-1}.order-lg-last{order:13}.order-lg-0{order:0}.order-lg-1{order:1}.order-lg-2{order:2}.order-lg-3{order:3}.order-lg-4{order:4}.order-lg-5{order:5}.order-lg-6{order:6}.order-lg-7{order:7}.order-lg-8{order:8}.order-lg-9{order:9}.order-lg-10{order:10}.order-lg-11{order:11}.order-lg-12{order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{flex-basis:0;flex-grow:1;max-width:100%}.col-xl-auto{flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{flex:0 0 25%;max-width:25%}.col-xl-4{flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{flex:0 0 50%;max-width:50%}.col-xl-7{flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{flex:0 0 75%;max-width:75%}.col-xl-10{flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{flex:0 0 100%;max-width:100%}.order-xl-first{order:-1}.order-xl-last{order:13}.order-xl-0{order:0}.order-xl-1{order:1}.order-xl-2{order:2}.order-xl-3{order:3}.order-xl-4{order:4}.order-xl-5{order:5}.order-xl-6{order:6}.order-xl-7{order:7}.order-xl-8{order:8}.order-xl-9{order:9}.order-xl-10{order:10}.order-xl-11{order:11}.order-xl-12{order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-fill{flex:1 1 auto!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}@media (min-width:576px){.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}}@media (min-width:768px){.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}@font-face{font-family:windows;src:url(windows.woff2) format("woff2"),url(windows.woff) format("woff")}body{font-family:windows,monospace;font-size:.8rem;line-height:1.7;padding:60px 0;color:#000;background-color:#008483}code,mark{background-color:#c3c3c3}code{padding:2px 4px;font-family:windows}.btn,button,input[type=submit]{font-size:.8rem;position:relative;display:inline-block;width:auto;height:30px;padding:2px 15px;border-color:#fff #000 #000 #fff;border-style:solid;border-width:2px;background-color:#c3c3c3}.btn:before,button:before,input[type=submit]:before{position:absolute;top:0;right:0;bottom:0;left:0;display:block;content:"";border-color:#dedede grey grey #dedede;border-style:solid;border-width:2px}.btn.active,.btn:focus,button.active,button:focus,input[type=submit].active,input[type=submit]:focus{border-color:#000 #fff #fff #000;outline:none}.btn.active:before,.btn:focus:before,button.active:before,button:focus:before,input[type=submit].active:before,input[type=submit]:focus:before{border-color:grey #dedede #dedede grey}.btn.disabled,button.disabled,input[type=submit].disabled{background-color:#c3c3c3}.input,input,select,textarea{font-size:.6rem;width:100%;height:30px;padding-left:8px;border-color:#000 #fff #fff #000;border-style:solid;border-width:2px;background-color:#fff}.input:focus,input:focus,select:focus,textarea:focus{outline:none}.disabled.input,.input:disabled,input.disabled,input:disabled,select.disabled,select:disabled,textarea.disabled,textarea:disabled{background-color:#c3c3c3}textarea{min-height:150px}select{padding:2px 32px 2px 4px;border-radius:0;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAO8AAADvCAYAAAAacIO5AAAAAXNSR0IArs4c6QAACaVJREFUeAHt3LFuU9sSBmDn3hshISGQQBRISFBR0tCmoOVZ6KGgCEhIUNFQ8hy8AHkQHoACCmi52Uc60vGRjeNktmftmc8SRRJ7Zs03+/d2YXH0+/yx8iBAYFECr1+/Xv1nUSd2WAIEVlNwT09Phde1QGBJAn8HdzqzO++SNuesrQX+GdwJQnhbXw6GX4rAv4M7nVt4l7I952wrsCm4E4bwtr0kDL4EgW3Bnc4uvEvYoDO2FPhTcCcQ4W15WRh6dIFdwZ3OL7yjb9H52glcJLgTivC2uzQMPLLARYM7zSC8I2/S2VoJ7BPcCUZ4W10ehh1VYN/gTnMI76jbdK42ApcJ7oQjvG0uEYOOKHDZ4E6zCO+IG3WmFgJXCe4EJLwtLhNDjiZw1eBO8wjvaFt1nvICEcGdkI6i/ieNs7Oz8ugGJBAhcHJyElHGnTdEURECCQI+Niega0kgQkB4IxTVIJAgILwJ6FoSiBAQ3ghFNQgkCAhvArqWBCIEhDdCUQ0CCQLCm4CuJYEIAeGNUFSDQIKA8Caga0kgQkB4IxTVIJAgILwJ6FoSiBAQ3ghFNQgkCAhvArqWBCIEhDdCUQ0CCQLCm4CuJYEIAeGNUFSDQIKA8Caga0kgQkB4IxTVIJAgILwJ6FoSiBAQ3ghFNQgkCAhvArqWBCIEhDdCUQ0CCQLCm4CuJYEIAeGNUFSDQIKA8Caga0kgQkB4IxTVIJAgILwJ6FoSiBAQ3ghFNQgkCAhvArqWBCIEhDdCUQ0CCQLCm4CuJYEIAeGNUFSDQIKA8Caga0kgQkB4IxTVIJAgILwJ6FoSiBAQ3ghFNQgkCAhvArqWBCIEhDdCUQ0CCQLCm4CuJYEIAeGNUFSDQIKA8Caga0kgQkB4IxTVIJAgILwJ6FoSiBAQ3ghFNQgkCAhvArqWBCIEhDdCUQ0CCQLCm4CuJYEIAeGNUFSDQIKA8Caga0kgQkB4IxTVIJAgILwJ6FoSiBAQ3ghFNQgkCAhvArqWBCIEhDdCUQ0CCQLCm4CuJYEIAeGNUFSDQIKA8Caga0kgQkB4IxTVIJAgILwJ6FoSiBAQ3ghFNQgkCAhvArqWBCIEhDdCUQ0CCQLCm4CuJYEIAeGNUFSDQIKA8Caga0kgQkB4IxTVIJAgILwJ6FoSiBAQ3ghFNQgkCAhvArqWBCIE/hdRZCk1Tk5OlnJU57yCwJcvX67w6uW81J13ObtyUgJrAsK7xuEHAssREN7l7MpJCawJCO8ahx8ILEdAeJezKyclsCYgvGscfiCwHAHhXc6unJTAmoDwrnH4gcByBIR3ObtyUgJrAsK7xuEHAssREN7l7MpJCawJtPpu8+fPn9eGv8gPHz9+XF3mdRep7Tl/Fnj27Nnq+fPnf35S47+2Cu+NGzf2XvXx8fHer/GCGIHJ/jI7i+k+fhUfm8ffkRMS2CggvBtZ/JLA+ALCO/6OnJDARgHh3cjilwTGFxDe8XfkhAQ2CgjvRha/JDC+gPCOvyMnJLBRQHg3svglgfEFhHf8HTkhgY0CwruRxS8JjC9w9Pv8EXHMs7OziDLD1fj+/fvq169fe53r69evqxcvXuz1mupPfv/+/erBgwd7jXn9+vXVrVu39nrNEp4c9f+Ht/pu82UWO108+15A+4b9Muda2mvu3r27unfv3tKOPfR5fWweej0OR2C7gPBut/EXAkMLCO/Q63E4AtsFhHe7jb8QGFpAeIdej8MR2C4gvNtt/IXA0ALCO/R6HI7AdgHh3W7jLwSGFhDeodfjcAS2Cwjvdht/ITC0gK9HzrCe+/fvrz59+rSz8rdv31YvX77c+byRn/Du3bvVnTt3dh5xMvGIFRDeWM+/ql27dm316NGjnZUr/J/EDx8+9J3lnZue5wk+Ns/jqiqB2QWEd3ZiDQjMIyC887iqSmB2AeGdnVgDAvMICO88rqoSmF1AeGcn1oDAPALCO4+rqgRmFxDe2Yk1IDCPgPDO46oqgdkFhHd2Yg0IzCPg65HzuF6o6u3bt1cfPnzY+dyfP3+uXr16tfN5kU94+/btavp/k3c9phk8cgSEN8f9r67Td6CfPHmy8wQ/fvzY+ZzoJzx+/Hh18+bN6LLqBQr42ByIqRSBQwoI7yG19SIQKCC8gZhKETikgPAeUlsvAoECwhuIqRSBQwoI7yG19SIQKCC8gZhKETikgPAeUlsvAoECwhuIqRSBQwoI7yG19SIQKODrkYGYc5WavmP85s2bucpvrHuR7zVvfKFfHkxAeA9GfflGx8fHq6dPn16+gFeWFPCxueRaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMJQWEt+RaDdVBQHg7bNmMNQV+BzxOT09/n+v4x8A1cMhr4KrZFVxvWt64k66Bq4RXcJOWdsh3d73G/TRx2fAKruC64yZfA5cJr+AmL83dcNy74SF3s294BVdw3XEHuQb2Ca/gDrK0Q7676zXuXf6i4RVcwXXHHewauEh4BXewpbkbjns3PORudoVXcAXXHXfQa+BP4RXcQZd2yHd3vca9y28Lr+AKrjvu4NfApvAK7uBLczcc9254yN38O7yCK7juuAu5Bv4ZXsFdyNIO+e6u17h3+b/DK7iC6467sGtgCq/gLmxp7obj3g0PuJv/Hh0dnWf39PxN14MAgSUJ/B9DhDGbr5D4RQAAAABJRU5ErkJggg==");background-repeat:no-repeat;background-position:100%;background-size:26px;-webkit-appearance:none}fieldset{position:relative;margin-bottom:20px;padding:10px;border-color:grey #fff #fff grey;background-color:#c3c3c3}fieldset,fieldset:before{border-style:solid;border-width:2px}fieldset:before{position:absolute;top:11px;right:0;bottom:0;left:0;display:block;content:"";border-color:#dedede grey grey #dedede}legend{font-size:.9rem;font-weight:700;position:relative;display:inline-block;width:auto;padding:0 3px;background-color:#c3c3c3}.input-field{margin-bottom:20px}.link,.link:active,.link:hover,.link:link,.link:visited,a,a:active,a:hover,a:link,a:visited{color:#c3c3c3}::-webkit-scrollbar{width:20px;background-image:url(bg-pattern.png);background-size:20px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{position:relative;border-color:#fff #000 #000 #fff;border-style:solid;border-width:2px;background:#c3c3c3;background-color:#c3c3c3}::-webkit-scrollbar-thumb:before{position:absolute;top:0;right:0;bottom:0;left:0;display:block;content:"";border-color:#dedede grey grey #dedede;border-style:solid;border-width:2px}.card,.modal-dialog{position:relative;border-color:#fff #000 #000 #fff;border-style:solid;border-width:2px;background-color:#c3c3c3}.card:before,.modal-dialog:before{position:absolute;top:0;right:0;bottom:0;left:0;display:block;content:"";pointer-events:none;border-color:#dedede grey grey #dedede;border-style:solid;border-width:2px}.card-header,.modal-header{width:calc(100% - 4px);margin:2px;padding:4px 10px 6px;color:#fff;background-color:#0f0086}.card-title,.modal-title{font-size:1rem;display:block;margin:0}.card-body,.modal-body{margin:10px}.modal{display:none}.modal.is-open{position:fixed;top:0;right:0;bottom:0;left:0;display:block;width:100vw;height:100vh}.modal-bg{position:relative;width:100%;height:100%;background-color:rgba(0,0,0,.5)}.modal-dialog{position:absolute;top:50%;right:0;left:0;width:100%;max-width:600px;margin:0 auto;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.modal-header{position:relative}.modal-close{position:absolute;top:5px;right:4px;height:24px;padding:0 4px}.has-bottom-nav{padding-bottom:80px}.has-top-nav{padding-top:80px}.nav{position:relative;display:flex;margin-bottom:40px;padding:4px;border-bottom:2px solid #fff;background-color:#c3c3c3;box-shadow:0 2px 2px rgba(0,0,0,.2);justify-content:space-between;align-items:center}.nav:before{position:absolute;bottom:0;left:0;display:block;width:100%;height:2px;content:"";background-color:#dedede}.nav.nav-bottom,.nav.nav-top{position:fixed;z-index:20;width:100%;margin:0}.nav.nav-top{top:0}.nav.nav-bottom{bottom:0;border-top:2px solid #fff;border-bottom:none;box-shadow:0 -2px 2px rgba(0,0,0,.2)}.nav.nav-bottom:before{top:0;bottom:unset}.nav-logo,.nav-logo:active,.nav-logo:hover,.nav-logo:link,.nav-logo:visited{display:inline-block;min-width:150px;height:40px;margin-right:4px;padding:2px 20px 2px 2px;color:#000;border-color:#fff #000 #000 #fff;border-style:solid;border-width:2px;background-color:#c3c3c3}.nav-logo:active:before,.nav-logo:before,.nav-logo:hover:before,.nav-logo:link:before,.nav-logo:visited:before{position:absolute;top:0;right:0;bottom:0;left:0;display:block;content:"";border-color:#dedede grey grey #dedede;border-style:solid;border-width:2px}.nav-logo:active img,.nav-logo:hover img,.nav-logo:link img,.nav-logo:visited img,.nav-logo img{max-height:32px}.nav-menu-btn{display:block;height:40px}@media screen and (min-width:768px){.nav-menu-btn{display:none}}.nav-menu{position:absolute;bottom:100%;left:0;display:none;width:100%;color:#000;border-color:#fff #000 #000 #fff;border-style:solid;border-width:2px;background-color:#c3c3c3;flex-flow:column}.nav-menu.active{display:flex}.nav-menu:before{position:absolute;top:0;right:0;bottom:0;left:0;display:block;content:"";border-color:#dedede grey grey #dedede;border-style:solid;border-width:2px}@media screen and (min-width:768px){.nav-menu{position:relative;display:flex;border:none;flex-flow:row}.nav-menu:before{content:none}}.nav-link,.nav-link:active,.nav-link:hover,.nav-link:link,.nav-link:visited{line-height:2;position:relative;display:block;height:40px;margin:0 2px;padding:4px 40px 4px 10px;color:#000}.nav-link:active:hover,.nav-link:hover,.nav-link:hover:hover,.nav-link:link:hover,.nav-link:visited:hover{color:#fff;background-color:#0f0086}@media screen and (min-width:768px){.nav-link:active:hover,.nav-link:hover,.nav-link:hover:hover,.nav-link:link:hover,.nav-link:visited:hover{color:#000;background-color:#c3c3c3}}@media screen and (min-width:768px){.nav-link,.nav-link:active,.nav-link:hover,.nav-link:link,.nav-link:visited{border-color:#fff #000 #000 #fff;border-style:solid;border-width:2px;background-color:#c3c3c3}.nav-link:active:before,.nav-link:before,.nav-link:hover:before,.nav-link:link:before,.nav-link:visited:before{position:absolute;top:0;right:0;bottom:0;left:0;display:block;content:"";border-color:#dedede grey grey #dedede;border-style:solid;border-width:2px}.nav-link.active,.nav-link:active.active,.nav-link:hover.active,.nav-link:link.active,.nav-link:visited.active{border-color:#000 #fff #fff #000;background-image:url(bg-pattern.png)}.nav-link.active:before,.nav-link:active.active:before,.nav-link:hover.active:before,.nav-link:link.active:before,.nav-link:visited.active:before{border-color:grey #dedede #dedede grey}}


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


--------------------------------------------------------------------------------
/src/less/vendor/bg-pattern.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/src/less/vendor/bg-pattern.png


--------------------------------------------------------------------------------
/src/less/vendor/dropdown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/src/less/vendor/dropdown.png


--------------------------------------------------------------------------------
/src/less/vendor/windows.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/src/less/vendor/windows.woff


--------------------------------------------------------------------------------
/src/less/vendor/windows.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/src/less/vendor/windows.woff2


--------------------------------------------------------------------------------
/src/main/about-panel.ts:
--------------------------------------------------------------------------------
 1 | import { AboutPanelOptionsOptions, app } from "electron";
 2 | 
 3 | /**
 4 |  * Sets Fiddle's About panel options on Linux and macOS
 5 |  *
 6 |  * @returns
 7 |  */
 8 | export function setupAboutPanel(): void {
 9 |   if (process.platform === "win32") return;
10 | 
11 |   const options: AboutPanelOptionsOptions = {
12 |     applicationName: "windows95",
13 |     applicationVersion: app.getVersion(),
14 |     version: process.versions.electron,
15 |     copyright: "Felix Rieseberg",
16 |   };
17 | 
18 |   switch (process.platform) {
19 |     case "linux":
20 |       options.website = "https://github.com/felixrieseberg/windows95";
21 |     case "darwin":
22 |       options.credits = "https://github.com/felixrieseberg/windows95";
23 |     default:
24 |     // fallthrough
25 |   }
26 | 
27 |   app.setAboutPanelOptions(options);
28 | }
29 | 


--------------------------------------------------------------------------------
/src/main/fileserver/encoding.ts:
--------------------------------------------------------------------------------
 1 | export function encode(text: string) {
 2 |   // Convert to windows-1252 compatible string by removing unsupported chars
 3 |   let result = text.replaceAll(/[^\x00-\xFF]/g, '');
 4 | 
 5 |   // If result would be empty, return original
 6 |   if (!result.trim()) {
 7 |     return text;
 8 |   }
 9 | 
10 |   return result;
11 | }
12 | 
13 | export function getEncoding() {
14 |   return `<meta http-equiv="Content-Type" content="text/html; charset=windows-1252">`;
15 | }
16 | 


--------------------------------------------------------------------------------
/src/main/fileserver/fileserver.ts:
--------------------------------------------------------------------------------
  1 | import { protocol, net } from 'electron';
  2 | import * as fs from 'fs';
  3 | import * as path from 'path';
  4 | import { generateDirectoryListing } from './page-directory-listing';
  5 | import { generateErrorPage } from './page-error';
  6 | import { log } from '../logging';
  7 | 
  8 | export interface FileEntry {
  9 |   name: string;
 10 |   fullPath: string;
 11 |   stats: fs.Stats;
 12 | }
 13 | 
 14 | export const APP_INTERCEPT = 'http://windows95/';
 15 | export const MY_COMPUTER_INTERCEPT = 'http://my-computer/';
 16 | 
 17 | const interceptedUrls = [
 18 |   MY_COMPUTER_INTERCEPT,
 19 |   APP_INTERCEPT
 20 | ];
 21 | 
 22 | export function setupFileServer() {
 23 |   // Register protocol handler for our custom schema
 24 |   protocol.handle('http', async (request) => {
 25 |     if (!interceptedUrls.some(url => request.url.startsWith(url))) {
 26 |       return fetch(request.url, {
 27 |         headers: request.headers,
 28 |         method: request.method,
 29 |         body: request.body,
 30 |       });
 31 |     }
 32 | 
 33 |     try {
 34 |       const { fullPath, decodedPath } = getFilePath(request.url);
 35 | 
 36 |       log(`FileServer: Handling request for ${request.url}`, { fullPath, decodedPath });
 37 | 
 38 |       // Check if path exists
 39 |       if (!fs.existsSync(fullPath)) {
 40 |         return new Response(generateErrorPage(
 41 |           'File or Directory Not Found',
 42 |           decodedPath
 43 |         ), {
 44 |           status: 404,
 45 |           headers: {
 46 |             'Content-Type': 'text/html'
 47 |           }
 48 |         });
 49 |       }
 50 | 
 51 |       // Check if it's a directory
 52 |       const stats = await fs.promises.stat(fullPath);
 53 |       if (stats.isDirectory()) {
 54 |         // If we're in an app-intercept, check if there's an index.htm file in the directory
 55 |         if (request.url.startsWith(APP_INTERCEPT)) {
 56 |           const indexHtmlPath = path.join(fullPath, 'index.htm');
 57 |           if (fs.existsSync(indexHtmlPath)) {
 58 |             return serveFile(indexHtmlPath);
 59 |           }
 60 |         }
 61 | 
 62 |         // Generate directory listing
 63 |         const files = await fs.promises.readdir(fullPath);
 64 |         const listing = generateDirectoryListing(fullPath, files);
 65 |         return new Response(listing, {
 66 |           status: 200,
 67 |           headers: {
 68 |             'Content-Type': 'text/html'
 69 |           }
 70 |         });
 71 |       } else {
 72 |         try {
 73 |           return await serveFile(fullPath);
 74 |         } catch (error) {
 75 |           // Handle specific file read errors
 76 |           if (error.code === 'EACCES') {
 77 |             return new Response(generateErrorPage(
 78 |               'Access Denied',
 79 |               'You do not have permission to access this file'
 80 |             ), {
 81 |               status: 403,
 82 |               headers: {
 83 |                 'Content-Type': 'text/html'
 84 |               }
 85 |             });
 86 |           }
 87 | 
 88 |           // Re-throw other errors to be caught by outer try-catch
 89 |           throw error;
 90 |         }
 91 |       }
 92 |     } catch (error) {
 93 |       const errorPage = generateErrorPage(
 94 |         'Internal Server Error',
 95 |         `An error occurred while processing your request: ${error.message}`
 96 |       );
 97 |       return new Response(errorPage, {
 98 |         status: 500,
 99 |         headers: {
100 |           'Content-Type': 'text/html'
101 |         }
102 |       });
103 |     }
104 |   });
105 | }
106 | 
107 | function getFilePath(url: string) {
108 |   let urlPath: string;
109 |   let fullPath: string;
110 |   let decodedPath: string;
111 | 
112 |   if (url.startsWith(APP_INTERCEPT)) {
113 |     fullPath = path.resolve(__dirname, '../../../static/www', url.replace(APP_INTERCEPT, ''));
114 |     decodedPath = '.';
115 |   } else if (url.startsWith(MY_COMPUTER_INTERCEPT)) {
116 |     urlPath = url.replace(MY_COMPUTER_INTERCEPT, '');
117 |     decodedPath = decodeURIComponent(urlPath);
118 |     fullPath = path.join('/', decodedPath);
119 |   } else {
120 |     throw new Error('Invalid URL');
121 |   }
122 | 
123 |   return { fullPath, decodedPath };
124 | }
125 | 
126 | async function serveFile(fullPath: string): Promise<Response> {
127 |   const fileData = await fs.promises.readFile(fullPath);
128 | 
129 |   // Determine content type based on file extension
130 |   const ext = path.extname(fullPath).toLowerCase();
131 |   let contentType = 'application/octet-stream';
132 | 
133 |   // Common content types
134 |   const contentTypes: Record<string, string> = {
135 |     '.htm': 'text/html',
136 |     '.html': 'text/html',
137 |     '.txt': 'text/plain',
138 |     '.css': 'text/css',
139 |     '.js': 'text/javascript',
140 |     '.jpg': 'image/jpeg',
141 |     '.jpeg': 'image/jpeg',
142 |     '.png': 'image/png',
143 |     '.gif': 'image/gif'
144 |   };
145 | 
146 |   if (ext in contentTypes) {
147 |     contentType = contentTypes[ext];
148 |   }
149 | 
150 |   return new Response(fileData, {
151 |     status: 200,
152 |     headers: {
153 |       'Content-Type': contentType
154 |     }
155 |   });
156 | }
157 | 
158 | 


--------------------------------------------------------------------------------
/src/main/fileserver/hide-files.ts:
--------------------------------------------------------------------------------
 1 | import { Stats } from "fs";
 2 | import { settings } from "../settings";
 3 | import { FileEntry } from "./fileserver";
 4 | 
 5 | const FILES_TO_HIDE_ON_DARWIN: string[] = [
 6 |   '.DS_Store',
 7 |   '.localized',
 8 |   '.Trashes',
 9 |   '.fseventsd',
10 |   '.Spotlight-V100',
11 |   '.file',
12 |   '.hotfiles.btree',
13 |   '.DocumentRevisions-V100',
14 |   '.TemporaryItems',
15 |   '.file (resource fork files)',
16 |   '.VolumeIcon.icns',
17 | ];
18 | 
19 | const FILES_TO_HIDE_ON_WINDOWS: string[] = [
20 |   'desktop.ini',
21 |   'Thumbs.db',
22 |   'ehthumbs.db',
23 |   'ehthumbs.db-shm',
24 |   'ehthumbs.db-wal',
25 | ];
26 | 
27 | const FILES_TO_HIDE_ON_LINUX: string[] = [];
28 | 
29 | export function shouldHideFile(file: FileEntry) {
30 |   if (isHiddenFile(file) && !settings.get('isFileServerShowingHiddenFiles')) {
31 |     return true;
32 |   }
33 | 
34 |   if (isSystemHiddenFile(file) && !settings.get('isFileServerShowingSystemHiddenFiles')) {
35 |     return true;
36 |   }
37 | 
38 |   return false;
39 | }
40 | 
41 | export function isHiddenFile(file: FileEntry) {
42 |   if (process.platform === 'win32') {
43 |     return (file.stats.mode & 0x2) === 0x2;
44 |   } else {
45 |     return file.name.startsWith('.');
46 |   }
47 | }
48 | 
49 | export function isSystemHiddenFile(file: FileEntry) {
50 |   return getFilesToHide().some(hiddenFile => file.name.endsWith(hiddenFile));
51 | }
52 | 
53 | let _filesToHide: string[];
54 | 
55 | function getFilesToHide() {
56 |   if (_filesToHide) {
57 |     return _filesToHide;
58 |   }
59 | 
60 |   if (process.platform === 'darwin') {
61 |     _filesToHide = FILES_TO_HIDE_ON_DARWIN;
62 |   } else if (process.platform === 'win32') {
63 |     _filesToHide = FILES_TO_HIDE_ON_WINDOWS;
64 |   } else {
65 |     _filesToHide = FILES_TO_HIDE_ON_LINUX;
66 |   }
67 | 
68 |   return _filesToHide;
69 | }
70 | 
71 | 
72 | 
73 | 


--------------------------------------------------------------------------------
/src/main/fileserver/page-directory-listing.ts:
--------------------------------------------------------------------------------
  1 | import path from "path";
  2 | import fs from "fs";
  3 | 
  4 | import { APP_INTERCEPT, FileEntry, MY_COMPUTER_INTERCEPT } from "./fileserver";
  5 | import { shouldHideFile } from "./hide-files";
  6 | import { encode, getEncoding } from "./encoding";
  7 | import { log } from "console";
  8 | import { app } from "electron";
  9 | 
 10 | export function generateDirectoryListing(currentPath: string, files: string[]): string {
 11 |   const parentPath = path.dirname(currentPath || '/');
 12 |   const title = currentPath === '/' ? 'My Host Computer' : `Directory: ${encode(currentPath)}`;
 13 | 
 14 |   // Get file info and sort (directories first, then alphabetically)
 15 |   const items = files
 16 |     .map(name => {
 17 |       const fullPath = path.join(currentPath, name);
 18 |       let stats: fs.Stats;
 19 |       try {
 20 |         stats = fs.statSync(fullPath);
 21 |       } catch (error) {
 22 |         log(`FileServer: Failed to get stats for ${fullPath}: ${error}`);
 23 |         stats = new fs.Stats();
 24 |       }
 25 | 
 26 |       return {
 27 |         name,
 28 |         fullPath,
 29 |         stats
 30 |       } as FileEntry;
 31 |     })
 32 |     .filter(entry => entry.stats && !shouldHideFile(entry))
 33 |     .sort((a, b) => {
 34 |       if (a.stats.isDirectory() !== b.stats.isDirectory()) {
 35 |         return a.stats.isDirectory() ? -1 : 1;
 36 |       }
 37 |       return a.name.localeCompare(b.name);
 38 |     })
 39 |     .map(getFileLiHtml)
 40 |     .join('')
 41 | 
 42 |   // Generate very simple HTML that works in IE 5.5
 43 |   return `
 44 |     <html>
 45 |     <head>
 46 |       ${getEncoding()}
 47 |       <title>${title}</title>
 48 |     </head>
 49 |     <body>
 50 |       <h2>${title}</h2>
 51 |       <p>${getParentFolderLinkHtml(parentPath)} | ${getDesktopLinkHtml()} | ${getDownloadsLinkHtml()}</p>
 52 |       <p>
 53 |       <ul>
 54 |         ${items}
 55 |       </ul>
 56 |     </body>
 57 |     </html>
 58 |   `;
 59 | }
 60 | 
 61 | function getParentFolderLinkHtml(parentPath: string) {
 62 |   return `
 63 |     ${getIconHtml('folder.gif')}
 64 |     <a href="${MY_COMPUTER_INTERCEPT}${encodeURI(parentPath)}">
 65 |       [Parent Directory]
 66 |     </a>
 67 |   `;
 68 | }
 69 | 
 70 | function getDesktopLinkHtml() {
 71 |   const desktopPath = app.getPath('desktop');
 72 | 
 73 |   return `
 74 |     ${getIconHtml('desktop.gif')}
 75 |     <a href="${MY_COMPUTER_INTERCEPT}${encodeURI(desktopPath)}">
 76 |       Desktop
 77 |     </a>
 78 |   `;
 79 | }
 80 | 
 81 | function getDownloadsLinkHtml() {
 82 |   const downloadsPath = app.getPath('downloads');
 83 | 
 84 |   return `
 85 |     ${getIconHtml('network.gif')}
 86 |     <a href="${MY_COMPUTER_INTERCEPT}${encodeURI(downloadsPath)}">
 87 |       Downloads
 88 |     </a>
 89 |   `;
 90 | }
 91 | 
 92 | function getIconHtml(icon: string) {
 93 |   return `<img src="${APP_INTERCEPT}images/${icon}" style="vertical-align: middle; margin-right: 5px;" width="16" height="16">`;
 94 | }
 95 | 
 96 | function getFileLiHtml(entry: FileEntry) {
 97 |   const encodedPath = encodeURI(entry.fullPath);
 98 |   const sizeDisplay = entry.stats.isDirectory() ? '' : ` (${formatFileSize(entry.stats.size)})`;
 99 |   const icon = entry.stats.isDirectory() ? getIconHtml('folder.gif') : getIconHtml('doc.gif');
100 | 
101 |   return `<li>
102 |     ${icon}
103 |     <a href="${MY_COMPUTER_INTERCEPT}${encodedPath}">
104 |       ${getDisplayName(entry)}
105 |     </a>
106 |     ${sizeDisplay}
107 |   </li>`;
108 | }
109 | 
110 | function getDisplayName(entry: FileEntry) {
111 |   return encode(entry.stats.isDirectory() ? `[${entry.name}]` : entry.name);
112 | }
113 | 
114 | function formatFileSize(bytes: number): string {
115 |   if (bytes === 0) return '0 B';
116 |   const k = 1024;
117 |   const sizes = ['B', 'KB', 'MB', 'GB'];
118 |   const i = Math.floor(Math.log(bytes) / Math.log(k));
119 |   return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
120 | }
121 | 


--------------------------------------------------------------------------------
/src/main/fileserver/page-error.ts:
--------------------------------------------------------------------------------
 1 | import { getEncoding } from "./encoding";
 2 | import { MY_COMPUTER_INTERCEPT } from "./fileserver";
 3 | 
 4 | export function generateErrorPage(errorMessage: string, requestedPath: string): string {
 5 |   return `
 6 |     <html>
 7 |     <head>
 8 |       ${getEncoding()}
 9 |       <title>Error - File Not Found</title>
10 |     </head>
11 |     <body>
12 |       <h2>Error: ${errorMessage}</h2>
13 |       <p>windows95 failed to find the file or directory on your host computer: <code>${requestedPath}</code></p>
14 |       <p>Options:</p>
15 |       <ul>
16 |         <li><a href="${MY_COMPUTER_INTERCEPT}">Return to root directory</a></li>
17 |         <li><a href="javascript:history.back()">Go back to previous page</a></li>
18 |       </ul>
19 |     </body>
20 |     </html>
21 |   `;
22 | }
23 | 


--------------------------------------------------------------------------------
/src/main/ipc.ts:
--------------------------------------------------------------------------------
 1 | import { ipcMain, app } from "electron";
 2 | import * as path from "path";
 3 | 
 4 | import { IPC_COMMANDS } from "../constants";
 5 | 
 6 | export function setupIpcListeners() {
 7 |   ipcMain.handle(IPC_COMMANDS.GET_STATE_PATH, () => {
 8 |     return path.join(app.getPath("userData"), "state-v4.bin");
 9 |   });
10 | 
11 |   ipcMain.handle(IPC_COMMANDS.APP_QUIT, () => {
12 |     app.quit();
13 |   });
14 | }
15 | 


--------------------------------------------------------------------------------
/src/main/logging.ts:
--------------------------------------------------------------------------------
1 | export function log(message: string, ...args: unknown[]) {
2 |   console.log(`[${new Date().toLocaleString()}] ${message}`, ...args);
3 | }
4 | 


--------------------------------------------------------------------------------
/src/main/main.ts:
--------------------------------------------------------------------------------
 1 | import { app } from "electron";
 2 | 
 3 | import { isDevMode } from "../utils/devmode";
 4 | import { setupAboutPanel } from "./about-panel";
 5 | import { shouldQuit } from "./squirrel";
 6 | import { setupUpdates } from "./update";
 7 | import { getOrCreateWindow } from "./windows";
 8 | import { setupMenu } from "./menu";
 9 | import { setupIpcListeners } from "./ipc";
10 | import { setupSession } from "./session";
11 | import { setupFileServer } from './fileserver/fileserver';
12 | 
13 | /**
14 |  * Handle the app's "ready" event. This is essentially
15 |  * the method that takes care of booting the application.
16 |  */
17 | export async function onReady() {
18 |   if (!isDevMode()) process.env.NODE_ENV = "production";
19 | 
20 |   setupSession();
21 |   setupIpcListeners();
22 |   getOrCreateWindow();
23 |   setupAboutPanel();
24 |   setupMenu();
25 |   setupUpdates();
26 |   setupFileServer();
27 | }
28 | 
29 | /**
30 |  * Handle the "before-quit" event
31 |  *
32 |  * @export
33 |  */
34 | export function onBeforeQuit() {
35 |   (global as any).isQuitting = true;
36 | }
37 | 
38 | /**
39 |  * All windows have been closed, quit on anything but
40 |  * macOS.
41 |  */
42 | export function onWindowsAllClosed() {
43 |   // On OS X it is common for applications and their menu bar
44 |   // to stay active until the user quits explicitly with Cmd + Q
45 |   if (process.platform !== "darwin") {
46 |     app.quit();
47 |   }
48 | }
49 | 
50 | /**
51 |  * The main method - and the first function to run
52 |  * when Fiddle is launched.
53 |  *
54 |  * Exported for testing purposes.
55 |  */
56 | export function main() {
57 |   // Handle creating/removing shortcuts on Windows when
58 |   // installing/uninstalling.
59 |   if (shouldQuit()) {
60 |     app.quit();
61 |     return;
62 |   }
63 | 
64 |   // Set the app's name
65 |   app.setName("windows95");
66 | 
67 |   // Launch
68 |   app.on("ready", onReady);
69 |   app.on("before-quit", onBeforeQuit);
70 |   app.on("window-all-closed", onWindowsAllClosed);
71 | }
72 | 
73 | main();
74 | 


--------------------------------------------------------------------------------
/src/main/menu.ts:
--------------------------------------------------------------------------------
  1 | import { app, shell, Menu, BrowserWindow, ipcMain, dialog } from "electron";
  2 | 
  3 | import { clearCaches } from "../cache";
  4 | import { IPC_COMMANDS } from "../constants";
  5 | import { isDevMode } from "../utils/devmode";
  6 | import { log } from "./logging";
  7 | 
  8 | const LINKS = {
  9 |   homepage: "https://www.felixrieseberg.com",
 10 |   repo: "https://github.com/felixrieseberg/windows95",
 11 |   credits: "https://github.com/felixrieseberg/windows95/blob/master/CREDITS.md",
 12 |   help: "https://github.com/felixrieseberg/windows95/blob/master/HELP.md",
 13 | };
 14 | 
 15 | export async function setupMenu() {
 16 |   await createMenu();
 17 | 
 18 |   ipcMain.on(IPC_COMMANDS.MACHINE_STARTED, () =>
 19 |     createMenu({ isRunning: true }),
 20 |   );
 21 |   ipcMain.on(IPC_COMMANDS.MACHINE_STOPPED, () =>
 22 |     createMenu({ isRunning: false }),
 23 |   );
 24 | }
 25 | 
 26 | function send(cmd: string) {
 27 |   const windows = BrowserWindow.getAllWindows();
 28 | 
 29 |   if (windows[0]) {
 30 |     log(`Sending "${cmd}"`);
 31 |     windows[0].webContents.send(cmd);
 32 |   } else {
 33 |     log(`Tried to send "${cmd}", but could not find window`);
 34 |   }
 35 | }
 36 | 
 37 | async function createMenu({ isRunning } = { isRunning: false }) {
 38 |   const template: Array<Electron.MenuItemConstructorOptions> = [
 39 |     {
 40 |       label: "View",
 41 |       submenu: [
 42 |         {
 43 |           label: "Toggle Full Screen",
 44 |           accelerator: (function () {
 45 |             if (process.platform === "darwin") {
 46 |               return "Ctrl+Command+F";
 47 |             } else {
 48 |               return "F11";
 49 |             }
 50 |           })(),
 51 |           click: function (_item, focusedWindow) {
 52 |             if (focusedWindow) {
 53 |               focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
 54 |             }
 55 |           },
 56 |         },
 57 |         {
 58 |           label: "Toggle Developer Tools",
 59 |           accelerator: (function () {
 60 |             if (process.platform === "darwin") {
 61 |               return "Alt+Command+I";
 62 |             } else {
 63 |               return "Ctrl+Shift+I";
 64 |             }
 65 |           })(),
 66 |           click: function (_item, focusedWindow) {
 67 |             if (focusedWindow) {
 68 |               focusedWindow.webContents.toggleDevTools();
 69 |             }
 70 |           },
 71 |         },
 72 |         {
 73 |           type: "separator",
 74 |         },
 75 |         {
 76 |           label: "Toggle Emulator Info",
 77 |           click: () => send(IPC_COMMANDS.TOGGLE_INFO),
 78 |         },
 79 |         {
 80 |           type: "separator",
 81 |         },
 82 |         {
 83 |           role: "reload",
 84 |         },
 85 |       ],
 86 |     },
 87 |     {
 88 |       role: "editMenu",
 89 |       visible: isDevMode(),
 90 |     },
 91 |     {
 92 |       label: "Window",
 93 |       role: "window",
 94 |       submenu: [
 95 |         {
 96 |           label: "Minimize",
 97 |           accelerator: "CmdOrCtrl+M",
 98 |           role: "minimize",
 99 |         },
100 |         {
101 |           label: "Close",
102 |           accelerator: "CmdOrCtrl+W",
103 |           role: "close",
104 |         },
105 |         {
106 |           type: "separator",
107 |         },
108 |         {
109 |           label: "Zoom in",
110 |           click: () => send(IPC_COMMANDS.ZOOM_IN),
111 |           enabled: isRunning,
112 |         },
113 |         {
114 |           label: "Zoom out",
115 |           click: () => send(IPC_COMMANDS.ZOOM_OUT),
116 |           enabled: isRunning,
117 |         },
118 |         {
119 |           label: "Reset zoom",
120 |           click: () => send(IPC_COMMANDS.ZOOM_RESET),
121 |           enabled: isRunning,
122 |         },
123 |       ],
124 |     },
125 |     {
126 |       label: "Machine",
127 |       submenu: [
128 |         {
129 |           label: "Send Ctrl+Alt+Del",
130 |           click: () => send(IPC_COMMANDS.MACHINE_CTRL_ALT_DEL),
131 |           enabled: isRunning,
132 |         },
133 |         {
134 |           label: "Send Alt+F4",
135 |           click: () => send(IPC_COMMANDS.MACHINE_ALT_F4),
136 |           enabled: isRunning,
137 |         },
138 |         {
139 |           label: "Send Alt+Enter",
140 |           click: () => send(IPC_COMMANDS.MACHINE_ALT_ENTER),
141 |           enabled: isRunning,
142 |         },
143 |         {
144 |           label: "Send Esc",
145 |           click: () => send(IPC_COMMANDS.MACHINE_ESC),
146 |           enabled: isRunning,
147 |         },
148 |         {
149 |           type: "separator",
150 |         },
151 |         isRunning
152 |           ? {
153 |               label: "Stop",
154 |               click: () => send(IPC_COMMANDS.MACHINE_STOP),
155 |             }
156 |           : {
157 |               label: "Start",
158 |               click: () => send(IPC_COMMANDS.MACHINE_START),
159 |             },
160 |         {
161 |           label: "Restart",
162 |           click: () => send(IPC_COMMANDS.MACHINE_RESTART),
163 |           enabled: isRunning,
164 |         },
165 |         {
166 |           label: "Reset",
167 |           click: async () => {
168 |             const result = await dialog.showMessageBox({
169 |               type: 'warning',
170 |               buttons: ['Reset', 'Cancel'],
171 |               defaultId: 1,
172 |               title: 'Reset Machine',
173 |               message: 'Are you sure you want to reset the machine?',
174 |               detail: 'This will delete the machine state, including all changes you have made.',
175 |             });
176 | 
177 |             if (result.response === 0) {
178 |               send(IPC_COMMANDS.MACHINE_RESET);
179 |             }
180 |           },
181 |           enabled: isRunning,
182 |         },
183 |         {
184 |           type: "separator",
185 |         },
186 |         {
187 |           label: "Go to Disk Image",
188 |           click: () => send(IPC_COMMANDS.SHOW_DISK_IMAGE),
189 |         },
190 |       ],
191 |     },
192 |     {
193 |       label: "Help",
194 |       role: "help",
195 |       submenu: [
196 |         {
197 |           label: "Author",
198 |           click: () => shell.openExternal(LINKS.homepage),
199 |         },
200 |         {
201 |           label: "windows95 on GitHub",
202 |           click: () => shell.openExternal(LINKS.repo),
203 |         },
204 |         {
205 |           label: "Help",
206 |           click: () => shell.openExternal(LINKS.help),
207 |         },
208 |         {
209 |           type: "separator",
210 |         },
211 |         {
212 |           label: "Troubleshooting",
213 |           submenu: [
214 |             {
215 |               label: "Clear Cache and Restart",
216 |               async click() {
217 |                 await clearCaches();
218 | 
219 |                 app.relaunch();
220 |                 app.quit();
221 |               },
222 |             },
223 |           ],
224 |         },
225 |       ],
226 |     },
227 |   ];
228 | 
229 |   if (process.platform === "darwin") {
230 |     template.unshift({
231 |       label: "windows95",
232 |       submenu: [
233 |         {
234 |           role: "about",
235 |         },
236 |         {
237 |           type: "separator",
238 |         },
239 |         {
240 |           role: "services",
241 |         },
242 |         {
243 |           type: "separator",
244 |         },
245 |         {
246 |           label: "Hide windows95",
247 |           accelerator: "Command+H",
248 |           role: "hide",
249 |         },
250 |         {
251 |           label: "Hide Others",
252 |           accelerator: "Command+Shift+H",
253 |           role: "hideothers",
254 |         },
255 |         {
256 |           role: "unhide",
257 |         },
258 |         {
259 |           type: "separator",
260 |         },
261 |         {
262 |           label: "Quit",
263 |           accelerator: "Command+Q",
264 |           click() {
265 |             app.quit();
266 |           },
267 |         },
268 |       ],
269 |     } as any);
270 |   }
271 | 
272 |   Menu.setApplicationMenu(Menu.buildFromTemplate(template as any));
273 | }
274 | 


--------------------------------------------------------------------------------
/src/main/session.ts:
--------------------------------------------------------------------------------
 1 | import { session } from "electron";
 2 | 
 3 | export function setupSession() {
 4 |   const s = session.defaultSession;
 5 | 
 6 |   s.webRequest.onBeforeSendHeaders(
 7 |     (details, callback) => {
 8 |       callback({ requestHeaders: { Origin: '*', ...details.requestHeaders } });
 9 |     },
10 |   );
11 | 
12 |   s.webRequest.onHeadersReceived((details, callback) => {
13 |     callback({
14 |       responseHeaders: {
15 |         'Access-Control-Allow-Origin': ['*'],
16 |         ...details.responseHeaders,
17 |       },
18 |     });
19 |   });
20 | }
21 | 


--------------------------------------------------------------------------------
/src/main/settings.ts:
--------------------------------------------------------------------------------
 1 | import * as fs from 'fs';
 2 | import * as path from 'path';
 3 | import { app } from 'electron';
 4 | 
 5 | export interface Settings {
 6 |   isFileServerEnabled: boolean;
 7 |   isFileServerShowingHiddenFiles: boolean;
 8 |   isFileServerShowingSystemHiddenFiles: boolean;
 9 | }
10 | 
11 | const DEFAULT_SETTINGS: Settings = {
12 |   isFileServerEnabled: true,
13 |   isFileServerShowingHiddenFiles: false,
14 |   isFileServerShowingSystemHiddenFiles: false,
15 | };
16 | 
17 | class SettingsManager {
18 |   private filePath: string;
19 |   private data: Settings;
20 | 
21 |   constructor() {
22 |     this.filePath = path.join(app.getPath('userData'), 'settings.json');
23 |     this.data = this.load();
24 |   }
25 | 
26 |   private load(): Settings {
27 |     try {
28 |       if (fs.existsSync(this.filePath)) {
29 |         const fileContent = fs.readFileSync(this.filePath, 'utf8');
30 |         const parsed = JSON.parse(fileContent);
31 | 
32 |         return {
33 |           ...DEFAULT_SETTINGS,
34 |           ...parsed,
35 |         };
36 |       }
37 |     } catch (error) {
38 |       console.error('Error loading settings:', error);
39 |     }
40 | 
41 |     return DEFAULT_SETTINGS;
42 |   }
43 | 
44 |   private save(): void {
45 |     try {
46 |       fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
47 |     } catch (error) {
48 |       console.error('Error saving settings:', error);
49 |     }
50 |   }
51 | 
52 |   get(key: keyof Settings): any {
53 |     return this.data[key];
54 |   }
55 | 
56 |   set(key: keyof Settings, value: any): void {
57 |     this.data[key] = value;
58 |     this.save();
59 |   }
60 | 
61 |   delete(key: keyof Settings): void {
62 |     delete this.data[key];
63 |     this.save();
64 |   }
65 | 
66 |   clear(): void {
67 |     this.data = DEFAULT_SETTINGS;
68 |     this.save();
69 |   }
70 | }
71 | 
72 | export const settings = new SettingsManager();
73 | 


--------------------------------------------------------------------------------
/src/main/squirrel.ts:
--------------------------------------------------------------------------------
1 | export function shouldQuit() {
2 |   return require("electron-squirrel-startup");
3 | }
4 | 


--------------------------------------------------------------------------------
/src/main/update.ts:
--------------------------------------------------------------------------------
 1 | import { app } from "electron";
 2 | 
 3 | export function setupUpdates() {
 4 |   if (app.isPackaged) {
 5 |     require("update-electron-app")({
 6 |       repo: "felixrieseberg/windows95",
 7 |       updateInterval: "1 hour",
 8 |     });
 9 |   }
10 | }
11 | 


--------------------------------------------------------------------------------
/src/main/windows.ts:
--------------------------------------------------------------------------------
 1 | import { BrowserWindow, shell } from "electron";
 2 | 
 3 | let mainWindow: BrowserWindow | null = null;
 4 | 
 5 | export function getOrCreateWindow(): BrowserWindow {
 6 |   if (mainWindow) return mainWindow;
 7 | 
 8 |   // Create the browser window.
 9 |   mainWindow = new BrowserWindow({
10 |     width: 1024,
11 |     height: 768,
12 |     useContentSize: true,
13 |     webPreferences: {
14 |       nodeIntegration: true,
15 |       sandbox: false,
16 |       webviewTag: false,
17 |       contextIsolation: false,
18 |     },
19 |   });
20 | 
21 |   // mainWindow.webContents.toggleDevTools();
22 |   mainWindow.loadFile("./dist/static/index.html");
23 | 
24 |   mainWindow.webContents.on("will-navigate", (event, url) =>
25 |     handleNavigation(event, url),
26 |   );
27 | 
28 |   mainWindow.on("closed", () => {
29 |     mainWindow = null;
30 |   });
31 | 
32 |   return mainWindow;
33 | }
34 | 
35 | function handleNavigation(event: Electron.Event, url: string) {
36 |   if (url.startsWith("http")) {
37 |     event.preventDefault();
38 |     shell.openExternal(url);
39 |   }
40 | }
41 | 


--------------------------------------------------------------------------------
/src/renderer/app.tsx:
--------------------------------------------------------------------------------
 1 | export interface Win95Window extends Window {
 2 |   emulator: any;
 3 |   win95: {
 4 |     app: App;
 5 |   };
 6 | }
 7 | 
 8 | declare let window: Win95Window;
 9 | 
10 | /**
11 |  * The top-level class controlling the whole app. This is *not* a React component,
12 |  * but it does eventually render all components.
13 |  *
14 |  * @class App
15 |  */
16 | export class App {
17 |   /**
18 |    * Initial setup call, loading Monaco and kicking off the React
19 |    * render process.
20 |    */
21 |   public async setup(): Promise<void | Element> {
22 |     const React = await import("react");
23 |     const { render } = await import("react-dom");
24 |     const { Emulator } = await import("./emulator");
25 | 
26 |     const className = `${process.platform}`;
27 |     const app = (
28 |       <div className={className}>
29 |         <Emulator />
30 |       </div>
31 |     );
32 | 
33 |     const rendered = render(app, document.getElementById("app"));
34 | 
35 |     return rendered;
36 |   }
37 | }
38 | 
39 | window.win95 = window.win95 || {
40 |   app: new App(),
41 | };
42 | 
43 | window.win95.app.setup();
44 | 


--------------------------------------------------------------------------------
/src/renderer/card-settings.tsx:
--------------------------------------------------------------------------------
  1 | import * as React from "react";
  2 | 
  3 | import { resetState } from "./utils/reset-state";
  4 | 
  5 | interface CardSettingsProps {
  6 |   bootFromScratch: () => void;
  7 |   setFloppy: (file: File) => void;
  8 |   setCdrom: (cdrom: File) => void;
  9 |   floppy?: File;
 10 |   cdrom?: File;
 11 | }
 12 | 
 13 | interface CardSettingsState {
 14 |   isStateReset: boolean;
 15 | }
 16 | 
 17 | export class CardSettings extends React.Component<
 18 |   CardSettingsProps,
 19 |   CardSettingsState
 20 | > {
 21 |   constructor(props: CardSettingsProps) {
 22 |     super(props);
 23 | 
 24 |     this.onChangeFloppy = this.onChangeFloppy.bind(this);
 25 |     this.onChangeCdrom = this.onChangeCdrom.bind(this);
 26 |     this.onResetState = this.onResetState.bind(this);
 27 | 
 28 |     this.state = {
 29 |       isStateReset: false,
 30 |     };
 31 |   }
 32 | 
 33 |   public render() {
 34 |     return (
 35 |       <section>
 36 |         <div className="card settings">
 37 |           <div className="card-header">
 38 |             <h2 className="card-title">
 39 |               <img src="../../static/settings.png" />
 40 |               Settings
 41 |             </h2>
 42 |           </div>
 43 |           <div className="card-body">
 44 |             {this.renderCdrom()}
 45 |             <hr />
 46 |             {this.renderFloppy()}
 47 |             <hr />
 48 |             {this.renderState()}
 49 |           </div>
 50 |         </div>
 51 |       </section>
 52 |     );
 53 |   }
 54 | 
 55 |   public renderCdrom() {
 56 |     // CD is currently not working, so.. let's return nothing.
 57 |     return null;
 58 | 
 59 |     const { cdrom } = this.props;
 60 | 
 61 |     return (
 62 |       <fieldset>
 63 |         <legend>
 64 |           <img src="../../static/cdrom.png" />
 65 |           CD-ROM
 66 |         </legend>
 67 |         <input
 68 |           id="cdrom-input"
 69 |           type="file"
 70 |           onChange={this.onChangeCdrom}
 71 |           style={{ display: "none" }}
 72 |         />
 73 |         <p>
 74 |           windows95 comes with a virtual CD drive. It can mount images in the
 75 |           "iso" format.
 76 |         </p>
 77 |         <p id="floppy-path">
 78 |           {cdrom ? `Inserted CD: ${cdrom?.path}` : `No CD mounted`}
 79 |         </p>
 80 |         <button
 81 |           className="btn"
 82 |           onClick={() =>
 83 |             (document.querySelector("#cdrom-input") as any).click()
 84 |           }
 85 |         >
 86 |           <img src="../../static/select-cdrom.png" />
 87 |           <span>Mount CD</span>
 88 |         </button>
 89 |       </fieldset>
 90 |     );
 91 |   }
 92 | 
 93 |   public renderFloppy() {
 94 |     const { floppy } = this.props;
 95 | 
 96 |     return (
 97 |       <fieldset>
 98 |         <legend>
 99 |           <img src="../../static/floppy.png" />
100 |           Floppy
101 |         </legend>
102 |         <input
103 |           id="floppy-input"
104 |           type="file"
105 |           onChange={this.onChangeFloppy}
106 |           style={{ display: "none" }}
107 |         />
108 |         <p>
109 |           windows95 comes with a virtual floppy drive. It can mount floppy disk
110 |           images in the "img" format.
111 |         </p>
112 |         <p>
113 |           Back in the 90s and before CD-ROMs became a popular, software was
114 |           typically distributed on floppy disks. Some developers have since
115 |           released their apps or games for free, usually on virtual floppy disks
116 |           using the "img" format.
117 |         </p>
118 |         <p>
119 |           Once you've mounted a disk image, you might have to boot your virtual
120 |           windows95 machine from scratch.
121 |         </p>
122 |         <p id="floppy-path">
123 |           {floppy
124 |             ? `Inserted Floppy Disk: ${floppy.name}`
125 |             : `No floppy mounted`}
126 |         </p>
127 |         <button
128 |           className="btn"
129 |           onClick={() =>
130 |             (document.querySelector("#floppy-input") as any).click()
131 |           }
132 |         >
133 |           <img src="../../static/select-floppy.png" />
134 |           <span>Mount floppy disk</span>
135 |         </button>
136 |       </fieldset>
137 |     );
138 |   }
139 | 
140 |   public renderState() {
141 |     const { isStateReset } = this.state;
142 |     const { bootFromScratch } = this.props;
143 | 
144 |     return (
145 |       <fieldset>
146 |         <legend>
147 |           <img src="../../static/reset.png" />
148 |           Reset machine state
149 |         </legend>
150 |         <div>
151 |           <p>
152 |             windows95 stores changes to your machine (like saved files) in a
153 |             state file. If you encounter any trouble, you can reset your state
154 |             or boot Windows 95 from scratch.{" "}
155 |             <strong>All your changes will be lost.</strong>
156 |           </p>
157 |           <button
158 |             className="btn"
159 |             onClick={this.onResetState}
160 |             disabled={isStateReset}
161 |             style={{ marginRight: "5px" }}
162 |           >
163 |             <img src="../../static/reset-state.png" />
164 |             {isStateReset ? "State reset" : "Reset state"}
165 |           </button>
166 |           <button className="btn" onClick={bootFromScratch}>
167 |             <img src="../../static/boot-fresh.png" />
168 |             Boot from scratch
169 |           </button>
170 |         </div>
171 |       </fieldset>
172 |     );
173 |   }
174 | 
175 |   /**
176 |    * Handle a change in the floppy input
177 |    *
178 |    * @param event
179 |    */
180 |   private onChangeFloppy(event: React.ChangeEvent<HTMLInputElement>) {
181 |     const floppyFile =
182 |       event.target.files && event.target.files.length > 0
183 |         ? event.target.files[0]
184 |         : null;
185 | 
186 |     if (floppyFile) {
187 |       this.props.setFloppy(floppyFile);
188 |     } else {
189 |       console.log(`Floppy: Input changed but no file selected`);
190 |     }
191 |   }
192 | 
193 |   /**
194 |    * Handle a change in the cdrom input
195 |    *
196 |    * @param event
197 |    */
198 |   private onChangeCdrom(event: React.ChangeEvent<HTMLInputElement>) {
199 |     const CdromFile =
200 |       event.target.files && event.target.files.length > 0
201 |         ? event.target.files[0]
202 |         : null;
203 | 
204 |     if (CdromFile) {
205 |       this.props.setCdrom(CdromFile);
206 |     } else {
207 |       console.log(`Cdrom: Input changed but no file selected`);
208 |     }
209 |   }
210 | 
211 |   /**
212 |    * Handle the state reset
213 |    */
214 |   private async onResetState() {
215 |     await resetState();
216 |     this.setState({ isStateReset: true });
217 |   }
218 | }
219 | 


--------------------------------------------------------------------------------
/src/renderer/card-start.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | 
 3 | export interface CardStartProps {
 4 |   startEmulator: () => void;
 5 | }
 6 | 
 7 | export class CardStart extends React.Component<CardStartProps, {}> {
 8 |   public render() {
 9 |     return (
10 |       <section id="section-start">
11 |         <button className="btn" id="win95" onClick={this.props.startEmulator}>
12 |           <img src="../../static/run.png" />
13 |           <span>Start Windows 95</span>
14 |         </button>
15 |         <small>Hit ESC to lock or unlock your mouse</small>
16 |       </section>
17 |     );
18 |   }
19 | }
20 | 


--------------------------------------------------------------------------------
/src/renderer/emulator-info.tsx:
--------------------------------------------------------------------------------
  1 | import * as React from "react";
  2 | 
  3 | interface EmulatorInfoProps {
  4 |   toggleInfo: () => void;
  5 |   emulator: any;
  6 | }
  7 | 
  8 | interface EmulatorInfoState {
  9 |   cpu: number;
 10 |   disk: string;
 11 |   lastCounter: number;
 12 |   lastTick: number;
 13 | }
 14 | 
 15 | export class EmulatorInfo extends React.Component<
 16 |   EmulatorInfoProps,
 17 |   EmulatorInfoState
 18 | > {
 19 |   private cpuInterval = -1;
 20 | 
 21 |   constructor(props: EmulatorInfoProps) {
 22 |     super(props);
 23 | 
 24 |     this.cpuCount = this.cpuCount.bind(this);
 25 |     this.onIDEReadStart = this.onIDEReadStart.bind(this);
 26 |     this.onIDEReadWriteEnd = this.onIDEReadWriteEnd.bind(this);
 27 | 
 28 |     this.state = {
 29 |       cpu: 0,
 30 |       disk: "Idle",
 31 |       lastCounter: 0,
 32 |       lastTick: 0,
 33 |     };
 34 |   }
 35 | 
 36 |   public render() {
 37 |     const { cpu, disk } = this.state;
 38 | 
 39 |     return (
 40 |       <div id="status">
 41 |         Disk: <span>{disk}</span> | CPU Speed: <span>{cpu}</span> |{" "}
 42 |         <a href="#" onClick={this.props.toggleInfo}>
 43 |           Hide
 44 |         </a>
 45 |       </div>
 46 |     );
 47 |   }
 48 | 
 49 |   public componentWillUnmount() {
 50 |     this.uninstallListeners();
 51 |   }
 52 | 
 53 |   /**
 54 |    * The emulator starts whenever, so install or uninstall listeners
 55 |    * at the right time
 56 |    *
 57 |    * @param newProps
 58 |    */
 59 |   public componentDidUpdate(prevProps: EmulatorInfoProps) {
 60 |     if (prevProps.emulator !== this.props.emulator) {
 61 |       if (this.props.emulator) {
 62 |         this.installListeners();
 63 |       } else {
 64 |         this.uninstallListeners();
 65 |       }
 66 |     }
 67 |   }
 68 | 
 69 |   /**
 70 |    * Let's start listening to what the emulator is up to.
 71 |    */
 72 |   private installListeners() {
 73 |     const { emulator } = this.props;
 74 | 
 75 |     if (!emulator) {
 76 |       console.log(
 77 |         `Emulator info: Tried to install listeners, but emulator not defined yet.`,
 78 |       );
 79 |       return;
 80 |     }
 81 | 
 82 |     // CPU
 83 |     if (this.cpuInterval > -1) {
 84 |       clearInterval(this.cpuInterval);
 85 |     }
 86 | 
 87 |     // TypeScript think's we're using a Node.js setInterval. We're not.
 88 |     this.cpuInterval = setInterval(this.cpuCount, 500) as unknown as number;
 89 | 
 90 |     // Disk
 91 |     emulator.add_listener("ide-read-start", this.onIDEReadStart);
 92 |     emulator.add_listener("ide-read-end", this.onIDEReadWriteEnd);
 93 |     emulator.add_listener("ide-write-end", this.onIDEReadWriteEnd);
 94 | 
 95 |     // Screen
 96 |     emulator.add_listener("screen-set-size-graphical", console.log);
 97 |   }
 98 | 
 99 |   /**
100 |    * Stop listening to the emulator.
101 |    */
102 |   private uninstallListeners() {
103 |     const { emulator } = this.props;
104 | 
105 |     if (!emulator) {
106 |       console.log(
107 |         `Emulator info: Tried to uninstall listeners, but emulator not defined yet.`,
108 |       );
109 |       return;
110 |     }
111 | 
112 |     // CPU
113 |     if (this.cpuInterval > -1) {
114 |       clearInterval(this.cpuInterval);
115 |     }
116 | 
117 |     // Disk
118 |     emulator.remove_listener("ide-read-start", this.onIDEReadStart);
119 |     emulator.remove_listener("ide-read-end", this.onIDEReadWriteEnd);
120 |     emulator.remove_listener("ide-write-end", this.onIDEReadWriteEnd);
121 | 
122 |     // Screen
123 |     emulator.remove_listener("screen-set-size-graphical", console.log);
124 |   }
125 | 
126 |   /**
127 |    * The virtual IDE is handling read (start).
128 |    */
129 |   private onIDEReadStart() {
130 |     this.requestIdle(() => this.setState({ disk: "Read" }));
131 |   }
132 | 
133 |   /**
134 |    * The virtual IDE is handling read/write (end).
135 |    */
136 |   private onIDEReadWriteEnd() {
137 |     this.requestIdle(() => this.setState({ disk: "Idle" }));
138 |   }
139 | 
140 |   /**
141 |    * Request an idle callback with a 3s timeout.
142 |    *
143 |    * @param fn
144 |    */
145 |   private requestIdle(fn: () => void) {
146 |     (window as any).requestIdleCallback(fn, { timeout: 3000 });
147 |   }
148 | 
149 |   /**
150 |    * Calculates what's up with the virtual cpu.
151 |    */
152 |   private cpuCount() {
153 |     const { lastCounter, lastTick } = this.state;
154 | 
155 |     const now = Date.now();
156 |     const instructionCounter = this.props.emulator.get_instruction_counter();
157 |     const ips = instructionCounter - lastCounter;
158 |     const deltaTime = now - lastTick;
159 | 
160 |     this.setState({
161 |       lastTick: now,
162 |       lastCounter: instructionCounter,
163 |       cpu: Math.round(ips / deltaTime),
164 |     });
165 |   }
166 | }
167 | 


--------------------------------------------------------------------------------
/src/renderer/emulator.tsx:
--------------------------------------------------------------------------------
  1 | import * as React from "react";
  2 | import * as fs from "fs";
  3 | import * as path from "path";
  4 | import { ipcRenderer, shell, webUtils } from "electron";
  5 | 
  6 | import { CONSTANTS, IPC_COMMANDS } from "../constants";
  7 | import { getDiskImageSize } from "../utils/disk-image-size";
  8 | import { CardStart } from "./card-start";
  9 | import { StartMenu } from "./start-menu";
 10 | import { CardSettings } from "./card-settings";
 11 | import { EmulatorInfo } from "./emulator-info";
 12 | import { getStatePath } from "./utils/get-state-path";
 13 | import { Win95Window } from "./app";
 14 | import { resetState } from "./utils/reset-state";
 15 | 
 16 | declare let window: Win95Window;
 17 | 
 18 | export interface EmulatorState {
 19 |   currentUiCard: "start" | "settings";
 20 |   emulator?: any;
 21 |   scale: number;
 22 |   floppyFile?: File;
 23 |   cdromFile?: File;
 24 |   isBootingFresh: boolean;
 25 |   isCursorCaptured: boolean;
 26 |   isInfoDisplayed: boolean;
 27 |   isRunning: boolean;
 28 | }
 29 | 
 30 | export class Emulator extends React.Component<{}, EmulatorState> {
 31 |   private isQuitting = false;
 32 |   private isResetting = false;
 33 | 
 34 |   constructor(props: {}) {
 35 |     super(props);
 36 | 
 37 |     this.startEmulator = this.startEmulator.bind(this);
 38 |     this.stopEmulator = this.stopEmulator.bind(this);
 39 |     this.restartEmulator = this.restartEmulator.bind(this);
 40 |     this.resetEmulator = this.resetEmulator.bind(this);
 41 |     this.bootFromScratch = this.bootFromScratch.bind(this);
 42 | 
 43 |     this.state = {
 44 |       isBootingFresh: false,
 45 |       isCursorCaptured: false,
 46 |       isRunning: false,
 47 |       currentUiCard: "start",
 48 |       isInfoDisplayed: true,
 49 |       // We can start pretty large
 50 |       // If it's too large, it'll just grow until it hits borders
 51 |       scale: 2,
 52 |     };
 53 | 
 54 |     this.setupInputListeners();
 55 |     this.setupIpcListeners();
 56 |     this.setupUnloadListeners();
 57 |   }
 58 | 
 59 |   /**
 60 |    * We want to capture and release the mouse at appropriate times.
 61 |    */
 62 |   public setupInputListeners() {
 63 |     // ESC
 64 |     document.onkeydown = (evt) => {
 65 |       const { isCursorCaptured } = this.state;
 66 | 
 67 |       evt = evt || window.event;
 68 | 
 69 |       if (evt.keyCode === 27) {
 70 |         if (isCursorCaptured) {
 71 |           this.unlockMouse();
 72 |         } else {
 73 |           this.lockMouse();
 74 |         }
 75 | 
 76 |         evt.stopPropagation();
 77 |       }
 78 |     };
 79 | 
 80 |     // Click
 81 |     document.addEventListener("click", () => {
 82 |       const { isRunning } = this.state;
 83 | 
 84 |       if (isRunning) {
 85 |         this.lockMouse();
 86 |       }
 87 |     });
 88 |   }
 89 | 
 90 |   /**
 91 |    * Save the emulator's state to disk during exit.
 92 |    */
 93 |   public setupUnloadListeners() {
 94 |     const handleClose = async () => {
 95 |       await this.saveState();
 96 | 
 97 |       console.log(`Unload: Now done, quitting again.`);
 98 |       this.isQuitting = true;
 99 | 
100 |       setImmediate(() => {
101 |         ipcRenderer.invoke(IPC_COMMANDS.APP_QUIT);
102 |       });
103 |     };
104 | 
105 |     window.onbeforeunload = (event: Event) => {
106 |       if (this.isQuitting || this.isResetting) {
107 |         console.log(`Unload: Not preventing`);
108 |         return;
109 |       }
110 | 
111 |       console.log(`Unload: Preventing to first save state`);
112 | 
113 |       handleClose();
114 |       event.preventDefault();
115 |       event.returnValue = false;
116 |     };
117 |   }
118 | 
119 |   /**
120 |    * Setup the various IPC messages sent to the renderer
121 |    * from the main process
122 |    */
123 |   public setupIpcListeners() {
124 |     ipcRenderer.on(IPC_COMMANDS.MACHINE_CTRL_ALT_DEL, () => {
125 |       this.sendKeys([
126 |         0x1d, // ctrl
127 |         0x38, // alt
128 |         0x53, // delete
129 |       ]);
130 |     });
131 | 
132 |     ipcRenderer.on(IPC_COMMANDS.MACHINE_ALT_F4, () => {
133 |       this.sendKeys([
134 |         0x38, // alt
135 |         0x3e, // f4
136 |       ]);
137 |     });
138 | 
139 |     ipcRenderer.on(IPC_COMMANDS.MACHINE_ALT_ENTER, () => {
140 |       this.sendKeys([
141 |         0x38, // alt
142 |         0, // enter
143 |       ]);
144 |     });
145 | 
146 |     ipcRenderer.on(IPC_COMMANDS.MACHINE_ESC, () => {
147 |       this.sendKeys([
148 |         0x18, // alt
149 |       ]);
150 |     });
151 | 
152 |     ipcRenderer.on(IPC_COMMANDS.MACHINE_STOP, this.stopEmulator);
153 |     ipcRenderer.on(IPC_COMMANDS.MACHINE_RESET, this.resetEmulator);
154 |     ipcRenderer.on(IPC_COMMANDS.MACHINE_START, this.startEmulator);
155 |     ipcRenderer.on(IPC_COMMANDS.MACHINE_RESTART, this.restartEmulator);
156 | 
157 |     ipcRenderer.on(IPC_COMMANDS.TOGGLE_INFO, () => {
158 |       this.setState({ isInfoDisplayed: !this.state.isInfoDisplayed });
159 |     });
160 | 
161 |     ipcRenderer.on(IPC_COMMANDS.SHOW_DISK_IMAGE, () => {
162 |       this.showDiskImage();
163 |     });
164 | 
165 |     ipcRenderer.on(IPC_COMMANDS.ZOOM_IN, () => {
166 |       this.setScale(this.state.scale * 1.2);
167 |     });
168 | 
169 |     ipcRenderer.on(IPC_COMMANDS.ZOOM_OUT, () => {
170 |       this.setScale(this.state.scale * 0.8);
171 |     });
172 | 
173 |     ipcRenderer.on(IPC_COMMANDS.ZOOM_RESET, () => {
174 |       this.setScale(1);
175 |     });
176 |   }
177 | 
178 |   /**
179 |    * If the emulator isn't running, this is rendering the, erm, UI.
180 |    *
181 |    * 🤡
182 |    */
183 |   public renderUI() {
184 |     const { isRunning, currentUiCard, floppyFile, cdromFile } = this.state;
185 | 
186 |     if (isRunning) {
187 |       return null;
188 |     }
189 | 
190 |     let card;
191 | 
192 |     if (currentUiCard === "settings") {
193 |       card = (
194 |         <CardSettings
195 |           setFloppy={(floppyFile) => this.setState({ floppyFile })}
196 |           setCdrom={(cdromFile) => this.setState({ cdromFile })}
197 |           bootFromScratch={this.bootFromScratch}
198 |           floppy={floppyFile}
199 |           cdrom={cdromFile}
200 |         />
201 |       );
202 |     } else {
203 |       card = <CardStart startEmulator={this.startEmulator} />;
204 |     }
205 | 
206 |     return (
207 |       <>
208 |         {card}
209 |         <StartMenu
210 |           navigate={(target) => this.setState({ currentUiCard: target as "start" | "settings" })}
211 |         />
212 |       </>
213 |     );
214 |   }
215 | 
216 |   /**
217 |    * Yaknow, render things and stuff.
218 |    */
219 |   public render() {
220 |     return (
221 |       <>
222 |         {this.renderInfo()}
223 |         {this.renderUI()}
224 |         <div id="emulator">
225 |           <div id="emulator-text-screen"></div>
226 |           <canvas id="emulator-canvas"></canvas>
227 |         </div>
228 |       </>
229 |     );
230 |   }
231 | 
232 |   /**
233 |    * Render the little info thingy
234 |    */
235 |   public renderInfo() {
236 |     if (!this.state.isInfoDisplayed) {
237 |       return null;
238 |     }
239 | 
240 |     return (
241 |       <EmulatorInfo
242 |         emulator={this.state.emulator}
243 |         toggleInfo={() => {
244 |           this.setState({ isInfoDisplayed: !this.state.isInfoDisplayed });
245 |         }}
246 |       />
247 |     );
248 |   }
249 | 
250 |   /**
251 |    * Boot the emulator without restoring state
252 |    */
253 |   public bootFromScratch() {
254 |     this.setState({ isBootingFresh: true });
255 |     this.startEmulator();
256 |   }
257 | 
258 |   /**
259 |    * Show the disk image on disk
260 |    */
261 |   public showDiskImage() {
262 |     // Contents/Resources/app/dist/static
263 |     console.log(`Showing disk image in ${CONSTANTS.IMAGE_PATH}`);
264 | 
265 |     shell.showItemInFolder(CONSTANTS.IMAGE_PATH);
266 |   }
267 | 
268 |   /**
269 |    * Start the actual emulator
270 |    */
271 |   private async startEmulator() {
272 |     document.body.classList.remove("paused");
273 | 
274 |     const cdromPath = this.state.cdromFile
275 |       ? webUtils.getPathForFile(this.state.cdromFile)
276 |       : null;
277 | 
278 |     const options = {
279 |       wasm_path: path.join(__dirname, "build/v86.wasm"),
280 |       memory_size: 128 * 1024 * 1024,
281 |       vga_memory_size: 64 * 1024 * 1024,
282 |       screen: {
283 |         container: document.getElementById("emulator"),
284 |         scale: 0
285 |       },
286 |       preserve_mac_from_state_image: true,
287 |       net_device: {
288 |         relay_url: "fetch",
289 |         type: "ne2k",
290 |       },
291 |       bios: {
292 |         url: path.join(__dirname, "../../bios/seabios.bin"),
293 |       },
294 |       vga_bios: {
295 |         url: path.join(__dirname, "../../bios/vgabios.bin"),
296 |       },
297 |       hda: {
298 |         url: CONSTANTS.IMAGE_PATH,
299 |         async: true,
300 |         size: await getDiskImageSize(CONSTANTS.IMAGE_PATH),
301 |       },
302 |       fda: this.state.floppyFile
303 |         ? {
304 |             buffer: this.state.floppyFile,
305 |           }
306 |         : undefined,
307 |       cdrom: cdromPath
308 |         ? {
309 |             url: cdromPath,
310 |             async: true,
311 |             size: await getDiskImageSize(cdromPath),
312 |           }
313 |         : undefined,
314 |       boot_order: 0x132,
315 |     };
316 | 
317 |     console.log(`🚜 Starting emulator with options`, options);
318 | 
319 |     window["emulator"] = new V86(options);
320 | 
321 |     // New v86 instance
322 |     this.setState({
323 |       emulator: window["emulator"],
324 |       isRunning: true,
325 |     });
326 | 
327 |     ipcRenderer.send(IPC_COMMANDS.MACHINE_STARTED);
328 | 
329 |     // Restore state. We can't do this right away
330 |     // and randomly chose 500ms as the appropriate
331 |     // wait time (lol)
332 |     setTimeout(async () => {
333 |       if (!this.state.isBootingFresh) {
334 |         this.restoreState();
335 |       }
336 | 
337 |       this.lockMouse();
338 |       this.state.emulator.run();
339 |       this.state.emulator.screen_set_scale(this.state.scale);
340 |     }, 500);
341 |   }
342 | 
343 |   /**
344 |    * Restart emulator
345 |    */
346 |   private restartEmulator() {
347 |     if (this.state.emulator && this.state.isRunning) {
348 |       console.log(`🚜 Restarting emulator`);
349 |       this.state.emulator.restart();
350 |     } else {
351 |       console.log(`🚜 Restarting emulator failed: Emulator not running`);
352 |     }
353 |   }
354 | 
355 |   /**
356 |    * Stop the emulator
357 |    */
358 |   private async stopEmulator() {
359 |     const { emulator, isRunning } = this.state;
360 | 
361 |     if (!emulator || !isRunning) {
362 |       return;
363 |     }
364 | 
365 |     console.log(`🚜 Stopping emulator`);
366 | 
367 |     await this.saveState();
368 |     this.unlockMouse();
369 |     await emulator.stop();
370 |     this.setState({ isRunning: false });
371 |     this.resetCanvas();
372 | 
373 |     document.body.classList.add("paused");
374 |     ipcRenderer.send(IPC_COMMANDS.MACHINE_STOPPED);
375 |   }
376 | 
377 |   /**
378 |    * Reset the emulator by reloading the whole page
379 |    */
380 |   private async resetEmulator() {
381 |     this.isResetting = true;
382 | 
383 |     await this.stopEmulator();
384 |     await resetState();
385 | 
386 |     document.location.reload();
387 |   }
388 | 
389 |   /**
390 |    * Take the emulators state and write it to disk. This is possibly
391 |    * a fairly big file.
392 |    */
393 |   private async saveState(): Promise<void> {
394 |     const { emulator } = this.state;
395 |     const statePath = await getStatePath();
396 | 
397 |     if (!emulator || !emulator.save_state) {
398 |       console.log(`restoreState: No emulator present`);
399 |       return;
400 |     }
401 | 
402 |     try {
403 |       const newState = await emulator.save_state();
404 |       await fs.promises.writeFile(statePath, Buffer.from(newState), {
405 |         flush: true
406 |       });
407 |     } catch (error) {
408 |       console.warn(`saveState: Could not save state`, error);
409 |     }
410 |   }
411 | 
412 |   /**
413 |    * Restores state to the emulator.
414 |    */
415 |   private async restoreState() {
416 |     const { emulator, isBootingFresh } = this.state;
417 |     const state = await this.getState();
418 | 
419 |     if (isBootingFresh) {
420 |       console.log(`restoreState: Booting fresh, not restoring.`);
421 |       return;
422 |     } else if (!state) {
423 |       console.log(`restoreState: No state present, not restoring.`);
424 |       return;
425 |     } else if (!emulator) {
426 |       console.log(`restoreState: No emulator present`);
427 |       return;
428 |     }
429 | 
430 |     try {
431 |       await this.state.emulator.restore_state(state);
432 |     } catch (error) {
433 |       console.log(
434 |         `restoreState: Could not read state file. Maybe none exists?`,
435 |         error,
436 |       );
437 |     }
438 |   }
439 | 
440 |   /**
441 |    * Returns the current machine's state - either what
442 |    * we have saved or alternatively the default state.
443 |    *
444 |    * @returns {ArrayBuffer}
445 |    */
446 |   private async getState(): Promise<ArrayBuffer | null> {
447 |     const expectedStatePath = await getStatePath();
448 |     const statePath = fs.existsSync(expectedStatePath)
449 |       ? expectedStatePath
450 |       : CONSTANTS.DEFAULT_STATE_PATH;
451 | 
452 |     if (fs.existsSync(statePath)) {
453 |       return fs.readFileSync(statePath).buffer;
454 |     } else {
455 |       console.log(`getState: No state file found at ${statePath}`);
456 |     }
457 | 
458 |     return null;
459 |   }
460 | 
461 |   private unlockMouse() {
462 |     const { emulator } = this.state;
463 | 
464 |     this.setState({ isCursorCaptured: false });
465 | 
466 |     if (emulator) {
467 |       emulator.mouse_set_status(false);
468 |     }
469 | 
470 |     document.exitPointerLock();
471 |   }
472 | 
473 |   private lockMouse() {
474 |     const { emulator } = this.state;
475 | 
476 |     if (emulator) {
477 |       this.setState({ isCursorCaptured: true });
478 |       emulator.mouse_set_status(true);
479 |       emulator.lock_mouse();
480 |     } else {
481 |       console.warn(
482 |         `Emulator: Tried to lock mouse, but no emulator or not running`,
483 |       );
484 |     }
485 |   }
486 | 
487 |   /**
488 |    * Set the emulator's scale
489 |    *
490 |    * @param target
491 |    */
492 |   private setScale(target: number) {
493 |     const { emulator, isRunning } = this.state;
494 | 
495 |     if (emulator && isRunning) {
496 |       emulator.screen_set_scale(target);
497 |       this.setState({ scale: target });
498 |     }
499 |   }
500 | 
501 |   /**
502 |    * Send keys to the emulator (including the key-up),
503 |    * if it's running
504 |    *
505 |    * @param {Array<number>} codes
506 |    */
507 |   private sendKeys(codes: Array<number>) {
508 |     if (this.state.emulator && this.state.isRunning) {
509 |       const scancodes = codes;
510 | 
511 |       // Push break codes (key-up)
512 |       for (const scancode of scancodes) {
513 |         scancodes.push(scancode | 0x80);
514 |       }
515 | 
516 |       this.state.emulator.keyboard_send_scancodes(scancodes);
517 |     }
518 |   }
519 | 
520 |   /**
521 |    * Reset the canvas
522 |    */
523 |   private resetCanvas() {
524 |     const canvas = document.getElementById("emulator-canvas");
525 | 
526 |     if (canvas instanceof HTMLCanvasElement) {
527 |       const ctx = canvas.getContext('2d');
528 |       ctx?.clearRect(0, 0, canvas.width, canvas.height);
529 |     }
530 |   }
531 | }
532 | 


--------------------------------------------------------------------------------
/src/renderer/global.d.ts:
--------------------------------------------------------------------------------
1 | declare const V86: any;
2 | declare const win95: any;
3 | 


--------------------------------------------------------------------------------
/src/renderer/lib/LICENSE.md:
--------------------------------------------------------------------------------
 1 | Copyright (c) 2012, The v86 contributors
 2 | All rights reserved.
 3 | 
 4 | Redistribution and use in source and binary forms, with or without
 5 | modification, are permitted provided that the following conditions are met:
 6 | 
 7 | 1. Redistributions of source code must retain the above copyright notice, this
 8 |    list of conditions and the following disclaimer.
 9 | 2. Redistributions in binary form must reproduce the above copyright notice,
10 |    this list of conditions and the following disclaimer in the documentation
11 |    and/or other materials provided with the distribution.
12 | 
13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


--------------------------------------------------------------------------------
/src/renderer/lib/build/v86.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/src/renderer/lib/build/v86.wasm


--------------------------------------------------------------------------------
/src/renderer/start-menu.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | 
 3 | export interface StartMenuProps {
 4 |   navigate: (to: string) => void;
 5 | }
 6 | 
 7 | export class StartMenu extends React.Component<StartMenuProps, {}> {
 8 |   constructor(props: StartMenuProps) {
 9 |     super(props);
10 | 
11 |     this.navigate = this.navigate.bind(this);
12 |   }
13 | 
14 |   public render() {
15 |     return (
16 |       <nav className="nav nav-bottom">
17 |         <a onClick={this.navigate} href="#" id="start" className="nav-link">
18 |           <img src="../../static/start.png" alt="Start" />
19 |           <span>Start</span>
20 |         </a>
21 |         <div className="nav-menu">
22 |           <a
23 |             onClick={this.navigate}
24 |             href="#"
25 |             id="settings"
26 |             className="nav-link"
27 |           >
28 |             <img src="../../static/settings.png" />
29 |             <span>Settings</span>
30 |           </a>
31 |         </div>
32 |       </nav>
33 |     );
34 |   }
35 | 
36 |   private navigate(event: React.SyntheticEvent<HTMLAnchorElement>) {
37 |     this.props.navigate(event.currentTarget.id);
38 |   }
39 | }
40 | 


--------------------------------------------------------------------------------
/src/renderer/status.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/src/renderer/status.tsx


--------------------------------------------------------------------------------
/src/renderer/utils/get-state-path.ts:
--------------------------------------------------------------------------------
 1 | import { ipcRenderer } from "electron";
 2 | import { IPC_COMMANDS } from "../../constants";
 3 | 
 4 | let _statePath = "";
 5 | 
 6 | export async function getStatePath(): Promise<string> {
 7 |   if (_statePath) {
 8 |     return _statePath;
 9 |   }
10 | 
11 |   const statePath = await ipcRenderer.invoke(IPC_COMMANDS.GET_STATE_PATH);
12 |   return (_statePath = statePath);
13 | }
14 | 


--------------------------------------------------------------------------------
/src/renderer/utils/reset-state.ts:
--------------------------------------------------------------------------------
 1 | import fs from "fs";
 2 | import { getStatePath } from "./get-state-path";
 3 | 
 4 | export async function resetState() {
 5 |   const statePath = await getStatePath();
 6 | 
 7 |   if (fs.existsSync(statePath)) {
 8 |     try {
 9 |       await fs.promises.unlink(statePath);
10 |     } catch (error) {
11 |       console.error(`Failed to delete state file: ${error}`);
12 |     }
13 |   }
14 | }
15 | 


--------------------------------------------------------------------------------
/src/utils/devmode.ts:
--------------------------------------------------------------------------------
1 | /**
2 |  * Are we currently running in development mode?
3 |  *
4 |  * @returns {boolean}
5 |  */
6 | export function isDevMode() {
7 |   return !!process.defaultApp;
8 | }
9 | 


--------------------------------------------------------------------------------
/src/utils/disk-image-size.ts:
--------------------------------------------------------------------------------
 1 | import * as fs from "fs";
 2 | 
 3 | import { CONSTANTS } from "../constants";
 4 | 
 5 | /**
 6 |  * Get the size of the disk image
 7 |  *
 8 |  * @returns {number}
 9 |  */
10 | export async function getDiskImageSize(path: string) {
11 |   try {
12 |     const stats = await fs.promises.stat(path);
13 | 
14 |     if (stats) {
15 |       return stats.size;
16 |     }
17 |   } catch (error) {
18 |     console.warn(`Could not determine image size`, error);
19 |   }
20 | 
21 |   return CONSTANTS.IMAGE_DEFAULT_SIZE;
22 | }
23 | 


--------------------------------------------------------------------------------
/static/boot-fresh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/boot-fresh.png


--------------------------------------------------------------------------------
/static/cdrom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/cdrom.png


--------------------------------------------------------------------------------
/static/entitlements.plist:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 3 | <plist version="1.0">
 4 |   <dict>
 5 |     <key>com.apple.security.cs.allow-jit</key>
 6 |     <true/>
 7 |     <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
 8 |     <true/>
 9 |   </dict>
10 | </plist>


--------------------------------------------------------------------------------
/static/floppy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/floppy.png


--------------------------------------------------------------------------------
/static/index.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html>
 3 | <head>
 4 |   <meta charset="utf-8" />
 5 |   <meta http-equiv="X-UA-Compatible" content="IE=edge">
 6 |   <title>windows95</title>
 7 |   <meta name="viewport" content="width=device-width, initial-scale=1">
 8 |   <link rel="stylesheet" href="../src/less/vendor/95css.css">
 9 |   <link rel="stylesheet" href="../src/less/root.less">
10 |   <!-- libv86 -->
11 | </head>
12 | <body class="paused windows95">
13 |   <div id="app"></div>
14 |   <script src="../src/renderer/app.tsx"></script>
15 | </body>
16 | </html>


--------------------------------------------------------------------------------
/static/reset-state.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/reset-state.png


--------------------------------------------------------------------------------
/static/reset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/reset.png


--------------------------------------------------------------------------------
/static/run.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/run.png


--------------------------------------------------------------------------------
/static/select-cdrom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/select-cdrom.png


--------------------------------------------------------------------------------
/static/select-floppy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/select-floppy.png


--------------------------------------------------------------------------------
/static/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/settings.png


--------------------------------------------------------------------------------
/static/show-disk-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/show-disk-image.png


--------------------------------------------------------------------------------
/static/start.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/start.png


--------------------------------------------------------------------------------
/static/www/apps.htm:
--------------------------------------------------------------------------------
 1 | <html>
 2 | <head>
 3 |   <title>windows95 Help</title>
 4 | </head>
 5 | <body bgcolor="#C0C0C0">
 6 |   <table width="100%" cellpadding="10" cellspacing="0">
 7 |     <tr>
 8 |       <td>
 9 |         <font face="Arial" color="#000000">
10 |           <font size="5"><b>windows95 Apps & Games</b></font>
11 |           <hr>
12 | 
13 |           <p>I've installed a few apps and games for you to try out. Check out the Games folder on the desktop!</p>
14 |           <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>
15 |         </font>
16 |       </td>
17 |     </tr>
18 |   </table>
19 | </body>
20 | </html>
21 | 


--------------------------------------------------------------------------------
/static/www/buttons/macos.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/www/buttons/macos.gif


--------------------------------------------------------------------------------
/static/www/buttons/madewithelectron.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/www/buttons/madewithelectron.gif


--------------------------------------------------------------------------------
/static/www/buttons/msie.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/www/buttons/msie.gif


--------------------------------------------------------------------------------
/static/www/credits.htm:
--------------------------------------------------------------------------------
 1 | <html>
 2 | <head>
 3 |   <title>Windows 95 Credits</title>
 4 | </head>
 5 | <body bgcolor="#C0C0C0">
 6 |   <table width="100%" cellpadding="10" cellspacing="0">
 7 |     <tr>
 8 |       <td>
 9 |         <font face="Arial" color="#000000">
10 |           <font size="5"><b>windows95 Credits</b></font>
11 |           <hr>
12 | 
13 |           <h3>Emulation Engine</h3>
14 |           <p>
15 |             None of this would be possible without the people working on v86, in particular Fabian Hemmer aka copy.
16 |             You can visit his website at <a href="http://copy.sh" target="_blank">copy.sh</a>. It also wouldn't be
17 |             possible without the QEMU project. If you enjoy running old systems, you probably want QEMU - windows95
18 |             is merely a toy, QEMU lets you actually run old systems in a stable manner.
19 |           </p>
20 | 
21 |           <h3>Electron</h3>
22 |           <p>
23 |             Electron is a framework for building desktop applications using web technologies. It's what powers windows95.
24 |             You can visit the project's website at electronjs.org (in a modern browser, not in windows95).
25 |           </p>
26 |         </font>
27 |       </td>
28 |     </tr>
29 |   </table>
30 | </body>
31 | </html>
32 | 


--------------------------------------------------------------------------------
/static/www/help.htm:
--------------------------------------------------------------------------------
 1 | <html>
 2 | <head>
 3 |   <title>windows95 Help</title>
 4 | </head>
 5 | <body bgcolor="#C0C0C0">
 6 |   <table width="100%" cellpadding="10" cellspacing="0">
 7 |     <tr>
 8 |       <td>
 9 |         <font face="Arial" color="#000000">
10 |           <font size="5"><b>windows95 Help</b></font>
11 |           <hr>
12 | 
13 |           <h3>MS-DOS Display Issues</h3>
14 |           <p>If MS-DOS seems to mess up the screen:</p>
15 |           <ol>
16 |             <li>Hit <code>Alt + Enter</code> to make the command screen "Full Screen" (as far as Windows 95 is concerned)</li>
17 |             <li>This should restore the display from the garbled mess you see and allow you to access the Command Prompt</li>
18 |             <li>Press Alt-Enter again to leave Full Screen and go back to Window Mode</li>
19 |           </ol>
20 |           <p><i>(Thanks to @DisplacedGamers for that wisdom)</i></p>
21 | 
22 |           <h3>windows95 Stuck in Bad State</h3>
23 |           <p>If windows95 becomes unresponsive or stuck:</p>
24 |           <ol>
25 |             <li>On the app's home screen, select "Settings" in the lower menu</li>
26 |             <li>Delete your machine's state before starting it again</li>
27 |             <li>This should resolve the issue when you restart</li>
28 |           </ol>
29 |         </font>
30 |       </td>
31 |     </tr>
32 |   </table>
33 | </body>
34 | </html>
35 | 


--------------------------------------------------------------------------------
/static/www/home.htm:
--------------------------------------------------------------------------------
 1 | <html>
 2 | <head>
 3 |   <title>Welcome to Windows 95!</title>
 4 | </head>
 5 | <body bgcolor="#C0C0C0">
 6 |   <table width="100%" cellpadding="10" cellspacing="0">
 7 |     <tr>
 8 |       <td>
 9 |         <center>
10 |           <marquee scrollamount="3">
11 |             <font face="Arial" size="6" color="#000000">
12 |               <blink>Welcome to Windows 95!</blink>
13 |             </font>
14 |           </marquee>
15 |         </center>
16 | 
17 |         <font face="Arial" color="#000000">
18 |           <p>Hi, I'm Felix, the maker behind windows95. I hope you're having fun!</p>
19 | 
20 |           <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>
21 | 
22 |           <hr width="75%">
23 |           <a name="internet"></a>
24 |           <font size="5" color="#000000"><img src="images/ie.gif" width="16" height="16" border="0" align="absmiddle"> <b>The Internet!</b></font>
25 |           <hr width="75%">
26 | 
27 |           <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>
28 |         </font>
29 | 
30 |         <center>
31 |           <font size="2" color="#000000">Last updated: 2025</font>
32 |         </center>
33 |       </td>
34 |     </tr>
35 |   </table>
36 | </body>
37 | </html>
38 | 


--------------------------------------------------------------------------------
/static/www/images/bg.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/www/images/bg.gif


--------------------------------------------------------------------------------
/static/www/images/desktop.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/www/images/desktop.gif


--------------------------------------------------------------------------------
/static/www/images/doc.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/www/images/doc.gif


--------------------------------------------------------------------------------
/static/www/images/folder.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/www/images/folder.gif


--------------------------------------------------------------------------------
/static/www/images/help.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/www/images/help.gif


--------------------------------------------------------------------------------
/static/www/images/ie.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/www/images/ie.gif


--------------------------------------------------------------------------------
/static/www/images/network.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/www/images/network.gif


--------------------------------------------------------------------------------
/static/www/images/programs.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/www/images/programs.gif


--------------------------------------------------------------------------------
/static/www/index.htm:
--------------------------------------------------------------------------------
 1 | <html>
 2 | 
 3 | <head>
 4 |   <title>Welcome to Windows 95!</title>
 5 | </head>
 6 | 
 7 | <frameset cols="200,*" border="0" framespacing="0" frameborder="NO">
 8 |   <frame src="navigation.htm" name="nav" scrolling="auto" noresize>
 9 |   <frame src="home.htm" name="main" scrolling="auto" noresize>
10 |   <noframes>
11 |     <body bgcolor="#000080">
12 |       <font face="Arial" color="#FFFFFF">
13 |         <h2>Frame Alert!</h2>
14 |         <p>This page uses frames, but your browser doesn't support them.</p>
15 |         <p>Please upgrade to Netscape Navigator 2.0 or Internet Explorer 3.0!</p>
16 |       </font>
17 |     </body>
18 |   </noframes>
19 | </frameset>
20 | 
21 | </html>
22 | 


--------------------------------------------------------------------------------
/static/www/navigation.htm:
--------------------------------------------------------------------------------
 1 | <html>
 2 | <head>
 3 |   <title>Navigation</title>
 4 | </head>
 5 | <body bgcolor="#C0C0C0" background="images/bg.gif">
 6 |   <table width="100%" cellpadding="4" cellspacing="1" bgcolor="#000000">
 7 |     <tr><td bgcolor="#C0C0C0">
 8 |       <font face="Arial" size="2" color="#000000">
 9 |         <img src="images/folder.gif" width="16" height="16" border="0" align="absmiddle"> <b>Navigation</b>
10 |       </font>
11 |     </td></tr>
12 |     <tr><td bgcolor="#C0C0C0">
13 |       <font face="Arial" size="2" color="#000000">
14 |         <img src="images/desktop.gif" width="16" height="16" border="0" align="absmiddle"> <a href="home.htm" target="main">Home</a>
15 |       </font>
16 |     </td></tr>
17 |     <tr><td bgcolor="#C0C0C0">
18 |       <font face="Arial" size="2" color="#000000">
19 |         <img src="images/programs.gif" width="16" height="16" border="0" align="absmiddle"> <a href="apps.htm" target="main">Apps & Games</a>
20 |       </font>
21 |     </td></tr>
22 |     <tr><td bgcolor="#C0C0C0">
23 |       <font face="Arial" size="2" color="#000000">
24 |         <img src="images/help.gif" width="16" height="16" border="0" align="absmiddle"> <a href="help.htm" target="main">Help</a>
25 |       </font>
26 |     </td></tr>
27 |     <tr><td bgcolor="#C0C0C0">
28 |       <font face="Arial" size="2" color="#000000">
29 |         <img src="images/doc.gif" width="16" height="16" border="0" align="absmiddle"> <a href="credits.htm" target="main">Credits</a>
30 |       </font>
31 |     </td></tr>
32 |   </table>
33 |   <br>
34 |   <center>
35 |     <p>
36 |       <font face="Arial" size="1" color="#000000">
37 |         Best viewed with<br>
38 |         Internet Explorer 5.5 and windows95
39 |       </font>
40 |     </p>
41 |     <img src="buttons/madewithelectron.gif" width="88" height="31" border="0" align="absmiddle">
42 |     <img src="buttons/macos.gif" width="88" height="31" border="0" align="absmiddle">
43 |     <img src="buttons/msie.gif" width="88" height="31" border="0" align="absmiddle">
44 |   </center>
45 | </body>
46 | </html>
47 | 


--------------------------------------------------------------------------------
/tools/add-macos-cert.sh:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env sh
 2 | 
 3 | KEY_CHAIN=build.keychain
 4 | MACOS_CERT_P12_FILE=certificate.p12
 5 | 
 6 | # Recreate the certificate from the secure environment variable
 7 | echo $MACOS_CERT_P12 | base64 --decode > $MACOS_CERT_P12_FILE
 8 | 
 9 | #create a keychain
10 | security create-keychain -p actions $KEY_CHAIN
11 | 
12 | # Make the keychain the default so identities are found
13 | security default-keychain -s $KEY_CHAIN
14 | 
15 | # Unlock the keychain
16 | security unlock-keychain -p actions $KEY_CHAIN
17 | 
18 | security import $MACOS_CERT_P12_FILE -k $KEY_CHAIN -P $MACOS_CERT_PASSWORD -T /usr/bin/codesign;
19 | 
20 | security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN
21 | 
22 | # remove certs
23 | rm -fr *.p12
24 | 


--------------------------------------------------------------------------------
/tools/check-links.js:
--------------------------------------------------------------------------------
 1 | const fs = require('fs/promises')
 2 | const path = require('path')
 3 | const fetch = require('node-fetch')
 4 | 
 5 | const LINK_RGX = /(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?/g;
 6 | 
 7 | async function main() {
 8 |   const readmePath = path.join(__dirname, '../README.md')
 9 |   const readme = await fs.readFile(readmePath, 'utf-8')
10 |   const links = readme.match(LINK_RGX)
11 |   let failed = false
12 | 
13 |   for (const link of links) {
14 |     try {
15 |       const response = await fetch(link, { method: 'HEAD' })
16 | 
17 |       if (!response.ok) {
18 |         // If we're inside GitHub's release asset server, we just ran into AWS not allowing
19 |         // HEAD requests, which is different from a 404.
20 |         if (!response.url.startsWith('https://github-production-release-asset')) {
21 |           throw new Error (`HTTP Error Response: ${response.status} ${response.statusText}`)
22 |         }
23 |       }
24 | 
25 |       console.log(`✅ ${link}`);
26 |     } catch (error) {
27 |       failed = true
28 | 
29 |       console.log(`❌ ${link}\n${error}`)
30 |     }
31 |   }
32 | 
33 |   if (failed) {
34 |     process.exit(-1);
35 |   }
36 | }
37 | 
38 | main()
39 | 


--------------------------------------------------------------------------------
/tools/download-disk.ps1:
--------------------------------------------------------------------------------
 1 | mkdir images
 2 | cd images
 3 | 
 4 | $wc = New-Object System.Net.WebClient
 5 | $wc.DownloadFile($env:DISK_URL, "$(Resolve-Path .)\images.zip")
 6 | 
 7 | 7z x images.zip -y -aoa
 8 | Remove-Item images.zip
 9 | Remove-Item __MACOSX -Recurse -ErrorAction Ignore
10 | cd ..
11 | Tree ./ /F
12 | 


--------------------------------------------------------------------------------
/tools/download-disk.sh:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env sh
 2 | 
 3 | mkdir -p ./images
 4 | cd ./images
 5 | wget -O images.zip $DISK_URL
 6 | unzip -o images.zip
 7 | rm images.zip
 8 | rm -r __MACOSX
 9 | cd -
10 | ls images
11 | 


--------------------------------------------------------------------------------
/tools/generateAssets.js:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | 
3 | const { compileParcel } = require('./parcel-build')
4 | 
5 | module.exports = async () => {
6 |   await Promise.all([compileParcel()])
7 | }
8 | 


--------------------------------------------------------------------------------
/tools/parcel-build.js:
--------------------------------------------------------------------------------
 1 | /* tslint:disable */
 2 | 
 3 | const Bundler = require('parcel-bundler')
 4 | const path = require('path')
 5 | const fs = require('fs')
 6 | 
 7 | async function copyLib() {
 8 |   const target = path.join(__dirname, '../dist/static')
 9 |   const lib = path.join(__dirname, '../src/renderer/lib')
10 |   const index = path.join(target, 'index.html')
11 | 
12 |   // Copy in lib
13 |   await fs.promises.cp(lib, target, { recursive: true });
14 | 
15 |   // Patch so that fs.read is used
16 |   const libv86path = path.join(target, 'libv86.js')
17 |   const libv86 = fs.readFileSync(libv86path, 'utf-8')
18 | 
19 |   let patchedLibv86 = libv86.replace('k.load_file="undefined"===typeof XMLHttpRequest?pa:qa', 'k.load_file=pa')
20 |   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)}')
21 |   patchedLibv86 = patchedLibv86.replace('this.fetch=fetch;', 'this.fetch=(...args)=>fetch(...args);')
22 | 
23 |   fs.writeFileSync(libv86path, patchedLibv86)
24 | 
25 |   // Overwrite
26 |   const indexContents = fs.readFileSync(index, 'utf-8');
27 |   const replacedContents = indexContents.replace('<!-- libv86 -->', '<script src="libv86.js"></script>')
28 |   fs.writeFileSync(index, replacedContents)
29 | }
30 | 
31 | async function compileParcel (options = {}) {
32 |   const entryFiles = [
33 |     path.join(__dirname, '../static/index.html'),
34 |     path.join(__dirname, '../src/main/main.ts')
35 |   ]
36 | 
37 |   const bundlerOptions = {
38 |     outDir: './dist', // The out directory to put the build files in, defaults to dist
39 |     outFile: undefined, // The name of the outputFile
40 |     publicUrl: '../', // The url to server on, defaults to dist
41 |     watch: false, // whether to watch the files and rebuild them on change, defaults to process.env.NODE_ENV !== 'production'
42 |     cache: false, // Enabled or disables caching, defaults to true
43 |     cacheDir: '.cache', // The directory cache gets put in, defaults to .cache
44 |     contentHash: false, // Disable content hash from being included on the filename
45 |     minify: false, // Minify files, enabled if process.env.NODE_ENV === 'production'
46 |     scopeHoist: false, // turn on experimental scope hoisting/tree shaking flag, for smaller production bundles
47 |     target: 'electron', // browser/node/electron, defaults to browser
48 |     // https: { // Define a custom {key, cert} pair, use true to generate one or false to use http
49 |     //   cert: './ssl/c.crt', // path to custom certificate
50 |     //   key: './ssl/k.key' // path to custom key
51 |     // },
52 |     logLevel: 3, // 3 = log everything, 2 = log warnings & errors, 1 = log errors
53 |     hmr: false, // Enable or disable HMR while watching
54 |     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)
55 |     sourceMaps: false, // Enable or disable sourcemaps, defaults to enabled (minified builds currently always create sourcemaps)
56 |     hmrHostname: '', // A hostname for hot module reload, default to ''
57 |     detailedReport: false, // Prints a detailed report of the bundles, assets, filesizes and times, defaults to false, reports are only printed if watch is disabled,
58 |     ...options
59 |   }
60 | 
61 |   const bundler = new Bundler(entryFiles, bundlerOptions)
62 | 
63 |   // Run the bundler, this returns the main bundle
64 |   // Use the events if you're using watch mode as this promise will only trigger once and not for every rebuild
65 |   await bundler.bundle()
66 | 
67 |   await copyLib();
68 | }
69 | 
70 | module.exports = {
71 |   compileParcel
72 | }
73 | 
74 | if (require.main === module) compileParcel()
75 | 


--------------------------------------------------------------------------------
/tools/parcel-watch.js:
--------------------------------------------------------------------------------
 1 | const { compileParcel } = require('./parcel-build')
 2 | 
 3 | async function watchParcel () {
 4 |   return compileParcel({ watch: true })
 5 | }
 6 | 
 7 | module.exports = {
 8 |   watchParcel
 9 | }
10 | 
11 | if (require.main === module) watchParcel()
12 | 


--------------------------------------------------------------------------------
/tools/resedit.js:
--------------------------------------------------------------------------------
 1 | const path = require('path');
 2 | 
 3 | const resedit = require('../node_modules/@electron/packager/dist/resedit.js')
 4 | const package = require('../package.json');
 5 | 
 6 | const exePath = process.argv[process.argv.length - 1]
 7 | 
 8 | console.log(exePath)
 9 | 
10 | async function main() {
11 |   await resedit.resedit(exePath, {
12 |     "productVersion": package.version,
13 |     "fileVersion": package.version,
14 |     "productName": package.productName,
15 |     "iconPath": path.join(__dirname, "../assets/icon.ico"),
16 |     "win32Metadata": {
17 |       "FileDescription": package.productName,
18 |       "InternalName": package.name,
19 |       "OriginalFilename": `${package.name}.exe`,
20 |       "ProductName": package.productName,
21 |       "CompanyName": package.author
22 |     }
23 |   });
24 | }
25 | 
26 | main();
27 | 


--------------------------------------------------------------------------------
/tools/run-bin.js:
--------------------------------------------------------------------------------
 1 | /* tslint:disable */
 2 | 
 3 | const childProcess = require('child_process')
 4 | const path = require('path')
 5 | 
 6 | async function run (name, bin, args = []) {
 7 |   await new Promise((resolve, reject) => {
 8 |     console.info(`Running ${name}`)
 9 | 
10 |     const cmd = process.platform === 'win32' ? `${bin}.cmd` : bin
11 |     const child = childProcess.spawn(
12 |       path.resolve(__dirname, '..', 'node_modules', '.bin', cmd),
13 |       args,
14 |       {
15 |         cwd: path.resolve(__dirname, '..'),
16 |         stdio: 'inherit'
17 |       }
18 |     )
19 | 
20 |     child.on('exit', (code) => {
21 |       console.log('')
22 |       if (code === 0) return resolve()
23 |       reject(new Error(`${name} failed`))
24 |     })
25 |   })
26 | };
27 | 
28 | module.exports = {
29 |   run
30 | }
31 | 


--------------------------------------------------------------------------------
/tools/tsc.js:
--------------------------------------------------------------------------------
 1 | /* tslint:disable */
 2 | 
 3 | const { run } = require('./run-bin')
 4 | 
 5 | async function compileTypeScript () {
 6 |   await run('TypeScript', 'tsc', ['-p', 'tsconfig.json'])
 7 | };
 8 | 
 9 | module.exports = {
10 |   compileTypeScript
11 | }
12 | 
13 | if (require.main === module) compileTypeScript()
14 | 


--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "outDir": "./dist",
 4 |     "allowJs": true,
 5 |     "allowSyntheticDefaultImports": true,
 6 |     "experimentalDecorators": true,
 7 |     "removeComments": false,
 8 |     "preserveConstEnums": true,
 9 |     "sourceMap": true,
10 |     "lib": [
11 |       "es2023",
12 |       "dom"
13 |     ],
14 |     "noImplicitAny": true,
15 |     "noImplicitReturns": true,
16 |     "strictNullChecks": true,
17 |     "noUnusedLocals": true,
18 |     "noImplicitThis": true,
19 |     "noUnusedParameters": true,
20 |     "noEmitHelpers": false,
21 |     "module": "commonjs",
22 |     "moduleResolution": "node",
23 |     "pretty": true,
24 |     "target": "es2023",
25 |     "jsx": "react",
26 |     "typeRoots": [
27 |       "./node_modules/@types"
28 |     ],
29 |     "baseUrl": "."
30 |   },
31 |   "include": [
32 |     "src/**/*"
33 |   ],
34 |   "exclude": [
35 |     "node_modules"
36 |   ],
37 |   "formatCodeOptions": {
38 |     "indentSize": 2,
39 |     "tabSize": 2
40 |   }
41 | }
42 | 


--------------------------------------------------------------------------------