24 Commits

Author SHA1 Message Date
Felix Rieseberg
94021edb61 Fix missing icon 2025-02-20 21:42:49 -08:00
Felix Rieseberg
6f2025ffc0 Improve reset 2025-02-20 21:19:55 -08:00
Felix Rieseberg
3a7b37fff0 Fix a few smaller build issues 2025-02-20 17:27:00 -08:00
Felix Rieseberg
16eb63e13b Add bg image 2025-02-20 17:16:51 -08:00
Felix Rieseberg
54fe721f4f Better gitignore 2025-02-20 17:16:31 -08:00
Felix Rieseberg
6dee2f45a2 Hide text screen when paused 2025-02-20 14:31:10 -08:00
Felix Rieseberg
e7e047b0a0 Deal with resedit segfaults 2025-02-20 10:54:02 -08:00
Felix Rieseberg
5a334abb13 Use dotenv 2025-02-20 07:57:57 -08:00
Felix Rieseberg
aacfae7ada Update forge config 2025-02-19 13:23:21 -08:00
Felix Rieseberg
9b87b77570 Update links 2025-02-18 23:03:37 -08:00
Felix Rieseberg
e6a0d931af Move out helper images 2025-02-18 23:03:08 -08:00
Felix Rieseberg
973580d60b Ignore more files 2025-02-18 23:01:44 -08:00
Felix Rieseberg
8fcf5eaed3 Remove old forge settings 2025-02-18 22:54:30 -08:00
Felix Rieseberg
e15d918fb3 Update version number to 4.0.0 2025-02-18 22:50:32 -08:00
Felix Rieseberg
b442c6db08 Update UI 2025-02-18 22:49:52 -08:00
Felix Rieseberg
5c946bbca4 Now with working network 2025-02-18 22:39:47 -08:00
Felix Rieseberg
c9e45a9f39 Update qemu docs 2025-02-18 22:39:35 -08:00
Felix Rieseberg
bc42ce3231 Remove fs-extra 2025-02-15 09:45:22 -08:00
Felix Rieseberg
d91e72ccc5 Upgrade TypeScript 2025-02-15 09:38:41 -08:00
Felix Rieseberg
bd40f00f8d Remove node-abi 2025-02-15 09:38:04 -08:00
Felix Rieseberg
1cbfca7451 Upgrade rimraf, node-abi, electron-squirrel-startup 2025-02-15 09:37:38 -08:00
Felix Rieseberg
7710c4b7af Upgrade prettier 2025-02-15 09:36:37 -08:00
Felix Rieseberg
4cce1f0740 Upgrade electron & electron-forge 2025-02-15 09:35:50 -08:00
Felix Rieseberg
f8ae78f247 Update v86 2025-02-15 09:34:41 -08:00
54 changed files with 19405 additions and 9635 deletions

14
.gitignore vendored
View File

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

21
HELP.md
View File

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

View File

@@ -6,23 +6,43 @@ around.
Disk image creation
```sh
qemu-img create -f qcow2 win95.qcow2 1G
qemu-img create -f raw windows95_v4.raw 1G
```
ISO CD image creation
```sh
hdiutil makehybrid -o output.iso /path/to/folder -iso -joliet
```
Installation
```sh
qemu-system-i386 -netdev user,id=mynet0 -device ne2k_isa,netdev=mynet0 -hda win95.qcow2 -soundhw sb16 -m 128 -cpu pentium -device cirrus-vga,vgamem_mb=64 -fda boot_floppy.img -cdrom Win95_OSR25.iso -boot a -soundhw pcspk
qemu-system-i386 \
-cdrom Win95_OSR25.iso \
-m 128 \
-hda windows95.img \
-device sb16 \
-nic user,model=ne2k_pci \
-fda Win95_boot.img \
-boot a \
-M pc,acpi=off \
-cpu pentium
```
Running
- Boot from floppy
- Run `fdisk` and `format c:`
- Run `D:\setup.exe` with `24796-OEM-0014736-66386`
- After completing setup and restarting your computer, you might get an IOS Windows protection error
- Use `fix95cpu.ima` as a bootable floppy to fix
- Use `vga-driver.iso` to install different video driver
With `ne2k_isa`
```sh
qemu-system-i386 -netdev user,id=mynet0 -device ne2k_isa,netdev=mynet0 -drive file=win95.img,format=raw,index=0,media=disk -soundhw sb16 -m 128 -cpu pentium -device cirrus-vga,vgamem_mb=16 -soundhw pcspk -cdrom Win95_OSR25.iso
qemu-system-i386 \
-m 128 \
-hda images/windows95.img \
-device sb16 \
-M pc,acpi=off \
-cpu pentium \
-netdev user,id=mynet0 \
-device ne2k_isa,netdev=mynet0,irq=10
```
With `ne2k_pci`
```sh
qemu-system-i386 -net nic,model=ne2k_pci -net user -drive file=win95_ne2k_pci.img,format=raw,index=0,media=disk -soundhw sb16 -m 128 -cpu pentium -device cirrus-vga,vgamem_mb=16 -soundhw pcspk -cdrom Win95_OSR25.iso --enable-kvm
```

View File

@@ -2,13 +2,32 @@ const path = require('path');
const fs = require('fs');
const package = require('./package.json');
if (process.env['WINDOWS_CODESIGN_FILE']) {
const certPath = path.join(__dirname, 'win-certificate.pfx');
const certExists = fs.existsSync(certPath);
require('dotenv').config()
if (certExists) {
process.env['WINDOWS_CODESIGN_FILE'] = certPath;
}
process.env.TEMP = process.env.TMP = `C:\\Users\\FelixRieseberg\\AppData\\Local\\Temp`
const FLAGS = {
SIGNTOOL_PATH: process.env.SIGNTOOL_PATH,
AZURE_CODE_SIGNING_DLIB: process.env.AZURE_CODE_SIGNING_DLIB || path.join(__dirname, 'Microsoft.Trusted.Signing.Client.1.0.60/bin/x64/Azure.CodeSigning.Dlib.dll'),
AZURE_METADATA_JSON: process.env.AZURE_METADATA_JSON || path.resolve(__dirname, 'trusted-signing-metadata.json'),
AZURE_TENANT_ID: process.env.AZURE_TENANT_ID,
AZURE_CLIENT_ID: process.env.AZURE_CLIENT_ID,
AZURE_CLIENT_SECRET: process.env.AZURE_CLIENT_SECRET,
APPLE_ID: process.env.APPLE_ID,
APPLE_ID_PASSWORD: process.env.APPLE_ID_PASSWORD,
}
fs.writeFileSync(FLAGS.AZURE_METADATA_JSON, JSON.stringify({
Endpoint: process.env.AZURE_CODE_SIGNING_ENDPOINT || "https://wcus.codesigning.azure.net",
CodeSigningAccountName: process.env.AZURE_CODE_SIGNING_ACCOUNT_NAME,
CertificateProfileName: process.env.AZURE_CODE_SIGNING_CERTIFICATE_PROFILE_NAME,
}, null, 2));
const windowsSign = {
signToolPath: FLAGS.SIGNTOOL_PATH,
signWithParams: `/v /dlib ${FLAGS.AZURE_CODE_SIGNING_DLIB} /dmdf ${FLAGS.AZURE_METADATA_JSON}`,
timestampServer: "http://timestamp.acs.microsoft.com",
hashes: ["sha256"],
}
module.exports = {
@@ -26,29 +45,38 @@ module.exports = {
},
osxSign: {
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)',
'hardened-runtime': true,
'gatekeeper-assess': false,
'entitlements': 'assets/entitlements.plist',
'entitlements-inherit': 'assets/entitlements.plist',
'signature-flags': 'library'
},
osxNotarize: {
appBundleId: 'com.felixrieseberg.macintoshjs',
appleId: process.env['APPLE_ID'],
appleIdPassword: process.env['APPLE_ID_PASSWORD'],
ascProvider: 'LT94ZKYDCJ'
appleId: FLAGS.APPLE_ID,
appleIdPassword: FLAGS.APPLE_ID_PASSWORD,
teamId: 'LT94ZKYDCJ'
},
windowsSign,
ignore: [
/\/assets(\/?)/,
/\/docs(\/?)/,
/\/tools(\/?)/,
/\/src\/.*\.ts/,
/\/test(\/?)/,
/\/@types(\/?)/,
/\/helper-images(\/?)/,
/package-lock\.json/,
/README\.md/,
/tsconfig\.json/,
/Dockerfile/,
/issue_template\.md/,
/HELP\.md/,
/forge\.config\.js/,
/\.github(\/?)/,
/\.circleci(\/?)/,
/\.vscode(\/?)/,
/\.gitignore/,
/\.gitattributes/,
/\.eslintignore/,
/\.eslintrc/,
/\.prettierrc/,
/\/Microsoft\.Trusted\.Signing\.Client.*/,
/\/trusted-signing-metadata/,
]
},
makers: [
@@ -66,8 +94,7 @@ module.exports = {
loadingGif: './assets/boot.gif',
setupExe: `windows95-${package.version}-setup-${arch}.exe`,
setupIcon: path.resolve(__dirname, 'assets', 'icon.ico'),
certificateFile: process.env['WINDOWS_CODESIGN_FILE'],
certificatePassword: process.env['WINDOWS_CODESIGN_PASSWORD'],
windowsSign
}
}
},

17729
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "windows95",
"productName": "windows95",
"version": "3.1.2",
"version": "4.0.0",
"description": "Windows 95, in an app. I'm sorry.",
"main": "./dist/src/main/main.js",
"scripts": {
@@ -12,7 +12,8 @@
"lint": "prettier --write src/**/*.{ts,tsx} && npm run check-links",
"less": "node ./tools/lessc.js",
"tsc": "tsc -p tsconfig.json --noEmit",
"check-links": "node tools/check-links.js"
"check-links": "node tools/check-links.js",
"postinstall": "patch-package"
},
"keywords": [],
"author": "Felix Rieseberg, felix@felixrieseberg.com",
@@ -21,31 +22,29 @@
"forge": "./forge.config.js"
},
"dependencies": {
"electron-squirrel-startup": "^1.0.0",
"fs-extra": "^10.1.0",
"electron-squirrel-startup": "^1.0.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"tslib": "^2.4.0",
"update-electron-app": "^2.0.1"
},
"devDependencies": {
"@electron-forge/cli": "6.2.1",
"@electron-forge/maker-deb": "6.2.1",
"@electron-forge/maker-flatpak": "^6.2.1",
"@electron-forge/maker-rpm": "^6.2.1",
"@electron-forge/maker-squirrel": "^6.2.1",
"@electron-forge/maker-zip": "^6.2.1",
"@electron-forge/publisher-github": "^6.2.1",
"@types/fs-extra": "^9.0.13",
"@types/node": "^12.19.9",
"@electron-forge/cli": "7.6.1",
"@electron-forge/maker-deb": "7.6.1",
"@electron-forge/maker-flatpak": "^7.6.1",
"@electron-forge/maker-rpm": "^7.6.1",
"@electron-forge/maker-squirrel": "^7.6.1",
"@electron-forge/maker-zip": "^7.6.1",
"@electron-forge/publisher-github": "^7.6.1",
"@types/node": "^20",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"electron": "25.3.0",
"dotenv": "^16.4.7",
"electron": "34.2.0",
"less": "^3.13.0",
"node-abi": "^3.45.0",
"parcel-bundler": "^1.12.5",
"prettier": "^3.0.0",
"rimraf": "^5.0.1",
"typescript": "^5.1.6"
"patch-package": "^8.0.0",
"prettier": "^3.5.1",
"rimraf": "^6.0.1",
"typescript": "^5.7.3"
}
}

View File

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

View File

@@ -1,9 +1,12 @@
import * as path from "path";
const IMAGES_PATH = path.join(__dirname, "../../images");
export const CONSTANTS = {
IMAGE_PATH: path.join(__dirname, "../../images/windows95.img"),
IMAGES_PATH,
IMAGE_PATH: path.join(IMAGES_PATH, "windows95.img"),
IMAGE_DEFAULT_SIZE: 1073741824, // 1GB
DEFAULT_STATE_PATH: path.join(__dirname, "../../images/default-state.bin"),
DEFAULT_STATE_PATH: path.join(IMAGES_PATH, "default-state.bin"),
};
export const IPC_COMMANDS = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import { IPC_COMMANDS } from "../constants";
export function setupIpcListeners() {
ipcMain.handle(IPC_COMMANDS.GET_STATE_PATH, () => {
return path.join(app.getPath("userData"), "state-v3.bin");
return path.join(app.getPath("userData"), "state-v4.bin");
});
ipcMain.handle(IPC_COMMANDS.APP_QUIT, () => {

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

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

View File

@@ -7,6 +7,8 @@ import { setupUpdates } from "./update";
import { getOrCreateWindow } from "./windows";
import { setupMenu } from "./menu";
import { setupIpcListeners } from "./ipc";
import { setupSession } from "./session";
import { setupFileServer } from './fileserver/fileserver';
/**
* Handle the app's "ready" event. This is essentially
@@ -15,11 +17,13 @@ import { setupIpcListeners } from "./ipc";
export async function onReady() {
if (!isDevMode()) process.env.NODE_ENV = "production";
setupSession();
setupIpcListeners();
getOrCreateWindow();
setupAboutPanel();
setupMenu();
setupUpdates();
setupFileServer();
}
/**

View File

@@ -1,11 +1,12 @@
import { app, shell, Menu, BrowserWindow, ipcMain } from "electron";
import { app, shell, Menu, BrowserWindow, ipcMain, dialog } from "electron";
import { clearCaches } from "../cache";
import { IPC_COMMANDS } from "../constants";
import { isDevMode } from "../utils/devmode";
import { log } from "./logging";
const LINKS = {
homepage: "https://www.twitter.com/felixrieseberg",
homepage: "https://www.felixrieseberg.com",
repo: "https://github.com/felixrieseberg/windows95",
credits: "https://github.com/felixrieseberg/windows95/blob/master/CREDITS.md",
help: "https://github.com/felixrieseberg/windows95/blob/master/HELP.md",
@@ -15,10 +16,10 @@ export async function setupMenu() {
await createMenu();
ipcMain.on(IPC_COMMANDS.MACHINE_STARTED, () =>
createMenu({ isRunning: true })
createMenu({ isRunning: true }),
);
ipcMain.on(IPC_COMMANDS.MACHINE_STOPPED, () =>
createMenu({ isRunning: false })
createMenu({ isRunning: false }),
);
}
@@ -26,10 +27,10 @@ function send(cmd: string) {
const windows = BrowserWindow.getAllWindows();
if (windows[0]) {
console.log(`Sending "${cmd}"`);
log(`Sending "${cmd}"`);
windows[0].webContents.send(cmd);
} else {
console.log(`Tried to send "${cmd}", but could not find window`);
log(`Tried to send "${cmd}", but could not find window`);
}
}
@@ -163,7 +164,20 @@ async function createMenu({ isRunning } = { isRunning: false }) {
},
{
label: "Reset",
click: () => send(IPC_COMMANDS.MACHINE_RESET),
click: async () => {
const result = await dialog.showMessageBox({
type: 'warning',
buttons: ['Reset', 'Cancel'],
defaultId: 1,
title: 'Reset Machine',
message: 'Are you sure you want to reset the machine?',
detail: 'This will delete the machine state, including all changes you have made.',
});
if (result.response === 0) {
send(IPC_COMMANDS.MACHINE_RESET);
}
},
enabled: isRunning,
},
{

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

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

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

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

View File

@@ -22,10 +22,7 @@ export function getOrCreateWindow(): BrowserWindow {
mainWindow.loadFile("./dist/static/index.html");
mainWindow.webContents.on("will-navigate", (event, url) =>
handleNavigation(event, url)
);
mainWindow.webContents.on("new-window", (event, url) =>
handleNavigation(event, url)
handleNavigation(event, url),
);
mainWindow.on("closed", () => {

View File

@@ -1,3 +1,12 @@
export interface Win95Window extends Window {
emulator: any;
win95: {
app: App;
};
}
declare let window: Win95Window;
/**
* The top-level class controlling the whole app. This is *not* a React component,
* but it does eventually render all components.
@@ -27,8 +36,8 @@ export class App {
}
}
window["win95"] = window["win95"] || {
window.win95 = window.win95 || {
app: new App(),
};
window["win95"].app.setup();
window.win95.app.setup();

View File

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

View File

@@ -1,7 +1,6 @@
import * as React from "react";
import * as fs from "fs-extra";
import { getStatePath } from "./utils/get-state-path";
import { resetState } from "./utils/reset-state";
interface CardSettingsProps {
bootFromScratch: () => void;
@@ -122,7 +121,7 @@ export class CardSettings extends React.Component<
</p>
<p id="floppy-path">
{floppy
? `Inserted Floppy Disk: ${floppy.path}`
? `Inserted Floppy Disk: ${floppy.name}`
: `No floppy mounted`}
</p>
<button
@@ -213,12 +212,7 @@ export class CardSettings extends React.Component<
* Handle the state reset
*/
private async onResetState() {
const statePath = await getStatePath();
if (fs.existsSync(statePath)) {
await fs.remove(statePath);
}
await resetState();
this.setState({ isStateReset: true });
}
}

View File

@@ -74,7 +74,7 @@ export class EmulatorInfo extends React.Component<
if (!emulator) {
console.log(
`Emulator info: Tried to install listeners, but emulator not defined yet.`
`Emulator info: Tried to install listeners, but emulator not defined yet.`,
);
return;
}
@@ -104,7 +104,7 @@ export class EmulatorInfo extends React.Component<
if (!emulator) {
console.log(
`Emulator info: Tried to uninstall listeners, but emulator not defined yet.`
`Emulator info: Tried to uninstall listeners, but emulator not defined yet.`,
);
return;
}

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import * as fs from "fs-extra";
import * as fs from "fs";
import * as path from "path";
import { ipcRenderer, shell } from "electron";
import { ipcRenderer, shell, webUtils } from "electron";
import { CONSTANTS, IPC_COMMANDS } from "../constants";
import { getDiskImageSize } from "../utils/disk-image-size";
@@ -9,11 +9,14 @@ import { CardStart } from "./card-start";
import { StartMenu } from "./start-menu";
import { CardSettings } from "./card-settings";
import { EmulatorInfo } from "./emulator-info";
import { CardDrive } from "./card-drive";
import { getStatePath } from "./utils/get-state-path";
import { Win95Window } from "./app";
import { resetState } from "./utils/reset-state";
declare let window: Win95Window;
export interface EmulatorState {
currentUiCard: string;
currentUiCard: "start" | "settings";
emulator?: any;
scale: number;
floppyFile?: File;
@@ -51,10 +54,6 @@ export class Emulator extends React.Component<{}, EmulatorState> {
this.setupInputListeners();
this.setupIpcListeners();
this.setupUnloadListeners();
if (document.location.hash.includes("AUTO_START")) {
this.startEmulator();
}
}
/**
@@ -200,8 +199,6 @@ export class Emulator extends React.Component<{}, EmulatorState> {
cdrom={cdromFile}
/>
);
} else if (currentUiCard === "drive") {
card = <CardDrive showDiskImage={this.showDiskImage} />;
} else {
card = <CardStart startEmulator={this.startEmulator} />;
}
@@ -210,7 +207,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
<>
{card}
<StartMenu
navigate={(target) => this.setState({ currentUiCard: target })}
navigate={(target) => this.setState({ currentUiCard: target as "start" | "settings" })}
/>
</>
);
@@ -225,8 +222,8 @@ export class Emulator extends React.Component<{}, EmulatorState> {
{this.renderInfo()}
{this.renderUI()}
<div id="emulator">
<div></div>
<canvas></canvas>
<div id="emulator-text-screen"></div>
<canvas id="emulator-canvas"></canvas>
</div>
</>
);
@@ -263,11 +260,9 @@ export class Emulator extends React.Component<{}, EmulatorState> {
*/
public showDiskImage() {
// Contents/Resources/app/dist/static
const imagePath = path.join(__dirname, "../../images/windows95.img");
console.log(`Showing disk image in ${CONSTANTS.IMAGE_PATH}`);
console.log(`Showing disk image in ${imagePath}`);
shell.showItemInFolder(imagePath);
shell.showItemInFolder(CONSTANTS.IMAGE_PATH);
}
/**
@@ -276,19 +271,23 @@ export class Emulator extends React.Component<{}, EmulatorState> {
private async startEmulator() {
document.body.classList.remove("paused");
const cdrom: any = {};
const cdromFile: any = this.state.cdromFile;
if (cdromFile?.path) {
cdrom.url = cdromFile.path;
cdrom.async = true;
cdrom.size = await getDiskImageSize(cdromFile.path);
}
const cdromPath = this.state.cdromFile
? webUtils.getPathForFile(this.state.cdromFile)
: null;
const options = {
wasm_path: path.join(__dirname, "build/v86.wasm"),
memory_size: 128 * 1024 * 1024,
vga_memory_size: 32 * 1024 * 1024,
screen_container: document.getElementById("emulator"),
vga_memory_size: 64 * 1024 * 1024,
screen: {
container: document.getElementById("emulator"),
scale: 0
},
preserve_mac_from_state_image: true,
net_device: {
relay_url: "fetch",
type: "ne2k",
},
bios: {
url: path.join(__dirname, "../../bios/seabios.bin"),
},
@@ -300,18 +299,24 @@ export class Emulator extends React.Component<{}, EmulatorState> {
async: true,
size: await getDiskImageSize(CONSTANTS.IMAGE_PATH),
},
fda: {
buffer: this.state.floppyFile,
},
cdrom: cdrom,
fda: this.state.floppyFile
? {
buffer: this.state.floppyFile,
}
: undefined,
cdrom: cdromPath
? {
url: cdromPath,
async: true,
size: await getDiskImageSize(cdromPath),
}
: undefined,
boot_order: 0x132,
// One day, maybe!
// network_relay_url: "ws://localhost:8080/"
};
console.log(`🚜 Starting emulator with options`, options);
window["emulator"] = new V86Starter(options);
window["emulator"] = new V86(options);
// New v86 instance
this.setState({
@@ -363,17 +368,21 @@ export class Emulator extends React.Component<{}, EmulatorState> {
this.unlockMouse();
await emulator.stop();
this.setState({ isRunning: false });
this.resetCanvas();
document.body.classList.add("paused");
ipcRenderer.send(IPC_COMMANDS.MACHINE_STOPPED);
}
/**
* Reset the emulator by reloading the whole page (lol)
* Reset the emulator by reloading the whole page
*/
private async resetEmulator() {
this.isResetting = true;
document.location.hash = `#AUTO_START`;
await this.stopEmulator();
await resetState();
document.location.reload();
}
@@ -392,7 +401,9 @@ export class Emulator extends React.Component<{}, EmulatorState> {
try {
const newState = await emulator.save_state();
await fs.outputFile(statePath, Buffer.from(newState));
await fs.promises.writeFile(statePath, Buffer.from(newState), {
flush: true
});
} catch (error) {
console.warn(`saveState: Could not save state`, error);
}
@@ -402,24 +413,26 @@ export class Emulator extends React.Component<{}, EmulatorState> {
* Restores state to the emulator.
*/
private async restoreState() {
const { emulator } = this.state;
const { emulator, isBootingFresh } = this.state;
const state = await this.getState();
// Nothing to do with if we don't have a state
if (!state) {
if (isBootingFresh) {
console.log(`restoreState: Booting fresh, not restoring.`);
return;
} else if (!state) {
console.log(`restoreState: No state present, not restoring.`);
}
if (!emulator) {
return;
} else if (!emulator) {
console.log(`restoreState: No emulator present`);
return;
}
try {
await this.state.emulator.restore_state(state);
} catch (error) {
console.log(
`State: Could not read state file. Maybe none exists?`,
error
`restoreState: Could not read state file. Maybe none exists?`,
error,
);
}
}
@@ -438,6 +451,8 @@ export class Emulator extends React.Component<{}, EmulatorState> {
if (fs.existsSync(statePath)) {
return fs.readFileSync(statePath).buffer;
} else {
console.log(`getState: No state file found at ${statePath}`);
}
return null;
@@ -464,7 +479,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
emulator.lock_mouse();
} else {
console.warn(
`Emulator: Tried to lock mouse, but no emulator or not running`
`Emulator: Tried to lock mouse, but no emulator or not running`,
);
}
}
@@ -501,4 +516,16 @@ export class Emulator extends React.Component<{}, EmulatorState> {
this.state.emulator.keyboard_send_scancodes(scancodes);
}
}
/**
* Reset the canvas
*/
private resetCanvas() {
const canvas = document.getElementById("emulator-canvas");
if (canvas instanceof HTMLCanvasElement) {
const ctx = canvas.getContext('2d');
ctx?.clearRect(0, 0, canvas.width, canvas.height);
}
}
}

View File

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

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

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

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

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

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

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

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

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

View File

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

27
tools/resedit.js Normal file
View File

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

View File

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

8563
yarn.lock

File diff suppressed because it is too large Load Diff