keebie/scripts/layouts/parser_qmk.gd
2025-08-02 06:01:31 +10:00

358 lines
8.7 KiB
GDScript

class_name ParserQMK extends AbstractParser
const KEYCODE_MAP: Dictionary[String, Key] = {
"ENT": KEY_ENTER,
"ESC": KEY_ESCAPE,
"BSPC": KEY_BACKSPACE,
"SPC": KEY_SPACE,
"MINS": KEY_MINUS,
"EQL": KEY_EQUAL,
"LEFT_BRACKET": KEY_BRACKETLEFT,
"LBRC": KEY_BRACKETLEFT,
"RIGHT_BRACKET": KEY_BRACKETRIGHT,
"RBRC": KEY_BRACKETRIGHT,
"BSLS": KEY_BACKSLASH,
"NONUS_HASH": KEY_BACKSLASH,
"NUHS": KEY_BACKSLASH,
"SCLN": KEY_SEMICOLON,
"QUOTE": KEY_APOSTROPHE,
"QUOT": KEY_APOSTROPHE,
"GRAVE": KEY_QUOTELEFT,
"GRV": KEY_QUOTELEFT,
"COMM": KEY_COMMA,
"DOT": KEY_PERIOD,
"SLSH": KEY_SLASH,
"NONUS_BACKSLASH": KEY_SECTION,
"NUBS": KEY_SECTION,
"CAPS_LOCK": KEY_CAPSLOCK,
"CAPS": KEY_CAPSLOCK,
"SCROLL_LOCK": KEY_SCROLLLOCK,
"SCRL": KEY_SCROLLLOCK,
"BRMD": KEY_SCROLLLOCK,
"NUM_LOCK": KEY_NUMLOCK,
"NUM": KEY_NUMLOCK,
"PRINT_SCREEN": KEY_PRINT,
"PSCR": KEY_PRINT,
"PAUS": KEY_PAUSE,
"BRK": KEY_PAUSE,
"BRMU": KEY_PAUSE,
"INS": KEY_INSERT,
"PAGE_UP": KEY_PAGEUP,
"PGUP": KEY_PAGEUP,
"DEL": KEY_DELETE,
"PAGE_DOWN": KEY_PAGEDOWN,
"PGDN": KEY_PAGEDOWN,
"RGHT": KEY_RIGHT,
"APPLICATION": KEY_MENU,
"APP": KEY_MENU,
"SYSTEM_REQUEST": KEY_SYSREQ,
"SYRQ": KEY_SYSREQ,
"CLR": KEY_CLEAR,
"RETURN": KEY_ENTER,
"RETN": KEY_ENTER,
"AUDIO_MUTE": KEY_VOLUMEMUTE,
"MUTE": KEY_VOLUMEMUTE,
"AUDIO_VOL_UP": KEY_VOLUMEUP,
"VOLU": KEY_VOLUMEUP,
"AUDIO_VOL_DOWN": KEY_VOLUMEDOWN,
"VOLD": KEY_VOLUMEDOWN,
"MEDIA_NEXT_TRACK": KEY_MEDIANEXT,
"MNXT": KEY_MEDIANEXT,
"MEDIA_PREV_TRACK": KEY_MEDIAPREVIOUS,
"MPRV": KEY_MEDIAPREVIOUS,
"MEDIA_STOP": KEY_MEDIASTOP,
"MSTP": KEY_MEDIASTOP,
"MEDIA_PLAY_PAUSE": KEY_MEDIAPLAY,
"MPLY": KEY_MEDIAPLAY,
"MAIL": KEY_LAUNCHMAIL,
"WWW_SEARCH": KEY_SEARCH,
"WSCH": KEY_SEARCH,
"WWW_HOME": KEY_SEARCH,
"WHOM": KEY_SEARCH,
"WWW_BACK": KEY_BACK,
"WBAK": KEY_BACK,
"WWW_FORWARD": KEY_FORWARD,
"WFWD": KEY_FORWARD,
"WWW_STOP": KEY_STOP,
"WSTP": KEY_STOP,
"WWW_REFRESH": KEY_REFRESH,
"WREF": KEY_REFRESH,
"WWW_FAVORITES": KEY_FAVORITES,
"WFAV": KEY_FAVORITES,
"MEDIA_FAST_FORWARD": KEY_MEDIANEXT,
"MFFD": KEY_MEDIANEXT,
"MEDIA_REWIND": KEY_MEDIAPREVIOUS,
"MRWD": KEY_MEDIAPREVIOUS,
"INTERNATIONAL_1": KEY_BACKSLASH,
"INT1": KEY_BACKSLASH,
"INTERNATIONAL_3": KEY_YEN,
"INT3": KEY_YEN,
"KP_SLASH": KEY_KP_DIVIDE,
"PSLS": KEY_KP_DIVIDE,
"KP_ASTERISK": KEY_KP_MULTIPLY,
"PAST": KEY_KP_MULTIPLY,
"KP_MINUS": KEY_KP_SUBTRACT,
"PMNS": KEY_KP_SUBTRACT,
"KP_PLUS": KEY_KP_ADD,
"PPLS": KEY_KP_ADD,
"KP_ENTER": KEY_KP_ENTER,
"PENT": KEY_KP_ENTER,
"KP_1": KEY_KP_1,
"P1": KEY_KP_1,
"KP_2": KEY_KP_2,
"P2": KEY_KP_2,
"KP_3": KEY_KP_3,
"P3": KEY_KP_3,
"KP_4": KEY_KP_4,
"P4": KEY_KP_4,
"KP_5": KEY_KP_5,
"P5": KEY_KP_5,
"KP_6": KEY_KP_6,
"P6": KEY_KP_6,
"KP_7": KEY_KP_7,
"P7": KEY_KP_7,
"KP_8": KEY_KP_8,
"P8": KEY_KP_8,
"KP_9": KEY_KP_9,
"P9": KEY_KP_9,
"KP_0": KEY_KP_0,
"P0": KEY_KP_0,
"KP_DOT": KEY_KP_PERIOD,
"PDOT": KEY_KP_PERIOD,
"KP_COMMA": KEY_KP_PERIOD,
"PCMM": KEY_KP_PERIOD,
}
const KEYCODE_MODIFIER_LEFT_MAP: Dictionary[String, Key] = {
"LEFT_CTRL": KEY_CTRL,
"LCTL": KEY_CTRL,
"LEFT_SHIFT": KEY_SHIFT,
"LSFT": KEY_SHIFT,
"LEFT_ALT": KEY_ALT,
"LOPT": KEY_ALT,
"LEFT_GUI": KEY_META,
"LGUI": KEY_META,
"LCMD": KEY_META,
"LWIN": KEY_META,
}
const KEYCODE_MODIFIER_RIGHT_MAP: Dictionary[String, Key] = {
"RIGHT_CTRL": KEY_CTRL,
"RCTL": KEY_CTRL,
"RIGHT_SHIFT": KEY_SHIFT,
"RSFT": KEY_SHIFT,
"RIGHT_ALT": KEY_ALT,
"ROPT": KEY_ALT,
"RIGHT_GUI": KEY_META,
"RGUI": KEY_META,
"RCMD": KEY_META,
"RWIN": KEY_META,
}
const KEYBOARD_NAME := "keyboard_name"
const LAYOUTS := "layouts"
const LAYOUT := "layout"
const MATRIX := "matrix"
const W := "w"
const H := "h"
const X := "x"
const Y := "y"
const R := "r"
const RX := "rx"
const RY := "ry"
var _name: String
var _rows: Array[Array]
var _file_name: String
func _init(data: Dictionary, file_name: String) -> void:
_file_name = file_name
if data.has(KEYBOARD_NAME) and data[KEYBOARD_NAME] is String:
_name = data[KEYBOARD_NAME]
if (
not data.has(LAYOUTS)
or data[LAYOUTS] is not Dictionary
or (data[LAYOUTS] as Dictionary).size() == 0
):
return
var layout_name := (data[LAYOUTS] as Dictionary).keys()[0] as String
var keymap_keys := _get_keymap_keys(file_name)
if (
data[LAYOUTS][layout_name] is not Dictionary
or not (data[LAYOUTS][layout_name] as Dictionary).has(LAYOUT)
or data[LAYOUTS][layout_name][LAYOUT] is not Array
):
return
_rows = _deserialize_keys(data[LAYOUTS][layout_name][LAYOUT] as Array, keymap_keys)
func get_name() -> String:
return _name
func get_rows() -> Array[Array]:
return _rows
func _deserialize_keys(data_keys: Array, keymap_keys: Array[String]) -> Array[Array]:
var layout_rows: Dictionary[float, Array] = {}
var rows_max_xs: Dictionary[float, float] = {}
var prev_pos_x: float = 0
var prev_pos_y: float = 0
for i in range(data_keys.size()):
if data_keys[i] is not Dictionary:
continue
var data_key := data_keys[i] as Dictionary
if (
not data_key.has(MATRIX)
or not data_key.has(X)
or not data_key.has(Y)
or data_key[MATRIX] is not Array
):
continue
if not layout_rows.has(data_key[Y]):
prev_pos_x = 0
prev_pos_y = data_key[Y]
layout_rows[data_key[Y]] = []
elif prev_pos_y != data_key[Y]:
prev_pos_x = rows_max_xs[data_key[Y]]
prev_pos_y = data_key[Y]
var keycode := (
_get_keycode_from_keymap_key(keymap_keys[i])
if i < keymap_keys.size()
else KEY_NONE
)
var key_dict := {KeyProps.KEY: keycode}
key_dict.merge(
_deserialize_key(
data_key,
data_key[X] as float - prev_pos_x,
data_key[Y] as float - prev_pos_y
)
)
var location := (
_get_key_location(keymap_keys[i])
if i < keymap_keys.size()
else KEY_LOCATION_UNSPECIFIED
)
if location != KEY_LOCATION_UNSPECIFIED:
key_dict[KeyProps.LOC] = location
layout_rows[data_key[Y]].append(key_dict)
var width: float = data_key[W] if data_key.has(W) else 1.0
if not rows_max_xs.has(data_key[Y]):
rows_max_xs[data_key[Y]] = width
elif rows_max_xs[data_key[Y]] < data_key[X] + width:
rows_max_xs[data_key[Y]] = data_key[X] + width
prev_pos_x = data_key[X] + width
prev_pos_y = data_key[Y]
return layout_rows.values()
func _deserialize_key(data_key: Dictionary, pos_x: float, pos_y: float) -> Dictionary:
var key_dict: Dictionary = {}
if data_key.has(W):
key_dict[KeyProps.W] = data_key[W]
if data_key.has(H):
key_dict[KeyProps.H] = data_key[H]
if pos_x != 0:
key_dict[KeyProps.X] = pos_x
if pos_y != 0:
key_dict[KeyProps.Y] = pos_y
if data_key.has(R):
key_dict[KeyProps.R] = data_key[R]
if data_key.has(RX):
key_dict[KeyProps.PX] = data_key[RX]
if data_key.has(RY):
key_dict[KeyProps.PY] = data_key[RY]
return key_dict
func _get_keymap_keys(json_file_name: String) -> Array[String]:
if json_file_name.get_extension().to_lower() != "json":
return []
var c_file_name := json_file_name.substr(0, json_file_name.length() - 4) + "c"
var file := FileAccess.open(
LayoutConfig.CUSTOM_LAYOUTS_PATH.path_join(c_file_name), FileAccess.READ
)
if not file:
var file_err := FileAccess.get_open_error()
if file_err == ERR_FILE_NOT_FOUND:
printerr("file not found '%s'" % _file_name)
else:
printerr("error opening file '%s': %s" % [_file_name, file_err])
return []
var content := file.get_as_text()
var layout_regex := RegEx.new()
layout_regex.compile(
r".*=\s(?<name>LAYOUT.*)\s*\((?<keys>[^()]*(?:\([^()]*\)[^()]*)*)\)"
)
var keys_regex := RegEx.new()
keys_regex.compile(r"\w+(?:\(.*?\))?")
var keymap_keys: Array[String] = []
for layout_matches in layout_regex.search_all(content):
for key_matches in keys_regex.search_all(layout_matches.get_string("keys")):
keymap_keys.append(key_matches.get_string())
break
return keymap_keys
func _get_keymap_key_unprefixed(keymap_key_prefixed: String) -> String:
return keymap_key_prefixed.substr(3)
func _get_keycode_from_keymap_key(keymap_key_prefixed: String) -> Key:
var keymap_key := _get_keymap_key_unprefixed(keymap_key_prefixed)
var keycode := KEY_NONE
keycode = OS.find_keycode_from_string(keymap_key)
if keycode == KEY_NONE and KEYCODE_MAP.has(keymap_key):
keycode = KEYCODE_MAP[keymap_key]
if keycode == KEY_NONE and KEYCODE_MODIFIER_LEFT_MAP.has(keymap_key):
keycode = KEYCODE_MODIFIER_LEFT_MAP[keymap_key]
if keycode == KEY_NONE and KEYCODE_MODIFIER_RIGHT_MAP.has(keymap_key):
keycode = KEYCODE_MODIFIER_RIGHT_MAP[keymap_key]
if keycode == KEY_NONE:
printerr(
"%s: could not recognize key label %s" % [_file_name, keymap_key_prefixed]
)
return keycode
func _get_key_location(keymap_key_prefixed: String) -> KeyLocation:
var keymap_key := _get_keymap_key_unprefixed(keymap_key_prefixed)
if KEYCODE_MODIFIER_LEFT_MAP.has(keymap_key):
return KEY_LOCATION_LEFT
if KEYCODE_MODIFIER_RIGHT_MAP.has(keymap_key):
return KEY_LOCATION_RIGHT
return KEY_LOCATION_UNSPECIFIED