Staged idle on Omarchy: both monitors physically OFF until real activity + USB keyboard power-cut (+ two upstream bugs found on the way)
Environment
| Component |
Version |
| Omarchy |
dev (93010924) |
| Kernel |
7.1.9-arch1-2 |
| Hyprland |
0.56.2-1 |
| Quickshell |
0.3.1-1 |
| hypridle |
0.1.8-1 (now disabled) |
| hyprlock |
not installed |
TL;DR
Goal: laptop panel + external ultrawide must switch completely off together after idle, stay off despite the screensaver/wake machinery, keyboard lose USB power, then suspend later — surviving reboots and OS updates.
How it works now: a small shell plugin replacing omarchy.idle stages four timeouts (dim → screensaver → physical output disable + keyboard power-cut → suspend). The displays are physically disabled, not just DPMS-blanked, so nothing in the desktop can accidentally light them up again; they come back only through one explicit, ordered restore chain triggered by real activity. Verified end-to-end live, including a genuine suspend/resume round-trip.
On the way, two reproducible upstream problems surfaced (each with diagnosis + workaround below):
- A jittery wireless-mouse dongle streaming ~1.3 KB/s of phantom
REL_X/REL_Y events defeats idle detection for the entire desktop — quickshell and hypridle alike.
- Quickshell's
IdleMonitor object goes stale across suspend→resume and silently stops reporting idleness until the shell restarts.
THE CORE SOLUTION: keeping both monitors off
Why every "normal" approach failed
- hypridle + DPMS: worked once, then broke after reboot/update. Root causes stacked: the manually-launched daemon died with its terminal, and hypridle is scheduled for retirement in upcoming releases anyway.
- DPMS-off alone: the lock/screensaver flow itself wakes displays, and any activity runs the wake path (
omarchy-system-wake) which re-enables DPMS + brightness. Net effect observed: dim → DPMS-off → screensaver lights everything back up → off again → external panel comes back with the screensaver. Unusable.
- Physically disabling outputs (
hl.monitor({output = ..., disabled = true})) is the only thing that actually sticks — provided you control exactly when they come back. Key property discovered by testing: input events do NOT revive a disabled output. Nothing spontaneous lights the panels; only code that explicitly re-adds them can. That flips the problem from "fight the compositor" to "own one boolean".
Mechanism 1 — physical disable
hyprctl eval 'hl.monitor({ output = "eDP-1", disabled = true })'
hyprctl eval 'hl.monitor({ output = "HDMI-A-1", disabled = true })'
Note the Lua-expression form: on current builds plain dispatcher args are parsed as Lua source, so keyword monitor X,disable errors out.
Mechanism 2 — staying off: three guards
- Single authority: one
displaysOffActive flag owns the state. Restore logic is a no-op unless that flag says we powered down.
- Anti-fight-loop guard: disabling an output can make the compositor close/migrate the screensaver window without user activity. The plugin distinguishes "screensaver closed because WE turned the displays off" (ignore, log
screensaver-close-ignored) from "user dismissed it" (cancel pending stages). Without this, display-off and cancel loop against each other within seconds.
- No accidental wake paths: while
displaysOffActive, compositor-reported activity is ignored if it coincides with the screensaver teardown; only the real activity signal fires the restore.
Mechanism 3 — the restore chain, and why the order matters
Triggered by genuine activity (or resume-from-suspend):
omarchy-system-wake # stock DPMS/brightness/clamshell recovery first
hyprctl eval 'hl.monitor({ output = "eDP-1", disabled = false })' # explicit re-add...
hyprctl eval 'hl.monitor({ output = "HDMI-A-1", disabled = false })' # ...preserves configured mode/scale
brightnessctl -r # backlight back
sudo -n /usr/local/sbin/omarchy-kbd-power on # keyboard power restored
Critical detail: DPMS-enable does not revive a physically disabled output, so the explicit re-add is mandatory — and because it goes through the Lua layer, the per-output scale from monitors.lua (laptop at 1.09, ultrawide at 1.0) is preserved instead of reset. Verified byte-identical layout before/after.
Mechanism 4 — cutting USB power to the keyboard
/sys/bus/usb/devices/*/power/control is root-writable only, and months of ... 2>/dev/null && echo auto > ... had been failing with silent EPERM. Three traps:
- Node identification:
3-3 was a Genesys hub; the real devices live on child ports. Writing to the hub does nothing useful.
- Ports move: after re-plugging hardware our receiver silently migrated
3-3.1 → 1-3; hardcoded ports rot invisibly (the failure was swallowed by || true).
- Whitelisting: don't hand the agent blanket sudo. One strict helper + one narrow rule:
The helper resolves targets dynamically by VID:PID at call time, so re-plugs don't break it:
#!/bin/bash
# /usr/local/sbin/omarchy-kbd-power — cut/restore USB power by device identity
set -u
action="${1:-}"
case "$action" in off|on) ;; *) echo "usage: omarchy-kbd-power off|on" >&2; exit 2 ;; esac
targets=("145f:0322" "3554:fc06") # keyboard + wireless receiver
rc=0
for vidpid in "${targets[@]}"; do
node=""
for d in /sys/bus/usb/devices/*/idVendor; do
dir=${d%/*}
[[ -f $dir/idProduct ]] || continue
id="$(printf '%s:%s' "$(cat "$dir/idVendor")" "$(cat "$dir/idProduct")")"
if [[ ${id,,} == "$vidpid" ]]; then node=${dir##*/}; break; fi
done
if [[ -z $node ]]; then echo "device $vidpid not found on any USB bus" >&2; rc=1; continue; fi
base="/sys/bus/usb/devices/$node"
case "$action" in
off) echo auto > "$base/power/control" || rc=1
echo 0 > "$base/power/autosuspend_delay_ms" || rc=1 ;;
on) echo on > "$base/power/control" || rc=1 ;;
esac
done
exit $rc
# /etc/sudoers.d/<user>-omarchy-kbd-power (0440, visudo-validated)
<user> ALL=(root) NOPASSWD: /usr/local/sbin/omarchy-kbd-power
Semantics: off = control=auto + autosuspend_delay_ms=0 (immediate autosuspend), on = control=on. Failure policy: called with || true, so a missing sudo rule or absent device degrades only the keyboard stage — never display-off.
Honest runtime-PM limitation: control=auto permits autosuspend but cannot force a device whose firmware streams interrupt-IN reports continuously. Our Trust keyboard's SINO WEALTH controller transmits non-stop, so runtime_status stays active and its LEDs stay lit through the idle cycle (the quieter receiver suspends fine). A guaranteed physical cut needs hub per-port power switching (uhubctl-style PPPS) or vendor-specific hidraw commands — worst case, the suspend stage powers everything down regardless.
Staging & configuration
Plugin <user>.idle (clone of the built-in service pattern, community parado.idle as starting point) supersedes the builtin via disabledPlugins: ["omarchy.idle"]. All timings live in ~/.config/omarchy/shell.json; a missing key disables that stage (safe defaults):
| Stage |
Key |
Production |
Action |
| Dim |
idle.brightness |
300 s |
brightnessctl -s set 10 |
| Screensaver |
idle.screensaver |
330 s |
omarchy-launch-screensaver |
| Display off |
idle.displayOff |
360 s |
both outputs disabled + kbd-power off |
| Suspend |
idle.suspend |
1800 s |
systemctl suspend |
Because the plugin is user-owned config, OS updates don't touch it — no chattr +i, no pacman hooks fighting the package manager.
Upstream finding A: phantom HID traffic defeats idle detection desktop-wide
A budget 2.4 GHz receiver (3554:fc06 2.4G Receiver) streamed continuous phantom pointer motion while untouched:
45 s true-quiet, /dev/input/event6 ("2.4G Receiver Mouse"):
59,184 bytes — REL_X ×730, REL_Y ×803, SYN ×921 (~1.3 KB/s)
Every REL_* counts as input → ext-idle-notify-v1 never fires → quickshell idle, hypridle, everything stays awake forever. Invisible in logs. USB autosuspend does not mitigate (device never idles long enough to suspend).
Diagnosis one-shot (60 s hands-off):
python3 - <<'EOF'
import os, select, time, glob
devs = {p: os.open(p, os.O_RDONLY | os.O_NONBLOCK) for p in glob.glob('/dev/input/event*')}
end = time.time() + 60; counts = {}
while time.time() < end:
r, _, _ = select.select(list(devs.values()), [], [], 0.5)
for fd in r:
n = len(os.read(fd, 4096))
name = [k for k, v in devs.items() if v == fd][0]
counts[name] = counts.get(name, 0) + n
for p, b in sorted(counts.items(), key=lambda x: -x[1]): print(f"{b:>8} B {p}")
EOF
grep NAME /sys/class/input/input*/device/name ; lsusb -t
Fix was hardware (battery/re-pair/moving dongle away from BT+webcam RF): zero bytes afterwards, idle fires immediately. Suggestion: troubleshooting-doc entry for "idle never fires" — this meter turns hours into minutes.
Upstream finding B: Quickshell IdleMonitor stale after suspend→resume
After one idle cycle ended in suspend, the monitor object never reported idleness again post-resume (independent Wayland probe fired IDLED while quickshell stayed silent; only a shell restart fixed detection).
Workaround in the plugin — wrap the monitor in a Loader, recreate it when provably stale:
Loader {
id: idleLoader
sourceComponent: Component {
IdleMonitor { enabled: root.idleEnabled && root.firstIdleTimeoutSeconds > 0
timeout: Math.max(1, root.firstIdleTimeoutSeconds)
respectInhibitors: root.respectInhibitors }
}
}
Timer { interval: 30000; repeat: true; running: root.idleEnabled
onTriggered: cursorProbe.running = true }
Process { id: cursorProbe
command: ["hyprctl", "cursorpos"]
stdout: StdioCollector { onStreamFinished: root.noteCursorPos(this.text.trim()) } }
Heal condition (fail-safe): cursor static > max(120s, 3×timeout) while monitor claims active, no cycle running, valid hyprctl data; rate-limited to one recreate per quiet window. Suggestion: re-arm the quickshell monitor on compositor resume, or adopt this pattern in omarchy.idle.
Live verification (journal timestamps)
t+0 idle-cycle-start (test timings 15/20/25/150)
t+15 brightness-dim exitCode=0
t+20 screensaver launched exitCode=0
t+25 display-off exitCode=0 — eDP-1+HDMI-A-1 disabled, kbd nodes control=auto
close-guard fired twice: "screensaver-close-ignored" ✓ (no fight loop)
t+152 systemctl suspend exitCode=0 → kernel PM suspend entry (deep)
+65min user input → PM suspend exit → idle-cycle-cancel: activity
restore exitCode=0 — monitors identical to baseline (scale preserved),
brightness 100%, kbd nodes control=on, zero plugin errors
Staged idle on Omarchy: both monitors physically OFF until real activity + USB keyboard power-cut (+ two upstream bugs found on the way)
Environment
TL;DR
Goal: laptop panel + external ultrawide must switch completely off together after idle, stay off despite the screensaver/wake machinery, keyboard lose USB power, then suspend later — surviving reboots and OS updates.
How it works now: a small shell plugin replacing
omarchy.idlestages four timeouts (dim → screensaver → physical output disable + keyboard power-cut → suspend). The displays are physically disabled, not just DPMS-blanked, so nothing in the desktop can accidentally light them up again; they come back only through one explicit, ordered restore chain triggered by real activity. Verified end-to-end live, including a genuine suspend/resume round-trip.On the way, two reproducible upstream problems surfaced (each with diagnosis + workaround below):
REL_X/REL_Yevents defeats idle detection for the entire desktop — quickshell and hypridle alike.IdleMonitorobject goes stale across suspend→resume and silently stops reporting idleness until the shell restarts.THE CORE SOLUTION: keeping both monitors off
Why every "normal" approach failed
omarchy-system-wake) which re-enables DPMS + brightness. Net effect observed: dim → DPMS-off → screensaver lights everything back up → off again → external panel comes back with the screensaver. Unusable.hl.monitor({output = ..., disabled = true})) is the only thing that actually sticks — provided you control exactly when they come back. Key property discovered by testing: input events do NOT revive a disabled output. Nothing spontaneous lights the panels; only code that explicitly re-adds them can. That flips the problem from "fight the compositor" to "own one boolean".Mechanism 1 — physical disable
Note the Lua-expression form: on current builds plain dispatcher args are parsed as Lua source, so
keyword monitor X,disableerrors out.Mechanism 2 — staying off: three guards
displaysOffActiveflag owns the state. Restore logic is a no-op unless that flag says we powered down.screensaver-close-ignored) from "user dismissed it" (cancel pending stages). Without this, display-off and cancel loop against each other within seconds.displaysOffActive, compositor-reported activity is ignored if it coincides with the screensaver teardown; only the real activity signal fires the restore.Mechanism 3 — the restore chain, and why the order matters
Triggered by genuine activity (or resume-from-suspend):
Critical detail: DPMS-enable does not revive a physically disabled output, so the explicit re-add is mandatory — and because it goes through the Lua layer, the per-output scale from
monitors.lua(laptop at 1.09, ultrawide at 1.0) is preserved instead of reset. Verified byte-identical layout before/after.Mechanism 4 — cutting USB power to the keyboard
/sys/bus/usb/devices/*/power/controlis root-writable only, and months of... 2>/dev/null && echo auto > ...had been failing with silentEPERM. Three traps:3-3was a Genesys hub; the real devices live on child ports. Writing to the hub does nothing useful.3-3.1→1-3; hardcoded ports rot invisibly (the failure was swallowed by|| true).The helper resolves targets dynamically by VID:PID at call time, so re-plugs don't break it:
Semantics:
off=control=auto+autosuspend_delay_ms=0(immediate autosuspend),on=control=on. Failure policy: called with|| true, so a missing sudo rule or absent device degrades only the keyboard stage — never display-off.Staging & configuration
Plugin
<user>.idle(clone of the built-in service pattern, communityparado.idleas starting point) supersedes the builtin viadisabledPlugins: ["omarchy.idle"]. All timings live in~/.config/omarchy/shell.json; a missing key disables that stage (safe defaults):idle.brightnessbrightnessctl -s set 10idle.screensaveromarchy-launch-screensaveridle.displayOffkbd-power offidle.suspendsystemctl suspendBecause the plugin is user-owned config, OS updates don't touch it — no
chattr +i, no pacman hooks fighting the package manager.Upstream finding A: phantom HID traffic defeats idle detection desktop-wide
A budget 2.4 GHz receiver (
3554:fc06 2.4G Receiver) streamed continuous phantom pointer motion while untouched:Every
REL_*counts as input →ext-idle-notify-v1never fires → quickshell idle, hypridle, everything stays awake forever. Invisible in logs. USB autosuspend does not mitigate (device never idles long enough to suspend).Diagnosis one-shot (60 s hands-off):
Fix was hardware (battery/re-pair/moving dongle away from BT+webcam RF): zero bytes afterwards, idle fires immediately. Suggestion: troubleshooting-doc entry for "idle never fires" — this meter turns hours into minutes.
Upstream finding B: Quickshell
IdleMonitorstale after suspend→resumeAfter one idle cycle ended in suspend, the monitor object never reported idleness again post-resume (independent Wayland probe fired
IDLEDwhile quickshell stayed silent; only a shell restart fixed detection).Workaround in the plugin — wrap the monitor in a
Loader, recreate it when provably stale:Heal condition (fail-safe): cursor static >
max(120s, 3×timeout)while monitor claims active, no cycle running, valid hyprctl data; rate-limited to one recreate per quiet window. Suggestion: re-arm the quickshell monitor on compositor resume, or adopt this pattern inomarchy.idle.Live verification (journal timestamps)