Skip to content

Releases: alebairos/omarchy-mx-plugin

v1.2.0 — hardware keys instant, OSD back on r2095, tested like Omarchy

Choose a tag to compare

@alebairos alebairos released this 10 Sep 22:43

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 the bar kind —
    which means replacing the bar, not being a bar-widget on it. No
    manifest a bar widget can write satisfies any of the three, so
    summon("omarchy.osd") returned false and 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 by tests/solaar-rules.js and asserted
    equal to that generator by the test suite, each passing externalState "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
    own diversion module rather than by inspection. The old
    externalEffect and deviceChanged entry 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 from solaar show or the plugin's own
    transport, after which Feature: BACKLIGHT2 never 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. request answers
    None on 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 existing MXD_STUB_REJECT fails 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/hidraw from 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 engine data[2:],
    so the effect sits at data[3].

    Execute cannot substitute a matched value into its argument list, so
    solaar-rule.yaml now enumerates one rule per effect —
    TestBytes: [3, 4, N, N] paired with a literal N — 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 in qs 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. busy never 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.externalEffectAction treats 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 deviceChanged rule keeps working, so an existing
    ~/.config/solaar/rules.yaml from 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/shell and
    test/acceptance.
    npm run test:shell launches the real
    MxQuickControl.qml in a throwaway quickshell under a fake bar and the
    fake transport and drives it with QtTest's TestEvent — 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 reported 3:2 applies 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
    diversion engine (the check that separates "the rule is wrong" from
    "Solaar is not evaluating"). npm run test:acceptance runs inside a live
    session: the panel is proven on screen via hyprctl layers 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

Choose a tag to compare

@alebairos alebairos released this 06 Sep 22:10

[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,
    refreshMinutes and showBattery, 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 the BACKLIGHT2
    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 add from 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 solaar rather 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

Choose a tag to compare

@alebairos alebairos released this 06 Sep 21:18

[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.yaml is 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.

  • status over 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.
    A Process's onExited and its StdioCollector's onStreamFinished
    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 from Ui/KeyboardPanel reading
    bar.position, and the panel sizes itself rather than assuming geometry.
    Left-side bars remain untested.

v1.0.0-rc.4 — lighting effects

Pre-release

Choose a tag to compare

@alebairos alebairos released this 06 Sep 20:18

[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 own logitech_receiver library, so it adds no dependency and
    does not touch /dev/hidraw directly. 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 \U unicode 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 with solaar calls
    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
    enabled flag 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
    \U000F0068 is not a valid QML escape.

Changed

  • Real device serials removed from the committed test fixture.
  • Repository opened to contributors: main is 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

Choose a tag to compare

@alebairos alebairos released this 05 Sep 00:10

[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 add performs a full git clone into
    ~/.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 with specify 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, because plugin add clones 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, rescanPlugins silently 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.md to
    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

Choose a tag to compare

@alebairos alebairos released this 04 Sep 22:32

[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 the solaar show parser 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 fake solaar that 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 mirroring omarchy-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
    with fc-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 in plugin/,
    and since omarchy plugin add validates manifest.json at the clone
    root, the documented one-line install rejected this repository outright.
  • manifest.json declares barWidget.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.log from the write path.