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")
}