diff --git a/src/cthulhu/plugins/nvda2cthulhu/plugin.py b/src/cthulhu/plugins/nvda2cthulhu/plugin.py index f551ff6..a2c89e5 100644 --- a/src/cthulhu/plugins/nvda2cthulhu/plugin.py +++ b/src/cthulhu/plugins/nvda2cthulhu/plugin.py @@ -29,6 +29,9 @@ from collections import OrderedDict import subprocess import threading import urllib.parse +from typing import Any, Callable + +from gi.repository import GLib try: import msgpack @@ -202,17 +205,27 @@ class Nvda2Cthulhu(Plugin): return True def handle_message(self, message): - request = self._parse_request(message) + try: + request = self._parse_request(message) + except Exception as exc: + logger.warning(f"NVDA to Cthulhu: failed to parse message: {exc}") + debug.printException(debug.LEVEL_WARNING) + return + if not request: return - requestType, payload = request - if requestType == "SpeakText": - self._handle_speak(payload) - elif requestType == "BrailleText": - self._handle_braille(payload) - elif requestType == "CancelSpeech": - speech.stop() + try: + requestType, payload = request + if requestType == "SpeakText": + self._handle_speak(payload) + elif requestType == "BrailleText": + self._schedule_on_main_thread(self._handle_braille, payload) + elif requestType == "CancelSpeech": + self._schedule_on_main_thread(self._handle_cancel_speech) + except Exception as exc: + logger.warning(f"NVDA to Cthulhu: failed to handle message: {exc}") + debug.printException(debug.LEVEL_WARNING) def _server_main(self): if not self._dependencies_available(): @@ -234,7 +247,7 @@ class Nvda2Cthulhu(Plugin): self.ioLoop.start() except Exception as exc: logger.error(f"NVDA to Cthulhu failed to start server: {exc}") - self._present_message("NVDA to Cthulhu server failed to start") + self._schedule_on_main_thread(self._present_message, "NVDA to Cthulhu server failed to start") finally: self.httpServer = None if self.ioLoop: @@ -261,7 +274,7 @@ class Nvda2Cthulhu(Plugin): if not self._dependencies_available(): return None if isinstance(message, str): - return "SpeakText", message + return self._parse_payload(message) if not isinstance(message, (bytes, bytearray)): return None @@ -318,7 +331,13 @@ class Nvda2Cthulhu(Plugin): translated = self._translate_text(text) logger.info(f"NVDA to Cthulhu: translated to: {translated[:50]}") text = translated - speech.speak(text, interrupt=self.interruptEnabled) + self._schedule_on_main_thread(self._speak_text, text, self.interruptEnabled) + + def _speak_text(self, text: str, interrupt: bool) -> None: + speech.speak(text, interrupt=interrupt) + + def _handle_cancel_speech(self) -> None: + speech.stop() def _handle_braille(self, text): if not text or not text.strip(): @@ -350,6 +369,23 @@ class Nvda2Cthulhu(Plugin): except Exception: logger.info(message) + def _schedule_on_main_thread(self, callback: Callable[..., Any], *args: Any) -> bool: + try: + GLib.idle_add(self._run_on_main_thread, callback, args) + return True + except Exception as exc: + logger.warning(f"NVDA to Cthulhu: failed to queue main-loop callback: {exc}") + debug.printException(debug.LEVEL_WARNING) + return False + + def _run_on_main_thread(self, callback: Callable[..., Any], args: tuple[Any, ...]) -> bool: + try: + callback(*args) + except Exception as exc: + logger.warning(f"NVDA to Cthulhu: main-loop callback failed: {exc}") + debug.printException(debug.LEVEL_WARNING) + return False + def _dependencies_available(self): return msgpack is not None and tornado is not None