"use strict"; // 8bitworkshop IDE user interface import $ = require("jquery"); import * as bootstrap from "bootstrap"; import { CodeProject } from "./project"; import { WorkerResult, WorkerOutput, VerilogOutput, SourceFile, WorkerError, FileData } from "./workertypes"; import { ProjectWindows } from "./windows"; import { Platform, Preset, DebugSymbols, DebugEvalCondition } from "./baseplatform"; import { PLATFORMS, EmuHalt, Toolbar } from "./emu"; import * as Views from "./views"; import { createNewPersistentStore } from "./store"; import { getFilenameForPath, getFilenamePrefix, highlightDifferences, invertMap, byteArrayToString, compressLZG, byteArrayToUTF8, isProbablyBinary, getWithBinary } from "./util"; import { StateRecorderImpl } from "./recorder"; import { GithubService } from "./services"; // external libs (TODO) declare var Tour, GIF, saveAs, JSZip, Mousetrap, Split, firebase; // 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 export var platform : Platform; // platform 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_entry : Preset; // current preset object (if selected) var store; // persistent store export var compparams; // received build params from worker export var lastDebugState; // last debug state (object) var lastDebugInfo; // last debug info (CPU text) var debugCategory; // current debug category var debugTickPaused = false; var recorderActive = false; var lastBreakExpr = "c.PC == 0x6000"; // TODO: codemirror multiplex support? var TOOL_TO_SOURCE_STYLE = { 'dasm': '6502', 'acme': '6502', 'cc65': 'text/x-csrc', 'ca65': '6502', 'z80asm': 'z80', 'sdasz80': 'z80', 'sdcc': 'text/x-csrc', 'verilator': 'verilog', 'jsasm': 'z80', 'zmac': 'z80', 'bataribasic': 'bataribasic', 'markdown': 'markdown', 'xasm6809': 'z80' } 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); localStorage.removeItem(key); return true; } catch (e) { return false; } }(); function getCurrentPresetTitle() : string { if (!current_preset_entry) return current_project.mainPath || "ROM"; else return current_preset_entry.title || current_preset_entry.name || current_project.mainPath || "ROM"; } function setLastPreset(id:string) { if (hasLocalStorage) { localStorage.setItem("__lastplatform", platform_id); localStorage.setItem("__lastid_"+platform_id, id); } } function unsetLastPreset() { if (hasLocalStorage) { delete qs['file']; localStorage.removeItem("__lastid_"+platform_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", "#"); if (id == projectWindows.getActiveID()) $(a).addClass("dropdown-item-checked"); a.appendChild(document.createTextNode(name)); li.appendChild(a); ul.append(li); if (createfn) { projectWindows.setCreateFunc(id, createfn); $(a).click( (e) => { projectWindows.createOrShow(id); ul.find('a').removeClass("dropdown-item-checked"); ul.find(e.target).addClass("dropdown-item-checked"); }); } } 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) { var data = current_project.getFile(id); if (typeof data === 'string') addWindowItem(id, getFilenameForPath(id), loadEditor); else if (data instanceof Uint8Array) addWindowItem(id, getFilenameForPath(id), () => { 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 // TODO: update listing when recompiling 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) { addWindowItem("#disasm", "Disassembly", () => { return new Views.DisassemblerView(); }); } if (platform.readAddress) { addWindowItem("#memory", "Memory Browser", () => { return new Views.MemoryView(); }); } if (platform.readVRAMAddress) { addWindowItem("#memvram", "VRAM Browser", () => { return new Views.VRAMMemoryView(); }); } if (current_project.segments) { addWindowItem("#memmap", "Memory Map", () => { return new Views.MemoryMapView(); }); } if (platform.startProfiling && platform.runEval && platform.getRasterScanline) { addWindowItem("#profiler", "Profiler", () => { return new Views.ProfileView(); }); } addWindowItem('#asseteditor', 'Asset Editor', () => { return new Views.AssetEditorView(); }); } // can pass integer or string id function loadProject(preset_id:string) { var index = parseInt(preset_id+""); // might fail -1 for (var i=0; i= 0) { // load the preset current_preset_entry = PRESETS[index]; preset_id = current_preset_entry.id; } // set current file ID current_project.mainPath = preset_id; setLastPreset(preset_id); // load files from storage or web URLs current_project.loadFiles([preset_id], function(err, result) { if (err) { alert(err); } else if (result && result.length) { // 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); } }); } function reloadPresetNamed(id:string) { qs['platform'] = platform_id; qs['file'] = id; gotoNewLocation(); } function getSkeletonFile(fileid:string, callback) { var ext = platform.getToolForFilename(fileid); // TODO: .mame $.get( "presets/"+platform_id+"/skeleton."+ext, function( text ) { callback(null, text); }, 'text') .fail(() => { alert("Could not load skeleton for " + platform_id + "/" + ext + "; using blank file"); callback(null, '\n'); }); } function checkEnteredFilename(fn : string) : boolean { if (fn.indexOf(" ") >= 0) { alert("No spaces in filenames, please."); return false; } return true; } function _createNewFile(e) { // TODO: support spaces var filename = prompt("Create New File", "newfile" + platform.getDefaultExtension()); if (filename && filename.trim().length > 0) { if (!checkEnteredFilename(filename)) return; if (filename.indexOf(".") < 0) { filename += platform.getDefaultExtension(); } var path = "local/" + filename; getSkeletonFile(path, function(err, result) { if (result) { store.setItem(path, result, function(err, result) { if (err) alert(err+""); if (result != null) reloadPresetNamed("local/" + filename); }); } }); } return true; } function _uploadNewFile(e) { $("#uploadFileElem").click(); } function handleFileUpload(files: File[]) { console.log(files); var index = 0; var gotoMainFile = (files.length == 1); function uploadNextFile() { var f = files[index++]; if (!f) { console.log("Done uploading"); if (gotoMainFile) { gotoNewLocation(); } else { updateSelector(); alert("Files uploaded."); } } else { var path = "local/" + 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 // TODO: use projectWindows uploadFile() store.setItem(path, data, function(err, result) { if (err) alert("Error uploading " + path + ": " + err); else { console.log("Uploaded " + path + " " + data.length + " bytes"); if (index == 1) { qs['file'] = path; // TODO? } 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) { 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) { loadScript("lib/octokat.js", () => { // get github API key from cookie var ghkey = getCookie('__github_key'); var ghopts = {token:ghkey}; githubService = new GithubService(new exports['Octokat'](ghopts), store, current_project); }); } return githubService; } function _importProjectFromGithub(e) { getGithubService(); // load it var modal = $("#importGithubModal"); var btn = $("#importGithubButton"); modal.modal('show'); btn.off('click').on('click', () => { var githuburl = $("#importGithubURL").val()+""; getGithubService().import(githuburl).then( (sess) => { // TODO : redirect to main file modal.modal('hide'); }).catch( (e) => { modal.modal('hide'); alert("Could not import " + githuburl + ": " + e); }); }); } function _publishProjectToGithub(e) { getGithubService(); // load it var modal = $("#publishGithubModal"); var btn = $("#publishGithubButton"); 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()+""; getGithubService().publish(name, desc, license, priv).then( (sess) => { modal.modal('hide'); // TODO: commit files // TODO: migrate project files console.log(sess); alert("Created repository at " + sess.url); pushChangesToGithub('initial import from 8bitworkshop.com'); }).catch( (e) => { modal.modal('hide'); alert("Could not create GitHub repository: " + e); }); }); } function _pushProjectToGithub(e) { getGithubService(); // load it var modal = $("#pushGithubModal"); var btn = $("#pushGithubButton"); modal.modal('show'); btn.off('click').on('click', () => { var commitMsg = $("#githubCommitMsg").val()+""; modal.modal('hide'); pushChangesToGithub(commitMsg); }); } function pushChangesToGithub(message:string) { var ghurl = getGithubService().getBoundURL(current_project.mainPath); console.log("Github URL: " + ghurl); if (!ghurl) { alert("This project is not bound to a GitHub project."); return; } // build file list for push var files = []; for (var path in current_project.filedata) { var newpath = current_project.stripLocalPath(path); files.push({path:newpath, data:current_project.filedata[path]}); } // push files setWaitDialog(true); getGithubService().commitPush(ghurl, message, files).then( (sess) => { setWaitDialog(false); }); } // TODO: remove? function loadSharedGist(gistkey : string) { loadScript("lib/octokat.js", () => { var github = new exports['Octokat'](); var gist = this.github.gists(gistkey); gist.fetch().done( (val) => { var filename; var newid; console.log("Fetched " + gistkey, val); store = createNewPersistentStore(platform_id, (store) => { for (filename in val.files) { store.setItem('shared/'+filename, val.files[filename].content); if (!newid) newid = 'shared/'+filename; } // TODO: wait for set? delete qs['gistkey']; reloadPresetNamed(newid); }); }).fail(function(err) { alert("Error loading share file: " + err.message); }); }); } function _shareEmbedLink(e) { if (current_output == null) { // TODO alert("Please fix errors before sharing."); return true; } if (!(current_output instanceof Uint8Array)) { alert("Can't share a Verilog executable yet. (It's not actually a ROM...)"); return true; } loadClipboardLibrary(); loadScript('lib/liblzg.js', () => { // 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 = '