From 5dcfe196f42b6101a85c02675cb02112b242abe7 Mon Sep 17 00:00:00 2001 From: deltragon Date: Fri, 17 May 2024 16:26:52 +0200 Subject: [PATCH] Smartpause: add support for ext-idle-notify-v1 using pywayland --- safeeyes/plugins/smartpause/config.json | 2 +- .../plugins/smartpause/dependency_checker.py | 5 + .../plugins/smartpause/ext_idle_notify.py | 93 +++++++++++++++++++ safeeyes/plugins/smartpause/plugin.py | 39 ++++++++ setup.py | 1 + 5 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 safeeyes/plugins/smartpause/ext_idle_notify.py diff --git a/safeeyes/plugins/smartpause/config.json b/safeeyes/plugins/smartpause/config.json index 9ca35a6..5c52c8a 100644 --- a/safeeyes/plugins/smartpause/config.json +++ b/safeeyes/plugins/smartpause/config.json @@ -5,7 +5,7 @@ "version": "0.0.3" }, "dependencies": { - "python_modules": [], + "python_modules": ["pywayland"], "shell_commands": [], "operating_systems": [], "desktop_environments": [], diff --git a/safeeyes/plugins/smartpause/dependency_checker.py b/safeeyes/plugins/smartpause/dependency_checker.py index d6526f7..6d9b7b2 100644 --- a/safeeyes/plugins/smartpause/dependency_checker.py +++ b/safeeyes/plugins/smartpause/dependency_checker.py @@ -25,6 +25,11 @@ def validate(plugin_config, plugin_settings): command = "dbus-send" elif utility.DESKTOP_ENVIRONMENT == "sway": command = "swayidle" + elif utility.IS_WAYLAND: + if not utility.module_exist("pywayland"): + return _("Please install the Python module '%s'") % "pywayland" + # no command needed with pywayland + return None else: command = "xprintidle" if not utility.command_exist(command): diff --git a/safeeyes/plugins/smartpause/ext_idle_notify.py b/safeeyes/plugins/smartpause/ext_idle_notify.py new file mode 100644 index 0000000..ad85e8c --- /dev/null +++ b/safeeyes/plugins/smartpause/ext_idle_notify.py @@ -0,0 +1,93 @@ +# Safe Eyes is a utility to remind you to take break frequently +# to protect your eyes from eye strain. + +# Copyright (C) 2017 Gobinath + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# This file is heavily inspired by https://github.com/juienpro/easyland/blob/efc26a0b22d7bdbb0f8436183428f7036da4662a/src/easyland/idle.py + +import logging +import threading +import datetime + +from pywayland.client import Display +from pywayland.protocol.wayland.wl_seat import WlSeat +from pywayland.protocol.ext_idle_notify_v1 import ( + ExtIdleNotificationV1, + ExtIdleNotifierV1 +) + + +class ExtIdleNotify: + _idle_notifier = None + _seat = None + _notification = None + _notifier_set = False + _running = True + _thread = None + + _idle_since = None + + def __init__(self): + self._display = Display() + self._display.connect() + + def stop(self): + self._running = False + self._notification.destroy() + self._notification = None + self._seat = None + self._thread.join() + + def run(self): + self._thread = threading.Thread(target=self._run, name="ExtIdleNotify", daemon=False) + self._thread.start() + + def _run(self): + reg = self._display.get_registry() + reg.dispatcher['global'] = self._global_handler + + while self._running: + self._display.dispatch(block=True) + + self._display.disconnect() + + def _global_handler(self, reg, id_num, iface_name, version): + if iface_name == 'wl_seat': + self._seat = reg.bind(id_num, WlSeat, version) + if iface_name == "ext_idle_notifier_v1": + self._idle_notifier = reg.bind(id_num, ExtIdleNotifierV1, version) + + if self._idle_notifier and self._seat and not self._notifier_set: + self._notifier_set = True + timeout_sec = 1 + self._notification = self._idle_notifier.get_idle_notification(timeout_sec * 1000, self._seat) + self._notification.dispatcher['idled'] = self._idle_notifier_handler + self._notification.dispatcher['resumed'] = self._idle_notifier_resume_handler + + def _idle_notifier_handler(self, notification): + self._idle_since = datetime.datetime.now() + + def _idle_notifier_resume_handler(self, notification): + self._idle_since = None + + def get_idle_time_seconds(self): + if self._idle_since is None: + return 0 + + result = datetime.datetime.now() - self._idle_since + return result.total_seconds() + + diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index 296fe51..04ce376 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -46,11 +46,16 @@ waiting_time = 2 is_wayland_and_gnome = False use_swayidle = False +use_ext_idle_notify = False swayidle_process = None swayidle_lock = threading.Lock() swayidle_idle = 0 swayidle_active = 0 +ext_idle_notify_lock = threading.Lock() +ext_idle_notification_obj = None + +# swayidle def __swayidle_running(): return (swayidle_process is not None and swayidle_process.poll() is None) @@ -88,6 +93,33 @@ def __swayidle_idle_time(): return idle_time return 0 +# ext idle +def __start_ext_idle_monitor(): + global ext_idle_notification_obj + + from .ext_idle_notify import ExtIdleNotify + + ext_idle_notification_obj = ExtIdleNotify() + ext_idle_notification_obj.run() + +def __stop_ext_idle_monitor(): + global ext_idle_notification_obj + + with ext_idle_notify_lock: + if ext_idle_notification_obj is not None: + ext_idle_notification_obj.stop() + ext_idle_notification_obj = None; + +def __ext_idle_idle_time(): + global ext_idle_notification_obj + with ext_idle_notify_lock: + if ext_idle_notification_obj is None: + __start_ext_idle_monitor() + else: + return ext_idle_notification_obj.get_idle_time_seconds() + return 0 + +# gnome def __gnome_wayland_idle_time(): """ Determine system idle time in seconds, specifically for gnome with wayland. @@ -119,6 +151,8 @@ def __system_idle_time(): return __gnome_wayland_idle_time() elif use_swayidle: return __swayidle_idle_time() + elif use_ext_idle_notify: + return __ext_idle_idle_time() # Convert to seconds return int(subprocess.check_output(['xprintidle']).decode('utf-8')) / 1000 except BaseException: @@ -159,6 +193,7 @@ def init(ctx, safeeyes_config, plugin_config): global postpone_if_active global is_wayland_and_gnome global use_swayidle + global use_ext_idle_notify logging.debug('Initialize Smart Pause plugin') context = ctx enable_safeeyes = context['api']['enable_safeeyes'] @@ -172,6 +207,7 @@ def init(ctx, safeeyes_config, plugin_config): waiting_time = min(2, idle_time) # If idle time is 1 sec, wait only 1 sec is_wayland_and_gnome = context['desktop'] == 'gnome' and context['is_wayland'] use_swayidle = context['desktop'] == 'sway' + use_ext_idle_notify = context['is_wayland'] and not use_swayidle and not is_wayland_and_gnome def __start_idle_monitor(): @@ -245,6 +281,9 @@ def on_stop(): idle_condition.notify_all() idle_condition.release() + if use_ext_idle_notify: + __stop_ext_idle_monitor() + def update_next_break(break_obj, dateTime): """ diff --git a/setup.py b/setup.py index dc89916..a963bed 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,7 @@ import setuptools requires = [ + 'pywayland', 'babel', 'psutil', 'croniter',