From ef209e6f0d6969ba8307b12697a0f74e44a0b61f Mon Sep 17 00:00:00 2001 From: Ken Sanislo Date: Tue, 7 Jul 2026 21:00:18 -0700 Subject: [PATCH] perkey: add PRO X RAPID media top row layout The PRO X RAPID (reported as "PRO X RAPID", USB 046D:C35B) wires five dedicated media keys above the F-row with board-specific 0x8081 zone ids (150 Bright, 155 Prev, 152 Play, 154 Next, 153 Mute) that the canonical extras map (153-158) doesn't cover: 150/152/154 were dropped as phantoms and never surfaced in the painter. Ids and positions are hardware-probed (ported from the OpenRGB key-map for this model). Add keyboard_pro_x_rapid.with_media_top_row(), which pushes any regional TKL base down one row and prepends the media row as explicit bound cells. No EXTRAS_ALLOWLIST change is needed - the media keys paint directly, while the base's extra_zones keeps filtering the phantom zones this board shares with the G515 (47, 97, 99-103, 254). Registered per region ahead of the generic family matchers so country-code routing (ANSI/ISO/AZERTY/ JIS) still selects the main block; only the media top row differs. The logo has no addressable LED on this model, so no logo zone is mapped. Labels are words rather than media glyphs: the media/emoji codepoints aren't in DejaVu (the baseline Linux font) and tofu on minimal installs, unlike the nav-arrow glyphs which the base font covers. --- lib/solaar/ui/perkey/layouts/__init__.py | 24 ++++++ .../ui/perkey/layouts/keyboard_pro_x_rapid.py | 74 +++++++++++++++++++ tests/solaar/ui/test_perkey_layouts.py | 62 ++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 lib/solaar/ui/perkey/layouts/keyboard_pro_x_rapid.py create mode 100644 tests/solaar/ui/test_perkey_layouts.py diff --git a/lib/solaar/ui/perkey/layouts/__init__.py b/lib/solaar/ui/perkey/layouts/__init__.py index 128b3e39..e44dcc39 100644 --- a/lib/solaar/ui/perkey/layouts/__init__.py +++ b/lib/solaar/ui/perkey/layouts/__init__.py @@ -32,6 +32,7 @@ from . import keyboard_iso_azerty from . import keyboard_iso_qwerty from . import keyboard_iso_qwertz from . import keyboard_jis +from . import keyboard_pro_x_rapid from . import mouse_g502x # (feature_id, matcher, layout). Matcher receives a `hint` dict the editor @@ -135,6 +136,29 @@ def _keyboard_matcher(family: str, full_size: bool) -> Callable[[dict], bool]: return match +def _pro_x_rapid_matcher(family: str) -> Callable[[dict], bool]: + """Match the PRO X RAPID (reported as "PRO X RAPID", TKL) for one region. + Same country-code family routing as the generic keyboards; only the media + top row differs.""" + named = _name_contains("PRO X RAPID") + + def match(hint: dict) -> bool: + if hint.get("kind") != "keyboard": + return False + if not named(hint): + return False + if _has_numpad(hint): # PRO X RAPID is TKL + return False + return _keyboard_family(hint) == family + + return match + + +# PRO X RAPID — regional generated base + a customized media top row. +# Registered ahead of the generic family matchers so it wins for this model. +for _family, (_full, _tkl) in _FAMILY_LAYOUTS.items(): + register_layout(0x8081, _pro_x_rapid_matcher(_family), keyboard_pro_x_rapid.with_media_top_row(_tkl)) + # PER_KEY_LIGHTING_V2 = 0x8081 for _family, (_full, _tkl) in _FAMILY_LAYOUTS.items(): register_layout(0x8081, _keyboard_matcher(_family, full_size=True), _full) diff --git a/lib/solaar/ui/perkey/layouts/keyboard_pro_x_rapid.py b/lib/solaar/ui/perkey/layouts/keyboard_pro_x_rapid.py new file mode 100644 index 00000000..bd6af6e1 --- /dev/null +++ b/lib/solaar/ui/perkey/layouts/keyboard_pro_x_rapid.py @@ -0,0 +1,74 @@ +## Copyright (C) 2026 Solaar Contributors https://pwr-solaar.github.io/Solaar/ +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License along +## with this program; if not, write to the Free Software Foundation, Inc., +## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +"""PRO X RAPID (PRO X TKL RAPID) per-key layout. + +Same generated regional main block as any TKL keyboard, plus a customized +media top row. This board wires five dedicated media keys above the F-row with +board-specific zone ids that the canonical extras map (153-158) does not cover; +ids and positions are hardware-probed (ported from the OpenRGB key-map for this +model). The logo has no addressable LED here. +""" + +from __future__ import annotations + +from ..layout import Cell +from ..layout import Layout + +# Dedicated media keys, hardware-probed. Columns align above the F-row +# (F4=col5, F9-F12=cols10-13). Zone ids are board-specific — NOT the canonical +# 153-158 extras — and the logo (canonical 210) is unlit on this model. +# group="media" keeps these out of strip_groups so they render in the matrix. +MEDIA_TOP_ROW: tuple[Cell, ...] = ( + Cell(zone_id=150, row=0, col=5, group="media", label="Bright"), + Cell(zone_id=155, row=0, col=10, group="media", label="Prev"), + Cell(zone_id=152, row=0, col=11, group="media", label="Play"), + Cell(zone_id=154, row=0, col=12, group="media", label="Next"), + Cell(zone_id=153, row=0, col=13, group="media", label="Mute"), +) + + +def with_media_top_row(base: Layout) -> Layout: + """Return `base` with every cell pushed down one row and the PRO X RAPID + media top row placed at row 0. + + The media keys are explicit bound cells, so they paint directly with no + EXTRAS_ALLOWLIST entry, while `base`'s `extra_zones` still filters the + phantom zones this board shares with the G515 (47, 97, 99-103, 254). + """ + shifted = tuple( + Cell( + zone_id=c.zone_id, + row=c.row + 1, + col=c.col, + width=c.width, + height=c.height, + group=c.group, + label=c.label, + x=c.x, + y=c.y, + ) + for c in base.cells + ) + return Layout( + cells=MEDIA_TOP_ROW + shifted, + rows=base.rows + 1, + cols=base.cols, + strip_groups=base.strip_groups, + supported_tools=base.supported_tools, + extra_zones=base.extra_zones, + description="PRO X RAPID", + ) diff --git a/tests/solaar/ui/test_perkey_layouts.py b/tests/solaar/ui/test_perkey_layouts.py new file mode 100644 index 00000000..d96755fd --- /dev/null +++ b/tests/solaar/ui/test_perkey_layouts.py @@ -0,0 +1,62 @@ +from solaar.ui.perkey.binding import bind +from solaar.ui.perkey.layouts import layout_for + +# PRO X RAPID hardware-probed media zones (board-specific, not canonical 153-158). +_RAPID_MEDIA = {150, 152, 153, 154, 155} +# Phantom zones this board shares with the G515 (not physical keys). +_SHARED_PHANTOMS = {47, 97} + + +def _rapid_hint(name="PRO X RAPID", country=1): + return { + "kind": "keyboard", + "wpid": "C35B", + "codename": name, + "name": name, + "keyboard_layout": country, # 1 = ANSI + "zones": [38, 55, 66, *sorted(_RAPID_MEDIA), *sorted(_SHARED_PHANTOMS)], + "zone_count": 9, + } + + +def test_pro_x_rapid_gets_media_top_row(): + layout = layout_for(0x8081, _rapid_hint()) + + assert layout.description == "PRO X RAPID" + media = {c.zone_id: (c.row, c.col, c.label) for c in layout.cells if c.group == "media"} + assert media == { + 150: (0, 5, "Bright"), + 155: (0, 10, "Prev"), + 152: (0, 11, "Play"), + 154: (0, 12, "Next"), + 153: (0, 13, "Mute"), + } + # The generated base is pushed down one row to make room for the media row. + esc = next(c for c in layout.cells if c.zone_id == 38) + assert esc.row == 1 + + +def test_pro_x_rapid_media_keys_are_paintable_and_phantoms_dropped(): + layout = layout_for(0x8081, _rapid_hint()) + bound = bind(layout, _rapid_hint()["zones"], lambda z: f"KEY {z}") + + media_bound = {bc.cell.zone_id: bc.bound for bc in bound.matrix if bc.cell.group == "media"} + assert media_bound == {z: True for z in _RAPID_MEDIA} + # Shared phantoms must not surface as paintable strip swatches. + strip_zones = {bc.cell.zone_id for bc in bound.strip} + assert _SHARED_PHANTOMS.isdisjoint(strip_zones) + + +def test_pro_x_rapid_still_regional(): + # ISO country code still routes to the ISO main block, with the media row on top. + iso = layout_for(0x8081, _rapid_hint(country=2)) # 2 = ISO QWERTY + assert iso.description == "PRO X RAPID" + assert any(c.group == "media" for c in iso.cells) + + +def test_non_rapid_tkl_unaffected(): + # A G515 TKL keeps the generic layout: no media top row. + layout = layout_for(0x8081, _rapid_hint(name="G515 LS TKL")) + + assert layout.description != "PRO X RAPID" + assert not any(c.group == "media" for c in layout.cells)