Skip to main content

node-x11 documentation

node-x11 is a pure-JavaScript X Window System protocol client for Node.js. There is no native code and no runtime dependency: the library speaks the X11 wire protocol directly over a unix socket or TCP connection.

Connecting

const x11 = require('x11');

x11.createClient((err, display) => {
if (err) throw err;
const X = display.client; // the request interface
// ...
});

createClient(options, callback) accepts an optional first argument:

optionmeaning
displaydisplay string, e.g. ':0', 'localhost:1.0' or a literal socket path (default: $DISPLAY)
debuglog outgoing requests and capture per-request stack traces for errors
disableBigRequestsskip the automatic BIG-REQUESTS handshake done at connect time

The client connects over a unix socket when the display refers to the local host (on macOS the display must be a literal socket path, XQuartz launchd style, e.g. /private/tmp/com.apple.launchd.../org.xquartz:0), and over TCP (port 6000 + display number) otherwise. ~/.Xauthority (or $XAUTHORITY) is used for authentication automatically.

createClient returns the client object immediately; subscribe to 'error' on it to catch connection-phase failures.

The display object

The display passed to the callback describes the connection setup block:

  • display.client — the X client object all requests are called on
  • display.screen[n] — one entry per screen, with root (root window id), white_pixel, black_pixel, pixel_width/pixel_height, mm_width/mm_height, default_colormap, root_depth, depths, …
  • display.min_keycode / display.max_keycode

Making requests

All requests live on X = display.client and follow the callback style — no promises. Requests with a reply take a trailing callback(err, result). Requests with no reply ("void" requests) can be fired and forgotten, or given a trailing callback(err) that fires exactly once: with null once the server has processed the request without error, or with the X error it caused (the client issues a cheap sync round trip when needed, the way xcb_request_check does):

const wid = X.AllocID(); // allocate a resource id
X.CreateWindow(wid, display.screen[0].root, // no reply: fire and forget
10, 10, 400, 300);
X.MapWindow(wid);

X.InternAtom(false, 'WM_NAME', (err, atom) => { // with reply
// ...
});

X.ChangeWindowAttributes(root, { // void request, checked:
eventMask: x11.eventMask.SubstructureRedirect
}, err => {
// err === null → the request succeeded (e.g. we are the WM now)
// err instanceof Error → e.g. BadAccess: another WM is running
});

Resource ids (windows, pixmaps, GCs, …) are allocated client-side with X.AllocID() and can be recycled with X.ReleaseID(id) once the resource is destroyed. When you are done with the connection, call X.terminate(); the client emits 'end' when the stream closes.

See core-requests.md for the complete reference.

Flow control

Requests are buffered and written to the socket immediately, but the socket may not keep up (mouse-driven redraws, full-speed rendering). Three tools keep memory bounded:

  • Return value + 'drain'. Every request method returns false when the socket applied backpressure — same contract as stream.Writable's write(). Stop producing and resume on the client's 'drain' event:

    function render() {
    let ok = true;
    while (ok && hasWork())
    ok = X.PolyFillRectangle(wid, gc, nextBatch());
    if (!ok)
    X.once('drain', render); // resume when the socket caught up
    }
  • X.flush([cb]) — the callback fires once everything buffered so far has been handed to the OS. Returns a Promise when called without a callback.

  • X.sync([cb]) — a full round trip: the callback fires once the server has processed every request issued so far (the equivalent of XSync). Any errors those requests caused have been delivered by then. Returns a Promise when called without a callback. Pacing a render loop with sync throttles to what the server actually consumes:

    function frame() {
    drawFrame(X, wid, gc);
    X.sync(() => frame()); // next frame only when this one is done
    }

Sequence numbers are 16-bit on the wire but full-width on the client (err.seq keeps growing past 65535); the client transparently inserts a cheap round-trip request once per 60000 reply-less requests to keep the mapping unambiguous, the same way libxcb does.

Listening for events

Select the events you want on a window — either at creation time or later — using masks from x11.eventMask, then listen on the client:

X.ChangeWindowAttributes(wid, {
eventMask: x11.eventMask.Exposure | x11.eventMask.PointerMotion
});
X.on('event', ev => {
if (ev.name === 'MotionNotify')
console.log(ev.rootx, ev.rooty);
});

Every event object carries name, type, seq and the event-specific fields; ev.rawData is the raw wire packet (useful with SendEvent). Extension events are delivered through the same 'event' emitter once the extension has been initialised (its requireExt registers the parsers). GenericEvents (X Generic Event Extension, used by Present and XInput 2) are framed by their length field and dispatched to per-extension parsers registered in X.geEventParsers.

See core-events.md for every core event and its fields.

Error handling

X errors arrive asynchronously. Errors caused by a request issued with a callback — whether it has a reply or not — are routed to that callback as err (an Error with error code, seq, badParam, majorOpcode/minorOpcode). Errors from requests issued without a callback — and errors nobody claims — are emitted as 'error' on the client:

X.on('error', err => console.error(err.message, err.badParam));

If a reply callback receives an error and returns true, the error is considered handled and is not re-emitted on the client. With createClient({debug: true}, …) each error also carries the stack trace of the request that caused it.

Extensions

Extensions are loaded at runtime with X.require(name, cb). The callback receives an extension object carrying the extension's requests, enums and version info; any events are registered on the client automatically:

X.require('randr', (err, Randr) => {
if (err) throw err; // extension missing on this server
Randr.GetScreenResources(display.screen[0].root, (err, res) => {
console.log(res.outputs);
});
});

name is the module name below (the file in lib/ext/), not the on-the-wire extension name. Requiring an extension twice returns the cached instance.

moduleX namepage
apple-wmApple-WMext/apple-wm.md
big-requestsBIG-REQUESTSext/big-requests.md
compositeCompositeext/composite.md
damageDAMAGEext/damage.md
dbeDOUBLE-BUFFERext/dbe.md
dpmsDPMSext/dpms.md
fixesXFIXESext/fixes.md
geGeneric Event Extensionext/ge.md
glxGLXext/glx.md
presentPresentext/present.md
randrRANDRext/randr.md
recordRECORDext/record.md
renderRENDERext/render.md
resX-Resourceext/res.md
screen-saverMIT-SCREEN-SAVERext/screen-saver.md
shapeSHAPEext/shape.md
shmMIT-SHMext/shm.md
syncSYNCext/sync.md
xc-miscXC-MISCext/xc-misc.md
xineramaXINERAMAext/xinerama.md
xinputXInputExtensionext/xinput.md
xkbXKEYBOARDext/xkb.md
xtestXTESText/xtest.md
xvXVideoext/xv.md

Other exports

  • x11.eventMask — event mask bit names
  • x11.keySyms — keysym tables (lazy-loaded)
  • x11.gcFunction — GC raster operation constants (lazy-loaded)
  • x11.createServer — experimental X server implementation
  • Window class constants x11.CopyFromParent / x11.InputOutput / x11.InputOnly and SendEvent destinations x11.PointerWindow / x11.InputFocus

Running against a test server

The test suite runs against a private Xvfb: npm run test:local (see scripts/test-local.sh). Runnable demos live in examples/.