Skip to content

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

Latest

Choose a tag to compare

@alebairos alebairos released this 10 Sep 22:43
· 13 commits to main since this release

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.