diff --git a/plugins/core/core.plugin.json b/plugins/core/core.plugin.json index 68bdbfb3..9be62ec0 100644 --- a/plugins/core/core.plugin.json +++ b/plugins/core/core.plugin.json @@ -13,7 +13,8 @@ "requiredContext": {} }, "Insert Current Date": { - "invoke": "insert_nice_date" + "invoke": "insert_nice_date", + "slashCommand": "/insert-today" }, "Toggle : Heading 1": { "invoke": "toggle_h1", diff --git a/server/server.ts b/server/server.ts index 95e20f63..a1d24ac6 100644 --- a/server/server.ts +++ b/server/server.ts @@ -6,24 +6,31 @@ import { oakCors } from "https://deno.land/x/cors@v1.2.0/mod.ts"; import { readAll } from "https://deno.land/std@0.126.0/streams/mod.ts"; import { exists } from "https://deno.land/std@0.126.0/fs/mod.ts"; +type NuggetMeta = { + name: string; + lastModified: number; +}; + const fsPrefix = "/fs"; const nuggetsPath = "../nuggets"; const fsRouter = new Router(); -fsRouter.use(oakCors()); +fsRouter.use(oakCors({ methods: ["OPTIONS", "GET", "PUT", "POST"] })); fsRouter.get("/", async (context) => { const localPath = nuggetsPath; - let fileNames: string[] = []; + let fileNames: NuggetMeta[] = []; for await (const dirEntry of Deno.readDir(localPath)) { if (dirEntry.isFile) { - fileNames.push( - dirEntry.name.substring( + const stat = await Deno.stat(`${localPath}/${dirEntry.name}`); + fileNames.push({ + name: dirEntry.name.substring( 0, dirEntry.name.length - path.extname(dirEntry.name).length - ) - ); + ), + lastModified: stat.mtime?.getTime()!, + }); } } context.response.body = JSON.stringify(fileNames); @@ -33,7 +40,9 @@ fsRouter.get("/:nugget", async (context) => { const nuggetName = context.params.nugget; const localPath = `${nuggetsPath}/${nuggetName}.md`; try { + const stat = await Deno.stat(localPath); const text = await Deno.readTextFile(localPath); + context.response.headers.set("Last-Modified", "" + stat.mtime?.getTime()); context.response.body = text; } catch (e) { context.response.status = 404; @@ -46,7 +55,9 @@ fsRouter.options("/:nugget", async (context) => { try { const stat = await Deno.stat(localPath); context.response.headers.set("Content-length", `${stat.size}`); + context.response.headers.set("Last-Modified", "" + stat.mtime?.getTime()); } catch (e) { + // For CORS context.response.status = 200; context.response.body = ""; } @@ -69,8 +80,10 @@ fsRouter.put("/:nugget", async (context) => { const text = await readAll(result.value); file.write(text); file.close(); + const stat = await Deno.stat(localPath); console.log("Wrote to", localPath); context.response.status = existingNugget ? 200 : 201; + context.response.headers.set("Last-Modified", "" + stat.mtime?.getTime()); context.response.body = "OK"; }); diff --git a/webapp/src/boot.ts b/webapp/src/boot.ts new file mode 100644 index 00000000..c57217db --- /dev/null +++ b/webapp/src/boot.ts @@ -0,0 +1,15 @@ +import { Editor } from "./editor"; +import { HttpFileSystem } from "./fs"; +import { safeRun } from "./util"; + +let editor = new Editor( + new HttpFileSystem(`http://${location.hostname}:2222/fs`), + document.getElementById("root")! +); + +safeRun(async () => { + await editor.init(); +}); + +// @ts-ignore +window.editor = editor; diff --git a/webapp/src/components/command_palette.tsx b/webapp/src/components/command_palette.tsx index 33cd7975..f71a418d 100644 --- a/webapp/src/components/command_palette.tsx +++ b/webapp/src/components/command_palette.tsx @@ -1,4 +1,5 @@ import { AppCommand } from "../types"; +import { isMacLike } from "../util"; import { FilterList, Option } from "./filter"; export function CommandPalette({ @@ -9,8 +10,12 @@ export function CommandPalette({ onTrigger: (command: AppCommand | undefined) => void; }) { let options: Option[] = []; + const isMac = isMacLike(); for (let [name, def] of commands.entries()) { - options.push({ name: name }); + options.push({ + name: name, + hint: isMac && def.command.mac ? def.command.mac : def.command.key, + }); } console.log("Commands", options); return ( diff --git a/webapp/src/components/filter.tsx b/webapp/src/components/filter.tsx index a1a40c0a..3f159f9f 100644 --- a/webapp/src/components/filter.tsx +++ b/webapp/src/components/filter.tsx @@ -2,26 +2,28 @@ import React, { useEffect, useRef, useState } from "react"; export interface Option { name: string; + orderId?: number; hint?: string; } function magicSorter(a: Option, b: Option): number { - if (a.name.toLowerCase() < b.name.toLowerCase()) { - return -1; - } else { - return 1; + if (a.orderId && b.orderId) { + return a.orderId < b.orderId ? -1 : 1; } + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; } export function FilterList({ placeholder, options, onSelect, + onKeyPress, allowNew = false, newHint, }: { placeholder: string; options: Option[]; + onKeyPress?: (key: string, currentText: string) => void; onSelect: (option: Option | undefined) => void; allowNew?: boolean; newHint?: string; @@ -75,7 +77,6 @@ export function FilterList({ document.addEventListener("click", closer); return () => { - console.log("Unsubscribing"); document.removeEventListener("click", closer); }; }, []); @@ -90,6 +91,9 @@ export function FilterList({ onChange={filter} onKeyDown={(e: React.KeyboardEvent) => { console.log("Key up", e.key); + if (onKeyPress) { + onKeyPress(e.key, text); + } switch (e.key) { case "ArrowUp": setSelectionOption(Math.max(0, selectedOption - 1)); diff --git a/webapp/src/components/navigation_bar.tsx b/webapp/src/components/navigation_bar.tsx index e9252f66..dc94d22c 100644 --- a/webapp/src/components/navigation_bar.tsx +++ b/webapp/src/components/navigation_bar.tsx @@ -1,14 +1,16 @@ +import { NuggetMeta } from "../types"; + export function NavigationBar({ currentNugget, onClick, }: { - currentNugget?: string; + currentNugget?: NuggetMeta; onClick: () => void; }) { return (
- » {currentNugget} + » {currentNugget?.name}
); diff --git a/webapp/src/components/nugget_navigator.tsx b/webapp/src/components/nugget_navigator.tsx index c937221f..14dcac04 100644 --- a/webapp/src/components/nugget_navigator.tsx +++ b/webapp/src/components/nugget_navigator.tsx @@ -11,7 +11,11 @@ export function NuggetNavigator({ return ( ({ + ...meta, + // Order by last modified date in descending order + orderId: -meta.lastModified.getTime(), + }))} allowNew={true} newHint="Create nugget" onSelect={(opt) => { diff --git a/webapp/src/editor.tsx b/webapp/src/editor.tsx index e7e4f38f..6dde192a 100644 --- a/webapp/src/editor.tsx +++ b/webapp/src/editor.tsx @@ -1,5 +1,6 @@ import { autocompletion, + Completion, CompletionContext, completionKeymap, CompletionResult, @@ -11,12 +12,12 @@ import { indentOnInput, syntaxTree } from "@codemirror/language"; import { bracketMatching } from "@codemirror/matchbrackets"; import { searchKeymap } from "@codemirror/search"; import { EditorState, StateField, Transaction } from "@codemirror/state"; -import { KeyBinding } from "@codemirror/view"; import { drawSelection, dropCursor, EditorView, highlightSpecialChars, + KeyBinding, keymap, } from "@codemirror/view"; import React, { useEffect, useReducer } from "react"; @@ -28,12 +29,12 @@ import { CommandPalette } from "./components/command_palette"; import { NavigationBar } from "./components/navigation_bar"; import { NuggetNavigator } from "./components/nugget_navigator"; import { StatusBar } from "./components/status_bar"; -import { FileSystem, HttpFileSystem } from "./fs"; +import { FileSystem } from "./fs"; import { lineWrapper } from "./lineWrapper"; import { markdown } from "./markdown"; import customMarkDown from "./parser"; import { BrowserSystem } from "./plugins/browser_system"; -import { Manifest } from "./plugins/types"; +import { Manifest, slashCommandRegexp } from "./plugins/types"; import reducer from "./reducer"; import customMarkdownStyle from "./style"; import dbSyscalls from "./syscalls/db.localstorage"; @@ -44,19 +45,24 @@ import { AppViewState, CommandContext, initialViewState, + NuggetMeta, } from "./types"; import { safeRun } from "./util"; class NuggetState { editorState: EditorState; scrollTop: number; + meta: NuggetMeta; - constructor(editorState: EditorState, scrollTop: number) { + constructor(editorState: EditorState, scrollTop: number, meta: NuggetMeta) { this.editorState = editorState; this.scrollTop = scrollTop; + this.meta = meta; } } +const watchInterval = 5000; + export class Editor { editorView?: EditorView; viewState: AppViewState; @@ -78,6 +84,7 @@ export class Editor { parent: document.getElementById("editor")!, }); this.addListeners(); + this.watch(); } async init() { @@ -93,15 +100,15 @@ export class Editor { await system.bootServiceWorker(); console.log("Now loading core plugin"); - let mainCartridge = await system.load("core", coreManifest as Manifest); + let mainPlugin = await system.load("core", coreManifest as Manifest); this.editorCommands = new Map(); - const cmds = mainCartridge.manifest!.commands; + const cmds = mainPlugin.manifest!.commands; for (let name in cmds) { let cmd = cmds[name]; this.editorCommands.set(name, { command: cmd, run: async (arg: CommandContext): Promise => { - return await mainCartridge.invoke(cmd.invoke, [arg]); + return await mainPlugin.invoke(cmd.invoke, [arg]); }, }); } @@ -111,7 +118,7 @@ export class Editor { }); } - get currentNugget(): string | undefined { + get currentNugget(): NuggetMeta | undefined { return this.viewState.currentNugget; } @@ -146,7 +153,10 @@ export class Editor { bracketMatching(), closeBrackets(), autocompletion({ - override: [this.nuggetCompleter.bind(this)], + override: [ + this.nuggetCompleter.bind(this), + this.commandCompleter.bind(this), + ], }), EditorView.lineWrapping, lineWrapper([ @@ -176,15 +186,10 @@ export class Editor { run: commands.insertMarker("_"), }, { - key: "Ctrl-s", - mac: "Cmd-s", - run: (target: EditorView): boolean => { - Promise.resolve() - .then(async () => { - console.log("Saving"); - await this.save(); - }) - .catch((e) => console.error(e)); + key: "Ctrl-e", + mac: "Cmd-e", + run: (): boolean => { + window.open(location.href, "_blank")!.focus(); return true; }, }, @@ -200,7 +205,9 @@ export class Editor { key: "Ctrl-.", mac: "Cmd-.", run: (target): boolean => { - this.viewDispatch({ type: "show-palette" }); + this.viewDispatch({ + type: "show-palette", + }); return true; }, }, @@ -220,12 +227,10 @@ export class Editor { } nuggetCompleter(ctx: CompletionContext): CompletionResult | null { - let prefix = ctx.matchBefore(/\[\[\w*/); + let prefix = ctx.matchBefore(/\[\[[\w\s]*/); if (!prefix) { return null; } - // TODO: Lots of optimization potential here - // TODO: put something in the cm-completionIcon-nugget style return { from: prefix.from + 2, options: this.viewState.allNuggets.map((nuggetMeta) => ({ @@ -235,6 +240,39 @@ export class Editor { }; } + commandCompleter(ctx: CompletionContext): CompletionResult | null { + let prefix = ctx.matchBefore(slashCommandRegexp); + if (!prefix) { + return null; + } + let options: Completion[] = []; + for (let [name, def] of this.viewState.commands) { + if (!def.command.slashCommand) { + continue; + } + options.push({ + label: def.command.slashCommand, + detail: name, + apply: () => { + this.editorView?.dispatch({ + changes: { + from: prefix!.from, + to: ctx.pos, + insert: "", + }, + }); + safeRun(async () => { + def.run(buildContext(def, this)); + }); + }, + }); + } + return { + from: prefix.from + 1, + options: options, + }; + } + update(value: null, transaction: Transaction): null { if (transaction.docChanged) { this.viewDispatch({ @@ -276,22 +314,26 @@ export class Editor { return; } // Write to file system - const created = await this.fs.writeNugget( - this.currentNugget, + let nuggetMeta = await this.fs.writeNugget( + this.currentNugget.name, editorState.sliceDoc() ); // Update in open nugget cache this.openNuggets.set( - this.currentNugget, - new NuggetState(editorState, this.editorView!.scrollDOM.scrollTop) + this.currentNugget.name, + new NuggetState( + editorState, + this.editorView!.scrollDOM.scrollTop, + nuggetMeta + ) ); // Dispatch update to view - this.viewDispatch({ type: "nugget-saved" }); + this.viewDispatch({ type: "nugget-saved", meta: nuggetMeta }); // If a new nugget was created, let's refresh the nugget list - if (created) { + if (nuggetMeta.created) { await this.loadNuggetList(); } } @@ -304,6 +346,34 @@ export class Editor { }); } + watch() { + setInterval(() => { + safeRun(async () => { + if (!this.currentNugget) { + return; + } + const currentNuggetName = this.currentNugget.name; + let newNuggetMeta = await this.fs.getMeta(currentNuggetName); + if ( + this.currentNugget.lastModified.getTime() < + newNuggetMeta.lastModified.getTime() + ) { + console.log("File changed on disk, reloading"); + let nuggetData = await this.fs.readNugget(currentNuggetName); + this.openNuggets.set( + newNuggetMeta.name, + new NuggetState( + this.createEditorState(nuggetData.text), + 0, + newNuggetMeta + ) + ); + await this.loadNugget(currentNuggetName); + } + }); + }, watchInterval); + } + focus() { this.editorView!.focus(); } @@ -323,25 +393,33 @@ export class Editor { return; } - let nuggetState = this.openNuggets.get(nuggetName); - if (!nuggetState) { - let text = await this.fs.readNugget(nuggetName); - nuggetState = new NuggetState(this.createEditorState(text), 0); - } - this.openNuggets.set(nuggetName, nuggetState!); - this.editorView!.setState(nuggetState!.editorState); - this.editorView.scrollDOM.scrollTop = nuggetState!.scrollTop; - - this.viewDispatch({ - type: "nugget-loaded", - name: nuggetName, - }); + await this.loadNugget(nuggetName); }) .catch((e) => { console.error(e); }); } + async loadNugget(nuggetName: string) { + let nuggetState = this.openNuggets.get(nuggetName); + if (!nuggetState) { + let nuggetData = await this.fs.readNugget(nuggetName); + nuggetState = new NuggetState( + this.createEditorState(nuggetData.text), + 0, + nuggetData.meta + ); + this.openNuggets.set(nuggetName, nuggetState!); + } + this.editorView!.setState(nuggetState!.editorState); + this.editorView!.scrollDOM.scrollTop = nuggetState!.scrollTop; + + this.viewDispatch({ + type: "nugget-loaded", + meta: nuggetState.meta, + }); + } + addListeners() { this.$hashChange = this.hashChange.bind(this); window.addEventListener("hashchange", this.$hashChange); @@ -380,7 +458,7 @@ export class Editor { useEffect(() => { if (viewState.currentNugget) { - document.title = viewState.currentNugget; + document.title = viewState.currentNugget.name; } }, [viewState.currentNugget]); @@ -437,19 +515,3 @@ export class Editor { ReactDOM.render(, container); } } - -let ed = new Editor( - new HttpFileSystem("http://localhost:2222/fs"), - document.getElementById("root")! -); - -ed.loadPlugins().catch((e) => { - console.error(e); -}); - -safeRun(async () => { - await ed.init(); -}); - -// @ts-ignore -window.editor = ed; diff --git a/webapp/src/fs.ts b/webapp/src/fs.ts index 56f31767..e8aa20e0 100644 --- a/webapp/src/fs.ts +++ b/webapp/src/fs.ts @@ -2,9 +2,9 @@ import { NuggetMeta } from "./types"; export interface FileSystem { listNuggets(): Promise; - readNugget(name: string): Promise; - // @return whether a new nugget was created for this - writeNugget(name: string, text: string): Promise; + readNugget(name: string): Promise<{ text: string; meta: NuggetMeta }>; + writeNugget(name: string, text: string): Promise; + getMeta(name: string): Promise; } export class HttpFileSystem implements FileSystem { @@ -17,20 +17,43 @@ export class HttpFileSystem implements FileSystem { method: "GET", }); - return (await req.json()).map((name: string) => ({ name })); + return (await req.json()).map((meta: any) => ({ + name: meta.name, + lastModified: new Date(meta.lastModified), + })); } - async readNugget(name: string): Promise { + async readNugget(name: string): Promise<{ text: string; meta: NuggetMeta }> { let req = await fetch(`${this.url}/${name}`, { method: "GET", }); - return await req.text(); + return { + text: await req.text(), + meta: { + lastModified: new Date(+req.headers.get("Last-Modified")!), + name: name, + }, + }; } - async writeNugget(name: string, text: string): Promise { + async writeNugget(name: string, text: string): Promise { let req = await fetch(`${this.url}/${name}`, { method: "PUT", body: text, }); // 201 (Created) means a new nugget was created - return req.status === 201; + return { + lastModified: new Date(+req.headers.get("Last-Modified")!), + name: name, + created: req.status === 201, + }; + } + + async getMeta(name: string): Promise { + let req = await fetch(`${this.url}/${name}`, { + method: "OPTIONS", + }); + return { + name: name, + lastModified: new Date(+req.headers.get("Last-Modified")!), + }; } } diff --git a/webapp/src/index.html b/webapp/src/index.html index 28f97b51..76cee25c 100644 --- a/webapp/src/index.html +++ b/webapp/src/index.html @@ -4,7 +4,7 @@ Nugget - + diff --git a/webapp/src/plugin_sw.ts b/webapp/src/plugin_sw.ts index 961e3474..6357ef23 100644 --- a/webapp/src/plugin_sw.ts +++ b/webapp/src/plugin_sw.ts @@ -66,13 +66,13 @@ self.addEventListener("fetch", (event: any) => { return await handlePut(req, path); } - let [cartridgeName, resourceType, functionName] = path.split("/"); + let [pluginName, resourceType, functionName] = path.split("/"); - let manifest = await getManifest(cartridgeName); + let manifest = await getManifest(pluginName); if (!manifest) { - // console.log("Ain't got", cartridgeName); - return new Response(`Cartridge not loaded: ${cartridgeName}`, { + // console.log("Ain't got", pluginName); + return new Response(`Plugin not loaded: ${pluginName}`, { status: 404, }); } diff --git a/webapp/src/plugins/browser_system.ts b/webapp/src/plugins/browser_system.ts index a144029a..295ac50f 100644 --- a/webapp/src/plugins/browser_system.ts +++ b/webapp/src/plugins/browser_system.ts @@ -1,8 +1,8 @@ -import { CartridgeLoader, System } from "./runtime"; +import { PluginLoader, System } from "./runtime"; import { Manifest } from "./types"; import { sleep } from "../util"; -export class BrowserLoader implements CartridgeLoader { +export class BrowserLoader implements PluginLoader { readonly pathPrefix: string; constructor(pathPrefix: string) { diff --git a/webapp/src/plugins/runtime.ts b/webapp/src/plugins/runtime.ts index 461e6bdb..5ff904e9 100644 --- a/webapp/src/plugins/runtime.ts +++ b/webapp/src/plugins/runtime.ts @@ -1,10 +1,10 @@ import { Manifest } from "./types"; export class SyscallContext { - public cartridge: Cartridge; + public plugin: Plugin; - constructor(cartridge: Cartridge) { - this.cartridge = cartridge; + constructor(Plugin: Plugin) { + this.plugin = Plugin; } } @@ -19,9 +19,9 @@ export class FunctionWorker { private initCallback: any; private invokeResolve?: (result?: any) => void; private invokeReject?: (reason?: any) => void; - private cartridge: Cartridge; + private plugin: Plugin; - constructor(cartridge: Cartridge, pathPrefix: string, name: string) { + constructor(plugin: Plugin, pathPrefix: string, name: string) { // this.worker = new Worker(new URL("function_worker.ts", import.meta.url), { // type: "classic", // }); @@ -40,7 +40,7 @@ export class FunctionWorker { this.inited = new Promise((resolve) => { this.initCallback = resolve; }); - this.cartridge = cartridge; + this.plugin = plugin; } async onmessage(evt: MessageEvent) { @@ -51,8 +51,8 @@ export class FunctionWorker { this.initCallback(); break; case "syscall": - const ctx = new SyscallContext(this.cartridge); - let result = await this.cartridge.system.syscall( + const ctx = new SyscallContext(this.plugin); + let result = await this.plugin.system.syscall( ctx, data.name, data.args @@ -92,11 +92,11 @@ export class FunctionWorker { } } -export interface CartridgeLoader { +export interface PluginLoader { load(name: string, manifest: Manifest): Promise; } -export class Cartridge { +export class Plugin { pathPrefix: string; system: System; private runningFunctions: Map; @@ -112,7 +112,7 @@ export class Cartridge { async load(manifest: Manifest) { this.manifest = manifest; - await this.system.cartridgeLoader.load(this.name, manifest); + await this.system.pluginLoader.load(this.name, manifest); await this.dispatchEvent("load"); } @@ -149,15 +149,15 @@ export class Cartridge { } export class System { - protected cartridges: Map; + protected plugins: Map; protected pathPrefix: string; registeredSyscalls: SysCallMapping; - cartridgeLoader: CartridgeLoader; + pluginLoader: PluginLoader; - constructor(cartridgeLoader: CartridgeLoader, pathPrefix: string) { - this.cartridgeLoader = cartridgeLoader; + constructor(PluginLoader: PluginLoader, pathPrefix: string) { + this.pluginLoader = PluginLoader; this.pathPrefix = pathPrefix; - this.cartridges = new Map(); + this.plugins = new Map(); this.registeredSyscalls = {}; } @@ -184,16 +184,16 @@ export class System { return Promise.resolve(callback(ctx, ...args)); } - async load(name: string, manifest: Manifest): Promise { - const cartridge = new Cartridge(this, this.pathPrefix, name); - await cartridge.load(manifest); - this.cartridges.set(name, cartridge); - return cartridge; + async load(name: string, manifest: Manifest): Promise { + const plugin = new Plugin(this, this.pathPrefix, name); + await plugin.load(manifest); + this.plugins.set(name, plugin); + return plugin; } async stop(): Promise { return Promise.all( - Array.from(this.cartridges.values()).map((cartridge) => cartridge.stop()) + Array.from(this.plugins.values()).map((plugin) => plugin.stop()) ); } } diff --git a/webapp/src/plugins/types.ts b/webapp/src/plugins/types.ts index cfa5f41d..905e4875 100644 --- a/webapp/src/plugins/types.ts +++ b/webapp/src/plugins/types.ts @@ -8,6 +8,8 @@ export interface Manifest { }; } +export const slashCommandRegexp = /\/[\w\-]*/; + export interface CommandDef { // Function name to invoke invoke: string; @@ -15,6 +17,11 @@ export interface CommandDef { // Bind to keyboard shortcut key?: string; mac?: string; + + // If to show in slash invoked menu and if so, with what label + // should match slashCommandRegexp + slashCommand?: string; + // Required context to be passed in as function arguments requiredContext?: { text?: boolean; diff --git a/webapp/src/reducer.ts b/webapp/src/reducer.ts index f562f4d9..02d44447 100644 --- a/webapp/src/reducer.ts +++ b/webapp/src/reducer.ts @@ -9,12 +9,13 @@ export default function reducer( case "nugget-loaded": return { ...state, - currentNugget: action.name, + currentNugget: action.meta, isSaved: true, }; case "nugget-saved": return { ...state, + currentNugget: action.meta, isSaved: true, }; case "nugget-updated": diff --git a/webapp/src/styles.css b/webapp/src/styles.css index d7cc1254..02c9a678 100644 --- a/webapp/src/styles.css +++ b/webapp/src/styles.css @@ -177,8 +177,8 @@ body { border: #333 1px solid; z-index: 1000; position: absolute; - left: 8px; - top: 8px; + left: 25px; + top: 10px; right: 10px; } diff --git a/webapp/src/syscalls/event.native.ts b/webapp/src/syscalls/event.native.ts index 20a7b0c0..bc0ad18c 100644 --- a/webapp/src/syscalls/event.native.ts +++ b/webapp/src/syscalls/event.native.ts @@ -2,6 +2,6 @@ import { SyscallContext } from "../plugins/runtime"; export default { "event.publish": async (ctx: SyscallContext, name: string, data: any) => { - await ctx.cartridge.dispatchEvent(name, data); + await ctx.plugin.dispatchEvent(name, data); }, }; diff --git a/webapp/src/syscalls/ui.browser.ts b/webapp/src/syscalls/ui.browser.ts index caaf2e60..5448ee8d 100644 --- a/webapp/src/syscalls/ui.browser.ts +++ b/webapp/src/syscalls/ui.browser.ts @@ -8,7 +8,7 @@ window.addEventListener("message", async (event) => { let data = messageEvent.data; if (data.type === "iframe_event") { // @ts-ignore - window.mainCartridge.dispatchEvent(data.data.event, data.data.data); + window.mainPlugin.dispatchEvent(data.data.event, data.data.data); } }); diff --git a/webapp/src/types.ts b/webapp/src/types.ts index 3806299f..38c4b364 100644 --- a/webapp/src/types.ts +++ b/webapp/src/types.ts @@ -2,6 +2,8 @@ import { CommandDef } from "./plugins/types"; export type NuggetMeta = { name: string; + lastModified: Date; + created?: boolean; }; export type CommandContext = { @@ -14,7 +16,7 @@ export type AppCommand = { }; export type AppViewState = { - currentNugget?: string; + currentNugget?: NuggetMeta; isSaved: boolean; showNuggetNavigator: boolean; showCommandPalette: boolean; @@ -31,8 +33,8 @@ export const initialViewState: AppViewState = { }; export type Action = - | { type: "nugget-loaded"; name: string } - | { type: "nugget-saved" } + | { type: "nugget-loaded"; meta: NuggetMeta } + | { type: "nugget-saved"; meta: NuggetMeta } | { type: "nugget-updated" } | { type: "nuggets-listed"; nuggets: NuggetMeta[] } | { type: "start-navigate" } diff --git a/webapp/src/util.ts b/webapp/src/util.ts index 16b5ca40..de823705 100644 --- a/webapp/src/util.ts +++ b/webapp/src/util.ts @@ -21,3 +21,7 @@ export function sleep(ms: number): Promise { }, ms); }); } + +export function isMacLike() { + return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform); +} diff --git a/webapp/src/watcher.ts b/webapp/src/watcher.ts new file mode 100644 index 00000000..35b8f24f --- /dev/null +++ b/webapp/src/watcher.ts @@ -0,0 +1,2 @@ +import { Editor } from "./editor"; +import { safeRun } from "./util";