#!/bin/env node // --- const TYPE_CHARS = /^[@!:=\s]/ const KEY_SYMS = require("./keys.js") const KEY_SYMS_KEYS = KEY_SYMS.keys.map(keySym => keySym.toUpperCase()) const KEY_SYMS_MODS = KEY_SYMS.modifiers.map(keySym => keySym.toUpperCase()) // --- const fs = require("fs") const { exec } = require("child_process") if (!process.argv[2]) { console.error("You need to provide a path to the config!") exit(1) } if (!fs.existsSync(process.argv[2])) { console.error("That file doesn't exist!") process.exit(2) } const CONFIG = fs.readFileSync(process.argv[2]).toString() // Remove irrelevant lines. (Empty and comments) const LINES = CONFIG.split('\n').filter(LINE => LINE && !LINE.match(/^#[\S\s]*$/)) // Tokenize the lines into KBD (keyboard shortcuts), CMD (commands), and DEF (variable definitions) const TOKENS = LINES.map( LINE => { const TYPES = LINE.match(TYPE_CHARS) if (!TYPES) return { type: "KBD", repeat: false, release: false, mode: "normal", binding: LINE.replace(TYPE_CHARS, ''), } if (TYPES[0].match(/^\s/)) return { type: "CMD", command: LINE.replace(/^\s*/, '') } if (TYPES[0].match(/^=/)) return { type: "DEF", key: LINE.replace(/=\s*/, '').split(/\s+/)[0], value: LINE.replace(/=\s*/, '').split(/\s+/).slice(1).join(' ') } const IS_REPEATING = TYPES[0].match(/!/) ? true : false const IS_ON_RELEASE = TYPES[0].match(/@/) ? true : false const IS_IN_DIFFERENT_MODE = TYPES[0].match(/:/) ? true : false return { type: "KBD", repeat: IS_REPEATING, release: IS_ON_RELEASE && IS_REPEATING ? false : IS_ON_RELEASE, mode: IS_IN_DIFFERENT_MODE ? LINE.replace(TYPE_CHARS, '').match(/^\S+/)[0] : "normal", binding: IS_IN_DIFFERENT_MODE ? LINE.replace(TYPE_CHARS, '').replace(/^\S+\s+/, '') : LINE.replace(TYPE_CHARS, '') } } ) const parseVariables = (input, defMap) => input.replace( /\$\w+/, match => { const VAR_NAME = match.replace("$", "") if (defMap[VAR_NAME]) return defMap[VAR_NAME] console.error("\nUsed undefined or empty variable:", VAR_NAME, "\nContext:", input, "\nVariables in scope:", defMap, ) } ) let defMap = {} let cursor = 0 while (cursor < TOKENS.length) { const TOKEN = TOKENS[cursor] switch (TOKEN.type) { case "DEF": defMap[TOKEN.key] = TOKEN.value ++cursor break case "KBD": const PARSE_VARS = parseVariables(TOKEN.binding, defMap) const ALL_KEYS = PARSE_VARS.split(/\s*\+\s*/) const MOD_KEYS = ALL_KEYS.filter(key => KEY_SYMS_MODS.includes(key.toUpperCase())) const NORM_KEY = ALL_KEYS.filter(key => KEY_SYMS_KEYS.includes(key.toUpperCase())) const WTF_KEYS = ALL_KEYS.filter(key => !KEY_SYMS_KEYS.includes(key.toUpperCase()) && !KEY_SYMS_MODS.includes(key.toUpperCase())) const WTF_KEYS_ERR = WTF_KEYS[0] ? true : false const NORM_KEY_ERR = NORM_KEY.length > 1 const NO_KEY_ERR = !NORM_KEY[0] ? true : false const NXT_TKN_ERR = TOKENS[cursor + 1]?.type !== "CMD" if (WTF_KEYS_ERR || NORM_KEY_ERR || NO_KEY_ERR || NXT_TKN_ERR) { WTF_KEYS_ERR ? console.error("\nWtf do you mean with these keys?", WTF_KEYS) : null NORM_KEY_ERR ? console.error("\nYou have more than one key specified!", NORM_KEY) : null NO_KEY_ERR ? console.error("\nYou haven't specified a key!") : null NXT_TKN_ERR ? console.error("\nKeybinding is not followed by command!", ALL_KEYS.join(" + ")) : null ++cursor break } const MODE = TOKEN.mode const REPEAT = TOKEN.repeat ? "-repeat " : "" const RELEASE = TOKEN.release ? "-release " : "" const MOD = MOD_KEYS[0] ? MOD_KEYS.join("+") : "None" const KEY = NORM_KEY[0] ?? "" const COMMAND = TOKENS[cursor + 1].command const RUN_CMD = `riverctl map ${REPEAT}${RELEASE}${MODE} ${MOD} ${KEY} ${COMMAND}` console.log("\nRunning command:", RUN_CMD) exec( RUN_CMD, (err, stdout, stderr) => { err && console.error("\nError while running command!\n", err) stdout && console.log("\nSTDOUT:", stdout) stderr && console.log("\nSTDOUT:", stderr) } ) cursor += 2 break default: ++cursor break } }