Skip to content

Commit

Permalink
feat!: Parse and map modifier functions
Browse files Browse the repository at this point in the history
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)": ...`
  • Loading branch information
caksoylar committed Oct 22, 2023
1 parent 0da87c1 commit e99ff64
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 1 deletion.
18 changes: 18 additions & 0 deletions keymap_drawer/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions keymap_drawer/keymap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
32 changes: 32 additions & 0 deletions keymap_drawer/parse/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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]
Expand Down
43 changes: 42 additions & 1 deletion keymap_drawer/parse/qmk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)

Expand Down
13 changes: 13 additions & 0 deletions keymap_drawer/parse/zmk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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():
Expand Down

0 comments on commit e99ff64

Please sign in to comment.