@runmu.sh/ext-widgets
v1.0.0
Published
Lets the game show cards, menus, forms, tables and gauges in your client over GMCP Client.Widget, and tells it what you picked. Plain text only, with limits.
Maintainers
Readme
Server widgets (@runmu.sh/ext-widgets, id widgets)
Lets a game put small widgets in your client: a card (text and buttons), a menu (a list of choices), a form (fields and a submit), a table and a gauge. When you use one, the client sends its command and tells the game what you picked. Install it from Extensions → Discover and enable it for the worlds you trust: a game can show nothing until you do.
What a game sends is cut down before it is shown: plain text only (no HTML, no links, no images, no colours but the theme's), every text and list has a limit, at most 12 widgets per session, and a widget's command can only go to the game, never to the client's own Lua console.
Widgets appear on the session's widget layer: gauges stacked at the top left (220px), the rest stacked at the
bottom right above the command bar. A card, menu or form that can be closed has a [ CLOSE ], and closes once you
use it. New cards, menus and forms are read out by screen readers ("Form: Register. …").
GMCP contract
Package Client.Widget 1, announced to the game with Core.Supports.Add while the extension runs in the world.
Each message has a schema in schema/ that μClient checks before the extension sees it (inbound) or before it is
sent (outbound, in development).
| Message | Direction | Payload |
|---|---|---|
| Client.Widget.Show | game → client | a widget (below), or a one-item list holding it. Shows it, or replaces the one with the same id |
| Client.Widget.Remove | game → client | { "id": … } or a bare id: remove that widget. {} or nothing: remove every widget of the session |
| Client.Widget.Action | client → game | what the player used (below) |
Client.Widget.Show
| Field | Types | For | What |
|---|---|---|---|
| id | string, number | all | required; the game's name for it (64 characters) |
| type | string | all | required: card, menu, form, table or gauge |
| title | string | all | the heading (80) |
| body | string | card, menu, form, table | plain text (2000); tags are removed, <br> and block ends become line breaks, entities are decoded |
| buttons | [{ label, cmd? }] or labels | card | up to 8; label 40, cmd 500 |
| options | [{ label, cmd? }] or labels | menu | up to 20 |
| fields | [{ name, label?, type?, options?, value?, placeholder? }] | form | up to 8. name: A-Z a-z 0-9 _ . -, 32. type: input (default), select (up to 30 options), textarea |
| cmd | string | form | the command template: {name} is replaced by that field's value (on one line) |
| submit (or submit_label) | string | form | the submit label; default "Submit" |
| columns | list | table | up to 8 headings (80 each) |
| rows | list of lists | table | up to 40 rows; cells (80 each) beyond the columns are dropped |
| value, max | number | gauge | max defaults to 100 |
| label | string | gauge | the gauge's label (defaults to title) |
| color | string | gauge | a theme colour: accent (default), accent-bright, ok, gold, alert |
| order | number | gauge | its place among gauges, lower first (default 100; the Vitals gauges are 1–4) |
| dismissible | boolean | card, menu, form | false: no Close, and it stays after use. Gauges never close |
Anything over a limit is cut, and a line in the extension's log says what. A 13th widget removes the oldest one the
player could have closed. A cmd that starts with lua or #lua is dropped (the game still gets the Action).
Client.Widget.Action
{ "id": "door", "action": "button", "index": 0, "label": "Open", "cmd": "open door" }
{ "id": "reg", "action": "submit", "label": "Submit", "values": { "name": "Ash", "cls": "mage" }, "cmd": "register Ash mage" }action is button, option or submit; index is the button's or option's place (from 0); values holds every
field of a submitted form; cmd is the command that was sent with it ("" when there was none). The command is
sent first, as if typed, then the Action.
Sessions
- Each session has its own widgets. When the link drops, its widgets go; the game shows them again after login.
- Widgets the player cannot close (gauges,
dismissible: false) are kept for the session: they come back after a reload, when the extension is enabled mid-session, and on another device attached to the session. - The GMCP replay on reconnect is ignored: send the widgets again after login.
Exported API
Typed in src/types.ts (shipped with the package).
const widgets = await ctx.api<WidgetsApi>('widgets');
widgets.parseShow(data); // { id, spec, warnings } | null: a Show payload as the extension would show it
widgets.removeId(data); // the id a Remove names ('' for all)
widgets.limits; // the limits above
widgets.shown(sid?); // the game ids shown in a session now, oldest firstOn the host's layer a widget's id is widgets.<id> (its element has data-testid="widget-widgets.<id>").
Develop
npm install
npm run build # dist/index.js + manifest check
npm run typecheck
npm test # the extension in the headless host (@runmu.sh/dev/test)
npm run dev # dev server with hot reload; open μClient with ?demo&ext-dev=<url>In the demo, drive it from the console: __demo.gmcp('s-us-1', 'Client.Widget.Show', { id: 'hi', type: 'card', title: 'Hi', buttons: [{ label: 'Wave', cmd: 'wave' }] }), and read what was sent back in __demo.sentGmcp.
