diff --git a/Toby Doom Launcher.py b/Toby Doom Launcher.py index 4b6ad59..b308abb 100755 --- a/Toby Doom Launcher.py +++ b/Toby Doom Launcher.py @@ -49,6 +49,62 @@ from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QHBoxLayout, from PySide6.QtCore import Qt, QTimer import webbrowser +class OrcaRemoteController: + """D-Bus interface for Orca screen reader remote control""" + + def __init__(self): + self.service_name = "org.gnome.Orca.Service" + self.main_path = "/org/gnome/Orca/Service" + self.available = self._test_availability() + + def _test_availability(self): + """Test if Orca remote controller is available""" + try: + result = subprocess.run([ + "gdbus", "call", "--session", + "--dest", self.service_name, + "--object-path", self.main_path, + "--method", "org.gnome.Orca.Service.ListCommands" + ], check=True, capture_output=True, text=True, timeout=2) + return True + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + return False + + def present_message(self, message): + """Present a message via Orca speech/braille output""" + if not self.available: + return False + + try: + result = subprocess.run([ + "gdbus", "call", "--session", + "--dest", self.service_name, + "--object-path", self.main_path, + "--method", "org.gnome.Orca.Service.PresentMessage", + message + ], check=True, capture_output=True, text=True, timeout=2) + return "true" in result.stdout.lower() + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + return False + + def interrupt_speech(self): + """Interrupt current speech via SpeechAndVerbosityManager""" + if not self.available: + return False + + try: + result = subprocess.run([ + "gdbus", "call", "--session", + "--dest", self.service_name, + "--object-path", f"{self.main_path}/SpeechAndVerbosityManager", + "--method", "org.gnome.Orca.Module.ExecuteCommand", + "InterruptSpeech", "false" + ], check=True, capture_output=True, text=True, timeout=2) + return "true" in result.stdout.lower() + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + return False + + # Initialize speech provider based on platform if platform.system() == "Windows": # Set up DLL paths for Windows @@ -70,23 +126,28 @@ if platform.system() == "Windows": print(f"Failed to initialize accessible_output2: {e}") sys.exit() else: - # Linux/Mac path - try: - output = subprocess.check_output(["pgrep", "cthulhu"]) - speechProvider = "cthulhu" - except (subprocess.CalledProcessError, FileNotFoundError): + # Linux/Mac path - prioritize Orca remote controller + orca_remote = OrcaRemoteController() + if orca_remote.available: + speechProvider = "orca_remote" + orca = orca_remote + else: try: - import accessible_output2.outputs.auto - s = accessible_output2.outputs.auto.Auto() - speechProvider = "accessible_output2" - except ImportError as e: + output = subprocess.check_output(["pgrep", "cthulhu"]) + speechProvider = "cthulhu" + except (subprocess.CalledProcessError, FileNotFoundError): try: - import speechd - spd = speechd.Client() - speechProvider = "speechd" - except ImportError: - print("No speech providers found.") - sys.exit() + import accessible_output2.outputs.auto + s = accessible_output2.outputs.auto.Auto() + speechProvider = "accessible_output2" + except ImportError as e: + try: + import speechd + spd = speechd.Client() + speechProvider = "speechd" + except ImportError: + print("No speech providers found.") + sys.exit() class AccessibleComboBox(QComboBox): @@ -205,7 +266,12 @@ class SpeechHandler: if not text or not self.use_tts: return - if speechProvider == "speechd": + if speechProvider == "orca_remote": + # Try Orca first, interrupt current speech then present message + orca.interrupt_speech() + if not orca.present_message(text): + print(f"Orca remote error - message not delivered", file=sys.stderr) + elif speechProvider == "speechd": spd.cancel() spd.speak(text) elif speechProvider == "accessible_output2":