A drawer for the Omarchy bar. One chip holds several third-party bar widgets, so a stack of plugins you are trialling takes a single slot instead of one each.
Barback is the one who stocks the bar. Sibling to Barkeep — Barkeep tends, Barback shelves.
The drawer is storage, not management. It renders what it is told to render and nothing more.
- An empty drawer is one small dimmed chip. Click it and pick a widget — that is the whole of the empty state.
- Collapsed it is one glyph and a count. Hover expands the row of
residents along the bar axis, the same 600ms feel as
omarchy.tray. - Click pins it open,
Esccloses, and clicking away closes it too. - Right-click for the menu: eject a resident, Add a widget…, Manage in Barkeep…, Eject all, and for About.
- Vertical bars get
vertical: truepassed through, so hosted widgets pick their own compact forms for free.
Stock omarchy.* widgets stay on the bar; the drawer is for third-party
plugins that are installed and working but do not need to be looked at every
minute.
git clone https://github.com/ninepointlabs/barback ~/Projects/barback
cd ~/Projects/barback
./install.sh # copies + validates only
omarchy bar put ninepointlabs.barback --section right
barback add com.omastorm.radarinstall.sh stages the copy beside the live plugin directory, runs
omarchy plugin validate against the staged copy, and only then swaps it in
with an atomic rename. It does not enable the plugin, touch the bar layout, or
restart the shell.
That last step is optional: once the chip is on the bar, clicking it opens the picker and you can fill the drawer from there.
Two ways in:
- Click the chip while the drawer is empty. An empty drawer has nothing to expand, so the click that would pin it opens the picker instead.
- Right-click → Add a widget… once the drawer has something in it, because then the chip's own click is the pin gesture.
It lists every eligible installed third-party bar widget by display name, with
the plugin id beside it. Picking one runs barback add — the picker has no
writer of its own — which takes the widget off the bar, keeps the settings it
had there, and parks it in the drawer.
What it will not offer you, and why:
| Excluded | Why |
|---|---|
hidden .-directories |
omarchy plugin remove backups and clone staging dirs are not plugins any more |
omarchy.* |
stock widgets stay on the bar |
| Barback itself | a drawer cannot hold itself |
| another drawer | barWidget.hostsBarWidgets, or a clone of Barback: nested drawers are out of scope for v1 |
| anything already in the drawer | it is already in the drawer |
| service- or panel-only plugins | nothing to draw on a bar |
a missing or unsafe entryPoints.barWidget, or one whose file is gone |
the shell could not load it either |
| a manifest the shell would reject | missing name/version, empty kinds, bad defaultSection |
anything in disabledPlugins[] |
you turned it off |
The picker and barback add share one rule set, so the picker can never offer
something the CLI then refuses.
Barback's own manifest declares it:
{ "barWidget": { "hostsBarWidgets": true } }It means "this widget hosts other bar widgets", and it is how Barback recognises
a drawer — including a clone of itself, which omarchy plugin clone gives a new
id but the same manifest. A drawer is never offered as a resident and never
mounted as one. If you write another drawer-shaped widget, set it.
Both of these — the visible empty chip and the picker itself — are deliberate departures from the design spec, authorised by Tim on 2026-09-12. See DEVIATIONS.md.
The button at the top right of the chip's right-click menu, or of the
Add a widget picker, opens About in the menu: the version (read from
manifest.json), the license (Read it shows the full text in place), Nine
Point Labs, when Barback was created, what it does with your data, and the
support address, help@ninepointlabs.com, with
Email (your mail app, with the version details filled in), Copy and Copy
version details buttons. Back (or Esc) returns to the menu; closing the
menu leaves About.
barback list [--json] residents, their ids, and whether each resolved
barback add <plugin-id> [--compact] park it: drop its layout entry, keep its settings,
add to items[]
barback remove <plugin-id> eject: back off the bar, still installed
barback clear eject everything
barback probe [--json] re-check each resident resolves to a live entry point
barback --version print the installed version
Every write goes through omarchy-shell-config, the same shared helper
omarchy-bar itself sources: one jq program, an atomic replace, then the
running shell is told to reload. There is no omarchy bar remove <id> — the
stock bar group is use / reset / defaults / position / transparent / put / move / set — so taking a widget off the bar means deleting its entry from the
layout array through that helper, which is what barback add does before it
parks anything.
These are two different things and Barback never conflates them.
| Intent | Where | What happens |
|---|---|---|
| Eject | barback remove, the chip's right-click menu, Barkeep |
Leaves the drawer. The plugin stays installed, its folder is untouched, and it has no layout entry — it is now "off the bar". Never prompts. |
| Uninstall | Barkeep, with its own confirmation | Deletes the plugin. Barback only has to prune its items[] afterwards, which it does on the next scan. |
barback remove and barback clear never call omarchy plugin remove, and
never delete anything from disk.
Barback's own shell.json entry is the only home its residents have. They have
no entry of their own in bar.layout, and their settings live inside Barback's
entry. On the first add the plugin's manifest.barWidget.defaults are the
floor, and anything the widget actually had set in its own layout entry wins, so
moving a configured widget off the bar does not reset it.
{
"id": "ninepointlabs.barback",
"section": "right",
"items": [
{ "id": "io.github.dougfour.grok-usage", "settings": {}, "compact": true },
{ "id": "com.omastorm.radar", "settings": { "units": "imperial" } }
]
}compact is a per-item toggle. Expanded, a text-bearing widget makes the
drawer wide, so compact injects the settings widgets in the wild use to drop
their text label (showTrack, showNext, showTime, showLabel, …) unless
that item's settings already say otherwise. A widget that has never heard of
those keys ignores them.
A hosted widget may expose either of these on its bar-widget root:
property int barbackBadge // > 0 means "I am signalling"
property bool barbackAttention // true counts as 1Barback reads whichever it finds off the live item and shows a dot on the collapsed chip, with the total in the chip's tooltip. Widgets that implement neither simply never signal, which is fine and is the expected case.
The default is badge only, no auto-expand. A resident that genuinely needs eyes can opt in per item:
{ "id": "tim.ring-cameras", "settings": { "autoExpandOnAttention": true } }which expands the drawer while that resident is signalling and folds it again when it clears. Anything whose whole job is to alert you belongs on the bar, not in the drawer; this is an escape hatch, not a design goal.
The shell.json bar layout is flat — bar.layout.{left,center,right}, entries
are {id, ...settings} — and there is no container or group kind, so a drawer
cannot be expressed as config. Barback is a plugin that mounts other plugins'
QML itself, the same way shell.qml does:
- Scan
~/.config/omarchy/pluginsfrom disk. A bar widget is handedbar,moduleNameandsettingsand nothing else — nopluginRegistry, nomanifest— so the catalogue is rebuilt rather than asked for. Plugins are keyed by manifest id, not folder name; several plugins in the wild disagree on those. Dot-directories are skipped, which is what keeps anomarchy plugin removebackup from ever being mounted. The same scan checks that eachentryPoints.barWidgetis really a file on disk, and readsdisabledPlugins[]out ofshell.json, because both decide what the picker may offer. - Read
entryPoints.barWidgetfrom the resident's manifest and load it. - Inject
bar,moduleNameandsettings— exactly the three propertiesBar.qml'sModuleSlotinjects into every widget slot.
The bar a resident receives is a real qs.Ui PluginBarApi, the same type the
host hands a third-party widget, with its scalars copied from Barback's own
facade and its operations delegated back through it. Two entries matter:
serviceFor(id)returnsnullon purpose. The common pattern in the wild issharedService: bar.shell.serviceFor(moduleName) … : nullthenservice: sharedService || localServicewithService { active: sharedService === null }. Returning null puts each widget on its own tested local-service path.moduleWidgets(id)returns that widget's live instances across monitors. Barback tracks its residents per monitor, so it can answer this as well as the stock bar answers it for a widget that is really in the layout.
updateEntryInline(id, entry) from a resident merges into items[id].settings
inside Barback's own entry and persists; nothing else in the drawer is touched.
- Identity moves. A resident's click targets and popouts register under
ninepointlabs.barback, so arrow-key panel switching walks within the drawer rather than on to the next bar item. - A resident has no layout entry, which is how the shell defines "enabled"
for a bar widget. While a plugin is in the drawer the shell will not mount
its other kinds — a
servicekind is not started, apaneloroverlaykind is not registered, andomarchy-shell shell toggle <id>will not reach it. Its bar widget still runs, with its own local service, inside the drawer. Barkeep lists it under "Off the bar" until Barkeep grows a Drawer group. - Width on a horizontal bar. Collapsed is one chip; expanded, a
text-bearing resident makes the drawer wide. That is what
compactis for. - Outside the documented contract. Nothing here touches upstream, but a
shell update could move a property out from under the shim. Every resident is
resolved before it is loaded and instantiated inside a guarded
Loader; one that is gone is pruned with a single log line, and one that is installed but cannot be drawn renders an inert placeholder. Nothing throws. A drawer that goes quiet always beats a bar that dies.
tests/run.shtests/model_test.js— the pure model under node: resident resolution, pruning a plugin uninstalled behind our back, skipping dot-directories, every picker exclusion above, the settings-preserving move, theshell.jsonshapes.tests/cli_test.sh— the CLI against a scratchshell.jsonwith a mocked shell reload, asserting the resulting entry is exactly the shape above, that a widget moved in off the bar keeps its settings, and thataddrefuses a nested drawer, a Barback clone, a plugin whose QML file is gone and a disabled plugin. The realomarchy-shell-configdoes the writing; only$HOMEand the reload are isolated.tests/about_test.js— the About page's facts checked againstmanifest.json,LICENSEand the first commit, the prefilled support email, and that the privacy statement names every way the shipped code reaches the network (it fails if a host or network command is added without being named there).tests/about_qml_test.sh— the realBarWidget.qmlunderqswith a scratchHOME: opens About from the menu's button, checks what it shows, Back, and that closing the menu leaves it, and fails on any QML warning.tests/qml_compat_test.sh— the realBarWidget.qmlhosting the real installed plugins, with no live side effects: scratchHOME, a copied plugins directory, every helper binary the residents spawn replaced by a logging stub,bar.runrecorded rather than executed, and the whole run inside a network namespace the script verifies is empty before it starts. It loads a second, resident-free instance as well, which is where the empty chip and the picker are checked — including that picking a widget runs the bundled CLI exactly once, with the id it offered. The CLI is stubbed there too, so the run still writes nothing.
Drag-and-drop onto the chip, nested drawers, hosting stock omarchy.*
widgets, and marketplace submission.