Pause X screen blanking while an inhibit is active

Browsers don't pause X blanking themselves, so the screen still went
black after 10 minutes even with xautolock disabled. Use
XScreenSaverSuspend (MIT-SCREEN-SAVER) instead of xset: no global
setting is changed, and the X server ends the pause on its own if the
daemon exits or crashes. The suspend also holds off DPMS.

Add libx11 and libxss to depends.
This commit is contained in:
Daniele Fucini
2026-10-09 21:11:21 +02:00
parent 90b8838835
commit d862eb49da
2 changed files with 63 additions and 10 deletions
+60 -9
View File
@@ -1,7 +1,11 @@
#!/usr/bin/env python3
"""Own org.freedesktop.ScreenSaver and turn inhibit requests into xautolock -disable/-enable."""
"""Own org.freedesktop.ScreenSaver and turn inhibit requests into xautolock -disable/-enable.
While an inhibit is active, X screen blanking is also paused (like mpv does).
"""
import argparse
import ctypes
import itertools
import signal
import subprocess
@@ -62,9 +66,49 @@ def xautolock(arg):
log(f"xautolock {arg} failed: {e}")
class XBlanking:
"""Pause X screen blanking with XScreenSaverSuspend (MIT-SCREEN-SAVER extension).
Unlike `xset s off -dpms` this changes no global setting: the X server ends the
pause by itself when this client releases it or its connection closes (exit, crash).
"""
def __init__(self):
self.dpy = None
try:
self.x11 = ctypes.CDLL("libX11.so.6")
self.xss = ctypes.CDLL("libXss.so.1")
except OSError as e:
log(f"Cannot load X11 libraries ({e}); X blanking will not be paused")
return
self.x11.XOpenDisplay.argtypes = [ctypes.c_char_p]
self.x11.XOpenDisplay.restype = ctypes.c_void_p
self.x11.XFlush.argtypes = [ctypes.c_void_p]
self.xss.XScreenSaverQueryExtension.argtypes = [
ctypes.c_void_p, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int)]
self.xss.XScreenSaverSuspend.argtypes = [ctypes.c_void_p, ctypes.c_int]
dpy = self.x11.XOpenDisplay(None)
if not dpy:
log("Cannot open the X display; X blanking will not be paused")
return
event_base, error_base = ctypes.c_int(), ctypes.c_int()
if not self.xss.XScreenSaverQueryExtension(dpy, ctypes.byref(event_base), ctypes.byref(error_base)):
log("X server lacks the MIT-SCREEN-SAVER extension; X blanking will not be paused")
return
self.dpy = dpy
def suspend(self, on):
if self.dpy:
self.xss.XScreenSaverSuspend(self.dpy, int(on))
self.x11.XFlush(self.dpy)
log(f"X blanking {'paused' if on else 'resumed'}")
class Inhibitor:
def __init__(self, ignored_reasons, release_delay):
def __init__(self, ignored_reasons, release_delay, blanking):
self.ignored_reasons = ignored_reasons
self.blanking = blanking
self.release_delay = release_delay
self.cookies = {} # cookie -> (sender, application_name, reason, counts)
self.next_cookie = itertools.count(1)
@@ -85,8 +129,7 @@ class Inhibitor:
GLib.source_remove(self.release_timer)
self.release_timer = 0
if active and not self.disabled:
xautolock("-disable")
self.disabled = True
self.block()
elif not active and self.disabled and not self.release_timer:
# Browsers send quick Inhibit/UnInhibit bursts when a video starts.
self.release_timer = GLib.timeout_add_seconds(self.release_delay, self.on_release_timeout)
@@ -94,17 +137,25 @@ class Inhibitor:
def on_release_timeout(self):
self.release_timer = 0
if not self.active_count() and self.disabled:
xautolock("-enable")
self.disabled = False
self.unblock()
return GLib.SOURCE_REMOVE
def block(self):
xautolock("-disable")
self.blanking.suspend(True)
self.disabled = True
def unblock(self):
xautolock("-enable")
self.blanking.suspend(False)
self.disabled = False
def shutdown(self):
if self.release_timer:
GLib.source_remove(self.release_timer)
self.release_timer = 0
if self.disabled:
xautolock("-enable")
self.disabled = False
self.unblock()
# --- D-Bus ------------------------------------------------------------
@@ -205,7 +256,7 @@ def main():
ignored = set()
else:
ignored = set(args.ignore_reason or ["Playing audio"])
inhibitor = Inhibitor(ignored, max(args.release_delay, 0))
inhibitor = Inhibitor(ignored, max(args.release_delay, 0), XBlanking())
loop = GLib.MainLoop()
exit_code = 0
acquired = False