river-binder/index.js

147 lines
5.0 KiB
JavaScript
Executable File

#!/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
}
}