#!/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 . """ 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