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 +15 -0
- package/lib/clipboard.js +18 -26
- package/lib/cursor.js +61 -2
- package/lib/events_map.js +3 -0
- package/lib/index.js +7 -2
- package/lib/keyboard.js +114 -0
- package/lib/text/keysym-unicode.js +1186 -0
- package/lib/window.js +842 -124
- 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/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: ${
|
|
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
|
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
|
+
}
|