omarchy-pods README

The project's README from github.com/thisisgm/omarchy-pods. As one markdown file: README.md.

AirPods for Omarchy

Battery for each pod and the case, the listening modes, adaptive noise level, Conversation Awareness, One-Bud ANC and ear detection, drawn in Omarchy's own panel idiom.

On omarchyplugins.com Latest tag

The AirPods panel open in the Omarchy bar

What it shows

Deliberately absent

Screenshots

The panel is built from the capability keys the daemon publishes, so three different AirPods give three different panels. Same plugin, same build, nothing configured differently between them.

AirPods 4 with ANC
AirPods 4 with ANC
all four modes, and One-Bud ANC to hold them with one pod in
AirPods Pro 3
AirPods Pro 3
no Off row: the Pro 3 dropped it
AirPods Max 2
AirPods Max 2
one battery, no case, and no One-Bud ANC to offer

Both AirPods 4 variants say AirPods 4 in the title, because the name is the family and the rows underneath are what the unit can actually do. The plain AirPods 4 has no listening modes at all, so it gets no listening section.

Every screenshot in this section, the Pro 3 included, was made by writing one status line by hand and photographing the panel that came back. An AirPods Pro 3 is the only pair on hand here, and it would not have posed for all of these anyway. It works because the panel reads that file and nothing else.

States

Noise Cancellation
Noise Cancellation, both pods in
Adaptive
Adaptive, with the noise level and Conversation Awareness on
Transparency
Transparency
One pod in the case
One pod in the case, lid open, One-Bud ANC on
Both pods in the case
Both pods charging, lid closed
Daemon not running
librepods not running

Requirements

How it works

The plugin does not poll. The daemon writes its status to $XDG_STATE_HOME/librepods/status.json whenever that status changes, and removes the file when it stops. The panel watches it, so an idle desktop runs no processes at all on its behalf. librepods-ctl is used only when you actually change something.

The plugin never talks to Bluetooth itself. If librepods-ctl is missing or the daemon is not running, the panel says so in one line instead of drawing an empty surface.

The output codec, and what the microphone costs

The daemon always selects the highest-bitrate playback profile the card offers, which is SBC-XQ at 453 kbps ahead of SBC at 328 and AAC at 256. That is the default, and it is re-applied every time the daemon activates the card, because PipeWire's own profile priority puts AAC first and would otherwise win.

Selecting the AirPods as a microphone gives that up. Over the standard Bluetooth profiles, two-way voice runs on a separate low-bandwidth channel, so the only profiles exposing a source are headset-head-unit at 16 kHz mSBC and headset-head-unit-cvsd at 8 kHz, and each one replaces the playback profile rather than joining it. A card cannot offer a high-quality sink and a microphone at the same time. macOS is bound by the same profiles and simply negotiates a better voice codec on them.

So take the microphone from another device and leave the AirPods on SBC-XQ. The daemon helps here: it defers its activation ladder while a capture is live, so it will not pull a live call off the headset profile mid-sentence.

If the headset profiles are missing from the card altogether, so that nothing can offer the AirPods microphone at all, BlueZ has no HFP connection to the device. The card carries those two profiles only while that connection is up, which is why nothing logs an error: no profile is malformed, one is simply not connected. Reconnecting that one profile brings them back within a few seconds and leaves A2DP playing:

busctl call org.bluez /org/bluez/hci0/dev_<MAC> org.bluez.Device1 \
  ConnectProfile s 0000111e-0000-1000-8000-00805f9b34fb

Disconnecting and reconnecting the whole device also works, at the cost of the playback link. This daemon registers no Bluetooth profile, so that list is BlueZ's and PipeWire's to produce.

There is a route around all of this that uses no audio profile at all: the AirPods can send a high-resolution microphone stream over the same AACP channel this daemon already uses for battery and controls, which leaves A2DP playback running untouched. Upstream librepods has an implementation of it for its Rust rewrite in PR 655, unmerged at the time of writing. Nothing in this daemon does that today.

Install

omarchy plugin add https://github.com/thisisgm/omarchy-pods --enable
~/.config/omarchy/plugins/io.github.thisisgm.omapods/setup

--enable already places the widget on the right of the bar. The marketplace listing installs to the same place.

setup installs cmake, ninja, qt6-connectivity, qt6-tools, qt6-declarative, pkgconf and libpulse if they are missing, builds the daemon into ~/.local, and enables librepods.service. The icon stays hidden until AirPods are connected (hideWhenDisconnected). To keep it visible:

omarchy bar set io.github.thisisgm.omapods hideWhenDisconnected false --json

To build the daemon by hand instead of running setup:

omarchy pkg add cmake ninja qt6-connectivity qt6-tools qt6-declarative pkgconf libpulse
cd ~/.config/omarchy/plugins/io.github.thisisgm.omapods/daemon
cmake -B build -G Ninja -DBUILD_TESTING=OFF && cmake --build build
cmake --install build --prefix ~/.local
systemctl --user daemon-reload
systemctl --user enable librepods.service
systemctl --user restart librepods.service

~/.local is the prefix the unit expects, because it runs %h/.local/bin/librepods, and Omarchy already puts ~/.local/bin on PATH, which is where the panel finds librepods-ctl. The unit is bound to graphical-session.target, so the daemon comes back after a reboot.

Remove

systemctl --user disable --now librepods.service
xargs rm -f < ~/.config/omarchy/plugins/io.github.thisisgm.omapods/daemon/build/install_manifest.txt
rm -rf ~/.config/AirPodsTrayApp ~/.local/state/librepods
omarchy plugin remove io.github.thisisgm.omapods

The daemon installs into ~/.local, so it outlives the plugin. CMake lists what it put there in install_manifest.txt, which lives in the build tree, so that line has to run before the plugin directory goes.

~/.config/AirPodsTrayApp holds the paired device's name and its magicAccIRK and magicAccEncKey, which are pairing secrets, so the directory and the file are created owner only and both go with the plugin. ~/.local/state/librepods/status.json is the published status line the panel reads, and it goes too.

Keyboard

Key Action
j / k, ↓ / ↑ move between rows
enter / space activate the current row
← / → adjust the adaptive noise level
o Off, on the models that have it
t Transparency
a Adaptive
n Noise Cancellation
c toggle Conversation Awareness
b toggle One-Bud ANC
e cycle ear detection
r refresh
tab move to the next panel
esc close

Every listening key is ignored on a device the daemon says lacks the mode, so a does nothing on an AirPods Pro 1 and the four mode keys do nothing on an AirPods 3. A daemon older than the capability keys cannot say, and the panel falls back to what it gated on before.

Left click opens the panel. Right click cycles the listening mode without opening anything.

Settings

Setting Default Notes
Hide when disconnected on Leaves the bar entirely rather than sitting there with nothing to say.
Path to librepods-ctl empty Leave empty to find it on PATH.

Tests

Model.js holds the parsing and formatting, with no QML imports, so it runs outside the shell. The suite covers the shapes that bite: the objects the daemon omits entirely, a pod it has stopped hearing from, an empty file, a line that is not JSON, and a schema newer than this panel reads.

deno run --allow-read tests/model.test.js

Contributing

Bug reports and pull requests are welcome. CONTRIBUTING.md covers the build, what review will ask you to prove, and the house style; AGENTS.md adds the traps coding agents hit in this tree.

Credits

The hard part is not this panel. It is librepods by Kavish Devar, which reverse-engineered Apple's AAP protocol over L2CAP and the BLE advertisement path that carries battery, in-ear and case lid state. The daemon in daemon/ is a modified copy of his work, and this panel is a display for it.

Support

If this saved you an afternoon, you can buy me a coffee.

Licence

Two programs live here, and they are licensed separately because they are separate works that talk over a state file and a command line.

Path Licence
repository root, the bar widget MIT LICENSE
daemon/, a modified copy of librepods GPL-3.0 daemon/LICENSE

Shipping both in one repository is aggregation, not combination, so the widget stays MIT and the daemon stays GPL-3.0. What was modified, and the upstream commit it was forked from, are recorded in daemon/UPSTREAM.md.

The three product outlines in AirPodsIcon.qml are Apple's, taken from the chapter navigation on apple.com/airpods so the bar shows the hardware you actually own. AirPods, AirPods Pro and AirPods Max are trademarks of Apple Inc., which does not sponsor or endorse this plugin. Those outlines are not this project's to license, so the MIT grant above does not reach them and nothing here gives you permission to reuse them.

omarchy-pods (thisisgm/omarchy-pods)

arch-linux
Searching Database…

What is this?It is an Omarchy bar plugin that shows AirPods battery for each pod and the case, the active listening mode, adaptive noise level, Conversation Awareness, One-Bud ANC and ear detection, drawn in the bar's own visual style.
Is this a standalone app or a plugin for something else?It is a plugin that adds a widget to the Omarchy desktop's status bar (Omarchy is an Arch/Hyprland-based Linux setup), not a separate application; the panel itself never talks to Bluetooth and instead reads a status file written by a bundled daemon.
Do I need anything beyond just adding the plugin?Yes: the daemon in daemon/ (a modified, more complete fork of librepods) has to be built and running, and your AirPods need to be paired to the machine through the normal Bluetooth flow first.
Are there things it deliberately leaves out?Yes: no Spatial Audio row (no Linux renderer exists for it), no mic-mode toggle (it is not actually an AirPods-side setting), and connect/disconnect/forget stay in the stock Bluetooth panel. Using the AirPods as a microphone also forces playback down to a low-bandwidth codec, since Bluetooth cannot offer a high-quality sink and a mic at the same time.
Does this work on any Linux desktop, or only on Omarchy?It is built specifically for Omarchy's bar panel idiom on its Hyprland-based Arch Linux setup, not a generic cross-desktop Bluetooth battery indicator.
How does the panel stay updated without constantly polling Bluetooth?The daemon writes its status to a JSON file whenever something changes and removes the file when it stops; the panel just watches that file, so an idle desktop runs no extra processes for this.
Why does the case lid state sometimes freeze instead of updating?Lid state comes from BLE advertisements, and the daemon pauses that discovery while the AirPods are actively connected to avoid audio crackle, so the lid reading holds its last value until the pods disconnect.
Where did the underlying AirPods protocol support come from?It is based on librepods by Kavish Devar, who reverse-engineered Apple's AAP protocol over L2CAP and the BLE advertisement path; this project ships a modified daemon fork with wider model coverage and extra status verbs upstream lacks.
Can I control listening mode and ANC from the keyboard?Yes: keys like t, a and n switch Transparency, Adaptive and Noise Cancellation, c toggles Conversation Awareness, b toggles One-Bud ANC, and e cycles ear detection; keys for modes a device does not support are simply ignored.
Creator
thisisgm
Size
2.2 MB
Stars
235
Age
2mo
Commits · 90 Days