added 'sudo' functionality and partially fixed combos
This commit is contained in:
parent
2356ecd801
commit
1eb5ed1d5a
5 changed files with 112 additions and 52 deletions
35
README.md
35
README.md
|
@ -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`
|
||||||
|
|
41
config.json
41
config.json
|
@ -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
|
||||||
|
|
|
@ -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,
|
key: key,
|
||||||
type: event.type.action,
|
type: event.type.action,
|
||||||
command: event.command,
|
command: event.command,
|
||||||
delay: event.delay
|
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 &&
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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
|
Loading…
Reference in a new issue