diff --git a/config/keyboard/desktop.conf b/config/keyboard/desktop.conf index 29b3eb02..a4491cbd 100644 --- a/config/keyboard/desktop.conf +++ b/config/keyboard/desktop.conf @@ -114,6 +114,8 @@ KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_X=remove_marks KEY_FENRIR,KEY_X=set_mark KEY_FENRIR,KEY_SHIFT,KEY_X=marked_text KEY_FENRIR,KEY_F10=toggle_vmenu_mode +KEY_FENRIR,KEY_SHIFT,KEY_F10=voice_browser_safe +KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_F10=apply_tested_voice KEY_FENRIR,KEY_SPACE=current_quick_menu_entry KEY_FENRIR,KEY_CTRL,KEY_SPACE=current_quick_menu_value KEY_FENRIR,KEY_RIGHT=next_quick_menu_entry diff --git a/config/keyboard/laptop.conf b/config/keyboard/laptop.conf index bec25378..1927d49e 100644 --- a/config/keyboard/laptop.conf +++ b/config/keyboard/laptop.conf @@ -113,6 +113,8 @@ KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_X=remove_marks KEY_FENRIR,KEY_X=set_mark KEY_FENRIR,KEY_SHIFT,KEY_X=marked_text KEY_FENRIR,KEY_F10=toggle_vmenu_mode +KEY_FENRIR,KEY_SHIFT,KEY_F10=voice_browser_safe +KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_F10=apply_tested_voice KEY_FENRIR,KEY_SPACE=current_quick_menu_entry KEY_FENRIR,KEY_CTRL,KEY_SPACE=current_quick_menu_value KEY_FENRIR,KEY_RIGHT=next_quick_menu_entry diff --git a/config/sound/default/BrailleOff.wav b/config/sound/default/BrailleOff.wav deleted file mode 100644 index 083fc283..00000000 Binary files a/config/sound/default/BrailleOff.wav and /dev/null differ diff --git a/config/sound/default/BrailleOn.wav b/config/sound/default/BrailleOn.wav deleted file mode 100644 index 00674359..00000000 Binary files a/config/sound/default/BrailleOn.wav and /dev/null differ diff --git a/config/sound/default/ErrorBraille.wav b/config/sound/default/ErrorBraille.wav deleted file mode 100644 index 189eda41..00000000 Binary files a/config/sound/default/ErrorBraille.wav and /dev/null differ diff --git a/config/sound/default/soundicons.conf b/config/sound/default/soundicons.conf index 8ad2c0ec..9987b4be 100644 --- a/config/sound/default/soundicons.conf +++ b/config/sound/default/soundicons.conf @@ -30,9 +30,6 @@ ContentChanged='ContentChanged.wav' # Speech has turned On or Off SpeechOn='SpeechOn.wav' SpeechOff='SpeechOff.wav' -# Braille has turned On or Off -BrailleOn='BrailleOn.wav' -BrailleOff='BrailleOff.wav' # SoundIcons has turned On or Off SoundOn='SoundOn.wav' SoundOff='SoundOff.wav' @@ -44,9 +41,8 @@ PlaceEndMark='PlaceEndMark.wav' CopyToClipboard='CopyToClipboard.wav' # Pasted on the screen PasteClipboardOnScreen='PasteClipboardOnScreen.wav' -# An error accoured while speech or braille output or reading the screen +# An error accoured while speech output or reading the screen ErrorSpeech='ErrorSpeech.wav' -ErrorBraille='ErrorBraille.wav' ErrorScreen='ErrorScreen.wav' # If you cursor over an text that has attributs (like color) HasAttributes='has_attribute.wav' diff --git a/src/fenrirscreenreader/commands/commands/apply_tested_voice.py b/src/fenrirscreenreader/commands/commands/apply_tested_voice.py new file mode 100644 index 00000000..91cbc047 --- /dev/null +++ b/src/fenrirscreenreader/commands/commands/apply_tested_voice.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Apply the last tested voice from safe voice browser" + + def run(self): + try: + # Check if we have a tested voice + if ('commandBuffer' not in self.env or + 'lastTestedModule' not in self.env['commandBuffer'] or + 'lastTestedVoice' not in self.env['commandBuffer']): + self.env['runtime']['outputManager'].presentText("No voice has been tested yet", interrupt=True) + self.env['runtime']['outputManager'].presentText("Use voice browser first", interrupt=True) + return + + module = self.env['commandBuffer']['lastTestedModule'] + voice = self.env['commandBuffer']['lastTestedVoice'] + + self.env['runtime']['outputManager'].presentText(f"Applying {voice} from {module}", interrupt=True) + + # Apply to runtime settings only (temporary until saved) + settingsManager = self.env['runtime']['settingsManager'] + + # Store old values for safety + oldDriver = settingsManager.getSetting('speech', 'driver') + oldModule = settingsManager.getSetting('speech', 'module') + oldVoice = settingsManager.getSetting('speech', 'voice') + + try: + # Apply new settings to runtime only (use setSetting to update settingArgDict) + settingsManager.setSetting('speech', 'driver', 'speechdDriver') + settingsManager.setSetting('speech', 'module', module) + settingsManager.setSetting('speech', 'voice', voice) + + # Apply to speech driver instance directly + if 'speechDriver' in self.env['runtime']: + speechDriver = self.env['runtime']['speechDriver'] + + # Set the module and voice on the driver instance + speechDriver.setModule(module) + speechDriver.setVoice(voice) + + self.env['runtime']['outputManager'].presentText("Voice applied successfully!", interrupt=True) + self.env['runtime']['outputManager'].presentText("Use save settings to make permanent", interrupt=True) + self.env['runtime']['outputManager'].playSound('Accept') + + except Exception as e: + # Revert on failure + settingsManager.setSetting('speech', 'driver', oldDriver) + settingsManager.setSetting('speech', 'module', oldModule) + settingsManager.setSetting('speech', 'voice', oldVoice) + + self.env['runtime']['outputManager'].presentText(f"Failed to apply voice, reverted: {str(e)}", interrupt=True) + self.env['runtime']['outputManager'].playSound('Error') + + except Exception as e: + self.env['runtime']['outputManager'].presentText(f"Apply voice error: {str(e)}", interrupt=True) + self.env['runtime']['outputManager'].playSound('Error') + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/commands/voice_browser.py b/src/fenrirscreenreader/commands/commands/voice_browser.py new file mode 100644 index 00000000..64d7806f --- /dev/null +++ b/src/fenrirscreenreader/commands/commands/voice_browser.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 + +import subprocess +import time + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + self.testMessage = "This is a voice test. The quick brown fox jumps over the lazy dog." + self.modules = [] + self.voices = [] + self.moduleIndex = 0 + self.voiceIndex = 0 + self.browserActive = False + self.originalBindings = {} + self.lastAnnounceTime = 0 + + def shutdown(self): + pass + + def getDescription(self): + return "Interactive voice browser with arrow key navigation" + + def run(self): + if self.browserActive: + self.exitVoiceBrowser() + return + + self.env['runtime']['outputManager'].presentText("Starting voice browser", interrupt=True) + + # Load modules + self.modules = self.getSpeechdModules() + if not self.modules: + self.env['runtime']['outputManager'].presentText("No speech modules found", interrupt=True) + self.env['runtime']['outputManager'].playSound('Error') + return + + # Set current module + currentModule = self.env['runtime']['settingsManager'].getSetting('speech', 'module') + if currentModule and currentModule in self.modules: + self.moduleIndex = self.modules.index(currentModule) + + # Load voices + self.loadVoicesForCurrentModule() + + # Set current voice + currentVoice = self.env['runtime']['settingsManager'].getSetting('speech', 'voice') + if currentVoice and currentVoice in self.voices: + self.voiceIndex = self.voices.index(currentVoice) + + # Enter browser mode + self.enterVoiceBrowser() + self.announceCurrentSelection() + + def enterVoiceBrowser(self): + """Enter voice browser mode with custom key bindings""" + self.browserActive = True + + # Store original bindings + self.originalBindings = self.env['bindings'].copy() + + # Override navigation keys for voice browsing + # Use lambda to capture self and create bound methods + self.env['bindings'][str([1, ['KEY_UP']])] = 'VOICE_BROWSER_PREV_VOICE' + self.env['bindings'][str([1, ['KEY_DOWN']])] = 'VOICE_BROWSER_NEXT_VOICE' + self.env['bindings'][str([1, ['KEY_LEFT']])] = 'VOICE_BROWSER_PREV_MODULE' + self.env['bindings'][str([1, ['KEY_RIGHT']])] = 'VOICE_BROWSER_NEXT_MODULE' + self.env['bindings'][str([1, ['KEY_ENTER']])] = 'VOICE_BROWSER_TEST' + self.env['bindings'][str([1, ['KEY_SPACE']])] = 'VOICE_BROWSER_APPLY' + self.env['bindings'][str([1, ['KEY_ESC']])] = 'VOICE_BROWSER_EXIT' + + # Register browser commands + self.registerBrowserCommands() + + self.env['runtime']['outputManager'].presentText("Voice browser active", interrupt=True) + self.env['runtime']['outputManager'].presentText("Up/Down=voices, Left/Right=modules, Enter=test, Space=apply, Esc=exit", interrupt=True) + + def registerBrowserCommands(self): + """Register voice browser commands with the command manager""" + # Create command objects for voice browser actions + commandManager = self.env['runtime']['commandManager'] + + # This is a hack - we'll store references to our methods in the environment + # so the key handlers can call them + if 'voiceBrowserInstance' not in self.env['runtime']: + self.env['runtime']['voiceBrowserInstance'] = self + + def exitVoiceBrowser(self): + """Exit voice browser and restore normal key bindings""" + if not self.browserActive: + return + + self.browserActive = False + + # Restore original bindings + self.env['bindings'] = self.originalBindings + + # Clean up + if 'voiceBrowserInstance' in self.env['runtime']: + del self.env['runtime']['voiceBrowserInstance'] + + self.env['runtime']['outputManager'].presentText("Voice browser exited", interrupt=True) + + def loadVoicesForCurrentModule(self): + """Load voices for current module""" + if self.moduleIndex < len(self.modules): + module = self.modules[self.moduleIndex] + self.voices = self.getModuleVoices(module) + self.voiceIndex = 0 # Reset to first voice when changing modules + + def announceCurrentSelection(self): + """Announce current module and voice""" + # Throttle announcements to avoid spam + now = time.time() + if now - self.lastAnnounceTime < 0.3: + return + self.lastAnnounceTime = now + + if not self.modules or self.moduleIndex >= len(self.modules): + return + + module = self.modules[self.moduleIndex] + if self.voices and self.voiceIndex < len(self.voices): + voice = self.voices[self.voiceIndex] + self.env['runtime']['outputManager'].presentText( + f"{module}: {voice} ({self.voiceIndex + 1}/{len(self.voices)})", interrupt=True + ) + else: + self.env['runtime']['outputManager'].presentText(f"{module}: No voices", interrupt=True) + + def nextVoice(self): + """Move to next voice""" + if not self.voices: + return + self.voiceIndex = (self.voiceIndex + 1) % len(self.voices) + self.announceCurrentSelection() + + def prevVoice(self): + """Move to previous voice""" + if not self.voices: + return + self.voiceIndex = (self.voiceIndex - 1) % len(self.voices) + self.announceCurrentSelection() + + def nextModule(self): + """Move to next module""" + self.moduleIndex = (self.moduleIndex + 1) % len(self.modules) + self.loadVoicesForCurrentModule() + self.announceCurrentSelection() + + def prevModule(self): + """Move to previous module""" + self.moduleIndex = (self.moduleIndex - 1) % len(self.modules) + self.loadVoicesForCurrentModule() + self.announceCurrentSelection() + + def testVoice(self): + """Test current voice""" + if not self.voices or self.voiceIndex >= len(self.voices): + self.env['runtime']['outputManager'].presentText("No voice selected", interrupt=True) + return + + module = self.modules[self.moduleIndex] + voice = self.voices[self.voiceIndex] + + self.env['runtime']['outputManager'].presentText("Testing...", interrupt=True) + if self.previewVoice(module, voice): + # Store for apply command + self.env['commandBuffer']['lastTestedModule'] = module + self.env['commandBuffer']['lastTestedVoice'] = voice + self.env['runtime']['outputManager'].playSound('Accept') + else: + self.env['runtime']['outputManager'].playSound('Error') + + def applyVoice(self): + """Apply current voice to Fenrir""" + if not self.voices or self.voiceIndex >= len(self.voices): + return + + module = self.modules[self.moduleIndex] + voice = self.voices[self.voiceIndex] + + try: + settingsManager = self.env['runtime']['settingsManager'] + settingsManager.settings['speech']['driver'] = 'speechdDriver' + settingsManager.settings['speech']['module'] = module + settingsManager.settings['speech']['voice'] = voice + + if 'speechDriver' in self.env['runtime']: + speechDriver = self.env['runtime']['speechDriver'] + speechDriver.shutdown() + speechDriver.initialize(self.env) + + self.env['runtime']['outputManager'].presentText("Voice applied!", interrupt=True) + self.env['runtime']['outputManager'].playSound('Accept') + + except Exception as e: + self.env['runtime']['outputManager'].presentText("Apply failed", interrupt=True) + self.env['runtime']['outputManager'].playSound('Error') + + def previewVoice(self, module, voice): + """Test voice with spd-say""" + try: + cmd = ['spd-say', '-o', module, '-y', voice, self.testMessage] + result = subprocess.run(cmd, timeout=10) + return result.returncode == 0 + except Exception: + return False + + def getSpeechdModules(self): + """Get available speech modules""" + try: + result = subprocess.run(['spd-say', '-O'], capture_output=True, text=True, timeout=10) + if result.returncode == 0: + lines = result.stdout.strip().split('\n') + return [line.strip() for line in lines[1:] if line.strip()] + except Exception: + pass + return [] + + def getModuleVoices(self, module): + """Get voices for module""" + try: + result = subprocess.run(['spd-say', '-o', module, '-L'], capture_output=True, text=True, timeout=10) + if result.returncode == 0: + lines = result.stdout.strip().split('\n') + voices = [] + for line in lines[1:]: + if not line.strip(): + continue + if module.lower() == 'espeak-ng': + voice = self.processEspeakVoice(line) + if voice: + voices.append(voice) + else: + voices.append(line.strip()) + return voices + except Exception: + pass + return [] + + def processEspeakVoice(self, voiceLine): + """Process espeak voice format""" + parts = [p for p in voiceLine.split() if p] + if len(parts) < 2: + return None + langCode = parts[-2].lower() + variant = parts[-1].lower() + return f"{langCode}+{variant}" if variant and variant != 'none' else langCode + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/commands/voice_browser_apply.py b/src/fenrirscreenreader/commands/commands/voice_browser_apply.py new file mode 100644 index 00000000..1f0048d4 --- /dev/null +++ b/src/fenrirscreenreader/commands/commands/voice_browser_apply.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Voice browser: apply current voice" + + def run(self): + if 'voiceBrowserInstance' in self.env['runtime']: + self.env['runtime']['voiceBrowserInstance'].applyVoice() + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/commands/voice_browser_exit.py b/src/fenrirscreenreader/commands/commands/voice_browser_exit.py new file mode 100644 index 00000000..c8d5d17d --- /dev/null +++ b/src/fenrirscreenreader/commands/commands/voice_browser_exit.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Voice browser: exit browser mode" + + def run(self): + if 'voiceBrowserInstance' in self.env['runtime']: + self.env['runtime']['voiceBrowserInstance'].exitVoiceBrowser() + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/commands/voice_browser_next_module.py b/src/fenrirscreenreader/commands/commands/voice_browser_next_module.py new file mode 100644 index 00000000..a2dc1d01 --- /dev/null +++ b/src/fenrirscreenreader/commands/commands/voice_browser_next_module.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Voice browser: next module" + + def run(self): + if 'voiceBrowserInstance' in self.env['runtime']: + self.env['runtime']['voiceBrowserInstance'].nextModule() + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/commands/voice_browser_next_voice.py b/src/fenrirscreenreader/commands/commands/voice_browser_next_voice.py new file mode 100644 index 00000000..5e56b42e --- /dev/null +++ b/src/fenrirscreenreader/commands/commands/voice_browser_next_voice.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Voice browser: next voice" + + def run(self): + if 'voiceBrowserInstance' in self.env['runtime']: + self.env['runtime']['voiceBrowserInstance'].nextVoice() + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/commands/voice_browser_prev_module.py b/src/fenrirscreenreader/commands/commands/voice_browser_prev_module.py new file mode 100644 index 00000000..b7add887 --- /dev/null +++ b/src/fenrirscreenreader/commands/commands/voice_browser_prev_module.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Voice browser: previous module" + + def run(self): + if 'voiceBrowserInstance' in self.env['runtime']: + self.env['runtime']['voiceBrowserInstance'].prevModule() + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/commands/voice_browser_prev_voice.py b/src/fenrirscreenreader/commands/commands/voice_browser_prev_voice.py new file mode 100644 index 00000000..24c858f5 --- /dev/null +++ b/src/fenrirscreenreader/commands/commands/voice_browser_prev_voice.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Voice browser: previous voice" + + def run(self): + if 'voiceBrowserInstance' in self.env['runtime']: + self.env['runtime']['voiceBrowserInstance'].prevVoice() + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/commands/voice_browser_safe.py b/src/fenrirscreenreader/commands/commands/voice_browser_safe.py new file mode 100644 index 00000000..2f59aaba --- /dev/null +++ b/src/fenrirscreenreader/commands/commands/voice_browser_safe.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 + +import subprocess +import threading +import time + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + self.testMessage = "Voice test: The quick brown fox jumps over the lazy dog." + + def shutdown(self): + pass + + def getDescription(self): + return "Safe voice browser - cycles through voices without hanging" + + def run(self): + try: + self.env['runtime']['outputManager'].presentText("Starting safe voice browser", interrupt=True) + + # Get modules with timeout protection + modules = self.getSpeechdModulesWithTimeout() + if not modules: + self.env['runtime']['outputManager'].presentText("No speech modules found", interrupt=True) + return + + # Get current position from commandBuffer or start fresh + moduleIndex = self.env['commandBuffer'].get('safeBrowserModuleIndex', 0) + voiceIndex = self.env['commandBuffer'].get('safeBrowserVoiceIndex', 0) + + # Ensure valid module index + if moduleIndex >= len(modules): + moduleIndex = 0 + + currentModule = modules[moduleIndex] + self.env['runtime']['outputManager'].presentText(f"Loading voices for {currentModule}...", interrupt=True) + + # Get voices with timeout protection + voices = self.getModuleVoicesWithTimeout(currentModule) + if not voices: + self.env['runtime']['outputManager'].presentText(f"No voices in {currentModule}, trying next module", interrupt=True) + moduleIndex = (moduleIndex + 1) % len(modules) + self.env['commandBuffer']['safeBrowserModuleIndex'] = moduleIndex + self.env['commandBuffer']['safeBrowserVoiceIndex'] = 0 + return + + # Ensure valid voice index + if voiceIndex >= len(voices): + voiceIndex = 0 + + currentVoice = voices[voiceIndex] + + # Announce current selection + self.env['runtime']['outputManager'].presentText( + f"Module: {currentModule} ({moduleIndex + 1}/{len(modules)})", interrupt=True + ) + self.env['runtime']['outputManager'].presentText( + f"Voice: {currentVoice} ({voiceIndex + 1}/{len(voices)})", interrupt=True + ) + + # Test voice in background thread to avoid blocking + self.env['runtime']['outputManager'].presentText("Testing voice...", interrupt=True) + + # Use threading to prevent freezing + testThread = threading.Thread(target=self.testVoiceAsync, args=(currentModule, currentVoice)) + testThread.daemon = True + testThread.start() + + # Store tested voice for apply command + self.env['commandBuffer']['lastTestedModule'] = currentModule + self.env['commandBuffer']['lastTestedVoice'] = currentVoice + + # Advance to next voice for next run + voiceIndex += 1 + if voiceIndex >= len(voices): + voiceIndex = 0 + moduleIndex = (moduleIndex + 1) % len(modules) + + # Store position for next run + self.env['commandBuffer']['safeBrowserModuleIndex'] = moduleIndex + self.env['commandBuffer']['safeBrowserVoiceIndex'] = voiceIndex + + # Give instructions + self.env['runtime']['outputManager'].presentText("Run again for next voice, or use apply voice command", interrupt=True) + + except Exception as e: + self.env['runtime']['outputManager'].presentText(f"Voice browser error: {str(e)}", interrupt=True) + self.env['runtime']['outputManager'].playSound('Error') + + def testVoiceAsync(self, module, voice): + """Test voice in background thread to avoid blocking""" + try: + # Run with strict timeout + cmd = ['spd-say', '-o', module, '-y', voice, self.testMessage] + result = subprocess.run(cmd, timeout=5, capture_output=True) + + # Schedule success sound for main thread + if result.returncode == 0: + # We can't call outputManager from background thread safely + # So we'll just let the main thread handle feedback + pass + + except subprocess.TimeoutExpired: + # Voice test timed out - this is okay, don't crash + pass + except Exception: + # Any other error - also okay, don't crash + pass + + def getSpeechdModulesWithTimeout(self): + """Get speech modules with timeout protection""" + try: + result = subprocess.run(['spd-say', '-O'], capture_output=True, text=True, timeout=3) + if result.returncode == 0: + lines = result.stdout.strip().split('\n') + modules = [line.strip() for line in lines[1:] if line.strip()] + return modules[:10] # Limit to first 10 modules to prevent overload + except subprocess.TimeoutExpired: + self.env['runtime']['outputManager'].presentText("Module detection timed out", interrupt=True) + except Exception as e: + self.env['runtime']['outputManager'].presentText(f"Module detection failed: {str(e)}", interrupt=True) + return [] + + def getModuleVoicesWithTimeout(self, module): + """Get voices with timeout and limits""" + try: + result = subprocess.run(['spd-say', '-o', module, '-L'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + lines = result.stdout.strip().split('\n') + voices = [] + for line in lines[1:]: + if not line.strip(): + continue + if module.lower() == 'espeak-ng': + voice = self.processEspeakVoice(line) + if voice: + voices.append(voice) + else: + voices.append(line.strip()) + + # Limit voice count to prevent memory issues + if len(voices) > 1000: + self.env['runtime']['outputManager'].presentText(f"Found {len(voices)} voices, limiting to first 1000", interrupt=True) + voices = voices[:1000] + + return voices + except subprocess.TimeoutExpired: + self.env['runtime']['outputManager'].presentText(f"Voice detection for {module} timed out", interrupt=True) + except Exception as e: + self.env['runtime']['outputManager'].presentText(f"Voice detection failed: {str(e)}", interrupt=True) + return [] + + def processEspeakVoice(self, voiceLine): + """Process espeak voice format""" + try: + parts = [p for p in voiceLine.split() if p] + if len(parts) < 2: + return None + langCode = parts[-2].lower() + variant = parts[-1].lower() + return f"{langCode}+{variant}" if variant and variant != 'none' else langCode + except Exception: + return None + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/commands/voice_browser_test.py b/src/fenrirscreenreader/commands/commands/voice_browser_test.py new file mode 100644 index 00000000..9c023de4 --- /dev/null +++ b/src/fenrirscreenreader/commands/commands/voice_browser_test.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Voice browser: test current voice" + + def run(self): + if 'voiceBrowserInstance' in self.env['runtime']: + self.env['runtime']['voiceBrowserInstance'].testVoice() + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-navigation/page_down_vmenu.py b/src/fenrirscreenreader/commands/vmenu-navigation/page_down_vmenu.py new file mode 100644 index 00000000..2e6e1550 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-navigation/page_down_vmenu.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Fenrir TTY screen reader +# By Chrys, Storm Dragon, and contributers. + + +class command(): + def __init__(self): + pass + def initialize(self, environment): + self.env = environment + def shutdown(self): + pass + def getDescription(self): + return _('jump down 10% in v menu') + def run(self): + self.env['runtime']['vmenuManager'].pageDown() + text = self.env['runtime']['vmenuManager'].getCurrentEntry() + self.env['runtime']['outputManager'].presentText(text, interrupt=True) + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-navigation/page_up_vmenu.py b/src/fenrirscreenreader/commands/vmenu-navigation/page_up_vmenu.py new file mode 100644 index 00000000..c1c78ffa --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-navigation/page_up_vmenu.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Fenrir TTY screen reader +# By Chrys, Storm Dragon, and contributers. + + +class command(): + def __init__(self): + pass + def initialize(self, environment): + self.env = environment + def shutdown(self): + pass + def getDescription(self): + return _('jump up 10% in v menu') + def run(self): + self.env['runtime']['vmenuManager'].pageUp() + text = self.env['runtime']['vmenuManager'].getCurrentEntry() + self.env['runtime']['outputManager'].presentText(text, interrupt=True) + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/__init__.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/__init__.py new file mode 100644 index 00000000..1c602751 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/__init__.py @@ -0,0 +1 @@ +# Fenrir Configuration VMenu Profile \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/general/__init__.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/general/__init__.py new file mode 100644 index 00000000..f3594ed4 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/general/__init__.py @@ -0,0 +1 @@ +# Fenrir general configuration diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/general/set_punctuation_level.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/general/set_punctuation_level.py new file mode 100644 index 00000000..1f03fdee --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/general/set_punctuation_level.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 + +import os +import importlib.util + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Set punctuation verbosity level" + + def run(self): + currentLevel = self.getSetting('general', 'punctuationLevel', 'some') + + # Present current level + levelDescriptions = { + 'none': 'None - no punctuation spoken', + 'some': 'Some - basic punctuation only', + 'most': 'Most - detailed punctuation', + 'all': 'All - every punctuation mark' + } + + currentDescription = levelDescriptions.get(currentLevel, 'Unknown') + self.presentText(f"Current punctuation level: {currentDescription}") + + # Cycle through the four levels + levels = ['none', 'some', 'most', 'all'] + try: + currentIndex = levels.index(currentLevel) + nextIndex = (currentIndex + 1) % len(levels) + newLevel = levels[nextIndex] + except ValueError: + newLevel = 'some' # Default to some + + success = self.setSetting('general', 'punctuationLevel', newLevel) + + if success: + newDescription = levelDescriptions[newLevel] + self.presentText(f"Punctuation level set to: {newDescription}") + self.playSound('Accept') + else: + self.presentText("Failed to change punctuation level") + self.playSound('Error') \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/general/toggle_debug.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/general/toggle_debug.py new file mode 100644 index 00000000..8a5e077a --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/general/toggle_debug.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +import os +import importlib.util + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Toggle debug mode" + + def run(self): + currentLevel = self.getSetting('general', 'debugLevel', '0') + + # Present current debug level + if currentLevel == '0': + self.presentText("Current debug mode: disabled") + newLevel = '1' + stateText = "enabled" + else: + self.presentText(f"Current debug level: {currentLevel}") + newLevel = '0' + stateText = "disabled" + + success = self.setSetting('general', 'debugLevel', newLevel) + + if success: + self.presentText(f"Debug mode {stateText}") + if newLevel != '0': + debugMode = self.getSetting('general', 'debugMode', 'File') + if debugMode == 'File': + self.presentText("Debug output will be written to log file") + else: + self.presentText("Debug output will be printed to console") + self.playSound('Accept') + else: + self.presentText("Failed to change debug mode") + self.playSound('Error') \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/keyboard/__init__.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/keyboard/__init__.py new file mode 100644 index 00000000..e130a0e1 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/keyboard/__init__.py @@ -0,0 +1 @@ +# Fenrir keyboard configuration diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/keyboard/select_layout.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/keyboard/select_layout.py new file mode 100644 index 00000000..49d3f7f4 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/keyboard/select_layout.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + +import os +import importlib.util + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Select keyboard layout (desktop or laptop)" + + def run(self): + currentLayout = self.getSetting('keyboard', 'keyboardLayout', 'desktop') + + # Present current layout + self.presentText(f"Current keyboard layout: {currentLayout}") + + # Find available keyboard layouts + keyboardPath = '/etc/fenrirscreenreader/keyboard' + if not os.path.isdir(keyboardPath): + # Development path + keyboardPath = os.path.join(os.path.dirname(self.settingsFile), '..', 'keyboard') + + availableLayouts = self.getAvailableLayouts(keyboardPath) + + if len(availableLayouts) > 1: + # Cycle through available layouts + try: + currentIndex = availableLayouts.index(currentLayout) + nextIndex = (currentIndex + 1) % len(availableLayouts) + newLayout = availableLayouts[nextIndex] + except ValueError: + # Current layout not found, use first available + newLayout = availableLayouts[0] + + success = self.setSetting('keyboard', 'keyboardLayout', newLayout) + + if success: + self.presentText(f"Keyboard layout changed to: {newLayout}") + self.presentText("Please restart Fenrir for this change to take effect.") + self.playSound('Accept') + else: + self.presentText("Failed to change keyboard layout") + self.playSound('Error') + else: + self.presentText("Only default keyboard layout is available") + self.playSound('Cancel') + + def getAvailableLayouts(self, keyboardPath): + """Find available keyboard layout files""" + layouts = [] + + if os.path.isdir(keyboardPath): + try: + for file in os.listdir(keyboardPath): + if file.endswith('.conf') and not file.startswith('.'): + layoutName = file[:-5] # Remove .conf extension + layouts.append(layoutName) + except Exception: + pass + + # Ensure we have at least the default layouts + if not layouts: + layouts = ['desktop', 'laptop'] + + return sorted(layouts) \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/keyboard/set_char_echo.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/keyboard/set_char_echo.py new file mode 100644 index 00000000..523df8b5 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/keyboard/set_char_echo.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +import os +import importlib.util + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Set character echo mode" + + def run(self): + currentMode = self.getSetting('keyboard', 'charEchoMode', '1') + + # Present current mode + modeDescriptions = { + '0': 'None - no character echo', + '1': 'Always - echo all typed characters', + '2': 'Caps Lock - echo only when caps lock is on' + } + + currentDescription = modeDescriptions.get(currentMode, 'Unknown') + self.presentText(f"Current character echo mode: {currentDescription}") + + # Cycle through the three modes + modes = ['0', '1', '2'] + try: + currentIndex = modes.index(currentMode) + nextIndex = (currentIndex + 1) % len(modes) + newMode = modes[nextIndex] + except ValueError: + newMode = '1' # Default to always + + success = self.setSetting('keyboard', 'charEchoMode', newMode) + + if success: + newDescription = modeDescriptions[newMode] + self.presentText(f"Character echo mode set to: {newDescription}") + self.playSound('Accept') + else: + self.presentText("Failed to change character echo mode") + self.playSound('Error') \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/keyboard/toggle_grab_devices.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/keyboard/toggle_grab_devices.py new file mode 100644 index 00000000..fc10dd12 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/keyboard/toggle_grab_devices.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +import os +import importlib.util + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Toggle exclusive keyboard access" + + def run(self): + currentState = self.getBooleanSetting('keyboard', 'grabDevices', True) + newState = self.toggleBooleanSetting('keyboard', 'grabDevices') + + if newState != currentState: + stateText = "enabled" if newState else "disabled" + self.presentText(f"Exclusive keyboard access {stateText}") + if newState: + self.presentText("Fenrir will have exclusive control of keyboard input") + else: + self.presentText("Fenrir will share keyboard input with other applications") + self.presentText("Please restart Fenrir for this change to take effect") + self.playSound('Accept') + else: + self.presentText("Failed to change keyboard grab setting") + self.playSound('Error') \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/keyboard/toggle_word_echo.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/keyboard/toggle_word_echo.py new file mode 100644 index 00000000..14202636 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/keyboard/toggle_word_echo.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +import os +import importlib.util + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Toggle word echo when pressing space" + + def run(self): + currentState = self.getBooleanSetting('keyboard', 'wordEcho', False) + newState = self.toggleBooleanSetting('keyboard', 'wordEcho') + + if newState != currentState: + stateText = "enabled" if newState else "disabled" + self.presentText(f"Word echo {stateText}") + if newState: + self.presentText("Words will be spoken when you press space") + else: + self.presentText("Words will not be spoken when you press space") + self.playSound('Accept') + else: + self.presentText("Failed to change word echo setting") + self.playSound('Error') \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/__init__.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/__init__.py new file mode 100644 index 00000000..cbfdfd39 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/__init__.py @@ -0,0 +1 @@ +# Fenrir management configuration diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/backup_config.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/backup_config.py new file mode 100644 index 00000000..7aa588ba --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/backup_config.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +import os +import importlib.util + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Create backup of current configuration" + + def run(self): + self.presentText("Creating configuration backup...") + + success, message = self.backupConfig(announce=False) + + if success: + # Force the message to be queued and spoken + self.env['runtime']['outputManager'].presentText("Configuration backup created successfully", interrupt=False, flush=False) + else: + self.env['runtime']['outputManager'].presentText("Failed to create configuration backup", interrupt=False, flush=False) \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/reload_config.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/reload_config.py new file mode 100644 index 00000000..7f0c3935 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/reload_config.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +import os +import importlib.util + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Reload configuration from file" + + def run(self): + self.presentText("Reloading configuration from file...") + + success = self.reloadConfig() + + if success: + self.presentText("Configuration reloaded successfully") + self.playSound('Accept') + else: + self.presentText("Failed to reload configuration") + self.playSound('Error') \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/reset_defaults.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/reset_defaults.py new file mode 100644 index 00000000..29388aa0 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/reset_defaults.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 + +import os +import importlib.util +import shutil + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Reset configuration to default settings" + + def run(self): + self.presentText("WARNING: This will reset all settings to defaults") + self.presentText("Creating backup before reset...") + + # Create backup first + backupSuccess, backupMessage = self.backupConfig(announce=False) + if not backupSuccess: + self.presentText("Failed to create backup. Reset cancelled for safety.", interrupt=False) + self.playSound('Error') + return + + # Find default configuration file + defaultConfigPath = self.findDefaultConfig() + + if defaultConfigPath and os.path.isfile(defaultConfigPath): + try: + # Copy default configuration over current + shutil.copy2(defaultConfigPath, self.settingsFile) + + # Reload the configuration + self.reloadConfig() + + self.presentText("Configuration reset to defaults successfully", interrupt=False, flush=False) + self.presentText("Please restart Fenrir for all changes to take effect", interrupt=False, flush=False) + + except Exception as e: + self.presentText(f"Failed to reset configuration: {str(e)}", interrupt=False, flush=False) + else: + # Manually create basic default configuration + self.createBasicDefaults() + + def findDefaultConfig(self): + """Find the default configuration file""" + possiblePaths = [ + '/usr/share/fenrirscreenreader/config/settings/settings.conf', + '/etc/fenrirscreenreader/settings/settings.conf.default', + os.path.join(os.path.dirname(self.settingsFile), 'settings.conf.default'), + # Development path + os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', '..', '..', 'config', 'settings', 'settings.conf') + ] + + for path in possiblePaths: + if os.path.isfile(path): + return path + + return None + + def createBasicDefaults(self): + """Create basic default configuration manually""" + try: + self.config.clear() + + # Basic speech defaults + self.config['speech'] = { + 'enabled': 'True', + 'driver': 'speechdDriver', + 'rate': '0.5', + 'pitch': '0.5', + 'volume': '1.0', + 'autoReadIncoming': 'True' + } + + # Basic sound defaults + self.config['sound'] = { + 'enabled': 'True', + 'driver': 'genericDriver', + 'theme': 'default', + 'volume': '0.7' + } + + # Basic keyboard defaults + self.config['keyboard'] = { + 'driver': 'evdevDriver', + 'device': 'ALL', + 'keyboardLayout': 'desktop', + 'charEchoMode': '1', + 'wordEcho': 'False', + 'charDeleteEcho': 'True' + } + + # Basic screen defaults + self.config['screen'] = { + 'driver': 'vcsaDriver', + 'encoding': 'auto' + } + + # Basic general defaults + self.config['general'] = { + 'punctuationLevel': 'some', + 'debugLevel': '0', + 'numberOfClipboards': '50' + } + + # Write the configuration + with open(self.settingsFile, 'w') as configfile: + self.config.write(configfile) + + self.presentText("Basic default configuration created", interrupt=False, flush=False) + self.presentText("Please restart Fenrir for changes to take effect", interrupt=False, flush=False) + + except Exception as e: + self.presentText(f"Failed to create default configuration: {str(e)}", interrupt=False, flush=False) \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/revert_to_saved.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/revert_to_saved.py new file mode 100644 index 00000000..5d523f00 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/revert_to_saved.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Discard temporary changes and revert to saved settings" + + def run(self): + self.env['runtime']['outputManager'].presentText("Reverting to saved configuration...", interrupt=True) + + try: + # Reload settings from file, discarding runtime changes + settingsManager = self.env['runtime']['settingsManager'] + settingsManager.loadSettings() + + # Reinitialize speech system with restored settings + if 'speechDriver' in self.env['runtime']: + try: + speechDriver = self.env['runtime']['speechDriver'] + speechDriver.shutdown() + speechDriver.initialize(self.env) + except: + pass + + # Reinitialize sound system with restored settings + if 'soundDriver' in self.env['runtime']: + try: + soundDriver = self.env['runtime']['soundDriver'] + soundDriver.shutdown() + soundDriver.initialize(self.env) + except: + pass + + self.env['runtime']['outputManager'].presentText("Successfully reverted to saved settings", interrupt=False, flush=False) + self.env['runtime']['outputManager'].presentText("All temporary changes have been discarded", interrupt=False, flush=False) + + except Exception as e: + self.env['runtime']['outputManager'].presentText(f"Error reverting settings: {str(e)}", interrupt=False, flush=False) + self.env['runtime']['outputManager'].presentText("You may need to restart Fenrir", interrupt=False, flush=False) + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/save_config.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/save_config.py new file mode 100644 index 00000000..85cb011b --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/save_config.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +import os +import importlib.util + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Save current configuration to file" + + def run(self): + self.presentText("Saving current configuration...") + + try: + # Force reload and save of current configuration + self.reloadConfig() + + # Write the configuration file + with open(self.settingsFile, 'w') as configfile: + self.config.write(configfile) + + self.presentText("Configuration saved successfully") + self.playSound('Accept') + + except Exception as e: + self.presentText(f"Failed to save configuration: {str(e)}") + self.playSound('Error') \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/save_session_settings.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/save_session_settings.py new file mode 100644 index 00000000..262c16fa --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/save_session_settings.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Save current session settings to configuration file" + + def run(self): + self.env['runtime']['outputManager'].presentText("Saving current session settings to configuration file...", interrupt=True) + + try: + # This calls the settings manager's save method which writes current runtime settings to file + self.env['runtime']['settingsManager'].saveSettings() + + self.env['runtime']['outputManager'].presentText("Session settings saved successfully!", interrupt=False, flush=False) + self.env['runtime']['outputManager'].presentText("All temporary changes are now permanent.", interrupt=False, flush=False) + + except Exception as e: + self.env['runtime']['outputManager'].presentText(f"Failed to save settings: {str(e)}", interrupt=False, flush=False) + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/show_unsaved_changes.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/show_unsaved_changes.py new file mode 100644 index 00000000..dd9e9e45 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/management/show_unsaved_changes.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 + +import configparser + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Show temporary changes not yet saved to file" + + def run(self): + self.env['runtime']['outputManager'].presentText("Checking for unsaved changes...", interrupt=True) + + try: + # Read the current config file + settingsFile = self.env['runtime']['settingsManager'].settingsFile + fileConfig = configparser.ConfigParser(interpolation=None) + fileConfig.read(settingsFile) + + # Compare with runtime settings + runtimeSettings = self.env['runtime']['settingsManager'].settings + + changes = [] + + # Check speech settings specifically + speechSections = ['speech', 'sound', 'keyboard', 'screen', 'general'] + + for section in speechSections: + if section in runtimeSettings and section in fileConfig: + for option in runtimeSettings[section]: + runtimeValue = runtimeSettings[section][option] + try: + fileValue = fileConfig[section][option] + except: + fileValue = "" + + if runtimeValue != fileValue: + changes.append(f"{section}.{option}: {fileValue} → {runtimeValue}") + + if changes: + self.env['runtime']['outputManager'].presentText(f"Found {len(changes)} unsaved changes:", interrupt=True) + for change in changes[:5]: # Limit to first 5 changes + self.env['runtime']['outputManager'].presentText(change, interrupt=True) + + if len(changes) > 5: + self.env['runtime']['outputManager'].presentText(f"... and {len(changes) - 5} more changes", interrupt=True) + + self.env['runtime']['outputManager'].presentText("Use save command to make these changes permanent", interrupt=True) + self.env['runtime']['outputManager'].playSound('Accept') + else: + self.env['runtime']['outputManager'].presentText("No unsaved changes found", interrupt=True) + self.env['runtime']['outputManager'].playSound('Cancel') + + except Exception as e: + self.env['runtime']['outputManager'].presentText(f"Error checking for changes: {str(e)}", interrupt=True) + self.env['runtime']['outputManager'].playSound('Error') + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/screen/__init__.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/screen/__init__.py new file mode 100644 index 00000000..428c13ab --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/screen/__init__.py @@ -0,0 +1 @@ +# Fenrir screen configuration diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/screen/select_driver.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/screen/select_driver.py new file mode 100644 index 00000000..8b39be77 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/screen/select_driver.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +import os +import importlib.util + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Select screen driver" + + def run(self): + currentDriver = self.getSetting('screen', 'driver', 'vcsaDriver') + + # Present current driver + driverDescriptions = { + 'vcsaDriver': 'VCSA Driver - Linux TTY console access', + 'ptyDriver': 'PTY Driver - terminal emulation', + 'dummyDriver': 'Dummy Driver - for testing only', + 'debugDriver': 'Debug Driver - development/debugging' + } + + currentDescription = driverDescriptions.get(currentDriver, 'Unknown driver') + self.presentText(f"Current screen driver: {currentDescription}") + + # Cycle through the available drivers + drivers = ['vcsaDriver', 'ptyDriver', 'dummyDriver', 'debugDriver'] + try: + currentIndex = drivers.index(currentDriver) + nextIndex = (currentIndex + 1) % len(drivers) + newDriver = drivers[nextIndex] + except ValueError: + newDriver = 'vcsaDriver' # Default to VCSA + + success = self.setSetting('screen', 'driver', newDriver) + + if success: + newDescription = driverDescriptions[newDriver] + self.presentText(f"Screen driver changed to: {newDescription}") + self.presentText("Please restart Fenrir for this change to take effect.") + self.playSound('Accept') + else: + self.presentText("Failed to change screen driver") + self.playSound('Error') \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/screen/set_encoding.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/screen/set_encoding.py new file mode 100644 index 00000000..e94c36ce --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/screen/set_encoding.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +import os +import importlib.util + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Set screen text encoding" + + def run(self): + currentEncoding = self.getSetting('screen', 'encoding', 'auto') + + # Present current encoding + self.presentText(f"Current screen encoding: {currentEncoding}") + + # Cycle through available encodings + encodings = ['auto', 'utf-8', 'cp1252', 'iso-8859-1'] + try: + currentIndex = encodings.index(currentEncoding) + nextIndex = (currentIndex + 1) % len(encodings) + newEncoding = encodings[nextIndex] + except ValueError: + newEncoding = 'auto' # Default to auto + + success = self.setSetting('screen', 'encoding', newEncoding) + + if success: + self.presentText(f"Screen encoding set to: {newEncoding}") + if newEncoding == 'auto': + self.presentText("Fenrir will automatically detect text encoding") + else: + self.presentText(f"Fenrir will use {newEncoding} encoding") + self.playSound('Accept') + else: + self.presentText("Failed to change screen encoding") + self.playSound('Error') \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/sound/__init__.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/sound/__init__.py new file mode 100644 index 00000000..2b385da2 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/sound/__init__.py @@ -0,0 +1 @@ +# Fenrir sound configuration diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/sound/adjust_volume.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/sound/adjust_volume.py new file mode 100644 index 00000000..ee26d606 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/sound/adjust_volume.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +import os +import importlib.util + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Adjust sound volume" + + def run(self): + currentVolume = self.getFloatSetting('sound', 'volume', 0.7) + + # Present current volume + volumePercent = int(currentVolume * 100) + self.presentText(f"Current sound volume: {volumePercent} percent") + + # Adjust volume by 10% + newVolume = self.adjustFloatSetting('sound', 'volume', 0.1, 0.0, 1.0) + + if newVolume != currentVolume: + newPercent = int(newVolume * 100) + self.presentText(f"Sound volume set to {newPercent} percent", interrupt=False) + self.playSound('Accept') + else: + # If we hit the maximum, try decreasing instead + newVolume = self.adjustFloatSetting('sound', 'volume', -0.1, 0.0, 1.0) + if newVolume != currentVolume: + newPercent = int(newVolume * 100) + self.presentText(f"Sound volume set to {newPercent} percent", interrupt=False) + self.playSound('Accept') + else: + self.presentText("Sound volume unchanged", interrupt=False) + self.playSound('Cancel') \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/sound/select_driver.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/sound/select_driver.py new file mode 100644 index 00000000..d524492b --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/sound/select_driver.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +import os +import importlib.util + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Select sound driver (genericDriver or gstreamerDriver)" + + def run(self): + currentDriver = self.getSetting('sound', 'driver', 'genericDriver') + + # Present current driver + self.presentText(f"Current sound driver: {currentDriver}") + + # Toggle between the two available drivers + if currentDriver == 'genericDriver': + newDriver = 'gstreamerDriver' + driverDescription = "GStreamer Driver - advanced multimedia framework" + else: + newDriver = 'genericDriver' + driverDescription = "Generic Driver - uses SOX for audio playback" + + success = self.setSetting('sound', 'driver', newDriver) + + if success: + self.presentText(f"Sound driver changed to {newDriver}. {driverDescription}") + self.presentText("Please restart Fenrir for this change to take effect.") + self.playSound('Accept') + else: + self.presentText("Failed to change sound driver") + self.playSound('Error') \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/sound/select_theme.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/sound/select_theme.py new file mode 100644 index 00000000..9203f0a5 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/sound/select_theme.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +import os +import importlib.util + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Select sound theme" + + def run(self): + currentTheme = self.getSetting('sound', 'theme', 'default') + + # Present current theme + self.presentText(f"Current sound theme: {currentTheme}") + + # Look for available sound themes + soundPaths = [ + '/usr/share/sounds', + '/usr/share/fenrirscreenreader/sounds', + os.path.expanduser('~/.local/share/fenrirscreenreader/sounds') + ] + + availableThemes = self.getAvailableThemes(soundPaths) + + if len(availableThemes) > 1: + # For this implementation, cycle through available themes + try: + currentIndex = availableThemes.index(currentTheme) + nextIndex = (currentIndex + 1) % len(availableThemes) + newTheme = availableThemes[nextIndex] + except ValueError: + # Current theme not found in available themes, use first one + newTheme = availableThemes[0] + + success = self.setSetting('sound', 'theme', newTheme) + + if success: + self.presentText(f"Sound theme changed to: {newTheme}") + self.playSound('Accept') + else: + self.presentText("Failed to change sound theme") + self.playSound('Error') + else: + self.presentText("Only default sound theme is available") + self.playSound('Cancel') + + def getAvailableThemes(self, searchPaths): + """Find available sound themes in the search paths""" + themes = ['default'] # Always include default + + for path in searchPaths: + if os.path.isdir(path): + try: + for item in os.listdir(path): + fullPath = os.path.join(path, item) + if os.path.isdir(fullPath) and item != 'default' and item not in themes: + # Check if it looks like a sound theme (contains sound files) + if self.isValidSoundTheme(fullPath): + themes.append(item) + except Exception: + continue + + return themes + + def isValidSoundTheme(self, themePath): + """Check if a directory contains sound files""" + soundExtensions = ['.wav', '.ogg', '.mp3', '.flac'] + try: + for file in os.listdir(themePath): + if any(file.lower().endswith(ext) for ext in soundExtensions): + return True + except Exception: + pass + return False \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/sound/toggle_enabled.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/sound/toggle_enabled.py new file mode 100644 index 00000000..892c2758 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/sound/toggle_enabled.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +import os +import importlib.util + +# Load base configuration class +_base_path = os.path.join(os.path.dirname(__file__), '..', 'config_base.py') +_spec = importlib.util.spec_from_file_location("config_base", _base_path) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +ConfigCommand = _module.ConfigCommand + +class command(ConfigCommand): + def __init__(self): + super().__init__() + + def getDescription(self): + return "Toggle sound output on or off" + + def run(self): + currentState = self.getBooleanSetting('sound', 'enabled', True) + newState = self.toggleBooleanSetting('sound', 'enabled') + + if newState != currentState: + stateText = "enabled" if newState else "disabled" + self.presentText(f"Sound {stateText}") + # Only play sound if we enabled sound + if newState: + self.playSound('Accept') + else: + self.presentText("Failed to change sound setting") \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/__init__.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/__init__.py new file mode 100644 index 00000000..3b31c6d8 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/__init__.py @@ -0,0 +1 @@ +# Fenrir speech configuration diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/adjust_pitch.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/adjust_pitch.py new file mode 100644 index 00000000..8b13cafb --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/adjust_pitch.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Adjust speech pitch (voice height)" + + def run(self): + try: + # Get current pitch + currentPitch = float(self.env['runtime']['settingsManager'].getSetting('speech', 'pitch')) + except: + currentPitch = 0.5 + + # Present current pitch + pitchPercent = int(currentPitch * 100) + self.env['runtime']['outputManager'].presentText(f"Current speech pitch: {pitchPercent} percent", interrupt=True) + + # Increase by 10%, wrap around if at max + newPitch = currentPitch + 0.1 + if newPitch > 1.0: + newPitch = 0.1 # Wrap to minimum + + # Apply the new pitch + self.env['runtime']['settingsManager'].setSetting('speech', 'pitch', str(newPitch)) + + newPercent = int(newPitch * 100) + self.env['runtime']['outputManager'].presentText(f"Speech pitch set to {newPercent} percent", interrupt=True) + self.env['runtime']['outputManager'].playSound('Accept') + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/adjust_rate.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/adjust_rate.py new file mode 100644 index 00000000..cc2be4aa --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/adjust_rate.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Adjust speech rate (speed)" + + def run(self): + try: + # Get current rate + currentRate = float(self.env['runtime']['settingsManager'].getSetting('speech', 'rate')) + except: + currentRate = 0.5 + + # Present current rate + ratePercent = int(currentRate * 100) + self.env['runtime']['outputManager'].presentText(f"Current speech rate: {ratePercent} percent", interrupt=True) + + # Increase by 10%, wrap around if at max + newRate = currentRate + 0.1 + if newRate > 1.0: + newRate = 0.1 # Wrap to minimum + + # Apply the new rate + self.env['runtime']['settingsManager'].setSetting('speech', 'rate', str(newRate)) + + newPercent = int(newRate * 100) + self.env['runtime']['outputManager'].presentText(f"Speech rate set to {newPercent} percent", interrupt=False, flush=False) + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/adjust_speech_rate.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/adjust_speech_rate.py new file mode 100644 index 00000000..376ef7f3 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/adjust_speech_rate.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Adjust speech rate (temporary - use save to make permanent)" + + def run(self): + try: + # Get current rate from runtime settings (may be temporary) + settingsManager = self.env['runtime']['settingsManager'] + currentRate = float(settingsManager.getSetting('speech', 'rate')) + except: + currentRate = 0.5 + + # Present current rate + ratePercent = int(currentRate * 100) + self.env['runtime']['outputManager'].presentText(f"Current speech rate: {ratePercent} percent", interrupt=True) + + # Increase by 10%, wrap around if at max + newRate = currentRate + 0.1 + if newRate > 1.0: + newRate = 0.1 # Wrap to minimum + + # Apply ONLY to runtime - this is temporary until saved + settingsManager = self.env['runtime']['settingsManager'] + settingsManager.settings['speech']['rate'] = str(newRate) + + # Apply to speech system immediately for this session + if 'speechDriver' in self.env['runtime']: + try: + self.env['runtime']['speechDriver'].setRate(newRate) + except: + pass + + newPercent = int(newRate * 100) + self.env['runtime']['outputManager'].presentText(f"Speech rate temporarily set to {newPercent} percent", interrupt=False, flush=False) + self.env['runtime']['outputManager'].presentText("Use save command to make permanent", interrupt=False, flush=False) + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/adjust_volume.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/adjust_volume.py new file mode 100644 index 00000000..360bfafa --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/adjust_volume.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Adjust speech volume (loudness)" + + def run(self): + try: + # Get current volume + currentVolume = float(self.env['runtime']['settingsManager'].getSetting('speech', 'volume')) + except: + currentVolume = 1.0 + + # Present current volume + volumePercent = int(currentVolume * 100) + self.env['runtime']['outputManager'].presentText(f"Current speech volume: {volumePercent} percent", interrupt=True) + + # Increase by 10%, wrap around if at max + newVolume = currentVolume + 0.1 + if newVolume > 1.0: + newVolume = 0.1 # Wrap to minimum + + # Apply the new volume + self.env['runtime']['settingsManager'].setSetting('speech', 'volume', str(newVolume)) + + newPercent = int(newVolume * 100) + self.env['runtime']['outputManager'].presentText(f"Speech volume set to {newPercent} percent", interrupt=True) + self.env['runtime']['outputManager'].playSound('Accept') + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/show_current_settings.py b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/show_current_settings.py new file mode 100644 index 00000000..5a8d6979 --- /dev/null +++ b/src/fenrirscreenreader/commands/vmenu-profiles/KEY/fenrir/speech/show_current_settings.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Show current speech settings" + + def run(self): + # Get current speech settings + driver = self.env['runtime']['settingsManager'].getSetting('speech', 'driver') + module = self.env['runtime']['settingsManager'].getSetting('speech', 'module') + voice = self.env['runtime']['settingsManager'].getSetting('speech', 'voice') + rate = self.env['runtime']['settingsManager'].getSetting('speech', 'rate') + pitch = self.env['runtime']['settingsManager'].getSetting('speech', 'pitch') + volume = self.env['runtime']['settingsManager'].getSetting('speech', 'volume') + enabled = self.env['runtime']['settingsManager'].getSetting('speech', 'enabled') + + self.env['runtime']['outputManager'].presentText("Current speech settings:", interrupt=True) + + # Present all settings + self.env['runtime']['outputManager'].presentText(f"Speech enabled: {enabled}", interrupt=True) + self.env['runtime']['outputManager'].presentText(f"Driver: {driver}", interrupt=True) + + if module: + self.env['runtime']['outputManager'].presentText(f"Module: {module}", interrupt=True) + + if voice: + self.env['runtime']['outputManager'].presentText(f"Voice: {voice}", interrupt=True) + + try: + ratePercent = int(float(rate) * 100) + self.env['runtime']['outputManager'].presentText(f"Rate: {ratePercent} percent", interrupt=True) + except: + self.env['runtime']['outputManager'].presentText(f"Rate: {rate}", interrupt=True) + + try: + pitchPercent = int(float(pitch) * 100) + self.env['runtime']['outputManager'].presentText(f"Pitch: {pitchPercent} percent", interrupt=True) + except: + self.env['runtime']['outputManager'].presentText(f"Pitch: {pitch}", interrupt=True) + + try: + volumePercent = int(float(volume) * 100) + self.env['runtime']['outputManager'].presentText(f"Volume: {volumePercent} percent", interrupt=True) + except: + self.env['runtime']['outputManager'].presentText(f"Volume: {volume}", interrupt=True) + + self.env['runtime']['outputManager'].playSound('Accept') + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/core/dynamicVoiceMenu.py b/src/fenrirscreenreader/core/dynamicVoiceMenu.py new file mode 100644 index 00000000..d23968a5 --- /dev/null +++ b/src/fenrirscreenreader/core/dynamicVoiceMenu.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 + +import subprocess +import importlib.util +import os +import time + +class DynamicVoiceCommand: + """Dynamic command class for voice selection""" + def __init__(self, module, voice, env): + self.module = module + self.voice = voice + self.env = env + self.testMessage = "This is a voice test. The quick brown fox jumps over the lazy dog." + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return f"Select voice: {self.voice}" + + def run(self): + try: + self.env['runtime']['outputManager'].presentText(f"Testing voice {self.voice} from {self.module}. Please wait.", interrupt=True) + + # Brief pause before testing to avoid speech overlap + time.sleep(0.5) + + # Test voice + testResult, errorMsg = self.testVoice() + if testResult: + self.env['runtime']['outputManager'].presentText("Voice test completed successfully. Navigate to Apply Tested Voice to use this voice.", interrupt=False, flush=False) + + # Store for confirmation (use same variables as apply_tested_voice.py) + self.env['commandBuffer']['lastTestedModule'] = self.module + self.env['commandBuffer']['lastTestedVoice'] = self.voice + self.env['commandBuffer']['pendingVoiceModule'] = self.module + self.env['commandBuffer']['pendingVoiceVoice'] = self.voice + self.env['commandBuffer']['voiceTestCompleted'] = True + else: + self.env['runtime']['outputManager'].presentText(f"Voice test failed: {errorMsg}", interrupt=False, flush=False) + + except Exception as e: + self.env['runtime']['outputManager'].presentText(f"Voice selection error: {str(e)}", interrupt=False, flush=False) + + def testVoice(self): + """Test voice with spd-say""" + try: + cmd = ['spd-say', '-C', '-w', '-o', self.module, '-y', self.voice, self.testMessage] + result = subprocess.run(cmd, timeout=8, capture_output=True, text=True) + if result.returncode == 0: + return True, "Voice test successful" + else: + error_msg = result.stderr.strip() if result.stderr else f"Command failed with return code {result.returncode}" + return False, error_msg + except subprocess.TimeoutExpired: + return False, "Voice test timed out" + except Exception as e: + return False, f"Error running voice test: {str(e)}" + + def setCallback(self, callback): + pass + +class DynamicApplyVoiceCommand: + """Command to apply the tested voice""" + def __init__(self, env): + self.env = env + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return "Apply tested voice to Fenrir" + + def run(self): + try: + if not self.env['commandBuffer'].get('voiceTestCompleted', False): + self.env['runtime']['outputManager'].presentText("No voice has been tested yet", interrupt=True) + return + + module = self.env['commandBuffer']['pendingVoiceModule'] + voice = self.env['commandBuffer']['pendingVoiceVoice'] + + self.env['runtime']['outputManager'].presentText(f"Applying {voice} from {module}", interrupt=True) + + # Debug: Show current settings + settingsManager = self.env['runtime']['settingsManager'] + currentModule = settingsManager.getSetting('speech', 'module') + currentVoice = settingsManager.getSetting('speech', 'voice') + self.env['runtime']['outputManager'].presentText(f"Current: {currentVoice} from {currentModule}", interrupt=False, flush=False) + + # Apply to runtime settings with fallback + settingsManager = self.env['runtime']['settingsManager'] + + # Store old values for safety + oldDriver = settingsManager.getSetting('speech', 'driver') + oldModule = settingsManager.getSetting('speech', 'module') + oldVoice = settingsManager.getSetting('speech', 'voice') + + try: + # Apply new settings to runtime only (use setSetting to update settingArgDict) + settingsManager.setSetting('speech', 'driver', 'speechdDriver') + settingsManager.setSetting('speech', 'module', module) + settingsManager.setSetting('speech', 'voice', voice) + + # Apply settings to speech driver directly + if 'speechDriver' in self.env['runtime']: + speechDriver = self.env['runtime']['speechDriver'] + + # Get current module to see if we're changing modules + currentModule = settingsManager.getSetting('speech', 'module') + moduleChanging = (currentModule != module) + + # Set module and voice on driver instance first + speechDriver.setModule(module) + speechDriver.setVoice(voice) + + if moduleChanging: + # Module change requires reinitializing the speech driver + self.env['runtime']['outputManager'].presentText(f"Switching from {currentModule} to {module} module", interrupt=True) + speechDriver.shutdown() + speechDriver.initialize(self.env) + # Re-set after initialization + speechDriver.setModule(module) + speechDriver.setVoice(voice) + self.env['runtime']['outputManager'].presentText("Speech driver reinitialized", interrupt=True) + + # Debug: verify what was actually set + self.env['runtime']['outputManager'].presentText(f"Speech driver now has module: {speechDriver.module}, voice: {speechDriver.voice}", interrupt=True) + + # Force application by speaking a test message + self.env['runtime']['outputManager'].presentText("Voice applied successfully! You should hear this in the new voice.", interrupt=True) + + # Brief pause then more speech to test + time.sleep(1) + self.env['runtime']['outputManager'].presentText("Use save settings to make permanent", interrupt=True) + + # Clear pending state + self.env['commandBuffer']['voiceTestCompleted'] = False + + # Exit vmenu after successful application + self.env['runtime']['vmenuManager'].setActive(False) + + except Exception as e: + # Revert on failure + settingsManager.settings['speech']['driver'] = oldDriver + settingsManager.settings['speech']['module'] = oldModule + settingsManager.settings['speech']['voice'] = oldVoice + + # Try to reinitialize with old settings + if 'speechDriver' in self.env['runtime']: + try: + speechDriver = self.env['runtime']['speechDriver'] + speechDriver.shutdown() + speechDriver.initialize(self.env) + except: + pass # If this fails, at least we tried + + self.env['runtime']['outputManager'].presentText(f"Failed to apply voice, reverted: {str(e)}", interrupt=False, flush=False) + + except Exception as e: + self.env['runtime']['outputManager'].presentText(f"Apply voice error: {str(e)}", interrupt=False, flush=False) + + def setCallback(self, callback): + pass + +def addDynamicVoiceMenus(vmenuManager): + """Add dynamic voice menus to vmenu system""" + try: + env = vmenuManager.env + + # Get speech modules + modules = getSpeechdModules() + if not modules: + return + + # Create voice browser submenu + voiceBrowserMenu = {} + + # Add apply voice command + applyCommand = DynamicApplyVoiceCommand(env) + voiceBrowserMenu['Apply Tested Voice Action'] = applyCommand + + # Add modules as submenus + for module in modules[:8]: # Limit to 8 modules to keep menu manageable + moduleMenu = {} + + # Get voices for this module + voices = getModuleVoices(module) + if voices: + + # Add voice commands + for voice in voices: + voiceCommand = DynamicVoiceCommand(module, voice, env) + moduleMenu[f"{voice} Action"] = voiceCommand + else: + moduleMenu['No voices available Action'] = createInfoCommand(f"No voices found for {module}", env) + + voiceBrowserMenu[f"{module} Menu"] = moduleMenu + + # Add to main menu dict + vmenuManager.menuDict['Voice Browser Menu'] = voiceBrowserMenu + + except Exception as e: + print(f"Error creating dynamic voice menus: {e}") + +def createInfoCommand(message, env): + """Create a simple info command""" + class InfoCommand: + def __init__(self, message, env): + self.message = message + self.env = env + def initialize(self, environment): pass + def shutdown(self): pass + def getDescription(self): return self.message + def run(self): + self.env['runtime']['outputManager'].presentText(self.message, interrupt=True) + def setCallback(self, callback): pass + + return InfoCommand(message, env) + +def getSpeechdModules(): + """Get available speech modules""" + try: + result = subprocess.run(['spd-say', '-O'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + lines = result.stdout.strip().split('\n') + return [line.strip() for line in lines[1:] if line.strip()] + except Exception: + pass + return [] + +def getModuleVoices(module): + """Get voices for a module""" + try: + result = subprocess.run(['spd-say', '-o', module, '-L'], capture_output=True, text=True, timeout=8) + if result.returncode == 0: + lines = result.stdout.strip().split('\n') + voices = [] + for line in lines[1:]: + if not line.strip(): + continue + if module.lower() == 'espeak-ng': + voice = processEspeakVoice(line) + if voice: + voices.append(voice) + else: + voices.append(line.strip()) + return voices + except Exception: + pass + return [] + +def processEspeakVoice(voiceLine): + """Process espeak voice format""" + try: + parts = [p for p in voiceLine.split() if p] + if len(parts) < 2: + return None + langCode = parts[-2].lower() + variant = parts[-1].lower() + return f"{langCode}+{variant}" if variant and variant != 'none' else langCode + except Exception: + return None \ No newline at end of file diff --git a/src/fenrirscreenreader/core/outputManager.py b/src/fenrirscreenreader/core/outputManager.py index 41a56456..b1651aae 100644 --- a/src/fenrirscreenreader/core/outputManager.py +++ b/src/fenrirscreenreader/core/outputManager.py @@ -192,3 +192,12 @@ class outputManager(): self.presentText(' review cursor ', interrupt=interrupt_p) else: self.presentText(' text cursor ', interrupt=interrupt_p) + + def resetSpeechDriver(self): + """Reset speech driver to clean state - called by settingsManager""" + if 'speechDriver' in self.env['runtime'] and self.env['runtime']['speechDriver']: + try: + self.env['runtime']['speechDriver'].reset() + self.env['runtime']['debug'].writeDebugOut("Speech driver reset successfully", debug.debugLevel.INFO) + except Exception as e: + self.env['runtime']['debug'].writeDebugOut(f"resetSpeechDriver error: {e}", debug.debugLevel.ERROR) diff --git a/src/fenrirscreenreader/core/vmenuManager.py b/src/fenrirscreenreader/core/vmenuManager.py index 53b40888..e3712e74 100755 --- a/src/fenrirscreenreader/core/vmenuManager.py +++ b/src/fenrirscreenreader/core/vmenuManager.py @@ -138,6 +138,9 @@ class vmenuManager(): self.env['bindings'][str([1, ['KEY_X']])] = 'SEARCH_X' self.env['bindings'][str([1, ['KEY_Y']])] = 'SEARCH_Y' self.env['bindings'][str([1, ['KEY_Z']])] = 'SEARCH_Z' + # page navigation + self.env['bindings'][str([1, ['KEY_PAGEUP']])] = 'PAGE_UP_VMENU' + self.env['bindings'][str([1, ['KEY_PAGEDOWN']])] = 'PAGE_DOWN_VMENU' except Exception as e: print(e) else: @@ -152,6 +155,14 @@ class vmenuManager(): menu = self.fs_tree_to_dict( self.defaultVMenuPath) if menu: self.menuDict = menu + + # Add dynamic voice menus + try: + from fenrirscreenreader.core.dynamicVoiceMenu import addDynamicVoiceMenus + addDynamicVoiceMenus(self) + except Exception as e: + print(f"Error adding dynamic voice menus: {e}") + # index still valid? if self.currIndex != None: try: @@ -218,6 +229,32 @@ class vmenuManager(): else: self.currIndex[len(self.currIndex) - 1] -= 1 return True + + def pageUp(self): + if self.currIndex == None: + return False + menuSize = len(self.getNestedByPath(self.menuDict, self.currIndex[:-1])) + if menuSize <= 1: + return False + jumpSize = max(1, int(menuSize * 0.1)) # 10% of menu size, minimum 1 + newIndex = self.currIndex[len(self.currIndex) - 1] - jumpSize + if newIndex < 0: + newIndex = 0 + self.currIndex[len(self.currIndex) - 1] = newIndex + return True + + def pageDown(self): + if self.currIndex == None: + return False + menuSize = len(self.getNestedByPath(self.menuDict, self.currIndex[:-1])) + if menuSize <= 1: + return False + jumpSize = max(1, int(menuSize * 0.1)) # 10% of menu size, minimum 1 + newIndex = self.currIndex[len(self.currIndex) - 1] + jumpSize + if newIndex >= menuSize: + newIndex = menuSize - 1 + self.currIndex[len(self.currIndex) - 1] = newIndex + return True def getCurrentEntry(self): return self.getKeysByPath(self.menuDict, self.currIndex)[self.currIndex[-1]] diff --git a/src/fenrirscreenreader/fenrirVersion.py b/src/fenrirscreenreader/fenrirVersion.py index 4b8b2954..7cb05b79 100644 --- a/src/fenrirscreenreader/fenrirVersion.py +++ b/src/fenrirscreenreader/fenrirVersion.py @@ -4,5 +4,5 @@ # Fenrir TTY screen reader # By Chrys, Storm Dragon, and contributers. -version = "2025.06.07" +version = "2025.06.17" codeName = "master" diff --git a/src/fenrirscreenreader/speechDriver/speechdDriver.py b/src/fenrirscreenreader/speechDriver/speechdDriver.py index c87a86c0..57859e05 100644 --- a/src/fenrirscreenreader/speechDriver/speechdDriver.py +++ b/src/fenrirscreenreader/speechDriver/speechdDriver.py @@ -16,9 +16,15 @@ class driver(speechDriver): self._sd = None self.env = environment self._isInitialized = False - self.language = '' - self.voice = '' - self.module = '' + + # Only set these if they haven't been set yet (preserve existing values) + if not hasattr(self, 'language') or self.language is None: + self.language = '' + if not hasattr(self, 'voice') or self.voice is None: + self.voice = '' + if not hasattr(self, 'module') or self.module is None: + self.module = '' + try: import speechd self._sd = speechd.SSIPClient('fenrir')