From 241606d0973d193935b49d7925df225373217d14 Mon Sep 17 00:00:00 2001 From: Felix Rieseberg Date: Wed, 21 Aug 2019 09:48:49 +0200 Subject: [PATCH] feat: Move to TypeScript --- package.json | 2 +- src/cache.js | 25 --- src/cache.ts | 25 +++ src/{constants.js => constants.ts} | 17 +- src/es6.js | 36 --- src/index.js | 55 ----- src/main/about-panel.ts | 28 +++ src/main/main.ts | 67 ++++++ src/main/menu.ts | 197 ++++++++++++++++ src/main/squirrel.ts | 3 + src/main/update.ts | 10 + src/main/windows.ts | 21 ++ src/menu.js | 182 --------------- src/preload.js | 42 ---- src/renderer/app-state.js | 9 - src/renderer/app.tsx | 34 +++ src/renderer/buttons.js | 56 ----- src/renderer/card-floppy.tsx | 78 +++++++ src/renderer/card-start.tsx | 23 ++ src/renderer/card-state.tsx | 47 ++++ src/renderer/emulator-info.tsx | 166 ++++++++++++++ src/renderer/emulator.tsx | 350 +++++++++++++++++++++++++++++ src/renderer/global.d.ts | 2 + src/renderer/index.html | 75 ------- src/renderer/info.js | 94 -------- src/renderer/ipc.js | 44 ---- src/renderer/listeners.js | 47 ---- src/renderer/renderer.js | 72 ------ src/renderer/start-menu.tsx | 39 ++++ src/renderer/status.tsx | 0 src/state.js | 81 ------- src/utils/devmode.ts | 8 + src/utils/disk-image-size.js | 26 --- src/utils/disk-image-size.ts | 22 ++ static/index.html | 16 ++ {src/less => static}/start.png | Bin tools/generateAssets.js | 7 + tools/parcel-build.js | 47 ++++ tools/parcel-watch.js | 11 + tools/run-bin.js | 30 +++ tools/tsc.js | 13 ++ tsconfig.json | 43 ++++ 42 files changed, 1294 insertions(+), 856 deletions(-) delete mode 100644 src/cache.js create mode 100644 src/cache.ts rename src/{constants.js => constants.ts} (52%) delete mode 100644 src/es6.js delete mode 100644 src/index.js create mode 100644 src/main/about-panel.ts create mode 100644 src/main/main.ts create mode 100644 src/main/menu.ts create mode 100644 src/main/squirrel.ts create mode 100644 src/main/update.ts create mode 100644 src/main/windows.ts delete mode 100644 src/menu.js delete mode 100644 src/preload.js delete mode 100644 src/renderer/app-state.js create mode 100644 src/renderer/app.tsx delete mode 100644 src/renderer/buttons.js create mode 100644 src/renderer/card-floppy.tsx create mode 100644 src/renderer/card-start.tsx create mode 100644 src/renderer/card-state.tsx create mode 100644 src/renderer/emulator-info.tsx create mode 100644 src/renderer/emulator.tsx create mode 100644 src/renderer/global.d.ts delete mode 100644 src/renderer/index.html delete mode 100644 src/renderer/info.js delete mode 100644 src/renderer/ipc.js delete mode 100644 src/renderer/listeners.js delete mode 100644 src/renderer/renderer.js create mode 100644 src/renderer/start-menu.tsx create mode 100644 src/renderer/status.tsx delete mode 100644 src/state.js create mode 100644 src/utils/devmode.ts delete mode 100644 src/utils/disk-image-size.js create mode 100644 src/utils/disk-image-size.ts create mode 100644 static/index.html rename {src/less => static}/start.png (100%) create mode 100644 tools/generateAssets.js create mode 100644 tools/parcel-build.js create mode 100644 tools/parcel-watch.js create mode 100644 tools/run-bin.js create mode 100644 tools/tsc.js create mode 100644 tsconfig.json diff --git a/package.json b/package.json index 668ac44..8d690e6 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "package": "electron-forge package", "make": "electron-forge make", "publish": "electron-forge publish", - "lint": "standard \"src/**/*.js\"", + "lint": "prettier --write src/**/*.{ts,tsx}", "less": "node ./tools/lessc.js" }, "keywords": [], diff --git a/src/cache.js b/src/cache.js deleted file mode 100644 index 1c5ebfd..0000000 --- a/src/cache.js +++ /dev/null @@ -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 -} diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..fd56d93 --- /dev/null +++ b/src/cache.ts @@ -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) + }) +} diff --git a/src/constants.js b/src/constants.ts similarity index 52% rename from src/constants.js rename to src/constants.ts index 882c5d1..998b586 100644 --- a/src/constants.js +++ b/src/constants.ts @@ -1,24 +1,19 @@ -const { remote, app } = require('electron') -const path = require('path') +import { remote, app } from 'electron'; +import * as path from 'path'; const _app = app || remote.app -const CONSTANTS = { - IMAGE_PATH: path.join(__dirname, 'images/windows95.img'), +export const CONSTANTS = { + IMAGE_PATH: path.join(__dirname, '../../images/windows95.img'), 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') } -const IPC_COMMANDS = { +export const IPC_COMMANDS = { TOGGLE_INFO: 'TOGGLE_INFO', MACHINE_RESTART: 'MACHINE_RESTART', MACHINE_RESET: 'MACHINE_RESET', MACHINE_CTRL_ALT_DEL: 'MACHINE_CTRL_ALT_DEL', SHOW_DISK_IMAGE: 'SHOW_DISK_IMAGE' } - -module.exports = { - CONSTANTS, - IPC_COMMANDS -} diff --git a/src/es6.js b/src/es6.js deleted file mode 100644 index aed1ea0..0000000 --- a/src/es6.js +++ /dev/null @@ -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 -} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index b2d044c..0000000 --- a/src/index.js +++ /dev/null @@ -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() - } -}) diff --git a/src/main/about-panel.ts b/src/main/about-panel.ts new file mode 100644 index 0000000..498275b --- /dev/null +++ b/src/main/about-panel.ts @@ -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); +} diff --git a/src/main/main.ts b/src/main/main.ts new file mode 100644 index 0000000..409de82 --- /dev/null +++ b/src/main/main.ts @@ -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(); diff --git a/src/main/menu.ts b/src/main/menu.ts new file mode 100644 index 0000000..f4216f1 --- /dev/null +++ b/src/main/menu.ts @@ -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 = [ + { + 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)); +} diff --git a/src/main/squirrel.ts b/src/main/squirrel.ts new file mode 100644 index 0000000..f41be24 --- /dev/null +++ b/src/main/squirrel.ts @@ -0,0 +1,3 @@ +export function shouldQuit() { + return require("electron-squirrel-startup"); +} diff --git a/src/main/update.ts b/src/main/update.ts new file mode 100644 index 0000000..a012b62 --- /dev/null +++ b/src/main/update.ts @@ -0,0 +1,10 @@ +import { app } from "electron"; + +export function setupUpdates() { + if (app.isPackaged) { + require("update-electron-app")({ + repo: "felixrieseberg/windows95", + updateInterval: "1 hour" + }); + } +} diff --git a/src/main/windows.ts b/src/main/windows.ts new file mode 100644 index 0000000..81f7ea5 --- /dev/null +++ b/src/main/windows.ts @@ -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; + }); +} diff --git a/src/menu.js b/src/menu.js deleted file mode 100644 index 0ab6d49..0000000 --- a/src/menu.js +++ /dev/null @@ -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 -} diff --git a/src/preload.js b/src/preload.js deleted file mode 100644 index 1ec1176..0000000 --- a/src/preload.js +++ /dev/null @@ -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() diff --git a/src/renderer/app-state.js b/src/renderer/app-state.js deleted file mode 100644 index a1b8ef3..0000000 --- a/src/renderer/app-state.js +++ /dev/null @@ -1,9 +0,0 @@ -export function setupState () { - window.appState = { - isResetting: false, - isQuitting: false, - cursorCaptured: false, - floppyFile: null, - bootFresh: false - } -} diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx new file mode 100644 index 0000000..668dfff --- /dev/null +++ b/src/renderer/app.tsx @@ -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 { + const React = await import("react"); + const { render } = await import("react-dom"); + const { Emulator } = await import("./emulator"); + + const className = `${process.platform}`; + const app = ( +
+ +
+ ); + + const rendered = render(app, document.getElementById("app")); + + return rendered; + } +} + +window["win95"] = window["win95"] || { + app: new App() +}; + +window["win95"].app.setup(); diff --git a/src/renderer/buttons.js b/src/renderer/buttons.js deleted file mode 100644 index cc2f582..0000000 --- a/src/renderer/buttons.js +++ /dev/null @@ -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') -} diff --git a/src/renderer/card-floppy.tsx b/src/renderer/card-floppy.tsx new file mode 100644 index 0000000..9b316ec --- /dev/null +++ b/src/renderer/card-floppy.tsx @@ -0,0 +1,78 @@ +import * as React from "react"; + +export interface CardFloppyProps { + setFloppyPath: (path: string) => void; + floppyPath?: string; +} + +export class CardFloppy extends React.Component { + constructor(props: CardFloppyProps) { + super(props); + + this.onChange = this.onChange.bind(this); + } + + public render() { + return ( +
+
+
+

Floppy Drive

+
+
+ +

+ windows95 comes with a virtual floppy drive. If you have floppy + disk images in the "img" format, you can mount them here. +

+

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

+

+ Once you've mounted a disk image, you might have to reboot your + virtual windows95 machine from scratch. +

+

+ {this.props.floppyPath + ? `Inserted Floppy Disk: ${this.props.floppyPath}` + : `No floppy mounted`} +

+ + +
+
+
+ ); + } + + public onChange(event: React.ChangeEvent) { + 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`); + } + } +} diff --git a/src/renderer/card-start.tsx b/src/renderer/card-start.tsx new file mode 100644 index 0000000..96d9612 --- /dev/null +++ b/src/renderer/card-start.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; + +export interface CardStartProps { + startEmulator: () => void; +} + +export class CardStart extends React.Component { + public render() { + return ( +
+
+ Start Windows 95 +
+ Hit ESC to lock or unlock your mouse +
+
+ ); + } +} diff --git a/src/renderer/card-state.tsx b/src/renderer/card-state.tsx new file mode 100644 index 0000000..92d3d2e --- /dev/null +++ b/src/renderer/card-state.tsx @@ -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 { + constructor(props: CardStateProps) { + super(props); + + this.onReset = this.onReset.bind(this); + } + + public render() { + return ( +
+
+
+

Machine State

+
+
+

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

+ + +
+
+
+ ); + } + + public async onReset() { + if (fs.existsSync(CONSTANTS.STATE_PATH)) { + await fs.remove(CONSTANTS.STATE_PATH); + } + } +} diff --git a/src/renderer/emulator-info.tsx b/src/renderer/emulator-info.tsx new file mode 100644 index 0000000..40f7ae6 --- /dev/null +++ b/src/renderer/emulator-info.tsx @@ -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 ( +
+ Disk: {disk}| CPU Speed: {cpu}|{" "} + + Hide + +
+ ); + } + + 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) + }); + } +} diff --git a/src/renderer/emulator.tsx b/src/renderer/emulator.tsx new file mode 100644 index 0000000..ad0f8bc --- /dev/null +++ b/src/renderer/emulator.tsx @@ -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 = ( + this.setState({ floppyFile })} + floppyPath={this.state.floppyFile} + /> + ); + } else if (currentUiCard === "state") { + card = ; + } else { + card = ; + } + + return ( + <> + {card} + this.setState({ currentUiCard: target })} + /> + + ); + } + + /** + * Yaknow, render things and stuff. + */ + public render() { + return ( + <> + { + this.setState({ isInfoDisplayed: !this.state.isInfoDisplayed }); + }} + /> + {this.renderUI()} +
+
+ +
+ + ); + } + + /** + * 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 { + 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; + } +} diff --git a/src/renderer/global.d.ts b/src/renderer/global.d.ts new file mode 100644 index 0000000..8c4a2d0 --- /dev/null +++ b/src/renderer/global.d.ts @@ -0,0 +1,2 @@ +declare const V86Starter: any; +declare const win95: any; diff --git a/src/renderer/index.html b/src/renderer/index.html deleted file mode 100644 index 348bbf8..0000000 --- a/src/renderer/index.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - Windows - - - - - - -
- Disk: Idle - | CPU Speed: 0 - | Hide -
-
-
-
- Start Windows 95 -
- - Hit ESC to lock or unlock your mouse -
-
-
-
-
-

Floppy Drive

-
-
- -

windows95 comes with a virtual floppy drive. If you have floppy disk images in the "img" format, you can - mount them here.

-

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.

-

Once you've mounted a disk image, you might have to reboot your virtual windows95 machine from scratch.

-

No floppy mounted

- - -
-
-
-
- -
-
- -
- - -
-
-
- -
- - - - \ No newline at end of file diff --git a/src/renderer/info.js b/src/renderer/info.js deleted file mode 100644 index a37ad5e..0000000 --- a/src/renderer/info.js +++ /dev/null @@ -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) -} diff --git a/src/renderer/ipc.js b/src/renderer/ipc.js deleted file mode 100644 index 0befbc7..0000000 --- a/src/renderer/ipc.js +++ /dev/null @@ -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() - }) -} diff --git a/src/renderer/listeners.js b/src/renderer/listeners.js deleted file mode 100644 index 4f46041..0000000 --- a/src/renderer/listeners.js +++ /dev/null @@ -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) -} diff --git a/src/renderer/renderer.js b/src/renderer/renderer.js deleted file mode 100644 index 3a6396f..0000000 --- a/src/renderer/renderer.js +++ /dev/null @@ -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() -} diff --git a/src/renderer/start-menu.tsx b/src/renderer/start-menu.tsx new file mode 100644 index 0000000..38c0f55 --- /dev/null +++ b/src/renderer/start-menu.tsx @@ -0,0 +1,39 @@ +import * as React from "react"; + +export interface StartMenuProps { + navigate: (to: string) => void; +} + +export class StartMenu extends React.Component { + constructor(props: StartMenuProps) { + super(props); + + this.navigate = this.navigate.bind(this); + } + + public render() { + return ( + + ); + } + + private navigate(event: React.SyntheticEvent) { + this.props.navigate(event.currentTarget.id); + } +} diff --git a/src/renderer/status.tsx b/src/renderer/status.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/state.js b/src/state.js deleted file mode 100644 index aa07f0a..0000000 --- a/src/state.js +++ /dev/null @@ -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} - */ -async function resetState () { - if (fs.existsSync(CONSTANTS.STATE_PATH)) { - return fs.remove(CONSTANTS.STATE_PATH) - } -} - -/** - * Saves the current VM's state. - * - * @returns {Promise} - */ -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 -} diff --git a/src/utils/devmode.ts b/src/utils/devmode.ts new file mode 100644 index 0000000..d936a68 --- /dev/null +++ b/src/utils/devmode.ts @@ -0,0 +1,8 @@ +/** + * Are we currently running in development mode? + * + * @returns {boolean} + */ +export function isDevMode() { + return !!process.defaultApp; +} diff --git a/src/utils/disk-image-size.js b/src/utils/disk-image-size.js deleted file mode 100644 index fc6f7d1..0000000 --- a/src/utils/disk-image-size.js +++ /dev/null @@ -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 -} diff --git a/src/utils/disk-image-size.ts b/src/utils/disk-image-size.ts new file mode 100644 index 0000000..fece5ff --- /dev/null +++ b/src/utils/disk-image-size.ts @@ -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; +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..cb7b4a5 --- /dev/null +++ b/static/index.html @@ -0,0 +1,16 @@ + + + + + + windows95 + + + + + + +
+ + + \ No newline at end of file diff --git a/src/less/start.png b/static/start.png similarity index 100% rename from src/less/start.png rename to static/start.png diff --git a/tools/generateAssets.js b/tools/generateAssets.js new file mode 100644 index 0000000..241d2e0 --- /dev/null +++ b/tools/generateAssets.js @@ -0,0 +1,7 @@ +/* tslint:disable */ + +const { compileParcel } = require('./parcel-build') + +module.exports = async () => { + await Promise.all([compileParcel()]) +} diff --git a/tools/parcel-build.js b/tools/parcel-build.js new file mode 100644 index 0000000..b3bac78 --- /dev/null +++ b/tools/parcel-build.js @@ -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() diff --git a/tools/parcel-watch.js b/tools/parcel-watch.js new file mode 100644 index 0000000..1eda750 --- /dev/null +++ b/tools/parcel-watch.js @@ -0,0 +1,11 @@ +const { compileParcel } = require('./parcel-build') + +async function watchParcel () { + return compileParcel({ watch: true }) +} + +module.exports = { + watchParcel +} + +if (require.main === module) watchParcel() diff --git a/tools/run-bin.js b/tools/run-bin.js new file mode 100644 index 0000000..89a92e1 --- /dev/null +++ b/tools/run-bin.js @@ -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 +} diff --git a/tools/tsc.js b/tools/tsc.js new file mode 100644 index 0000000..ffa0894 --- /dev/null +++ b/tools/tsc.js @@ -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() diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2fc3ca1 --- /dev/null +++ b/tsconfig.json @@ -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 + } +}