// 8bitworkshop IDE user interface import $ = require("jquery"); import * as bootstrap from "bootstrap"; import { CodeProject } from "./project"; import { WorkerResult, WorkerOutput, VerilogOutput, SourceFile, WorkerError, FileData } from "../common/workertypes"; import { ProjectWindows } from "./windows"; import { Platform, Preset, DebugSymbols, DebugEvalCondition, isDebuggable, EmuState } from "../common/baseplatform"; import { PLATFORMS, EmuHalt, Toolbar } from "../common/emu"; import * as Views from "./views"; import { createNewPersistentStore } from "./store"; import { getFilenameForPath, getFilenamePrefix, highlightDifferences, invertMap, byteArrayToString, compressLZG, stringToByteArray, byteArrayToUTF8, isProbablyBinary, getWithBinary, getBasePlatform, getRootBasePlatform, hex } from "../common/util"; import { StateRecorderImpl } from "../common/recorder"; import { GHSession, GithubService, getRepos, parseGithubURL } from "./services"; // external libs (TODO) declare var Tour, GIF, saveAs, JSZip, Mousetrap, Split, firebase; declare var ga; // in index.html declare var exports; // make sure VCS doesn't start if (window['Javatari']) window['Javatari'].AUTO_START = false; var PRESETS : Preset[]; // presets array export var platform_id : string; // platform ID string (platform) export var store_id : string; // store ID string (repo || platform) export var repo_id : string; // repository ID (repo) export var platform : Platform; // emulator object var toolbar = $("#controls_top"); var uitoolbar : Toolbar; export var current_project : CodeProject; // current CodeProject object export var projectWindows : ProjectWindows; // window manager var stateRecorder : StateRecorderImpl; var userPaused : boolean; // did user explicitly pause? var current_output : WorkerOutput; // current ROM var current_preset : Preset; // current preset object (if selected) var store; // persistent store export var compparams; // received build params from worker export var lastDebugState : EmuState; // last debug state (object) var lastDebugInfo; // last debug info (CPU text) var debugCategory; // current debug category var debugTickPaused = false; var recorderActive = false; var lastViewClicked = null; var lastBreakExpr = "c.PC == 0x6000"; // TODO: codemirror multiplex support? // TODO: move to views.ts? var TOOL_TO_SOURCE_STYLE = { 'dasm': '6502', 'acme': '6502', 'cc65': 'text/x-csrc', 'ca65': '6502', 'nesasm': '6502', 'z80asm': 'z80', 'sdasz80': 'z80', 'sdcc': 'text/x-csrc', 'verilator': 'verilog', 'jsasm': 'z80', 'zmac': 'z80', 'bataribasic': 'bataribasic', 'markdown': 'markdown', 'js': 'javascript', 'xasm6809': 'z80', 'cmoc': 'text/x-csrc', 'yasm': 'gas', 'smlrc': 'text/x-csrc', 'inform6': 'inform6', 'fastbasic': 'fastbasic', 'basic': 'basic', } function gaEvent(category:string, action:string, label?:string, value?:string) { if (window['ga']) ga('send', 'event', category, action, label, value); } function alertError(s:string) { gaEvent('error', platform_id||'error', s); setWaitDialog(false); bootbox.alert({ title: ' Alert', message: s }); } function alertInfo(s:string) { setWaitDialog(false); bootbox.alert(s); } export function loadScript(scriptfn:string) : Promise { return new Promise( (resolve, reject) => { var script = document.createElement('script'); script.onload = resolve; script.onerror = reject; script.src = scriptfn; document.getElementsByTagName('head')[0].appendChild(script); }); } function newWorker() : Worker { return new Worker("./src/worker/loader.js"); } const hasLocalStorage : boolean = function() { try { const key = "__some_random_key_you_are_not_going_to_use__"; localStorage.setItem(key, key); var has = localStorage.getItem(key) == key; localStorage.removeItem(key); return has; } catch (e) { return false; } }(); // https://developers.google.com/web/updates/2016/06/persistent-storage function requestPersistPermission(interactive: boolean, failureonly: boolean) { if (navigator.storage && navigator.storage.persist) { navigator.storage.persist().then(persistent=>{ console.log("requestPersistPermission =", persistent); if (persistent) { interactive && !failureonly && alertInfo("Your browser says it will persist your local file edits, but you may want to back up your work anyway."); } else { interactive && alertError("Your browser refused to expand the peristent storage quota. Your edits may not be preserved after closing the page."); } }); } else { interactive && alertError("Your browser doesn't support expanding the persistent storage quota. Your edits may not be preserved after closing the page."); } } function getCurrentPresetTitle() : string { if (!current_preset) return current_project.mainPath || "ROM"; else return current_preset.title || current_preset.name || current_project.mainPath || "ROM"; } function setLastPreset(id:string) { if (hasLocalStorage) { if (repo_id && platform_id && !isElectron) localStorage.setItem("__lastrepo_" + platform_id, repo_id); else localStorage.removeItem("__lastrepo_" + platform_id); localStorage.setItem("__lastplatform", platform_id); localStorage.setItem("__lastid_" + store_id, id); } } function unsetLastPreset() { if (hasLocalStorage) { delete qs['file']; localStorage.removeItem("__lastid_"+store_id); } } function initProject() { current_project = new CodeProject(newWorker(), platform_id, platform, store); projectWindows = new ProjectWindows($("#workspace")[0] as HTMLElement, current_project); if (isElectronWorkspace) { current_project.persistent = false; current_project.callbackGetRemote = getElectronFile; current_project.callbackStoreFile = putWorkspaceFile; } else { current_project.callbackGetRemote = getWithBinary; } current_project.callbackBuildResult = (result:WorkerResult) => { setCompileOutput(result); refreshWindowList(); }; current_project.callbackBuildStatus = (busy:boolean) => { if (busy) { toolbar.addClass("is-busy"); } else { toolbar.removeClass("is-busy"); toolbar.removeClass("has-errors"); // may be added in next callback projectWindows.setErrors(null); hideErrorAlerts(); } $('#compile_spinner').css('visibility', busy ? 'visible' : 'hidden'); }; } function refreshWindowList() { var ul = $("#windowMenuList").empty(); var separate = false; function addWindowItem(id, name, createfn) { if (separate) { ul.append(document.createElement("hr")); separate = false; } var li = document.createElement("li"); var a = document.createElement("a"); a.setAttribute("class", "dropdown-item"); a.setAttribute("href", "#"); a.setAttribute("data-wndid", id); if (id == projectWindows.getActiveID()) $(a).addClass("dropdown-item-checked"); a.appendChild(document.createTextNode(name)); li.appendChild(a); ul.append(li); if (createfn) { var onopen = (id, wnd) => { ul.find('a').removeClass("dropdown-item-checked"); $(a).addClass("dropdown-item-checked"); }; projectWindows.setCreateFunc(id, createfn); projectWindows.setShowFunc(id, onopen); $(a).click( (e) => { projectWindows.createOrShow(id); lastViewClicked = id; }); } } function loadEditor(path:string) { var tool = platform.getToolForFilename(path); var mode = tool && TOOL_TO_SOURCE_STYLE[tool]; return new Views.SourceEditor(path, mode); } function addEditorItem(id:string) { addWindowItem(id, getFilenameForPath(id), () => { var data = current_project.getFile(id); if (typeof data === 'string') return loadEditor(id); else if (data instanceof Uint8Array) return new Views.BinaryFileView(id, data as Uint8Array); }); } // add main file editor addEditorItem(current_project.mainPath); // add other source files current_project.iterateFiles( (id, text) => { if (text && id != current_project.mainPath) { addEditorItem(id); } }); // add listings separate = true; var listings = current_project.getListings(); if (listings) { for (var lstfn in listings) { var lst = listings[lstfn]; // add listing if source/assembly file exists and has text if ((lst.assemblyfile && lst.assemblyfile.text) || (lst.sourcefile && lst.sourcefile.text)) { addWindowItem(lstfn, getFilenameForPath(lstfn), (path) => { return new Views.ListingView(path); }); } } } // add other tools separate = true; if (platform.disassemble && platform.saveState) { addWindowItem("#disasm", "Disassembly", () => { return new Views.DisassemblerView(); }); } if (platform.readAddress) { addWindowItem("#memory", "Memory Browser", () => { return new Views.MemoryView(); }); } if (current_project.segments && current_project.segments.length) { addWindowItem("#memmap", "Memory Map", () => { return new Views.MemoryMapView(); }); } if (platform.readVRAMAddress) { addWindowItem("#memvram", "VRAM Browser", () => { return new Views.VRAMMemoryView(); }); } if (platform.startProbing) { addWindowItem("#memheatmap", "Memory Probe", () => { return new Views.AddressHeatMapView(); }); // TODO: only if raster addWindowItem("#crtheatmap", "CRT Probe", () => { return new Views.RasterPCHeatMapView(); }); addWindowItem("#probelog", "Probe Log", () => { return new Views.ProbeLogView(); }); addWindowItem("#symbolprobe", "Symbol Profiler", () => { return new Views.ProbeSymbolView(); }); addWindowItem("#callstack", "Call Stack", () => { return new Views.CallStackView(); }); /* addWindowItem("#framecalls", "Frame Profiler", () => { return new Views.FrameCallsView(); }); */ } if (platform.getDebugTree) { addWindowItem("#debugview", "Debug Tree", () => { return new Views.DebugBrowserView(); }); } addWindowItem('#asseteditor', 'Asset Editor', () => { return new Views.AssetEditorView(); }); } function loadMainWindow(preset_id:string) { // we need this to build create functions for the editor refreshWindowList(); // show main file projectWindows.createOrShow(preset_id); // build project current_project.setMainFile(preset_id); } async function loadProject(preset_id:string) { // set current file ID // TODO: this is done twice (mainPath and mainpath!) current_project.mainPath = preset_id; setLastPreset(preset_id); // load files from storage or web URLs var result = await current_project.loadFiles([preset_id]); measureTimeLoad = new Date(); // for timing calc. if (result && result.length) { // file found; continue loadMainWindow(preset_id); } else { var skel = await getSkeletonFile(preset_id); current_project.filedata[preset_id] = skel || "\n"; loadMainWindow(preset_id); // don't alert if we selected "new file" if (!qs['newfile']) { alertInfo("Could not find file \"" + preset_id + "\". Loading default file."); } else { requestPersistPermission(true, true); } delete qs['newfile']; replaceURLState(); } } function reloadProject(id:string) { // leave repository == '/' if (id == '/') { qs = {repo:'/'}; } else if (id.indexOf('://') >= 0) { var urlparse = parseGithubURL(id); if (urlparse) { qs = {repo:urlparse.repopath}; } } else { qs['platform'] = platform_id; qs['file'] = id; } gotoNewLocation(); } async function getSkeletonFile(fileid:string) : Promise { var ext = platform.getToolForFilename(fileid); try { return await $.get( "presets/"+getBasePlatform(platform_id)+"/skeleton."+ext, 'text'); } catch(e) { alertError("Could not load skeleton for " + platform_id + "/" + ext + "; using blank file"); } } function checkEnteredFilename(fn : string) : boolean { if (fn.indexOf(" ") >= 0) { alertError("No spaces in filenames, please."); return false; } return true; } function _createNewFile(e) { // TODO: support spaces bootbox.prompt({ title:"Enter the name of your new main source file.", placeholder:"newfile" + platform.getDefaultExtension(), callback:(filename) => { if (filename && filename.trim().length > 0) { if (!checkEnteredFilename(filename)) return; if (filename.indexOf(".") < 0) { filename += platform.getDefaultExtension(); } var path = filename; gaEvent('workspace', 'file', 'new'); qs['newfile'] = '1'; reloadProject(path); } } } as any); return true; } function _uploadNewFile(e) { $("#uploadFileElem").click(); } // called from index.html function handleFileUpload(files: File[]) { console.log(files); var index = 0; function uploadNextFile() { var f = files[index++]; if (!f) { console.log("Done uploading", index); if (index > 2) { alertInfo("Files uploaded."); setTimeout(updateSelector, 1000); // TODO: wait for files to upload } else { qs['file'] = files[0].name; bootbox.confirm({ message: "Open '" + qs['file'] + "' as main project file?", buttons: { confirm: { label: "Open As New Project" }, cancel: { label: "Include/Link With Project Later" }, }, callback: (result) => { if (result) gotoNewLocation(); else setTimeout(updateSelector, 1000); // TODO: wait for files to upload } }); } gaEvent('workspace', 'file', 'upload'); } else { var path = f.name; var reader = new FileReader(); reader.onload = function(e) { var arrbuf = (e.target).result as ArrayBuffer; var data : FileData = new Uint8Array(arrbuf); // convert to UTF8, unless it's a binary file if (isProbablyBinary(path, data)) { //gotoMainFile = false; } else { data = byteArrayToUTF8(data).replace('\r\n','\n'); // convert CRLF to LF } // store in local forage projectWindows.updateFile(path, data); console.log("Uploaded " + path + " " + data.length + " bytes"); uploadNextFile(); } reader.readAsArrayBuffer(f); // read as binary } } if (files) uploadNextFile(); } function getCurrentMainFilename() : string { return getFilenameForPath(current_project.mainPath); } function getCurrentEditorFilename() : string { return getFilenameForPath(projectWindows.getActiveID()); } // GITHUB stuff (TODO: move) var githubService : GithubService; function getCookie(name) : string { var nameEQ = name + "="; var ca = document.cookie.split(';'); for(var i=0;i < ca.length;i++) { var c = ca[i]; while (c.charAt(0)==' ') c = c.substring(1,c.length); if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); } return null; } function getGithubService() { if (!githubService) { // get github API key from cookie // TODO: move to service? var ghkey = getCookie('__github_key'); githubService = new GithubService(exports['Octokat'], ghkey, store, current_project); console.log("loaded github service"); } return githubService; } function getBoundGithubURL() : string { var toks = (repo_id||'').split('/'); if (toks.length != 2) { alertError("

You are not in a GitHub repository.

Choose one from the pulldown, or Import or Publish one.

"); return null; } return 'https://github.com/' + toks[0] + '/' + toks[1]; } function importProjectFromGithub(githuburl:string, replaceURL:boolean) { var sess : GHSession; var urlparse = parseGithubURL(githuburl); if (!urlparse) { alertError('Could not parse Github URL.'); return; } // redirect to repo if exists var existing = getRepos()[urlparse.repopath]; if (existing && !confirm("You've already imported " + urlparse.repopath + " -- do you want to replace all local files?")) { return; } // create new store for imported repository setWaitDialog(true); var newstore = createNewPersistentStore(urlparse.repopath); // import into new store setWaitProgress(0.25); return getGithubService().import(githuburl).then( (sess1:GHSession) => { sess = sess1; setWaitProgress(0.75); return getGithubService().pull(githuburl, newstore); }).then( (sess2:GHSession) => { // TODO: only first session has mainPath? // reload repo qs = {repo:sess.repopath}; // file:sess.mainPath, platform:sess.platform_id}; setWaitDialog(false); gaEvent('sync', 'import', githuburl); gotoNewLocation(replaceURL); }).catch( (e) => { setWaitDialog(false); console.log(e); alertError("

Could not import " + githuburl + ".

" + e); }); } function _loginToGithub(e) { getGithubService().login().then(() => { alertInfo("You are signed in to Github."); }).catch( (e) => { alertError("

Could not sign in.

" + e); }); } function _logoutOfGithub(e) { getGithubService().logout().then(() => { alertInfo("You are logged out of Github."); }); } function _importProjectFromGithub(e) { var modal = $("#importGithubModal"); var btn = $("#importGithubButton"); modal.modal('show'); btn.off('click').on('click', () => { var githuburl = $("#importGithubURL").val()+""; modal.modal('hide'); importProjectFromGithub(githuburl, false); }); } function _publishProjectToGithub(e) { if (repo_id) { if (!confirm("This project (" + current_project.mainPath + ") is already bound to a Github repository. Do you want to re-publish to a new repository? (You can instead choose 'Push Changes' to update files in the existing repository.)")) return; } var modal = $("#publishGithubModal"); var btn = $("#publishGithubButton"); $("#githubRepoName").val(getFilenamePrefix(getFilenameForPath(current_project.mainPath))); modal.modal('show'); btn.off('click').on('click', () => { var name = $("#githubRepoName").val()+""; var desc = $("#githubRepoDesc").val()+""; var priv = $("#githubRepoPrivate").val() == 'private'; var license = $("#githubRepoLicense").val()+""; var sess; if (!name) { alertError("You did not enter a project name."); return; } modal.modal('hide'); setWaitDialog(true); getGithubService().login().then( () => { setWaitProgress(0.25); return getGithubService().publish(name, desc, license, priv); }).then( (_sess) => { sess = _sess; setWaitProgress(0.5); repo_id = qs['repo'] = sess.repopath; return pushChangesToGithub('initial import from 8bitworkshop.com'); }).then( () => { gaEvent('sync', 'publish', priv?"":name); importProjectFromGithub(sess.url, false); }).catch( (e) => { setWaitDialog(false); console.log(e); alertError("Could not publish GitHub repository: " + e); }); }); } function _pushProjectToGithub(e) { var ghurl = getBoundGithubURL(); if (!ghurl) return; var modal = $("#pushGithubModal"); var btn = $("#pushGithubButton"); modal.modal('show'); btn.off('click').on('click', () => { var commitMsg = $("#githubCommitMsg").val()+""; modal.modal('hide'); pushChangesToGithub(commitMsg); }); } function _pullProjectFromGithub(e) { var ghurl = getBoundGithubURL(); if (!ghurl) return; bootbox.confirm("Pull from repository and replace all local files? Any changes you've made will be overwritten.", (ok) => { if (ok) { setWaitDialog(true); getGithubService().pull(ghurl).then( (sess:GHSession) => { setWaitDialog(false); projectWindows.updateAllOpenWindows(store); }); } }); } function confirmCommit(sess) : Promise { return new Promise( (resolve, reject) => { var files = sess.commit.files; console.log(files); // anything changed? if (files.length == 0) { setWaitDialog(false); bootbox.alert("No files changed."); return; } // build commit confirm message var msg = ""; for (var f of files) { msg += f.filename + ": " + f.status; if (f.additions || f.deletions || f.changes) { msg += " (" + f.additions + " additions, " + f.deletions + " deletions, " + f.changes + " changes)"; }; msg += "
"; } // show dialog, continue when yes bootbox.confirm(msg, (ok) => { if (ok) { resolve(sess); } else { setWaitDialog(false); } }); }); } function pushChangesToGithub(message:string) { var ghurl = getBoundGithubURL(); if (!ghurl) return; // build file list for push var files = []; for (var path in current_project.filedata) { var newpath = current_project.stripLocalPath(path); var data = current_project.filedata[path]; if (newpath && data) { files.push({path:newpath, data:data}); } } // include built ROM file in bin/[mainfile].rom if (current_output instanceof Uint8Array) { let binpath = "bin/"+getCurrentMainFilename()+".rom"; files.push({path:binpath, data:current_output}); } // push files setWaitDialog(true); return getGithubService().login().then( () => { setWaitProgress(0.5); return getGithubService().commit(ghurl, message, files); }).then( (sess) => { return confirmCommit(sess); }).then( (sess) => { return getGithubService().push(sess); }).then( (sess) => { setWaitDialog(false); alertInfo("Pushed files to " + ghurl); return sess; }).catch( (e) => { setWaitDialog(false); console.log(e); alertError("Could not push GitHub repository: " + e); }); } function _deleteRepository() { var ghurl = getBoundGithubURL(); if (!ghurl) return; bootbox.prompt("

Are you sure you want to delete this repository (" + ghurl + ") from browser storage?

All changes since last commit will be lost.

Type DELETE to proceed.

", (yes) => { if (yes.trim().toUpperCase() == "DELETE") { deleteRepository(); } }); } function deleteRepository() { var ghurl = getBoundGithubURL(); var gh; setWaitDialog(true); // delete all keys in storage store.keys().then((keys:string[]) => { return Promise.all(keys.map((key) => { return store.removeItem(key); })); }).then(() => { gh = getGithubService(); return gh.getGithubSession(ghurl); }).then((sess) => { // un-bind repo from list gh.bind(sess, false); }).then(() => { setWaitDialog(false); // leave repository qs = {repo:'/'}; gotoNewLocation(); }); } function _shareEmbedLink(e) { if (current_output == null) { alertError("Please fix errors before sharing."); return true; } if (!(current_output instanceof Uint8Array)) { alertError("Can't share a Verilog executable yet. (It's not actually a ROM...)"); return true; } loadClipboardLibrary(); loadScript('lib/liblzg.js').then( () => { // TODO: Module is bad var name (conflicts with MAME) var lzgrom = compressLZG( window['Module'], Array.from(current_output) ); window['Module'] = null; // so we load it again next time var lzgb64 = btoa(byteArrayToString(lzgrom)); var embed = { p: platform_id, //n: current_project.mainPath, r: lzgb64 }; var linkqs = $.param(embed); var fulllink = get8bitworkshopLink(linkqs, 'embed.html'); var iframelink = '