From e5d2dfa87d31b07d0b5dadaa6a3ddc1fdf4385d9 Mon Sep 17 00:00:00 2001 From: Cem Aksoylar Date: Sun, 15 Dec 2024 21:40:21 -0800 Subject: [PATCH] feat: Understand and map ZMK physical layout selections Fixes #125. Also add a new physical layout type, zmk_shared_layout, which fetches a layout from ZMK repository's `app/dts/layouts` folder. This isn't currently advertised externally and only used for certain generic layouts. --- keymap_drawer/parse/zmk.py | 8 +-- keymap_drawer/physical_layout.py | 47 ++++++++++++++--- resources/zmk_keyboard_layouts.yaml | 78 +++++++++++++++++++++-------- 3 files changed, 102 insertions(+), 31 deletions(-) diff --git a/keymap_drawer/parse/zmk.py b/keymap_drawer/parse/zmk.py index 4d87a62..e3be264 100644 --- a/keymap_drawer/parse/zmk.py +++ b/keymap_drawer/parse/zmk.py @@ -256,8 +256,7 @@ def parse_layers(layers: list[str], node_name) -> list[str]: combo["l"] = parse_layers(layers, node.name) # see if combo had additional properties specified in the config, if so merge them in - cfg_combo = ComboSpec.normalize_fields(self.cfg.zmk_combos.get(node.name, {})) - combos.append(ComboSpec(**(combo | cfg_combo))) + combos.append(ComboSpec(**(combo | ComboSpec.normalize_fields(self.cfg.zmk_combos.get(node.name, {}))))) return combos def _get_physical_layout(self, file_name: str | None, dts: DeviceTree) -> dict: @@ -273,11 +272,14 @@ def _get_physical_layout(self, file_name: str | None, dts: DeviceTree) -> dict: # if no chosen set, use first transform as the default if ( transform := ( - dts.get_chosen_property("zmk,matrix-transform") or dts.get_chosen_property("zmk,matrix_transform") + dts.get_chosen_property("zmk,physical-layout") + or dts.get_chosen_property("zmk,matrix-transform") + or dts.get_chosen_property("zmk,matrix_transform") ) ) is None: return next(iter(keyboard_layouts.values())) + logger.debug("found selected layout %s in ZMK keymap", transform) return keyboard_layouts.get(transform, {}) def _parse(self, in_str: str, file_name: str | None = None) -> tuple[dict, KeymapData]: diff --git a/keymap_drawer/physical_layout.py b/keymap_drawer/physical_layout.py index 7bb5a36..a9e242b 100644 --- a/keymap_drawer/physical_layout.py +++ b/keymap_drawer/physical_layout.py @@ -28,6 +28,9 @@ QMK_LAYOUTS_PATH = Path(__file__).parent.parent / "resources" / "qmk_layouts" QMK_METADATA_URL = "https://keyboards.qmk.fm/v1/keyboards/{keyboard}/info.json" QMK_DEFAULT_LAYOUTS_URL = "https://raw.githubusercontent.com/qmk/qmk_firmware/master/layouts/default/{layout}/info.json" +ZMK_SHARED_LAYOUTS_URL = ( + "https://raw.githubusercontent.com/zmkfirmware/zmk/refs/heads/main/app/dts/layouts/{layout}.dtsi" +) CACHE_LAYOUTS_PATH = Path(user_cache_dir("keymap-drawer", False)) / "qmk_layouts" QMK_MAPPINGS_PATH = Path(__file__).parent.parent / "resources" / "qmk_keyboard_mappings.yaml" @@ -185,9 +188,10 @@ def normalize(self) -> "PhysicalLayout": return PhysicalLayout(keys=[k - min_pt for k in self.keys]) -def layout_factory( +def layout_factory( # pylint: disable=too-many-locals config: Config, qmk_keyboard: str | None = None, + zmk_shared_layout: str | None = None, qmk_info_json: Path | BytesIO | None = None, dts_layout: Path | BytesIO | None = None, layout_name: str | None = None, @@ -197,11 +201,14 @@ def layout_factory( ) -> PhysicalLayout: """Create and return a physical layout, as determined by the combination of arguments passed.""" if ( - sum(arg is not None for arg in (qmk_keyboard, qmk_info_json, dts_layout, ortho_layout, cols_thumbs_notation)) + sum( + arg is not None + for arg in (qmk_keyboard, zmk_shared_layout, qmk_info_json, dts_layout, ortho_layout, cols_thumbs_notation) + ) != 1 ): raise ValueError( - 'Please provide exactly one of "qmk_keyboard", "qmk_info_json", "dts_layout", "ortho_layout" ' + 'Please provide exactly one of "qmk_keyboard", "zmk_shared_layout", "qmk_info_json", "dts_layout", "ortho_layout" ' 'or "cpt_spec" specs for physical layout' ) @@ -230,6 +237,9 @@ def layout_factory( layouts = {name: val["layout"] for name, val in qmk_info["layouts"].items()} return QmkLayout(layouts=layouts).generate(layout_name=layout_name, key_size=draw_cfg.key_h) + if zmk_shared_layout is not None: + fetched = _get_zmk_shared_layout(zmk_shared_layout, draw_cfg.use_local_cache) + return _parse_dts_layout(fetched, parse_cfg).generate(layout_name=None, key_size=draw_cfg.key_h) if dts_layout is not None: return _parse_dts_layout(dts_layout, parse_cfg).generate(layout_name=layout_name, key_size=draw_cfg.key_h) if ortho_layout is not None: @@ -477,7 +487,7 @@ def get_qmk_mappings() -> dict[str, str]: @lru_cache(maxsize=128) -def _get_qmk_info(qmk_keyboard: str, use_local_cache: bool = False): +def _get_qmk_info(qmk_keyboard: str, use_local_cache: bool = False) -> dict: """ Get a QMK info.json file from either self-maintained folder of layouts, local file cache if enabled, or from QMK keyboards metadata API. @@ -516,12 +526,37 @@ def _get_qmk_info(qmk_keyboard: str, use_local_cache: bool = False): ) from exc -def _parse_dts_layout(dts_in: Path | BytesIO, cfg: ParseConfig) -> QmkLayout: # pylint: disable=too-many-locals +@lru_cache(maxsize=128) +def _get_zmk_shared_layout(zmk_shared_layout: str, use_local_cache: bool = False) -> bytes: + cache_path = CACHE_LAYOUTS_PATH / f"zmk.{zmk_shared_layout.replace('/', '@')}.dtsi" + if use_local_cache and cache_path.is_file(): + logger.debug("found ZMK shared layout %s in local cache", zmk_shared_layout) + with open(cache_path, "rb") as f: + return f.read() + try: + with urlopen(ZMK_SHARED_LAYOUTS_URL.format(layout=zmk_shared_layout)) as f: + logger.debug("getting ZMK shared layout %s from Github ZMK repo", zmk_shared_layout) + layout = f.read() + if use_local_cache: + cache_path.parent.mkdir(parents=True, exist_ok=True) + with open(cache_path, "wb") as f_out: + f_out.write(layout) + return layout + except HTTPError as exc: + raise ValueError( + f"ZMK shared layout '{zmk_shared_layout}' not found, please make sure you specify an existing layout " + "(hint: check from https://github.com/zmkfirmware/zmk/tree/main/app/dts/layouts)" + ) from exc + + +def _parse_dts_layout(dts_in: Path | BytesIO | bytes, cfg: ParseConfig) -> QmkLayout: # pylint: disable=too-many-locals if isinstance(dts_in, Path): with open(dts_in, "r", encoding="utf-8") as f: in_str, file_name = f.read(), str(dts_in) - else: # BytesIO + elif isinstance(dts_in, BytesIO): in_str, file_name = dts_in.read().decode("utf-8"), None + else: # bytes + in_str, file_name = dts_in.decode("utf-8"), None dts = DeviceTree( in_str, diff --git a/resources/zmk_keyboard_layouts.yaml b/resources/zmk_keyboard_layouts.yaml index 9516c26..25de8f5 100644 --- a/resources/zmk_keyboard_layouts.yaml +++ b/resources/zmk_keyboard_layouts.yaml @@ -2,12 +2,37 @@ _corne: &corne default_transform: {qmk_keyboard: corne_rotated, layout_name: LAYOUT_split_3x6_3} five_column_transform: {qmk_keyboard: corne_rotated, layout_name: LAYOUT_split_3x5_3} + foostan_corne_6col_layout: {qmk_keyboard: corne_rotated, layout_name: LAYOUT_split_3x6_3} + foostan_corne_5col_layout: {qmk_keyboard: corne_rotated, layout_name: LAYOUT_split_3x5_3} _generic_3x5_3: &generic_3x5_3 default_transform: {qmk_keyboard: obosob/arch_36} _ferris: &ferris default_transform: {qmk_keyboard: ferris_rotated} _generic_60p: &generic_60p - default_transform: {qmk_keyboard: 1upkeyboards/1up60rgb, layout_name: LAYOUT_60_ansi} + default_transform: {zmk_shared_layout: common/60percent/ansi} + layout_60_all1u: {zmk_shared_layout: common/60percent/all1u} + layout_60_ansi: {zmk_shared_layout: common/60percent/ansi} + layout_60_iso: {zmk_shared_layout: common/60percent/iso} + layout_60_hhkb: {zmk_shared_layout: common/60percent/hhkb} +_generic_65p: &generic_65p + default_transform: {zmk_shared_layout: common/65percent/ansi} + layout_65_all1u: {zmk_shared_layout: common/65percent/all1u} + layout_65_ansi: {zmk_shared_layout: common/65percent/ansi} + layout_65_hhkb: {zmk_shared_layout: common/65percent/hhkb} + layout_65_iso: {zmk_shared_layout: common/65percent/iso} +_generic_75p: &generic_75p + default_transform: {zmk_shared_layout: common/75percent/ansi} + layout_75_all1u: {zmk_shared_layout: common/75percent/all1u} + layout_75_ansi: {zmk_shared_layout: common/75percent/ansi} + layout_75_iso: {zmk_shared_layout: common/75percent/iso} +_generic_ortho_4x12: &generic_ortho_4x12 + layout_ortho_4x12_all1u: {zmk_shared_layout: common/ortho_4x12/all1u} + layout_ortho_4x12_1x2u: {zmk_shared_layout: common/ortho_4x12/1x2u} + layout_ortho_4x12_2x2u: {zmk_shared_layout: common/ortho_4x12/2x2u} +_generic_ortho_5x12: &generic_ortho_5x12 + layout_ortho_5x12_all1u: {zmk_shared_layout: common/ortho_5x12/all1u} + layout_ortho_5x12_1x2u: {zmk_shared_layout: common/ortho_5x12/1x2u} + layout_ortho_5x12_2x2u: {zmk_shared_layout: common/ortho_5x12/2x2u} _pinky_cluster_34: &pinky_cluster_34 default_transform: {qmk_keyboard: clog} _hummingbird_splay: &hummingbird_splay @@ -17,18 +42,12 @@ _hummingbird_splay: &hummingbird_splay bdn9: &bdn9 null: {qmk_keyboard: keebio/bdn9/rev2} bdn9_rev2: *bdn9 -#bt60: *generic_60p # these boards seem to have extra keys, hard to find matching layouts -#bt60_v1: *generic_60p -#bt60_v1_hs: *generic_60p -bt60_v2: # missing a couple transforms - ansi_transform: {qmk_keyboard: 1upkeyboards/1up60rgb, layout_name: LAYOUT_60_ansi} - iso_transform: {qmk_keyboard: 1upkeyboards/1up60rgb, layout_name: LAYOUT_60_iso} -bt65_v1: # missing a couple transforms - ansi_transform: {qmk_keyboard: wilba_tech/wt65_d, layout_name: LAYOUT_65_ansi} - iso_transform: {qmk_keyboard: wilba_tech/wt65_d, layout_name: LAYOUT_65_iso} -bt75_v1: # missing a couple transforms - ansi_transform: {qmk_keyboard: kbdfans/kbd75/rev2, layout_name: LAYOUT_75_ansi} - iso_transform: {qmk_keyboard: kbdfans/kbd75/rev2, layout_name: LAYOUT_75_iso} +bt60: *generic_60p +bt60_v1: *generic_60p # doesn't quite match, has an extra key on bottom row right +bt60_v1_hs: *generic_60p +bt60_v2: *generic_60p +bt65_v1: *generic_65p +bt75_v1: *generic_75p corne-ish_zen: *corne corneish_zen: *corne corneish_zen_v1: *corne @@ -38,16 +57,17 @@ dz60rgb: &dz60rgb dz60rgb_rev1: *dz60rgb ferris: *ferris ferris_rev02: *ferris -kbdfans_tofu65: &kbdfans_tofu65 - default_transform: {qmk_keyboard: tada68, layout_name: LAYOUT_65_ansi} -kbdfans_tofu65_v2: *kbdfans_tofu65 +kbdfans_tofu65: *generic_65p +kbdfans_tofu65_v2: *generic_65p nice60: *generic_60p planck: &planck + <<: *generic_ortho_4x12 layout_grid_transform: {qmk_keyboard: planck/rev6, layout_name: LAYOUT_ortho_4x12} layout_mit_transform: {qmk_keyboard: planck/rev6, layout_name: LAYOUT_planck_1x2uC} layout_2x2u_transform: {qmk_keyboard: planck/rev6, layout_name: LAYOUT_planck_2x2u} planck_rev6: *planck preonic: &preonic + <<: *generic_ortho_5x12 layout_grid_transform: {qmk_keyboard: preonic/rev3, layout_name: LAYOUT_ortho_5x12} layout_mit_transform: {qmk_keyboard: preonic/rev3, layout_name: LAYOUT_preonic_1x2uC} layout_2x2u_transform: {qmk_keyboard: preonic/rev3, layout_name: LAYOUT_preonic_2x2u} @@ -64,16 +84,14 @@ bfo9000: default_transform: {qmk_keyboard: keebio/bfo9000} boardsource3x4: null: {qmk_keyboard: boardsource/3x4} -boardsource5x12: - null: {qmk_keyboard: boardsource/5x12} +boardsource5x12: *generic_ortho_5x12 chalice: default_transform: {qmk_keyboard: chalice, layout_name: LAYOUT_default} splitbs_transform: {qmk_keyboard: chalice, layout_name: LAYOUT_split_bs} clog: *pinky_cluster_34 clueboard_california: null: {qmk_keyboard: clueboard/california} -contra: - null: {qmk_keyboard: contra} +contra: *generic_ortho_4x12 corne: *corne cradio: *ferris crbn: @@ -99,10 +117,17 @@ jian: default_transform: {qmk_keyboard: jian/rev2} crkbd_transform: {qmk_keyboard: crkbd/rev1, layout_name: LAYOUT_split_3x6_3} five_column_transform: {qmk_keyboard: crkbd/rev1, layout_name: LAYOUT_split_3x5_3} + kgoh_jian_full_layout: {qmk_keyboard: jian/rev2} + kgoh_jian_6col_layout: {qmk_keyboard: crkbd/rev1, layout_name: LAYOUT_split_3x6_3} + kgoh_jian_5col_layout: {qmk_keyboard: crkbd/rev1, layout_name: LAYOUT_split_3x5_3} jiran: - default_transform: {qmk_keyboard: jiran/rev2} + joran_transform: {qmk_keyboard: jiran/rev2} + ladniy_jiran_full_layout: {qmk_keyboard: jiran/rev2} jian_transform: {qmk_keyboard: jian/rev2} crkbd_transform: {qmk_keyboard: crkbd/rev1, layout_name: LAYOUT_split_3x6_3} + default_transform: {qmk_keyboard: jiran/rev2} + kgoh_jian_full_layout: {qmk_keyboard: jian/rev2} + kgoh_jian_6col_layout: {qmk_keyboard: crkbd/rev1, layout_name: LAYOUT_split_3x6_3} jorne: &jorne default_transform: {qmk_keyboard: jorne/rev1} crkbd_transform: {qmk_keyboard: crkbd/rev1, layout_name: LAYOUT_split_3x6_3} @@ -112,6 +137,8 @@ knob_goblin: # this has some empty spots that are not removed through a matrix kyria: &kyria default_transform: {qmk_keyboard: splitkb/kyria/rev1} five_column_transform: {qmk_keyboard: splitkb/kyria/rev1, layout_name: LAYOUT_split_3x5_5} # non-existent layout + splitkb_kyria_6col_layout: {qmk_keyboard: splitkb/kyria/rev1} + splitkb_kyria_5col_layout: {qmk_keyboard: splitkb/kyria/rev1, layout_name: LAYOUT_split_3x5_5} # non-existent layout kyria_rev2: *kyria kyria_rev3: *kyria leeloo: @@ -135,7 +162,10 @@ pancake: null: {qmk_keyboard: spaceman/pancake/rev1/promicro, layout_name: LAYOUT_ortho_4x12} qaz: &qaz default_transform: {qmk_keyboard: tominabox1/qaz} -quefrency: + split_big_bar_layout: {qmk_keyboard: zmk.qaz, layout_name: split_big_bar_layout} + split_bar_layout: {qmk_keyboard: zmk.qaz, layout_name: split_bar_layout} + big_bar_layout: {qmk_keyboard: zmk.qaz, layout_name: big_bar_layout} +quefrency: # this doesn't match, 70 keys vs. 72 default_transform: {qmk_keyboard: keebio/quefrency/rev2, layout_name: LAYOUT_65} redox: default_transform: {qmk_keyboard: redox/rev1/base} @@ -249,6 +279,7 @@ le_chiff_ble: levinson: default_transform: {qmk_keyboard: keebio/levinson/rev3} lpgalaxy_blank_slate: + <<: *generic_ortho_4x12 ortho_transform: {qmk_keyboard: planck/rev6, layout_name: LAYOUT_ortho_4x12} mit_transform: {qmk_keyboard: planck/rev6, layout_name: LAYOUT_planck_1x2uC} dual_2u_transform: {qmk_keyboard: planck/rev6, layout_name: LAYOUT_planck_2x2u} @@ -301,6 +332,9 @@ technikable: ortho_transform: {qmk_keyboard: technikable, layout_name: LAYOUT_technikable_4x12} mit_transform: {qmk_keyboard: technikable, layout_name: LAYOUT_technikable_1x2uC} dual_2u_transform: {qmk_keyboard: technikable, layout_name: LAYOUT_technikable_2x2u} + technikable_ortho_layout: {qmk_keyboard: technikable, layout_name: LAYOUT_technikable_4x12} + technikable_mit_layout: {qmk_keyboard: technikable, layout_name: LAYOUT_technikable_1x2uC} + technikable_dual_2u_layout: {qmk_keyboard: technikable, layout_name: LAYOUT_technikable_2x2u} tipper_tf: *ferris tornblue: default_transform: {qmk_keyboard: torn}