283 lines
9.1 KiB
Python
283 lines
9.1 KiB
Python
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
import datetime
|
|
import logging
|
|
import subprocess
|
|
import threading
|
|
import re
|
|
import os
|
|
|
|
from safeeyes import utility
|
|
from safeeyes.model import State
|
|
|
|
"""
|
|
Safe Eyes smart pause plugin
|
|
"""
|
|
|
|
context = None
|
|
idle_condition = threading.Condition()
|
|
lock = threading.Lock()
|
|
active = False
|
|
idle_time = 0
|
|
enable_safe_eyes = None
|
|
disable_safe_eyes = None
|
|
smart_pause_activated = False
|
|
idle_start_time = None
|
|
next_break_time = None
|
|
next_break_duration = 0
|
|
short_break_interval = 0
|
|
waiting_time = 2
|
|
interpret_idle_as_break = False
|
|
is_wayland_and_gnome = False
|
|
|
|
use_swayidle = False
|
|
swayidle_process = None
|
|
swayidle_lock = threading.Lock()
|
|
swayidle_idle = 0
|
|
swayidle_active = 0
|
|
|
|
def __swayidle_running():
|
|
return (swayidle_process is not None and
|
|
swayidle_process.poll() is None)
|
|
|
|
def __start_swayidle_monitor():
|
|
global swayidle_process
|
|
global swayidle_start
|
|
global swayidle_idle
|
|
global swayidle_active
|
|
logging.debug('Starting swayidle subprocess')
|
|
swayidle_process = subprocess.Popen([
|
|
"swayidle", "timeout", "1", "date +S%s", "resume", "date +R%s"
|
|
], stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8')
|
|
for line in swayidle_process.stdout:
|
|
with swayidle_lock:
|
|
typ = line[0]
|
|
timestamp = int(line[1:])
|
|
if typ == 'S':
|
|
swayidle_idle = timestamp
|
|
elif typ == 'R':
|
|
swayidle_active = timestamp
|
|
|
|
def __stop_swayidle_monitor():
|
|
if __swayidle_running():
|
|
logging.debug('Stopping swayidle subprocess')
|
|
swayidle_process.terminate()
|
|
|
|
def __swayidle_idle_time():
|
|
with swayidle_lock:
|
|
if not __swayidle_running():
|
|
utility.start_thread(__start_swayidle_monitor)
|
|
# Idle more recently than active, meaning idle time isn't stale.
|
|
if swayidle_idle > swayidle_active:
|
|
idle_time = int(datetime.datetime.now().timestamp()) - swayidle_idle
|
|
return idle_time
|
|
return 0
|
|
|
|
def __gnome_wayland_idle_time():
|
|
"""
|
|
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
|
|
"""
|
|
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 BaseException as e:
|
|
logging.warning("Failed to get system idle time for gnome/wayland.")
|
|
logging.warning(str(e))
|
|
return 0
|
|
|
|
|
|
def __system_idle_time():
|
|
"""
|
|
Get system idle time in minutes.
|
|
Return the idle time if xprintidle is available, otherwise return 0.
|
|
"""
|
|
try:
|
|
if is_wayland_and_gnome:
|
|
return __gnome_wayland_idle_time()
|
|
elif use_swayidle:
|
|
return __swayidle_idle_time()
|
|
# Convert to seconds
|
|
return int(subprocess.check_output(['xprintidle']).decode('utf-8')) / 1000
|
|
except BaseException:
|
|
return 0
|
|
|
|
|
|
def __is_active():
|
|
"""
|
|
Thread safe function to see if this plugin is active or not.
|
|
"""
|
|
is_active = False
|
|
with lock:
|
|
is_active = active
|
|
return is_active
|
|
|
|
|
|
def __set_active(is_active):
|
|
"""
|
|
Thread safe function to change the state of the plugin.
|
|
"""
|
|
global active
|
|
with lock:
|
|
active = is_active
|
|
|
|
|
|
def init(ctx, safeeyes_config, plugin_config):
|
|
"""
|
|
Initialize the plugin.
|
|
"""
|
|
global context
|
|
global enable_safe_eyes
|
|
global disable_safe_eyes
|
|
global postpone
|
|
global idle_time
|
|
global short_break_interval
|
|
global long_break_duration
|
|
global waiting_time
|
|
global interpret_idle_as_break
|
|
global postpone_if_active
|
|
global is_wayland_and_gnome
|
|
global use_swayidle
|
|
logging.debug('Initialize Smart Pause plugin')
|
|
context = ctx
|
|
enable_safe_eyes = context['api']['enable_safeeyes']
|
|
disable_safe_eyes = context['api']['disable_safeeyes']
|
|
postpone = context['api']['postpone']
|
|
idle_time = plugin_config['idle_time']
|
|
interpret_idle_as_break = plugin_config['interpret_idle_as_break']
|
|
postpone_if_active = plugin_config['postpone_if_active']
|
|
short_break_interval = safeeyes_config.get(
|
|
'short_break_interval') * 60 # Convert to seconds
|
|
long_break_duration = safeeyes_config.get('long_break_duration')
|
|
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'
|
|
|
|
|
|
def __start_idle_monitor():
|
|
"""
|
|
Continuously 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() - datetime.timedelta(seconds=system_idle_time)
|
|
logging.info('Pause Safe Eyes due to system idle')
|
|
disable_safe_eyes(None)
|
|
elif system_idle_time < idle_time and context['state'] == State.STOPPED and idle_start_time is not None:
|
|
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
|
|
logging.debug("Idle for %d seconds, long break %d", idle_seconds, long_break_duration)
|
|
enable_safe_eyes(-1, idle_seconds >= long_break_duration)
|
|
elif idle_seconds < short_break_interval:
|
|
# Credit back the idle time
|
|
if next_break_time is not None:
|
|
# This method runs in a thread since the start.
|
|
# It may run before next_break is initialized in the update_next_break method
|
|
next_break = next_break_time + idle_period
|
|
enable_safe_eyes(next_break.timestamp())
|
|
else:
|
|
enable_safe_eyes()
|
|
else:
|
|
# User is idle for more than the time between two breaks
|
|
enable_safe_eyes()
|
|
|
|
|
|
def on_start():
|
|
"""
|
|
Start a thread to continuously call xprintidle.
|
|
"""
|
|
global active
|
|
if not __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)
|
|
|
|
|
|
def on_stop():
|
|
"""
|
|
Stop the thread from continuously calling xprintidle.
|
|
"""
|
|
global active
|
|
global smart_pause_activated
|
|
if smart_pause_activated:
|
|
# Safe Eyes is stopped due to system idle
|
|
smart_pause_activated = False
|
|
return
|
|
logging.debug('Stop Smart Pause plugin')
|
|
if use_swayidle:
|
|
__stop_swayidle_monitor()
|
|
__set_active(False)
|
|
idle_condition.acquire()
|
|
idle_condition.notify_all()
|
|
idle_condition.release()
|
|
|
|
|
|
def update_next_break(break_obj, dateTime):
|
|
"""
|
|
Update the next break time.
|
|
"""
|
|
global next_break_time
|
|
global next_break_duration
|
|
next_break_time = dateTime
|
|
next_break_duration = break_obj.duration
|
|
|
|
|
|
def on_start_break(break_obj):
|
|
"""
|
|
Lifecycle method executes just before the break.
|
|
"""
|
|
if postpone_if_active:
|
|
# Postpone this break if the user is active
|
|
system_idle_time = __system_idle_time()
|
|
if system_idle_time < 2:
|
|
postpone(2) # Postpone for 2 seconds
|
|
|
|
|
|
def disable():
|
|
"""
|
|
SmartPause plugin was active earlier but now user has disabled it.
|
|
"""
|
|
# Remove the idle_period
|
|
context.pop('idle_period', None)
|