ntk 4.0.0 → 4.1.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 +15 -0
- package/lib/clipboard.js +18 -26
- package/lib/events_map.js +3 -0
- package/lib/index.js +4 -1
- package/lib/keyboard.js +114 -0
- package/lib/text/keysym-unicode.js +1186 -0
- package/lib/window.js +833 -121
- package/package.json +2 -3
package/lib/window.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import keysym from 'keysym';
|
|
2
1
|
import x11 from 'x11';
|
|
3
2
|
|
|
4
3
|
import { safeRelease } from './cleanup.js';
|
|
5
4
|
import Drawable from './drawable.js';
|
|
6
5
|
import Pixmap from './pixmap.js';
|
|
7
6
|
import * as xevents from './events_map.js';
|
|
7
|
+
import { decodeKey } from './keyboard.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* How many rectangles a frame's dirty region is allowed to hold.
|
|
@@ -135,6 +135,125 @@ const forwardedXAttributes = [
|
|
|
135
135
|
'cursor'
|
|
136
136
|
];
|
|
137
137
|
|
|
138
|
+
// ICCCM 4.1.2.3 WM_NORMAL_HINTS flags. The first four say who chose the
|
|
139
|
+
// window's geometry — without one of them a window manager is free to place
|
|
140
|
+
// the window wherever it likes, whatever x/y it was created with.
|
|
141
|
+
const SIZE_HINT = {
|
|
142
|
+
USPosition: 1,
|
|
143
|
+
USSize: 2,
|
|
144
|
+
PPosition: 4,
|
|
145
|
+
PSize: 8,
|
|
146
|
+
PMinSize: 16,
|
|
147
|
+
PMaxSize: 32,
|
|
148
|
+
PResizeInc: 64,
|
|
149
|
+
PAspect: 128,
|
|
150
|
+
PBaseSize: 256,
|
|
151
|
+
PWinGravity: 512
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// ICCCM 4.1.2.4 WM_HINTS flags. MessageHint (128) is obsolete and unused.
|
|
155
|
+
const WM_HINT = {
|
|
156
|
+
Input: 1,
|
|
157
|
+
State: 2,
|
|
158
|
+
IconPixmap: 4,
|
|
159
|
+
IconWindow: 8,
|
|
160
|
+
IconPosition: 16,
|
|
161
|
+
IconMask: 32,
|
|
162
|
+
WindowGroup: 64,
|
|
163
|
+
Urgency: 256
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const SIZE_HINT_KEYS = new Set([
|
|
167
|
+
'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
|
|
168
|
+
'widthInc', 'heightInc', 'baseWidth', 'baseHeight',
|
|
169
|
+
'minAspect', 'maxAspect', 'gravity', 'resizable',
|
|
170
|
+
'position', 'size', 'x', 'y', 'width', 'height'
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
const WM_HINT_KEYS = new Set([
|
|
174
|
+
'input', 'initialState', 'urgent', 'icon', 'iconPixmap',
|
|
175
|
+
'iconMask', 'iconWindow', 'iconX', 'iconY', 'windowGroup'
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
// Hint keys `createWindow` also accepts at the top level, per the API sketch
|
|
179
|
+
// in ntk#19. x/y/width/height are deliberately absent: at the top level they
|
|
180
|
+
// are the window's geometry, and only inside a `hints`/`sizeHints` object do
|
|
181
|
+
// they mean the WM_NORMAL_HINTS fields of the same name. `position`/`size`
|
|
182
|
+
// are absent for the same reason — as bare creation arguments they read as
|
|
183
|
+
// geometry rather than as "who chose it".
|
|
184
|
+
const CREATION_HINT_KEYS = [
|
|
185
|
+
...[...SIZE_HINT_KEYS].filter(
|
|
186
|
+
(k) => !['x', 'y', 'width', 'height', 'position', 'size', 'resizable'].includes(k)
|
|
187
|
+
),
|
|
188
|
+
...WM_HINT_KEYS,
|
|
189
|
+
'transientFor',
|
|
190
|
+
'protocols'
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
// EWMH 5.7 _NET_WM_STATE, in spec order. FOCUSED and HIDDEN are set by the
|
|
194
|
+
// window manager rather than asked for, but a client still reads them.
|
|
195
|
+
const EWMH_STATES = [
|
|
196
|
+
'modal',
|
|
197
|
+
'sticky',
|
|
198
|
+
'maximized_vert',
|
|
199
|
+
'maximized_horz',
|
|
200
|
+
'shaded',
|
|
201
|
+
'skip_taskbar',
|
|
202
|
+
'skip_pager',
|
|
203
|
+
'hidden',
|
|
204
|
+
'fullscreen',
|
|
205
|
+
'above',
|
|
206
|
+
'below',
|
|
207
|
+
'demands_attention',
|
|
208
|
+
'focused'
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
/** 'fullscreen' -> '_NET_WM_STATE_FULLSCREEN'; a full atom name passes through. */
|
|
212
|
+
function stateAtomName(name) {
|
|
213
|
+
return name.startsWith('_NET_WM_STATE_') ? name : `_NET_WM_STATE_${name.toUpperCase()}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** The inverse, for reporting what the window manager put on the window. */
|
|
217
|
+
function stateShortName(atomName) {
|
|
218
|
+
return atomName.startsWith('_NET_WM_STATE_')
|
|
219
|
+
? atomName.slice('_NET_WM_STATE_'.length).toLowerCase()
|
|
220
|
+
: atomName;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Normalize the argument of setWmState.
|
|
225
|
+
*
|
|
226
|
+
* 'maximized' is the one name that is not an atom: EWMH maximizes an axis at
|
|
227
|
+
* a time, and giving the message both atoms at once is exactly what its two
|
|
228
|
+
* state slots are for.
|
|
229
|
+
*/
|
|
230
|
+
function expandStateNames(names) {
|
|
231
|
+
const list = Array.isArray(names) ? names : [names];
|
|
232
|
+
return list.flatMap((n) =>
|
|
233
|
+
n === 'maximized' ? ['maximized_vert', 'maximized_horz'] : [n]
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Warn once per process about a hint that will do nothing.
|
|
239
|
+
*
|
|
240
|
+
* These mistakes are silent everywhere else: the server stores whatever
|
|
241
|
+
* property bytes it is handed, and only a window manager could tell that
|
|
242
|
+
* what it read means nothing. Same shape as node-x11's ChangeProperty
|
|
243
|
+
* warning, so a caller who gets it wrong on every window hears it once.
|
|
244
|
+
*/
|
|
245
|
+
const warnedHints = new Set();
|
|
246
|
+
function warnHint(key, message) {
|
|
247
|
+
if (warnedHints.has(key)) return;
|
|
248
|
+
warnedHints.add(key);
|
|
249
|
+
console.warn(`ntk: ${message} Further occurrences are not reported.`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** A Window, a Pixmap, or a bare XID — all of these name a server resource. */
|
|
253
|
+
function resourceId(value) {
|
|
254
|
+
return typeof value === 'number' ? value : (value?.id ?? 0);
|
|
255
|
+
}
|
|
256
|
+
|
|
138
257
|
/**
|
|
139
258
|
* Turn a GetProperty reply into the shape the caller asked for. X property
|
|
140
259
|
* types are atoms, so the only distinction we can make without a round trip
|
|
@@ -191,6 +310,17 @@ export default class Window extends Drawable {
|
|
|
191
310
|
this._mapped = false;
|
|
192
311
|
this._destroyed = false;
|
|
193
312
|
this._titleSerial = 0;
|
|
313
|
+
this._overrideRedirect = !!args.overrideRedirect;
|
|
314
|
+
// WM_PROTOCOLS is a set: read-modify-write, serialized (see addProtocol)
|
|
315
|
+
this._protocols = null;
|
|
316
|
+
this._protocolQueue = Promise.resolve();
|
|
317
|
+
// so is _NET_WM_STATE, on the unmapped path (see setWmState)
|
|
318
|
+
this._wmStateQueue = Promise.resolve();
|
|
319
|
+
this._netWmStateAtom = 0; // resolved when a 'statechange' listener appears
|
|
320
|
+
// what setHints has accumulated per struct, so a later call can rewrite
|
|
321
|
+
// the whole property without dropping what an earlier one set
|
|
322
|
+
this._sizeHints = null;
|
|
323
|
+
this._wmHints = null;
|
|
194
324
|
this._readyPromiseResolve = null;
|
|
195
325
|
this._readyPromise = new Promise((resolve) => {
|
|
196
326
|
this._readyPromiseResolve = resolve;
|
|
@@ -302,8 +432,23 @@ export default class Window extends Drawable {
|
|
|
302
432
|
if (args.windowType) {
|
|
303
433
|
this.setWindowType(args.windowType);
|
|
304
434
|
}
|
|
305
|
-
|
|
306
|
-
|
|
435
|
+
// ICCCM/EWMH hints at creation: `hints: { ... }`, the `sizeHints`/
|
|
436
|
+
// `resizable` arguments that predate it, or the hint names on their own
|
|
437
|
+
// at the top level. All of them land in one setHints call so that a
|
|
438
|
+
// window created with `minWidth` and a `hints` block writes
|
|
439
|
+
// WM_NORMAL_HINTS once, with both.
|
|
440
|
+
const hints = { ...args.sizeHints, ...args.hints };
|
|
441
|
+
if (args.resizable !== undefined) hints.resizable = args.resizable;
|
|
442
|
+
for (const key of CREATION_HINT_KEYS) {
|
|
443
|
+
if (args[key] !== undefined) hints[key] = args[key];
|
|
444
|
+
}
|
|
445
|
+
if (Object.keys(hints).length) {
|
|
446
|
+
this.setHints(hints);
|
|
447
|
+
}
|
|
448
|
+
// EWMH wants a pid and a client machine on top-level windows; child
|
|
449
|
+
// windows are an implementation detail nothing asks about
|
|
450
|
+
if (!args.id && args.pid !== false && parentId === app.display.screen[0].root) {
|
|
451
|
+
this.setPid(typeof args.pid === 'number' ? args.pid : undefined);
|
|
307
452
|
}
|
|
308
453
|
if (args.alwaysOnTop) {
|
|
309
454
|
this.setAlwaysOnTop(true);
|
|
@@ -330,23 +475,32 @@ export default class Window extends Drawable {
|
|
|
330
475
|
this._handleExpose(ev);
|
|
331
476
|
return;
|
|
332
477
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
ev.codepoint = entry.unicode;
|
|
478
|
+
if (eventName === 'keydown' || eventName === 'keyup') {
|
|
479
|
+
// the active layout arrives in the event's own state bits, not in the
|
|
480
|
+
// keymap — see lib/keyboard.js. Keys that type nothing (arrows,
|
|
481
|
+
// F-keys, modifiers) leave `codepoint` absent rather than reporting 0.
|
|
482
|
+
const key = decodeKey(X.keycode2keysyms[ev.keycode], ev.buttons);
|
|
483
|
+
if (key) {
|
|
484
|
+
ev.keysym = key.keysym;
|
|
485
|
+
ev.baseKeysym = key.baseKeysym;
|
|
486
|
+
ev.group = key.group;
|
|
487
|
+
if (key.codepoint !== undefined) ev.codepoint = key.codepoint;
|
|
344
488
|
}
|
|
345
489
|
}
|
|
346
490
|
if (this._coalesce && xevents.coalesce[eventName]) {
|
|
347
491
|
this._enqueueCoalesced(eventName, ntkev);
|
|
348
492
|
return;
|
|
349
493
|
}
|
|
494
|
+
// the window manager reporting what it did with the window: a user who
|
|
495
|
+
// hit maximize or a fullscreen hotkey changes the state behind the
|
|
496
|
+
// app's back, and this is the only way an app that mirrors it in its
|
|
497
|
+
// own UI stays honest
|
|
498
|
+
if (eventName === 'property' && ev.atom === this._netWmStateAtom) {
|
|
499
|
+
this.getWmStates().then(
|
|
500
|
+
(states) => this.emit('statechange', states),
|
|
501
|
+
() => {}
|
|
502
|
+
);
|
|
503
|
+
}
|
|
350
504
|
// deliver buffered state events before a discrete one so handlers see
|
|
351
505
|
// them in the order they happened (a drag sees the move, then the up)
|
|
352
506
|
this._flushCoalesced();
|
|
@@ -373,6 +527,16 @@ export default class Window extends Drawable {
|
|
|
373
527
|
});
|
|
374
528
|
|
|
375
529
|
this.on('newListener', (name) => {
|
|
530
|
+
// 'statechange' is derived from a PropertyNotify, so it needs the atom
|
|
531
|
+
// to compare against as well as the mask the table below selects
|
|
532
|
+
if (name === 'statechange' && !this._netWmStateAtom) {
|
|
533
|
+
this.atom('_NET_WM_STATE').then(
|
|
534
|
+
(atom) => {
|
|
535
|
+
this._netWmStateAtom = atom;
|
|
536
|
+
},
|
|
537
|
+
() => {}
|
|
538
|
+
);
|
|
539
|
+
}
|
|
376
540
|
// extend the server-side event mask if this event needs it
|
|
377
541
|
const eventMask = xevents.mask[name];
|
|
378
542
|
if (!eventMask) return;
|
|
@@ -876,16 +1040,25 @@ export default class Window extends Drawable {
|
|
|
876
1040
|
}
|
|
877
1041
|
|
|
878
1042
|
/**
|
|
879
|
-
* ICCCM WM_NORMAL_HINTS — how the window manager may
|
|
1043
|
+
* ICCCM WM_NORMAL_HINTS — how the window manager may size and place this
|
|
1044
|
+
* window.
|
|
880
1045
|
*
|
|
881
1046
|
* setSizeHints({ minWidth, minHeight, maxWidth, maxHeight,
|
|
882
1047
|
* widthInc, heightInc, baseWidth, baseHeight,
|
|
883
1048
|
* minAspect: [num, den], maxAspect: [num, den],
|
|
884
|
-
* gravity })
|
|
1049
|
+
* gravity, position, size, resizable })
|
|
885
1050
|
*
|
|
886
1051
|
* `resizable: false` is shorthand for pinning min and max to the current
|
|
887
1052
|
* size. Without this property a WM lets the user resize a window to any
|
|
888
1053
|
* size at all, which is why fixed-size dialogs need it.
|
|
1054
|
+
*
|
|
1055
|
+
* `position` and `size` are `'user'` or `'program'` and declare who chose
|
|
1056
|
+
* the geometry — a window manager that sees neither is free to place the
|
|
1057
|
+
* window wherever its own policy says, whatever x/y it was created with.
|
|
1058
|
+
* Passing `x`/`y` or `width`/`height` here implies `'program'`.
|
|
1059
|
+
*
|
|
1060
|
+
* This writes the whole struct: hints not passed are not carried over from
|
|
1061
|
+
* an earlier call. `setHints()` is the accumulating form.
|
|
889
1062
|
*/
|
|
890
1063
|
setSizeHints(hints = {}) {
|
|
891
1064
|
const {
|
|
@@ -900,8 +1073,13 @@ export default class Window extends Drawable {
|
|
|
900
1073
|
minAspect,
|
|
901
1074
|
maxAspect,
|
|
902
1075
|
gravity,
|
|
903
|
-
resizable
|
|
1076
|
+
resizable,
|
|
1077
|
+
x,
|
|
1078
|
+
y,
|
|
1079
|
+
width,
|
|
1080
|
+
height
|
|
904
1081
|
} = hints;
|
|
1082
|
+
let { position, size } = hints;
|
|
905
1083
|
|
|
906
1084
|
let minW = minWidth;
|
|
907
1085
|
let minH = minHeight;
|
|
@@ -913,50 +1091,75 @@ export default class Window extends Drawable {
|
|
|
913
1091
|
maxW = maxW ?? this.width;
|
|
914
1092
|
maxH = maxH ?? this.height;
|
|
915
1093
|
}
|
|
1094
|
+
// naming a geometry here is itself the statement that the program chose
|
|
1095
|
+
// it; the flag is what carries that, and the fields alone are obsolete
|
|
1096
|
+
if (position === undefined && (x !== undefined || y !== undefined)) position = 'program';
|
|
1097
|
+
if (size === undefined && (width !== undefined || height !== undefined)) size = 'program';
|
|
916
1098
|
|
|
917
1099
|
// XSizeHints: 18 CARD32s, flags first (ICCCM 4.1.2.3)
|
|
918
|
-
const PMinSize = 16;
|
|
919
|
-
const PMaxSize = 32;
|
|
920
|
-
const PResizeInc = 64;
|
|
921
|
-
const PAspect = 128;
|
|
922
|
-
const PBaseSize = 256;
|
|
923
|
-
const PWinGravity = 512;
|
|
924
|
-
|
|
925
1100
|
const v = new Uint32Array(18);
|
|
926
1101
|
let flags = 0;
|
|
1102
|
+
if (position) {
|
|
1103
|
+
flags |= position === 'user' ? SIZE_HINT.USPosition : SIZE_HINT.PPosition;
|
|
1104
|
+
v[1] = x ?? this.x ?? 0;
|
|
1105
|
+
v[2] = y ?? this.y ?? 0;
|
|
1106
|
+
}
|
|
1107
|
+
if (size) {
|
|
1108
|
+
flags |= size === 'user' ? SIZE_HINT.USSize : SIZE_HINT.PSize;
|
|
1109
|
+
v[3] = width ?? this.width ?? 0;
|
|
1110
|
+
v[4] = height ?? this.height ?? 0;
|
|
1111
|
+
}
|
|
927
1112
|
if (minW !== undefined || minH !== undefined) {
|
|
928
|
-
flags |= PMinSize;
|
|
1113
|
+
flags |= SIZE_HINT.PMinSize;
|
|
929
1114
|
v[5] = minW ?? 0;
|
|
930
1115
|
v[6] = minH ?? 0;
|
|
931
1116
|
}
|
|
932
1117
|
if (maxW !== undefined || maxH !== undefined) {
|
|
933
|
-
flags |= PMaxSize;
|
|
1118
|
+
flags |= SIZE_HINT.PMaxSize;
|
|
934
1119
|
v[7] = maxW ?? 0;
|
|
935
1120
|
v[8] = maxH ?? 0;
|
|
936
1121
|
}
|
|
937
1122
|
if (widthInc !== undefined || heightInc !== undefined) {
|
|
938
|
-
flags |= PResizeInc;
|
|
1123
|
+
flags |= SIZE_HINT.PResizeInc;
|
|
939
1124
|
v[9] = widthInc ?? 1;
|
|
940
1125
|
v[10] = heightInc ?? 1;
|
|
941
1126
|
}
|
|
942
1127
|
if (minAspect || maxAspect) {
|
|
943
|
-
flags |= PAspect;
|
|
1128
|
+
flags |= SIZE_HINT.PAspect;
|
|
944
1129
|
v[11] = minAspect?.[0] ?? 0;
|
|
945
1130
|
v[12] = minAspect?.[1] ?? 1;
|
|
946
1131
|
v[13] = maxAspect?.[0] ?? 0;
|
|
947
1132
|
v[14] = maxAspect?.[1] ?? 1;
|
|
948
1133
|
}
|
|
949
1134
|
if (baseWidth !== undefined || baseHeight !== undefined) {
|
|
950
|
-
flags |= PBaseSize;
|
|
1135
|
+
flags |= SIZE_HINT.PBaseSize;
|
|
951
1136
|
v[15] = baseWidth ?? 0;
|
|
952
1137
|
v[16] = baseHeight ?? 0;
|
|
953
1138
|
}
|
|
954
1139
|
if (gravity !== undefined) {
|
|
955
|
-
flags |= PWinGravity;
|
|
1140
|
+
flags |= SIZE_HINT.PWinGravity;
|
|
956
1141
|
v[17] = gravity;
|
|
957
1142
|
}
|
|
958
1143
|
v[0] = flags;
|
|
959
1144
|
|
|
1145
|
+
if (flags === 0) {
|
|
1146
|
+
// WM_NORMAL_HINTS with flags 0 is a legal property meaning "I declare
|
|
1147
|
+
// nothing" — indistinguishable from never having written it, and
|
|
1148
|
+
// reported by nobody. `resizable: true` legitimately means that, so it
|
|
1149
|
+
// is the one silent case; anything else here is a typo or a key from
|
|
1150
|
+
// the wrong hint struct.
|
|
1151
|
+
const keys = Object.keys(hints);
|
|
1152
|
+
const onlyResizable = keys.length === 1 && resizable === true;
|
|
1153
|
+
if (!onlyResizable) {
|
|
1154
|
+
warnHint(
|
|
1155
|
+
'sizeHints-empty',
|
|
1156
|
+
`setSizeHints({ ${keys.join(', ')} }) sets no WM_NORMAL_HINTS flag, so nothing was ` +
|
|
1157
|
+
'written — a flags word of 0 declares nothing and no window manager reports it.'
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
return this;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
960
1163
|
safeRelease(this.X, () => {
|
|
961
1164
|
this.X.ChangeProperty(
|
|
962
1165
|
0,
|
|
@@ -970,6 +1173,345 @@ export default class Window extends Drawable {
|
|
|
970
1173
|
return this;
|
|
971
1174
|
}
|
|
972
1175
|
|
|
1176
|
+
/**
|
|
1177
|
+
* ICCCM WM_HINTS — what the window wants from the window manager beyond
|
|
1178
|
+
* its geometry.
|
|
1179
|
+
*
|
|
1180
|
+
* setWmHints({ input, initialState, urgent, icon, iconMask, iconWindow,
|
|
1181
|
+
* iconX, iconY, windowGroup })
|
|
1182
|
+
*
|
|
1183
|
+
* - `input` is the ICCCM 4.1.7 input model: true if the window expects the
|
|
1184
|
+
* window manager to give it the keyboard focus. Set it before relying on
|
|
1185
|
+
* focus at all — a window manager reading no input hint is entitled to
|
|
1186
|
+
* assume the window takes focus for itself.
|
|
1187
|
+
* - `initialState` is `'normal'` or `'iconic'` — the state to start in,
|
|
1188
|
+
* read once, when the window is first mapped.
|
|
1189
|
+
* - `urgent` is the attention flag: taskbars flash, some window managers
|
|
1190
|
+
* raise. Clear it when the user has looked.
|
|
1191
|
+
* - `icon` (an ntk `Pixmap` or an XID) is the ICCCM icon pixmap, which is
|
|
1192
|
+
* the old 1-bit-or-depth-matched mechanism. Modern desktops prefer
|
|
1193
|
+
* EWMH `_NET_WM_ICON`; set both if you care about old window managers.
|
|
1194
|
+
* - `windowGroup` names a window the others belong to — the group
|
|
1195
|
+
* `setTransientFor('root')` refers to.
|
|
1196
|
+
*
|
|
1197
|
+
* Like `setSizeHints`, this writes the whole struct rather than merging
|
|
1198
|
+
* with what an earlier call set; `setHints()` is the accumulating form.
|
|
1199
|
+
*/
|
|
1200
|
+
setWmHints(hints = {}) {
|
|
1201
|
+
const {
|
|
1202
|
+
input,
|
|
1203
|
+
initialState,
|
|
1204
|
+
urgent,
|
|
1205
|
+
icon,
|
|
1206
|
+
iconPixmap,
|
|
1207
|
+
iconMask,
|
|
1208
|
+
iconWindow,
|
|
1209
|
+
iconX,
|
|
1210
|
+
iconY,
|
|
1211
|
+
windowGroup
|
|
1212
|
+
} = hints;
|
|
1213
|
+
|
|
1214
|
+
// XWMHints: 9 CARD32s, flags first (ICCCM 4.1.2.4)
|
|
1215
|
+
const v = new Uint32Array(9);
|
|
1216
|
+
let flags = 0;
|
|
1217
|
+
if (input !== undefined) {
|
|
1218
|
+
flags |= WM_HINT.Input;
|
|
1219
|
+
v[1] = input ? 1 : 0;
|
|
1220
|
+
}
|
|
1221
|
+
if (initialState !== undefined) {
|
|
1222
|
+
flags |= WM_HINT.State;
|
|
1223
|
+
// NormalState 1, IconicState 3 (ICCCM 4.1.2.4); the states between
|
|
1224
|
+
// them were withdrawn from the spec
|
|
1225
|
+
v[2] = initialState === 'iconic' ? 3 : initialState === 'normal' ? 1 : initialState;
|
|
1226
|
+
}
|
|
1227
|
+
const pixmap = iconPixmap ?? icon;
|
|
1228
|
+
if (pixmap !== undefined) {
|
|
1229
|
+
flags |= WM_HINT.IconPixmap;
|
|
1230
|
+
v[3] = resourceId(pixmap);
|
|
1231
|
+
}
|
|
1232
|
+
if (iconWindow !== undefined) {
|
|
1233
|
+
flags |= WM_HINT.IconWindow;
|
|
1234
|
+
v[4] = resourceId(iconWindow);
|
|
1235
|
+
}
|
|
1236
|
+
if (iconX !== undefined || iconY !== undefined) {
|
|
1237
|
+
flags |= WM_HINT.IconPosition;
|
|
1238
|
+
v[5] = iconX ?? 0;
|
|
1239
|
+
v[6] = iconY ?? 0;
|
|
1240
|
+
}
|
|
1241
|
+
if (iconMask !== undefined) {
|
|
1242
|
+
flags |= WM_HINT.IconMask;
|
|
1243
|
+
v[7] = resourceId(iconMask);
|
|
1244
|
+
}
|
|
1245
|
+
if (windowGroup !== undefined) {
|
|
1246
|
+
flags |= WM_HINT.WindowGroup;
|
|
1247
|
+
v[8] = resourceId(windowGroup);
|
|
1248
|
+
}
|
|
1249
|
+
if (urgent) flags |= WM_HINT.Urgency;
|
|
1250
|
+
v[0] = flags;
|
|
1251
|
+
|
|
1252
|
+
// `urgent: false` is the one call that legitimately produces flags 0:
|
|
1253
|
+
// clearing attention means rewriting the struct without the bit. So the
|
|
1254
|
+
// test here is whether any key was understood, not whether a flag came
|
|
1255
|
+
// out of it.
|
|
1256
|
+
const keys = Object.keys(hints);
|
|
1257
|
+
const understood = keys.filter((k) => WM_HINT_KEYS.has(k));
|
|
1258
|
+
if (understood.length === 0) {
|
|
1259
|
+
warnHint(
|
|
1260
|
+
'wmHints-empty',
|
|
1261
|
+
`setWmHints({ ${keys.join(', ')} }) recognises none of those keys, so no WM_HINTS was written.`
|
|
1262
|
+
);
|
|
1263
|
+
return this;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
safeRelease(this.X, () => {
|
|
1267
|
+
this.X.ChangeProperty(
|
|
1268
|
+
0,
|
|
1269
|
+
this.id,
|
|
1270
|
+
this.X.atoms.WM_HINTS,
|
|
1271
|
+
this.X.atoms.WM_HINTS,
|
|
1272
|
+
32,
|
|
1273
|
+
Buffer.from(v.buffer, v.byteOffset, v.byteLength)
|
|
1274
|
+
);
|
|
1275
|
+
});
|
|
1276
|
+
return this;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* ICCCM WM_TRANSIENT_FOR — the window this one belongs to. It is what
|
|
1281
|
+
* makes a second top-level window a *dialog* rather than an unrelated
|
|
1282
|
+
* application window: the window manager stacks it above its owner, keeps
|
|
1283
|
+
* it out of the taskbar and pager, iconifies it alongside, places it
|
|
1284
|
+
* relative to the owner and gives it a dialog's reduced frame.
|
|
1285
|
+
*
|
|
1286
|
+
* Accepts a `Window`, an XID, `'root'` (transient for the whole window
|
|
1287
|
+
* group — see `setWmHints({ windowGroup })`), or `null` to clear.
|
|
1288
|
+
*
|
|
1289
|
+
* Both atoms involved are predefined, so this needs no round trip and is
|
|
1290
|
+
* on the wire before a `map()` on the next line — which is what ICCCM
|
|
1291
|
+
* 4.1.2.6 expects, since a window manager may read the property only when
|
|
1292
|
+
* the transient is mapped.
|
|
1293
|
+
*
|
|
1294
|
+
* Related but not the same as `setWindowType('dialog')`: this names *which*
|
|
1295
|
+
* window is the owner, the type says *what kind* of window this is. EWMH
|
|
1296
|
+
* treats a managed window with WM_TRANSIENT_FOR and no `_NET_WM_WINDOW_TYPE`
|
|
1297
|
+
* as a dialog, but setting any type at all turns that fallback off — so
|
|
1298
|
+
* real toolkits set both.
|
|
1299
|
+
*/
|
|
1300
|
+
setTransientFor(owner) {
|
|
1301
|
+
const X = this.X;
|
|
1302
|
+
if (owner == null) {
|
|
1303
|
+
safeRelease(X, () => X.DeleteProperty(this.id, X.atoms.WM_TRANSIENT_FOR));
|
|
1304
|
+
return this;
|
|
1305
|
+
}
|
|
1306
|
+
const id = owner === 'root' ? this.app.display.screen[0].root : resourceId(owner);
|
|
1307
|
+
if (this._overrideRedirect) {
|
|
1308
|
+
// ICCCM 4.1.2.6 contrasts the two mechanisms: this property is for
|
|
1309
|
+
// windows the WM manages, and an override-redirect window is never
|
|
1310
|
+
// managed, so the property just sits there
|
|
1311
|
+
warnHint(
|
|
1312
|
+
'transient-override-redirect',
|
|
1313
|
+
'setTransientFor on an override-redirect window has no effect — the window manager ' +
|
|
1314
|
+
'never sees the window, so nothing reads the property.'
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
safeRelease(X, () => {
|
|
1318
|
+
X.ChangeProperty(0, this.id, X.atoms.WM_TRANSIENT_FOR, X.atoms.WINDOW, 32, [id]);
|
|
1319
|
+
});
|
|
1320
|
+
return this;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/** The XID in WM_TRANSIENT_FOR, or null. The read side, for WM helpers. */
|
|
1324
|
+
async getTransientFor() {
|
|
1325
|
+
const v = await this.getProperty('WM_TRANSIENT_FOR', { as: 'numbers' }).catch(() => null);
|
|
1326
|
+
return v && v.length ? v[0] : null;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* ICCCM WM_PROTOCOLS — the messages this window is willing to receive, by
|
|
1331
|
+
* atom name: `'WM_DELETE_WINDOW'`, `'WM_TAKE_FOCUS'`, `'_NET_WM_PING'`,
|
|
1332
|
+
* `'_NET_WM_SYNC_REQUEST'`. Each arrives as a `'message'` event.
|
|
1333
|
+
*
|
|
1334
|
+
* Replaces the whole list. `addProtocol`/`removeProtocol` are the
|
|
1335
|
+
* accumulating forms, and the ones to reach for: the property is a *set*,
|
|
1336
|
+
* and a plain write of one atom silently drops the rest.
|
|
1337
|
+
*
|
|
1338
|
+
* @returns {Promise<Window>}
|
|
1339
|
+
*/
|
|
1340
|
+
async setProtocols(names) {
|
|
1341
|
+
const list = (Array.isArray(names) ? names : [names]).filter(Boolean);
|
|
1342
|
+
const atoms = await Promise.all(list.map((n) => this.atom(n)));
|
|
1343
|
+
this._protocols = new Set(atoms);
|
|
1344
|
+
return this._writeProtocols();
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Add one protocol to WM_PROTOCOLS, keeping the ones already there.
|
|
1349
|
+
* @returns {Promise<Window>}
|
|
1350
|
+
*/
|
|
1351
|
+
addProtocol(name) {
|
|
1352
|
+
return this._changeProtocols(name, true);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/** Remove one protocol from WM_PROTOCOLS. @returns {Promise<Window>} */
|
|
1356
|
+
removeProtocol(name) {
|
|
1357
|
+
return this._changeProtocols(name, false);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/** The atom names in WM_PROTOCOLS. @returns {Promise<string[]>} */
|
|
1361
|
+
async getProtocols() {
|
|
1362
|
+
const atoms = await this.getProperty('WM_PROTOCOLS', { as: 'numbers' }).catch(() => null);
|
|
1363
|
+
if (!atoms || !atoms.length) return [];
|
|
1364
|
+
return Promise.all(
|
|
1365
|
+
atoms.map(
|
|
1366
|
+
(a) =>
|
|
1367
|
+
new Promise((resolve) => this.X.GetAtomName(a, (err, name) => resolve(err ? null : name)))
|
|
1368
|
+
)
|
|
1369
|
+
).then((names) => names.filter(Boolean));
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* Read-modify-write on the atom set, serialized.
|
|
1374
|
+
*
|
|
1375
|
+
* Two adds in the same tick would otherwise each read the list before the
|
|
1376
|
+
* other wrote it, and the second would drop the first — the same clobber
|
|
1377
|
+
* this method exists to prevent, just harder to see.
|
|
1378
|
+
*/
|
|
1379
|
+
_changeProtocols(name, add) {
|
|
1380
|
+
const run = async () => {
|
|
1381
|
+
const [current, atom] = await Promise.all([this._loadProtocols(), this.atom(name)]);
|
|
1382
|
+
if (add) current.add(atom);
|
|
1383
|
+
else current.delete(atom);
|
|
1384
|
+
return this._writeProtocols();
|
|
1385
|
+
};
|
|
1386
|
+
this._protocolQueue = this._protocolQueue.then(run, run);
|
|
1387
|
+
return this._protocolQueue;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* The atom set as it stands on the server, read once.
|
|
1392
|
+
*
|
|
1393
|
+
* A window we created has none. A window adopted by id may already carry a
|
|
1394
|
+
* list its own client wrote, and replacing that is exactly the bug.
|
|
1395
|
+
*/
|
|
1396
|
+
async _loadProtocols() {
|
|
1397
|
+
if (this._protocols) return this._protocols;
|
|
1398
|
+
const current = await this.getProperty('WM_PROTOCOLS', { as: 'numbers' }).catch(() => null);
|
|
1399
|
+
this._protocols = new Set(current || []);
|
|
1400
|
+
return this._protocols;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
async _writeProtocols() {
|
|
1404
|
+
const property = await this.atom('WM_PROTOCOLS');
|
|
1405
|
+
if (this._destroyed) return this;
|
|
1406
|
+
const atoms = [...this._protocols];
|
|
1407
|
+
safeRelease(this.X, () => {
|
|
1408
|
+
this.X.ChangeProperty(0, this.id, property, this.X.atoms.ATOM, 32, atoms);
|
|
1409
|
+
});
|
|
1410
|
+
return this;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* EWMH `_NET_WM_PID` and ICCCM `WM_CLIENT_MACHINE` — which process on
|
|
1415
|
+
* which host owns this window. Together they are how a desktop offers to
|
|
1416
|
+
* force-quit an unresponsive application, and how `xkill`-style tools name
|
|
1417
|
+
* what they are about to kill; EWMH requires the machine for the pid to
|
|
1418
|
+
* mean anything, so both are written or neither is.
|
|
1419
|
+
*
|
|
1420
|
+
* Top-level windows get this automatically; pass `pid: false` at creation
|
|
1421
|
+
* to opt out. In a browser bundle there is no pid and no hostname, so the
|
|
1422
|
+
* call does nothing.
|
|
1423
|
+
*/
|
|
1424
|
+
setPid(pid, hostname) {
|
|
1425
|
+
const proc = globalThis.process;
|
|
1426
|
+
const id = pid ?? proc?.pid;
|
|
1427
|
+
const host = hostname ?? Window._hostname();
|
|
1428
|
+
if (id === undefined || !host) return this;
|
|
1429
|
+
const X = this.X;
|
|
1430
|
+
safeRelease(X, () => {
|
|
1431
|
+
X.ChangeProperty(
|
|
1432
|
+
0,
|
|
1433
|
+
this.id,
|
|
1434
|
+
X.atoms.WM_CLIENT_MACHINE,
|
|
1435
|
+
X.atoms.STRING,
|
|
1436
|
+
8,
|
|
1437
|
+
Buffer.from(String(host), 'latin1')
|
|
1438
|
+
);
|
|
1439
|
+
});
|
|
1440
|
+
return this._withAtoms(['_NET_WM_PID'], (atoms) => {
|
|
1441
|
+
safeRelease(X, () => {
|
|
1442
|
+
X.ChangeProperty(0, this.id, atoms._NET_WM_PID, X.atoms.CARDINAL, 32, [id]);
|
|
1443
|
+
});
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/** os.hostname(), looked up once, and absent in a browser bundle. */
|
|
1448
|
+
static _hostname() {
|
|
1449
|
+
if (Window._cachedHostname === undefined) {
|
|
1450
|
+
const os = globalThis.process?.getBuiltinModule?.('node:os');
|
|
1451
|
+
Window._cachedHostname = os?.hostname?.() ?? null;
|
|
1452
|
+
}
|
|
1453
|
+
return Window._cachedHostname;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* Set window manager hints by name, across whichever properties they
|
|
1458
|
+
* belong to, keeping the ones set before.
|
|
1459
|
+
*
|
|
1460
|
+
* wnd.setHints({ transientFor: main, maxWidth: 900, urgent: true });
|
|
1461
|
+
*
|
|
1462
|
+
* Every key `setSizeHints` and `setWmHints` understand works here, plus
|
|
1463
|
+
* `transientFor` and `protocols`. The same keys are accepted by
|
|
1464
|
+
* `createWindow`, at the top level or under `hints`.
|
|
1465
|
+
*
|
|
1466
|
+
* The difference from calling the two setters directly is that this one
|
|
1467
|
+
* remembers: each property is rewritten from everything set on this window
|
|
1468
|
+
* so far, so `setHints({ urgent: true })` after `setHints({ input: true })`
|
|
1469
|
+
* keeps the input hint. Those setters write their struct whole.
|
|
1470
|
+
*
|
|
1471
|
+
* Everything but `protocols` is on the wire when this returns — a
|
|
1472
|
+
* `map()` on the next line cannot overtake it. `protocols` needs the atom
|
|
1473
|
+
* interned first; await `setProtocols()` if the ordering matters.
|
|
1474
|
+
*/
|
|
1475
|
+
setHints(hints = {}) {
|
|
1476
|
+
const size = {};
|
|
1477
|
+
const wm = {};
|
|
1478
|
+
let sizeTouched = false;
|
|
1479
|
+
let wmTouched = false;
|
|
1480
|
+
const unknown = [];
|
|
1481
|
+
|
|
1482
|
+
for (const key of Object.keys(hints)) {
|
|
1483
|
+
if (SIZE_HINT_KEYS.has(key)) {
|
|
1484
|
+
size[key] = hints[key];
|
|
1485
|
+
sizeTouched = true;
|
|
1486
|
+
} else if (WM_HINT_KEYS.has(key)) {
|
|
1487
|
+
wm[key] = hints[key];
|
|
1488
|
+
wmTouched = true;
|
|
1489
|
+
} else if (key !== 'transientFor' && key !== 'protocols') {
|
|
1490
|
+
unknown.push(key);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (unknown.length) {
|
|
1494
|
+
warnHint(
|
|
1495
|
+
`hints-unknown-${unknown.join(',')}`,
|
|
1496
|
+
`setHints ignored unknown hint${unknown.length > 1 ? 's' : ''}: ${unknown.join(', ')}.`
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
if (hints.transientFor !== undefined) this.setTransientFor(hints.transientFor);
|
|
1501
|
+
if (sizeTouched) {
|
|
1502
|
+
this._sizeHints = { ...this._sizeHints, ...size };
|
|
1503
|
+
this.setSizeHints(this._sizeHints);
|
|
1504
|
+
}
|
|
1505
|
+
if (wmTouched) {
|
|
1506
|
+
this._wmHints = { ...this._wmHints, ...wm };
|
|
1507
|
+
this.setWmHints(this._wmHints);
|
|
1508
|
+
}
|
|
1509
|
+
if (hints.protocols !== undefined) {
|
|
1510
|
+
this.setProtocols(hints.protocols).catch((err) => this.app.options.onXError?.(err));
|
|
1511
|
+
}
|
|
1512
|
+
return this;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
973
1515
|
/**
|
|
974
1516
|
* ICCCM WM_CLASS — the instance/class pair taskbars and WMs use to group
|
|
975
1517
|
* windows, match icons and apply per-application rules. Two NUL-
|
|
@@ -1013,6 +1555,171 @@ export default class Window extends Drawable {
|
|
|
1013
1555
|
});
|
|
1014
1556
|
}
|
|
1015
1557
|
|
|
1558
|
+
/**
|
|
1559
|
+
* EWMH `_NET_WM_STATE` — fullscreen, maximized, sticky, skip-taskbar and
|
|
1560
|
+
* the rest of the states a window can be in.
|
|
1561
|
+
*
|
|
1562
|
+
* await wnd.setWmState('fullscreen'); // add, the default
|
|
1563
|
+
* await wnd.setWmState('maximized', 'add'); // both axes at once
|
|
1564
|
+
* await wnd.setWmState(['skip_taskbar', 'skip_pager'], 'add');
|
|
1565
|
+
* await wnd.setWmState('fullscreen', 'toggle');
|
|
1566
|
+
*
|
|
1567
|
+
* Names are the EWMH atoms without their `_NET_WM_STATE_` prefix, in
|
|
1568
|
+
* lower case; full atom names work too. `'maximized'` expands to the
|
|
1569
|
+
* `MAXIMIZED_VERT` + `MAXIMIZED_HORZ` pair, which is why the spec allows
|
|
1570
|
+
* two states per message.
|
|
1571
|
+
*
|
|
1572
|
+
* How the change is made depends on whether the window is mapped, and the
|
|
1573
|
+
* two are not interchangeable (EWMH 7.7): a mapped window *asks* the
|
|
1574
|
+
* window manager with a ClientMessage to the root, an unmapped one
|
|
1575
|
+
* *declares* its initial state by writing the property. This asks the
|
|
1576
|
+
* server which it is rather than trusting the last map/unmap event, so it
|
|
1577
|
+
* is right even on the line after `map()`.
|
|
1578
|
+
*
|
|
1579
|
+
* @param {string|string[]} names
|
|
1580
|
+
* @param {'add'|'remove'|'toggle'} [action]
|
|
1581
|
+
* @returns {Promise<boolean>} whether the window manager advertises every
|
|
1582
|
+
* state asked for in `_NET_SUPPORTED`. The request is made either way —
|
|
1583
|
+
* an unmapped window may legitimately declare a state before any window
|
|
1584
|
+
* manager is running — but `false` means nothing is listening for it.
|
|
1585
|
+
*/
|
|
1586
|
+
async setWmState(names, action = 'add') {
|
|
1587
|
+
const list = expandStateNames(names);
|
|
1588
|
+
if (!list.length) return false;
|
|
1589
|
+
if (list.length > 2) {
|
|
1590
|
+
// EWMH gives the message two atom slots and no more; splitting into
|
|
1591
|
+
// several messages is fine, but silently sending the first two is not
|
|
1592
|
+
throw new RangeError(
|
|
1593
|
+
`setWmState: a _NET_WM_STATE message carries at most 2 states, got ${list.length}`
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
const mode = { remove: 0, add: 1, toggle: 2 }[action];
|
|
1597
|
+
if (mode === undefined) {
|
|
1598
|
+
throw new TypeError(`setWmState: action must be add, remove or toggle, got ${action}`);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
const [atoms, supported, attrs] = await Promise.all([
|
|
1602
|
+
this._stateAtoms(list),
|
|
1603
|
+
this._netSupported(),
|
|
1604
|
+
this.getAttributes().catch(() => null)
|
|
1605
|
+
]);
|
|
1606
|
+
if (this._destroyed) return false;
|
|
1607
|
+
const ids = list.map((n) => atoms.get(n));
|
|
1608
|
+
// mapState: 0 Unmapped, 1 Unviewable, 2 Viewable. Unviewable is mapped
|
|
1609
|
+
// under an unmapped ancestor, which is still mapped as far as EWMH cares
|
|
1610
|
+
const mapped = attrs ? attrs.mapState !== 0 : this._mapped;
|
|
1611
|
+
|
|
1612
|
+
if (mapped) {
|
|
1613
|
+
const root = this.app.display.screen[0].root;
|
|
1614
|
+
const messageType = await this.atom('_NET_WM_STATE');
|
|
1615
|
+
if (this._destroyed) return false;
|
|
1616
|
+
safeRelease(this.X, () => {
|
|
1617
|
+
this.X.SendClientMessage(root, this.id, messageType, 32, [
|
|
1618
|
+
mode,
|
|
1619
|
+
ids[0],
|
|
1620
|
+
ids[1] ?? 0,
|
|
1621
|
+
1 // source indication: a normal application, not a pager
|
|
1622
|
+
]);
|
|
1623
|
+
});
|
|
1624
|
+
} else {
|
|
1625
|
+
await this._writeWmState(ids, mode);
|
|
1626
|
+
}
|
|
1627
|
+
return ids.every((id) => supported.has(id));
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/** `setWmState(names, 'add')`. @returns {Promise<boolean>} */
|
|
1631
|
+
addWmState(names) {
|
|
1632
|
+
return this.setWmState(names, 'add');
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/** `setWmState(names, 'remove')`. @returns {Promise<boolean>} */
|
|
1636
|
+
removeWmState(names) {
|
|
1637
|
+
return this.setWmState(names, 'remove');
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* The states currently in `_NET_WM_STATE`, as short lower-case names.
|
|
1642
|
+
* `[]` when the window has none.
|
|
1643
|
+
*
|
|
1644
|
+
* This is what the window manager put there, so it is the answer to "am I
|
|
1645
|
+
* actually fullscreen", where `setWmState` is only the request.
|
|
1646
|
+
*
|
|
1647
|
+
* @returns {Promise<string[]>}
|
|
1648
|
+
*/
|
|
1649
|
+
async getWmStates() {
|
|
1650
|
+
const ids = await this.getProperty('_NET_WM_STATE', { as: 'numbers' }).catch(() => null);
|
|
1651
|
+
if (!ids || !ids.length) return [];
|
|
1652
|
+
const known = await this._stateAtoms(EWMH_STATES);
|
|
1653
|
+
const byAtom = new Map([...known].map(([name, atom]) => [atom, name]));
|
|
1654
|
+
return Promise.all(
|
|
1655
|
+
ids.map(async (id) => {
|
|
1656
|
+
const name = byAtom.get(id);
|
|
1657
|
+
if (name) return name;
|
|
1658
|
+
// a state this build does not know about, or a vendor one
|
|
1659
|
+
const full = await new Promise((resolve) =>
|
|
1660
|
+
this.X.GetAtomName(id, (err, n) => resolve(err ? null : n))
|
|
1661
|
+
);
|
|
1662
|
+
return full ? stateShortName(full) : null;
|
|
1663
|
+
})
|
|
1664
|
+
).then((names) => names.filter(Boolean));
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* Read-modify-write on the atom list an unmapped window declares.
|
|
1669
|
+
*
|
|
1670
|
+
* `_NET_WM_STATE` is a list, so a plain Replace with one atom drops the
|
|
1671
|
+
* rest — the same trap WM_PROTOCOLS had. Serialized for the same reason:
|
|
1672
|
+
* two changes in a tick would each read the list before the other wrote.
|
|
1673
|
+
*/
|
|
1674
|
+
_writeWmState(ids, mode) {
|
|
1675
|
+
const run = async () => {
|
|
1676
|
+
const current = new Set(
|
|
1677
|
+
(await this.getProperty('_NET_WM_STATE', { as: 'numbers' }).catch(() => null)) || []
|
|
1678
|
+
);
|
|
1679
|
+
for (const id of ids) {
|
|
1680
|
+
const add = mode === 2 ? !current.has(id) : mode === 1;
|
|
1681
|
+
if (add) current.add(id);
|
|
1682
|
+
else current.delete(id);
|
|
1683
|
+
}
|
|
1684
|
+
const property = await this.atom('_NET_WM_STATE');
|
|
1685
|
+
if (this._destroyed) return this;
|
|
1686
|
+
safeRelease(this.X, () => {
|
|
1687
|
+
this.X.ChangeProperty(0, this.id, property, this.X.atoms.ATOM, 32, [...current]);
|
|
1688
|
+
});
|
|
1689
|
+
return this;
|
|
1690
|
+
};
|
|
1691
|
+
this._wmStateQueue = this._wmStateQueue.then(run, run);
|
|
1692
|
+
return this._wmStateQueue;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
/** Intern the atoms for a list of short state names. @returns {Promise<Map>} */
|
|
1696
|
+
async _stateAtoms(names) {
|
|
1697
|
+
const ids = await Promise.all(names.map((n) => this.atom(stateAtomName(n))));
|
|
1698
|
+
return new Map(names.map((n, i) => [n, ids[i]]));
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/**
|
|
1702
|
+
* The atoms in the root window's `_NET_SUPPORTED`, read once per
|
|
1703
|
+
* connection.
|
|
1704
|
+
*
|
|
1705
|
+
* It is one property on one window describing one window manager, so
|
|
1706
|
+
* re-reading it per window per call is pure round trips. A window manager
|
|
1707
|
+
* that restarts and advertises differently is the cost, and the same one
|
|
1708
|
+
* every toolkit accepts.
|
|
1709
|
+
*
|
|
1710
|
+
* @returns {Promise<Set<number>>}
|
|
1711
|
+
*/
|
|
1712
|
+
_netSupported() {
|
|
1713
|
+
if (!this.app._netSupportedPromise) {
|
|
1714
|
+
this.app._netSupportedPromise = this.app
|
|
1715
|
+
.rootWindow()
|
|
1716
|
+
.getProperty('_NET_SUPPORTED', { as: 'numbers' })
|
|
1717
|
+
.then((ids) => new Set(ids || []))
|
|
1718
|
+
.catch(() => new Set());
|
|
1719
|
+
}
|
|
1720
|
+
return this.app._netSupportedPromise;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1016
1723
|
/**
|
|
1017
1724
|
* Keep this window above normal windows.
|
|
1018
1725
|
*
|
|
@@ -1024,50 +1731,12 @@ export default class Window extends Drawable {
|
|
|
1024
1731
|
* parent is root — not our own id.
|
|
1025
1732
|
*/
|
|
1026
1733
|
setAlwaysOnTop(on = true) {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
safeRelease(X, () => {
|
|
1034
|
-
X.GetProperty(0, root, atoms._NET_SUPPORTED, X.atoms.ATOM, 0, 1024, (err, prop) => {
|
|
1035
|
-
if (err || this._destroyed) return;
|
|
1036
|
-
let supported = false;
|
|
1037
|
-
const data = prop?.data;
|
|
1038
|
-
for (let i = 0; data && i + 4 <= data.length; i += 4) {
|
|
1039
|
-
if (data.readUInt32LE(i) === atoms._NET_WM_STATE_ABOVE) {
|
|
1040
|
-
supported = true;
|
|
1041
|
-
break;
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
if (supported) {
|
|
1045
|
-
// a mapped window changes state by asking the WM, not by
|
|
1046
|
-
// writing the property (EWMH 7.7). SendEvent takes raw event
|
|
1047
|
-
// bytes, so pack the 32-byte ClientMessage by hand.
|
|
1048
|
-
const ev = Buffer.alloc(32);
|
|
1049
|
-
ev.writeUInt8(33, 0); // ClientMessage
|
|
1050
|
-
ev.writeUInt8(32, 1); // format
|
|
1051
|
-
ev.writeUInt32LE(this.id >>> 0, 4);
|
|
1052
|
-
ev.writeUInt32LE(atoms._NET_WM_STATE >>> 0, 8);
|
|
1053
|
-
ev.writeUInt32LE(on ? _NET_WM_STATE_ADD : _NET_WM_STATE_REMOVE, 12);
|
|
1054
|
-
ev.writeUInt32LE(atoms._NET_WM_STATE_ABOVE >>> 0, 16);
|
|
1055
|
-
ev.writeUInt32LE(0, 20); // second property: none
|
|
1056
|
-
ev.writeUInt32LE(1, 24); // source indication: application
|
|
1057
|
-
safeRelease(X, () => {
|
|
1058
|
-
X.SendEvent(
|
|
1059
|
-
root,
|
|
1060
|
-
false,
|
|
1061
|
-
x11.eventMask.SubstructureRedirect | x11.eventMask.SubstructureNotify,
|
|
1062
|
-
ev
|
|
1063
|
-
);
|
|
1064
|
-
});
|
|
1065
|
-
} else {
|
|
1066
|
-
this._appleWMSetLevel(on);
|
|
1067
|
-
}
|
|
1068
|
-
});
|
|
1069
|
-
});
|
|
1070
|
-
});
|
|
1734
|
+
this.setWmState('above', on ? 'add' : 'remove').then(
|
|
1735
|
+
(supported) => {
|
|
1736
|
+
if (!supported && !this._destroyed) this._appleWMSetLevel(on);
|
|
1737
|
+
},
|
|
1738
|
+
(err) => this.app.options.onXError?.(err)
|
|
1739
|
+
);
|
|
1071
1740
|
return this;
|
|
1072
1741
|
}
|
|
1073
1742
|
|
|
@@ -1286,6 +1955,27 @@ export default class Window extends Drawable {
|
|
|
1286
1955
|
return this;
|
|
1287
1956
|
}
|
|
1288
1957
|
|
|
1958
|
+
/**
|
|
1959
|
+
* Remove a property.
|
|
1960
|
+
*
|
|
1961
|
+
* Not the same as writing an empty value, and not at all the same as
|
|
1962
|
+
* writing a zero: a deleted property reads back as type None — which is
|
|
1963
|
+
* how "this client never declared that" is spelled — and a window manager
|
|
1964
|
+
* watching gets a PropertyNotify with state Delete. `setTransientFor(null)`
|
|
1965
|
+
* goes through here for exactly that reason; a `WM_TRANSIENT_FOR` of 0
|
|
1966
|
+
* would name window None as the owner rather than saying there is none.
|
|
1967
|
+
*
|
|
1968
|
+
* @returns {Promise<Window>}
|
|
1969
|
+
*/
|
|
1970
|
+
async deleteProperty(name) {
|
|
1971
|
+
const atom = await this.atom(name);
|
|
1972
|
+
if (this._destroyed) return this;
|
|
1973
|
+
safeRelease(this.X, () => {
|
|
1974
|
+
this.X.DeleteProperty(this.id, atom);
|
|
1975
|
+
});
|
|
1976
|
+
return this;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1289
1979
|
/**
|
|
1290
1980
|
* Intern an atom by name. node-x11 caches them per connection, so asking
|
|
1291
1981
|
* again for one already interned costs no round trip.
|
|
@@ -1310,16 +2000,27 @@ export default class Window extends Drawable {
|
|
|
1310
2000
|
|
|
1311
2001
|
/**
|
|
1312
2002
|
* WM_NORMAL_HINTS as an object shaped like setSizeHints' argument —
|
|
1313
|
-
* `{
|
|
1314
|
-
*
|
|
1315
|
-
* only if the client set its flag.
|
|
1316
|
-
* no hints, so callers can
|
|
2003
|
+
* `{ position, size, x, y, width, height, minWidth, minHeight, maxWidth,
|
|
2004
|
+
* maxHeight, widthInc, heightInc, baseWidth, baseHeight, minAspect,
|
|
2005
|
+
* maxAspect, gravity }`, each present only if the client set its flag.
|
|
2006
|
+
* Resolves to `{}` when the window has no hints, so callers can
|
|
2007
|
+
* destructure without a null check.
|
|
1317
2008
|
*/
|
|
1318
2009
|
async getSizeHints() {
|
|
1319
2010
|
const v = await this.getProperty('WM_NORMAL_HINTS', { as: 'numbers' }).catch(() => null);
|
|
1320
2011
|
if (!v || v.length < 18) return {};
|
|
1321
2012
|
const flags = v[0];
|
|
1322
2013
|
const hints = {};
|
|
2014
|
+
if (flags & (SIZE_HINT.USPosition | SIZE_HINT.PPosition)) {
|
|
2015
|
+
hints.position = flags & SIZE_HINT.USPosition ? 'user' : 'program';
|
|
2016
|
+
hints.x = v[1];
|
|
2017
|
+
hints.y = v[2];
|
|
2018
|
+
}
|
|
2019
|
+
if (flags & (SIZE_HINT.USSize | SIZE_HINT.PSize)) {
|
|
2020
|
+
hints.size = flags & SIZE_HINT.USSize ? 'user' : 'program';
|
|
2021
|
+
hints.width = v[3];
|
|
2022
|
+
hints.height = v[4];
|
|
2023
|
+
}
|
|
1323
2024
|
if (flags & 16) {
|
|
1324
2025
|
hints.minWidth = v[5];
|
|
1325
2026
|
hints.minHeight = v[6];
|
|
@@ -1344,6 +2045,30 @@ export default class Window extends Drawable {
|
|
|
1344
2045
|
return hints;
|
|
1345
2046
|
}
|
|
1346
2047
|
|
|
2048
|
+
/**
|
|
2049
|
+
* WM_HINTS as an object shaped like setWmHints' argument, each field
|
|
2050
|
+
* present only if the client set its flag. `{}` when the window has none.
|
|
2051
|
+
* `urgent` is reported whenever the property exists, since its absence is
|
|
2052
|
+
* meaningful — it is what "the user has looked" looks like.
|
|
2053
|
+
*/
|
|
2054
|
+
async getWmHints() {
|
|
2055
|
+
const v = await this.getProperty('WM_HINTS', { as: 'numbers' }).catch(() => null);
|
|
2056
|
+
if (!v || v.length < 9) return {};
|
|
2057
|
+
const flags = v[0];
|
|
2058
|
+
const hints = { urgent: !!(flags & WM_HINT.Urgency) };
|
|
2059
|
+
if (flags & WM_HINT.Input) hints.input = !!v[1];
|
|
2060
|
+
if (flags & WM_HINT.State) hints.initialState = v[2] === 3 ? 'iconic' : 'normal';
|
|
2061
|
+
if (flags & WM_HINT.IconPixmap) hints.iconPixmap = v[3];
|
|
2062
|
+
if (flags & WM_HINT.IconWindow) hints.iconWindow = v[4];
|
|
2063
|
+
if (flags & WM_HINT.IconPosition) {
|
|
2064
|
+
hints.iconX = v[5];
|
|
2065
|
+
hints.iconY = v[6];
|
|
2066
|
+
}
|
|
2067
|
+
if (flags & WM_HINT.IconMask) hints.iconMask = v[7];
|
|
2068
|
+
if (flags & WM_HINT.WindowGroup) hints.windowGroup = v[8];
|
|
2069
|
+
return hints;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
1347
2072
|
/**
|
|
1348
2073
|
* GetWindowAttributes — `{ mapState, overrideRedirect, ... }`. A window
|
|
1349
2074
|
* manager adopting the windows that already existed when it started
|
|
@@ -1419,19 +2144,19 @@ export default class Window extends Drawable {
|
|
|
1419
2144
|
height = this.height,
|
|
1420
2145
|
borderWidth = 0
|
|
1421
2146
|
} = geometry;
|
|
1422
|
-
const ev = Buffer.alloc(32);
|
|
1423
|
-
ev[0] = 22; // ConfigureNotify
|
|
1424
|
-
ev.writeUInt32LE(this.id >>> 0, 4); // event window
|
|
1425
|
-
ev.writeUInt32LE(this.id >>> 0, 8); // the window itself
|
|
1426
|
-
ev.writeUInt32LE(0, 12); // above-sibling: None
|
|
1427
|
-
ev.writeInt16LE(x, 16);
|
|
1428
|
-
ev.writeInt16LE(y, 18);
|
|
1429
|
-
ev.writeUInt16LE(width, 20);
|
|
1430
|
-
ev.writeUInt16LE(height, 22);
|
|
1431
|
-
ev.writeUInt16LE(borderWidth, 24);
|
|
1432
|
-
ev[26] = 0; // override-redirect
|
|
1433
2147
|
safeRelease(this.X, () =>
|
|
1434
|
-
this.X.SendEvent(this.id, 0, x11.eventMask.StructureNotify,
|
|
2148
|
+
this.X.SendEvent(this.id, 0, x11.eventMask.StructureNotify, {
|
|
2149
|
+
name: 'ConfigureNotify',
|
|
2150
|
+
wid: this.id, // event window
|
|
2151
|
+
wid1: this.id, // the window the event is about
|
|
2152
|
+
aboveSibling: 0, // None: bottom of the stack
|
|
2153
|
+
x,
|
|
2154
|
+
y,
|
|
2155
|
+
width,
|
|
2156
|
+
height,
|
|
2157
|
+
borderWidth,
|
|
2158
|
+
overrideRedirect: 0
|
|
2159
|
+
})
|
|
1435
2160
|
);
|
|
1436
2161
|
return this;
|
|
1437
2162
|
}
|
|
@@ -1456,14 +2181,12 @@ export default class Window extends Drawable {
|
|
|
1456
2181
|
return false;
|
|
1457
2182
|
}
|
|
1458
2183
|
const wmProtocols = await this.atom('WM_PROTOCOLS');
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
ev.writeUInt32LE(0, 16); // CurrentTime
|
|
1466
|
-
safeRelease(this.X, () => this.X.SendEvent(this.id, 0, 0, ev));
|
|
2184
|
+
// mask 0: deliver to the client that created the window, not to whoever
|
|
2185
|
+
// selected events on it — a WM_PROTOCOLS message is addressed to the
|
|
2186
|
+
// owner, so it must not go out with the EWMH substructure default
|
|
2187
|
+
safeRelease(this.X, () =>
|
|
2188
|
+
this.X.SendClientMessage(this.id, this.id, wmProtocols, 32, [deleteAtom, 0 /* CurrentTime */], 0)
|
|
2189
|
+
);
|
|
1467
2190
|
return true;
|
|
1468
2191
|
}
|
|
1469
2192
|
|
|
@@ -1535,30 +2258,19 @@ export default class Window extends Drawable {
|
|
|
1535
2258
|
return new Window(this.app, { parent: this, ...params });
|
|
1536
2259
|
}
|
|
1537
2260
|
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
2261
|
+
/**
|
|
2262
|
+
* Opt in to the WM_DELETE_WINDOW protocol: the window manager sends a
|
|
2263
|
+
* 'message' event instead of killing the connection when the user closes
|
|
2264
|
+
* the window.
|
|
2265
|
+
*
|
|
2266
|
+
* Kept for compatibility, and now an alias — it used to write
|
|
2267
|
+
* WM_PROTOCOLS as a list of exactly one atom, so the next protocol added
|
|
2268
|
+
* by any other means erased it. Prefer `addProtocol('WM_DELETE_WINDOW')`,
|
|
2269
|
+
* which is awaitable.
|
|
2270
|
+
*/
|
|
1541
2271
|
setActions() {
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
// same deferred-chain shape as setTitle: each step runs from a reply
|
|
1545
|
-
// callback and must no-op instead of throwing if the connection
|
|
1546
|
-
// started closing (or the window died) while the replies were in flight
|
|
1547
|
-
safeRelease(X, () => {
|
|
1548
|
-
X.InternAtom(false, 'WM_PROTOCOLS', (err, WM_PROTOCOLS) => {
|
|
1549
|
-
if (err) return;
|
|
1550
|
-
safeRelease(X, () => {
|
|
1551
|
-
X.InternAtom(false, 'WM_DELETE_WINDOW', (err2, WM_DELETE_WINDOW) => {
|
|
1552
|
-
if (err2 || this._destroyed) return;
|
|
1553
|
-
const data = Buffer.alloc(4);
|
|
1554
|
-
data.writeUInt32LE(WM_DELETE_WINDOW, 0);
|
|
1555
|
-
safeRelease(X, () => {
|
|
1556
|
-
X.ChangeProperty(0, wid, WM_PROTOCOLS, X.atoms.ATOM, 32, data);
|
|
1557
|
-
});
|
|
1558
|
-
});
|
|
1559
|
-
});
|
|
1560
|
-
});
|
|
1561
|
-
});
|
|
2272
|
+
this.addProtocol('WM_DELETE_WINDOW').catch((err) => this.app.options.onXError?.(err));
|
|
2273
|
+
return this;
|
|
1562
2274
|
}
|
|
1563
2275
|
|
|
1564
2276
|
destroy() {
|