Both of the keyboard's own keys now raise the on-screen display instantly,
the display survives Omarchy 4.0.0.r2095's plugin-API change, and the
widget is tested the way Omarchy tests itself. Includes everything listed
under 1.1.0, which was never tagged on its own.
Fixed
-
The on-screen display stopped appearing on Omarchy
4.0.0.r2095. That release narrowed the plugin shell API: a
third-party plugin may summon another only if it owns the target, is a
clone of one of four first-party plugins, or declares thebarkind —
which means replacing the bar, not being abar-widgeton it. No
manifest a bar widget can write satisfies any of the three, so
summon("omarchy.osd")returnedfalseand opened nothing, silently:
the denial logs no warning, so the keyboard, the Solaar rules, the IPC
call and the widget's own state all kept working and only the OSD went
missing. The native call is still tried first and the shell's ungated
CLI (omarchy-shell shell summon) covers the refusal, so this
disappears by itself if the gate is ever widened. See
specs/research/osd-summon-gate.md. -
The keyboard's brightness keys (F4/F5) raise the OSD as instantly as the
effect key. They had been left paying the 2.0–2.3s device read that the
per-effect Solaar rules removed for the effect key, because a rule can only
pass what it matched and only one rule fires per notification, so carrying
the level as well needs one rule per (level, effect) pair. The rules file
is now those 128 pairs, generated bytests/solaar-rules.jsand asserted
equal to that generator by the test suite, each passingexternalState "L:E"; the widget compares both values with what it holds and shows the
OSD for whichever moved, reading the device only when nothing did. The
byte offsets were confirmed by evaluating captured frames through Solaar's
owndiversionmodule rather than by inspection. The old
externalEffectanddeviceChangedentry points stay for anyone still
running the 16-rule file.Also recorded, because it cost an hour: Solaar can lose the keyboard's
feature table under contention fromsolaar showor the plugin's own
transport, after whichFeature: BACKLIGHT2never matches and no rule
fires — silently, with a clean journal. Restarting Solaar
(systemctl --user restart app-solaar@autostart.service) re-enumerates it. -
Effect switching no longer writes over LEDs it failed to clear. The
backlight is blanked between effects to stop the previous effect's last
frame staying lit; that write's return value was discarded, so a blank that
lost a race failed silently and the new effect was written anyway — the
intermittent "Wave to Static leaves a frozen wave" fault.requestanswers
Noneon a timeout or error, so the failure was already being reported and
thrown away. The blank is now retried on its own small budget, and a blank
that never lands is reported ("blanked": false) rather than raised: the
effect change itself succeeded, and failing the call would show an error
for something that worked.Reproducible on demand for the first time, via a new
MXD_STUB_FAILED_WRITES
knob — the existingMXD_STUB_REJECTfails every write, so it could only
model a device refusing outright, never contention. -
The keyboard's own effect key no longer waits on a device read. Pressing
it took 3.08s to raise an OSD, measured off/dev/hidrawfrom the device's
own notification to the last frame of the read it triggered. The value was
in that first notification the whole time: a BACKLIGHT2 broadcast reports
[levels, level, ?, effect], and Solaar hands its rule enginedata[2:],
so the effect sits atdata[3].Executecannot substitute a matched value into its argument list, so
solaar-rule.yamlnow enumerates one rule per effect —
TestBytes: [3, 4, N, N]paired with a literalN— and the rule list
short-circuits, so exactly one fires.Verified on the reference hardware against the running shell, watching the
wire and the widget's own state at the same time:device broadcast widget shows the new effect gap 14:15:54.075 → effect 0 14:15:54.231 156ms 14:15:56.130 → effect 3 14:15:56.305 175ms 14:15:59.002 → effect 2 14:15:59.069 67ms Those gaps are upper bounds: the observer polled at 100ms and spent ~36ms
per sample inqs ipc call, so the 67ms row is the one that bounds the real
figure. Under 100ms, against 3.08s.The stronger evidence is the absence: three frames crossed the wire in
the twenty seconds covering all three presses — the three notifications
themselves and nothing else.busynever went true. One press previously
produced around forty frames of receiver enumeration.Brightness from F4/F5 is unchanged in speed and deliberately so: the
notification is a full state report rather than a delta, so it arrives
carrying an unchanged effect.Model.externalEffectActiontreats that as
"something moved that this rule cannot name" and falls back to the previous
read, rather than mistaking it for nothing having happened and swallowing
the change.The old single
deviceChangedrule keeps working, so an existing
~/.config/solaar/rules.yamlfrom 1.0.0 needs no edit to keep behaving as
it does today.
Internal
- Two test tiers in the shape of Omarchy's own
test/shelland
test/acceptance.npm run test:shelllaunches the real
MxQuickControl.qmlin a throwawayquickshellunder a fake bar and the
fake transport and drives it with QtTest'sTestEvent— a click on the
bar icon opens the panel, a click on the slider writes level 7, a click on
the toggle writes level 0, a reported3:2applies both values and asks
for two OSDs without a read, Escape closes — then asserts from the fake's
log that exactly two reads and two writes were sent. Beside it: every
glyph the QML draws exists in an installed font, the QML parses, and the
generated rules fire for captured frames when evaluated by Solaar's own
diversionengine (the check that separates "the rule is wrong" from
"Solaar is not evaluating").npm run test:acceptanceruns inside a live
session: the panel is proven on screen viahyprctllayers and to say
"Backlight" via OCR, the OSD layer is proven to appear for a reported
level with no device read, and one brightness write is checked against
one read of device truth, with a screenshot per step. Each file skips,
saying why, on a machine that lacks what it needs, so CI runs the tier
without a desktop and the scripts stay honest about coverage.