ntk 8.2.0 → 8.3.1

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
@@ -1,14 +1,19 @@
1
+ import { connectionGone } from './cleanup.js';
1
2
  import Clipboard from './clipboard.js';
2
3
  import { CursorCache } from './cursor.js';
3
4
  import { GLError, backendFor, glCapabilities, glError, resolveGLPolicy } from './gl.js';
4
5
  import { chooseGLXConfig } from './glx.js';
5
6
  import Picture from './picture.js';
6
7
  import Pixmap from './pixmap.js';
8
+ import Region, { REGION_DOCS } from './region.js';
9
+ import { formatForDepth, parsePictFormats, visualDepths, visualFormats } from './pictformat.js';
7
10
  import { DEFAULT_RASTER_POLICY, defaultRasterizer } from './rasterize.js';
8
11
  import { dropShadowSurfaces } from './shadow.js';
12
+ import { sharedGlyphsFor } from './sharedglyphs.js';
9
13
  import { ShmUploader } from './shm-upload.js';
10
14
  import FontManager from './text/fontmanager.js';
11
15
  import Window from './window.js';
16
+ import * as xevents from './events_map.js';
12
17
 
13
18
  /**
14
19
  * The highest predefined atom id in the core protocol, `XA_WM_TRANSIENT_FOR`.
@@ -54,6 +59,38 @@ function modeRate(mode) {
54
59
  return mode.dot_clock / (mode.h_total * vTotal);
55
60
  }
56
61
 
62
+ /**
63
+ * XFIXES is missing, so no server-side regions.
64
+ *
65
+ * Worth a sentence about who is likely reading it: every X.Org release since
66
+ * 2004 and every XQuartz ship XFIXES, so a server without it is a deliberately
67
+ * minimal one — an embedded/nested server, or node-x11's own pure-JS server,
68
+ * which implements RENDER but not this. That is a fact about the display, not
69
+ * about the call, which is why it carries a `code`: a host that can degrade
70
+ * should branch on it rather than crash.
71
+ *
72
+ * @param {Error} [cause] whatever the extension query failed with
73
+ */
74
+ function noXFixesError(cause) {
75
+ const err = new Error(
76
+ 'ntk: the X server has no XFIXES extension, so it has no regions.\n' +
77
+ '\n' +
78
+ 'Region clips (ctx.clipRegion), damage regions and window shapes all need it.\n' +
79
+ 'Rectangular and path clips do not — ctx.clip() is core RENDER and works\n' +
80
+ 'everywhere:\n' +
81
+ '\n' +
82
+ ' ctx.save();\n' +
83
+ ' ctx.beginPath();\n' +
84
+ ' ctx.rect(x, y, w, h);\n' +
85
+ ' ctx.clip();\n' +
86
+ '\n' +
87
+ `${REGION_DOCS}`,
88
+ cause ? { cause } : undefined
89
+ );
90
+ err.code = 'ERR_NTK_NO_XFIXES';
91
+ return err;
92
+ }
93
+
57
94
  /**
58
95
  * A connection to an X server. Owns the underlying node-x11 client
59
96
  * (`app.X`) and acts as a factory for windows and pixmaps.
@@ -90,12 +127,34 @@ export default class App {
90
127
  this._isolateAtoms();
91
128
  this._fonts = null;
92
129
  this._clipboard = null;
130
+ // Decide the shared-glyph question now, while the environment the app
131
+ // was created under is what the caller sees: the kill switch and the
132
+ // option are sampled once per connection, not per page. Constructing the
133
+ // client costs no I/O — discovery waits for the first page to bind.
134
+ sharedGlyphsFor(this);
93
135
  this._cursors = null;
94
136
  this._solidPictures = new Map();
95
137
  this._rasterizer = undefined;
96
138
  this._shm = undefined;
97
- this._xinputPromise = null;
139
+ this._extensionPromises = new Map();
140
+ // extension event routing (see _routeExtensionEvents): server-assigned
141
+ // event type code -> events_map.extension entry. Null until the first
142
+ // extension with events is required, which is also when the client-level
143
+ // listener that consults it is attached.
144
+ this._extEvents = null;
98
145
  this._devicesPromise = null;
146
+ // XFIXES, once something has asked for it (regions, `app.fixes()`). The
147
+ // promise lives in _extensionPromises with the other extensions; the
148
+ // resolved object is also kept here because a region clip has to be
149
+ // installed synchronously, in request order with the drawing around it —
150
+ // see RenderingContext2d#clipRegion.
151
+ this._fixes = null;
152
+ // The visual -> picture format table and the formats list it was built
153
+ // from, read once out of node-x11's cached QueryPictFormats reply. Kept
154
+ // resolved alongside the promise so that binding a picture, which happens
155
+ // synchronously, can consult it without waiting (see pictFormats).
156
+ this._pictFormats = null;
157
+ this._pictFormatsPromise = null;
99
158
  // node-x11 emits X errors it cannot route to a request callback as
100
159
  // 'error' on the client — from inside its packet parser. With no
101
160
  // listener that emit throws and the parser never re-arms, silently
@@ -195,6 +254,12 @@ export default class App {
195
254
  _probeRefreshRate() {
196
255
  this._refreshProbe = null; // in progress; only ever started once
197
256
  const X = this.X;
257
+ // A connection on its way out is one more "no rate to be had". The probe
258
+ // is started lazily by the first window built on this connection, which
259
+ // may be one adopted from an event still arriving as it closes — and
260
+ // node-x11 throws synchronously at a request from then on, where the
261
+ // event dispatch has no caller to catch it (issue #321).
262
+ if (connectionGone(X)) return;
198
263
  X.require('randr', (err, R) => {
199
264
  if (err || !R) return;
200
265
  const root = this.display.screen[0].root;
@@ -302,6 +367,89 @@ export default class App {
302
367
  });
303
368
  }
304
369
 
370
+ /**
371
+ * The node-x11 object for extension `name`, or `null` where the server has
372
+ * none — asked once per connection, the absent answer included.
373
+ *
374
+ * node-x11 caches the `QueryExtension` reply itself since 4.0.1
375
+ * ([node-x11#287](https://github.com/sidorares/node-x11/issues/287)), so a
376
+ * repeated probe no longer costs a round trip even when the answer is
377
+ * absent — "can this machine run a compositor?" is a question about what is
378
+ * *not* there, and it used to pay for asking. This cache stays for what it
379
+ * does beyond that: one promise per name, so concurrent callers share the
380
+ * one query, `_routeExtensionEvents` runs once rather than per call, and an
381
+ * absent extension answers `null` instead of an error.
382
+ *
383
+ * `name` is node-x11's module name (`'fixes'`, not `'XFIXES'`), and the
384
+ * accessors below are the whole supported set. Deliberately not public: a
385
+ * name that arrives misspelled resolves `null` forever, which is
386
+ * indistinguishable from a server that does not have the extension.
387
+ *
388
+ * @param {string} name
389
+ * @returns {Promise<object|null>}
390
+ */
391
+ _extension(name) {
392
+ let pending = this._extensionPromises.get(name);
393
+ if (!pending) {
394
+ pending = new Promise((resolve) => {
395
+ this.X.require(name, (err, ext) => {
396
+ if (!err && ext) this._routeExtensionEvents(name, ext);
397
+ resolve(err ? null : ext);
398
+ });
399
+ });
400
+ this._extensionPromises.set(name, pending);
401
+ }
402
+ return pending;
403
+ }
404
+
405
+ /**
406
+ * Give this extension's events names, and a route to the object they name.
407
+ *
408
+ * A non-generic extension event carries a server-assigned type code
409
+ * (`firstEvent` + a fixed offset) and names its target under the field its
410
+ * own protocol calls it — DamageNotify a `drawable`, ShapeNotify and the
411
+ * XFIXES notifies a `window` — never under the `wid` node-x11 dispatches
412
+ * per-window consumers by. So without this they reach the client's raw
413
+ * `'event'` stream and nothing else (issue #290).
414
+ *
415
+ * Registered here, when the extension is first required through the
416
+ * accessors above, because the type codes exist only once the server has
417
+ * answered. From then on the listener below hands each one to the Window
418
+ * or Pixmap it names — through `_deliverEvent`, so a window's coalescing
419
+ * and frame pacing apply: `damage` unions its rectangles per paced frame
420
+ * exactly as `expose` does. The lookup goes through `X.event_consumers`,
421
+ * where every Window already sits for core dispatch and a Pixmap enrols
422
+ * itself when given a listener (see lib/pixmap.js).
423
+ */
424
+ _routeExtensionEvents(module, ext) {
425
+ const events = xevents.extension[module];
426
+ if (!events || !ext.firstEvent || !ext.events) return;
427
+ if (!this._extEvents) {
428
+ this._extEvents = new Map();
429
+ this.X.on('event', (ev) => {
430
+ // by the server-assigned type code, which every extension event
431
+ // carries since x11 4.0.0 (node-x11#284 — DamageNotify and a couple
432
+ // of others used to name themselves without it). Only the codes this
433
+ // table registered can match.
434
+ const route = this._extEvents.get(ev.type);
435
+ if (!route) return;
436
+ const target = this.X.event_consumers[ev[route.target]];
437
+ // a consumer that is not an ntk drawable (a caller's own) keeps
438
+ // reading the raw client stream it always read
439
+ if (typeof target?._deliverEvent !== 'function') return;
440
+ const ntkev = route.translate(ev);
441
+ ntkev.target = target;
442
+ if (target instanceof Window) ntkev.window = target;
443
+ target._deliverEvent(route.name, ntkev);
444
+ });
445
+ }
446
+ for (const [key, spec] of Object.entries(events)) {
447
+ const offset = ext.events[key];
448
+ if (offset === undefined) continue;
449
+ this._extEvents.set(ext.firstEvent + offset, spec);
450
+ }
451
+ }
452
+
305
453
  /**
306
454
  * The XInput extension, or `null` where the server has none — asked once
307
455
  * per connection.
@@ -312,12 +460,59 @@ export default class App {
312
460
  * @returns {Promise<object|null>}
313
461
  */
314
462
  xinput() {
315
- if (!this._xinputPromise) {
316
- this._xinputPromise = new Promise((resolve) => {
317
- this.X.require('xinput', (err, ext) => resolve(err ? null : ext));
318
- });
319
- }
320
- return this._xinputPromise;
463
+ return this._extension('xinput');
464
+ }
465
+
466
+ /**
467
+ * The Composite extension, or `null` where the server has none: the
468
+ * redirection half of a compositing manager — `RedirectSubwindows`,
469
+ * `NameWindowPixmap`, `Get`/`ReleaseOverlayWindow`
470
+ * (docs/app.md#extensions).
471
+ *
472
+ * `null` here is a live answer rather than a defensive one, and the one
473
+ * worth branching on: XQuartz carries DAMAGE, XFIXES, SHAPE and RENDER but
474
+ * no Composite at all, so this is where "can this machine run a
475
+ * compositor?" is decided.
476
+ *
477
+ * @returns {Promise<object|null>}
478
+ */
479
+ composite() {
480
+ return this._extension('composite');
481
+ }
482
+
483
+ /**
484
+ * The DAMAGE extension, or `null` where the server has none: what turns
485
+ * "this drawable changed" into an event, so a compositor repaints the
486
+ * region a client drew into rather than the screen
487
+ * (docs/app.md#extensions).
488
+ *
489
+ * @returns {Promise<object|null>}
490
+ */
491
+ damage() {
492
+ return this._extension('damage');
493
+ }
494
+
495
+ /**
496
+ * The XFIXES extension, or `null` where the server has none: server-side
497
+ * regions and the algebra over them, which is how a damaged area becomes a
498
+ * clip — `SetPictureClipRegion(ctx.picture.id, 0, 0, region)` narrows a 2d
499
+ * context to one (docs/app.md#extensions).
500
+ *
501
+ * @returns {Promise<object|null>}
502
+ */
503
+ xfixes() {
504
+ return this._extension('fixes');
505
+ }
506
+
507
+ /**
508
+ * The SHAPE extension, or `null` where the server has none: non-rectangular
509
+ * window bounding/clip/input shapes, which a compositor has to read to
510
+ * paint a shaped client correctly (docs/app.md#extensions).
511
+ *
512
+ * @returns {Promise<object|null>}
513
+ */
514
+ shape() {
515
+ return this._extension('shape');
321
516
  }
322
517
 
323
518
  /**
@@ -355,6 +550,18 @@ export default class App {
355
550
  return this._clipboard;
356
551
  }
357
552
 
553
+ /**
554
+ * The cross-process shared glyph cache client (docs/shared-glyphs.md), or
555
+ * `null` when the feature is off — `createClient({ sharedGlyphs: false })`
556
+ * or the `NTK_NO_SHARED_GLYPHS` environment kill switch. Purely lazy: the
557
+ * accessor itself touches no server state; discovery (and the first app's
558
+ * self-election as the display's glyph directory) happens when the first
559
+ * glyph page binds.
560
+ */
561
+ get sharedGlyphs() {
562
+ return sharedGlyphsFor(this);
563
+ }
564
+
358
565
  /** per-connection cache of X11 cursor-font cursors (see lib/cursor.js) */
359
566
  get cursors() {
360
567
  if (!this._cursors) this._cursors = new CursorCache(this);
@@ -456,6 +663,102 @@ export default class App {
456
663
  return null;
457
664
  }
458
665
 
666
+ /**
667
+ * The RENDER picture formats this server publishes, and which one belongs
668
+ * to each visual: `{ formats, byVisual }`.
669
+ *
670
+ * No round trip of ntk's own: node-x11 sends `QueryPictFormats` itself
671
+ * while requiring RENDER — that is where its `rgb24`/`rgba32`/`a8` come
672
+ * from — and since 4.0.0 it keeps the reply as `Render.pictFormats`. So
673
+ * the answer is already here before anything can draw. `formats` is the
674
+ * list as objects (`{ id, type, depth, redShift, redMask, ... }`);
675
+ * `byVisual` is a `Map` from visual id to format id.
676
+ *
677
+ * @returns {Promise<{formats: Array<object>, byVisual: Map<number, number>}>}
678
+ */
679
+ pictFormats() {
680
+ if (this._pictFormatsPromise) return this._pictFormatsPromise;
681
+ this._pictFormatsPromise = new Promise((resolve, reject) => {
682
+ const reply = this.display.Render?.pictFormats;
683
+ if (!reply) {
684
+ reject(new Error('ntk: this connection has no RENDER extension, so it has no picture formats'));
685
+ return;
686
+ }
687
+ const formats = parsePictFormats(reply);
688
+ this._pictFormats = {
689
+ formats,
690
+ byVisual: visualFormats(this.display, formats, reply)
691
+ };
692
+ resolve(this._pictFormats);
693
+ });
694
+ return this._pictFormatsPromise;
695
+ }
696
+
697
+ /**
698
+ * The picture format a drawable on `visual` is read and written through.
699
+ *
700
+ * Depth is not enough to name a format and a visual is: a depth-16 visual
701
+ * can be 5:6:5 or 5:5:5, a depth-24 one RGB or BGR, and 10:10:10:2 is 32
702
+ * bits wide like 8:8:8:8. Anything drawing on a drawable it did not create
703
+ * — a compositor holding another client's pixmap, an adopted or embedded
704
+ * window — has to ask, or RENDER reads the channels wrong without ever
705
+ * complaining (issue #295).
706
+ *
707
+ * const attrs = await wnd.getAttributes();
708
+ * const format = await app.pictFormatFor(attrs.visual);
709
+ *
710
+ * `visual` may be an id or a handshake visual object. `depth` is the
711
+ * fallback: where the server names no format for the visual — an indexed
712
+ * visual, or one this connection was never told about — the standard
713
+ * format for that depth is returned, which is what ntk used to assume
714
+ * everywhere.
715
+ *
716
+ * @param {number|object} visual visual id, or `{ vid }`
717
+ * @param {object} [options]
718
+ * @param {number} [options.depth] the drawable's depth, for the fallback
719
+ * @returns {Promise<number|undefined>} picture format id
720
+ */
721
+ async pictFormatFor(visual, { depth } = {}) {
722
+ const id = typeof visual === 'object' && visual ? visual.vid : visual;
723
+ try {
724
+ await this.pictFormats();
725
+ } catch {
726
+ // a server that cannot answer leaves the fallback
727
+ }
728
+ return this._knownPictFormat(id, depth) ?? formatForDepth(this.display.Render, depth);
729
+ }
730
+
731
+ /**
732
+ * The cached answer to `pictFormatFor`, or `undefined` when the table has
733
+ * not arrived, names no format for this visual, or the visual does not
734
+ * describe a drawable of this depth.
735
+ *
736
+ * That last one is the guard against handing RENDER a format it will
737
+ * reject: a picture's format and its drawable's depth have to agree, and
738
+ * the visual on hand is not always the drawable's own — a window's backing
739
+ * pixmap is allocated at a depth ntk sometimes has to assume. Where they
740
+ * disagree the depth is what the drawable really is, so the depth-based
741
+ * format is what it gets.
742
+ *
743
+ * A picture is bound synchronously, in request order with the drawing
744
+ * around it, so the format has to be available without a wait. Callers
745
+ * fall back to the depth and re-bind once `pictFormats()` resolves.
746
+ */
747
+ _knownPictFormat(visual, depth) {
748
+ if (!visual) return undefined;
749
+ const id = visual >>> 0;
750
+ const format = this._pictFormats?.byVisual.get(id);
751
+ if (format === undefined) return undefined;
752
+ // Depth 0 is not a depth: it is CopyFromParent, which ntk never resolved
753
+ // because only the server did — and a window on that visual is of that
754
+ // visual's depth by definition, so there is nothing to disagree with.
755
+ if (depth) {
756
+ this._visualDepths ??= visualDepths(this.display);
757
+ if (this._visualDepths.get(id) !== depth) return undefined;
758
+ }
759
+ return format;
760
+ }
761
+
459
762
  rootWindow(screen = 0) {
460
763
  return new Window(this, { id: this.display.screen[screen].root });
461
764
  }
@@ -497,6 +800,42 @@ export default class App {
497
800
  return new Pixmap(this, args);
498
801
  }
499
802
 
803
+ /**
804
+ * The XFIXES extension for this connection, loaded once and shared.
805
+ *
806
+ * Regions live here — the server-side rectangle sets X uses for damage,
807
+ * window shapes and compositor bookkeeping — along with the requests that
808
+ * combine them and the one that hangs a region on a Picture as its clip.
809
+ *
810
+ * The throwing spelling of `app.xfixes()`: same query, same cache, but a
811
+ * server without XFIXES rejects with code `'ERR_NTK_NO_XFIXES'` instead of
812
+ * resolving `null` — regions cannot degrade, so their absence is an error
813
+ * here rather than an answer.
814
+ *
815
+ * @returns {Promise<object>} node-x11's XFIXES extension object
816
+ */
817
+ async fixes() {
818
+ const ext = await this.xfixes();
819
+ if (!ext) throw noXFixesError();
820
+ this._fixes = ext;
821
+ return ext;
822
+ }
823
+
824
+ /**
825
+ * A server-side region of the given rectangles (`{ x, y, width, height }`
826
+ * or ntk's own `{ x, y, w, h }`), empty by default.
827
+ *
828
+ * Async because XFIXES is loaded on first use; a second region costs one
829
+ * request and no round trip. Free it with `destroy()` — or `using`, or let
830
+ * the GC do it (docs/resource-management.md).
831
+ *
832
+ * @param {Array<object>} [rects]
833
+ * @returns {Promise<Region>}
834
+ */
835
+ async createRegion(rects = []) {
836
+ return new Region(this, await this.fixes(), rects);
837
+ }
838
+
500
839
  /**
501
840
  * A repeating source Picture of one colour, for compositing. Components
502
841
  * are 0..1 floats, premultiplied by alpha.
package/lib/cleanup.js CHANGED
@@ -1,11 +1,27 @@
1
- // Release a server-side resource, tolerating a connection that is closing or
2
- // already gone — the X server frees all of a client's resources on disconnect,
3
- // so there is nothing left to do and nothing worth throwing about. This
4
- // matters for FinalizationRegistry callbacks, which run after app.close() if
5
- // wrappers get garbage collected late and have no user code around them to
6
- // catch.
1
+ /**
2
+ * Whether a request issued now would throw rather than reach the server.
3
+ * node-x11 throws *synchronously* ("client is in closing state") once
4
+ * `close()` has begun, and a destroyed or ended stream is the same state one
5
+ * step further along.
6
+ *
7
+ * The check matters wherever a request is issued from somewhere with no
8
+ * caller to catch: a FinalizationRegistry callback, a paced frame's timer, or
9
+ * the X event dispatch itself — events already in the read buffer keep being
10
+ * delivered after `close()` (issue #321).
11
+ */
12
+ export function connectionGone(X) {
13
+ return !!(X._closing || !X.stream || X.stream.destroyed || X.stream.writableEnded);
14
+ }
15
+
16
+ // Issue requests — typically releasing a server-side resource — tolerating a
17
+ // connection that is closing or already gone. The X server frees all of a
18
+ // client's resources on disconnect, so there is nothing left to do and
19
+ // nothing worth throwing about. This matters wherever the requests have no
20
+ // caller around them to catch: FinalizationRegistry callbacks, which run
21
+ // after app.close() if wrappers get garbage collected late, a paced frame's
22
+ // timer, and the X event dispatch (issue #321).
7
23
  export function safeRelease(X, fn) {
8
- if (X._closing || !X.stream || X.stream.destroyed || X.stream.writableEnded) return;
24
+ if (connectionGone(X)) return;
9
25
  try {
10
26
  fn();
11
27
  } catch {
package/lib/drawable.js CHANGED
@@ -11,6 +11,16 @@ export default class Drawable extends EventEmitter {
11
11
  if (!factory) throw new Error(`Unknown rendering context: ${name}`);
12
12
  return factory(this, ...args);
13
13
  }
14
+
15
+ /**
16
+ * Deliver one named event to this drawable's listeners. Window overrides
17
+ * this with per-frame coalescing and pacing; the base emits directly,
18
+ * for drawables with no frame clock — a Pixmap a DAMAGE object watches
19
+ * (see App#_routeExtensionEvents).
20
+ */
21
+ _deliverEvent(name, ev) {
22
+ this.emit(name, ev);
23
+ }
14
24
  }
15
25
 
16
26
  // populated by the renderingcontext_* modules on import
package/lib/events_map.js CHANGED
@@ -86,6 +86,10 @@ export const coalesce = {
86
86
  mousemove: 'last',
87
87
  resize: 'last',
88
88
  expose: 'union',
89
+ // DamageNotify is the expose case again — a burst of rectangles whose
90
+ // union is what a repaint wants — reported about a drawable's content
91
+ // instead of a window's visibility
92
+ damage: 'union',
89
93
  // 'accumulate' — scroll distance adds up. Keeping the last delta instead
90
94
  // would throw away everything but the final step of a fast scroll, and a
91
95
  // frame's worth of a touchpad's sub-notch deltas is exactly the case the
@@ -93,6 +97,101 @@ export const coalesce = {
93
97
  wheel: 'accumulate'
94
98
  };
95
99
 
100
+ // XFixes SelectionNotify subtype -> why the ownership changed, the same
101
+ // vocabulary clipboard.watch already answers in. The codes are fixed by
102
+ // xfixesproto (SelectionEvent), not assigned by the server.
103
+ const selectionReason = ['new-owner', 'destroyed', 'closed'];
104
+
105
+ // ShapeNotify kind -> which of the window's three shapes changed
106
+ // (shapeproto ShapeKind)
107
+ const shapeKind = ['bounding', 'clip', 'input'];
108
+
109
+ /**
110
+ * The non-generic extension events, and how to deliver them.
111
+ *
112
+ * Unlike core events their type codes are assigned by the server at
113
+ * QueryExtension time (`ext.firstEvent` + a fixed offset), and the drawable
114
+ * each one names arrives under the field its own protocol calls it —
115
+ * DamageNotify a `drawable`, the others a `window` — never under the `wid`
116
+ * node-x11 dispatches per-window consumers by. So they need both halves of
117
+ * this table: a name, and which field to route by. App reads it when an
118
+ * extension is required through its accessors (`app.damage()` and friends)
119
+ * and hands each event to the Window or Pixmap it names — see
120
+ * App#_routeExtensionEvents and docs/app.md "Extension events".
121
+ *
122
+ * Keyed by node-x11's module name, then by the event's key in `ext.events`.
123
+ * `translate` shapes the raw node-x11 event into the one delivered.
124
+ */
125
+ export const extension = {
126
+ damage: {
127
+ DamageNotify: {
128
+ name: 'damage',
129
+ target: 'drawable',
130
+ // expose-shaped, because it is the same news: the box in
131
+ // x/y/width/height so 'union' coalescing applies to it unchanged.
132
+ // Bit 7 of the level byte is the wire's own "more follow" flag —
133
+ // split out, since the report level it rides on is 0..3.
134
+ translate: (ev) => ({
135
+ x: ev.area.x,
136
+ y: ev.area.y,
137
+ width: ev.area.w,
138
+ height: ev.area.h,
139
+ geometry: ev.geometry,
140
+ damage: ev.damage,
141
+ level: ev.level & 0x7f,
142
+ more: !!(ev.level & 0x80),
143
+ time: ev.time
144
+ })
145
+ }
146
+ },
147
+ fixes: {
148
+ // 'selection' is taken — it is core SelectionNotify, a conversion
149
+ // answered — and this event is about who owns the selection, hence the
150
+ // qualified name
151
+ SelectionNotify: {
152
+ name: 'selection_owner',
153
+ target: 'window',
154
+ translate: (ev) => ({
155
+ selection: ev.selection,
156
+ owner: ev.owner,
157
+ reason: selectionReason[ev.subtype] ?? ev.subtype,
158
+ timestamp: ev.timestamp,
159
+ selectionTimestamp: ev.selectionTimestamp
160
+ })
161
+ },
162
+ CursorNotify: {
163
+ name: 'cursor',
164
+ target: 'window',
165
+ translate: (ev) => ({
166
+ cursorSerial: ev.cursorSerial,
167
+ cursorName: ev.cursorName,
168
+ time: ev.timestamp
169
+ })
170
+ }
171
+ },
172
+ shape: {
173
+ ShapeNotify: {
174
+ name: 'shape',
175
+ target: 'window',
176
+ translate: (ev) => ({
177
+ kind: shapeKind[ev.kind] ?? ev.kind,
178
+ x: ev.x,
179
+ y: ev.y,
180
+ width: ev.width,
181
+ height: ev.height,
182
+ shaped: !!ev.shaped,
183
+ time: ev.time
184
+ })
185
+ }
186
+ }
187
+ };
188
+
189
+ // every routed extension event name — what a Pixmap watches `newListener`
190
+ // for to enrol itself in the routing table (see lib/pixmap.js)
191
+ export const extensionEventNames = new Set(
192
+ Object.values(extension).flatMap((events) => Object.values(events).map((spec) => spec.name))
193
+ );
194
+
96
195
  export const toSnake = {
97
196
  onMouseMove: 'mousemove',
98
197
  onMouseOver: 'mouseover',
@@ -124,4 +223,4 @@ export const maskCamelCase = Object.fromEntries(
124
223
  Object.entries(toSnake).map(([camel, snake]) => [camel, mask[snake]])
125
224
  );
126
225
 
127
- export default { eventName, mask, maskCamelCase, toSnake, coalesce };
226
+ export default { eventName, mask, maskCamelCase, toSnake, coalesce, extension, extensionEventNames };