From db72a5a635bc58ad5ed9021128d18057134192ee Mon Sep 17 00:00:00 2001 From: Samuel O'Neal Date: Fri, 14 Aug 2026 18:16:31 -0600 Subject: [PATCH] add hypr-lid service for clamshell and delayed lid sleep --- hyprland/.config/hypr/binds.lua | 5 +- hyprland/.config/hypr/exec.lua | 1 + hyprland/.config/hypr/monitors.lua | 6 +- .../.config/systemd/user/hypr-lid.service | 13 +++ hyprland/.local/bin/hypr-lid.sh | 109 ++++++++++++++++++ systemd/etc/systemd/logind.conf.d/lid.conf | 7 ++ 6 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 hyprland/.config/systemd/user/hypr-lid.service create mode 100755 hyprland/.local/bin/hypr-lid.sh create mode 100644 systemd/etc/systemd/logind.conf.d/lid.conf diff --git a/hyprland/.config/hypr/binds.lua b/hyprland/.config/hypr/binds.lua index 1092a60..1f3602a 100644 --- a/hyprland/.config/hypr/binds.lua +++ b/hyprland/.config/hypr/binds.lua @@ -65,6 +65,5 @@ hl.bind(mainMod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" })) hl.bind(mainMod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) hl.bind(mainMod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true }) --- Laptop lid for opening and closing -hl.bind("switch:on:Lid Switch", hl.dsp.exec_cmd("hyprctl dpms off && hyprctl dispatch exec hyprlock"), { locked = true }) -hl.bind("switch:off:Lid Switch", hl.dsp.exec_cmd("hyprctl dispatch dpms on"), { locked = true }) +-- Laptop lid is handled by hypr-lid.service (clamshell when docked, delayed +-- lock/suspend otherwise) instead of these binds; see .local/bin/hypr-lid.sh. diff --git a/hyprland/.config/hypr/exec.lua b/hyprland/.config/hypr/exec.lua index f22ba2c..23f6028 100644 --- a/hyprland/.config/hypr/exec.lua +++ b/hyprland/.config/hypr/exec.lua @@ -3,6 +3,7 @@ hl.on("hyprland.start", function() hl.exec_cmd("hyprctl setcursor catppuccin-mocha-dark-cursors 28 &") hl.exec_cmd("systemctl --user start hyprpolkitagent &") hl.exec_cmd("waybar & mako & hyprpaper & hypridle & wluma & hyprshell run &") + hl.exec_cmd("systemctl --user start hypr-lid.service &") hl.exec_cmd("blueman-applet &") hl.exec_cmd("1password --silent &") hl.exec_cmd("op signin &") diff --git a/hyprland/.config/hypr/monitors.lua b/hyprland/.config/hypr/monitors.lua index fe34ef5..f58fbff 100644 --- a/hyprland/.config/hypr/monitors.lua +++ b/hyprland/.config/hypr/monitors.lua @@ -1,6 +1,8 @@ --- Generated by nwg-displays on 2026-05-12 at 10:26:18. Do not edit manually. +-- External displays (docked layout). The built-in panel is managed at runtime +-- by hypr-lid: it is enabled here, then disabled while the lid is closed with +-- external displays attached (clamshell). See hyprland/.local/bin/hypr-lid.sh. -- See https://wiki.hypr.land/Configuring/Basics/Monitors/ for more -hl.monitor({ output = "desc:BOE 0x0BC9", disabled = true }) +hl.monitor({ output = "desc:BOE 0x0BC9", mode = "preferred", position = "0x0", scale = 1.0 }) hl.monitor({ output = "desc:Viewteck Co. Ltd. GNV34DB", mode = "3440x1440@50.0", position = "3795x1785", scale = 1.0 }) hl.monitor({ output = "desc:Dell Inc. DELL U2515HX GMDK46C4053L", mode = "2560x1440@59.95", position = "5488x345", scale = 1.0 }) hl.monitor({ output = "desc:Dell Inc. DELL U2518D 0WG2J9C5B3WL", mode = "2560x1440@59.95", position = "2928x345", scale = 1.0 }) diff --git a/hyprland/.config/systemd/user/hypr-lid.service b/hyprland/.config/systemd/user/hypr-lid.service new file mode 100644 index 0000000..2fca4cc --- /dev/null +++ b/hyprland/.config/systemd/user/hypr-lid.service @@ -0,0 +1,13 @@ +[Unit] +Description=Hyprland lid and clamshell monitor handler +After=graphical-session.target +PartOf=graphical-session.target + +[Service] +Type=simple +ExecStart=%h/.local/bin/hypr-lid.sh +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=graphical-session.target diff --git a/hyprland/.local/bin/hypr-lid.sh b/hyprland/.local/bin/hypr-lid.sh new file mode 100755 index 0000000..f8f8f32 --- /dev/null +++ b/hyprland/.local/bin/hypr-lid.sh @@ -0,0 +1,109 @@ +#!/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 diff --git a/systemd/etc/systemd/logind.conf.d/lid.conf b/systemd/etc/systemd/logind.conf.d/lid.conf new file mode 100644 index 0000000..ba7f8ff --- /dev/null +++ b/systemd/etc/systemd/logind.conf.d/lid.conf @@ -0,0 +1,7 @@ +# Hyprland handles the laptop lid itself via the hypr-lid service +# (clamshell when external displays are attached, delayed lock/suspend +# otherwise). logind must not suspend immediately on lid close. +[Login] +HandleLidSwitch=ignore +HandleLidSwitchExternalPower=ignore +HandleLidSwitchDocked=ignore