From 5c946bbca4b215d481a0e26c291186c48cd6b469 Mon Sep 17 00:00:00 2001 From: Felix Rieseberg Date: Tue, 18 Feb 2025 22:39:47 -0800 Subject: [PATCH] Now with working network --- HELP.md | 21 --- src/constants.ts | 2 +- src/main/fileserver/encoding.ts | 15 ++ src/main/fileserver/fileserver.ts | 157 ++++++++++++++++++ src/main/fileserver/hide-files.ts | 72 ++++++++ src/main/fileserver/page-directory-listing.ts | 120 +++++++++++++ src/main/fileserver/page-error.ts | 22 +++ src/main/ipc.ts | 2 +- src/main/logging.ts | 3 + src/main/main.ts | 4 + src/main/menu.ts | 5 +- src/main/session.ts | 20 +++ src/main/settings.ts | 72 ++++++++ src/main/windows.ts | 3 - src/renderer/emulator.tsx | 9 +- static/www/apps.htm | 20 +++ static/www/buttons/macos.gif | Bin 0 -> 1641 bytes static/www/buttons/madewithelectron.gif | Bin 0 -> 2213 bytes static/www/buttons/msie.gif | Bin 0 -> 8609 bytes static/www/credits.htm | 31 ++++ static/www/help.htm | 34 ++++ static/www/home.htm | 37 +++++ static/www/index.htm | 21 +++ static/www/navigation.htm | 46 +++++ tools/parcel-build.js | 1 + tsconfig.json | 3 +- 26 files changed, 688 insertions(+), 32 deletions(-) create mode 100644 src/main/fileserver/encoding.ts create mode 100644 src/main/fileserver/fileserver.ts create mode 100644 src/main/fileserver/hide-files.ts create mode 100644 src/main/fileserver/page-directory-listing.ts create mode 100644 src/main/fileserver/page-error.ts create mode 100644 src/main/logging.ts create mode 100644 src/main/session.ts create mode 100644 src/main/settings.ts create mode 100644 static/www/apps.htm create mode 100644 static/www/buttons/macos.gif create mode 100644 static/www/buttons/madewithelectron.gif create mode 100644 static/www/buttons/msie.gif create mode 100644 static/www/credits.htm create mode 100644 static/www/help.htm create mode 100644 static/www/home.htm create mode 100644 static/www/index.htm create mode 100644 static/www/navigation.htm diff --git a/HELP.md b/HELP.md index 3700dd5..85e73b8 100644 --- a/HELP.md +++ b/HELP.md @@ -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 diff --git a/src/constants.ts b/src/constants.ts index 55ea5f2..4ff64a5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,7 +1,7 @@ import * as path from "path"; export const CONSTANTS = { - IMAGE_PATH: path.join(__dirname, "../../images/windows95.img"), + IMAGE_PATH: path.join(__dirname, "../../images/windows95_v4.raw"), IMAGE_DEFAULT_SIZE: 1073741824, // 1GB DEFAULT_STATE_PATH: path.join(__dirname, "../../images/default-state.bin"), }; diff --git a/src/main/fileserver/encoding.ts b/src/main/fileserver/encoding.ts new file mode 100644 index 0000000..68422cc --- /dev/null +++ b/src/main/fileserver/encoding.ts @@ -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 ``; +} diff --git a/src/main/fileserver/fileserver.ts b/src/main/fileserver/fileserver.ts new file mode 100644 index 0000000..a11c2f7 --- /dev/null +++ b/src/main/fileserver/fileserver.ts @@ -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 { + 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 = { + '.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 + } + }); +} + diff --git a/src/main/fileserver/hide-files.ts b/src/main/fileserver/hide-files.ts new file mode 100644 index 0000000..8b84cc2 --- /dev/null +++ b/src/main/fileserver/hide-files.ts @@ -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; +} + + + diff --git a/src/main/fileserver/page-directory-listing.ts b/src/main/fileserver/page-directory-listing.ts new file mode 100644 index 0000000..ef96004 --- /dev/null +++ b/src/main/fileserver/page-directory-listing.ts @@ -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 ` + + + ${getEncoding()} + ${title} + + +

${title}

+

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

+

+

    + ${items} +
+ + + `; +} + +function getParentFolderLinkHtml(parentPath: string) { + return ` + ${getIconHtml('folder.gif')} + + [Parent Directory] + + `; +} + +function getDesktopLinkHtml() { + const desktopPath = app.getPath('desktop'); + + return ` + ${getIconHtml('desktop.gif')} + + Desktop + + `; +} + +function getDownloadsLinkHtml() { + const downloadsPath = app.getPath('downloads'); + + return ` + ${getIconHtml('network.gif')} + + Downloads + + `; +} + +function getIconHtml(icon: string) { + return ``; +} + +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 `
  • + ${icon} + + ${getDisplayName(entry)} + + ${sizeDisplay} +
  • `; +} + +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]; +} diff --git a/src/main/fileserver/page-error.ts b/src/main/fileserver/page-error.ts new file mode 100644 index 0000000..94cfe56 --- /dev/null +++ b/src/main/fileserver/page-error.ts @@ -0,0 +1,22 @@ +import { getEncoding } from "./encoding"; +import { MY_COMPUTER_INTERCEPT } from "./fileserver"; + +export function generateErrorPage(errorMessage: string, requestedPath: string): string { + return ` + + + ${getEncoding()} + Error - File Not Found + + +

    Error: ${errorMessage}

    +

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

    +

    Options:

    + + + + `; +} diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 84d3501..6c6fc58 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -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, () => { diff --git a/src/main/logging.ts b/src/main/logging.ts new file mode 100644 index 0000000..0a71210 --- /dev/null +++ b/src/main/logging.ts @@ -0,0 +1,3 @@ +export function log(message: string, ...args: unknown[]) { + console.log(`[${new Date().toLocaleString()}] ${message}`, ...args); +} diff --git a/src/main/main.ts b/src/main/main.ts index 7960590..8d5d609 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -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(); } /** diff --git a/src/main/menu.ts b/src/main/menu.ts index f990f54..09136aa 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -3,6 +3,7 @@ import { app, shell, Menu, BrowserWindow, ipcMain } 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", @@ -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`); } } diff --git a/src/main/session.ts b/src/main/session.ts new file mode 100644 index 0000000..f91bb7a --- /dev/null +++ b/src/main/session.ts @@ -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, + }, + }); + }); +} diff --git a/src/main/settings.ts b/src/main/settings.ts new file mode 100644 index 0000000..7be8f49 --- /dev/null +++ b/src/main/settings.ts @@ -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(); diff --git a/src/main/windows.ts b/src/main/windows.ts index 53b52e9..d850056 100644 --- a/src/main/windows.ts +++ b/src/main/windows.ts @@ -24,9 +24,6 @@ export function getOrCreateWindow(): BrowserWindow { mainWindow.webContents.on("will-navigate", (event, url) => handleNavigation(event, url), ); - mainWindow.webContents.on("new-window", (event, url) => - handleNavigation(event, url), - ); mainWindow.on("closed", () => { mainWindow = null; diff --git a/src/renderer/emulator.tsx b/src/renderer/emulator.tsx index 7bcdff6..4102618 100644 --- a/src/renderer/emulator.tsx +++ b/src/renderer/emulator.tsx @@ -279,8 +279,13 @@ export class Emulator extends React.Component<{}, EmulatorState> { const options = { wasm_path: path.join(__dirname, "build/v86.wasm"), memory_size: 128 * 1024 * 1024, - vga_memory_size: 32 * 1024 * 1024, + vga_memory_size: 64 * 1024 * 1024, screen_container: document.getElementById("emulator"), + preserve_mac_from_state_image: true, + net_device: { + relay_url: "fetch", + type: "ne2k", + }, bios: { url: path.join(__dirname, "../../bios/seabios.bin"), }, @@ -367,7 +372,7 @@ export class Emulator extends React.Component<{}, EmulatorState> { } /** - * Reset the emulator by reloading the whole page (lol) + * Reset the emulator by reloading the whole page */ private async resetEmulator() { this.isResetting = true; diff --git a/static/www/apps.htm b/static/www/apps.htm new file mode 100644 index 0000000..e18d852 --- /dev/null +++ b/static/www/apps.htm @@ -0,0 +1,20 @@ + + + windows95 Help + + + + + + +
    + + windows95 Apps & Games +
    + +

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

    +

    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.

    +
    +
    + + diff --git a/static/www/buttons/macos.gif b/static/www/buttons/macos.gif new file mode 100644 index 0000000000000000000000000000000000000000..52d3f7209cabb788e9d246c5140b32f2dc6ef3f0 GIT binary patch literal 1641 zcmchUYgdv70D#~31r$XLFmI{Tm=cN_ifJk}z%Xo*;e|8?k(a9gk8BnFz^BZ zmY0`pHrxHFnep*)tJP+;T1UQn)Iaj5zrWvX9=LjY_R^(GMx*hYzQ+cmO|RGMuFSzk zCv338Fx+CCZ)$4N80{K^{do5iHEdO@)hhiHl}e?AZHM6>_V3>>*F2D`&7U{fWlEb& zZYwPAD{x2B||&_x4uk(tgPpwsE`@$oeF0F4dP*nApYO`)+V6ginpCeqAA znw&_J5Q&t4fB=6Y#oyl_Pc-2D>G*#IZ@}a6SS%Ker=Zb%G#U?}(Ey+Z{$C>h%l-EP za9vga6_r&-Rn^C8)U|arg8If2nv=D8c@?1c6x`O{(WwIwfnjnT-R-zAfz&#| z#YMMlufZ|zQNbBR6x}u-e0w@7sz0{bkF&&d^8_~{(fQwr2SRLd+JkH{(v9E~Bf2IZ z+ER=5WT~#%8AIK?>$qHuB6O@W3WLjLwsHeagjuLR^77{kfGX)mRlp7LOn1@BFUi93 zxM~8)m-vZua#xA!9Q*;Dk@-C3VrH02Hf{$c>s|zI|FgoHP{qZAj%Ce2V8nLk)aJAT zXFbnged5Q<<@DmrCDnsLS2p%cU+?HS!Wz}0Ui5lZABbAiYv>8(lQM$vfX5-}tDs3qFx>6Zy6noLz$DesZ5u zldwf5AbF2%tMGUu&1CK;F94QHcFwvS9bA_) zN8*%$Cr{!g>`DNVD=J;#KxzsCj8h=mDEW?p;wk2KgwN^x%WQZ2jQklYFZ{qxQXhGo z>`5xS0(k~V9jpu5R!cEy!y-`l#fINZ!?K+F?2!!OAAdX z5J;Gz814qiieV3^@Sz2~$4(5xCWkJxh^UsOA>39Dj)@F9ZK*){P{Z?gSAS3toQMd? zf?}ODT}$hoHMNx>jHX5}pLyZ1L%r>D{_~#O(m^(X_12Vv-Aq3*gx%yfcn4eMX3Rp3 z%_RVYr!&oYf<%c%8~^SYwS3Ku`#8pprzI0Fg&pnz!wu^~LH1Zy`e$sPbU)@McivRb zOp)M_WzaQoMHpi3hHuRyN4zHrh9=wk>r+UChUDRgmYbNxhKx5w;?i?3lV?heB}0RV zQSVU%ROIWvN4Ref@vAi_C0zsUK>u8QuCONa&IHN29JKG^m&$U+{DH`I$K%T580W?zQxOSzd7% zz-=q*dj%E_I_@VSn?8O2=W~RfMMCMkxN#Gb%wKm;QUV#jL~;%J4A!fQHQmu1UzaN_ zZh_M1xCyk%MFC)9rq8-<%=(_*$UEvoEyVnlr|6214yL2+{6~M==VO&hU`Kfs`VgDu zzJzqC>OwA)N1zwru(I*)25zhC_V-V37GJZoHeYRb-6Dlvq3l4k40NHJpt++NagRB` z+m1~md&5J7pJgh%jwIZQIPkhwPpv+0g*Vnl$_R MxO|Qk0szVX0Ft|m)&Kwi literal 0 HcmV?d00001 diff --git a/static/www/buttons/madewithelectron.gif b/static/www/buttons/madewithelectron.gif new file mode 100644 index 0000000000000000000000000000000000000000..935f93affa0386db79b82846527a33eda6a7ffa6 GIT binary patch literal 2213 zcmd6o>sQiw0>-}}0^$wK4DbeO=2#XeCSIx^SMid9prR)o@Ur41#TF+mtsfwwiHWGG zNhYSH<~VrCv}^{um}awUj_vAfdyZXZwr0|pUNtA%(0~Stdp5piqX+gh%IDB#f3%2@lx}LhevexoeZ{Jw$9`{ zl^{WymXTNe<-HtTTWoxq`u2Q!diveFcf(^6(@$v<<>jKns+yXbW@CFyY?_CMN4Bgu zI5@aeS$Q7TpFVwBQKZu98mrD|6c?|Y$S#aa%qZ&`jAW;1W)`HG`H4B@F#>6S{|$q| zkX7CIMgOFv^qf|!6^q5u(a|9xA^!gUEM5u{iDWXFbUMAg^_#A)t^`@x z;Ly#K;o2xFf0JLJ?CiS2<#Kg(O$7x7IXO9%m6b=K zBvs#7enVGvb#>E4)1~#dsi~<)jvPUuP~P6&2M!#Fh>BxICtSXKIUy) z@g!mnOUMg7NV7wH;-mhGLLoo(fj?OPmOIFBZnGJ#MCOE_kg!>$5dGaeFC*xdfD35SK=Yo49_wIgozpO|Oir0gx6gumxcB z-Bi7VO|=)T8<*X$bgL;5Ds7N^%jyhWfWfa>^P?2PV!< zVp*p5hfd47=_?Q_!RLteVo=b~`KnX=+L0)=qh+pY@M*+DQE8p;<(!~6-=im_!iNIa zNlx28*EcKXoLs376(3d1gb<+mTDq*9>w}YAwuH|ZMY*}a5yf+%{Y!5>thC}XZDX21 zUv^O8qVX{SWP&Hc9Ry{5rQ7jyz3zfVp`E9;;SRi$JK2Jw?jneXiydIcyw!Zw3qpFN z{ISO|gndF_EwlVN;rR>(!X5-xM_+QtO9V*yhy-Uq^?O0c7`XJzNwxWM9s^3O+;hKE z>pZ{__g+yheiiTTH>afQob|Ty`MqK;eZg6~Mvyq;OC&4;sY*z7-0w8U4z02>wI06b z>=)@S%;hEi*CT#BlBaaT$DqH`F2aVsV78MS&@#Jy&|X@s4MnzaG0-hM6uf+aPb{f{TJ4D| zwYc*^)L;HyIB2s&((_bH0WsTSBe=8bXZj&x#*V!N6A=LO+_Q}Tm~(Auv5F7`#>O<> z-Z1Myz8Haz@KG-q)vn_VnJ&b60STdmvs*9{$RHT8XM+;YM^TIfwCkob7=|j;3B3~^ zt?0)y1zTg}+!uF54yFS4U-{^MtH|#-V}^iqSGdnPfK3w`q`hrjz_@s{8I_0_^SHXUAf4h^03BaOF$%NgGlaNImky&kzDPeSha;0ZgzhyMkrT z2j(f80kBY!3bywSplP1wp)lkAw*7tp{r$QNI9u;TVq)7Or|-MCxIOV&5OZ!fN`S=tDFDyx_s1BO_S?LYU{ci_)|#TD(O$Vt7#j({cL;tAaRYt| z_KWL!KG_VAC7P}@2t9nVX5+d1He4Y2u$v6{_)L6Ak%LD%Pt5^`WGTQ`goFuhm zyCP%>AV_u=4IzmDG7xN~fg~A+&Ke@%d*zYogxdecq#!Nzy@0)_L)0S6>;Vj81cPc4 z!uo9~P7Piyu~2JxM3y}-xs7pTk0(d7irn$5Fj-K;rh)@(t59LqIJ zWv*3Dj@eSs<>)M}&6W<9);YRB3p&?t&MajhCopg}&~gR>XJ-S=mY-#m*+89TPz#gg zY@wNB3e1@ev&VXmF!c`OeA06BoA0su0ZrN^v=MrYPpZroD0m`*oqsp#g+ z((M23|Dpo)S=(q~!s<|7tT4crjet!d0QxUT0B_O4drptJFtG;T06LH=Hq;xHpo3?c zDVHuQ!7*3WiQ5|o%SeGqc(7i1alccNni}4rAFB3P-ICUwFA38)r(R0fu)iSLap__2Jl|_W`r}uzPGN76p2H`_)ZkBP*Pl3?j?wdrK2Nx$SAWYu z>Q?UYJi9URc*-gG6by!%}jEAFrv8pG|Qn?hgrTz(~8tIeb@Ya{>i z{jlD;;!Z&Wf3OT<4r9|gCUaLeLxg3{=$9wsE>-#654uEI{7B&^m~tKo6_i4ZbW%=M zicidS(W$m$g}Ux&LygbXq|{uST*$$#8P@>chK7D^*mtOA*J1aO7gM(IMb9ROKDv89 zY8eY*!A%KIxlvr`FT$x1Q73MEysn}bZp+-3 zjKh2xUVqyb@$DhX-hBUycPawoPd!N3J}n;QvN0hroUrhlZ?|K`V_Y3GXnHg+_DQEg zRr&YC*ox6a*M|M@!LvjS!NBFMXS{4`1lJv#20%u0clA!}rH5`cJI;l#PbwKtSzVO! z_E0||sb{#q)>GE)jKp$jMkKTRu|Diq_Lganx)0yhezUa_Mwo83|NhifRB|04*m1|s zJ$6F`mG?>G$&erIKQ^x^s^Tup?oHK7I!pt$xJb>X$jHMrDKWeE1t*=sKB|Xd&X*om zpoG>|{64j*#O`)tBptkiUy%SnECm~B z?Z9>MZKluM{(|(CbBOLs36|&rh0z+HMiOT9@>{VFe6+(2b}tQ`L8W*iPW-9bNMC!mlxYc4`cp z8aBOE11OI%E)4c3;@Ldv)fyVOc5%2?L;i6wRD%AD#$}HSA!bA^DX&X!!h3-A<*|8r zV^vx(qrA3PF^F{b6tt>_T=Yg)Kf!RqsVtScjv+|fy^}qK^hhdq3-(M@LC26~Q=NRH z6jvs$cK$Kd;8E1ZT7$-T!3XS=B6Ew!8nzk%VO?aL%$W-y+D}%V@=Mm1I2)|}GE%0& zr0!$JCq%W>^0IJy5X3~7>V>2MOH*xU!+f)8VPRE&-byR1+KMQ+qQt zrfHL>WMg7oirPrzJ!*T}-qCzhPb(mLKc8rM+`ZJb>>xYqY%jxy$G=dqys>2KDZieJ zZ7U7p^+Q#O1%Ht~cyRf&$a_t~#|D#~=1r!{m<;KN$NhnPv}!6PXpC*rG!&CTq#@;| zArlMtP2uX;Fp;pBxIQ*EE_+`uW0B6g`AZX|Y+OM$^i#dL}*r z;7a#sii&-CQmpCpv_FH|IBBx~CbATdViY#@5^)M^BIazzEr_-E*tRz}c4OJOdHtt8otDV$vc3l+Z)of=00XK2zkEryvc|@ z;i7?-q&++)1KpY>yg&BBUMdCb9F`awcngryPKmLvVUW5IjBJo)mJ+Crb`1SYqsd+_ z;y3R0SHP_xMk#*z^svdUmMQ90onp9FyRNjGkTtA+{PppG{lngzm%>uWzH(Kt{eijo zN+{0-CWNiG1hL4M6Nq0JN*EAxS%gDvU#1N9JrrzRZ?nXyvc z#<{g#zx@J*tLx08^a#i_;C+)d7tIAXD=e9RXwoMXc%0XdiSfD>|$Yp>M8H+84$#BW5i~*sf?rx zd$WAv-fOz08Uqw68|Ec!P{8ZfVoXL^%5Sw-H(I7L)y=N~r8>;b zj~1D7?i-qx$X+3pL6gN784T(!N9fR4vW81VIqbds^M|}kU+E@$<5VE-$l}2ZHE*(( zRJ)2w%lCii%-*K>>RU-Qn)hn^@YYuk*B?}L@CGCzoXBHb1-C-}fiwQTq($FpU}tJe zg!v}Zh-S=wA#V!_k_wdaVo|>3DYFDx1sSBJWg$y0S9!|R1?_QT_=BkTFLcrMF2zJw zVQqHDB)YQBe|dDAe?#8^h#9Ze+zL<{UD+6tsaMG_WBGaLN%RC2ni_25ID%d~fP{f5 zZ(O_@+tk%o6}>k#yrNP^n7sgYx^K%}B5KhEVB`3zGIDZjWV4r1j<8|cpD%z-7NP8r zj8JXtjSd8kfvHS@^q`cYai76G`CZx?yt)0C%p^*3n*0$3*ke53Jnt#aIHrF8MXZdN=&iKNe-o-Tok!l^dc`#i zH<$*wXoEA-{ibpj#YNy@c$|rRz$wCf;%vI`0-?dVn7Vx`lmEvr$bU~6eqZC*`R=s* zvyfbr|1`F1zywiw2}n67*QSyRE~6OpLI*+8lJw>-kJ$cVrKlV~0vs-mFpd9V90=#l~v+g0Hq z#}XIM#5{$S43JnG4Dm@SHIY+>Bx^I#wvNf*a4qaaX{HSmGr{Hy^blXvF_iM)s+X?^>2+(RXn= z>Dwo`3)1EoU!fS_bxexgBD!pM7CyAn%-pb{Ji9Up79JH!a3&zl>ifG^godNsh?^(v z_m&MN5AO!-3j~Glj~v25?B;(8H&#t1Dogs#F=W9MwQUiuXHv!40{YMSURjvAm}j(reJib)+-mo5y6Mt84(Z_3=i*Y+0p{6~H0RrfVhd+G2jBT<0dvYVtb+3J^( zTio2m!H*!fg-2~!@eROrV$aZf*Y~bmaiw}@0cGgD+##(iJujh(FX)xrkAUnZabY%# zr^b|4{|hn6lyAk^$Z}WYA(@~683r+h3FTdK=0$Nim{%V(3V|3qvSh3mHfoAcEObJV z8yKd8jy^eQBY<+KifO+d6&JT{mEyF`n&Dc;))HeyP?*{YwyxJAX2;jVJBQWbElet> zfZs2K!{GK^CZK(+ipojH@1bW2_M?*=r@KJj?) z*O-0!*ljDX;75|(zuG>gz$_;H(em=2I|cvADK{{*B6>Ub+&IV;h`PF0<8p^?fgrt5wK&) z%8uO&9~NnjaX2J4x3~A&DRxj0;gtd$s;OPuTM(@WF%$lXB1#B9-mS7^O74vJ(_~k= zZgOf9PUU$yR55o|Wr&feHz%Rr^hfr<9HnFONaUZ+Oz#5Q=J|x>%}FFuhUEQo64v0- zs-BJ6Q91c{Vn>dvwN-H8s@;u|rX(wAUBf@>oR7GwW z=|dMetG`R4u?HnMhF|49>1Z}u?iHhv8E%InOP#EKYtc}Y8>%nEfL6n@KA4qKnEu8m zU&*@a<%8_3FMCzQV=m2bWNjV`u%i$v8AX}6jtC|;5ZAj(Cc94DXmjcFc2zb+3Phnv zCe2IwQ(|9*X26KQ5c=Y=@Z5mf{>$G^l13J|{8By3XE^RGSrm#rMI4cXZnd$0wqTmu zvC^9?$q0#(W-lnxxJK5HQl)yZlF3OgFOX};L@K|8JH7I?Dm6n?e!$GB9;<>pB?B@` z{Nd=hO!T#a;fJ;)zd{t(;} z$W&(HrBfY&dgKJF{5Ibg{yNvz(HpxSw`N%V_=F&(WaaK!6pYnrb1UTor1UB)u*#v< zsc!3F2`{z3mRAp8)JLqAK5d>5Uaa&L29bt=&R@7Cq}2XvQUL;6>km<$t(+;2tq^?m z`YZaG2dnm3Wu9DQ$r4i}|0Dh8wkYg;Z5n@fLIR~78ki`9{O00#rU-=mB+Vokq6Df! z1eQ|s;){kLO3-rit9V8f#FQ#jFs2ws7y4dukv^XUZDf1J4-iifAkJ z_HwBuamEKZ0XA@?X`h3iM;-g{wR*dH&za9R-&W4a*6d3lWed`w-SU@H#Z3Hj?dOSk)i{^ z)^nb@6SkcNN^{L&UYAhWR)G(vPgq|s5voXIwBICJqVK8~EuU8pWv!OtMlAXj{LlRFANu`c`9ma7b~aditCDXCVut;=jWvy1 zJ_RzvDyv8fC^qs@xw_IXKzcLQ(a2r0HL@Zgh2USF(NbFo8Fb}(nn;(SCZ%qD;&wq-#M+Xqp5U}m#|qTN31o=gM@~cwE^o& zp=G;ryw&*vG{IZRQF+2mwscq5j%(BS2`+*4=}$o^s?!&t$TB$8u}!j?z)jlw5=`W$ zFg9DEAPD(nq><$WI*u^K>26NrZOF#1CRIc6&`%$=1pc692oz(%(gD9wDBbE@EiK_m zCc|l0^VGATwW-!iPrS9{S%c|@*rV}Lr}}~(6Gf%Fx4hYb)G$gGmj0z#5w@i+I)nC! z&x|NrE?oZh?@>qV3DY#!3gm+HfeAj;M?Z4+P zmf-z&#Y9|cS9bWEV#SjUmgGZATHs5jD;}w9d^Vr!m5$a&9r8(RB5C;pc9xiJ90(6o z_tyu@Nzdr>HNp(%Z2uzh=p$|^ zoM69;hIL$&ygep3OV<-RirSh+uZLZ*quyn186KmD4gC7b8uc$MS^j&$DyQiDA~oa} znYFwtd*JdRPm$g*9X}{!2H4)3oDNub`G{vtwzsF~~{G4u|rTc))6RHD#7<;n+ z_>=voo!VFBa+CW&CGS{D`>IZlyXR>FQe6ie{N`X%#wyvEL~&O^Nr`3OVHTmQ-`f@7 z9}q9WIwmO!I&bNQ%50i%H#te`tx)~kw$IJrPoE*6qg&IupYvZ{cXsxb z^WdO&u+2Pp@jKXd9?W?M+r5MR=TR=02hXE~=E3tQ9p=IFC<*i6d6dL=aKJoD@;rDR z<-&RJJW8i|@I1;z^Wb@u^mlOJJWA#~cpjzeJa`_Z`#g9aC2Jl$kJ9TM9P|%LVA;O` DlAzrK literal 0 HcmV?d00001 diff --git a/static/www/credits.htm b/static/www/credits.htm new file mode 100644 index 0000000..7716724 --- /dev/null +++ b/static/www/credits.htm @@ -0,0 +1,31 @@ + + + Windows 95 Credits + + + + + + +
    + + windows95 Credits +
    + +

    Emulation Engine

    +

    + None of this would be possible without the people working on v86, in particular Fabian Hemmer aka copy. + You can visit his website at copy.sh. 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. +

    + +

    Electron

    +

    + 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). +

    +
    +
    + + diff --git a/static/www/help.htm b/static/www/help.htm new file mode 100644 index 0000000..86b98dc --- /dev/null +++ b/static/www/help.htm @@ -0,0 +1,34 @@ + + + windows95 Help + + + + + + +
    + + windows95 Help +
    + +

    MS-DOS Display Issues

    +

    If MS-DOS seems to mess up the screen:

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

    (Thanks to @DisplacedGamers for that wisdom)

    + +

    windows95 Stuck in Bad State

    +

    If windows95 becomes unresponsive or stuck:

    +
      +
    1. On the app's home screen, select "Settings" in the lower menu
    2. +
    3. Delete your machine's state before starting it again
    4. +
    5. This should resolve the issue when you restart
    6. +
    +
    +
    + + diff --git a/static/www/home.htm b/static/www/home.htm new file mode 100644 index 0000000..4c13bf6 --- /dev/null +++ b/static/www/home.htm @@ -0,0 +1,37 @@ + + + Welcome to Windows 95! + + + + + + +
    +
    + + + Welcome to Windows 95! + + +
    + + +

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

    + +

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

    + +
    + + The Internet! +
    + +

    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.

    +
    + +
    + Last updated: 2025 +
    +
    + + diff --git a/static/www/index.htm b/static/www/index.htm new file mode 100644 index 0000000..3d201d1 --- /dev/null +++ b/static/www/index.htm @@ -0,0 +1,21 @@ + + + + Welcome to Windows 95! + + + + + + + <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> + + + + diff --git a/static/www/navigation.htm b/static/www/navigation.htm new file mode 100644 index 0000000..126246e --- /dev/null +++ b/static/www/navigation.htm @@ -0,0 +1,46 @@ + + + Navigation + + + + + + + + +
    + + Navigation + +
    + + Home + +
    + + Apps & Games + +
    + + Help + +
    + + Credits + +
    +
    +
    +

    + + Best viewed with
    + Internet Explorer 5.5 and windows95 +
    +

    + + + +
    + + diff --git a/tools/parcel-build.js b/tools/parcel-build.js index 597dc9f..4190a80 100644 --- a/tools/parcel-build.js +++ b/tools/parcel-build.js @@ -18,6 +18,7 @@ async function copyLib() { 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) diff --git a/tsconfig.json b/tsconfig.json index 2fc3ca1..4fe8fd4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,12 +8,11 @@ "preserveConstEnums": true, "sourceMap": true, "lib": [ - "es2017", + "es2021", "dom" ], "noImplicitAny": true, "noImplicitReturns": true, - "suppressImplicitAnyIndexErrors": true, "strictNullChecks": true, "noUnusedLocals": true, "noImplicitThis": true,