commit 1dac711f5088a0e8418cc1cf8a6a87c37118e4e5
Author: MrJensK <jens.se@icloud.com>
AuthorDate: Wed Aug 12 20:05:04 2026 +0200
Commit: MrJensK <jens.se@icloud.com>
CommitDate: Wed Aug 12 20:05:04 2026 +0200
Add screensaver toggle functionality and improve screensaver behavior
- Introduced a new script `screensaver-toggle` to pause/resume screensaver detection.
- Enhanced `screensaverd` to check for media playback and pause state before launching the screensaver.
- Updated `screensaver-launch` to improve window placement reliability.
- Added a new patch `strut-fresh-screen-size.patch` to ensure correct screen size readings.
- Updated installation script to include the new toggle script.
- Added keyboard shortcut for toggling the screensaver in `sxwmrc`.
---
README.md | 1 +
bin/screensaver-launch | 27 ++++++++++++++++++++-------
bin/screensaver-toggle | 17 +++++++++++++++++
bin/screensaverd | 23 +++++++++++++++++++++--
config/sxwmrc | 1 +
install.sh | 15 ++++++++++++---
strut-fresh-screen-size.patch | 23 +++++++++++++++++++++++
7 files changed, 95 insertions(+), 12 deletions(-)
diff --git a/README.md b/README.md
index f98ab6e..f7534e8 100644
--- a/README.md
+++ b/README.md
@@ -121,6 +121,7 @@ order:
| [`multi-monitor-struts.patch`](multi-monitor-struts.patch) | `_NET_WM_STRUT_PARTIAL` reservations weren't clamped to the panel's own monitor, so a monitor's reserved area could exceed its actual size and push windows off-screen |
| [`monitor-hotplug-remap.patch`](monitor-hotplug-remap.patch) | Windows lost their correct monitor association on hotplug (e.g. `lid-display-watch` toggling eDP); monitor ownership is now recomputed from each window's actual position |
| [`fullscreen-monitor-fix.patch`](fullscreen-monitor-fix.patch) | Fullscreening a window that had been moved to another monitor by an external tool (`xdotool`, `screensaver-launch`) fullscreened it on the wrong screen; the target monitor is now recomputed from the window's real geometry |
+| [`strut-fresh-screen-size.patch`](strut-fresh-screen-size.patch) | A bottom/right-edge panel (e.g. sxbar's `secondary_bar`) could end up hidden behind normal windows — `reserve_bottom`/`reserve_right` are measured from the *opposite* screen edge and need the full screen size, but read it from `scr_width`/`scr_height`, cached globals only refreshed by `update_mons()`; `reserve_top`/`reserve_left` never depend on screen size so stayed correct. Reads the screen size fresh from the X server instead |
## Keyboard shortcuts (sxwm)
diff --git a/bin/screensaver-launch b/bin/screensaver-launch
index 7c4e5b4..7160c4d 100755
--- a/bin/screensaver-launch
+++ b/bin/screensaver-launch
@@ -28,6 +28,7 @@ for mon in $(xrandr --listmonitors | awk 'NR>1 {print $3}'); do
--override font_size=18 \
--override window_padding_width=0 \
--override background_opacity=1 \
+ --override mouse_hide_wait=0.1 \
-e "$SCRIPT_DIR/screensaver" &
kpid=$!
@@ -46,16 +47,28 @@ for mon in $(xrandr --listmonitors | awk 'NR>1 {print $3}'); do
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
+ # appears in `xdotool search`, so retry placement to ride out the
+ # occasional BadWindow race with the window manager. `wmctrl`
+ # returning success only means the fullscreen request was sent, not
+ # that the WM actually acted on it (e.g. it's silently dropped if the
+ # window isn't mapped yet in the WM's eyes) -- read the geometry back
+ # and keep retrying until it actually matches the monitor, instead of
+ # giving up after a fixed few tries and leaving it windowed.
+ for attempt in $(seq 1 30); 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
+ 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
+
+ geo="$(xdotool getwindowgeometry --shell "$newwin" 2>/dev/null)"
+ gx="$(grep -m1 '^X=' <<<"$geo" | cut -d= -f2)"
+ gy="$(grep -m1 '^Y=' <<<"$geo" | cut -d= -f2)"
+ gw="$(grep -m1 '^WIDTH=' <<<"$geo" | cut -d= -f2)"
+ gh="$(grep -m1 '^HEIGHT=' <<<"$geo" | cut -d= -f2)"
+ if [[ "$gx" == "$x" && "$gy" == "$y" && "$gw" == "$w" && "$gh" == "$h" ]]; then
break
fi
- sleep 0.2
+ sleep 0.1
done
fi
done
diff --git a/bin/screensaver-toggle b/bin/screensaver-toggle
new file mode 100755
index 0000000..b0bd488
--- /dev/null
+++ b/bin/screensaver-toggle
@@ -0,0 +1,17 @@
+#!/bin/bash
+# Pauses/resumes screensaverd's idle detection. Run once to pause (also
+# stops the screensaver immediately if it's showing right now), run again
+# to resume. State is a flag file screensaverd polls every second, so
+# toggling takes effect within ~1s regardless of what screensaverd is
+# doing at the time.
+set -u
+
+PAUSE_FILE="${XDG_RUNTIME_DIR:-/tmp}/screensaver-paused"
+
+if [[ -e "$PAUSE_FILE" ]]; then
+ rm -f "$PAUSE_FILE"
+ echo "screensaver: återupptagen"
+else
+ touch "$PAUSE_FILE"
+ echo "screensaver: pausad"
+fi
diff --git a/bin/screensaverd b/bin/screensaverd
index 0d20a4e..ec1edf2 100755
--- a/bin/screensaverd
+++ b/bin/screensaverd
@@ -11,11 +11,30 @@ export PATH="$HOME/.local/bin:$PATH"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
IDLE_SECONDS="${SCREENSAVER_IDLE_SECONDS:-150}"
WIN_CLASS="screensaver"
+PAUSE_FILE="${XDG_RUNTIME_DIR:-/tmp}/screensaver-paused"
is_running() {
xdotool search --class "^${WIN_CLASS}\$" >/dev/null 2>&1
}
+# Set by screensaver-toggle. Checked fresh every loop iteration (not just
+# at launch time) so toggling pause while the screensaver is already
+# showing kills it immediately, same as real input would.
+is_paused() {
+ [[ -e "$PAUSE_FILE" ]]
+}
+
+# xssstate only tracks keyboard/mouse input, so a video playing quietly
+# (no input for the whole runtime) still counts as "idle" and would get
+# covered by the screensaver. playerctl (already a dependency for the
+# media keybindings) reports MPRIS playback state for players that
+# support it (mpv, VLC, Firefox/Chromium, ...), so use that as an extra
+# "really idle" check.
+media_playing() {
+ command -v playerctl >/dev/null 2>&1 || return 1
+ [ "$(playerctl status 2>/dev/null)" = "Playing" ]
+}
+
stop_screensaver() {
pkill -f "$SCRIPT_DIR/screensaver\$" 2>/dev/null
pkill -x tte 2>/dev/null
@@ -28,9 +47,9 @@ while true; do
idle_ms=$(xssstate -i)
idle_s=$((idle_ms / 1000))
- if (( idle_s >= IDLE_SECONDS )) && ! is_running; then
+ if (( idle_s >= IDLE_SECONDS )) && ! is_running && ! media_playing && ! is_paused; then
"$SCRIPT_DIR/screensaver-launch"
- elif (( idle_s < 1 )) && is_running; then
+ elif is_running && { (( idle_s < 1 )) || media_playing || is_paused; }; then
stop_screensaver
fi
diff --git a/config/sxwmrc b/config/sxwmrc
index d3fcab1..f1bf215 100644
--- a/config/sxwmrc
+++ b/config/sxwmrc
@@ -38,6 +38,7 @@ mod_key : super
bind : mod + Return : "kitty"
bind : mod + b : "firefox"
bind : mod + p : "dmenu_run"
+bind : mod + shift + s : "screensaver-toggle"
# Window Management:
call : mod + q : close_window
diff --git a/install.sh b/install.sh
index f5b8914..6fafccc 100755
--- a/install.sh
+++ b/install.sh
@@ -73,6 +73,15 @@ patch -p1 < "$SCRIPT_DIR/monitor-hotplug-remap.patch"
# 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"
+# En bottom/right-panel (t.ex. sxbars secondary_bar) kunde hamna dold bakom
+# vanliga fönster -- update_struts() räknar reserve_bottom/reserve_right som
+# avstånd från *motsatt* skärmkant och behövde då hela skärmens bredd/höjd,
+# men läste den ur scr_width/scr_height som bara uppdateras i update_mons()
+# (start + monitor-hotplug). Blir de cachade värdena inaktuella räknas bara
+# bottom/right fel (top/left beror aldrig på skärmstorleken, bara på
+# monitorns egen x/y, och påverkades inte). Läser bredd/höjd direkt från
+# X-servern i update_struts() istället för att lita på cachen.
+patch -p1 < "$SCRIPT_DIR/strut-fresh-screen-size.patch"
echo "==> Bygger och installerar sxwm..."
make
@@ -182,9 +191,9 @@ 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/"
+cp "$SCRIPT_DIR/bin/screensaver" "$SCRIPT_DIR/bin/screensaverd" "$SCRIPT_DIR/bin/screensaver-launch" "$SCRIPT_DIR/bin/screensaver-toggle" "$SCRIPT_DIR/bin/screensaver.txt" "$HOME/.local/bin/"
+chmod +x "$HOME/.local/bin/screensaver" "$HOME/.local/bin/screensaverd" "$HOME/.local/bin/screensaver-launch" "$HOME/.local/bin/screensaver-toggle"
+echo " screensaver, screensaverd, screensaver-launch, screensaver-toggle, screensaver.txt -> ~/.local/bin/"
echo ""
echo "==> Automatisk avstängning av interna skärmen vid stängt lock"
diff --git a/strut-fresh-screen-size.patch b/strut-fresh-screen-size.patch
new file mode 100644
index 0000000..8baec65
--- /dev/null
+++ b/strut-fresh-screen-size.patch
@@ -0,0 +1,23 @@
+diff --git a/src/sxwm.c b/src/sxwm.c
+--- a/src/sxwm.c
++++ b/src/sxwm.c
+@@ -3235,8 +3235,17 @@
+ if (!XQueryTree(dpy, root, &root_ret, &parent_ret, &children, &n_children))
+ return;
+
+- int screen_w = scr_width;
+- int screen_h = scr_height;
++ /* scr_width/scr_height are only refreshed by update_mons(), on
++ * startup and root ConfigureNotify -- unlike reserve_top/reserve_left
++ * (measured from a monitor's own x/y, independent of total screen
++ * size), reserve_bottom/reserve_right are measured as a distance from
++ * the *opposite* edge of the whole screen, so a stale cached
++ * scr_width/scr_height silently corrupts only those two, never
++ * left/top. Query the server directly here instead of trusting the
++ * cache, since this runs cheaply and infrequently (on retile, not
++ * per-frame). */
++ int screen_w = XDisplayWidth(dpy, DefaultScreen(dpy));
++ int screen_h = XDisplayHeight(dpy, DefaultScreen(dpy));
+
+ for (unsigned int i = 0; i < n_children; i++) {
+ Window w = children[i];