Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 17 additions & 18 deletions castervoice/lib/ctrl/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@

@author: synkarius
'''
import os, sys, time, pkg_resources
from pkg_resources import VersionConflict, DistributionNotFound
import os, sys, time
from importlib.metadata import version, PackageNotFoundError
from packaging.version import Version
from castervoice.lib import printer

DARWIN = sys.platform == "darwin"
Expand All @@ -13,10 +14,8 @@
def install_type():
# Checks if Caster install is Classic or PIP.
try:
pkg_resources.require("castervoice")
except VersionConflict:
pass
except DistributionNotFound:
version("castervoice")
except PackageNotFoundError:
return "classic"
return "pip"

Expand All @@ -39,11 +38,9 @@ def dep_missing():
for dep in requirements:
dep = dep.split(">=", 1)[0]
try:
pkg_resources.require("{}".format(dep))
except VersionConflict:
pass
except DistributionNotFound:
missing_list.append('{0}'.format(dep))
version(dep)
Comment on lines 39 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize requirement names before calling metadata lookup

dep_missing() now passes raw requirement strings to importlib.metadata.version(), but only strips ">=" first. Entries like pillow==9.5.0 in requirements.txt and dragonfly2[kaldi] in requirements-mac-linux.txt are not valid distribution names for version(), so they are always treated as missing even when installed. In classic installs this causes persistent false missing-dependency warnings (and the 10-second sleep) on every startup.

Useful? React with 👍 / 👎.

except PackageNotFoundError:
missing_list.append(dep)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version specifier not stripped from package name lookup

High Severity

The dep_missing() function only splits on >= (dep.split(">=", 1)[0]) but requirements.txt also contains == pinned packages like pillow==9.5.0 and pyvda==0.0.8. The old pkg_resources.require() understood version specifiers natively, but importlib.metadata.version() expects a bare package name. Passing "pillow==9.5.0" to version() always raises PackageNotFoundError, causing these installed packages to be falsely reported as missing on every startup (with a 10-second sleep).

Fix in Cursor Fix in Web

if missing_list:
pippackages = (' '.join(map(str, missing_list)))
printer.out("\nCaster: dependencys are missing. Use 'python -m pip install {0}'".format(pippackages))
Expand All @@ -60,18 +57,20 @@ def dep_min_version():
for dep in listdependency:
package = dep[0]
operator = dep[1]
version = dep[2]
req_version = dep[2]
issue_url = dep[3]
try:
pkg_resources.require('{0} {1} {2}'.format(package, operator, version))
except VersionConflict as e:
if operator == ">=":
installed = Version(version(package))
required = Version(req_version)
if operator == ">=" and installed < required:
if issue_url is not None:
printer.out("\nCaster: Requires {0} v{1} or greater.\nIssue reference: {2}".format(package, version, issue_url))
printer.out("\nCaster: Requires {0} v{1} or greater.\nIssue reference: {2}".format(package, req_version, issue_url))
printer.out("Update with: 'python -m pip install {} --upgrade' \n".format(package))
if operator == "==":
elif operator == "==" and installed != required:
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))
printer.out("Install with: 'python -m pip install {0}=={1}' \n".format(package, req_version))
except PackageNotFoundError:
pass


class DependencyMan:
Expand Down
3 changes: 2 additions & 1 deletion castervoice/lib/ctrl/mgr/engine_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ def __init__(self, ExclusiveManager):
self.engine_state = None
self.previous_engine_state = None
self.mic_state = None
self.engine = get_current_engine().name
engine = get_current_engine()
self.engine = engine.name if engine is not None else "text"
self._exclusive_manager = ExclusiveManager

# Remove "normal" and "off" from 'states' for non-DNS based engines.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ def __init__(self):
def get_pronunciation(self):
return "formatting"

def run(self, event_content):
pronunciation = event_content["pronunciation"]
_apply_format(pronunciation)
def run(self, event):
if event.active:
_apply_format(event.rule_class_name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Format hook uses class name instead of pronunciation

High Severity

_apply_format(event.rule_class_name) passes the Python class name (e.g., "CPP", "CSharp", "SQL", "Matlab") to _apply_format, but settings.SETTINGS["formats"] is keyed by pronunciation strings (e.g., "C plus plus", "C sharp", "sequel", "matlab"). For any rule where the class name differs from the pronunciation, the lookup silently fails and text formatting is cleared instead of being applied.

Fix in Cursor Fix in Web

else:
textformat.format.clear_text_format()
textformat.secondary_format.clear_text_format()


def get_hook():
Expand Down
23 changes: 22 additions & 1 deletion castervoice/rules/core/navigation_rules/window_mgmt_rule.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
from dragonfly import MappingRule, Function, Repeat, ShortIntegerRef
from dragonfly import DictListRef, Function, MappingRule, Repeat, Repetition, ShortIntegerRef

from castervoice.lib import utilities
from castervoice.lib import virtual_desktops
from castervoice.lib.actions import Key
from castervoice.lib.ctrl.mgr.rule_details import RuleDetails
from castervoice.lib.merge.state.short import R

try: # Try first loading from caster user directory
from navigation_rules.window_mgmt_rule_support import ( # pylint: disable=import-error
debug_window_switching, open_windows_dictlist, switch_window, timerinstance,
)
except ImportError:
from castervoice.rules.core.navigation_rules.window_mgmt_rule_support import (
debug_window_switching,
open_windows_dictlist,
switch_window,
timerinstance,
)


class WindowManagementRule(MappingRule):
mapping = {
Expand All @@ -17,6 +29,10 @@ class WindowManagementRule(MappingRule):
R(Function(utilities.restore_window)),
'window close':
R(Function(utilities.close_window)),
"window switch <windows>":
R(Function(switch_window), rdescript=""),
"window switch show":
R(Function(debug_window_switching)),

# Workspace management
"show work [spaces]":
Expand All @@ -42,9 +58,14 @@ class WindowManagementRule(MappingRule):

extras = [
ShortIntegerRef("n", 1, 20, default=1),
Repetition(name="windows", min=1, max=5,
child=DictListRef("window_by_keyword", open_windows_dictlist)),
]


timerinstance.set()


def get_rule():
details = RuleDetails(name="window management rule")
return WindowManagementRule, details
149 changes: 149 additions & 0 deletions castervoice/rules/core/navigation_rules/window_mgmt_rule_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# All credit goes to caspark
# This is adapted from caspark's grammar at https://gist.github.com/caspark/9c2c5e2853a14b6e28e9aa4f121164a6

from __future__ import print_function

import re
import time

import six
from dragonfly import DictList, Window, get_current_engine, get_engine

from castervoice.lib.util import recognition_history

_history = recognition_history.get_and_register_history(1)

open_windows_dictlist = DictList("open_windows")

WORD_SPLITTER = re.compile('[^a-zA-Z0-9]+')


def get_caster_messaging_window():
if get_current_engine().name == 'natlink':
from natlinkcore import natlinkstatus # pylint: disable=import-error
status = natlinkstatus.NatlinkStatus()
if status.NatlinkIsEnabled() == 1:
return "Messages from Natlink"
return "Caster: Status Window"


def lower_if_not_abbreviation(s):
if len(s) <= 4 and s.upper() == s:
return s
else:
return s.lower()


def find_window(window_matcher_func, timeout_ms=3000):
"""
Returns a Window matching the given matcher function, or raises an error otherwise
"""
steps = int(timeout_ms / 100)
for i in range(steps):
for win in Window.get_all_windows():
if window_matcher_func(win):
return win
time.sleep(0.1)
raise ValueError(
"no matching window found within {} ms".format(timeout_ms))


def refresh_open_windows_dictlist():
"""
Refreshes `open_windows_dictlist`
"""
window_options = {}
for window in (x for x in Window.get_all_windows() if
x.is_valid and
x.is_enabled and
x.is_visible and
not x.executable.startswith("C:\\Windows") and
x.classname != "DgnResultsBoxWindow"):
for word in {lower_if_not_abbreviation(word)
for word
in WORD_SPLITTER.split(window.title)
if len(word)}:
if word in window_options:
window_options[word] += [window]
else:
window_options[word] = [window]

window_options = {k: v for k,
v in six.iteritems(window_options) if v is not None}
open_windows_dictlist.set(window_options)


def debug_window_switching():
"""
Prints out contents of `open_windows_dictlist`
"""
options = open_windows_dictlist.copy()
print("*** Windows known:\n",
"\n".join(sorted({w.title for list_of_windows in six.itervalues(options)
for w in list_of_windows})))

print("*** Single word switching options:\n", "\n".join(
"{}: '{}'".format(
k.ljust(20), "', '".join(window.title for window in options[k])
) for k in sorted(six.iterkeys(options)) if len(options[k]) == 1))
print("*** Ambiguous switching options:\n", "\n".join(
"{}: '{}'".format(
k.ljust(20), "', '".join(window.title for window in options[k])
) for k in sorted(six.iterkeys(options)) if len(options[k]) > 1))


def switch_window(windows):
"""
Matches keywords to window titles stored in `open_windows_dictlist`
"""
matched_window_handles = {w.handle: w for w in windows[0]}
for window_options in windows[1:]:
matched_window_handles = {
w.handle: w for w in window_options if w.handle in matched_window_handles}
if six.PY2:
matched_windows = matched_window_handles.values()
else:
matched_windows = list(matched_window_handles.values())
if len(matched_windows) == 1:
window = matched_windows[0]
print("Window Management: Switching to", window.title)
window.set_foreground()
else:
try:
messaging_title = get_caster_messaging_window()
messaging_window = find_window(
lambda w: messaging_title in w.title, timeout_ms=100)
if messaging_window.is_minimized:
messaging_window.restore()
else:
messaging_window.set_foreground()
except ValueError:
pass
if len(matched_windows) >= 2:
print("Ambiguous window switch command:\n", "\n".join(
"'{}' from {} (handle: {})".format(w.title, w.executable, w.handle)
for w in matched_windows))
else:
spec_n_word = 2
words = list(map(str, _history[0]))
del words[:spec_n_word]
print("Window Management: No matching window title containing keywords: `{}`".
format(' '.join(map(str, words))))


class Timer:
"""
Dragonfly timer runs every 2 seconds updating open_windows_dictlist
"""
timer = None

def __init__(self):
pass

def set(self):
if self.timer is None:
self.timer = get_engine().create_timer(refresh_open_windows_dictlist, 2)
self.timer.start()


timerinstance = Timer()
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from dragonfly import get_engine

get_engine("text")
3 changes: 2 additions & 1 deletion tests/lib/ctrl/test_EngineModesManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ def set_mode(self, mode, modetype):


class TestEngineModesManager(TestCase):
_manager = EngineModesManager(mockExclusiveManager())
def setUp(self):
self._manager = EngineModesManager(mockExclusiveManager())

def test_set_engine_mode(self):
self._manager.set_engine_mode(mode="numbers", state=True)
Expand Down
Loading