settings: provide symbolic names for per-key lighting keys

This commit is contained in:
Peter F. Patel-Schneider 2024-03-26 15:04:12 -04:00
parent 4d0f93b35c
commit f38fbcf949
4 changed files with 167 additions and 11 deletions

View File

@ -51,7 +51,7 @@ connect via a USB cable or via bluetooth can be determined by their USB or
Bluetooth product ID.
# Pairing and Unpairing
## Pairing and Unpairing
Solaar is able to pair and unpair devices with
receivers as supported by the device and receiver.
@ -174,6 +174,18 @@ is sent to the Solaar rule system so that rules can detect these notifications.
For more information on Mouse Gestures rule conditions see
[the rules page](https://pwr-solaar.github.io/Solaar/rules).
### Keyboard Key Names and Locations
Solaar uses the standard Logitech names for keyboard keys. Some Logitech keyboards have different icons on some of their keys and have different functionality than suggested by these names.
Solaar is uses the standard US keyboard layout. This currently only matters for the `Per-key Lighting` setting. Users who want to have the key names for this setting reflect the keyboard layout that they use can create and edit `~/.config/solaar/keys.yaml` which contains a YAML dictionary of key names and locations. For example, switching the `Y` and `Z` keys can be done as:
Z: 25
Y: 26
This is an experimental feature and may be modified or even eliminated.
### Device Profiles
Some mice store one or more profiles, which control aspects of the behavior of the device.

View File

@ -1579,7 +1579,7 @@ class PerKeyLighting(_Settings):
label = _("Per-key Lighting")
description = _("Control per-key lighting.")
feature = _F.PER_KEY_LIGHTING_V2
keys_universe = _NamedInts.range(1, 254)
keys_universe = _special_keys.KEYCODES
choices_universe = _special_keys.COLORS
def read(self, cached=True):
@ -1607,7 +1607,7 @@ class PerKeyLighting(_Settings):
return map
def write_key_value(self, key, value, save=True):
result = super().write_key_value(key, value, save)
result = super().write_key_value(int(key), value, save)
if self._device.online:
self._device.feature_request(self.feature, 0x70, 0x00) # signal device to make the change
return result
@ -1624,7 +1624,8 @@ class PerKeyLighting(_Settings):
key_bitmap += device.feature_request(setting_class.feature, 0x00, 0x00, 0x02)[2:]
for i in range(1, 255):
if (key_bitmap[i // 8] >> i % 8) & 0x01:
choices_map[setting_class.keys_universe[i]] = setting_class.choices_universe
key = setting_class.keys_universe[i] if i in setting_class.keys_universe else _NamedInt(i, "KEY " + str(i))
choices_map[key] = setting_class.choices_universe
result = cls(choices_map) if choices_map else None
return result

View File

@ -17,9 +17,16 @@
# Reprogrammable keys information
# Mostly from Logitech documentation, but with some edits for better Lunix compatibility
import os as _os
import yaml as _yaml
from .common import NamedInts as _NamedInts
from .common import UnsortedNamedInts as _UnsortedNamedInts
_XDG_CONFIG_HOME = _os.environ.get("XDG_CONFIG_HOME") or _os.path.expanduser(_os.path.join("~", ".config"))
_keys_file_path = _os.path.join(_XDG_CONFIG_HOME, "solaar", "keys.yaml")
# <controls.xml awk -F\" '/<Control /{sub(/^LD_FINFO_(CTRLID_)?/, "", $2);printf("\t%s=0x%04X,\n", $2, $4)}' | sort -t= -k2
CONTROL = _NamedInts(
{
@ -1392,3 +1399,139 @@ COLORS = _UnsortedNamedInts(
"light green": 0x90EE90,
}
)
KEYCODES = _NamedInts(
{
"A": 1,
"B": 2,
"C": 3,
"D": 4,
"E": 5,
"F": 6,
"G": 7,
"H": 8,
"I": 9,
"J": 10,
"K": 11,
"L": 12,
"M": 13,
"N": 14,
"O": 15,
"P": 16,
"Q": 17,
"R": 18,
"S": 19,
"T": 20,
"U": 21,
"V": 22,
"W": 23,
"X": 24,
"Y": 25,
"Z": 26,
"1": 27,
"2": 28,
"3": 29,
"4": 30,
"5": 31,
"6": 32,
"7": 33,
"8": 34,
"9": 35,
"0": 36,
"ENTER": 37,
"ESC": 38,
"BACKSPACE": 39,
"TAB": 40,
"SPACE": 41,
"-": 42,
"=": 43,
"[": 44,
"]": 45,
"\\": 45,
"~": 47,
";": 48,
"'": 49,
"`": 50,
",": 51,
".": 52,
"/": 53,
"CAPS LOCK": 54,
"F1": 55,
"F2": 56,
"F3": 57,
"F4": 58,
"F5": 59,
"F6": 60,
"F7": 61,
"F8": 62,
"F9": 63,
"F10": 64,
"F11": 65,
"F12": 66,
"PRINT": 67,
"SCROLL LOCK": 68,
"PASTE": 69,
"INSERT": 70,
"HOME": 71,
"PAGE UP": 72,
"DELETE": 73,
"END": 74,
"PAGE DOWN": 75,
"RIGHT": 76,
"LEFT": 77,
"DOWN": 78,
"UP": 79,
"NUMLOCK": 80,
"KEYPAD /": 81,
"KEYPAD *": 82,
"KEYPAD -": 83,
"KEYPAD +": 84,
"KEYPAD ENTER": 85,
"KEYPAD 1": 86,
"KEYPAD 2": 87,
"KEYPAD 3": 88,
"KEYPAD 4": 89,
"KEYPAD 5": 90,
"KEYPAD 6": 91,
"KEYPAD 7": 92,
"KEYPAD 8": 93,
"KEYPAD 9": 94,
"KEYPAD 0": 95,
"KEYPAD .": 96,
"COMPOSE": 98,
"POWER": 99,
"LEFT CTRL": 104,
"LEFT SHIFT": 105,
"LEFT ALT": 106,
"LEFT WINDOWS": 107,
"RIGHT CTRL": 108,
"RIGHT SHIFT": 109,
"RIGHT ALTGR": 110,
"RIGHT WINDOWS": 111,
"BRIGHTNESS": 153,
"PAUSE": 155,
"MUTE": 156,
"NEXT": 157,
"PREVIOUS": 158,
"G1": 180,
"G2": 181,
"G3": 182,
"G4": 183,
"G5": 184,
"LOGO": 210,
}
)
# load in override dictionary for KEYCODES
try:
with open(_keys_file_path) as keys_file:
keys = _yaml.safe_load(keys_file)
if isinstance(keys, dict):
keys = _NamedInts(**keys)
for k in KEYCODES:
if int(k) not in keys and str(k) not in keys:
keys[int(k)] = str(k)
KEYCODES = keys
except Exception as e:
print(e)

View File

@ -560,12 +560,12 @@ key_tests = [
5,
),
{
common.NamedInt(1, "1"): special_keys.COLORS,
common.NamedInt(2, "2"): special_keys.COLORS,
common.NamedInt(9, "9"): special_keys.COLORS,
common.NamedInt(10, "10"): special_keys.COLORS,
common.NamedInt(113, "113"): special_keys.COLORS,
common.NamedInt(114, "114"): special_keys.COLORS,
common.NamedInt(1, "A"): special_keys.COLORS,
common.NamedInt(2, "B"): special_keys.COLORS,
common.NamedInt(9, "I"): special_keys.COLORS,
common.NamedInt(10, "J"): special_keys.COLORS,
common.NamedInt(113, "KEY 113"): special_keys.COLORS,
common.NamedInt(114, "KEY 114"): special_keys.COLORS,
},
hidpp.Response("00000606000000000000000000000000", 0x0400, "0000"), # first group of keys
hidpp.Response("00000600000000000000000000000000", 0x0400, "0001"), # second group of keys
@ -586,7 +586,7 @@ def test_key_template(test, mocker):
spy_request = mocker.spy(device, "request")
setting = settings_templates.check_feature(device, tst.sclass)
assert setting is not None
assert setting
if isinstance(setting, list):
setting = setting[0]
assert setting.choices == test.choices