diff --git a/src/cthulhu/plugins/IndentationAudio/plugin.py b/src/cthulhu/plugins/IndentationAudio/plugin.py index 1669f7f..f3c92ab 100644 --- a/src/cthulhu/plugins/IndentationAudio/plugin.py +++ b/src/cthulhu/plugins/IndentationAudio/plugin.py @@ -340,6 +340,10 @@ class IndentationAudio(Plugin): def _on_caret_moved(self, event): """Handle caret movement events.""" try: + # Check if plugin is activated first (prevents race conditions during deactivation) + if not self._activated or not self._enabled: + return + if not self._beeps_enabled(): return @@ -674,11 +678,15 @@ class IndentationAudio(Plugin): def check_indentation_change(self, obj, line_text): """Check if indentation has changed and play audio cue if needed. - + This method is intended to be called by scripts during line navigation. """ debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: check_indentation_change called: line='{line_text}'", True) + # Check if plugin is activated and enabled + if not self._activated or not self._enabled: + return + if not line_text or not self._beeps_enabled(): return diff --git a/src/cthulhu/plugins/nvda2cthulhu/README.md b/src/cthulhu/plugins/nvda2cthulhu/README.md index 79654da..6a5f0ad 100644 --- a/src/cthulhu/plugins/nvda2cthulhu/README.md +++ b/src/cthulhu/plugins/nvda2cthulhu/README.md @@ -20,3 +20,5 @@ will not start the server. if set; otherwise it defaults to 3457. - Toggle interrupt/no-interrupt mode with cthulhu+shift+n. - Toggle translation with cthulhu+control+shift+t (requires translate-shell). +- Translation defaults to your system locale; override with `NVDA2CTHULHU_TRANSLATE_TARGET` + (e.g., `NVDA2CTHULHU_TRANSLATE_TARGET=en`). diff --git a/src/cthulhu/plugins/nvda2cthulhu/__init__.py b/src/cthulhu/plugins/nvda2cthulhu/__init__.py index 778b2a3..294e379 100644 --- a/src/cthulhu/plugins/nvda2cthulhu/__init__.py +++ b/src/cthulhu/plugins/nvda2cthulhu/__init__.py @@ -1 +1,5 @@ """NVDA to Cthulhu bridge plugin.""" + +from .plugin import Nvda2Cthulhu + +__all__ = ['Nvda2Cthulhu'] diff --git a/src/cthulhu/plugins/nvda2cthulhu/plugin.py b/src/cthulhu/plugins/nvda2cthulhu/plugin.py index 8e13dad..7fc34c5 100644 --- a/src/cthulhu/plugins/nvda2cthulhu/plugin.py +++ b/src/cthulhu/plugins/nvda2cthulhu/plugin.py @@ -21,6 +21,7 @@ """NVDA to Cthulhu bridge plugin.""" import asyncio +import locale import logging import os import shutil @@ -46,6 +47,7 @@ except Exception: # pragma: no cover - optional dependency from cthulhu.plugin import Plugin, cthulhu_hookimpl from cthulhu import braille +from cthulhu import debug from cthulhu import speech from cthulhu import settings_manager @@ -90,20 +92,29 @@ class Nvda2Cthulhu(Plugin): """Bridge server that accepts NVDA to Cthulhu messages.""" def __init__(self): - super().__init__() - self.settingsManager = settings_manager.getManager() - self.interruptEnabled = True - self.serverThread = None - self.serverLock = threading.Lock() - self.httpServer = None - self.ioLoop = None - self.asyncioLoop = None - self.translationCache = OrderedDict() - self.translationCacheLock = threading.Lock() + logger.info("NVDA to Cthulhu __init__() called") + try: + super().__init__() + logger.info("NVDA to Cthulhu super().__init__() completed") + self.settingsManager = settings_manager.getManager() + self.interruptEnabled = True + self.serverThread = None + self.serverLock = threading.Lock() + self.httpServer = None + self.ioLoop = None + self.asyncioLoop = None + self.translationCache = OrderedDict() + self.translationCacheLock = threading.Lock() + logger.info("NVDA to Cthulhu __init__() completed successfully") + except Exception as e: + logger.error(f"NVDA to Cthulhu __init__() failed: {e}") + raise @cthulhu_hookimpl def activate(self, plugin=None): + logger.info(f"NVDA to Cthulhu activate() called with plugin={plugin}, self={self}") if plugin is not None and plugin is not self: + logger.info(f"NVDA to Cthulhu activate() early return: plugin is not self") return logger.info("Activating NVDA to Cthulhu plugin") @@ -300,7 +311,10 @@ class Nvda2Cthulhu(Plugin): if not text or not text.strip(): return if self._translation_enabled(): - text = self._translate_text(text) + logger.info(f"NVDA to Cthulhu: translating text: {text[:50]}") + translated = self._translate_text(text) + logger.info(f"NVDA to Cthulhu: translated to: {translated[:50]}") + text = translated speech.speak(text, interrupt=self.interruptEnabled) def _handle_braille(self, text): @@ -345,36 +359,81 @@ class Nvda2Cthulhu(Plugin): def _translation_command_available(self): return shutil.which(TRANSLATE_COMMAND[0]) is not None + def _get_target_language(self): + """Get the target language code from system locale.""" + try: + # Get system locale (e.g., 'en_US.UTF-8') + system_locale = locale.getdefaultlocale()[0] + if system_locale: + # Extract language code (e.g., 'en' from 'en_US') + lang_code = system_locale.split('_')[0] + return lang_code + except Exception as exc: + logger.warning(f"NVDA to Cthulhu: failed to get system locale: {exc}") + + # Fallback to environment variable + try: + lang_env = os.environ.get('LANG', '') + if lang_env: + # Extract language code from LANG (e.g., 'en' from 'en_US.UTF-8') + lang_code = lang_env.split('_')[0] + if lang_code: + return lang_code + except Exception: + pass + + return None + + def _get_translate_target(self): + env_target = os.environ.get("NVDA2CTHULHU_TRANSLATE_TARGET") + if env_target: + return env_target + return self._get_target_language() + def _translate_text(self, text): cached = self._get_cached_translation(text) if cached is not None: + logger.info(f"NVDA to Cthulhu: using cached translation") return cached if not self._translation_command_available(): logger.warning("NVDA to Cthulhu translation failed: translate-shell not available") return text + + translate_target = self._get_translate_target() + command = list(TRANSLATE_COMMAND) + if translate_target: + command.append(f":{translate_target}") + + logger.info(f"NVDA to Cthulhu: running trans command on: {text[:50]}") try: result = subprocess.run( - TRANSLATE_COMMAND, + command, input=text, text=True, + encoding="utf-8", + errors="replace", capture_output=True, check=False, timeout=TRANSLATE_TIMEOUT ) except subprocess.TimeoutExpired: logger.warning("NVDA to Cthulhu translation failed: timed out") + debug.printMessage(debug.LEVEL_INFO, "NVDA to Cthulhu translation failed: timed out", True) return text except Exception as exc: logger.warning(f"NVDA to Cthulhu translation failed: {exc}") + debug.printMessage(debug.LEVEL_INFO, f"NVDA to Cthulhu translation failed: {exc}", True) return text if result.returncode != 0: stderr = result.stderr.strip() if stderr: logger.warning(f"NVDA to Cthulhu translation failed: {stderr}") + debug.printMessage(debug.LEVEL_INFO, f"NVDA to Cthulhu translation failed: {stderr}", True) return text output = result.stdout.strip() + logger.info(f"NVDA to Cthulhu: trans output: {output[:50]}") if not output: return text self._set_cached_translation(text, output) diff --git a/src/cthulhu/settings.py b/src/cthulhu/settings.py index d912296..85f6afd 100644 --- a/src/cthulhu/settings.py +++ b/src/cthulhu/settings.py @@ -494,7 +494,20 @@ presentChatRoomLast = False presentLiveRegionFromInactiveTab = False # Plugins -activePlugins = ['DisplayVersion', 'OCR', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu', 'WindowTitleReader'] +activePlugins = [ + 'PluginManager', + 'ByeCthulhu', + 'Clipboard', + 'HelloCthulhu', + 'DisplayVersion', + 'GameMode', + 'IndentationAudio', + 'nvda2cthulhu', + 'OCR', + 'SpeechHistory', + 'SSIPProxy', + 'WindowTitleReader' +] pluginSources = [] # AI Assistant settings (disabled by default for opt-in behavior)