diff --git a/bin/omarchy-hyprland-monitor-clamshell b/bin/omarchy-hyprland-monitor-clamshell index 8f5675d2..8bd501f1 100755 --- a/bin/omarchy-hyprland-monitor-clamshell +++ b/bin/omarchy-hyprland-monitor-clamshell @@ -3,9 +3,61 @@ # omarchy:summary=Apply clamshell display state to Hyprland monitors # 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-mirror recover >/dev/null 2>&1 || true -if omarchy-hw-clamshell; then - omarchy-hyprland-monitor-internal off >/dev/null 2>&1 || true +if omarchy-hw-clamshell && omarchy-hyprland-monitor-external-active; then + disable_internal +else + enable_internal fi diff --git a/bin/omarchy-hyprland-monitor-external-active b/bin/omarchy-hyprland-monitor-external-active new file mode 100755 index 00000000..93902745 --- /dev/null +++ b/bin/omarchy-hyprland-monitor-external-active @@ -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 diff --git a/bin/omarchy-hyprland-monitor-internal b/bin/omarchy-hyprland-monitor-internal index 6b72c2bd..c78a54ef 100755 --- a/bin/omarchy-hyprland-monitor-internal +++ b/bin/omarchy-hyprland-monitor-internal @@ -7,18 +7,29 @@ TOGGLE="internal-monitor-disable" TOGGLE_FLAG="$HOME/.local/state/omarchy/toggles/hypr/$TOGGLE.lua" MIRROR_TOGGLE="internal-monitor-mirror" -# Get internal monitor name dynamically -INTERNAL=$(hyprctl monitors -j | jq -r '.[] | select(.name | contains("eDP")).name' | head -n 1) +# Get internal monitor name dynamically, including disabled outputs. +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() { if omarchy-hyprland-toggle-enabled $TOGGLE; then omarchy-hyprland-toggle $TOGGLE off omarchy-notification-send -g 󰍹 "Laptop display enabled" fi + + wake } 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" exit 1 fi @@ -31,8 +42,12 @@ off() { } recover() { - if ! omarchy-hw-external-monitors && omarchy-hyprland-toggle-enabled $TOGGLE; then - omarchy-hyprland-toggle $TOGGLE off + if ! omarchy-hyprland-monitor-external-active; then + if omarchy-hyprland-toggle-enabled $TOGGLE; then + omarchy-hyprland-toggle $TOGGLE off + fi + + wake fi } diff --git a/bin/omarchy-hyprland-monitor-internal-mirror b/bin/omarchy-hyprland-monitor-internal-mirror index 451d4305..85d78154 100755 --- a/bin/omarchy-hyprland-monitor-internal-mirror +++ b/bin/omarchy-hyprland-monitor-internal-mirror @@ -48,7 +48,7 @@ toggle() { } 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 fi } diff --git a/bin/omarchy-hyprland-monitor-watch b/bin/omarchy-hyprland-monitor-watch index e03b8f46..bd2a2a1e 100755 --- a/bin/omarchy-hyprland-monitor-watch +++ b/bin/omarchy-hyprland-monitor-watch @@ -3,36 +3,43 @@ # 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" +LOCK="${XDG_RUNTIME_DIR:-/tmp}/omarchy-monitor-clamshell.lock" sync_clamshell() { - omarchy-hyprland-monitor-clamshell -} - -recover_after_monitor_removal() { - sync_clamshell - ( - sleep 1 - sync_clamshell - ) & + flock -n 9 || exit 0 + omarchy-hyprland-monitor-clamshell + ) 9>"$LOCK" } sync_clamshell_after_monitor_change() { sync_clamshell ( - sleep 1 - sync_clamshell + for delay in 1 3 7; do + 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 case "$event" in monitoradded\>\>*|monitoraddedv2\>\>*) sync_clamshell_after_monitor_change ;; monitorremoved\>\>*|monitorremovedv2\>\>*) - recover_after_monitor_removal + sync_clamshell_after_monitor_change ;; esac done diff --git a/default/hypr/bindings/utilities.lua b/default/hypr/bindings/utilities.lua index 00720bf7..bb7f934e 100644 --- a/default/hypr/bindings/utilities.lua +++ b/default/hypr/bindings/utilities.lua @@ -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("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("switch:on:Lid Switch", nil, "omarchy-hw-external-monitors && omarchy-hyprland-monitor-internal off", { locked = true }) -o.bind("switch:off:Lid Switch", nil, "omarchy-hyprland-monitor-internal on", { 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-clamshell", { locked = true }) o.bind("PRINT", "Screenshot", "omarchy-capture-screenshot") o.bind("ALT + PRINT", "Screenrecording", "omarchy-capture-screenrecording --stop-recording || omarchy-menu toggle trigger.capture.screenrecord") diff --git a/test/shell.d/monitor-recovery-test.sh b/test/shell.d/monitor-recovery-test.sh index 9c9a9083..5ef6a1c4 100755 --- a/test/shell.d/monitor-recovery-test.sh +++ b/test/shell.d/monitor-recovery-test.sh @@ -5,14 +5,20 @@ set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" 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" system_wake="$ROOT/bin/omarchy-system-wake" clamshell="$ROOT/bin/omarchy-hyprland-monitor-clamshell" lock_service="$ROOT/shell/plugins/lock/Service.qml" 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 1' "$monitor_watch" >/dev/null +grep -F 'sleep "$delay"' "$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 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 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 'omarchy-hw-external-monitors' "$hw_clamshell" >/dev/null 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-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-hyprland-monitor-internal off >/dev/null 2>&1 || true' "$clamshell" >/dev/null -pass "clamshell monitor sync recovers stale toggles and disables internal display" +pass "clamshell monitor sync disables laptop output and force-recovers it" + +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 '(( attempt % 5 == 0 )) && sync_clamshell' "$sleep_lock" >/dev/null