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);
+ }
+ }
+ }