PerKeyLighting: drop misleading live-read output in solaar show

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.
This commit is contained in:
Ken Sanislo 2026-05-10 23:24:21 -07:00 committed by Peter F. Patel-Schneider
parent fc19860f76
commit ad96dd395b
3 changed files with 33 additions and 10 deletions

View File

@ -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.

View File

@ -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

View File

@ -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:")