#!/usr/bin/env bash set -euo pipefail PRIMARY="eDP-2" RIGHT_EXTERNAL_NAME="DP-2" MIDDLE_EXTERNAL_NAME="HDMI-A-1" # Exact enable lines (from your config) EXT1='desc:Samsung Electric Company S24F350 H4ZR302705, highres@highrr, auto-right, 1' EXT2='desc:Samsung Electric Company S24F350 H4ZK111233, highres@highrr, auto-right, 1, transform, 1' # In dual mode, which one do we prefer? DUAL_MAIN="$EXT1" # Optional: restart Waybar after layout changes (0=off, 1=on) RESTART_WAYBAR=1 # Dock/MST settle timing (tune if needed) SETTLE_SECS=1.0 DPMS_KICK_RETRIES=2 DPMS_KICK_SLEEP=0.35 notify() { if command -v notify-send >/dev/null 2>&1; then notify-send -a "Hyprland" "$1" "${2:-}" else [ -n "${2:-}" ] && printf '%s: %s\n' "$1" "$2" >&2 || printf '%s\n' "$1" >&2 fi } hypr() { local out if ! out="$(hyprctl "$@" 2>&1)"; then notify "hyprctl failed" "$out" exit 1 fi printf '%s' "$out" } hypr_batch() { local out if ! out="$(hyprctl --batch "$1" 2>&1)"; then notify "hyprctl --batch failed" "$out" exit 1 fi printf '%s' "$out" } restart_waybar() { [ "${RESTART_WAYBAR:-0}" -eq 1 ] || return 0 if pgrep -x waybar >/dev/null 2>&1; then pkill -SIGUSR2 waybar >/dev/null 2>&1 || true fi } jqok() { command -v jq >/dev/null 2>&1; } monjson() { hyprctl -j monitors 2>/dev/null || true; } sleep_s() { python - </dev/null || sleep 1 import time time.sleep(float("$1")) PY } # --- monitor discovery helpers --- enabled_monitor_names() { if jqok; then monjson | jq -r '.[] | select(.disabled != true) | .name' else # Fallback is weaker; jq is strongly recommended hyprctl monitors | awk ' /^Monitor /{name=$2} /disabled: false/{print name} ' fi } focused_monitor_name() { if jqok; then monjson | jq -r '.[] | select(.focused==true) | .name // empty' else hyprctl monitors | awk ' /^Monitor /{name=$2} /focused: yes/{print name; exit} ' fi } external_enabled() { local n while IFS= read -r n; do [ "$n" != "$PRIMARY" ] && return 0 done < <(enabled_monitor_names) return 1 } # We don’t try to parse desc lines; we just treat them as “enable rules” enable_rule() { local rule="$1" [ -n "$rule" ] && printf 'keyword monitor %s; ' "$rule" } disable_by_name() { local name="$1" [ -n "$name" ] && printf 'keyword monitor %s, disable; ' "$name" } # Disable all *currently enabled* externals by name (minimal churn) batch_disable_enabled_externals() { local batch="" n="" while IFS= read -r n; do [ "$n" = "$PRIMARY" ] && continue batch+="$(disable_by_name "$n")" done < <(enabled_monitor_names) printf '%s' "$batch" } # Dock settle wait: give MST/alt-mode time to re-enumerate before we apply rules / kick DPMS dock_settle() { sleep_s "$SETTLE_SECS" } # DPMS kick for all enabled externals (works well for dock hotplug weirdness) kick_externals() { local names=() n="" if jqok; then mapfile -t names < <(monjson | jq -r --arg P "$PRIMARY" ' .[] | select(.name != $P and (.disabled != true)) | .name ') else mapfile -t names < <(enabled_monitor_names | awk -v P="$PRIMARY" '$0!=P') fi [ "${#names[@]}" -eq 0 ] && return 0 for _ in $(seq 1 "$DPMS_KICK_RETRIES"); do for n in "${names[@]}"; do hypr dispatch dpms off "$n" >/dev/null 2>&1 || true; done sleep_s "$DPMS_KICK_SLEEP" for n in "${names[@]}"; do hypr dispatch dpms on "$n" >/dev/null 2>&1 || true; done sleep_s "$DPMS_KICK_SLEEP" done } enforce_triple_order() { # Keep HDMI between the laptop panel and DP-2, with DP-2 always on the right. enabled_monitor_names | grep -qx "$MIDDLE_EXTERNAL_NAME" || return 0 enabled_monitor_names | grep -qx "$RIGHT_EXTERNAL_NAME" || return 0 local py=0 px=0 pspan=1920 mspan=1920 mx rx if jqok; then read -r px py pspan < <(monjson | jq -r --arg N "$PRIMARY" ' .[] | select(.name == $N) | (.transform // 0) as $t | "\(.x // 0) \(.y // 0) \(if (($t % 2) == 1) then (.height // 1080) else (.width // 1920) end)" ') read -r mspan < <(monjson | jq -r --arg N "$MIDDLE_EXTERNAL_NAME" ' .[] | select(.name == $N) | (.transform // 0) as $t | "\(if (($t % 2) == 1) then (.height // 1080) else (.width // 1920) end)" ') fi mx=$((px + pspan)) rx=$((mx + mspan)) hypr keyword monitor "$MIDDLE_EXTERNAL_NAME, highres@highrr, ${mx}x${py}, 1, transform, 1" >/dev/null 2>&1 || true hypr keyword monitor "$RIGHT_EXTERNAL_NAME, highres@highrr, ${rx}x${py}, 1" >/dev/null 2>&1 || true } apply_profile() { local label="$1" local batch="$2" dock_settle [ -n "$batch" ] && hypr_batch "$batch" >/dev/null dock_settle kick_externals notify "Profile: $label" "" restart_waybar } profile_laptop() { # Minimal: disable only currently enabled externals local batch="" batch+="$(batch_disable_enabled_externals)" apply_profile "Laptop-only" "$batch" } profile_dual() { # Disable enabled externals, then enable preferred main external rule local batch="" batch+="$(batch_disable_enabled_externals)" batch+="$(enable_rule "$DUAL_MAIN")" apply_profile "Dual" "$batch" } profile_triple() { # Disable enabled externals, then enable both rules # Order: EXT2 first so it tends to appear “middle” with auto-right local batch="" batch+="$(batch_disable_enabled_externals)" batch+="$(enable_rule "$EXT2")" batch+="$(enable_rule "$EXT1")" apply_profile "Triple" "$batch" dock_settle enforce_triple_order } toggle_externals() { if external_enabled; then profile_laptop else if [ -n "$EXT1" ] && [ -n "$EXT2" ]; then profile_triple else profile_dual fi fi } dpms_toggle_focused() { local name name="$(focused_monitor_name)" [ -z "$name" ] && { notify "No focused monitor" ""; exit 2; } hypr dispatch dpms toggle "$name" >/dev/null notify "DPMS toggle" "$name" } status() { echo "Enabled monitors:" enabled_monitor_names | sed 's/^/ - /' echo echo "Focused: $(focused_monitor_name || true)" } case "${1:-}" in laptop) profile_laptop ;; dual) profile_dual ;; triple) profile_triple ;; toggle-externals) toggle_externals ;; dpms-toggle-focused) dpms_toggle_focused ;; kick-externals) dock_settle; kick_externals; notify "Kicked externals (DPMS)" "" ;; status) status ;; *) cat < (dual/triple) $(basename "$0") dpms-toggle-focused # blank/unblank focused output (layout unchanged) $(basename "$0") kick-externals # DPMS off/on for enabled externals $(basename "$0") status # print enabled + focused EOF exit 2 ;; esac