diff --git a/src/core/patcher.ts b/src/core/patcher.ts index a3f961f..e2607f1 100644 --- a/src/core/patcher.ts +++ b/src/core/patcher.ts @@ -16,6 +16,8 @@ export function applyPatches(mainScript: HTMLScriptElement) { console.group("[ ropeswing-patcher ]"); for (let extension of extensions) { + if (!extension.patches) continue; + for (let patch of extension.patches) { if (patch.executable) continue; // TODO: Using `as string` is bad, but TypeScript wasn't having it with my replace typings, and this works @@ -26,3 +28,16 @@ export function applyPatches(mainScript: HTMLScriptElement) { console.groupEnd(); } + +export function executePostload() { + console.group("[ ropeswing-postload ]"); + + for (let extension of extensions) { + if (!extension.onLoad) continue; + + extension.onLoad(); + console.log(`executed onLoad of ${extension.manifest.name}`); + } + + console.groupEnd(); +} diff --git a/src/ext/housekeeper.ts b/src/ext/housekeeper.ts new file mode 100644 index 0000000..ccfe6bc --- /dev/null +++ b/src/ext/housekeeper.ts @@ -0,0 +1,45 @@ +export default { + patches: [ + { + find: /Windows 96 main
/, + replace: (match) => `${match}ropeswing ${ROPESWING_COMMIT}
`, + }, + { + find: /Other trademarks and logos are property of their respective owners\./, + replace: (match) => `${match}\n\nropeswing ${ROPESWING_COMMIT}, an uwunet project.`, + executable: "C:/system/local/bin/about-ui", + } + ], + onLoad() { + if (localStorage["ropeswing-welcome"] !== "true") { + localStorage["ropeswing-welcome"] = "true"; + w96.WApplication.execAsync(this.dialog(w96.WApplication), []); + } + }, + manifest: { + name: "housekeeper", + description: "basic ropeswing info, e.g. welcome dialog, version listing", + authors: ["redstonekasi", "Beef"], + }, + core: true, + dialog: (WApplication: typeof w96.WApplication) => new (class WelcomeDialog extends WApplication { + async main(argv: string[]) { + super.main(argv); + // @ts-expect-error + const popup = this.createWindow({ + title: "ropeswing", + initialHeight: 120, + initialWidth: 260, + resizable: false, + bodyClass: "dlg-run-box", + controlBoxStyle: "WS_CBX_CLOSE", + }); + const body = popup.getBodyContainer(); + body.innerHTML = `
ropeswing has been installed!
Check your system settings in order to configure it.
`; + body.querySelector("button")?.addEventListener("click", () => popup.close(false)); + + popup.setPosition(window.innerWidth / 2 - 130, window.innerHeight / 2 - 60); + popup.show(); + } + })(), +} as Extension; diff --git a/src/ext/royalmail/index.ts b/src/ext/royalmail/index.ts index 6046613..2173e27 100644 --- a/src/ext/royalmail/index.ts +++ b/src/ext/royalmail/index.ts @@ -25,14 +25,14 @@ export default { authors: ["Beef"], }, core: true, - app: (WApplication: any, components: Record) => class RoyalmailApplet extends WApplication { + app: (WApplication: typeof w96.WApplication, components: Record) => class RoyalmailApplet extends WApplication { constructor() { super(); } // TODO: Check that this whole thing disposes properly. // By all right, it should, however I am unsure if the weird stuff I do with the pages will mess anything up. - public main = (argv: string[]) => useRoot((dispose) => { + public main = async (argv: string[]) => useRoot((dispose) => { // HACK: Persist the original onterminated behaviour because we are an applet, but also dispose our reactive root before("onterminated", this, dispose, true); if ((super.main(argv), document.querySelector(".royalmail-applet"))) return; diff --git a/src/ext/treebranch.ts b/src/ext/treebranch.ts index dce36c1..8583e57 100644 --- a/src/ext/treebranch.ts +++ b/src/ext/treebranch.ts @@ -16,10 +16,12 @@ export default { async getFileContent(path: string) { let content = await w96.FS.readstr(path); - for (const ext of extensions) { - for (let patch of ext.patches) { + for (const extension of extensions) { + if (!extension.patches) continue; + + for (let patch of extension.patches) { if (patch.executable !== path) continue; - content = content.replace(patch.find, contextify(patch.replace, ext.manifest.name) as string); + content = content.replace(patch.find, contextify(patch.replace, extension.manifest.name) as string); } } diff --git a/src/ext/version.ts b/src/ext/version.ts deleted file mode 100644 index 9a232b5..0000000 --- a/src/ext/version.ts +++ /dev/null @@ -1,19 +0,0 @@ -export default { - patches: [ - { - find: /Windows 96 main
/, - replace: (match) => `${match}ropeswing ${ROPESWING_COMMIT}
`, - }, - { - find: /Other trademarks and logos are property of their respective owners\./, - replace: (match) => `${match}\n\nropeswing ${ROPESWING_COMMIT}, an uwunet project.`, - executable: "C:/system/local/bin/about-ui", - } - ], - manifest: { - name: "version", - description: "show ropeswing version info on desktop", - authors: ["redstonekasi"], - }, - core: true, -} as Extension; diff --git a/src/index.ts b/src/index.ts index 2d3858b..ffb3f15 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import { instead } from "spitroast"; import { writeLine } from "@lib/console"; -import { applyPatches } from "@core/patcher"; +import { applyPatches, executePostload } from "@core/patcher"; import api from "@core/api"; // W96 like to do this, let's follow the convention @@ -23,35 +23,8 @@ const unpatch = instead("appendChild", document.head, (args, orig) => { writeLine("booting original!"); orig(...args); - if (localStorage["ropeswing-welcome"] !== "true") { - localStorage["ropeswing-welcome"] = "true"; - w96.evt.sys.on("init-complete", () => - w96.WApplication.execAsync( - new (class extends w96.WApplication { - main(argv) { - super.main(argv); - const popup = this.createWindow({ - title: "ropeswing", - initialHeight: 120, - initialWidth: 260, - resizable: false, - bodyClass: "dlg-run-box", - controlBoxStyle: "WS_CBX_CLOSE", - }); - const body = popup.getBodyContainer(); - body.innerHTML = `
ropeswing has been installed!
Check your system settings in order to configure it.
`; - body.querySelector("button").addEventListener("click", () => { - popup.close(); - }); - popup.setPosition(window.innerWidth / 2 - 130, window.innerHeight / 2 - 60); - popup.show(); - } - })(), - null, - null - ) - ); - } + writeLine("assigning postload init event..."); + w96.evt.sys.on("init-complete", executePostload); }); console.log("kernel done!"); diff --git a/src/types/core.d.ts b/src/types/core.d.ts index 985239f..c1a02a1 100644 --- a/src/types/core.d.ts +++ b/src/types/core.d.ts @@ -10,7 +10,11 @@ interface Patch { } interface Extension { - patches: Patch[]; + patches?: Patch[]; + /** Runs *after* w96 has initialised */ + onLoad?: () => void; + /** Currently unused. */ + onUnload?: () => void; manifest: { name: string; description: string; diff --git a/src/types/w96.d.ts b/src/types/w96.d.ts index 0cf79fa..db421fe 100644 --- a/src/types/w96.d.ts +++ b/src/types/w96.d.ts @@ -1,5 +1,135 @@ +// adapted from https://git.sys36.net/windows-96/utilities/api-typings/-/blob/master/win96.d.ts + +interface EmitterEvent { + name: string; + callback: Function; + type: "recurring" | "single"; +} + +interface EventEmitter { + eventQueue: EmitterEvent[]; + on(evtName: string, callback: Function): void; + once(evtName: string, callback: Function): void; + emit(evtName: string, ...args: any): void; +} + +interface WindowParams { + initialX?: number; + initialY?: number; + minHeight?: number; + minWidth?: number; + initialHeight?: number; + initialWidth?: number; + title?: string; + resizable?: boolean; + draggable?: boolean; + taskbar?: boolean; + icon?: string; + center?: boolean; + body?: string; + bodyClass?: string; + windowClass?: string; + controlBoxStyle?: "WS_CBX_MINMAXCLOSE" | "WS_CBX_CLOSE" | "WS_CBX_MINCLOSE" | "WS_CBX_NONE"; + mobResize?: boolean; + iframeFix?: boolean; + animations?: { + windowOpen: string; + windowClose: string; + }; + wndContainer?: HTMLDivElement; +} + +declare class StandardWindow { + readonly id: string; + readonly useIcon: boolean; + readonly wndObject: HTMLDivElement; + readonly animations: {}; + readonly params: WindowParams; + readonly isRegistered: boolean; + readonly appbarRegistered: boolean; + readonly shown: boolean; + readonly _wmStylingAllowed: boolean; + readonly maximized: boolean; + readonly minimized: boolean; + readonly uiUpdating: boolean; + readonly windowIcon: string; + readonly title: string; + readonly maximizeInfo: { + x: string; + y: string; + h: string; + w: string; + }; + + onclose: (e: { canceled: boolean }) => void; + onload: () => void; + ondarkenelements: () => void; + onlightenelements: () => void; + _ext: { + windowSnap: { + snapped: boolean; + originalSize: null; + }; + }; + showTitlebarMenu(e: MouseEvent): void; + registerWindow(): void; + registerAppBar(): void; + setTitle(text: string): void; + setHtml(text: string): void; + randomizePosition(): void; + show(): void; + center(current: boolean): void; + activate(): void; + toggleMinimize(): void; + toggleMaximize(): void; + close(ignoreEvents: boolean): void; + darkenElements(): void; + lightenElements(): void; + setWindowIcon(icon_url: string): HTMLDivElement; + setControlBoxStyle(cbstyle: "WS_CBX_CLOSE" | "WS_CBX_MINCLOSE" | "WS_CBX_MINMAXCLOSE" | "WS_CBX_NONE"): void; + setSize(w: number, h: number, ignoreThemeOffsets?: boolean): void; + setPosition(x: number, y: number): void; + getBounds(): { + x: number; + y: number; + height: number; + width: number; + }; + getComputedBounds(): DOMRect; + getBodyContainer(): HTMLDivElement; + constructor(params: WindowParams); +} + +declare class WApplication { + readonly appId: number; + readonly appWindow: StandardWindow; + readonly windows: StandardWindow[]; + onterminated: (result: any) => void; + _running: boolean; + _terminating: boolean; + _appResult: any; + setAppResult(v: any): void; + terminate(): void; + createWindow(params: WindowParams, isAppWindow: boolean): StandardWindow; + main(argv: string[]): Promise; + ontermination(): void; +} + interface W96API { FS: { readstr(path: string): Promise; }; + evt: { + fs: EventEmitter; + sys: EventEmitter; + wm: EventEmitter; + ui: EventEmitter; + }; + WApplication: { + new(): WApplication; + prototype: WApplication; + + kill(app_id: number, force: boolean): void; + execAsync(instance: WApplication, args: string[]): Promise; + }; }