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 每只耳机和充电盒的电量、当前聆听模式、自适应降噪等级、对话感知、单耳 ANC 和佩戴检测。
这是独立应用,还是其他东西的插件?它是给 Omarchy 桌面的状态栏添加小部件的插件(Omarchy 是一套基于 Arch/Hyprland 的 Linux 环境),不是独立应用;面板本身从不直接与 Bluetooth 通信,而是读取由捆绑守护进程写入的状态文件。
除了添加插件,我还需要别的吗?需要:daemon/ 中的守护进程(librepods 的一个修改过、更完整的分叉)必须构建并运行,而且你的 AirPods 需要先通过常规 Bluetooth 流程与这台机器配对。
它有刻意不做的功能吗?有:没有空间音频一栏(Linux 上不存在对应的渲染器)、没有麦克风模式切换(它其实并不是 AirPods 侧的设置项),连接/断开/忽略设备仍留在原生 Bluetooth 面板中。把 AirPods 用作麦克风还会迫使播放降级到低带宽编解码器,因为 Bluetooth 无法同时提供高质量音频输出和麦克风。
它在任何 Linux 桌面上都能用,还是仅限 Omarchy?它是专门为 Omarchy 基于 Hyprland 的 Arch Linux 环境中的状态栏面板形式打造的,不是通用的跨桌面 Bluetooth 电量指示器。
面板在不持续轮询 Bluetooth 的情况下如何保持更新?守护进程在状态变化时把状态写入一个 JSON 文件,停止时删除该文件;面板只需监视这个文件,因此桌面空闲时不会为它运行任何额外进程。
为什么充电盒盖状态有时会卡住不更新?盒盖状态来自 BLE 广播,而在 AirPods 处于活动连接时守护进程会暂停该发现过程以避免音频爆音,因此盒盖读数会保持最后一个值,直到耳机断开连接。
底层的 AirPods 协议支持来自哪里?它基于 Kavish Devar 的 librepods,后者逆向工程了 Apple 在 L2CAP 和 BLE 广播路径上的 AAP 协议;本项目发布了一个修改过的守护进程分叉,覆盖更多型号并提供上游缺失的额外状态指令。
我能用键盘控制聆听模式和 ANC 吗?可以:t、a、n 等按键分别切换通透模式(Transparency)、自适应(Adaptive)和降噪(Noise Cancellation),c 切换对话感知,b 切换单耳 ANC,e 循环切换佩戴检测;设备不支持的模式对应按键会被直接忽略。
Creator
thisisgm
Size
2.2 MB
Stars
236
Age
2mo
Commits · 90 Days