UNPKG

11 kBJavaScriptView Raw
1const EventEmitter = require('events');
2const path = require('path');
3
4const runtime = process.versions['electron'] ? 'electron' : 'node';
5const essential =
6 runtime +
7 '-v' +
8 process.versions.modules +
9 '-' +
10 process.platform +
11 '-' +
12 process.arch;
13const modulePath = path.join(
14 __dirname,
15 'builds',
16 essential,
17 'build',
18 'Release',
19 'iohook.node'
20);
21if (process.env.DEBUG) {
22 console.info('Loading native binary:', modulePath);
23}
24let NodeHookAddon = require(modulePath);
25
26const events = {
27 3: 'keypress',
28 4: 'keydown',
29 5: 'keyup',
30 6: 'mouseclick',
31 7: 'mousedown',
32 8: 'mouseup',
33 9: 'mousemove',
34 10: 'mousedrag',
35 11: 'mousewheel',
36};
37
38class IOHook extends EventEmitter {
39 constructor() {
40 super();
41 this.active = false;
42 this.shortcuts = [];
43 this.eventProperty = 'keycode';
44 this.activatedShortcuts = [];
45
46 this.lastKeydownShift = false;
47 this.lastKeydownAlt = false;
48 this.lastKeydownCtrl = false;
49 this.lastKeydownMeta = false;
50
51 this.load();
52 this.setDebug(false);
53 }
54
55 /**
56 * Start hook process
57 * @param {boolean} [enableLogger] Turn on debug logging
58 */
59 start(enableLogger) {
60 if (!this.active) {
61 this.active = true;
62 this.setDebug(enableLogger);
63 }
64 }
65
66 /**
67 * Shutdown event hook
68 */
69 stop() {
70 if (this.active) {
71 this.active = false;
72 }
73 }
74
75 /**
76 * Register global shortcut. When all keys in keys array pressed, callback will be called
77 * @param {Array} keys Array of keycodes
78 * @param {Function} callback Callback for when shortcut pressed
79 * @param {Function} [releaseCallback] Callback for when shortcut has been released
80 * @return {number} ShortcutId for unregister
81 */
82 registerShortcut(keys, callback, releaseCallback) {
83 let shortcut = {};
84 let shortcutId = Date.now() + Math.random();
85 keys.forEach((keyCode) => {
86 shortcut[keyCode] = false;
87 });
88 shortcut.id = shortcutId;
89 shortcut.callback = callback;
90 shortcut.releaseCallback = releaseCallback;
91 this.shortcuts.push(shortcut);
92 return shortcutId;
93 }
94
95 /**
96 * Unregister shortcut by ShortcutId
97 * @param shortcutId
98 */
99 unregisterShortcut(shortcutId) {
100 this.shortcuts.forEach((shortcut, i) => {
101 if (shortcut.id === shortcutId) {
102 this.shortcuts.splice(i, 1);
103 }
104 });
105 }
106
107 /**
108 * Unregister shortcut via its key codes
109 * @param {string} keyCodes Keyboard keys matching the shortcut that should be unregistered
110 */
111 unregisterShortcutByKeys(keyCodes) {
112 // A traditional loop is used in order to access `this` from inside
113 for (let i = 0; i < this.shortcuts.length; i++) {
114 let shortcut = this.shortcuts[i];
115
116 // Convert any keycode numbers to strings
117 keyCodes.forEach((key, index) => {
118 if (typeof key !== 'string' && !(key instanceof String)) {
119 // Convert to string
120 keyCodes[index] = key.toString();
121 }
122 });
123
124 // Check if this is our shortcut
125 Object.keys(shortcut).every((key) => {
126 if (key === 'callback' || key === 'id') return;
127
128 // Remove all given keys from keyCodes
129 // If any are not in this shortcut, then this shortcut does not match
130 // If at the end we have eliminated all codes in keyCodes, then we have succeeded
131 let index = keyCodes.indexOf(key);
132 if (index === -1) return false; // break
133
134 // Remove this key from the given keyCodes array
135 keyCodes.splice(index, 1);
136 return true;
137 });
138
139 // Is this the shortcut we want to remove?
140 if (keyCodes.length === 0) {
141 // Unregister this shortcut
142 this.shortcuts.splice(i, 1);
143 return;
144 }
145 }
146 }
147
148 /**
149 * Unregister all shortcuts
150 */
151 unregisterAllShortcuts() {
152 this.shortcuts.splice(0, this.shortcuts.length);
153 }
154
155 /**
156 * Load native module
157 */
158 load() {
159 NodeHookAddon.startHook(this._handler.bind(this), this.debug || false);
160 }
161
162 /**
163 * Unload native module and stop hook
164 */
165 unload() {
166 this.stop();
167 NodeHookAddon.stopHook();
168 }
169
170 /**
171 * Enable or disable native debug output
172 * @param {Boolean} mode
173 */
174 setDebug(mode) {
175 NodeHookAddon.debugEnable(mode);
176 }
177
178 /**
179 * Specify that key event's `rawcode` property should be used instead of
180 * `keycode` when listening for key presses.
181 *
182 * This allows iohook to be used in conjunction with other programs that may
183 * only provide a keycode.
184 * @param {Boolean} using
185 */
186 useRawcode(using) {
187 // If true, use rawcode, otherwise use keycode
188 this.eventProperty = using ? 'rawcode' : 'keycode';
189 }
190
191 /**
192 * Disable mouse click propagation.
193 * The click event are captured and the event emitted but not propagated to the window.
194 */
195 disableClickPropagation() {
196 NodeHookAddon.grabMouseClick(true);
197 }
198
199 /**
200 * Enable mouse click propagation (enabled by default).
201 * The click event are emitted and propagated.
202 */
203 enableClickPropagation() {
204 NodeHookAddon.grabMouseClick(false);
205 }
206
207 /**
208 * Local event handler. Don't use it in your code!
209 * @param msg Raw event message
210 * @private
211 */
212 _handler(msg) {
213 if (this.active === false || !msg) return;
214
215 if (events[msg.type]) {
216 const event = msg.mouse || msg.keyboard || msg.wheel;
217
218 event.type = events[msg.type];
219
220 this._handleShift(event);
221 this._handleAlt(event);
222 this._handleCtrl(event);
223 this._handleMeta(event);
224
225 this.emit(events[msg.type], event);
226
227 // If there is any registered shortcuts then handle them.
228 if (
229 (event.type === 'keydown' || event.type === 'keyup') &&
230 iohook.shortcuts.length > 0
231 ) {
232 this._handleShortcut(event);
233 }
234 }
235 }
236
237 /**
238 * Handles the shift key. Whenever shift is pressed, all future events would
239 * contain { shiftKey: true } in its object, until the shift key is released.
240 * @param event Event object
241 * @private
242 */
243 _handleShift(event) {
244 if (event.type === 'keyup' && event.shiftKey) {
245 this.lastKeydownShift = false;
246 }
247
248 if (event.type === 'keydown' && event.shiftKey) {
249 this.lastKeydownShift = true;
250 }
251
252 if (this.lastKeydownShift) {
253 event.shiftKey = true;
254 }
255 }
256
257 /**
258 * Handles the alt key. Whenever alt is pressed, all future events would
259 * contain { altKey: true } in its object, until the alt key is released.
260 * @param event Event object
261 * @private
262 */
263 _handleAlt(event) {
264 if (event.type === 'keyup' && event.altKey) {
265 this.lastKeydownAlt = false;
266 }
267
268 if (event.type === 'keydown' && event.altKey) {
269 this.lastKeydownAlt = true;
270 }
271
272 if (this.lastKeydownAlt) {
273 event.altKey = true;
274 }
275 }
276
277 /**
278 * Handles the ctrl key. Whenever ctrl is pressed, all future events would
279 * contain { ctrlKey: true } in its object, until the ctrl key is released.
280 * @param event Event object
281 * @private
282 */
283 _handleCtrl(event) {
284 if (event.type === 'keyup' && event.ctrlKey) {
285 this.lastKeydownCtrl = false;
286 }
287
288 if (event.type === 'keydown' && event.ctrlKey) {
289 this.lastKeydownCtrl = true;
290 }
291
292 if (this.lastKeydownCtrl) {
293 event.ctrlKey = true;
294 }
295 }
296
297 /**
298 * Handles the meta key. Whenever meta is pressed, all future events would
299 * contain { metaKey: true } in its object, until the meta key is released.
300 * @param event Event object
301 * @private
302 */
303 _handleMeta(event) {
304 if (event.type === 'keyup' && event.metaKey) {
305 this.lastKeydownMeta = false;
306 }
307
308 if (event.type === 'keydown' && event.metaKey) {
309 this.lastKeydownMeta = true;
310 }
311
312 if (this.lastKeydownMeta) {
313 event.metaKey = true;
314 }
315 }
316
317 /**
318 * Local shortcut event handler
319 * @param event Event object
320 * @private
321 */
322 _handleShortcut(event) {
323 if (this.active === false) {
324 return;
325 }
326
327 // Keep track of shortcuts that are currently active
328 let activatedShortcuts = this.activatedShortcuts;
329
330 if (event.type === 'keydown') {
331 this.shortcuts.forEach((shortcut) => {
332 if (shortcut[event[this.eventProperty]] !== undefined) {
333 // Mark this key as currently being pressed
334 shortcut[event[this.eventProperty]] = true;
335
336 let keysTmpArray = [];
337 let callme = true;
338
339 // Iterate through each keyboard key in this shortcut
340 Object.keys(shortcut).forEach((key) => {
341 if (key === 'callback' || key === 'releaseCallback' || key === 'id')
342 return;
343
344 // If one of the keys aren't pressed...
345 if (shortcut[key] === false) {
346 // Don't call the callback and empty our temp tracking array
347 callme = false;
348 keysTmpArray.splice(0, keysTmpArray.length);
349
350 return;
351 }
352
353 // Otherwise, this key is being pressed.
354 // Add it to the array of keyboard keys we will send as an argument
355 // to our callback
356 keysTmpArray.push(key);
357 });
358 if (callme) {
359 shortcut.callback(keysTmpArray);
360
361 // Add this shortcut from our activate shortcuts array if not
362 // already activated
363 if (activatedShortcuts.indexOf(shortcut) === -1) {
364 activatedShortcuts.push(shortcut);
365 }
366 }
367 }
368 });
369 } else if (event.type === 'keyup') {
370 // Mark this key as currently not being pressed in all of our shortcuts
371 this.shortcuts.forEach((shortcut) => {
372 if (shortcut[event[this.eventProperty]] !== undefined) {
373 shortcut[event[this.eventProperty]] = false;
374 }
375 });
376
377 // Check if any of our currently pressed shortcuts have been released
378 // "released" means that all of the keys that the shortcut defines are no
379 // longer being pressed
380 this.activatedShortcuts.forEach((shortcut) => {
381 if (shortcut[event[this.eventProperty]] === undefined) return;
382
383 let shortcutReleased = true;
384 let keysTmpArray = [];
385 Object.keys(shortcut).forEach((key) => {
386 if (key === 'callback' || key === 'releaseCallback' || key === 'id')
387 return;
388 keysTmpArray.push(key);
389
390 // If any key is true, and thus still pressed, the shortcut is still
391 // being held
392 if (shortcut[key]) {
393 shortcutReleased = false;
394 }
395 });
396
397 if (shortcutReleased) {
398 // Call the released function handler
399 if (shortcut.releaseCallback) {
400 shortcut.releaseCallback(keysTmpArray);
401 }
402
403 // Remove this shortcut from our activate shortcuts array
404 const index = this.activatedShortcuts.indexOf(shortcut);
405 if (index !== -1) this.activatedShortcuts.splice(index, 1);
406 }
407 });
408 }
409 }
410}
411
412const iohook = new IOHook();
413
414module.exports = iohook;