initial commit
This commit is contained in:
commit
bf1242f350
12 changed files with 546 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules/
|
||||||
|
yarn.lock
|
14
.vscode/launch.json
vendored
Normal file
14
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "pwa-node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "ninwa",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"program": "${workspaceFolder}/ninwa.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# ninwa
|
||||||
|
|
||||||
|
node input watcher
|
42
config.json
Normal file
42
config.json
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "debug",
|
||||||
|
"timestamp": "DD.MM.YYYY HH:mm:ss:SS"
|
||||||
|
},
|
||||||
|
"watchers": [
|
||||||
|
{
|
||||||
|
"device": "usb-Razer_Razer_Blade_Stealth-if01-event-kbd",
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"key": "f9",
|
||||||
|
"type": "keydown",
|
||||||
|
"command": "notify-send",
|
||||||
|
"args": [
|
||||||
|
"F9 DOWN",
|
||||||
|
"$(date)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "f8",
|
||||||
|
"type": "keyup",
|
||||||
|
"delay": 2000,
|
||||||
|
"command": "notify-send",
|
||||||
|
"args": [
|
||||||
|
"F8 UP",
|
||||||
|
"$(date)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "f7",
|
||||||
|
"type": "keyhold",
|
||||||
|
"delay": 1000,
|
||||||
|
"command": "notify-send",
|
||||||
|
"args": [
|
||||||
|
"F7 HOLD",
|
||||||
|
"$(date)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
44
libs/cli.js
Normal file
44
libs/cli.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
var startTime = new Date().getTime();
|
||||||
|
var resultData = "";
|
||||||
|
var resultError = "";
|
||||||
|
command = command.trim();
|
||||||
|
logger.debug('executing command \'' + command + '\' (args: \'' + args + '\') ...');
|
||||||
|
try {
|
||||||
|
var process = spawn(command, args);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
process.stdout.on('data', (data) => {
|
||||||
|
resultData += data;
|
||||||
|
});
|
||||||
|
process.stderr.on('data', (data) => {
|
||||||
|
resultError += data;
|
||||||
|
});
|
||||||
|
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) {
|
||||||
|
msg += " > data: " + resultData;
|
||||||
|
}
|
||||||
|
if (resultError.length > 0) {
|
||||||
|
msg += " >>> error: " + resultError;
|
||||||
|
reject(msg);
|
||||||
|
}
|
||||||
|
resolve(msg);
|
||||||
|
});
|
||||||
|
process.on('error', (err) => {
|
||||||
|
resultError += err;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
execute
|
||||||
|
}
|
84
libs/keyfilter.js
Normal file
84
libs/keyfilter.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
const logger = require('./logger.js');
|
||||||
|
|
||||||
|
const FILTER_START = 'Testing ... (interrupt to exit)';
|
||||||
|
|
||||||
|
const KEY_PREFIX = 'KEY_';
|
||||||
|
|
||||||
|
const ACTION_KEYUP = { id: 0, action: 'keyup' };
|
||||||
|
const ACTION_KEYDOWN = { id: 1, action: 'keydown' };
|
||||||
|
const ACTION_KEYHOLD = { id: 2, action: 'keyhold' };
|
||||||
|
|
||||||
|
class Keyfilter {
|
||||||
|
constructor(config) {
|
||||||
|
if (config == undefined || config.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.actions = new Map();
|
||||||
|
for (var index = 0; index < config.length; 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(KEY_PREFIX + config[index].key.toUpperCase(), { type, command, args, delay });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filterActive(input) {
|
||||||
|
if (this.active) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
filter(input) {
|
||||||
|
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) {
|
||||||
|
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.executed = new Date().getTime();
|
||||||
|
return { key: key, type: action.type.action, command: action.command, args: action.args };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shouldBeDelayed(action) {
|
||||||
|
if (action.delay == undefined || action.delay == 0 || action.executed == undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return new Date().getTime() - action.executed < action.delay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Keyfilter;
|
98
libs/logger.js
Normal file
98
libs/logger.js
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
const config = require('../config.json');
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
|
// constants
|
||||||
|
const LOG_PREFIX_DEBUG = 'debug';
|
||||||
|
const LOG_PREFIX_INFO = 'info';
|
||||||
|
const LOG_PREFIX_WARNING = 'warning';
|
||||||
|
const LOG_PREFIX_ERROR = 'error';
|
||||||
|
const LOGLEVEL_DEBUG = 0;
|
||||||
|
const LOGLEVEL_INFO = 1;
|
||||||
|
const LOGLEVEL_WARNING = 2;
|
||||||
|
const LOGLEVEL_ERROR = 3;
|
||||||
|
|
||||||
|
// set loglevel on 'require'
|
||||||
|
const loglevel = function () {
|
||||||
|
switch (config.log.level) {
|
||||||
|
case LOG_PREFIX_DEBUG:
|
||||||
|
case LOGLEVEL_DEBUG:
|
||||||
|
return LOGLEVEL_DEBUG;
|
||||||
|
case LOG_PREFIX_INFO:
|
||||||
|
case LOGLEVEL_INFO:
|
||||||
|
return LOGLEVEL_INFO;
|
||||||
|
case LOG_PREFIX_WARNING:
|
||||||
|
case LOGLEVEL_WARNING:
|
||||||
|
return LOGLEVEL_WARNING;
|
||||||
|
case LOG_PREFIX_ERROR:
|
||||||
|
case LOGLEVEL_ERROR:
|
||||||
|
return LOGLEVEL_ERROR;
|
||||||
|
default:
|
||||||
|
return LOGLEVEL_INFO;
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
// prefix log with 'info'
|
||||||
|
function info(message) {
|
||||||
|
if (loglevel > LOGLEVEL_INFO) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
trace(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefix log with 'info'
|
||||||
|
function warn(message) {
|
||||||
|
if (loglevel > LOGLEVEL_WARNING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
trace(message, 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefix log with 'debug'
|
||||||
|
function debug(message) {
|
||||||
|
if (loglevel > LOGLEVEL_DEBUG) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
trace(message, 'debug');
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefix log with 'error'
|
||||||
|
function error(message) {
|
||||||
|
if (loglevel > LOGLEVEL_ERROR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
trace(message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// default logging function
|
||||||
|
function trace(message, prefix) {
|
||||||
|
if (message === undefined || message === null || message.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (prefix === undefined || prefix === null || prefix.length === 0) {
|
||||||
|
prefix = 'info';
|
||||||
|
}
|
||||||
|
let print;
|
||||||
|
switch (prefix) {
|
||||||
|
case 'error':
|
||||||
|
print = console.error;
|
||||||
|
break;
|
||||||
|
case 'debug':
|
||||||
|
print = console.debug;
|
||||||
|
break;
|
||||||
|
case 'warning':
|
||||||
|
print = console.warn;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
print = console.log;
|
||||||
|
}
|
||||||
|
message = moment().format(config.log.timestamp) + ' | ' + prefix + ' > ' + message;
|
||||||
|
print(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// exports
|
||||||
|
module.exports = {
|
||||||
|
info,
|
||||||
|
warn,
|
||||||
|
debug,
|
||||||
|
error
|
||||||
|
};
|
43
libs/util.js
Normal file
43
libs/util.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
const realpath = require('fs/promises').realpath;
|
||||||
|
const stat = require('fs/promises').stat;
|
||||||
|
|
||||||
|
function fileExists(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (file == undefined) {
|
||||||
|
reject('error: no file given');
|
||||||
|
}
|
||||||
|
resolvePath(file)
|
||||||
|
.then((path) => {
|
||||||
|
stat(path)
|
||||||
|
.then((stats) => {
|
||||||
|
resolve({path, stats});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePath(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (file == undefined) {
|
||||||
|
reject('error: no file given');
|
||||||
|
}
|
||||||
|
realpath(file)
|
||||||
|
.then(resolve)
|
||||||
|
.catch((err) => {
|
||||||
|
reject('error: resolving path \'' + file + '\' encountered an error >>> ' + err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeCommand(command) {
|
||||||
|
if (command == undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
command = command.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
fileExists,
|
||||||
|
resolvePath
|
||||||
|
}
|
120
libs/watcher.js
Normal file
120
libs/watcher.js
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
const logger = require('./logger.js');
|
||||||
|
const util = require('./util.js');
|
||||||
|
const Keyfilter = require('./keyfilter.js');
|
||||||
|
const cli = require('./cli.js');
|
||||||
|
const spawn = require('child_process').spawn;
|
||||||
|
|
||||||
|
const inputDevices = '/dev/input/';
|
||||||
|
const inputDevicesById = '/dev/input/by-id/';
|
||||||
|
|
||||||
|
class Watcher {
|
||||||
|
constructor(config, callback) {
|
||||||
|
if (config == undefined || config.device == undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (var key in config) {
|
||||||
|
this[key] = config[key];
|
||||||
|
}
|
||||||
|
// if (!this.device.startsWith(inputDevices)) {
|
||||||
|
// this.device = inputDevices + this.device;
|
||||||
|
// }
|
||||||
|
this.keyfilter = new Keyfilter(config.keys);
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
if (this.process != undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.debug('starting watcher \'' + this.device + '\'...');
|
||||||
|
this.process = spawn("evtest", [this.device]);
|
||||||
|
this.attachListeners();
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
if (this.process == undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.debug('stopping watcher \'' + this.device + '\'...');
|
||||||
|
this.process.kill();
|
||||||
|
if (this.callback != undefined) {
|
||||||
|
this.callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attachListeners() {
|
||||||
|
if (this.process == undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.addStdOutListener();
|
||||||
|
this.addStdErrListener();
|
||||||
|
this.addCloseListener();
|
||||||
|
this.addErrorListener();
|
||||||
|
}
|
||||||
|
addStdOutListener() {
|
||||||
|
if (this.process == undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.debug('adding stdout listener to watcher \'' + this.device + '\'...');
|
||||||
|
this.process.stdout.on('data', (data) => {
|
||||||
|
if (this.keyfilter == undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var filtered = this.keyfilter.filter(data);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
.catch(logger.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
addStdErrListener() {
|
||||||
|
if (this.process == undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.debug('adding stderr listener to watcher \'' + this.device + '\'...');
|
||||||
|
this.process.stderr.on('data', (data) => {
|
||||||
|
logger.error(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
addCloseListener() {
|
||||||
|
if (this.process == undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.debug('adding close listener to watcher \'' + this.device + '\'...');
|
||||||
|
this.process.on('close', (code) => {
|
||||||
|
logger.info('watcher \'' + this.device + '\' finished with exit code ' + code);
|
||||||
|
if (this.callback != undefined) {
|
||||||
|
this.callback(null, this.device, code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
addErrorListener() {
|
||||||
|
if (this.process == undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.debug('adding error listener to \'' + this.device + '\'...');
|
||||||
|
this.process.on('error', (err) => {
|
||||||
|
logger.error('error: watcher \'' + this.device + '\' encountered an error >>> ' + err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
check() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Promise.any([inputDevices + this.device, inputDevicesById + this.device].map(util.fileExists))
|
||||||
|
.then((result) => {
|
||||||
|
if (result.path != this.device) {
|
||||||
|
logger.info('resolved watcher for device \'' + this.device + '\' to \'' + result.path + '\'')
|
||||||
|
}
|
||||||
|
logger.info('resolved ')
|
||||||
|
this.device = result.path;
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = Watcher;
|
62
libs/watchers.js
Normal file
62
libs/watchers.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
const configJSON = require('../config.json');
|
||||||
|
const logger = require('./logger.js');
|
||||||
|
const Watcher = require('./watcher.js');
|
||||||
|
|
||||||
|
const watchers = [];
|
||||||
|
|
||||||
|
function initialize() {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (configJSON == undefined || configJSON.watchers == undefined || configJSON.watchers.length == 0) {
|
||||||
|
reject('error: no input devices defined');
|
||||||
|
}
|
||||||
|
var tmp = []
|
||||||
|
for (var index = 0; index < configJSON.watchers.length; index++) {
|
||||||
|
tmp.push(new Watcher(configJSON.watchers[index]));
|
||||||
|
}
|
||||||
|
Promise.all(tmp.map(check)).then(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function check(watcher) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (watcher == undefined) {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
watcher.check()
|
||||||
|
.then(() => {
|
||||||
|
logger.info('watcher \'' + watcher.device)
|
||||||
|
watcher.start();
|
||||||
|
watchers.push(watcher);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(err);d
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
logger.debug('starting ' + watchers.size + ' watcher(s)...');
|
||||||
|
for (const watcher of watchers.entries()) {
|
||||||
|
watcher[1].start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
const count = watchers.size;
|
||||||
|
logger.info('stopping all ' + count + ' watchers...');
|
||||||
|
for (var index = 0; index < count; index++) {
|
||||||
|
var watcher = watchers[index];
|
||||||
|
logger.debug('stopping watcher ' + watcher.id + '...');
|
||||||
|
watcher.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function callback(err, id, code) {
|
||||||
|
this.watchers.delete(id);
|
||||||
|
logger.debug('removed watcher \'' + id + '\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initialize,
|
||||||
|
start,
|
||||||
|
stop
|
||||||
|
}
|
12
ninwa.js
Normal file
12
ninwa.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
const watchers = require('./libs/watchers.js');
|
||||||
|
const logger = require('./libs/logger.js');
|
||||||
|
const packageJSON = require('./package.json');
|
||||||
|
|
||||||
|
logger.info(packageJSON.name + ' ' + packageJSON.version);
|
||||||
|
|
||||||
|
watchers.initialize()
|
||||||
|
.then(watchers.start)
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
22
package.json
Normal file
22
package.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "ninwa",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "node input watcher",
|
||||||
|
"main": "ninwa.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.velvettear.de/velvettear/ninwa.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"input",
|
||||||
|
"keyboard"
|
||||||
|
],
|
||||||
|
"author": "Daniel Sommer <daniel.sommer@velvettear.de>",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"moment": "^2.29.1"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue