vincentbernat.i3wm-configur.../bin/xss-dimmer
Vincent Bernat e52f0e4bc3 Revert "xss-dimmer: play a sound to notice dimmer when not looking at the screen"
This reverts commit 398ef9263b. pygame
intercepts SIGTERM. Maybe we could use Gstreamer instead. No time for
that, let just remove this feature for now.
2022-05-11 20:55:42 +02:00

196 lines
6.2 KiB
Python
Executable file

#!/usr/bin/env python3
"""Simple dimmer for xss-lock.
It dim the screen using a provided delay and display a countdown. It
will stop itself when the locker window is mapped.
"""
# It assumes we are using a compositor.
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf
import cairo
import argparse
import threading
import time
import math
from Xlib import display, X
from Xlib.error import BadWindow
from Xlib.protocol.event import MapNotify
def on_xevent(source, condition, xdisplay, locker):
while xdisplay.pending_events():
event = xdisplay.next_event()
if event.type != X.MapNotify:
continue
try:
wmclass = event.window.get_wm_class()
except BadWindow:
continue
if wmclass and wmclass[1] == locker:
Gtk.main_quit()
return False
return True
def on_realize(widget):
window = widget.get_window()
window.set_override_redirect(True)
def on_draw(widget, event, options, background, start):
x, y = widget.get_position()
wwidth, wheight = widget.get_size()
delta = options.end_opacity - options.start_opacity
elapsed = time.monotonic() - start
current = easing_functions[options.easing_function](elapsed / options.delay)
opacity = delta * current + options.start_opacity
cctx = event
# Background
scale = widget.get_scale_factor()
bg = None
if background:
bg = background.new_subpixbuf(x, y, wwidth * scale, wheight * scale)
cctx.set_operator(cairo.OPERATOR_SOURCE)
if not bg:
cctx.set_source_rgba(0, 0, 0, opacity)
cctx.paint()
else:
cctx.save()
cctx.scale(1 / scale, 1 / scale)
Gdk.cairo_set_source_pixbuf(cctx, bg, 0, 0)
cctx.paint_with_alpha(opacity)
cctx.restore()
# Remaining time
cctx.set_operator(cairo.OPERATOR_OVER)
remaining = str(round(options.delay - elapsed))
cctx.select_font_face(options.font, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
cctx.set_font_size(wheight // 4)
_, _, twidth, theight, _, _ = cctx.text_extents("8" * len(remaining))
text_position = wwidth // 2 - twidth // 2, wheight // 2 + theight // 2
cctx.move_to(*text_position)
cctx.set_source_rgba(1, 1, 1, opacity)
cctx.show_text(remaining)
cctx.move_to(*text_position)
cctx.set_source_rgba(0, 0, 0, opacity * 2)
cctx.set_line_width(4)
cctx.text_path(remaining)
cctx.stroke()
def on_refresh(window, options, start):
window.queue_draw()
elapsed = time.monotonic() - start
if elapsed < options.delay:
next_step = min(options.step, options.delay - elapsed)
GLib.timeout_add(options.step * 1000, on_refresh, window, options, start)
# See: https://easings.net/
easing_functions = {
"none": lambda x: x,
"out-circ": lambda x: math.sqrt(1 - pow(x - 1, 2)),
"out-sine": lambda x: math.sin(x * math.pi / 2),
"out-cubic": lambda x: 1 - pow(1 - x, 3),
"out-quint": lambda x: 1 - pow(1 - x, 5),
"out-expo": lambda x: 1 - pow(2, -10 * x),
"out-quad": lambda x: 1 - (1 - x) * (1 - x),
"out-bounce": (
lambda n1, d1: lambda x: n1 * x * x
if x < 1 / d1
else n1 * pow(x - 1.5 / d1, 2) + 0.75
if x < 2 / d1
else n1 * pow(x - 2.25 / d1, 2) + 0.9375
if (x < 2.5 / d1)
else n1 * pow(x - 2.625 / d1, 2) + 0.984375
)(7.5625, 2.75),
"out-elastic": (
lambda x: pow(2, -10 * x) * math.sin((x * 10 - 0.75) * (2 * math.pi) / 3) + 1
),
"inout-quad": lambda x: 2 * x * x if x < 0.5 else 1 - pow(-2 * x + 2, 2) / 2,
"inout-quart": (
lambda x: 8 * x * x * x * x if x < 0.5 else 1 - pow(-2 * x + 2, 4) / 2
),
"inout-expo": (
lambda x: pow(2, 20 * x - 10) / 2 if x < 0.5 else (2 - pow(2, -20 * x + 10)) / 2
),
"inout-bounce": (
lambda x: (1 - easing_functions["out-bounce"](1 - 2 * x)) / 2
if x < 0.5
else (1 + easing_functions["out-bounce"](2 * x - 1)) / 2
),
}
if __name__ == "__main__":
now = time.monotonic()
parser = argparse.ArgumentParser()
add = parser.add_argument
add("--start-opacity", type=float, default=0, help="initial opacity")
add("--end-opacity", type=float, default=1, help="final opacity")
add("--step", type=float, default=0.1, help="step for changing opacity")
add("--delay", type=float, default=10, help="delay from start to end")
add("--font", default="Iosevka Aile", help="font for countdown")
add("--locker", default="xsecurelock", help="quit if window class detected")
add("--background", help="use a background instead of black")
add(
"--easing-function",
default="none",
choices=easing_functions.keys(),
help="easing function for opacity",
)
options = parser.parse_args()
background = None
if options.background:
try:
background = GdkPixbuf.Pixbuf.new_from_file(options.background)
except Exception:
pass
# Setup dimmer windows on each monitor
gdisplay = Gdk.Display.get_default()
for i in range(gdisplay.get_n_monitors()):
geom = gdisplay.get_monitor(i).get_geometry()
window = Gtk.Window()
window.set_app_paintable(True)
window.set_type_hint(Gdk.WindowTypeHint.SPLASHSCREEN)
window.set_visual(window.get_screen().get_rgba_visual())
window.set_default_size(geom.width, geom.height)
window.move(geom.x, geom.y)
window.connect("draw", on_draw, options, background, now)
window.connect("delete-event", Gtk.main_quit)
window.connect("realize", on_realize)
window.show_all()
# Schedule refresh with window.queue_draw()
on_refresh(window, options, now)
# Watch for locker window
xdisplay = display.Display()
root = xdisplay.screen().root
root.change_attributes(event_mask=X.SubstructureNotifyMask)
channel = GLib.IOChannel.unix_new(xdisplay.fileno())
channel.set_encoding(None)
channel.set_buffered(False)
GLib.io_add_watch(
channel,
GLib.PRIORITY_DEFAULT,
GLib.IOCondition.IN,
on_xevent,
xdisplay,
options.locker,
)
xdisplay.pending_events() # otherwise, socket is inactive
# Main loop
Gtk.main()