Skip to content

Commit

Permalink
feat: Understand and map ZMK physical layout selections
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
caksoylar committed Dec 16, 2024
1 parent c5175d5 commit e5d2dfa
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 31 deletions.
8 changes: 5 additions & 3 deletions keymap_drawer/parse/zmk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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]:
Expand Down
47 changes: 41 additions & 6 deletions keymap_drawer/physical_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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,
Expand All @@ -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'
)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
78 changes: 56 additions & 22 deletions resources/zmk_keyboard_layouts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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}
Expand All @@ -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:
Expand All @@ -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}
Expand All @@ -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:
Expand All @@ -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}
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand Down

0 comments on commit e5d2dfa

Please sign in to comment.