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 CHANGED
@@ -17,6 +17,21 @@ export default class App {
17
17
  * instead of querying the server for one) }
18
18
  */
19
19
  constructor(display, options = {}) {
20
+ // ntk decodes property payloads and atom lists with little-endian
21
+ // readers. That is not an assumption about the host: node-x11 declares a
22
+ // byte order in its connection hello (`display.byte_order`, 0 LSBFirst /
23
+ // 1 MSBFirst, x11 >= 3.4) and then encodes every request LSBFirst
24
+ // regardless — so on a big-endian host the declared order and the actual
25
+ // encoding disagree and the connection is garbage from its first
26
+ // request, well before anything reaches us. Say so, rather than
27
+ // rendering nonsense. (An older x11 leaves the field undefined, which
28
+ // reads as LSBFirst and is right everywhere node-x11 currently works.)
29
+ if (display.byte_order) {
30
+ throw new Error(
31
+ 'ntk: this X connection is MSBFirst (big-endian); node-x11 encodes requests ' +
32
+ 'LSBFirst only, so the connection cannot be used'
33
+ );
34
+ }
20
35
  this.display = display;
21
36
  this.X = display.client;
22
37
  this.options = options;
package/lib/clipboard.js CHANGED
@@ -27,21 +27,6 @@ import { safeRelease } from './cleanup.js';
27
27
 
28
28
  const DEFAULT_TIMEOUT = 2000;
29
29
 
30
- // SendEvent takes the raw 32-byte wire form of the event to deliver and
31
- // node-x11 has no packer for outgoing events, so build SelectionNotify
32
- // (code 31) by hand: CARD8 code, pad, CARD16 sequence (filled server-side),
33
- // TIMESTAMP, requestor WINDOW, then selection/target/property ATOMs.
34
- function encodeSelectionNotify(time, requestor, selection, target, property) {
35
- const b = Buffer.alloc(32);
36
- b[0] = 31;
37
- b.writeUInt32LE(time >>> 0, 4);
38
- b.writeUInt32LE(requestor >>> 0, 8);
39
- b.writeUInt32LE(selection >>> 0, 12);
40
- b.writeUInt32LE(target >>> 0, 16);
41
- b.writeUInt32LE(property >>> 0, 20);
42
- return b;
43
- }
44
-
45
30
  export default class Clipboard {
46
31
  constructor(app) {
47
32
  this.app = app;
@@ -192,11 +177,13 @@ export default class Clipboard {
192
177
  if (text === undefined) {
193
178
  property = 0; // raced with a SelectionClear we haven't seen yet
194
179
  } else if (ev.target === a.TARGETS) {
195
- const data = Buffer.alloc(12);
196
- data.writeUInt32LE(a.TARGETS, 0);
197
- data.writeUInt32LE(a.UTF8_STRING, 4);
198
- data.writeUInt32LE(X.atoms.STRING, 8);
199
- X.ChangeProperty(0, ev.requestor, property, X.atoms.ATOM, 32, data);
180
+ // x11 >= 3.4 encodes a number array at the property's declared
181
+ // format, so this reaches the requestor as three CARD32 atoms
182
+ X.ChangeProperty(0, ev.requestor, property, X.atoms.ATOM, 32, [
183
+ a.TARGETS,
184
+ a.UTF8_STRING,
185
+ X.atoms.STRING
186
+ ]);
200
187
  } else if (ev.target === a.UTF8_STRING) {
201
188
  X.ChangeProperty(0, ev.requestor, property, a.UTF8_STRING, 8, Buffer.from(text, 'utf8'));
202
189
  } else if (ev.target === X.atoms.STRING) {
@@ -208,12 +195,17 @@ export default class Clipboard {
208
195
  // with property None per ICCCM
209
196
  property = 0;
210
197
  }
211
- X.SendEvent(
212
- ev.requestor,
213
- 0,
214
- 0,
215
- encodeSelectionNotify(ev.time, ev.requestor, ev.selection, ev.target, property)
216
- );
198
+ // mask 0: SelectionNotify is addressed to the requestor itself, so it
199
+ // goes to the client that created that window rather than to whoever
200
+ // selected events on it (ICCCM 2.2)
201
+ X.SendEvent(ev.requestor, 0, 0, {
202
+ name: 'SelectionNotify',
203
+ time: ev.time,
204
+ requestor: ev.requestor,
205
+ selection: ev.selection,
206
+ target: ev.target,
207
+ property
208
+ });
217
209
  });
218
210
  }
219
211
 
package/lib/events_map.js CHANGED
@@ -64,6 +64,9 @@ export const mask = {
64
64
  destroy: x11.eventMask.StructureNotify,
65
65
  keyup: x11.eventMask.KeyRelease,
66
66
  property: x11.eventMask.PropertyChange,
67
+ // not an X event of its own: Window derives it from the PropertyNotify for
68
+ // _NET_WM_STATE, so it needs the same mask a 'property' listener does
69
+ statechange: x11.eventMask.PropertyChange,
67
70
  selection: 0,
68
71
  selection_request: 0,
69
72
  message: 0
package/lib/index.js CHANGED
@@ -4,6 +4,7 @@ import App from './app.js';
4
4
  import Clipboard from './clipboard.js';
5
5
  import { CursorCache, cursorShapes, resolveCursorShape } from './cursor.js';
6
6
  import Window from './window.js';
7
+ import { decodeKey, groupForState } from './keyboard.js';
7
8
  import Pixmap from './pixmap.js';
8
9
  import Picture from './picture.js';
9
10
  import { Image, decodeImage, loadImage } from './image.js';
@@ -138,7 +139,9 @@ export {
138
139
  cssColor,
139
140
  cssColorStraight,
140
141
  premultiply,
141
- cssLength
142
+ cssLength,
143
+ decodeKey,
144
+ groupForState
142
145
  };
143
146
  // the yoga-layout instance ntk lays HtmlView out with — downstream layout
144
147
  // consumers (e.g. the react-x11 renderer) must share it to avoid loading a
@@ -0,0 +1,114 @@
1
+ import { keysymToUnicode } from './text/keysym-unicode.js';
2
+
3
+ // Turning a key event into a character.
4
+ //
5
+ // X's core keyboard map is a flat list of keysyms per keycode, and XKB lays
6
+ // its groups over that list in pairs: [g1l1, g1l2, g2l1, g2l2, ...]. A layout
7
+ // switch moves the *active group*, which arrives in bits 13-14 of every key
8
+ // event's state field — the keymap itself does not change, so no
9
+ // MappingNotify is sent and nothing can be refetched in response. Reading
10
+ // those two bits is the whole of layout support for the common case.
11
+ //
12
+ // What this cannot do is level 3, the AltGr row. The core map is ambiguous
13
+ // there: four keysyms on a keycode are two groups of two levels under
14
+ // `us,ru`, and one group of four levels under `us(intl)`, and nothing in the
15
+ // core protocol distinguishes them. The request that resolves it is
16
+ // XkbGetMap, which node-x11 does not implement — its xkb module says so in
17
+ // as many words. Guessing would break multi-layout users to half-serve
18
+ // AltGr users, so this reads groups only.
19
+
20
+ const SHIFT_MASK = 1;
21
+ const LOCK_MASK = 2;
22
+
23
+ // XKB reports the effective group in bits 13-14 of the core state field
24
+ const GROUP_SHIFT = 13;
25
+ const GROUP_MASK = 3;
26
+
27
+ // Pairs, because that is all the core keyboard map can express
28
+ const LEVELS_PER_GROUP = 2;
29
+
30
+ /** The active XKB layout group, 0-3, from a key event's state field. */
31
+ export function groupForState(state) {
32
+ return (state >> GROUP_SHIFT) & GROUP_MASK;
33
+ }
34
+
35
+ /** NoSymbol is 0, and a short keysym list simply ends. */
36
+ function present(sym) {
37
+ return sym !== undefined && sym !== 0;
38
+ }
39
+
40
+ /**
41
+ * The character a keysym types, or undefined for keys that type nothing —
42
+ * arrows, function keys, modifiers.
43
+ */
44
+ function charOf(sym) {
45
+ const cp = present(sym) ? keysymToUnicode(sym) : undefined;
46
+ return cp === undefined ? undefined : String.fromCodePoint(cp);
47
+ }
48
+
49
+ /**
50
+ * Whether a key's two levels are the lower and upper case of one letter.
51
+ *
52
+ * This is what decides whether CapsLock applies. XKB asks the key's *type*:
53
+ * its ALPHABETIC type folds Lock into the level, and the two-level type used
54
+ * for the number row does not — which is why CapsLock does not turn `1` into
55
+ * `!` on any real desktop. Unicode case mapping answers the same question
56
+ * without the XKB map, and answers it for Cyrillic and Greek too.
57
+ */
58
+ function isCasePair(lowerChar, upperChar) {
59
+ if (lowerChar === undefined || upperChar === undefined) return false;
60
+ return lowerChar !== upperChar && lowerChar.toUpperCase() === upperChar;
61
+ }
62
+
63
+ /**
64
+ * Decode a key event against the keyboard map.
65
+ *
66
+ * @param {number[]} syms the keycode's keysyms, as `GetKeyboardMapping`
67
+ * returns them — `X.keycode2keysyms[ev.keycode]`
68
+ * @param {number} state the event's modifier/group state (`ev.buttons`)
69
+ * @returns {{ keysym: number, baseKeysym: number, group: number,
70
+ * codepoint: number|undefined }|undefined} undefined when the keycode maps
71
+ * to nothing at all
72
+ */
73
+ export function decodeKey(syms, state) {
74
+ if (!syms || syms.length === 0) return undefined;
75
+
76
+ const group = groupForState(state);
77
+ let base = group * LEVELS_PER_GROUP;
78
+ // X core protocol: a group with no symbols on this key falls back to
79
+ // group 1. Layouts differ in which keys they define, so this is normal —
80
+ // a Cyrillic layout leaves the function row to the Latin one.
81
+ if (!present(syms[base]) && !present(syms[base + 1])) base = 0;
82
+
83
+ const first = syms[base];
84
+ if (!present(first)) return undefined;
85
+ const firstChar = charOf(first);
86
+
87
+ let second = syms[base + 1];
88
+ let secondChar = charOf(second);
89
+ if (!present(second)) {
90
+ // "if the second element is NoSymbol, the group is treated as the lower
91
+ // and upper case of the first when the first has a case, and as the first
92
+ // twice otherwise" — X core protocol, Keyboard and Pointer. There is no
93
+ // uppercase *keysym* to name in that case, only an uppercase character.
94
+ second = first;
95
+ const upper = firstChar === undefined ? undefined : firstChar.toUpperCase();
96
+ secondChar = upper !== undefined && upper.length === 1 ? upper : firstChar;
97
+ }
98
+
99
+ const shift = (state & SHIFT_MASK) !== 0;
100
+ const lock = (state & LOCK_MASK) !== 0 && isCasePair(firstChar, secondChar);
101
+ const upperLevel = shift !== lock;
102
+
103
+ const keysym = upperLevel ? second : first;
104
+ const char = upperLevel ? secondChar : firstChar;
105
+ return {
106
+ keysym,
107
+ // group 1, level 1 — what a keyboard shortcut should match against, so
108
+ // Ctrl+Z stays Ctrl+Z while the user is typing Cyrillic. GTK, Qt and
109
+ // browsers all resolve accelerators this way.
110
+ baseKeysym: present(syms[0]) ? syms[0] : keysym,
111
+ group,
112
+ codepoint: char === undefined ? undefined : char.codePointAt(0)
113
+ };
114
+ }