ntk 7.3.2 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/drawable.js CHANGED
@@ -1,5 +1,10 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
 
3
+ // lib/ may not statically import node builtins (see docs/packaging.md); this
4
+ // file is the one sanctioned exception for node:events, so it is also where
5
+ // the rest of lib/ takes EventEmitter from.
6
+ export { EventEmitter };
7
+
3
8
  export default class Drawable extends EventEmitter {
4
9
  getContext(name, ...args) {
5
10
  const factory = Drawable.renderingContextFactory[name];
package/lib/index.js CHANGED
@@ -6,6 +6,16 @@ import { BLANK_CURSOR, CursorCache, cursorNames, cursorShapes, resolveCursorShap
6
6
  import { DEFAULT_GL_POLICY, GLError, GL_MODES, resolveGLPolicy, wantsDirect } from './gl.js';
7
7
  import { GLXError } from './glx.js';
8
8
  import Window from './window.js';
9
+ import {
10
+ FocusProxy,
11
+ XEMBED,
12
+ XEMBED_OPCODE_NAMES,
13
+ XEmbedPlug,
14
+ XEmbedSocket,
15
+ decodeXEmbedInfo,
16
+ encodeXEmbedInfo,
17
+ readXEmbedInfo
18
+ } from './xembed.js';
9
19
  import { loadLayout } from './yoga.js';
10
20
  import { decodeKey, groupForState } from './keyboard.js';
11
21
  import Pixmap from './pixmap.js';
@@ -210,6 +220,15 @@ export {
210
220
  App,
211
221
  Clipboard,
212
222
  Window,
223
+ // XEmbed (docs/xembed.md): also available as `ntk/xembed`
224
+ XEmbedSocket,
225
+ XEmbedPlug,
226
+ FocusProxy,
227
+ XEMBED,
228
+ XEMBED_OPCODE_NAMES,
229
+ encodeXEmbedInfo,
230
+ decodeXEmbedInfo,
231
+ readXEmbedInfo,
213
232
  BLANK_CURSOR,
214
233
  CursorCache,
215
234
  cursorNames,
@@ -2965,15 +2965,21 @@ class RenderingContext2d {
2965
2965
  rect.h,
2966
2966
  ]);
2967
2967
  }
2968
+ // The scratch mask is surface-sized and surface-aligned — the coverage
2969
+ // went into it at (dx, dy) — and so is a gradient source, so both are
2970
+ // sampled at the destination offset. Reading either from (0, 0) draws a
2971
+ // dw x dh window out of the wrong part of a full-surface picture: for
2972
+ // the mask that is the region just cleared to zero, so nothing paints at
2973
+ // all unless dx and dy are both zero.
2968
2974
  R.Composite(
2969
2975
  op,
2970
2976
  this._backgroundPicture.id,
2971
2977
  this.fillMask.id,
2972
2978
  this.picture.id,
2973
- 0,
2974
- 0,
2975
- 0,
2976
- 0,
2979
+ dx,
2980
+ dy,
2981
+ dx,
2982
+ dy,
2977
2983
  dx,
2978
2984
  dy,
2979
2985
  dw,
@@ -2996,13 +3002,16 @@ class RenderingContext2d {
2996
3002
  rect.h,
2997
3003
  ]);
2998
3004
  }
3005
+ // the source is a 1x1 repeating solid (offset irrelevant) or the fill
3006
+ // picture, which for a gradient is surface-aligned: same offset as the
3007
+ // destination, exactly as in _fillPolys
2999
3008
  R.Composite(
3000
3009
  op,
3001
3010
  this._coverageSource().id,
3002
3011
  picture.id,
3003
3012
  this.picture.id,
3004
- 0,
3005
- 0,
3013
+ dx,
3014
+ dy,
3006
3015
  mx,
3007
3016
  my,
3008
3017
  dx,
package/lib/window.js CHANGED
@@ -499,13 +499,15 @@ export default class Window extends Drawable {
499
499
  // window was asked to be, so the window manager's first ConfigureNotify
500
500
  // reports only what it actually overrode
501
501
  this._deliveredGeom = { x: this.x, y: this.y, width: this.width, height: this.height };
502
+ this.windowClass = args.windowClass ?? 0;
502
503
  const values = {
503
- // NorthWest bit gravity: keep the old content anchored during
504
- // resize instead of discarding it (less flicker between the resize
505
- // and our redraw)
506
- bitGravity: 1,
507
504
  eventMask: this.eventMask
508
505
  };
506
+ // NorthWest bit gravity: keep the old content anchored during resize
507
+ // instead of discarding it (less flicker between the resize and our
508
+ // redraw). An InputOnly window (class 2) has no content to anchor and
509
+ // the attribute is not one it accepts — setting it is a BadMatch.
510
+ if (this.windowClass !== 2) values.bitGravity = 1;
509
511
  for (const name of forwardedXAttributes) {
510
512
  if (args[name] !== undefined) values[name] = args[name];
511
513
  }
@@ -514,7 +516,6 @@ export default class Window extends Drawable {
514
516
  // parent of a different depth is a BadMatch (X11 CreateWindow)
515
517
  this.visual = args.visual ?? 0;
516
518
  this.depth = args.depth ?? 0;
517
- this.windowClass = args.windowClass ?? 0;
518
519
  if (this.visual) {
519
520
  if (values.colormap === undefined) {
520
521
  this._ownedColormap = app.createColormap(this.visual);
@@ -2612,16 +2613,21 @@ export default class Window extends Drawable {
2612
2613
 
2613
2614
  if (mapped) {
2614
2615
  const root = this.app.display.screen[0].root;
2615
- const messageType = await this.atom('_NET_WM_STATE');
2616
- if (this._destroyed) return false;
2617
- safeRelease(this.X, () => {
2618
- this.X.SendClientMessage(root, this.id, messageType, 32, [
2616
+ // EWMH 7.7: delivered to the root with the substructure mask, so the
2617
+ // window manager watching there sees it, but about this window
2618
+ await this.sendClientMessage(
2619
+ '_NET_WM_STATE',
2620
+ [
2619
2621
  mode,
2620
2622
  ids[0],
2621
2623
  ids[1] ?? 0,
2622
2624
  1 // source indication: a normal application, not a pager
2623
- ]);
2624
- });
2625
+ ],
2626
+ {
2627
+ target: root,
2628
+ mask: x11.eventMask.SubstructureRedirect | x11.eventMask.SubstructureNotify
2629
+ }
2630
+ );
2625
2631
  } else {
2626
2632
  await this._writeWmState(ids, mode);
2627
2633
  }
@@ -3168,6 +3174,58 @@ export default class Window extends Drawable {
3168
3174
  return this;
3169
3175
  }
3170
3176
 
3177
+ /**
3178
+ * Send a ClientMessage about this window (X SendEvent with a
3179
+ * ClientMessage payload).
3180
+ *
3181
+ * ClientMessage is the carrier of every convention layered over the core
3182
+ * protocol — EWMH state changes, WM_PROTOCOLS, XEmbed, XDND, the system
3183
+ * tray — and they differ only in the atom naming the message and the five
3184
+ * 32-bit words in it. `type` is an atom name (or an already-interned id),
3185
+ * `data` the words; anything past the end of the list is sent as zero.
3186
+ *
3187
+ * await wnd.sendClientMessage('WM_PROTOCOLS', [deleteAtom, time]);
3188
+ *
3189
+ * Two options carry the meaning that the raw request buries in argument
3190
+ * order:
3191
+ *
3192
+ * - `target` — where the message is *delivered*, which is not always the
3193
+ * window it is *about*. EWMH messages are about a client window and
3194
+ * delivered to the root, so the window manager (which selected
3195
+ * SubstructureRedirect there) sees them.
3196
+ * - `mask` — who on the target gets it. `0`, the default, delivers to the
3197
+ * client that owns the target window whatever it selected, which is
3198
+ * what a message addressed to another client needs. Root-window EWMH
3199
+ * messages instead pass
3200
+ * `SubstructureRedirect | SubstructureNotify`.
3201
+ *
3202
+ * @param {string|number} type atom name of the message type, or its id
3203
+ * @param {number[]} [data] up to five 32-bit words (ten at format 16,
3204
+ * twenty at format 8)
3205
+ * @param {object} [options] `{ target = this.id, format = 32, mask = 0 }`
3206
+ * @returns {Promise<Window>}
3207
+ */
3208
+ async sendClientMessage(type, data = [], options = {}) {
3209
+ const { target = this.id, format = 32, mask = 0 } = options;
3210
+ const slots = { 8: 20, 16: 10, 32: 5 }[format];
3211
+ if (!slots) {
3212
+ throw new TypeError(`sendClientMessage: format must be 8, 16 or 32, got ${format}`);
3213
+ }
3214
+ if (data.length > slots) {
3215
+ // the wire event is 32 bytes and no more; sending the first `slots`
3216
+ // words of a longer list would drop the rest without a sound
3217
+ throw new RangeError(
3218
+ `sendClientMessage: a format-${format} ClientMessage carries ${slots} values, got ${data.length}`
3219
+ );
3220
+ }
3221
+ const messageType = typeof type === 'number' ? type : await this.atom(type);
3222
+ if (this._destroyed) return this;
3223
+ safeRelease(this.X, () => {
3224
+ this.X.SendClientMessage(target, this.id, messageType, format, data, mask);
3225
+ });
3226
+ return this;
3227
+ }
3228
+
3171
3229
  /**
3172
3230
  * Ask the client to close, the polite way: a WM_DELETE_WINDOW client
3173
3231
  * message if it advertised the protocol in WM_PROTOCOLS, so it can save
@@ -3187,13 +3245,11 @@ export default class Window extends Drawable {
3187
3245
  safeRelease(this.X, () => this.X.KillClient(this.id));
3188
3246
  return false;
3189
3247
  }
3190
- const wmProtocols = await this.atom('WM_PROTOCOLS');
3191
- // mask 0: deliver to the client that created the window, not to whoever
3192
- // selected events on it — a WM_PROTOCOLS message is addressed to the
3193
- // owner, so it must not go out with the EWMH substructure default
3194
- safeRelease(this.X, () =>
3195
- this.X.SendClientMessage(this.id, this.id, wmProtocols, 32, [deleteAtom, 0 /* CurrentTime */], 0)
3196
- );
3248
+ // the default mask of 0 delivers to the client that created the window,
3249
+ // not to whoever selected events on it — a WM_PROTOCOLS message is
3250
+ // addressed to the owner, so it must not go out with the EWMH
3251
+ // substructure mask
3252
+ await this.sendClientMessage('WM_PROTOCOLS', [deleteAtom, 0 /* CurrentTime */]);
3197
3253
  return true;
3198
3254
  }
3199
3255
 
package/lib/xembed.js ADDED
@@ -0,0 +1,922 @@
1
+ import x11 from 'x11';
2
+
3
+ import { safeRelease } from './cleanup.js';
4
+ import { EventEmitter } from './drawable.js';
5
+
6
+ // XEmbed (http://specifications.freedesktop.org/xembed/0.5/): putting one
7
+ // client's top-level window inside another client's window hierarchy, across
8
+ // toolkits. Two roles — the embedder (GTK's socket, Qt's container) and the
9
+ // client (GTK's plug) — and no X extension: core requests only.
10
+ //
11
+ // The mechanics are deliberately short. The client sets `_XEMBED_INFO` on an
12
+ // unmapped top-level window; the embedder adds that window to its save-set,
13
+ // reparents it into one of its own and says so with an `XEMBED_EMBEDDED_NOTIFY`
14
+ // ClientMessage; from then on the two sides talk in `_XEMBED` messages —
15
+ // activation, focus, modality — while the embedder keeps the client's mapped
16
+ // state in step with the `XEMBED_MAPPED` bit of `_XEMBED_INFO`.
17
+ //
18
+ // Most of the interesting clients do not speak any of it. `xterm -into WID`,
19
+ // `mpv --wid=WID`, VLC and Wine want plain reparenting and set no
20
+ // `_XEMBED_INFO` at all, so a missing property means "map it now, skip the
21
+ // message protocol" — the same thing GtkSocket does, and the path that makes a
22
+ // terminal pane or a video surface work.
23
+ //
24
+ // Two things about focus are worth knowing before reading XEmbedSocket:
25
+ //
26
+ // - The embedder holds the real X input focus; the client gets *logical* focus
27
+ // by message. So activation and focus are things the embedder tells the
28
+ // client about, not things the client observes.
29
+ // - Key events therefore have to be routed by hand. X delivers a key press to
30
+ // the focus window, or to the deepest descendant of it under the pointer —
31
+ // so with focus on the embedder's toplevel, whether the embedded client sees
32
+ // a keystroke would depend on where the mouse happens to be. FocusProxy is
33
+ // the classic answer: an InputOnly window that holds the real focus while
34
+ // the client is logically focused, forwarding what it receives with
35
+ // SendEvent.
36
+
37
+ /**
38
+ * Protocol constants: the version, the `_XEMBED_INFO` flags, the message
39
+ * opcodes and the details `XEMBED_FOCUS_IN` can carry.
40
+ */
41
+ export const XEMBED = {
42
+ /** highest protocol version this implementation speaks */
43
+ VERSION: 0,
44
+
45
+ // _XEMBED_INFO flags
46
+ /** bit 0 of the flags word: the client wants to be mapped */
47
+ MAPPED: 1 << 0,
48
+
49
+ // opcodes (message data l[1])
50
+ EMBEDDED_NOTIFY: 0,
51
+ WINDOW_ACTIVATE: 1,
52
+ WINDOW_DEACTIVATE: 2,
53
+ REQUEST_FOCUS: 3,
54
+ FOCUS_IN: 4,
55
+ FOCUS_OUT: 5,
56
+ FOCUS_NEXT: 6,
57
+ FOCUS_PREV: 7,
58
+ MODALITY_ON: 10,
59
+ MODALITY_OFF: 11,
60
+ REGISTER_ACCELERATOR: 12,
61
+ UNREGISTER_ACCELERATOR: 13,
62
+ ACTIVATE_ACCELERATOR: 14,
63
+
64
+ // XEMBED_FOCUS_IN details (message data l[2])
65
+ /** focus the client without moving its own logical focus */
66
+ FOCUS_CURRENT: 0,
67
+ /** a Tab arriving from outside: focus the first widget in the tab chain */
68
+ FOCUS_FIRST: 1,
69
+ /** a back-Tab arriving from outside: focus the last one */
70
+ FOCUS_LAST: 2
71
+ };
72
+
73
+ /** opcode -> name, for logging and for the `'message'` events */
74
+ export const XEMBED_OPCODE_NAMES = Object.freeze({
75
+ 0: 'EMBEDDED_NOTIFY',
76
+ 1: 'WINDOW_ACTIVATE',
77
+ 2: 'WINDOW_DEACTIVATE',
78
+ 3: 'REQUEST_FOCUS',
79
+ 4: 'FOCUS_IN',
80
+ 5: 'FOCUS_OUT',
81
+ 6: 'FOCUS_NEXT',
82
+ 7: 'FOCUS_PREV',
83
+ 10: 'MODALITY_ON',
84
+ 11: 'MODALITY_OFF',
85
+ 12: 'REGISTER_ACCELERATOR',
86
+ 13: 'UNREGISTER_ACCELERATOR',
87
+ 14: 'ACTIVATE_ACCELERATOR'
88
+ });
89
+
90
+ const NAME = '_XEMBED';
91
+ const INFO = '_XEMBED_INFO';
92
+
93
+ /**
94
+ * The two CARD32s of an `_XEMBED_INFO` property: the protocol version the
95
+ * client speaks and its flags word.
96
+ *
97
+ * @param {object} [info] `{ version, flags, mapped }` — `mapped` is the
98
+ * XEMBED_MAPPED bit spelled out, and wins over the same bit in `flags`
99
+ * @returns {number[]} `[version, flags]`
100
+ */
101
+ export function encodeXEmbedInfo(info = {}) {
102
+ const { version = XEMBED.VERSION, flags = 0, mapped } = info;
103
+ let word = flags >>> 0;
104
+ if (mapped !== undefined) {
105
+ word = mapped ? word | XEMBED.MAPPED : word & ~XEMBED.MAPPED;
106
+ }
107
+ return [version >>> 0, word >>> 0];
108
+ }
109
+
110
+ /**
111
+ * The inverse. `null` for a property that is absent or too short to be one —
112
+ * which is not an error but the ordinary case: a client that speaks no XEmbed
113
+ * sets no `_XEMBED_INFO`, and is embedded by plain reparenting.
114
+ *
115
+ * @param {number[]|null} words the property read `{ as: 'numbers' }`
116
+ * @returns {{version: number, flags: number, mapped: boolean}|null}
117
+ */
118
+ export function decodeXEmbedInfo(words) {
119
+ if (!words || words.length < 2) return null;
120
+ const flags = words[1] >>> 0;
121
+ return { version: words[0] >>> 0, flags, mapped: (flags & XEMBED.MAPPED) !== 0 };
122
+ }
123
+
124
+ /** Read `_XEMBED_INFO` off a window. `null` when it is not set. */
125
+ export async function readXEmbedInfo(window) {
126
+ const words = await window.getProperty(INFO, { as: 'numbers' }).catch(() => null);
127
+ return decodeXEmbedInfo(words);
128
+ }
129
+
130
+ /**
131
+ * The window that holds the real X input focus on the client's behalf, and
132
+ * forwards keystrokes into it.
133
+ *
134
+ * X delivers a key press to the focus window or to the deepest descendant of
135
+ * it that contains the pointer, so an embedder that simply focuses its own
136
+ * toplevel would send the embedded client keys only while the mouse is over
137
+ * it. A 1x1 InputOnly window, mapped but clipped entirely outside its parent,
138
+ * has no descendants and covers no pixels: it can hold focus, it can never be
139
+ * entered by the pointer, and everything it receives belongs to whoever is
140
+ * logically focused.
141
+ *
142
+ * Forwarded events carry X's `send_event` flag, as any SendEvent does. Toolkit
143
+ * clients accept them — this is how XEmbed keyboard input has always worked —
144
+ * but a client that filters synthetic events will not see them.
145
+ */
146
+ export class FocusProxy {
147
+ /**
148
+ * @param {import('./window.js').default} parent the window to create the
149
+ * proxy under; it must be viewable for the proxy to be able to take focus
150
+ */
151
+ constructor(parent) {
152
+ this.app = parent.app;
153
+ this.X = parent.X;
154
+ this.target = 0;
155
+ // -1,-1: entirely outside the parent, so the pointer can never be in it
156
+ this.window = this.app.createWindow({
157
+ parent,
158
+ x: -1,
159
+ y: -1,
160
+ width: 1,
161
+ height: 1,
162
+ windowClass: 2, // InputOnly
163
+ eventMask: x11.eventMask.KeyPress | x11.eventMask.KeyRelease
164
+ });
165
+ this.window.map();
166
+ this._forward = this._forward.bind(this);
167
+ this.window.on('keydown', this._forward);
168
+ this.window.on('keyup', this._forward);
169
+ }
170
+
171
+ /**
172
+ * Take the X input focus and forward what arrives to `windowId`.
173
+ * @param {number} windowId
174
+ */
175
+ take(windowId) {
176
+ this.target = windowId >>> 0;
177
+ this.window.focus(2); // revert to the parent when we stop being viewable
178
+ return this;
179
+ }
180
+
181
+ /** Stop forwarding. Does not move the focus — that is the embedder's call. */
182
+ release() {
183
+ this.target = 0;
184
+ return this;
185
+ }
186
+
187
+ destroy() {
188
+ this.target = 0;
189
+ this.window.removeListener('keydown', this._forward);
190
+ this.window.removeListener('keyup', this._forward);
191
+ this.window.destroy();
192
+ }
193
+
194
+ _forward(ev) {
195
+ if (!this.target) return;
196
+ // the same event, re-addressed: `wid` is the window it is reported on and
197
+ // `child` the descendant it happened over, neither of which survives the
198
+ // move. Mask 0 delivers it to the window's owner whatever it selected.
199
+ safeRelease(this.X, () =>
200
+ this.X.SendEvent(this.target, 0, 0, { ...ev, wid: this.target, child: 0 })
201
+ );
202
+ }
203
+ }
204
+
205
+ /**
206
+ * The embedder half: a container window that another client's top-level
207
+ * window is reparented into.
208
+ *
209
+ * const socket = new XEmbedSocket(parentWindow, { x, y, width, height });
210
+ * await socket.embed(clientWindowId);
211
+ *
212
+ * Events:
213
+ * 'embedded' `{ id, window, version, xembed }` — `xembed:false` is a
214
+ * client that set no `_XEMBED_INFO` and got plain reparenting
215
+ * 'mappedChange' `(mapped)` — the client asked to be mapped or unmapped
216
+ * 'requestFocus' the client wants the logical focus
217
+ * 'focusNext' / 'focusPrev' — the client ran off the end of its tab chain
218
+ * 'message' `{ opcode, name, detail, data1, data2, time }` — an
219
+ * `_XEMBED` message this class does not act on itself
220
+ * 'gone' the client was destroyed or reparented away by someone else
221
+ */
222
+ export class XEmbedSocket extends EventEmitter {
223
+ /**
224
+ * @param {import('./window.js').default} parent where the container window
225
+ * goes
226
+ * @param {object} [options] `{ x, y, width, height }` for the container
227
+ * window this creates, or `{ window }` to embed into a window the caller
228
+ * already owns (which it also keeps responsible for destroying).
229
+ * `{ version }` caps the protocol version offered to the client;
230
+ * `{ focusOnRequest: false }` stops `XEMBED_REQUEST_FOCUS` from being
231
+ * answered automatically, for an embedder with a focus manager of its own.
232
+ */
233
+ constructor(parent, options = {}) {
234
+ super();
235
+ const {
236
+ x = 0,
237
+ y = 0,
238
+ width = 1,
239
+ height = 1,
240
+ window = null,
241
+ version = XEMBED.VERSION,
242
+ focusOnRequest = true
243
+ } = options;
244
+
245
+ this.app = parent.app;
246
+ this.X = this.app.X;
247
+ this.version = version;
248
+ this._focusOnRequest = focusOnRequest;
249
+ this._ownsWindow = !window;
250
+ /** the container window; the client becomes a child of this */
251
+ this.window = window ?? this.app.createWindow({ parent, x, y, width, height });
252
+
253
+ /** the embedded client, as a Window, or null */
254
+ this.client = null;
255
+ /** whether the client answered with `_XEMBED_INFO` */
256
+ this.xembed = false;
257
+ /** the protocol version in use, once a client is embedded */
258
+ this.clientVersion = 0;
259
+ this.active = false;
260
+ this.focused = false;
261
+
262
+ this._proxy = null;
263
+ this._mapped = false;
264
+ this._destroyed = false;
265
+ this._infoAtom = 0;
266
+ this._xembedAtom = 0;
267
+ // the newest server timestamp we have seen, which is what a message
268
+ // should carry: CurrentTime is not a timestamp and cannot arbitrate
269
+ this._time = 0;
270
+
271
+ this._onClientProperty = this._onClientProperty.bind(this);
272
+ this._onClientDestroy = this._onClientDestroy.bind(this);
273
+ this._onClientReparent = this._onClientReparent.bind(this);
274
+ this._onMessage = this._onMessage.bind(this);
275
+ this.window.on('message', this._onMessage);
276
+ }
277
+
278
+ /**
279
+ * Take over `clientId` — a top-level window belonging to another client —
280
+ * and put it inside this socket.
281
+ *
282
+ * Resolves once the client is reparented and, for a client that speaks the
283
+ * protocol, told about it; the `'embedded'` event carries the same
284
+ * information. Rejects if the window does not exist.
285
+ *
286
+ * @param {number} clientId
287
+ * @returns {Promise<{id: number, window: object, version: number, xembed: boolean}>}
288
+ */
289
+ async embed(clientId) {
290
+ if (this.client) {
291
+ throw new Error(
292
+ `xembed: this socket already holds window ${this.client.id}; call release() first, ` +
293
+ 'or use one socket per embedded client'
294
+ );
295
+ }
296
+ if (!clientId) throw new Error('xembed: embed() needs the client window id');
297
+
298
+ return this._attach(this.app.createWindow({ id: clientId }), true);
299
+ }
300
+
301
+ /**
302
+ * Take whatever turns up as a child of this socket as the client.
303
+ *
304
+ * `xterm -into WID` and `mpv --wid=WID` are handed a window id and put
305
+ * their own window inside it, so by the time you hear about them there is
306
+ * nothing left to reparent — but everything after that (sizing, the
307
+ * synthetic ConfigureNotify, the mapped state, `'gone'`) is the same
308
+ * embedding. Give the program `socket.window.id` and call this:
309
+ *
310
+ * const socket = new XEmbedSocket(pane, { width: 640, height: 480 });
311
+ * spawn('xterm', ['-into', String(socket.window.id)]);
312
+ * await socket.adopt();
313
+ *
314
+ * A child that is already there is taken immediately.
315
+ *
316
+ * @param {object} [options] `{ timeout }` in ms — without one this waits
317
+ * for as long as it takes, which is right for a program that is still
318
+ * starting up and wrong for one that has already failed to
319
+ * @returns {Promise<{id: number, window: object, version: number, xembed: boolean}>}
320
+ */
321
+ async adopt(options = {}) {
322
+ const { timeout = 0 } = options;
323
+ if (this.client) {
324
+ throw new Error(`xembed: this socket already holds window ${this.client.id}`);
325
+ }
326
+ if (this._ownsWindow) this.window.map();
327
+
328
+ const child = await new Promise((resolve, reject) => {
329
+ let timer = null;
330
+ let done = false;
331
+ const settle = (fn, value) => {
332
+ if (done) return;
333
+ done = true;
334
+ this.window.removeListener('create', look);
335
+ this.window.removeListener('reparent', look);
336
+ if (timer) clearTimeout(timer);
337
+ fn(value);
338
+ };
339
+ // Which event announces the window depends on how the program got it
340
+ // there: `mpv --wid` creates it as our child (CreateNotify), `xterm
341
+ // -into` creates a toplevel and reparents it in (ReparentNotify, which
342
+ // also fires for a window leaving us). Rather than tell those apart,
343
+ // ask the server what our children actually are.
344
+ const look = () => {
345
+ if (done) return;
346
+ this._firstChild().then((id) => {
347
+ if (id) settle(resolve, this.app.createWindow({ id }));
348
+ }, () => {});
349
+ };
350
+ // Listen first, then look: a program racing us would otherwise fall
351
+ // between the two, appearing after the QueryTree that found nothing and
352
+ // before the selection that would have reported it. `on(…)` selects
353
+ // SubstructureNotify, and requests on one connection are ordered, so by
354
+ // the time the QueryTree is answered the selection is in force.
355
+ this.window.on('create', look);
356
+ this.window.on('reparent', look);
357
+ look();
358
+ if (timeout) {
359
+ timer = setTimeout(() => {
360
+ settle(
361
+ reject,
362
+ new Error(
363
+ `xembed: nothing put a window inside ${this.window.id} within ${timeout}ms. ` +
364
+ 'Check that the program was given that id (xterm -into ID, mpv --wid=ID) ' +
365
+ 'and that it is talking to the same display.'
366
+ )
367
+ );
368
+ }, timeout);
369
+ timer.unref?.();
370
+ }
371
+ });
372
+ return this._attach(child, false);
373
+ }
374
+
375
+ /** the first child of the container that is not something we made */
376
+ _firstChild() {
377
+ return new Promise((resolve) => {
378
+ let asked = false;
379
+ safeRelease(this.X, () => {
380
+ asked = true;
381
+ this.X.QueryTree(this.window.id, (err, tree) => {
382
+ if (err) return resolve(0);
383
+ resolve(tree.children.find((id) => id !== this._proxy?.window.id) ?? 0);
384
+ });
385
+ });
386
+ if (!asked) resolve(0); // the connection is gone
387
+ });
388
+ }
389
+
390
+ async _attach(client, reparent) {
391
+ // PropertyChange for _XEMBED_INFO, StructureNotify for the lifecycle
392
+ // (destroy, and a reparent that takes the client away from us). This is
393
+ // also the round trip that says whether the window exists at all.
394
+ await client.selectInput(x11.eventMask.PropertyChange | x11.eventMask.StructureNotify);
395
+
396
+ const [info, infoAtom, xembedAtom] = await Promise.all([
397
+ readXEmbedInfo(client),
398
+ this.window.atom(INFO),
399
+ this.window.atom(NAME)
400
+ ]);
401
+ if (this._destroyed) return { id: client.id, window: client, version: 0, xembed: false };
402
+
403
+ this._infoAtom = infoAtom;
404
+ this._xembedAtom = xembedAtom;
405
+ this.client = client;
406
+ this.xembed = !!info;
407
+ // both sides speak the lower of the two versions
408
+ this.clientVersion = info ? Math.min(this.version, info.version) : 0;
409
+
410
+ client.on('property', this._onClientProperty);
411
+ client.on('destroy', this._onClientDestroy);
412
+ client.on('reparent', this._onClientReparent);
413
+
414
+ // the client must outlive us: the save-set puts it back under the root if
415
+ // this connection dies while it is our child, instead of taking it with us
416
+ client.addToSaveSet();
417
+ if (reparent) {
418
+ // a container nobody has mapped makes the client unviewable, however
419
+ // mapped the client itself is. One passed in as `{ window }` is the
420
+ // caller's to map.
421
+ if (this._ownsWindow) this.window.map();
422
+ client.reparentTo(this.window, 0, 0);
423
+ }
424
+ client.moveResize(0, 0, this.window.width, this.window.height);
425
+
426
+ if (this.xembed) {
427
+ // data1 is the window the client has been embedded into — the one it
428
+ // sends its own messages back to — and data2 the version in use
429
+ this._send(XEMBED.EMBEDDED_NOTIFY, 0, this.window.id, this.clientVersion);
430
+ }
431
+ // A client that speaks the protocol is mapped only when it asks to be; one
432
+ // that does not never will, so it is mapped now. A window waiting to be
433
+ // embedded is unmapped — that is what waiting looks like — and mapping an
434
+ // adopted one that already showed itself costs nothing.
435
+ this._setMapped(this.xembed ? info.mapped : true);
436
+ await this._syncConfigure();
437
+
438
+ const embedded = {
439
+ id: client.id,
440
+ window: client,
441
+ version: this.clientVersion,
442
+ xembed: this.xembed
443
+ };
444
+ this.emit('embedded', embedded);
445
+ return embedded;
446
+ }
447
+
448
+ /**
449
+ * Move and resize the container, and the client with it.
450
+ *
451
+ * The client also gets a synthetic ConfigureNotify with root-relative
452
+ * coordinates: its own says where it is inside our window, which is not the
453
+ * question a client asks (ICCCM 4.1.5).
454
+ *
455
+ * @param {object} [geometry] `{ x, y, width, height }`; anything omitted
456
+ * stays as it is
457
+ * @returns {Promise<XEmbedSocket>}
458
+ */
459
+ async resize(geometry = {}) {
460
+ const win = this.window;
461
+ const { x = win.x, y = win.y, width = win.width, height = win.height } = geometry;
462
+ if (this._ownsWindow) {
463
+ win.moveResize(x, y, width, height);
464
+ win.x = x;
465
+ win.y = y;
466
+ win.width = width;
467
+ win.height = height;
468
+ }
469
+ if (this.client && !this.client._destroyed) {
470
+ this.client.moveResize(0, 0, width, height);
471
+ await this._syncConfigure(width, height);
472
+ }
473
+ return this;
474
+ }
475
+
476
+ /**
477
+ * Tell the client whether the toplevel it lives in is the active window
478
+ * (`XEMBED_WINDOW_ACTIVATE` / `XEMBED_WINDOW_DEACTIVATE`). A client starts
479
+ * out inactive, so this only has to be called when that changes.
480
+ *
481
+ * @param {boolean} [active]
482
+ * @param {object} [options] `{ time }` — the server timestamp of the event
483
+ * that caused this
484
+ */
485
+ activate(active = true, options = {}) {
486
+ if (this.active === !!active) return this;
487
+ this.active = !!active;
488
+ this._send(active ? XEMBED.WINDOW_ACTIVATE : XEMBED.WINDOW_DEACTIVATE, 0, 0, 0, options);
489
+ return this;
490
+ }
491
+
492
+ /** `activate(false)`. */
493
+ deactivate(options = {}) {
494
+ return this.activate(false, options);
495
+ }
496
+
497
+ /**
498
+ * Give the client the logical focus (`XEMBED_FOCUS_IN`) and start routing
499
+ * keystrokes into it.
500
+ *
501
+ * @param {number} [detail] `XEMBED.FOCUS_CURRENT` (a click: focus the client
502
+ * without disturbing which of its widgets is focused),
503
+ * `XEMBED.FOCUS_FIRST` or `XEMBED.FOCUS_LAST` (a Tab or back-Tab arriving
504
+ * from outside)
505
+ * @param {object} [options] `{ time }`
506
+ */
507
+ focusIn(detail = XEMBED.FOCUS_CURRENT, options = {}) {
508
+ if (!this.client) return this;
509
+ this.focused = true;
510
+ this._send(XEMBED.FOCUS_IN, detail, 0, 0, options);
511
+ this._ensureProxy().take(this.client.id);
512
+ return this;
513
+ }
514
+
515
+ /** Take the logical focus away again (`XEMBED_FOCUS_OUT`). */
516
+ focusOut(options = {}) {
517
+ if (!this.focused) return this;
518
+ this.focused = false;
519
+ this._proxy?.release();
520
+ this._send(XEMBED.FOCUS_OUT, 0, 0, 0, options);
521
+ return this;
522
+ }
523
+
524
+ /**
525
+ * Turn a modality shield on or off (`XEMBED_MODALITY_ON` / `_OFF`): the
526
+ * client should stop responding to input because the embedder has put up a
527
+ * modal dialog somewhere else.
528
+ */
529
+ modality(on = true, options = {}) {
530
+ this._send(on ? XEMBED.MODALITY_ON : XEMBED.MODALITY_OFF, 0, 0, 0, options);
531
+ return this;
532
+ }
533
+
534
+ /**
535
+ * Send a raw `_XEMBED` message. Everything above is a wrapper over this;
536
+ * it is here for the opcodes ntk does not model (the accelerator ones).
537
+ */
538
+ send(opcode, detail = 0, data1 = 0, data2 = 0, options = {}) {
539
+ this._send(opcode, detail, data1, data2, options);
540
+ return this;
541
+ }
542
+
543
+ /**
544
+ * Hand the client back: reparent it to the root, drop it from our save-set
545
+ * and stop watching it. It becomes an ordinary top-level window again, at
546
+ * the position it occupied on screen, which is what "unplugging" means for
547
+ * `xterm -into` and for GtkSocket alike.
548
+ *
549
+ * @returns {Promise<XEmbedSocket>}
550
+ */
551
+ async release() {
552
+ const client = this.client;
553
+ if (!client) return this;
554
+ this._detach();
555
+ if (client._destroyed) return this;
556
+
557
+ const { x, y } = await this._rootCoords();
558
+ // stop selecting events on a window that is no longer ours before the
559
+ // reparent, so its ReparentNotify does not come back to us
560
+ safeRelease(this.X, () => {
561
+ client.eventMask = 0;
562
+ this.X.ChangeWindowAttributes(client.id, { eventMask: 0 }, () => {});
563
+ });
564
+ client.reparentTo(this.app.rootWindow(), x, y);
565
+ client.removeFromSaveSet();
566
+ return this;
567
+ }
568
+
569
+ /**
570
+ * Release the client, then drop the container window and the focus proxy.
571
+ * A container passed in as `{ window }` belongs to the caller and is left
572
+ * alone.
573
+ */
574
+ async destroy() {
575
+ if (this._destroyed) return this;
576
+ this._destroyed = true;
577
+ await this.release();
578
+ this.window.removeListener('message', this._onMessage);
579
+ this._proxy?.destroy();
580
+ this._proxy = null;
581
+ if (this._ownsWindow) this.window.destroy();
582
+ return this;
583
+ }
584
+
585
+ // -------------------------------------------------------------------
586
+
587
+ _ensureProxy() {
588
+ if (!this._proxy) this._proxy = new FocusProxy(this.window);
589
+ return this._proxy;
590
+ }
591
+
592
+ _send(opcode, detail = 0, data1 = 0, data2 = 0, options = {}) {
593
+ if (!this.client || this.client._destroyed) return;
594
+ const { time = this._time } = options;
595
+ this.client
596
+ .sendClientMessage(this._xembedAtom || NAME, [time, opcode, detail, data1, data2])
597
+ .catch((err) => this.app.options?.onXError?.(err));
598
+ }
599
+
600
+ _setMapped(mapped) {
601
+ if (this._mapped === !!mapped) return;
602
+ this._mapped = !!mapped;
603
+ if (!this.client || this.client._destroyed) return;
604
+ if (this._mapped) this.client.map();
605
+ else this.client.unmap();
606
+ this.emit('mappedChange', this._mapped);
607
+ }
608
+
609
+ /** where the container's origin is on the root window */
610
+ async _rootCoords() {
611
+ const root = this.app.display.screen[0].root;
612
+ return new Promise((resolve) => {
613
+ safeRelease(this.X, () =>
614
+ this.X.TranslateCoordinates(this.window.id, root, 0, 0, (err, res) =>
615
+ resolve(err ? { x: 0, y: 0 } : { x: res.destX, y: res.destY })
616
+ )
617
+ );
618
+ });
619
+ }
620
+
621
+ async _syncConfigure(width = this.window.width, height = this.window.height) {
622
+ const client = this.client;
623
+ if (!client || client._destroyed) return;
624
+ const { x, y } = await this._rootCoords();
625
+ if (client._destroyed) return;
626
+ client.sendConfigureNotify({ x, y, width, height });
627
+ }
628
+
629
+ _onMessage(ev) {
630
+ if (!this._xembedAtom || ev.message_type !== this._xembedAtom) return;
631
+ const [time, opcode, detail, data1, data2] = ev.data;
632
+ if (time) this._time = time >>> 0;
633
+ switch (opcode) {
634
+ case XEMBED.REQUEST_FOCUS:
635
+ this.emit('requestFocus');
636
+ // the client saying the user clicked it. An embedder with its own
637
+ // focus manager turns this off and calls focusIn() itself.
638
+ if (this._focusOnRequest) this.focusIn(XEMBED.FOCUS_CURRENT);
639
+ break;
640
+ case XEMBED.FOCUS_NEXT:
641
+ this.emit('focusNext');
642
+ break;
643
+ case XEMBED.FOCUS_PREV:
644
+ this.emit('focusPrev');
645
+ break;
646
+ default:
647
+ this.emit('message', {
648
+ opcode,
649
+ name: XEMBED_OPCODE_NAMES[opcode],
650
+ detail,
651
+ data1,
652
+ data2,
653
+ time
654
+ });
655
+ }
656
+ }
657
+
658
+ _onClientProperty(ev) {
659
+ if (ev.atom !== this._infoAtom) return;
660
+ if (ev.time) this._time = ev.time >>> 0;
661
+ // state 1 is Delete. The spec has no meaning for a client withdrawing the
662
+ // property mid-embedding, so the mapped state it last asked for stands.
663
+ if (ev.state === 1) return;
664
+ readXEmbedInfo(this.client).then(
665
+ (info) => {
666
+ if (info) this._setMapped(info.mapped);
667
+ },
668
+ () => {}
669
+ );
670
+ }
671
+
672
+ _onClientDestroy() {
673
+ this._detach();
674
+ this.emit('gone');
675
+ }
676
+
677
+ _onClientReparent(ev) {
678
+ // ours is the reparent *into* the socket; anything else is the client
679
+ // being taken away from us
680
+ if (ev.parent === this.window.id) return;
681
+ this._detach();
682
+ this.emit('gone');
683
+ }
684
+
685
+ /** forget the client without touching it — it is gone, or no longer ours */
686
+ _detach() {
687
+ const client = this.client;
688
+ if (!client) return;
689
+ client.removeListener('property', this._onClientProperty);
690
+ client.removeListener('destroy', this._onClientDestroy);
691
+ client.removeListener('reparent', this._onClientReparent);
692
+ this.client = null;
693
+ this.xembed = false;
694
+ this.clientVersion = 0;
695
+ this.active = false;
696
+ this.focused = false;
697
+ this._mapped = false;
698
+ this._proxy?.release();
699
+ }
700
+ }
701
+
702
+ /**
703
+ * The client half: a top-level window that offers itself to be embedded.
704
+ *
705
+ * const plug = new XEmbedPlug(app, { width: 300, height: 200 });
706
+ * await plug.ready;
707
+ * console.log(plug.window.id); // hand this to the embedder
708
+ *
709
+ * The window is deliberately left unmapped — an embedder maps it, once it has
710
+ * reparented it and read `XEMBED_MAPPED` out of `_XEMBED_INFO`. Draw into
711
+ * `plug.window` as usual.
712
+ *
713
+ * Events:
714
+ * 'embedded' `(embedderId)` — reparented into someone else's window
715
+ * 'activate' / 'deactivate' — the toplevel we are inside became (in)active
716
+ * 'focusIn' `(detail)` — one of `XEMBED.FOCUS_CURRENT/FIRST/LAST`
717
+ * 'focusOut'
718
+ * 'modality' `(on)`
719
+ * 'message' `{ opcode, name, detail, data1, data2, time }` — anything
720
+ * this class does not model
721
+ * 'released' reparented back to the root: the protocol is over
722
+ */
723
+ export class XEmbedPlug extends EventEmitter {
724
+ /**
725
+ * @param {import('./app.js').default} app
726
+ * @param {object} [options] `{ version, mapped }` — the two words of
727
+ * `_XEMBED_INFO` — plus `{ window }` to adopt a window instead of
728
+ * creating one. Anything else is passed to `app.createWindow()`.
729
+ */
730
+ constructor(app, options = {}) {
731
+ super();
732
+ const { version = XEMBED.VERSION, mapped = true, window = null, ...windowArgs } = options;
733
+
734
+ this.app = app;
735
+ this.X = app.X;
736
+ this.version = version >>> 0;
737
+ /** the window to draw into; its id is what an embedder needs */
738
+ this.window = window ?? app.createWindow(windowArgs);
739
+ this._ownsWindow = !window;
740
+
741
+ /** the embedder's window id, or 0 while free-standing */
742
+ this.embedder = 0;
743
+ this._embedderWindow = null;
744
+ /** whether the embedder speaks the message protocol */
745
+ this.xembed = false;
746
+ this.active = false;
747
+ this.focused = false;
748
+ this.modal = false;
749
+ this._mapped = !!mapped;
750
+ this._embedded = false;
751
+ this._xembedAtom = 0;
752
+ this._destroyed = false;
753
+
754
+ this._onMessage = this._onMessage.bind(this);
755
+ this._onReparent = this._onReparent.bind(this);
756
+ this.window.on('message', this._onMessage);
757
+ this.window.on('reparent', this._onReparent);
758
+
759
+ /**
760
+ * Resolves once `_XEMBED_INFO` is on the window — which is what has to be
761
+ * true before its id is given to anyone.
762
+ * @type {Promise<XEmbedPlug>}
763
+ */
764
+ this.ready = this._publish().then(() => this);
765
+ }
766
+
767
+ /** The `XEMBED_MAPPED` bit: whether the embedder should map us. */
768
+ get mapped() {
769
+ return this._mapped;
770
+ }
771
+
772
+ /**
773
+ * Ask the embedder to map or unmap us. An embedder may leave us unmapped
774
+ * while the bit is set (a tab that is not the current one), but must unmap
775
+ * promptly when it clears.
776
+ * @returns {Promise<XEmbedPlug>}
777
+ */
778
+ async setMapped(mapped) {
779
+ this._mapped = !!mapped;
780
+ await this._publish();
781
+ return this;
782
+ }
783
+
784
+ /**
785
+ * Ask for the logical focus (`XEMBED_REQUEST_FOCUS`) — what a client sends
786
+ * when the user clicks it. The embedder answers with `XEMBED_FOCUS_IN`, or
787
+ * does not.
788
+ */
789
+ requestFocus() {
790
+ return this._send(XEMBED.REQUEST_FOCUS);
791
+ }
792
+
793
+ /**
794
+ * Hand the focus back to the embedder because we ran off the end of our own
795
+ * tab chain: it moves on to its next (or previous) widget.
796
+ */
797
+ focusNext() {
798
+ return this._send(XEMBED.FOCUS_NEXT);
799
+ }
800
+
801
+ /** @see focusNext */
802
+ focusPrev() {
803
+ return this._send(XEMBED.FOCUS_PREV);
804
+ }
805
+
806
+ /** Send a raw `_XEMBED` message to the embedder. */
807
+ send(opcode, detail = 0, data1 = 0, data2 = 0, options = {}) {
808
+ return this._send(opcode, detail, data1, data2, options);
809
+ }
810
+
811
+ /** Stop listening, and destroy the window if this created it. */
812
+ destroy() {
813
+ if (this._destroyed) return this;
814
+ this._destroyed = true;
815
+ this.window.removeListener('message', this._onMessage);
816
+ this.window.removeListener('reparent', this._onReparent);
817
+ if (this._ownsWindow) this.window.destroy();
818
+ return this;
819
+ }
820
+
821
+ // -------------------------------------------------------------------
822
+
823
+ async _publish() {
824
+ if (!this._xembedAtom) this._xembedAtom = await this.window.atom(NAME);
825
+ await this.window.setProperty(
826
+ INFO,
827
+ encodeXEmbedInfo({ version: this.version, mapped: this._mapped }),
828
+ // the property's type is its own name — that is what the spec says it
829
+ // is, not CARDINAL
830
+ { type: INFO, format: 32 }
831
+ );
832
+ return this;
833
+ }
834
+
835
+ _send(opcode, detail = 0, data1 = 0, data2 = 0, options = {}) {
836
+ if (!this.embedder) return this;
837
+ const { time = 0 } = options;
838
+ // A client's messages go to the embedder's window, and name it: in both
839
+ // directions the message is about the window it is delivered to, so mask
840
+ // 0 hands it to that window's owner whatever it selected.
841
+ if (this._embedderWindow?.id !== this.embedder) {
842
+ this._embedderWindow = this.app.createWindow({ id: this.embedder });
843
+ }
844
+ this._embedderWindow
845
+ .sendClientMessage(this._xembedAtom || NAME, [time, opcode, detail, data1, data2])
846
+ .catch((err) => this.app.options?.onXError?.(err));
847
+ return this;
848
+ }
849
+
850
+ _onReparent(ev) {
851
+ const root = this.app.display.screen[0].root;
852
+ if (ev.parent === root) {
853
+ if (!this._embedded) return;
854
+ // spec step 5: back under the root ends the protocol
855
+ this._embedded = false;
856
+ this.embedder = 0;
857
+ this._embedderWindow = null;
858
+ this.xembed = false;
859
+ this.active = this.focused = this.modal = false;
860
+ this.emit('released');
861
+ return;
862
+ }
863
+ this.embedder = ev.parent;
864
+ if (this._embedded) return;
865
+ this._embedded = true;
866
+ // An XEmbed-aware embedder sends XEMBED_EMBEDDED_NOTIFY straight after
867
+ // the reparent, so `xembed` and `version` are worth reading on the next
868
+ // turn rather than inside this handler; an embedder that only reparents
869
+ // (xterm -into, mpv --wid) sends nothing and leaves them as they are.
870
+ this.emit('embedded', this.embedder);
871
+ }
872
+
873
+ _onMessage(ev) {
874
+ if (!this._xembedAtom || ev.message_type !== this._xembedAtom) return;
875
+ const [time, opcode, detail, data1, data2] = ev.data;
876
+ switch (opcode) {
877
+ case XEMBED.EMBEDDED_NOTIFY:
878
+ this.xembed = true;
879
+ // data1 is the embedder's window; older embedders leave it 0 and the
880
+ // ReparentNotify is then the only source
881
+ if (data1) this.embedder = data1 >>> 0;
882
+ this.version = Math.min(this.version, data2 >>> 0);
883
+ if (!this._embedded && this.embedder) {
884
+ this._embedded = true;
885
+ this.emit('embedded', this.embedder);
886
+ }
887
+ break;
888
+ case XEMBED.WINDOW_ACTIVATE:
889
+ this.active = true;
890
+ this.emit('activate');
891
+ break;
892
+ case XEMBED.WINDOW_DEACTIVATE:
893
+ this.active = false;
894
+ this.emit('deactivate');
895
+ break;
896
+ case XEMBED.FOCUS_IN:
897
+ this.focused = true;
898
+ this.emit('focusIn', detail);
899
+ break;
900
+ case XEMBED.FOCUS_OUT:
901
+ this.focused = false;
902
+ this.emit('focusOut');
903
+ break;
904
+ case XEMBED.MODALITY_ON:
905
+ case XEMBED.MODALITY_OFF:
906
+ this.modal = opcode === XEMBED.MODALITY_ON;
907
+ this.emit('modality', this.modal);
908
+ break;
909
+ default:
910
+ this.emit('message', {
911
+ opcode,
912
+ name: XEMBED_OPCODE_NAMES[opcode],
913
+ detail,
914
+ data1,
915
+ data2,
916
+ time
917
+ });
918
+ }
919
+ }
920
+ }
921
+
922
+ export default { XEMBED, XEmbedSocket, XEmbedPlug, FocusProxy };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "7.3.2",
3
+ "version": "7.4.0",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",
@@ -24,7 +24,8 @@
24
24
  "type": "module",
25
25
  "main": "./lib/index.js",
26
26
  "exports": {
27
- ".": "./lib/index.js"
27
+ ".": "./lib/index.js",
28
+ "./xembed": "./lib/xembed.js"
28
29
  },
29
30
  "files": [
30
31
  "lib"