diff --git a/.travis.yml b/.travis.yml index f1083a67..c6dcbb91 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,23 +1,45 @@ sudo: false -install: true language: node_js install: - - pip install --user awscli -node_js: - - "8" + - pip install --user awscli +node_js: + - '12' env: - global: - # include $HOME/.local/bin for `aws` - - PATH=$HOME/.local/bin:$PATH - matrix: - - TEST_SUITE=short-tests - - TEST_SUITE=long-tests - - TEST_SUITE=lint -script: - make -j2 ${TEST_SUITE} - + global: + - PATH=$HOME/.local/bin:$PATH + # GH_TOKEN= maps to ecb...ca5 + - secure: iSv2VWrSWlGERo6jl6V2SLwfWPxx/Y54jTyXBOQLz/nhuzHYFmzvCxNQ9uZkbPC+cQwopcWsxAM+rgWsOJtGiKay2hIJ9qBHknHcSlKaRwiA+2fr8ljTe52kHW3HY6IDj1PyCX7JDG7xFIhYP+URDF3eO9W5XxowIP5d2VP6GGI= +cache: + directories: + - node_modules + - "$HOME/.cache/electron" + - "$HOME/.cache/electron-builder" + - "$HOME/.npm/_prebuilds" jobs: - include: - - stage: deploy - if: branch = master and type != pull_request - env: TEST_SUITE=upload + include: + - name: Short tests + script: make -j2 short-tests + - name: Long tests + script: make -j2 long-tests + - name: Lint + script: make -j2 lint + - stage: deploy + name: Website + if: branch = master and type != pull_request + env: TEST_SUITE=upload + - stage: deploy + name: Electron Windows & Mac + os: osx + osx_image: xcode11.4 + script: + - npm install + - npm run dist -- --mac --win + before_cache: + - rm -rf $HOME/.cache/electron-builder/wine + - stage: deploy + name: Linux + os: linux + dist: trusty + script: + - npm install + - npm run dist diff --git a/app.js b/app.js deleted file mode 100644 index abc8bdb6..00000000 --- a/app.js +++ /dev/null @@ -1,68 +0,0 @@ -"use strict"; -var requirejs = require('requirejs'); -var Png = require('node-png').PNG; -var fs = require('fs'); - -requirejs.config({ - paths: { - 'jsunzip': 'lib/jsunzip', - 'promise': 'lib/promise-6.0.0', - 'underscore': 'lib/underscore-min' - } -}); - -requirejs(['video', 'fake6502', 'fdc', 'models'], - function (Video, Fake6502, disc, models) { - var fb32 = new Uint32Array(1280 * 768); - var frame = 0; - var screenshotRequest = null; - var video = new Video.Video(false, fb32, function (minx, miny, maxx, maxy) { - frame++; - if (screenshotRequest) { - var width = maxx - minx; - var height = maxy - miny; - var addr = 0; - var png = new Png({width: width, height: height}); - for (var y = miny; y < maxy; ++y) { - for (var x = minx; x < maxx; ++x) { - var col = fb32[1280 * y + x]; - png.data[addr++] = col & 0xff; - png.data[addr++] = (col >>> 8) & 0xff; - png.data[addr++] = (col >>> 16) & 0xff; - png.data[addr++] = 0xff; - } - } - console.log("Scheduling save of " + width + "x" + height + " screenshot to " + screenshotRequest); - png.pack().pipe(fs.createWriteStream(screenshotRequest)); - screenshotRequest = null; - } - }); - - function benchmarkCpu(cpu, numCycles) { - numCycles = numCycles || 10 * 1000 * 1000; - var startTime = Date.now(); - cpu.execute(numCycles); - var endTime = Date.now(); - var msTaken = endTime - startTime; - var virtualMhz = (numCycles / msTaken) / 1000; - console.log("Took " + msTaken + "ms to execute " + numCycles + " cycles"); - console.log("Virtual " + virtualMhz.toFixed(2) + "MHz"); - } - - var discName = "elite"; - var cpu = Fake6502.fake6502(models.findModel('B'), {video: video}); - cpu.initialise().then(function () { - return disc.load("discs/" + discName + ".ssd"); - }).then(function (data) { - cpu.fdc.loadDisc(0, disc.discFor(cpu.fdc, '', data)); - cpu.sysvia.keyDown(16); - cpu.execute(10 * 1000 * 1000); - cpu.sysvia.keyUp(16); - for (var i = 0; i < 10; ++i) { - screenshotRequest = "/tmp/" + discName + "-" + i + ".png"; - benchmarkCpu(cpu, 10 * 1000 * 1000); - } - }).catch(function (err) { - console.log("Got error: ", err); - }); - }); diff --git a/app/.jshintrc b/app/.jshintrc new file mode 100644 index 00000000..2fdf33c4 --- /dev/null +++ b/app/.jshintrc @@ -0,0 +1,13 @@ +{ + "strict": true, + "undef": true, + "browser": true, + "node": true, + "globals": { + "define": false, + "requirejs": false, + "ga": false + }, + "esversion": 8, + "eqeqeq": true +} diff --git a/app/app.js b/app/app.js new file mode 100644 index 00000000..d8b9c7a3 --- /dev/null +++ b/app/app.js @@ -0,0 +1,148 @@ +"use strict"; +const {app, dialog, Menu, BrowserWindow} = require('electron'); +const fs = require('fs'); +const path = require('path'); +const {ArgumentParser} = require('argparse'); + +const isMac = process.platform === 'darwin'; + +function getArguments() { + // Heinous hack to get "built" versions working + if (path.basename(process.argv[0]) === 'jsbeeb') // Is this ia "built" version? + return process.argv.slice(1); + return process.argv.slice(2); +} + +const parser = new ArgumentParser({ + prog: 'jsbeeb', + addHelp: true, + description: 'Emulate a Beeb' +}); +parser.addArgument(["--noboot"], {action: 'storeTrue', help: "don't autoboot if given a disc image"}); +parser.addArgument(["disc1"], {nargs: '?', help: "image to load in drive 0"}); +parser.addArgument(["disc2"], {nargs: '?', help: "image to load in drive 1"}); +const args = parser.parseArgs(getArguments()); + + +function getFileParam(filename) { + try { + return "file://" + fs.realpathSync(filename); + } catch (e) { + console.error("Unable to open file " + filename); + throw e; + } +} + +async function createWindow() { + const win = new BrowserWindow({ + width: 1280, + height: 1024, + webPreferences: { + preload: path.join(__dirname, 'preload.js') + } + }); + + const query = {}; + if (args.disc1 && !args.noboot) query.autoboot = true; + if (args.disc1) query.disc1 = getFileParam(args.disc1); + if (args.disc2) query.disc2 = getFileParam(args.disc2); + await win.loadFile('index.html', {query}); + + app.on('activate', function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +} + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit(); +}); + +app.whenReady().then(createWindow) + .catch(e => { + console.error("Unhandled exception", e); + app.exit(1); + }); + +function makeLoader(drive) { + return async (_, browserWindow) => { + const result = await dialog.showOpenDialog(browserWindow, { + title: "Load a disc image", + filters: [ + {name: 'Disc images', extensions: ['ssd', 'dsd']}, + {name: 'ZIPped disc images', extensions: ['zip']}, + ], + properties: ['openFile'] + }); + if (!result.canceled) { + browserWindow.webContents.send('load', {drive, path: getFileParam(result.filePaths[0])}); + } + }; +} + +const template = [ + // { role: 'appMenu' } + ...(isMac ? [{ + label: app.name, + submenu: [ + {role: 'about'}, + {type: 'separator'}, + {role: 'services'}, + {type: 'separator'}, + {role: 'hide'}, + {role: 'hideothers'}, + {role: 'unhide'}, + {type: 'separator'}, + {role: 'quit'} + ] + }] : []), + // { role: 'fileMenu' } + { + label: 'File', + submenu: [ + { + label: 'Load disc 0', + click: makeLoader(0) + }, + { + label: 'Load disc 1', + click: makeLoader(1) + }, + isMac ? {role: 'close'} : {role: 'quit'} + ] + }, + // { role: 'editMenu' } + { + label: 'Edit', + submenu: [{role: 'paste'}] + }, + // { role: 'viewMenu' } + { + label: 'View', + submenu: [ + {role: 'reload'}, + {role: 'forcereload'}, + {role: 'toggledevtools'}, + {type: 'separator'}, + {role: 'resetzoom'}, + {role: 'zoomin'}, + {role: 'zoomout'}, + {type: 'separator'}, + {role: 'togglefullscreen'} + ] + }, + { + role: 'help', + submenu: [ + { + label: 'Learn More', + click: async () => { + const {shell} = require('electron'); + await shell.openExternal('https://github.com/mattgodbolt/jsbeeb/'); + } + } + ] + } +]; + +const menu = Menu.buildFromTemplate(template); +Menu.setApplicationMenu(menu); diff --git a/app/electron.js b/app/electron.js new file mode 100644 index 00000000..bafe65e9 --- /dev/null +++ b/app/electron.js @@ -0,0 +1,17 @@ +define([], function () { + 'use strict'; + if (typeof window.nodeRequire === 'undefined') return function () { + }; + + function init(args) { + const {loadDiscImage, processor} = args; + const electron = window.nodeRequire('electron'); + electron.ipcRenderer.on('load', async (event, message) => { + const {drive, path} = message; + const image = await loadDiscImage(path); + processor.fdc.loadDisc(drive, image); + }); + } + + return init; +}); diff --git a/app/preload.js b/app/preload.js new file mode 100644 index 00000000..e447254e --- /dev/null +++ b/app/preload.js @@ -0,0 +1,8 @@ +'use strict'; +window.nodeRequire = require; + +window.addEventListener('DOMContentLoaded', () => { + for (const node of document.getElementsByClassName("not-electron")) { + node.remove(); + } +}); diff --git a/build/icon.ico b/build/icon.ico new file mode 100644 index 00000000..8d22d021 Binary files /dev/null and b/build/icon.ico differ diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 00000000..e673caaf Binary files /dev/null and b/build/icon.png differ diff --git a/index.html b/index.html index e82f7b73..d7b4582a 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,8 @@ + JSBeeb - Javascript BBC Micro emulator @@ -31,13 +33,12 @@ ga('create', 'UA-55180-8', 'godbolt.org'); ga('send', 'pageview'); - -