added most of the combo functionality
This commit is contained in:
parent
246363b056
commit
b8e5aa22b5
5 changed files with 207 additions and 69 deletions
|
@ -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`
|
- 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
|
- 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*]
|
### 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
|
- 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*]
|
- keys: [*object-array*]
|
||||||
- key: [*string*] name of the key
|
- key: [*string*] name of the key
|
||||||
- type: [*string*] type of the key event; either `keyup`, `keydown` or `keyhold`
|
- type: [*string*] type of the key event; either `keyup`, `keydown` or `keyhold`
|
||||||
|
|
15
config.json
15
config.json
|
@ -7,8 +7,21 @@
|
||||||
{
|
{
|
||||||
"device": "usb-Razer_Razer_Blade_Stealth-if01-event-kbd",
|
"device": "usb-Razer_Razer_Blade_Stealth-if01-event-kbd",
|
||||||
"restart": true,
|
"restart": true,
|
||||||
"grep": "EV_KEY",
|
|
||||||
"keys": [
|
"keys": [
|
||||||
|
{
|
||||||
|
"key": "key_f1",
|
||||||
|
"combo": [
|
||||||
|
"key_f2",
|
||||||
|
"key_f3"
|
||||||
|
],
|
||||||
|
"event": "EV_KEY",
|
||||||
|
"type": "keydown",
|
||||||
|
"command": "notify-send",
|
||||||
|
"args": [
|
||||||
|
"combo",
|
||||||
|
"{{ key }} {{ type }}"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "key_enter",
|
"key": "key_enter",
|
||||||
"type": "keydown",
|
"type": "keydown",
|
||||||
|
|
36
libs/cli.js
36
libs/cli.js
|
@ -1,10 +1,14 @@
|
||||||
const logger = require('./logger.js');
|
const logger = require('./logger.js');
|
||||||
const spawn = require('child_process').spawn;
|
const spawn = require('child_process').spawn;
|
||||||
|
|
||||||
function execute(command, args) {
|
async function execute(command, args, returnOnClose) {
|
||||||
return new Promise((resolve, reject) => {
|
// return new Promise((resolve, reject) => {
|
||||||
if (command == undefined || command.length == 0) {
|
if (command === undefined || command.length === 0) {
|
||||||
reject();
|
return;
|
||||||
|
// reject();
|
||||||
|
}
|
||||||
|
if (returnOnClose === undefined) {
|
||||||
|
returnOnClose = false;
|
||||||
}
|
}
|
||||||
var startTime = new Date().getTime();
|
var startTime = new Date().getTime();
|
||||||
var resultData = "";
|
var resultData = "";
|
||||||
|
@ -22,6 +26,16 @@ function execute(command, args) {
|
||||||
process.stderr.on('data', (data) => {
|
process.stderr.on('data', (data) => {
|
||||||
resultError += 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) => {
|
process.on('close', (code) => {
|
||||||
var msg = 'command \'' + command + '\' (args: \'' + args + '\') finished with exit code ' + code + ' after ' + (new Date().getTime() - startTime) + 'ms';
|
var msg = 'command \'' + command + '\' (args: \'' + args + '\') finished with exit code ' + code + ' after ' + (new Date().getTime() - startTime) + 'ms';
|
||||||
if (resultData.length > 0) {
|
if (resultData.length > 0) {
|
||||||
|
@ -29,14 +43,16 @@ function execute(command, args) {
|
||||||
}
|
}
|
||||||
if (resultError.length > 0) {
|
if (resultError.length > 0) {
|
||||||
msg += " >>> error: " + resultError;
|
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 = {
|
module.exports = {
|
||||||
|
|
|
@ -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_KEYUP = { id: 0, action: 'keyup' };
|
||||||
const ACTION_KEYDOWN = { id: 1, action: 'keydown' };
|
const ACTION_KEYDOWN = { id: 1, action: 'keydown' };
|
||||||
|
@ -8,69 +8,118 @@ const VARIABLE_KEY = '{{ key }}';
|
||||||
const VARIABLE_TYPE = '{{ type }}';
|
const VARIABLE_TYPE = '{{ type }}';
|
||||||
|
|
||||||
class Keyfilter {
|
class Keyfilter {
|
||||||
constructor(config) {
|
constructor(keys, combos) {
|
||||||
if (config == undefined || config.length == 0) {
|
if ((keys === undefined || keys.length === 0) && (combos === undefined || combos.length === 0)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.actions = new Map();
|
this.actions = new Map();
|
||||||
for (var index = 0; index < config.length; index++) {
|
for (var index = 0; index < keys.length; index++) {
|
||||||
var grep = config[index].grep;
|
this.setAction(keys[index]);
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
this.currentCombo = undefined;
|
||||||
}
|
}
|
||||||
filterActive(input) {
|
setAction(config) {
|
||||||
if (this.active) {
|
var type = ACTION_KEYDOWN;
|
||||||
return true;
|
switch (config.type.toLowerCase()) {
|
||||||
|
case ACTION_KEYUP.action:
|
||||||
|
type = ACTION_KEYUP;
|
||||||
|
break;
|
||||||
|
case ACTION_KEYHOLD.action:
|
||||||
|
type = ACTION_KEYHOLD;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
input = input.toString();
|
this.actions.set(config.key.toUpperCase(),
|
||||||
var index = input.indexOf(FILTER_START);
|
{
|
||||||
if (index == -1) {
|
type: type,
|
||||||
return;
|
event: config.event,
|
||||||
}
|
command: config.command,
|
||||||
input = input.substring(index + FILTER_START.length).trim();
|
args: config.args,
|
||||||
this.active = true;
|
combo: config.combo,
|
||||||
return true;
|
delay: function () {
|
||||||
|
if (config.combo === undefined) {
|
||||||
|
return config.delay;
|
||||||
|
}
|
||||||
|
return config.delay || 1000;
|
||||||
|
}(),
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
filter(input) {
|
filter(input) {
|
||||||
if (input == undefined || input.length == 0) {
|
if (input === undefined || input.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
input = input.toString();
|
input = input.toString();
|
||||||
if (!this.filterActive(input)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var lines = input.split("\n");
|
var lines = input.split("\n");
|
||||||
for (var index = 0; index < lines.length; index++) {
|
for (var index = 0; index < lines.length; index++) {
|
||||||
var line = lines[index];
|
var line = lines[index];
|
||||||
if (line.length == 0) {
|
if (line.length === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (var [key, value] of this.actions) {
|
if (!line.startsWith(LINE_START)) {
|
||||||
if (!line.includes(key)) {
|
continue;
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
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) {
|
replaceVariables(filtered) {
|
||||||
|
@ -80,15 +129,68 @@ class Keyfilter {
|
||||||
}
|
}
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
shouldBeDelayed(action) {
|
shouldBeDelayed(event) {
|
||||||
if (action.delay == undefined || action.delay == 0 || action.captured == undefined) {
|
if (event.delay === undefined || event.delay === 0 || event.captured === undefined) {
|
||||||
return false;
|
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() {
|
isValid() {
|
||||||
return this.actions != undefined && this.actions.size > 0;
|
return this.actions != undefined && this.actions.size > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = Keyfilter;
|
module.exports = Keyfilter;
|
|
@ -15,9 +15,8 @@ class Watcher {
|
||||||
for (var key in config) {
|
for (var key in config) {
|
||||||
this[key] = config[key];
|
this[key] = config[key];
|
||||||
}
|
}
|
||||||
this.keyfilter = new Keyfilter(config.keys);
|
this.keyfilter = new Keyfilter(config.keys, config.combos);
|
||||||
this.restart = config.restart;
|
this.restart = config.restart;
|
||||||
this.grep = config.grep;
|
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
}
|
}
|
||||||
start() {
|
start() {
|
||||||
|
@ -26,11 +25,7 @@ class Watcher {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.debug('starting watcher \'' + this.device + '\'...');
|
logger.debug('starting watcher \'' + this.device + '\'...');
|
||||||
var args = [this.device];
|
this.process = spawn("evtest", [this.device]);
|
||||||
if (this.grep) {
|
|
||||||
args.push('|', 'grep', this.grep);
|
|
||||||
}
|
|
||||||
this.process = spawn("evtest", args);
|
|
||||||
this.attachListeners(resolve, reject);
|
this.attachListeners(resolve, reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -65,11 +60,18 @@ class Watcher {
|
||||||
if (filtered == undefined) {
|
if (filtered == undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var msg = 'watcher \'' + this.device + '\' captured event';
|
|
||||||
if (filtered.delayed) {
|
if (filtered.delayed) {
|
||||||
logger.debug('delaying captured \'' + filtered.type + '\' event for \'' + filtered.key + '\' from watcher \'' + this.device + '\'');
|
logger.debug('delaying captured \'' + filtered.type + '\' event for \'' + filtered.key + '\' from watcher \'' + this.device + '\'');
|
||||||
return;
|
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 + '\'');
|
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)
|
cli.execute(filtered.command, filtered.args)
|
||||||
.then(logger.info)
|
.then(logger.info)
|
||||||
|
|
Loading…
Reference in a new issue