From ad96dd395b14519412249c4d6533caf5ee9dcf96 Mon Sep 17 00:00:00 2001 From: Ken Sanislo Date: Sun, 10 May 2026 23:24:21 -0700 Subject: [PATCH] PerKeyLighting: drop misleading live-read output in solaar show MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 0x8081 PerKeyLighting v2 protocol has no GetIndividualRgbZones function — there's no way to ask the device what colors are currently on the per-key buffer over HID++. PerKeyLighting.read() papered over this gap by fabricating an all-"No change" sentinel map whenever the caller asked for a live (uncached) read, which produced misleading solaar show output like: Per-key Lighting (saved): {1:0xc01c28, 2:0xc52d26, ...} Per-key Lighting : {1:No change, 2:No change, ...} — even with the keyboard clearly running the saved colors. Two-part fix: 1. PerKeyLighting.read() now returns self._value if populated (from prior write or persister) regardless of `cached`. Only fabricates the all-"No change" map when there's truly no state — fresh device, no prior write, nothing persisted. This is the right starting state in that case because per-key writes are additive over a buffer of unknown content. Other callers benefit too: solaar config calls read(cached=False) on per-key settings to display current values, and prior to this change it would get fabricated sentinels back instead of the in-memory map. 2. Add a `live_readable = True` class attribute to Setting (default preserves existing behavior). Override to False on PerKeyLighting. solaar show gates its live-read print on this flag, so non-readable settings show only the (saved) line — which is the authoritative record of what was last written to a setting whose live state can't be read back. After both fixes solaar show prints just: Per-key Lighting (saved): {1:0xc01c28, 2:0xc52d26, ...} — honest about what we know. 690 tests pass; pre-commit clean. --- lib/logitech_receiver/settings.py | 5 +++++ lib/logitech_receiver/settings_templates.py | 17 +++++++++++++++-- lib/solaar/cli/show.py | 21 +++++++++++++-------- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/lib/logitech_receiver/settings.py b/lib/logitech_receiver/settings.py index b5ddd23c..39c8c872 100644 --- a/lib/logitech_receiver/settings.py +++ b/lib/logitech_receiver/settings.py @@ -61,6 +61,11 @@ class Setting: validator_class = None validator_options = {} display = True # display setting in UI + # Set False for settings whose value cannot be read back from the device + # (e.g. PerKeyLighting — the 0x8081 protocol has no GetIndividualRgbZones). + # `solaar show` uses this to suppress the "(live)" output line that would + # otherwise print a fabricated value misleadingly. + live_readable = True # Optional UI editor override as "module.path:ClassName". Resolved by the # config panel before the Kind dispatch. Kept as a string so this module # stays free of GTK imports — the FE/BE seam is preserved. diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index ef5a7688..6e6e7d22 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -1894,6 +1894,11 @@ class PerKeyLighting(settings.Settings): feature = _F.PER_KEY_LIGHTING_V2 keys_universe = special_keys.KEYCODES editor_class = "solaar.ui.perkey.control:PerKeyControl" + # 0x8081 has no GetIndividualRgbZones — there's no way to ask the device + # what colors are currently on the per-key buffer. read() returns the + # canonical in-memory map for callers that need a value, but `solaar show` + # honors this flag and skips its live-read line to avoid misleading output. + live_readable = False @staticmethod def _wrap_color(value): @@ -1916,12 +1921,20 @@ class PerKeyLighting(settings.Settings): super().update_key_value(key, self._wrap_color(value), save) def read(self, cached=True): + # The 0x8081 protocol has no GetIndividualRgbZones — the device cannot + # report its current per-key buffer back. So a "live" read is fictional: + # we either return what we last wrote (the persisted/in-memory map, + # which is the canonical truth for this setting) or, on a fresh device + # with no persisted state, fabricate an all-"No change" sentinel map + # as the starting point. Returning the cached value unconditionally + # also fixes `solaar show` showing every key as "No change" on the + # live line — it now matches what's actually on the keyboard. self._pre_read(cached) - if cached and self._value is not None: + if self._value is not None: return self._value reply_map = {} for key in self._validator.choices: - reply_map[int(key)] = special_keys.COLORSPLUS["No change"] # this signals no change + reply_map[int(key)] = special_keys.COLORSPLUS["No change"] # starting state, no per-key write yet self._value = reply_map return reply_map diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py index 0dad16b7..e6d7a242 100644 --- a/lib/solaar/cli/show.py +++ b/lib/solaar/cli/show.py @@ -404,14 +404,19 @@ def _print_device(dev, num=None): ): v = setting.val_to_string(setting._device.persister.get(setting.name)) print(f" {setting.label} (saved): {v}") - try: - v = setting.read(False) - v = setting.val_to_string(v) - except exceptions.FeatureCallError as e: - v = "HID++ error " + str(e) - except AssertionError as e: - v = "AssertionError " + str(e) - print(f" {setting.label} : {v}") + # Settings whose value cannot be read back from the device + # (e.g. PerKeyLighting — 0x8081 has no GetIndividualRgbZones) + # suppress the live-read line; the saved line above is the + # authoritative record. See Setting.live_readable. + if getattr(setting, "live_readable", True): + try: + v = setting.read(False) + v = setting.val_to_string(v) + except exceptions.FeatureCallError as e: + v = "HID++ error " + str(e) + except AssertionError as e: + v = "AssertionError " + str(e) + print(f" {setting.label} : {v}") if dev.online and dev.keys: print(f" Has {len(dev.keys)} reprogrammable keys:")