feat: Move to TypeScript
This commit is contained in:
parent
b7aa6a760d
commit
241606d097
|
@ -9,7 +9,7 @@
|
||||||
"package": "electron-forge package",
|
"package": "electron-forge package",
|
||||||
"make": "electron-forge make",
|
"make": "electron-forge make",
|
||||||
"publish": "electron-forge publish",
|
"publish": "electron-forge publish",
|
||||||
"lint": "standard \"src/**/*.js\"",
|
"lint": "prettier --write src/**/*.{ts,tsx}",
|
||||||
"less": "node ./tools/lessc.js"
|
"less": "node ./tools/lessc.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
|
25
src/cache.js
25
src/cache.js
|
@ -1,25 +0,0 @@
|
||||||
const { session } = require('electron')
|
|
||||||
|
|
||||||
const clearCaches = async () => {
|
|
||||||
await clearCache()
|
|
||||||
await clearStorageData()
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearCache = () => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
session.defaultSession.clearCache(resolve)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearStorageData = () => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
session.defaultSession.clearStorageData({
|
|
||||||
storages: 'appcache, cookies, filesystem, indexdb, localstorage, shadercache, websql, serviceworkers',
|
|
||||||
quotas: 'temporary, persistent, syncable'
|
|
||||||
}, resolve)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
clearCaches
|
|
||||||
}
|
|
25
src/cache.ts
Normal file
25
src/cache.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { session } from 'electron';
|
||||||
|
|
||||||
|
export async function clearCaches() {
|
||||||
|
await clearCache()
|
||||||
|
await clearStorageData()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearCache() {
|
||||||
|
if (session.defaultSession) {
|
||||||
|
await session.defaultSession.clearCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearStorageData() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!session.defaultSession) {
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
session.defaultSession.clearStorageData({
|
||||||
|
storages: [ 'appcache', 'cookies', 'filesystem', 'indexdb', 'localstorage', 'shadercache', 'websql', 'serviceworkers' ],
|
||||||
|
quotas: [ 'temporary', 'persistent', 'syncable' ]
|
||||||
|
}, resolve)
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,24 +1,19 @@
|
||||||
const { remote, app } = require('electron')
|
import { remote, app } from 'electron';
|
||||||
const path = require('path')
|
import * as path from 'path';
|
||||||
|
|
||||||
const _app = app || remote.app
|
const _app = app || remote.app
|
||||||
|
|
||||||
const CONSTANTS = {
|
export const CONSTANTS = {
|
||||||
IMAGE_PATH: path.join(__dirname, 'images/windows95.img'),
|
IMAGE_PATH: path.join(__dirname, '../../images/windows95.img'),
|
||||||
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'),
|
||||||
STATE_PATH: path.join(_app.getPath('userData'), 'state-v2.bin')
|
STATE_PATH: path.join(_app.getPath('userData'), 'state-v2.bin')
|
||||||
}
|
}
|
||||||
|
|
||||||
const IPC_COMMANDS = {
|
export const IPC_COMMANDS = {
|
||||||
TOGGLE_INFO: 'TOGGLE_INFO',
|
TOGGLE_INFO: 'TOGGLE_INFO',
|
||||||
MACHINE_RESTART: 'MACHINE_RESTART',
|
MACHINE_RESTART: 'MACHINE_RESTART',
|
||||||
MACHINE_RESET: 'MACHINE_RESET',
|
MACHINE_RESET: 'MACHINE_RESET',
|
||||||
MACHINE_CTRL_ALT_DEL: 'MACHINE_CTRL_ALT_DEL',
|
MACHINE_CTRL_ALT_DEL: 'MACHINE_CTRL_ALT_DEL',
|
||||||
SHOW_DISK_IMAGE: 'SHOW_DISK_IMAGE'
|
SHOW_DISK_IMAGE: 'SHOW_DISK_IMAGE'
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
CONSTANTS,
|
|
||||||
IPC_COMMANDS
|
|
||||||
}
|
|
36
src/es6.js
36
src/es6.js
|
@ -1,36 +0,0 @@
|
||||||
const { protocol } = require('electron')
|
|
||||||
const fs = require('fs-extra')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const ES6_PATH = path.join(__dirname, 'renderer')
|
|
||||||
|
|
||||||
protocol.registerSchemesAsPrivileged([
|
|
||||||
{
|
|
||||||
scheme: 'es6',
|
|
||||||
privileges: {
|
|
||||||
standard: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
async function setupProtocol () {
|
|
||||||
protocol.registerBufferProtocol('es6', async (req, cb) => {
|
|
||||||
console.log(req)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const filePath = path.join(ES6_PATH, req.url.replace('es6://', ''))
|
|
||||||
.replace('.js/', '.js')
|
|
||||||
.replace('.js\\', '.js')
|
|
||||||
|
|
||||||
const fileContent = await fs.readFile(filePath)
|
|
||||||
|
|
||||||
cb({ mimeType: 'text/javascript', data: fileContent }) // eslint-disable-line
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
setupProtocol
|
|
||||||
}
|
|
55
src/index.js
55
src/index.js
|
@ -1,55 +0,0 @@
|
||||||
const { app, BrowserWindow } = require('electron')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const { createMenu } = require('./menu')
|
|
||||||
const { setupProtocol } = require('./es6')
|
|
||||||
|
|
||||||
if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (app.isPackaged) {
|
|
||||||
require('update-electron-app')({
|
|
||||||
repo: 'felixrieseberg/windows95',
|
|
||||||
updateInterval: '1 hour'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let mainWindow
|
|
||||||
|
|
||||||
const createWindow = () => {
|
|
||||||
// Create the browser window.
|
|
||||||
mainWindow = new BrowserWindow({
|
|
||||||
width: 1024,
|
|
||||||
height: 768,
|
|
||||||
useContentSize: true,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: false,
|
|
||||||
preload: path.join(__dirname, 'preload.js')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
mainWindow.loadURL(`file://${__dirname}/renderer/index.html`)
|
|
||||||
|
|
||||||
mainWindow.on('closed', () => {
|
|
||||||
mainWindow = null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on('ready', async () => {
|
|
||||||
await setupProtocol()
|
|
||||||
await createMenu()
|
|
||||||
|
|
||||||
createWindow()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Quit when all windows are closed.
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
app.quit()
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (mainWindow === null) {
|
|
||||||
createWindow()
|
|
||||||
}
|
|
||||||
})
|
|
28
src/main/about-panel.ts
Normal file
28
src/main/about-panel.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { AboutPanelOptionsOptions, app } from "electron";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets Fiddle's About panel options on Linux and macOS
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function setupAboutPanel(): void {
|
||||||
|
if (process.platform === "win32") return;
|
||||||
|
|
||||||
|
const options: AboutPanelOptionsOptions = {
|
||||||
|
applicationName: "windows95",
|
||||||
|
applicationVersion: app.getVersion(),
|
||||||
|
version: process.versions.electron,
|
||||||
|
copyright: "Felix Rieseberg"
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (process.platform) {
|
||||||
|
case "linux":
|
||||||
|
options.website = "https://github.com/felixrieseberg/windows95";
|
||||||
|
case "darwin":
|
||||||
|
options.credits = "https://github.com/felixrieseberg/windows95";
|
||||||
|
default:
|
||||||
|
// fallthrough
|
||||||
|
}
|
||||||
|
|
||||||
|
app.setAboutPanelOptions(options);
|
||||||
|
}
|
67
src/main/main.ts
Normal file
67
src/main/main.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { app } from "electron";
|
||||||
|
|
||||||
|
import { isDevMode } from "../utils/devmode";
|
||||||
|
import { setupAboutPanel } from "./about-panel";
|
||||||
|
import { shouldQuit } from "./squirrel";
|
||||||
|
import { setupUpdates } from "./update";
|
||||||
|
import { createWindow } from "./windows";
|
||||||
|
import { setupMenu } from "./menu";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the app's "ready" event. This is essentially
|
||||||
|
* the method that takes care of booting the application.
|
||||||
|
*/
|
||||||
|
export async function onReady() {
|
||||||
|
if (!isDevMode()) process.env.NODE_ENV = "production";
|
||||||
|
|
||||||
|
createWindow();
|
||||||
|
setupAboutPanel();
|
||||||
|
setupMenu();
|
||||||
|
setupUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the "before-quit" event
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export function onBeforeQuit() {
|
||||||
|
(global as any).isQuitting = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All windows have been closed, quit on anything but
|
||||||
|
* macOS.
|
||||||
|
*/
|
||||||
|
export function onWindowsAllClosed() {
|
||||||
|
// On OS X it is common for applications and their menu bar
|
||||||
|
// to stay active until the user quits explicitly with Cmd + Q
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main method - and the first function to run
|
||||||
|
* when Fiddle is launched.
|
||||||
|
*
|
||||||
|
* Exported for testing purposes.
|
||||||
|
*/
|
||||||
|
export function main() {
|
||||||
|
// Handle creating/removing shortcuts on Windows when
|
||||||
|
// installing/uninstalling.
|
||||||
|
if (shouldQuit()) {
|
||||||
|
app.quit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the app's name
|
||||||
|
app.setName("windows95");
|
||||||
|
|
||||||
|
// Launch
|
||||||
|
app.on("ready", onReady);
|
||||||
|
app.on("before-quit", onBeforeQuit);
|
||||||
|
app.on("window-all-closed", onWindowsAllClosed);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
197
src/main/menu.ts
Normal file
197
src/main/menu.ts
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
import { app, shell, Menu, BrowserWindow } from "electron";
|
||||||
|
|
||||||
|
import { clearCaches } from "../cache";
|
||||||
|
import { IPC_COMMANDS } from "../constants";
|
||||||
|
import { isDevMode } from "../utils/devmode";
|
||||||
|
|
||||||
|
const LINKS = {
|
||||||
|
homepage: "https://www.twitter.com/felixrieseberg",
|
||||||
|
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"
|
||||||
|
};
|
||||||
|
|
||||||
|
function send(cmd: string) {
|
||||||
|
const windows = BrowserWindow.getAllWindows();
|
||||||
|
|
||||||
|
if (windows[0]) {
|
||||||
|
windows[0].webContents.send(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupMenu() {
|
||||||
|
const template: Array<Electron.MenuItemConstructorOptions> = [
|
||||||
|
{
|
||||||
|
label: "View",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Toggle Full Screen",
|
||||||
|
accelerator: (function() {
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
return "Ctrl+Command+F";
|
||||||
|
} else {
|
||||||
|
return "F11";
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
click: function(_item, focusedWindow) {
|
||||||
|
if (focusedWindow) {
|
||||||
|
focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Toggle Developer Tools",
|
||||||
|
accelerator: (function() {
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
return "Alt+Command+I";
|
||||||
|
} else {
|
||||||
|
return "Ctrl+Shift+I";
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
click: function(_item, focusedWindow) {
|
||||||
|
if (focusedWindow) {
|
||||||
|
focusedWindow.webContents.toggleDevTools();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Toggle Emulator Info",
|
||||||
|
click: () => send(IPC_COMMANDS.TOGGLE_INFO)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "reload"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "editMenu",
|
||||||
|
visible: isDevMode()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Window",
|
||||||
|
role: "window",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Minimize",
|
||||||
|
accelerator: "CmdOrCtrl+M",
|
||||||
|
role: "minimize"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Close",
|
||||||
|
accelerator: "CmdOrCtrl+W",
|
||||||
|
role: "close"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Machine",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Send Ctrl+Alt+Del",
|
||||||
|
click: () => send(IPC_COMMANDS.MACHINE_CTRL_ALT_DEL)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Restart",
|
||||||
|
click: () => send(IPC_COMMANDS.MACHINE_RESTART)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reset",
|
||||||
|
click: () => send(IPC_COMMANDS.MACHINE_RESET)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Go to Disk Image",
|
||||||
|
click: () => send(IPC_COMMANDS.SHOW_DISK_IMAGE)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Help",
|
||||||
|
role: "help",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Author",
|
||||||
|
click: () => shell.openExternal(LINKS.homepage)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "windows95 on GitHub",
|
||||||
|
click: () => shell.openExternal(LINKS.repo)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Help",
|
||||||
|
click: () => shell.openExternal(LINKS.help)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Troubleshooting",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Clear Cache and Restart",
|
||||||
|
async click() {
|
||||||
|
await clearCaches();
|
||||||
|
|
||||||
|
app.relaunch();
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
template.unshift({
|
||||||
|
label: "windows95",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
role: "about"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "services"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Hide windows95",
|
||||||
|
accelerator: "Command+H",
|
||||||
|
role: "hide"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Hide Others",
|
||||||
|
accelerator: "Command+Shift+H",
|
||||||
|
role: "hideothers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "unhide"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Quit",
|
||||||
|
accelerator: "Command+Q",
|
||||||
|
click() {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu.setApplicationMenu(Menu.buildFromTemplate(template as any));
|
||||||
|
}
|
3
src/main/squirrel.ts
Normal file
3
src/main/squirrel.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export function shouldQuit() {
|
||||||
|
return require("electron-squirrel-startup");
|
||||||
|
}
|
10
src/main/update.ts
Normal file
10
src/main/update.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { app } from "electron";
|
||||||
|
|
||||||
|
export function setupUpdates() {
|
||||||
|
if (app.isPackaged) {
|
||||||
|
require("update-electron-app")({
|
||||||
|
repo: "felixrieseberg/windows95",
|
||||||
|
updateInterval: "1 hour"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
21
src/main/windows.ts
Normal file
21
src/main/windows.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { BrowserWindow } from "electron";
|
||||||
|
|
||||||
|
let mainWindow;
|
||||||
|
|
||||||
|
export function createWindow() {
|
||||||
|
// Create the browser window.
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1024,
|
||||||
|
height: 768,
|
||||||
|
useContentSize: true,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.loadFile("./dist/static/index.html");
|
||||||
|
|
||||||
|
mainWindow.on("closed", () => {
|
||||||
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
}
|
182
src/menu.js
182
src/menu.js
|
@ -1,182 +0,0 @@
|
||||||
const { app, shell, Menu, BrowserWindow } = require('electron')
|
|
||||||
|
|
||||||
const { clearCaches } = require('./cache')
|
|
||||||
const { IPC_COMMANDS } = require('./constants')
|
|
||||||
|
|
||||||
const LINKS = {
|
|
||||||
homepage: 'https://www.twitter.com/felixrieseberg',
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
|
|
||||||
function send (cmd) {
|
|
||||||
const windows = BrowserWindow.getAllWindows()
|
|
||||||
|
|
||||||
if (windows[0]) {
|
|
||||||
windows[0].webContents.send(cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createMenu () {
|
|
||||||
const template = [
|
|
||||||
{
|
|
||||||
label: 'View',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'Toggle Full Screen',
|
|
||||||
accelerator: (function () {
|
|
||||||
if (process.platform === 'darwin') { return 'Ctrl+Command+F' } else { return 'F11' }
|
|
||||||
})(),
|
|
||||||
click: function (_item, focusedWindow) {
|
|
||||||
if (focusedWindow) { focusedWindow.setFullScreen(!focusedWindow.isFullScreen()) }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Toggle Developer Tools',
|
|
||||||
accelerator: (function () {
|
|
||||||
if (process.platform === 'darwin') { return 'Alt+Command+I' } else { return 'Ctrl+Shift+I' }
|
|
||||||
})(),
|
|
||||||
click: function (_item, focusedWindow) {
|
|
||||||
if (focusedWindow) { focusedWindow.toggleDevTools() }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Toggle Emulator Info',
|
|
||||||
click: () => send(IPC_COMMANDS.TOGGLE_INFO)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Window',
|
|
||||||
role: 'window',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'Minimize',
|
|
||||||
accelerator: 'CmdOrCtrl+M',
|
|
||||||
role: 'minimize'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Close',
|
|
||||||
accelerator: 'CmdOrCtrl+W',
|
|
||||||
role: 'close'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Machine',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'Send Ctrl+Alt+Del',
|
|
||||||
click: () => send(IPC_COMMANDS.MACHINE_CTRL_ALT_DEL)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Restart',
|
|
||||||
click: () => send(IPC_COMMANDS.MACHINE_RESTART)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Reset',
|
|
||||||
click: () => send(IPC_COMMANDS.MACHINE_RESET)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Go to Disk Image',
|
|
||||||
click: () => send(IPC_COMMANDS.SHOW_DISK_IMAGE)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Help',
|
|
||||||
role: 'help',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'Author',
|
|
||||||
click: () => shell.openExternal(LINKS.homepage)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'windows95 on GitHub',
|
|
||||||
click: () => shell.openExternal(LINKS.repo)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Help',
|
|
||||||
click: () => shell.openExternal(LINKS.help)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Troubleshooting',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'Clear Cache and Restart',
|
|
||||||
async click () {
|
|
||||||
await clearCaches()
|
|
||||||
|
|
||||||
app.relaunch()
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
template.unshift({
|
|
||||||
label: 'windows95',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'About windows95',
|
|
||||||
role: 'about'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Services',
|
|
||||||
role: 'services',
|
|
||||||
submenu: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Hide windows95',
|
|
||||||
accelerator: 'Command+H',
|
|
||||||
role: 'hide'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Hide Others',
|
|
||||||
accelerator: 'Command+Shift+H',
|
|
||||||
role: 'hideothers'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Show All',
|
|
||||||
role: 'unhide'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Quit',
|
|
||||||
accelerator: 'Command+Q',
|
|
||||||
click () {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
createMenu
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
const { remote, shell, ipcRenderer } = require('electron')
|
|
||||||
const path = require('path')
|
|
||||||
const EventEmitter = require('events')
|
|
||||||
|
|
||||||
const { resetState, restoreState, saveState } = require('./state')
|
|
||||||
const { getDiskImageSize } = require('./utils/disk-image-size')
|
|
||||||
const { IPC_COMMANDS, CONSTANTS } = require('./constants')
|
|
||||||
|
|
||||||
class Windows95 extends EventEmitter {
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
this.CONSTANTS = CONSTANTS
|
|
||||||
this.IPC_COMMANDS = IPC_COMMANDS
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
this.getDiskImageSize = getDiskImageSize
|
|
||||||
this.restoreState = restoreState
|
|
||||||
this.resetState = resetState
|
|
||||||
this.saveState = saveState
|
|
||||||
|
|
||||||
Object.keys(IPC_COMMANDS).forEach((command) => {
|
|
||||||
ipcRenderer.on(command, (...args) => {
|
|
||||||
this.emit(command, args)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
showDiskImage () {
|
|
||||||
const imagePath = path.join(__dirname, 'images/windows95.img')
|
|
||||||
.replace('app.asar', 'app.asar.unpacked')
|
|
||||||
|
|
||||||
shell.showItemInFolder(imagePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
quit () {
|
|
||||||
remote.app.quit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.windows95 = new Windows95()
|
|
|
@ -1,9 +0,0 @@
|
||||||
export function setupState () {
|
|
||||||
window.appState = {
|
|
||||||
isResetting: false,
|
|
||||||
isQuitting: false,
|
|
||||||
cursorCaptured: false,
|
|
||||||
floppyFile: null,
|
|
||||||
bootFresh: false
|
|
||||||
}
|
|
||||||
}
|
|
34
src/renderer/app.tsx
Normal file
34
src/renderer/app.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* The top-level class controlling the whole app. This is *not* a React component,
|
||||||
|
* but it does eventually render all components.
|
||||||
|
*
|
||||||
|
* @class App
|
||||||
|
*/
|
||||||
|
export class App {
|
||||||
|
/**
|
||||||
|
* Initial setup call, loading Monaco and kicking off the React
|
||||||
|
* render process.
|
||||||
|
*/
|
||||||
|
public async setup(): Promise<void | Element> {
|
||||||
|
const React = await import("react");
|
||||||
|
const { render } = await import("react-dom");
|
||||||
|
const { Emulator } = await import("./emulator");
|
||||||
|
|
||||||
|
const className = `${process.platform}`;
|
||||||
|
const app = (
|
||||||
|
<div className={className}>
|
||||||
|
<Emulator />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const rendered = render(app, document.getElementById("app"));
|
||||||
|
|
||||||
|
return rendered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window["win95"] = window["win95"] || {
|
||||||
|
app: new App()
|
||||||
|
};
|
||||||
|
|
||||||
|
window["win95"].app.setup();
|
|
@ -1,56 +0,0 @@
|
||||||
const $ = document.querySelector.bind(document)
|
|
||||||
const $$ = document.querySelectorAll.bind(document)
|
|
||||||
|
|
||||||
export function setupButtons (start) {
|
|
||||||
// Sections
|
|
||||||
$('a#start').addEventListener('click', () => setVisibleSection('start'))
|
|
||||||
$('a#floppy').addEventListener('click', () => setVisibleSection('floppy'))
|
|
||||||
$('a#state').addEventListener('click', () => setVisibleSection('state'))
|
|
||||||
$('a#disk').addEventListener('click', () => setVisibleSection('disk'))
|
|
||||||
|
|
||||||
// Start
|
|
||||||
$('.btn-start').addEventListener('click', start)
|
|
||||||
|
|
||||||
// Disk Image
|
|
||||||
$('#disk-image-show').addEventListener('click', () => windows95.showDiskImage())
|
|
||||||
|
|
||||||
// Reset
|
|
||||||
$('#reset').addEventListener('click', () => windows95.resetState())
|
|
||||||
|
|
||||||
$('#discard-state').addEventListener('click', () => {
|
|
||||||
window.appState.bootFresh = true
|
|
||||||
|
|
||||||
start()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Floppy
|
|
||||||
$('#floppy-select').addEventListener('click', () => {
|
|
||||||
$('#floppy-input').click()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Floppy (Hidden Input)
|
|
||||||
$('#floppy-input').addEventListener('change', (event) => {
|
|
||||||
window.appState.floppyFile = event.target.files && event.target.files.length > 0
|
|
||||||
? event.target.files[0]
|
|
||||||
: null
|
|
||||||
|
|
||||||
if (window.appState.floppyFile) {
|
|
||||||
$('#floppy-path').innerHTML = `Inserted Floppy Disk: ${window.appState.floppyFile.path}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toggleSetup (forceTo) {
|
|
||||||
const buttonElements = $('#setup')
|
|
||||||
|
|
||||||
if (buttonElements.style.display !== 'none' || forceTo === false) {
|
|
||||||
buttonElements.style.display = 'none'
|
|
||||||
} else {
|
|
||||||
buttonElements.style.display = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setVisibleSection(id = '') {
|
|
||||||
$$(`section`).forEach((s) => s.classList.remove('visible'))
|
|
||||||
$(`section#section-${id}`).classList.add('visible')
|
|
||||||
}
|
|
78
src/renderer/card-floppy.tsx
Normal file
78
src/renderer/card-floppy.tsx
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface CardFloppyProps {
|
||||||
|
setFloppyPath: (path: string) => void;
|
||||||
|
floppyPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CardFloppy extends React.Component<CardFloppyProps, {}> {
|
||||||
|
constructor(props: CardFloppyProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.onChange = this.onChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h2 className="card-title">Floppy Drive</h2>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<input
|
||||||
|
id="floppy-input"
|
||||||
|
type="file"
|
||||||
|
onChange={this.onChange}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
windows95 comes with a virtual floppy drive. If you have floppy
|
||||||
|
disk images in the "img" format, you can mount them here.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Back in the 90s and before CD-ROM became a popular format,
|
||||||
|
software was typically distributed on floppy disks. Some
|
||||||
|
developers have since released their apps or games for free,
|
||||||
|
usually on virtual floppy disks using the "img" format.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Once you've mounted a disk image, you might have to reboot your
|
||||||
|
virtual windows95 machine from scratch.
|
||||||
|
</p>
|
||||||
|
<p id="floppy-path">
|
||||||
|
{this.props.floppyPath
|
||||||
|
? `Inserted Floppy Disk: ${this.props.floppyPath}`
|
||||||
|
: `No floppy mounted`}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
id="floppy-select"
|
||||||
|
className="btn"
|
||||||
|
onClick={() =>
|
||||||
|
(document.querySelector("#floppy-input") as any).click()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Mount floppy disk
|
||||||
|
</button>
|
||||||
|
<button id="floppy-reboot" className="btn">
|
||||||
|
Reboot from scratch
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const floppyFile =
|
||||||
|
event.target.files && event.target.files.length > 0
|
||||||
|
? event.target.files[0]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (floppyFile) {
|
||||||
|
this.props.setFloppyPath(floppyFile.path);
|
||||||
|
} else {
|
||||||
|
console.log(`Floppy: Input changed but no file selected`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
src/renderer/card-start.tsx
Normal file
23
src/renderer/card-start.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface CardStartProps {
|
||||||
|
startEmulator: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CardStart extends React.Component<CardStartProps, {}> {
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<section id="section-start" className="visible">
|
||||||
|
<div
|
||||||
|
className="btn btn-start"
|
||||||
|
id="win95"
|
||||||
|
onClick={this.props.startEmulator}
|
||||||
|
>
|
||||||
|
Start Windows 95
|
||||||
|
<br />
|
||||||
|
<small>Hit ESC to lock or unlock your mouse</small>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
47
src/renderer/card-state.tsx
Normal file
47
src/renderer/card-state.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as fs from "fs-extra";
|
||||||
|
|
||||||
|
import { CONSTANTS } from "../constants";
|
||||||
|
|
||||||
|
export interface CardStateProps {
|
||||||
|
bootFromScratch: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CardState extends React.Component<CardStateProps, {}> {
|
||||||
|
constructor(props: CardStateProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.onReset = this.onReset.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h2 className="card-title">Machine State</h2>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<p>
|
||||||
|
windows95 stores any changes to your machine (like saved files) in
|
||||||
|
a state file. If you encounter any trouble, you can either reset
|
||||||
|
your state or boot Windows 95 from scratch.
|
||||||
|
</p>
|
||||||
|
<button className="btn" onClick={this.onReset}>
|
||||||
|
Reset state
|
||||||
|
</button>
|
||||||
|
<button className="btn" onClick={this.props.bootFromScratch}>
|
||||||
|
Reboot from scratch
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async onReset() {
|
||||||
|
if (fs.existsSync(CONSTANTS.STATE_PATH)) {
|
||||||
|
await fs.remove(CONSTANTS.STATE_PATH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
166
src/renderer/emulator-info.tsx
Normal file
166
src/renderer/emulator-info.tsx
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
interface EmulatorInfoProps {
|
||||||
|
toggleInfo: () => void;
|
||||||
|
emulator: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EmulatorInfoState {
|
||||||
|
cpu: number;
|
||||||
|
disk: string;
|
||||||
|
lastCounter: number;
|
||||||
|
lastTick: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmulatorInfo extends React.Component<
|
||||||
|
EmulatorInfoProps,
|
||||||
|
EmulatorInfoState
|
||||||
|
> {
|
||||||
|
private cpuInterval = -1;
|
||||||
|
|
||||||
|
constructor(props: EmulatorInfoProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.cpuCount = this.cpuCount.bind(this);
|
||||||
|
this.onIDEReadStart = this.onIDEReadStart.bind(this);
|
||||||
|
this.onIDEReadWriteEnd = this.onIDEReadWriteEnd.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
cpu: 0,
|
||||||
|
disk: "Idle",
|
||||||
|
lastCounter: 0,
|
||||||
|
lastTick: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { cpu, disk } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="status">
|
||||||
|
Disk: <span>{disk}</span>| CPU Speed: <span>{cpu}</span>|{" "}
|
||||||
|
<a href="#" onClick={this.props.toggleInfo}>
|
||||||
|
Hide
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this.uninstallListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The emulator starts whenever, so install or uninstall listeners
|
||||||
|
* at the right time
|
||||||
|
*
|
||||||
|
* @param newProps
|
||||||
|
*/
|
||||||
|
public componentDidUpdate(prevProps: EmulatorInfoProps) {
|
||||||
|
if (prevProps.emulator !== this.props.emulator) {
|
||||||
|
if (this.props.emulator) {
|
||||||
|
this.installListeners();
|
||||||
|
} else {
|
||||||
|
this.uninstallListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Let's start listening to what the emulator is up to.
|
||||||
|
*/
|
||||||
|
private installListeners() {
|
||||||
|
const { emulator } = this.props;
|
||||||
|
|
||||||
|
if (!emulator) {
|
||||||
|
console.log(
|
||||||
|
`Emulator info: Tried to install listeners, but emulator not defined yet.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU
|
||||||
|
if (this.cpuInterval > -1) {
|
||||||
|
clearInterval(this.cpuInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeScript think's we're using a Node.js setInterval. We're not.
|
||||||
|
this.cpuInterval = (setInterval(this.cpuCount, 500) as unknown) as number;
|
||||||
|
|
||||||
|
// Disk
|
||||||
|
emulator.add_listener("ide-read-start", this.onIDEReadStart);
|
||||||
|
emulator.add_listener("ide-read-end", this.onIDEReadWriteEnd);
|
||||||
|
emulator.add_listener("ide-write-end", this.onIDEReadWriteEnd);
|
||||||
|
|
||||||
|
// Screen
|
||||||
|
emulator.add_listener("screen-set-size-graphical", console.log);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop listening to the emulator.
|
||||||
|
*/
|
||||||
|
private uninstallListeners() {
|
||||||
|
const { emulator } = this.props;
|
||||||
|
|
||||||
|
if (!emulator) {
|
||||||
|
console.log(
|
||||||
|
`Emulator info: Tried to uninstall listeners, but emulator not defined yet.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU
|
||||||
|
if (this.cpuInterval > -1) {
|
||||||
|
clearInterval(this.cpuInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disk
|
||||||
|
emulator.remove_listener("ide-read-start", this.onIDEReadStart);
|
||||||
|
emulator.remove_listener("ide-read-end", this.onIDEReadWriteEnd);
|
||||||
|
emulator.remove_listener("ide-write-end", this.onIDEReadWriteEnd);
|
||||||
|
|
||||||
|
// Screen
|
||||||
|
emulator.remove_listener("screen-set-size-graphical", console.log);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The virtual IDE is handling read (start).
|
||||||
|
*/
|
||||||
|
private onIDEReadStart() {
|
||||||
|
this.requestIdle(() => this.setState({ disk: "Read" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The virtual IDE is handling read/write (end).
|
||||||
|
*/
|
||||||
|
private onIDEReadWriteEnd() {
|
||||||
|
this.requestIdle(() => this.setState({ disk: "Idle" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request an idle callback with a 3s timeout.
|
||||||
|
*
|
||||||
|
* @param fn
|
||||||
|
*/
|
||||||
|
private requestIdle(fn: () => void) {
|
||||||
|
(window as any).requestIdleCallback(fn, { timeout: 3000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates what's up with the virtual cpu.
|
||||||
|
*/
|
||||||
|
private cpuCount() {
|
||||||
|
const { lastCounter, lastTick } = this.state;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const instructionCounter = this.props.emulator.get_instruction_counter();
|
||||||
|
const ips = instructionCounter - lastCounter;
|
||||||
|
const deltaTime = now - lastTick;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
lastTick: now,
|
||||||
|
lastCounter: instructionCounter,
|
||||||
|
cpu: Math.round(ips / deltaTime)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
350
src/renderer/emulator.tsx
Normal file
350
src/renderer/emulator.tsx
Normal file
|
@ -0,0 +1,350 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as fs from "fs-extra";
|
||||||
|
import * as path from "path";
|
||||||
|
import { ipcRenderer, remote, shell } from "electron";
|
||||||
|
|
||||||
|
import { CONSTANTS, IPC_COMMANDS } from "../constants";
|
||||||
|
import { getDiskImageSize } from "../utils/disk-image-size";
|
||||||
|
import { CardStart } from "./card-start";
|
||||||
|
import { StartMenu } from "./start-menu";
|
||||||
|
import { CardFloppy } from "./card-floppy";
|
||||||
|
import { CardState } from "./card-state";
|
||||||
|
import { EmulatorInfo } from "./emulator-info";
|
||||||
|
|
||||||
|
export interface EmulatorState {
|
||||||
|
currentUiCard: string;
|
||||||
|
emulator?: any;
|
||||||
|
floppyFile?: string;
|
||||||
|
isBootingFresh: boolean;
|
||||||
|
isCursorCaptured: boolean;
|
||||||
|
isInfoDisplayed: boolean;
|
||||||
|
isRunning: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Emulator extends React.Component<{}, EmulatorState> {
|
||||||
|
private isQuitting = false;
|
||||||
|
private isResetting = false;
|
||||||
|
|
||||||
|
constructor(props: {}) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.startEmulator = this.startEmulator.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isBootingFresh: false,
|
||||||
|
isCursorCaptured: false,
|
||||||
|
isRunning: false,
|
||||||
|
currentUiCard: "start",
|
||||||
|
isInfoDisplayed: true
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setupInputListeners();
|
||||||
|
this.setupIpcListeners();
|
||||||
|
this.setupUnloadListeners();
|
||||||
|
|
||||||
|
if (document.location.hash.includes("AUTO_START")) {
|
||||||
|
this.startEmulator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We want to capture and release the mouse at appropriate times.
|
||||||
|
*/
|
||||||
|
public setupInputListeners() {
|
||||||
|
// ESC
|
||||||
|
document.onkeydown = evt => {
|
||||||
|
const { emulator, isCursorCaptured } = this.state;
|
||||||
|
|
||||||
|
evt = evt || window.event;
|
||||||
|
|
||||||
|
if (evt.keyCode === 27) {
|
||||||
|
if (isCursorCaptured) {
|
||||||
|
this.setState({ isCursorCaptured: false });
|
||||||
|
|
||||||
|
if (emulator) {
|
||||||
|
emulator.mouse_set_status(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.exitPointerLock();
|
||||||
|
} else {
|
||||||
|
this.setState({ isCursorCaptured: true });
|
||||||
|
|
||||||
|
if (emulator) {
|
||||||
|
emulator.lock_mouse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Click
|
||||||
|
document.addEventListener("click", () => {
|
||||||
|
if (!this.state.isCursorCaptured) {
|
||||||
|
this.setState({ isCursorCaptured: true });
|
||||||
|
this.state.emulator.mouse_set_status(true);
|
||||||
|
this.state.emulator.lock_mouse();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the emulator's state to disk during exit.
|
||||||
|
*/
|
||||||
|
public setupUnloadListeners() {
|
||||||
|
const handleClose = async () => {
|
||||||
|
await this.saveState();
|
||||||
|
this.isQuitting = true;
|
||||||
|
remote.app.quit();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.onbeforeunload = event => {
|
||||||
|
if (this.isQuitting) return;
|
||||||
|
if (this.isResetting) return;
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
event.preventDefault();
|
||||||
|
event.returnValue = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the various IPC messages sent to the renderer
|
||||||
|
* from the main process
|
||||||
|
*/
|
||||||
|
public setupIpcListeners() {
|
||||||
|
ipcRenderer.on(IPC_COMMANDS.MACHINE_CTRL_ALT_DEL, () => {
|
||||||
|
if (this.state.emulator && this.state.isRunning) {
|
||||||
|
this.state.emulator.keyboard_send_scancodes([
|
||||||
|
0x1d, // ctrl
|
||||||
|
0x38, // alt
|
||||||
|
0x53, // delete
|
||||||
|
|
||||||
|
// break codes
|
||||||
|
0x1d | 0x80,
|
||||||
|
0x38 | 0x80,
|
||||||
|
0x53 | 0x80
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on(IPC_COMMANDS.MACHINE_RESTART, () => {
|
||||||
|
if (this.state.emulator && this.state.isRunning) {
|
||||||
|
this.state.emulator.restart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on(IPC_COMMANDS.TOGGLE_INFO, () => {
|
||||||
|
this.setState({ isInfoDisplayed: !this.state.isInfoDisplayed });
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on(IPC_COMMANDS.SHOW_DISK_IMAGE, () => {
|
||||||
|
this.showDiskImage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the emulator isn't running, this is rendering the, erm, UI.
|
||||||
|
*
|
||||||
|
* 🤡
|
||||||
|
*/
|
||||||
|
public renderUI() {
|
||||||
|
const { isRunning, currentUiCard } = this.state;
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let card;
|
||||||
|
|
||||||
|
if (currentUiCard === "floppy") {
|
||||||
|
card = (
|
||||||
|
<CardFloppy
|
||||||
|
setFloppyPath={floppyFile => this.setState({ floppyFile })}
|
||||||
|
floppyPath={this.state.floppyFile}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (currentUiCard === "state") {
|
||||||
|
card = <CardState bootFromScratch={this.bootFromScratch} />;
|
||||||
|
} else {
|
||||||
|
card = <CardStart startEmulator={this.startEmulator} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{card}
|
||||||
|
<StartMenu
|
||||||
|
navigate={target => this.setState({ currentUiCard: target })}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yaknow, render things and stuff.
|
||||||
|
*/
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EmulatorInfo
|
||||||
|
emulator={this.state.emulator}
|
||||||
|
toggleInfo={() => {
|
||||||
|
this.setState({ isInfoDisplayed: !this.state.isInfoDisplayed });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{this.renderUI()}
|
||||||
|
<div id="emulator">
|
||||||
|
<div></div>
|
||||||
|
<canvas></canvas>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot the emulator without restoring state
|
||||||
|
*/
|
||||||
|
public bootFromScratch() {
|
||||||
|
this.setState({ isBootingFresh: true });
|
||||||
|
this.startEmulator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the disk image on disk
|
||||||
|
*/
|
||||||
|
public showDiskImage() {
|
||||||
|
const imagePath = path
|
||||||
|
.join(__dirname, "images/windows95.img")
|
||||||
|
.replace("app.asar", "app.asar.unpacked");
|
||||||
|
|
||||||
|
shell.showItemInFolder(imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the actual emulator
|
||||||
|
*/
|
||||||
|
public async startEmulator() {
|
||||||
|
document.body.classList.remove("paused");
|
||||||
|
|
||||||
|
const imageSize = await getDiskImageSize();
|
||||||
|
const options = {
|
||||||
|
memory_size: 128 * 1024 * 1024,
|
||||||
|
video_memory_size: 32 * 1024 * 1024,
|
||||||
|
screen_container: document.getElementById("emulator"),
|
||||||
|
bios: {
|
||||||
|
url: "../../bios/seabios.bin"
|
||||||
|
},
|
||||||
|
vga_bios: {
|
||||||
|
url: "../../bios/vgabios.bin"
|
||||||
|
},
|
||||||
|
hda: {
|
||||||
|
url: "../../images/windows95.img",
|
||||||
|
async: true,
|
||||||
|
size: imageSize
|
||||||
|
},
|
||||||
|
fda: {
|
||||||
|
buffer: this.state.floppyFile
|
||||||
|
},
|
||||||
|
boot_order: 0x132
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`Starting emulator with options`, options);
|
||||||
|
|
||||||
|
// New v86 instance
|
||||||
|
this.setState({
|
||||||
|
emulator: new V86Starter(options),
|
||||||
|
isRunning: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore state. We can't do this right away
|
||||||
|
// and randomly chose 500ms as the appropriate
|
||||||
|
// wait time (lol)
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (!this.state.isBootingFresh) {
|
||||||
|
this.restoreState();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.emulator.lock_mouse();
|
||||||
|
this.state.emulator.run();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the emulator by reloading the whole page (lol)
|
||||||
|
*/
|
||||||
|
public async resetEmulator() {
|
||||||
|
this.isResetting = true;
|
||||||
|
document.location.hash = `#AUTO_START`;
|
||||||
|
document.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take the emulators state and write it to disk. This is possibly
|
||||||
|
* a fairly big file.
|
||||||
|
*/
|
||||||
|
public async saveState(): Promise<void> {
|
||||||
|
const { emulator } = this.state;
|
||||||
|
|
||||||
|
if (!emulator || !emulator.save_state) {
|
||||||
|
console.log(`restoreState: No emulator present`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
emulator.save_state(async (error: Error, newState: ArrayBuffer) => {
|
||||||
|
if (error) {
|
||||||
|
console.warn(`saveState: Could not save state`, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.outputFile(CONSTANTS.STATE_PATH, Buffer.from(newState));
|
||||||
|
|
||||||
|
console.log(`saveState: Saved state to ${CONSTANTS.STATE_PATH}`);
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores state to the emulator.
|
||||||
|
*/
|
||||||
|
public restoreState() {
|
||||||
|
const { emulator } = this.state;
|
||||||
|
const state = this.getState();
|
||||||
|
|
||||||
|
// Nothing to do with if we don't have a state
|
||||||
|
if (!state) {
|
||||||
|
console.log(`restoreState: No state present, not restoring.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emulator) {
|
||||||
|
console.log(`restoreState: No emulator present`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.state.emulator.restore_state(state);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(
|
||||||
|
`State: Could not read state file. Maybe none exists?`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current machine's state - either what
|
||||||
|
* we have saved or alternatively the default state.
|
||||||
|
*
|
||||||
|
* @returns {ArrayBuffer}
|
||||||
|
*/
|
||||||
|
public getState(): ArrayBuffer | null {
|
||||||
|
const statePath = fs.existsSync(CONSTANTS.STATE_PATH)
|
||||||
|
? CONSTANTS.STATE_PATH
|
||||||
|
: CONSTANTS.DEFAULT_STATE_PATH;
|
||||||
|
|
||||||
|
if (fs.existsSync(statePath)) {
|
||||||
|
return fs.readFileSync(statePath).buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
2
src/renderer/global.d.ts
vendored
Normal file
2
src/renderer/global.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
declare const V86Starter: any;
|
||||||
|
declare const win95: any;
|
|
@ -1,75 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" class="windows95">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>Windows</title>
|
|
||||||
<script src="./lib/libv86.js"></script>
|
|
||||||
<link rel="stylesheet" href="style/vendor/95css.css">
|
|
||||||
<link rel="stylesheet" href="style/style.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="paused">
|
|
||||||
<div id="status">
|
|
||||||
Disk: <span id="disk-status">Idle</span>
|
|
||||||
| CPU Speed: <span id="cpu-status">0</span>
|
|
||||||
| <a href="#" id="toggle-status">Hide</a>
|
|
||||||
</div>
|
|
||||||
<div id="setup">
|
|
||||||
<section id="section-start" class="visible">
|
|
||||||
<div class="btn btn-start" id="win95">
|
|
||||||
Start Windows 95
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<small>Hit ESC to lock or unlock your mouse</small>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section id="section-floppy">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h2 class="card-title">Floppy Drive</h2>
|
|
||||||
</div class="card-header">
|
|
||||||
<div class="card-body">
|
|
||||||
<input id="floppy-input" type='file' style="display: none">
|
|
||||||
<p>windows95 comes with a virtual floppy drive. If you have floppy disk images in the "img" format, you can
|
|
||||||
mount them here.</p>
|
|
||||||
<p>Back in the 90s and before CD-ROM became a popular format, software was typically distributed on floppy
|
|
||||||
disks. Some developers have since released their apps or games for free, usually on virtual floppy disks
|
|
||||||
using the "img" format.</p>
|
|
||||||
<p>Once you've mounted a disk image, you might have to reboot your virtual windows95 machine from scratch.</p>
|
|
||||||
<p id="floppy-path">No floppy mounted</p>
|
|
||||||
<button id="floppy-select" class="btn">Mount floppy disk</button>
|
|
||||||
<button id="floppy-reboot" class="btn">Reboot from scratch</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section id="section-state">
|
|
||||||
|
|
||||||
</section>
|
|
||||||
<section id="section-disk">
|
|
||||||
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<nav class="nav nav-bottom">
|
|
||||||
<a href="#" id="start" class="nav-logo">
|
|
||||||
<img src="style/start.png" alt=""><span>Start</span>
|
|
||||||
</a>
|
|
||||||
<div class="nav-menu">
|
|
||||||
<a href="#" id="floppy" class="nav-link">Floppy Disk</a>
|
|
||||||
<a href="#" id="state" class="nav-link">Reset Machine</a>
|
|
||||||
<a href="#" id="disk" class="nav-link">Modify C: Drive</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<div id="emulator" style="height: 100vh; width: 100vw">
|
|
||||||
<div style="white-space: pre; font: 14px monospace; line-height: 14px"></div>
|
|
||||||
<canvas style="display: none"></canvas>
|
|
||||||
</div>
|
|
||||||
<script type="module">
|
|
||||||
import("es6://renderer.js")
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,94 +0,0 @@
|
||||||
const $ = document.querySelector.bind(document)
|
|
||||||
const status = $('#status')
|
|
||||||
const diskStatus = $('#disk-status')
|
|
||||||
const cpuStatus = $('#cpu-status')
|
|
||||||
const toggleStatus = $('#toggle-status')
|
|
||||||
|
|
||||||
let lastCounter = 0
|
|
||||||
let lastTick = 0
|
|
||||||
let infoInterval = null
|
|
||||||
|
|
||||||
const onIDEReadStart = () => {
|
|
||||||
diskStatus.innerHTML = 'Read'
|
|
||||||
}
|
|
||||||
|
|
||||||
const onIDEReadWriteEnd = () => {
|
|
||||||
diskStatus.innerHTML = 'Idle'
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleStatus.onclick = toggleInfo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle the information display
|
|
||||||
*/
|
|
||||||
export function toggleInfo () {
|
|
||||||
if (status.style.display !== 'none') {
|
|
||||||
disableInfo()
|
|
||||||
} else {
|
|
||||||
enableInfo()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start information gathering, but only if the panel is visible
|
|
||||||
*/
|
|
||||||
export function startInfoMaybe () {
|
|
||||||
if (status.style.display !== 'none') {
|
|
||||||
enableInfo()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable the gathering of information (and hide the little information tab)
|
|
||||||
*/
|
|
||||||
export function enableInfo () {
|
|
||||||
// Show the info thingy
|
|
||||||
status.style.display = 'block'
|
|
||||||
|
|
||||||
// We can only do the rest with an emulator
|
|
||||||
if (!window.emulator.add_listener) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set listeners
|
|
||||||
window.emulator.add_listener('ide-read-start', onIDEReadStart)
|
|
||||||
window.emulator.add_listener('ide-read-end', onIDEReadWriteEnd)
|
|
||||||
window.emulator.add_listener('ide-write-end', onIDEReadWriteEnd)
|
|
||||||
window.emulator.add_listener('screen-set-size-graphical', console.log)
|
|
||||||
|
|
||||||
// Set an interval
|
|
||||||
infoInterval = setInterval(() => {
|
|
||||||
const now = Date.now()
|
|
||||||
const instructionCounter = window.emulator.get_instruction_counter()
|
|
||||||
const ips = instructionCounter - lastCounter
|
|
||||||
const deltaTime = now - lastTick
|
|
||||||
|
|
||||||
lastTick = now
|
|
||||||
lastCounter = instructionCounter
|
|
||||||
|
|
||||||
cpuStatus.innerHTML = Math.round(ips / deltaTime)
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disable the gathering of information (and hide the little information tab)
|
|
||||||
*/
|
|
||||||
export function disableInfo () {
|
|
||||||
// Hide the info thingy
|
|
||||||
status.style.display = 'none'
|
|
||||||
|
|
||||||
// Clear the interval
|
|
||||||
clearInterval(infoInterval)
|
|
||||||
infoInterval = null
|
|
||||||
|
|
||||||
// We can only do the rest with an emulator
|
|
||||||
if (!window.emulator.remove_listener) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unset the listeners
|
|
||||||
window.emulator.remove_listener('ide-read-start', onIDEReadStart)
|
|
||||||
window.emulator.remove_listener('ide-read-end', onIDEReadWriteEnd)
|
|
||||||
window.emulator.remove_listener('ide-write-end', onIDEReadWriteEnd)
|
|
||||||
window.emulator.remove_listener('screen-set-size-graphical', console.log)
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { toggleInfo } from 'es6://info.js'
|
|
||||||
|
|
||||||
export function setupIpcListeners (start) {
|
|
||||||
const { windows95 } = window
|
|
||||||
|
|
||||||
windows95.addListener(windows95.IPC_COMMANDS.TOGGLE_INFO, () => {
|
|
||||||
toggleInfo()
|
|
||||||
})
|
|
||||||
|
|
||||||
windows95.addListener(windows95.IPC_COMMANDS.MACHINE_RESTART, () => {
|
|
||||||
console.log(`Restarting machine`)
|
|
||||||
|
|
||||||
if (!window.emulator || !window.emulator.is_running) return
|
|
||||||
|
|
||||||
window.emulator.restart()
|
|
||||||
})
|
|
||||||
|
|
||||||
windows95.addListener(windows95.IPC_COMMANDS.MACHINE_RESET, () => {
|
|
||||||
console.log(`Resetting machine`)
|
|
||||||
|
|
||||||
window.appState.isResetting = true
|
|
||||||
document.location.hash = `#AUTO_START`
|
|
||||||
document.location.reload()
|
|
||||||
})
|
|
||||||
|
|
||||||
windows95.addListener(windows95.IPC_COMMANDS.MACHINE_CTRL_ALT_DEL, () => {
|
|
||||||
if (!window.emulator || !window.emulator.is_running) return
|
|
||||||
|
|
||||||
window.emulator.keyboard_send_scancodes([
|
|
||||||
0x1D, // ctrl
|
|
||||||
0x38, // alt
|
|
||||||
0x53, // delete
|
|
||||||
|
|
||||||
// break codes
|
|
||||||
0x1D | 0x80,
|
|
||||||
0x38 | 0x80,
|
|
||||||
0x53 | 0x80
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
windows95.addListener(windows95.IPC_COMMANDS.SHOW_DISK_IMAGE, () => {
|
|
||||||
windows95.showDiskImage()
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
export function setupCloseListener () {
|
|
||||||
window.appState.isQuitting = false
|
|
||||||
|
|
||||||
const handleClose = async () => {
|
|
||||||
await windows95.saveState()
|
|
||||||
window.appState.isQuitting = true
|
|
||||||
windows95.quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onbeforeunload = (event) => {
|
|
||||||
if (window.appState.isQuitting) return
|
|
||||||
if (window.appState.isResetting) return
|
|
||||||
|
|
||||||
handleClose()
|
|
||||||
event.preventDefault()
|
|
||||||
event.returnValue = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setupEscListener () {
|
|
||||||
document.onkeydown = function (evt) {
|
|
||||||
evt = evt || window.event
|
|
||||||
if (evt.keyCode === 27) {
|
|
||||||
if (window.appState.cursorCaptured) {
|
|
||||||
window.appState.cursorCaptured = false
|
|
||||||
window.emulator.mouse_set_status(false)
|
|
||||||
document.exitPointerLock()
|
|
||||||
} else {
|
|
||||||
window.appState.cursorCaptured = true
|
|
||||||
window.emulator.lock_mouse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDocumentClick () {
|
|
||||||
if (!window.appState.cursorCaptured) {
|
|
||||||
window.appState.cursorCaptured = true
|
|
||||||
window.emulator.mouse_set_status(true)
|
|
||||||
window.emulator.lock_mouse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setupClickListener () {
|
|
||||||
document.removeEventListener('click', onDocumentClick)
|
|
||||||
document.addEventListener('click', onDocumentClick)
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
/* We're using modern esm imports here */
|
|
||||||
import { setupState } from 'es6://app-state.js'
|
|
||||||
import { setupClickListener, setupEscListener, setupCloseListener } from 'es6://listeners.js'
|
|
||||||
import { toggleSetup, setupButtons } from 'es6://buttons.js'
|
|
||||||
import { startInfoMaybe } from 'es6://info.js'
|
|
||||||
import { setupIpcListeners } from 'es6://ipc.js'
|
|
||||||
|
|
||||||
setupState()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The main method executing the VM.
|
|
||||||
*/
|
|
||||||
async function main () {
|
|
||||||
const imageSize = await window.windows95.getDiskImageSize()
|
|
||||||
const options = {
|
|
||||||
memory_size: 128 * 1024 * 1024,
|
|
||||||
video_memory_size: 32 * 1024 * 1024,
|
|
||||||
screen_container: document.getElementById('emulator'),
|
|
||||||
bios: {
|
|
||||||
url: './bios/seabios.bin'
|
|
||||||
},
|
|
||||||
vga_bios: {
|
|
||||||
url: './bios/vgabios.bin'
|
|
||||||
},
|
|
||||||
hda: {
|
|
||||||
url: '../images/windows95.img',
|
|
||||||
async: true,
|
|
||||||
size: imageSize
|
|
||||||
},
|
|
||||||
fda: {
|
|
||||||
buffer: window.appState.floppyFile || undefined
|
|
||||||
},
|
|
||||||
boot_order: 0x132
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Starting emulator with options`, options)
|
|
||||||
|
|
||||||
// New v86 instance
|
|
||||||
window.emulator = new V86Starter(options)
|
|
||||||
|
|
||||||
// Restore state. We can't do this right away
|
|
||||||
// and randomly chose 500ms as the appropriate
|
|
||||||
// wait time (lol)
|
|
||||||
setTimeout(async () => {
|
|
||||||
if (!window.appState.bootFresh) {
|
|
||||||
windows95.restoreState()
|
|
||||||
}
|
|
||||||
|
|
||||||
startInfoMaybe()
|
|
||||||
|
|
||||||
window.appState.cursorCaptured = true
|
|
||||||
window.emulator.lock_mouse()
|
|
||||||
window.emulator.run()
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
function start () {
|
|
||||||
document.body.className = ''
|
|
||||||
|
|
||||||
toggleSetup(false)
|
|
||||||
setupClickListener()
|
|
||||||
main()
|
|
||||||
}
|
|
||||||
|
|
||||||
setupIpcListeners(start)
|
|
||||||
setupEscListener()
|
|
||||||
setupCloseListener()
|
|
||||||
setupButtons(start)
|
|
||||||
|
|
||||||
if (document.location.hash.includes('AUTO_START')) {
|
|
||||||
start()
|
|
||||||
}
|
|
39
src/renderer/start-menu.tsx
Normal file
39
src/renderer/start-menu.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface StartMenuProps {
|
||||||
|
navigate: (to: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StartMenu extends React.Component<StartMenuProps, {}> {
|
||||||
|
constructor(props: StartMenuProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.navigate = this.navigate.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<nav className="nav nav-bottom">
|
||||||
|
<a onClick={this.navigate} href="#" id="start" className="nav-logo">
|
||||||
|
<img src="../../static/start.png" alt="" />
|
||||||
|
<span>Start</span>
|
||||||
|
</a>
|
||||||
|
<div className="nav-menu">
|
||||||
|
<a onClick={this.navigate} href="#" id="floppy" className="nav-link">
|
||||||
|
Floppy Disk
|
||||||
|
</a>
|
||||||
|
<a onClick={this.navigate} href="#" id="state" className="nav-link">
|
||||||
|
Reset Machine
|
||||||
|
</a>
|
||||||
|
<a onClick={this.navigate} href="#" id="disk" className="nav-link">
|
||||||
|
Modify C: Drive
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private navigate(event: React.SyntheticEvent<HTMLAnchorElement>) {
|
||||||
|
this.props.navigate(event.currentTarget.id);
|
||||||
|
}
|
||||||
|
}
|
0
src/renderer/status.tsx
Normal file
0
src/renderer/status.tsx
Normal file
81
src/state.js
81
src/state.js
|
@ -1,81 +0,0 @@
|
||||||
const fs = require('fs-extra')
|
|
||||||
|
|
||||||
const { CONSTANTS } = require('./constants')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current machine's state - either what
|
|
||||||
* we have saved or alternatively the default state.
|
|
||||||
*
|
|
||||||
* @returns {ArrayBuffer}
|
|
||||||
*/
|
|
||||||
function getState () {
|
|
||||||
const statePath = fs.existsSync(CONSTANTS.STATE_PATH)
|
|
||||||
? CONSTANTS.STATE_PATH
|
|
||||||
: CONSTANTS.DEFAULT_STATE_PATH
|
|
||||||
|
|
||||||
if (fs.existsSync(statePath)) {
|
|
||||||
return fs.readFileSync(statePath).buffer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets a saved state by simply deleting it.
|
|
||||||
*
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function resetState () {
|
|
||||||
if (fs.existsSync(CONSTANTS.STATE_PATH)) {
|
|
||||||
return fs.remove(CONSTANTS.STATE_PATH)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the current VM's state.
|
|
||||||
*
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function saveState () {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (!window.emulator || !window.emulator.save_state) {
|
|
||||||
return resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.emulator.save_state(async (error, newState) => {
|
|
||||||
if (error) {
|
|
||||||
console.warn(`State: Could not save state`, error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.outputFile(CONSTANTS.STATE_PATH, Buffer.from(newState))
|
|
||||||
|
|
||||||
console.log(`State: Saved state to ${CONSTANTS.STATE_PATH}`)
|
|
||||||
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the VM's state.
|
|
||||||
*/
|
|
||||||
function restoreState () {
|
|
||||||
const state = getState()
|
|
||||||
|
|
||||||
// Nothing to do with if we don't have a state
|
|
||||||
if (!state) {
|
|
||||||
console.log(`State: No state present, not restoring.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
window.emulator.restore_state(state)
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`State: Could not read state file. Maybe none exists?`, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
saveState,
|
|
||||||
restoreState,
|
|
||||||
resetState,
|
|
||||||
getState
|
|
||||||
}
|
|
8
src/utils/devmode.ts
Normal file
8
src/utils/devmode.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/**
|
||||||
|
* Are we currently running in development mode?
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isDevMode() {
|
||||||
|
return !!process.defaultApp;
|
||||||
|
}
|
|
@ -1,26 +0,0 @@
|
||||||
const fs = require('fs-extra')
|
|
||||||
|
|
||||||
const { CONSTANTS } = require('../constants')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the size of the disk image
|
|
||||||
*
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
async function getDiskImageSize () {
|
|
||||||
try {
|
|
||||||
const stats = await fs.stat(CONSTANTS.IMAGE_PATH)
|
|
||||||
|
|
||||||
if (stats) {
|
|
||||||
return stats.size
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Could not determine image size`, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
return CONSTANTS.IMAGE_DEFAULT_SIZE
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getDiskImageSize
|
|
||||||
}
|
|
22
src/utils/disk-image-size.ts
Normal file
22
src/utils/disk-image-size.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import * as fs from "fs-extra";
|
||||||
|
|
||||||
|
import { CONSTANTS } from "../constants";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of the disk image
|
||||||
|
*
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export async function getDiskImageSize() {
|
||||||
|
try {
|
||||||
|
const stats = await fs.stat(CONSTANTS.IMAGE_PATH);
|
||||||
|
|
||||||
|
if (stats) {
|
||||||
|
return stats.size;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Could not determine image size`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CONSTANTS.IMAGE_DEFAULT_SIZE;
|
||||||
|
}
|
16
static/index.html
Normal file
16
static/index.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>windows95</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="../src/less/vendor/95css.css">
|
||||||
|
<link rel="stylesheet" href="../src/less/root.less">
|
||||||
|
<script src="../src/renderer/lib/libv86.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="paused windows95">
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="../src/renderer/app.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
7
tools/generateAssets.js
Normal file
7
tools/generateAssets.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
|
||||||
|
const { compileParcel } = require('./parcel-build')
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
await Promise.all([compileParcel()])
|
||||||
|
}
|
47
tools/parcel-build.js
Normal file
47
tools/parcel-build.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
|
||||||
|
const Bundler = require('parcel-bundler')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
async function compileParcel (options = {}) {
|
||||||
|
const entryFiles = [
|
||||||
|
path.join(__dirname, '../static/index.html'),
|
||||||
|
path.join(__dirname, '../src/main/main.ts')
|
||||||
|
]
|
||||||
|
|
||||||
|
const bundlerOptions = {
|
||||||
|
outDir: './dist', // The out directory to put the build files in, defaults to dist
|
||||||
|
outFile: undefined, // The name of the outputFile
|
||||||
|
publicUrl: '../', // The url to server on, defaults to dist
|
||||||
|
watch: false, // whether to watch the files and rebuild them on change, defaults to process.env.NODE_ENV !== 'production'
|
||||||
|
cache: false, // Enabled or disables caching, defaults to true
|
||||||
|
cacheDir: '.cache', // The directory cache gets put in, defaults to .cache
|
||||||
|
contentHash: false, // Disable content hash from being included on the filename
|
||||||
|
minify: false, // Minify files, enabled if process.env.NODE_ENV === 'production'
|
||||||
|
scopeHoist: false, // turn on experimental scope hoisting/tree shaking flag, for smaller production bundles
|
||||||
|
target: 'electron', // browser/node/electron, defaults to browser
|
||||||
|
// https: { // Define a custom {key, cert} pair, use true to generate one or false to use http
|
||||||
|
// cert: './ssl/c.crt', // path to custom certificate
|
||||||
|
// key: './ssl/k.key' // path to custom key
|
||||||
|
// },
|
||||||
|
logLevel: 3, // 3 = log everything, 2 = log warnings & errors, 1 = log errors
|
||||||
|
hmr: true, // 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)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundler = new Bundler(entryFiles, bundlerOptions)
|
||||||
|
|
||||||
|
// Run the bundler, this returns the main bundle
|
||||||
|
// Use the events if you're using watch mode as this promise will only trigger once and not for every rebuild
|
||||||
|
await bundler.bundle()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
compileParcel
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) compileParcel()
|
11
tools/parcel-watch.js
Normal file
11
tools/parcel-watch.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
const { compileParcel } = require('./parcel-build')
|
||||||
|
|
||||||
|
async function watchParcel () {
|
||||||
|
return compileParcel({ watch: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
watchParcel
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) watchParcel()
|
30
tools/run-bin.js
Normal file
30
tools/run-bin.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
|
||||||
|
const childProcess = require('child_process')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
async function run (name, bin, args = []) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
console.info(`Running ${name}`)
|
||||||
|
|
||||||
|
const cmd = process.platform === 'win32' ? `${bin}.cmd` : bin
|
||||||
|
const child = childProcess.spawn(
|
||||||
|
path.resolve(__dirname, '..', 'node_modules', '.bin', cmd),
|
||||||
|
args,
|
||||||
|
{
|
||||||
|
cwd: path.resolve(__dirname, '..'),
|
||||||
|
stdio: 'inherit'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
child.on('exit', (code) => {
|
||||||
|
console.log('')
|
||||||
|
if (code === 0) return resolve()
|
||||||
|
reject(new Error(`${name} failed`))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
run
|
||||||
|
}
|
13
tools/tsc.js
Normal file
13
tools/tsc.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
|
||||||
|
const { run } = require('./run-bin')
|
||||||
|
|
||||||
|
async function compileTypeScript () {
|
||||||
|
await run('TypeScript', 'tsc', ['-p', 'tsconfig.json'])
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
compileTypeScript
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) compileTypeScript()
|
43
tsconfig.json
Normal file
43
tsconfig.json
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"allowJs": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"removeComments": false,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"lib": [
|
||||||
|
"es2017",
|
||||||
|
"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",
|
||||||
|
"jsx": "react",
|
||||||
|
"typeRoots": [
|
||||||
|
"./node_modules/@types"
|
||||||
|
],
|
||||||
|
"baseUrl": "."
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
|
"formatCodeOptions": {
|
||||||
|
"indentSize": 2,
|
||||||
|
"tabSize": 2
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue