@novnc/novnc 1.3.0 → 1.4.0-beta

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.
Files changed (64) hide show
  1. package/AUTHORS +2 -2
  2. package/LICENSE.txt +1 -1
  3. package/README.md +23 -7
  4. package/core/decoders/jpeg.js +141 -0
  5. package/core/decoders/raw.js +1 -1
  6. package/core/decoders/zrle.js +185 -0
  7. package/core/des.js +1 -1
  8. package/core/display.js +12 -0
  9. package/core/encodings.js +4 -0
  10. package/core/input/keyboard.js +10 -0
  11. package/core/ra2.js +567 -0
  12. package/core/rfb.js +469 -84
  13. package/core/util/browser.js +56 -7
  14. package/core/util/cursor.js +4 -0
  15. package/core/util/md5.js +79 -0
  16. package/docs/API.md +318 -157
  17. package/lib/base64.js +20 -34
  18. package/lib/decoders/copyrect.js +5 -12
  19. package/lib/decoders/hextile.js +17 -47
  20. package/lib/decoders/jpeg.js +149 -0
  21. package/lib/decoders/raw.js +10 -23
  22. package/lib/decoders/rre.js +5 -16
  23. package/lib/decoders/tight.js +13 -79
  24. package/lib/decoders/tightpng.js +8 -28
  25. package/lib/decoders/zrle.js +188 -0
  26. package/lib/deflator.js +9 -23
  27. package/lib/des.js +24 -37
  28. package/lib/display.js +62 -108
  29. package/lib/encodings.js +7 -8
  30. package/lib/inflator.js +6 -19
  31. package/lib/input/domkeytable.js +77 -48
  32. package/lib/input/fixedkeys.js +8 -3
  33. package/lib/input/gesturehandler.js +86 -153
  34. package/lib/input/keyboard.js +62 -91
  35. package/lib/input/keysym.js +14 -270
  36. package/lib/input/keysymdef.js +5 -7
  37. package/lib/input/util.js +43 -85
  38. package/lib/input/vkeys.js +0 -3
  39. package/lib/input/xtscancodes.js +1 -168
  40. package/lib/ra2.js +1005 -0
  41. package/lib/rfb.js +795 -923
  42. package/lib/util/browser.js +66 -29
  43. package/lib/util/cursor.js +29 -66
  44. package/lib/util/element.js +3 -5
  45. package/lib/util/events.js +23 -30
  46. package/lib/util/eventtarget.js +5 -14
  47. package/lib/util/int.js +1 -2
  48. package/lib/util/logging.js +1 -19
  49. package/lib/util/md5.js +77 -0
  50. package/lib/util/strings.js +3 -5
  51. package/lib/vendor/pako/lib/utils/common.js +8 -17
  52. package/lib/vendor/pako/lib/zlib/adler32.js +3 -7
  53. package/lib/vendor/pako/lib/zlib/constants.js +2 -5
  54. package/lib/vendor/pako/lib/zlib/crc32.js +5 -12
  55. package/lib/vendor/pako/lib/zlib/deflate.js +213 -618
  56. package/lib/vendor/pako/lib/zlib/gzheader.js +1 -13
  57. package/lib/vendor/pako/lib/zlib/inffast.js +60 -176
  58. package/lib/vendor/pako/lib/zlib/inflate.js +398 -888
  59. package/lib/vendor/pako/lib/zlib/inftrees.js +63 -169
  60. package/lib/vendor/pako/lib/zlib/messages.js +1 -11
  61. package/lib/vendor/pako/lib/zlib/trees.js +246 -588
  62. package/lib/vendor/pako/lib/zlib/zstream.js +2 -18
  63. package/lib/websock.js +37 -88
  64. package/package.json +32 -35
package/AUTHORS CHANGED
@@ -1,9 +1,9 @@
1
1
  maintainers:
2
- - Joel Martin (@kanaka)
3
- - Solly Ross (@directxman12)
4
2
  - Samuel Mannehed for Cendio AB (@samhed)
5
3
  - Pierre Ossman for Cendio AB (@CendioOssman)
6
4
  maintainersEmeritus:
5
+ - Joel Martin (@kanaka)
6
+ - Solly Ross (@directxman12)
7
7
  - @astrand
8
8
  contributors:
9
9
  # There are a bunch of people that should be here.
package/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- noVNC is Copyright (C) 2019 The noVNC Authors
1
+ noVNC is Copyright (C) 2022 The noVNC Authors
2
2
  (./AUTHORS)
3
3
 
4
4
  The noVNC core library files are licensed under the MPL 2.0 (Mozilla
package/README.md CHANGED
@@ -65,10 +65,14 @@ Please tweet [@noVNC](http://www.twitter.com/noVNC) if you do.
65
65
  ### Features
66
66
 
67
67
  * Supports all modern browsers including mobile (iOS, Android)
68
- * Supported VNC encodings: raw, copyrect, rre, hextile, tight, tightPNG
68
+ * Supported authentication methods: none, classical VNC, RealVNC's
69
+ RSA-AES, Tight, VeNCrypt Plain, XVP, Apple's Diffie-Hellman,
70
+ UltraVNC's MSLogonII
71
+ * Supported VNC encodings: raw, copyrect, rre, hextile, tight, tightPNG,
72
+ ZRLE, JPEG
69
73
  * Supports scaling, clipping and resizing the desktop
70
74
  * Local cursor rendering
71
- * Clipboard copy/paste
75
+ * Clipboard copy/paste with full Unicode support
72
76
  * Translations
73
77
  * Touch gestures for emulating common mouse actions
74
78
  * Licensed mainly under the [MPL 2.0](http://www.mozilla.org/MPL/2.0/), see
@@ -113,8 +117,13 @@ proxy.
113
117
  used to specify the location of a running VNC server:
114
118
 
115
119
  `./utils/novnc_proxy --vnc localhost:5901`
120
+
121
+ * If you don't need to expose the web server to public internet, you can
122
+ bind to localhost:
123
+
124
+ `./utils/novnc_proxy --vnc localhost:5901 --listen localhost:6081`
116
125
 
117
- * Point your browser to the cut-and-paste URL that is output by the `novnc_procy`
126
+ * Point your browser to the cut-and-paste URL that is output by the `novnc_proxy`
118
127
  script. Hit the Connect button, enter a password if the VNC server has one
119
128
  configured, and enjoy!
120
129
 
@@ -123,13 +132,17 @@ Running the command below will install the latest release of noVNC from Snap:
123
132
 
124
133
  `sudo snap install novnc`
125
134
 
126
- #### Running noVNC
135
+ #### Running noVNC from Snap Directly
127
136
 
128
137
  You can run the Snap-package installed novnc directly with, for example:
129
138
 
130
139
  `novnc --listen 6081 --vnc localhost:5901 # /snap/bin/novnc if /snap/bin is not in your PATH`
131
140
 
132
- #### Running as a Service (Daemon)
141
+ If you want to use certificate files, due to standard Snap confinement restrictions you need to have them in the /home/\<user\>/snap/novnc/current/ directory. If your username is jsmith an example command would be:
142
+
143
+ `novnc --listen 8443 --cert ~jsmith/snap/novnc/current/self.crt --key ~jsmith/snap/novnc/current/self.key --vnc ubuntu.example.com:5901`
144
+
145
+ #### Running noVNC from Snap as a Service (Daemon)
133
146
  The Snap package also has the capability to run a 'novnc' service which can be
134
147
  configured to listen on multiple ports connecting to multiple VNC servers
135
148
  (effectively a service runing multiple instances of novnc).
@@ -194,15 +207,18 @@ See [AUTHORS](AUTHORS) for a (full-ish) list of authors. If you're not on
194
207
  that list and you think you should be, feel free to send a PR to fix that.
195
208
 
196
209
  * Core team:
197
- * [Joel Martin](https://github.com/kanaka)
198
210
  * [Samuel Mannehed](https://github.com/samhed) (Cendio)
199
- * [Solly Ross](https://github.com/DirectXMan12) (Red Hat / OpenStack)
200
211
  * [Pierre Ossman](https://github.com/CendioOssman) (Cendio)
201
212
 
213
+ * Previous core contributors:
214
+ * [Joel Martin](https://github.com/kanaka) (Project founder)
215
+ * [Solly Ross](https://github.com/DirectXMan12) (Red Hat / OpenStack)
216
+
202
217
  * Notable contributions:
203
218
  * UI and Icons : Pierre Ossman, Chris Gordon
204
219
  * Original Logo : Michael Sersen
205
220
  * tight encoding : Michael Tinglof (Mercuri.ca)
221
+ * RealVNC RSA AES authentication : USTC Vlab Team
206
222
 
207
223
  * Included libraries:
208
224
  * base64 : Martijn Pieters (Digital Creations 2), Samuel Sieb (sieb.net)
@@ -0,0 +1,141 @@
1
+ /*
2
+ * noVNC: HTML5 VNC client
3
+ * Copyright (C) 2019 The noVNC Authors
4
+ * Licensed under MPL 2.0 (see LICENSE.txt)
5
+ *
6
+ * See README.md for usage and integration instructions.
7
+ *
8
+ */
9
+
10
+ export default class JPEGDecoder {
11
+ constructor() {
12
+ // RealVNC will reuse the quantization tables
13
+ // and Huffman tables, so we need to cache them.
14
+ this._quantTables = [];
15
+ this._huffmanTables = [];
16
+ this._cachedQuantTables = [];
17
+ this._cachedHuffmanTables = [];
18
+
19
+ this._jpegLength = 0;
20
+ this._segments = [];
21
+ }
22
+
23
+ decodeRect(x, y, width, height, sock, display, depth) {
24
+ // A rect of JPEG encodings is simply a JPEG file
25
+ if (!this._parseJPEG(sock.rQslice(0))) {
26
+ return false;
27
+ }
28
+ const data = sock.rQshiftBytes(this._jpegLength);
29
+ if (this._quantTables.length != 0 && this._huffmanTables.length != 0) {
30
+ // If there are quantization tables and Huffman tables in the JPEG
31
+ // image, we can directly render it.
32
+ display.imageRect(x, y, width, height, "image/jpeg", data);
33
+ return true;
34
+ } else {
35
+ // Otherwise we need to insert cached tables.
36
+ const sofIndex = this._segments.findIndex(
37
+ x => x[1] == 0xC0 || x[1] == 0xC2
38
+ );
39
+ if (sofIndex == -1) {
40
+ throw new Error("Illegal JPEG image without SOF");
41
+ }
42
+ let segments = this._segments.slice(0, sofIndex);
43
+ segments = segments.concat(this._quantTables.length ?
44
+ this._quantTables :
45
+ this._cachedQuantTables);
46
+ segments.push(this._segments[sofIndex]);
47
+ segments = segments.concat(this._huffmanTables.length ?
48
+ this._huffmanTables :
49
+ this._cachedHuffmanTables,
50
+ this._segments.slice(sofIndex + 1));
51
+ let length = 0;
52
+ for (let i = 0; i < segments.length; i++) {
53
+ length += segments[i].length;
54
+ }
55
+ const data = new Uint8Array(length);
56
+ length = 0;
57
+ for (let i = 0; i < segments.length; i++) {
58
+ data.set(segments[i], length);
59
+ length += segments[i].length;
60
+ }
61
+ display.imageRect(x, y, width, height, "image/jpeg", data);
62
+ return true;
63
+ }
64
+ }
65
+
66
+ _parseJPEG(buffer) {
67
+ if (this._quantTables.length != 0) {
68
+ this._cachedQuantTables = this._quantTables;
69
+ }
70
+ if (this._huffmanTables.length != 0) {
71
+ this._cachedHuffmanTables = this._huffmanTables;
72
+ }
73
+ this._quantTables = [];
74
+ this._huffmanTables = [];
75
+ this._segments = [];
76
+ let i = 0;
77
+ let bufferLength = buffer.length;
78
+ while (true) {
79
+ let j = i;
80
+ if (j + 2 > bufferLength) {
81
+ return false;
82
+ }
83
+ if (buffer[j] != 0xFF) {
84
+ throw new Error("Illegal JPEG marker received (byte: " +
85
+ buffer[j] + ")");
86
+ }
87
+ const type = buffer[j+1];
88
+ j += 2;
89
+ if (type == 0xD9) {
90
+ this._jpegLength = j;
91
+ this._segments.push(buffer.slice(i, j));
92
+ return true;
93
+ } else if (type == 0xDA) {
94
+ // start of scan
95
+ let hasFoundEndOfScan = false;
96
+ for (let k = j + 3; k + 1 < bufferLength; k++) {
97
+ if (buffer[k] == 0xFF && buffer[k+1] != 0x00 &&
98
+ !(buffer[k+1] >= 0xD0 && buffer[k+1] <= 0xD7)) {
99
+ j = k;
100
+ hasFoundEndOfScan = true;
101
+ break;
102
+ }
103
+ }
104
+ if (!hasFoundEndOfScan) {
105
+ return false;
106
+ }
107
+ this._segments.push(buffer.slice(i, j));
108
+ i = j;
109
+ continue;
110
+ } else if (type >= 0xD0 && type < 0xD9 || type == 0x01) {
111
+ // No length after marker
112
+ this._segments.push(buffer.slice(i, j));
113
+ i = j;
114
+ continue;
115
+ }
116
+ if (j + 2 > bufferLength) {
117
+ return false;
118
+ }
119
+ const length = (buffer[j] << 8) + buffer[j+1] - 2;
120
+ if (length < 0) {
121
+ throw new Error("Illegal JPEG length received (length: " +
122
+ length + ")");
123
+ }
124
+ j += 2;
125
+ if (j + length > bufferLength) {
126
+ return false;
127
+ }
128
+ j += length;
129
+ const segment = buffer.slice(i, j);
130
+ if (type == 0xC4) {
131
+ // Huffman tables
132
+ this._huffmanTables.push(segment);
133
+ } else if (type == 0xDB) {
134
+ // Quantization tables
135
+ this._quantTables.push(segment);
136
+ }
137
+ this._segments.push(segment);
138
+ i = j;
139
+ }
140
+ }
141
+ }
@@ -51,7 +51,7 @@ export default class RawDecoder {
51
51
 
52
52
  // Max sure the image is fully opaque
53
53
  for (let i = 0; i < pixels; i++) {
54
- data[i * 4 + 3] = 255;
54
+ data[index + i * 4 + 3] = 255;
55
55
  }
56
56
 
57
57
  display.blitImage(x, curY, width, currHeight, data, index);
@@ -0,0 +1,185 @@
1
+ /*
2
+ * noVNC: HTML5 VNC client
3
+ * Copyright (C) 2021 The noVNC Authors
4
+ * Licensed under MPL 2.0 (see LICENSE.txt)
5
+ *
6
+ * See README.md for usage and integration instructions.
7
+ *
8
+ */
9
+
10
+ import Inflate from "../inflator.js";
11
+
12
+ const ZRLE_TILE_WIDTH = 64;
13
+ const ZRLE_TILE_HEIGHT = 64;
14
+
15
+ export default class ZRLEDecoder {
16
+ constructor() {
17
+ this._length = 0;
18
+ this._inflator = new Inflate();
19
+
20
+ this._pixelBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4);
21
+ this._tileBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4);
22
+ }
23
+
24
+ decodeRect(x, y, width, height, sock, display, depth) {
25
+ if (this._length === 0) {
26
+ if (sock.rQwait("ZLib data length", 4)) {
27
+ return false;
28
+ }
29
+ this._length = sock.rQshift32();
30
+ }
31
+ if (sock.rQwait("Zlib data", this._length)) {
32
+ return false;
33
+ }
34
+
35
+ const data = sock.rQshiftBytes(this._length);
36
+
37
+ this._inflator.setInput(data);
38
+
39
+ for (let ty = y; ty < y + height; ty += ZRLE_TILE_HEIGHT) {
40
+ let th = Math.min(ZRLE_TILE_HEIGHT, y + height - ty);
41
+
42
+ for (let tx = x; tx < x + width; tx += ZRLE_TILE_WIDTH) {
43
+ let tw = Math.min(ZRLE_TILE_WIDTH, x + width - tx);
44
+
45
+ const tileSize = tw * th;
46
+ const subencoding = this._inflator.inflate(1)[0];
47
+ if (subencoding === 0) {
48
+ // raw data
49
+ const data = this._readPixels(tileSize);
50
+ display.blitImage(tx, ty, tw, th, data, 0, false);
51
+ } else if (subencoding === 1) {
52
+ // solid
53
+ const background = this._readPixels(1);
54
+ display.fillRect(tx, ty, tw, th, [background[0], background[1], background[2]]);
55
+ } else if (subencoding >= 2 && subencoding <= 16) {
56
+ const data = this._decodePaletteTile(subencoding, tileSize, tw, th);
57
+ display.blitImage(tx, ty, tw, th, data, 0, false);
58
+ } else if (subencoding === 128) {
59
+ const data = this._decodeRLETile(tileSize);
60
+ display.blitImage(tx, ty, tw, th, data, 0, false);
61
+ } else if (subencoding >= 130 && subencoding <= 255) {
62
+ const data = this._decodeRLEPaletteTile(subencoding - 128, tileSize);
63
+ display.blitImage(tx, ty, tw, th, data, 0, false);
64
+ } else {
65
+ throw new Error('Unknown subencoding: ' + subencoding);
66
+ }
67
+ }
68
+ }
69
+ this._length = 0;
70
+ return true;
71
+ }
72
+
73
+ _getBitsPerPixelInPalette(paletteSize) {
74
+ if (paletteSize <= 2) {
75
+ return 1;
76
+ } else if (paletteSize <= 4) {
77
+ return 2;
78
+ } else if (paletteSize <= 16) {
79
+ return 4;
80
+ }
81
+ }
82
+
83
+ _readPixels(pixels) {
84
+ let data = this._pixelBuffer;
85
+ const buffer = this._inflator.inflate(3*pixels);
86
+ for (let i = 0, j = 0; i < pixels*4; i += 4, j += 3) {
87
+ data[i] = buffer[j];
88
+ data[i + 1] = buffer[j + 1];
89
+ data[i + 2] = buffer[j + 2];
90
+ data[i + 3] = 255; // Add the Alpha
91
+ }
92
+ return data;
93
+ }
94
+
95
+ _decodePaletteTile(paletteSize, tileSize, tilew, tileh) {
96
+ const data = this._tileBuffer;
97
+ const palette = this._readPixels(paletteSize);
98
+ const bitsPerPixel = this._getBitsPerPixelInPalette(paletteSize);
99
+ const mask = (1 << bitsPerPixel) - 1;
100
+
101
+ let offset = 0;
102
+ let encoded = this._inflator.inflate(1)[0];
103
+
104
+ for (let y=0; y<tileh; y++) {
105
+ let shift = 8-bitsPerPixel;
106
+ for (let x=0; x<tilew; x++) {
107
+ if (shift<0) {
108
+ shift=8-bitsPerPixel;
109
+ encoded = this._inflator.inflate(1)[0];
110
+ }
111
+ let indexInPalette = (encoded>>shift) & mask;
112
+
113
+ data[offset] = palette[indexInPalette * 4];
114
+ data[offset + 1] = palette[indexInPalette * 4 + 1];
115
+ data[offset + 2] = palette[indexInPalette * 4 + 2];
116
+ data[offset + 3] = palette[indexInPalette * 4 + 3];
117
+ offset += 4;
118
+ shift-=bitsPerPixel;
119
+ }
120
+ if (shift<8-bitsPerPixel && y<tileh-1) {
121
+ encoded = this._inflator.inflate(1)[0];
122
+ }
123
+ }
124
+ return data;
125
+ }
126
+
127
+ _decodeRLETile(tileSize) {
128
+ const data = this._tileBuffer;
129
+ let i = 0;
130
+ while (i < tileSize) {
131
+ const pixel = this._readPixels(1);
132
+ const length = this._readRLELength();
133
+ for (let j = 0; j < length; j++) {
134
+ data[i * 4] = pixel[0];
135
+ data[i * 4 + 1] = pixel[1];
136
+ data[i * 4 + 2] = pixel[2];
137
+ data[i * 4 + 3] = pixel[3];
138
+ i++;
139
+ }
140
+ }
141
+ return data;
142
+ }
143
+
144
+ _decodeRLEPaletteTile(paletteSize, tileSize) {
145
+ const data = this._tileBuffer;
146
+
147
+ // palette
148
+ const palette = this._readPixels(paletteSize);
149
+
150
+ let offset = 0;
151
+ while (offset < tileSize) {
152
+ let indexInPalette = this._inflator.inflate(1)[0];
153
+ let length = 1;
154
+ if (indexInPalette >= 128) {
155
+ indexInPalette -= 128;
156
+ length = this._readRLELength();
157
+ }
158
+ if (indexInPalette > paletteSize) {
159
+ throw new Error('Too big index in palette: ' + indexInPalette + ', palette size: ' + paletteSize);
160
+ }
161
+ if (offset + length > tileSize) {
162
+ throw new Error('Too big rle length in palette mode: ' + length + ', allowed length is: ' + (tileSize - offset));
163
+ }
164
+
165
+ for (let j = 0; j < length; j++) {
166
+ data[offset * 4] = palette[indexInPalette * 4];
167
+ data[offset * 4 + 1] = palette[indexInPalette * 4 + 1];
168
+ data[offset * 4 + 2] = palette[indexInPalette * 4 + 2];
169
+ data[offset * 4 + 3] = palette[indexInPalette * 4 + 3];
170
+ offset++;
171
+ }
172
+ }
173
+ return data;
174
+ }
175
+
176
+ _readRLELength() {
177
+ let length = 0;
178
+ let current = 0;
179
+ do {
180
+ current = this._inflator.inflate(1)[0];
181
+ length += current;
182
+ } while (current === 255);
183
+ return length + 1;
184
+ }
185
+ }
package/core/des.js CHANGED
@@ -81,7 +81,7 @@
81
81
  const PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3,
82
82
  25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39,
83
83
  50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ],
84
- totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28];
84
+ totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28];
85
85
 
86
86
  const z = 0x0;
87
87
  let a,b,c,d,e,f;
package/core/display.js CHANGED
@@ -224,6 +224,18 @@ export default class Display {
224
224
  this.viewportChangePos(0, 0);
225
225
  }
226
226
 
227
+ getImageData() {
228
+ return this._drawCtx.getImageData(0, 0, this.width, this.height);
229
+ }
230
+
231
+ toDataURL(type, encoderOptions) {
232
+ return this._backbuffer.toDataURL(type, encoderOptions);
233
+ }
234
+
235
+ toBlob(callback, type, quality) {
236
+ return this._backbuffer.toBlob(callback, type, quality);
237
+ }
238
+
227
239
  // Track what parts of the visible canvas that need updating
228
240
  _damage(x, y, w, h) {
229
241
  if (x < this._damageBounds.left) {
package/core/encodings.js CHANGED
@@ -12,7 +12,9 @@ export const encodings = {
12
12
  encodingRRE: 2,
13
13
  encodingHextile: 5,
14
14
  encodingTight: 7,
15
+ encodingZRLE: 16,
15
16
  encodingTightPNG: -260,
17
+ encodingJPEG: 21,
16
18
 
17
19
  pseudoEncodingQualityLevel9: -23,
18
20
  pseudoEncodingQualityLevel0: -32,
@@ -38,7 +40,9 @@ export function encodingName(num) {
38
40
  case encodings.encodingRRE: return "RRE";
39
41
  case encodings.encodingHextile: return "Hextile";
40
42
  case encodings.encodingTight: return "Tight";
43
+ case encodings.encodingZRLE: return "ZRLE";
41
44
  case encodings.encodingTightPNG: return "TightPNG";
45
+ case encodings.encodingJPEG: return "JPEG";
42
46
  default: return "[unknown encoding " + num + "]";
43
47
  }
44
48
  }
@@ -153,6 +153,16 @@ export default class Keyboard {
153
153
  keysym = this._keyDownList[code];
154
154
  }
155
155
 
156
+ // macOS doesn't send proper key releases if a key is pressed
157
+ // while meta is held down
158
+ if ((browser.isMac() || browser.isIOS()) &&
159
+ (e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) {
160
+ this._sendKeyEvent(keysym, code, true);
161
+ this._sendKeyEvent(keysym, code, false);
162
+ stopEvent(e);
163
+ return;
164
+ }
165
+
156
166
  // macOS doesn't send proper key events for modifiers, only
157
167
  // state change events. That gets extra confusing for CapsLock
158
168
  // which toggles on each press, but not on release. So pretend