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(); for (let index = 0; index < config.length; index++) { this.setAction(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; } setAction(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(); } 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; }(), } ); } filter(input) { if (input === undefined || input.length === 0) { return; } input = input.toString(); let lines = input.split("\n"); for (let index = 0; index < lines.length; index++) { let line = lines[index]; if (line.length === 0) { continue; } if (!line.startsWith(LINE_START)) { continue; } const parsedEvent = this.parseLine(line); if (parsedEvent === undefined) { continue; } for (let [key, event] of this.actions) { if (this.currentCombo === undefined && !this.isParsedEventValid(key, event, parsedEvent)) { continue; } if (this.isStartOfCombo(parsedEvent)) { return this.setComboResult(this.getFilterResult(parsedEvent.key, parsedEvent.type.action)); } if (this.isPartOfCombo(parsedEvent)) { if (parsedEvent.ignore) { continue; } return this.setComboResult(this.getFilterResult(parsedEvent.key, parsedEvent.type.action)); } if (!this.isParsedEventValid(key, event, parsedEvent)) { continue; } if (this.shouldBeDelayed(event)) { const result = this.getFilterResult(parsedEvent.key, event.type.action, event.command); result.delayed = true; return result; } event.captured = new Date().getTime(); return this.getFilterResult(parsedEvent.key, event.type.action, event.command); } } } isParsedEventValid(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) { 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) { if (this.currentCombo !== undefined) { 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(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 }; return true; } isPartOfCombo(parsedEvent) { if (this.hasComboTimedOut()) { this.resetCurrentCombo(); } 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) { break; } 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; } return true; } hasComboTimedOut() { return global.config?.combos?.delay !== undefined && this.currentCombo?.timestamp !== undefined && new Date().getTime() - this.currentCombo.timestamp > global.config?.combos?.delay; } resetCurrentCombo() { this.currentCombo = undefined; } isValid() { return this.actions != undefined && this.actions.size > 0; } } module.exports = Keyfilter;