ntk 3.7.2 → 3.9.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/app.js CHANGED
@@ -81,8 +81,41 @@ export default class App {
81
81
  return chooseGLXConfig(this, spec);
82
82
  }
83
83
 
84
- rootWindow() {
85
- return new Window(this, { id: this.display.screen[0].root });
84
+ rootWindow(screen = 0) {
85
+ return new Window(this, { id: this.display.screen[screen].root });
86
+ }
87
+
88
+ /**
89
+ * Release a pointer or keyboard frozen by a synchronous grab (X
90
+ * AllowEvents). A window manager grabs buttons on client windows with
91
+ * `pointerMode: 0` so it sees the press first; this decides what happens
92
+ * to it:
93
+ *
94
+ * 'replay' — hand the event back to the client as if we never saw it
95
+ * (click-to-focus: raise the window, let the click through)
96
+ * 'async' — thaw, keeping the event for ourselves
97
+ * 'sync' — thaw for one more event, then freeze again
98
+ *
99
+ * The keyboard and both-devices variants carry their X names.
100
+ *
101
+ * @param {string|number} [mode] one of the names above, or a raw X mode
102
+ * @param {number} [time] CurrentTime by default
103
+ */
104
+ allowEvents(mode = 'replay', time = 0) {
105
+ const MODES = {
106
+ async: 0, // AsyncPointer
107
+ sync: 1, // SyncPointer
108
+ replay: 2, // ReplayPointer
109
+ async_keyboard: 3,
110
+ sync_keyboard: 4,
111
+ replay_keyboard: 5,
112
+ async_both: 6,
113
+ sync_both: 7
114
+ };
115
+ const value = typeof mode === 'number' ? mode : MODES[mode];
116
+ if (value === undefined) throw new Error(`ntk: unknown AllowEvents mode '${mode}'`);
117
+ this.X.AllowEvents(value, time);
118
+ return this;
86
119
  }
87
120
 
88
121
  createPixmap(args) {
package/lib/events_map.js CHANGED
@@ -14,6 +14,9 @@ eventName[8] = 'mouseout';
14
14
  eventName[9] = 'focus';
15
15
  eventName[10] = 'blur';
16
16
  eventName[12] = 'expose';
17
+ // CreateNotify: a child window appeared under a parent we watch. Carries a
18
+ // `parent` field, so it arrives as a child-event like the *_request ones.
19
+ eventName[16] = 'create';
17
20
  eventName[17] = 'destroy';
18
21
  eventName[18] = 'unmap';
19
22
  eventName[19] = 'map';
@@ -21,7 +24,7 @@ eventName[20] = 'map_request';
21
24
  eventName[21] = 'reparent';
22
25
  eventName[22] = 'resize';
23
26
  eventName[23] = 'configure_request';
24
- // 25 - gravity
27
+ eventName[24] = 'gravity';
25
28
  eventName[25] = 'resize_request';
26
29
  eventName[26] = 'circulate';
27
30
  eventName[27] = 'circulate_request';
@@ -49,6 +52,15 @@ export const mask = {
49
52
  expose: x11.eventMask.Exposure,
50
53
  map_request: x11.eventMask.SubstructureRedirect,
51
54
  configure_request: x11.eventMask.SubstructureRedirect,
55
+ circulate_request: x11.eventMask.SubstructureRedirect,
56
+ // a child window appeared under a window we watch — the one
57
+ // SubstructureNotify event with its own name, because the rest
58
+ // (map/unmap/destroy/resize/reparent of a child) are the same event types
59
+ // a window gets about itself and are emitted on the child's own wrapper
60
+ create: x11.eventMask.SubstructureNotify,
61
+ gravity: x11.eventMask.StructureNotify,
62
+ circulate: x11.eventMask.StructureNotify,
63
+ resize_request: x11.eventMask.ResizeRedirect,
52
64
  destroy: x11.eventMask.StructureNotify,
53
65
  keyup: x11.eventMask.KeyRelease,
54
66
  property: x11.eventMask.PropertyChange,
@@ -85,6 +97,8 @@ export const toSnake = {
85
97
  onExpose: 'expose',
86
98
  onMapRequest: 'map_request',
87
99
  onConfigureRequest: 'configure_request',
100
+ onCirculateRequest: 'circulate_request',
101
+ onCreate: 'create',
88
102
  onDestroy: 'destroy',
89
103
  onKeyUp: 'keyup',
90
104
  onPropertyChange: 'property',
package/lib/window.js CHANGED
@@ -33,14 +33,49 @@ const forwardedXAttributes = [
33
33
  'cursor'
34
34
  ];
35
35
 
36
+ /**
37
+ * Turn a GetProperty reply into the shape the caller asked for. X property
38
+ * types are atoms, so the only distinction we can make without a round trip
39
+ * is against the predefined ones: STRING is latin-1 by definition (ICCCM),
40
+ * and everything else carrying text — UTF8_STRING above all — is UTF-8.
41
+ */
42
+ function decodeProperty(prop, as, X) {
43
+ if (as === 'string') {
44
+ const encoding = prop.type === X.atoms.STRING ? 'latin1' : 'utf8';
45
+ // these properties are conventionally NUL-terminated, sometimes twice
46
+ return prop.data.toString(encoding).replace(/\0+$/, '');
47
+ }
48
+ if (as === 'numbers') {
49
+ const out = [];
50
+ for (let i = 0; i + 4 <= prop.data.length; i += 4) out.push(prop.data.readUInt32LE(i));
51
+ return out;
52
+ }
53
+ return { type: prop.type, data: prop.data };
54
+ }
55
+
36
56
  export default class Window extends Drawable {
37
57
  // one wrapper per X window id and connection: constructing a Window for a
38
- // known id returns the cached instance
39
- static cache = new Map();
58
+ // known id returns the cached instance. Keyed by connection as well as
59
+ // id, because ids are only unique within a server, not within a process —
60
+ // two connections to the same display see the same root window, and a
61
+ // window manager sees every other client's ids.
62
+ static cache = new WeakMap();
63
+
64
+ static _cacheFor(app) {
65
+ let byId = Window.cache.get(app);
66
+ if (!byId) Window.cache.set(app, (byId = new Map()));
67
+ return byId;
68
+ }
40
69
 
41
70
  constructor(app, args = {}) {
71
+ // 0 is X None, not a window. Without this it fails the truthiness test
72
+ // below and falls through to the create path, silently making a real
73
+ // 800x800 window out of what was almost certainly a missing id.
74
+ if (args.id === 0) {
75
+ throw new Error('ntk: 0 (None) is not a window id');
76
+ }
42
77
  if (args.id) {
43
- const cached = Window.cache.get(args.id);
78
+ const cached = Window._cacheFor(app).get(args.id);
44
79
  // TODO: check event mask, if more events in args - update it
45
80
  if (cached) return cached;
46
81
  }
@@ -151,7 +186,7 @@ export default class Window extends Drawable {
151
186
  });
152
187
  }
153
188
 
154
- Window.cache.set(this.id, this);
189
+ Window._cacheFor(app).set(this.id, this);
155
190
 
156
191
  if (args.title) {
157
192
  this.setTitle(args.title);
@@ -218,14 +253,19 @@ export default class Window extends Drawable {
218
253
  if (this._dirty) this._present();
219
254
  });
220
255
 
256
+ // Events about a *child* of this window: the substructure requests a
257
+ // window manager lives on (map_request, configure_request) plus
258
+ // create. The whole X event is carried through — a ConfigureRequest
259
+ // without its geometry and value mask says only "someone wants
260
+ // something", which is not enough to answer it — with `parent` and
261
+ // `window` upgraded from raw ids to Window objects.
221
262
  this.on('child-event', (ev) => {
222
263
  const eventName = xevents.eventName[ev.type];
223
- const ntkev = {
224
- parent: this,
225
- window: new Window(app, { id: ev.wid })
226
- };
264
+ if (!eventName) return;
265
+ const child = new Window(app, { id: ev.wid });
266
+ const ntkev = { ...ev, parent: this, window: child, target: child };
227
267
  // wait until we know that we track correct x,y,w,h values
228
- ntkev.window._readyPromise.then(() => {
268
+ child._readyPromise.then(() => {
229
269
  this.emit(eventName, ntkev);
230
270
  });
231
271
  });
@@ -236,7 +276,14 @@ export default class Window extends Drawable {
236
276
  if (!eventMask) return;
237
277
  if ((eventMask & this.eventMask) === 0) {
238
278
  this.eventMask |= eventMask;
239
- X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, () => {});
279
+ // the selection can legitimately fail — SubstructureRedirect is
280
+ // one-client-only, so `on('map_request')` is how you find out
281
+ // another window manager owns this window. Route it to the app's
282
+ // error hook rather than dropping it; selectInput() is the
283
+ // explicit form that hands the error straight back.
284
+ X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, (err) => {
285
+ if (err) this.app.options.onXError?.(err);
286
+ });
240
287
  }
241
288
  });
242
289
 
@@ -257,7 +304,7 @@ export default class Window extends Drawable {
257
304
  }
258
305
 
259
306
  _forget() {
260
- Window.cache.delete(this.id);
307
+ Window._cacheFor(this.app).delete(this.id);
261
308
  delete this.X.event_consumers[this.id];
262
309
  }
263
310
 
@@ -997,6 +1044,308 @@ export default class Window extends Drawable {
997
1044
 
998
1045
  reparentTo(newParent, x, y) {
999
1046
  this.X.ReparentWindow(this.id, newParent.id, x, y);
1047
+ return this;
1048
+ }
1049
+
1050
+ /** Move to the top of the stacking order among its siblings. */
1051
+ raise() {
1052
+ safeRelease(this.X, () => this.X.RaiseWindow(this.id));
1053
+ return this;
1054
+ }
1055
+
1056
+ /** Move to the bottom of the stacking order among its siblings. */
1057
+ lower() {
1058
+ safeRelease(this.X, () => this.X.ConfigureWindow(this.id, { stackMode: 1 }));
1059
+ return this;
1060
+ }
1061
+
1062
+ // ---------------------------------------------------------------------
1063
+ // Reading properties
1064
+ //
1065
+ // ntk could write window properties (setTitle, setSizeHints, setClass)
1066
+ // but never read them, which is fine for a client — it knows what it
1067
+ // wrote — and useless for a window manager, whose whole job is to act on
1068
+ // what *other* clients declare about themselves.
1069
+ // ---------------------------------------------------------------------
1070
+
1071
+ /**
1072
+ * Read a property. `name` is an atom name ('WM_NAME', '_NET_WM_STATE');
1073
+ * interned atoms are cached per connection, so repeat reads cost no
1074
+ * extra round trip.
1075
+ *
1076
+ * Resolves to `null` when the property is not set, otherwise to the
1077
+ * decoded value chosen by `as`:
1078
+ * 'buffer' (default) — `{ type, data }` with the raw bytes
1079
+ * 'string' — text, UTF-8 or latin-1 by the property's type
1080
+ * 'numbers' — an array of 32-bit values (atom lists, cardinals)
1081
+ *
1082
+ * @param {string} name
1083
+ * @param {object} [options] `{ as, type, length }` — `type` restricts the
1084
+ * read to one property type (0, the default, accepts any), `length` is
1085
+ * the cap in 32-bit units.
1086
+ */
1087
+ async getProperty(name, options = {}) {
1088
+ const { as = 'buffer', type = 0, length = 0x1fffffff } = options;
1089
+ const atom = await this.atom(name);
1090
+ return new Promise((resolve, reject) => {
1091
+ safeRelease(this.X, () =>
1092
+ this.X.GetProperty(0, this.id, atom, type, 0, length, (err, prop) => {
1093
+ if (err) return reject(err);
1094
+ // an unset property answers with type None and no bytes
1095
+ if (!prop || !prop.type || !prop.data.length) return resolve(null);
1096
+ resolve(decodeProperty(prop, as, this.X));
1097
+ })
1098
+ );
1099
+ });
1100
+ }
1101
+
1102
+ /**
1103
+ * Write a property — the counterpart of getProperty, and the general form
1104
+ * of setTitle/setClass/setSizeHints. Strings are written as UTF8_STRING,
1105
+ * arrays of numbers as 32-bit lists; `type` names the property type atom
1106
+ * ('ATOM', 'CARDINAL', 'WINDOW', 'UTF8_STRING', 'STRING'), which is what
1107
+ * EWMH properties are picky about.
1108
+ *
1109
+ * wnd.setProperty('_NET_WM_NAME', 'a title');
1110
+ * wnd.setProperty('_NET_CLIENT_LIST', ids, { type: 'WINDOW' });
1111
+ *
1112
+ * @param {string} name atom name of the property
1113
+ * @param {string|number[]|Buffer} value
1114
+ * @param {object} [options] `{ type, format }`
1115
+ */
1116
+ async setProperty(name, value, options = {}) {
1117
+ const isList = Array.isArray(value);
1118
+ const { type = isList ? 'CARDINAL' : 'UTF8_STRING', format = isList ? 32 : 8 } =
1119
+ options;
1120
+ const [property, typeAtom] = await Promise.all([this.atom(name), this.atom(type)]);
1121
+ let data = value;
1122
+ if (isList) {
1123
+ const words = new Uint32Array(value);
1124
+ data = Buffer.from(words.buffer, words.byteOffset, words.byteLength);
1125
+ } else if (typeof value === 'string') {
1126
+ data = Buffer.from(value, type === 'STRING' ? 'latin1' : 'utf8');
1127
+ }
1128
+ if (this._destroyed) return this;
1129
+ safeRelease(this.X, () => {
1130
+ this.X.ChangeProperty(0, this.id, property, typeAtom, format, data);
1131
+ });
1132
+ return this;
1133
+ }
1134
+
1135
+ /**
1136
+ * Intern an atom by name. node-x11 caches them per connection, so asking
1137
+ * again for one already interned costs no round trip.
1138
+ * @returns {Promise<number>} the atom id
1139
+ */
1140
+ atom(name) {
1141
+ return new Promise((resolve, reject) => {
1142
+ this.X.InternAtom(false, name, (err, id) => (err ? reject(err) : resolve(id)));
1143
+ });
1144
+ }
1145
+
1146
+ /**
1147
+ * The window's title as the ICCCM/EWMH pair defines it: _NET_WM_NAME
1148
+ * (UTF-8) if the client set one, else WM_NAME. Resolves to null when the
1149
+ * window has neither. The read counterpart of setTitle.
1150
+ */
1151
+ async getTitle() {
1152
+ const netName = await this.getProperty('_NET_WM_NAME', { as: 'string' }).catch(() => null);
1153
+ if (netName) return netName;
1154
+ return this.getProperty('WM_NAME', { as: 'string' }).catch(() => null);
1155
+ }
1156
+
1157
+ /**
1158
+ * WM_NORMAL_HINTS as an object shaped like setSizeHints' argument —
1159
+ * `{ minWidth, minHeight, maxWidth, maxHeight, widthInc, heightInc,
1160
+ * baseWidth, baseHeight, minAspect, maxAspect, gravity }`, each present
1161
+ * only if the client set its flag. Resolves to `{}` when the window has
1162
+ * no hints, so callers can destructure without a null check.
1163
+ */
1164
+ async getSizeHints() {
1165
+ const v = await this.getProperty('WM_NORMAL_HINTS', { as: 'numbers' }).catch(() => null);
1166
+ if (!v || v.length < 18) return {};
1167
+ const flags = v[0];
1168
+ const hints = {};
1169
+ if (flags & 16) {
1170
+ hints.minWidth = v[5];
1171
+ hints.minHeight = v[6];
1172
+ }
1173
+ if (flags & 32) {
1174
+ hints.maxWidth = v[7];
1175
+ hints.maxHeight = v[8];
1176
+ }
1177
+ if (flags & 64) {
1178
+ hints.widthInc = v[9];
1179
+ hints.heightInc = v[10];
1180
+ }
1181
+ if (flags & 128) {
1182
+ hints.minAspect = [v[11], v[12]];
1183
+ hints.maxAspect = [v[13], v[14]];
1184
+ }
1185
+ if (flags & 256) {
1186
+ hints.baseWidth = v[15];
1187
+ hints.baseHeight = v[16];
1188
+ }
1189
+ if (flags & 512) hints.gravity = v[17];
1190
+ return hints;
1191
+ }
1192
+
1193
+ /**
1194
+ * GetWindowAttributes — `{ mapState, overrideRedirect, ... }`. A window
1195
+ * manager adopting the windows that already existed when it started
1196
+ * needs both: override-redirect windows are none of its business, and
1197
+ * only mapped ones should get a frame.
1198
+ */
1199
+ getAttributes() {
1200
+ return new Promise((resolve, reject) => {
1201
+ safeRelease(this.X, () =>
1202
+ this.X.GetWindowAttributes(this.id, (err, attrs) => (err ? reject(err) : resolve(attrs)))
1203
+ );
1204
+ });
1205
+ }
1206
+
1207
+ // ---------------------------------------------------------------------
1208
+ // Managing other clients' windows
1209
+ // ---------------------------------------------------------------------
1210
+
1211
+ /**
1212
+ * Select an event mask explicitly, reporting failure. Handler-driven
1213
+ * selection (`on('map_request')`, the onXxx constructor args) covers the
1214
+ * ordinary case; this exists for the mask that can be refused —
1215
+ * SubstructureRedirect belongs to one client at a time, so
1216
+ *
1217
+ * await app.rootWindow().selectInput(SubstructureRedirect | SubstructureNotify)
1218
+ *
1219
+ * either makes you the window manager or rejects with BadAccess because
1220
+ * something else already is. The mask is OR-ed into whatever handlers
1221
+ * have already asked for.
1222
+ */
1223
+ selectInput(mask) {
1224
+ this.eventMask |= mask;
1225
+ return new Promise((resolve, reject) => {
1226
+ this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, (err) =>
1227
+ err ? reject(err) : resolve(this)
1228
+ );
1229
+ });
1230
+ }
1231
+
1232
+ /**
1233
+ * Add this window to our save-set (X ChangeSaveSet). A window manager
1234
+ * reparents clients into frames it owns; without the save-set, the
1235
+ * clients would be destroyed along with those frames if the WM exits.
1236
+ * With it the server reparents them back to the root and remaps them.
1237
+ */
1238
+ addToSaveSet() {
1239
+ safeRelease(this.X, () => this.X.ChangeSaveSet(1, this.id));
1240
+ return this;
1241
+ }
1242
+
1243
+ removeFromSaveSet() {
1244
+ safeRelease(this.X, () => this.X.ChangeSaveSet(0, this.id));
1245
+ return this;
1246
+ }
1247
+
1248
+ /**
1249
+ * Tell the client where it really ended up (ICCCM 4.1.5). A reparented
1250
+ * window's own ConfigureNotify carries coordinates relative to its
1251
+ * frame, which is not what the client asked about, and a
1252
+ * ConfigureRequest the window manager decided to refuse produces no
1253
+ * ConfigureNotify at all — clients that resize themselves hang waiting
1254
+ * for one. Both cases are answered by a synthetic event carrying
1255
+ * root-relative coordinates.
1256
+ *
1257
+ * @param {object} geometry `{ x, y, width, height, borderWidth }` in
1258
+ * root coordinates; anything omitted comes from the window.
1259
+ */
1260
+ sendConfigureNotify(geometry = {}) {
1261
+ const {
1262
+ x = this.x,
1263
+ y = this.y,
1264
+ width = this.width,
1265
+ height = this.height,
1266
+ borderWidth = 0
1267
+ } = geometry;
1268
+ const ev = Buffer.alloc(32);
1269
+ ev[0] = 22; // ConfigureNotify
1270
+ ev.writeUInt32LE(this.id >>> 0, 4); // event window
1271
+ ev.writeUInt32LE(this.id >>> 0, 8); // the window itself
1272
+ ev.writeUInt32LE(0, 12); // above-sibling: None
1273
+ ev.writeInt16LE(x, 16);
1274
+ ev.writeInt16LE(y, 18);
1275
+ ev.writeUInt16LE(width, 20);
1276
+ ev.writeUInt16LE(height, 22);
1277
+ ev.writeUInt16LE(borderWidth, 24);
1278
+ ev[26] = 0; // override-redirect
1279
+ safeRelease(this.X, () =>
1280
+ this.X.SendEvent(this.id, 0, x11.eventMask.StructureNotify, ev)
1281
+ );
1282
+ return this;
1283
+ }
1284
+
1285
+ /**
1286
+ * Ask the client to close, the polite way: a WM_DELETE_WINDOW client
1287
+ * message if it advertised the protocol in WM_PROTOCOLS, so it can save
1288
+ * work or put up a confirmation. Clients that did not advertise it have
1289
+ * no such path and are killed outright (X KillClient), which is what
1290
+ * `xkill` does.
1291
+ *
1292
+ * This is the window manager's side of the protocol `setActions()` opts
1293
+ * a window into.
1294
+ *
1295
+ * @returns {Promise<boolean>} true if asked politely, false if killed
1296
+ */
1297
+ async close() {
1298
+ const protocols = await this.getProperty('WM_PROTOCOLS', { as: 'numbers' }).catch(() => null);
1299
+ const deleteAtom = await this.atom('WM_DELETE_WINDOW').catch(() => 0);
1300
+ if (!deleteAtom || !protocols || !protocols.includes(deleteAtom)) {
1301
+ safeRelease(this.X, () => this.X.KillClient(this.id));
1302
+ return false;
1303
+ }
1304
+ const wmProtocols = await this.atom('WM_PROTOCOLS');
1305
+ const ev = Buffer.alloc(32);
1306
+ ev[0] = 33; // ClientMessage
1307
+ ev[1] = 32; // format
1308
+ ev.writeUInt32LE(this.id >>> 0, 4);
1309
+ ev.writeUInt32LE(wmProtocols >>> 0, 8);
1310
+ ev.writeUInt32LE(deleteAtom >>> 0, 12);
1311
+ ev.writeUInt32LE(0, 16); // CurrentTime
1312
+ safeRelease(this.X, () => this.X.SendEvent(this.id, 0, 0, ev));
1313
+ return true;
1314
+ }
1315
+
1316
+ /**
1317
+ * Grab a mouse button on this window (X GrabButton). A window manager
1318
+ * uses this to see clicks that belong to the client: with
1319
+ * `pointerMode: 0` (synchronous) the pointer freezes on press until
1320
+ * `app.allowEvents()` decides what happens — 'replay' hands the click
1321
+ * back to the client, so click-to-focus can raise the window without
1322
+ * swallowing the click.
1323
+ *
1324
+ * @param {object} [options] `{ button (0 = any), modifiers (0x8000 =
1325
+ * any), ownerEvents, events, pointerMode, keyboardMode, confineTo,
1326
+ * cursor }`
1327
+ */
1328
+ grabButton(options = {}) {
1329
+ const {
1330
+ button = 0,
1331
+ modifiers = 0x8000,
1332
+ ownerEvents = false,
1333
+ events = x11.eventMask.ButtonPress | x11.eventMask.ButtonRelease,
1334
+ pointerMode = 1,
1335
+ keyboardMode = 1,
1336
+ confineTo = 0,
1337
+ cursor = 0
1338
+ } = options;
1339
+ this.X.GrabButton(
1340
+ this.id, ownerEvents, events, pointerMode, keyboardMode,
1341
+ confineTo, cursor, button, modifiers
1342
+ );
1343
+ return this;
1344
+ }
1345
+
1346
+ ungrabButton(button = 0, modifiers = 0x8000) {
1347
+ safeRelease(this.X, () => this.X.UngrabButton(this.id, button, modifiers));
1348
+ return this;
1000
1349
  }
1001
1350
 
1002
1351
  queryTree(callback) {
@@ -1004,12 +1353,13 @@ export default class Window extends Drawable {
1004
1353
  this.X.QueryTree(this.id, (err, tree) => {
1005
1354
  if (err) return callback(err);
1006
1355
  const children = tree.children.map((id) => new Window(app, { id }));
1007
- const parent = new Window(app, { id: tree.parent });
1356
+ // the root window has no parent — QueryTree answers None (0) for it
1357
+ const parent = tree.parent ? new Window(app, { id: tree.parent }) : null;
1008
1358
  // note that this root may be different from app.rootWindow() because
1009
1359
  // there can be multiple screens (and roots)
1010
1360
  const root = new Window(app, { id: tree.root });
1011
- const readyPromises = [...children, parent, root].map((w) => w._readyPromise);
1012
- Promise.all(readyPromises).then(() => {
1361
+ const wrappers = [...children, root, ...(parent ? [parent] : [])];
1362
+ Promise.all(wrappers.map((w) => w._readyPromise)).then(() => {
1013
1363
  callback(null, { parent, root, children });
1014
1364
  });
1015
1365
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "3.7.2",
3
+ "version": "3.9.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",
@@ -51,7 +51,7 @@
51
51
  "parse-color": "^1.0.0",
52
52
  "pngjs": "^7.0.0",
53
53
  "postcss": "^8.5.23",
54
- "x11": "^3.1.2",
54
+ "x11": "^3.2.0",
55
55
  "yoga-layout": "^3.2.1"
56
56
  },
57
57
  "scripts": {