22
33import logging
44import re
5+ from dataclasses import dataclass
56from functools import cache
67from itertools import chain
78from 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