├── .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 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 " 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 | 8 | 9 | 10 | 11 | 12 | 16 | 43 | 44 | 45 | 49 | 62 | 63 | 64 | 68 | 77 | 78 |
13 |
14 | Windows 15 |
17 | 32-bit 18 | 19 | 💿 Installer 20 | | 21 | 22 | 📦 Standalone Zip 23 | 24 |
25 | 64-bit 26 | 27 | 💿 Installer 28 | | 29 | 30 | 📦 Standalone Zip 31 |
32 | ARM64 33 | 34 | 💿 Installer 35 | | 36 | 37 | 📦 Standalone Zip 38 |
39 | 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 | 42 |
46 |
47 | macOS 48 |
50 | Apple Silicon Processor 51 | 52 | 📦 Standalone Zip 53 |
54 | Intel Processor 55 | 56 | 📦 Standalone Zip 57 | 58 | 59 | ❓ Don't know what kind of chip you have? If you bought your computer after 2020, select "Apple Silicon". Learn more at apple.com. 60 | 61 |
65 |
66 | Linux 67 |
69 | 64-bit 70 | 71 | 💿 rpm 72 | | 73 | 74 | 💿 deb 75 |
76 |
79 | 80 |
81 | 82 | 83 | 84 | 87 | 90 | 91 |
85 | Screenshot showing Windows 95 86 | 88 | Screenshot showing Windows 95 89 |
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 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.disable-library-validation 10 | 11 | com.apple.security.cs.disable-executable-page-protection 12 | 13 | com.apple.security.automation.apple-events 14 | 15 | 16 | 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. 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 ``; 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 { 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 = { 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 | 45 | 46 | ${getEncoding()} 47 | ${title} 48 | 49 | 50 |

${title}

51 |

${getParentFolderLinkHtml(parentPath)} | ${getDesktopLinkHtml()} | ${getDownloadsLinkHtml()}

52 |

53 |

    54 | ${items} 55 |
56 | 57 | 58 | `; 59 | } 60 | 61 | function getParentFolderLinkHtml(parentPath: string) { 62 | return ` 63 | ${getIconHtml('folder.gif')} 64 | 65 | [Parent Directory] 66 | 67 | `; 68 | } 69 | 70 | function getDesktopLinkHtml() { 71 | const desktopPath = app.getPath('desktop'); 72 | 73 | return ` 74 | ${getIconHtml('desktop.gif')} 75 | 76 | Desktop 77 | 78 | `; 79 | } 80 | 81 | function getDownloadsLinkHtml() { 82 | const downloadsPath = app.getPath('downloads'); 83 | 84 | return ` 85 | ${getIconHtml('network.gif')} 86 | 87 | Downloads 88 | 89 | `; 90 | } 91 | 92 | function getIconHtml(icon: string) { 93 | return ``; 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 `
  • 102 | ${icon} 103 | 104 | ${getDisplayName(entry)} 105 | 106 | ${sizeDisplay} 107 |
  • `; 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 | 7 | 8 | ${getEncoding()} 9 | Error - File Not Found 10 | 11 | 12 |

    Error: ${errorMessage}

    13 |

    windows95 failed to find the file or directory on your host computer: ${requestedPath}

    14 |

    Options:

    15 | 19 | 20 | 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 = [ 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 { 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 |
    29 | 30 |
    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 |
    36 |
    37 |
    38 |

    39 | 40 | Settings 41 |

    42 |
    43 |
    44 | {this.renderCdrom()} 45 |
    46 | {this.renderFloppy()} 47 |
    48 | {this.renderState()} 49 |
    50 |
    51 |
    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 |
    63 | 64 | 65 | CD-ROM 66 | 67 | 73 |

    74 | windows95 comes with a virtual CD drive. It can mount images in the 75 | "iso" format. 76 |

    77 |

    78 | {cdrom ? `Inserted CD: ${cdrom?.path}` : `No CD mounted`} 79 |

    80 | 89 |
    90 | ); 91 | } 92 | 93 | public renderFloppy() { 94 | const { floppy } = this.props; 95 | 96 | return ( 97 |
    98 | 99 | 100 | Floppy 101 | 102 | 108 |

    109 | windows95 comes with a virtual floppy drive. It can mount floppy disk 110 | images in the "img" format. 111 |

    112 |

    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 |

    118 |

    119 | Once you've mounted a disk image, you might have to boot your virtual 120 | windows95 machine from scratch. 121 |

    122 |

    123 | {floppy 124 | ? `Inserted Floppy Disk: ${floppy.name}` 125 | : `No floppy mounted`} 126 |

    127 | 136 |
    137 | ); 138 | } 139 | 140 | public renderState() { 141 | const { isStateReset } = this.state; 142 | const { bootFromScratch } = this.props; 143 | 144 | return ( 145 |
    146 | 147 | 148 | Reset machine state 149 | 150 |
    151 |

    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 | All your changes will be lost. 156 |

    157 | 166 | 170 |
    171 |
    172 | ); 173 | } 174 | 175 | /** 176 | * Handle a change in the floppy input 177 | * 178 | * @param event 179 | */ 180 | private onChangeFloppy(event: React.ChangeEvent) { 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) { 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 { 8 | public render() { 9 | return ( 10 |
    11 | 15 | Hit ESC to lock or unlock your mouse 16 |
    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 |
    41 | Disk: {disk} | CPU Speed: {cpu} |{" "} 42 | 43 | Hide 44 | 45 |
    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 | 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 = ; 204 | } 205 | 206 | return ( 207 | <> 208 | {card} 209 | 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 |
    225 |
    226 | 227 |
    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 | { 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 { 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 { 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} codes 506 | */ 507 | private sendKeys(codes: Array) { 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 { 8 | constructor(props: StartMenuProps) { 9 | super(props); 10 | 11 | this.navigate = this.navigate.bind(this); 12 | } 13 | 14 | public render() { 15 | return ( 16 | 33 | ); 34 | } 35 | 36 | private navigate(event: React.SyntheticEvent) { 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 { 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 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | 10 | -------------------------------------------------------------------------------- /static/floppy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixrieseberg/windows95/a6d57c6538da2b331aa81c6af35163b804aaa2f6/static/floppy.png -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | windows95 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 | 15 | 16 | -------------------------------------------------------------------------------- /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 | 2 | 3 | windows95 Help 4 | 5 | 6 | 7 | 8 | 17 | 18 |
    9 | 10 | windows95 Apps & Games 11 |
    12 | 13 |

    I've installed a few apps and games for you to try out. Check out the Games folder on the desktop!

    14 |

    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 your host's Download folder.

    15 |
    16 |
    19 | 20 | 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 | 2 | 3 | Windows 95 Credits 4 | 5 | 6 | 7 | 8 | 28 | 29 |
    9 | 10 | windows95 Credits 11 |
    12 | 13 |

    Emulation Engine

    14 |

    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 copy.sh. 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 |

    20 | 21 |

    Electron

    22 |

    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 |

    26 |
    27 |
    30 | 31 | 32 | -------------------------------------------------------------------------------- /static/www/help.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | windows95 Help 4 | 5 | 6 | 7 | 8 | 31 | 32 |
    9 | 10 | windows95 Help 11 |
    12 | 13 |

    MS-DOS Display Issues

    14 |

    If MS-DOS seems to mess up the screen:

    15 |
      16 |
    1. Hit Alt + Enter to make the command screen "Full Screen" (as far as Windows 95 is concerned)
    2. 17 |
    3. This should restore the display from the garbled mess you see and allow you to access the Command Prompt
    4. 18 |
    5. Press Alt-Enter again to leave Full Screen and go back to Window Mode
    6. 19 |
    20 |

    (Thanks to @DisplacedGamers for that wisdom)

    21 | 22 |

    windows95 Stuck in Bad State

    23 |

    If windows95 becomes unresponsive or stuck:

    24 |
      25 |
    1. On the app's home screen, select "Settings" in the lower menu
    2. 26 |
    3. Delete your machine's state before starting it again
    4. 27 |
    5. This should resolve the issue when you restart
    6. 28 |
    29 |
    30 |
    33 | 34 | 35 | -------------------------------------------------------------------------------- /static/www/home.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | Welcome to Windows 95! 4 | 5 | 6 | 7 | 8 | 34 | 35 |
    9 |
    10 | 11 | 12 | Welcome to Windows 95! 13 | 14 | 15 |
    16 | 17 | 18 |

    Hi, I'm Felix, the maker behind windows95. I hope you're having fun!

    19 | 20 |

    Reach out to me in a modern browser (as in: not in windows95) on felixrieseberg.com or find me on Bluesky at @felixrieseberg.

    21 | 22 |
    23 | 24 | The Internet! 25 |
    26 | 27 |

    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 The Old Net to travel back in time.

    28 |
    29 | 30 |
    31 | Last updated: 2025 32 |
    33 |
    36 | 37 | 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 | 2 | 3 | 4 | Welcome to Windows 95! 5 | 6 | 7 | 8 | 9 | 10 | 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 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /static/www/navigation.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | Navigation 4 | 5 | 6 | 7 | 12 | 17 | 22 | 27 | 32 |
    8 | 9 | Navigation 10 | 11 |
    13 | 14 | Home 15 | 16 |
    18 | 19 | Apps & Games 20 | 21 |
    23 | 24 | Help 25 | 26 |
    28 | 29 | Credits 30 | 31 |
    33 |
    34 |
    35 |

    36 | 37 | Best viewed with
    38 | Internet Explorer 5.5 and windows95 39 |
    40 |

    41 | 42 | 43 | 44 |
    45 | 46 | 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('', '') 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 | --------------------------------------------------------------------------------