diff --git a/safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml b/safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml
index 660aa6aa..45d2dab2 100644
--- a/safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml
+++ b/safeeyes/platform/io.github.slgobinath.SafeEyes.metainfo.xml
@@ -29,8 +29,8 @@
Remind you to take breaks with exercises to reduce RSI, Disable keyboard during breaks,
Notification before and after breaks, Smart pause if system is idle, Multi-screen
- support, Customizable user interface, RPC API to control externally, Command-line
- arguments to control the running instance, Customizable using plug-ins
+ support, Customizable user interface, Command-line arguments to control the running
+ instance, Customizable using plug-ins
diff --git a/safeeyes/rpc.py b/safeeyes/rpc.py
deleted file mode 100644
index e1398b5e..00000000
--- a/safeeyes/rpc.py
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/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 .
-"""RPC server and client implementation."""
-
-import logging
-from threading import Thread
-from xmlrpc.server import SimpleXMLRPCServer
-from xmlrpc.client import ServerProxy
-
-
-class RPCServer:
- """An asynchronous RPC server."""
-
- def __init__(self, port, context):
- self.__running = False
- logging.info("Setting up an RPC server on port %d", port)
- self.__server = SimpleXMLRPCServer(
- ("localhost", port), logRequests=False, allow_none=True
- )
- self.__server.register_function(
- context["api"]["show_settings"], "show_settings"
- )
- self.__server.register_function(context["api"]["show_about"], "show_about")
- self.__server.register_function(
- context["api"]["enable_safeeyes"], "enable_safeeyes"
- )
- self.__server.register_function(
- context["api"]["disable_safeeyes"], "disable_safeeyes"
- )
- self.__server.register_function(context["api"]["take_break"], "take_break")
- self.__server.register_function(context["api"]["status"], "status")
- self.__server.register_function(context["api"]["quit"], "quit")
-
- def start(self):
- """Start the RPC server."""
- if not self.__running:
- self.__running = True
- logging.info("Start the RPC server")
- server_thread = Thread(target=self.__server.serve_forever)
- server_thread.start()
-
- def stop(self):
- """Stop the server."""
- if self.__running:
- logging.info("Stop the RPC server")
- self.__running = False
- self.__server.shutdown()
-
-
-class RPCClient:
- """An RPC client to communicate with the RPC server."""
-
- def __init__(self, port):
- self.port = port
- self.proxy = ServerProxy("http://localhost:%d/" % self.port, allow_none=True)
-
- def show_settings(self):
- """Show the settings dialog."""
- self.proxy.show_settings()
-
- def show_about(self):
- """Show the about dialog."""
- self.proxy.show_about()
-
- def enable_safeeyes(self):
- """Enable Safe Eyes."""
- self.proxy.enable_safeeyes()
-
- def disable_safeeyes(self):
- """Disable Safe Eyes."""
- self.proxy.disable_safeeyes(None)
-
- def take_break(self):
- """Take a break now."""
- self.proxy.take_break()
-
- def status(self):
- """Return the status of Safe Eyes."""
- return self.proxy.status()
-
- def quit(self):
- """Quit Safe Eyes."""
- self.proxy.quit()
diff --git a/safeeyes/safeeyes.py b/safeeyes/safeeyes.py
index 4ea5a93e..56c1e48a 100644
--- a/safeeyes/safeeyes.py
+++ b/safeeyes/safeeyes.py
@@ -22,6 +22,7 @@
import atexit
import logging
+import typing
from threading import Timer
from importlib import metadata
@@ -31,7 +32,6 @@
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.translations import translate as _
from safeeyes.plugin_manager import PluginManager
from safeeyes.core import SafeEyesCore
@@ -49,27 +49,176 @@ class SafeEyes(Gtk.Application):
required_plugin_dialog_active = False
retry_errored_plugins_count = 0
- def __init__(self, system_locale, config, cli_args):
+ def __init__(self, system_locale, config) -> None:
super().__init__(
application_id="io.github.slgobinath.SafeEyes",
- # This is necessary for compatibility with Ubuntu 22.04.
- flags=Gio.ApplicationFlags.FLAGS_NONE,
+ flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
)
+
self.active = False
self.break_screen = None
self.safe_eyes_core = None
self.config = config
- self.context = {}
+ self.context: typing.Any = {}
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()
+ self.__register_cli_arguments()
+ self.__register_actions()
+
+ def __register_cli_arguments(self):
+ flags = [
+ # startup window
+ ("about", "a", _("show the about dialog")),
+ ("settings", "s", _("start safeeyes in debug mode")),
+ ("take-break", "t", _("Take a break now").lower()),
+ # activate action
+ ("disable", "d", _("disable the currently running safeeyes instance")),
+ ("enable", "e", _("enable the currently running safeeyes instance")),
+ ("quit", "q", _("quit the running safeeyes instance and exit")),
+ # special handling
+ (
+ "status",
+ None,
+ _("print the status of running safeeyes instance and exit"),
+ ),
+ # toggle
+ ("debug", None, _("start safeeyes in debug mode")),
+ # TODO: translate
+ ("version", None, "show program's version number and exit"),
+ ]
+
+ for flag, short, desc in flags:
+ # all flags are booleans
+ self.add_main_option(
+ flag,
+ ord(short) if short else 0,
+ GLib.OptionFlags.NONE,
+ GLib.OptionArg.NONE,
+ desc,
+ None,
+ )
+
+ def __register_actions(self) -> None:
+ actions = [
+ ("show_about", self.show_about),
+ ("show_settings", self.show_settings),
+ ("take_break", self.take_break),
+ ("enable_safeeyes", self.enable_safeeyes),
+ ("disable_safeeyes", self.disable_safeeyes),
+ ("quit", self.quit),
+ ]
+
+ # this is needed because of late bindings...
+ def create_cb_discard_args(callback):
+ return lambda parameter, user_data: callback()
+
+ for name, callback in actions:
+ action = Gio.SimpleAction.new(name, None)
+ action.connect("activate", create_cb_discard_args(callback))
+ self.add_action(action)
+
+ def do_handle_local_options(self, options):
+ Gtk.Application.do_handle_local_options(self, options)
+
+ # do not call options.end() here - this will clear the dict,
+ # and make it empty/broken inside do_command_line
+
+ debug = False
+ if options.contains("debug"):
+ debug = True
+
+ # Initialize the logging
+ utility.initialize_logging(debug)
+ utility.initialize_platform()
+ utility.cleanup_old_user_stylesheet()
+
+ if options.contains("version"):
+ print(f"safeeyes {SAFE_EYES_VERSION}")
+ return 0 # exit
+
+ # needed for calling is_remote
+ self.register(None)
+
+ is_remote = self.get_is_remote()
+
+ if is_remote:
+ logging.info("Remote instance")
+
+ if options.contains("status"):
+ # fall through the default handling
+ # this will call do_command_line on the primary instance
+ # where we will handle this
+ return -1
+
+ if options.contains("quit"):
+ self.activate_action("quit", None)
+ return 0
+
+ if options.contains("enable"):
+ self.activate_action("enable_safeeyes", None)
+ return 0
+
+ if options.contains("disable"):
+ self.activate_action("disable_safeeyes", None)
+ return 0
+
+ if options.contains("about"):
+ self.activate_action("show_about", None)
+ return 0
+
+ if options.contains("settings"):
+ self.activate_action("show_settings", None)
+ return 0
+
+ if options.contains("take-break"):
+ self.activate_action("take_break", None)
+ return 0
+
+ logging.info("Safe Eyes is already running")
+ return 0 # TODO: return error code here?
+
+ else:
+ logging.info("Primary instance")
+
+ if (
+ options.contains("enable")
+ or options.contains("disable")
+ or options.contains("status")
+ or options.contains("quit")
+ ):
+ print(_("Safe Eyes is not running"))
+ self.activate_action("quit", None)
+ return 1
+
+ return -1 # continue default handling
+
+ def do_command_line(self, command_line):
+ Gtk.Application.do_command_line(self, command_line)
+
+ cli = command_line.get_options_dict().end().unpack()
+
+ if cli.get("status"):
+ # this is only invoked remotely
+ # this code runs in the primary instance, but will print to the output
+ # of the remote instance
+ command_line.print_literal(self.status())
+ return 0
+
+ logging.info("Handle primary command line")
+
+ self.activate()
+
+ if cli.get("about"):
+ self.show_about()
+ elif cli.get("settings"):
+ self.show_settings()
+ elif cli.get("take-break"):
+ self.take_break()
+
+ return 0
def do_startup(self):
Gtk.Application.do_startup(self)
@@ -135,9 +284,6 @@ def do_startup(self):
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
@@ -155,17 +301,6 @@ def do_activate(self):
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 _initialize_styles(self):
utility.load_css_file(
utility.SYSTEM_STYLE_SHEET_PATH, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
@@ -257,9 +392,10 @@ def quit(self):
self.plugins_manager.stop()
self.safe_eyes_core.stop()
self.plugins_manager.exit()
- self.__stop_rpc_server()
self.persist_session()
+ self.release()
+
super().quit()
def handle_suspend_callback(self, sleeping):
@@ -341,13 +477,6 @@ def save_settings(self, 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)
@@ -434,13 +563,3 @@ def persist_session(self):
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
diff --git a/safeeyes/translations.py b/safeeyes/translations.py
index f9ee344d..be71d466 100644
--- a/safeeyes/translations.py
+++ b/safeeyes/translations.py
@@ -19,8 +19,8 @@
"""Translation setup and helpers."""
import locale
-import logging
import gettext
+import sys
from safeeyes import utility
_translations = gettext.NullTranslations()
@@ -38,9 +38,10 @@ def setup():
# locale.bindtextdomain is required for Glade files
locale.bindtextdomain("safeeyes", utility.LOCALE_PATH)
except AttributeError:
- logging.warning(
+ print(
"installed python's gettext module does not support locale.bindtextdomain."
- " locale.bindtextdomain is required for Glade files"
+ " locale.bindtextdomain is required for Glade files",
+ file=sys.stderr,
)
return _translations
diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py
index 1189d109..76daf7e6 100644
--- a/safeeyes/ui/settings_dialog.py
+++ b/safeeyes/ui/settings_dialog.py
@@ -64,7 +64,6 @@ def __init__(self, application, config, on_save_settings):
self.last_short_break_interval = config.get("short_break_interval")
self.initializing = True
self.infobar_long_break_shown = False
- self.warn_bar_rpc_server_shown = False
builder = utility.create_gtk_builder(SETTINGS_DIALOG_GLADE)
@@ -88,11 +87,8 @@ def __init__(self, application, config, on_save_settings):
self.switch_random_order = builder.get_object("switch_random_order")
self.switch_postpone = builder.get_object("switch_postpone")
self.switch_persist = builder.get_object("switch_persist")
- self.switch_rpc_server = builder.get_object("switch_rpc_server")
self.info_bar_long_break = builder.get_object("info_bar_long_break")
- self.warn_bar_rpc_server = builder.get_object("warn_bar_rpc_server")
self.info_bar_long_break.hide()
- self.warn_bar_rpc_server.hide()
self.window.connect("close-request", self.on_window_delete)
builder.get_object("reset_menu").connect("clicked", self.on_reset_menu_clicked)
@@ -104,8 +100,6 @@ def __init__(self, application, config, on_save_settings):
self.spin_long_break_interval.connect(
"value-changed", self.on_spin_long_break_interval_change
)
- self.warn_bar_rpc_server.connect("close", self.on_warn_bar_rpc_server_close)
- self.warn_bar_rpc_server.connect("response", self.on_warn_bar_rpc_server_close)
builder.get_object("btn_add_break").connect("clicked", self.add_break)
# Set the current values of input fields
@@ -116,11 +110,6 @@ def __init__(self, application, config, on_save_settings):
self.on_switch_postpone_activate(
self.switch_postpone, self.switch_postpone.get_active()
)
- # Add event listener to RPC server switch
- self.switch_rpc_server.connect("state-set", self.on_switch_rpc_server_activate)
- self.on_switch_rpc_server_activate(
- self.switch_rpc_server, self.switch_rpc_server.get_active()
- )
self.initializing = False
@@ -148,7 +137,6 @@ def __initialize(self, config):
self.switch_random_order.set_active(config.get("random_order"))
self.switch_postpone.set_active(config.get("allow_postpone"))
self.switch_persist.set_active(config.get("persist_state"))
- self.switch_rpc_server.set_active(config.get("use_rpc_server"))
self.infobar_long_break_shown = False
def __create_break_item(self, break_config, is_short):
@@ -357,22 +345,6 @@ def on_info_bar_long_break_close(self, infobar, *user_data):
"""Event handler for info bar close action."""
self.info_bar_long_break.hide()
- def on_switch_rpc_server_activate(self, switch, enabled):
- """Event handler to the state change of the rpc server switch.
-
- Show or hide the self.warn_bar_rpc_server based on the state of
- the rpc server.
- """
- if not self.initializing and not enabled and not self.warn_bar_rpc_server_shown:
- self.warn_bar_rpc_server_shown = True
- self.warn_bar_rpc_server.show()
- if enabled:
- self.warn_bar_rpc_server.hide()
-
- def on_warn_bar_rpc_server_close(self, warnbar, *user_data):
- """Event handler for warning bar close action."""
- self.warn_bar_rpc_server.hide()
-
def add_break(self, button) -> None:
"""Event handler for add break button."""
dialog = NewBreakDialog(
@@ -412,7 +384,6 @@ def on_window_delete(self, *args):
self.config.set("random_order", self.switch_random_order.get_active())
self.config.set("allow_postpone", self.switch_postpone.get_active())
self.config.set("persist_state", self.switch_persist.get_active())
- self.config.set("use_rpc_server", self.switch_rpc_server.get_active())
for plugin in self.config.get("plugins"):
if plugin["id"] in self.plugin_switches:
plugin["enabled"] = self.plugin_switches[plugin["id"]].get_active()
diff --git a/safeeyes/utility.py b/safeeyes/utility.py
index 5444ae9e..28875471 100644
--- a/safeeyes/utility.py
+++ b/safeeyes/utility.py
@@ -46,7 +46,6 @@
from gi.repository import GLib
from gi.repository import GdkPixbuf
from packaging.version import parse
-from safeeyes.translations import translate as _
BIN_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
HOME_DIRECTORY = os.environ.get("HOME") or os.path.expanduser("~")
@@ -181,6 +180,8 @@ def delete(file_path):
def check_plugin_dependencies(plugin_id, plugin_config, plugin_settings, plugin_path):
"""Check the plugin dependencies."""
+ from safeeyes.translations import translate as _
+
# Check the desktop environment
if plugin_config["dependencies"]["desktop_environments"]:
# Plugin has restrictions on desktop environments
@@ -449,6 +450,8 @@ def cleanup_old_user_stylesheet():
logging.info("Deleting old stylesheet containing default content")
delete(OLD_STYLE_SHEET_PATH)
else:
+ from safeeyes.translations import translate as _
+
# Stylesheet was likely customized, don't delete but warn
logging.warning(
_(
@@ -711,6 +714,8 @@ def open_session():
def create_gtk_builder(glade_file):
"""Create a Gtk builder and load the glade file."""
+ from safeeyes.translations import translate as _
+
builder = Gtk.Builder()
builder.set_translation_domain("safeeyes")
builder.add_from_file(glade_file)