Fix clamshell display recovery

This commit is contained in:
David Heinemeier Hansson
2026-06-23 09:23:29 +02:00
parent ee65304b92
commit aa7350ab60
7 changed files with 142 additions and 26 deletions
+54 -2
View File
@@ -3,9 +3,61 @@
# omarchy:summary=Apply clamshell display state to Hyprland monitors # omarchy:summary=Apply clamshell display state to Hyprland monitors
# omarchy:hidden=true # omarchy:hidden=true
TOGGLES_DIR="$HOME/.local/state/omarchy/toggles/hypr"
CLAMSHELL_FLAG="$TOGGLES_DIR/internal-monitor-clamshell.lua"
MANUAL_DISABLE_FLAG="$TOGGLES_DIR/internal-monitor-disable.lua"
INTERNAL=$(hyprctl monitors all -j | jq -r '.[] | select(.name | contains("eDP")).name' | head -n 1)
enable_internal_output() {
[[ -n $INTERNAL ]] || return 0
hyprctl eval "hl.monitor({ output = \"$INTERNAL\", mode = \"preferred\", position = \"auto\", scale = \"auto\" })" >/dev/null 2>&1 || true
}
dpms_internal() {
local action="$1"
[[ -n $INTERNAL ]] || return 0
hyprctl dispatch "hl.dsp.dpms({ action = \"$action\", monitor = \"$INTERNAL\" })" >/dev/null 2>&1 || true
}
enable_internal() {
local changed=0
if [[ -f $CLAMSHELL_FLAG ]]; then
rm -f "$CLAMSHELL_FLAG"
changed=1
fi
if (( changed )); then
hyprctl reload >/dev/null 2>&1 || true
fi
[[ -f $MANUAL_DISABLE_FLAG ]] && omarchy-hyprland-monitor-external-active && return 0
enable_internal_output
dpms_internal enable
}
disable_internal() {
[[ -n $INTERNAL ]] || exit 0
[[ -f $MANUAL_DISABLE_FLAG ]] && return 0
mkdir -p "$TOGGLES_DIR"
local config
config=$(printf 'hl.monitor({ output = "%s", disabled = true })' "$INTERNAL")
if [[ ! -f $CLAMSHELL_FLAG ]] || [[ $(< "$CLAMSHELL_FLAG") != $config ]]; then
printf '%s\n' "$config" >"$CLAMSHELL_FLAG"
hyprctl reload >/dev/null 2>&1 || true
fi
}
omarchy-hyprland-monitor-internal recover >/dev/null 2>&1 || true omarchy-hyprland-monitor-internal recover >/dev/null 2>&1 || true
omarchy-hyprland-monitor-internal-mirror recover >/dev/null 2>&1 || true omarchy-hyprland-monitor-internal-mirror recover >/dev/null 2>&1 || true
if omarchy-hw-clamshell; then if omarchy-hw-clamshell && omarchy-hyprland-monitor-external-active; then
omarchy-hyprland-monitor-internal off >/dev/null 2>&1 || true disable_internal
else
enable_internal
fi fi
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
# omarchy:summary=Returns true when Hyprland has an active external monitor
# omarchy:hidden=true
hyprctl monitors -j | jq -e '.[] | select(.name | contains("eDP") | not) | select(.disabled == false)' >/dev/null 2>&1
+20 -5
View File
@@ -7,18 +7,29 @@ TOGGLE="internal-monitor-disable"
TOGGLE_FLAG="$HOME/.local/state/omarchy/toggles/hypr/$TOGGLE.lua" TOGGLE_FLAG="$HOME/.local/state/omarchy/toggles/hypr/$TOGGLE.lua"
MIRROR_TOGGLE="internal-monitor-mirror" MIRROR_TOGGLE="internal-monitor-mirror"
# Get internal monitor name dynamically # Get internal monitor name dynamically, including disabled outputs.
INTERNAL=$(hyprctl monitors -j | jq -r '.[] | select(.name | contains("eDP")).name' | head -n 1) INTERNAL=$(hyprctl monitors all -j | jq -r '.[] | select(.name | contains("eDP")).name' | head -n 1)
wake() {
hyprctl dispatch 'hl.dsp.dpms({ action = "enable" })' >/dev/null 2>&1 || true
}
on() { on() {
if omarchy-hyprland-toggle-enabled $TOGGLE; then if omarchy-hyprland-toggle-enabled $TOGGLE; then
omarchy-hyprland-toggle $TOGGLE off omarchy-hyprland-toggle $TOGGLE off
omarchy-notification-send -g 󰍹 "Laptop display enabled" omarchy-notification-send -g 󰍹 "Laptop display enabled"
fi fi
wake
} }
off() { off() {
if ! omarchy-hw-external-monitors; then if [[ -z $INTERNAL ]]; then
omarchy-notification-send -g 󰍹 "No laptop display found"
exit 1
fi
if ! omarchy-hyprland-monitor-external-active; then
omarchy-notification-send -g 󰍹 "Can't disable the only active display" omarchy-notification-send -g 󰍹 "Can't disable the only active display"
exit 1 exit 1
fi fi
@@ -31,8 +42,12 @@ off() {
} }
recover() { recover() {
if ! omarchy-hw-external-monitors && omarchy-hyprland-toggle-enabled $TOGGLE; then if ! omarchy-hyprland-monitor-external-active; then
omarchy-hyprland-toggle $TOGGLE off if omarchy-hyprland-toggle-enabled $TOGGLE; then
omarchy-hyprland-toggle $TOGGLE off
fi
wake
fi fi
} }
+1 -1
View File
@@ -48,7 +48,7 @@ toggle() {
} }
recover() { recover() {
if ! omarchy-hw-external-monitors && omarchy-hyprland-toggle-enabled $TOGGLE; then if ! omarchy-hyprland-monitor-external-active && omarchy-hyprland-toggle-enabled $TOGGLE; then
omarchy-hyprland-toggle $TOGGLE off omarchy-hyprland-toggle $TOGGLE off
fi fi
} }
+19 -12
View File
@@ -3,36 +3,43 @@
# omarchy:summary=Watch Hyprland monitor events and recover monitor toggles when a monitor is removed # omarchy:summary=Watch Hyprland monitor events and recover monitor toggles when a monitor is removed
SOCKET="$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" SOCKET="$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock"
LOCK="${XDG_RUNTIME_DIR:-/tmp}/omarchy-monitor-clamshell.lock"
sync_clamshell() { sync_clamshell() {
omarchy-hyprland-monitor-clamshell
}
recover_after_monitor_removal() {
sync_clamshell
( (
sleep 1 flock -n 9 || exit 0
sync_clamshell omarchy-hyprland-monitor-clamshell
) & ) 9>"$LOCK"
} }
sync_clamshell_after_monitor_change() { sync_clamshell_after_monitor_change() {
sync_clamshell sync_clamshell
( (
sleep 1 for delay in 1 3 7; do
sync_clamshell sleep "$delay"
sync_clamshell
done
) & ) &
} }
poll_clamshell_state() {
while true; do
sleep 2
sync_clamshell
done
}
sync_clamshell_after_monitor_change
poll_clamshell_state &
socat -U - "UNIX-CONNECT:$SOCKET" | while read -r event; do socat -U - "UNIX-CONNECT:$SOCKET" | while read -r event; do
case "$event" in case "$event" in
monitoradded\>\>*|monitoraddedv2\>\>*) monitoradded\>\>*|monitoraddedv2\>\>*)
sync_clamshell_after_monitor_change sync_clamshell_after_monitor_change
;; ;;
monitorremoved\>\>*|monitorremovedv2\>\>*) monitorremoved\>\>*|monitorremovedv2\>\>*)
recover_after_monitor_removal sync_clamshell_after_monitor_change
;; ;;
esac esac
done done
+2 -2
View File
@@ -33,8 +33,8 @@ o.bind_toggle("SUPER + CTRL + I", "Toggle locking on idle", "idle")
o.bind_toggle("SUPER + CTRL + N", "Toggle nightlight", "nightlight") o.bind_toggle("SUPER + CTRL + N", "Toggle nightlight", "nightlight")
o.bind("SUPER + CTRL + Delete", "Toggle laptop display", "omarchy-hyprland-monitor-internal toggle") o.bind("SUPER + CTRL + Delete", "Toggle laptop display", "omarchy-hyprland-monitor-internal toggle")
o.bind("SUPER + CTRL + ALT + Delete", "Toggle laptop display mirroring", "omarchy-hyprland-monitor-internal-mirror toggle") o.bind("SUPER + CTRL + ALT + Delete", "Toggle laptop display mirroring", "omarchy-hyprland-monitor-internal-mirror toggle")
o.bind("switch:on:Lid Switch", nil, "omarchy-hw-external-monitors && omarchy-hyprland-monitor-internal off", { locked = true }) o.bind("switch:on:Lid Switch", nil, "omarchy-hyprland-monitor-clamshell", { locked = true })
o.bind("switch:off:Lid Switch", nil, "omarchy-hyprland-monitor-internal on", { locked = true }) o.bind("switch:off:Lid Switch", nil, "omarchy-hyprland-monitor-clamshell", { locked = true })
o.bind("PRINT", "Screenshot", "omarchy-capture-screenshot") o.bind("PRINT", "Screenshot", "omarchy-capture-screenshot")
o.bind("ALT + PRINT", "Screenrecording", "omarchy-capture-screenrecording --stop-recording || omarchy-menu toggle trigger.capture.screenrecord") o.bind("ALT + PRINT", "Screenrecording", "omarchy-capture-screenrecording --stop-recording || omarchy-menu toggle trigger.capture.screenrecord")
+40 -4
View File
@@ -5,14 +5,20 @@ set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
monitor_watch="$ROOT/bin/omarchy-hyprland-monitor-watch" monitor_watch="$ROOT/bin/omarchy-hyprland-monitor-watch"
monitor_internal="$ROOT/bin/omarchy-hyprland-monitor-internal"
monitor_mirror="$ROOT/bin/omarchy-hyprland-monitor-internal-mirror"
monitor_external_active="$ROOT/bin/omarchy-hyprland-monitor-external-active"
sleep_lock="$ROOT/bin/omarchy-system-sleep-lock" sleep_lock="$ROOT/bin/omarchy-system-sleep-lock"
system_wake="$ROOT/bin/omarchy-system-wake" system_wake="$ROOT/bin/omarchy-system-wake"
clamshell="$ROOT/bin/omarchy-hyprland-monitor-clamshell" clamshell="$ROOT/bin/omarchy-hyprland-monitor-clamshell"
lock_service="$ROOT/shell/plugins/lock/Service.qml" lock_service="$ROOT/shell/plugins/lock/Service.qml"
hw_clamshell="$ROOT/bin/omarchy-hw-clamshell" hw_clamshell="$ROOT/bin/omarchy-hw-clamshell"
utilities="$ROOT/default/hypr/bindings/utilities.lua"
grep -F 'recover_after_monitor_removal' "$monitor_watch" >/dev/null grep -F 'sleep "$delay"' "$monitor_watch" >/dev/null
grep -F 'sleep 1' "$monitor_watch" >/dev/null grep -F 'for delay in 1 3 7; do' "$monitor_watch" >/dev/null
grep -F 'poll_clamshell_state &' "$monitor_watch" >/dev/null
grep -F 'flock -n 9' "$monitor_watch" >/dev/null
grep -F 'omarchy-hyprland-monitor-clamshell' "$monitor_watch" >/dev/null grep -F 'omarchy-hyprland-monitor-clamshell' "$monitor_watch" >/dev/null
pass "monitor watcher retries internal monitor recovery after removal" pass "monitor watcher retries internal monitor recovery after removal"
@@ -20,15 +26,45 @@ grep -F 'monitoradded\>\>*|monitoraddedv2\>\>*)' "$monitor_watch" >/dev/null
grep -F 'omarchy-hyprland-monitor-clamshell' "$monitor_watch" >/dev/null grep -F 'omarchy-hyprland-monitor-clamshell' "$monitor_watch" >/dev/null
pass "monitor watcher disables the internal monitor after closed-lid external hotplug" pass "monitor watcher disables the internal monitor after closed-lid external hotplug"
grep -F 'sync_clamshell_after_monitor_change' "$monitor_watch" >/dev/null
grep -F 'socat -U - "UNIX-CONNECT:$SOCKET"' "$monitor_watch" >/dev/null
pass "monitor watcher reconciles clamshell state on startup"
grep -F '/proc/acpi/button/lid/*/state' "$hw_clamshell" >/dev/null grep -F '/proc/acpi/button/lid/*/state' "$hw_clamshell" >/dev/null
grep -F 'omarchy-hw-external-monitors' "$hw_clamshell" >/dev/null grep -F 'omarchy-hw-external-monitors' "$hw_clamshell" >/dev/null
pass "clamshell helper detects closed-lid external monitor state" pass "clamshell helper detects closed-lid external monitor state"
grep -F 'hyprctl monitors -j' "$monitor_external_active" >/dev/null
grep -F 'select(.name | contains("eDP") | not)' "$monitor_external_active" >/dev/null
grep -F 'select(.disabled == false)' "$monitor_external_active" >/dev/null
pass "active external monitor helper checks Hyprland outputs"
grep -F 'omarchy-hyprland-monitor-internal recover >/dev/null 2>&1 || true' "$clamshell" >/dev/null grep -F 'omarchy-hyprland-monitor-internal recover >/dev/null 2>&1 || true' "$clamshell" >/dev/null
grep -F 'omarchy-hyprland-monitor-internal-mirror recover >/dev/null 2>&1 || true' "$clamshell" >/dev/null grep -F 'omarchy-hyprland-monitor-internal-mirror recover >/dev/null 2>&1 || true' "$clamshell" >/dev/null
grep -F 'internal-monitor-clamshell.lua' "$clamshell" >/dev/null
grep -F 'disabled = true' "$clamshell" >/dev/null
grep -F 'MANUAL_DISABLE_FLAG' "$clamshell" >/dev/null
! grep -F 'rm -f "$MANUAL_DISABLE_FLAG"' "$clamshell" >/dev/null
! grep -F '>"$MANUAL_DISABLE_FLAG"' "$clamshell" >/dev/null
grep -F 'hyprctl eval "hl.monitor({ output = \"$INTERNAL\", mode = \"preferred\", position = \"auto\", scale = \"auto\" })"' "$clamshell" >/dev/null
grep -F 'hyprctl dispatch "hl.dsp.dpms({ action = \"$action\", monitor = \"$INTERNAL\" })"' "$clamshell" >/dev/null
grep -F 'hyprctl monitors all -j' "$clamshell" >/dev/null
grep -F 'omarchy-hyprland-monitor-external-active' "$clamshell" >/dev/null
grep -F 'omarchy-hw-clamshell' "$clamshell" >/dev/null grep -F 'omarchy-hw-clamshell' "$clamshell" >/dev/null
grep -F 'omarchy-hyprland-monitor-internal off >/dev/null 2>&1 || true' "$clamshell" >/dev/null pass "clamshell monitor sync disables laptop output and force-recovers it"
pass "clamshell monitor sync recovers stale toggles and disables internal display"
grep -F "hyprctl dispatch 'hl.dsp.dpms({ action = \"enable\" })' >/dev/null 2>&1 || true" "$monitor_internal" >/dev/null
grep -F 'hyprctl monitors all -j' "$monitor_internal" >/dev/null
grep -F 'omarchy-hyprland-monitor-external-active' "$monitor_internal" >/dev/null
grep -F 'wake' "$monitor_internal" >/dev/null
pass "internal monitor helper can re-enable disabled laptop displays"
grep -F 'omarchy-hyprland-monitor-external-active' "$monitor_mirror" >/dev/null
pass "internal mirror helper recovers when no active external display remains"
grep -F 'switch:on:Lid Switch", nil, "omarchy-hyprland-monitor-clamshell"' "$utilities" >/dev/null
grep -F 'switch:off:Lid Switch", nil, "omarchy-hyprland-monitor-clamshell"' "$utilities" >/dev/null
pass "lid switch bindings reconcile clamshell display state"
grep -F 'omarchy-hyprland-monitor-clamshell >/dev/null 2>&1 || true' "$sleep_lock" >/dev/null grep -F 'omarchy-hyprland-monitor-clamshell >/dev/null 2>&1 || true' "$sleep_lock" >/dev/null
grep -F '(( attempt % 5 == 0 )) && sync_clamshell' "$sleep_lock" >/dev/null grep -F '(( attempt % 5 == 0 )) && sync_clamshell' "$sleep_lock" >/dev/null