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)

Searching Database…

Что это такое?Это плагин панели Omarchy, показывающий заряд AirPods для каждого наушника и кейса, активный режим прослушивания, адаптивный уровень шума, Conversation Awareness, One-Bud ANC и обнаружение в ухе, отрисованный в собственном визуальном стиле панели.
Это отдельное приложение или плагин для чего-то другого?Это плагин, добавляющий виджет в строку состояния рабочего стола Omarchy (Omarchy — Linux-сборка на базе Arch/Hyprland), а не отдельное приложение; сама панель никогда не общается с Bluetooth, а читает файл состояния, который пишет входящий в комплект демон.
Нужно ли что-то сверх простого добавления плагина?Да: демон в daemon/ (изменённый, более полный форк librepods) должен быть собран и запущен, а ваши AirPods должны быть сначала сопряжены с машиной через обычный Bluetooth-процесс.
Есть ли то, что он сознательно не делает?Да: нет строки Spatial Audio (для неё нет рендерера под Linux), нет переключателя режима микрофона (на деле это не настройка на стороне AirPods), а подключение/отключение/забыть остаются в штатной Bluetooth-панели. Использование AirPods как микрофона также принуждает воспроизведение к низкоскоростному кодеку, ведь Bluetooth не может одновременно дать качественный приёмник звука и микрофон.
Работает ли это на любом Linux-рабочем столе или только на Omarchy?Он сделан специально под идиомы панели Omarchy в его Arch Linux-сборке на Hyprland, а не как универсальный индикатор заряда Bluetooth для любых десктопов.
Как панель остаётся актуальной без постоянного опроса Bluetooth?Демон пишет своё состояние в JSON-файл при каждом изменении и удаляет файл при остановке; панель просто следит за этим файлом, так что простаивающий рабочий стол не запускает ради этого лишних процессов.
Почему состояние крышки кейса иногда зависает вместо обновления?Состояние крышки берётся из BLE-объявлений, и демон приостанавливает это обнаружение, пока AirPods активно подключены, чтобы избежать треска в звуке, поэтому показание крышки держит последнее значение, пока наушники не отключатся.
Откуда взялась поддержка протокола AirPods в основе?Она основана на librepods от Kavish Devar, который реверс-инжинирнул протокол Apple AAP поверх L2CAP и путь BLE-объявлений; этот проект поставляет изменённый форк демона с более широким покрытием моделей и дополнительными командами статуса, которых нет в оригинале.
Можно ли управлять режимом прослушивания и ANC с клавиатуры?Да: клавиши вроде t, a и n переключают Transparency, Adaptive и Noise Cancellation, c включает Conversation Awareness, b — One-Bud ANC, а e листает обнаружение в ухе; клавиши для режимов, которых устройство не поддерживает, просто игнорируются.
Creator
thisisgm
Size
2.2 MB
Stars
234
Age
2mo
Commits · 90 Days