Files
Daniele Fucini 441b86448e Add xidle-dim notifier to dim the screen before xss-lock locks
Fades a black window in over the screen through the compositor, so it works with redshift (no gamma
changes); does nothing without a compositor. README shows it in the xss-lock setup.
2026-10-23 21:36:18 +02:00

120 lines
4.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Dim the screen until killed, as an xss-lock notifier: xss-lock -n xidle-dim -- slock.
Maps a black window over the whole screen and fades in its opacity through the compositor; gamma is left alone,
so it doesn't clash with redshift. Without a compositor it does nothing, as the window would be solid black.
"""
import argparse
import ctypes
import signal
import sys
import time
from types import FrameType
CW_BACK_PIXEL = 1 << 1
CW_OVERRIDE_REDIRECT = 1 << 9
INPUT_OUTPUT = 1
XA_CARDINAL = 6
PROP_MODE_REPLACE = 0
OPACITY_MAX = 0xFFFFFFFF
class XSetWindowAttributes(ctypes.Structure):
_fields_ = [
("background_pixmap", ctypes.c_ulong),
("background_pixel", ctypes.c_ulong),
("border_pixmap", ctypes.c_ulong),
("border_pixel", ctypes.c_ulong),
("bit_gravity", ctypes.c_int),
("win_gravity", ctypes.c_int),
("backing_store", ctypes.c_int),
("backing_planes", ctypes.c_ulong),
("backing_pixel", ctypes.c_ulong),
("save_under", ctypes.c_int),
("event_mask", ctypes.c_long),
("do_not_propagate_mask", ctypes.c_long),
("override_redirect", ctypes.c_int),
("colormap", ctypes.c_ulong),
("cursor", ctypes.c_ulong),
]
def load_x11() -> ctypes.CDLL:
x11 = ctypes.CDLL("libX11.so.6")
p, ul, i = ctypes.c_void_p, ctypes.c_ulong, ctypes.c_int
x11.XOpenDisplay.argtypes = [ctypes.c_char_p]
x11.XOpenDisplay.restype = p
for name in ("XDefaultScreen",):
getattr(x11, name).argtypes = [p]
for name in ("XRootWindow", "XBlackPixel"):
getattr(x11, name).argtypes = [p, i]
getattr(x11, name).restype = ul
for name in ("XDisplayWidth", "XDisplayHeight"):
getattr(x11, name).argtypes = [p, i]
x11.XInternAtom.argtypes = [p, ctypes.c_char_p, i]
x11.XInternAtom.restype = ul
x11.XGetSelectionOwner.argtypes = [p, ul]
x11.XGetSelectionOwner.restype = ul
x11.XCreateWindow.argtypes = [p, ul, i, i, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, i, ctypes.c_uint, p, ul,
ctypes.POINTER(XSetWindowAttributes)]
x11.XCreateWindow.restype = ul
x11.XChangeProperty.argtypes = [p, ul, ul, ul, i, i, p, i]
x11.XMapRaised.argtypes = [p, ul]
x11.XFlush.argtypes = [p]
return x11
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--opacity", type=float, default=0.6, help="final darkness, 0 to 1 (default: %(default)s)")
parser.add_argument("--fade", type=float, default=1.5, help="fade-in time in seconds (default: %(default)s)")
args = parser.parse_args()
if not 0 <= args.opacity <= 1 or args.fade < 0:
parser.error("--opacity must be between 0 and 1 and --fade not negative")
return args
def exit_on_signal(_signum: int, _frame: FrameType | None) -> None:
sys.exit(0) # the X server removes the window when the connection closes
def main() -> None:
args = parse_args()
# xss-lock sends SIGHUP on user activity and SIGTERM when the locker starts.
for sig in (signal.SIGHUP, signal.SIGTERM, signal.SIGINT):
signal.signal(sig, exit_on_signal)
x11 = load_x11()
dpy = x11.XOpenDisplay(None)
if not dpy:
sys.exit("xidle-dim: cannot open display")
screen = x11.XDefaultScreen(dpy)
if not x11.XGetSelectionOwner(dpy, x11.XInternAtom(dpy, f"_NET_WM_CM_S{screen}".encode(), False)):
print("xidle-dim: no compositor running, not dimming", file=sys.stderr)
signal.pause() # stay alive like a notifier should, until xss-lock kills us
attrs = XSetWindowAttributes(background_pixel=x11.XBlackPixel(dpy, screen), override_redirect=True)
win = x11.XCreateWindow(dpy, x11.XRootWindow(dpy, screen), 0, 0, x11.XDisplayWidth(dpy, screen),
x11.XDisplayHeight(dpy, screen), 0, 0, INPUT_OUTPUT, None,
CW_BACK_PIXEL | CW_OVERRIDE_REDIRECT, ctypes.byref(attrs))
opacity_atom = x11.XInternAtom(dpy, b"_NET_WM_WINDOW_OPACITY", False)
def set_opacity(level: float) -> None:
value = ctypes.c_ulong(int(level * OPACITY_MAX)) # format 32 properties are passed as longs
x11.XChangeProperty(dpy, win, opacity_atom, XA_CARDINAL, 32, PROP_MODE_REPLACE, ctypes.byref(value), 1)
x11.XFlush(dpy)
set_opacity(0)
x11.XMapRaised(dpy, win)
steps = max(1, int(args.fade * 30))
for n in range(1, steps + 1):
set_opacity(args.opacity * n / steps)
time.sleep(args.fade / steps)
while True:
signal.pause()
if __name__ == "__main__":
main()