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/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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
package/lib/keyboard.js
ADDED
|
@@ -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
|
+
}
|