From 8ffd1b9369f01019f6750a8def1be750fba75592 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sat, 22 Feb 2020 19:10:13 -0800 Subject: [PATCH 01/13] smartpause: factor out X11 implementation --- safeeyes/plugins/smartpause/plugin.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index 8bd8db02..924229c8 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -68,6 +68,11 @@ def __gnome_wayland_idle_time(): return 0 +def __x11_idle_time(): + # Convert to seconds + return int(subprocess.check_output(['xprintidle']).decode('utf-8')) / 1000 + + def __system_idle_time(): """ Get system idle time in minutes. @@ -76,8 +81,8 @@ def __system_idle_time(): try: if is_wayland_and_gnome: return __gnome_wayland_idle_time() - # Convert to seconds - return int(subprocess.check_output(['xprintidle']).decode('utf-8')) / 1000 + else: + return __x11_idle_time() except BaseException: return 0 From f95755d2e9feb8c167a8c6e85d2c3fc427756266 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sat, 22 Feb 2020 20:46:56 -0800 Subject: [PATCH 02/13] smartpause: replace xprintidle subprocess with equivalent X11 API call --- .../plugins/smartpause/dependency_checker.py | 11 +++---- safeeyes/plugins/smartpause/plugin.py | 30 +++++++++++++++---- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/safeeyes/plugins/smartpause/dependency_checker.py b/safeeyes/plugins/smartpause/dependency_checker.py index 9d186134..c2d8fce0 100644 --- a/safeeyes/plugins/smartpause/dependency_checker.py +++ b/safeeyes/plugins/smartpause/dependency_checker.py @@ -20,12 +20,9 @@ def validate(plugin_config): - command = None if Utility.DESKTOP_ENVIRONMENT == "gnome" and Utility.IS_WAYLAND: command = "dbus-send" - else: - command = "xprintidle" - if not Utility.command_exist(command): - return _("Please install the command-line tool '%s'") % command - else: - return None + if not Utility.command_exist(command): + return _("Please install the command-line tool '%s'") % command + + return None diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index 924229c8..2895f8ff 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -22,6 +22,11 @@ import threading import re import os +from typing import Optional + +import xcffib +import xcffib.xproto +import xcffib.screensaver from safeeyes import Utility from safeeyes.model import State @@ -45,6 +50,7 @@ waiting_time = 2 interpret_idle_as_break = False is_wayland_and_gnome = False +xcb_connection: Optional[xcffib.Connection] = None def __gnome_wayland_idle_time(): @@ -69,14 +75,22 @@ def __gnome_wayland_idle_time(): def __x11_idle_time(): - # Convert to seconds - return int(subprocess.check_output(['xprintidle']).decode('utf-8')) / 1000 + assert xcb_connection is not None + try: + ext = xcb_connection(xcffib.screensaver.key) + root_window = xcb_connection.get_setup().roots[0].root + query = ext.QueryInfo(root_window) + info = query.reply() + # Convert to seconds + return info.ms_since_user_input / 1000 + except Exception: + logging.warning("Failed to get system idle time from XScreenSaver API", exc_info=True) + return 0 def __system_idle_time(): """ - Get system idle time in minutes. - Return the idle time if xprintidle is available, otherwise return 0. + Get system idle time in seconds. """ try: if is_wayland_and_gnome: @@ -120,6 +134,7 @@ def init(ctx, safeeyes_config, plugin_config): global interpret_idle_as_break global postpone_if_active global is_wayland_and_gnome + global xcb_connection logging.debug('Initialize Smart Pause plugin') context = ctx enable_safe_eyes = context['api']['enable_safeeyes'] @@ -176,7 +191,8 @@ def on_start(): """ Start a thread to continuously call xprintidle. """ - global active + global xcb_connection + xcb_connection = xcffib.connect() if not __is_active(): # If SmartPause is already started, do not start it again logging.debug('Start Smart Pause plugin') @@ -190,6 +206,10 @@ def on_stop(): """ global active global smart_pause_activated + global xcb_connection + if xcb_connection is not None: + xcb_connection.disconnect() + xcb_connection = None if smart_pause_activated: # Safe Eyes is stopped due to system idle smart_pause_activated = False From 2bb041da4db0cc81b540251fcdf6205d97ffcb0f Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sat, 22 Feb 2020 21:01:21 -0800 Subject: [PATCH 03/13] update requirements replacing xprintidle with xcffib --- debian/control | 2 +- setup.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/debian/control b/debian/control index 609e15af..2f6d5669 100644 --- a/debian/control +++ b/debian/control @@ -9,7 +9,7 @@ Homepage: https://github.com/slgobinath/SafeEyes/ Package: safeeyes Architecture: all -Depends: ${misc:Depends}, ${python3:Depends}, gir1.2-appindicator3-0.1, python3 (>= 3.4.0), python3-xlib, python3-dbus, gir1.2-notify-0.7, python3-babel, x11-utils, xprintidle, alsa-utils, python3-psutil +Depends: ${misc:Depends}, ${python3:Depends}, gir1.2-appindicator3-0.1, python3 (>= 3.4.0), python3-xlib, python3-dbus, gir1.2-notify-0.7, python3-babel, x11-utils, python3-xcffib, alsa-utils, python3-psutil Description: Safe Eyes Safe Eyes is a simple tool to remind you to take periodic breaks for your eyes. This is essential for anyone spending more time on the computer to avoid eye strain and other physical problems. . diff --git a/setup.py b/setup.py index ca9b234f..3715c713 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,8 @@ 'babel', 'psutil', 'PyGObject', - 'python-xlib' + 'python-xlib', + 'xcffib' ] _ROOT = os.path.abspath(os.path.dirname(__file__)) From 36b5923453326c59b5ffb6e57089da1ad8237467 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sun, 23 Feb 2020 10:11:24 -0800 Subject: [PATCH 04/13] smartpause: type hints --- safeeyes/plugins/smartpause/plugin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index 2895f8ff..88e64d08 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -22,7 +22,7 @@ import threading import re import os -from typing import Optional +from typing import Optional, Callable import xcffib import xcffib.xproto @@ -35,16 +35,16 @@ Safe Eyes smart pause plugin """ -context = None idle_condition = threading.Condition() lock = threading.Lock() +context: Optional[dict] = None active = False idle_time = 0 -enable_safe_eyes = None -disable_safe_eyes = None +enable_safe_eyes: Optional[Callable[[Optional[int]], None]] = None +disable_safe_eyes: Optional[Callable[[Optional[str]], None]] = None smart_pause_activated = False -idle_start_time = None -next_break_time = None +idle_start_time: Optional[datetime.datetime] = None +next_break_time: Optional[datetime.datetime] = None next_break_duration = 0 break_interval = 0 waiting_time = 2 From 4365b719942a5f4ca54f88a4daa35439e62f615a Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sun, 23 Feb 2020 11:25:32 -0800 Subject: [PATCH 05/13] smartpause: replace thread with timed events --- safeeyes/plugins/smartpause/plugin.py | 128 +++++++++++++------------- 1 file changed, 62 insertions(+), 66 deletions(-) diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index 88e64d08..1d66fe6a 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -19,24 +19,23 @@ import datetime import logging import subprocess -import threading import re -import os from typing import Optional, Callable import xcffib import xcffib.xproto import xcffib.screensaver -from safeeyes import Utility from safeeyes.model import State +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import GLib + """ Safe Eyes smart pause plugin """ -idle_condition = threading.Condition() -lock = threading.Lock() context: Optional[dict] = None active = False idle_time = 0 @@ -50,6 +49,7 @@ waiting_time = 2 interpret_idle_as_break = False is_wayland_and_gnome = False +timer: Optional[int] = None xcb_connection: Optional[xcffib.Connection] = None @@ -92,32 +92,25 @@ def __system_idle_time(): """ Get system idle time in seconds. """ - try: - if is_wayland_and_gnome: - return __gnome_wayland_idle_time() - else: - return __x11_idle_time() - except BaseException: - return 0 + if is_wayland_and_gnome: + return __gnome_wayland_idle_time() + else: + return __x11_idle_time() def __is_active(): """ - Thread safe function to see if this plugin is active or not. + Function to see if this plugin is active or not. """ - is_active = False - with lock: - is_active = active - return is_active + return active def __set_active(is_active): """ - Thread safe function to change the state of the plugin. + Function to change the state of the plugin. """ global active - with lock: - active = is_active + active = is_active def init(ctx, safeeyes_config, plugin_config): @@ -149,76 +142,79 @@ def init(ctx, safeeyes_config, plugin_config): is_wayland_and_gnome = context['desktop'] == 'gnome' and context['is_wayland'] -def __start_idle_monitor(): +def __idle_monitor(): """ - Continuously check the system idle time and pause/resume Safe Eyes based on it. + Check the system idle time and pause/resume Safe Eyes based on it. """ global smart_pause_activated global idle_start_time - while __is_active(): - # Wait for waiting_time seconds - idle_condition.acquire() - idle_condition.wait(waiting_time) - idle_condition.release() - - if __is_active(): - # Get the system idle time - system_idle_time = __system_idle_time() - if system_idle_time >= idle_time and context['state'] == State.WAITING: - smart_pause_activated = True - idle_start_time = datetime.datetime.now() - logging.info('Pause Safe Eyes due to system idle') - disable_safe_eyes(None) - elif system_idle_time < idle_time and context['state'] == State.STOPPED: - logging.info('Resume Safe Eyes due to user activity') - smart_pause_activated = False - idle_period = (datetime.datetime.now() - idle_start_time) - idle_seconds = idle_period.total_seconds() - context['idle_period'] = idle_seconds - if interpret_idle_as_break and idle_seconds >= next_break_duration: - # User is idle for break duration and wants to consider it as a break - enable_safe_eyes() - elif idle_seconds < break_interval: - # Credit back the idle time - next_break = next_break_time + idle_period - enable_safe_eyes(next_break.timestamp()) - else: - # User is idle for more than the time between two breaks - enable_safe_eyes() + + if not __is_active(): + return False # stop the timeout handler. + + system_idle_time = __system_idle_time() + if system_idle_time >= idle_time and context['state'] == State.WAITING: + smart_pause_activated = True + idle_start_time = datetime.datetime.now() + logging.info('Pause Safe Eyes due to system idle') + disable_safe_eyes(None) + elif system_idle_time < idle_time and context['state'] == State.STOPPED: + logging.info('Resume Safe Eyes due to user activity') + smart_pause_activated = False + idle_period = (datetime.datetime.now() - idle_start_time) + idle_seconds = idle_period.total_seconds() + context['idle_period'] = idle_seconds + if interpret_idle_as_break and idle_seconds >= next_break_duration: + # User is idle for break duration and wants to consider it as a break + enable_safe_eyes() + elif idle_seconds < break_interval: + # Credit back the idle time + next_break = next_break_time + idle_period + enable_safe_eyes(next_break.timestamp()) + else: + # User is idle for more than the time between two breaks + enable_safe_eyes() + + return True # keep this timeout handler registered. def on_start(): """ - Start a thread to continuously call xprintidle. + Begin polling to check user idle time. """ global xcb_connection - xcb_connection = xcffib.connect() - if not __is_active(): + global timer + + if __is_active(): # If SmartPause is already started, do not start it again - logging.debug('Start Smart Pause plugin') - __set_active(True) - Utility.start_thread(__start_idle_monitor) + return + + logging.debug('Start Smart Pause plugin') + __set_active(True) + xcb_connection = xcffib.connect() # TODO: whether we need this depends on the monitor method + + # FIXME: need to make sure that this gets updated if the waiting_time config changes + timer = GLib.timeout_add_seconds(waiting_time, __idle_monitor) def on_stop(): """ - Stop the thread from continuously calling xprintidle. + Stop polling to check user idle time. """ - global active global smart_pause_activated + global timer global xcb_connection - if xcb_connection is not None: - xcb_connection.disconnect() - xcb_connection = None if smart_pause_activated: # Safe Eyes is stopped due to system idle smart_pause_activated = False return logging.debug('Stop Smart Pause plugin') __set_active(False) - idle_condition.acquire() - idle_condition.notify_all() - idle_condition.release() + GLib.source_remove(timer) + timer = None + if xcb_connection is not None: + xcb_connection.disconnect() + xcb_connection = None def update_next_break(break_obj, dateTime): From 9f6a996c3866ceee780f72ac332ccf017c194c19 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sun, 23 Feb 2020 11:28:28 -0800 Subject: [PATCH 06/13] smartpause: fix docstring placement --- safeeyes/plugins/smartpause/plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index 1d66fe6a..79139695 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -15,6 +15,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +""" +Safe Eyes smart pause plugin +""" import datetime import logging @@ -32,9 +35,6 @@ gi.require_version('Gtk', '3.0') from gi.repository import GLib -""" -Safe Eyes smart pause plugin -""" context: Optional[dict] = None active = False From 7c043620313fc0a692b64310223b1e5f3e84794c Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sun, 23 Feb 2020 11:40:34 -0800 Subject: [PATCH 07/13] =?UTF-8?q?smartpause:=20linting=20=F0=9F=9A=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- safeeyes/plugins/smartpause/plugin.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index 79139695..f53b5875 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -41,6 +41,7 @@ idle_time = 0 enable_safe_eyes: Optional[Callable[[Optional[int]], None]] = None disable_safe_eyes: Optional[Callable[[Optional[str]], None]] = None +postpone: Optional[Callable[[Optional[int]], None]] = None smart_pause_activated = False idle_start_time: Optional[datetime.datetime] = None next_break_time: Optional[datetime.datetime] = None @@ -48,6 +49,7 @@ break_interval = 0 waiting_time = 2 interpret_idle_as_break = False +postpone_if_active = False is_wayland_and_gnome = False timer: Optional[int] = None xcb_connection: Optional[xcffib.Connection] = None @@ -59,6 +61,7 @@ def __gnome_wayland_idle_time(): If there's a failure, return 0. https://unix.stackexchange.com/a/492328/222290 """ + # noinspection PyBroadException try: output = subprocess.check_output([ 'dbus-send', @@ -68,14 +71,14 @@ def __gnome_wayland_idle_time(): 'org.gnome.Mutter.IdleMonitor.GetIdletime' ]) return int(re.search(rb'\d+$', output).group(0)) / 1000 - except BaseException as e: - logging.warning("Failed to get system idle time for gnome/wayland.") - logging.warning(str(e)) + except Exception: + logging.warning("Failed to get system idle time for gnome/wayland.", exc_info=True) return 0 def __x11_idle_time(): assert xcb_connection is not None + # noinspection PyBroadException try: ext = xcb_connection(xcffib.screensaver.key) root_window = xcb_connection.get_setup().roots[0].root @@ -217,17 +220,17 @@ def on_stop(): xcb_connection = None -def update_next_break(break_obj, dateTime): +def update_next_break(break_obj, break_time): """ Update the next break time. """ global next_break_time global next_break_duration - next_break_time = dateTime + next_break_time = break_time next_break_duration = break_obj.duration -def on_start_break(break_obj): +def on_start_break(_break_obj): """ Lifecycle method executes just before the break. """ From 88f2c3c950b5a0e973ed6d26b38eeeede6417313 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sun, 23 Feb 2020 13:51:21 -0800 Subject: [PATCH 08/13] smartpause: refactor idle checker functions to objects because they may require their own resources --- safeeyes/plugins/smartpause/interface.py | 25 +++++ safeeyes/plugins/smartpause/plugin.py | 117 ++++++++++++++--------- 2 files changed, 99 insertions(+), 43 deletions(-) create mode 100644 safeeyes/plugins/smartpause/interface.py diff --git a/safeeyes/plugins/smartpause/interface.py b/safeeyes/plugins/smartpause/interface.py new file mode 100644 index 00000000..5d508805 --- /dev/null +++ b/safeeyes/plugins/smartpause/interface.py @@ -0,0 +1,25 @@ +from abc import ABC, abstractmethod +from numbers import Integral + + +class IdleTimeInterface(ABC): + """Tells how long the user has been idle.""" + # This could be a typing.Protocol in Python 3.8. + + @classmethod + @abstractmethod + def is_applicable(cls, context) -> bool: + """Is this implementation usable in this context?""" + pass + + @abstractmethod + def idle_seconds(self) -> Integral: + """Time since last user input, in seconds. + + Returns 0 on failure.""" + pass + + @abstractmethod + def destroy(self) -> None: + """Destroy this checker (clean up any resources).""" + pass diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index f53b5875..f62e2f73 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -29,7 +29,9 @@ import xcffib.xproto import xcffib.screensaver +from safeeyes import Utility from safeeyes.model import State +from .interface import IdleTimeInterface import gi gi.require_version('Gtk', '3.0') @@ -53,52 +55,76 @@ is_wayland_and_gnome = False timer: Optional[int] = None xcb_connection: Optional[xcffib.Connection] = None +idle_checker: Optional[IdleTimeInterface] = None -def __gnome_wayland_idle_time(): +class GnomeWaylandIdleTime(IdleTimeInterface): """ Determine system idle time in seconds, specifically for gnome with wayland. - If there's a failure, return 0. https://unix.stackexchange.com/a/492328/222290 """ - # noinspection PyBroadException - try: - output = subprocess.check_output([ - 'dbus-send', - '--print-reply', - '--dest=org.gnome.Mutter.IdleMonitor', - '/org/gnome/Mutter/IdleMonitor/Core', - 'org.gnome.Mutter.IdleMonitor.GetIdletime' - ]) - return int(re.search(rb'\d+$', output).group(0)) / 1000 - except Exception: - logging.warning("Failed to get system idle time for gnome/wayland.", exc_info=True) - return 0 - - -def __x11_idle_time(): - assert xcb_connection is not None - # noinspection PyBroadException - try: - ext = xcb_connection(xcffib.screensaver.key) - root_window = xcb_connection.get_setup().roots[0].root - query = ext.QueryInfo(root_window) - info = query.reply() - # Convert to seconds - return info.ms_since_user_input / 1000 - except Exception: - logging.warning("Failed to get system idle time from XScreenSaver API", exc_info=True) - return 0 - - -def __system_idle_time(): - """ - Get system idle time in seconds. - """ - if is_wayland_and_gnome: - return __gnome_wayland_idle_time() - else: - return __x11_idle_time() + + @classmethod + def is_applicable(cls, ctx) -> bool: + if ctx['desktop'] == 'gnome' and ctx['is_wayland']: + # ? Might work in all Gnome environments running Mutter whether they're Wayland or X? + return Utility.command_exist("dbus-send") + + return False + + def idle_seconds(self): + # noinspection PyBroadException + try: + output = subprocess.check_output([ + 'dbus-send', + '--print-reply', + '--dest=org.gnome.Mutter.IdleMonitor', + '/org/gnome/Mutter/IdleMonitor/Core', + 'org.gnome.Mutter.IdleMonitor.GetIdletime' + ]) + return int(re.search(rb'\d+$', output).group(0)) / 1000 + except Exception: + logging.warning("Failed to get system idle time for gnome/wayland.", exc_info=True) + return 0 + + def destroy(self) -> None: + pass + + +class X11IdleTime(IdleTimeInterface): + + @classmethod + def is_applicable(cls, _context) -> bool: + return True + + def idle_seconds(self): + # noinspection PyBroadException + try: + ext = xcb_connection(xcffib.screensaver.key) + root_window = xcb_connection.get_setup().roots[0].root + query = ext.QueryInfo(root_window) + info = query.reply() + # Convert to seconds + return info.ms_since_user_input / 1000 + except Exception: + logging.warning("Failed to get system idle time from XScreenSaver API", exc_info=True) + return 0 + + def destroy(self) -> None: + pass + + +_idle_checkers = [ + GnomeWaylandIdleTime, + X11IdleTime +] + + +def idle_checker_for_platform(): + for cls in _idle_checkers: + if cls.is_applicable(context): + return cls() + return None def __is_active(): @@ -130,7 +156,6 @@ def init(ctx, safeeyes_config, plugin_config): global interpret_idle_as_break global postpone_if_active global is_wayland_and_gnome - global xcb_connection logging.debug('Initialize Smart Pause plugin') context = ctx enable_safe_eyes = context['api']['enable_safeeyes'] @@ -155,7 +180,7 @@ def __idle_monitor(): if not __is_active(): return False # stop the timeout handler. - system_idle_time = __system_idle_time() + system_idle_time = idle_checker.idle_seconds() if system_idle_time >= idle_time and context['state'] == State.WAITING: smart_pause_activated = True idle_start_time = datetime.datetime.now() @@ -185,6 +210,7 @@ def on_start(): """ Begin polling to check user idle time. """ + global idle_checker global xcb_connection global timer @@ -193,6 +219,8 @@ def on_start(): return logging.debug('Start Smart Pause plugin') + idle_checker = idle_checker_for_platform() + __set_active(True) xcb_connection = xcffib.connect() # TODO: whether we need this depends on the monitor method @@ -206,6 +234,7 @@ def on_stop(): """ global smart_pause_activated global timer + global idle_checker global xcb_connection if smart_pause_activated: # Safe Eyes is stopped due to system idle @@ -215,6 +244,8 @@ def on_stop(): __set_active(False) GLib.source_remove(timer) timer = None + idle_checker.destroy() + idle_checker = None if xcb_connection is not None: xcb_connection.disconnect() xcb_connection = None @@ -236,7 +267,7 @@ def on_start_break(_break_obj): """ if postpone_if_active: # Postpone this break if the user is active - system_idle_time = __system_idle_time() + system_idle_time = idle_checker.idle_seconds() if system_idle_time < 2: postpone(2) # Postpone for 2 seconds From 5d75cc70d38e78c192f92ff8a2b2b6a30363239e Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sun, 23 Feb 2020 13:55:11 -0800 Subject: [PATCH 09/13] smartpause: keep wayland logic in the wayland checker --- safeeyes/plugins/smartpause/plugin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index f62e2f73..001840b5 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -52,7 +52,6 @@ waiting_time = 2 interpret_idle_as_break = False postpone_if_active = False -is_wayland_and_gnome = False timer: Optional[int] = None xcb_connection: Optional[xcffib.Connection] = None idle_checker: Optional[IdleTimeInterface] = None @@ -155,7 +154,6 @@ def init(ctx, safeeyes_config, plugin_config): global waiting_time global interpret_idle_as_break global postpone_if_active - global is_wayland_and_gnome logging.debug('Initialize Smart Pause plugin') context = ctx enable_safe_eyes = context['api']['enable_safeeyes'] @@ -167,7 +165,6 @@ def init(ctx, safeeyes_config, plugin_config): break_interval = safeeyes_config.get( 'short_break_interval') * 60 # Convert to seconds 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'] def __idle_monitor(): From 2aba4e19bf4cc5fd9530c402bfc8b9dff6be7c72 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sun, 23 Feb 2020 14:13:22 -0800 Subject: [PATCH 10/13] smartpause: only make xcffib connection when using that idle type which is why we refactored these to objects. --- safeeyes/plugins/smartpause/plugin.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index 001840b5..b0370ccf 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -53,7 +53,6 @@ interpret_idle_as_break = False postpone_if_active = False timer: Optional[int] = None -xcb_connection: Optional[xcffib.Connection] = None idle_checker: Optional[IdleTimeInterface] = None @@ -92,6 +91,10 @@ def destroy(self) -> None: class X11IdleTime(IdleTimeInterface): + def __init__(self): + self.connection = xcffib.connect() + self.screensaver_ext = self.connection(xcffib.screensaver.key) + @classmethod def is_applicable(cls, _context) -> bool: return True @@ -99,9 +102,8 @@ def is_applicable(cls, _context) -> bool: def idle_seconds(self): # noinspection PyBroadException try: - ext = xcb_connection(xcffib.screensaver.key) - root_window = xcb_connection.get_setup().roots[0].root - query = ext.QueryInfo(root_window) + root_window = self.connection.get_setup().roots[0].root + query = self.screensaver_ext.QueryInfo(root_window) info = query.reply() # Convert to seconds return info.ms_since_user_input / 1000 @@ -110,7 +112,7 @@ def idle_seconds(self): return 0 def destroy(self) -> None: - pass + self.connection.disconnect() _idle_checkers = [ @@ -208,7 +210,6 @@ def on_start(): Begin polling to check user idle time. """ global idle_checker - global xcb_connection global timer if __is_active(): @@ -219,7 +220,6 @@ def on_start(): idle_checker = idle_checker_for_platform() __set_active(True) - xcb_connection = xcffib.connect() # TODO: whether we need this depends on the monitor method # FIXME: need to make sure that this gets updated if the waiting_time config changes timer = GLib.timeout_add_seconds(waiting_time, __idle_monitor) @@ -232,7 +232,7 @@ def on_stop(): global smart_pause_activated global timer global idle_checker - global xcb_connection + if smart_pause_activated: # Safe Eyes is stopped due to system idle smart_pause_activated = False @@ -241,11 +241,9 @@ def on_stop(): __set_active(False) GLib.source_remove(timer) timer = None - idle_checker.destroy() - idle_checker = None - if xcb_connection is not None: - xcb_connection.disconnect() - xcb_connection = None + if idle_checker is not None: + idle_checker.destroy() + idle_checker = None def update_next_break(break_obj, break_time): From f15939d831698c6588432690b03de42345ae600a Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sun, 23 Feb 2020 14:23:35 -0800 Subject: [PATCH 11/13] smartpause: s/timer/_timer_event_id/ trying to make it clearer that this is part of the event system and not a configuration parameter or something. --- safeeyes/plugins/smartpause/plugin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index b0370ccf..313e29aa 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -52,8 +52,8 @@ waiting_time = 2 interpret_idle_as_break = False postpone_if_active = False -timer: Optional[int] = None idle_checker: Optional[IdleTimeInterface] = None +_timer_event_id: Optional[int] = None class GnomeWaylandIdleTime(IdleTimeInterface): @@ -210,7 +210,7 @@ def on_start(): Begin polling to check user idle time. """ global idle_checker - global timer + global _timer_event_id if __is_active(): # If SmartPause is already started, do not start it again @@ -222,7 +222,7 @@ def on_start(): __set_active(True) # FIXME: need to make sure that this gets updated if the waiting_time config changes - timer = GLib.timeout_add_seconds(waiting_time, __idle_monitor) + _timer_event_id = GLib.timeout_add_seconds(waiting_time, __idle_monitor) def on_stop(): @@ -230,7 +230,7 @@ def on_stop(): Stop polling to check user idle time. """ global smart_pause_activated - global timer + global _timer_event_id global idle_checker if smart_pause_activated: @@ -239,8 +239,8 @@ def on_stop(): return logging.debug('Stop Smart Pause plugin') __set_active(False) - GLib.source_remove(timer) - timer = None + GLib.source_remove(_timer_event_id) + _timer_event_id = None if idle_checker is not None: idle_checker.destroy() idle_checker = None From 11b2875095a71214479dd7fe8ffa5a423427fda5 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sun, 23 Feb 2020 14:25:12 -0800 Subject: [PATCH 12/13] smartpause: tell the debug log which idle checker we choose --- safeeyes/plugins/smartpause/plugin.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/safeeyes/plugins/smartpause/plugin.py b/safeeyes/plugins/smartpause/plugin.py index 313e29aa..2506b048 100644 --- a/safeeyes/plugins/smartpause/plugin.py +++ b/safeeyes/plugins/smartpause/plugin.py @@ -121,10 +121,17 @@ def destroy(self) -> None: ] -def idle_checker_for_platform(): +def idle_checker_for_platform(ctx) -> Optional[IdleTimeInterface]: + """ + Create the appropriate idle checker for this context. + """ for cls in _idle_checkers: - if cls.is_applicable(context): - return cls() + if cls.is_applicable(ctx): + checker = cls() + logging.debug("Using idle checker %s", checker) + return checker + + logging.warning("Could not find any appropriate idle checker.") return None @@ -217,7 +224,7 @@ def on_start(): return logging.debug('Start Smart Pause plugin') - idle_checker = idle_checker_for_platform() + idle_checker = idle_checker_for_platform(context) __set_active(True) From 62077c2705e38507e16fe6e559b175bf3610588a Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sun, 23 Feb 2020 15:06:59 -0800 Subject: [PATCH 13/13] README: update installation notes replacing xprintidle with xcb --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2ee83e96..bcb1cdea 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ sudo emerge -av x11-misc/safeeyes ### Debian ```bash -sudo apt-get install gir1.2-appindicator3-0.1 gir1.2-notify-0.7 python3-psutil python3-xlib xprintidle python3-pip +sudo apt-get install gir1.2-appindicator3-0.1 gir1.2-notify-0.7 python3-psutil python3-xlib python3-xcffib python3-pip sudo pip3 install safeeyes sudo update-icon-caches /usr/share/icons/hicolor ``` @@ -72,7 +72,7 @@ sudo apt-get install safeeyes ### Fedora ```bash -sudo dnf install libappindicator-gtk3 python3-psutil cairo-devel python3-devel gobject-introspection-devel cairo-gobject-devel +sudo dnf install libappindicator-gtk3 python3-psutil cairo-devel python3-devel gobject-introspection-devel cairo-gobject-devel python-xcffib sudo pip3 install safeeyes sudo gtk-update-icon-cache /usr/share/icons/hicolor ``` @@ -85,7 +85,7 @@ Ensure to meet the following dependencies: - gir1.2-notify-0.7 - libappindicator-gtk3 - python3-psutil -- xprintidle (optional) +- python3-xcffib **To install Safe Eyes:** @@ -113,7 +113,7 @@ Some Linux systems like Cent OS do not have matching dependencies available in t 1. Install the necessary dependencies ``` - sudo yum install dbus dbus-devel cairo cairo-devel cairomm-devel libjpeg-turbo-devel pango pango-devel pangomm pangomm-devel + sudo yum install dbus dbus-devel cairo cairo-devel cairomm-devel libjpeg-turbo-devel pango pango-devel pangomm pangomm-devel libxcb-devel ``` 2. Create a virtual environment in your home folder