initial commit

This commit is contained in:
Daniel Sommer 2022-02-15 04:33:19 +01:00
commit bf1242f350
12 changed files with 546 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules/
yarn.lock

14
.vscode/launch.json vendored Normal file
View 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
View file

@ -0,0 +1,3 @@
# ninwa
node input watcher

42
config.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"
}
}