Skip to content

Commit bd6df95

Browse files
committed
feat: Support single key ZMK combos
Fixes #186
1 parent f258868 commit bd6df95

File tree

1 file changed

+33
-8
lines changed

1 file changed

+33
-8
lines changed

keymap_drawer/parse/zmk.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import logging
44
import re
5+
from dataclasses import dataclass
56
from functools import cache
67
from itertools import chain
78
from pathlib import Path
@@ -34,6 +35,14 @@ class ZmkKeymapParser(KeymapParser):
3435
"RG": ["right_gui"],
3536
}
3637

38+
@dataclass(slots=True, frozen=True)
39+
class Singleton:
40+
"""Simple container for temporarily representing a combo with single key position."""
41+
42+
position: int
43+
layers: list[str]
44+
key: LayoutKey
45+
3746
def __init__(
3847
self,
3948
config: ParseConfig,
@@ -210,7 +219,7 @@ def _get_layers(self, dts: DeviceTree) -> dict[str, list[LayoutKey]]:
210219
raise ParseError(f'Could not parse `bindings` property under layer node "{node.name}"')
211220
return layers
212221

213-
def _get_combos(self, dts: DeviceTree) -> list[ComboSpec]:
222+
def _get_combos(self, dts: DeviceTree) -> tuple[list[ComboSpec], list[Singleton]]:
214223

215224
def parse_layers(layers: list[str], node_name) -> list[str]:
216225
assert self.layer_names is not None
@@ -227,36 +236,43 @@ def parse_layers(layers: list[str], node_name) -> list[str]:
227236
return out
228237

229238
if not (combo_parents := dts.get_compatible_nodes("zmk,combos")):
230-
return []
239+
return [], []
231240
combo_nodes = chain.from_iterable(parent.children for parent in combo_parents)
232241

233242
combos = []
243+
singletons = []
234244
for node in combo_nodes:
235245
if not (bindings := node.get_phandle_array("bindings")):
236246
raise ParseError(f'Could not parse `bindings` for combo node "{node.name}"')
237247
if not (key_pos_strs := node.get_array("key-positions")):
238248
raise ParseError(f'Could not parse `key-positions` for combo node "{node.name}"')
239249

250+
combo: dict = {}
240251
try:
241-
key_pos = [int(pos) for pos in key_pos_strs]
252+
combo["p"] = [int(pos) for pos in key_pos_strs]
242253
except ValueError as exc:
243254
raise ParseError(f'Cannot parse key positions "{key_pos_strs}" in combo node "{node.name}"') from exc
244255

245256
try:
246257
# ignore current layer for combos
247-
parsed_key = self._str_to_key(bindings[0], None, key_pos, no_shifted=True)
258+
combo["k"] = self._str_to_key(bindings[0], None, combo["p"], no_shifted=True)
248259
except Exception as err:
249260
raise ParseError(
250261
f'Could not parse binding "{bindings[0]}" in combo node "{node.name}" with exception "{err}"'
251262
) from err
252263

253-
combo = {"k": parsed_key, "p": key_pos}
254264
if (layers := node.get_array("layers")) and layers[0].lower() != "0xff":
255265
combo["l"] = parse_layers(layers, node.name)
256266

257267
# see if combo had additional properties specified in the config, if so merge them in
258-
combos.append(ComboSpec(**(combo | ComboSpec.normalize_fields(self.cfg.zmk_combos.get(node.name, {})))))
259-
return combos
268+
combo |= ComboSpec.normalize_fields(self.cfg.zmk_combos.get(node.name, {}))
269+
270+
if len(combo["p"]) == 1:
271+
logger.warning("found a single key ZMK combo %s, it will override layer keys for that position", combo)
272+
singletons.append(self.Singleton(position=combo["p"][0], layers=combo["l"], key=combo["k"]))
273+
else:
274+
combos.append(ComboSpec(**combo))
275+
return combos, singletons
260276

261277
def _get_physical_layout(self, file_name: str | None, dts: DeviceTree) -> dict[str, str]:
262278
if not file_name:
@@ -272,6 +288,14 @@ def _get_physical_layout(self, file_name: str | None, dts: DeviceTree) -> dict[s
272288
logger.debug("inferred ZMK keyboard name %s and layout %s", keyboard_name, layout_name)
273289
return {"zmk_keyboard": keyboard_name} | ({"layout_name": layout_name} if layout_name else {})
274290

291+
def _add_singleton_combos(
292+
self, layers: dict[str, list[LayoutKey]], singletons: list[Singleton]
293+
) -> dict[str, list[LayoutKey]]:
294+
for singleton in reversed(singletons):
295+
for layer in singleton.layers:
296+
layers[layer][singleton.position] = singleton.key
297+
return layers
298+
275299
def _parse(self, in_str: str, file_name: str | None = None) -> tuple[dict, KeymapData]:
276300
"""
277301
Parse a ZMK keymap with its content and path and return the layout spec and KeymapData to be dumped to YAML.
@@ -291,7 +315,8 @@ def _parse(self, in_str: str, file_name: str | None = None) -> tuple[dict, Keyma
291315
self._update_conditional_layers(dts)
292316
layers = self._get_layers(dts)
293317
layers = self.append_virtual_layers(layers)
294-
combos = self._get_combos(dts)
318+
combos, singletons = self._get_combos(dts)
319+
layers = self._add_singleton_combos(layers, singletons)
295320
layers = self.add_held_keys(layers)
296321

297322
keymap_data = KeymapData(layers=layers, combos=combos, layout=None, config=None)

0 commit comments

Comments
 (0)