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