pict-externalui
v1.0.0
Published
Host a pict application over a socket-first external UI protocol, so native providers (Godot, Tauri, and others) render its views.
Maintainers
Readme
pict-externalui
Host a pict application over a socket-first external UI protocol, so native
providers (Godot, Tauri, and others) render its views. The exact sibling of
pict-terminalui: it swaps the pict ContentAssignment seam, but instead of writing
to blessed widgets in-process, it projects an addressable widget tree to one or more
providers over a socket and routes addressed events back. The pict application is
unchanged; this is a third consumer of the ContentAssignment hook, alongside the
browser DOM and blessed. Base pict is never modified.
Architecture
A pict app runs in Node with pict-externalui attached. Every view render, which
pict would normally write to the DOM, becomes a projection message: an addressable
widget tree. A provider, a thin generic client, builds native widgets from that tree
and sends event messages back when the user interacts. The host marshals a value
event to the control's own data address and dispatches a command event to the
control's own registered action, then the resulting re-render projects again. All
application logic stays in the host; the provider holds no application state, so one
provider renders any pict app, and a late or reconnecting provider is caught up by a
replay of the current tree.
Node (the host) the provider
+-------------------------------+ +--------------------------+
| pict app: views, AppData, | | build native widgets |
| actions | | from the projected tree, |
| | | | report addressed events |
| | render (ContentAssignment | ws / json | |
| v seam, swapped) | =========> | Godot (native) |
| projection {op,address,widget}| projection | web (Tauri/Electron/ |
| | | Sciter) |
| event {address,kind,value} | <========= | NodeGui (Qt) |
| ^ marshal / dispatch | event | |
+-------------------------------+ +--------------------------+The transport is swappable behind a small interface (WebSocket is the reference).
The wire contract is versioned and schema-backed: every message family has a JSON
Schema in schemas/, and the host validates every inbound message against it. See
docs/PROTOCOL.md for the contract and docs/TRANSPORTS.md for the transport
interface.
Thirty-second quick start
Attach the host to a pict app, register the actions its command controls invoke, and start the socket:
const libPictExternalUI = require('pict-externalui');
let tmpUI = new libPictExternalUI(pict, { Port: 8090 });
tmpUI.registerAction('addTask', (pBinding, pEvent) => { /* mutate AppData, re-render */ });
tmpUI.start((pError, pAddress) => { /* pAddress = { host, port, url } */ });Views emit a JSON widget descriptor as their rendered content (see
conformance/fixtures/Fixture-Todo.js for a complete, runnable example), and the
host turns each render into a projection.
Then point a provider at it. The shared web client needs no build:
# from this module, serve the client and drive it once a browser connects
node conformance/run-conformance-web.js
# open the printed URL in any browserOr run the native Godot provider (with Godot installed):
node conformance/run-conformance.js godotTrust model
The host binds to loopback (127.0.0.1) by default. Binding to a non-loopback
address exposes the app to the network and requires an explicit opt-in
(options.AllowRemote = true); without it the host refuses to start. The event
channel is untrusted: every inbound message is validated against its schema, an
event's address must resolve to a binding the host actually projected before any
value is marshaled, and only a control's own registered action is ever dispatched. A
malformed or hostile message is logged and dropped, never thrown. The full trust
model is in docs/PROTOCOL.md.
Writing a provider
A provider is a generic client that renders any pict app. It speaks the protocol in
docs/PROTOCOL.md; the shortest path is to copy the closest reference provider under
providers/ and adapt the widget building. The contract:
Connect and send
helloonce, before anything else, with your protocol version, a name, thecontrolsyou can render, and adriverflag:{ "type": "hello", "protocol": "1.0", "provider": "mykit", "capabilities": { "controls": ["text","select","number","checkbox","button","label","list"], "escapeHatch": "webview" | "native" | false, "driver": true } }The host replies
welcome, then replays the current tree (and the theme, if any).Handle
projection, a patch to the widget tree at an address.opisreplace(the default: clear the region, then build),append,prepend, orremove.widgetis a node tree (Form,List,Container,Row,Field,Button,Label,Escape); map eachcontrolkind on aFieldto a native widget. Tolerate a node type or control kind you do not understand by skipping it, so a projection you cannot fully render does not fail the whole tree.On a real widget signal, send an
eventby the control's address:{ "type": "event", "address": "#Form/Name", "kind": "value", "value": "Water" } { "type": "event", "address": "#Form/Add", "kind": "command" }A value event carries the control's new value; a command event carries none. Do not put a data address in the event; the host uses the descriptor's own binding.
Handle
drive(test only, gated on yourdriverflag) by firing the addressed widget's real signal path, not by sending the event directly. That is what makes a passing conformance run certify your wiring rather than a mock.Handle
themeby mapping the toolkit-neutral tokens to your theming, or ignore it if you cannot theme. Handle anEscapenode by rendering itshtmlin an embedded webview (advertiseescapeHatch: "native"if you are a browser,"webview"if you can embed one), or show a placeholder that keeps the region's address.Reconnect if the socket drops, re-sending
hello. The host replays the tree, so you survive starting before the host or a host restart.
The schemas in schemas/ are standard draft-07, so you can validate messages in any
language while building a provider.
Providers and conformance
Each provider under providers/ is generic (renders any pict app) and passes the
same conformance scripts. A provider is certified by driving its real widgets, so a
green run proves the wiring, not a mock.
| Provider | What it is | Run / certify |
| --- | --- | --- |
| mock | headless, in-process | npm test (every run) |
| web | shared client (Tauri, Electron, Sciter host it) | npm run conformance:web, then open the URL; providers/web/docs/HOSTING.md |
| godot | native Control nodes, desktop and mobile | node conformance/run-conformance.js godot; providers/godot/docs/PACKAGING.md |
| tauri | shared web client in the OS webview | providers/tauri/docs/PACKAGING.md |
| nodegui | real Qt widgets | HOST_PORT=8091 npm run conformance:external, then run the provider windowed; providers/nodegui/docs/PACKAGING.md |
The web and NodeGui runs use a host that waits for the provider to connect, because a
browser cannot be spawned and NodeGui's qode launcher re-execs a native Qt binary
that does not complete its socket when spawned as a child. In CI, set BROWSER_BIN
(for example chromium --headless=new --disable-gpu) so conformance:web launches a
headless browser itself.
Testing
npm test # host unit tests + transport + validator + in-process conformance (mock) + provider smoke
npm run coverage
npm run smoke # load every provider as far as this machine allowsnpm test runs fast and headless: the host, transport, and schema validator unit
tests (including the malformed and hostile input cases), both conformance fixtures
driven through the in-process mock provider over the real socket, and a smoke pass
that parses the JavaScript providers and checks the native providers' project files.
The real-GUI providers are certified by the documented commands above.
Layout
source/
Pict-ExternalUI.js the host
Message-Schema-Validator.js runtime validation against schemas/
transports/WebSocket-Transport.js the reference transport
schemas/ draft-07 JSON Schema per message family
docs/
PROTOCOL.md the versioned, schema-backed wire spec
TRANSPORTS.md the swappable transport interface
DESIGN.md sequence diagrams of the flows
conformance/
fixtures/ real pict apps used by the suite
Mock-Provider.js a headless provider for testing the host
Conformance-Harness.js drives a provider through a script, asserts
scripts/ the canonical conformance scripts
run-conformance*.js runners for out-of-process providers
smoke.js the CI smoke check
providers/
godot/ tauri/ web/ nodegui/ the reference providers, each with docs
test/ mocha TDD unit and conformance tests