From 2b7b18c66d0abe1b246f4983de9293846ec7f9f1 Mon Sep 17 00:00:00 2001 From: Samuel O'Neal Date: Fri, 14 Aug 2026 21:25:38 -0600 Subject: [PATCH] manage monitors with monique; reduce hypr-lid to lock/suspend timers Add the monique GUI/daemon for monitor arrangement, hotplug profiles and clamshell handling (writes monitors.lua, keybind SUPER+SHIFT+M, stowed settings). hypr-lid is trimmed to hypr-lid-power: display management moved to moniqued, it now only runs the delayed lock/suspend timers when the lid is closed undocked. Update logind drop-in comments accordingly. --- README.md | 1 + hyprland/.config/hypr/binds.lua | 7 +- hyprland/.config/hypr/monitors.lua | 6 +- ...ypr-lid.service => hypr-lid-power.service} | 4 +- hyprland/.local/bin/hypr-lid-power.sh | 91 +++++++++++++++ hyprland/.local/bin/hypr-lid.sh | 109 ------------------ monique/.config/monique/settings.json | 5 + systemd/etc/systemd/logind.conf.d/lid.conf | 7 +- 8 files changed, 111 insertions(+), 119 deletions(-) rename hyprland/.config/systemd/user/{hypr-lid.service => hypr-lid-power.service} (57%) create mode 100755 hyprland/.local/bin/hypr-lid-power.sh delete mode 100755 hyprland/.local/bin/hypr-lid.sh create mode 100644 monique/.config/monique/settings.json diff --git a/README.md b/README.md index 7758420..4299b66 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ All the configuration files for my current machine setup. - Ghostty - Hyprland - Local +- Monique - NVIM - SSH - SystemD diff --git a/hyprland/.config/hypr/binds.lua b/hyprland/.config/hypr/binds.lua index 1f3602a..41792ab 100644 --- a/hyprland/.config/hypr/binds.lua +++ b/hyprland/.config/hypr/binds.lua @@ -22,6 +22,8 @@ hl.bind(mainMod .. " + V", hl.dsp.window.float({ action = "toggle" })) hl.bind(mainMod .. " + space", hl.dsp.exec_cmd(menu)) hl.bind(SUPER_SHIFT .. " + L", hl.dsp.exec_cmd("hyprlock")) hl.bind(SUPER_SHIFT .. " + R", hl.dsp.exec_cmd("hyprctl reload")) +-- Monique: GUI monitor arrangement/display settings +hl.bind(SUPER_SHIFT .. " + M", hl.dsp.exec_cmd("monique")) hl.bind(SUPER_SHIFT .. " + D", hl.dsp.exec_cmd("hyprctl keyword monitor eDP-1, disable")) hl.bind(SUPER_SHIFT .. " + F", hl.dsp.exec_cmd("hyprctl keyword monitor eDP-1, enable")) @@ -65,5 +67,6 @@ 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 is handled by hypr-lid.service (clamshell when docked, delayed --- lock/suspend otherwise) instead of these binds; see .local/bin/hypr-lid.sh. +-- Laptop lid is handled by hypr-lid-power.service (delayed lock/suspend when +-- closed undocked; display/clamshell handling is done by moniqued) instead of +-- these binds; see .local/bin/hypr-lid-power.sh. diff --git a/hyprland/.config/hypr/monitors.lua b/hyprland/.config/hypr/monitors.lua index f58fbff..f9e6726 100644 --- a/hyprland/.config/hypr/monitors.lua +++ b/hyprland/.config/hypr/monitors.lua @@ -1,6 +1,6 @@ --- 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. +-- Managed by monique (run `monique` or SUPER+SHIFT+M to arrange). The built-in +-- panel is managed at runtime by moniqued's clamshell mode: enabled here, then +-- disabled while the lid is closed with external displays attached. -- See https://wiki.hypr.land/Configuring/Basics/Monitors/ for more 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 }) diff --git a/hyprland/.config/systemd/user/hypr-lid.service b/hyprland/.config/systemd/user/hypr-lid-power.service similarity index 57% rename from hyprland/.config/systemd/user/hypr-lid.service rename to hyprland/.config/systemd/user/hypr-lid-power.service index 2fca4cc..4d859e0 100644 --- a/hyprland/.config/systemd/user/hypr-lid.service +++ b/hyprland/.config/systemd/user/hypr-lid-power.service @@ -1,11 +1,11 @@ [Unit] -Description=Hyprland lid and clamshell monitor handler +Description=Hyprland lid lock/suspend timers (display/clamshell handled by moniqued) After=graphical-session.target PartOf=graphical-session.target [Service] Type=simple -ExecStart=%h/.local/bin/hypr-lid.sh +ExecStart=%h/.local/bin/hypr-lid-power.sh Restart=on-failure RestartSec=3 diff --git a/hyprland/.local/bin/hypr-lid-power.sh b/hyprland/.local/bin/hypr-lid-power.sh new file mode 100755 index 0000000..ebe8547 --- /dev/null +++ b/hyprland/.local/bin/hypr-lid-power.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# hypr-lid-power: delayed lock/suspend for the laptop lid. +# +# Display/clamshell/hotplug handling is owned by moniqued (see +# ~/.config/hypr/monitors.lua and moniqued's clamshell mode). This service only +# runs the power timers while the lid is closed with no external display +# attached: +# - lock after LOCK_SECONDS +# - suspend after SUSPEND_SECONDS +# +# Runs as a systemd user service (hypr-lid-power.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 + +RUNTIME="/run/user/$(id -u)" +LOCK_SECONDS=60 +SUSPEND_SECONDS=600 +POLL_SECONDS=2 + +log() { printf '[hypr-lid-power] %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 +} + +powering=no + +while true; do + if ! hypr monitors >/dev/null 2>&1; then + # No Hyprland running (e.g. another compositor, or before first launch). + powering=no + sleep 3 + continue + fi + + want_power="no" + if lid_closed && ! has_external; then + want_power="yes" + fi + + if [ "$want_power" != "$powering" ]; then + log "power mode: $powering -> $want_power" + if [ "$want_power" = "yes" ]; then + ensure_timer lock "$LOCK_SECONDS" loginctl lock-sessions + ensure_timer suspend "$SUSPEND_SECONDS" systemctl suspend + else + stop_timers + fi + powering="$want_power" + fi + + sleep "$POLL_SECONDS" +done diff --git a/hyprland/.local/bin/hypr-lid.sh b/hyprland/.local/bin/hypr-lid.sh deleted file mode 100755 index f8f8f32..0000000 --- a/hyprland/.local/bin/hypr-lid.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/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/monique/.config/monique/settings.json b/monique/.config/monique/settings.json new file mode 100644 index 0000000..cc1987d --- /dev/null +++ b/monique/.config/monique/settings.json @@ -0,0 +1,5 @@ +{ + "clamshell_mode": true, + "update_sddm": true, + "migrate_workspaces": true +} diff --git a/systemd/etc/systemd/logind.conf.d/lid.conf b/systemd/etc/systemd/logind.conf.d/lid.conf index ba7f8ff..45c3621 100644 --- a/systemd/etc/systemd/logind.conf.d/lid.conf +++ b/systemd/etc/systemd/logind.conf.d/lid.conf @@ -1,6 +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. +# Hyprland handles the laptop lid itself: moniqued owns the display side +# (clamshell when external displays are attached) and hypr-lid-power runs the +# delayed lock/suspend timers otherwise. logind must not suspend immediately +# on lid close. [Login] HandleLidSwitch=ignore HandleLidSwitchExternalPower=ignore