Repository navigation
Earbuds
Bluetooth audio devices get more than a "connected" badge: battery per bud and for the case, noise control (noise cancelling / transparency / off, and on some models adaptive or more states), and on some devices conversation awareness and an equalizer — in the Dashboard, the bar, the Dynamic Island, the connection popup, a desktop widget and Settings. The same controls appear everywhere because every surface talks to one router (EarbudsControlService) instead of knowing about brands.
Bluetooth itself is always BlueZ's job — pairing, connecting and disconnecting go through the normal Bluetooth stack and the shell's native Bluetooth service. The earbuds services only add telemetry and controls on top; with none of them, you still get a generic device with whatever BlueZ reports.
For each connected device the router picks the first provider that claims it:
| # | Provider | Handles | Needs |
|---|---|---|---|
| 1 |
BudsLink (budslink) |
Whatever the optional BudsLink companion app supports — Samsung Galaxy Buds, Sony, Pixel Buds, Nothing, AirPods… with enriched controls | BudsLink installed and running (session D-Bus service io.github.maniacx.BudsLink), Enable BudsLink on |
| 2 |
Soundcore (soundcore) |
Anker Soundcore headphones — developed for the Life Q30 — ANC mode: Normal, Transparency, Noise Cancelling |
python3 (no extra package) |
| 3 |
Built-in buds support (legacyBuds) |
Galaxy Buds, Nothing Buds, Pixel Buds, Sony WH/WF, AirPods and Beats, LinkBuds, other "buds/ear" devices — noise control via the vendors' own Bluetooth protocols |
gjs (GNOME JavaScript) |
| 4 | Generic BlueZ | Anything else — battery as BlueZ reports it | — |
If BudsLink claims a device, the built-in buds support steps aside for it (and stays parked while BudsLink is still being probed), because both want the same BlueZ RFCOMM profile and BlueZ allows only one owner — whoever lost the race would silently end up without the device.
-
Battery — left, right and case with charging indicators (
BluetoothBatteryBreakdown), when the device reports them; otherwise the single BlueZ percentage. - Noise control — a selector with 2, 3 or 4 states depending on the model (for example Off / Transparency / ANC, plus Adaptive), with a continuous slider where the device exposes a level.
- Conversation awareness (speak-to-chat / voice detection) — a standard toggle.
- BudsLink extras — the controls the device itself publishes (extension toggles, sliders, check boxes, radio buttons and the equalizer) are drawn dynamically in Material You; a Device settings button opens the device's own settings window.
| Surface | What |
|---|---|
| Dashboard quick toggles | The Bluetooth toggle's panel (battery + noise control), an optional Soundcore ANC toggle that cycles the modes, and an earbuds "stem" battery toggle |
| Bar | The Bluetooth devices widget and popup (battery + controls; bluetooth.budsLink.showControlsInBarPopup) |
| Bluetooth connection popup | A card when the device connects: battery, noise control, status (showControlsInConnectionPopup) |
| Dynamic Island | An Earbuds bubble / notch (dynamicIsland.widgets.earbuds, side left or right) |
| Desktop widgets | Bluetooth Earbuds Stem, headphone and cookie-style battery widgets ([[Desktop Widgets |
| Settings → Network → Bluetooth | Per connected device: the Noise Control, Conversation Awareness and Device settings card (showEnhancedSettingsCard) |
| Modes & Routines | An earbuds noise-control action ([[Modes & Routines |
BudsLink is deliberately opt-in and niche: the shell never installs or starts it.
- Install BudsLink and make sure its session D-Bus service is available.
-
Settings → Network → Bluetooth → Enable BudsLink (
bluetooth.budsLink.enabled, default off). With it off, the bridge process never spawns and everything falls back to the built-in providers or generic BlueZ. - The shell starts a small bridge (
scripts/budslink/bridge.js, GJS) that holds the BudsLink service, talks to it over D-Bus and speaks JSON lines to the shell. A 120-second heartbeat keeps the lease alive; the bridge releases the service on disconnect. The service version must be the supported one (0.0.1); an incompatible BudsLink is reported contextually, without breaking generic Bluetooth.
Only a short allow-list of actions can be sent back to the device (two toggles, sliders and their drag state, check boxes, radio buttons, "settings button clicked"); everything else a UI might try to send is refused.
Options (Config.options.bluetooth.budsLink): enabled, preferBudsLink (let BudsLink win over the built-in support, default on), showIntegrationNotices, showBatteryBreakdown, showControlsInBarPopup, showControlsInConnectionPopup, showEnhancedSettingsCard.
Detection is by name (a connected device whose name contains "soundcore" or "q30"). Setting a mode sends the Soundcore protocol packet directly over RFCOMM from scripts/soundcore/soundcore_ctl.py — it deliberately avoids registering a BlueZ profile so it can't collide with BudsLink — trying channel 13 first and then 1, 2, 3. The last mode set is cached per device (/tmp/quickshell-soundcore/), because the headphones don't announce changes made with their own buttons; the toggle updates optimistically. Modes: Normal, Transparency, Noise Cancelling. If soundcore_ctl.py is missing, the wrapper falls back to the openscq30_cli command-line tool. Config.options.soundcore.macAddress / model remember a device and model.
scripts/buds/core.js (GJS) speaks the vendors' protocols over BlueZ sockets: Samsung Galaxy Buds, Nothing, Google Fast Pair / Pixel Buds, Sony (two protocol generations) and Apple AirPods / Beats. Invocation: gjs -m core.js get <MAC> and gjs -m core.js set <MAC> <anc|transparency|off|adaptive>; the supported modes are reported per device so the selector offers only what the model has.
| Piece | Role |
|---|---|
services/EarbudsControlService.qml |
The single router: provider resolution, normalized state and actions for every UI |
services/BudsLinkService.qml, services/budslink/BudsLinkSemantics.js, scripts/budslink/bridge.js
|
BudsLink companion |
services/SoundcoreService.qml, scripts/soundcore/
|
Soundcore |
services/BudsService.qml, scripts/buds/
|
Built-in vendor protocols |
services/BluetoothStatus.qml |
Connected devices and BlueZ state (shared with the rest of the shell) |
modules/common/widgets/bluetooth/ |
BluetoothBatteryBreakdown, EarbudsNoiseControlSelector, EarbudsConversationAwareness, MultiStateSlider, budslink/BudsLinkDynamicControls
|