#!/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()