// 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; declare var browserDetect; // 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? 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', } function gaEvent(category:string, action:string, label?:string, value?:string) { if (ga) ga('send', 'event', category, action, label, value); } function alertError(s:string) { gaEvent('error', platform_id||'error', s); setWaitDialog(false); bootbox.alert(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"); } var 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() { if (navigator.storage && navigator.storage.persist) { navigator.storage.persist().then(persistent=>{ if (persistent) { alertInfo("Your browser says you have unlimited persistent storage quota for this site. If you got an error while storing something, make sure you have enough storage space, and please try again."); } else { alertInfo("Your browser says your local files may not be persisted. Are you in a private window?"); } }); } else { alertInfo("Your browser doesn't support expanding the persistent storage quota."); } } 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) 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); 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); $("#error_alert").hide(); } $('#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("#spheatmap", "Stack Probe", () => { return new Views.RasterStackMapView(); }); */ } 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."); } 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 = '