mirror of
https://github.com/felixrieseberg/windows95.git
synced 2026-05-09 00:24:09 +00:00
Now with working network
This commit is contained in:
21
HELP.md
21
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
|
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.
|
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
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
export const CONSTANTS = {
|
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
|
IMAGE_DEFAULT_SIZE: 1073741824, // 1GB
|
||||||
DEFAULT_STATE_PATH: path.join(__dirname, "../../images/default-state.bin"),
|
DEFAULT_STATE_PATH: path.join(__dirname, "../../images/default-state.bin"),
|
||||||
};
|
};
|
||||||
|
|||||||
15
src/main/fileserver/encoding.ts
Normal file
15
src/main/fileserver/encoding.ts
Normal 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">`;
|
||||||
|
}
|
||||||
157
src/main/fileserver/fileserver.ts
Normal file
157
src/main/fileserver/fileserver.ts
Normal 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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
72
src/main/fileserver/hide-files.ts
Normal file
72
src/main/fileserver/hide-files.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
120
src/main/fileserver/page-directory-listing.ts
Normal file
120
src/main/fileserver/page-directory-listing.ts
Normal 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];
|
||||||
|
}
|
||||||
22
src/main/fileserver/page-error.ts
Normal file
22
src/main/fileserver/page-error.ts
Normal 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { IPC_COMMANDS } from "../constants";
|
|||||||
|
|
||||||
export function setupIpcListeners() {
|
export function setupIpcListeners() {
|
||||||
ipcMain.handle(IPC_COMMANDS.GET_STATE_PATH, () => {
|
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, () => {
|
ipcMain.handle(IPC_COMMANDS.APP_QUIT, () => {
|
||||||
|
|||||||
3
src/main/logging.ts
Normal file
3
src/main/logging.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function log(message: string, ...args: unknown[]) {
|
||||||
|
console.log(`[${new Date().toLocaleString()}] ${message}`, ...args);
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import { setupUpdates } from "./update";
|
|||||||
import { getOrCreateWindow } from "./windows";
|
import { getOrCreateWindow } from "./windows";
|
||||||
import { setupMenu } from "./menu";
|
import { setupMenu } from "./menu";
|
||||||
import { setupIpcListeners } from "./ipc";
|
import { setupIpcListeners } from "./ipc";
|
||||||
|
import { setupSession } from "./session";
|
||||||
|
import { setupFileServer } from './fileserver/fileserver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the app's "ready" event. This is essentially
|
* Handle the app's "ready" event. This is essentially
|
||||||
@@ -15,11 +17,13 @@ import { setupIpcListeners } from "./ipc";
|
|||||||
export async function onReady() {
|
export async function onReady() {
|
||||||
if (!isDevMode()) process.env.NODE_ENV = "production";
|
if (!isDevMode()) process.env.NODE_ENV = "production";
|
||||||
|
|
||||||
|
setupSession();
|
||||||
setupIpcListeners();
|
setupIpcListeners();
|
||||||
getOrCreateWindow();
|
getOrCreateWindow();
|
||||||
setupAboutPanel();
|
setupAboutPanel();
|
||||||
setupMenu();
|
setupMenu();
|
||||||
setupUpdates();
|
setupUpdates();
|
||||||
|
setupFileServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { app, shell, Menu, BrowserWindow, ipcMain } from "electron";
|
|||||||
import { clearCaches } from "../cache";
|
import { clearCaches } from "../cache";
|
||||||
import { IPC_COMMANDS } from "../constants";
|
import { IPC_COMMANDS } from "../constants";
|
||||||
import { isDevMode } from "../utils/devmode";
|
import { isDevMode } from "../utils/devmode";
|
||||||
|
import { log } from "./logging";
|
||||||
|
|
||||||
const LINKS = {
|
const LINKS = {
|
||||||
homepage: "https://www.twitter.com/felixrieseberg",
|
homepage: "https://www.twitter.com/felixrieseberg",
|
||||||
@@ -26,10 +27,10 @@ function send(cmd: string) {
|
|||||||
const windows = BrowserWindow.getAllWindows();
|
const windows = BrowserWindow.getAllWindows();
|
||||||
|
|
||||||
if (windows[0]) {
|
if (windows[0]) {
|
||||||
console.log(`Sending "${cmd}"`);
|
log(`Sending "${cmd}"`);
|
||||||
windows[0].webContents.send(cmd);
|
windows[0].webContents.send(cmd);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Tried to send "${cmd}", but could not find window`);
|
log(`Tried to send "${cmd}", but could not find window`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
src/main/session.ts
Normal file
20
src/main/session.ts
Normal 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
72
src/main/settings.ts
Normal 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();
|
||||||
@@ -24,9 +24,6 @@ export function getOrCreateWindow(): BrowserWindow {
|
|||||||
mainWindow.webContents.on("will-navigate", (event, url) =>
|
mainWindow.webContents.on("will-navigate", (event, url) =>
|
||||||
handleNavigation(event, url),
|
handleNavigation(event, url),
|
||||||
);
|
);
|
||||||
mainWindow.webContents.on("new-window", (event, url) =>
|
|
||||||
handleNavigation(event, url),
|
|
||||||
);
|
|
||||||
|
|
||||||
mainWindow.on("closed", () => {
|
mainWindow.on("closed", () => {
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
|
|||||||
@@ -279,8 +279,13 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
|||||||
const options = {
|
const options = {
|
||||||
wasm_path: path.join(__dirname, "build/v86.wasm"),
|
wasm_path: path.join(__dirname, "build/v86.wasm"),
|
||||||
memory_size: 128 * 1024 * 1024,
|
memory_size: 128 * 1024 * 1024,
|
||||||
vga_memory_size: 32 * 1024 * 1024,
|
vga_memory_size: 64 * 1024 * 1024,
|
||||||
screen_container: document.getElementById("emulator"),
|
screen_container: document.getElementById("emulator"),
|
||||||
|
preserve_mac_from_state_image: true,
|
||||||
|
net_device: {
|
||||||
|
relay_url: "fetch",
|
||||||
|
type: "ne2k",
|
||||||
|
},
|
||||||
bios: {
|
bios: {
|
||||||
url: path.join(__dirname, "../../bios/seabios.bin"),
|
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() {
|
private async resetEmulator() {
|
||||||
this.isResetting = true;
|
this.isResetting = true;
|
||||||
|
|||||||
20
static/www/apps.htm
Normal file
20
static/www/apps.htm
Normal 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>
|
||||||
BIN
static/www/buttons/macos.gif
Normal file
BIN
static/www/buttons/macos.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
static/www/buttons/madewithelectron.gif
Normal file
BIN
static/www/buttons/madewithelectron.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
static/www/buttons/msie.gif
Normal file
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
31
static/www/credits.htm
Normal 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
34
static/www/help.htm
Normal 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
37
static/www/home.htm
Normal 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>
|
||||||
21
static/www/index.htm
Normal file
21
static/www/index.htm
Normal 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
46
static/www/navigation.htm
Normal 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>
|
||||||
@@ -18,6 +18,7 @@ async function copyLib() {
|
|||||||
|
|
||||||
let patchedLibv86 = libv86.replace('k.load_file="undefined"===typeof XMLHttpRequest?pa:qa', 'k.load_file=pa')
|
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('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)
|
fs.writeFileSync(libv86path, patchedLibv86)
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,11 @@
|
|||||||
"preserveConstEnums": true,
|
"preserveConstEnums": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"es2017",
|
"es2021",
|
||||||
"dom"
|
"dom"
|
||||||
],
|
],
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"suppressImplicitAnyIndexErrors": true,
|
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noImplicitThis": true,
|
"noImplicitThis": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user