#!/usr/bin/env bash # hypr-lid: clamshell + lid handling for Hyprland. # # - Lid closed + external display connected -> disable the built-in panel # (clamshell mode); external displays keep working. # - Lid closed + no external display -> turn built-in off, lock after # LOCK_SECONDS, suspend after SUSPEND_SECONDS. # - Lid open -> enable built-in, turn display on. # # Runs as a systemd user service (hypr-lid.service) and polls the lid + monitor # state, so connecting/disconnecting a dock while the lid is closed is handled # too. Lock/suspend use transient systemd timers, which pause during suspend and # are recreated afterwards, so if the machine wakes while the lid stays closed # it will sleep again after the full timeout. set -uo pipefail INTERNAL="desc:BOE 0x0BC9" RUNTIME="/run/user/$(id -u)" LOCK_SECONDS=60 SUSPEND_SECONDS=600 POLL_SECONDS=2 log() { printf '[hypr-lid] %s %s\n' "$(date '+%H:%M:%S')" "$*"; } # Run hyprctl against the active Hyprland instance, if any. hypr() { local sig="" for d in "$RUNTIME"/hypr/*/; do if [ -S "$d/.socket.sock" ]; then sig="$(basename "$d")" break fi done [ -n "$sig" ] || return 1 HYPRLAND_INSTANCE_SIGNATURE="$sig" command hyprctl "$@" } lid_closed() { local state state="$(cat /proc/acpi/button/lid/*/state 2>/dev/null)" || return 1 [[ "$state" == *closed* ]] } # True when Hyprland has any output up that is not the built-in panel/headless. has_external() { hypr monitors -j 2>/dev/null | grep '"name": "' | grep -qvE '"(eDP|HEADLESS)-' } stop_timers() { systemctl --user stop hypr-lid-lock.timer hypr-lid-suspend.timer 2>/dev/null || true } # Re-create the named timer unless it is already waiting. Transient timers are # collected after they fire, so this also restarts them after a suspend. ensure_timer() { local name="$1" seconds="$2" shift 2 if ! systemctl --user --quiet is-active "hypr-lid-$name.timer"; then systemd-run --user --quiet --unit="hypr-lid-$name" --on-active="${seconds}s" --collect "$@" fi } mode=unknown while true; do if ! hypr monitors >/dev/null 2>&1; then # No Hyprland running (e.g. another compositor, or before first launch). mode=unknown sleep 3 continue fi new_mode="open" if lid_closed; then if has_external; then new_mode="clamshell" else new_mode="regular" fi fi if [ "$new_mode" != "$mode" ]; then log "transition: $mode -> $new_mode" case "$new_mode" in open) hypr keyword monitor "$INTERNAL, preferred, auto, 1" hypr dispatch dpms on stop_timers ;; clamshell) hypr keyword monitor "$INTERNAL, disabled" stop_timers ;; regular) hypr keyword monitor "$INTERNAL, preferred, auto, 1" hypr dispatch dpms off ;; esac mode="$new_mode" fi if [ "$mode" = "regular" ]; then ensure_timer lock "$LOCK_SECONDS" loginctl lock-sessions ensure_timer suspend "$SUSPEND_SECONDS" systemctl suspend fi sleep "$POLL_SECONDS" done