foxygit / dotfiles Log in
commit d00a54274e335b3e9dbec7ead84e0b4f0d69adae
Author:     MrJensK <jens.se@icloud.com>
AuthorDate: Sun Aug 9 16:05:27 2026 +0200
Commit:     MrJensK <jens.se@icloud.com>
CommitDate: Sun Aug 9 16:05:27 2026 +0200

    new
---
 bin/internal-display         |  59 ++++++++++++++
 bin/lid-display-watch        |  44 ++++++++++
 bin/lidsuspend               |  48 +++++++++++
 bin/mouse-settings           |  23 ++++++
 bin/screensaver              |  38 +++++++++
 bin/screensaver-launch       |  61 ++++++++++++++
 bin/screensaver.txt          |   5 ++
 bin/screensaverd             |  38 +++++++++
 config/sxbarc                | 107 ++++++++++++++++++++-----
 config/sxwmrc                |  28 +++++--
 cursor-dpi-fix.patch         | 187 ++++++++++++++-----------------------------
 fullscreen-monitor-fix.patch |  24 ++++++
 install.sh                   |  81 +++++++++++++++++++
 monitor-hotplug-remap.patch  |  53 ++++++++++++
 multi-monitor-struts.patch   | 164 +++++++++++++++++++++++++++++++++++++
 15 files changed, 805 insertions(+), 155 deletions(-)

diff --git a/bin/internal-display b/bin/internal-display
new file mode 100755
index 0000000..25cc42b
--- /dev/null
+++ b/bin/internal-display
@@ -0,0 +1,59 @@
+#!/bin/bash
+# internal-display on|off|toggle|status
+# (De)aktiverar den interna skärmen (eDP) via xrandr.
+# Vägrar stänga av den om det är den enda aktiva skärmen just nu.
+
+set -euo pipefail
+
+INTERNAL="eDP"
+LAYOUT_SCRIPT="$HOME/.screenlayout/xrantui.sh"
+
+active_outputs() {
+    xrandr --listmonitors | tail -n +2 | awk '{print $NF}'
+}
+
+is_internal_active() {
+    active_outputs | grep -qx "$INTERNAL"
+}
+
+case "${1:-}" in
+    off)
+        if ! is_internal_active; then
+            echo "Interna skärmen ($INTERNAL) är redan avstängd."
+            exit 0
+        fi
+        others=$(active_outputs | grep -vx "$INTERNAL" || true)
+        if [ -z "$others" ]; then
+            echo "Vägrar stänga av $INTERNAL - det är den enda aktiva skärmen just nu." >&2
+            exit 1
+        fi
+        xrandr --output "$INTERNAL" --off
+        echo "Interna skärmen avstängd."
+        ;;
+    on)
+        if [ -x "$LAYOUT_SCRIPT" ]; then
+            "$LAYOUT_SCRIPT"
+        else
+            xrandr --output "$INTERNAL" --auto
+        fi
+        echo "Interna skärmen påslagen."
+        ;;
+    toggle)
+        if is_internal_active; then
+            exec "$0" off
+        else
+            exec "$0" on
+        fi
+        ;;
+    status)
+        if is_internal_active; then
+            echo "PÅ"
+        else
+            echo "AV"
+        fi
+        ;;
+    *)
+        echo "Användning: $(basename "$0") on|off|toggle|status" >&2
+        exit 1
+        ;;
+esac
diff --git a/bin/lid-display-watch b/bin/lid-display-watch
new file mode 100755
index 0000000..a6967b7
--- /dev/null
+++ b/bin/lid-display-watch
@@ -0,0 +1,44 @@
+#!/bin/bash
+# lid-display-watch
+# Körs i bakgrunden (startas av sxwm via "exec"). Bevakar lockets
+# state via systemd-logind (org.freedesktop.login1) och styr den
+# interna skärmen automatiskt:
+#   - locket stängs  -> försök stänga av eDP
+#   - locket öppnas  -> slå på eDP igen
+# internal-display vägrar redan stänga av eDP om den är den enda
+# aktiva skärmen, så den spärren ärvs härifrån automatiskt.
+
+LOCKFILE="$HOME/.cache/lid-display-watch.pid"
+INTERNAL_DISPLAY="$HOME/.local/bin/internal-display"
+INTERVAL=2
+
+if [ -f "$LOCKFILE" ] && kill -0 "$(cat "$LOCKFILE" 2>/dev/null)" 2>/dev/null; then
+    exit 0
+fi
+mkdir -p "$(dirname "$LOCKFILE")"
+echo $$ > "$LOCKFILE"
+trap 'rm -f "$LOCKFILE"' EXIT
+
+prev=""
+while true; do
+    state=$(busctl get-property org.freedesktop.login1 /org/freedesktop/login1 \
+        org.freedesktop.login1.Manager LidClosed 2>/dev/null | awk '{print $2}')
+
+    if [ -n "$state" ] && [ "$state" != "$prev" ]; then
+        if [ "$state" = "true" ]; then
+            "$INTERNAL_DISPLAY" off
+        else
+            # eDP-panelen kan behöva en stund på sig att vakna/träna om
+            # sitt EDID efter att ha varit avstängd - ett modeset som
+            # skjuts iväg för tidigt kan landa som spegling/fel
+            # upplösning. Vänta lite och lägg på layouten två gånger.
+            sleep 1
+            "$INTERNAL_DISPLAY" on
+            sleep 1
+            "$INTERNAL_DISPLAY" on
+        fi
+        prev="$state"
+    fi
+
+    sleep "$INTERVAL"
+done
diff --git a/bin/lidsuspend b/bin/lidsuspend
new file mode 100755
index 0000000..bfbe4ce
--- /dev/null
+++ b/bin/lidsuspend
@@ -0,0 +1,48 @@
+#!/bin/bash
+# lidsuspend on|off|status
+# Temporarily (dis/re)able what happens when the lid is closed, without
+# touching /etc/systemd/logind.conf. Holds a systemd inhibitor lock alive
+# in the background for as long as it's disabled.
+
+PIDFILE="$HOME/.cache/lidsuspend.pid"
+
+is_active() {
+    [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null
+}
+
+case "$1" in
+    off)
+        if is_active; then
+            echo "Already disabled (pid $(cat "$PIDFILE"))"
+            exit 0
+        fi
+        mkdir -p "$(dirname "$PIDFILE")"
+        setsid systemd-inhibit \
+            --what=handle-lid-switch \
+            --who="lidsuspend" \
+            --why="manually disabled" \
+            --mode=block \
+            sleep infinity &
+        disown
+        echo $! > "$PIDFILE"
+        echo "Lid-suspend disabled - closing the lid will do nothing (pid $!)"
+        ;;
+    on)
+        if is_active; then
+            kill "$(cat "$PIDFILE")" 2>/dev/null
+        fi
+        rm -f "$PIDFILE"
+        echo "Lid-suspend re-enabled - normal behaviour on lid close"
+        ;;
+    status)
+        if is_active; then
+            echo "OFF - lid close is currently ignored"
+        else
+            echo "ON - normal behaviour"
+        fi
+        ;;
+    *)
+        echo "Usage: lidsuspend on|off|status"
+        exit 1
+        ;;
+esac
diff --git a/bin/mouse-settings b/bin/mouse-settings
new file mode 100644
index 0000000..774231a
--- /dev/null
+++ b/bin/mouse-settings
@@ -0,0 +1,23 @@
+#!/bin/bash
+# mouse-settings
+# Applies libinput touchpad tweaks via xinput. Safe to re-run any time
+# (e.g. from a keybind) - only touches properties that exist on the
+# matched device. Natural scrolling is only set on the touchpad - a
+# physical scroll-wheel mouse keeps the traditional (non-natural)
+# direction, since reversing that feels backwards on a wheel.
+
+set_prop() {
+    local device="$1" prop="$2"; shift 2
+    xinput list-props "$device" 2>/dev/null | grep -qF "$prop" && \
+        xinput set-prop "$device" "$prop" "$@"
+}
+
+xinput list --name-only 2>/dev/null | while IFS= read -r name; do
+    case "$name" in
+        *[Tt]ouchpad*)
+            set_prop "$name" "libinput Natural Scrolling Enabled" 1
+            set_prop "$name" "libinput Tapping Enabled" 1
+            set_prop "$name" "libinput Disable While Typing Enabled" 1
+            ;;
+    esac
+done
diff --git a/bin/screensaver b/bin/screensaver
new file mode 100755
index 0000000..e8ae213
--- /dev/null
+++ b/bin/screensaver
@@ -0,0 +1,38 @@
+#!/bin/bash
+# Runs random TTE (terminal text effects) animations in a loop.
+# Meant to be launched inside a fullscreen terminal by screensaver-launch.
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ART_FILE="${SCREENSAVER_ART:-$SCRIPT_DIR/screensaver.txt}"
+WIN_CLASS="screensaver"
+
+screensaver_in_focus() {
+  local id
+  id=$(xdotool getactivewindow 2>/dev/null) || return 1
+  xprop -id "$id" WM_CLASS 2>/dev/null | grep -q "\"$WIN_CLASS\""
+}
+
+exit_screensaver() {
+  tput cnorm 2>/dev/null
+  pkill -x tte 2>/dev/null
+  exit 0
+}
+
+trap exit_screensaver SIGINT SIGTERM SIGHUP SIGQUIT
+
+printf '\033]11;rgb:00/00/00\007' # black background
+tput civis 2>/dev/null            # hide text cursor
+
+tty=$(tty 2>/dev/null)
+
+while true; do
+  tte -i "$ART_FILE" \
+    --frame-rate 120 --canvas-width 0 --canvas-height 0 --reuse-canvas --anchor-canvas c --anchor-text c \
+    --random-effect --no-eol --no-restore-cursor &
+
+  while pgrep -t "${tty#/dev/}" -x tte >/dev/null; do
+    if read -n1 -t 1 || ! screensaver_in_focus; then
+      exit_screensaver
+    fi
+  done
+done
diff --git a/bin/screensaver-launch b/bin/screensaver-launch
new file mode 100755
index 0000000..7c4e5b4
--- /dev/null
+++ b/bin/screensaver-launch
@@ -0,0 +1,61 @@
+#!/bin/bash
+# Launches the screensaver fullscreen on every connected monitor (X11 / kitty).
+set -u
+
+export PATH="$HOME/.local/bin:$PATH"
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+WIN_CLASS="screensaver"
+
+command -v tte >/dev/null || { echo "tte not found (pipx install terminaltexteffects)" >&2; exit 1; }
+
+# Already running? Don't stack another instance.
+if xdotool search --class "^${WIN_CLASS}\$" >/dev/null 2>&1; then
+  exit 0
+fi
+
+for mon in $(xrandr --listmonitors | awk 'NR>1 {print $3}'); do
+  # mon looks like 3440/798x1440/334+0+0
+  geom="${mon%%+*}"          # 3440/798x1440/334
+  offsets="+${mon#*+}"       # +0+0
+  w="${geom%%/*}"
+  h_part="${geom#*x}"
+  h="${h_part%%/*}"
+  x="$(echo "$offsets" | cut -d+ -f2)"
+  y="$(echo "$offsets" | cut -d+ -f3)"
+
+  kitty --class="$WIN_CLASS" \
+    --override font_size=18 \
+    --override window_padding_width=0 \
+    --override background_opacity=1 \
+    -e "$SCRIPT_DIR/screensaver" &
+  kpid=$!
+
+  # Wait for the window owned by this kitty process to appear, then pin it
+  # to this monitor and fullscreen it.
+  newwin=""
+  for _ in $(seq 1 50); do
+    for win in $(xdotool search --class "^${WIN_CLASS}\$" 2>/dev/null); do
+      if [[ "$(xdotool getwindowpid "$win" 2>/dev/null)" == "$kpid" ]]; then
+        newwin="$win"
+        break 2
+      fi
+    done
+    sleep 0.1
+  done
+
+  if [[ -n "$newwin" ]]; then
+    # The window may not be fully mapped/reparented yet right after it
+    # appears in `xdotool search`, so retry the placement a few times to
+    # ride out the occasional BadWindow race with the window manager.
+    for attempt in 1 2 3; do
+      xdotool windowactivate --sync "$newwin" 2>/dev/null
+      if xdotool windowmove "$newwin" "$x" "$y" 2>/dev/null \
+        && xdotool windowsize "$newwin" "$w" "$h" 2>/dev/null \
+        && wmctrl -ir "$newwin" -b add,fullscreen 2>/dev/null; then
+        break
+      fi
+      sleep 0.2
+    done
+  fi
+done
diff --git a/bin/screensaver.txt b/bin/screensaver.txt
new file mode 100644
index 0000000..2e96315
--- /dev/null
+++ b/bin/screensaver.txt
@@ -0,0 +1,5 @@
+·▄▄▄      ▐▄• ▄  ▄· ▄▌  ·▄▄▄▄  ▄▄▄ ..▄▄ · ▄ •▄ ▄▄▄▄▄       ▄▄▄·
+█  · ▄█▀▄  █▌█▌▪▐█▪██▌  ██· ██ ▀▄.▀·▐█ ▀. █▌▄▌▪•██   ▄█▀▄ ▐█ ▄█
+█▀▀▪▐█▌.▐▌ ·██· ▐█▌▐█▪  ▐█▪ ▐█▌▐▀▀▪▄▄▀▀▀█▄▐▀▀▄· ▐█.▪▐█▌.▐▌ ██▀·
+██ .▐█▌.▐▌▪▐█·█▌ ▐█▀·.  ██. ██ ▐█▄▄▌▐█▄▪▐█▐█.█▌ ▐█▌·▐█▌.▐▌▐█▪·•
+▀▀▀  ▀█▄▀▪•▀▀ ▀▀  ▀ •   ▀▀▀▀▀•  ▀▀▀  ▀▀▀▀ ·▀  ▀ ▀▀▀  ▀█▄▀▪.▀
diff --git a/bin/screensaverd b/bin/screensaverd
new file mode 100755
index 0000000..0d20a4e
--- /dev/null
+++ b/bin/screensaverd
@@ -0,0 +1,38 @@
+#!/bin/bash
+# Idle watcher: launches the screensaver after IDLE_SECONDS of inactivity
+# and kills it the moment real activity resumes.
+set -u
+
+# sxwm's `exec` runs commands via execvp with whatever PATH the X session
+# started with, which may not include a pipx-installed `tte`. Make sure it's
+# found regardless.
+export PATH="$HOME/.local/bin:$PATH"
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+IDLE_SECONDS="${SCREENSAVER_IDLE_SECONDS:-150}"
+WIN_CLASS="screensaver"
+
+is_running() {
+  xdotool search --class "^${WIN_CLASS}\$" >/dev/null 2>&1
+}
+
+stop_screensaver() {
+  pkill -f "$SCRIPT_DIR/screensaver\$" 2>/dev/null
+  pkill -x tte 2>/dev/null
+  for win in $(xdotool search --class "^${WIN_CLASS}\$" 2>/dev/null); do
+    xdotool windowkill "$win" 2>/dev/null
+  done
+}
+
+while true; do
+  idle_ms=$(xssstate -i)
+  idle_s=$((idle_ms / 1000))
+
+  if (( idle_s >= IDLE_SECONDS )) && ! is_running; then
+    "$SCRIPT_DIR/screensaver-launch"
+  elif (( idle_s < 1 )) && is_running; then
+    stop_screensaver
+  fi
+
+  sleep 1
+done
diff --git a/config/sxbarc b/config/sxbarc
index ed1aedb..eabac5e 100644
--- a/config/sxbarc
+++ b/config/sxbarc
@@ -9,6 +9,9 @@ version_text         : poop
 # Appearance
 height              : 20
 bottom_bar          : false
+# secondary bar (opposite edge from bottom_bar, so: bottom) hosts the
+# taskbar module below
+secondary_bar       : true
 vertical_padding    : 5
 horizontal_padding  : 5
 text_padding        : 5
@@ -19,14 +22,37 @@ foreground_colour   : #7abccd
 border_colour       : #ffffff
 font                : JetBrainsMono Nerd Font:size=10

-# Built-in modules
+
+workspace_icon : 1 : ""   # globe
+workspace_icon : 2 : ""   # terminal
+workspace_icon : 3 : ""   # code
+
+# Modules -- any name works, not just the ones below (see "Your own
+# modules" further down); each resolves to ~/.config/sxbar/scripts/<name>.sh
 # module : name : enabled : refresh_interval_seconds
-# Available: clock, date, battery, volume, cpu
-module : clock   : true : 1
-module : date    : true : 60
-module : battery : true : 30
-module : volume  : true : 5
-module : cpu     : true : 3
+module : clock      : true : 1
+module : date       : true : 60
+module : battery    : true : 30
+module : volume     : true : 5
+module : cpu        : true : 3
+# brightness: hover to reveal a slider (needs brightnessctl)
+module : brightness : true : 5
+# bluetooth: click to open a floating on/off/scan/pair menu (needs bluetoothctl)
+module : bluetooth  : true : 10
+# usermenu: click to open a floating sleep/log out/shut down menu
+module : usermenu   : true : 300
+# network: click to open a floating menu showing WiFi/Ethernet + IPs
+module : network    : true : 10
+# media: hover to reveal album art + Previous/Play-Pause/Next (needs playerctl; curl for remote art)
+module : media      : true : 2
+# taskbar: click an entry to focus that window on the current workspace
+# (needs wmctrl, and sxwm built with the _NET_ACTIVE_WINDOW patch --
+# patches/net-active-window-mrjensk.patch in the sxwm repo)
+module : taskbar    : true : 1
+bar    : taskbar    : secondary
+# startmenu: hover for your favorites as popup buttons -- edit
+# ~/.config/sxbar/scripts/startmenu.sh's popup_item lines to customize
+module : startmenu  : true : 3600

 # Prefix/icon shown before a module's output (needs a Nerd Font, see `font` above)
 # prefix : module_name : "icon "
@@ -35,28 +61,65 @@ prefix : date    : " "
 prefix_cmd : battery : "~/.config/sxbar/scripts/battery_icon.sh"
 prefix_cmd : volume  : "~/.config/sxbar/scripts/volume_icon.sh"
 prefix     : cpu     : " "
+prefix     : brightness : " "
+prefix     : bluetooth  : " "
+prefix     : usermenu   : " "
+prefix     : network   : " "
+prefix     : media     : " "
+icon_only  : network   : true
+icon_only  : usermenu  : true

 # Reserve a fixed pixel width per module so the rest of the bar doesn't
 # shift when a value's digit count changes (e.g. cpu: 9% -> 16%).
 # width : module_name : min_pixels
 # 48px covers icon + " " + up to "100%" in JetBrainsMono Nerd Font:size=10.
-width : battery : 48
-width : volume  : 48
-width : cpu     : 48
+width : battery : 40
+width : volume  : 40
+width : cpu     : 40
+
+# Cap media's title width -- long track names scroll (marquee) instead of
+# stretching the bar.
+max_width : media : 220
+
+# Album art box size in the media popup (default: 160px square).
+popup_image_size : media : 220
+

+colour : clock      : #50fa7b
+colour : volume     : #ff79c6
+colour : battery    : #ffb86c
+colour : date       : #8be9fd
+colour : cpu        : #bd93f9
+colour : brightness : #f1fa8c
+colour : bluetooth  : #4a9eff
+colour : usermenu   : #ff5555
+colour : media      : #cba6f7
+colour : network    : #2ee6d6

-colour : clock   : #50fa7b
-colour : volume  : #ff79c6
-colour : battery : #ffb86c
-colour : date    : #8be9fd
+# Center the clock
+align : clock : center


-# Custom script modules (like polybar exec)
-# Command must be quoted. Output is displayed in the bar.
-# custom : label : "command or script path" : refresh_interval_seconds
+# Your own modules -- write a script, drop it in
+# ~/.config/sxbar/scripts/<name>.sh (executable), then enable it exactly
+# like any module above. No more "custom" directive -- there's no
+# distinction between a module sxbar ships a script for and one you wrote.
+#
+# module : temp : true : 5
+#
+# ...with ~/.config/sxbar/scripts/temp.sh being e.g.:
+#   #!/bin/sh
+#   sensors | grep 'Package' | awk '{print $4}'
+#
+# Try-it-yourself demo popup -- safe to click/drag as much as you like,
+# combines one of every popup row type (text, image, button, slider,
+# segmented buttons). It's entirely self-contained in
+# ~/.config/sxbar/scripts/demo_menu.sh -- edit that script once you're
+# happy with the feel and want to wire a real module's popup up similarly.
+#
+# module : demo_menu : true : 999
+# colour : demo_menu : #f8f8f2
 #
-# Examples:
-# custom : temp     : "sensors | grep 'Package' | awk '{print $4}'" : 5
-# custom : mem      : "free -h | awk '/^Mem:/{print $3\"/\"$2}'" : 10
-# custom : network  : "~/.config/sxbar/scripts/network.sh" : 5
-# custom : updates  : "checkupdates | wc -l | tr -d ' '" : 300
+# More examples:
+# module : mem     : true : 10   # ~/.config/sxbar/scripts/mem.sh: free -h | awk '/^Mem:/{print $3"/"$2}'
+# module : updates : true : 300  # ~/.config/sxbar/scripts/updates.sh: checkupdates | wc -l | tr -d ' '
diff --git a/config/sxwmrc b/config/sxwmrc
index 3530109..d3fcab1 100644
--- a/config/sxwmrc
+++ b/config/sxwmrc
@@ -1,15 +1,18 @@
 # exec : "xrandr --dpi 96 --output eDP-1 --mode 1920x1080 --pos 680x1480 --primary --output DP-1 --mode 3440x1440 --pos 0x40"
 exec : "sxbar"
 exec : "feh --bg-scale /home/mrfox/BG/j9huwdxo1zzg1.jpeg"
+exec : "compton --config ~/.config/compton.conf -b"
+exec : "mouse-settings"
+exec : "screensaverd"

 # Colour Themes:
-focused_border_colour    : #000000
-unfocused_border_colour  : #444444
+focused_border_colour    : #FC38FF
+unfocused_border_colour  : #3F99EE
 swap_border_colour       : #eeeeee

 # General Options:
 gaps                    : 10
-border_width            : 1
+border_width            : 2
 master_width            : 60 # Percentage of screen width
 resize_master_amount    : 1
 resize_stack_amount     : 20
@@ -17,7 +20,7 @@ move_window_amount      : 50
 resize_window_amount    : 50
 snap_distance           : 5
 motion_throttle         : 60 # Set to screen refresh rate for smoothest motions
-should_float            : "pcmanfm", "obs"
+should_float            : "pcmanfm", "obs", "screensaver"
 new_win_focus           : true
 warp_cursor             : true
 floating_on_top         : true
@@ -37,9 +40,10 @@ bind : mod + b : "firefox"
 bind : mod + p : "dmenu_run"

 # Window Management:
-call : mod + shift + q : close_window
+call : mod + q : close_window
 call : mod + c : centre_window
 call : mod + shift + e : quit
+# call : mod + shift + r : reload
 call : mod + m : toggle_monocle

 # Focus Movement:
@@ -78,8 +82,8 @@ call : mod + shift + Left : resize_win_left
 call : mod + shift + Right : resize_win_right

 # Gaps
-call : mod + equal : increase_gaps
-call : mod + minus : decrease_gaps
+call : mod + o : increase_gaps
+call : mod + i : decrease_gaps

 # Floating/Fullscreen
 call : mod + space : toggle_floating
@@ -89,6 +93,16 @@ call : mod + shift + f : fullscreen
 # Reload Config
 call : mod + r : reload_config

+# Function Keys (volume/brightness/media)
+bind : XF86AudioRaiseVolume : "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"
+bind : XF86AudioLowerVolume : "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"
+bind : XF86AudioMute        : "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"
+bind : XF86MonBrightnessUp   : "brightnessctl set 5%+"
+bind : XF86MonBrightnessDown : "brightnessctl set 5%-"
+bind : XF86AudioPlay : "playerctl play-pause"
+bind : XF86AudioNext : "playerctl next"
+bind : XF86AudioPrev : "playerctl previous"
+
 # Scratchpads
 scratchpad : mod + alt + 1 : create 1
 scratchpad : mod + alt + 2 : create 2
diff --git a/cursor-dpi-fix.patch b/cursor-dpi-fix.patch
index 81d4809..b554dd5 100644
--- a/cursor-dpi-fix.patch
+++ b/cursor-dpi-fix.patch
@@ -1,18 +1,5 @@
-diff --git a/Makefile b/Makefile
-index 2653a39..46afb75 100644
---- a/Makefile
-+++ b/Makefile
-@@ -6,7 +6,7 @@ PREFIX = /usr/local
- MANPREFIX = ${PREFIX}/share/man
-
- # libs
--LIBS = -lX11 -lXinerama -lXcursor
-+LIBS = -lX11 -lXinerama -lXrandr -lXcursor
-
- # flags
- CPPFLAGS = -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=700
 diff --git a/src/sxwm.c b/src/sxwm.c
-index e61953a..2968c8a 100644
+index 354c865..749ad65 100644
 --- a/src/sxwm.c
 +++ b/src/sxwm.c
 @@ -30,6 +30,7 @@
@@ -23,23 +10,23 @@ index e61953a..2968c8a 100644
  #include <X11/Xcursor/Xcursor.h>

  #include "defs.h"
-@@ -134,6 +135,7 @@ Bool window_should_float(Window w);
- Bool window_should_start_fullscreen(Window w);
- int xerr(Display *d, XErrorEvent *ee);
- void xev_case(XEvent *xev);
+@@ -72,6 +73,7 @@ void hdl_unmap_ntf(XEvent *xev);
+ /* void inc_gaps(void); */
+ void init_defaults(void);
+ Bool is_child_proc(pid_t pid1, pid_t pid2);
 +void load_cursors(void);
-
- static Atom atoms[ATOM_COUNT];
- static const char *atom_names[ATOM_COUNT] = {
-@@ -174,6 +176,7 @@ static const char *atom_names[ATOM_COUNT] = {
- Cursor cursor_normal;
- Cursor cursor_move;
- Cursor cursor_resize;
-+static Bool cursor_size_user_set = False;
-
- Client *workspaces[NUM_WORKSPACES] = {NULL};
- Config user_config;
-@@ -2215,15 +2218,13 @@ void setup(void)
+ /* void move_master_next(void); */
+ /* void move_master_prev(void); */
+ /* void move_next_mon(void); */
+@@ -1035,6 +1037,7 @@ void hdl_config_ntf(XEvent *xev)
+ {
+ 	if (xev->xconfigure.window == root) {
+ 		update_mons();
++		load_cursors();
+ 		tile();
+ 		update_borders();
+ 	}
+@@ -2250,10 +2253,7 @@ void setup(void)
  	grab_keys();
  	startup_exec();

@@ -47,48 +34,48 @@ index e61953a..2968c8a 100644
 -	cursor_move = XcursorLibraryLoadCursor(dpy, "fleur");
 -	cursor_resize = XcursorLibraryLoadCursor(dpy, "bottom_right_corner");
 -	XDefineCursor(dpy, root, cursor_normal);
-+	cursor_size_user_set = (getenv("XCURSOR_SIZE") != NULL);
++	load_cursors();

  	scr_width = XDisplayWidth(dpy, DefaultScreen(dpy));
  	scr_height = XDisplayHeight(dpy, DefaultScreen(dpy));
-
- 	update_mons();
-+	load_cursors();
-
- 	/* select events wm should look for on root */
- 	Mask wm_masks = StructureNotifyMask | SubstructureRedirectMask | SubstructureNotifyMask |
-@@ -3020,6 +3021,53 @@ void update_modifier_masks(void)
+@@ -3055,6 +3055,60 @@ void update_modifier_masks(void)
  	XFreeModifiermap(mod_mapping);
  }

++/* (re)loads the pointer cursors, sizing them from the *primary* monitor's
++ * real DPI instead of Xcursor's own default. Xinerama has no notion of
++ * physical monitor dimensions, so XcursorLibraryLoadCursor falls back to
++ * deriving DPI from the whole virtual screen's pixel height against
++ * whatever mm-height DisplayHeightMM() happens to report for it -- once a
++ * second monitor is added that no longer corresponds to any single
++ * monitor's real DPI, and the cursor renders far too large. Query the
++ * primary monitor's own size via RandR (which does expose mm dimensions)
++ * and set XCURSOR_SIZE from that instead, unless the user already set
++ * XCURSOR_SIZE themselves. Called once at startup and again from
++ * hdl_config_ntf() on every root ConfigureNotify (monitor hotplug/layout
++ * change), so docking/undocking keeps the cursor correctly sized. */
 +void load_cursors(void)
 +{
-+	if (!cursor_size_user_set) {
-+		/*
-+		 * Xinerama gives no physical mm dimensions, so use RandR to find
-+		 * the primary monitor's mheight and compute a correct per-monitor DPI.
-+		 * Without this, XcursorLibraryLoadCursor derives DPI from the full
-+		 * virtual-screen height (e.g. 2520 px / 167 mm ≈ 2× too large).
-+		 */
++	if (!getenv("XCURSOR_SIZE")) {
 +		int rr_event_base, rr_err_base;
 +		if (XRRQueryExtension(dpy, &rr_event_base, &rr_err_base)) {
 +			int n_info;
 +			XRRMonitorInfo *info = XRRGetMonitors(dpy, root, True, &n_info);
-+			if (info && n_info > 0) {
++			if (info) {
 +				int idx = 0;
 +				for (int i = 0; i < n_info; i++) {
-+					if (info[i].primary) { idx = i; break; }
++					if (info[i].primary) {
++						idx = i;
++						break;
++					}
 +				}
-+				if (info[idx].mheight > 0 && info[idx].height > 0) {
-+					/*
-+					 * cursor_size = DPI * 16 / 96
-+					 * DPI = height_px * 25.4 / mheight_mm
-+					 * Combined: height_px * 16 * 254 / (mheight_mm * 960)
-+					 */
++				if (n_info > 0 && info[idx].mheight > 0 && info[idx].height > 0) {
++					/* cursor_size = dpi * 16 / 96;
++					 * dpi = height_px * 25.4 / mheight_mm */
 +					int size = info[idx].height * 16 * 254 / (info[idx].mheight * 960);
-+					size = MAX(16, (size + 7) & ~7);
++					size = MAX(16, (size + 7) & ~7); /* round up to a multiple of 8 */
 +					char buf[16];
-+					snprintf(buf, sizeof(buf), "%d", size);
++					snprintf(buf, sizeof buf, "%d", size);
 +					setenv("XCURSOR_SIZE", buf, 1);
 +				}
 +				XRRFreeMonitors(info);
@@ -96,84 +83,32 @@ index e61953a..2968c8a 100644
 +		}
 +	}
 +
-+	if (cursor_normal) XFreeCursor(dpy, cursor_normal);
-+	if (cursor_move)   XFreeCursor(dpy, cursor_move);
-+	if (cursor_resize) XFreeCursor(dpy, cursor_resize);
++	if (cursor_normal)
++		XFreeCursor(dpy, cursor_normal);
++	if (cursor_move)
++		XFreeCursor(dpy, cursor_move);
++	if (cursor_resize)
++		XFreeCursor(dpy, cursor_resize);
 +
 +	cursor_normal = XcursorLibraryLoadCursor(dpy, "left_ptr");
-+	cursor_move   = XcursorLibraryLoadCursor(dpy, "fleur");
++	cursor_move = XcursorLibraryLoadCursor(dpy, "fleur");
 +	cursor_resize = XcursorLibraryLoadCursor(dpy, "bottom_right_corner");
-+
-+	for (int s = 0; s < ScreenCount(dpy); s++)
-+		XDefineCursor(dpy, RootWindow(dpy, s), cursor_normal);
++	XDefineCursor(dpy, root, cursor_normal);
 +}
 +
  void update_mons(void)
  {
  	XineramaScreenInfo *info;
-@@ -3030,12 +3078,13 @@ void update_mons(void)
+diff --git a/Makefile b/Makefile
+index 2653a39..46afb75 100644
+--- a/Makefile
++++ b/Makefile
+@@ -6,7 +6,7 @@ PREFIX = /usr/local
+ MANPREFIX = ${PREFIX}/share/man

- 	for (int s = 0; s < ScreenCount(dpy); s++) {
- 		Window scr_root = RootWindow(dpy, s);
--		XDefineCursor(dpy, scr_root, cursor_normal);
-+		if (cursor_normal)
-+			XDefineCursor(dpy, scr_root, cursor_normal);
- 	}
+ # libs
+-LIBS = -lX11 -lXinerama -lXcursor
++LIBS = -lX11 -lXinerama -lXrandr -lXcursor

- 	if (XineramaIsActive(dpy)) {
- 		info = XineramaQueryScreens(dpy, &n_mons);
--		mons = malloc(sizeof *mons * n_mons);
-+		mons = calloc(n_mons, sizeof *mons);
- 		if (!mons) {
- 			fputs("sxwm: failed to allocate monitors\n", stderr);
- 			exit(EXIT_FAILURE);
-@@ -3050,7 +3099,7 @@ void update_mons(void)
- 	}
- 	else {
- 		n_mons = 1;
--		mons = malloc(sizeof *mons);
-+		mons = calloc(1, sizeof *mons);
- 		if (!mons) {
- 			fputs("sxwm: failed to allocate monitor\n", stderr);
- 			exit(EXIT_FAILURE);
-@@ -3205,12 +3254,14 @@ void update_struts(void)
- 					long span_end   = top_end_x;
- 					if (span_end >= mx && span_start <= mx + mw - 1) {
- 						/*
--						 top is distance from root top to reserved area
--							 mons top is at my, amount eaten:
--							 reserve_top = MAX(0, top - my)
-+						 top is distance from root top to reserved area.
-+						 Only apply to monitors whose vertical range contains
-+						 the bottom of the reserved strip (top <= my + mh).
-+						 Without this, a panel at y=1440 (top of laptop) sets
-+						 top=1470 and incorrectly eats the entire ultrawide above.
- 						 */
- 						int reserve = (int)MAX(0, top - my);
--						if (reserve > 0)
-+						if (reserve > 0 && top <= (long)(my + mh))
- 							mons[m].reserve_top = MAX(mons[m].reserve_top, reserve);
- 					}
- 				}
-@@ -3220,16 +3271,15 @@ void update_struts(void)
- 					long span_end   = bot_end_x;
- 					if (span_end >= mx && span_start <= mx + mw - 1) {
- 						/*
--						 bottom is distance from root bottom to reserved area
--						 global_reserved_top = screen_h - bottom;
--						 overlap to mon:
--						   overlap = (my + mh) - global_reserved_top;
--						   reserve_bottom = MAX(0, overlap)
-+						 bottom is distance from root bottom to reserved area.
-+						 Only apply to monitors whose vertical range contains
-+						 the top of the reserved strip (bottom <= screen_h - my).
-+						 Symmetric guard to the top case above.
- 						 */
- 						int global_reserved_top = screen_h - (int)bottom;
- 						int overlap = (my + mh) - global_reserved_top;
- 						int reserve = MAX(0, overlap);
--						if (reserve > 0)
-+						if (reserve > 0 && (int)bottom <= screen_h - my)
- 							mons[m].reserve_bottom = MAX(mons[m].reserve_bottom, reserve);
- 					}
- 				}
+ # flags
+ CPPFLAGS = -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=700
diff --git a/fullscreen-monitor-fix.patch b/fullscreen-monitor-fix.patch
new file mode 100644
index 0000000..bd05094
--- /dev/null
+++ b/fullscreen-monitor-fix.patch
@@ -0,0 +1,24 @@
+diff --git a/src/sxwm.c b/src/sxwm.c
+index eab858a..0610288 100644
+--- a/src/sxwm.c
++++ b/src/sxwm.c
+@@ -332,7 +332,18 @@ void apply_fullscreen(Client *c, Bool on)
+
+ 		c->fullscreen = True;
+
+-		int mon = CLAMP(c->mon, 0, n_mons - 1);
++		/* c->mon is only set at map time (from the cursor's monitor) and
++		 * otherwise never kept in sync with the window's real position,
++		 * so a client moved by an external tool (xdotool, a pager) after
++		 * mapping would fullscreen onto the wrong monitor. Re-derive it
++		 * from the window's actual current geometry, same as the
++		 * restore-from-fullscreen path below already does. */
++		c->x = win_attr.x;
++		c->y = win_attr.y;
++		c->w = win_attr.width;
++		c->h = win_attr.height;
++		int mon = CLAMP(get_monitor_for(c), 0, n_mons - 1);
++		c->mon = mon;
+ 		/* make window fill mon */
+ 		XSetWindowBorderWidth(dpy, c->win, 0);
+ 		XMoveResizeWindow(dpy, c->win, mons[mon].x, mons[mon].y, mons[mon].w, mons[mon].h);
diff --git a/install.sh b/install.sh
index 2ae1387..f5b8914 100755
--- a/install.sh
+++ b/install.sh
@@ -28,12 +28,17 @@ sudo apt-get install -y \
     pipewire-audio \
     alsa-utils \
     bluez \
+    iwd \
+    xinput \
     patch \
     wget \
     curl \
     unzip \
     fontconfig \
     wmctrl \
+    xdotool \
+    x11-utils \
+    pipx \

 mkdir -p "$BUILD_DIR"
 cd "$BUILD_DIR"
@@ -49,6 +54,25 @@ patch -p1 < "$SCRIPT_DIR/cursor-dpi-fix.patch"
 # -- utan denna gör klick i taskbaren ingenting, sxwm har ingen annan
 # extern mekanism för att fokusera ett specifikt fönster)
 patch -p1 < "$SCRIPT_DIR/net-active-window.patch"
+# Fixar _NET_WM_STRUT_PARTIAL-hantering vid monitorer av olika storlek --
+# reservationer band tidigare bara till varje monitors x/y-spann, inte
+# till panelfönstrets egen monitor, vilket kunde göra en monitors
+# reserverade yta större än dess egen höjd/bredd (fönster öppnades då
+# långt utanför synligt område, bara ram synlig)
+patch -p1 < "$SCRIPT_DIR/multi-monitor-struts.patch"
+# Windows tappar rätt monitor-koppling vid hotplug (t.ex. lid-display-watch
+# som slår av/på eDP) -- client->mon är bara ett index i mons[], och blir
+# meningslöst så fort monitor-antalet/ordningen ändras, vilket kan flytta
+# fönster till fel skärm eller klämma in dem för litet. Räknar om mon-
+# tillhörighet från varje fönsters faktiska position vid varje hotplug.
+patch -p1 < "$SCRIPT_DIR/monitor-hotplug-remap.patch"
+# apply_fullscreen() placerade fönstret på c->mon, som bara sätts en gång vid
+# map-tid (till skärmen muspekaren råkade stå på) och aldrig hölls i synk med
+# fönstrets verkliga position -- ett fönster flyttat av ett externt verktyg
+# (xdotool, screensaver-launch) till en annan skärm innan det gjordes
+# fullskärm hamnade då fullskärm på FEL skärm. Räknar om målskärmen från
+# fönstrets faktiska geometri istället.
+patch -p1 < "$SCRIPT_DIR/fullscreen-monitor-fix.patch"

 echo "==> Bygger och installerar sxwm..."
 make
@@ -112,6 +136,11 @@ if [[ $REPLY =~ ^[Jj]$ ]]; then
     fi
     cat > "$HOME/.xinitrc" << 'EOF'
 #!/bin/bash
+# ~/.bash_profile sourcar inte ~/.profile/~/.bashrc, så PATH-tillägget för
+# ~/.local/bin når annars aldrig sxwm eller dess "exec"-kommandon
+# (mouse-settings, lid-display-watch m.fl. failar tyst utan denna rad)
+export PATH="$HOME/.local/bin:$PATH"
+
 # Stänga av skärmarna efter 5 min
 xset s 300 300
 xset dpms 300 300 300
@@ -135,6 +164,58 @@ else
     echo "  exec sxwm"
 fi

+echo "==> Installerar lidsuspend..."
+mkdir -p "$HOME/.local/bin"
+cp "$SCRIPT_DIR/bin/lidsuspend" "$HOME/.local/bin/lidsuspend"
+chmod +x "$HOME/.local/bin/lidsuspend"
+echo "    lidsuspend -> ~/.local/bin/lidsuspend"
+
+echo "==> Installerar mouse-settings..."
+mkdir -p "$HOME/.local/bin"
+cp "$SCRIPT_DIR/bin/mouse-settings" "$HOME/.local/bin/mouse-settings"
+chmod +x "$HOME/.local/bin/mouse-settings"
+echo "    mouse-settings -> ~/.local/bin/mouse-settings"
+
+echo "==> Installerar tte (terminaltexteffects) via pipx..."
+pipx install terminaltexteffects
+pipx ensurepath
+
+echo "==> Installerar screensaver..."
+mkdir -p "$HOME/.local/bin"
+cp "$SCRIPT_DIR/bin/screensaver" "$SCRIPT_DIR/bin/screensaverd" "$SCRIPT_DIR/bin/screensaver-launch" "$SCRIPT_DIR/bin/screensaver.txt" "$HOME/.local/bin/"
+chmod +x "$HOME/.local/bin/screensaver" "$HOME/.local/bin/screensaverd" "$HOME/.local/bin/screensaver-launch"
+echo "    screensaver, screensaverd, screensaver-launch, screensaver.txt -> ~/.local/bin/"
+
+echo ""
+echo "==> Automatisk avstängning av interna skärmen vid stängt lock"
+echo ""
+read -p "Installera lid-display-watch (stänger av interna skärmen när locket stängs, men bara om en extern skärm är ansluten)? (j/n) " -n 1 -r
+echo
+if [[ $REPLY =~ ^[Jj]$ ]]; then
+    cp "$SCRIPT_DIR/bin/internal-display" "$HOME/.local/bin/internal-display"
+    chmod +x "$HOME/.local/bin/internal-display"
+    cp "$SCRIPT_DIR/bin/lid-display-watch" "$HOME/.local/bin/lid-display-watch"
+    chmod +x "$HOME/.local/bin/lid-display-watch"
+    echo "    internal-display, lid-display-watch -> ~/.local/bin/"
+
+    if ! grep -qF 'exec : "lid-display-watch"' "$HOME/.config/sxwmrc" 2>/dev/null; then
+        echo 'exec : "lid-display-watch"' >> "$HOME/.config/sxwmrc"
+        echo "    lid-display-watch tillagd i ~/.config/sxwmrc"
+    fi
+else
+    echo "    Hoppar över lid-display-watch"
+fi
+
+echo "==> Konfigurerar skärmsläckning på TTY1 (innan inloggning/efter utloggning)..."
+GRUB_FILE="/etc/default/grub"
+if grep -q 'consoleblank=' "$GRUB_FILE" 2>/dev/null; then
+    echo "    consoleblank redan konfigurerad i $GRUB_FILE, hoppar över"
+else
+    sudo sed -i '/^GRUB_CMDLINE_LINUX_DEFAULT=/ s/"$/ consoleblank=300"/' "$GRUB_FILE"
+    sudo update-grub
+    echo "    consoleblank=300 tillagt (5 min), kräver omstart för att aktiveras"
+fi
+
 echo "==> Stänger av PC-speaker beep..."
 echo "blacklist pcspkr" | sudo tee /etc/modprobe.d/nobeep.conf > /dev/null
 sudo rmmod pcspkr 2>/dev/null || true
diff --git a/monitor-hotplug-remap.patch b/monitor-hotplug-remap.patch
new file mode 100644
index 0000000..e1cc9b4
--- /dev/null
+++ b/monitor-hotplug-remap.patch
@@ -0,0 +1,53 @@
+diff --git a/src/sxwm.c b/src/sxwm.c
+index 55fe97b..9c1b2ae 100644
+--- a/src/sxwm.c
++++ b/src/sxwm.c
+@@ -3077,6 +3151,48 @@ void update_mons(void)
+ 	}
+
+ 	free(old);
++
++	/*
++	 client->mon is a plain index into mons[], but monitor count/order
++	 can change across a hotplug (e.g. the internal panel toggled off
++	 for a closed lid, then back on). A stale index gets silently
++	 reinterpreted as whatever monitor now sits at that slot, dragging
++	 windows onto the wrong screen -- or, once clamped, squeezing them
++	 into bounds far smaller than they were tiled for. Re-derive mon
++	 from each client's real last-known position instead of trusting
++	 the old index, and pull floating windows back on-screen if the
++	 monitor they end up on no longer covers where they were sitting.
++	 */
++	for (int ws = 0; ws < NUM_WORKSPACES; ws++) {
++		for (Client *c = workspaces[ws]; c; c = c->next) {
++			c->mon = CLAMP(get_monitor_for(c), 0, n_mons - 1);
++
++			if (!c->floating)
++				continue;
++
++			int mx = mons[c->mon].x, my = mons[c->mon].y;
++			int mw = mons[c->mon].w, mh = mons[c->mon].h;
++			int x = c->x, y = c->y;
++
++			if (x < mx)
++				x = mx;
++			if (y < my)
++				y = my;
++			if (x + c->w > mx + mw)
++				x = mx + mw - c->w;
++			if (y + c->h > my + mh)
++				y = my + mh - c->h;
++
++			if (x != c->x || y != c->y) {
++				c->x = x;
++				c->y = y;
++				if (c->mapped)
++					XMoveWindow(dpy, c->win, x, y);
++			}
++		}
++	}
++
++	current_mon = CLAMP(current_mon, 0, n_mons - 1);
+ }
+
+ void update_net_client_list(void)
diff --git a/multi-monitor-struts.patch b/multi-monitor-struts.patch
new file mode 100644
index 0000000..8219097
--- /dev/null
+++ b/multi-monitor-struts.patch
@@ -0,0 +1,164 @@
+diff --git a/src/sxwm.c b/src/sxwm.c
+index 55fe97b..eab858a 100644
+--- a/src/sxwm.c
++++ b/src/sxwm.c
+@@ -3160,14 +3276,6 @@ void update_struts(void)
+ 			long right = str[1];
+ 			long top = str[2];
+ 			long bottom = str[3];
+-			long left_start_y = str[4];
+-			long left_end_y = str[5];
+-			long right_start_y = str[6];
+-			long right_end_y = str[7];
+-			long top_start_x = str[8];
+-			long top_end_x = str[9];
+-			long bot_start_x = str[10];
+-			long bot_end_x = str[11];
+
+ 			XFree(str);
+
+@@ -3175,79 +3283,76 @@ void update_struts(void)
+ 			if (!left && !right && !top && !bottom)
+ 				continue;
+
+-			for (int m = 0; m < n_mons; m++) {
+-				int mx = mons[m].x;
+-				int my = mons[m].y;
+-				int mw = mons[m].w;
+-				int mh = mons[m].h;
+-
+-				/* strip monitors whose vertical span dostn intersect */
+-				if (left > 0) {
+-					long span_start = left_start_y;
+-					long span_end   = left_end_y;
+-					if (span_end >= my && span_start <= my + mh - 1) {
+-						/*
+-						 left is distance from root left edge to reserved area
+-						 to map to mon, the portion is:
+-						     reserve_left = MAX(0, left - mx)
+-						 */
+-						int reserve = (int)MAX(0, left - mx);
+-						if (reserve > 0)
+-							mons[m].reserve_left = MAX(mons[m].reserve_left, reserve);
++			/*
++			 _NET_WM_STRUT(_PARTIAL) values are all measured from the
++			 absolute edges of the root window, not from the panel's own
++			 monitor. That's fine for single-monitor setups, but falls
++			 apart once monitors differ in size/offset: a panel sitting on
++			 a monitor that isn't flush with the desktop's top-left corner
++			 still reports e.g. "top" as its root-relative position, which
++			 can dwarf (or even exceed) the height of a smaller monitor
++			 that happens to sit at the actual top of the desktop. Anchor
++			 the reservation to the panel's own monitor -- found via the
++			 panel window's own geometry -- instead of trusting the
++			 x/y-span fields to disambiguate that for us.
++			 */
++			XWindowAttributes dock_wa;
++			int dock_mon = -1;
++			if (XGetWindowAttributes(dpy, w, &dock_wa)) {
++				int dcx = dock_wa.x + dock_wa.width / 2;
++				int dcy = dock_wa.y + dock_wa.height / 2;
++				for (int m = 0; m < n_mons; m++) {
++					if (dcx >= mons[m].x && dcx < mons[m].x + mons[m].w &&
++						dcy >= mons[m].y && dcy < mons[m].y + mons[m].h) {
++						dock_mon = m;
++						break;
+ 					}
+ 				}
++			}
+
+-				if (right > 0) {
+-					long span_start = right_start_y;
+-					long span_end   = right_end_y;
+-					if (span_end >= my && span_start <= my + mh - 1) {
+-						/*
+-						 right is distance from root right edge to reserved area:
+-						     right edge = screen_w
+-							 mons right edge = mx + mw
+-							 amount that cuts into monitor = MAX(0, (screen_w - right) - mx)
+-						 */
+-						int global_reserved_left = screen_w - (int)right;
+-						int overlap = (mx + mw) - global_reserved_left;
+-						int reserve = MAX(0, overlap);
+-						if (reserve > 0)
+-							mons[m].reserve_right = MAX(mons[m].reserve_right, reserve);
+-					}
+-				}
++			/*
++			 a dock whose centre falls outside every current monitor is
++			 orphaned -- almost always because the monitor it was drawn
++			 for got hotplugged away (e.g. a per-monitor bar drawn for
++			 eDP, left in place after the lid closes and eDP is turned
++			 off). Falling back to monitor 0 here would attribute a
++			 stale, unrelated reservation to whatever monitor survived,
++			 which can eat most of its height. Drop the strut instead.
++			 */
++			if (dock_mon < 0)
++				continue;
+
+-				if (top > 0) {
+-					long span_start = top_start_x;
+-					long span_end   = top_end_x;
+-					if (span_end >= mx && span_start <= mx + mw - 1) {
+-						/*
+-						 top is distance from root top to reserved area
+-							 mons top is at my, amount eaten:
+-							 reserve_top = MAX(0, top - my)
+-						 */
+-						int reserve = (int)MAX(0, top - my);
+-						if (reserve > 0)
+-							mons[m].reserve_top = MAX(mons[m].reserve_top, reserve);
+-					}
+-				}
++			int mx = mons[dock_mon].x;
++			int my = mons[dock_mon].y;
++			int mw = mons[dock_mon].w;
++			int mh = mons[dock_mon].h;
+
+-				if (bottom > 0) {
+-					long span_start = bot_start_x;
+-					long span_end   = bot_end_x;
+-					if (span_end >= mx && span_start <= mx + mw - 1) {
+-						/*
+-						 bottom is distance from root bottom to reserved area
+-						 global_reserved_top = screen_h - bottom;
+-						 overlap to mon:
+-						   overlap = (my + mh) - global_reserved_top;
+-						   reserve_bottom = MAX(0, overlap)
+-						 */
+-						int global_reserved_top = screen_h - (int)bottom;
+-						int overlap = (my + mh) - global_reserved_top;
+-						int reserve = MAX(0, overlap);
+-						if (reserve > 0)
+-							mons[m].reserve_bottom = MAX(mons[m].reserve_bottom, reserve);
+-					}
+-				}
++			if (left > 0) {
++				int reserve = (int)MIN(mw, MAX(0, left - mx));
++				if (reserve > 0)
++					mons[dock_mon].reserve_left = MAX(mons[dock_mon].reserve_left, reserve);
++			}
++
++			if (right > 0) {
++				int global_reserved_left = screen_w - (int)right;
++				int overlap = (mx + mw) - global_reserved_left;
++				int reserve = MIN(mw, MAX(0, overlap));
++				if (reserve > 0)
++					mons[dock_mon].reserve_right = MAX(mons[dock_mon].reserve_right, reserve);
++			}
++
++			if (top > 0) {
++				int reserve = (int)MIN(mh, MAX(0, top - my));
++				if (reserve > 0)
++					mons[dock_mon].reserve_top = MAX(mons[dock_mon].reserve_top, reserve);
++			}
++
++			if (bottom > 0) {
++				int global_reserved_top = screen_h - (int)bottom;
++				int overlap = (my + mh) - global_reserved_top;
++				int reserve = MIN(mh, MAX(0, overlap));
++				if (reserve > 0)
++					mons[dock_mon].reserve_bottom = MAX(mons[dock_mon].reserve_bottom, reserve);
+ 			}
+ 		}
+ 	}