diff --git a/.github/Install-VCForPython27.ps1 b/.github/Install-VCForPython27.ps1 deleted file mode 100644 index 761141526..000000000 --- a/.github/Install-VCForPython27.ps1 +++ /dev/null @@ -1,55 +0,0 @@ -# Install Visual Studio for Python 2.7 - -Function Update-ScriptPath { - $env:PATH = [Environment]::GetEnvironmentVariable('PATH', [EnvironmentVariableTarget]::Machine); -} - -Function Install-FromMsi { - Param( - [Parameter(Mandatory)] - [string] $name, - [Parameter(Mandatory)] - [string] $url, - [Parameter()] - [switch] $noVerify = $false, - [Parameter()] - [string[]] $options = @() - ) - - $installerPath = Join-Path ([System.IO.Path]::GetTempPath()) ('{0}.msi' -f $name); - - Write-Host ('Downloading {0} installer from {1} ..' -f $name, $url); - (New-Object System.Net.WebClient).DownloadFile($url, $installerPath); - Write-Host ('Downloaded {0} bytes' -f (Get-Item $installerPath).length); - - $args = @('/i', $installerPath, '/quiet', '/qn'); - $args += $options; - - Write-Host ('Installing {0} ...' -f $name); - Write-Host ('msiexec {0}' -f ($args -Join ' ')); - - Start-Process msiexec -Wait -ArgumentList $args; - - # Update path - Update-ScriptPath; - - if (!$noVerify) { - Write-Host ('Verifying {0} install ...' -f $name); - $verifyCommand = (' {0} --version' -f $name); - Write-Host $verifyCommand; - Invoke-Expression $verifyCommand; - } - - Write-Host ('Removing {0} installer ...' -f $name); - Remove-Item $installerPath -Force; - - Write-Host ('{0} install complete.' -f $name); -} -# VCForPython27.msi on Vultr -$vcForPythonUrl = 'http://45.77.79.136/VCForPython27.msi'; - -# Alternatively from web.archive.org -# https://web.archive.org/web/20200709160228if_/https://download.microsoft.com/download/7/9/6/796EF2E4-801B-4FC4-AB28-B59FBF6D907B/VCForPython27.msi -# Source: https://web.archive.org/web/20190720195601/http://www.microsoft.com/en-us/download/confirmation.aspx?id=44266 - -Install-FromMsi -Name 'VCForPython27' -Url $vcForPythonUrl -NoVerify; diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index aaa0a4ff0..ca976ef74 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -3,36 +3,6 @@ name: Caster Lint/Unit Tests on: [push, pull_request] jobs: - python-windows-2-7: - name: python 2 windows - runs-on: windows-latest - strategy: - matrix: - python-version: [2.7.x] - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - architecture: 'x86' - - name: Install C++ Compiler for Python 2.7 - run: .\.github\Install-VCForPython27.ps1 - shell: powershell - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install --upgrade setuptools - pip install wheel - pip install -U -r requirements-dev.txt - - name: Lint with pylint - run: | - pylint -E _caster.py - pylint -E castervoice - - name: Unit Tests Via Testrunner - run: | - python tests/testrunner.py - python-windows-3-8-x: name: python 3 windows runs-on: windows-latest diff --git a/.pylintrc b/.pylintrc index cb1920f99..bcda3d499 100644 --- a/.pylintrc +++ b/.pylintrc @@ -3,7 +3,7 @@ generated-members=pythoncom.*,wx.* ignored-modules=natlink,natlinkstatus [MASTER] -extension-pkg-whitelist=pythoncom,wx,pydevd,win32gui +extension-pkg-whitelist=pythoncom,wx,pydevd,win32gui,PySide2 [MESSAGES CONTROL] disable=all diff --git a/Install_Caster_DNS-WSR.bat b/Install_Caster_DNS-WSR.bat index 026874f39..9c1d1e54d 100644 --- a/Install_Caster_DNS-WSR.bat +++ b/Install_Caster_DNS-WSR.bat @@ -1,10 +1,28 @@ + @echo off + +SetLocal EnableDelayedExpansion +set python_version=3.8-32 set currentpath=%~dp0 echo Installation path: %currentpath% + +@REM execute python launcher for python directory +FOR /F "tokens=1 USEBACKQ delims=" %%i IN (`py -%python_version% -c "import sys; print(sys.exec_prefix)"`) DO ( set python_path=%%i ) + +@REM whack a funny trailing character (newline?) from end +set python_path=!python_path:~0,-1! + +set PATH=%python_path%;%python_path%/Scripts;%PATH% +echo %PATH% + +echo Next line should clearly state python version: +python --version + echo Using this python/pip: -python -m pip -V + +py -%python_version% -m pip install --upgrade pip echo Installing Caster Dependencies for DNS/WSR -python -m pip install -r "%currentpath%requirements.txt" +py -%python_version% -m pip install -r "%currentpath%requirements.txt" -pause 1 \ No newline at end of file +pause 1 diff --git a/_caster.py b/_caster.py index be96ff890..2c0ffb18c 100644 --- a/_caster.py +++ b/_caster.py @@ -1,33 +1,27 @@ -#! python2.7 ''' main Caster module Created on Jun 29, 2014 ''' -import six - -if six.PY2: - import logging - logging.basicConfig() - +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 @@ -38,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..cd1ede104 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 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,21 @@ def _get_title(hmc_type): return default +def main(): + import PySide2.QtWidgets + from castervoice.asynch.hmc.homunculus import Homunculus + from castervoice.lib.merge.communication import Communicator + 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 33a4715f8..8fad0cce5 100644 --- a/castervoice/asynch/hmc/hmc_ask_directory.py +++ b/castervoice/asynch/hmc/hmc_ask_directory.py @@ -1,25 +1,21 @@ -import six -if six.PY2: - from Tkinter import Label, Entry, StringVar # pylint: disable=import-error - import tkFileDialog # pylint: disable=import-error -else: - from tkinter import Label, Entry, StringVar, filedialog as tkFileDialog import os import sys from threading import Timer +from tkinter import Entry, Label, StringVar +from tkinter import filedialog as tkFileDialog 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 import settings from castervoice.asynch.hmc.homunculus import Homunculus + from castervoice.lib import settings 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 0531a5fe5..18e38ea32 100644 --- a/castervoice/asynch/hmc/hmc_confirm.py +++ b/castervoice/asynch/hmc/hmc_confirm.py @@ -1,8 +1,4 @@ -import six -if six.PY2: - from Tkinter import Label # pylint: disable=import-error -else: - from tkinter import Label +from tkinter import Label import os import sys from threading import Timer @@ -18,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 9ba5d198d..5d6dd45ad 100644 --- a/castervoice/asynch/hmc/hmc_recording.py +++ b/castervoice/asynch/hmc/hmc_recording.py @@ -1,13 +1,8 @@ import sys, os from threading import Timer -import six -if six.PY2: - from Tkinter import Label, Entry, Checkbutton # pylint: disable=import-error - import Tkinter as tk # pylint: disable=import-error -else: - from tkinter import Label, Entry, Checkbutton - import tkinter as tk +from tkinter import Label, Entry, Checkbutton +import tkinter as tk try: # Style C -- may be imported into Caster, or externally BASE_PATH = os.path.realpath(__file__).rsplit(os.path.sep + "castervoice", 1)[0] @@ -26,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 a8a5fb82d..882558c8c 100644 --- a/castervoice/asynch/hmc/homunculus.py +++ b/castervoice/asynch/hmc/homunculus.py @@ -1,15 +1,25 @@ +import os import sys -import six -if six.PY2: - from SimpleXMLRPCServer import SimpleXMLRPCServer # pylint: disable=import-error - import Tkinter as tk # pylint: disable=import-error - from Tkinter import Label, Text # pylint: disable=import-error -else: - from xmlrpc.server import SimpleXMLRPCServer # pylint: disable=no-name-in-module - from tkinter import Label, Text - import tkinter as tk -import signal, os -from threading import Timer +import threading + +import dragonfly + +# 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] @@ -17,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/hmc_rules/hmc_launch_rule.py b/castervoice/asynch/hmc_rules/hmc_launch_rule.py index af1a62231..8f29053fe 100644 --- a/castervoice/asynch/hmc_rules/hmc_launch_rule.py +++ b/castervoice/asynch/hmc_rules/hmc_launch_rule.py @@ -9,7 +9,7 @@ def receive_settings(data): settings.SETTINGS = data - settings.save_config() + settings.save_config(paths = True) # TODO: apply new settings 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/asynch/mouse/grids.py b/castervoice/asynch/mouse/grids.py index 6c13b8e6a..35bd2ee8e 100644 --- a/castervoice/asynch/mouse/grids.py +++ b/castervoice/asynch/mouse/grids.py @@ -1,19 +1,13 @@ -from __future__ import division - import getopt import os import signal -import six import sys import threading as th -import time from dragonfly import monitors -if six.PY2: - from SimpleXMLRPCServer import SimpleXMLRPCServer # pylint: disable=import-error - import Tkinter as tk # pylint: disable=import-error -else: - from xmlrpc.server import SimpleXMLRPCServer # pylint: disable=no-name-in-module - import tkinter as tk + +from xmlrpc.server import SimpleXMLRPCServer +import tkinter as tk + 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: diff --git a/castervoice/asynch/mouse/legion.py b/castervoice/asynch/mouse/legion.py index f7b368e4e..fb7355388 100644 --- a/castervoice/asynch/mouse/legion.py +++ b/castervoice/asynch/mouse/legion.py @@ -18,11 +18,7 @@ from castervoice.lib import settings, utilities settings.initialize() -import six -if six.PY2: - from castervoice.lib.util.pathlib import Path -else: - from pathlib import Path # pylint: disable=import-error +from pathlib import Path try: from PIL import ImageGrab, ImageFilter, Image @@ -191,7 +187,7 @@ def tirg_scan(self, img): bbstring = self.tirg_dll.getTextBBoxesFromBytes(img.tobytes(), img.size[0], img.size[1]) # clean the results in case any garbage letters come through - result = re.sub("[^0-9,]", "", bbstring.decode(locale.getpreferredencoding())) + result = re.sub("[^0-9,]", "", str(bbstring)) return result def scan(self, bbox=None, rough=True): diff --git a/castervoice/asynch/settingswindow_qt.py b/castervoice/asynch/settingswindow_qt.py index b7003d711..8106e0589 100644 --- a/castervoice/asynch/settingswindow_qt.py +++ b/castervoice/asynch/settingswindow_qt.py @@ -3,7 +3,7 @@ import sys import threading -from xmlrpc.server import SimpleXMLRPCServer # pylint: disable=no-name-in-module +from xmlrpc.server import SimpleXMLRPCServer try: # Style C -- may be imported into Caster, or externally BASE_PATH = os.path.realpath(__file__).rsplit(os.path.sep + "castervoice", 1)[0] @@ -14,24 +14,21 @@ from castervoice.lib import settings from castervoice.lib.merge.communication import Communicator -# TODO: Remove this try wrapper when CI server supports Qt -try: - import PySide2.QtCore - from PySide2.QtGui import QPalette - from PySide2.QtWidgets import QApplication - from PySide2.QtWidgets import QDialogButtonBox - from PySide2.QtWidgets import QCheckBox - from PySide2.QtWidgets import QDialog - from PySide2.QtWidgets import QFormLayout - from PySide2.QtWidgets import QGroupBox - from PySide2.QtWidgets import QLabel - from PySide2.QtWidgets import QLineEdit - from PySide2.QtWidgets import QScrollArea - from PySide2.QtWidgets import QTabWidget - from PySide2.QtWidgets import QVBoxLayout - from PySide2.QtWidgets import QWidget -except ImportError: - sys.exit(0) +from PySide2 import QtCore +from PySide2.QtGui import QPalette +from PySide2.QtWidgets import QApplication +from PySide2.QtWidgets import QDialogButtonBox +from PySide2.QtWidgets import QCheckBox +from PySide2.QtWidgets import QDialog +from PySide2.QtWidgets import QFormLayout +from PySide2.QtWidgets import QGroupBox +from PySide2.QtWidgets import QLabel +from PySide2.QtWidgets import QLineEdit +from PySide2.QtWidgets import QScrollArea +from PySide2.QtWidgets import QTabWidget +from PySide2.QtWidgets import QVBoxLayout +from PySide2.QtWidgets import QWidget + settings.initialize() @@ -42,10 +39,10 @@ NUMBER_SETTING = 16 BOOLEAN_SETTING = 32 -CONTROL_KEY = PySide2.QtCore.Qt.Key_Meta if sys.platform == "darwin" else PySide2.QtCore.Qt.Key_Control -SHIFT_TAB_KEY = int(PySide2.QtCore.Qt.Key_Tab) + 1 +CONTROL_KEY = QtCore.Qt.Key_Meta if sys.platform == "darwin" else QtCore.Qt.Key_Control +SHIFT_TAB_KEY = int(QtCore.Qt.Key_Tab) + 1 -RPC_COMPLETE_EVENT = PySide2.QtCore.QEvent.Type(PySide2.QtCore.QEvent.registerEventType(-1)) +RPC_COMPLETE_EVENT = QtCore.QEvent.Type(QtCore.QEvent.registerEventType(-1)) class Field: @@ -74,8 +71,8 @@ def __init__(self, server): buttons = QDialogButtonBox( QDialogButtonBox.StandardButtons((int(QDialogButtonBox.StandardButton.Ok) | int(QDialogButtonBox.StandardButton.Cancel)))) - buttons.accepted.connect(self.accept) - buttons.rejected.connect(self.reject) + buttons.accepted.connect(self.accept) # pylint: disable=no-member + buttons.rejected.connect(self.reject) # pylint: disable=no-member mainLayout = QVBoxLayout() mainLayout.addWidget(self.tabs) mainLayout.addWidget(buttons) @@ -86,11 +83,11 @@ def __init__(self, server): self.expiration.start() def event(self, event): - if event.type() == PySide2.QtCore.QEvent.KeyRelease: + if event.type() == QtCore.QEvent.KeyRelease: if self.modifier == 1: curr = self.tabs.currentIndex() tabs_count = self.tabs.count() - if event.key() == PySide2.QtCore.Qt.Key_Tab: + if event.key() == QtCore.Qt.Key_Tab: next = curr + 1 next = 0 if next == tabs_count else next self.tabs.setCurrentIndex(next) @@ -219,7 +216,7 @@ def xmlrpc_get_message(self): return None def xmlrpc_complete(self): - PySide2.QtCore.QCoreApplication.postEvent(self, PySide2.QtCore.QEvent(RPC_COMPLETE_EVENT)) + QtCore.QCoreApplication.postEvent(self, QtCore.QEvent(RPC_COMPLETE_EVENT)) def accept(self): self.xmlrpc_complete() diff --git a/castervoice/asynch/settingswindow_wx.py b/castervoice/asynch/settingswindow_wx.py index 17c532853..4cb9d2607 100644 --- a/castervoice/asynch/settingswindow_wx.py +++ b/castervoice/asynch/settingswindow_wx.py @@ -1,14 +1,9 @@ -from __future__ import print_function import numbers import os -import six import sys import threading -if six.PY2: - from SimpleXMLRPCServer import SimpleXMLRPCServer # pylint: disable=import-error -else: - from xmlrpc.server import SimpleXMLRPCServer # pylint: disable=no-name-in-module +from xmlrpc.server import SimpleXMLRPCServer try: # Style C -- may be imported into Caster, or externally BASE_PATH = os.path.realpath(__file__).rsplit(os.path.sep + "castervoice", 1)[0] @@ -188,11 +183,11 @@ def get_fields(self, page, vbox, field): def field_from_value(self, window, value, field): item = None - if isinstance(value, six.string_types): + if isinstance(value, str): item = TextCtrl(window, value=value) field.text_type = STRING_SETTING elif isinstance(value, list): - if isinstance(value[0], six.string_types): + if isinstance(value[0], str): item = TextCtrl(window, value=", ".join(value)) field.text_type = STRING_LIST_SETTING elif isinstance(value[0], numbers.Real): diff --git a/castervoice/bin/share/API command grammars example/browser/browser_shared.py b/castervoice/bin/share/API command grammars example/browser/browser_shared.py deleted file mode 100644 index e0c390f66..000000000 --- a/castervoice/bin/share/API command grammars example/browser/browser_shared.py +++ /dev/null @@ -1,74 +0,0 @@ -# pylint: skip-file -# -# __author__ = "lexxish" -# -from dragonfly import Choice, ShortIntegerRef - -from castervoice.rules.apps.shared.directions import FORWARD, RIGHT, BACK, LEFT - -OPEN_NEW_WINDOW = "(new window|win new)" -OPEN_NEW_INCOGNITO_WINDOW = "(new incognito window | incognito)" -CLOSE_WINDOW = "win close|close all tabs" -NEW_TAB_N_TIMES = "new tab []|tab new []" -REOPEN_TAB_N_TIMES = "reopen tab []|tab reopen []" -CLOSE_TAB_N_TIMES = "close tab []|tab close []" -NEXT_TAB_N_TIMES = "%s tab []|tab %s []" % (FORWARD, RIGHT) -PREVIOUS_TAB_N_TIMES = "%s tab []|tab %s []" % (BACK, LEFT) -OPEN_NEW_TAB_BASED_ON_CURSOR = "new tab that" -SWITCH_TO_TAB_N = "tab | tab" -SWITCH_TO_LAST_TAB = "last tab" -SWITCH_TO_SECOND_TO_LAST_TAB = "second last tab" -GO_FORWARD_N_TIMES = "go %s []" % FORWARD -GO_BACK_N_TIMES = "go %s []" % BACK -ZOOM_IN_N_TIMES = "zoom in []" -ZOOM_OUT_N_TIMES = "zoom out []" -ZOOM_RESET_DEFAULT = "zoom reset" -FORCE_HARD_REFRESH = "(hard refresh|super refresh)" -FIND_NEXT_MATCH = "find %s [match] []" % FORWARD -FIND_PREVIOUS_MATCH = "find %s [match] []" % BACK -TOGGLE_CARET_BROWSING = "[toggle] caret browsing" -GO_TO_HOMEPAGE = "[go] home [page]" -SHOW_HISTORY = "[show] history" -SELECT_ADDRESS_BAR = "address bar" -SHOW_DOWNLOADS = "[show] downloads" -ADD_BOOKMARK = "[add] bookmark" -BOOKMARK_ALL_TABS = "bookmark all [tabs]" -TOGGLE_BOOKMARK_TOOLBAR = "[toggle] bookmark bar" -SHOW_BOOKMARKS = "[show] bookmarks" -TOGGLE_FULL_SCREEN = "[toggle] full screen" -SHOW_PAGE_SOURCE = "(show|view) page source" -DEBUG_RESUME = "resume" -DEBUG_STEP_OVER = "step over" -DEBUG_STEP_INTO = "step into" -DEBUG_STEP_OUT = "step out" -DUPLICATE_TAB = "(duplicate tab|tab duple)" -DUPLICATE_WINDOW = "(duplicate window|win duple)" -SHOW_EXTENSIONS = "[show] (extensions|plugins)" -SHOW_MENU = "[show] (menu | three dots)" -SHOW_SETTINGS = "[show] settings" -SHOW_TASK_MANAGER = "[show chrome] task manager" -CLEAR_BROWSING_DATA = "(clear history|clear browsing data)" -SHOW_DEVELOPER_TOOLS = "[show] developer tools" -CHECKOUT_PR = "checkout [this] pull request [locally]" -UPDATE_PR = "update [this] pull request [locally]" - - -def get_defaults(): - return {"n": 1, "m":"", "nth": ""} - - -def get_extras(): - return [ - Choice("nth", { - "first": "1", - "second": "2", - "third": "3", - "fourth": "4", - "fifth": "5", - "sixth": "6", - "seventh": "7", - "eighth": "8", - }), - ShortIntegerRef("n", 1, 100), - ShortIntegerRef("m", 1, 10) - ] diff --git a/castervoice/bin/share/API command grammars example/browser/browser_shared_commands.py b/castervoice/bin/share/API command grammars example/browser/browser_shared_commands.py deleted file mode 100644 index 6f6db8580..000000000 --- a/castervoice/bin/share/API command grammars example/browser/browser_shared_commands.py +++ /dev/null @@ -1,104 +0,0 @@ -# -# __author__ = "lexxish" -# -# pylint: skip-file -from dragonfly import Repeat, Pause, Function - -from castervoice.lib.actions import Key, Mouse -from castervoice.rules.apps.browser import browser_shared -from castervoice.lib import github_automation -from castervoice.lib.actions import Text -from castervoice.lib.merge.state.short import R - - -class BrowserSharedCommands(object): - - @staticmethod - def merge_dictionaries(x, y): - z = x.copy() - z.update(y) - return z - - chromeAndFirefoxMapping = { - browser_shared.OPEN_NEW_WINDOW: - R(Key("c-n")), - browser_shared.OPEN_NEW_INCOGNITO_WINDOW: - R(Key("cs-n")), - browser_shared.NEW_TAB_N_TIMES: - R(Key("c-t") * Repeat(extra="n")), - browser_shared.REOPEN_TAB_N_TIMES: - R(Key("cs-t")) * Repeat(extra="n"), - browser_shared.CLOSE_TAB_N_TIMES: - R(Key("c-w")) * Repeat(extra='n'), - browser_shared.CLOSE_WINDOW: - R(Key("cs-w")), - browser_shared.NEXT_TAB_N_TIMES: - R(Key("c-tab")) * Repeat(extra="n"), - browser_shared.OPEN_NEW_TAB_BASED_ON_CURSOR: - R(Mouse("middle") + Pause("20") + Key("c-tab")), - browser_shared.GO_BACK_N_TIMES: - R(Key("a-left/20")) * Repeat(extra="n"), - browser_shared.GO_FORWARD_N_TIMES: - R(Key("a-right/20")) * Repeat(extra="n"), - browser_shared.ZOOM_IN_N_TIMES: - R(Key("c-plus/20")) * Repeat(extra="n"), - browser_shared.ZOOM_OUT_N_TIMES: - R(Key("c-minus/20")) * Repeat(extra="n"), - browser_shared.ZOOM_RESET_DEFAULT: - R(Key("c-0")), - browser_shared.FORCE_HARD_REFRESH: - R(Key("c-f5")), - browser_shared.FIND_NEXT_MATCH: - R(Key("c-g/20")) * Repeat(extra="n"), - browser_shared.FIND_PREVIOUS_MATCH: - R(Key("cs-g/20")) * Repeat(extra="n"), - # requires an extension in some browsers such as chrome - browser_shared.TOGGLE_CARET_BROWSING: - R(Key("f7")), - browser_shared.GO_TO_HOMEPAGE: - R(Key("a-home")), - browser_shared.SHOW_HISTORY: - R(Key("c-h")), - browser_shared.SELECT_ADDRESS_BAR: - R(Key("c-l")), - browser_shared.SHOW_DOWNLOADS: - R(Key("c-j")), - browser_shared.ADD_BOOKMARK: - R(Key("c-d")), - browser_shared.BOOKMARK_ALL_TABS: - R(Key("cs-d")), - browser_shared.SHOW_BOOKMARKS: - R(Key("cs-o")), - browser_shared.TOGGLE_FULL_SCREEN: - R(Key("f11")), - browser_shared.SHOW_PAGE_SOURCE: - R(Key("c-u")), - browser_shared.DEBUG_RESUME: - R(Key("f8")), - browser_shared.DEBUG_STEP_OVER: - R(Key("f10")), - browser_shared.DEBUG_STEP_INTO: - R(Key("f11")), - browser_shared.DEBUG_STEP_OUT: - R(Key("s-f11")), - browser_shared.DUPLICATE_TAB: - R(Key("a-d,a-c,c-t/15,c-v/15, enter")), - browser_shared.DUPLICATE_WINDOW: - R(Key("a-d,a-c,c-n/15,c-v/15, enter")), - browser_shared.SHOW_MENU: - R(Key("a-f")), - browser_shared.SHOW_SETTINGS: - R(Key("a-f/5, s")), - browser_shared.SHOW_TASK_MANAGER: - R(Key("s-escape")), - browser_shared.CLEAR_BROWSING_DATA: - R(Key("cs-del")), - browser_shared.SHOW_DEVELOPER_TOOLS: - R(Key("cs-i")), - browser_shared.CHECKOUT_PR: - R(Function(github_automation.github_checkoutupdate_pull_request, new=True)), - browser_shared.UPDATE_PR: - R(Function(github_automation.github_checkoutupdate_pull_request, new=False)), - "IRC identify": - R(Text("/msg NickServ identify PASSWORD")), - } diff --git a/castervoice/bin/share/API command grammars example/browser/chrome.py b/castervoice/bin/share/API command grammars example/browser/chrome.py deleted file mode 100644 index 1124ad65f..000000000 --- a/castervoice/bin/share/API command grammars example/browser/chrome.py +++ /dev/null @@ -1,51 +0,0 @@ -# pylint: skip-file -from dragonfly import Repeat, MappingRule - -import browser_shared -from castervoice.lib.actions import Key -from castervoice.rules.apps.browser.browser_shared_commands import BrowserSharedCommands -from castervoice.lib.actions import Text -from castervoice.lib.ctrl.mgr.rule_details import RuleDetails -from castervoice.lib.merge.state.short import R -from castervoice.lib.temporary import Store, Retrieve - - -class ChromeRule(MappingRule): - _mapping = { - browser_shared.PREVIOUS_TAB_N_TIMES: - R(Key("cs-tab")) * Repeat(extra="n"), - browser_shared.SWITCH_TO_TAB_N: - R(Key("c-%(m)s%(nth)s")), - browser_shared.SWITCH_TO_LAST_TAB: - R(Key("c-9")), - browser_shared.SWITCH_TO_SECOND_TO_LAST_TAB: - R(Key("c-9, cs-tab")), - "switch focus []": - R(Key("f6/20")) * Repeat(extra="n"), - browser_shared.TOGGLE_BOOKMARK_TOOLBAR: - R(Key("cs-b")), - "switch user": - R(Key("cs-m")), - "focus notification": - R(Key("a-n")), - "allow notification": - R(Key("as-a")), - "deny notification": - R(Key("as-a")), - "google that": - R(Store(remove_cr=True) + Key("c-t") + Retrieve() + Key("enter")), - "wikipedia that": - R(Store(space="+", remove_cr=True) + Key("c-t") + Text( - "https://en.wikipedia.org/w/index.php?search=") + Retrieve() + Key("enter")), - browser_shared.SHOW_EXTENSIONS: - R(Key("a-f/20, l, e/15, enter")), - "more tools": - R(Key("a-f/5, l")), - } - mapping = BrowserSharedCommands.merge_dictionaries(_mapping, BrowserSharedCommands.chromeAndFirefoxMapping) - extras = browser_shared.get_extras() - defaults = browser_shared.get_defaults() - - -#def get_rule(): -# return ChromeRule, RuleDetails(name="google chrome", executable="chrome") diff --git a/castervoice/bin/share/API command grammars example/browser/firefox.py b/castervoice/bin/share/API command grammars example/browser/firefox.py deleted file mode 100644 index 5d6005ca9..000000000 --- a/castervoice/bin/share/API command grammars example/browser/firefox.py +++ /dev/null @@ -1,29 +0,0 @@ -# pylint: skip-file -from dragonfly import Repeat, MappingRule - -import browser_shared -from castervoice.lib.actions import Key -from castervoice.rules.apps.browser.browser_shared_commands import BrowserSharedCommands -from castervoice.lib.ctrl.mgr.rule_details import RuleDetails -from castervoice.lib.merge.state.short import R - - -class FirefoxRule(MappingRule): - _mapping = { - browser_shared.PREVIOUS_TAB_N_TIMES: - # control shift tab doesn't work and this appears to be an undocumented workaround - R(Key("c-tab/30")) * Repeat(extra="n"), - browser_shared.FIND_NEXT_MATCH: - R(Key("c-g/20")) * Repeat(extra="n"), - browser_shared.TOGGLE_BOOKMARK_TOOLBAR: - R(Key("c-b")), - browser_shared.SHOW_EXTENSIONS: - R(Key("a-a, l, e/15, enter")), - } - mapping = BrowserSharedCommands.merge_dictionaries(_mapping, BrowserSharedCommands.chromeAndFirefoxMapping) - extras = browser_shared.get_extras() - defaults = browser_shared.get_defaults() - - -#def get_rule(): -# return FirefoxRule, RuleDetails(name="fire fox", executable="firefox") diff --git a/castervoice/bin/share/API command grammars example/editor/ide/ide_shared.py b/castervoice/bin/share/API command grammars example/editor/ide/ide_shared.py deleted file mode 100644 index 74d6357f2..000000000 --- a/castervoice/bin/share/API command grammars example/editor/ide/ide_shared.py +++ /dev/null @@ -1,94 +0,0 @@ -# pylint: skip-file -# -# __author__ = "lexxish" -# -from castervoice.rules.apps.shared.directions import FORWARD, RIGHT, BACK, LEFT, UP, DOWN - -method = "(meth|method)" -methods = "(meths|methods)" - -# general -EXPAND_SELECTION = "expand [selection] []" -SHRINK_SELECTION = "shrink [selection] []" -SMART_AUTO_COMPLETE = "(skraken|smart kraken)" -GENERATE_CODE = "jen code" -QUICK_FIX = "quick fix" -FORMAT_ALL_CODE = "format [code]" -BUILD_PROJECT = "build" -RUN_PROJECT = "run" -DEBUG_PROJECT = "debug" -BUILD_AND_RUN_PROJECT = "build and run" -DEBUG_CURRENT_FILE = "debug file" -RUN_CURRENT_FILE = "run file" -NEXT_ERROR = "(%s error|error %s)" % (FORWARD, RIGHT) -PREVIOUS_ERROR = "(%s error|error %s)" % (BACK, LEFT) -REDO = "redo []" -SHOW_SETTINGS = "[show] settings" -RENAME_CURRENT_FILE = "file rename | rename file" - -# window navigation -NEXT_TAB = "%s tab []|tab %s []" % (FORWARD, RIGHT) -PREVIOUS_TAB = "%s tab []|tab %s []" % (BACK, LEFT) -CLOSE_TAB_N_TIMES = "close tab []|tab close []" -GO_TO_EDITOR = "focus editor" -GO_TO_PROJECT_EXPLORER = "go [to] project" -TOGGLE_TERMINAL = "[toggle] (term|terminal)" -NEW_FILE = "new file" - -# editor management -SPLIT_WINDOW_UP = "split [pane] %s" % UP -SPLIT_WINDOW_DOWN = "split [pane] %s" % DOWN -SPLIT_WINDOW_RIGHT = "split [pane] %s" % RIGHT -SPLIT_WINDOW_LEFT = "split [pane] %s" % LEFT -SPLIT_MOVE_UP = "pane %s []" % UP -SPLIT_MOVE_DOWN = "pane %s []" % DOWN -SPLIT_MOVE_RIGHT = "(pane %s|%s pane) []" % (RIGHT, RIGHT) -SPLIT_MOVE_LEFT = "(pane %s|%s pane) []" % (LEFT, LEFT) -CLOSE_PANE_N_TIMES = "close pane []|pane close []" - -# navigation -GO_TO_LINE = "go [to line] []" -METHOD_FORWARD = "%s %s []" % (method, FORWARD) -METHOD_BACKWARD = "%s %s []" % (method, BACK) -NAVIGATE_FORWARD = "go %s []" % FORWARD -NAVIGATE_BACKWARD = "go %s []" % BACK -GO_TO_DECLARATION = "[go to] (source|declaration)" - -# search and replace -FIND_IN_CURRENT_FILE = "find" -FIND_NEXT_MATCH = "find %s [match] []" % BACK -FIND_PREVIOUS_MATCH = "find %s [match] []" % FORWARD -REPLACE_IN_CURRENT_FILE = "replace" -FIND_IN_ALL_FILES = "find [in] (all|files)" -REPLACE_IN_ALL_FILES = "replace [in] (all|files)" -FIND_USAGE = "[find] (usage|usages)" -SEARCH_FOR_ALL_IN_ALL_FILES = "search" -SEARCH_FOR_SYMBOL_IN_ALL_FILES = "find symbol" -SEARCH_FOR_FILE_IN_ALL_FILES = "find file" -SEARCH_FOR_CLASS_IN_ALL_FILES = "find class" - -# line operations -MOVE_LINE_UP = "[move] line %s []" % UP -MOVE_LINE_DOWN = "[move] line %s []" % DOWN -DELETE_LINE = "kill [line]" -DELETE_TO_LINE_END = "kill %s" % FORWARD -DELETE_TO_LINE_START = "kill %s" % BACK -COMMENT_LINE = "(comment|rem) [line]" -UNCOMMENT_LINE = "(uncomment|unrem) [line]" -DUPLICATE_LINE_UP = "(duplicate|duple) %s" % UP -DUPLICATE_LINE_DOWN = "(duplicate|duple) %s" % DOWN - -# refactor -OPTIMIZE_IMPORTS = "[organize|optimize] imports" -REFACTOR = "refactor" -RENAME = "rename" -INLINE = "inline" -extract = "(pull|extract)" -EXTRACT_METHOD = "%s %s" % (extract, method) -EXTRACT_VARIABLE = "%s [variable|var]" % extract -EXTRACT_FIELD = "%s field" % extract -EXTRACT_CONSTANT = "%s constant" % extract -EXTRACT_PARAMETER = "%s (param|parameter)" % extract -IMPLEMENT_METHODS = "implement (%s|%s)" % (method, methods) -OVERRIDE_METHOD = "override %s" % method -AUTO_INDENT = "auto indent" diff --git a/castervoice/bin/share/API command grammars example/editor/ide/jetbrains.py b/castervoice/bin/share/API command grammars example/editor/ide/jetbrains.py deleted file mode 100644 index 1e91ae0cf..000000000 --- a/castervoice/bin/share/API command grammars example/editor/ide/jetbrains.py +++ /dev/null @@ -1,111 +0,0 @@ -# pylint: skip-file -from dragonfly import Dictation, Repeat, MappingRule, ShortIntegerRef - -from castervoice.lib.actions import Text, Key -from castervoice.lib.ctrl.mgr.rule_details import RuleDetails -import ide_shared -from castervoice.lib.merge.state.short import R - - -class JetbrainsRule(MappingRule): - extras = [ - Dictation("text"), - Dictation("mim"), - ShortIntegerRef("n", 1, 1000), - ] - - DELAY = "20" - - mapping = { - ide_shared.QUICK_FIX: R(Key("a-enter")), - ide_shared.DUPLICATE_LINE_DOWN: R(Key("c-d")), - "auto complete": R(Key("cs-a")), - ide_shared.FORMAT_ALL_CODE: R(Key("ca-l")), - "show doc": R(Key("c-q")), - "show param": R(Key("c-p")), - ide_shared.GENERATE_CODE: R(Key("a-insert")), - ide_shared.NEW_FILE: R(Key("a-insert")), - "jump to source": R(Key("f4")), - ide_shared.DELETE_LINE: R(Key("c-y")), - ide_shared.SEARCH_FOR_SYMBOL_IN_ALL_FILES: R(Key("cas-n")), - ide_shared.SEARCH_FOR_FILE_IN_ALL_FILES: R(Key("c-n")), - ide_shared.SEARCH_FOR_CLASS_IN_ALL_FILES: R(Key("c-n")), - ide_shared.BUILD_PROJECT: R(Key("c-f9")), - ide_shared.BUILD_AND_RUN_PROJECT: R(Key("s-f10")), - ide_shared.NEXT_TAB: R(Key("a-right/%s" % DELAY)) * Repeat(extra="n"), - ide_shared.PREVIOUS_TAB: R(Key("a-left/%s" % DELAY)) * Repeat(extra="n"), - ide_shared.COMMENT_LINE: R(Key("c-slash")), - ide_shared.UNCOMMENT_LINE: R(Key("cs-slash")), - "select ex" : R(Key("c-w")), - "select ex down" : R(Key("cs-w")), - ide_shared.SEARCH_FOR_ALL_IN_ALL_FILES: R(Key("shift, shift")), - ide_shared.FIND_IN_CURRENT_FILE: R(Key("c-f")), - ide_shared.FIND_NEXT_MATCH: R(Key("enter")) * Repeat(extra="n"), - ide_shared.FIND_PREVIOUS_MATCH: R(Key("s-enter")) * Repeat(extra="n"), - ide_shared.REPLACE_IN_CURRENT_FILE: R(Key("c-r")), - ide_shared.FIND_IN_ALL_FILES: R(Key("cs-f")), - ide_shared.REPLACE_IN_ALL_FILES: R(Key("cs-r")), - ide_shared.GO_TO_LINE: R(Key("c-g/%s" % DELAY) + Text("%(n)s") + Key("enter")), - ide_shared.IMPLEMENT_METHODS: R(Key("c-i")), - ide_shared.OVERRIDE_METHOD: R(Key("c-o")), - "run config": R(Key("as-f10")), - ide_shared.FIND_USAGE: R(Key("a-f7")), - ide_shared.GO_TO_DECLARATION: R(Key("c-b")), - ide_shared.SMART_AUTO_COMPLETE: R(Key("cs-space")), - ide_shared.NAVIGATE_BACKWARD: R(Key("ca-left")) * Repeat(extra="n"), - ide_shared.NAVIGATE_FORWARD: R(Key("ca-right")) * Repeat(extra="n"), - ide_shared.METHOD_FORWARD: R(Key("a-down")) * Repeat(extra="n"), - ide_shared.METHOD_BACKWARD: R(Key("a-up")) * Repeat(extra="n"), - ide_shared.NEXT_ERROR: R(Key("f2")) * Repeat(extra="n"), - ide_shared.PREVIOUS_ERROR: R(Key("s-f2")) * Repeat(extra="n"), - ide_shared.OPTIMIZE_IMPORTS: R(Key("ca-o")) * Repeat(extra="n"), - ide_shared.MOVE_LINE_UP: R(Key("as-up")) * Repeat(extra="n"), - ide_shared.MOVE_LINE_DOWN: R(Key("as-down")) * Repeat(extra="n"), - ide_shared.EXPAND_SELECTION: R(Key("c-w")) * Repeat(extra="n"), - ide_shared.SHRINK_SELECTION: R(Key("cs-w")) * Repeat(extra="n"), - ide_shared.AUTO_INDENT: R(Key("ca-i")), - ide_shared.CLOSE_TAB_N_TIMES: R(Key("c-f4/%s" % DELAY)) * Repeat(extra="n"), - ide_shared.RUN_PROJECT: R(Key("s-f10")), - ide_shared.DEBUG_PROJECT: R(Key("s-f9")), - ide_shared.REDO: R(Key("cs-z")) * Repeat(extra="n"), - ide_shared.SHOW_SETTINGS: R(Key("ca-s")), - # only works if you disable tabs. - ide_shared.CLOSE_PANE_N_TIMES: R(Key("c-f4/%s" % DELAY)) * Repeat(extra="n"), - - # refactoring - ide_shared.REFACTOR: R(Key("cas-t")), - ide_shared.RENAME: R(Key("s-f6")), - ide_shared.INLINE: R(Key("ca-n")), - ide_shared.EXTRACT_METHOD: R(Key("ca-m")), - ide_shared.EXTRACT_VARIABLE: R(Key("ca-v")) * Repeat(extra="n"), - ide_shared.EXTRACT_FIELD: R(Key("ca-f")) * Repeat(extra="n"), - ide_shared.EXTRACT_CONSTANT: R(Key("ca-c")) * Repeat(extra="n"), - ide_shared.EXTRACT_PARAMETER: R(Key("ca-p")) * Repeat(extra="n"), - - # window navigation - ide_shared.GO_TO_EDITOR: R(Key("escape")), - ide_shared.GO_TO_PROJECT_EXPLORER: R(Key("a-1")), - ide_shared.TOGGLE_TERMINAL: R(Key("a-f12")), - - # must be bound manually below this point - ide_shared.DELETE_TO_LINE_START: R(Key("a-d,0")), - ide_shared.DELETE_TO_LINE_END: R(Key("a-d,$")), - # jet brains can only split horizontally or vertically - ide_shared.SPLIT_WINDOW_UP: R(Key("cs-s,h")), - ide_shared.SPLIT_WINDOW_DOWN: R(Key("cs-s,h")), - ide_shared.SPLIT_WINDOW_RIGHT: R(Key("cs-s,v")), - ide_shared.SPLIT_WINDOW_LEFT: R(Key("cs-s,v")), - ide_shared.SPLIT_MOVE_UP: R(Key("cs-s,up")) * Repeat(extra="n"), - ide_shared.SPLIT_MOVE_DOWN: R(Key("cs-s,down")) * Repeat(extra="n"), - ide_shared.SPLIT_MOVE_RIGHT: R(Key("cs-s,right")) * Repeat(extra="n"), - ide_shared.SPLIT_MOVE_LEFT: R(Key("cs-s,left")) * Repeat(extra="n"), - ide_shared.RENAME_CURRENT_FILE: R(Key("cas-r")), - } - - defaults = {"n": 1, "mim": ""} - - -def get_rule(): - details = RuleDetails(name="jet brains", - executable=["idea", "idea64", "studio64", "pycharm"]) - return JetbrainsRule, details diff --git a/castervoice/bin/share/API command grammars example/shared/directions.py b/castervoice/bin/share/API command grammars example/shared/directions.py deleted file mode 100644 index af00f939f..000000000 --- a/castervoice/bin/share/API command grammars example/shared/directions.py +++ /dev/null @@ -1,10 +0,0 @@ -# -# __author__ = "lexxish" -# - -RIGHT = "(right|sauce)" -LEFT = "(left|lease)" -UP = "(up|sauce)" -DOWN = "(down|dunce)" -FORWARD = "(%s|next|forward)" % RIGHT -BACK = "(%s|back|prev|prior|previous)" % LEFT diff --git a/castervoice/lib/available_commands_tracker.py b/castervoice/lib/available_commands_tracker.py index c68c7ee40..0993e03ff 100644 --- a/castervoice/lib/available_commands_tracker.py +++ b/castervoice/lib/available_commands_tracker.py @@ -1,4 +1,3 @@ -import six class AvailableCommandsTracker(object): """ @@ -14,7 +13,7 @@ def __init__(self): self._available_commands = "Available commands not set yet." def set_available_commands(self, commands): - if not isinstance(commands, six.string_types): + if not isinstance(commands, str): raise Exception("Do not set 'commands' to a non-string format.") self._available_commands = commands diff --git a/castervoice/lib/ctrl/configure_engine.py b/castervoice/lib/ctrl/configure_engine.py index 4a3598212..6446f342c 100644 --- a/castervoice/lib/ctrl/configure_engine.py +++ b/castervoice/lib/ctrl/configure_engine.py @@ -1,17 +1,36 @@ 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 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 +54,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/loading/load/content_request_generator.py b/castervoice/lib/ctrl/mgr/loading/load/content_request_generator.py index cdf22b07a..a241d664f 100644 --- a/castervoice/lib/ctrl/mgr/loading/load/content_request_generator.py +++ b/castervoice/lib/ctrl/mgr/loading/load/content_request_generator.py @@ -1,8 +1,5 @@ import re import os -import six -if six.PY2: - from io import open from castervoice.lib.ctrl.mgr.loading.load.content_request import ContentRequest from castervoice.lib.ctrl.mgr.loading.load.content_type import ContentType diff --git a/castervoice/lib/ctrl/mgr/rule_details.py b/castervoice/lib/ctrl/mgr/rule_details.py index e9ef908b4..b6a4e3832 100644 --- a/castervoice/lib/ctrl/mgr/rule_details.py +++ b/castervoice/lib/ctrl/mgr/rule_details.py @@ -1,12 +1,9 @@ +import inspect import os import traceback -import inspect +from pathlib import Path + from castervoice.lib import printer -import six -if six.PY2: - from castervoice.lib.util.pathlib import Path -else: - from pathlib import Path # pylint: disable=import-error class RuleDetails(object): @@ -40,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: @@ -49,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/ctrl/mgr/validation/combo/treerule_validator.py b/castervoice/lib/ctrl/mgr/validation/combo/treerule_validator.py index 971f2b8cc..867668c95 100644 --- a/castervoice/lib/ctrl/mgr/validation/combo/treerule_validator.py +++ b/castervoice/lib/ctrl/mgr/validation/combo/treerule_validator.py @@ -1,5 +1,4 @@ from dragonfly import ActionBase -import six from castervoice.lib.ctrl.mgr.validation.combo.base_combo_validator import BaseComboValidator from castervoice.lib.merge.selfmod.tree_rule.tree_node import TreeNode from castervoice.lib.merge.selfmod.tree_rule.tree_rule import TreeRule @@ -21,7 +20,7 @@ def _validate_node(node): err = str(spec) + ", " + str(action) + ", " + str(children) invalidations = [] - if not isinstance(spec, six.string_types): + if not isinstance(spec, str): invalidations.append("node spec must be string ({})".format(err)) if not isinstance(action, ActionBase): invalidations.append("node base must be ActionBase ({})".format(err)) diff --git a/castervoice/lib/github_automation.py b/castervoice/lib/github_automation.py index 16fa0ea77..b27bfb544 100644 --- a/castervoice/lib/github_automation.py +++ b/castervoice/lib/github_automation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import os import shutil @@ -10,7 +9,6 @@ from castervoice.lib.utilities import load_toml_file from castervoice.lib import settings - def _copy_path(): if not os.path.isfile(settings.SETTINGS["paths"]["GIT_REPO_LOCAL_REMOTE_PATH"]): git_match_default = settings.SETTINGS["paths"]["GIT_REPO_LOCAL_REMOTE_DEFAULT_PATH"] @@ -49,16 +47,16 @@ def github_checkoutupdate_pull_request(new): TERMINAL_PATH = settings.SETTINGS["paths"]["TERMINAL_PATH"] AHK_PATH = settings.SETTINGS["paths"]["AHK_PATH"] ahk_installed = os.path.isfile(AHK_PATH) - print("AHK_PATH = " + AHK_PATH) if TERMINAL_PATH != "": load_terminal = True # set default value # ready fetch command string to be appended to fetch_command = "" # find the equivalent ahk script with the same name as this one ahk_script = __file__.replace(".pyc", ".ahk").replace(".py", ".ahk") - pattern_match = "MINGW64" # the string we expect to find in the title of git bash when loaded + # if autohotkey is installed if ahk_installed: + pattern_match = "MINGW64" # the string we expect to find in the title of git bash when loaded # open the script which checks that git bash window is open or not p = Popen([AHK_PATH, ahk_script, "exists", pattern_match], stdout=PIPE) # retrieve the output from the ahk script @@ -81,7 +79,10 @@ def github_checkoutupdate_pull_request(new): print("Fallback: load new instance of :" + pattern_match) if load_terminal: # open up a new git bash terminal - terminal = Popen(TERMINAL_PATH, cwd=local_directory) + if os.path.isfile(TERMINAL_PATH): + terminal = Popen(TERMINAL_PATH, cwd=local_directory) + else: + raise Exception("Error: terminal path not set correctly in settings.toml") # if autohotkey is installed if ahk_installed: # open the script which checks that git bash windoow is ready or not for input 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/ccrmerging2/hooks/standard_hooks/show_window_on_error_hook.py b/castervoice/lib/merge/ccrmerging2/hooks/standard_hooks/show_window_on_error_hook.py index d2ca5b1c3..759a4e21c 100644 --- a/castervoice/lib/merge/ccrmerging2/hooks/standard_hooks/show_window_on_error_hook.py +++ b/castervoice/lib/merge/ccrmerging2/hooks/standard_hooks/show_window_on_error_hook.py @@ -3,7 +3,6 @@ from castervoice.lib.merge.ccrmerging2.hooks.events.event_types import EventType from castervoice.lib import printer -import six from dragonfly import get_current_engine from dragonfly.windows.window import Window @@ -11,13 +10,10 @@ def show_window(): title = None engine = get_current_engine().name if engine == 'natlink': - import natlinkstatus # pylint: disable=import-error + from natlinkcore import natlinkstatus # pylint: disable=import-error status = natlinkstatus.NatlinkStatus() if status.NatlinkIsEnabled() == 1: - if six.PY2: - title = "Messages from Python Macros" - else: - title= "Messages from Natlink" + title= "Messages from Natlink" else: title = "Caster: Status Window" if engine != 'natlink': diff --git a/castervoice/lib/merge/ccrmerging2/transformers/text_replacer/text_replacer.py b/castervoice/lib/merge/ccrmerging2/transformers/text_replacer/text_replacer.py index d3eb77f13..7dd2b60c1 100644 --- a/castervoice/lib/merge/ccrmerging2/transformers/text_replacer/text_replacer.py +++ b/castervoice/lib/merge/ccrmerging2/transformers/text_replacer/text_replacer.py @@ -4,7 +4,7 @@ from castervoice.lib.merge.ccrmerging2.transformers.base_transformer import BaseRuleTransformer from castervoice.lib.merge.ccrmerging2.transformers.text_replacer.tr_extra_data import TextReplacementExtraData from castervoice.lib.merge.ccrmerging2.transformers.text_replacer.tr_parser import TRParser -import six + def _analyze_extras(spec): """ @@ -114,7 +114,7 @@ def _spec_override_from_config(rule, definitions): if len(defaults) > 0: for default_key in list(defaults.keys()): # value = defaults[default_key] - if isinstance(value, six.string_types): + if isinstance(value, str): '''only replace strings; also, only replace values, not keys: default_key should not be changed - it will never be spoken''' diff --git a/castervoice/lib/merge/communication.py b/castervoice/lib/merge/communication.py index 9000ea2ab..74f8cf2b3 100644 --- a/castervoice/lib/merge/communication.py +++ b/castervoice/lib/merge/communication.py @@ -1,8 +1,4 @@ -import six -if six.PY2: - import xmlrpclib # pylint: disable=import-error -else: - import xmlrpc.client as xmlrpclib +import xmlrpc.client as xmlrpclib class Communicator: LOCALHOST = "127.0.0.1" @@ -10,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/merge/state/stack.py b/castervoice/lib/merge/state/stack.py index ef6948b39..453f82578 100644 --- a/castervoice/lib/merge/state/stack.py +++ b/castervoice/lib/merge/state/stack.py @@ -3,15 +3,8 @@ @author: dave ''' -import six -if six.PY2: - import Queue # pylint: disable=import-error -else: - import queue as Queue +import queue as Queue -from dragonfly import RecognitionHistory - -from castervoice.lib import settings, utilities from castervoice.lib.merge.state.stackitems import StackItemSeeker, \ StackItemRegisteredAction, StackItemAsynchronous, StackItemConfirm diff --git a/castervoice/lib/migration.py b/castervoice/lib/migration.py index a8839a09d..63c60afab 100644 --- a/castervoice/lib/migration.py +++ b/castervoice/lib/migration.py @@ -1,15 +1,10 @@ +from pathlib import Path import shutil -import six - from castervoice.lib import settings from castervoice.lib.ctrl.mgr.loading.load.content_root import ContentRoot from castervoice.lib.merge.selfmod.sm_config import SelfModStateSavingConfig -if six.PY2: - from castervoice.lib.util.pathlib import Path -else: - from pathlib import Path # pylint: disable=import-error class UserDirUpdater(object): diff --git a/castervoice/lib/navigation.py b/castervoice/lib/navigation.py index 3f568f0d3..f55e9296e 100644 --- a/castervoice/lib/navigation.py +++ b/castervoice/lib/navigation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- ''' master_text_nav shouldn't take strings as arguments - it should take ints, so it can be language-agnostic ''' diff --git a/castervoice/lib/printer.py b/castervoice/lib/printer.py index 2cf6cbb8f..5cd9448ae 100644 --- a/castervoice/lib/printer.py +++ b/castervoice/lib/printer.py @@ -1,6 +1,3 @@ -from __future__ import print_function - - class _DelegatingPrinterMessageHandler(object): def __init__(self): 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 030242449..a66815c89 100644 --- a/castervoice/lib/settings.py +++ b/castervoice/lib/settings.py @@ -1,24 +1,14 @@ -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals -from builtins import str - +import sys, os import collections import io -import tomlkit -from past.builtins import xrange - -from castervoice.lib import printer -from castervoice.lib import version -from castervoice.lib.util import guidance +import copy +from pathlib import Path +import tomlkit from appdirs import * - -import six -if six.PY2: - from castervoice.lib.util.pathlib import Path -else: - from pathlib import Path # pylint: disable=import-error +from castervoice.lib import printer, version +from castervoice.lib.util import guidance +from past.builtins import xrange # consts: some of these can easily be moved out of this file GENERIC_HELP_MESSAGE = """ @@ -33,6 +23,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" @@ -116,10 +107,7 @@ def _find_natspeak(): ''' try: - if six.PY2: - import _winreg as winreg - else: - import winreg + import winreg except ImportError: printer.out("Could not import winreg") return "" @@ -205,6 +193,7 @@ def _init(path): if num_default_added > 0: printer.out("Default settings values added: {} ".format(num_default_added)) _save(result, _SETTINGS_PATH) + result['paths'] = {k: os.path.expandvars(v) for k, v in result['paths'].items()} return result @@ -229,14 +218,25 @@ def _deep_merge_defaults(data, defaults): def _get_defaults(): - terminal_path_default = "C:/Program Files/Git/git-bash.exe" + + if sys.platform == "win32": + terminal_path_default = "C:/Program Files/Git/git-bash.exe" + elif sys.platform.startswith("linux"): + terminal_path_default = "/usr/bin/gnome-terminal" + elif sys.platform == "darwin": + terminal_path_default = "/Applications/Utilities/Terminal.app" + if not os.path.isfile(terminal_path_default): terminal_path_default = "" ahk_path_default = "C:/Program Files/AutoHotkey/AutoHotkey.exe" if not os.path.isfile(ahk_path_default): ahk_path_default = "" - + + if sys.platform == "win32": + terminal_loading_time = 5 + else: + terminal_loading_time = 1 return { "paths": { "BASE_PATH": @@ -264,7 +264,7 @@ def _get_defaults(): "DLL_PATH": str(Path(_BASE_PATH).joinpath("lib/dll/")), "GDEF_FILE": - str(Path(_USER_DIR).joinpath("transformers/words.txt")), + str(Path(_USER_DIR).joinpath("caster_user_content/transformers/words.txt")), "LOG_PATH": str(Path(_USER_DIR).joinpath("log.txt")), "SAVED_CLIPBOARD_PATH": @@ -288,13 +288,15 @@ def _get_defaults(): # EXECUTABLES "AHK_PATH": - str(Path(_BASE_PATH).joinpath(ahk_path_default)), + str(Path(ahk_path_default)), "DOUGLAS_PATH": str(Path(_BASE_PATH).joinpath("asynch/mouse/grids.py")), "ENGINE_PATH": _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": @@ -352,8 +354,9 @@ def _get_defaults(): }, # gitbash settings + # This really should be labelled "terminal" but was named when caster was Windows only. "gitbash": { - "loading_time": 5, # the time to initialise the git bash window in seconds + "loading_time": terminal_loading_time, # the time to initialise the git bash window in seconds "fetching_time": 3 # the time to fetch a github repository in seconds }, @@ -370,7 +373,7 @@ def _get_defaults(): # Default enabled hooks: Use hook class name "hooks": { - "default_hooks": ['PrinterHook'], + "default_hooks": ['PrinterHook', 'RulesLoadedHook'], }, # miscellaneous section @@ -460,11 +463,31 @@ def settings(key_path, default_value=None): return value -def save_config(): +def save_config(paths = False): """ Save the current in-memory settings to disk """ - _save(SETTINGS, _SETTINGS_PATH) + guidance.offer() + if not paths: + # Use the paths on disk + result = {} + try: + with io.open(_SETTINGS_PATH, "rt", encoding="utf-8") as f: + result = tomlkit.loads(f.read()).value + except ValueError as e: + printer.out("\n\n {} while loading settings file: {} \n\n".format(repr(e), _SETTINGS_PATH)) + printer.out(sys.exc_info()) + except IOError as e: + printer.out("\n\n {} while loading settings file: {} \nAttempting to recover...\n\n".format(repr(e), _SETTINGS_PATH)) + SETTINGS_tmp = copy.deepcopy(SETTINGS) + if "paths" in result: + SETTINGS_tmp['paths'] = result['paths'] + _save(SETTINGS_tmp, _SETTINGS_PATH) + else: + _save(SETTINGS, _SETTINGS_PATH) + + + def initialize(): @@ -494,8 +517,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/util/pathlib/__init__.py b/castervoice/lib/util/pathlib/__init__.py deleted file mode 100644 index 2565c36a5..000000000 --- a/castervoice/lib/util/pathlib/__init__.py +++ /dev/null @@ -1,1807 +0,0 @@ -# pylint: skip-file -# Copyright (c) 2014-2017 Matthias C. M. Troffaes -# Copyright (c) 2012-2014 Antoine Pitrou and contributors -# Distributed under the terms of the MIT License. - -# Remove this file after transitioning Caster to Python 3 and dropping Python 2 support. - -import ctypes -import fnmatch -import functools -import io -import ntpath -import os -import posixpath -import re -import six -import sys - -from errno import EINVAL, ENOENT, ENOTDIR, EBADF -from errno import EEXIST, EPERM, EACCES -from operator import attrgetter -from stat import ( - S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO) - -try: - from collections.abc import Sequence -except ImportError: - from collections import Sequence - -try: - from urllib import quote as urlquote_from_bytes -except ImportError: - from urllib.parse import quote_from_bytes as urlquote_from_bytes - - -supports_symlinks = True -if os.name == 'nt': - import nt - if sys.getwindowsversion()[:2] >= (6, 0) and sys.version_info >= (3, 2): - from nt import _getfinalpathname - else: - supports_symlinks = False - _getfinalpathname = None -else: - nt = None - -try: - from os import scandir as os_scandir -except ImportError: - from scandir import scandir as os_scandir - -__all__ = [ - "PurePath", "PurePosixPath", "PureWindowsPath", - "Path", "PosixPath", "WindowsPath", - ] - -# -# Internals -# - -# EBADF - guard agains macOS `stat` throwing EBADF -_IGNORED_ERROS = (ENOENT, ENOTDIR, EBADF) - -_IGNORED_WINERRORS = ( - 21, # ERROR_NOT_READY - drive exists but is not accessible -) - - -def _ignore_error(exception): - return (getattr(exception, 'errno', None) in _IGNORED_ERROS or - getattr(exception, 'winerror', None) in _IGNORED_WINERRORS) - - -def _py2_fsencode(parts): - # py2 => minimal unicode support - assert six.PY2 - return [part.encode('ascii') if isinstance(part, six.text_type) - else part for part in parts] - - -def _try_except_fileexistserror(try_func, except_func, else_func=None): - if sys.version_info >= (3, 3): - try: - try_func() - except FileExistsError as exc: - except_func(exc) - else: - if else_func is not None: - else_func() - else: - try: - try_func() - except EnvironmentError as exc: - if exc.errno != EEXIST: - raise - else: - except_func(exc) - else: - if else_func is not None: - else_func() - - -def _try_except_filenotfounderror(try_func, except_func): - if sys.version_info >= (3, 3): - try: - try_func() - except FileNotFoundError as exc: - except_func(exc) - elif os.name != 'nt': - try: - try_func() - except EnvironmentError as exc: - if exc.errno != ENOENT: - raise - else: - except_func(exc) - else: - try: - try_func() - except WindowsError as exc: - # errno contains winerror - # 2 = file not found - # 3 = path not found - if exc.errno not in (2, 3): - raise - else: - except_func(exc) - except EnvironmentError as exc: - if exc.errno != ENOENT: - raise - else: - except_func(exc) - - -def _try_except_permissionerror_iter(try_iter, except_iter): - if sys.version_info >= (3, 3): - try: - for x in try_iter(): - yield x - except PermissionError as exc: - for x in except_iter(exc): - yield x - else: - try: - for x in try_iter(): - yield x - except EnvironmentError as exc: - if exc.errno not in (EPERM, EACCES): - raise - else: - for x in except_iter(exc): - yield x - - -def _win32_get_unique_path_id(path): - # get file information, needed for samefile on older Python versions - # see http://timgolden.me.uk/python/win32_how_do_i/ - # see_if_two_files_are_the_same_file.html - from ctypes import POINTER, Structure, WinError - from ctypes.wintypes import DWORD, HANDLE, BOOL - - class FILETIME(Structure): - _fields_ = [("datetime_lo", DWORD), - ("datetime_hi", DWORD), - ] - - class BY_HANDLE_FILE_INFORMATION(Structure): - _fields_ = [("attributes", DWORD), - ("created_at", FILETIME), - ("accessed_at", FILETIME), - ("written_at", FILETIME), - ("volume", DWORD), - ("file_hi", DWORD), - ("file_lo", DWORD), - ("n_links", DWORD), - ("index_hi", DWORD), - ("index_lo", DWORD), - ] - - CreateFile = ctypes.windll.kernel32.CreateFileW - CreateFile.argtypes = [ctypes.c_wchar_p, DWORD, DWORD, ctypes.c_void_p, - DWORD, DWORD, HANDLE] - CreateFile.restype = HANDLE - GetFileInformationByHandle = ( - ctypes.windll.kernel32.GetFileInformationByHandle) - GetFileInformationByHandle.argtypes = [ - HANDLE, POINTER(BY_HANDLE_FILE_INFORMATION)] - GetFileInformationByHandle.restype = BOOL - CloseHandle = ctypes.windll.kernel32.CloseHandle - CloseHandle.argtypes = [HANDLE] - CloseHandle.restype = BOOL - GENERIC_READ = 0x80000000 - FILE_SHARE_READ = 0x00000001 - FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 - OPEN_EXISTING = 3 - if os.path.isdir(path): - flags = FILE_FLAG_BACKUP_SEMANTICS - else: - flags = 0 - hfile = CreateFile(path, GENERIC_READ, FILE_SHARE_READ, - None, OPEN_EXISTING, flags, None) - if hfile == 0xffffffff: - if sys.version_info >= (3, 3): - raise FileNotFoundError(path) - else: - exc = OSError("file not found: path") - exc.errno = ENOENT - raise exc - info = BY_HANDLE_FILE_INFORMATION() - success = GetFileInformationByHandle(hfile, info) - CloseHandle(hfile) - if success == 0: - raise WinError() - return info.volume, info.index_hi, info.index_lo - - -def _is_wildcard_pattern(pat): - # Whether this pattern needs actual matching using fnmatch, or can - # be looked up directly as a file. - return "*" in pat or "?" in pat or "[" in pat - - -class _Flavour(object): - - """A flavour implements a particular (platform-specific) set of path - semantics.""" - - def __init__(self): - self.join = self.sep.join - - def parse_parts(self, parts): - if six.PY2: - parts = _py2_fsencode(parts) - parsed = [] - sep = self.sep - altsep = self.altsep - drv = root = '' - it = reversed(parts) - for part in it: - if not part: - continue - if altsep: - part = part.replace(altsep, sep) - drv, root, rel = self.splitroot(part) - if sep in rel: - for x in reversed(rel.split(sep)): - if x and x != '.': - parsed.append(x) - else: - if rel and rel != '.': - parsed.append(rel) - if drv or root: - if not drv: - # If no drive is present, try to find one in the previous - # parts. This makes the result of parsing e.g. - # ("C:", "/", "a") reasonably intuitive. - for part in it: - if not part: - continue - if altsep: - part = part.replace(altsep, sep) - drv = self.splitroot(part)[0] - if drv: - break - break - if drv or root: - parsed.append(drv + root) - parsed.reverse() - return drv, root, parsed - - def join_parsed_parts(self, drv, root, parts, drv2, root2, parts2): - """ - Join the two paths represented by the respective - (drive, root, parts) tuples. Return a new (drive, root, parts) tuple. - """ - if root2: - if not drv2 and drv: - return drv, root2, [drv + root2] + parts2[1:] - elif drv2: - if drv2 == drv or self.casefold(drv2) == self.casefold(drv): - # Same drive => second path is relative to the first - return drv, root, parts + parts2[1:] - else: - # Second path is non-anchored (common case) - return drv, root, parts + parts2 - return drv2, root2, parts2 - - -class _WindowsFlavour(_Flavour): - # Reference for Windows paths can be found at - # http://msdn.microsoft.com/en-us/library/aa365247%28v=vs.85%29.aspx - - sep = '\\' - altsep = '/' - has_drv = True - pathmod = ntpath - - is_supported = (os.name == 'nt') - - drive_letters = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') - ext_namespace_prefix = '\\\\?\\' - - reserved_names = ( - set(['CON', 'PRN', 'AUX', 'NUL']) | - set(['COM%d' % i for i in range(1, 10)]) | - set(['LPT%d' % i for i in range(1, 10)]) - ) - - # Interesting findings about extended paths: - # - '\\?\c:\a', '//?/c:\a' and '//?/c:/a' are all supported - # but '\\?\c:/a' is not - # - extended paths are always absolute; "relative" extended paths will - # fail. - - def splitroot(self, part, sep=sep): - first = part[0:1] - second = part[1:2] - if (second == sep and first == sep): - # XXX extended paths should also disable the collapsing of "." - # components (according to MSDN docs). - prefix, part = self._split_extended_path(part) - first = part[0:1] - second = part[1:2] - else: - prefix = '' - third = part[2:3] - if (second == sep and first == sep and third != sep): - # is a UNC path: - # vvvvvvvvvvvvvvvvvvvvv root - # \\machine\mountpoint\directory\etc\... - # directory ^^^^^^^^^^^^^^ - index = part.find(sep, 2) - if index != -1: - index2 = part.find(sep, index + 1) - # a UNC path can't have two slashes in a row - # (after the initial two) - if index2 != index + 1: - if index2 == -1: - index2 = len(part) - if prefix: - return prefix + part[1:index2], sep, part[index2 + 1:] - else: - return part[:index2], sep, part[index2 + 1:] - drv = root = '' - if second == ':' and first in self.drive_letters: - drv = part[:2] - part = part[2:] - first = third - if first == sep: - root = first - part = part.lstrip(sep) - return prefix + drv, root, part - - def casefold(self, s): - return s.lower() - - def casefold_parts(self, parts): - return [p.lower() for p in parts] - - def resolve(self, path, strict=False): - s = str(path) - if not s: - return os.getcwd() - previous_s = None - if _getfinalpathname is not None: - if strict: - return self._ext_to_normal(_getfinalpathname(s)) - else: - # End of the path after the first one not found - tail_parts = [] - - def _try_func(): - result[0] = self._ext_to_normal(_getfinalpathname(s)) - # if there was no exception, set flag to 0 - result[1] = 0 - - def _exc_func(exc): - pass - - while True: - result = [None, 1] - _try_except_filenotfounderror(_try_func, _exc_func) - if result[1] == 1: # file not found exception raised - previous_s = s - s, tail = os.path.split(s) - tail_parts.append(tail) - if previous_s == s: - return path - else: - s = result[0] - return os.path.join(s, *reversed(tail_parts)) - # Means fallback on absolute - return None - - def _split_extended_path(self, s, ext_prefix=ext_namespace_prefix): - prefix = '' - if s.startswith(ext_prefix): - prefix = s[:4] - s = s[4:] - if s.startswith('UNC\\'): - prefix += s[:3] - s = '\\' + s[3:] - return prefix, s - - def _ext_to_normal(self, s): - # Turn back an extended path into a normal DOS-like path - return self._split_extended_path(s)[1] - - def is_reserved(self, parts): - # NOTE: the rules for reserved names seem somewhat complicated - # (e.g. r"..\NUL" is reserved but not r"foo\NUL"). - # We err on the side of caution and return True for paths which are - # not considered reserved by Windows. - if not parts: - return False - if parts[0].startswith('\\\\'): - # UNC paths are never reserved - return False - return parts[-1].partition('.')[0].upper() in self.reserved_names - - def make_uri(self, path): - # Under Windows, file URIs use the UTF-8 encoding. - drive = path.drive - if len(drive) == 2 and drive[1] == ':': - # It's a path on a local drive => 'file:///c:/a/b' - rest = path.as_posix()[2:].lstrip('/') - return 'file:///%s/%s' % ( - drive, urlquote_from_bytes(rest.encode('utf-8'))) - else: - # It's a path on a network drive => 'file://host/share/a/b' - return 'file:' + urlquote_from_bytes( - path.as_posix().encode('utf-8')) - - def gethomedir(self, username): - if 'HOME' in os.environ: - userhome = os.environ['HOME'] - elif 'USERPROFILE' in os.environ: - userhome = os.environ['USERPROFILE'] - elif 'HOMEPATH' in os.environ: - try: - drv = os.environ['HOMEDRIVE'] - except KeyError: - drv = '' - userhome = drv + os.environ['HOMEPATH'] - else: - raise RuntimeError("Can't determine home directory") - - if username: - # Try to guess user home directory. By default all users - # directories are located in the same place and are named by - # corresponding usernames. If current user home directory points - # to nonstandard place, this guess is likely wrong. - if os.environ['USERNAME'] != username: - drv, root, parts = self.parse_parts((userhome,)) - if parts[-1] != os.environ['USERNAME']: - raise RuntimeError("Can't determine home directory " - "for %r" % username) - parts[-1] = username - if drv or root: - userhome = drv + root + self.join(parts[1:]) - else: - userhome = self.join(parts) - return userhome - - -class _PosixFlavour(_Flavour): - sep = '/' - altsep = '' - has_drv = False - pathmod = posixpath - - is_supported = (os.name != 'nt') - - def splitroot(self, part, sep=sep): - if part and part[0] == sep: - stripped_part = part.lstrip(sep) - # According to POSIX path resolution: - # http://pubs.opengroup.org/onlinepubs/009695399/basedefs/ - # xbd_chap04.html#tag_04_11 - # "A pathname that begins with two successive slashes may be - # interpreted in an implementation-defined manner, although more - # than two leading slashes shall be treated as a single slash". - if len(part) - len(stripped_part) == 2: - return '', sep * 2, stripped_part - else: - return '', sep, stripped_part - else: - return '', '', part - - def casefold(self, s): - return s - - def casefold_parts(self, parts): - return parts - - def resolve(self, path, strict=False): - sep = self.sep - accessor = path._accessor - seen = {} - - def _resolve(path, rest): - if rest.startswith(sep): - path = '' - - for name in rest.split(sep): - if not name or name == '.': - # current dir - continue - if name == '..': - # parent dir - path, _, _ = path.rpartition(sep) - continue - newpath = path + sep + name - if newpath in seen: - # Already seen this path - path = seen[newpath] - if path is not None: - # use cached value - continue - # The symlink is not resolved, so we must have a symlink - # loop. - raise RuntimeError("Symlink loop from %r" % newpath) - # Resolve the symbolic link - try: - target = accessor.readlink(newpath) - except OSError as e: - if e.errno != EINVAL and strict: - raise - # Not a symlink, or non-strict mode. We just leave the path - # untouched. - path = newpath - else: - seen[newpath] = None # not resolved symlink - path = _resolve(path, target) - seen[newpath] = path # resolved symlink - - return path - # NOTE: according to POSIX, getcwd() cannot contain path components - # which are symlinks. - base = '' if path.is_absolute() else os.getcwd() - return _resolve(base, str(path)) or sep - - def is_reserved(self, parts): - return False - - def make_uri(self, path): - # We represent the path using the local filesystem encoding, - # for portability to other applications. - bpath = bytes(path) - return 'file://' + urlquote_from_bytes(bpath) - - def gethomedir(self, username): - if not username: - try: - return os.environ['HOME'] - except KeyError: - import pwd - return pwd.getpwuid(os.getuid()).pw_dir - else: - import pwd - try: - return pwd.getpwnam(username).pw_dir - except KeyError: - raise RuntimeError("Can't determine home directory " - "for %r" % username) - - -_windows_flavour = _WindowsFlavour() -_posix_flavour = _PosixFlavour() - - -class _Accessor: - - """An accessor implements a particular (system-specific or not) way of - accessing paths on the filesystem.""" - - -class _NormalAccessor(_Accessor): - - def _wrap_strfunc(strfunc): - @functools.wraps(strfunc) - def wrapped(pathobj, *args): - return strfunc(str(pathobj), *args) - return staticmethod(wrapped) - - def _wrap_binary_strfunc(strfunc): - @functools.wraps(strfunc) - def wrapped(pathobjA, pathobjB, *args): - return strfunc(str(pathobjA), str(pathobjB), *args) - return staticmethod(wrapped) - - stat = _wrap_strfunc(os.stat) - - lstat = _wrap_strfunc(os.lstat) - - open = _wrap_strfunc(os.open) - - listdir = _wrap_strfunc(os.listdir) - - scandir = _wrap_strfunc(os_scandir) - - chmod = _wrap_strfunc(os.chmod) - - if hasattr(os, "lchmod"): - lchmod = _wrap_strfunc(os.lchmod) - else: - def lchmod(self, pathobj, mode): - raise NotImplementedError("lchmod() not available on this system") - - mkdir = _wrap_strfunc(os.mkdir) - - unlink = _wrap_strfunc(os.unlink) - - rmdir = _wrap_strfunc(os.rmdir) - - rename = _wrap_binary_strfunc(os.rename) - - if sys.version_info >= (3, 3): - replace = _wrap_binary_strfunc(os.replace) - - if nt: - if supports_symlinks: - symlink = _wrap_binary_strfunc(os.symlink) - else: - def symlink(a, b, target_is_directory): - raise NotImplementedError( - "symlink() not available on this system") - else: - # Under POSIX, os.symlink() takes two args - @staticmethod - def symlink(a, b, target_is_directory): - return os.symlink(str(a), str(b)) - - utime = _wrap_strfunc(os.utime) - - # Helper for resolve() - def readlink(self, path): - return os.readlink(path) - - -_normal_accessor = _NormalAccessor() - - -# -# Globbing helpers -# - -def _make_selector(pattern_parts): - pat = pattern_parts[0] - child_parts = pattern_parts[1:] - if pat == '**': - cls = _RecursiveWildcardSelector - elif '**' in pat: - raise ValueError( - "Invalid pattern: '**' can only be an entire path component") - elif _is_wildcard_pattern(pat): - cls = _WildcardSelector - else: - cls = _PreciseSelector - return cls(pat, child_parts) - - -if hasattr(functools, "lru_cache"): - _make_selector = functools.lru_cache()(_make_selector) - - -class _Selector: - - """A selector matches a specific glob pattern part against the children - of a given path.""" - - def __init__(self, child_parts): - self.child_parts = child_parts - if child_parts: - self.successor = _make_selector(child_parts) - self.dironly = True - else: - self.successor = _TerminatingSelector() - self.dironly = False - - def select_from(self, parent_path): - """Iterate over all child paths of `parent_path` matched by this - selector. This can contain parent_path itself.""" - path_cls = type(parent_path) - is_dir = path_cls.is_dir - exists = path_cls.exists - scandir = parent_path._accessor.scandir - if not is_dir(parent_path): - return iter([]) - return self._select_from(parent_path, is_dir, exists, scandir) - - -class _TerminatingSelector: - - def _select_from(self, parent_path, is_dir, exists, scandir): - yield parent_path - - -class _PreciseSelector(_Selector): - - def __init__(self, name, child_parts): - self.name = name - _Selector.__init__(self, child_parts) - - def _select_from(self, parent_path, is_dir, exists, scandir): - def try_iter(): - path = parent_path._make_child_relpath(self.name) - if (is_dir if self.dironly else exists)(path): - for p in self.successor._select_from( - path, is_dir, exists, scandir): - yield p - - def except_iter(exc): - return - yield - - for x in _try_except_permissionerror_iter(try_iter, except_iter): - yield x - - -class _WildcardSelector(_Selector): - - def __init__(self, pat, child_parts): - self.pat = re.compile(fnmatch.translate(pat)) - _Selector.__init__(self, child_parts) - - def _select_from(self, parent_path, is_dir, exists, scandir): - def try_iter(): - cf = parent_path._flavour.casefold - entries = list(scandir(parent_path)) - for entry in entries: - if not self.dironly or entry.is_dir(): - name = entry.name - casefolded = cf(name) - if self.pat.match(casefolded): - path = parent_path._make_child_relpath(name) - for p in self.successor._select_from( - path, is_dir, exists, scandir): - yield p - - def except_iter(exc): - return - yield - - for x in _try_except_permissionerror_iter(try_iter, except_iter): - yield x - - -class _RecursiveWildcardSelector(_Selector): - - def __init__(self, pat, child_parts): - _Selector.__init__(self, child_parts) - - def _iterate_directories(self, parent_path, is_dir, scandir): - yield parent_path - - def try_iter(): - entries = list(scandir(parent_path)) - for entry in entries: - entry_is_dir = False - try: - entry_is_dir = entry.is_dir() - except OSError as e: - if not _ignore_error(e): - raise - if entry_is_dir and not entry.is_symlink(): - path = parent_path._make_child_relpath(entry.name) - for p in self._iterate_directories(path, is_dir, scandir): - yield p - - def except_iter(exc): - return - yield - - for x in _try_except_permissionerror_iter(try_iter, except_iter): - yield x - - def _select_from(self, parent_path, is_dir, exists, scandir): - def try_iter(): - yielded = set() - try: - successor_select = self.successor._select_from - for starting_point in self._iterate_directories( - parent_path, is_dir, scandir): - for p in successor_select( - starting_point, is_dir, exists, scandir): - if p not in yielded: - yield p - yielded.add(p) - finally: - yielded.clear() - - def except_iter(exc): - return - yield - - for x in _try_except_permissionerror_iter(try_iter, except_iter): - yield x - - -# -# Public API -# - -class _PathParents(Sequence): - - """This object provides sequence-like access to the logical ancestors - of a path. Don't try to construct it yourself.""" - __slots__ = ('_pathcls', '_drv', '_root', '_parts') - - def __init__(self, path): - # We don't store the instance to avoid reference cycles - self._pathcls = type(path) - self._drv = path._drv - self._root = path._root - self._parts = path._parts - - def __len__(self): - if self._drv or self._root: - return len(self._parts) - 1 - else: - return len(self._parts) - - def __getitem__(self, idx): - if idx < 0 or idx >= len(self): - raise IndexError(idx) - return self._pathcls._from_parsed_parts(self._drv, self._root, - self._parts[:-idx - 1]) - - def __repr__(self): - return "<{0}.parents>".format(self._pathcls.__name__) - - -class PurePath(object): - - """PurePath represents a filesystem path and offers operations which - don't imply any actual filesystem I/O. Depending on your system, - instantiating a PurePath will return either a PurePosixPath or a - PureWindowsPath object. You can also instantiate either of these classes - directly, regardless of your system. - """ - __slots__ = ( - '_drv', '_root', '_parts', - '_str', '_hash', '_pparts', '_cached_cparts', - ) - - def __new__(cls, *args): - """Construct a PurePath from one or several strings and or existing - PurePath objects. The strings and path objects are combined so as - to yield a canonicalized path, which is incorporated into the - new PurePath object. - """ - if cls is PurePath: - cls = PureWindowsPath if os.name == 'nt' else PurePosixPath - return cls._from_parts(args) - - def __reduce__(self): - # Using the parts tuple helps share interned path parts - # when pickling related paths. - return (self.__class__, tuple(self._parts)) - - @classmethod - def _parse_args(cls, args): - # This is useful when you don't want to create an instance, just - # canonicalize some constructor arguments. - parts = [] - for a in args: - if isinstance(a, PurePath): - parts += a._parts - else: - if sys.version_info >= (3, 6): - a = os.fspath(a) - else: - # duck typing for older Python versions - if hasattr(a, "__fspath__"): - a = a.__fspath__() - if isinstance(a, str): - # Force-cast str subclasses to str (issue #21127) - parts.append(str(a)) - # also handle unicode for PY2 (six.text_type = unicode) - elif six.PY2 and isinstance(a, six.text_type): - # cast to str using filesystem encoding - # note: in rare circumstances, on Python < 3.2, - # getfilesystemencoding can return None, in that - # case fall back to ascii - parts.append(a.encode( - sys.getfilesystemencoding() or "ascii")) - else: - raise TypeError( - "argument should be a str object or an os.PathLike " - "object returning str, not %r" - % type(a)) - return cls._flavour.parse_parts(parts) - - @classmethod - def _from_parts(cls, args, init=True): - # We need to call _parse_args on the instance, so as to get the - # right flavour. - self = object.__new__(cls) - drv, root, parts = self._parse_args(args) - self._drv = drv - self._root = root - self._parts = parts - if init: - self._init() - return self - - @classmethod - def _from_parsed_parts(cls, drv, root, parts, init=True): - self = object.__new__(cls) - self._drv = drv - self._root = root - self._parts = parts - if init: - self._init() - return self - - @classmethod - def _format_parsed_parts(cls, drv, root, parts): - if drv or root: - return drv + root + cls._flavour.join(parts[1:]) - else: - return cls._flavour.join(parts) - - def _init(self): - # Overridden in concrete Path - pass - - def _make_child(self, args): - drv, root, parts = self._parse_args(args) - drv, root, parts = self._flavour.join_parsed_parts( - self._drv, self._root, self._parts, drv, root, parts) - return self._from_parsed_parts(drv, root, parts) - - def __str__(self): - """Return the string representation of the path, suitable for - passing to system calls.""" - try: - return self._str - except AttributeError: - self._str = self._format_parsed_parts(self._drv, self._root, - self._parts) or '.' - return self._str - - def __fspath__(self): - return str(self) - - def as_posix(self): - """Return the string representation of the path with forward (/) - slashes.""" - f = self._flavour - return str(self).replace(f.sep, '/') - - def __bytes__(self): - """Return the bytes representation of the path. This is only - recommended to use under Unix.""" - if sys.version_info < (3, 2): - raise NotImplementedError("needs Python 3.2 or later") - return os.fsencode(str(self)) - - def __repr__(self): - return "{0}({1!r})".format(self.__class__.__name__, self.as_posix()) - - def as_uri(self): - """Return the path as a 'file' URI.""" - if not self.is_absolute(): - raise ValueError("relative path can't be expressed as a file URI") - return self._flavour.make_uri(self) - - @property - def _cparts(self): - # Cached casefolded parts, for hashing and comparison - try: - return self._cached_cparts - except AttributeError: - self._cached_cparts = self._flavour.casefold_parts(self._parts) - return self._cached_cparts - - def __eq__(self, other): - if not isinstance(other, PurePath): - return NotImplemented - return ( - self._cparts == other._cparts - and self._flavour is other._flavour) - - def __ne__(self, other): - return not self == other - - def __hash__(self): - try: - return self._hash - except AttributeError: - self._hash = hash(tuple(self._cparts)) - return self._hash - - def __lt__(self, other): - if (not isinstance(other, PurePath) - or self._flavour is not other._flavour): - return NotImplemented - return self._cparts < other._cparts - - def __le__(self, other): - if (not isinstance(other, PurePath) - or self._flavour is not other._flavour): - return NotImplemented - return self._cparts <= other._cparts - - def __gt__(self, other): - if (not isinstance(other, PurePath) - or self._flavour is not other._flavour): - return NotImplemented - return self._cparts > other._cparts - - def __ge__(self, other): - if (not isinstance(other, PurePath) - or self._flavour is not other._flavour): - return NotImplemented - return self._cparts >= other._cparts - - drive = property(attrgetter('_drv'), - doc="""The drive prefix (letter or UNC path), if any.""") - - root = property(attrgetter('_root'), - doc="""The root of the path, if any.""") - - @property - def anchor(self): - """The concatenation of the drive and root, or ''.""" - anchor = self._drv + self._root - return anchor - - @property - def name(self): - """The final path component, if any.""" - parts = self._parts - if len(parts) == (1 if (self._drv or self._root) else 0): - return '' - return parts[-1] - - @property - def suffix(self): - """The final component's last suffix, if any.""" - name = self.name - i = name.rfind('.') - if 0 < i < len(name) - 1: - return name[i:] - else: - return '' - - @property - def suffixes(self): - """A list of the final component's suffixes, if any.""" - name = self.name - if name.endswith('.'): - return [] - name = name.lstrip('.') - return ['.' + suffix for suffix in name.split('.')[1:]] - - @property - def stem(self): - """The final path component, minus its last suffix.""" - name = self.name - i = name.rfind('.') - if 0 < i < len(name) - 1: - return name[:i] - else: - return name - - def with_name(self, name): - """Return a new path with the file name changed.""" - if not self.name: - raise ValueError("%r has an empty name" % (self,)) - drv, root, parts = self._flavour.parse_parts((name,)) - if (not name or name[-1] in [self._flavour.sep, self._flavour.altsep] - or drv or root or len(parts) != 1): - raise ValueError("Invalid name %r" % (name)) - return self._from_parsed_parts(self._drv, self._root, - self._parts[:-1] + [name]) - - def with_suffix(self, suffix): - """Return a new path with the file suffix changed. If the path - has no suffix, add given suffix. If the given suffix is an empty - string, remove the suffix from the path. - """ - # XXX if suffix is None, should the current suffix be removed? - f = self._flavour - if f.sep in suffix or f.altsep and f.altsep in suffix: - raise ValueError("Invalid suffix %r" % (suffix)) - if suffix and not suffix.startswith('.') or suffix == '.': - raise ValueError("Invalid suffix %r" % (suffix)) - name = self.name - if not name: - raise ValueError("%r has an empty name" % (self,)) - old_suffix = self.suffix - if not old_suffix: - name = name + suffix - else: - name = name[:-len(old_suffix)] + suffix - return self._from_parsed_parts(self._drv, self._root, - self._parts[:-1] + [name]) - - def relative_to(self, *other): - """Return the relative path to another path identified by the passed - arguments. If the operation is not possible (because this is not - a subpath of the other path), raise ValueError. - """ - # For the purpose of this method, drive and root are considered - # separate parts, i.e.: - # Path('c:/').relative_to('c:') gives Path('/') - # Path('c:/').relative_to('/') raise ValueError - if not other: - raise TypeError("need at least one argument") - parts = self._parts - drv = self._drv - root = self._root - if root: - abs_parts = [drv, root] + parts[1:] - else: - abs_parts = parts - to_drv, to_root, to_parts = self._parse_args(other) - if to_root: - to_abs_parts = [to_drv, to_root] + to_parts[1:] - else: - to_abs_parts = to_parts - n = len(to_abs_parts) - cf = self._flavour.casefold_parts - if (root or drv) if n == 0 else cf(abs_parts[:n]) != cf(to_abs_parts): - formatted = self._format_parsed_parts(to_drv, to_root, to_parts) - raise ValueError("{0!r} does not start with {1!r}" - .format(str(self), str(formatted))) - return self._from_parsed_parts('', root if n == 1 else '', - abs_parts[n:]) - - @property - def parts(self): - """An object providing sequence-like access to the - components in the filesystem path.""" - # We cache the tuple to avoid building a new one each time .parts - # is accessed. XXX is this necessary? - try: - return self._pparts - except AttributeError: - self._pparts = tuple(self._parts) - return self._pparts - - def joinpath(self, *args): - """Combine this path with one or several arguments, and return a - new path representing either a subpath (if all arguments are relative - paths) or a totally different path (if one of the arguments is - anchored). - """ - return self._make_child(args) - - def __truediv__(self, key): - return self._make_child((key,)) - - def __rtruediv__(self, key): - return self._from_parts([key] + self._parts) - - if six.PY2: - __div__ = __truediv__ - __rdiv__ = __rtruediv__ - - @property - def parent(self): - """The logical parent of the path.""" - drv = self._drv - root = self._root - parts = self._parts - if len(parts) == 1 and (drv or root): - return self - return self._from_parsed_parts(drv, root, parts[:-1]) - - @property - def parents(self): - """A sequence of this path's logical parents.""" - return _PathParents(self) - - def is_absolute(self): - """True if the path is absolute (has both a root and, if applicable, - a drive).""" - if not self._root: - return False - return not self._flavour.has_drv or bool(self._drv) - - def is_reserved(self): - """Return True if the path contains one of the special names reserved - by the system, if any.""" - return self._flavour.is_reserved(self._parts) - - def match(self, path_pattern): - """ - Return True if this path matches the given pattern. - """ - cf = self._flavour.casefold - path_pattern = cf(path_pattern) - drv, root, pat_parts = self._flavour.parse_parts((path_pattern,)) - if not pat_parts: - raise ValueError("empty pattern") - if drv and drv != cf(self._drv): - return False - if root and root != cf(self._root): - return False - parts = self._cparts - if drv or root: - if len(pat_parts) != len(parts): - return False - pat_parts = pat_parts[1:] - elif len(pat_parts) > len(parts): - return False - for part, pat in zip(reversed(parts), reversed(pat_parts)): - if not fnmatch.fnmatchcase(part, pat): - return False - return True - - -# Can't subclass os.PathLike from PurePath and keep the constructor -# optimizations in PurePath._parse_args(). -if sys.version_info >= (3, 6): - os.PathLike.register(PurePath) - - -class PurePosixPath(PurePath): - _flavour = _posix_flavour - __slots__ = () - - -class PureWindowsPath(PurePath): - """PurePath subclass for Windows systems. - - On a Windows system, instantiating a PurePath should return this object. - However, you can also instantiate it directly on any system. - """ - _flavour = _windows_flavour - __slots__ = () - - -# Filesystem-accessing classes - - -class Path(PurePath): - """PurePath subclass that can make system calls. - - Path represents a filesystem path but unlike PurePath, also offers - methods to do system calls on path objects. Depending on your system, - instantiating a Path will return either a PosixPath or a WindowsPath - object. You can also instantiate a PosixPath or WindowsPath directly, - but cannot instantiate a WindowsPath on a POSIX system or vice versa. - """ - __slots__ = ( - '_accessor', - '_closed', - ) - - def __new__(cls, *args, **kwargs): - if cls is Path: - cls = WindowsPath if os.name == 'nt' else PosixPath - self = cls._from_parts(args, init=False) - if not self._flavour.is_supported: - raise NotImplementedError("cannot instantiate %r on your system" - % (cls.__name__,)) - self._init() - return self - - def _init(self, - # Private non-constructor arguments - template=None, - ): - self._closed = False - if template is not None: - self._accessor = template._accessor - else: - self._accessor = _normal_accessor - - def _make_child_relpath(self, part): - # This is an optimization used for dir walking. `part` must be - # a single part relative to this path. - parts = self._parts + [part] - return self._from_parsed_parts(self._drv, self._root, parts) - - def __enter__(self): - if self._closed: - self._raise_closed() - return self - - def __exit__(self, t, v, tb): - self._closed = True - - def _raise_closed(self): - raise ValueError("I/O operation on closed path") - - def _opener(self, name, flags, mode=0o666): - # A stub for the opener argument to built-in open() - return self._accessor.open(self, flags, mode) - - def _raw_open(self, flags, mode=0o777): - """ - Open the file pointed by this path and return a file descriptor, - as os.open() does. - """ - if self._closed: - self._raise_closed() - return self._accessor.open(self, flags, mode) - - # Public API - - @classmethod - def cwd(cls): - """Return a new path pointing to the current working directory - (as returned by os.getcwd()). - """ - return cls(os.getcwd()) - - @classmethod - def home(cls): - """Return a new path pointing to the user's home directory (as - returned by os.path.expanduser('~')). - """ - return cls(cls()._flavour.gethomedir(None)) - - def samefile(self, other_path): - """Return whether other_path is the same or not as this file - (as returned by os.path.samefile()). - """ - if hasattr(os.path, "samestat"): - st = self.stat() - try: - other_st = other_path.stat() - except AttributeError: - other_st = os.stat(other_path) - return os.path.samestat(st, other_st) - else: - filename1 = six.text_type(self) - filename2 = six.text_type(other_path) - st1 = _win32_get_unique_path_id(filename1) - st2 = _win32_get_unique_path_id(filename2) - return st1 == st2 - - def iterdir(self): - """Iterate over the files in this directory. Does not yield any - result for the special paths '.' and '..'. - """ - if self._closed: - self._raise_closed() - for name in self._accessor.listdir(self): - if name in ('.', '..'): - # Yielding a path object for these makes little sense - continue - yield self._make_child_relpath(name) - if self._closed: - self._raise_closed() - - def glob(self, pattern): - """Iterate over this subtree and yield all existing files (of any - kind, including directories) matching the given relative pattern. - """ - if not pattern: - raise ValueError("Unacceptable pattern: {0!r}".format(pattern)) - pattern = self._flavour.casefold(pattern) - drv, root, pattern_parts = self._flavour.parse_parts((pattern,)) - if drv or root: - raise NotImplementedError("Non-relative patterns are unsupported") - selector = _make_selector(tuple(pattern_parts)) - for p in selector.select_from(self): - yield p - - def rglob(self, pattern): - """Recursively yield all existing files (of any kind, including - directories) matching the given relative pattern, anywhere in - this subtree. - """ - pattern = self._flavour.casefold(pattern) - drv, root, pattern_parts = self._flavour.parse_parts((pattern,)) - if drv or root: - raise NotImplementedError("Non-relative patterns are unsupported") - selector = _make_selector(("**",) + tuple(pattern_parts)) - for p in selector.select_from(self): - yield p - - def absolute(self): - """Return an absolute version of this path. This function works - even if the path doesn't point to anything. - - No normalization is done, i.e. all '.' and '..' will be kept along. - Use resolve() to get the canonical path to a file. - """ - # XXX untested yet! - if self._closed: - self._raise_closed() - if self.is_absolute(): - return self - # FIXME this must defer to the specific flavour (and, under Windows, - # use nt._getfullpathname()) - obj = self._from_parts([os.getcwd()] + self._parts, init=False) - obj._init(template=self) - return obj - - def resolve(self, strict=False): - """ - Make the path absolute, resolving all symlinks on the way and also - normalizing it (for example turning slashes into backslashes under - Windows). - """ - if self._closed: - self._raise_closed() - s = self._flavour.resolve(self, strict=strict) - if s is None: - # No symlink resolution => for consistency, raise an error if - # the path is forbidden - # but not raise error if file does not exist (see issue #54). - - def _try_func(): - self.stat() - - def _exc_func(exc): - pass - - _try_except_filenotfounderror(_try_func, _exc_func) - s = str(self.absolute()) - else: - # ensure s is a string (normpath requires this on older python) - s = str(s) - # Now we have no symlinks in the path, it's safe to normalize it. - normed = self._flavour.pathmod.normpath(s) - obj = self._from_parts((normed,), init=False) - obj._init(template=self) - return obj - - def stat(self): - """ - Return the result of the stat() system call on this path, like - os.stat() does. - """ - return self._accessor.stat(self) - - def owner(self): - """ - Return the login name of the file owner. - """ - import pwd - return pwd.getpwuid(self.stat().st_uid).pw_name - - def group(self): - """ - Return the group name of the file gid. - """ - import grp - return grp.getgrgid(self.stat().st_gid).gr_name - - def open(self, mode='r', buffering=-1, encoding=None, - errors=None, newline=None): - """ - Open the file pointed by this path and return a file object, as - the built-in open() function does. - """ - if self._closed: - self._raise_closed() - if sys.version_info >= (3, 3): - return io.open( - str(self), mode, buffering, encoding, errors, newline, - opener=self._opener) - else: - return io.open(str(self), mode, buffering, - encoding, errors, newline) - - def read_bytes(self): - """ - Open the file in bytes mode, read it, and close the file. - """ - with self.open(mode='rb') as f: - return f.read() - - def read_text(self, encoding=None, errors=None): - """ - Open the file in text mode, read it, and close the file. - """ - with self.open(mode='r', encoding=encoding, errors=errors) as f: - return f.read() - - def write_bytes(self, data): - """ - Open the file in bytes mode, write to it, and close the file. - """ - if not isinstance(data, six.binary_type): - raise TypeError( - 'data must be %s, not %s' % - (six.binary_type.__name__, data.__class__.__name__)) - with self.open(mode='wb') as f: - return f.write(data) - - def write_text(self, data, encoding=None, errors=None): - """ - Open the file in text mode, write to it, and close the file. - """ - if not isinstance(data, six.text_type): - raise TypeError( - 'data must be %s, not %s' % - (six.text_type.__name__, data.__class__.__name__)) - with self.open(mode='w', encoding=encoding, errors=errors) as f: - return f.write(data) - - def touch(self, mode=0o666, exist_ok=True): - """ - Create this file with the given access mode, if it doesn't exist. - """ - if self._closed: - self._raise_closed() - if exist_ok: - # First try to bump modification time - # Implementation note: GNU touch uses the UTIME_NOW option of - # the utimensat() / futimens() functions. - try: - self._accessor.utime(self, None) - except OSError: - # Avoid exception chaining - pass - else: - return - flags = os.O_CREAT | os.O_WRONLY - if not exist_ok: - flags |= os.O_EXCL - fd = self._raw_open(flags, mode) - os.close(fd) - - def mkdir(self, mode=0o777, parents=False, exist_ok=False): - """ - Create a new directory at this given path. - """ - if self._closed: - self._raise_closed() - - def _try_func(): - self._accessor.mkdir(self, mode) - - def _exc_func(exc): - if not parents or self.parent == self: - raise exc - self.parent.mkdir(parents=True, exist_ok=True) - self.mkdir(mode, parents=False, exist_ok=exist_ok) - - try: - _try_except_filenotfounderror(_try_func, _exc_func) - except OSError: - # Cannot rely on checking for EEXIST, since the operating system - # could give priority to other errors like EACCES or EROFS - if not exist_ok or not self.is_dir(): - raise - - def chmod(self, mode): - """ - Change the permissions of the path, like os.chmod(). - """ - if self._closed: - self._raise_closed() - self._accessor.chmod(self, mode) - - def lchmod(self, mode): - """ - Like chmod(), except if the path points to a symlink, the symlink's - permissions are changed, rather than its target's. - """ - if self._closed: - self._raise_closed() - self._accessor.lchmod(self, mode) - - def unlink(self): - """ - Remove this file or link. - If the path is a directory, use rmdir() instead. - """ - if self._closed: - self._raise_closed() - self._accessor.unlink(self) - - def rmdir(self): - """ - Remove this directory. The directory must be empty. - """ - if self._closed: - self._raise_closed() - self._accessor.rmdir(self) - - def lstat(self): - """ - Like stat(), except if the path points to a symlink, the symlink's - status information is returned, rather than its target's. - """ - if self._closed: - self._raise_closed() - return self._accessor.lstat(self) - - def rename(self, target): - """ - Rename this path to the given path. - """ - if self._closed: - self._raise_closed() - self._accessor.rename(self, target) - - def replace(self, target): - """ - Rename this path to the given path, clobbering the existing - destination if it exists. - """ - if sys.version_info < (3, 3): - raise NotImplementedError("replace() is only available " - "with Python 3.3 and later") - if self._closed: - self._raise_closed() - self._accessor.replace(self, target) - - def symlink_to(self, target, target_is_directory=False): - """ - Make this path a symlink pointing to the given path. - Note the order of arguments (self, target) is the reverse of - os.symlink's. - """ - if self._closed: - self._raise_closed() - self._accessor.symlink(target, self, target_is_directory) - - # Convenience functions for querying the stat results - - def exists(self): - """ - Whether this path exists. - """ - try: - self.stat() - except OSError as e: - if not _ignore_error(e): - raise - return False - except ValueError: - # Non-encodable path - return False - return True - - def is_dir(self): - """ - Whether this path is a directory. - """ - try: - return S_ISDIR(self.stat().st_mode) - except OSError as e: - if not _ignore_error(e): - raise - # Path doesn't exist or is a broken symlink - # (see https://bitbucket.org/pitrou/pathlib/issue/12/) - return False - except ValueError: - # Non-encodable path - return False - - def is_file(self): - """ - Whether this path is a regular file (also True for symlinks pointing - to regular files). - """ - try: - return S_ISREG(self.stat().st_mode) - except OSError as e: - if not _ignore_error(e): - raise - # Path doesn't exist or is a broken symlink - # (see https://bitbucket.org/pitrou/pathlib/issue/12/) - return False - except ValueError: - # Non-encodable path - return False - - def is_mount(self): - """ - Check if this path is a POSIX mount point - """ - # Need to exist and be a dir - if not self.exists() or not self.is_dir(): - return False - - parent = Path(self.parent) - try: - parent_dev = parent.stat().st_dev - except OSError: - return False - - dev = self.stat().st_dev - if dev != parent_dev: - return True - ino = self.stat().st_ino - parent_ino = parent.stat().st_ino - return ino == parent_ino - - def is_symlink(self): - """ - Whether this path is a symbolic link. - """ - try: - return S_ISLNK(self.lstat().st_mode) - except OSError as e: - if not _ignore_error(e): - raise - # Path doesn't exist - return False - except ValueError: - # Non-encodable path - return False - - def is_block_device(self): - """ - Whether this path is a block device. - """ - try: - return S_ISBLK(self.stat().st_mode) - except OSError as e: - if not _ignore_error(e): - raise - # Path doesn't exist or is a broken symlink - # (see https://bitbucket.org/pitrou/pathlib/issue/12/) - return False - except ValueError: - # Non-encodable path - return False - - def is_char_device(self): - """ - Whether this path is a character device. - """ - try: - return S_ISCHR(self.stat().st_mode) - except OSError as e: - if not _ignore_error(e): - raise - # Path doesn't exist or is a broken symlink - # (see https://bitbucket.org/pitrou/pathlib/issue/12/) - return False - except ValueError: - # Non-encodable path - return False - - def is_fifo(self): - """ - Whether this path is a FIFO. - """ - try: - return S_ISFIFO(self.stat().st_mode) - except OSError as e: - if not _ignore_error(e): - raise - # Path doesn't exist or is a broken symlink - # (see https://bitbucket.org/pitrou/pathlib/issue/12/) - return False - except ValueError: - # Non-encodable path - return False - - def is_socket(self): - """ - Whether this path is a socket. - """ - try: - return S_ISSOCK(self.stat().st_mode) - except OSError as e: - if not _ignore_error(e): - raise - # Path doesn't exist or is a broken symlink - # (see https://bitbucket.org/pitrou/pathlib/issue/12/) - return False - except ValueError: - # Non-encodable path - return False - - def expanduser(self): - """ Return a new path with expanded ~ and ~user constructs - (as returned by os.path.expanduser) - """ - if (not (self._drv or self._root) - and self._parts and self._parts[0][:1] == '~'): - homedir = self._flavour.gethomedir(self._parts[0][1:]) - return self._from_parts([homedir] + self._parts[1:]) - - return self - - -class PosixPath(Path, PurePosixPath): - """Path subclass for non-Windows systems. - - On a POSIX system, instantiating a Path should return this object. - """ - __slots__ = () - - -class WindowsPath(Path, PureWindowsPath): - """Path subclass for Windows systems. - - On a Windows system, instantiating a Path should return this object. - """ - __slots__ = () - - def owner(self): - raise NotImplementedError("Path.owner() is unsupported on this system") - - def group(self): - raise NotImplementedError("Path.group() is unsupported on this system") - - def is_mount(self): - raise NotImplementedError( - "Path.is_mount() is unsupported on this system") diff --git a/castervoice/lib/utilities.py b/castervoice/lib/utilities.py index b6c0cd2c9..fe96b326c 100644 --- a/castervoice/lib/utilities.py +++ b/castervoice/lib/utilities.py @@ -1,41 +1,29 @@ -# -*- coding: utf-8 -*- - -from __future__ import print_function, unicode_literals -from builtins import str import io import json -import six import os +import json import re +import subprocess import sys +import six import time import traceback -import subprocess import webbrowser from locale import getpreferredencoding -from six import binary_type -try: - from urllib import unquote -except ImportError: - from urllib.parse import unquote -import tomlkit - -from dragonfly import Key, Window, get_current_engine +from pathlib import Path +from urllib.parse import unquote +import tomlkit from castervoice.lib.clipboard import Clipboard from castervoice.lib.util import guidance - -if six.PY2: - from castervoice.lib.util.pathlib import Path -else: - from pathlib import Path # pylint: disable=import-error +from dragonfly import Key, Window, get_current_engine 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, printer + from castervoice.lib import printer, settings DARWIN = sys.platform.startswith('darwin') LINUX = sys.platform.startswith('linux') @@ -44,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(): @@ -173,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 = [] @@ -189,7 +175,7 @@ def reboot(): printer.out(popen_parameters) subprocess.Popen(popen_parameters) if engine.name == 'natlink': - import natlinkstatus # pylint: disable=import-error + from natlinkcore import natlinkstatus # pylint: disable=import-error status = natlinkstatus.NatlinkStatus() if status.NatlinkIsEnabled() == 1: # Natlink in-process @@ -200,19 +186,16 @@ 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']) def default_browser_command(): if WIN32: - if six.PY2: - 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 - HKEY_CURRENT_USER, OpenKey, QueryValueEx) + from winreg import ( # pylint: disable=import-error,no-name-in-module + HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, CloseKey, + ConnectRegistry, OpenKey, QueryValueEx) ''' Tries to get default browser command, returns either a space delimited command string with '%1' as URL placeholder, or empty string. @@ -245,11 +228,11 @@ 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': - import natlinkstatus # pylint: disable=import-error + from natlinkcore import natlinkstatus # pylint: disable=import-error status = natlinkstatus.NatlinkStatus() if status.NatlinkIsEnabled() == 1: import win32gui # pylint: disable=import-error @@ -284,7 +267,7 @@ def get_clipboard_formats(): stdin=subprocess.PIPE, ) for line in iter(p.stdout.readline, b''): - if isinstance(line, binary_type): + if isinstance(line, str): line = line.decode(encoding) formats.append(line.strip()) except Exception as e: @@ -336,7 +319,7 @@ def enum_files_from_clipboard(target): stdin=subprocess.PIPE, ) for line in iter(p.stdout.readline, b''): - if isinstance(line, binary_type): + if isinstance(line, str): line = line.decode(encoding).strip() if line.startswith("file://"): line = line.replace("file://", "") diff --git a/castervoice/rules/ccr/recording_rules/bringme.py b/castervoice/rules/ccr/recording_rules/bringme.py index 5159306da..32213be09 100644 --- a/castervoice/rules/ccr/recording_rules/bringme.py +++ b/castervoice/rules/ccr/recording_rules/bringme.py @@ -5,11 +5,7 @@ from subprocess import Popen import re -import six -if six.PY2: - from castervoice.lib.util.pathlib import Path -else: - from pathlib import Path # pylint: disable=import-error +from pathlib import Path # pylint: disable=import-error from dragonfly import Function, Choice, Dictation, ContextAction from castervoice.lib.context import AppContext 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"), } diff --git a/docs/readthedocs/Installation/Windows/Dragon_NaturallySpeaking.md b/docs/readthedocs/Installation/Windows/Dragon_NaturallySpeaking.md index 0fafe2e39..e6df9320a 100644 --- a/docs/readthedocs/Installation/Windows/Dragon_NaturallySpeaking.md +++ b/docs/readthedocs/Installation/Windows/Dragon_NaturallySpeaking.md @@ -1,40 +1,19 @@ -# Dragon NaturallySpeaking- Classic Install +# Dragon NaturallySpeaking Install -**Install Dragon NaturallySpeaking** (DPI / DNS)- Caster only supports Dragon NaturallySpeaking 13 and Windows 7 or higher. +**Install Dragon NaturallySpeaking** (DPI / DNS)- Caster only supports Dragon NaturallySpeaking v13-v15 and Windows 7 or higher. -After installing Dragon Naturally Speaking, you can configure the DNS settings based on your preference. +After installing Dragon, you can configure the DNS settings based on your preference. - Disabling the DNS Browser plug-ins due to instability of Internet browsers and DNS is recommended. - Disable all the checkboxes in the “Correction” menu, except for the “Automatically add words to the active vocabulary” option. - Under the “Commands” menu check the “Require click to select…” checkboxes. Otherwise you will find yourself accidentally clicking buttons or menu items instead of inserting text into your editor. I’ve disabled the other checkboxes in that menu as well. - Set the “speed versus accuracy” slider in the “Miscellaneous” menu to a fairly high value. -- Uncheck the “Use the dictation box for unsupported applications” checkbox. Use Caster text manipulation instead. +- Uncheck the “Use the dictation box for unsupported applications” checkbox. -## Python +### Python -1. Download and install [Python v2.7.18 32-bit](https://www.python.org/downloads/release/python-2718/) listed as `Windows x86 MSI installer` not Python 3 or the Python 2.7 64-bit. +1. Download and install [**Python 3.8.X 32 bit**](https://www.python.org/downloads/release/python-3810/) as `Windows x86 MSI installer` and select **add Python to Path**. - - These dependencies will change when Natlink utilizes Python 3. - - Install [Microsoft Visual C++ Compiler for Python 2.7](https://web.archive.org/web/20190720195601/http://www.microsoft.com/en-us/download/confirmation.aspx?id=44266) from web.archive.org mirror - -2. Make sure to select `Add python to path`. - - - This can be done manually by searching for "edit environment variables for your account" and adding your Python folder to the list of Path values - -## NatLink - -- Download and install [Natlink](https://sourceforge.net/projects/natlink/files/natlink/natlink4.2/). Use `Natlink-4.2` or newer. - - 1. Verify (DPI / DNS) is not running. - - 2. Open the start menu and search for `Configure NatLink` and click `Configure NatLink via GUI`. - ![Configure start](https://mathfly.org/images/configure_start.png) - - 3. Register Natlink and Restart your computer. - ![Natlink-Setup1.jpg](https://i.postimg.cc/3wdKsJFS/Natlink-Setup1.jpg) - - 4. Relaunch `Configure NatLink via GUI`. Then disable Natlink. Done with Natlink setup. - ![Natlink-Setup2.jpg](https://i.postimg.cc/j20TGHMv/Natlink-Setup2.jpg) ### Caster @@ -43,64 +22,38 @@ After installing Dragon Naturally Speaking, you can configure the DNS settings b 3. Copy the contents of `Caster-master` folder. You can put it anywhere but it is common to use `%USERPROFILE%\Documents\Caster`. 4. Install dependencies and set up Natlink by running `Caster/Install_Caster_DNS-WSR.bat`. - - *Note: For this to work correctly, Python must be installed to `C:/Python27` - - Optional Step** for Caster's `Legion` MouseGrid- Legion Feature available on Windows 8 and above. + - Optional Step** for Caster's `Legion` MouseGrid- Legion Feature available on Windows 8 and above. -6. The Legion MouseGrid requires [Microsoft Visual C++ Redistributable Packages for Visual Studio 2015, 2017 and 2019 (x86).](https://support.microsoft.com/en-nz/help/2977003/the-latest-supported-visual-c-downloads) Note: Should not be needed if Windows 10 is up-to-date. +5. The Legion MouseGrid requires [Microsoft Visual C++ Redistributable Packages for Visual Studio 2015, 2017 and 2019 (x86).](https://support.microsoft.com/en-nz/help/2977003/the-latest-supported-visual-c-downloads) Note: Should not be needed if Windows 10 is up-to-date. -### **Setup and launch DNS for Classic Install.** +### NatLink Install via CLI -> 1. Start or restart Dragon. `Click Run_Caster_DNS.bat` Status Window appear and load Caster. Once loaded Caster commands should be available to dictate. -> 2. To test this, open Window's Notepad and try saying `arch brov char delta` producing `abcd` text. +1. Close Dragon if open +2. Open Command Prompt/PowerShell **as administrator** +3. Upgrade pip: `pip install --upgrade pip` +4. `pip install natlink` from [PyPI](https://pypi.org/project/natlink/) +6. `natlinkconfig_cli` # should auto setup and register itself. +7. (Optional) type `u` to see all CLI options +8. Set Natlink UserDirectory to Caster: type `n C:\Users\Your-User-Name\Documents\Caster` +9. Restart Dragon and the "Messages from Natlink" window should start with Dragon. ### Update Caster -> 1. Backup `%USERPROFILE%\Documents\Caster` -> 2. Delete `%USERPROFILE%\Documents\Caster` -> 3. Repeat Steps `1.- 4.` within the Caster install section - ------- - -### -Alternative- Natlink Configuration - -An alternative to the instructions above for configuring Natlink. Automatically launches Caster when DNS starts. The disadvantage of this method is when Caser restarts so does DNS. Open the start menu and search for `natlink` and click the file called `Configure NatLink via GUI`. - -1. Open Configure Natlink - - ![Configure start](https://mathfly.org/images/configure_start.png) -> Ensure that the details of your DNS setup are correct in the “info” tab. - -2. In the "configure" tab Register Natlink and Restart your computer. - - ![Caster-Natlink.jpg](https://i.postimg.cc/d1jN4xcw/Caster-Natlink.jpg) -3. Relaunch the GUI. In the “configure” tab- under “NatLink” and “UserDirectory”- click enable. When you are prompted for a folder, give it the folder - - `C:\Users\\Documents\Caster` - -**Caster Troubleshooting FAQ** - -1. Error during install `regex` package: `error: Microsoft Visual C++ 9.0 is required. Get it from http://aka.ms/vcpython27` - - Documented ["Microsoft Compilers for Python 2.7" is no longer supported by Microsoft](https://github.com/dictation-toolbox/Caster/issues/890) - - - Fix: Install [Microsoft Visual C++ Compiler for Python 2.7](https://web.archive.org/web/20190720195601/http://www.microsoft.com/en-us/download/confirmation.aspx?id=44266) from web.archive.org mirror: [source](https://stackoverflow.com/questions/43645519/microsoft-visual-c-9-0-is-required) +1. Backup `%USERPROFILE%\Documents\Caster` +2. Delete `%USERPROFILE%\Documents\Caster` +3. Repeat Steps `1.- 4.` within the Caster install section ### Natlink Troubleshooting FAQ -1. Visual C++ Runtime Error R6034 on Dragon launch. This is related to Natlink. You can safely ignore it. - - A Fix: "Core directory of NatLink (...\NatLink\MacroSystem\core) there is a directory msvcr100fix. Please consult the NatLink README.txt file. - - See if copying the dll file (msvcr100.dll) to the Core directory (one step up) solves your problem." - - Note: Not recommended for Windows 10. - - A dated discussion [VoiceCoder](https://groups.yahoo.com/neo/groups/VoiceCoder/conversations/topics/7925) on the issue. -2. When using `start_configurenatlink.py` gives `ImportError: No module named six"` or `ImportError: No module named future"` - - To fix pip Install `pip install six` or `pip install dragonfly2` in CMD -3. Cannot load compatibility module support `(GUID = {dd990001-bb89-1d2-b031-0060088dc929}))` +3. Cannot load compatibility module support `(GUID = {dd990001-bb89-1d2-b031-0060088dc929}))` aka **natlink.pyd** - - [Detailed Instructions](https://qh.antenna.nl/unimacro/installation/problemswithinstallation.html) Typically fixed by installing Microsoft Visual C++ 2010 Service Pack 1 Redistributable Package - - May need to unRegister and then reRegister Natlink from the GUI + - Verify **Python 3.8.X 32-bit** is installed and 32bit **python is on path** + - Verify Dragon NaturallySpeaking version is v13-v15 +2. _On non-administrator accounts_: + - You may need to manually delete **natlink.pyd** as administrator after closing the CLI + - Running terminal as administrator changes the user account causing a mismatch between user directories between administrator/non-administrator. This impacts where your settings are stored for natlink. + - Fix:- [Create an OS environment variable](https://phoenixnap.com/kb/windows-set-environment-variable) **DICTATIONTOOLBOXUSER** pointing to a directory to store `.natlink`. -4. Running "Configure NatLink via GUI" does not bring up the settings window- try running the program as an administrator: - 1. A Fix: Open an administrator command prompt by searching for "cmd" in start and right click run as administrator. - 2. Change directory to the folder where start_configurenatlink.py was installed. See command below: - 3. `cd C:\NatLink\NatLink\confignatlinkvocolaunimacro`. - 4. Run `python start_configurenatlink.py`. +### -Alternative Install - Natlink via GUI -See [qh.antenna troubleshooting guide](https://qh.antenna.nl/unimacro/installation/problemswithinstallation.html) has further solutions for NatLink Issues. \ No newline at end of file +The program **natlinkconfig**, being the GUI version, can be launched from a PowerShell running as in elevated mode (Admin Privileges). After the `natlink.pyd` file has been registered and Natlink is enabled, further configuration can be done. Note: unfortunately, Vocola and Unimacro cannot be enabled for the time being. \ No newline at end of file diff --git a/post_setup.py b/post_setup.py index 70441669f..bb2156207 100644 --- a/post_setup.py +++ b/post_setup.py @@ -26,7 +26,7 @@ def _copy_log(self): # Returns directory path for caster and engine "wrs" or "dns" def _find_directory(self): try: - import natlinkstatus + from natlinkcore import natlinkstatus # pylint: disable=import-error status = natlinkstatus.NatlinkStatus() directory = status.getUserDirectory() return directory, "dns" # NatLink MacroSystem Directory diff --git a/tests/lib/test_migration.py b/tests/lib/test_migration.py index 589999388..2734367a7 100644 --- a/tests/lib/test_migration.py +++ b/tests/lib/test_migration.py @@ -1,6 +1,6 @@ import shutil -import six +from pathlib import Path from castervoice.lib.ctrl.mgr.loading.load.content_root import ContentRoot from castervoice.lib.migration import UserDirUpdater @@ -8,10 +8,6 @@ from castervoice.lib.merge.selfmod.sm_config import SelfModStateSavingConfig from tests.test_util.settings_mocking import SettingsEnabledTestCase -if six.PY2: - from castervoice.lib.util.pathlib import Path -else: - from pathlib import Path # pylint: disable=import-error _INIT_PY = "__init__.py" diff --git a/tests/lib/util/test_biDiGraph.py b/tests/lib/util/test_biDiGraph.py index 6e15c0386..742608ad7 100644 --- a/tests/lib/util/test_biDiGraph.py +++ b/tests/lib/util/test_biDiGraph.py @@ -1,5 +1,4 @@ from unittest import TestCase -import six from castervoice.lib.util.bidi_graph import BiDiGraph @@ -9,11 +8,7 @@ class TestBiDiGraph(TestCase): # https://bugs.python.org/issue17866 @property def itemsAreEqual(self): - if six.PY2: - return self.assertItemsEqual - else: - return self.assertCountEqual - + return self.assertCountEqual def test_get_node_1(self): """ diff --git a/tests/testrunner.py b/tests/testrunner.py index 0633d8e48..d0a1bf499 100644 --- a/tests/testrunner.py +++ b/tests/testrunner.py @@ -1,11 +1,6 @@ import os import sys import unittest -import six - -if six.PY2: - import logging - logging.basicConfig() if os.path.dirname(os.path.dirname(os.path.abspath(__file__))) not in sys.path: sys.path.insert(0,os.path.dirname(os.path.dirname(os.path.abspath(__file__))))