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