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.
120 lines
4.6 KiB
Python
Executable File
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()
|