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);
}
})();