Development
Development
Linting and the test suite.
Omarchy plugins are Qt6/Quickshell, so lint with the Qt6 qmllint --
/usr/bin/qmllint on Arch is the Qt5 binary from qt5-declarative and exits
255 with no diagnostics on this file. The qs.* modules resolve only when the
import path contains a directory named qs:
mkdir -p /tmp/qs-imports && ln -sfn /usr/share/omarchy/shell /tmp/qs-imports/qs
/usr/lib/qt6/bin/qmllint -I /tmp/qs-imports *.qml
Remaining unqualified and missing-property warnings are baseline Quickshell
noise -- the stock Omarchy plugins report the same categories -- as are the
signal-handler-parameters warnings on Process.onExited, whose
QProcess::ExitStatus argument qmllint cannot see.
Validate the manifest against the schema the shell enforces:
omarchy plugin validate .
Tests
Regression suites need Node (and jq for the SSH-items suite). Each suite's header says what it covers; run them all the way CI does:
for t in tests/*.test.js; do node "$t" || echo "FAILED: $t"; done
The helpers have their own Rust tests:
cargo test --manifest-path agent/Cargo.toml
cargo test --manifest-path unlock-key/Cargo.toml
cargo test --manifest-path vault/Cargo.toml
Shared helpers live in tests/harness.js: loadModule() evaluates a
.pragma library file and exposes its top-level names, createSuite() records
and reports checks, and readPluginSource() returns QML as text. The vault
lives in Service.qml (loaded once per shell) and Panel.qml is the per-monitor
view; for Panel.qml, readPluginSource() returns the vault followed by the
view with root.vault. folded back to root., and service-host.test.js keeps
that fold exact. tests/legacy-keyring.js writes the pre-envelope keyring
entries that migration tests start from.
Multi-monitor behaviour can be checked on a single screen with a headless output, which gives the shell a second bar and the plugin a second view:
hyprctl output create headless QSBWTEST
omarchy-shell io.github.elevate08.qs-bitwarden-cli vaultHost # views: 2
hyprctl dispatch 'hl.dsp.focus({ monitor = "QSBWTEST" })' # move focus there
hyprctl output remove QSBWTEST
The performance suite generates invented 100-item/0.25 MiB, 500-item/1 MiB,
2,000-item/5 MiB and 5,000-item/14 MiB vaults. It reports p95 JSON parsing,
filtering and contextual-match times over 20 warm samples and fails on broad
regressions. It measures only in-process work after bw returns, so network,
server and CLI startup latency should be measured separately on the target
machine.
The 2026-08-24 auth benchmark used Bitwarden CLI 2026.2.0 and three runs with a deliberately invalid password. A normal unlock took 2,641 ms median from submit to result; after a three-second prewarm while the password screen was already open, it took 1,026 ms -- a 1,615 ms / 61.1% reduction. These figures are a same-machine comparison, not a universal latency promise.
On the same machine and CLI (2026-09-25), bw status printed its answer at
about 1.3 s and exited at about 3.2 s: after processResponse() records the
exit code, rxjs timers keep Node alive. With bw-fast-exit.js preloaded, on a
real vault, status and the list commands took about 0.8 s (items 1.6 s)
instead of 2.7-3.0 s, and get item/password/totp 1.25 s instead of
3.1 s, each with identical output and exit code. tests/bw-fast-exit.test.js
covers the guard that keeps the preload to bw itself.
Some suites need Qt rather than Node -- which any machine running the plugin already has. They cover the things only a real Qt can answer: that Escape reaches the panel from inside a text field, how Qt itself decides to draw a string (which is what makes a vault value markup or text), and how wide the kit's Button actually renders a given label in the shell's font.
That last one, tst_row_widths.qml, reads the panel's own QML and measures
every row of buttons against the width of the panel they sit in. It needs to
read those files from inside QML, which Qt gates behind an env var:
QML_XHR_ALLOW_FILE_READ=1 QT_QPA_PLATFORM=offscreen \
/usr/lib/qt6/bin/qmltestrunner -input tests/qml
Note the Qt6 binary. A bare qmltestrunner on Arch is the Qt5 one from
qt5-declarative; it reports Library import requires a version and exits 1
with no test output at all. If a run prints nothing whatsoever, that is why.
QT_ASSUME_STDERR_HAS_CONSOLE=1 is worth adding while debugging a QML test --
without it console.log() from inside QML is silently dropped.
End to end
tests/e2e/accounts.e2e.js runs the real Service.qml in a headless
Quickshell and drives it over a test-only IPC target (tests/e2e/config/,
never loaded by a real shell): signing two accounts in, a PIN each, switching,
a shell restart, logging out. bw, secret-tool and systemd-creds are
stand-ins from tests/e2e/bin/; the committed unlock tool, argon2, jq
and node run for real. It builds its own HOME, XDG and runtime directories
under /tmp and passes nothing of your environment on, so it cannot touch
your vault, keyring or shell. It is named *.e2e.js so the loop above skips
it, and needs quickshell:
node tests/e2e/accounts.e2e.js
KEEP_E2E=1 keeps the temporary directory (the shell log, the stand-in
bw's call log and keyring) for a failed run. CI runs it in the panel
end-to-end job: a digest-pinned Arch image with packages from a dated,
signature-checked Arch Linux Archive snapshot, as an unprivileged user, with
a read-only token and no secrets. Moving the snapshot date means moving the
image digest with it, so the keyring and the packages stay from the same day.