From b8e5aa22b566695f7377890742c60515b797936a Mon Sep 17 00:00:00 2001 From: velvettear Date: Thu, 3 Mar 2022 03:35:34 +0100 Subject: [PATCH] added most of the combo functionality --- README.md | 5 ++ config.json | 15 +++- libs/cli.js | 36 ++++++--- libs/keyfilter.js | 202 ++++++++++++++++++++++++++++++++++------------ libs/watcher.js | 18 +++-- 5 files changed, 207 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 8e661b8..e974247 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,13 @@ configuration is done entirely within the file `config.json`. - level: [*string*] verbosity of the log; either `debug`, `info`, `warning` or `error` - timestamp: [*string*] format string for the timestamp; review [moment.js](https://momentjs.com/docs/#/displaying/format/) for further information +### socket: [*object*] +- location: [*string*] path to the socket file; defaults to `/tmp/ninwa.sock` + ### watchers: [*object-array*] - device: [*string*] name of or path to an input device; ninwa automatically tries to locate the device in `/dev/input` and `/dev/input/by-id/` if only a name is given +- restart: [*boolean*] restart watcher on close +- grep: [*string*] pre-filter evtest's output with grep - keys: [*object-array*] - key: [*string*] name of the key - type: [*string*] type of the key event; either `keyup`, `keydown` or `keyhold` diff --git a/config.json b/config.json index 5d4e22f..535999f 100644 --- a/config.json +++ b/config.json @@ -7,8 +7,21 @@ { "device": "usb-Razer_Razer_Blade_Stealth-if01-event-kbd", "restart": true, - "grep": "EV_KEY", "keys": [ + { + "key": "key_f1", + "combo": [ + "key_f2", + "key_f3" + ], + "event": "EV_KEY", + "type": "keydown", + "command": "notify-send", + "args": [ + "combo", + "{{ key }} {{ type }}" + ] + }, { "key": "key_enter", "type": "keydown", diff --git a/libs/cli.js b/libs/cli.js index 9c4f106..c3c63da 100644 --- a/libs/cli.js +++ b/libs/cli.js @@ -1,10 +1,14 @@ const logger = require('./logger.js'); const spawn = require('child_process').spawn; -function execute(command, args) { - return new Promise((resolve, reject) => { - if (command == undefined || command.length == 0) { - reject(); +async function execute(command, args, returnOnClose) { + // return new Promise((resolve, reject) => { + if (command === undefined || command.length === 0) { + return; + // reject(); + } + if (returnOnClose === undefined) { + returnOnClose = false; } var startTime = new Date().getTime(); var resultData = ""; @@ -22,6 +26,16 @@ function execute(command, args) { process.stderr.on('data', (data) => { resultError += data; }); + process.on('spawn', () => { + logger.info('spawned command \'' + command + '\' (args: \'' + args + '\')'); + if (!returnOnClose) { + return; + } + }); + process.on('error', (err) => { + throw new Error(err); + // resultError += err; + }); process.on('close', (code) => { var msg = 'command \'' + command + '\' (args: \'' + args + '\') finished with exit code ' + code + ' after ' + (new Date().getTime() - startTime) + 'ms'; if (resultData.length > 0) { @@ -29,14 +43,16 @@ function execute(command, args) { } if (resultError.length > 0) { msg += " >>> error: " + resultError; - reject(msg); + throw new Error(msg); + // reject(msg); } - resolve(msg); + if (returnOnClose) { + return; + } + // resolve(msg); }); - process.on('error', (err) => { - resultError += err; - }); - }); + + // }); } module.exports = { diff --git a/libs/keyfilter.js b/libs/keyfilter.js index b770486..f614654 100644 --- a/libs/keyfilter.js +++ b/libs/keyfilter.js @@ -1,4 +1,4 @@ -const FILTER_START = 'Testing ... (interrupt to exit)'; +const LINE_START = 'Event: time'; const ACTION_KEYUP = { id: 0, action: 'keyup' }; const ACTION_KEYDOWN = { id: 1, action: 'keydown' }; @@ -8,69 +8,118 @@ const VARIABLE_KEY = '{{ key }}'; const VARIABLE_TYPE = '{{ type }}'; class Keyfilter { - constructor(config) { - if (config == undefined || config.length == 0) { + constructor(keys, combos) { + if ((keys === undefined || keys.length === 0) && (combos === undefined || combos.length === 0)) { return; } this.actions = new Map(); - for (var index = 0; index < config.length; index++) { - var grep = config[index].grep; - var type = ACTION_KEYDOWN; - switch (config[index].type.toLowerCase()) { - case ACTION_KEYUP.action: - type = ACTION_KEYUP; - break; - case ACTION_KEYHOLD.action: - type = ACTION_KEYHOLD; - break; - } - var command = config[index].command; - var args = config[index].args; - var delay = config[index].delay; - this.actions.set(config[index].key.toUpperCase(), { grep, type, command, args, delay }); + for (var index = 0; index < keys.length; index++) { + this.setAction(keys[index]); } + this.currentCombo = undefined; } - filterActive(input) { - if (this.active) { - return true; + setAction(config) { + var type = ACTION_KEYDOWN; + switch (config.type.toLowerCase()) { + case ACTION_KEYUP.action: + type = ACTION_KEYUP; + break; + case ACTION_KEYHOLD.action: + type = ACTION_KEYHOLD; + break; } - input = input.toString(); - var index = input.indexOf(FILTER_START); - if (index == -1) { - return; - } - input = input.substring(index + FILTER_START.length).trim(); - this.active = true; - return true; + this.actions.set(config.key.toUpperCase(), + { + type: type, + event: config.event, + command: config.command, + args: config.args, + combo: config.combo, + delay: function () { + if (config.combo === undefined) { + return config.delay; + } + return config.delay || 1000; + }(), + } + ); } filter(input) { - if (input == undefined || input.length == 0) { + if (input === undefined || input.length === 0) { return; } input = input.toString(); - if (!this.filterActive(input)) { - return; - } var lines = input.split("\n"); for (var index = 0; index < lines.length; index++) { var line = lines[index]; - if (line.length == 0) { + if (line.length === 0) { continue; } - for (var [key, value] of this.actions) { - if (!line.includes(key)) { - continue; - } - if (!line.endsWith('value ' + value.type.id)) { - continue; - } - var action = this.actions.get(key); - if (this.shouldBeDelayed(action)) { - return { key: key, type: action.type.action, delayed: true }; - } - action.captured = new Date().getTime(); - return this.replaceVariables({ key: key, type: action.type.action, command: action.command, args: action.args }); + if (!line.startsWith(LINE_START)) { + continue; } + const parsedEvent = this.parseLine(line); + if (parsedEvent === undefined) { + continue; + } + for (var [key, event] of this.actions) { + if (this.currentCombo === undefined && !this.isParsedEventValid(key, event, parsedEvent)) { + continue; + } + if (this.isStartOfCombo(key, event)) { + return this.getFilterResult(key, event, 'combo', this.currentCombo); + } + if (this.isPartOfCombo(parsedEvent)) { + if (parsedEvent.ignore) { + continue; + } + const result = this.getFilterResult(key, event, 'combo', this.currentCombo); + if (this.hasComboFinished()) { + this.resetCurrentCombo(); + } + return result; + } + if (!this.isParsedEventValid(key, event, parsedEvent)) { + continue; + } + if (this.shouldBeDelayed(event)) { + return this.getFilterResult(key, event, 'delayed', true); + } + event.captured = new Date().getTime(); + return this.getFilterResult(key, event); + } + } + } + isParsedEventValid(key, value, parsed) { + return key === parsed.key && (value.event === undefined || value.event === parsed.event) && value.type.id === parsed.type; + } + getFilterResult(key, event, extraName, extra) { + if (key === undefined || event === undefined) { + return; + } + let result = this.replaceVariables( + { + key: key, + type: event.type.action, + command: event.command, + args: event.args, + delay: event.delay + } + ) + if (extraName !== undefined && extra !== undefined) { + result[extraName] = extra; + } + return result; + } + parseLine(line) { + try { + const parts = line.split(','); + const event = parts[1].substring(parts[1].indexOf('(') + 1, parts[1].lastIndexOf(')')); + const key = parts[2].substring(parts[2].indexOf('(') + 1, parts[2].indexOf(')')); + const type = parseInt(parts[3].split(' ').pop()); + return { event: event, key: key, type: type }; + } catch (err) { + return; } } replaceVariables(filtered) { @@ -80,15 +129,68 @@ class Keyfilter { } return filtered; } - shouldBeDelayed(action) { - if (action.delay == undefined || action.delay == 0 || action.captured == undefined) { + shouldBeDelayed(event) { + if (event.delay === undefined || event.delay === 0 || event.captured === undefined) { return false; } - return new Date().getTime() - action.captured < action.delay; + return new Date().getTime() - event.captured < event.delay; + } + isStartOfCombo(key, event) { + if (this.currentCombo !== undefined || event.combo === undefined) { + return false; + } + this.currentCombo = { + key: key, + type: event.type, + start: new Date().getTime(), + done: [key], + remaining: JSON.parse(JSON.stringify(event.combo)), + finished: this.hasComboFinished() + }; + return true; + } + isPartOfCombo(parsedEvent) { + if (this.hasComboTimedOut()) { + this.resetCurrentCombo(); + } + if (this.currentCombo === undefined) { + return false; + } + if (this.currentCombo.done.includes(parsedEvent.key)) { + parsedEvent.ignore = true; + return true; + } + if (this.currentCombo.type.id !== parsedEvent.type) { + return false; + } + if (this.currentCombo.remaining[0].toUpperCase() !== parsedEvent.key) { + return false; + } + this.currentCombo.key = parsedEvent.key; + this.currentCombo.done.push(parsedEvent.key); + this.currentCombo.remaining.shift(); + this.currentCombo.finished = this.hasComboFinished(); + return true; + } + hasComboFinished() { + return this.currentCombo !== undefined && + this.currentCombo.remaining !== undefined && + this.currentCombo.remaining.length === 0; + } + hasComboTimedOut() { + return this.currentCombo !== undefined && + this.currentCombo.delay !== undefined && + this.currentCombo.start !== undefined && + new Date().getTime() - this.currentCombo.start > this.currentCombo.delay; + } + resetCurrentCombo() { + this.currentCombo = undefined; } isValid() { return this.actions != undefined && this.actions.size > 0; } } + + module.exports = Keyfilter; \ No newline at end of file diff --git a/libs/watcher.js b/libs/watcher.js index bc5de3c..c8c9353 100644 --- a/libs/watcher.js +++ b/libs/watcher.js @@ -15,9 +15,8 @@ class Watcher { for (var key in config) { this[key] = config[key]; } - this.keyfilter = new Keyfilter(config.keys); + this.keyfilter = new Keyfilter(config.keys, config.combos); this.restart = config.restart; - this.grep = config.grep; this.callback = callback; } start() { @@ -26,11 +25,7 @@ class Watcher { return; } logger.debug('starting watcher \'' + this.device + '\'...'); - var args = [this.device]; - if (this.grep) { - args.push('|', 'grep', this.grep); - } - this.process = spawn("evtest", args); + this.process = spawn("evtest", [this.device]); this.attachListeners(resolve, reject); }); } @@ -65,11 +60,18 @@ class Watcher { if (filtered == undefined) { return; } - var msg = 'watcher \'' + this.device + '\' captured event'; if (filtered.delayed) { logger.debug('delaying captured \'' + filtered.type + '\' event for \'' + filtered.key + '\' from watcher \'' + this.device + '\''); return; } + if (filtered.combo) { + if (!filtered.combo.finished) { + logger.debug('captured \'' + filtered.combo.type.action + '\' event for \'' + filtered.combo.key + '\' from watcher \'' + this.device + '\' is part of a combo (next key: \'' + filtered.combo.remaining[0] + '\')'); + return; + } + logger.debug('captured \'' + filtered.combo.type.action + '\' event for \'' + filtered.combo.key + '\' from watcher \'' + this.device + '\' is the last part of a combo'); + } + this.keyfilter.resetCurrentCombo(); logger.info('executing command \'' + filtered.command + '\' (args: \'' + filtered.args + '\') registered for captured \'' + filtered.type + '\' event for \'' + filtered.key + '\' from watcher \'' + this.device + '\''); cli.execute(filtered.command, filtered.args) .then(logger.info)