ntk 4.0.0 → 4.2.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/cursor.js CHANGED
@@ -23,10 +23,24 @@ export const cursorShapes = {
23
23
  'not-allowed': 0 // XC_X_cursor
24
24
  };
25
25
 
26
+ /**
27
+ * The invisible cursor, spelled as CSS spells it. It is a name but not a
28
+ * shape: the 'cursor' font has no glyph meaning "no cursor", so this one is
29
+ * built from an empty bitmap instead of looked up. Kept out of
30
+ * `cursorShapes` for exactly that reason — every value in there is a glyph
31
+ * index, and this has none.
32
+ */
33
+ export const BLANK_CURSOR = 'none';
34
+
35
+ /** Every name `setCursor` accepts, for the error message and for docs. */
36
+ export const cursorNames = [...Object.keys(cursorShapes), BLANK_CURSOR];
37
+
26
38
  /**
27
39
  * Resolve a friendly cursor name (see `cursorShapes`) or a raw cursor-font
28
40
  * glyph index to the glyph index. Throws on unknown names so typos surface
29
- * immediately instead of silently keeping the old cursor.
41
+ * immediately instead of silently keeping the old cursor. `'none'` is a
42
+ * valid cursor name but not a glyph, so it never reaches here — see
43
+ * `CursorCache.get`.
30
44
  */
31
45
  export function resolveCursorShape(nameOrShape) {
32
46
  if (typeof nameOrShape === 'number') {
@@ -38,7 +52,7 @@ export function resolveCursorShape(nameOrShape) {
38
52
  const shape = cursorShapes[nameOrShape];
39
53
  if (shape === undefined) {
40
54
  throw new Error(
41
- `unknown cursor name '${nameOrShape}' — valid names: ${Object.keys(cursorShapes).join(', ')}`
55
+ `unknown cursor name '${nameOrShape}' — valid names: ${cursorNames.join(', ')}`
42
56
  );
43
57
  }
44
58
  return shape;
@@ -56,10 +70,12 @@ export class CursorCache {
56
70
  this.X = app.X;
57
71
  this._font = 0; // the 'cursor' font, opened on first use
58
72
  this._byShape = new Map(); // glyph index -> cursor xid
73
+ this._blank = 0; // the invisible cursor, built on first use
59
74
  }
60
75
 
61
76
  /** cursor xid for a friendly name or raw glyph index, creating it on first use */
62
77
  get(nameOrShape) {
78
+ if (nameOrShape === BLANK_CURSOR) return this.blank();
63
79
  const shape = resolveCursorShape(nameOrShape);
64
80
  const cached = this._byShape.get(shape);
65
81
  if (cached) return cached;
@@ -79,6 +95,41 @@ export class CursorCache {
79
95
  return cid;
80
96
  }
81
97
 
98
+ /**
99
+ * The invisible cursor, created once per connection. Hiding the pointer is
100
+ * not the same thing as X's cursor None, which means *inherit the parent's*
101
+ * — for a top-level window that is the root's cursor, so the pointer stays
102
+ * on screen. A cursor whose mask is empty is the way to hide it: mask bits
103
+ * that are 0 are not drawn, so a 1x1 all-zero mask draws nothing at all.
104
+ */
105
+ blank() {
106
+ if (this._blank) return this._blank;
107
+ const X = this.X;
108
+ const pid = X.AllocID();
109
+ // depth 1 — CreateCursor takes bitmaps, not pixmaps of the window depth
110
+ X.CreatePixmap(pid, X.display.screen[0].root, 1, 1, 1);
111
+ // a freshly created pixmap's contents are *undefined* per the protocol,
112
+ // so it has to be cleared. Skipping this is how the recipe ends up
113
+ // hiding the cursor on one server and drawing a stray pixel on another
114
+ const gc = X.AllocID();
115
+ X.CreateGC(gc, pid, { foreground: 0 });
116
+ X.PolyFillRectangle(pid, gc, [0, 0, 1, 1]);
117
+ X.FreeGC(gc);
118
+ X.ReleaseID(gc);
119
+
120
+ const cid = X.AllocID();
121
+ const black = { R: 0, G: 0, B: 0 };
122
+ // source and mask are the same empty bitmap; the colours are immaterial
123
+ // when nothing is drawn, but the request needs them
124
+ X.CreateCursor(cid, pid, pid, black, black, 0, 0);
125
+ // the cursor keeps its own copy of the bitmaps, so the pixmap goes now
126
+ // rather than living as long as the connection
127
+ X.FreePixmap(pid);
128
+ X.ReleaseID(pid);
129
+ this._blank = cid;
130
+ return cid;
131
+ }
132
+
82
133
  /** free the server-side cursors (and the font); safe to call repeatedly */
83
134
  dispose() {
84
135
  const X = this.X;
@@ -89,6 +140,14 @@ export class CursorCache {
89
140
  });
90
141
  }
91
142
  this._byShape.clear();
143
+ if (this._blank) {
144
+ const cid = this._blank;
145
+ this._blank = 0;
146
+ safeRelease(X, () => {
147
+ X.FreeCursor(cid);
148
+ X.ReleaseID(cid);
149
+ });
150
+ }
92
151
  if (this._font) {
93
152
  const fid = this._font;
94
153
  this._font = 0;
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
@@ -2,8 +2,9 @@ import x11 from 'x11';
2
2
 
3
3
  import App from './app.js';
4
4
  import Clipboard from './clipboard.js';
5
- import { CursorCache, cursorShapes, resolveCursorShape } from './cursor.js';
5
+ import { BLANK_CURSOR, CursorCache, cursorNames, 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';
@@ -110,7 +111,9 @@ export {
110
111
  App,
111
112
  Clipboard,
112
113
  Window,
114
+ BLANK_CURSOR,
113
115
  CursorCache,
116
+ cursorNames,
114
117
  cursorShapes,
115
118
  resolveCursorShape,
116
119
  Pixmap,
@@ -138,7 +141,9 @@ export {
138
141
  cssColor,
139
142
  cssColorStraight,
140
143
  premultiply,
141
- cssLength
144
+ cssLength,
145
+ decodeKey,
146
+ groupForState
142
147
  };
143
148
  // the yoga-layout instance ntk lays HtmlView out with — downstream layout
144
149
  // 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
+ }