added 'sudo' functionality and partially fixed combos

This commit is contained in:
Daniel Sommer 2022-03-10 17:06:44 +01:00
parent 2356ecd801
commit 1eb5ed1d5a
5 changed files with 112 additions and 52 deletions

View file

@ -4,35 +4,44 @@ node input watcher
## requirements ## requirements
- node.js
- yarn
- evtest - evtest
- node.js
- [nvm](https://github.com/nvm-sh/nvm)
## setup ## setup (as root)
- clone the project - install nvm
`git clone https://git.velvettear.de/velvettear/ninwa.git`
- enter the project directory - to load nvm restart your terminal or `source ~/.nvm/nvm.sh`
`cd ninwa`
- installed required modules - clone the project (to '/opt/ninwa')
`yarn install` `git clone https://git.velvettear.de/velvettear/ninwa.git /opt/ninwa`
- modify `config.json` according to your needs - install and switch to a supported node.js version (automatically done via .nvmrc file)
`nvm install`
- install the required modules
`npm install`
- switch back to your system's default node.js version
`nvm deactivate`
- execute ninwa - execute ninwa
`node ninwa.js` `nvm run ninwa.js`
## systemd ## systemd
**for security reasons it is highly recommended to not run ninwa with root permissions!** **for security reasons it is highly recommended to not run ninwa with root permissions!**
- create a new system user - create a new system user
`useradd -r -s /usr/bin/nologin ninwa` `useradd -U -r -s /usr/bin/nologin node`
- make your install of nvm available to the new user
`cp -R ~/.nvm /opt/nvm`
`chown -R node /opt/nvm`
- symlink the provided systemd-service file and modify it according to your needs - symlink the provided systemd-service file and modify it according to your needs
`ln -s /path/to/ninwa/ninwa.service /etc/systemd/system/ninwa.service` `ln -s /opt/ninwa/ninwa.service /etc/systemd/system/ninwa.service`
- reload systemd-services - reload systemd-services
`systemctl daemon-reload` `systemctl daemon-reload`

View file

@ -13,26 +13,39 @@
"key": "key_f1", "key": "key_f1",
"combo": [ "combo": [
"key_f2", "key_f2",
"key_f4"
],
"event": "EV_KEY",
"type": "keydown",
"delay": 1000,
"command": "combo"
},
{
"key": "key_f1",
"combo": [
"key_f3" "key_f3"
], ],
"event": "EV_KEY", "event": "EV_KEY",
"type": "keydown", "type": "keydown",
"delay": 3000, "delay": 1000,
"command": "example" "command": "combo2"
}, },
{ {
"key": "key_enter", "key": "key_enter",
"type": "keydown", "type": "keydown",
"command": "example2" "event": "EV_KEY",
"command": "example"
}, },
{ {
"key": "key_esc", "key": "key_esc",
"type": "keyup", "type": "keyup",
"command": "example" "event": "EV_KEY",
"command": "example2"
}, },
{ {
"key": "key_space", "key": "key_space",
"type": "keyhold", "type": "keyhold",
"event": "EV_KEY",
"delay": 1000, "delay": 1000,
"command": "curl" "command": "curl"
} }
@ -40,10 +53,26 @@
} }
], ],
"commands": { "commands": {
"combo": {
"cmd": "notify-send",
"args": [
"combo #1",
"first combo"
],
"sudo": false
},
"combo2": {
"cmd": "notify-send",
"args": [
"combo #2",
"second combo"
],
"sudo": false
},
"example": { "example": {
"cmd": "notify-send", "cmd": "notify-send",
"args": [ "args": [
"example", "example #1",
"first example" "first example"
], ],
"sudo": false "sudo": false
@ -51,7 +80,7 @@
"example2": { "example2": {
"cmd": "notify-send", "cmd": "notify-send",
"args": [ "args": [
"example2", "example #2",
"second example" "second example"
], ],
"sudo": false "sudo": false

View file

@ -10,8 +10,8 @@ const VARIABLE_KEY = '{{ key }}';
const VARIABLE_TYPE = '{{ type }}'; const VARIABLE_TYPE = '{{ type }}';
class Keyfilter { class Keyfilter {
constructor(keys, combos) { constructor(keys) {
if ((keys === undefined || keys.length === 0) && (combos === undefined || combos.length === 0)) { if ((keys === undefined || keys.length === 0)) {
return; return;
} }
this.actions = new Map(); this.actions = new Map();
@ -30,7 +30,13 @@ class Keyfilter {
type = ACTION_KEYHOLD; type = ACTION_KEYHOLD;
break; break;
} }
this.actions.set(config.key.toUpperCase(), let key = config.key.toUpperCase();
if (config.combo !== undefined && config.combo.length > 0) {
const tmp = JSON.parse(JSON.stringify(config.combo));
tmp.unshift(config.key);
key = tmp.toString().toUpperCase();
}
this.actions.set(key,
{ {
type: type, type: type,
event: config.event, event: config.event,
@ -67,7 +73,7 @@ class Keyfilter {
if (this.currentCombo === undefined && !this.isParsedEventValid(key, event, parsedEvent)) { if (this.currentCombo === undefined && !this.isParsedEventValid(key, event, parsedEvent)) {
continue; continue;
} }
if (this.isStartOfCombo(key, event)) { if (this.isStartOfCombo(key, event, parsedEvent)) {
return this.getFilterResult(key, event, 'combo', this.currentCombo); return this.getFilterResult(key, event, 'combo', this.currentCombo);
} }
if (this.isPartOfCombo(parsedEvent)) { if (this.isPartOfCombo(parsedEvent)) {
@ -75,7 +81,7 @@ class Keyfilter {
continue; continue;
} }
const result = this.getFilterResult(key, event, 'combo', this.currentCombo); const result = this.getFilterResult(key, event, 'combo', this.currentCombo);
if (this.hasComboFinished()) { if (this.currentCombo.finished) {
this.resetCurrentCombo(); this.resetCurrentCombo();
} }
return result; return result;
@ -92,21 +98,22 @@ class Keyfilter {
} }
} }
isParsedEventValid(key, value, parsed) { isParsedEventValid(key, value, parsed) {
return key === parsed.key && (value.event === undefined || value.event === parsed.event) && value.type.id === parsed.type; let keyCheck = key === parsed.key;
if (value.combo !== undefined && value.combo.length > 0) {
keyCheck = key.includes(parsed.key);
}
return keyCheck && (value.event === undefined || value.event === parsed.event) && value.type.id === parsed.type;
} }
getFilterResult(key, event, extraName, extra) { getFilterResult(key, event, extraName, extra) {
if (key === undefined || event === undefined) { if (key === undefined || event === undefined) {
return; return;
} }
let result = let result = {
// this.replaceVariables( key: key,
{ type: event.type.action,
key: key, command: event.command,
type: event.type.action, delay: event.delay
command: event.command, };
delay: event.delay
}
// )
if (extraName !== undefined && extra !== undefined) { if (extraName !== undefined && extra !== undefined) {
result[extraName] = extra; result[extraName] = extra;
} }
@ -129,18 +136,27 @@ class Keyfilter {
} }
return new Date().getTime() - event.captured < event.delay; return new Date().getTime() - event.captured < event.delay;
} }
isStartOfCombo(key, event) { isStartOfCombo(key, event, parsedEvent) {
if (this.currentCombo !== undefined || event.combo === undefined) { if (event.combo === undefined || this.currentCombo !== undefined) {
return false; return false;
} }
let possibilities = [];
for (let [actionKey, actionEvent] of this.actions) {
if (actionEvent.combo === undefined || actionEvent.combo.length === 0) {
continue;
}
if (!actionKey.toUpperCase().startsWith(parsedEvent.key)) {
continue;
}
possibilities.push(actionEvent.combo);
}
this.currentCombo = { this.currentCombo = {
key: key, key: key,
type: event.type, type: event.type,
delay: event.delay, delay: event.delay,
timestamp: new Date().getTime(), timestamp: new Date().getTime(),
done: [key], done: [parsedEvent.key],
remaining: JSON.parse(JSON.stringify(event.combo)), possibilities: [event.combo]
finished: this.hasComboFinished()
}; };
return true; return true;
} }
@ -158,22 +174,27 @@ class Keyfilter {
if (this.currentCombo.type.id !== parsedEvent.type) { if (this.currentCombo.type.id !== parsedEvent.type) {
return false; return false;
} }
if (this.currentCombo.remaining[0].toUpperCase() !== parsedEvent.key) { let combos = [];
for (let index = 0; index < this.currentCombo.possibilities.length; index++) {
const possibility = this.currentCombo.possibilities[index];
if (possibility[0].toUpperCase() !== parsedEvent.key) {
continue;
}
combos.push(possibility);
}
if (combos.length === 0) {
return false; return false;
} }
this.currentCombo.key = parsedEvent.key; this.currentCombo.key = parsedEvent.key;
this.currentCombo.timestamp = new Date().getTime(); this.currentCombo.timestamp = new Date().getTime();
this.currentCombo.done.push(parsedEvent.key); this.currentCombo.done.push(parsedEvent.key);
this.currentCombo.remaining.shift(); if (this.currentCombo.possibilities.length === 1) {
this.currentCombo.finished = this.hasComboFinished(); this.currentCombo.finished = true;
}
return true; return true;
} }
hasComboFinished() {
return this.currentCombo !== undefined &&
this.currentCombo.remaining !== undefined &&
this.currentCombo.remaining.length === 0;
}
hasComboTimedOut() { hasComboTimedOut() {
return false;
const result = this.currentCombo !== undefined && const result = this.currentCombo !== undefined &&
this.currentCombo.delay !== undefined && this.currentCombo.delay !== undefined &&
this.currentCombo.timestamp !== undefined && this.currentCombo.timestamp !== undefined &&

View file

@ -16,7 +16,7 @@ class Watcher {
for (let key in config) { for (let key in config) {
this[key] = config[key]; this[key] = config[key];
} }
this.keyfilter = new Keyfilter(config.keys, config.combos); this.keyfilter = new Keyfilter(config.keys);
this.restart = config.restart; this.restart = config.restart;
this.callback = callback; this.callback = callback;
} }
@ -75,7 +75,7 @@ class Watcher {
} }
if (filtered.combo) { if (filtered.combo) {
if (!filtered.combo.finished) { 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] + '\')'); logger.debug('captured \'' + filtered.combo.type.action + '\' event for \'' + filtered.combo.key + '\' from watcher \'' + this.device + '\' is part of a combo (possibilities: \'' + filtered.combo.possibilities.length + '\')');
return; return;
} }
logger.debug('captured \'' + filtered.combo.type.action + '\' event for \'' + filtered.combo.key + '\' from watcher \'' + this.device + '\' is the last part of a combo'); logger.debug('captured \'' + filtered.combo.type.action + '\' event for \'' + filtered.combo.key + '\' from watcher \'' + this.device + '\' is the last part of a combo');

View file

@ -3,9 +3,10 @@ Description=ninwa (node input watcher)
[Service] [Service]
Type=simple Type=simple
User=ninwa User=node
Group=ninwa Group=node
ExecStart=node /opt/ninwa/ninwa.js WorkingDirectory=/opt/ninwa
ExecStart=/opt/nvm/nvm-exec node ninwa.js
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target