#!/usr/bin/env python3 """ break-idle - Report idle/resume to break-event, ignoring idle inhibitors swayidle obeys idle inhibitors (e.g. Waybar's idle_inhibitor), which stops activity tracking entirely while "stay awake" is on. This speaks the Wayland wire protocol directly and uses ext-idle-notify-v1's version 2 get_input_idle_notification, which only considers real user input. Usage: break-idle [timeout_seconds] (default 5) """ import os import socket import struct import subprocess import sys from pathlib import Path DISPLAY_ID, REGISTRY_ID, CALLBACK_ID, SEAT_ID, NOTIFIER_ID, NOTIFICATION_ID = range(1, 7) BREAK_EVENT = Path(__file__).resolve().parent / "break-event" def encode_string(s: str) -> bytes: data = s.encode() + b"\0" return struct.pack("=I", len(data)) + data + b"\0" * (-len(data) % 4) def decode_string(payload: bytes, offset: int) -> tuple[str, int]: (length,) = struct.unpack_from("=I", payload, offset) start = offset + 4 return payload[start:start + length - 1].decode(), start + length + (-length % 4) def send(sock: socket.socket, obj: int, opcode: int, args: bytes) -> None: sock.sendall(struct.pack("=II", obj, (8 + len(args)) << 16 | opcode) + args) def messages(sock: socket.socket): buf = b"" while True: chunk = sock.recv(4096) if not chunk: sys.exit("break-idle: compositor closed the connection") buf += chunk while len(buf) >= 8: obj, size_op = struct.unpack_from("=II", buf) size = size_op >> 16 if len(buf) < size: break yield obj, size_op & 0xFFFF, buf[8:size] buf = buf[size:] def main() -> None: timeout = int(sys.argv[1]) if len(sys.argv) > 1 else 5 display = os.environ.get("WAYLAND_DISPLAY", "wayland-0") path = display if display.startswith("/") else os.path.join(os.environ["XDG_RUNTIME_DIR"], display) sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.connect(path) send(sock, DISPLAY_ID, 1, struct.pack("=I", REGISTRY_ID)) # wl_display.get_registry send(sock, DISPLAY_ID, 0, struct.pack("=I", CALLBACK_ID)) # wl_display.sync globals_: dict[str, tuple[int, int]] = {} for obj, opcode, payload in messages(sock): if obj == DISPLAY_ID and opcode == 0: # wl_display.error sys.exit(f"break-idle: protocol error: {decode_string(payload, 8)[0]}") if obj == REGISTRY_ID and opcode == 0: # wl_registry.global (name,) = struct.unpack_from("=I", payload) interface, offset = decode_string(payload, 4) (version,) = struct.unpack_from("=I", payload, offset) globals_.setdefault(interface, (name, version)) elif obj == CALLBACK_ID: # wl_callback.done: all globals received break notifier = globals_.get("ext_idle_notifier_v1") if "wl_seat" not in globals_ or not notifier or notifier[1] < 2: sys.exit("break-idle: compositor lacks ext_idle_notifier_v1 version 2 " "(needed to ignore idle inhibitors)") for interface, obj, version in (("wl_seat", SEAT_ID, 1), ("ext_idle_notifier_v1", NOTIFIER_ID, 2)): args = struct.pack("=I", globals_[interface][0]) + encode_string(interface) + struct.pack("=II", version, obj) send(sock, REGISTRY_ID, 0, args) # wl_registry.bind # ext_idle_notifier_v1.get_input_idle_notification(id, timeout_ms, seat) send(sock, NOTIFIER_ID, 2, struct.pack("=III", NOTIFICATION_ID, timeout * 1000, SEAT_ID)) for obj, opcode, payload in messages(sock): if obj == DISPLAY_ID and opcode == 0: sys.exit(f"break-idle: protocol error: {decode_string(payload, 8)[0]}") if obj == NOTIFICATION_ID: # 0 = idled, 1 = resumed subprocess.run([BREAK_EVENT, "idle_start" if opcode == 0 else "idle_end"]) if __name__ == "__main__": main()