134 lines
5.4 KiB
Markdown
134 lines
5.4 KiB
Markdown
# Slerm TypeScript plugin SDK
|
|
|
|
Slerm runs JavaScript bundles in **embedded Boa**, with a separate worker thread
|
|
and context for each plugin. End users do not need Node.js or Bun. Plugin authors
|
|
use TypeScript and a bundler during development.
|
|
|
|
## Try the example
|
|
|
|
From the Slerm repository root:
|
|
|
|
```sh
|
|
npm ci --prefix sdk
|
|
npm run example --prefix sdk
|
|
```
|
|
|
|
Add this to your Slerm configuration, replacing the absolute path:
|
|
|
|
```toml
|
|
[[plugins]]
|
|
id = "workspace"
|
|
path = "/absolute/path/to/slerm/sdk/dist/workspace.js"
|
|
```
|
|
|
|
Start Slerm. The example opens **Workspace tools**, with controls to create a
|
|
floating pane, run a shell command, and inspect terminal text. Reopen the panel
|
|
from **Plugins → workspace → Show workspace tools**.
|
|
|
|
Bundle paths can also be relative to the configuration file's directory. Plugins
|
|
load at startup; restart Slerm after changing a bundle or the plugin configuration.
|
|
|
|
## Write a plugin
|
|
|
|
Build the SDK with `npm run build --prefix sdk`. In a separate plugin project,
|
|
install the local package with `npm install /absolute/path/to/slerm/sdk` and add
|
|
TypeScript and esbuild as development dependencies. The package has not been
|
|
published to a registry.
|
|
|
|
```ts
|
|
import { definePlugin } from "@slerm/plugin-sdk";
|
|
|
|
definePlugin({
|
|
async activate(slerm) {
|
|
await slerm.registerCommand({
|
|
command: { id: "scratch", title: "New scratch pane" },
|
|
});
|
|
},
|
|
async onEvent(event, slerm) {
|
|
if (event.event === "command" && event.command === "scratch") {
|
|
await slerm.execute({ action: "new_floating" });
|
|
}
|
|
},
|
|
});
|
|
```
|
|
|
|
Type-check your entry point, then bundle it:
|
|
|
|
```sh
|
|
npx tsc --noEmit --strict --target es2022 --module nodenext plugin.ts
|
|
npx esbuild plugin.ts --bundle --format=iife --platform=neutral --target=es2022 --outfile=plugin.js
|
|
```
|
|
|
|
Point a `[[plugins]]` entry at `plugin.js`. Each bundle calls `definePlugin` once.
|
|
All API calls return promises that resolve after Slerm applies the operation.
|
|
Events invoke `onEvent(event, slerm)`; callbacks can be async.
|
|
|
|
## Generated API
|
|
|
|
The Rust definitions in [`src/plugin_api.rs`](../src/plugin_api.rs) are the source
|
|
of truth. A Rust method list declares parameters and results; `ts-rs` generates
|
|
`src/generated.ts`, and TypeScript derives the client method signatures from those
|
|
types. Actions come from Slerm's existing Rust action enum. State, UI elements,
|
|
and events also use the same Rust types for serialization and type generation.
|
|
The generic runtime bridge does not duplicate a method list or handwritten types.
|
|
|
|
Regenerate after changing the Rust API:
|
|
|
|
```sh
|
|
nix develop -c cargo run -- --generate-plugin-sdk
|
|
npm test --prefix sdk
|
|
nix develop -c cargo test
|
|
nix develop -c cargo test bundled_typescript_example_runs_in_boa -- --ignored
|
|
```
|
|
|
|
Without Nix, run Cargo in an environment containing the project's Rust and Zig
|
|
build dependencies. Commit `sdk/src/generated.ts` with API changes. A Rust test
|
|
fails if the checked-in bindings differ from the schema. Bump `API_VERSION` for
|
|
incompatible changes; Slerm rejects bundles built for another API version.
|
|
|
|
| Method | Purpose |
|
|
| --- | --- |
|
|
| `getState()` | Read tabs, panes, titles, focus, floating state, and zoom |
|
|
| `execute({ action })` | Execute a built-in pane, tab, clipboard, or settings action |
|
|
| `focusPane({ pane })`, `focusTab({ tab })` | Focus by ID from `getState()` |
|
|
| `readTerminal({ pane })` | Read a plain-text screen and scrollback snapshot, up to 128 KiB |
|
|
| `writeTerminal({ pane, text })` | Send raw PTY input; include `\r` to submit a shell command |
|
|
| `registerCommand({ command })`, `unregisterCommand({ command })` | Manage entries in the Plugins menu |
|
|
| `setPanel({ panel })`, `removePanel({ panel })` | Create, replace, or remove a native panel |
|
|
| `reportError({ message })` | Display an error in Slerm |
|
|
|
|
Command and panel IDs are scoped to the plugin. Control IDs must be unique within
|
|
a panel. Pane and tab IDs come from Slerm and should be treated as opaque numbers.
|
|
|
|
Events are discriminated by `event.event`:
|
|
|
|
- `command`: `event.command` is the selected command ID.
|
|
- `state`: `event.state` is a snapshot when workspace metadata changes. Terminal
|
|
output changes alone do not produce this event; use `readTerminal` for snapshots.
|
|
- `ui`: `event.data` contains the panel/control IDs and either a `click`, or a
|
|
`change` with the new input `value`.
|
|
|
|
## Native UI
|
|
|
|
`setPanel` renders an egui floating window. Its `elements` accept `text`, `button`,
|
|
`input`, and `separator` variants. Inputs retain edits until replaced by another
|
|
`setPanel` call. UI focus receives typing without forwarding it to the terminal.
|
|
See [`examples/workspace.ts`](examples/workspace.ts) for a complete example.
|
|
|
|
## Runtime boundaries
|
|
|
|
The runtime supports ECMAScript and promises. This initial host does not expose
|
|
Node.js APIs, a DOM, filesystem/network APIs, or timers. Bundle imports ahead of
|
|
time and use libraries compatible with this environment. Report diagnostics with
|
|
`reportError`; `console` is not installed.
|
|
|
|
Plugins are trusted local extensions: terminal writes can run shell commands.
|
|
Boa contexts run in the same OS process as Slerm; worker threads and execution
|
|
limits are not an OS security sandbox. Slerm bounds loops, recursion, promise jobs,
|
|
request waits, bundle/message sizes, and UI counts. An uncaught callback error
|
|
stops that plugin and removes its commands and panels. Host-operation errors
|
|
reject the API promise and can be handled by the plugin.
|
|
|
|
Implementation references: [Boa](https://boajs.dev/docs/intro) and
|
|
[ts-rs](https://docs.rs/ts-rs/latest/ts_rs/).
|