diff --git a/README.md b/README.md index 1426d130..b094eb79 100644 --- a/README.md +++ b/README.md @@ -56,3 +56,23 @@ Use `⇧⌘P` to open the command menu. Terminal apps can also add their own act Don't know how to quit vim? With `edit`, you get the full monaco text editor right in your terminal. And you can just click the x to close. + +## Creating Rich Apps + +```js +// backend.js +const { display } = require('snail-sdk'); +display(require.resolve('./web.ts')); +``` + +```ts +// web.ts +import { setHeight } from 'snail-sdk/web'; +document.body.append('Hello World!'); +setHeight(document.body.offsetHeight); +``` + +``` +> node backend.js +Hello World! +``` diff --git a/host/ShellHost.d.ts b/host/ShellHost.d.ts index a4373183..59c49e8b 100644 --- a/host/ShellHost.d.ts +++ b/host/ShellHost.d.ts @@ -1,10 +1,4 @@ -export type MenuItem = { - title?: string; - enabled?: boolean; - checked?: boolean; - callback?: () => void; - submenu?: MenuItem[]; -}; +import type { MenuItem } from '../slug/sdk/web'; export interface ShellHost { obtainWebSocketId(): number; diff --git a/slug/apps/edit/index.ts b/slug/apps/edit/index.ts index 29bcaf13..cf3198ef 100644 --- a/slug/apps/edit/index.ts +++ b/slug/apps/edit/index.ts @@ -1,4 +1,4 @@ -/// +import * as snail from '../../sdk/web'; /// import './index.css'; import { RPC } from '../../sdk/rpc-js'; diff --git a/slug/apps/html/html.ts b/slug/apps/html/html.ts index 906ee616..6d8df4bd 100644 --- a/slug/apps/html/html.ts +++ b/slug/apps/html/html.ts @@ -1,4 +1,4 @@ -/// +import * as snail from '../../sdk/web'; const {args} = await snail.waitForMessage<{ args: string[], }>(); diff --git a/slug/apps/kang/web.ts b/slug/apps/kang/web.ts index 19058a66..06a70f5e 100644 --- a/slug/apps/kang/web.ts +++ b/slug/apps/kang/web.ts @@ -1,4 +1,4 @@ -/// +import * as snail from '../../sdk/web'; import './web.css'; import { RPC } from '../../sdk/rpc-js'; import { Editor } from '../../editor/js/editor'; diff --git a/slug/apps/logbook/web.ts b/slug/apps/logbook/web.ts index 6d7eed8f..7a98fdcc 100644 --- a/slug/apps/logbook/web.ts +++ b/slug/apps/logbook/web.ts @@ -1,4 +1,4 @@ -/// +import * as snail from '../../sdk/web'; import { RPC } from '../../sdk/rpc-js'; import { LogBookView } from './LogBookView'; snail.setIsFullscreen(true); diff --git a/slug/apps/ls/web.ts b/slug/apps/ls/web.ts index cdfea0ca..6f43cae6 100644 --- a/slug/apps/ls/web.ts +++ b/slug/apps/ls/web.ts @@ -1,4 +1,4 @@ -/// +import * as snail from '../../sdk/web'; import './web.css'; import {iconPathForPath, looksLikeImageOrVideo} from '../../icon_service/iconService'; import {DataGrid} from '../../datagrid/datagrid'; diff --git a/slug/apps/reconnect/web.ts b/slug/apps/reconnect/web.ts index dca31079..e41a7ede 100644 --- a/slug/apps/reconnect/web.ts +++ b/slug/apps/reconnect/web.ts @@ -1,4 +1,4 @@ -/// +import * as snail from '../../sdk/web'; import type { Metadata } from '../../shell/metadata'; import './reconnect.css'; diff --git a/slug/apps/show/index.ts b/slug/apps/show/index.ts index db696639..f57b2020 100644 --- a/slug/apps/show/index.ts +++ b/slug/apps/show/index.ts @@ -1,4 +1,4 @@ -/// +import * as snail from '../../sdk/web'; import { JoelEvent } from '../../cdp-ui/JoelEvent'; import { renderExcalidraw } from './excalidraw'; import './index.css'; diff --git a/slug/apps/top/index.ts b/slug/apps/top/index.ts index 78b169ea..caffa48d 100644 --- a/slug/apps/top/index.ts +++ b/slug/apps/top/index.ts @@ -1,4 +1,4 @@ -/// +import * as snail from '../../sdk/web'; import './index.css'; window.onresize = updateSize; diff --git a/slug/apps/xkcd/index.ts b/slug/apps/xkcd/index.ts index 7d288050..2d7c9de6 100644 --- a/slug/apps/xkcd/index.ts +++ b/slug/apps/xkcd/index.ts @@ -1,4 +1,4 @@ -/// +import * as snail from '../../sdk/web'; import './index.css'; window.onresize = updateSize; function updateSize() { diff --git a/slug/datagrid/datagrid.ts b/slug/datagrid/datagrid.ts index 7bc655bc..ccdf6585 100644 --- a/slug/datagrid/datagrid.ts +++ b/slug/datagrid/datagrid.ts @@ -1,4 +1,4 @@ -/// +import * as snail from '../sdk/web'; import './datagrid.css'; export interface ColumnDelegate { diff --git a/slug/debugger/TargetManager.ts b/slug/debugger/TargetManager.ts index 5cf9018f..efa0f51d 100644 --- a/slug/debugger/TargetManager.ts +++ b/slug/debugger/TargetManager.ts @@ -1,4 +1,4 @@ -/// +import * as snail from '../sdk/web'; import { RPC, type Transport} from '../protocol/RPC-ts'; import type { Protocol } from '../../src/protocol'; diff --git a/slug/debugger/elements/ChromiumDOM.ts b/slug/debugger/elements/ChromiumDOM.ts index a0c6b47c..3ea5f601 100644 --- a/slug/debugger/elements/ChromiumDOM.ts +++ b/slug/debugger/elements/ChromiumDOM.ts @@ -2,6 +2,7 @@ import type { ChromiumSession } from '../TargetManager'; import type { Protocol } from '../../../src/protocol'; import { JoelEvent } from '../../cdp-ui/JoelEvent'; import { RPC, type Transport} from '../../protocol/RPC-ts'; +import * as snail from '../../sdk/web'; export interface DOM { documentNodeForFrame(frameUUID: string|undefined, listener: { diff --git a/slug/debugger/web.ts b/slug/debugger/web.ts index 32b28692..04e41769 100644 --- a/slug/debugger/web.ts +++ b/slug/debugger/web.ts @@ -1,4 +1,4 @@ -/// +import * as snail from '../sdk/web'; import {TargetManager} from './TargetManager'; import { Console } from './Console'; import { Elements } from './elements/Elements'; diff --git a/slug/iframe/error.js b/slug/iframe/error.js index 7e3c1e7b..44a50a23 100644 --- a/slug/iframe/error.js +++ b/slug/iframe/error.js @@ -1,4 +1,3 @@ -/// const error = atob(window.snail_error); document.body.textContent = error; snail.setToJSON({ error }); diff --git a/slug/iframe/iframe.js b/slug/iframe/iframe.js index d455d8da..d2898a65 100644 --- a/slug/iframe/iframe.js +++ b/slug/iframe/iframe.js @@ -1,4 +1,4 @@ -/// +//@ts-check const messages = []; const callbacks = []; /** @type {import('../../src/shortcutParser').ParsedShortcut[]} */ @@ -14,8 +14,8 @@ async function waitForMessage() { function sendMessageToParent(message) { if (window.parent && window.parent !== window) window.parent.postMessage(message, '*'); - else if (window.electronAPI) - window.electronAPI.notify(message); + else if (window['electronAPI']) + window['electronAPI'].notify(message); else if (window['webkit']) window['webkit'].messageHandlers.wkMessage.postMessage(message); else @@ -36,8 +36,8 @@ if (window.parent && window.parent !== window) { return; onMessage(event.data); }); -} else if (window.electronAPI) { - window.electronAPI.onEvent('postMessage', onMessage); +} else if (window['electronAPI']) { + window['electronAPI'].onEvent('postMessage', onMessage); } else if (window['webkit']) { window['webkit_callback'] = onMessage; } else { @@ -120,7 +120,7 @@ window.addEventListener('keydown', event => { event.stopImmediatePropagation(); } } else { - if (event.key === 'Shift' || event.key === 'Control' && event.key === 'Alt' || event.key === 'Meta') + if (event.key === 'Shift' || event.key === 'Control' || event.key === 'Alt' || event.key === 'Meta') return; chording = false; sendMessageToParent({method: 'chordPressed', params: { @@ -181,10 +181,10 @@ window.addEventListener('keydown', event => { }); /** - * @param {HTMLElement} target + * @param {EventTarget|null} target */ function isEditing(target) { - if (!target) + if (!target || !(target instanceof HTMLElement)) return false; if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return true; @@ -196,10 +196,11 @@ function isEditing(target) { const contextMenuCallbacks = new Map(); let lastCallback = 0; /** - * @param {MenuItem[]} menuItems + * @param {import('../sdk/web').MenuItem[]} menuItems */ function serializeMenuItems(menuItems) { return menuItems.map(item => { + /** @type {any} */ const serialized = { ...item, }; @@ -214,7 +215,7 @@ function serializeMenuItems(menuItems) { }); } /** - * @param {MenuItem[]} menuItems + * @param {import('../sdk/web').MenuItem[]} descriptor * @param {boolean=} noDefaultItems */ function createContextMenu(descriptor, noDefaultItems) { @@ -255,7 +256,7 @@ async function getDevicePixelRatio() { return dprPromise; } -/** @type {{onMessage: (message: any, browserViewUUID?: string) => void, onDebuggeesChanged: (debuggees: {[key: string]: import('../src/CDPManager').DebuggingInfo}) => void}} */ +/** @type {{onMessage: (message: any, browserViewUUID?: string) => void, onDebuggeesChanged: (debuggees: {[key: string]: import('../sdk/web').DebuggingInfo}) => void}} */ let cdpListener; function openDevTools() { @@ -263,7 +264,7 @@ function openDevTools() { } const ua = navigator.userActivation; -function tryToRunCommand(command) { +async function tryToRunCommand(command) { if (!ua.isActive) return; sendMessageToParent({method: 'tryToRunCommand', params: {command}}); @@ -320,12 +321,12 @@ function expectingUserInput(name = 'Anonymous Work Context') { return () => sendMessageToParent({method: 'resolveUserInput', params: {id}}); } -/** @type {(params: import('../../src/Find').FindParams) => void} */ +/** @type {(params: import('../sdk/web').FindParams|null) => void} */ let findHandler; -/** @type {import('../../src/Find').FindParams|null} */ +/** @type {import('../sdk/web').FindParams|null} */ let findParams = null; /** - * @param {string} message + * @param {(params: import('../sdk/web').FindParams|null) => void} _findHandler */ function setFindHandler(_findHandler) { findHandler = _findHandler; diff --git a/slug/sdk/web.d.ts b/slug/sdk/web.d.ts new file mode 100644 index 00000000..829dc6d5 --- /dev/null +++ b/slug/sdk/web.d.ts @@ -0,0 +1,40 @@ +export type MenuItem = { + title?: string; + enabled?: boolean; + checked?: boolean; + callback?: () => void; + submenu?: MenuItem[]; +}; + +export type Action = { + id: string; + title: string; + shortcut?: string; + callback: () => void; +}; + +export type FindParams = { regex: RegExp, report: (matches: number) => void }; + +export type DebuggingInfo = { + browserViewUUID?: string; + frameUUID?: string; + type: 'webkit'|'chromium'|'node'; +}; + +export function waitForMessage(): Promise; +export function setHeight(height: number): void; +export function setIsFullscreen(isFullscreen: boolean): void; +export function sendInput(input: string): void; +export function createContextMenu(items: MenuItem[], noDefaultItems?: boolean): void; +export function saveItem(key: string, value: any): void; +export function loadItem(key: string): Promise; +export function getDevicePixelRatio(): Promise; +export function attachToCDP(listener: {onMessage: (message: any, browserViewUUID?: string) => void, onDebuggeesChanged: (debuggees: {[key: string]: DebuggingInfo}) => void}): Promise<(message: any, browserViewUUID?: string) => void>; +export function openDevTools(): void; +export function setToJSON(toJSON: any | (()=>any)): void; +export function setActions(actions: Action[] | (()=>Action[])): void; +export function startAsyncWork(name?: string): () => void; +export function expectingUserInput(name?: string): () => void; +export function tryToRunCommand(command: string): Promise; +export function close(): void; +export function setFindHandler(findHandler: (params: FindParams|null) => void); diff --git a/slug/sdk/web.js b/slug/sdk/web.js new file mode 100644 index 00000000..a59ddac7 --- /dev/null +++ b/slug/sdk/web.js @@ -0,0 +1 @@ +module.exports = window.snail; \ No newline at end of file diff --git a/src/CDPManager.ts b/src/CDPManager.ts index 32d4bc30..6a81e8f7 100644 --- a/src/CDPManager.ts +++ b/src/CDPManager.ts @@ -1,10 +1,7 @@ import { host } from "./host"; -export type DebuggingInfo = { - browserViewUUID?: string; - frameUUID?: string; - type: 'webkit'|'chromium'|'node'; -}; +import type { DebuggingInfo } from '../slug/sdk/web'; +export type { DebuggingInfo } from '../slug/sdk/web'; interface CDPListener { onMessage(message: any, browserViewUUID?: string): void; diff --git a/src/Find.ts b/src/Find.ts index 1a37560b..a05bf5c0 100644 --- a/src/Find.ts +++ b/src/Find.ts @@ -1,6 +1,9 @@ import { AntiFlicker } from './AntiFlicker'; import './find.css'; +import type { FindParams } from '../slug/sdk/web'; +export type { FindParams } from '../slug/sdk/web'; + export class Find { private _element = document.createElement('div'); private _input = document.createElement('input'); @@ -98,7 +101,6 @@ export class Find { this._input.select(); } } -export type FindParams = { regex: RegExp, report: (matches: number) => void }; export interface Findable { setFind(params: FindParams|null): void; } diff --git a/src/actions.ts b/src/actions.ts index 4cdb7d22..d737902c 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -1,6 +1,9 @@ import { rootBlock } from "./GridPane"; import { shortcutParser, type ParsedShortcut } from './shortcutParser'; +import type { Action } from '../slug/sdk/web'; +export type { Action } from '../slug/sdk/web'; + const globalActions: Action[] = []; export function registerGlobalAction(action: Action): void { const existing = globalActions.findIndex(a => a.id === action.id); @@ -18,12 +21,6 @@ export function availableActions(): Action[] { return true; }); } -export type Action = { - id: string; - title: string; - shortcut?: string; - callback: () => void; -} let continuationActions: {shortcut: ParsedShortcut, action: Action}[] = null; diff --git a/src/host.ts b/src/host.ts index 2b572091..96563467 100644 --- a/src/host.ts +++ b/src/host.ts @@ -1,4 +1,3 @@ -/// import type { ShellHost } from '../host/ShellHost'; export interface IHostAPI { sendMessage(message: {method: Key, params?: Parameters[0]}): Promise>; @@ -22,8 +21,9 @@ function makeHostAPI(): IHostAPI { }); return host; } - if (window.snail) { - window.snail.setIsFullscreen(true); + if (window['snail']) { + const snail: typeof import('../slug/sdk/web') = window['snail']; + snail.setIsFullscreen(true); const {host, callback} = hostApiHelper('snail', message => { if (message.method === 'contextMenu') { // async contextMenu({ menuItems }, client, sender) { @@ -69,11 +69,11 @@ function makeHostAPI(): IHostAPI { snail.createContextMenu(unserializeMenuItems(message.params.menuItems)); return; } - window.snail.sendInput(JSON.stringify(message) + '\n'); + snail.sendInput(JSON.stringify(message) + '\n'); }); (async function() { while(true) { - const message = await window.snail.waitForMessage(); + const message = await snail.waitForMessage(); callback(message); } })();