diff --git a/_caster.py b/_caster.py index 0662f1755..b0c0c1473 100644 --- a/_caster.py +++ b/_caster.py @@ -3,24 +3,55 @@ main Caster module Created on Jun 29, 2014 ''' +import imp +import logging import six +from dragonfly import get_engine +from dragonfly import RecognitionObserver +from castervoice.lib import control +from castervoice.lib import settings +from castervoice.lib.ctrl.dependencies import DependencyMan +from castervoice.lib.ctrl.updatecheck import UpdateChecker +from castervoice.lib.utilities import start_hud -if six.PY2: - import logging - logging.basicConfig() -from castervoice.lib.ctrl.dependencies import DependencyMan # requires nothing -DependencyMan().initialize() +class LoggingHandler(logging.Handler): + def __init__(self): + logging.Handler.__init__(self) + self.hud = control.nexus().comm.get_com("hud") -from castervoice.lib import settings -settings.initialize() + def emit(self, record): + try: + self.hud.send("# {}".format(record.msg)) + except ConnectionRefusedError: # pylint: disable=undefined-variable + print("# {}".format(record.msg)) -from castervoice.lib.ctrl.updatecheck import UpdateChecker # requires settings/dependencies -UpdateChecker().initialize() -from dragonfly import get_engine +class Observer(RecognitionObserver): + def __init__(self): + self.hud = control.nexus().comm.get_com("hud") + + def on_begin(self): + pass + + def on_recognition(self, words): + try: + self.hud.send("$ {}".format(" ".join(words))) + except ConnectionRefusedError: # pylint: disable=undefined-variable + print("$ {}".format(" ".join(words))) -_NEXUS = None + def on_failure(self): + try: + self.hud.send("?!") + except ConnectionRefusedError: # pylint: disable=undefined-variable + print("?!") + + +if six.PY2: + logging.basicConfig() +DependencyMan().initialize() # requires nothing +settings.initialize() +UpdateChecker().initialize() # requires settings/dependencies # get_engine() is used here as a workaround for running Natlink inprocess if get_engine().name in ["sapi5shared", "sapi5", "sapi5inproc"]: @@ -28,8 +59,6 @@ from castervoice.rules.ccr.standard import SymbolSpecs SymbolSpecs.set_cancel_word("escape") -from castervoice.lib import control - if control.nexus() is None: from castervoice.lib.ctrl.mgr.loading.load.content_loader import ContentLoader from castervoice.lib.ctrl.mgr.loading.load.content_request_generator import ContentRequestGenerator @@ -42,3 +71,13 @@ sikuli_controller.get_instance().bootstrap_start_server_proxy() print("\n*- Starting " + settings.SOFTWARE_NAME + " -*") + +try: + imp.find_module('PySide2') + start_hud() + _logger = logging.getLogger('caster') + _logger.addHandler(LoggingHandler()) # must be after nexus initialization + _logger.setLevel(logging.DEBUG) + Observer().register() # must be after HUD process has started +except ImportError: + pass # HUD is not available diff --git a/castervoice/asynch/hud.py b/castervoice/asynch/hud.py new file mode 100644 index 000000000..5ba12853d --- /dev/null +++ b/castervoice/asynch/hud.py @@ -0,0 +1,217 @@ +#! python +''' +Caster HUD Window module +''' +# pylint: disable=import-error,no-name-in-module +import html +import json +import os +import signal +import sys +import threading +import PySide2.QtCore +import PySide2.QtGui +import dragonfly +from xmlrpc.server import SimpleXMLRPCServer +from PySide2.QtWidgets import QApplication +from PySide2.QtWidgets import QMainWindow +from PySide2.QtWidgets import QTextEdit +from PySide2.QtWidgets import QTreeView +from PySide2.QtWidgets import QVBoxLayout +from PySide2.QtWidgets import QWidget +try: # Style C -- may be imported into Caster, or externally + BASE_PATH = os.path.realpath(__file__).rsplit(os.path.sep + "castervoice", 1)[0] + if BASE_PATH not in sys.path: + sys.path.append(BASE_PATH) +finally: + from castervoice.lib.merge.communication import Communicator + +HIDE_HUD_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1)) +SHOW_HUD_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1)) +HIDE_RULES_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1)) +SHOW_RULES_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1)) +SEND_COMMAND_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1)) + + +class RPCEvent(PySide2.QtCore.QEvent): + + def __init__(self, type, text): + PySide2.QtCore.QEvent.__init__(self, type) + self._text = text + + @property + def text(self): + return self._text + + +class RulesWindow(QWidget): + + _WIDTH = 600 + _MARGIN = 30 + + def __init__(self, text): + QWidget.__init__(self, f=(PySide2.QtCore.Qt.WindowStaysOnTopHint)) + x = dragonfly.monitors[0].rectangle.dx - (RulesWindow._WIDTH + RulesWindow._MARGIN) + y = 300 + dx = RulesWindow._WIDTH + dy = dragonfly.monitors[0].rectangle.dy - (y + 2 * RulesWindow._MARGIN) + self.setGeometry(x, y, dx, dy) + self.setWindowTitle("Active Rules") + rules_tree = PySide2.QtGui.QStandardItemModel() + rules_tree.setColumnCount(2) + rules_tree.setHorizontalHeaderLabels(['phrase', 'action']) + rules_dict = json.loads(text) + rules = rules_tree.invisibleRootItem() + for g in rules_dict: + gram = PySide2.QtGui.QStandardItem(g["name"]) if len(g["rules"]) > 1 else None + for r in g["rules"]: + rule = PySide2.QtGui.QStandardItem(r["name"]) + rule.setRowCount(len(r["specs"])) + rule.setColumnCount(2) + row = 0 + for s in r["specs"]: + phrase, _, action = s.partition('::') + rule.setChild(row, 0, PySide2.QtGui.QStandardItem(phrase)) + rule.setChild(row, 1, PySide2.QtGui.QStandardItem(action)) + row += 1 + if gram is None: + rules.appendRow(rule) + else: + gram.appendRow(rule) + if gram: + rules.appendRow(gram) + tree_view = QTreeView(self) + tree_view.setModel(rules_tree) + tree_view.setColumnWidth(0, RulesWindow._WIDTH / 2) + layout = QVBoxLayout() + layout.addWidget(tree_view) + self.setLayout(layout) + + +class HUDWindow(QMainWindow): + + _WIDTH = 300 + _HEIGHT = 200 + _MARGIN = 30 + + def __init__(self, server): + QMainWindow.__init__(self, flags=(PySide2.QtCore.Qt.WindowStaysOnTopHint)) + x = dragonfly.monitors[0].rectangle.dx - (HUDWindow._WIDTH + HUDWindow._MARGIN) + y = HUDWindow._MARGIN + dx = HUDWindow._WIDTH + dy = HUDWindow._HEIGHT + self.server = server + self.setup_xmlrpc_server() + self.setGeometry(x, y, dx, dy) + self.setWindowTitle("Caster HUD") + self.output = QTextEdit() + self.output.setReadOnly(True) + self.setCentralWidget(self.output) + self.rules_window = None + self.commands_count = 0 + + def event(self, event): + if event.type() == SHOW_HUD_EVENT: + self.show() + return True + if event.type() == HIDE_HUD_EVENT: + self.hide() + return True + if event.type() == SHOW_RULES_EVENT: + self.rules_window = RulesWindow(event.text) + self.rules_window.show() + return True + if event.type() == HIDE_RULES_EVENT and self.rules_window: + self.rules_window.close() + self.rules_window = None + return True + if event.type() == SEND_COMMAND_EVENT: + escaped_text = html.escape(event.text) + if escaped_text.startswith('$'): + formatted_text = '<{}'.format(escaped_text[1:]) + if self.commands_count == 0: + self.output.setHtml(formatted_text) + else: + # self.output.append('
') + self.output.append(formatted_text) + cursor = self.output.textCursor() + cursor.movePosition(PySide2.QtGui.QTextCursor.End) + self.output.setTextCursor(cursor) + self.output.ensureCursorVisible() + self.commands_count += 1 + if self.commands_count == 20: + self.commands_count = 0 + return True + elif escaped_text.startswith('#'): + formatted_text = '>{}'.format(escaped_text[1:]) + else: + formatted_text = escaped_text + self.output.append(formatted_text) + self.output.ensureCursorVisible() + return True + return QMainWindow.event(self, event) + + def closeEvent(self, event): + event.accept() + + def setup_xmlrpc_server(self): + self.server.register_function(self.xmlrpc_ping, "ping") + self.server.register_function(self.xmlrpc_hide_hud, "hide_hud") + self.server.register_function(self.xmlrpc_hide_rules, "hide_rules") + self.server.register_function(self.xmlrpc_kill, "kill") + self.server.register_function(self.xmlrpc_send, "send") + self.server.register_function(self.xmlrpc_show_hud, "show_hud") + self.server.register_function(self.xmlrpc_show_rules, "show_rules") + server_thread = threading.Thread(target=self.server.serve_forever) + server_thread.daemon = True + server_thread.start() + + def xmlrpc_ping(self): + return 0 + + def xmlrpc_hide_hud(self): + PySide2.QtCore.QCoreApplication.postEvent(self, PySide2.QtCore.QEvent(HIDE_HUD_EVENT)) + return 0 + + def xmlrpc_show_hud(self): + PySide2.QtCore.QCoreApplication.postEvent(self, PySide2.QtCore.QEvent(SHOW_HUD_EVENT)) + return 0 + + def xmlrpc_hide_rules(self): + PySide2.QtCore.QCoreApplication.postEvent(self, PySide2.QtCore.QEvent(HIDE_RULES_EVENT)) + return 0 + + def xmlrpc_kill(self): + QApplication.quit() + + def xmlrpc_send(self, text): + PySide2.QtCore.QCoreApplication.postEvent(self, RPCEvent(SEND_COMMAND_EVENT, text)) + return len(text) + + def xmlrpc_show_rules(self, text): + PySide2.QtCore.QCoreApplication.postEvent(self, RPCEvent(SHOW_RULES_EVENT, text)) + return len(text) + + +def handler(signum, frame): + """ + This handler doesn't stop the application when ^C is pressed, + but it prevents exceptions being thrown when later + the application is terminated from GUI. Normally, HUD is started + by the recognition process and can't be killed from shell prompt, + in which case this handler is not needed. + """ + pass + + +if __name__ == "__main__": + signal.signal(signal.SIGINT, handler) + server_address = (Communicator.LOCALHOST, Communicator().com_registry["hud"]) + # allow_none=True means Python constant None will be translated into XML + server = SimpleXMLRPCServer(server_address, logRequests=False, allow_none=True) + app = QApplication(sys.argv) + window = HUDWindow(server) + window.show() + exit_code = app.exec_() + server.shutdown() + sys.exit(exit_code) diff --git a/castervoice/lib/ctrl/mgr/grammar_manager.py b/castervoice/lib/ctrl/mgr/grammar_manager.py index 7177af885..81f021c86 100644 --- a/castervoice/lib/ctrl/mgr/grammar_manager.py +++ b/castervoice/lib/ctrl/mgr/grammar_manager.py @@ -1,4 +1,5 @@ -import os, traceback +import os +import traceback from dragonfly import Grammar @@ -11,6 +12,7 @@ from castervoice.lib.ctrl.mgr.rules_enabled_diff import RulesEnabledDiff from castervoice.lib.merge.ccrmerging2.hooks.events.activation_event import RuleActivationEvent from castervoice.lib.merge.ccrmerging2.hooks.events.on_error_event import OnErrorEvent +from castervoice.lib.merge.ccrmerging2.hooks.events.rules_loaded_event import RulesLoadedEvent from castervoice.lib.merge.ccrmerging2.sorting.config_ruleset_sorter import ConfigBasedRuleSetSorter from castervoice.lib.util.ordered_set import OrderedSet @@ -239,7 +241,7 @@ def _remerge_ccr_rules(self, enabled_rcns): active_rule_class_names = [rcn for rcn in enabled_rcns if rcn in loaded_enabled_rcns] active_mrs = [self._managed_rules[rcn] for rcn in active_rule_class_names] active_ccr_mrs = [mr for mr in active_mrs if mr.get_details().declared_ccrtype is not None] - + self._hooks_runner.execute(RulesLoadedEvent(rules=active_ccr_mrs)) ''' The merge may result in 1 to n+1 rules where n is the number of ccr app rules which are in the active rules list. @@ -298,7 +300,7 @@ def receive(self, file_path_changed): if class_name in self._config.get_enabled_rcns_ordered(): self._delegate_enable_rule(class_name, True) except Exception as error: - printer.out('Grammar Manager: {} - See error message above'.format(error)) + printer.out('Grammar Manager: {} - See error message above'.format(error)) self._hooks_runner.execute(OnErrorEvent()) def _get_invalidation(self, rule_class, details): diff --git a/castervoice/lib/ctrl/mgr/rule_details.py b/castervoice/lib/ctrl/mgr/rule_details.py index b25ea9cfd..e42d59f1a 100644 --- a/castervoice/lib/ctrl/mgr/rule_details.py +++ b/castervoice/lib/ctrl/mgr/rule_details.py @@ -1,5 +1,6 @@ -import os, traceback import inspect +import os +import traceback from castervoice.lib import printer @@ -34,20 +35,23 @@ def __init__(self, name=None, function_context=None, executable=None, title=None stack = inspect.stack(0) self._filepath = RuleDetails._calculate_filepath_from_frame(stack, 1) + def __str__(self): + return 'ccrtype {}'.format(self.declared_ccrtype if self.declared_ccrtype else '_') + @staticmethod def _calculate_filepath_from_frame(stack, index): try: frame = stack[index] module = inspect.getmodule(frame[0]) - filepath = module.__file__.replace("\\", "/") + filepath = module.__file__.replace("\\", "/") if filepath.endswith("pyc"): filepath = filepath[:-1] return filepath - except AttributeError as e: + except AttributeError: if not os.path.isfile(frame[1]): pyc = frame[1] + "c" if os.path.isfile(pyc): - printer.out("\n {} \n Caster Detected a stale .pyc file. The stale file has been removed please restart Caster. \n".format(pyc)) + printer.out('\n {}\n Caster removed a stale .pyc file. Please, restart Caster. \n'.format(pyc)) os.remove(pyc) else: traceback.print_exc() diff --git a/castervoice/lib/merge/ccrmerging2/hooks/events/event_types.py b/castervoice/lib/merge/ccrmerging2/hooks/events/event_types.py index f5b2eef89..b00acb8ba 100644 --- a/castervoice/lib/merge/ccrmerging2/hooks/events/event_types.py +++ b/castervoice/lib/merge/ccrmerging2/hooks/events/event_types.py @@ -2,3 +2,4 @@ class EventType(object): ACTIVATION = "activation" NODE_CHANGE = "node change" ON_ERROR = "on error" + RULES_LOADED = "rules loaded" diff --git a/castervoice/lib/merge/ccrmerging2/hooks/events/rules_loaded_event.py b/castervoice/lib/merge/ccrmerging2/hooks/events/rules_loaded_event.py new file mode 100644 index 000000000..3379accde --- /dev/null +++ b/castervoice/lib/merge/ccrmerging2/hooks/events/rules_loaded_event.py @@ -0,0 +1,8 @@ +from castervoice.lib.merge.ccrmerging2.hooks.events.base_event import BaseHookEvent +from castervoice.lib.merge.ccrmerging2.hooks.events.event_types import EventType + + +class RulesLoadedEvent(BaseHookEvent): + def __init__(self, rules=None): + super(RulesLoadedEvent, self).__init__(EventType.RULES_LOADED) + self.rules = rules diff --git a/castervoice/lib/merge/ccrmerging2/hooks/standard_hooks/rules_loaded_hook.py b/castervoice/lib/merge/ccrmerging2/hooks/standard_hooks/rules_loaded_hook.py new file mode 100644 index 000000000..35096f12d --- /dev/null +++ b/castervoice/lib/merge/ccrmerging2/hooks/standard_hooks/rules_loaded_hook.py @@ -0,0 +1,18 @@ +import castervoice.lib.rules_collection +from castervoice.lib.merge.ccrmerging2.hooks.base_hook import BaseHook +from castervoice.lib.merge.ccrmerging2.hooks.events.event_types import EventType + + +class RulesLoadedHook(BaseHook): + def __init__(self): + super(RulesLoadedHook, self).__init__(EventType.RULES_LOADED) + + def get_pronunciation(self): + return "rules loaded" + + def run(self, event_data): + castervoice.lib.rules_collection.get_instance().update(event_data.rules) + + +def get_hook(): + return RulesLoadedHook diff --git a/castervoice/lib/merge/communication.py b/castervoice/lib/merge/communication.py index 9000ea2ab..5614d1263 100644 --- a/castervoice/lib/merge/communication.py +++ b/castervoice/lib/merge/communication.py @@ -10,7 +10,8 @@ class Communicator: def __init__(self): self.coms = {} self.com_registry = { - "hmc": 1338, + "hmc": 1337, + "hud": 1338, "grids": 1339, "sikuli": 8000 } diff --git a/castervoice/lib/merge/state/actions.py b/castervoice/lib/merge/state/actions.py index 619a60660..a0cb929d6 100644 --- a/castervoice/lib/merge/state/actions.py +++ b/castervoice/lib/merge/state/actions.py @@ -1,3 +1,4 @@ +from functools import reduce from dragonfly import ActionBase from castervoice.lib import control @@ -34,6 +35,9 @@ def set_nexus(self, nexus): def nexus(self): return self._nexus or control.nexus() + def __str__(self): + return '{}'.format(self.base) + class ContextSeeker(RegisteredAction): def __init__(self, @@ -53,6 +57,10 @@ def __init__(self, def _execute(self, data=None): self.nexus().state.add(StackItemSeeker(self, data)) + def __str__(self): + tail = reduce((lambda x, y: "{}_{}".format(x, y)), self.forward) if isinstance(self.forward, list) else self.forward + return '{}!{}'.format(self.back, tail) if self.back else '!{}'.format(tail) + class AsynchronousAction(ContextSeeker): ''' @@ -91,6 +99,10 @@ def _execute(self, data=None): self.nexus().state.add(StackItemAsynchronous(self, data)) + def __str__(self): + action = reduce((lambda x, y: "{}${}".format(x, y)), self.forward) if isinstance(self.forward, list) else self.forward + return '#{}&{}*{}'.format(self.time_in_seconds, action, self.repetitions) + @staticmethod def hmc_complete(data_function): ''' returns a function which applies the passed in function to diff --git a/castervoice/lib/merge/state/contextoptions.py b/castervoice/lib/merge/state/contextoptions.py index 43b4970c4..365a913a2 100644 --- a/castervoice/lib/merge/state/contextoptions.py +++ b/castervoice/lib/merge/state/contextoptions.py @@ -4,6 +4,9 @@ @author: dave ''' +from functools import reduce +from types import FunctionType + class ContextSet: # ContextSet ''' @@ -27,6 +30,13 @@ def __init__(self, self.use_spoken = use_spoken self.use_rspec = use_rspec + def __str__(self): + prefix = reduce((lambda x, y: '{}`{}'.format(x, y)), + self.specTriggers) if len(self.specTriggers) > 1 else self.specTriggers[0].__str__() + params = reduce((lambda x, y: '{}, {}'.format(x, y)), self.parameters) if self.parameters else '' + action = self.f.__name__ if type(self.f) is FunctionType else self.f.__str__() + return '{}^{}({})'.format(prefix, action, params) + class ContextLevel: # ContextLevel ''' @@ -48,3 +58,10 @@ def copy(self): def number(self, index): # used for assigning indices self.index = index + + def __str__(self): + if len(self.sets) > 1: + return reduce((lambda x, y: '{}, {}'.format(x, y)), self.sets) + elif len(self.sets) == 1: + return '{}'.format(self.sets[0]) + return '' diff --git a/castervoice/lib/printer.py b/castervoice/lib/printer.py index c7dbd4390..92dbc84b2 100644 --- a/castervoice/lib/printer.py +++ b/castervoice/lib/printer.py @@ -1,6 +1,12 @@ +import logging + + +_log = logging.getLogger("caster") + + def out(*args): """ Use this as a printing interface to send messages to places other than the console. DO NOT import anything in this class. Use *args. """ - print("\n".join([str(o) for o in args])) + _log.debug("\n".join([str(o) for o in args])) diff --git a/castervoice/lib/rules_collection.py b/castervoice/lib/rules_collection.py new file mode 100644 index 000000000..f1ba607aa --- /dev/null +++ b/castervoice/lib/rules_collection.py @@ -0,0 +1,33 @@ +''' +Collection of rules that are merged into a CCR grammar. +''' +_RULES = None + + +class RulesCollection: + + def __init__(self): + self._rules = [] + + def update(self, rules=None): + self._rules = rules + + def serialize(self): + rules = [] + for rule in self._rules: + klass = rule.get_rule_class() + instance = rule.get_rule_instance() + mapping = instance._smr_mapping if '_smr_mapping' in instance.__dict__ else klass.mapping + specs = sorted(["{}::{}".format(x, mapping[x]) for x in mapping]) + rules.append({ + 'name': rule.get_rule_class_name(), + 'specs': specs + }) + return [{'name': 'ccr', 'rules': rules}] + + +def get_instance(): + global _RULES + if _RULES is None: + _RULES = RulesCollection() + return _RULES diff --git a/castervoice/lib/settings.py b/castervoice/lib/settings.py index 5657c3e6b..37595199b 100644 --- a/castervoice/lib/settings.py +++ b/castervoice/lib/settings.py @@ -35,6 +35,7 @@ HMC_TITLE_RECORDING = " :: Recording Manager" HMC_TITLE_DIRECTORY = " :: Directory Selector" HMC_TITLE_CONFIRM = " :: Confirm" +HUD_TITLE = "Caster HUD" LEGION_TITLE = "legiongrid" RAINBOW_TITLE = "rainbowgrid" DOUGLAS_TITLE = "douglasgrid" @@ -297,6 +298,8 @@ def _get_defaults(): _validate_engine_path(), "HOMUNCULUS_PATH": str(Path(_BASE_PATH).joinpath("asynch/hmc/h_launch.py")), + "HUD_PATH": + str(Path(_BASE_PATH).joinpath("asynch/hud.py")), "LEGION_PATH": str(Path(_BASE_PATH).joinpath("asynch/mouse/legion.py")), "MEDIA_PATH": @@ -361,7 +364,7 @@ def _get_defaults(): # Default enabled hooks: Use hook class name "hooks": { - "default_hooks": ['PrinterHook'], + "default_hooks": ['PrinterHook', 'RulesLoadedHook'], }, # miscellaneous section diff --git a/castervoice/lib/utilities.py b/castervoice/lib/utilities.py index 97775e996..ae210708b 100644 --- a/castervoice/lib/utilities.py +++ b/castervoice/lib/utilities.py @@ -1,25 +1,24 @@ # -*- coding: utf-8 -*- from __future__ import print_function, unicode_literals -from builtins import str import io import json -import locale -import six import os import re import sys import six +import subprocess import time -import traceback -from subprocess import Popen import tomlkit +import traceback -from dragonfly import Key, Pause, Window, get_current_engine +from dragonfly import CompoundRule, Key, MappingRule, Pause, Window, get_current_engine -from castervoice.lib.clipboard import Clipboard +from castervoice.lib import control from castervoice.lib import printer +from castervoice.lib.clipboard import Clipboard +from castervoice.lib.rules_collection import get_instance from castervoice.lib.util import guidance if six.PY2: @@ -32,25 +31,22 @@ if BASE_PATH not in sys.path: sys.path.append(BASE_PATH) finally: - from castervoice.lib import settings, printer + from castervoice.lib import settings # TODO: Move functions that manipulate or retrieve information from Windows to `window_mgmt_support` in navigation_rules. # TODO: Implement Optional exact title matching for `get_matching_windows` in Dragonfly def window_exists(windowname=None, executable=None): - if Window.get_matching_windows(title=windowname, executable=executable): - return True - else: - return False + return Window.get_matching_windows(title=windowname, executable=executable) and True -def get_window_by_title(title=None): +def get_window_by_title(title=None): # returns 0 if nothing found Matches = Window.get_matching_windows(title=title) if Matches: return Matches[0].handle else: - return 0 + return 0 def get_active_window_title(): @@ -193,23 +189,24 @@ def remote_debug(who_called_it=None): printer.out("ERROR: " + who_called_it + " called utilities.remote_debug() but the debug server wasn't running.") + def reboot(): # TODO: Save engine arguments elsewhere and retrieves for reboot. Allows for user-defined arguments. popen_parameters = [] engine = get_current_engine() if engine.name == 'kaldi': engine.disconnect() - Popen([sys.executable, '-m', 'dragonfly', 'load', '_*.py', '--engine', 'kaldi', '--no-recobs-messages']) + subprocess.Popen([sys.executable, '-m', 'dragonfly', 'load', '_*.py', '--engine', 'kaldi', '--no-recobs-messages']) if engine.name == 'sapi5inproc': engine.disconnect() - Popen([sys.executable, '-m', 'dragonfly', 'load', '--engine', 'sapi5inproc', '_*.py', '--no-recobs-messages']) + subprocess.Popen([sys.executable, '-m', 'dragonfly', 'load', '--engine', 'sapi5inproc', '_*.py', '--no-recobs-messages']) if engine.name in ["sapi5shared", "sapi5"]: popen_parameters.append(settings.SETTINGS["paths"]["REBOOT_PATH_WSR"]) popen_parameters.append(settings.SETTINGS["paths"]["WSR_PATH"]) printer.out(popen_parameters) - Popen(popen_parameters) + subprocess.Popen(popen_parameters) if engine.name == 'natlink': - import natlinkstatus # pylint: disable=import-error + import natlinkstatus # pylint: disable=import-error status = natlinkstatus.NatlinkStatus() if status.NatlinkIsEnabled() == 1: # Natlink in-process @@ -218,21 +215,21 @@ def reboot(): username = status.getUserName() popen_parameters.append(username) printer.out(popen_parameters) - Popen(popen_parameters) + subprocess.Popen(popen_parameters) else: - # Natlink out-of-process + # Natlink out-of-process engine.disconnect() - Popen([sys.executable, '-m', 'dragonfly', 'load', '--engine', 'natlink', '_*.py', '--no-recobs-messages']) + subprocess.Popen([sys.executable, '-m', 'dragonfly', 'load', '--engine', 'natlink', '_*.py', '--no-recobs-messages']) # TODO: Implement default_browser_command Mac/Linux def default_browser_command(): if sys.platform.startswith('win'): if six.PY2: - from _winreg import (CloseKey, ConnectRegistry, HKEY_CLASSES_ROOT, # pylint: disable=import-error,no-name-in-module + from _winreg import (CloseKey, ConnectRegistry, HKEY_CLASSES_ROOT, # pylint: disable=import-error,no-name-in-module HKEY_CURRENT_USER, OpenKey, QueryValueEx) else: - from winreg import (CloseKey, ConnectRegistry, HKEY_CLASSES_ROOT, # pylint: disable=import-error,no-name-in-module + from winreg import (CloseKey, ConnectRegistry, HKEY_CLASSES_ROOT, # pylint: disable=import-error,no-name-in-module HKEY_CURRENT_USER, OpenKey, QueryValueEx) ''' Tries to get default browser command, returns either a space delimited @@ -266,9 +263,9 @@ def clear_log(): # TODO: window_exists utilized when engine launched through Dragonfly CLI via bat in future try: if sys.platform.startswith('win'): - clearcmd = "cls" # Windows OS + clearcmd = "cls" # Windows OS else: - clearcmd = "clear" # Linux + clearcmd = "clear" # Linux if get_current_engine().name == 'natlink': import natlinkstatus # pylint: disable=import-error status = natlinkstatus.NatlinkStatus() @@ -348,3 +345,60 @@ def get_clipboard_files(folders=False): return files else: printer.out("get_clipboard_files: Not implemented for OS") + + +def start_hud(): + hud = control.nexus().comm.get_com("hud") + try: + hud.ping() + except ConnectionRefusedError: # pylint: disable=undefined-variable + subprocess.Popen([settings.SETTINGS["paths"]["PYTHONW"], + settings.SETTINGS["paths"]["HUD_PATH"]]) + + +def show_hud(): + hud = control.nexus().comm.get_com("hud") + hud.show_hud() + + +def hide_hud(): + hud = control.nexus().comm.get_com("hud") + hud.hide_hud() + + +def show_rules(): + """ + Get a list of active grammars loaded into the current engine, + including active rules and their attributes. Send the list + to HUD GUI for display. + """ + grammars = [] + engine = get_current_engine() + for grammar in engine.grammars: + if any([r.active for r in grammar.rules]): + rules = [] + for rule in grammar.rules: + if rule.active and not rule.name.startswith('_'): + if isinstance(rule, CompoundRule): + specs = [rule.spec] + elif isinstance(rule, MappingRule): + specs = sorted(["{}::{}".format(x, rule._mapping[x]) for x in rule._mapping]) + else: + specs = [rule.element.gstring()] + rules.append({ + "name": rule.name, + "exported": rule.exported, + "specs": specs + }) + grammars.append({"name": grammar.name, "rules": rules}) + grammars.extend(get_instance().serialize()) + hud = control.nexus().comm.get_com("hud") + hud.show_rules(json.dumps(grammars)) + + +def hide_rules(): + """ + Instruct HUD to hide the frame with the list of rules. + """ + hud = control.nexus().comm.get_com("hud") + hud.hide_rules() diff --git a/castervoice/rules/core/utility_rules/caster_rule.py b/castervoice/rules/core/utility_rules/caster_rule.py index ef8c53b52..0c7509043 100644 --- a/castervoice/rules/core/utility_rules/caster_rule.py +++ b/castervoice/rules/core/utility_rules/caster_rule.py @@ -1,10 +1,14 @@ -from dragonfly import MappingRule, Function, RunCommand, Playback +from dragonfly import MappingRule, Function, RunCommand from castervoice.lib import control, utilities from castervoice.lib.ctrl.dependencies import find_pip # pylint: disable=no-name-in-module from castervoice.lib.ctrl.updatecheck import update from castervoice.lib.ctrl.mgr.rule_details import RuleDetails from castervoice.lib.merge.state.short import R +from castervoice.lib.utilities import show_hud +from castervoice.lib.utilities import hide_hud +from castervoice.lib.utilities import show_rules +from castervoice.lib.utilities import hide_rules _PIP = find_pip() @@ -31,7 +35,7 @@ class CasterRule(MappingRule): "update dragonfly": R(_DependencyUpdate([_PIP, "install", "--upgrade", "dragonfly2"])), # update management ToDo: Fully implement castervoice PIP install - #"update caster": + #"update caster": # R(_DependencyUpdate([_PIP, "install", "--upgrade", "castervoice"])), # ccr de/activation @@ -39,6 +43,14 @@ class CasterRule(MappingRule): R(Function(lambda: control.nexus().set_ccr_active(True))), "disable (c c r|ccr)": R(Function(lambda: control.nexus().set_ccr_active(False))), + "show hud": + R(Function(show_hud), rdescript="Show the HUD window"), + "hide hud": + R(Function(hide_hud), rdescript="Hide the HUD window"), + "show rules": + R(Function(show_rules), rdescript="Open HUD frame with the list of active rules"), + "hide rules": + R(Function(hide_rules), rdescript="Hide the list of active rules") }