SafeEyes/safeeyes/safeeyes.py

447 lines
15 KiB
Python

#!/usr/bin/env 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/>.
"""
SafeEyes connects all the individual components and provide the complete application.
"""
import atexit
import logging
import os
from threading import Timer
import gi
from safeeyes import utility
from safeeyes.ui.about_dialog import AboutDialog
from safeeyes.ui.break_screen import BreakScreen
from safeeyes.ui.required_plugin_dialog import RequiredPluginDialog
from safeeyes.model import State, RequiredPluginException
from safeeyes.rpc import RPCServer
from safeeyes.plugin_manager import PluginManager
from safeeyes.core import SafeEyesCore
from safeeyes.ui.settings_dialog import SettingsDialog
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gio, GLib
SAFE_EYES_VERSION = "2.2.2"
class SafeEyes(Gtk.Application):
"""
This class represents a runnable Safe Eyes instance.
"""
required_plugin_dialog_active = False
retry_errored_plugins_count = 0
def __init__(self, system_locale, config, cli_args):
super().__init__(
application_id="io.github.slgobinath.SafeEyes",
flags=Gio.ApplicationFlags.FLAGS_NONE ## This is necessary for compatibility with Ubuntu 22.04.
)
self.active = False
self.break_screen = None
self.safe_eyes_core = None
self.config = config
self.context = {}
self.plugins_manager = None
self.settings_dialog_active = False
self.rpc_server = None
self._status = ''
self.cli_args = cli_args
self.system_locale = system_locale
def start(self):
"""
Start Safe Eyes
"""
self.run()
def do_startup(self):
Gtk.Application.do_startup(self)
logging.info('Starting up Application')
# Initialize the Safe Eyes Context
self.context['version'] = SAFE_EYES_VERSION
self.context['desktop'] = utility.desktop_environment()
self.context['is_wayland'] = utility.is_wayland()
self.context['locale'] = self.system_locale
self.context['api'] = {}
self.context['api']['show_settings'] = lambda: utility.execute_main_thread(
self.show_settings)
self.context['api']['show_about'] = lambda: utility.execute_main_thread(
self.show_about)
self.context['api']['enable_safeeyes'] = lambda next_break_time=-1, reset_breaks=False: \
utility.execute_main_thread(self.enable_safeeyes, next_break_time, reset_breaks)
self.context['api']['disable_safeeyes'] = lambda status=None, is_resting=False: utility.execute_main_thread(
self.disable_safeeyes, status, is_resting)
self.context['api']['status'] = self.status
self.context['api']['quit'] = lambda: utility.execute_main_thread(
self.quit)
if self.config.get('persist_state'):
self.context['session'] = utility.open_session()
else:
self.context['session'] = {'plugin': {}}
self.break_screen = BreakScreen(
self.context, self.on_skipped, self.on_postponed, utility.STYLE_SHEET_PATH)
self.break_screen.initialize(self.config)
self.plugins_manager = PluginManager()
self.safe_eyes_core = SafeEyesCore(self.context)
self.safe_eyes_core.on_pre_break += self.plugins_manager.pre_break
self.safe_eyes_core.on_start_break += self.on_start_break
self.safe_eyes_core.start_break += self.start_break
self.safe_eyes_core.on_count_down += self.countdown
self.safe_eyes_core.on_stop_break += self.stop_break
self.safe_eyes_core.on_update_next_break += self.update_next_break
self.safe_eyes_core.initialize(self.config)
self.context['api']['take_break'] = self.take_break
self.context['api']['has_breaks'] = self.safe_eyes_core.has_breaks
self.context['api']['postpone'] = self.safe_eyes_core.postpone
self.context['api']['get_break_time'] = self.safe_eyes_core.get_break_time
try:
self.plugins_manager.init(self.context, self.config)
except RequiredPluginException as e:
self.show_required_plugin_dialog(e)
self.hold()
atexit.register(self.persist_session)
if self.config.get('use_rpc_server', True):
self.__start_rpc_server()
if not self.plugins_manager.needs_retry() and not self.required_plugin_dialog_active and self.safe_eyes_core.has_breaks():
self.active = True
self.context['state'] = State.START
self.plugins_manager.start() # Call the start method of all plugins
self.safe_eyes_core.start()
self.handle_system_suspend()
def do_activate(self):
logging.info('Application activated')
if self.plugins_manager.needs_retry():
GLib.timeout_add_seconds(1, self._retry_errored_plugins)
if self.cli_args.about:
self.show_about()
elif self.cli_args.disable:
self.disable_safeeyes()
elif self.cli_args.enable:
self.enable_safeeyes()
elif self.cli_args.settings:
self.show_settings()
elif self.cli_args.take_break:
self.take_break()
def _retry_errored_plugins(self):
if not self.plugins_manager.needs_retry():
return
logging.info(f"Retry loading errored plugin")
self.plugins_manager.retry_errored_plugins()
error = self.plugins_manager.get_retryable_error()
if error is None:
# success
self.restart(self.config, set_active=True)
return
# errored again
if self.retry_errored_plugins_count >= 3:
self.show_required_plugin_dialog(error)
return
timeout = pow(2, self.retry_errored_plugins_count)
self.retry_errored_plugins_count += 1
GLib.timeout_add_seconds(timeout, self._retry_errored_plugins)
def show_settings(self):
"""
Listen to tray icon Settings action and send the signal to Settings dialog.
"""
if not self.settings_dialog_active:
logging.info("Show Settings dialog")
self.settings_dialog_active = True
settings_dialog = SettingsDialog(
self.config.clone(), self.save_settings)
settings_dialog.show()
def show_required_plugin_dialog(self, error: RequiredPluginException):
self.required_plugin_dialog_active = True
logging.info("Show RequiredPlugin dialog")
dialog = RequiredPluginDialog(
error.get_plugin_id(),
error.get_plugin_name(),
error.get_message(),
self.quit,
lambda: self.disable_plugin(plugin_id)
)
dialog.show()
def disable_plugin(self, plugin_id):
"""
Temporarily disable plugin, and restart SafeEyes.
"""
config = self.config.clone()
for plugin in config.get('plugins'):
if plugin['id'] == plugin_id:
plugin['enabled'] = False
self.required_plugin_dialog_active = False
self.restart(config, set_active=True)
def show_about(self):
"""
Listen to tray icon About action and send the signal to About dialog.
"""
logging.info("Show About dialog")
about_dialog = AboutDialog(SAFE_EYES_VERSION)
about_dialog.show()
def quit(self):
"""
Listen to the tray menu quit action and stop the core, notification and the app itself.
"""
logging.info("Quit Safe Eyes")
self.break_screen.close()
self.context['state'] = State.QUIT
self.plugins_manager.stop()
self.safe_eyes_core.stop()
self.plugins_manager.exit()
self.__stop_rpc_server()
self.persist_session()
super().quit()
def handle_suspend_callback(self, sleeping):
"""
If the system goes to sleep, Safe Eyes stop the core if it is already active.
If it was active, Safe Eyes will become active after wake up.
"""
if sleeping:
# Sleeping / suspending
if self.active:
logging.info("Stop Safe Eyes due to system suspend")
self.plugins_manager.stop()
self.safe_eyes_core.stop(True)
else:
# Resume from sleep
if self.active and self.safe_eyes_core.has_breaks():
logging.info("Resume Safe Eyes after system wakeup")
self.plugins_manager.start()
self.safe_eyes_core.start()
def handle_suspend_signal(self, proxy, sender, signal, parameters):
if signal != "PrepareForSleep":
return
(sleeping, ) = parameters
self.handle_suspend_callback(sleeping)
def handle_system_suspend(self):
"""
Setup system suspend listener.
"""
self.suspend_proxy = Gio.DBusProxy.new_for_bus_sync(
bus_type=Gio.BusType.SYSTEM,
flags=Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
info=None,
name='org.freedesktop.login1',
object_path='/org/freedesktop/login1',
interface_name='org.freedesktop.login1.Manager',
cancellable=None,
)
self.suspend_proxy.connect('g-signal', self.handle_suspend_signal)
def on_skipped(self):
"""
Listen to break screen Skip action and send the signal to core.
"""
logging.info("User skipped the break")
self.safe_eyes_core.skip()
self.plugins_manager.stop_break()
def on_postponed(self):
"""
Listen to break screen Postpone action and send the signal to core.
"""
logging.info("User postponed the break")
self.safe_eyes_core.postpone()
self.plugins_manager.stop_break()
def save_settings(self, config):
"""
Listen to Settings dialog Save action and write to the config file.
"""
self.settings_dialog_active = False
if self.config == config:
# Config is not modified
return
logging.info("Saving settings to safeeyes.json")
# Stop the Safe Eyes core
if self.active:
self.plugins_manager.stop()
self.safe_eyes_core.stop()
# Write the configuration to file
config.save()
self.persist_session()
self.restart(config)
def restart(self, config, set_active=False):
logging.info("Initialize SafeEyesCore with modified settings")
if self.rpc_server is None and config.get('use_rpc_server'):
# RPC server wasn't running but now enabled
self.__start_rpc_server()
elif self.rpc_server is not None and not config.get('use_rpc_server'):
# RPC server was running but now disabled
self.__stop_rpc_server()
# Restart the core and initialize the components
self.config = config
self.safe_eyes_core.initialize(config)
self.break_screen.initialize(config)
try:
self.plugins_manager.init(self.context, self.config)
except RequiredPluginException as e:
self.show_required_plugin_dialog(e)
return
if set_active:
self.active = True
if self.active and self.safe_eyes_core.has_breaks():
# 1 sec delay is required to give enough time for core to be stopped
Timer(1.0, self.safe_eyes_core.start).start()
self.plugins_manager.start()
def enable_safeeyes(self, scheduled_next_break_time=-1, reset_breaks=False):
"""
Listen to tray icon enable action and send the signal to core.
"""
if not self.required_plugin_dialog_active and not self.active and self.safe_eyes_core.has_breaks():
self.active = True
self.safe_eyes_core.start(scheduled_next_break_time, reset_breaks)
self.plugins_manager.start()
def disable_safeeyes(self, status=None, is_resting = False):
"""
Listen to tray icon disable action and send the signal to core.
"""
if self.active:
self.active = False
self.plugins_manager.stop()
self.safe_eyes_core.stop(is_resting)
if status is None:
status = _('Disabled until restart')
self._status = status
def on_start_break(self, break_obj):
"""
Pass the break information to plugins.
"""
if not self.plugins_manager.start_break(break_obj):
return False
return True
def start_break(self, break_obj):
"""
Pass the break information to break screen.
"""
# Get the HTML widgets content from plugins
widget = self.plugins_manager.get_break_screen_widgets(break_obj)
actions = self.plugins_manager.get_break_screen_tray_actions(break_obj)
self.break_screen.show_message(break_obj, widget, actions)
def countdown(self, countdown, seconds):
"""
Pass the countdown to plugins and break screen.
"""
self.break_screen.show_count_down(countdown, seconds)
self.plugins_manager.countdown(countdown, seconds)
return True
def update_next_break(self, break_obj, break_time):
"""
Update the next break to plugins and save the session.
"""
self.plugins_manager.update_next_break(break_obj, break_time)
self._status = _('Next break at %s') % (
utility.format_time(break_time))
if self.config.get('persist_state'):
utility.write_json(utility.SESSION_FILE_PATH,
self.context['session'])
def stop_break(self):
"""
Stop the current break.
"""
self.break_screen.close()
self.plugins_manager.stop_break()
return True
def take_break(self, break_type = None):
"""
Take a break now.
"""
utility.execute_main_thread(self.safe_eyes_core.take_break, break_type)
def status(self):
"""
Return the status of Safe Eyes.
"""
return self._status
def persist_session(self):
"""
Save the session object to the session file.
"""
if self.config.get('persist_state'):
utility.write_json(utility.SESSION_FILE_PATH,
self.context['session'])
else:
utility.delete(utility.SESSION_FILE_PATH)
def __start_rpc_server(self):
if self.rpc_server is None:
self.rpc_server = RPCServer(self.config.get('rpc_port'), self.context)
self.rpc_server.start()
def __stop_rpc_server(self):
if self.rpc_server is not None:
self.rpc_server.stop()
self.rpc_server = None