5 Commits
Author SHA1 Message Date
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
Daniele Fucini 6548bbb232 Update PKGBUILD 2026-10-23 19:56:14 +02:00
Daniele Fucini 0fc1ab2f47 Add xss-lock backend and rename to xidle-inhibit
The backend comes from ~/.config/xidle-inhibit/config, else the installed locker. With xss-lock, pausing X blanking alone blocks locking and Lock uses loginctl; the unit's ExecStopPost runs --cleanup. Inhibits from the old xautolock-inhibit state file are taken over once. Also fix mypy with pygobject-stubs 2.19.
2026-10-23 19:53:42 +02:00
Daniele Fucini ef9c8b858b Uniform installation instructions in README 2026-10-20 22:01:28 +02:00
Daniele Fucini ddd1f6f40a Update PKGBUILD 2026-10-17 16:50:04 +02:00
7 changed files with 300 additions and 60 deletions
+9 -6
View File
@@ -2,22 +2,25 @@
# Maintainer: Daniele Fucini <dfucini [at] gmail [dot] com> # Maintainer: Daniele Fucini <dfucini [at] gmail [dot] com>
pkgname=xautolock-inhibit pkgname=xidle-inhibit
pkgver=0.2 pkgver=0.3
pkgrel=1 pkgrel=1
pkgdesc='Honor org.freedesktop.ScreenSaver inhibit requests by disabling xautolock' pkgdesc='Honor org.freedesktop.ScreenSaver inhibit requests by blocking xautolock or xss-lock'
arch=('any') arch=('any')
url="https://git.shouldnt.work/fuxino/$pkgname" url="https://git.shouldnt.work/fuxino/$pkgname"
license=('GPL-3.0-or-later') license=('GPL-3.0-or-later')
depends=('libx11' depends=('libx11'
'libxss' 'libxss'
'python' 'python'
'python-gobject' 'python-gobject')
'xautolock') optdepends=('xautolock: xautolock backend'
'xss-lock: xss-lock backend')
makedepends=('git') makedepends=('git')
conflicts=('xautolock-inhibit')
replaces=('xautolock-inhibit')
source=("git+$url?signed#tag=$pkgver") source=("git+$url?signed#tag=$pkgver")
validpgpkeys=('7E12BC1FF3B6EDB2CD8053EB981A2B2A3BBF5514') validpgpkeys=('7E12BC1FF3B6EDB2CD8053EB981A2B2A3BBF5514')
sha256sums=('7de7d76f502583172f269311c54b4dfe56facf0ffd6aa21fd7883e296e67d798') sha256sums=('13390705ad8e0abea5136dbe57f21b7d9509681ee8628109dd0a302497188fbf')
package() package()
{ {
+72 -20
View File
@@ -1,77 +1,128 @@
# xautolock-inhibit # xidle-inhibit
Makes screen locking with `xautolock` respect the "don't lock the screen" requests that apps send, on X11 sessions without a desktop environment (for example a plain window manager like xmonad). Makes screen locking with `xautolock` or `xss-lock` respect the "don't lock the screen" requests that apps send, on X11 sessions without a desktop environment (for example a plain window manager like xmonad).
Browsers, Steam, gamemode and other apps ask the desktop not to lock or blank the screen while a video or game is running, through the D-Bus service `org.freedesktop.ScreenSaver`. Without a desktop environment nothing provides that service, so the requests are dropped and the screen locks in the middle of a video. xautolock-inhibit provides it: Browsers, Steam, gamemode and other apps ask the desktop not to lock or blank the screen while a video or game is running, through the D-Bus service `org.freedesktop.ScreenSaver`. Without a desktop environment nothing provides that service, so the requests are dropped and the screen locks in the middle of a video. xidle-inhibit provides it:
- While at least one request is active, it runs `xautolock -disable` and pauses X screen blanking and DPMS. - While at least one request is active, it pauses X screen blanking and DPMS, and with xautolock also runs `xautolock -disable`. xss-lock locks when the X screensaver starts, so pausing it is enough.
- When the last request is released, it waits a few seconds, then runs `xautolock -enable` and resumes blanking. - When the last request is released, it waits a few seconds, then resumes blanking (and runs `xautolock -enable`).
xautolock stays your locker; xautolock-inhibit only switches it off and on. Your locker setup stays as it is; xidle-inhibit only holds it off.
Formerly `xautolock-inhibit`, which supported only xautolock.
## How it works ## How it works
- Owns `org.freedesktop.ScreenSaver` on the session bus at both `/org/freedesktop/ScreenSaver` and `/ScreenSaver` (apps differ on the path), and implements `Inhibit` and `UnInhibit`. - Owns `org.freedesktop.ScreenSaver` on the session bus at both `/org/freedesktop/ScreenSaver` and `/ScreenSaver` (apps differ on the path), and implements `Inhibit`, `UnInhibit` and `Lock`.
- Tracks requests per D-Bus client. If a client disconnects without releasing them (for example a browser crash), its requests are released. - Tracks requests per D-Bus client. If a client disconnects without releasing them (for example a browser crash), its requests are released.
- Runs `xautolock` only when the number of active requests goes from 0 to 1 or back to 0. Re-enabling waits a few seconds (`--release-delay`), because browsers send short bursts of requests when a video starts. - Blocks and unblocks only when the number of active requests goes from 0 to 1 or back to 0. Unblocking waits a few seconds (`--release-delay`), because browsers send short bursts of requests when a video starts.
- Pauses X blanking with `XScreenSaverSuspend`, the same mechanism mpv uses, instead of changing `xset` settings. The X server ends the pause by itself if the daemon exits or crashes. - Pauses X blanking with `XScreenSaverSuspend`, the same mechanism mpv uses, instead of changing `xset` settings. The X server ends the pause by itself if the daemon exits or crashes.
- Requests whose reason is on an ignore list (default: `Playing audio`) are accepted and logged, but don't block locking. Firefox-based browsers send one for audio playback, which would otherwise keep the screen unlocked while music plays with nobody at the computer. - Requests whose reason is on an ignore list (default: `Playing audio`) are accepted and logged, but don't block locking. Firefox-based browsers send one for audio playback, which would otherwise keep the screen unlocked while music plays with nobody at the computer.
- Keeps the requests it holds in `$XDG_RUNTIME_DIR/xautolock-inhibit.json`, so a restarted or crashed daemon takes them over. They are taken over only if the previous daemon stopped at most 30 seconds ago (`--restore-window`) on the same session bus, and only for clients still connected. A request released while no daemon was running can't be seen, so an old file isn't trusted. - Keeps the requests it holds in `$XDG_RUNTIME_DIR/xidle-inhibit.json`, so a restarted or crashed daemon takes them over. They are taken over only if the previous daemon stopped at most 30 seconds ago (`--restore-window`) on the same session bus, and only for clients still connected. A request released while no daemon was running can't be seen, so an old file isn't trusted.
- On exit (including SIGTERM), it re-enables xautolock. The systemd unit also runs `xautolock -enable` when it stops, as a fallback. - With xautolock, it re-enables xautolock on exit (including SIGTERM). The systemd unit also runs `xidle-inhibit --cleanup` when it stops, which does the same, as a fallback.
- `Lock` runs `xautolock -locknow`, or `loginctl lock-session` with xss-lock.
- If another program (a desktop environment) already owns `org.freedesktop.ScreenSaver`, it exits with status 2 instead of taking over. - If another program (a desktop environment) already owns `org.freedesktop.ScreenSaver`, it exits with status 2 instead of taking over.
## Requirements ## Requirements
- `xautolock`, already running in the session - `xautolock` or `xss-lock`, already running in the session
- `python`, `python-gobject` - `python`, `python-gobject`
- `libx11`, `libxss` - `libx11`, `libxss`
- For `xidle-dim`: a compositor, such as picom
## Installation ## Installation
Build and install the package with the included PKGBUILD: Build and install the package with the included PKGBUILD, which builds the signed release tag set in `pkgver`:
``` ```
makepkg -si makepkg -si
``` ```
Then start it from your window manager's startup, after `xautolock`. For xmonad: Or, with aurutils, build it into a local repository with `aur build` and install it with pacman.
Then start it from your window manager's startup, after the locker. For xmonad with xautolock:
```haskell ```haskell
spawnOnce "xautolock -time 10 -locker slock -detectsleep" spawnOnce "xautolock -time 10 -locker slock -detectsleep"
spawnOnce "systemctl --user start xautolock-inhibit" spawnOnce "systemctl --user start xidle-inhibit"
``` ```
With xss-lock, the lock timeout is the X screensaver timeout (`xset s TIMEOUT CYCLE`). This dims the screen with `xidle-dim` at 9:50 and locks at 10:00:
```haskell
spawnOnce "xset s 590 10"
spawnOnce "xss-lock -n xidle-dim -- slock"
spawnOnce "systemctl --user start xidle-inhibit"
```
Without dimming, use `xset s 600 0` and `xss-lock -- slock`. Start xss-lock from the window manager, not from a terminal: terminals like kitty run commands outside the login session, and xss-lock then ignores `loginctl lock-session`.
The unit has no `[Install]` section on purpose. Enabling it at login would also start it in a desktop environment, or before X is up; starting it from the window manager limits it to that session. The user service manager needs `DISPLAY` (and `XAUTHORITY`) in its environment, which `/etc/X11/xinit/xinitrc.d/50-systemd-user.sh` sets up for `startx` sessions. The unit has no `[Install]` section on purpose. Enabling it at login would also start it in a desktop environment, or before X is up; starting it from the window manager limits it to that session. The user service manager needs `DISPLAY` (and `XAUTHORITY`) in its environment, which `/etc/X11/xinit/xinitrc.d/50-systemd-user.sh` sets up for `startx` sessions.
### Upgrading from xautolock-inhibit
The package replaces `xautolock-inhibit`. After upgrading, change the `systemctl --user start` line in your window manager's startup, then switch the running daemon:
```
systemctl --user stop xautolock-inhibit
systemctl --user start xidle-inhibit
```
Requests held by the old daemon are taken over if the new one starts within `--restore-window`.
## Configuration
The backend is set in `~/.config/xidle-inhibit/config` (`$XDG_CONFIG_HOME`):
```
backend = xss-lock
```
Values are `xautolock` and `xss-lock`. Without the file, xidle-inhibit uses whichever of the two is installed, and xautolock if both are. Restart the service after changing it.
## Usage ## Usage
``` ```
xautolock-inhibit [-i REASON] [--count-all] [-d SECONDS] [--restore-window SECONDS] xidle-inhibit [-i REASON] [--count-all] [-d SECONDS] [--restore-window SECONDS] [--cleanup]
``` ```
- `-i`, `--ignore-reason REASON`: request reason that doesn't block locking. Can be repeated; replaces the default (`Playing audio`). - `-i`, `--ignore-reason REASON`: request reason that doesn't block locking. Can be repeated; replaces the default (`Playing audio`).
- `--count-all`: every request blocks locking; no reasons are ignored. - `--count-all`: every request blocks locking; no reasons are ignored.
- `-d`, `--release-delay SECONDS`: wait this long after the last release before re-enabling xautolock (default 5). - `-d`, `--release-delay SECONDS`: wait this long after the last release before unblocking locking (default 5).
- `--restore-window SECONDS`: take over requests from a previous daemon that stopped at most this long ago (default 30; 0 disables). - `--restore-window SECONDS`: take over requests from a previous daemon that stopped at most this long ago (default 30; 0 disables).
- `--cleanup`: run `xautolock -enable` if xautolock is the backend, then exit. Used by the unit's `ExecStopPost`.
To change the options used by the service: To change the options used by the service:
``` ```
systemctl --user edit xautolock-inhibit.service systemctl --user edit xidle-inhibit.service
``` ```
```ini ```ini
[Service] [Service]
ExecStart= ExecStart=
ExecStart=/usr/bin/xautolock-inhibit --count-all ExecStart=/usr/bin/xidle-inhibit --count-all
``` ```
Every request, release and xautolock call is logged to the journal: Every request, release and locker command is logged to the journal:
``` ```
journalctl --user -u xautolock-inhibit journalctl --user -u xidle-inhibit
``` ```
### Dimming before the lock
`xidle-dim` is an xss-lock notifier (`-n`). It fades a black window over the screen until xss-lock stops it, on user activity or when the locker starts. It needs a compositor such as picom and does nothing without one. Because it doesn't change gamma, it works alongside redshift.
```
xidle-dim [--opacity 0-1] [--fade SECONDS]
```
- `--opacity`: final darkness (default 0.6).
- `--fade`: fade-in time (default 1.5).
Options go inside the notifier command: `xss-lock -n "xidle-dim --opacity 0.8" -- slock`.
## App notes ## App notes
- **Sandboxed apps** may be blocked from reaching the service. For a firejail profile that filters D-Bus (`dbus-user filter`, as the Firefox-based profiles do), allow it in the app's `.local` file, e.g. `zen-browser.local`: - **Sandboxed apps** may be blocked from reaching the service. For a firejail profile that filters D-Bus (`dbus-user filter`, as the Firefox-based profiles do), allow it in the app's `.local` file, e.g. `zen-browser.local`:
@@ -87,6 +138,7 @@ journalctl --user -u xautolock-inhibit
## Limitations ## Limitations
- A release sent while the daemon is down is lost: if the daemon is back within `--restore-window`, that request stays active until its app exits. After a longer stop, all held requests are dropped and apps don't send them again. - A release sent while the daemon is down is lost: if the daemon is back within `--restore-window`, that request stays active until its app exits. After a longer stop, all held requests are dropped and apps don't send them again.
- With xss-lock, requests block only the idle timeout: suspending, or `loginctl lock-session` from elsewhere, still locks.
- `SimulateUserActivity` is accepted but does nothing. Calls are logged once when a client starts sending them and once, with a count, after 5 minutes without any. - `SimulateUserActivity` is accepted but does nothing. Calls are logged once when a client starts sending them and once, with a count, after 5 minutes without any.
- `GetActive` always returns `false`. - `GetActive` always returns `false`.
+3 -2
View File
@@ -1,10 +1,11 @@
[tool.ruff] [tool.ruff]
line-length = 120 line-length = 120
extend-include = ["xautolock-inhibit"] extend-include = ["xidle-inhibit", "xidle-dim"]
[tool.ruff.lint] [tool.ruff.lint]
select = ["E", "F", "W", "I", "B", "UP"] select = ["E", "F", "W", "I", "B", "UP"]
[tool.mypy] [tool.mypy]
files = ["xautolock-inhibit"] # Scripts both map to __main__, so check xidle-dim separately: mypy xidle-dim
files = ["xidle-inhibit"]
strict = true strict = true
-10
View File
@@ -1,10 +0,0 @@
[Unit]
Description=Turn org.freedesktop.ScreenSaver inhibit requests into xautolock control
[Service]
Type=dbus
BusName=org.freedesktop.ScreenSaver
ExecStart=/usr/bin/xautolock-inhibit
ExecStopPost=-/usr/bin/xautolock -enable
Restart=on-failure
RestartPreventExitStatus=2
Executable
+119
View File
@@ -0,0 +1,119 @@
#!/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()
+84 -19
View File
@@ -1,13 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Own org.freedesktop.ScreenSaver and turn inhibit requests into xautolock -disable/-enable. """Own org.freedesktop.ScreenSaver and block screen locking by xautolock or xss-lock while inhibits are held.
While an inhibit is active, X screen blanking is also paused (like mpv does). While an inhibit is active, X screen blanking is paused (like mpv does), which also keeps xss-lock from
locking; with xautolock, it is also switched off with xautolock -disable.
""" """
import argparse import argparse
import ctypes import ctypes
import json import json
import os import os
import shutil
import signal import signal
import subprocess import subprocess
import sys import sys
@@ -19,7 +21,7 @@ from typing import Any
from gi.repository import Gio, GLib from gi.repository import Gio, GLib
try: try:
from gi.repository import GLibUnix # type: ignore[attr-defined] # GLib >= 2.80, not in pygobject-stubs from gi.repository import GLibUnix # GLib >= 2.80
signal_add = GLibUnix.signal_add signal_add = GLibUnix.signal_add
except ImportError: except ImportError:
signal_add = GLib.unix_signal_add signal_add = GLib.unix_signal_add
@@ -28,8 +30,11 @@ BUS_NAME = "org.freedesktop.ScreenSaver"
OBJECT_PATHS = ("/org/freedesktop/ScreenSaver", "/ScreenSaver") # Vivaldi uses the first, Zen the second OBJECT_PATHS = ("/org/freedesktop/ScreenSaver", "/ScreenSaver") # Vivaldi uses the first, Zen the second
EXIT_NAME_TAKEN = 2 EXIT_NAME_TAKEN = 2
ACTIVITY_QUIET = 300 # seconds without SimulateUserActivity that end a logged burst ACTIVITY_QUIET = 300 # seconds without SimulateUserActivity that end a logged burst
STATE_FILE = "xautolock-inhibit.json" # in $XDG_RUNTIME_DIR STATE_FILE = "xidle-inhibit.json" # in $XDG_RUNTIME_DIR
OLD_STATE_FILE = "xautolock-inhibit.json" # before the rename (0.2); read once when upgrading
STATE_HEARTBEAT = 10 # seconds between state file touches while inhibits are held STATE_HEARTBEAT = 10 # seconds between state file touches while inhibits are held
CONFIG_FILE = "xidle-inhibit/config" # in $XDG_CONFIG_HOME
BACKENDS = ("xautolock", "xss-lock")
Cookie = tuple[str, str, str, bool] # sender, application_name, reason, counts Cookie = tuple[str, str, str, bool] # sender, application_name, reason, counts
@@ -68,12 +73,39 @@ def log(msg: str) -> None:
print(msg, flush=True) print(msg, flush=True)
def xautolock(arg: str) -> None: def run(*cmd: str) -> None:
try: try:
subprocess.run(["xautolock", arg], check=True, timeout=10) subprocess.run(cmd, check=True, timeout=10)
log(f"xautolock {arg}") log(" ".join(cmd))
except (OSError, subprocess.SubprocessError) as e: except (OSError, subprocess.SubprocessError) as e:
log(f"xautolock {arg} failed: {e}") log(f"{' '.join(cmd)} failed: {e}")
def xautolock(arg: str) -> None:
run("xautolock", arg)
def read_backend() -> str:
"""Return the backend set in the config file, else the installed locker (xautolock if both are)."""
config_home = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")
path = os.path.join(config_home, CONFIG_FILE)
try:
with open(path) as f:
for line in f:
key, sep, value = line.partition("=")
if not sep or key.strip() != "backend":
continue
value = value.strip().strip("\"'")
if value in BACKENDS:
return value
log(f"{path}: unknown backend '{value}' (expected {' or '.join(BACKENDS)}); detecting")
except FileNotFoundError:
pass
except OSError as e:
log(f"Cannot read {path}: {e}; detecting the backend")
if not shutil.which("xautolock") and shutil.which("xss-lock"):
return "xss-lock"
return "xautolock"
class XBlanking: class XBlanking:
@@ -125,9 +157,10 @@ class ActivityBurst:
class Inhibitor: class Inhibitor:
def __init__( def __init__(
self, ignored_reasons: set[str], release_delay: int, blanking: XBlanking, self, backend: str, ignored_reasons: set[str], release_delay: int, blanking: XBlanking,
state_path: str | None, restore_window: int, state_path: str | None, restore_window: int,
) -> None: ) -> None:
self.xautolock = backend == "xautolock" # else xss-lock: pausing X blanking is enough
self.ignored_reasons = ignored_reasons self.ignored_reasons = ignored_reasons
self.blanking = blanking self.blanking = blanking
self.release_delay = release_delay self.release_delay = release_delay
@@ -149,7 +182,7 @@ class Inhibitor:
return sum(1 for *_, counts in self.cookies.values() if counts) return sum(1 for *_, counts in self.cookies.values() if counts)
def update(self) -> None: def update(self) -> None:
"""Run xautolock only on 0->1 and 1->0 transitions; delay re-enabling.""" """Block or unblock only on 0->1 and 1->0 transitions; delay unblocking."""
active = self.active_count() active = self.active_count()
if active and self.release_timer: if active and self.release_timer:
GLib.source_remove(self.release_timer) GLib.source_remove(self.release_timer)
@@ -167,11 +200,13 @@ class Inhibitor:
return GLib.SOURCE_REMOVE return GLib.SOURCE_REMOVE
def block(self) -> None: def block(self) -> None:
if self.xautolock:
xautolock("-disable") xautolock("-disable")
self.blanking.suspend(True) self.blanking.suspend(True)
self.disabled = True self.disabled = True
def unblock(self) -> None: def unblock(self) -> None:
if self.xautolock:
xautolock("-enable") xautolock("-enable")
self.blanking.suspend(False) self.blanking.suspend(False)
self.disabled = False self.disabled = False
@@ -189,13 +224,21 @@ class Inhibitor:
# --- state file ------------------------------------------------------- # --- state file -------------------------------------------------------
def start(self) -> None: def start(self) -> None:
"""Take over inhibits saved by a previous instance, then set xautolock to match.""" """Take over inhibits saved by a previous instance, then set the locker to match."""
self.restore() self.restore()
self.state_ready = True self.state_ready = True
self.save() self.save()
if self.state_path:
old = os.path.join(os.path.dirname(self.state_path), OLD_STATE_FILE)
try:
os.remove(old)
except FileNotFoundError:
pass
except OSError as e:
log(f"Cannot remove old state file {old}: {e}")
if self.active_count(): if self.active_count():
self.block() self.block()
else: elif self.xautolock:
# Start from a known state, in case a previous instance died while disabled. # Start from a known state, in case a previous instance died while disabled.
xautolock("-enable") xautolock("-enable")
@@ -204,18 +247,24 @@ class Inhibitor:
log("XDG_RUNTIME_DIR is not set; inhibits will not survive a restart") log("XDG_RUNTIME_DIR is not set; inhibits will not survive a restart")
return return
assert self.conn is not None assert self.conn is not None
path = self.state_path
old = os.path.join(os.path.dirname(path), OLD_STATE_FILE)
if not os.path.exists(path) and os.path.exists(old):
path = old
try: try:
with open(self.state_path) as f: with open(path) as f:
state = json.load(f) state = json.load(f)
age = time.time() - (state["stopped_at"] or os.stat(self.state_path).st_mtime) age = time.time() - (state["stopped_at"] or os.stat(path).st_mtime)
bus_id = state["bus_id"] bus_id = state["bus_id"]
last_cookie = int(state["last_cookie"]) last_cookie = int(state["last_cookie"])
saved = [(int(c), s, a, r) for c, s, a, r in state["cookies"]] saved = [(int(c), s, a, r) for c, s, a, r in state["cookies"]]
except FileNotFoundError: except FileNotFoundError:
return return
except (OSError, ValueError, KeyError, TypeError) as e: except (OSError, ValueError, KeyError, TypeError) as e:
log(f"Ignoring unreadable state file {self.state_path}: {e}") log(f"Ignoring unreadable state file {path}: {e}")
return return
if path == old:
log(f"Reading inhibits from {old} (xautolock-inhibit)")
if bus_id != self.conn.get_guid(): if bus_id != self.conn.get_guid():
# Unique names like :1.42 are reused by a new bus, so they would mean other clients. # Unique names like :1.42 are reused by a new bus, so they would mean other clients.
if saved: if saved:
@@ -380,7 +429,10 @@ class Inhibitor:
invocation.return_value(None) invocation.return_value(None)
elif method == "Lock": elif method == "Lock":
log(f"Lock from {self.proc_name(sender)}") log(f"Lock from {self.proc_name(sender)}")
if self.xautolock:
xautolock("-locknow") xautolock("-locknow")
else:
run("loginctl", "lock-session")
invocation.return_value(None) invocation.return_value(None)
elif method in ("GetActive", "SetActive"): elif method in ("GetActive", "SetActive"):
invocation.return_value(GLib.Variant("(b)", (False,))) invocation.return_value(GLib.Variant("(b)", (False,)))
@@ -388,7 +440,7 @@ class Inhibitor:
invocation.return_value(GLib.Variant("(u)", (0,))) invocation.return_value(GLib.Variant("(u)", (0,)))
def on_name_owner_changed( def on_name_owner_changed(
self, conn: Gio.DBusConnection, sender: str, path: str, iface: str, signal_name: str, self, conn: Gio.DBusConnection, sender: str | None, path: str, iface: str, signal_name: str,
params: GLib.Variant, params: GLib.Variant,
) -> None: ) -> None:
name, _old, new = params.unpack() name, _old, new = params.unpack()
@@ -418,15 +470,25 @@ def main() -> None:
) )
parser.add_argument( parser.add_argument(
"-d", "--release-delay", type=int, default=5, metavar="SECONDS", "-d", "--release-delay", type=int, default=5, metavar="SECONDS",
help="wait this long after the last release before re-enabling xautolock (default: 5)", help="wait this long after the last release before unblocking locking (default: 5)",
) )
parser.add_argument( parser.add_argument(
"--restore-window", type=int, default=30, metavar="SECONDS", "--restore-window", type=int, default=30, metavar="SECONDS",
help="take over inhibits from a previous instance that stopped at most this long ago " help="take over inhibits from a previous instance that stopped at most this long ago "
"(default: 30; 0 disables)", "(default: 30; 0 disables)",
) )
parser.add_argument(
"--cleanup", action="store_true",
help="re-enable xautolock if it is the backend, then exit (for the unit's ExecStopPost)",
)
args = parser.parse_args() args = parser.parse_args()
backend = read_backend()
if args.cleanup:
if backend == "xautolock":
xautolock("-enable")
return
ignored: set[str] ignored: set[str]
if args.count_all: if args.count_all:
ignored = set() ignored = set()
@@ -434,8 +496,11 @@ def main() -> None:
ignored = set(args.ignore_reason or ["Playing audio"]) ignored = set(args.ignore_reason or ["Playing audio"])
runtime_dir = os.environ.get("XDG_RUNTIME_DIR") runtime_dir = os.environ.get("XDG_RUNTIME_DIR")
state_path = os.path.join(runtime_dir, STATE_FILE) if runtime_dir else None state_path = os.path.join(runtime_dir, STATE_FILE) if runtime_dir else None
blanking = XBlanking()
if backend == "xss-lock" and not blanking.dpy:
log("Warning: with xss-lock, inhibits block locking only through X blanking, which is unavailable")
inhibitor = Inhibitor( inhibitor = Inhibitor(
ignored, max(args.release_delay, 0), XBlanking(), state_path, max(args.restore_window, 0), backend, ignored, max(args.release_delay, 0), blanking, state_path, max(args.restore_window, 0),
) )
loop = GLib.MainLoop() loop = GLib.MainLoop()
exit_code = 0 exit_code = 0
@@ -444,7 +509,7 @@ def main() -> None:
def on_name_acquired(conn: Gio.DBusConnection, name: str) -> None: def on_name_acquired(conn: Gio.DBusConnection, name: str) -> None:
nonlocal acquired nonlocal acquired
acquired = True acquired = True
log(f"Owning {name}; ignored reasons: {sorted(ignored) or 'none'}") log(f"Owning {name}; backend: {backend}; ignored reasons: {sorted(ignored) or 'none'}")
# Only after owning the name, so a second instance can't undo an active inhibit # Only after owning the name, so a second instance can't undo an active inhibit
# or overwrite the state file. # or overwrite the state file.
inhibitor.start() inhibitor.start()
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Block screen locking (xautolock or xss-lock) on org.freedesktop.ScreenSaver inhibit requests
[Service]
Type=dbus
BusName=org.freedesktop.ScreenSaver
ExecStart=/usr/bin/xidle-inhibit
ExecStopPost=-/usr/bin/xidle-inhibit --cleanup
Restart=on-failure
RestartPreventExitStatus=2