const logger = require('./logger.js'); const LINE_START = 'Event: time'; 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(); this.registered = { keys: [], events: [], types: [] } for (let index = 0; index < config.length; index++) { this.registerAction(config[index]); } this.currentCombo = undefined; } getCommand(command) { if (command === undefined || global.config?.commands === undefined) { return; } const result = global.config.commands[command]; if (result === undefined) { return; } result.name = command; return result; } getActionType(type) { let result = ACTION_KEYDOWN; if (isNaN(type)) { switch (type.toLowerCase()) { case ACTION_KEYUP.action: result = ACTION_KEYUP; break; case ACTION_KEYHOLD.action: result = ACTION_KEYHOLD; break; } } else { switch (type) { case ACTION_KEYUP.id: result = ACTION_KEYUP; break; case ACTION_KEYHOLD.id: result = ACTION_KEYHOLD; break; } } return result; } registerAction(config) { 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(); } if (Array.from(this.actions.keys()).includes(key)) { logger.warn('skipping already registered key(s) \'' + key + '\'...'); return; } this.actions.set(key, { type: this.getActionType(config.type), event: config.event, command: this.getCommand(config.command), combo: config.combo, delay: function () { if (config.combo === undefined) { return config.delay; } return config.delay || 1000; }(), } ); const singleKeys = key.split(','); for (let index = 0; index < singleKeys.length; index++) { const singleKey = singleKeys[index]; if (!this.registered.keys.includes(singleKey)) { this.registered.keys.push(singleKey); } } if (!this.registered.events.includes(this.actions.get(key).event)) { this.registered.events.push(this.actions.get(key).event); } if (!this.registered.types.includes(this.actions.get(key).type.id)) { this.registered.types.push(this.actions.get(key).type.id); } } filter(input) { if (input === undefined || input.length === 0) { return; } const parsedEvent = this.parseLine(input); if (this.parsedEventIsUnknown(parsedEvent)) { return; } if (this.isPartOfCombo(parsedEvent)) { if (parsedEvent.ignore) { return; } return this.setComboResult(this.getFilterResult(parsedEvent.key, parsedEvent.type.action)); } if (this.isStartOfCombo(parsedEvent)) { return this.setComboResult(this.getFilterResult(parsedEvent.key, parsedEvent.type.action)); } for (let [key, event] of this.actions) { if (!this.parsedEventIsValid(key, event, parsedEvent)) { continue; } const result = this.getFilterResult(parsedEvent.key, event.type.action, event.command); if (this.shouldBeDelayed(event)) { result.delayed = true; return result; } event.captured = new Date().getTime(); return result; } } parsedEventIsUnknown(parsedEvent) { return parsedEvent === undefined || !this.registered.keys.includes(parsedEvent.key) || !this.registered.events.includes(parsedEvent.event) || !this.registered.types.includes(parsedEvent.type.id); } parsedEventIsValid(key, value, parsed) { if (value.event !== parsed.event || value.type.id !== parsed.type.id) { return false; } if (value.combo === undefined || value.combo.length === 0) { return key === parsed.key; } if (this.currentCombo === undefined) { return key.startsWith(parsed.key); } for (let index = 0; index < this.currentCombo.possibilities.length; index++) { if (this.currentCombo.possibilities[index].combo[0].toUpperCase() === parsed.key) { this.resetCurrentCombo(); return true; } } return false; } setComboResult(result) { if (result === undefined || this.currentCombo === undefined) { return; } if (!this.currentCombo.finished) { result.combo = { possibilities: this.currentCombo.possibilities.length }; } else { result.combo = { finished: true, done: this.currentCombo.done }; result.command = this.currentCombo.command; } return result } getFilterResult(key, type, command) { if (key === undefined || type === undefined) { return; } return { key: key, type: type, command: command }; } parseLine(line) { if (line === undefined || line.length === 0 || !line.startsWith(LINE_START)) { return; } try { const parts = line.split(','); const event = parts[1].substring(parts[1].indexOf('(') + 1, parts[1].lastIndexOf(')')); const key = parts[2].substring(parts[2].indexOf('(') + 1, parts[2].indexOf(')')); const type = this.getActionType(parseInt(parts[3].split(' ').pop())); return { event: event, key: key, type: type }; } catch (err) { return; } } shouldBeDelayed(event) { if (event.delay === undefined || event.delay === 0 || event.captured === undefined) { return false; } return new Date().getTime() - event.captured < event.delay; } isStartOfCombo(parsedEvent) { let possibilities = []; for (let [actionKey, actionEvent] of this.actions) { if (actionEvent.combo === undefined || actionEvent.combo.length === 0) { continue; } if (!actionKey.toUpperCase().startsWith(parsedEvent.key) || actionEvent.type.id !== parsedEvent.type.id || actionEvent.event !== parsedEvent.event) { continue; } possibilities.push(JSON.parse(JSON.stringify(actionEvent))); } if (possibilities.length === 0) { return false; } this.currentCombo = { key: parsedEvent.key.toUpperCase(), timestamp: new Date().getTime(), type: parsedEvent.type, event: parsedEvent.event, done: [parsedEvent.key], possibilities: possibilities }; this.setComboTimeout(); return true; } isPartOfCombo(parsedEvent) { if (this.currentCombo === undefined || this.currentCombo.type.id !== parsedEvent.type.id || this.currentCombo.event !== parsedEvent.event) { return false; } let possibilities = []; for (let index = 0; index < this.currentCombo.possibilities.length; index++) { const possibility = this.currentCombo.possibilities[index]; if (possibility.combo.length === 0) { continue; } if (possibility.combo[0].toUpperCase() !== parsedEvent.key) { continue; } possibility.combo.shift(); possibilities.push(possibility); } if (possibilities.length === 0) { return false; } this.currentCombo.timestamp = new Date().getTime(); this.currentCombo.done.push(parsedEvent.key); this.currentCombo.possibilities = possibilities; this.currentCombo.finished = false; if (this.currentCombo.possibilities.length === 1 && this.currentCombo.possibilities[0].combo.length === 0) { const tmp = this.currentCombo.possibilities[0]; delete tmp.combo; tmp.finished = true; tmp.timestamp = this.currentCombo.timestamp; tmp.done = this.currentCombo.done; this.currentCombo = tmp; } this.setComboTimeout(); return true; } setComboTimeout() { if (this.currentCombo === undefined || global.config?.combos?.timeout === undefined || isNaN(global.config?.combos?.timeout)) { return; } this.clearComboTimeout(); logger.debug('setting timeout for current combo to ' + parseInt(global.config.combos.timeout) + 'ms'); this.comboTimeout = setTimeout(() => { this.resetCurrentCombo() }, parseInt(global.config.combos.timeout)); } clearComboTimeout() { if (this.comboTimeout === undefined) { return; } logger.debug('clearing timeout for current combo'); clearTimeout(this.comboTimeout); } resetCurrentCombo() { this.clearComboTimeout(); logger.debug('resetting current combo'); this.currentCombo = undefined; } isValid() { return this.actions != undefined && this.actions.size > 0; } } module.exports = Keyfilter;