Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@
![python version](https://img.shields.io/pypi/pyversions/dbus-idle.svg)
![license](https://img.shields.io/pypi/l/dbus-idle.svg)

Python library to detect user idle time in milliseconds or inactivity on Linux and Windows.
Python library to detect user idle time in milliseconds (and from that, inactivity) on Linux, Windows, and macOS.


## Requirements

* Python 3.7 or later
* One of the following:
* D-Bus with `org.freedesktop.DBus` interface
* `xprintidle` command
* libX11 with `XScreenSaverInfo`
* `swayidle` command
* Windows
* macOS


## Installation
Expand All @@ -20,6 +27,10 @@ sudo apt install meson libdbus-glib-1-dev patchelf
pip install dbus-idle
```

On non-Linux systems,
```
pip install dbus-idle
```

## Usage

Expand Down
68 changes: 55 additions & 13 deletions dbus_idle/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from sys import stdout
import time
import ctypes
import ctypes.util
Expand All @@ -12,10 +13,11 @@
class IdleMonitor:
subclasses: List[Type["IdleMonitor"]] = []

def __init__(self, *, idle_threshold: int = 120, debug: bool=False) -> None:
def __init__(self, *, idle_threshold: int = 120, debug: int | bool = 0) -> None:
self.idle_threshold = idle_threshold
self.class_used = None
if debug:
self.debug = int(debug)
if self.debug:
logger.setLevel(logging.DEBUG)

def __init_subclass__(self) -> None:
Expand All @@ -34,25 +36,35 @@ def get_monitor(self, **kwargs) -> "IdleMonitor":
logger.warning("Could not load %s", monitor_class, exc_info=True)
raise RuntimeError("Could not find a working monitor.")

def get_dbus_idle(self) -> float:
def get_dbus_idle(self) -> float | None:
"""
Return idle time in milliseconds.
"""
if self.class_used is None:
for monitor_class in self.subclasses:
try:
self.class_used = monitor_class()
logger.debug("Using: %s", monitor_class.__name__)
logger.info("Using: %s", monitor_class.__name__)
return self.class_used.get_dbus_idle()
except Exception:
logger.info("Could not load %s", monitor_class.__name__, exc_info=False)
logger.warning("Could not find any working monitor to get idle time.", exc_info=True)
logger.debug(
"Could not load %s",
monitor_class.__name__,
exc_info=self.debug > 1,
)
logger.warning(
"Could not find any working monitor to get idle time.",
exc_info=False,
)
return None
else:
try:
return self.class_used.get_dbus_idle()
except Exception as e:
logger.warning("Can't run the working monitor enymore.", exc_info=False)
except Exception:
logger.warning(
"Can't run the working monitor enymore.",
exc_info=self.debug > 1,
)
return None

def is_idle(self) -> bool:
Expand All @@ -64,7 +76,7 @@ def is_idle(self) -> bool:

class DBusIdleMonitor(IdleMonitor):
"""
Idle monitor for gnome running on wayland.
Idle monitor for gnome running on wayland (DBus IdleMonitor).

Based on
* https://unix.stackexchange.com/a/492328
Expand Down Expand Up @@ -123,7 +135,7 @@ def get_dbus_idle(self) -> float:

class X11IdleMonitor(IdleMonitor):
"""
Idle monitor for systems running X11.
Idle monitor for systems running X11 (XScreenSaverInfo).

Based on
* http://tperl.blogspot.com/2007/09/x11-idle-time-and-focused-window-in.html
Expand Down Expand Up @@ -212,7 +224,7 @@ def get_dbus_idle(self) -> float:

class WindowsIdleMonitor(IdleMonitor):
"""
Idle monitor for Windows.
Idle monitor for Windows (GetLastInputInfo).

Based on
* https://stackoverflow.com/q/911856
Expand All @@ -224,6 +236,36 @@ def __init__(self, **kwargs) -> None:
self.win32api = win32api

def get_dbus_idle(self) -> float:
current_tick = self.win32api.GetTickCount()
last_tick = self.win32api.GetLastInputInfo()
return current_tick - last_tick
current_tick = self.win32api.GetTickCount()
# Handle wraparound
return current_tick - last_tick + (2**32 if current_tick < last_tick else 0)

class IORegIdleMonitor(IdleMonitor):
"""
Idle monitor for macOS (IOHIDSystem.HIDIdleTime).

Based on
* https://stackoverflow.com/a/17966890
"""

def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
import plistlib
self.plistlib = plistlib
# make sure we can run
self.get_dbus_idle()

def get_dbus_idle(self) -> float:
command = subprocess.run(
["ioreg", "-arc", "IOHIDSystem"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if command.returncode != 0:
raise RuntimeError(
f"ioreg -arc IOHIDSystem returned {command.returncode}."
)
plist = self.plistlib.loads(command.stdout)

return plist[0]["HIDIdleTime"] / 1_000_000
32 changes: 30 additions & 2 deletions dbus_idle/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,41 @@
import argparse


def skim_docstring(im: type[IdleMonitor]) -> str:
"""Return the first line of the given IdleMonitor subclass."""
doc = im.__doc__
if doc is None:
return "No description available."
doclines = doc.strip().split("\n")
if len(doclines) == 0:
return "No description available."
return doclines[0]


def main():
parser = argparse.ArgumentParser(
prog="dbus-idle",
description="Get idle time in seconds from DBus")
parser.add_argument("-d", "--debug", action="store_true", help="Show debug messeges")
description="Get idle time in seconds from one of several sources.")
parser.add_argument(
"-d",
"--debug",
action="count",
help="Show debug messeges (repeat for exception trace)",
default=0,
)
parser.add_argument(
"-l", "--list", action="store_true", help="List available monitors"
)
args = parser.parse_args()

idle_monitor = IdleMonitor(debug=args.debug)
if args.list:
print("Available monitors:")
for monitor_class in IdleMonitor.subclasses:
print(f" - {monitor_class.__name__}: {skim_docstring(monitor_class)}")
milliseconds = idle_monitor.get_dbus_idle()
print(milliseconds)


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ classifiers = [
"Operating System :: OS Independent",
]
dependencies = [
"jeepney>=0.9.0 ; platform_system != 'Windows'",
"jeepney>=0.9.0 ; platform_system not in ('Windows', 'Darwin')",
"pywin32>=221 ; platform_system == 'Windows'",
]

Expand Down