diff --git a/_caster.py b/_caster.py
index bda4b7a0b..2c0ffb18c 100644
--- a/_caster.py
+++ b/_caster.py
@@ -2,26 +2,26 @@
main Caster module
Created on Jun 29, 2014
'''
-
+import logging
import importlib
-from castervoice.lib.ctrl.dependencies import DependencyMan # requires nothing
-DependencyMan().initialize()
-
-from castervoice.lib import settings # requires DependencyMan to be initialized
-settings.initialize()
+from dragonfly import get_engine, get_current_engine
+from castervoice.lib import control
+from castervoice.lib import settings
+from castervoice.lib import printer
+from castervoice.lib.ctrl.configure_engine import EngineConfigEarly, EngineConfigLate
+from castervoice.lib.ctrl.dependencies import DependencyMan
+from castervoice.lib.ctrl.updatecheck import UpdateChecker
+from castervoice.asynch import hud_support
-from castervoice.lib.ctrl.updatecheck import UpdateChecker # requires settings/dependencies
-UpdateChecker().initialize()
+printer.out("@ - Starting {} with `{}` Engine -\n".format(settings.SOFTWARE_NAME, get_engine().name))
-from castervoice.lib.ctrl.configure_engine import EngineConfigEarly, EngineConfigLate
+DependencyMan().initialize() # requires nothing
+settings.initialize()
+UpdateChecker().initialize() # requires settings/dependencies
EngineConfigEarly() # requires settings/dependencies
-_NEXUS = None
-
-from castervoice.lib import printer
-from castervoice.lib import control
-if control.nexus() is None: # Initialize Caster State
+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
from castervoice.lib.ctrl.mgr.loading.load.reload_fn_provider import ReloadFunctionProvider
@@ -32,9 +32,17 @@
_content_loader = ContentLoader(_crg, importlib.import_module, _rp.get_reload_fn(), _sma)
control.init_nexus(_content_loader)
EngineConfigLate() # Requires grammars to be loaded and nexus
-
+
if settings.SETTINGS["sikuli"]["enabled"]:
from castervoice.asynch.sikuli import sikuli_controller
sikuli_controller.get_instance().bootstrap_start_server_proxy()
-printer.out("\n*- Starting " + settings.SOFTWARE_NAME + " -*")
+try:
+ if get_current_engine().name != "text":
+ hud_support.start_hud()
+except ImportError:
+ pass # HUD is not available
+
+dh = printer.get_delegating_handler()
+dh.register_handler(hud_support.HudPrintMessageHandler()) # After hud starts
+printer.out("\n") # Force update to display text
diff --git a/castervoice/asynch/hmc/h_launch.py b/castervoice/asynch/hmc/h_launch.py
index ecbe6dbbe..e25aa4ff9 100644
--- a/castervoice/asynch/hmc/h_launch.py
+++ b/castervoice/asynch/hmc/h_launch.py
@@ -1,36 +1,26 @@
-from subprocess import Popen
-import sys, os
+import os
+import subprocess
+import sys
+
+from xmlrpc.server import SimpleXMLRPCServer # pylint: disable=no-name-in-module
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.asynch.hmc.hmc_ask_directory import HomunculusDirectory
- from castervoice.asynch.hmc.hmc_recording import HomunculusRecording
- from castervoice.asynch.hmc.hmc_confirm import HomunculusConfirm
- from castervoice.asynch.hmc.homunculus import Homunculus
from castervoice.lib import settings
-'''
-To add a new homunculus (pop-up ui window) type:
- (1) create the module
- (2) and its type and title constants to settings.py
- (3) add it to _get_title(), and "if __name__ == '__main__':" in this module
- (4) call launch() from this module with its type and any data it needs (data as a single string with no spaces)
-'''
def launch(hmc_type, data=None):
from dragonfly import (WaitWindow, FocusWindow, Key)
instructions = _get_instructions(hmc_type)
- if data is not None: # and callback!=None:
+ if data is not None:
instructions.append(data)
- Popen(instructions)
-
+ subprocess.Popen(instructions)
hmc_title = _get_title(hmc_type)
WaitWindow(title=hmc_title, timeout=5).execute()
FocusWindow(title=hmc_title).execute()
- Key("tab").execute()
def _get_instructions(hmc_type):
@@ -61,17 +51,25 @@ def _get_title(hmc_type):
return default
+def main():
+ # TODO: Remove this try wrapper when CI server supports Qt
+ try:
+ import PySide2.QtWidgets
+ from castervoice.asynch.hmc.homunculus import Homunculus
+ from castervoice.lib.merge.communication import Communicator
+ except ImportError:
+ sys.exit(0)
+ server_address = (Communicator.LOCALHOST, Communicator().com_registry["hmc"])
+ # Enabled by default logging causes RPC to malfunction when the GUI runs on
+ # pythonw. Explicitly disable logging for the XML server.
+ server = SimpleXMLRPCServer(server_address, logRequests=False, allow_none=True)
+ app = PySide2.QtWidgets.QApplication(sys.argv)
+ window = Homunculus(server, sys.argv)
+ window.show()
+ exit_code = app.exec_()
+ server.shutdown()
+ sys.exit(exit_code)
+
+
if __name__ == '__main__':
- found_word = None
- if len(sys.argv) > 2:
- found_word = sys.argv[2]
- if sys.argv[1] == settings.QTYPE_DEFAULT:
- app = Homunculus(sys.argv[1])
- elif sys.argv[1] == settings.QTYPE_RECORDING:
- app = HomunculusRecording([settings.QTYPE_RECORDING, found_word])
- elif sys.argv[1] == settings.QTYPE_INSTRUCTIONS:
- app = Homunculus(sys.argv[1], sys.argv[2])
- elif sys.argv[1] == settings.QTYPE_DIRECTORY:
- app = HomunculusDirectory(sys.argv[1])
- elif sys.argv[1] == settings.QTYPE_CONFIRM:
- app = HomunculusConfirm([sys.argv[1], sys.argv[2]])
+ main()
diff --git a/castervoice/asynch/hmc/hmc_ask_directory.py b/castervoice/asynch/hmc/hmc_ask_directory.py
index 29c979a58..8fad0cce5 100644
--- a/castervoice/asynch/hmc/hmc_ask_directory.py
+++ b/castervoice/asynch/hmc/hmc_ask_directory.py
@@ -15,7 +15,7 @@
class HomunculusDirectory(Homunculus):
def __init__(self, params):
- Homunculus.__init__(self, params[0])
+ Homunculus.__init__(self, params[0], args=None)
self.title(settings.HOMUNCULUS_VERSION + settings.HMC_TITLE_DIRECTORY)
self.geometry("640x50+" + str(int(self.winfo_screenwidth()/2 - 320)) + "+" +
diff --git a/castervoice/asynch/hmc/hmc_confirm.py b/castervoice/asynch/hmc/hmc_confirm.py
index 7c25da031..18e38ea32 100644
--- a/castervoice/asynch/hmc/hmc_confirm.py
+++ b/castervoice/asynch/hmc/hmc_confirm.py
@@ -14,7 +14,7 @@
class HomunculusConfirm(Homunculus):
def __init__(self, params):
- Homunculus.__init__(self, params[0])
+ Homunculus.__init__(self, params[0], args=None)
self.title(settings.HOMUNCULUS_VERSION + settings.HMC_TITLE_CONFIRM)
self.geometry("320x50+" + str(int(self.winfo_screenwidth()/2 - 160)) + "+" +
diff --git a/castervoice/asynch/hmc/hmc_recording.py b/castervoice/asynch/hmc/hmc_recording.py
index 64881947d..5d6dd45ad 100644
--- a/castervoice/asynch/hmc/hmc_recording.py
+++ b/castervoice/asynch/hmc/hmc_recording.py
@@ -21,7 +21,7 @@ def get_row(self, cut_off=0):
def __init__(self, params):
self.grid_row = 0
- Homunculus.__init__(self, params[0])
+ Homunculus.__init__(self, params[0], args=None)
self.title(settings.HOMUNCULUS_VERSION + settings.HMC_TITLE_RECORDING)
self.geometry("640x480+" + str(int(self.winfo_screenwidth()/2 - 320)) + "+" +
diff --git a/castervoice/asynch/hmc/homunculus.py b/castervoice/asynch/hmc/homunculus.py
index ba4a1c272..882558c8c 100644
--- a/castervoice/asynch/hmc/homunculus.py
+++ b/castervoice/asynch/hmc/homunculus.py
@@ -1,11 +1,25 @@
+import os
import sys
+import threading
-from xmlrpc.server import SimpleXMLRPCServer
-from tkinter import Label, Text
-import tkinter as tk
+import dragonfly
-import signal, os
-from threading import Timer
+# TODO: Remove this try wrapper when CI server supports Qt
+try:
+ import PySide2.QtCore
+ from PySide2.QtWidgets import QApplication
+ from PySide2.QtWidgets import QCheckBox
+ from PySide2.QtWidgets import QDialog
+ from PySide2.QtWidgets import QFileDialog
+ from PySide2.QtWidgets import QFormLayout
+ from PySide2.QtWidgets import QLabel
+ from PySide2.QtWidgets import QLineEdit
+ from PySide2.QtWidgets import QScrollArea
+ from PySide2.QtWidgets import QTextEdit
+ from PySide2.QtWidgets import QVBoxLayout
+ from PySide2.QtWidgets import QWidget
+except ImportError:
+ sys.exit(0)
try: # Style C -- may be imported into Caster, or externally
BASE_PATH = os.path.realpath(__file__).rsplit(os.path.sep + "castervoice", 1)[0]
@@ -13,80 +27,235 @@
sys.path.append(BASE_PATH)
finally:
from castervoice.lib import settings
- from castervoice.lib.merge.communication import Communicator
+RPC_DIR_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1))
-class Homunculus(tk.Tk):
- def __init__(self, htype, data=None):
- tk.Tk.__init__(self, baseName="")
- self.setup_xmlrpc_server()
- self.htype = htype
- self.completed = False
- self.max_after_completed = 10
- self.title(settings.HOMUNCULUS_VERSION)
- self.geometry("300x200+" + str(int(self.winfo_screenwidth()/2 - 150)) + "+" +
- str(int(self.winfo_screenheight()/2 - 100)))
- self.wm_attributes("-topmost", 1)
- self.protocol("WM_DELETE_WINDOW", self.xmlrpc_kill)
+class Homunculus(QDialog):
- #
+ def __init__(self, server, args):
+ QDialog.__init__(self, None)
+ self.htype = args[1]
+ self.completed = False
+ self.server = server
+ self.setup_xmlrpc_server()
+ self.mainLayout = QVBoxLayout()
+ found_word = None
+ if len(args) > 2:
+ found_word = args[2]
if self.htype == settings.QTYPE_DEFAULT:
- Label(
- self, text="Enter response then say 'complete'", name="pathlabel").pack()
- self.ext_box = Text(self, name="ext_box")
- self.ext_box.pack(side=tk.LEFT)
- self.data = [0, 0]
+ self.setup_base_window()
elif self.htype == settings.QTYPE_INSTRUCTIONS:
- self.data = data.split("|")
- Label(
- self,
- text=" ".join(self.data[0].split(settings.HMC_SEPARATOR)), # pylint: disable=no-member
- name="pathlabel").pack()
- self.ext_box = Text(self, name="ext_box")
- self.ext_box.pack(side=tk.LEFT)
-
- # start server, tk main loop
- def start_server():
- while not self.server_quit:
- self.server.handle_request()
-
- Timer(1, start_server).start()
- Timer(0.05, self.start_tk).start()
- # backup plan in case for whatever reason Dragon doesn't shut it down:
- Timer(300, self.xmlrpc_kill).start()
-
- def start_tk(self):
- self.mainloop()
+ self.setup_base_window(found_word)
+ elif self.htype == settings.QTYPE_CONFIRM:
+ self.setup_confirm_window(found_word)
+ elif self.htype == settings.QTYPE_DIRECTORY:
+ self.setup_directory_window()
+ elif self.htype == settings.QTYPE_RECORDING:
+ self.setup_recording_window(found_word)
+ self.setLayout(self.mainLayout)
+ self.expiration = threading.Timer(300, self.xmlrpc_kill)
+ self.expiration.start()
+
+ def setup_base_window(self, data=None):
+ x = dragonfly.monitors[0].rectangle.dx / 2 - 150
+ y = dragonfly.monitors[0].rectangle.dy / 2 - 100
+ self.setGeometry(x, y, 300, 200)
+ self.setWindowTitle(settings.HOMUNCULUS_VERSION)
+ self.data = data.split("|") if data else [0, 0]
+ label = QLabel(" ".join(self.data[0].split(settings.HMC_SEPARATOR))) if data else QLabel("Enter response then say 'complete'") # pylint: disable=no-member
+ label.setAlignment(PySide2.QtCore.Qt.AlignCenter)
+ self.ext_box = QTextEdit()
+ self.mainLayout.addWidget(label)
+ self.mainLayout.addWidget(self.ext_box)
+ self.setWindowTitle(settings.HOMUNCULUS_VERSION)
+
+ def setup_confirm_window(self, params):
+ x = dragonfly.monitors[0].rectangle.dx / 2 - 160
+ y = dragonfly.monitors[0].rectangle.dy / 2 - 25
+ self.setGeometry(x, y, 320, 50)
+ self.setWindowTitle(settings.HOMUNCULUS_VERSION + settings.HMC_TITLE_CONFIRM)
+ label1 = QLabel("Please confirm: " + " ".join(params.split(settings.HMC_SEPARATOR)))
+ label2 = QLabel("(say \"confirm\" or \"disconfirm\")")
+ self.mainLayout.addWidget(label1)
+ self.mainLayout.addWidget(label2)
+
+ def setup_directory_window(self):
+ x = dragonfly.monitors[0].rectangle.dx / 2 - 320
+ y = dragonfly.monitors[0].rectangle.dy / 2 - 25
+ self.setGeometry(x, y, 640, 50)
+ self.setWindowTitle(settings.HOMUNCULUS_VERSION + settings.HMC_TITLE_DIRECTORY)
+ label = QLabel("Enter directory or say 'browse'")
+ self.word_box = QLineEdit()
+ label.setBuddy(self.word_box)
+ self.mainLayout.addWidget(label)
+ self.mainLayout.addWidget(self.word_box)
+
+ def setup_recording_window(self, history):
+ self.grid_row = 0
+ x = dragonfly.monitors[0].rectangle.dx / 2 - 320
+ y = dragonfly.monitors[0].rectangle.dy / 2 - 240
+ self.setGeometry(x, y, 640, 480)
+ self.setWindowTitle(settings.HOMUNCULUS_VERSION + settings.HMC_TITLE_RECORDING)
+ label = QLabel("Macro Recording Options")
+ label.setAlignment(PySide2.QtCore.Qt.AlignCenter)
+ self.mainLayout.addWidget(label)
+ label = QLabel("Command Words:")
+ self.word_box = QLineEdit()
+ label.setBuddy(self.word_box)
+ self.mainLayout.addWidget(label)
+ self.mainLayout.addWidget(self.word_box)
+ self.repeatable = QCheckBox("Make Repeatable")
+ self.mainLayout.addWidget(self.repeatable)
+ label = QLabel("Dictation History")
+ label.setAlignment(PySide2.QtCore.Qt.AlignCenter)
+ self.mainLayout.addWidget(label)
+ self.word_state = []
+ cb_number = 1
+ sentences = history.split("[s]")
+ sentences.pop()
+ form = QFormLayout()
+ for sentence in sentences:
+ sentence_words = sentence.split("[w]")
+ sentence_words.pop()
+ display_sentence = " ".join(sentence_words)
+ cb = QCheckBox("(" + str(cb_number) + ")")
+ form.addRow(QLabel(display_sentence), cb)
+ self.word_state.append(cb)
+ cb_number += 1
+ self.word_state[0].setChecked(True)
+ self.cb_max = cb_number
+ area = QScrollArea(self)
+ area.setWidgetResizable(True)
+ group = QWidget(area)
+ group.setLayout(form)
+ area.setWidget(group)
+ self.mainLayout.addWidget(area)
+
+ def check_boxes(self, details):
+ for box_index in details:
+ if 0 < box_index and box_index < self.cb_max:
+ self.word_state[box_index - 1].setChecked(True)
+
+ def check_range_of_boxes(self, details):
+ box_index_from = details[0] - 1
+ box_index_to = details[1]
+ for i in range(max(0, box_index_from), min(box_index_to, self.cb_max - 1)):
+ self.word_state[i].setChecked(True)
+
+ def ask_directory(self):
+ result = QFileDialog.getExistingDirectory(self, "Please select directory", os.environ["HOME"], QFileDialog.ShowDirsOnly)
+ self.word_box.setText(result)
+
+ def event(self, event):
+ if event.type() == RPC_DIR_EVENT:
+ self.ask_directory()
+ return True
+ return QDialog.event(self, event)
+
+ def reject(self):
+ self.expiration.cancel()
+ QApplication.quit()
+
+ '''
+ XMLRPC methods
+ '''
def setup_xmlrpc_server(self):
- self.server_quit = 0
- comm = Communicator()
- self.server = SimpleXMLRPCServer(
- (Communicator.LOCALHOST, comm.com_registry["hmc"]),
- logRequests=False, allow_none=True)
- self.server.register_function(self.xmlrpc_do_action, "do_action")
- self.server.register_function(self.xmlrpc_complete, "complete")
- self.server.register_function(self.xmlrpc_get_message, "get_message")
self.server.register_function(self.xmlrpc_kill, "kill")
+ self.server.register_function(self.xmlrpc_complete, "complete")
+ if self.htype == settings.QTYPE_DEFAULT or self.htype == settings.QTYPE_INSTRUCTIONS:
+ self.server.register_function(self.xmlrpc_do_action, "do_action")
+ self.server.register_function(self.xmlrpc_get_message, "get_message")
+ elif self.htype == settings.QTYPE_CONFIRM:
+ self.server.register_function(self.xmlrpc_do_action_confirm, "do_action")
+ self.server.register_function(self.xmlrpc_get_message_confirm, "get_message")
+ elif self.htype == settings.QTYPE_DIRECTORY:
+ self.server.register_function(self.xmlrpc_do_action_directory, "do_action")
+ self.server.register_function(self.xmlrpc_get_message_directory, "get_message")
+ elif self.htype == settings.QTYPE_RECORDING:
+ self.server.register_function(self.xmlrpc_do_action_recording, "do_action")
+ self.server.register_function(self.xmlrpc_get_message_recording, "get_message")
+ server_thread = threading.Thread(target=self.server.serve_forever)
+ server_thread.daemon = True
+ server_thread.start()
def xmlrpc_kill(self):
- self.server_quit = 1
- self.destroy()
- os.kill(os.getpid(), signal.SIGTERM)
+ self.expiration.cancel()
+ QApplication.quit()
def xmlrpc_complete(self):
self.completed = True
- self.after(10, self.withdraw)
- Timer(self.max_after_completed, self.xmlrpc_kill).start()
+ threading.Timer(10, self.xmlrpc_kill).start()
+
+ def xmlrpc_do_action(self, action, details=None):
+ pass
def xmlrpc_get_message(self):
- '''override this for every new child class'''
+ response = None
if self.completed:
- Timer(1, self.xmlrpc_kill).start()
- return [self.ext_box.get("1.0", tk.END), self.data]
- else:
- return None
+ response = [self.ext_box.toPlainText(), self.data]
+ threading.Timer(1, self.xmlrpc_kill).start()
+ return response
- def xmlrpc_do_action(self, action, details=None):
- '''override'''
+ def xmlrpc_do_action_confirm(self, action, details=None):
+ if isinstance(action, bool):
+ self.completed = True
+ '''1 is True, 2 is False'''
+ self.value = 1 if action else 2
+
+ def xmlrpc_get_message_confirm(self):
+ response = None
+ if self.completed:
+ response = {"mode": "confirm"}
+ response["confirm"] = self.value
+ threading.Timer(1, self.xmlrpc_kill).start()
+ return response
+
+ def xmlrpc_do_action_directory(self, action, details=None):
+ if action == "dir":
+ PySide2.QtCore.QCoreApplication.postEvent(self, PySide2.QtCore.QEvent(RPC_DIR_EVENT))
+
+ def xmlrpc_get_message_directory(self):
+ response = None
+ if self.completed:
+ response = {"mode": "ask_dir"}
+ response["path"] = self.word_box.text()
+ threading.Timer(1, self.xmlrpc_kill).start()
+ return response
+
+ def xmlrpc_do_action_recording(self, action, details=None):
+ '''acceptable keys are numbers and w and p'''
+ if action == "check":
+ self.check_boxes(details)
+ elif action == "focus":
+ if details == "word":
+ self.word_box.setFocus()
+ elif action == "check_range":
+ self.check_range_of_boxes(details)
+ elif action == "exclude":
+ box_index = details
+ if 0 < box_index and box_index < self.cb_max:
+ self.word_state[box_index - 1].setChecked(False)
+ elif action == "repeatable":
+ self.repeatable.setChecked(not self.repeatable.isChecked())
+
+ def xmlrpc_get_message_recording(self):
+ response = None
+ if self.completed:
+ word = self.word_box.text()
+ if len(word) > 0:
+ response = {"mode": "recording"}
+ response["word"] = word
+ response["repeatable"] = self.repeatable.isChecked()
+ selected_indices = []
+ index = 0
+ for ws in self.word_state:
+ if ws.isChecked():
+ selected_indices.append(index)
+ index += 1
+ response["selected_indices"] = selected_indices
+ if len(selected_indices) == 0:
+ response = None
+ threading.Timer(1, self.xmlrpc_kill).start()
+ return response
diff --git a/castervoice/asynch/hud.py b/castervoice/asynch/hud.py
new file mode 100644
index 000000000..e82d0236f
--- /dev/null
+++ b/castervoice/asynch/hud.py
@@ -0,0 +1,230 @@
+#! 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
+ from castervoice.lib import settings
+
+CLEAR_HUD_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1))
+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(settings.HUD_TITLE)
+ 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 == 50:
+ self.commands_count = 0
+ return True
+ if escaped_text.startswith('@'):
+ formatted_text = '>{}'.format(escaped_text[1:])
+ elif escaped_text.startswith(''):
+ formatted_text = '>{}'.format(escaped_text)
+ else:
+ formatted_text = escaped_text
+ self.output.append(formatted_text)
+ self.output.ensureCursorVisible()
+ return True
+ if event.type() == CLEAR_HUD_EVENT:
+ self.commands_count = 0
+ return True
+ return QMainWindow.event(self, event)
+
+ def closeEvent(self, event):
+ event.accept()
+
+ def setup_xmlrpc_server(self):
+ self.server.register_function(self.xmlrpc_clear, "clear_hud")
+ 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_clear(self):
+ PySide2.QtCore.QCoreApplication.postEvent(self, PySide2.QtCore.QEvent(CLEAR_HUD_EVENT))
+ return 0
+
+ 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/asynch/hud_support.py b/castervoice/asynch/hud_support.py
new file mode 100644
index 000000000..167b334e3
--- /dev/null
+++ b/castervoice/asynch/hud_support.py
@@ -0,0 +1,130 @@
+import sys, subprocess, json
+
+from dragonfly import CompoundRule, MappingRule, get_current_engine
+
+from pathlib import Path
+
+try: # Style C -- may be imported into Caster, or externally
+ BASE_PATH = str(Path(__file__).resolve().parent.parent)
+ if BASE_PATH not in sys.path:
+ sys.path.append(BASE_PATH)
+finally:
+ from castervoice.lib import settings
+
+from castervoice.lib import printer
+from castervoice.lib import control
+from castervoice.lib.rules_collection import get_instance
+
+def start_hud():
+ hud = control.nexus().comm.get_com("hud")
+ try:
+ hud.ping()
+ except Exception:
+ subprocess.Popen([settings.SETTINGS["paths"]["PYTHONW"],
+ settings.SETTINGS["paths"]["HUD_PATH"]])
+
+
+def show_hud():
+ hud = control.nexus().comm.get_com("hud")
+ try:
+ hud.show_hud()
+ except Exception as e:
+ printer.out("Unable to show hud. Hud not available. \n{}".format(e))
+
+
+def hide_hud():
+ hud = control.nexus().comm.get_com("hud")
+ try:
+ hud.hide_hud()
+ except Exception as e:
+ printer.out("Unable to hide hud. Hud not available. \n{}".format(e))
+
+
+def clear_hud():
+ hud = control.nexus().comm.get_com("hud")
+ try:
+ hud.clear_hud()
+ except Exception as e:
+ printer.out("Unable to clear hud. Hud not available. \n{}".format(e))
+
+
+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")
+ try:
+ hud.show_rules(json.dumps(grammars))
+ except Exception as e:
+ printer.out("Unable to show hud. Hud not available. \n{}".format(e))
+
+def hide_rules():
+ """
+ Instruct HUD to hide the frame with the list of rules.
+ """
+ hud = control.nexus().comm.get_com("hud")
+ try:
+ hud.hide_rules()
+ except Exception as e:
+ printer.out("Unable to show hud. Hud not available. \n{}".format(e))
+
+
+class HudPrintMessageHandler(printer.BaseMessageHandler):
+ """
+ Hud message handler which prints formatted messages to the gui Hud.
+ Add symbols as the 1st character in strings utilizing printer.out
+
+ @ Purple arrow - Bold Text - Important Info
+ # Red arrow - Plain text - Caster Info
+ $ Blue arrow - Plain text - Commands/Dictation
+ """
+
+ def __init__(self):
+ super(HudPrintMessageHandler, self).__init__()
+ self.hud = control.nexus().comm.get_com("hud")
+ self.exception = False
+ try:
+ if get_current_engine().name != "text":
+ self.hud.ping() # HUD running?
+ except Exception as e:
+ self.exception = True
+ printer.out("Hud not available. \n{}".format(e))
+
+ def handle_message(self, items):
+ if self.exception is False:
+ # The timeout with the hud can interfere with the dragonfly speech recognition loop.
+ # This appears as a stutter in recognition.
+ # Exceptions are tracked so this stutter only happens to end user once.
+ # Make exception if the hud is not available/python 2/text engine
+ # TODO: handle raising exception gracefully
+ try:
+ self.hud.send("\n".join([str(m) for m in items]))
+ except Exception as e:
+ # If an exception, print is managed by SimplePrintMessageHandler
+ self.exception = True
+ printer.out("Hud not available. \n{}".format(e))
+ raise("") # pylint: disable=raising-bad-type
+ else:
+ raise("") # pylint: disable=raising-bad-type
\ No newline at end of file
diff --git a/castervoice/lib/ctrl/configure_engine.py b/castervoice/lib/ctrl/configure_engine.py
index 4a3598212..98bd442d0 100644
--- a/castervoice/lib/ctrl/configure_engine.py
+++ b/castervoice/lib/ctrl/configure_engine.py
@@ -1,17 +1,37 @@
import time
-from dragonfly import get_engine, get_current_engine, register_recognition_callback
+from dragonfly import get_current_engine, register_recognition_callback, RecognitionObserver
+from castervoice.lib.ctrl.mgr.engine_manager import EngineModesManager
from castervoice.lib import settings
+from castervoice.lib import printer
+
+
+class Observer(RecognitionObserver):
+ def __init__(self):
+ from castervoice.lib import control
+ self.mic_mode = None
+ self._engine_modes_manager = control.nexus().engine_modes_manager
+
+ def on_begin(self):
+ self.mic_mode = self._engine_modes_manager.get_mic_mode()
+
+ def on_recognition(self, words):
+ if not self.mic_mode == "sleeping":
+ printer.out("$ {}".format(" ".join(words)))
+
+ def on_failure(self):
+ if not self.mic_mode == "sleeping":
+ printer.out("?!")
+
class EngineConfigEarly:
"""
- Initializes engine specific customizations before Nexus initializes.
+ Initializes engine customizations before Nexus initializes.
Grammars are not loaded
"""
# get_engine used as a workaround for running Natlink inprocess
- engine = get_engine().name
-
def __init__(self):
+ self.engine = get_current_engine().name
self._set_cancel_word()
def _set_cancel_word(self):
@@ -35,6 +55,8 @@ def __init__(self):
self.engine = get_current_engine().name
self.sync_timer = None
self.sleep_timer = None
+ Observer().register()
+
if self.engine != 'natlink':
# Other engines besides natlink needs a default mic state for sleep_timer
diff --git a/castervoice/lib/ctrl/dependencies.py b/castervoice/lib/ctrl/dependencies.py
index 57bcf3b92..37fc06f34 100644
--- a/castervoice/lib/ctrl/dependencies.py
+++ b/castervoice/lib/ctrl/dependencies.py
@@ -5,6 +5,7 @@
'''
import os, sys, time, pkg_resources
from pkg_resources import VersionConflict, DistributionNotFound
+from castervoice.lib import printer
DARWIN = sys.platform == "darwin"
LINUX = sys.platform == "linux"
@@ -48,7 +49,7 @@ def dep_missing():
missing_list.append('{0}'.format(dep))
if missing_list:
pippackages = (' '.join(map(str, missing_list)))
- print("\nCaster: dependencys are missing. Use 'python -m pip install {0}'".format(pippackages))
+ printer.out("\nCaster: dependencys are missing. Use 'python -m pip install {0}'".format(pippackages))
time.sleep(10)
@@ -69,10 +70,10 @@ def dep_min_version():
except VersionConflict as e:
if operator == ">=":
if issue_url is not None:
- print("\nCaster: Requires {0} v{1} or greater.\nIssue reference: {2}".format(package, version, issue_url))
- print("Update with: 'python -m pip install {} --upgrade' \n".format(package))
+ printer.out("\nCaster: Requires {0} v{1} or greater.\nIssue reference: {2}".format(package, version, issue_url))
+ printer.out("Update with: 'python -m pip install {} --upgrade' \n".format(package))
if operator == "==":
- print("\nCaster: Requires an exact version of {0}.\nIssue reference: {1}".format(package, issue_url))
+ printer.out("\nCaster: Requires an exact version of {0}.\nIssue reference: {1}".format(package, issue_url))
print("Install with: 'python -m pip install {}' \n".format(e.req))
diff --git a/castervoice/lib/ctrl/mgr/engine_manager.py b/castervoice/lib/ctrl/mgr/engine_manager.py
index 75ebd3600..c95c85631 100644
--- a/castervoice/lib/ctrl/mgr/engine_manager.py
+++ b/castervoice/lib/ctrl/mgr/engine_manager.py
@@ -1,7 +1,7 @@
-from dragonfly import get_current_engine
+from dragonfly import get_engine, get_current_engine
from castervoice.lib import printer
-if get_current_engine().name == 'natlink':
+if get_engine().name == 'natlink':
import natlink
diff --git a/castervoice/lib/ctrl/mgr/grammar_manager.py b/castervoice/lib/ctrl/mgr/grammar_manager.py
index cc38e3d9d..9bc3a3b76 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
@@ -12,6 +13,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
@@ -240,7 +242,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.
@@ -302,7 +304,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 022e2f15c..b6a4e3832 100644
--- a/castervoice/lib/ctrl/mgr/rule_details.py
+++ b/castervoice/lib/ctrl/mgr/rule_details.py
@@ -37,6 +37,9 @@ 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:
@@ -46,11 +49,11 @@ def _calculate_filepath_from_frame(stack, index):
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 39664ec13..74f8cf2b3 100644
--- a/castervoice/lib/merge/communication.py
+++ b/castervoice/lib/merge/communication.py
@@ -6,9 +6,10 @@ class Communicator:
def __init__(self):
self.coms = {}
self.com_registry = {
- "hmc": 1338,
- "grids": 1339,
- "sikuli": 8000
+ "hmc": 8337,
+ "hud": 8338,
+ "grids": 8339,
+ "sikuli": 8340
}
def get_com(self, name):
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/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 9c36acec8..695026b0d 100644
--- a/castervoice/lib/settings.py
+++ b/castervoice/lib/settings.py
@@ -22,6 +22,7 @@
HMC_TITLE_RECORDING = " :: Recording Manager"
HMC_TITLE_DIRECTORY = " :: Directory Selector"
HMC_TITLE_CONFIRM = " :: Confirm"
+HUD_TITLE = "Caster HUD v " + SOFTWARE_VERSION_NUMBER
LEGION_TITLE = "legiongrid"
RAINBOW_TITLE = "rainbowgrid"
DOUGLAS_TITLE = "douglasgrid"
@@ -281,6 +282,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":
@@ -356,7 +359,7 @@ def _get_defaults():
# Default enabled hooks: Use hook class name
"hooks": {
- "default_hooks": ['PrinterHook'],
+ "default_hooks": ['PrinterHook', 'RulesLoadedHook'],
},
# miscellaneous section
@@ -480,8 +483,4 @@ def initialize():
if _debugger_path not in sys.path and os.path.isdir(_debugger_path):
sys.path.append(_debugger_path)
- # set up printer -- it doesn't matter where you do this; messages will start printing to the console after this
- dh = printer.get_delegating_handler()
- dh.register_handler(printer.SimplePrintMessageHandler())
- # begin using printer
printer.out("Caster User Directory: {}".format(_USER_DIR))
diff --git a/castervoice/lib/utilities.py b/castervoice/lib/utilities.py
index 2c73cc665..fe96b326c 100644
--- a/castervoice/lib/utilities.py
+++ b/castervoice/lib/utilities.py
@@ -1,9 +1,11 @@
import io
import json
import os
+import json
import re
import subprocess
import sys
+import six
import time
import traceback
import webbrowser
@@ -30,19 +32,16 @@
# 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():
@@ -159,6 +158,7 @@ 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 = []
@@ -186,7 +186,7 @@ def reboot():
printer.out(popen_parameters)
subprocess.Popen(popen_parameters)
else:
- # Natlink out-of-process
+ # Natlink out-of-process
engine.disconnect()
subprocess.Popen([sys.executable, '-m', 'dragonfly', 'load', '--engine', 'natlink', '_*.py', '--no-recobs-messages'])
@@ -228,9 +228,9 @@ def clear_log():
# TODO: window_exists utilized when engine launched through Dragonfly CLI via bat in future
try:
if WIN32:
- clearcmd = "cls" # Windows OS
+ clearcmd = "cls" # Windows OS
else:
- clearcmd = "clear" # Linux
+ clearcmd = "clear" # Linux
if get_current_engine().name == 'natlink':
from natlinkcore import natlinkstatus # pylint: disable=import-error
status = natlinkstatus.NatlinkStatus()
diff --git a/castervoice/rules/ccr/recording_rules/history.py b/castervoice/rules/ccr/recording_rules/history.py
index 6dfc24540..26e0c94f4 100644
--- a/castervoice/rules/ccr/recording_rules/history.py
+++ b/castervoice/rules/ccr/recording_rules/history.py
@@ -1,4 +1,3 @@
-from dragonfly import RecognitionHistory
from dragonfly.actions.action_base import Repeat
from dragonfly.actions.action_function import Function
from dragonfly.actions.action_playback import Playback
diff --git a/castervoice/rules/core/utility_rules/caster_rule.py b/castervoice/rules/core/utility_rules/caster_rule.py
index ef8c53b52..b001fc4fd 100644
--- a/castervoice/rules/core/utility_rules/caster_rule.py
+++ b/castervoice/rules/core/utility_rules/caster_rule.py
@@ -1,10 +1,15 @@
-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.asynch.hud_support import show_hud
+from castervoice.asynch.hud_support import hide_hud
+from castervoice.asynch.hud_support import show_rules
+from castervoice.asynch.hud_support import hide_rules
+from castervoice.asynch.hud_support import clear_hud
_PIP = find_pip()
@@ -31,7 +36,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 +44,16 @@ 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"),
+ "clear hud":
+ R(Function(clear_hud), rdescript="Clear output the HUD window"),
}