Skip to main content

Custom transports & DISPLAY protocols

Out of the box, node-x11 connects over a unix socket or TCP. But the client core only needs a duplex stream, and the connection layer is pluggable: you can register your own DISPLAY protocol or hand createClient an already-open stream. This is what makes it possible to run node-x11 in a browser, tunnel X over WebSockets or a message channel, or test against an in-process fake server.

The display string syntax

Display strings follow the standard X form, with an optional protocol prefix:

proto/host:display.screen

x11.parseDisplay(display) exposes the parser; it returns { display, protocol, host, displayNum, screenNum }:

x11.parseDisplay(':0');
// { protocol: '', host: '', displayNum: '0', screenNum: 0 }

x11.parseDisplay('tcp/somehost:1.2');
// { protocol: 'tcp', host: 'somehost', displayNum: '1', screenNum: '2' }

x11.parseDisplay('ws/example.org:3');
// { protocol: 'ws', host: 'example.org', displayNum: '3', screenNum: 0 }

The built-in protocol names unix, local, tcp, inet and inet6 force the corresponding standard transport. Any other prefix is looked up in the custom protocol registry. macOS launchd socket paths (/private/tmp/...:0) are recognised and kept whole as the host.

registerDisplayProtocol(name, connect)

Register a connection handler for a protocol prefix:

const x11 = require('x11');

x11.registerDisplayProtocol('ws', (params, options) => {
// params: { display, protocol, host, displayNum, screenNum }
// options: the options object passed to createClient
const stream = makeWebSocketDuplexSomehow(params.host, params.displayNum);
return stream;
});

x11.createClient({ display: 'ws/example.org:0' }, (err, display) => {
// speaks X11 over your transport
});

The connect function receives the parsed display and the createClient options, and must return a duplex-stream-like object — anything with write(buf) / end() and 'data' / 'end' / 'error' events. If the stream is not immediately usable, follow net.Socket semantics: set connecting = true and emit 'connect' once ready. A stream that is already open is used as-is.

Connecting with an unregistered protocol prefix throws unknown display protocol: <name>.

options.stream

If you already hold an open duplex stream, skip the registry and inject it directly:

x11.createClient({ display: ':9', stream: myDuplexStream }, (err, display) => {
// handshake happens over myDuplexStream; ':9' only provides
// displayNum/screenNum for the client object
});

The display string is still parsed (for displayNum and screenNum) but no socket is opened.

options.auth

Custom transports and injected streams have no ~/.Xauthority file to consult, so by default they perform an empty auth handshake ({ name: '', data: '' }). If your server expects a cookie, pass it explicitly:

x11.createClient({
display: 'ws/example.org:0',
auth: { name: 'MIT-MAGIC-COOKIE-1', data: cookieBuffer },
}, callback);

For the built-in unix/TCP transports, options.auth overrides the Xauthority lookup in the same way.

Testing without an X server

This mechanism is exercised end-to-end in test/display-protocols.js, which connects a client to a tiny in-process fake X server through an in-memory stream pair — a useful template if you want to unit-test code that talks to X without spawning Xvfb.

In the browser

The playground on this site is built entirely on this mechanism: the page boots the pure-JS X server from lib/xserver, registers a demo display protocol whose connect returns an in-memory stream pair wired to that server, and sets process.env.DISPLAY = 'demo/local:0'. The demo code you edit there is ordinary node-x11 client code — unchanged, it would run against a real display in node:

x11.registerDisplayProtocol('demo', () => serverConnectedStreamPair());
process.env.DISPLAY = 'demo/local:0';

// from here on, exactly like node:
x11.createClient((err, display) => { /* ... */ });