From f8f46862e7079297b5a3b305a2b42a95fb008a53 Mon Sep 17 00:00:00 2001 From: Cem Aksoylar Date: Sat, 16 Sep 2023 18:08:55 -0700 Subject: [PATCH] feat!: Parse and map modifier functions This is a breaking change, where using modified keycodes like `LA(F4)` in the keys of `parse_config.*_keycode_map` will not work as expected, since `LA` will be separated from `F4` before the keycode mapping is applied. The new way to achieve the same behavior is to place it in the `parse_config.raw_binding_map` instead, e.g. `"&kp LA(F4)": ...` --- keymap_drawer/config.py | 18 +++++++++++++++ keymap_drawer/keymap.py | 9 ++++++++ keymap_drawer/parse/parse.py | 32 +++++++++++++++++++++++++++ keymap_drawer/parse/qmk.py | 43 +++++++++++++++++++++++++++++++++++- keymap_drawer/parse/zmk.py | 13 +++++++++++ 5 files changed, 114 insertions(+), 1 deletion(-) diff --git a/keymap_drawer/config.py b/keymap_drawer/config.py index 2dab182..6893aca 100644 --- a/keymap_drawer/config.py +++ b/keymap_drawer/config.py @@ -217,6 +217,20 @@ class KeySidePars(BaseModel): class ParseConfig(BaseSettings, env_prefix="KEYMAP_", extra="ignore"): """Configuration settings related to parsing QMK/ZMK keymaps.""" + class ModifierFnMap(BaseModel): + """Mapping to replace modifiers in modifier functions with the given string.""" + + left_ctrl: str = "C" + right_ctrl: str = "C" + left_shift: str = "S" + right_shift: str = "S" + left_alt: str = "A" # Alt/Opt + right_alt: str = "A" # Alt/Opt/AltGr + left_gui: str = "G" # Cmd/Win + right_gui: str = "G" # Cmd/Win + keycode_joiner: str = "+" # string to join modifier functions with the modified keycode + internal_joiner: str = "" # string to join different modifier function strings + # run C preprocessor on ZMK keymaps preprocess: bool = True @@ -239,6 +253,10 @@ class ParseConfig(BaseSettings, env_prefix="KEYMAP_", extra="ignore"): # layer is active (which is the default behavior) or *any* of them (with this option) mark_alternate_layer_activators: bool = False + # convert modifiers in modifier functions (used in keycodes with built-in modifiers like LC(V) + # in ZMK or LCTL(KC_V) in QMK) to given symbols. set to None to disable the mapping. + modifier_fn_map: ModifierFnMap | None = ModifierFnMap() + # convert QMK keycodes to their display forms, omitting "KC_" prefix on the keys qmk_keycode_map: dict[str, str | dict] = { # QMK keycodes diff --git a/keymap_drawer/keymap.py b/keymap_drawer/keymap.py index b247e33..f545798 100644 --- a/keymap_drawer/keymap.py +++ b/keymap_drawer/keymap.py @@ -44,6 +44,15 @@ def dict(self, *args, no_tapstr: bool = False, **kwargs): return dict_repr return dict_repr.get("t") or dict_repr.get("tap", "") + def add_prefix(self, prefix: str) -> None: + """Add a prefix string to all non-empty fields.""" + if self.tap: + self.tap = prefix + self.tap + if self.hold: + self.hold = prefix + self.hold + if self.shifted: + self.shifted = prefix + self.shifted + class ComboSpec(BaseModel, allow_population_by_field_name=True): """ diff --git a/keymap_drawer/parse/parse.py b/keymap_drawer/parse/parse.py index d3cd3a4..bce7406 100644 --- a/keymap_drawer/parse/parse.py +++ b/keymap_drawer/parse/parse.py @@ -3,6 +3,7 @@ Do not use directly, use QmkJsonParser or ZmkKeymapParser instead. """ +import re from abc import ABC from io import TextIOWrapper from typing import Sequence @@ -14,6 +15,8 @@ class KeymapParser(ABC): # pylint: disable=too-many-instance-attributes """Abstract base class for parsing firmware keymap representations.""" + _modifier_fn_to_std: dict[str, list[str]] + def __init__( self, config: ParseConfig, @@ -29,6 +32,35 @@ def __init__( self.conditional_layers: dict[int, list[int]] = {} # then-layer to if-layers mapping self.trans_key = LayoutKey.from_key_spec(self.cfg.trans_legend) self.raw_binding_map = self.cfg.raw_binding_map.copy() + self.modifier_fn_re = re.compile( + "(" + "|".join(re.escape(mod) for mod in self._modifier_fn_to_std) + r") *\( *(.*) *\)" + ) + + def strip_modifier_fns(self, keycode: str) -> tuple[str, str]: + """ + Strip potential modifier functions from the keycode then return a tuple of the keycode and the modifiers + formatted according to parse_config.modifier_fn_map. + """ + if self.cfg.modifier_fn_map is None: + return keycode, "" + fn_map = self.cfg.modifier_fn_map.dict() + + def strip_modifiers(keycode: str, current_mods: list[str] | None = None) -> tuple[str, list[str]]: + if current_mods is None: + current_mods = [] + if not (m := self.modifier_fn_re.fullmatch(keycode)): + return keycode, current_mods + return strip_modifiers(m.group(2), current_mods + self._modifier_fn_to_std[m.group(1)]) + + keycode, mods = strip_modifiers(keycode) + if not mods: + return keycode, "" + + return ( + keycode, + fn_map.get("internal_joiner", "").join(fn_map.get(mod, "") for mod in mods) + + fn_map.get("keycode_joiner", ""), + ) def update_layer_activated_from( self, from_layers: Sequence[int], to_layer: int, key_positions: Sequence[int] diff --git a/keymap_drawer/parse/qmk.py b/keymap_drawer/parse/qmk.py index 6b0a368..fd4838e 100644 --- a/keymap_drawer/parse/qmk.py +++ b/keymap_drawer/parse/qmk.py @@ -20,6 +20,43 @@ class QmkJsonParser(KeymapParser): _osm_re = re.compile(r"OSM\(MOD_(\S+)\)") _osl_re = re.compile(r"OSL\((\d+)\)") + _modifier_fn_to_std = { + "LCTL": ["left_ctrl"], + "C": ["left_ctrl"], + "LSFT": ["left_shift"], + "S": ["left_shift"], + "LALT": ["left_alt"], + "A": ["left_alt"], + "LOPT": ["left_alt"], + "LGUI": ["left_gui"], + "G": ["left_gui"], + "LCMD": ["left_gui"], + "LWIN": ["left_gui"], + "RCTL": ["right_ctrl"], + "RSFT": ["right_shift"], + "RALT": ["right_alt"], + "ROPT": ["right_alt"], + "ALGR": ["right_alt"], + "RGUI": ["right_gui"], + "RCMD": ["right_gui"], + "RWIN": ["right_gui"], + "LSG": ["left_shift", "left_gui"], + "SGUI": ["left_shift", "left_gui"], + "SCMD": ["left_shift", "left_gui"], + "SWIN": ["left_shift", "left_gui"], + "LAG": ["left_alt", "left_gui"], + "RSG": ["right_shift", "right_gui"], + "RAG": ["right_alt", "right_gui"], + "LCA": ["left_ctrl", "left_alt"], + "LSA": ["left_shift", "left_alt"], + "RSA": ["right_shift", "right_alt"], + "SAGR": ["right_shift", "right_alt"], + "RCS": ["right_ctrl", "right_shift"], + "LCAG": ["left_ctrl", "left_alt", "left_gui"], + "MEH": ["left_ctrl", "left_shift", "left_alt"], + "HYPR": ["left_ctrl", "left_shift", "left_alt", "left_gui"], + } + def _str_to_key( # pylint: disable=too-many-return-statements self, key_str: str, current_layer: int, key_positions: Sequence[int] ) -> LayoutKey: @@ -31,7 +68,11 @@ def _str_to_key( # pylint: disable=too-many-return-statements assert self.layer_names is not None def mapped(key: str) -> LayoutKey: - return LayoutKey.from_key_spec(self.cfg.qmk_keycode_map.get(key, key.replace("_", " "))) + key, mod_prefix = self.strip_modifier_fns(key) + mapped = LayoutKey.from_key_spec(self.cfg.qmk_keycode_map.get(key, key.replace("_", " "))) + if mod_prefix: + mapped.add_prefix(mod_prefix) + return mapped key_str = self._prefix_re.sub("", key_str) diff --git a/keymap_drawer/parse/zmk.py b/keymap_drawer/parse/zmk.py index 84b5749..b5ad02d 100644 --- a/keymap_drawer/parse/zmk.py +++ b/keymap_drawer/parse/zmk.py @@ -19,6 +19,16 @@ class ZmkKeymapParser(KeymapParser): """Parser for ZMK devicetree keymaps, using C preprocessor and hacky pyparsing-based parsers.""" _numbers_re = re.compile(r"N(UM(BER)?_)?(\d)") + _modifier_fn_to_std = { + "LC": ["left_ctrl"], + "LS": ["left_shift"], + "LA": ["left_alt"], + "LG": ["left_gui"], + "RC": ["right_ctrl"], + "RS": ["right_shift"], + "RA": ["right_alt"], + "RG": ["right_gui"], + } def __init__( self, @@ -54,6 +64,7 @@ def _str_to_key( # pylint: disable=too-many-return-statements,too-many-locals assert self.layer_names is not None def mapped(key: str) -> LayoutKey: + key, mod_prefix = self.strip_modifier_fns(key) mapped = LayoutKey.from_key_spec( self.cfg.zmk_keycode_map.get( key, @@ -66,6 +77,8 @@ def mapped(key: str) -> LayoutKey: ) if no_shifted: mapped.shifted = "" + if mod_prefix: + mapped.add_prefix(mod_prefix) return mapped match binding.split():