From 5ce821f450a317ee5f667f861855ed4321f8bcbd Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 22 Mar 2026 16:32:37 -0700 Subject: [PATCH 01/10] Implement local storage setFileData --- src/ide/ui.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ide/ui.ts b/src/ide/ui.ts index 3e016724..80235833 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -691,7 +691,10 @@ async function getLocalFilesystem(repoid: string): Promise { return contents; }, setFileData: async (path, data) => { - //let vh = await dirHandle.getFileHandle(path, { create: true }); + const fileHandle = await dirHandle.getFileHandle(path, { create: true }); + const writable = await fileHandle.createWritable(); + await writable.write(data); + await writable.close(); } } } From 29ec96ce954f9e7e045c07a375b0c4dd45f01eb8 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 22 Mar 2026 16:33:19 -0700 Subject: [PATCH 02/10] Fix local storage getFileData binary file handling --- src/ide/ui.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ide/ui.ts b/src/ide/ui.ts index 80235833..8c54d972 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -683,11 +683,11 @@ async function getLocalFilesystem(repoid: string): Promise { getFileData: async (path) => { console.log('getFileData', path); let fileHandle = await dirHandle.getFileHandle(path, { create: false }); - console.log('getFileData', fileHandle); let file = await fileHandle.getFile(); - console.log('getFileData', file); - let contents = await (isProbablyBinary(path) ? file.binary() : file.text()); - console.log(fileHandle, file, contents); + let contents = await (isProbablyBinary(path) ? file.arrayBuffer() : file.text()); + if (contents instanceof ArrayBuffer) { + return new Uint8Array(contents); + } return contents; }, setFileData: async (path, data) => { From a5a0e9acbf47517f48e200c9ce0c78f29a1da72d Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 22 Mar 2026 17:27:07 -0700 Subject: [PATCH 03/10] Reformat src/ide/projects.ts --- src/ide/project.ts | 164 +++++++++++++++++++++++---------------------- 1 file changed, 83 insertions(+), 81 deletions(-) diff --git a/src/ide/project.ts b/src/ide/project.ts index c40c1376..a1d7c5e1 100644 --- a/src/ide/project.ts +++ b/src/ide/project.ts @@ -1,32 +1,32 @@ -import { FileData, Dependency, SourceLine, SourceFile, CodeListing, CodeListingMap, WorkerError, Segment, WorkerResult, WorkerOutputResult, isUnchanged, isOutputResult, WorkerMessage, WorkerItemUpdate, WorkerErrorResult, isErrorResult } from "../common/workertypes"; -import { getFilenamePrefix, getFolderForPath, isProbablyBinary, getBasePlatform, getWithBinary } from "../common/util"; -import { Platform } from "../common/baseplatform"; import localforage from "localforage"; +import { Platform } from "../common/baseplatform"; +import { getBasePlatform, getFilenamePrefix, getFolderForPath, getWithBinary, isProbablyBinary } from "../common/util"; +import { CodeListing, CodeListingMap, Dependency, FileData, Segment, SourceFile, WorkerErrorResult, WorkerItemUpdate, WorkerMessage, WorkerOutputResult, WorkerResult, isErrorResult, isOutputResult } from "../common/workertypes"; export interface ProjectFilesystem { - getFileData(path: string) : Promise; - setFileData(path: string, data: FileData) : Promise; + getFileData(path: string): Promise; + setFileData(path: string, data: FileData): Promise; } export class WebPresetsFileSystem implements ProjectFilesystem { - preset_id : string; + preset_id: string; constructor(platform_id: string) { this.preset_id = getBasePlatform(platform_id); // remove .suffix from preset name } async getRemoteFile(path: string): Promise { - return new Promise( (yes,no)=> { + return new Promise((yes, no) => { return getWithBinary(path, yes, isProbablyBinary(path) ? 'arraybuffer' : 'text'); }); } - async getFileData(path: string) : Promise { + async getFileData(path: string): Promise { // found on remote fetch? var webpath = "presets/" + this.preset_id + "/" + path; var data = await this.getRemoteFile(webpath); - if (data) console.log("read",webpath,data.length,'bytes'); + if (data) console.log("read", webpath, data.length, 'bytes'); return data; } - async setFileData(path: string, data: FileData) : Promise { + async setFileData(path: string, data: FileData): Promise { // not implemented } } @@ -42,7 +42,7 @@ export class NullFilesystem implements ProjectFilesystem { this.sets.push(path); return; } - + } export class OverlayFilesystem implements ProjectFilesystem { @@ -79,34 +79,34 @@ export class LocalForageFilesystem { } } -type BuildResultCallback = (result:WorkerResult) => void; -type BuildStatusCallback = (busy:boolean) => void; -type IterateFilesCallback = (path:string, data:FileData) => void; +type BuildResultCallback = (result: WorkerResult) => void; +type BuildStatusCallback = (busy: boolean) => void; +type IterateFilesCallback = (path: string, data: FileData) => void; -function isEmptyString(text : FileData) { +function isEmptyString(text: FileData) { return typeof text == 'string' && text.trim && text.trim().length == 0; } export class CodeProject { - filedata : {[path:string]:FileData} = {}; - listings : CodeListingMap; - segments : Segment[]; - mainPath : string; + filedata: { [path: string]: FileData } = {}; + listings: CodeListingMap; + segments: Segment[]; + mainPath: string; pendingWorkerMessages = 0; tools_preloaded = {}; - worker : Worker; - platform_id : string; - platform : Platform; - isCompiling : boolean = false; + worker: Worker; + platform_id: string; + platform: Platform; + isCompiling: boolean = false; filename2path = {}; // map stripped paths to full paths - filesystem : ProjectFilesystem; - dataItems : WorkerItemUpdate[]; - remoteTool? : string; + filesystem: ProjectFilesystem; + dataItems: WorkerItemUpdate[]; + remoteTool?: string; - callbackBuildResult : BuildResultCallback; - callbackBuildStatus : BuildStatusCallback; + callbackBuildResult: BuildResultCallback; + callbackBuildStatus: BuildStatusCallback; - constructor(worker, platform_id:string, platform, filesystem: ProjectFilesystem) { + constructor(worker, platform_id: string, platform, filesystem: ProjectFilesystem) { this.worker = worker; this.platform_id = platform_id; this.platform = platform; @@ -117,7 +117,7 @@ export class CodeProject { }; } - receiveWorkerMessage(data : WorkerResult) { + receiveWorkerMessage(data: WorkerResult) { var notfinal = this.pendingWorkerMessages > 1; if (notfinal) { this.sendBuild(); @@ -144,15 +144,15 @@ export class CodeProject { } } - preloadWorker(path:string) { + preloadWorker(path: string) { var tool = this.getToolForFilename(path); if (tool && !this.tools_preloaded[tool]) { - this.worker.postMessage({preload:tool, platform:this.platform_id}); + this.worker.postMessage({ preload: tool, platform: this.platform_id }); this.tools_preloaded[tool] = true; } } - pushAllFiles(files:string[], fn:string) { + pushAllFiles(files: string[], fn: string) { // look for local and preset files files.push(fn); // look for files in current (main file) folder @@ -162,7 +162,7 @@ export class CodeProject { } // TODO: use tool id to parse files, not platform - parseIncludeDependencies(text:string):string[] { + parseIncludeDependencies(text: string): string[] { let files = []; let m; if (this.platform_id.startsWith('verilog')) { @@ -179,7 +179,7 @@ export class CodeProject { // include .arch (json) statements let re2 = /^\s*([.]arch)\s+(\w+)/gmi; while (m = re2.exec(text)) { - this.pushAllFiles(files, m[2]+".json"); + this.pushAllFiles(files, m[2] + ".json"); } // include $readmem[bh] (TODO) let re3 = /\$readmem[bh]\("(.+?)"/gmi; @@ -226,7 +226,7 @@ export class CodeProject { return files; } - parseLinkDependencies(text:string):string[] { + parseLinkDependencies(text: string): string[] { let files = []; let m; if (this.platform_id.startsWith('verilog')) { @@ -240,8 +240,8 @@ export class CodeProject { } return files; } - - loadFileDependencies(text:string) : Promise { + + loadFileDependencies(text: string): Promise { let includes = this.parseIncludeDependencies(text); let linkfiles = this.parseLinkDependencies(text); let allfiles = includes.concat(linkfiles); @@ -256,49 +256,51 @@ export class CodeProject { }); } - okToSend():boolean { + okToSend(): boolean { return this.pendingWorkerMessages++ == 0 && this.mainPath != null; } - updateFileInStore(path:string, text:FileData) { + updateFileInStore(path: string, text: FileData) { this.filesystem.setFileData(path, text); } // TODO: test duplicate files, local paths mixed with presets - buildWorkerMessage(depends:Dependency[]) : WorkerMessage { + buildWorkerMessage(depends: Dependency[]): WorkerMessage { this.preloadWorker(this.mainPath); - var msg : WorkerMessage = {updates:[], buildsteps:[]}; + var msg: WorkerMessage = { updates: [], buildsteps: [] }; // TODO: add preproc directive for __MAINFILE__ var mainfilename = this.stripLocalPath(this.mainPath); var maintext = this.getFile(this.mainPath); var depfiles = []; - msg.updates.push({path:mainfilename, data:maintext}); + msg.updates.push({ path: mainfilename, data: maintext }); this.filename2path[mainfilename] = this.mainPath; const tool = this.getToolForFilename(this.mainPath); let usesRemoteTool = tool.startsWith('remote:'); for (var dep of depends) { // remote tools send both includes and linked files in one build step if (!dep.link || usesRemoteTool) { - msg.updates.push({path:dep.filename, data:dep.data}); + msg.updates.push({ path: dep.filename, data: dep.data }); depfiles.push(dep.filename); } this.filename2path[dep.filename] = dep.path; } msg.buildsteps.push({ - path:mainfilename, - files:[mainfilename].concat(depfiles), - platform:this.platform_id, - tool:this.getToolForFilename(this.mainPath), - mainfile:true}); + path: mainfilename, + files: [mainfilename].concat(depfiles), + platform: this.platform_id, + tool: this.getToolForFilename(this.mainPath), + mainfile: true + }); for (var dep of depends) { if (dep.data && dep.link) { this.preloadWorker(dep.filename); - msg.updates.push({path:dep.filename, data:dep.data}); + msg.updates.push({ path: dep.filename, data: dep.data }); msg.buildsteps.push({ - path:dep.filename, - files:[dep.filename].concat(depfiles), - platform:this.platform_id, - tool:this.getToolForFilename(dep.path)}); + path: dep.filename, + files: [dep.filename].concat(depfiles), + platform: this.platform_id, + tool: this.getToolForFilename(dep.path) + }); } } if (this.dataItems) msg.setitems = this.dataItems; @@ -306,14 +308,14 @@ export class CodeProject { } // TODO: get local file as well as presets? - async loadFiles(paths:string[]) : Promise { - var result : Dependency[] = []; - var addResult = (path:string, data:FileData) => { + async loadFiles(paths: string[]): Promise { + var result: Dependency[] = []; + var addResult = (path: string, data: FileData) => { result.push({ - path:path, - filename:this.stripLocalPath(path), - link:true, - data:data + path: path, + filename: this.stripLocalPath(path), + link: true, + data: data }); } for (var path of paths) { @@ -336,12 +338,12 @@ export class CodeProject { return result; } - getFile(path:string):FileData { + getFile(path: string): FileData { return this.filedata[path]; } // TODO: purge files not included in latest build? - iterateFiles(callback:IterateFilesCallback) { + iterateFiles(callback: IterateFilesCallback) { for (var path in this.filedata) { callback(path, this.getFile(path)); } @@ -354,18 +356,18 @@ export class CodeProject { if (maindata instanceof Uint8Array) { this.isCompiling = true; this.receiveWorkerMessage({ - output:maindata, - errors:[], - listings:null, - symbolmap:null, - params:{} + output: maindata, + errors: [], + listings: null, + symbolmap: null, + params: {} }); return; } // otherwise, make it a string var text = typeof maindata === "string" ? maindata : ''; // TODO: load dependencies of non-main files - return this.loadFileDependencies(text).then( (depends) => { + return this.loadFileDependencies(text).then((depends) => { if (!depends) depends = []; var workermsg = this.buildWorkerMessage(depends); this.worker.postMessage(workermsg); @@ -373,7 +375,7 @@ export class CodeProject { }); } - updateFile(path:string, text:FileData) { + updateFile(path: string, text: FileData) { if (this.filedata[path] == text) return; // unchanged, don't update this.updateFileInStore(path, text); // TODO: isBinary this.filedata[path] = text; @@ -383,7 +385,7 @@ export class CodeProject { } }; - setMainFile(path:string) { + setMainFile(path: string) { this.mainPath = path; if (this.callbackBuildStatus) this.callbackBuildStatus(true); this.sendBuild(); @@ -410,25 +412,25 @@ export class CodeProject { processBuildSegments(data: WorkerOutputResult) { // save and sort segment list - var segs : Segment[] = (this.platform.getMemoryMap && this.platform.getMemoryMap()["main"]) || []; + var segs: Segment[] = (this.platform.getMemoryMap && this.platform.getMemoryMap()["main"]) || []; if (segs?.length) { segs.forEach(seg => seg.source = 'native'); } if (data.segments) { data.segments.forEach(seg => seg.source = 'linker'); segs = segs.concat(data.segments || []); } - segs.sort((a,b) => {return a.start-b.start}); + segs.sort((a, b) => { return a.start - b.start }); this.segments = segs; } - getListings() : CodeListingMap { + getListings(): CodeListingMap { return this.listings; } // returns first listing in format [prefix].lst (TODO: could be better) - getListingForFile(path: string) : CodeListing { + getListingForFile(path: string): CodeListing { // ignore include files (TODO) //if (path.toLowerCase().endsWith('.h') || path.toLowerCase().endsWith('.inc')) - //return; + //return; var fnprefix = getFilenamePrefix(this.stripLocalPath(path)); // find listing with matching prefix var listings = this.getListings(); @@ -442,13 +444,13 @@ export class CodeProject { } } } - - stripLocalPath(path : string) : string { + + stripLocalPath(path: string): string { if (this.mainPath) { var folder = getFolderForPath(this.mainPath); // TODO: kinda weird if folder is same name as file prefix - if (folder != '' && path.startsWith(folder+'/')) { - path = path.substring(folder.length+1); + if (folder != '' && path.startsWith(folder + '/')) { + path = path.substring(folder.length + 1); } } return path; @@ -463,7 +465,7 @@ export class CodeProject { } -export function createNewPersistentStore(storeid:string) : LocalForage { +export function createNewPersistentStore(storeid: string): LocalForage { var store = localforage.createInstance({ name: "__" + storeid, version: 2.0 From 3a62eee58b883e1248fc9b883cc4ae55476dc27a Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 22 Mar 2026 17:27:36 -0700 Subject: [PATCH 04/10] Reformat src/ide/services.ts --- src/ide/services.ts | 287 ++++++++++++++++++++++---------------------- 1 file changed, 144 insertions(+), 143 deletions(-) diff --git a/src/ide/services.ts b/src/ide/services.ts index 236bada8..ddbb2f02 100644 --- a/src/ide/services.ts +++ b/src/ide/services.ts @@ -1,5 +1,5 @@ -import { getFolderForPath, isProbablyBinary, stringToByteArray, byteArrayToString, byteArrayToUTF8 } from "../common/util"; +import { byteArrayToString, byteArrayToUTF8, isProbablyBinary, stringToByteArray } from "../common/util"; import { FileData } from "../common/workertypes"; import { CodeProject, ProjectFilesystem } from "./project"; @@ -10,34 +10,34 @@ declare var firebase; // https://github.com/philschatz/octokat.js/tree/master/examples export interface GHRepoMetadata { - url : string; // github url - platform_id : string; // e.g. "vcs" - sha? : string; // head commit sha + url: string; // github url + platform_id: string; // e.g. "vcs" + sha?: string; // head commit sha mainPath?: string; // main file path - branch? : string; // "master" was default, now fetched from GH + branch?: string; // "master" was default, now fetched from GH } export interface GHSession extends GHRepoMetadata { - user : string; // user name - reponame : string; // repo name - repopath : string; // "user/repo" - subtreepath : string; // tree/[branch]/[...] - prefix : string; // file prefix, "local/" or "" - repo : any; // [repo object] - tree? : any; // [tree object] - head? : any; // [head ref] + user: string; // user name + reponame: string; // repo name + repopath: string; // "user/repo" + subtreepath: string; // tree/[branch]/[...] + prefix: string; // file prefix, "local/" or "" + repo: any; // [repo object] + tree?: any; // [tree object] + head?: any; // [head ref] commit?: any; // after commit() - paths? : string[]; + paths?: string[]; } const README_md_template = "$NAME\n=====\n\n[Open this project in 8bitworkshop](http://8bitworkshop.com/redir.html?platform=$PLATFORM&githubURL=$GITHUBURL&file=$MAINFILE).\n"; -export function getRepos() : {[key:string]:GHRepoMetadata} { +export function getRepos(): { [key: string]: GHRepoMetadata } { var repos = {}; - for (var i=0; i any, githubToken:string, store, project : CodeProject) { + constructor(githubCons: () => any, githubToken: string, store, project: CodeProject) { this.githubCons = githubCons; this.githubToken = githubToken; this.store = store; this.project = project; this.recreateGithub(); } - + recreateGithub() { - this.github = new this.githubCons({token:this.githubToken}); + this.github = new this.githubCons({ token: this.githubToken }); } - - login() : Promise { + + login(): Promise { // already logged in? return immediately if (this.githubToken && this.githubToken.length) { - return new Promise( (yes,no) => { + return new Promise((yes, no) => { yes(); }); } // login via popup var provider = new firebase.auth.GithubAuthProvider(); provider.addScope('repo'); - return firebase.auth().signInWithPopup(provider).then( (result) => { + return firebase.auth().signInWithPopup(provider).then((result) => { this.githubToken = result.credential.accessToken; var user = result.user; this.recreateGithub(); @@ -92,11 +92,11 @@ export class GithubService { console.log("Stored GitHub OAUTH key"); }); } - - logout() : Promise { + + logout(): Promise { // already logged out? return immediately if (!(this.githubToken && this.githubToken.length)) { - return new Promise( (yes,no) => { + return new Promise((yes, no) => { yes(); }); } @@ -107,8 +107,8 @@ export class GithubService { this.recreateGithub(); }); } - - isFileIgnored(s : string) : boolean { + + isFileIgnored(s: string): boolean { s = s.toUpperCase(); if (s.startsWith("LICENSE")) return true; if (s.startsWith("README")) return true; @@ -116,7 +116,7 @@ export class GithubService { return false; } - async getGithubSession(ghurl:string) : Promise { + async getGithubSession(ghurl: string): Promise { var urlparse = parseGithubURL(ghurl); if (!urlparse) { throw new Error("Please enter a valid GitHub URL."); @@ -149,103 +149,104 @@ export class GithubService { return sess; } - getGithubHEADTree(ghurl:string) : Promise { + getGithubHEADTree(ghurl: string): Promise { var sess; - return this.getGithubSession(ghurl).then( (session) => { + return this.getGithubSession(ghurl).then((session) => { sess = session; return sess.repo.git.refs.heads(sess.branch).fetch(); }) - .then( (head) => { - sess.head = head; - sess.sha = head.object.sha; - return sess.repo.git.trees(sess.sha).fetch(); - }) - .then( (tree) => { - if (sess.subtreepath) { - for (let subtree of tree.tree) { - if (subtree.type == 'tree' && subtree.path == sess.subtreepath && subtree.sha) { - return sess.repo.git.trees(subtree.sha).fetch(); + .then((head) => { + sess.head = head; + sess.sha = head.object.sha; + return sess.repo.git.trees(sess.sha).fetch(); + }) + .then((tree) => { + if (sess.subtreepath) { + for (let subtree of tree.tree) { + if (subtree.type == 'tree' && subtree.path == sess.subtreepath && subtree.sha) { + return sess.repo.git.trees(subtree.sha).fetch(); + } } + throw Error("Cannot find subtree '" + sess.subtreepath + "' in tree " + tree.sha); } - throw Error("Cannot find subtree '" + sess.subtreepath + "' in tree " + tree.sha); - } - return tree; - }) - .then( (tree) => { - sess.tree = tree; - return sess; - }); + return tree; + }) + .then((tree) => { + sess.tree = tree; + return sess; + }); } - - bind(sess:GHSession, dobind:boolean) { + + bind(sess: GHSession, dobind: boolean) { var key = '__repo__' + sess.repopath; if (dobind) { - var repodata : GHRepoMetadata = { - url:sess.url, - branch:sess.branch, - platform_id:sess.platform_id, - mainPath:sess.mainPath, - sha:sess.sha}; + var repodata: GHRepoMetadata = { + url: sess.url, + branch: sess.branch, + platform_id: sess.platform_id, + mainPath: sess.mainPath, + sha: sess.sha + }; console.log('storing', repodata); localStorage.setItem(key, JSON.stringify(repodata)); } else { localStorage.removeItem(key); } } - - import(ghurl:string) : Promise { - var sess : GHSession; - return this.getGithubSession(ghurl).then( (session) => { + + import(ghurl: string): Promise { + var sess: GHSession; + return this.getGithubSession(ghurl).then((session) => { sess = session; // load README return sess.repo.contents('README.md').read(); }) - .catch( (e) => { - console.log(e); - console.log('no README.md found') - // make user repo exists - return sess.repo.fetch().then( (_repo) => { - return ''; // empty README + .catch((e) => { + console.log(e); + console.log('no README.md found') + // make user repo exists + return sess.repo.fetch().then((_repo) => { + return ''; // empty README + }) }) - }) - .then( (readme) => { - var m; - // check README for main file - const re8main = /8bitworkshop.com[^)]+file=([^)&]+)/; - m = re8main.exec(readme); - if (m && m[1]) { - console.log("main path: '" + m[1] + "'"); - sess.mainPath = m[1]; - } - // check README for proper platform - // unless we use githubURL= - // TODO: cannot handle multiple URLs in README - const re8plat = /8bitworkshop.com[^)]+platform=([A-Za-z0-9._\-]+)/; - m = re8plat.exec(readme); - if (m) { - console.log("platform id: '" + m[1] + "'"); - if (sess.platform_id && !sess.platform_id.startsWith(m[1])) - throw Error("Platform mismatch: Repository is " + m[1] + ", you have " + sess.platform_id + " selected."); - sess.platform_id = m[1]; - } - // bind to repository - this.bind(sess, true); - // get head commit - return sess; - }); + .then((readme) => { + var m; + // check README for main file + const re8main = /8bitworkshop.com[^)]+file=([^)&]+)/; + m = re8main.exec(readme); + if (m && m[1]) { + console.log("main path: '" + m[1] + "'"); + sess.mainPath = m[1]; + } + // check README for proper platform + // unless we use githubURL= + // TODO: cannot handle multiple URLs in README + const re8plat = /8bitworkshop.com[^)]+platform=([A-Za-z0-9._\-]+)/; + m = re8plat.exec(readme); + if (m) { + console.log("platform id: '" + m[1] + "'"); + if (sess.platform_id && !sess.platform_id.startsWith(m[1])) + throw Error("Platform mismatch: Repository is " + m[1] + ", you have " + sess.platform_id + " selected."); + sess.platform_id = m[1]; + } + // bind to repository + this.bind(sess, true); + // get head commit + return sess; + }); } - - pull(ghurl:string, deststore?) : Promise { - var sess : GHSession; - return this.getGithubHEADTree(ghurl).then( (session) => { + + pull(ghurl: string, deststore?): Promise { + var sess: GHSession; + return this.getGithubHEADTree(ghurl).then((session) => { sess = session; let blobreads = []; sess.paths = []; - sess.tree.tree.forEach( (item) => { + sess.tree.tree.forEach((item) => { console.log(item.path, item.type, item.size); sess.paths.push(item.path); if (item.type == 'blob' && !this.isFileIgnored(item.path)) { - var read = sess.repo.git.blobs(item.sha).fetch().then( (blob) => { + var read = sess.repo.git.blobs(item.sha).fetch().then((blob) => { var path = sess.prefix + item.path; var size = item.size; var encoding = blob.encoding; @@ -267,18 +268,18 @@ export class GithubService { }); return Promise.all(blobreads); }) - .then( (blobs) => { - return sess; - }); + .then((blobs) => { + return sess; + }); } - - importAndPull(ghurl:string) { + + importAndPull(ghurl: string) { return this.import(ghurl).then((sess) => { return this.pull(ghurl); }); } - publish(reponame:string, desc:string, license:string, isprivate:boolean) : Promise { + publish(reponame: string, desc: string, license: string, isprivate: boolean): Promise { var repo; var platform_id = this.project.platform_id; var mainPath = this.project.stripLocalPath(this.project.mainPath); @@ -289,33 +290,33 @@ export class GithubService { auto_init: false, license_template: license }) - .then( (_repo) => { - repo = _repo; - // create README.md - var s = README_md_template; - s = s.replace(/\$NAME/g, encodeURIComponent(reponame)); - s = s.replace(/\$PLATFORM/g, encodeURIComponent(platform_id)); - s = s.replace(/\$GITHUBURL/g, encodeURIComponent(repo.htmlUrl)); - s = s.replace(/\$MAINFILE/g, encodeURIComponent(mainPath)); - var config = { - message: '8bitworkshop: updated metadata in README.md', - content: btoa(s) - } - return repo.contents('README.md').add(config); - }).then( () => { - return this.getGithubSession(repo.htmlUrl); - }); + .then((_repo) => { + repo = _repo; + // create README.md + var s = README_md_template; + s = s.replace(/\$NAME/g, encodeURIComponent(reponame)); + s = s.replace(/\$PLATFORM/g, encodeURIComponent(platform_id)); + s = s.replace(/\$GITHUBURL/g, encodeURIComponent(repo.htmlUrl)); + s = s.replace(/\$MAINFILE/g, encodeURIComponent(mainPath)); + var config = { + message: '8bitworkshop: updated metadata in README.md', + content: btoa(s) + } + return repo.contents('README.md').add(config); + }).then(() => { + return this.getGithubSession(repo.htmlUrl); + }); } - - commit( ghurl:string, message:string, files:{path:string,data:FileData}[] ) : Promise { - var sess : GHSession; + + commit(ghurl: string, message: string, files: { path: string, data: FileData }[]): Promise { + var sess: GHSession; if (!message) { message = "updated from 8bitworkshop.com"; } - return this.getGithubHEADTree(ghurl).then( (session) => { + return this.getGithubHEADTree(ghurl).then((session) => { sess = session; if (sess.subtreepath) { throw Error("Sorry, right now you can only commit files to the root directory of a repository."); } - return Promise.all(files.map( (file) => { + return Promise.all(files.map((file) => { if (typeof file.data === 'string') { return sess.repo.git.blobs.create({ content: file.data, @@ -328,9 +329,9 @@ export class GithubService { }); } })); - }).then( (blobs) => { + }).then((blobs) => { return sess.repo.git.trees.create({ - tree: files.map( (file, index) => { + tree: files.map((file, index) => { return { path: file.path, mode: '100644', @@ -340,7 +341,7 @@ export class GithubService { }), base_tree: sess.tree.sha }); - }).then( (newtree) => { + }).then((newtree) => { return sess.repo.git.commits.create({ message: message, tree: newtree.sha, @@ -348,24 +349,24 @@ export class GithubService { sess.head.object.sha ] }); - }).then( (commit1) => { + }).then((commit1) => { return sess.repo.commits(commit1.sha).fetch(); - }).then( (commit) => { + }).then((commit) => { sess.commit = commit; return sess; }); } - push(sess:GHSession) : Promise { + push(sess: GHSession): Promise { return sess.head.update({ sha: sess.commit.sha - }).then( (update) => { + }).then((update) => { return sess; }); } - - deleteRepository(ghurl:string) { - return this.getGithubSession(ghurl).then( (session) => { + + deleteRepository(ghurl: string) { + return this.getGithubSession(ghurl).then((session) => { return session.repo.remove(); }); } @@ -380,8 +381,8 @@ export class FirebaseProjectFilesystem implements ProjectFilesystem { var database = firebase.database(); this.ref = database.ref('users/' + user_id + "/" + store_id); } - getChildForPath(path:string) { - var encodedPath = encodeURIComponent(path).replace('-','%2D').replace('.','%2E'); + getChildForPath(path: string) { + var encodedPath = encodeURIComponent(path).replace('-', '%2D').replace('.', '%2E'); return this.ref.child(encodedPath); } async getFileData(path: string): Promise { From f00f4e60aff94e30b8903b5f10ca3f1b6d1c426f Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 22 Mar 2026 18:50:30 -0700 Subject: [PATCH 05/10] LocalForageFilesystem implements ProjectFilesystem Add missing interface --- src/ide/project.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ide/project.ts b/src/ide/project.ts index a1d7c5e1..18ebdec6 100644 --- a/src/ide/project.ts +++ b/src/ide/project.ts @@ -66,7 +66,7 @@ export class OverlayFilesystem implements ProjectFilesystem { } } -export class LocalForageFilesystem { +export class LocalForageFilesystem implements ProjectFilesystem { store: any; constructor(store: any) { this.store = store; From c71318fb8f5e5bd72a170c5bfca11f94ac07de9a Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 22 Mar 2026 18:50:16 -0700 Subject: [PATCH 06/10] ProjectFilesystem.onFileSystemUpdate Prework to enabled local file system updates to automatically be seen by the editor --- src/ide/project.ts | 15 ++++++++++++++- src/ide/services.ts | 3 +++ src/ide/ui.ts | 17 +++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/ide/project.ts b/src/ide/project.ts index 18ebdec6..0e4fd1fe 100644 --- a/src/ide/project.ts +++ b/src/ide/project.ts @@ -7,6 +7,7 @@ import { CodeListing, CodeListingMap, Dependency, FileData, Segment, SourceFile, export interface ProjectFilesystem { getFileData(path: string): Promise; setFileData(path: string, data: FileData): Promise; + onFileSystemUpdate(callback: (path: string) => void): void; } export class WebPresetsFileSystem implements ProjectFilesystem { @@ -29,6 +30,9 @@ export class WebPresetsFileSystem implements ProjectFilesystem { async setFileData(path: string, data: FileData): Promise { // not implemented } + onFileSystemUpdate(callback: (path: string) => void): void { + // not implemented + } } export class NullFilesystem implements ProjectFilesystem { @@ -42,7 +46,9 @@ export class NullFilesystem implements ProjectFilesystem { this.sets.push(path); return; } - + onFileSystemUpdate(callback: (path: string) => void): void { + // not implemented + } } export class OverlayFilesystem implements ProjectFilesystem { @@ -64,6 +70,10 @@ export class OverlayFilesystem implements ProjectFilesystem { await this.overlayfs.setFileData(path, data); return this.basefs.setFileData(path, data); } + onFileSystemUpdate(callback: (path: string) => void): void { + this.overlayfs.onFileSystemUpdate(callback); + this.basefs.onFileSystemUpdate(callback); + } } export class LocalForageFilesystem implements ProjectFilesystem { @@ -77,6 +87,9 @@ export class LocalForageFilesystem implements ProjectFilesystem { async setFileData(path: string, data: FileData): Promise { return this.store.setItem(path, data); } + onFileSystemUpdate(callback: (path: string) => void): void { + // not implemented + } } type BuildResultCallback = (result: WorkerResult) => void; diff --git a/src/ide/services.ts b/src/ide/services.ts index ddbb2f02..5290570c 100644 --- a/src/ide/services.ts +++ b/src/ide/services.ts @@ -395,4 +395,7 @@ export class FirebaseProjectFilesystem implements ProjectFilesystem { filedata: data }); } + onFileSystemUpdate(callback: (path: string) => void): void { + // not implemented + } } diff --git a/src/ide/ui.ts b/src/ide/ui.ts index 8c54d972..a9c25d60 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -695,6 +695,23 @@ async function getLocalFilesystem(repoid: string): Promise { const writable = await fileHandle.createWritable(); await writable.write(data); await writable.close(); + }, + onFileSystemUpdate: (callback: (path: string) => void) => { + // Experimental API: + // https://developer.mozilla.org/docs/Web/API/FileSystemObserver + if (typeof (window as any).FileSystemObserver === 'undefined') { + return; + } + const observer = new (window as any).FileSystemObserver((records, observer) => { + for (const record of records) { + // TODO Handle different types of changes intelligently. + // https://developer.mozilla.org/docs/Web/API/FileSystemChangeRecord#type + if (record.changedHandle) { + callback(record.changedHandle.name); + } + } + }); + observer.observe(dirHandle, { recursive: true }); } } } From 0bab39bbfe8d678c9a1ac4e7cbd271350a0ede8f Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 22 Mar 2026 18:15:36 -0700 Subject: [PATCH 07/10] Implement BinaryFileView.setData Prepare debug view so that it is able to accept new data after it has been created, in order to accept incoming updates from the local file system. --- src/ide/views/debugviews.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/ide/views/debugviews.ts b/src/ide/views/debugviews.ts index a344f4c7..d118700f 100644 --- a/src/ide/views/debugviews.ts +++ b/src/ide/views/debugviews.ts @@ -202,6 +202,7 @@ export class VRAMMemoryView extends MemoryView { export class BinaryFileView implements ProjectView { vlist: VirtualTextScroller; + parent: HTMLElement; maindiv: HTMLElement; path: string; data: Uint8Array; @@ -212,12 +213,23 @@ export class BinaryFileView implements ProjectView { this.data = data; } + private populateVlist() { + $(this.vlist.maindiv).empty(); + this.vlist.create(this.parent, ((this.data.length + 15) >> 4), this.getMemoryLineAt.bind(this)); + } + createDiv(parent: HTMLElement) { - this.vlist = new VirtualTextScroller(parent); - this.vlist.create(parent, ((this.data.length + 15) >> 4), this.getMemoryLineAt.bind(this)); + this.parent = parent; + this.vlist = new VirtualTextScroller(this.parent); + this.populateVlist(); return this.vlist.maindiv; } + setData(data: Uint8Array) { + this.data = data; + this.populateVlist(); + } + getMemoryLineAt(row: number): VirtualTextLine { var offset = row * 16; var n1 = 0; From 73a694c869ca994560872e8fa6b0e134f401712a Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 22 Mar 2026 20:00:31 -0700 Subject: [PATCH 08/10] Update views on local file changes Integrate change that allow text and binary local file changes to propogate to views. --- src/ide/project.ts | 11 +++++++++++ src/ide/ui.ts | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/ide/project.ts b/src/ide/project.ts index 0e4fd1fe..509ce7c7 100644 --- a/src/ide/project.ts +++ b/src/ide/project.ts @@ -118,6 +118,7 @@ export class CodeProject { callbackBuildResult: BuildResultCallback; callbackBuildStatus: BuildStatusCallback; + onFileChanged: (path: string, data: FileData) => void; constructor(worker, platform_id: string, platform, filesystem: ProjectFilesystem) { this.worker = worker; @@ -128,6 +129,16 @@ export class CodeProject { worker.onmessage = (e) => { this.receiveWorkerMessage(e.data); }; + + filesystem.onFileSystemUpdate(async (path: string) => { + if (path in this.filedata) { + var data = await this.filesystem.getFileData(path); + if (data) { + this.updateFile(path, data); + if (this.onFileChanged) this.onFileChanged(path, data); + } + } + }); } receiveWorkerMessage(data: WorkerResult) { diff --git a/src/ide/ui.ts b/src/ide/ui.ts index a9c25d60..bfeb0ab0 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -262,6 +262,19 @@ async function initProject() { current_project.callbackBuildStatus = (busy: boolean) => { setBusyStatus(busy); }; + // Update views when file contents change. + current_project.onFileChanged = (path: string, data: FileData) => { + var wnd = projectWindows.id2window[path]; + if (wnd) { + if (wnd instanceof SourceEditor && typeof data === 'string') { + wnd.setText(data); + } else if (wnd instanceof BinaryFileView && data instanceof Uint8Array) { + wnd.setData(data); + } else { + console.warn('onFileChanged: unknown view or data type'); + } + } + }; } function setBusyStatus(busy: boolean) { From ac38b62614526138a9cd546c564b9607212ed344 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 22 Mar 2026 21:05:59 -0700 Subject: [PATCH 09/10] Also refresh the asset editor if it's the active view --- src/ide/ui.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ide/ui.ts b/src/ide/ui.ts index bfeb0ab0..8501fcad 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -262,7 +262,7 @@ async function initProject() { current_project.callbackBuildStatus = (busy: boolean) => { setBusyStatus(busy); }; - // Update views when file contents change. + // Update file views when file contents change. current_project.onFileChanged = (path: string, data: FileData) => { var wnd = projectWindows.id2window[path]; if (wnd) { @@ -274,6 +274,11 @@ async function initProject() { console.warn('onFileChanged: unknown view or data type'); } } + // Also refresh the asset editor if it's the active view. + var assetWnd = projectWindows.id2window['#asseteditor']; + if (assetWnd && assetWnd === projectWindows.getActive()) { + assetWnd.refresh(true); + } }; } From 73dc16f50ab182651a060d2e8bf9462a1b80450f Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 22 Mar 2026 22:47:28 -0700 Subject: [PATCH 10/10] Fix asset editor local dir interaction Ignore filesystem file change notifications that are likely from our making edits in the asset editor, so that the editor isn't refreshed and changing out from under us. --- src/ide/ui.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ide/ui.ts b/src/ide/ui.ts index 8501fcad..9ff0cd4d 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -697,6 +697,7 @@ async function getLocalFilesystem(repoid: string): Promise { alertError(`Could not get permission to access filesystem.`); return; } + const lastWriteTime: { [path: string]: number } = {}; return { getFileData: async (path) => { console.log('getFileData', path); @@ -709,6 +710,7 @@ async function getLocalFilesystem(repoid: string): Promise { return contents; }, setFileData: async (path, data) => { + lastWriteTime[path] = Date.now(); const fileHandle = await dirHandle.getFileHandle(path, { create: true }); const writable = await fileHandle.createWritable(); await writable.write(data); @@ -725,7 +727,12 @@ async function getLocalFilesystem(repoid: string): Promise { // TODO Handle different types of changes intelligently. // https://developer.mozilla.org/docs/Web/API/FileSystemChangeRecord#type if (record.changedHandle) { - callback(record.changedHandle.name); + const path = record.changedHandle.name; + // Ignore filesystem notifications that are likely from our own recent writes, + // so that onFileChanged in ui.ts doesn't call assets editor's refresh(true). + // TODO: Consider better options than a time based threshold. + if (Date.now() - (lastWriteTime[path] || 0) < 2000) continue; + callback(path); } } });