SafeEyes/safeeyes/SafeEyesCore.py

365 lines
12 KiB
Python
Raw Normal View History

2016-10-15 06:11:27 +02:00
# Safe Eyes is a utility to remind you to take break frequently
# to protect your eyes from eye strain.
# Copyright (C) 2016 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/>.
2017-02-06 03:09:04 +01:00
2017-04-11 01:32:43 +02:00
import time, datetime, threading, sys, subprocess, logging
from safeeyes import Utility
2016-10-15 06:11:27 +02:00
"""
Core of Safe Eyes which runs the scheduler and notifies the breaks.
"""
2016-10-15 06:11:27 +02:00
class SafeEyesCore:
"""
Initialize the internal variables of the core.
"""
2017-04-09 02:29:51 +02:00
def __init__(self, context, show_notification, start_break, end_break, on_countdown, update_next_break_info):
# Initialize the variables
2017-02-11 22:56:49 +01:00
self.break_count = -1
2016-11-04 09:47:21 +01:00
self.long_break_message_index = -1
self.short_break_message_index = -1
self.active = False
2017-02-06 03:09:04 +01:00
self.running = False
self.show_notification = show_notification
2016-10-15 06:11:27 +02:00
self.start_break = start_break
self.end_break = end_break
self.on_countdown = on_countdown
self.update_next_break_info = update_next_break_info
self.notification_condition = threading.Condition()
2017-02-06 03:09:04 +01:00
self.idle_condition = threading.Condition()
self.lock = threading.Lock()
2017-04-09 02:29:51 +02:00
self.context = context
2017-04-19 01:34:22 +02:00
self.context['skipped'] = False
self.context['postponed'] = False
2016-10-15 06:11:27 +02:00
2016-10-15 06:11:27 +02:00
"""
Initialize the internal properties from configuration
"""
2016-11-04 09:47:21 +01:00
def initialize(self, config, language):
2016-11-08 14:17:48 +01:00
logging.info("Initialize the core")
2017-02-11 22:56:49 +01:00
self.short_break_exercises = [] #language['exercises']['short_break_exercises']
self.long_break_exercises = [] #language['exercises']['long_break_exercises']
2016-10-15 06:11:27 +02:00
self.no_of_short_breaks_per_long_break = config['no_of_short_breaks_per_long_break']
self.pre_break_warning_time = config['pre_break_warning_time']
self.long_break_duration = config['long_break_duration']
self.short_break_duration = config['short_break_duration']
self.break_interval = config['break_interval']
2017-02-06 03:09:04 +01:00
self.idle_time = config['idle_time']
2017-04-03 20:18:23 +02:00
self.postpone_duration = config['postpone_duration']
self.skip_break_window_classes = [x.lower() for x in config['active_window_class']['skip_break']]
self.take_break_window_classes = [x.lower() for x in config['active_window_class']['take_break']]
self.custom_exercises = config['custom_exercises']
# Enable idle time pause only if xprintidle is available
self.context['idle_pause_enabled'] = Utility.command_exist('xprintidle')
2016-10-15 06:11:27 +02:00
exercises = language['exercises']
2017-02-11 22:56:49 +01:00
for short_break_config in config['short_breaks']:
exercise_name = short_break_config['name']
name = None
if exercise_name in self.custom_exercises:
name = self.custom_exercises[exercise_name]
else:
name = exercises[exercise_name]
2017-02-11 22:56:49 +01:00
break_time = short_break_config.get('time', self.short_break_duration)
2017-02-12 14:36:09 +01:00
audible_alert = short_break_config.get('audible_alert', config['audible_alert'])
2017-04-07 22:20:23 +02:00
image = short_break_config.get('image')
2017-02-11 22:56:49 +01:00
# Validate time value
if not isinstance(break_time, int) or break_time <= 0:
logging.error('Invalid time in short break: ' + str(short_break_config))
continue
2017-04-09 02:29:51 +02:00
2017-04-07 22:20:23 +02:00
self.short_break_exercises.append([name, break_time, audible_alert, image])
2017-02-11 22:56:49 +01:00
for long_break_config in config['long_breaks']:
exercise_name = long_break_config['name']
name = None
if exercise_name in self.custom_exercises:
name = self.custom_exercises[exercise_name]
else:
name = exercises[exercise_name]
2017-02-12 14:36:09 +01:00
break_time = long_break_config.get('time', self.long_break_duration)
audible_alert = long_break_config.get('audible_alert', config['audible_alert'])
2017-04-07 22:20:23 +02:00
image = long_break_config.get('image')
2017-02-13 20:24:15 +01:00
2017-02-11 22:56:49 +01:00
# Validate time value
2017-02-12 14:36:09 +01:00
if not isinstance(break_time, int) or break_time <= 0:
logging.error('Invalid time in long break: ' + str(long_break_config))
2017-02-11 22:56:49 +01:00
continue
2017-04-09 02:29:51 +02:00
2017-04-07 22:20:23 +02:00
self.long_break_exercises.append([name, break_time, audible_alert, image])
2017-02-11 22:56:49 +01:00
"""
Start Safe Eyes is it is not running already.
"""
def start(self):
2017-02-06 03:09:04 +01:00
with self.lock:
if not self.active:
logging.info("Scheduling next break")
self.active = True
self.running = True
Utility.start_thread(self.__scheduler_job)
if self.context['idle_pause_enabled']:
Utility.start_thread(self.__start_idle_monitor)
"""
Stop Safe Eyes if it is running.
"""
def stop(self):
2017-02-06 03:09:04 +01:00
with self.lock:
if self.active:
logging.info("Stop the core")
# Reset the state properties in case of restart
# self.break_count = 0
# self.long_break_message_index = -1
# self.short_break_message_index = -1
2017-02-06 03:22:40 +01:00
# Stop the break thread
2017-02-06 03:09:04 +01:00
self.notification_condition.acquire()
self.active = False
self.running = False
self.notification_condition.notify_all()
self.notification_condition.release()
2017-02-06 03:09:04 +01:00
# Stop the idle monitor
self.idle_condition.acquire()
self.idle_condition.notify_all()
self.idle_condition.release()
2017-02-06 03:09:04 +01:00
"""
Pause Safe Eyes if it is running.
"""
def pause(self):
with self.lock:
if self.active and self.running:
self.notification_condition.acquire()
self.running = False
self.notification_condition.notify_all()
self.notification_condition.release()
"""
Resume Safe Eyes if it is not running.
"""
def resume(self):
with self.lock:
if self.active and not self.running:
self.running = True
Utility.start_thread(self.__scheduler_job)
"""
User skipped the break using Skip button
"""
def skip_break(self):
2017-04-19 01:34:22 +02:00
self.context['skipped'] = True
2017-04-03 20:18:23 +02:00
"""
User postponed the break using Postpone button
"""
def postpone_break(self):
2017-04-19 01:34:22 +02:00
self.context['postponed'] = True
2017-04-03 20:18:23 +02:00
2016-10-15 06:11:27 +02:00
"""
Scheduler task to execute during every interval
"""
def __scheduler_job(self):
2017-02-06 03:09:04 +01:00
if not self.__is_running():
2016-10-15 06:11:27 +02:00
return
2017-04-03 20:18:23 +02:00
time_to_wait = self.break_interval # In minutes
2017-04-19 01:34:22 +02:00
if self.context['postponed']:
2017-04-03 20:18:23 +02:00
# Reduce the break count by 1 to show the same break again
if self.break_count == 0:
self.break_count = -1
else:
self.break_count = ((self.break_count - 1) % self.no_of_short_breaks_per_long_break)
if self.__is_long_break():
self.long_break_message_index = (self.long_break_message_index - 1) % len(self.long_break_exercises)
else:
self.short_break_message_index = (self.short_break_message_index - 1) % len(self.short_break_exercises)
# Wait until the postpone time
time_to_wait = self.postpone_duration
2017-04-19 01:34:22 +02:00
self.context['postponed'] = False
2017-04-09 02:29:51 +02:00
2017-04-03 20:18:23 +02:00
next_break_time = datetime.datetime.now() + datetime.timedelta(minutes=time_to_wait)
self.update_next_break_info(next_break_time)
2016-11-08 14:17:48 +01:00
2016-10-24 05:52:15 +02:00
# Wait for the pre break warning period
2017-04-03 20:18:23 +02:00
logging.info("Pre-break waiting for {} minutes".format(time_to_wait))
2016-10-24 05:52:15 +02:00
self.notification_condition.acquire()
2017-04-03 20:18:23 +02:00
self.notification_condition.wait(time_to_wait * 60) # Convert to seconds
2016-10-24 05:52:15 +02:00
self.notification_condition.release()
2016-11-08 14:17:48 +01:00
logging.info("Pre-break waiting is over")
2017-02-06 03:09:04 +01:00
if not self.__is_running():
2016-10-24 05:52:15 +02:00
return
2016-10-15 06:11:27 +02:00
2016-11-08 14:17:48 +01:00
logging.info("Ready to show the break")
2016-10-15 06:11:27 +02:00
self.break_count = ((self.break_count + 1) % self.no_of_short_breaks_per_long_break)
2017-02-16 00:29:14 +01:00
self.is_before_break = False
Utility.execute_main_thread(self.__check_active_window)
2016-10-15 06:11:27 +02:00
"""
2017-02-16 00:29:14 +01:00
Show the notification and start the break after the notification.
"""
2017-02-16 00:29:14 +01:00
def __show_notification(self):
# Show the notification
self.show_notification()
2016-10-15 06:11:27 +02:00
2016-11-08 14:17:48 +01:00
logging.info("Wait for {} seconds which is the time to prepare".format(self.pre_break_warning_time))
2016-10-15 06:11:27 +02:00
# Wait for the pre break warning period
self.notification_condition.acquire()
self.notification_condition.wait(self.pre_break_warning_time)
self.notification_condition.release()
2016-10-15 06:11:27 +02:00
2017-02-16 00:29:14 +01:00
self.is_before_break = True
Utility.execute_main_thread(self.__check_active_window)
"""
Check the active window for full-screen and user defined exceptions.
"""
def __check_active_window(self):
# Check the active window again. (User might changed the window)
if self.__is_running() and Utility.is_active_window_skipped(self.skip_break_window_classes, self.take_break_window_classes, self.is_before_break):
# If full screen app found, do not show break screen
logging.info("Found a skip_break or full-screen window. Skip the break")
if self.__is_running():
# Schedule the break again
Utility.start_thread(self.__scheduler_job)
return
# Execute the post-operation
if self.is_before_break:
Utility.start_thread(self.__start_break)
else:
Utility.start_thread(self.__show_notification)
"""
Start the break screen.
"""
def __start_break(self):
2016-10-15 19:10:08 +02:00
# User can disable SafeEyes during notification
2017-02-06 03:09:04 +01:00
if self.__is_running():
2016-10-15 06:11:27 +02:00
message = ""
2017-04-07 22:20:23 +02:00
image = None
2017-02-11 22:56:49 +01:00
seconds = 0
2017-02-12 14:36:09 +01:00
audible_alert = None
if self.__is_long_break():
2016-11-08 14:17:48 +01:00
logging.info("Count is {}; get a long beak message".format(self.break_count))
2016-11-04 09:47:21 +01:00
self.long_break_message_index = (self.long_break_message_index + 1) % len(self.long_break_exercises)
2017-02-11 22:56:49 +01:00
message = self.long_break_exercises[self.long_break_message_index][0]
seconds = self.long_break_exercises[self.long_break_message_index][1]
2017-02-12 14:36:09 +01:00
audible_alert = self.long_break_exercises[self.long_break_message_index][2]
2017-04-07 22:20:23 +02:00
image = self.long_break_exercises[self.long_break_message_index][3]
2017-04-09 02:29:51 +02:00
self.context['break_type'] = 'long'
2016-10-15 06:11:27 +02:00
else:
2016-11-08 14:17:48 +01:00
logging.info("Count is {}; get a short beak message".format(self.break_count))
2016-11-04 09:47:21 +01:00
self.short_break_message_index = (self.short_break_message_index + 1) % len(self.short_break_exercises)
2017-02-11 22:56:49 +01:00
message = self.short_break_exercises[self.short_break_message_index][0]
seconds = self.short_break_exercises[self.short_break_message_index][1]
2017-02-12 14:36:09 +01:00
audible_alert = self.short_break_exercises[self.short_break_message_index][2]
2017-04-07 22:20:23 +02:00
image = self.short_break_exercises[self.short_break_message_index][3]
2017-04-09 02:29:51 +02:00
self.context['break_type'] = 'short'
2017-04-08 15:54:07 +02:00
2017-04-09 02:29:51 +02:00
self.context['break_length'] = seconds
self.context['audible_alert'] = audible_alert
total_break_time = seconds
2017-04-08 15:54:07 +02:00
# Show the break screen
2017-04-09 02:29:51 +02:00
self.start_break(message, image)
2016-10-15 06:11:27 +02:00
2017-02-06 03:09:04 +01:00
# Use self.active instead of self.__is_running to avoid idle pause interrupting the break
2017-04-19 01:34:22 +02:00
while seconds and self.active and not self.context['skipped'] and not self.context['postponed']:
2017-04-09 02:29:51 +02:00
self.context['count_down'] = total_break_time - seconds
mins, secs = divmod(seconds, 60)
timeformat = '{:02d}:{:02d}'.format(mins, secs)
self.on_countdown(timeformat)
time.sleep(1) # Sleep for 1 second
seconds -= 1
2016-10-15 06:11:27 +02:00
# Loop terminated because of timeout (not skipped) -> Close the break alert
2017-04-19 01:34:22 +02:00
if not self.context['skipped'] and not self.context['postponed']:
2017-04-03 20:18:23 +02:00
logging.info("Break is terminated automatically")
2017-02-12 14:36:09 +01:00
self.end_break(audible_alert)
2016-10-15 06:11:27 +02:00
2017-04-03 20:18:23 +02:00
# Reset the skipped flag
2017-04-19 01:34:22 +02:00
self.context['skipped'] = False
2017-04-03 20:18:23 +02:00
2016-10-24 05:52:15 +02:00
# Resume
2017-02-06 03:09:04 +01:00
if self.__is_running():
2017-01-11 16:49:06 +01:00
# Schedule the break again
2017-02-06 03:09:04 +01:00
Utility.start_thread(self.__scheduler_job)
2016-10-15 06:11:27 +02:00
2016-10-15 06:11:27 +02:00
"""
2017-02-06 03:09:04 +01:00
Tells whether Safe Eyes is running or not.
2016-10-15 06:11:27 +02:00
"""
2017-02-06 03:09:04 +01:00
def __is_running(self):
return self.active and self.running
2016-10-15 06:11:27 +02:00
"""
2017-02-06 03:09:04 +01:00
Check if the current break is long break or short current
2016-10-15 06:11:27 +02:00
"""
2017-02-06 03:09:04 +01:00
def __is_long_break(self):
return self.break_count == self.no_of_short_breaks_per_long_break - 1
2016-10-15 06:11:27 +02:00
"""
2017-02-06 03:09:04 +01:00
Continuously check the system idle time and pause/resume Safe Eyes based on it.
2016-10-15 06:11:27 +02:00
"""
2017-02-06 03:09:04 +01:00
def __start_idle_monitor(self):
while self.active:
# Wait for 2 seconds
self.idle_condition.acquire()
self.idle_condition.wait(2)
self.idle_condition.release()
if self.active:
# Get the system idle time
system_idle_time = Utility.system_idle_time()
if system_idle_time >= self.idle_time and self.running:
2017-02-06 03:22:40 +01:00
logging.info('Pause Safe Eyes due to system idle')
2017-02-06 03:09:04 +01:00
self.pause()
elif system_idle_time < self.idle_time and not self.running:
2017-02-06 03:22:40 +01:00
logging.info('Resume Safe Eyes due to user activity')
2017-02-06 03:09:04 +01:00
self.resume()