Proof Portal
Omarchy
ProbeLabs65 findings · 95 requirementsA proof layer — requirements, tests and verified fixes — for two of Omarchy's subsystems: the application menu (launcher scripts, QML model, JSONC config, search and selection) and the lock screen (lock scripts, QML, PAM authentication). Scope is deliberately limited to those components of omacom/omarchy; the rest of the distribution is not covered.
Bar's Bluetooth widget can't be turned back on after off, when rfkill cuts radio power (ThinkPad ACPI)
Reported by oogala-boogala · filed Sep 3, 2026Closed upstream on 2026-09-20 by bjarneo via an automated message, as a duplicate of #6956 (R-4603). GitHub close reason: completed.
At a glance
- 1 · What is claimed?
- Only the report text below — no validator has restated it as checkable claims yet.
- 2 · What does it affect?
- Not yet mapped to any requirement or component.
- 3 · Is it reproducible?
- No validation has been run yet.
- 4 · What remains unclear?
- Not validated yet: no claims, no reproducer.
- 5 · What happens next?
- Our staff decide whether the report is valid. Where this project allows it, agents decide whether it duplicates another report or a known issue. No decision has been recorded yet.
Original report
System details
Lenovo ThinkPad (product 4282AD4), Omarchy 4.0.2-1, quickshell 0.3.1-1, kernel 7.1.9-arch1-2. Bluetooth radio: Broadcom, USB (0a5c:217f), driver btusb, on USB port 2-1.4.
What's wrong?
On hardware where the Bluetooth rfkill switch cuts power to the radio (rather than a plain soft-block), turning Bluetooth off from the bar leaves it permanently un-recoverable from the UI: the bar icon disappears entirely and there's no way to turn it back on except via the CLI (rfkill unblock bluetooth).
Steps to reproduce
- Turn Bluetooth off via the bar's Bluetooth widget (or
omarchy bluetooth power off). - Observe: the Bluetooth icon disappears entirely from the bar.
- Try to turn it back on — there's no icon/switch left to click.
- Only workaround:
rfkill unblock bluetoothfrom a terminal, then wait.
Root cause
On this hardware, thinkpad_acpi's rfkill handler for Bluetooth doesn't just soft-block the radio — it cuts ACPI power to the USB port the BT chip sits on, and the device fully disconnects from the USB bus:
kernel: usb 2-1.4: USB disconnect, device number 3
kernel: Bluetooth: hci0: Opcode 0x0402 failed: -19
kernel: Bluetooth: hci0: Error when powering off device on rfkill (-19)Because the USB device is gone, BlueZ deletes the hci0 adapter object from D-Bus entirely (not just Powered: no). Turning it back on requires the port to re-power and the device to re-enumerate on USB, which we measured taking 25-51s across repeated tests.
plugins/panels/bluetooth/Panel.qml assumes a live BlueZ adapter object is always available whenever Bluetooth hardware exists on the machine, and uses adapter !== null as the signal for "does this machine have Bluetooth" for all of:
- the whole widget's visibility (
visible: adapter !== null— the icon itself disappears) - the power switch's visibility (
visible: !!root.adapter) toggleBluetooth()'s guard clause (if (!adapter) return)
That assumption holds on hardware that keeps the adapter object around with Powered: no during a soft rfkill block, but not on hardware like this where the adapter object is destroyed while off. The result: once you turn it off, there's no surviving UI affordance to turn it back on, and even IPC's toggleBluetooth() is a no-op in that state.
Separately, bin/omarchy-bluetooth-power's default POWER_WAIT_SECONDS=2 is far shorter than this hardware's real recovery time (~25-51s), though this doesn't affect the bar (which doesn't check the script's exit status), only direct CLI/script callers.
Proposed fix (tested working)
Use rfkill list bluetooth as a fallback signal for "Bluetooth hardware exists on this machine," independent of whether BlueZ currently has a live adapter object for it. The rfkill platform switch (tpacpi_bluetooth_sw here) persists across the whole off/on cycle even when hci0 disappears, so it's a reliable way to distinguish "hardware temporarily gone mid-power-cycle" from "no Bluetooth hardware on this machine at all."
Verified end-to-end via qs ipc call omarchy.bluetooth toggleBluetooth (the same call the bar switch makes): toggling off still disconnects the USB device as before, but the widget now stays visible and correctly shows "Turned Off"; toggling on again while the adapter is still absent now correctly runs omarchy-bluetooth-power on (previously a no-op), and the radio reliably comes back ~25s later with no manual intervention.
plugins/panels/bluetooth/Panel.qml diff
--- a/plugins/panels/bluetooth/Panel.qml
+++ b/plugins/panels/bluetooth/Panel.qml
@@ -23,6 +23,30 @@
readonly property var adapter: Bluetooth.defaultAdapter
+ // BlueZ deletes its adapter object entirely on some hardware (this radio's
+ // rfkill block cuts ACPI power to the USB port instead of just soft-blocking
+ // it) rather than keeping it around with Powered: no like most controllers.
+ // `adapter` alone can't tell "no Bluetooth hardware on this machine" apart
+ // from "hardware exists but is between power cycles" on that kind of setup,
+ // so rfkill — which still lists the platform switch even with no live
+ // adapter — is consulted as a fallback whenever the adapter disappears.
+ property bool rfkillHardwarePresent: false
+ readonly property bool hasHardware: adapter !== null || rfkillHardwarePresent
+ readonly property bool poweredOn: !!adapter && adapter.enabled
+
+ Process {
+ id: rfkillProbe
+ command: ["rfkill", "list", "bluetooth"]
+ stdout: StdioCollector {
+ id: rfkillProbeStdout
+ waitForEnd: true
+ }
+ onExited: root.rfkillHardwarePresent = (rfkillProbeStdout.text || "").trim() !== ""
+ }
+
+ Component.onCompleted: rfkillProbe.running = true
+ onAdapterChanged: if (!adapter) rfkillProbe.running = true
+
// True while this instance owes BlueZ a StopDiscovery: set when it starts
// discovery (or opens onto a session already running) and cleared once
// discovery is confirmed down after close. Ownership, not state — BlueZ's
@@ -56,8 +80,8 @@
readonly property var discoveredDevices: deviceGroups.discovered || []
readonly property string icon: {
- if (!adapter) return ""
- if (!adapter.enabled) return ""
+ if (!hasHardware) return ""
+ if (!poweredOn) return ""
if (connectedDevices.length > 0) return ""
return ""
}
@@ -73,10 +97,10 @@
"Wrangling codecs",
"Polishing packets"
]
- readonly property bool rotatingPhrases: adapter && adapter.enabled
+ readonly property bool rotatingPhrases: poweredOn
readonly property string heroStatusText: {
- if (!adapter) return "No adapter"
- if (!adapter.enabled) return "Turned Off"
+ if (!hasHardware) return "No adapter"
+ if (!poweredOn) return "Turned Off"
return activePhrases[phraseIndex % activePhrases.length]
}
@@ -101,7 +125,7 @@
// sits above the device sections so the adapter can be toggled by keyboard
// even when it is off and no device rows exist.
readonly property bool headerHasCursor: cursorActive && focusSection === "header"
- readonly property string toggleHint: root.adapter && root.adapter.enabled ? "Turn Bluetooth off" : "Turn Bluetooth on"
+ readonly property string toggleHint: root.poweredOn ? "Turn Bluetooth off" : "Turn Bluetooth on"
readonly property color hoverFill: bar
? Style.hoverFillFor(bar.foreground, Color.accent)
@@ -497,7 +521,7 @@
if (selectedIndex < 0) selectedIndex = 0
}
- visible: adapter !== null
+ visible: hasHardware
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
@@ -632,9 +656,13 @@
// Asking for a direction rather than a toggle: the helper runs detached and the
// switch only moves once BlueZ catches up, so a second click inside that window
// would re-read the old state and undo the first.
+ //
+ // Guarding on hasHardware rather than adapter: on hardware whose rfkill block
+ // cuts power to the radio, adapter is null for the whole time it's off, so a
+ // plain adapter check would leave this dead exactly when "on" is needed most.
function toggleBluetooth() {
- if (!adapter) return
- Quickshell.execDetached(["omarchy-bluetooth-power", adapter.enabled ? "off" : "on"])
+ if (!hasHardware) return
+ Quickshell.execDetached(["omarchy-bluetooth-power", poweredOn ? "off" : "on"])
}
IpcHandler {
@@ -705,15 +733,15 @@
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.display
- opacity: root.adapter && root.adapter.enabled ? 1.0 : 0.5
+ opacity: root.poweredOn ? 1.0 : 0.5
}
// Compact on/off switch on the trailing edge of the hero, and the
// header's only cursor target.
ToggleSwitch {
id: powerSwitch
- visible: !!root.adapter
- checked: !!root.adapter && root.adapter.enabled
+ visible: root.hasHardware
+ checked: root.poweredOn
hasCursor: root.headerHasCursor
foreground: root.bar.foreground
anchors.right: parent.right
@@ -867,8 +895,8 @@
Text {
textFormat: Text.PlainText
visible: root.connectedDevices.length === 0 && root.scrollRows.length === 0
- text: !root.adapter ? "No Bluetooth adapter"
- : !root.adapter.enabled ? "Turn Bluetooth on to scan"
+ text: !root.hasHardware ? "No Bluetooth adapter"
+ : !root.poweredOn ? "Turn Bluetooth on to scan"
: "Scanning for devices…"
color: Qt.darker(root.bar.foreground, 1.5)
font.family: root.bar.fontFamilybin/omarchy-bluetooth-power diff
--- a/bin/omarchy-bluetooth-power
+++ b/bin/omarchy-bluetooth-power
@@ -16,7 +16,13 @@
# Omarchy path that turns Bluetooth on or off goes through here, because a plain
# `bluetoothctl power on` fails outright while the block is set.
-POWER_WAIT_SECONDS=${OMARCHY_BLUETOOTH_POWER_WAIT_SECONDS:-2}
+# 2s covers a plain soft-unblock. Some radios (e.g. rfkill switches that cut
+# ACPI power to the USB port instead of just blocking) need the device to
+# re-enumerate on USB after unblock, which has been observed to take upward of
+# 50s on affected hardware — hence the larger default. wait_powered still
+# returns the moment the adapter is up, so this only raises the failure
+# threshold and costs nothing on hardware that comes back quickly.
+POWER_WAIT_SECONDS=${OMARCHY_BLUETOOTH_POWER_WAIT_SECONDS:-75}
controllers() {
timeout 2s bluetoothctl list 2>/dev/null | awk '{print $2}'Happy to open this as a PR instead if that's the preferred path — just let me know.
View raw text
### System details
Lenovo ThinkPad (product `4282AD4`), Omarchy 4.0.2-1, quickshell 0.3.1-1, kernel 7.1.9-arch1-2. Bluetooth radio: Broadcom, USB (`0a5c:217f`), driver `btusb`, on USB port `2-1.4`.
### What's wrong?
On hardware where the Bluetooth rfkill switch cuts power to the radio (rather than a plain soft-block), turning Bluetooth off from the bar leaves it permanently un-recoverable from the UI: the bar icon disappears entirely and there's no way to turn it back on except via the CLI (`rfkill unblock bluetooth`).
**Steps to reproduce**
1. Turn Bluetooth off via the bar's Bluetooth widget (or `omarchy bluetooth power off`).
2. Observe: the Bluetooth icon disappears entirely from the bar.
3. Try to turn it back on — there's no icon/switch left to click.
4. Only workaround: `rfkill unblock bluetooth` from a terminal, then wait.
**Root cause**
On this hardware, `thinkpad_acpi`'s rfkill handler for Bluetooth doesn't just soft-block the radio — it cuts ACPI power to the USB port the BT chip sits on, and the device fully disconnects from the USB bus:
```
kernel: usb 2-1.4: USB disconnect, device number 3
kernel: Bluetooth: hci0: Opcode 0x0402 failed: -19
kernel: Bluetooth: hci0: Error when powering off device on rfkill (-19)
```
Because the USB device is gone, BlueZ deletes the `hci0` adapter object from D-Bus entirely (not just `Powered: no`). Turning it back on requires the port to re-power and the device to re-enumerate on USB, which we measured taking 25-51s across repeated tests.
`plugins/panels/bluetooth/Panel.qml` assumes a live BlueZ adapter object is always available whenever Bluetooth hardware exists on the machine, and uses `adapter !== null` as the signal for "does this machine have Bluetooth" for all of:
- the whole widget's visibility (`visible: adapter !== null` — the icon itself disappears)
- the power switch's visibility (`visible: !!root.adapter`)
- `toggleBluetooth()`'s guard clause (`if (!adapter) return`)
That assumption holds on hardware that keeps the adapter object around with `Powered: no` during a soft rfkill block, but not on hardware like this where the adapter object is destroyed while off. The result: once you turn it off, there's no surviving UI affordance to turn it back on, and even IPC's `toggleBluetooth()` is a no-op in that state.
Separately, `bin/omarchy-bluetooth-power`'s default `POWER_WAIT_SECONDS=2` is far shorter than this hardware's real recovery time (~25-51s), though this doesn't affect the bar (which doesn't check the script's exit status), only direct CLI/script callers.
**Proposed fix (tested working)**
Use `rfkill list bluetooth` as a fallback signal for "Bluetooth hardware exists on this machine," independent of whether BlueZ currently has a live adapter object for it. The rfkill platform switch (`tpacpi_bluetooth_sw` here) persists across the whole off/on cycle even when `hci0` disappears, so it's a reliable way to distinguish "hardware temporarily gone mid-power-cycle" from "no Bluetooth hardware on this machine at all."
Verified end-to-end via `qs ipc call omarchy.bluetooth toggleBluetooth` (the same call the bar switch makes): toggling off still disconnects the USB device as before, but the widget now stays visible and correctly shows "Turned Off"; toggling on again while the adapter is still absent now correctly runs `omarchy-bluetooth-power on` (previously a no-op), and the radio reliably comes back ~25s later with no manual intervention.
<details>
<summary><code>plugins/panels/bluetooth/Panel.qml</code> diff</summary>
```diff
--- a/plugins/panels/bluetooth/Panel.qml
+++ b/plugins/panels/bluetooth/Panel.qml
@@ -23,6 +23,30 @@
readonly property var adapter: Bluetooth.defaultAdapter
+ // BlueZ deletes its adapter object entirely on some hardware (this radio's
+ // rfkill block cuts ACPI power to the USB port instead of just soft-blocking
+ // it) rather than keeping it around with Powered: no like most controllers.
+ // `adapter` alone can't tell "no Bluetooth hardware on this machine" apart
+ // from "hardware exists but is between power cycles" on that kind of setup,
+ // so rfkill — which still lists the platform switch even with no live
+ // adapter — is consulted as a fallback whenever the adapter disappears.
+ property bool rfkillHardwarePresent: false
+ readonly property bool hasHardware: adapter !== null || rfkillHardwarePresent
+ readonly property bool poweredOn: !!adapter && adapter.enabled
+
+ Process {
+ id: rfkillProbe
+ command: ["rfkill", "list", "bluetooth"]
+ stdout: StdioCollector {
+ id: rfkillProbeStdout
+ waitForEnd: true
+ }
+ onExited: root.rfkillHardwarePresent = (rfkillProbeStdout.text || "").trim() !== ""
+ }
+
+ Component.onCompleted: rfkillProbe.running = true
+ onAdapterChanged: if (!adapter) rfkillProbe.running = true
+
// True while this instance owes BlueZ a StopDiscovery: set when it starts
// discovery (or opens onto a session already running) and cleared once
// discovery is confirmed down after close. Ownership, not state — BlueZ's
@@ -56,8 +80,8 @@
readonly property var discoveredDevices: deviceGroups.discovered || []
readonly property string icon: {
- if (!adapter) return ""
- if (!adapter.enabled) return ""
+ if (!hasHardware) return ""
+ if (!poweredOn) return ""
if (connectedDevices.length > 0) return ""
return ""
}
@@ -73,10 +97,10 @@
"Wrangling codecs",
"Polishing packets"
]
- readonly property bool rotatingPhrases: adapter && adapter.enabled
+ readonly property bool rotatingPhrases: poweredOn
readonly property string heroStatusText: {
- if (!adapter) return "No adapter"
- if (!adapter.enabled) return "Turned Off"
+ if (!hasHardware) return "No adapter"
+ if (!poweredOn) return "Turned Off"
return activePhrases[phraseIndex % activePhrases.length]
}
@@ -101,7 +125,7 @@
// sits above the device sections so the adapter can be toggled by keyboard
// even when it is off and no device rows exist.
readonly property bool headerHasCursor: cursorActive && focusSection === "header"
- readonly property string toggleHint: root.adapter && root.adapter.enabled ? "Turn Bluetooth off" : "Turn Bluetooth on"
+ readonly property string toggleHint: root.poweredOn ? "Turn Bluetooth off" : "Turn Bluetooth on"
readonly property color hoverFill: bar
? Style.hoverFillFor(bar.foreground, Color.accent)
@@ -497,7 +521,7 @@
if (selectedIndex < 0) selectedIndex = 0
}
- visible: adapter !== null
+ visible: hasHardware
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
@@ -632,9 +656,13 @@
// Asking for a direction rather than a toggle: the helper runs detached and the
// switch only moves once BlueZ catches up, so a second click inside that window
// would re-read the old state and undo the first.
+ //
+ // Guarding on hasHardware rather than adapter: on hardware whose rfkill block
+ // cuts power to the radio, adapter is null for the whole time it's off, so a
+ // plain adapter check would leave this dead exactly when "on" is needed most.
function toggleBluetooth() {
- if (!adapter) return
- Quickshell.execDetached(["omarchy-bluetooth-power", adapter.enabled ? "off" : "on"])
+ if (!hasHardware) return
+ Quickshell.execDetached(["omarchy-bluetooth-power", poweredOn ? "off" : "on"])
}
IpcHandler {
@@ -705,15 +733,15 @@
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.display
- opacity: root.adapter && root.adapter.enabled ? 1.0 : 0.5
+ opacity: root.poweredOn ? 1.0 : 0.5
}
// Compact on/off switch on the trailing edge of the hero, and the
// header's only cursor target.
ToggleSwitch {
id: powerSwitch
- visible: !!root.adapter
- checked: !!root.adapter && root.adapter.enabled
+ visible: root.hasHardware
+ checked: root.poweredOn
hasCursor: root.headerHasCursor
foreground: root.bar.foreground
anchors.right: parent.right
@@ -867,8 +895,8 @@
Text {
textFormat: Text.PlainText
visible: root.connectedDevices.length === 0 && root.scrollRows.length === 0
- text: !root.adapter ? "No Bluetooth adapter"
- : !root.adapter.enabled ? "Turn Bluetooth on to scan"
+ text: !root.hasHardware ? "No Bluetooth adapter"
+ : !root.poweredOn ? "Turn Bluetooth on to scan"
: "Scanning for devices…"
color: Qt.darker(root.bar.foreground, 1.5)
font.family: root.bar.fontFamily
```
</details>
<details>
<summary><code>bin/omarchy-bluetooth-power</code> diff</summary>
```diff
--- a/bin/omarchy-bluetooth-power
+++ b/bin/omarchy-bluetooth-power
@@ -16,7 +16,13 @@
# Omarchy path that turns Bluetooth on or off goes through here, because a plain
# `bluetoothctl power on` fails outright while the block is set.
-POWER_WAIT_SECONDS=${OMARCHY_BLUETOOTH_POWER_WAIT_SECONDS:-2}
+# 2s covers a plain soft-unblock. Some radios (e.g. rfkill switches that cut
+# ACPI power to the USB port instead of just blocking) need the device to
+# re-enumerate on USB after unblock, which has been observed to take upward of
+# 50s on affected hardware — hence the larger default. wait_powered still
+# returns the moment the adapter is up, so this only raises the failure
+# threshold and costs nothing on hardware that comes back quickly.
+POWER_WAIT_SECONDS=${OMARCHY_BLUETOOTH_POWER_WAIT_SECONDS:-75}
controllers() {
timeout 2s bluetoothctl list 2>/dev/null | awk '{print $2}'
```
</details>
Happy to open this as a PR instead if that's the preferred path — just let me know.
Validation
Not checked yet. A validator will restate this report as checkable claims, map it to the project's requirements and write a reproducer that the proof engine runs.
Linked
Not linked to any known issue, other report or change yet.
Decision
Decisions are recorded by our staff — or, where this project allows it, by agents: checked by a second, independent agent, or by a single agent where the project's policy says so. Any decision reached so far is shown below.
No decision has been recorded yet.