Add regional keyboard layouts (ISO_QWERTY, QWERTZ, AZERTY, JIS) and fix copyright

Read HID++ feature 0x4540 KeyboardLayout to detect the device's country
code, then route the per-key painter to a matching regional layout.

Changes:

- lib/logitech_receiver/hidpp20.py: new get_keyboard_layout() returning the
  HID Usage Table country code from feature 0x4540's first response byte.
- lib/logitech_receiver/device.py: lazy device.keyboard_layout property,
  guarded by feature presence so devices without 0x4540 don't pay a query
  cost on access.
- lib/solaar/ui/perkey/control.py: thread the country code into the editor
  hint dict.
- lib/solaar/ui/perkey/layouts/_keyboard_base.py (new): factor out the
  function row, nav cluster, and numpad block as shared building blocks.
  Two main-block variants (ANSI with row 2 col 13 backslash, ISO without)
  cover all five regions. build_layout() applies per-zone label overrides
  on top of either main block.
- lib/solaar/ui/perkey/layouts/keyboard_ansi.py: refactored to use the
  builder; same LAYOUT_FULL/LAYOUT_TKL exports.
- lib/solaar/ui/perkey/layouts/keyboard_iso_qwerty.py (new): UK English
  ISO. Same shape as DE/FR/JIS but no label overrides.
- lib/solaar/ui/perkey/layouts/keyboard_iso_qwertz.py (new): DE/Swiss --
  Y/Z swap, Ü/Ö/Ä/ß placement.
- lib/solaar/ui/perkey/layouts/keyboard_iso_azerty.py (new): FR -- A↔Q,
  W↔Z, French digit-row symbols, M repositioning.
- lib/solaar/ui/perkey/layouts/keyboard_jis.py (new): JP -- @ / [ / :
  bracket-row relabels.
- lib/solaar/ui/perkey/layouts/__init__.py: country-code-aware matchers,
  five families × two sizes (full/TKL). Defaults to ANSI when 0x4540 is
  unsupported or returns an unknown code.

POUND, ISO_BACKSLASH, and the L-shape Enter top half (zone 46) are
intentionally omitted from the ISO layouts -- same coverage as OpenRGB.
ABNT2 (Brazilian) deferred until a confirmed Logitech BR RGB device shows
up; adding it later is one new layout file plus a country-code entry.

Also fix copyright headers on all new lib/solaar/ui/perkey/ files: the
files were created in 2026, not 2024 as the headers said.
This commit is contained in:
Ken Sanislo 2026-05-10 02:31:39 -07:00 committed by Peter F. Patel-Schneider
parent d8422d78d1
commit 1952e9ce98
20 changed files with 565 additions and 184 deletions

View File

@ -156,6 +156,7 @@ class Device:
self._tid_map = None # map from transports to product identifiers
self._persister = None # persister holds settings
self._led_effects = self._firmware = self._keys = self._remap_keys = self._gestures = self._force_buttons = None
self._keyboard_layout = None # lazy: country code from HID++ 0x4540, None if unsupported
self._profiles = self._backlight = self._settings = None
self.registers = []
self.notification_flags = None
@ -373,6 +374,13 @@ class Device:
self._polling_rate = rate if rate else self._polling_rate
return self._polling_rate
@property
def keyboard_layout(self):
if self._keyboard_layout is None and self.online and self.protocol >= 2.0:
if SupportedFeature.KEYBOARD_LAYOUT_2 in self.features:
self._keyboard_layout = _hidpp20.get_keyboard_layout(self)
return self._keyboard_layout
@property
def led_effects(self):
if not self._led_effects and self.online and self.protocol >= 2.0:

View File

@ -2031,6 +2031,18 @@ class Hidpp20:
SupportedFeature._fallback = lambda x: f"unknown:{x:04X}"
return result
def get_keyboard_layout(self, device: Device):
"""Return the device's keyboard layout country code, or None.
Country code semantics match the HID HUT keyboard country codes that
Logitech's KEYBOARD_LAYOUT_2 (0x4540) feature reports in the first byte.
Used by the per-key painter to pick the matching regional layout.
"""
result = device.feature_request(SupportedFeature.KEYBOARD_LAYOUT_2, 0x00)
if result:
return struct.unpack("!B", result[:1])[0]
return None
def config_change(self, device: Device, configuration, no_reply=False):
return device.feature_request(SupportedFeature.CONFIG_CHANGE, 0x10, configuration, no_reply=no_reply)

View File

@ -1,4 +1,4 @@
## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
## 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

View File

@ -1,4 +1,4 @@
## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
## 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

View File

@ -1,4 +1,4 @@
## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
## 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

View File

@ -1,4 +1,4 @@
## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
## 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
@ -243,6 +243,7 @@ class PerKeyControl(Gtk.Box):
"wpid": getattr(device, "wpid", None),
"codename": getattr(device, "codename", None),
"name": getattr(device, "name", None),
"keyboard_layout": getattr(device, "keyboard_layout", None),
"zones": list(self._sink.zones),
"zone_count": len(self._sink.zones),
}

View File

@ -1,4 +1,4 @@
## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
## 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

View File

@ -1,4 +1,4 @@
## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
## 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

View File

@ -1,4 +1,4 @@
## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
## 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

View File

@ -1,4 +1,4 @@
## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
## 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
@ -27,6 +27,10 @@ from collections.abc import Callable
from ..layout import Layout
from . import keyboard_ansi
from . import keyboard_iso_azerty
from . import keyboard_iso_qwerty
from . import keyboard_iso_qwertz
from . import keyboard_jis
from . import mouse_g502x
# (feature_id, matcher, layout). Matcher receives a `hint` dict the editor
@ -65,23 +69,74 @@ def _name_contains(*needles: str) -> Callable[[dict], bool]:
return match
# --- Keyboards: distinguish full-size from TKL by presence of a numpad zone.
# Counting zones is unreliable (G515 reports phantom zones 47, 97, 99-103, 254
# that diverge from the keycap count).
# --- Keyboard region routing ---
# Country code → layout family. Codes from HID++ feature 0x4540 KeyboardLayout.
_KEYBOARD_FAMILY_BY_COUNTRY: dict[int, str] = {
1: "ansi",
# ISO QWERTY (UK + ES/IT/PT/BE/Nordic — same shape, different keycap legends)
2: "iso_qwerty",
5: "iso_qwerty",
8: "iso_qwerty",
0x0B: "iso_qwerty",
0x0D: "iso_qwerty",
0x0E: "iso_qwerty",
0x0F: "iso_qwerty",
0x16: "iso_qwerty",
0x1D: "iso_qwerty",
0x21: "iso_qwerty",
0x24: "iso_qwerty",
# ISO QWERTZ (DE/Swiss)
3: "iso_qwertz",
7: "iso_qwertz",
# ISO AZERTY (FR)
4: "iso_azerty",
# JIS
9: "jis",
0x3E: "jis",
}
_FAMILY_LAYOUTS = {
"ansi": (keyboard_ansi.LAYOUT_FULL, keyboard_ansi.LAYOUT_TKL),
"iso_qwerty": (keyboard_iso_qwerty.LAYOUT_FULL, keyboard_iso_qwerty.LAYOUT_TKL),
"iso_qwertz": (keyboard_iso_qwertz.LAYOUT_FULL, keyboard_iso_qwertz.LAYOUT_TKL),
"iso_azerty": (keyboard_iso_azerty.LAYOUT_FULL, keyboard_iso_azerty.LAYOUT_TKL),
"jis": (keyboard_jis.LAYOUT_FULL, keyboard_jis.LAYOUT_TKL),
}
def _has_numpad(hint: dict) -> bool:
"""Numpad presence is read from the device's reported zone bitmap rather
than counting zones G515 reports phantom zones (47, 97, 99-103, 254)
that diverge from the keycap count.
"""
zones = set(hint.get("zones", ()))
return 80 in zones or 95 in zones
def _is_full_keyboard(hint: dict) -> bool:
return hint.get("kind") == "keyboard" and _has_numpad(hint)
def _keyboard_family(hint: dict) -> str:
"""Pick a layout family from the device's HID++ keyboard layout country
code. Defaults to "ansi" when the code is missing or unknown.
"""
code = hint.get("keyboard_layout")
if code is None:
return "ansi"
return _KEYBOARD_FAMILY_BY_COUNTRY.get(int(code), "ansi")
def _is_tkl_keyboard(hint: dict) -> bool:
return hint.get("kind") == "keyboard" and not _has_numpad(hint)
def _keyboard_matcher(family: str, full_size: bool) -> Callable[[dict], bool]:
def match(hint: dict) -> bool:
if hint.get("kind") != "keyboard":
return False
if _has_numpad(hint) != full_size:
return False
return _keyboard_family(hint) == family
return match
# PER_KEY_LIGHTING_V2 = 0x8081
register_layout(0x8081, _is_full_keyboard, keyboard_ansi.LAYOUT_FULL)
register_layout(0x8081, _is_tkl_keyboard, keyboard_ansi.LAYOUT_TKL)
for _family, (_full, _tkl) in _FAMILY_LAYOUTS.items():
register_layout(0x8081, _keyboard_matcher(_family, full_size=True), _full)
register_layout(0x8081, _keyboard_matcher(_family, full_size=False), _tkl)
register_layout(0x8081, _name_contains("G502 X"), mouse_g502x.LAYOUT)

View File

@ -0,0 +1,230 @@
## 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.
"""Shared building blocks for regional keyboard layouts.
Each region (ANSI, ISO_QWERTY, ISO_QWERTZ, ISO_AZERTY, JIS) shares the function
row, nav-cluster, and numpad blocks; only the main alpha block differs (ANSI
includes the row 2 col 13 backslash, ISO doesn't). Regional label overrides on
top of either main block produce the final layout.
Cell positions and groupings adapted from OpenRGB's KeyboardLayoutManager.
Zone IDs are firmware values reported by Logitech HID++ feature 0x8081
(PER_KEY_LIGHTING_V2).
"""
from __future__ import annotations
from ..layout import Cell
from ..layout import Layout
# --- Function row: ESC + F1..F12 (shared across all regions).
FN_ROW: tuple[Cell, ...] = (
Cell(zone_id=38, row=0, col=0, group="fn_row", label="Esc"),
Cell(zone_id=55, row=0, col=2, group="fn_row", label="F1"),
Cell(zone_id=56, row=0, col=3, group="fn_row", label="F2"),
Cell(zone_id=57, row=0, col=4, group="fn_row", label="F3"),
Cell(zone_id=58, row=0, col=5, group="fn_row", label="F4"),
Cell(zone_id=59, row=0, col=6, group="fn_row", label="F5"),
Cell(zone_id=60, row=0, col=7, group="fn_row", label="F6"),
Cell(zone_id=61, row=0, col=8, group="fn_row", label="F7"),
Cell(zone_id=62, row=0, col=9, group="fn_row", label="F8"),
Cell(zone_id=63, row=0, col=10, group="fn_row", label="F9"),
Cell(zone_id=64, row=0, col=11, group="fn_row", label="F10"),
Cell(zone_id=65, row=0, col=12, group="fn_row", label="F11"),
Cell(zone_id=66, row=0, col=13, group="fn_row", label="F12"),
)
# --- Nav cluster + arrows (shared).
EXTRAS: tuple[Cell, ...] = (
Cell(zone_id=67, row=0, col=14, group="extras", label="PrtSc"),
Cell(zone_id=68, row=0, col=15, group="extras", label="ScrLk"),
Cell(zone_id=69, row=0, col=16, group="extras", label="Pause"),
Cell(zone_id=70, row=1, col=14, group="extras", label="Ins"),
Cell(zone_id=71, row=1, col=15, group="extras", label="Home"),
Cell(zone_id=72, row=1, col=16, group="extras", label="PgUp"),
Cell(zone_id=73, row=2, col=14, group="extras", label="Del"),
Cell(zone_id=74, row=2, col=15, group="extras", label="End"),
Cell(zone_id=75, row=2, col=16, group="extras", label="PgDn"),
Cell(zone_id=79, row=4, col=15, group="extras", label=""),
Cell(zone_id=77, row=5, col=14, group="extras", label=""),
Cell(zone_id=78, row=5, col=15, group="extras", label=""),
Cell(zone_id=76, row=5, col=16, group="extras", label=""),
)
# --- Numpad block (only on full-size keyboards).
NUMPAD: tuple[Cell, ...] = (
Cell(zone_id=80, row=1, col=17, group="numpad", label="Num"),
Cell(zone_id=81, row=1, col=18, group="numpad", label="/"),
Cell(zone_id=82, row=1, col=19, group="numpad", label="*"),
Cell(zone_id=83, row=1, col=20, group="numpad", label="-"),
Cell(zone_id=92, row=2, col=17, group="numpad", label="7"),
Cell(zone_id=93, row=2, col=18, group="numpad", label="8"),
Cell(zone_id=94, row=2, col=19, group="numpad", label="9"),
Cell(zone_id=84, row=2, col=20, height=2.0, group="numpad", label="+"),
Cell(zone_id=89, row=3, col=17, group="numpad", label="4"),
Cell(zone_id=90, row=3, col=18, group="numpad", label="5"),
Cell(zone_id=91, row=3, col=19, group="numpad", label="6"),
Cell(zone_id=86, row=4, col=17, group="numpad", label="1"),
Cell(zone_id=87, row=4, col=18, group="numpad", label="2"),
Cell(zone_id=88, row=4, col=19, group="numpad", label="3"),
Cell(zone_id=85, row=4, col=20, height=2.0, group="numpad", label="Enter"),
Cell(zone_id=95, row=5, col=17, width=2.0, group="numpad", label="0"),
Cell(zone_id=96, row=5, col=19, group="numpad", label="."),
)
# --- Main alpha block, ANSI (104-key). Includes row 2 col 13 backslash and
# omits POUND (row 3 col 12) + ISO_BACKSLASH (row 4 col 1).
MAIN_ANSI: tuple[Cell, ...] = (
# Row 1: backtick + numbers + minus/equals + backspace
Cell(zone_id=50, row=1, col=0, group="main", label="`"),
Cell(zone_id=27, row=1, col=1, group="main", label="1"),
Cell(zone_id=28, row=1, col=2, group="main", label="2"),
Cell(zone_id=29, row=1, col=3, group="main", label="3"),
Cell(zone_id=30, row=1, col=4, group="main", label="4"),
Cell(zone_id=31, row=1, col=5, group="main", label="5"),
Cell(zone_id=32, row=1, col=6, group="main", label="6"),
Cell(zone_id=33, row=1, col=7, group="main", label="7"),
Cell(zone_id=34, row=1, col=8, group="main", label="8"),
Cell(zone_id=35, row=1, col=9, group="main", label="9"),
Cell(zone_id=36, row=1, col=10, group="main", label="0"),
Cell(zone_id=42, row=1, col=11, group="main", label="-"),
Cell(zone_id=43, row=1, col=12, group="main", label="="),
Cell(zone_id=39, row=1, col=13, group="main", label="Bksp"),
# Row 2: tab + qwerty + brackets + backslash
Cell(zone_id=40, row=2, col=0, group="main", label="Tab"),
Cell(zone_id=17, row=2, col=1, group="main", label="Q"),
Cell(zone_id=23, row=2, col=2, group="main", label="W"),
Cell(zone_id=5, row=2, col=3, group="main", label="E"),
Cell(zone_id=18, row=2, col=4, group="main", label="R"),
Cell(zone_id=20, row=2, col=5, group="main", label="T"),
Cell(zone_id=25, row=2, col=6, group="main", label="Y"),
Cell(zone_id=21, row=2, col=7, group="main", label="U"),
Cell(zone_id=9, row=2, col=8, group="main", label="I"),
Cell(zone_id=15, row=2, col=9, group="main", label="O"),
Cell(zone_id=16, row=2, col=10, group="main", label="P"),
Cell(zone_id=44, row=2, col=11, group="main", label="["),
Cell(zone_id=45, row=2, col=12, group="main", label="]"),
Cell(zone_id=46, row=2, col=13, group="main", label="\\"),
# Row 3: caps + asdf-row + semi/quote + enter
Cell(zone_id=54, row=3, col=0, group="main", label="Caps"),
Cell(zone_id=1, row=3, col=1, group="main", label="A"),
Cell(zone_id=19, row=3, col=2, group="main", label="S"),
Cell(zone_id=4, row=3, col=3, group="main", label="D"),
Cell(zone_id=6, row=3, col=4, group="main", label="F"),
Cell(zone_id=7, row=3, col=5, group="main", label="G"),
Cell(zone_id=8, row=3, col=6, group="main", label="H"),
Cell(zone_id=10, row=3, col=7, group="main", label="J"),
Cell(zone_id=11, row=3, col=8, group="main", label="K"),
Cell(zone_id=12, row=3, col=9, group="main", label="L"),
Cell(zone_id=48, row=3, col=10, group="main", label=";"),
Cell(zone_id=49, row=3, col=11, group="main", label="'"),
Cell(zone_id=37, row=3, col=13, group="main", label="Enter"),
# Row 4: shift + zxcv-row + comma/period/slash + rshift
Cell(zone_id=105, row=4, col=0, group="main", label="Shift"),
Cell(zone_id=26, row=4, col=2, group="main", label="Z"),
Cell(zone_id=24, row=4, col=3, group="main", label="X"),
Cell(zone_id=3, row=4, col=4, group="main", label="C"),
Cell(zone_id=22, row=4, col=5, group="main", label="V"),
Cell(zone_id=2, row=4, col=6, group="main", label="B"),
Cell(zone_id=14, row=4, col=7, group="main", label="N"),
Cell(zone_id=13, row=4, col=8, group="main", label="M"),
Cell(zone_id=51, row=4, col=9, group="main", label=","),
Cell(zone_id=52, row=4, col=10, group="main", label="."),
Cell(zone_id=53, row=4, col=11, group="main", label="/"),
Cell(zone_id=109, row=4, col=13, group="main", label="Shift"),
# Row 5: bottom row. Space spans cols 3..9 visually.
Cell(zone_id=104, row=5, col=0, group="main", label="Ctrl"),
Cell(zone_id=107, row=5, col=1, group="main", label="Win"),
Cell(zone_id=106, row=5, col=2, group="main", label="Alt"),
Cell(zone_id=41, row=5, col=3, width=7.0, group="main", label="Space"),
Cell(zone_id=110, row=5, col=10, group="main", label="AltGr"),
Cell(zone_id=111, row=5, col=11, group="main", label="Win"),
Cell(zone_id=98, row=5, col=12, group="main", label="Menu"),
Cell(zone_id=108, row=5, col=13, group="main", label="Ctrl"),
)
# --- Main alpha block, ISO. Same as ANSI minus the row 2 col 13 backslash;
# on ISO that position is the top half of the L-shape Enter, addressed
# by zone 37 (the main Enter cell at row 3 col 13). Zone 46 is silently
# unaddressable on ISO layouts — same limitation as OpenRGB's UI.
MAIN_ISO: tuple[Cell, ...] = tuple(c for c in MAIN_ANSI if not (c.row == 2 and c.col == 13))
# --- Curated allowlist for unmapped device zones surfaced in the bottom strip.
# G-keys, logo, media, brightness — the canonical "extras" Logitech firmware
# actually addresses. Phantom zones (e.g. G515's 47, 97, 99-103, 254) drop.
EXTRAS_ALLOWLIST: frozenset[int] = frozenset(
{
153, # Brightness
155, # Play/Pause
156, # Mute
157, # Next
158, # Previous
180, # G1
181, # G2
182, # G3
183, # G4
184, # G5
210, # Logo
}
)
def _relabel(cells: tuple[Cell, ...], overrides: dict[int, str]) -> tuple[Cell, ...]:
"""Return a new tuple where any cell whose zone_id is in `overrides` has
its label replaced. Unaffected cells pass through unchanged.
"""
if not overrides:
return cells
return tuple(
Cell(
zone_id=c.zone_id,
row=c.row,
col=c.col,
width=c.width,
height=c.height,
group=c.group,
label=overrides[c.zone_id] if c.zone_id in overrides else c.label,
x=c.x,
y=c.y,
)
for c in cells
)
def build_layout(
main_cells: tuple[Cell, ...],
*,
include_numpad: bool,
label_overrides: dict[int, str] | None = None,
description: str = "",
) -> Layout:
"""Assemble a regional keyboard layout from a chosen main block + the
shared fn-row / extras / (optionally) numpad blocks. Apply per-zone
label overrides to every cell whose zone matches.
"""
cells = FN_ROW + main_cells + EXTRAS
if include_numpad:
cells = cells + NUMPAD
cells = _relabel(cells, label_overrides or {})
cols = 21 if include_numpad else 17
return Layout(
cells=cells,
rows=6,
cols=cols,
extra_zones=EXTRAS_ALLOWLIST,
description=description,
)

View File

@ -1,4 +1,4 @@
## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
## 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
@ -20,177 +20,23 @@ Cell positions and groupings derived from OpenRGB's KeyboardLayoutManager
(KeyboardLayoutManager.cpp), Copyright (C) Chris M (Dr_No), licensed under
GPL-2.0-or-later. This file ports the static ANSI data only; the runtime
opcode interpreter for regional overlays is intentionally not included.
Zone IDs are the firmware values reported by Logitech HID++ feature 0x8081
(PER_KEY_LIGHTING_V2).
"""
from __future__ import annotations
from ..layout import Cell
from ..layout import Layout
from ._keyboard_base import MAIN_ANSI
from ._keyboard_base import build_layout
# Main alpha block (KLM keyboard_zone_main, ANSI variant).
# ANSI removes the ISO backslash (row 4 col 1) and POUND (row 3 col 12).
_MAIN: tuple[Cell, ...] = (
# Row 1: backtick + numbers + minus/equals + backspace
Cell(zone_id=50, row=1, col=0, group="main", label="`"),
Cell(zone_id=27, row=1, col=1, group="main", label="1"),
Cell(zone_id=28, row=1, col=2, group="main", label="2"),
Cell(zone_id=29, row=1, col=3, group="main", label="3"),
Cell(zone_id=30, row=1, col=4, group="main", label="4"),
Cell(zone_id=31, row=1, col=5, group="main", label="5"),
Cell(zone_id=32, row=1, col=6, group="main", label="6"),
Cell(zone_id=33, row=1, col=7, group="main", label="7"),
Cell(zone_id=34, row=1, col=8, group="main", label="8"),
Cell(zone_id=35, row=1, col=9, group="main", label="9"),
Cell(zone_id=36, row=1, col=10, group="main", label="0"),
Cell(zone_id=42, row=1, col=11, group="main", label="-"),
Cell(zone_id=43, row=1, col=12, group="main", label="="),
Cell(zone_id=39, row=1, col=13, group="main", label="Bksp"),
# Row 2: tab + qwerty + brackets + backslash
Cell(zone_id=40, row=2, col=0, group="main", label="Tab"),
Cell(zone_id=17, row=2, col=1, group="main", label="Q"),
Cell(zone_id=23, row=2, col=2, group="main", label="W"),
Cell(zone_id=5, row=2, col=3, group="main", label="E"),
Cell(zone_id=18, row=2, col=4, group="main", label="R"),
Cell(zone_id=20, row=2, col=5, group="main", label="T"),
Cell(zone_id=25, row=2, col=6, group="main", label="Y"),
Cell(zone_id=21, row=2, col=7, group="main", label="U"),
Cell(zone_id=9, row=2, col=8, group="main", label="I"),
Cell(zone_id=15, row=2, col=9, group="main", label="O"),
Cell(zone_id=16, row=2, col=10, group="main", label="P"),
Cell(zone_id=44, row=2, col=11, group="main", label="["),
Cell(zone_id=45, row=2, col=12, group="main", label="]"),
Cell(zone_id=46, row=2, col=13, group="main", label="\\"),
# Row 3: caps + asdf-row + semi/quote + enter
Cell(zone_id=54, row=3, col=0, group="main", label="Caps"),
Cell(zone_id=1, row=3, col=1, group="main", label="A"),
Cell(zone_id=19, row=3, col=2, group="main", label="S"),
Cell(zone_id=4, row=3, col=3, group="main", label="D"),
Cell(zone_id=6, row=3, col=4, group="main", label="F"),
Cell(zone_id=7, row=3, col=5, group="main", label="G"),
Cell(zone_id=8, row=3, col=6, group="main", label="H"),
Cell(zone_id=10, row=3, col=7, group="main", label="J"),
Cell(zone_id=11, row=3, col=8, group="main", label="K"),
Cell(zone_id=12, row=3, col=9, group="main", label="L"),
Cell(zone_id=48, row=3, col=10, group="main", label=";"),
Cell(zone_id=49, row=3, col=11, group="main", label="'"),
Cell(zone_id=37, row=3, col=13, group="main", label="Enter"),
# Row 4: shift + zxcv-row + comma/period/slash + rshift
Cell(zone_id=105, row=4, col=0, group="main", label="Shift"),
Cell(zone_id=26, row=4, col=2, group="main", label="Z"),
Cell(zone_id=24, row=4, col=3, group="main", label="X"),
Cell(zone_id=3, row=4, col=4, group="main", label="C"),
Cell(zone_id=22, row=4, col=5, group="main", label="V"),
Cell(zone_id=2, row=4, col=6, group="main", label="B"),
Cell(zone_id=14, row=4, col=7, group="main", label="N"),
Cell(zone_id=13, row=4, col=8, group="main", label="M"),
Cell(zone_id=51, row=4, col=9, group="main", label=","),
Cell(zone_id=52, row=4, col=10, group="main", label="."),
Cell(zone_id=53, row=4, col=11, group="main", label="/"),
Cell(zone_id=109, row=4, col=13, group="main", label="Shift"),
# Row 5: bottom row. Space spans cols 3..9 visually.
Cell(zone_id=104, row=5, col=0, group="main", label="Ctrl"),
Cell(zone_id=107, row=5, col=1, group="main", label="Win"),
Cell(zone_id=106, row=5, col=2, group="main", label="Alt"),
Cell(zone_id=41, row=5, col=3, width=7.0, group="main", label="Space"),
Cell(zone_id=110, row=5, col=10, group="main", label="AltGr"),
Cell(zone_id=111, row=5, col=11, group="main", label="Win"),
Cell(zone_id=98, row=5, col=12, group="main", label="Menu"),
Cell(zone_id=108, row=5, col=13, group="main", label="Ctrl"),
)
# Function row (KLM keyboard_zone_fn_row): ESC + F1..F12.
_FN_ROW: tuple[Cell, ...] = (
Cell(zone_id=38, row=0, col=0, group="fn_row", label="Esc"),
Cell(zone_id=55, row=0, col=2, group="fn_row", label="F1"),
Cell(zone_id=56, row=0, col=3, group="fn_row", label="F2"),
Cell(zone_id=57, row=0, col=4, group="fn_row", label="F3"),
Cell(zone_id=58, row=0, col=5, group="fn_row", label="F4"),
Cell(zone_id=59, row=0, col=6, group="fn_row", label="F5"),
Cell(zone_id=60, row=0, col=7, group="fn_row", label="F6"),
Cell(zone_id=61, row=0, col=8, group="fn_row", label="F7"),
Cell(zone_id=62, row=0, col=9, group="fn_row", label="F8"),
Cell(zone_id=63, row=0, col=10, group="fn_row", label="F9"),
Cell(zone_id=64, row=0, col=11, group="fn_row", label="F10"),
Cell(zone_id=65, row=0, col=12, group="fn_row", label="F11"),
Cell(zone_id=66, row=0, col=13, group="fn_row", label="F12"),
)
# Extras cluster (KLM keyboard_zone_extras): nav block + arrows.
_EXTRAS: tuple[Cell, ...] = (
Cell(zone_id=67, row=0, col=14, group="extras", label="PrtSc"),
Cell(zone_id=68, row=0, col=15, group="extras", label="ScrLk"),
Cell(zone_id=69, row=0, col=16, group="extras", label="Pause"),
Cell(zone_id=70, row=1, col=14, group="extras", label="Ins"),
Cell(zone_id=71, row=1, col=15, group="extras", label="Home"),
Cell(zone_id=72, row=1, col=16, group="extras", label="PgUp"),
Cell(zone_id=73, row=2, col=14, group="extras", label="Del"),
Cell(zone_id=74, row=2, col=15, group="extras", label="End"),
Cell(zone_id=75, row=2, col=16, group="extras", label="PgDn"),
Cell(zone_id=79, row=4, col=15, group="extras", label=""),
Cell(zone_id=77, row=5, col=14, group="extras", label=""),
Cell(zone_id=78, row=5, col=15, group="extras", label=""),
Cell(zone_id=76, row=5, col=16, group="extras", label=""),
)
# Numpad (KLM keyboard_zone_numpad). NumPad + and Enter span 2 rows tall.
_NUMPAD: tuple[Cell, ...] = (
Cell(zone_id=80, row=1, col=17, group="numpad", label="Num"),
Cell(zone_id=81, row=1, col=18, group="numpad", label="/"),
Cell(zone_id=82, row=1, col=19, group="numpad", label="*"),
Cell(zone_id=83, row=1, col=20, group="numpad", label="-"),
Cell(zone_id=92, row=2, col=17, group="numpad", label="7"),
Cell(zone_id=93, row=2, col=18, group="numpad", label="8"),
Cell(zone_id=94, row=2, col=19, group="numpad", label="9"),
Cell(zone_id=84, row=2, col=20, height=2.0, group="numpad", label="+"),
Cell(zone_id=89, row=3, col=17, group="numpad", label="4"),
Cell(zone_id=90, row=3, col=18, group="numpad", label="5"),
Cell(zone_id=91, row=3, col=19, group="numpad", label="6"),
Cell(zone_id=86, row=4, col=17, group="numpad", label="1"),
Cell(zone_id=87, row=4, col=18, group="numpad", label="2"),
Cell(zone_id=88, row=4, col=19, group="numpad", label="3"),
Cell(zone_id=85, row=4, col=20, height=2.0, group="numpad", label="Enter"),
Cell(zone_id=95, row=5, col=17, width=2.0, group="numpad", label="0"),
Cell(zone_id=96, row=5, col=19, group="numpad", label="."),
)
# Curated allowlist for unmapped device zones surfaced in the bottom strip.
# Mirrors OpenRGB's `hidpp20_key_name_to_zone` extras: brightness, media,
# G1-G5, logo. Anything else (e.g. G515 phantoms 47, 97, 99-103, 254) is
# dropped by the binder.
_EXTRAS_ALLOWLIST: frozenset[int] = frozenset(
{
153, # Brightness
155, # Play/Pause
156, # Mute
157, # Next
158, # Previous
180, # G1
181, # G2
182, # G3
183, # G4
184, # G5
210, # Logo
}
)
LAYOUT_FULL: Layout = Layout(
cells=_FN_ROW + _MAIN + _EXTRAS + _NUMPAD,
rows=6,
cols=21,
extra_zones=_EXTRAS_ALLOWLIST,
LAYOUT_FULL: Layout = build_layout(
MAIN_ANSI,
include_numpad=True,
description="ANSI QWERTY 104-key full-size",
)
LAYOUT_TKL: Layout = Layout(
cells=_FN_ROW + _MAIN + _EXTRAS,
rows=6,
cols=17,
extra_zones=_EXTRAS_ALLOWLIST,
LAYOUT_TKL: Layout = build_layout(
MAIN_ANSI,
include_numpad=False,
description="ANSI QWERTY tenkeyless",
)

View File

@ -0,0 +1,75 @@
## 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.
"""ISO AZERTY layout (FR).
ISO shape plus French label overrides AQ, WZ, M repositioned, French
digit-row symbols (& é " ' ( - è _ ç à ). Adapted from OpenRGB.
"""
from __future__ import annotations
from ..layout import Layout
from ._keyboard_base import MAIN_ISO
from ._keyboard_base import build_layout
# zone_id → French label
_OVERRIDES: dict[int, str] = {
# Row 1 (digit row → French symbols)
50: "²", # backtick → super-2
27: "&", # 1
28: "é", # 2
29: '"', # 3
30: "'", # 4
31: "(", # 5
32: "-", # 6
33: "è", # 7
34: "_", # 8
35: "ç", # 9
36: "à", # 0
42: ")", # minus → close-paren
# Row 2 — Q/A and W/Z swaps, brackets relabeled
17: "A", # Q-position → A
23: "Z", # W-position → Z
44: "^", # [-position → caret
45: "$", # ]-position → dollar
# Row 3 — A → Q; M moves up to ; position
1: "Q", # A-position → Q
48: "M", # ;-position → M
49: "ù", # '-position → ù
# Row 4 — Z-position becomes W; comma row shifts
26: "W", # Z-position → W
13: ",", # M-position → comma
51: ";", # ,-position → semicolon
52: ":", # .-position → colon
53: "!", # /-position → exclamation
}
LAYOUT_FULL: Layout = build_layout(
MAIN_ISO,
include_numpad=True,
label_overrides=_OVERRIDES,
description="ISO AZERTY (FR) full-size",
)
LAYOUT_TKL: Layout = build_layout(
MAIN_ISO,
include_numpad=False,
label_overrides=_OVERRIDES,
description="ISO AZERTY (FR) tenkeyless",
)

View File

@ -0,0 +1,45 @@
## 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.
"""ISO_QWERTY keyboard layouts (UK English ISO + other QWERTY ISO regions).
Same English keycap legends as ANSI; differs only in shape the row 2 col 13
backslash on ANSI doesn't exist on ISO (that position is the upper half of the
L-shape Enter, addressed by zone 37). Used for UK and any other region whose
country code maps to "iso_qwerty" without a more specific layout (Spanish,
Italian, Portuguese, Belgian, Nordic those keyboards have the same shape
as UK ISO; only their physical keycap legends differ, which our painter
doesn't reproduce verbatim).
"""
from __future__ import annotations
from ..layout import Layout
from ._keyboard_base import MAIN_ISO
from ._keyboard_base import build_layout
LAYOUT_FULL: Layout = build_layout(
MAIN_ISO,
include_numpad=True,
description="ISO QWERTY 103-key full-size",
)
LAYOUT_TKL: Layout = build_layout(
MAIN_ISO,
include_numpad=False,
description="ISO QWERTY tenkeyless",
)

View File

@ -0,0 +1,57 @@
## 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.
"""ISO QWERTZ layout (DE / CH).
ISO shape plus German label overrides (Y/Z swap, Ü/Ö/Ä/ß placement).
Adapted from OpenRGB.
"""
from __future__ import annotations
from ..layout import Layout
from ._keyboard_base import MAIN_ISO
from ._keyboard_base import build_layout
# zone_id → German label
_OVERRIDES: dict[int, str] = {
50: "^", # row 1 col 0 — caret/degree (DE keycap)
42: "ß", # row 1 col 11 — eszett
43: "´", # row 1 col 12 — acute accent
25: "Z", # row 2 col 6 — Y/Z swap
44: "Ü", # row 2 col 11
45: "+", # row 2 col 12
48: "Ö", # row 3 col 10
49: "Ä", # row 3 col 11
26: "Y", # row 4 col 2 — Y/Z swap
53: "-", # row 4 col 11
}
LAYOUT_FULL: Layout = build_layout(
MAIN_ISO,
include_numpad=True,
label_overrides=_OVERRIDES,
description="ISO QWERTZ (DE/CH) full-size",
)
LAYOUT_TKL: Layout = build_layout(
MAIN_ISO,
include_numpad=False,
label_overrides=_OVERRIDES,
description="ISO QWERTZ (DE/CH) tenkeyless",
)

View File

@ -0,0 +1,52 @@
## 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.
"""JIS layout (JP).
ISO shape with Japanese keycap relabels for the bracket / colon positions.
Adapted from OpenRGB. JIS keyboards also have additional kana-control keys
near the spacebar (henkan/muhenkan/kana) that aren't represented here —
matches OpenRGB's coverage.
"""
from __future__ import annotations
from ..layout import Layout
from ._keyboard_base import MAIN_ISO
from ._keyboard_base import build_layout
# zone_id → JIS label
_OVERRIDES: dict[int, str] = {
44: "@", # row 2 col 11 — bracket-position becomes at-sign
45: "[", # row 2 col 12 — bracket shifts left
49: ":", # row 3 col 11 — quote-position becomes colon
}
LAYOUT_FULL: Layout = build_layout(
MAIN_ISO,
include_numpad=True,
label_overrides=_OVERRIDES,
description="JIS (JP) full-size",
)
LAYOUT_TKL: Layout = build_layout(
MAIN_ISO,
include_numpad=False,
label_overrides=_OVERRIDES,
description="JIS (JP) tenkeyless",
)

View File

@ -1,4 +1,4 @@
## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
## 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

View File

@ -1,4 +1,4 @@
## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
## 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

View File

@ -1,4 +1,4 @@
## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
## 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

View File

@ -1,4 +1,4 @@
## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
## 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