Releases: alebairos/omarchy-mx-plugin
Release list
v1.2.0 — hardware keys instant, OSD back on r2095, tested like Omarchy
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.
v1.0.0
[1.0.0] — 2026-09-06
First stable release. Verified by removing the plugin entirely and
reinstalling from GitHub the way a new user would, then exercising every
control against the hardware.
Added
- Per-instance settings from
shell.json:defaultOnLevel,
refreshMinutesandshowBattery, read through the base panel's
setting()exactly as first-party widgets do, and clamped rather than
trusted.
Fixed
- A single degraded read could make the widget announce "no
backlight-capable keyboard" while the keyboard was working. The device
answers contention with a well-formed frame that omits theBACKLIGHT2
block, so a truncated read and a keyboard without a backlight look
identical. A loss now requires three consecutive confirmations, with a
fast re-read between them.
Verified for this release
- Clean
omarchy plugin addfrom GitHub, with the bundled helper arriving
executable - Backlight toggle, brightness, and effect changes reaching the device
- Panel refresh on open, and live following of the keyboard's own keys with
the optional Solaar rule - Vertical (right-side) bar
- 33 unit and functional tests, and both CI checks
Still true, and deliberately so
- Roughly two seconds per action: this shells out to
solaarrather than
holding a connection, and that startup cost is the floor for anything
that refuses to run a daemon. - Only an MX Mechanical Mini and a Signature M650 on a Bolt receiver have
ever been tested. Everything else — other backlit keyboards, Bluetooth
pairing, multiple keyboards — is expected to work rather than known to.
Reports are welcome, especially negative ones.
v1.0.0-rc.5 — follow the keyboard's own keys
[1.0.0-rc.5] — 2026-09-06
Closes the last of the README's known limitations: the panel can now follow
the keyboard's own backlight keys.
Added
-
Optional live sync with the keyboard's own keys.
solaar-rule.yamlis an opt-in Solaar rule that calls
the widget when the device reports a backlight change, so pressing F4/F5
or the effect key moves the panel and pops an on-screen display, in about
two seconds.The device has always announced these changes over HID++; hearing them
needs a process listening continuously, and this plugin deliberately is
not one (constitution Principle V). Solaar already is such a process for
anyone who runs it, and its rules engine exists to react to exactly these
notifications — so the listening is delegated rather than duplicated.
Without Solaar running nothing breaks: the panel catches up on open, as
before. -
statusover IPC now reports the current effect, so this class of problem
can be diagnosed from a terminal instead of by watching the screen.
Fixed
- The effect OSD never appeared while the brightness one always did.
AProcess'sonExitedand itsStdioCollector'sonStreamFinished
fire in no guaranteed order, and the queue driver cleared its "announce"
flag when the queue emptied — so whichever read ran last could lose the
race with itself. The effect read is last; the level read never is. - The hardware-key OSD lagged about five seconds. An external change ran
three reads (~7s) when the effect helper's single call already reports
level and effect together (~2s).
Verified
- Vertical bars. Confirmed working on a right-side bar, with no code
change required: placement comes fromUi/KeyboardPanelreading
bar.position, and the panel sizes itself rather than assuming geometry.
Left-side bars remain untested.
v1.0.0-rc.4 — lighting effects
[1.0.0-rc.4] — 2026-09-06
Adds lighting-effect control, and a good deal of hardening found by using it.
Added
- Lighting effects. The panel gains a third row selecting the keyboard's
effect — Static, Breathing, Contrast, Reaction, Random, Wave — clickable,
right-clickable to go back, and reachable by keyboard like the other rows.
Solaar's CLI has no setting for this; the plugin reaches it through
Solaar's ownlogitech_receiverlibrary, so it adds no dependency and
does not touch/dev/hidrawdirectly. The list comes from the device's
own capability bitmap rather than a hardcoded table, and each value was
calibrated against real hardware rather than guessed. - An "off" effect the device advertises is deliberately not offered: it
clears the backlight's enabled flag, and the panel already has a toggle
for that. Cycling skips it in both directions. - CI guards against a broken
\Uunicode escape in QML, and against real
device serials in test fixtures.
Fixed
- Every device read is now serialized. The effect helper was missing
from the single-flight guard, so it ran concurrently withsolaarcalls
and both received degraded frames; separately, the panel's refresh-on-open
dropped its queue whenever anything was in flight. Together these left a
stale brightness level on screen after F4/F5. - Switching to a static effect left the previous effect's last frame
frozen on the keys. A brief off-pulse before applying clears it. - The helper could strand the keyboard dark, by carrying through an
enabledflag the off-effect had cleared. - Degraded reads returned zeros rather than errors, which the panel
would have read as "this keyboard has no effects" and hidden the row. - The brightness slider's maximum is read from the device's reported level
count instead of assuming eight and correcting after a rejected write. - The effect row's icon rendered as the literal text
f0068, because
\U000F0068is not a valid QML escape.
Changed
- Real device serials removed from the committed test fixture.
- Repository opened to contributors:
mainis protected, merges are
rebase-only, and there are PR and issue templates, a security policy and a
code of conduct.
v1.0.0-rc.3 — slimmer install, contributor and agent docs
[1.0.0-rc.3] — 2026-09-04
Housekeeping release. No change to how the plugin behaves; a large change to
what lands on your machine when you install it.
Removed
- Spec-kit scaffolding (
.specify/,.claude/skills/speckit-*), about
5,100 lines.omarchy plugin addperforms a fullgit cloneinto
~/.config/omarchy/plugins/, so everything in this repository is copied
onto every user's machine. That scaffolding is generic — a grep for
anything naming Solaar, the backlight, or this device matched exactly one
file in it — and is regenerable withspecify init, so it was pure noise
in a directory Omarchy explicitly asks users to review before enabling.
A fresh install goes from 676K to 504K. The saving is smaller than the
removed 5,100 lines suggests, becauseplugin addclones full history
and the deleted files therefore still travel in.git; the real win is
that the working tree a reviewer opens now contains only the plugin, its
tests and its reasoning.
Added
CONTRIBUTING.md— how to test, how to verify against the device
rather than the widget, and the device quirks that look like redundant
work and must not be "simplified" away.AGENTS.md— guidance for AI agents and their supervisors, recording
the failure modes this project has already hit: QML not observing plain
object mutations,rescanPluginssilently not reloading a changed root
type, astral-plane glyphs mangled by naive edits, and the instruction not
to trust a green test suite without breaking the code first.- A CI check that the scaffolding cannot creep back and that every document
contributors are pointed at actually exists.
Changed
- The project constitution moved from
.specify/memory/constitution.mdto
specs/constitution.md, so the one project-specific file in the
removed scaffolding survives where the rest of the reasoning lives.
v1.0.0-rc.2 — tests, CI, and a native look
[1.0.0-rc.2] — 2026-09-04
First release with automated tests and CI. Everything below was verified
against real hardware in addition to the suite.
Added
- Test suite (25 tests, zero dependencies). Runs on node's built-in
test runner; the plugin itself still ships no JavaScript runtime
dependency. Unit tests cover thesolaar showparser against a real
captured fixture plus edge cases (mouse-only, no battery, no devices,
malformed output) and the state logic. Functional tests execute planned
commands against a fakesolaarthat records every invocation and
emulates the device's real quirks, asserting the exact command sequence. Model.js, holding the parser and state logic as pure functions,
following the convention of Omarchy's own plugins (bar/BarModel.js,
panels/power/Model.js).- CI on every push and pull request: the test suite, manifest
validation mirroringomarchy-plugin-validate, and a guard against emoji
re-entering the shipped QML. - Keyboard navigation. Arrow keys move a cursor across the backlight
toggle and the brightness slider, left/right adjust brightness, Enter
activates, Esc closes, and Tab hands off to an adjacent panel — matching
all nine first-party Omarchy panels. - Middle-click on the bar icon opens Solaar, the escape hatch to every
setting this widget deliberately does not expose. Mirrors the built-in
Microphone widget's middle-click-through. - The bar tooltip now reports device, battery and backlight state instead
of a fixed string.
Changed
- Icons are now Nerd Font glyphs tinted from the active theme
(mdi-keyboard,mdi-keyboard-off,mdi-brightness-7), replacing emoji.
No first-party Omarchy widget uses emoji: they render in a different font
at a different weight and, without a colour binding, ignore the theme
entirely. Every codepoint was verified present in JetBrainsMono Nerd Font
withfc-list :charset=…rather than assumed. - The plugin now lives at the repository root, so
omarchy plugin add <git-url>installs it directly. Previously the plugin sat inplugin/,
and sinceomarchy plugin addvalidatesmanifest.jsonat the clone
root, the documented one-line install rejected this repository outright. manifest.jsondeclaresbarWidget.defaultSection: "right"; the
installer previously offered "center" by default.
Fixed
- A parser test that passed for the wrong reason. It asserted that the live
backlight level is read rather than the(saved)one, but still passed
with the parser deliberately broken, because the live line follows the
saved line and simply overwrote it. Found by mutation-testing the suite;
fixed with a fixture containing only(saved)lines, which genuinely
distinguishes the two. - Removed a leftover debug
console.logfrom the write path.