Very experimental attempt to move the configure_fenrir script into fenrir itself using the vmenu system. Lots of testing please.

This commit is contained in:
Storm Dragon 2025-06-15 14:04:14 -04:00
parent e76b914d6e
commit 72bd334d65
55 changed files with 2810 additions and 6 deletions

View File

@ -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

View File

@ -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

Binary file not shown.

Binary file not shown.

View File

@ -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'

View File

@ -0,0 +1,69 @@
#!/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
settingsManager.settings['speech']['driver'] = 'speechdDriver'
settingsManager.settings['speech']['module'] = module
settingsManager.settings['speech']['voice'] = voice
# Try to reinitialize speech driver
if 'speechDriver' in self.env['runtime']:
speechDriver = self.env['runtime']['speechDriver']
speechDriver.shutdown()
speechDriver.initialize(self.env)
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.settings['speech']['driver'] = oldDriver
settingsManager.settings['speech']['module'] = oldModule
settingsManager.settings['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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
# Fenrir Configuration VMenu Profile

View File

@ -0,0 +1,302 @@
#!/usr/bin/env python3
import os
import configparser
import subprocess
import tempfile
from typing import Dict, List, Optional, Tuple, Any
class ConfigCommand:
"""Base class for Fenrir configuration vmenu commands"""
def __init__(self):
self.env = None
self.config = None
self.settingsFile = None
# Configuration presets from original configure_fenrir.py
self.presetOptions = {
'sound.driver': ['genericDriver', 'gstreamerDriver'],
'speech.driver': ['speechdDriver', 'genericDriver'],
'screen.driver': ['vcsaDriver', 'dummyDriver', 'ptyDriver', 'debugDriver'],
'keyboard.driver': ['evdevDriver', 'dummyDriver'],
'remote.driver': ['unixDriver', 'tcpDriver'],
'keyboard.charEchoMode': ['0', '1', '2'],
'general.punctuationLevel': ['none', 'some', 'most', 'all'],
'general.debugMode': ['File', 'Print'],
'keyboard.device': ['ALL', 'NOMICE'],
'screen.encoding': ['auto', 'utf-8', 'cp1252', 'iso-8859-1']
}
# Help text for various options
self.helpText = {
'sound.volume': 'Volume level from 0 (quietest) to 1.0 (loudest)',
'speech.rate': 'Speech rate from 0 (slowest) to 1.0 (fastest)',
'speech.pitch': 'Voice pitch from 0 (lowest) to 1.0 (highest)',
'speech.capitalPitch': 'Pitch for capital letters from 0 to 1.0',
'keyboard.charEchoMode': '0 = None, 1 = always, 2 = only while capslock',
'keyboard.doubleTapTimeout': 'Timeout for double tap in seconds',
'screen.screenUpdateDelay': 'Delay between screen updates in seconds',
'general.punctuationLevel': 'none = no punctuation, some = basic, most = detailed, all = everything',
'general.numberOfClipboards': 'Number of clipboard slots to maintain'
}
def initialize(self, environment):
"""Initialize with Fenrir environment"""
self.env = environment
self.settingsFile = self.env['runtime']['settingsManager'].settingsFile
self.config = configparser.ConfigParser(interpolation=None)
self.config.read(self.settingsFile)
def shutdown(self):
"""Cleanup resources"""
pass
def getDescription(self):
"""Return description for this configuration action"""
return "Base configuration command"
def run(self):
"""Execute the configuration action - to be overridden"""
self.presentText("Configuration base class - override run() method")
def presentText(self, text: str, interrupt: bool = True):
"""Present text using Fenrir's speech system"""
if self.env and 'runtime' in self.env and 'outputManager' in self.env['runtime']:
self.env['runtime']['outputManager'].presentText(text, interrupt=interrupt)
def presentConfirmation(self, text: str):
"""Present confirmation message that won't be interrupted by menu navigation"""
self.presentText(text, interrupt=False)
def playSound(self, soundName: str):
"""Play a sound using Fenrir's sound system (deprecated - sounds removed from vmenu)"""
# Sounds removed from vmenu commands to avoid configuration issues
pass
def getSetting(self, section: str, option: str, default: str = "") -> str:
"""Get a setting value from configuration"""
try:
if section in self.config and option in self.config[section]:
return self.config[section][option]
return default
except Exception:
return default
def setSetting(self, section: str, option: str, value: str) -> bool:
"""Set a setting value and save configuration"""
try:
if section not in self.config:
self.config[section] = {}
oldValue = self.getSetting(section, option)
self.config[section][option] = str(value)
# Write configuration file
with open(self.settingsFile, 'w') as configfile:
self.config.write(configfile)
# Apply setting immediately if possible
self.applySettingImmediate(section, option, value, oldValue)
return True
except Exception as e:
self.presentText(f"Error saving setting: {str(e)}")
return False
def applySettingImmediate(self, section: str, option: str, newValue: str, oldValue: str):
"""Apply setting immediately without restart where possible"""
try:
# Apply speech settings immediately
if section == 'speech':
if option in ['rate', 'pitch', 'volume']:
settingsManager = self.env['runtime']['settingsManager']
settingsManager.setSetting(section, option, newValue)
# Apply sound settings immediately
elif section == 'sound':
if option == 'volume':
settingsManager = self.env['runtime']['settingsManager']
settingsManager.setSetting(section, option, newValue)
except Exception as e:
# Silent fail - settings will apply on restart
pass
def validateInput(self, section: str, option: str, value: str) -> Tuple[bool, str]:
"""Validate user input based on option type and constraints"""
try:
# Validate float values (volume, rate, pitch)
if option in ['volume', 'rate', 'pitch', 'capitalPitch']:
floatVal = float(value)
if not 0 <= floatVal <= 1.0:
return False, "Value must be between 0 and 1.0"
return True, str(floatVal)
# Validate integer values
elif option in ['numberOfClipboards', 'ignoreScreen']:
intVal = int(value)
if intVal < 0:
return False, "Value must be 0 or greater"
return True, str(intVal)
# Validate float values with different ranges
elif option == 'doubleTapTimeout':
floatVal = float(value)
if not 0 <= floatVal <= 2.0:
return False, "Value must be between 0 and 2.0 seconds"
return True, str(floatVal)
elif option == 'screenUpdateDelay':
floatVal = float(value)
if not 0 <= floatVal <= 1.0:
return False, "Value must be between 0 and 1.0 seconds"
return True, str(floatVal)
# Validate boolean values
elif self.isBooleanOption(value):
if value.lower() in ['true', 'false']:
return True, value.capitalize()
return False, "Value must be True or False"
# Validate preset options
key = f"{section}.{option}"
if key in self.presetOptions:
if value in self.presetOptions[key]:
return True, value
return False, f"Value must be one of: {', '.join(self.presetOptions[key])}"
# Default validation - accept any string
return True, str(value).strip()
except ValueError:
return False, "Invalid number format"
except Exception as e:
return False, f"Validation error: {str(e)}"
def isBooleanOption(self, value: str) -> bool:
"""Check if the current value is likely a boolean option"""
return value.lower() in ['true', 'false']
def getBooleanSetting(self, section: str, option: str, default: bool = False) -> bool:
"""Get a boolean setting value"""
value = self.getSetting(section, option, str(default)).lower()
return value in ['true', '1', 'yes', 'on']
def setBooleanSetting(self, section: str, option: str, value: bool) -> bool:
"""Set a boolean setting value"""
return self.setSetting(section, option, 'True' if value else 'False')
def toggleBooleanSetting(self, section: str, option: str) -> bool:
"""Toggle a boolean setting and return new value"""
currentValue = self.getBooleanSetting(section, option)
newValue = not currentValue
success = self.setBooleanSetting(section, option, newValue)
return newValue if success else currentValue
def getFloatSetting(self, section: str, option: str, default: float = 0.0) -> float:
"""Get a float setting value"""
try:
return float(self.getSetting(section, option, str(default)))
except ValueError:
return default
def setFloatSetting(self, section: str, option: str, value: float) -> bool:
"""Set a float setting value"""
return self.setSetting(section, option, str(value))
def adjustFloatSetting(self, section: str, option: str, delta: float,
minVal: float = 0.0, maxVal: float = 1.0) -> float:
"""Adjust a float setting by delta amount"""
currentValue = self.getFloatSetting(section, option)
newValue = max(minVal, min(maxVal, currentValue + delta))
success = self.setFloatSetting(section, option, newValue)
return newValue if success else currentValue
def runCommand(self, cmd: List[str], timeout: int = 10) -> Optional[str]:
"""Run a command and return output"""
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
return result.stdout.strip() if result.returncode == 0 else None
except Exception:
return None
def getSpeechdModules(self) -> List[str]:
"""Get list of available speech-dispatcher modules"""
output = self.runCommand(['spd-say', '-O'])
if output:
lines = output.split('\n')
return [line.strip() for line in lines[1:] if line.strip()]
return []
def getModuleVoices(self, module: str) -> List[str]:
"""Get list of voices for a specific speech module"""
output = self.runCommand(['spd-say', '-o', module, '-L'])
if output:
lines = output.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
return []
def processEspeakVoice(self, voiceLine: str) -> Optional[str]:
"""Process espeak-ng voice line 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 testVoice(self, module: str, voice: str, testMessage: str = None) -> bool:
"""Test a voice configuration"""
if not testMessage:
testMessage = "This is a voice test. If you can hear this clearly, the voice is working properly."
try:
# Announce the test
self.presentText("Testing voice configuration. Listen for the test message.")
# Run voice test
cmd = ['spd-say', '-o', module, '-y', voice, testMessage]
result = subprocess.run(cmd, timeout=10)
return result.returncode == 0
except Exception:
return False
def backupConfig(self, announce: bool = True) -> tuple[bool, str]:
"""Create a backup of current configuration"""
try:
import shutil
from datetime import datetime
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backupFile = f"{self.settingsFile}.backup_{timestamp}"
shutil.copy2(self.settingsFile, backupFile)
message = f"Configuration backed up to {backupFile}"
if announce:
self.presentText(message, interrupt=False)
return True, message
except Exception as e:
error_msg = f"Error creating backup: {str(e)}"
if announce:
self.presentText(error_msg, interrupt=False)
return False, error_msg
def reloadConfig(self):
"""Reload configuration from file"""
try:
self.config.read(self.settingsFile)
return True
except Exception:
return False

View File

@ -0,0 +1 @@
# Fenrir general configuration

View File

@ -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')

View File

@ -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')

View File

@ -0,0 +1 @@
# Fenrir keyboard configuration

View File

@ -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)

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -0,0 +1 @@
# Fenrir management configuration

View File

@ -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)

View File

@ -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')

View File

@ -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)

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
# Fenrir screen configuration

View File

@ -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')

View File

@ -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')

View File

@ -0,0 +1 @@
# Fenrir sound configuration

View File

@ -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')

View File

@ -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')

View File

@ -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

View File

@ -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")

View File

@ -0,0 +1 @@
# Fenrir speech configuration

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,76 @@
#!/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 last previewed voice to current session (temporary)"
def run(self):
# Check if we have a tested voice stored (must be tested first)
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 test voice command first to confirm the voice works", interrupt=True)
self.env['runtime']['outputManager'].playSound('Cancel')
return
module = self.env['commandBuffer']['lastTestedModule']
voice = self.env['commandBuffer']['lastTestedVoice']
self.env['runtime']['outputManager'].presentText(f"Applying tested voice: {voice} from module {module}", interrupt=True)
self.env['runtime']['outputManager'].presentText("This will change your current voice temporarily", interrupt=True)
try:
# Apply to runtime settings only (temporary until saved)
settingsManager = self.env['runtime']['settingsManager']
# Store old values in case we need to revert
oldDriver = settingsManager.getSetting('speech', 'driver')
oldModule = settingsManager.getSetting('speech', 'module')
oldVoice = settingsManager.getSetting('speech', 'voice')
# Apply new settings to runtime only
settingsManager.settings['speech']['driver'] = 'speechdDriver'
settingsManager.settings['speech']['module'] = module
settingsManager.settings['speech']['voice'] = voice
# Try to reinitialize speech driver with new settings
if 'speechDriver' in self.env['runtime']:
try:
speechDriver = self.env['runtime']['speechDriver']
speechDriver.shutdown()
speechDriver.initialize(self.env)
# Test the new voice
self.env['runtime']['outputManager'].presentText("Voice applied successfully. This is how it sounds.", interrupt=False)
self.env['runtime']['outputManager'].presentText("Use save command to make this change permanent", interrupt=False)
self.env['runtime']['outputManager'].playSound('Accept')
except Exception as e:
# Revert on failure
settingsManager.settings['speech']['driver'] = oldDriver
settingsManager.settings['speech']['module'] = oldModule
settingsManager.settings['speech']['voice'] = oldVoice
self.env['runtime']['outputManager'].presentText(f"Failed to apply voice: {str(e)}", interrupt=False)
self.env['runtime']['outputManager'].presentText("Reverted to previous settings", interrupt=False)
self.env['runtime']['outputManager'].playSound('Error')
else:
self.env['runtime']['outputManager'].presentText("Speech driver not available", interrupt=False)
self.env['runtime']['outputManager'].playSound('Error')
except Exception as e:
self.env['runtime']['outputManager'].presentText(f"Error applying voice: {str(e)}", interrupt=False)
self.env['runtime']['outputManager'].playSound('Error')
def setCallback(self, callback):
pass

View File

@ -0,0 +1,182 @@
#!/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. If you can hear this message clearly, press Enter to accept this voice. Otherwise, wait 30 seconds to cancel."
def shutdown(self):
pass
def getDescription(self):
return "Configure speech module and voice with testing"
def run(self):
self.env['runtime']['outputManager'].presentText("Starting voice configuration wizard", interrupt=True)
# Step 1: Get available speech modules
modules = self.getSpeechdModules()
if not modules:
self.env['runtime']['outputManager'].presentText("No speech-dispatcher modules found. Please install speech-dispatcher.", interrupt=True)
self.env['runtime']['outputManager'].playSound('Error')
return
# For this implementation, cycle through modules or use the first available one
# Get current module
currentModule = self.env['runtime']['settingsManager'].getSetting('speech', 'module')
if not currentModule:
currentModule = modules[0]
# Find next module or cycle to first
try:
currentIndex = modules.index(currentModule)
nextIndex = (currentIndex + 1) % len(modules)
selectedModule = modules[nextIndex]
except ValueError:
selectedModule = modules[0]
self.env['runtime']['outputManager'].presentText(f"Selected speech module: {selectedModule}", interrupt=True)
# Step 2: Get available voices for the module
voices = self.getModuleVoices(selectedModule)
if not voices:
self.env['runtime']['outputManager'].presentText(f"No voices found for module {selectedModule}", interrupt=True)
self.env['runtime']['outputManager'].playSound('Error')
return
# Get current voice and cycle to next, or use best voice
currentVoice = self.env['runtime']['settingsManager'].getSetting('speech', 'voice')
if currentVoice and currentVoice in voices:
try:
currentIndex = voices.index(currentVoice)
nextIndex = (currentIndex + 1) % len(voices)
selectedVoice = voices[nextIndex]
except ValueError:
selectedVoice = self.selectBestVoice(voices)
else:
selectedVoice = self.selectBestVoice(voices)
self.env['runtime']['outputManager'].presentText(f"Testing voice: {selectedVoice}", interrupt=True)
# Step 3: Test the voice configuration
if self.testVoiceWithConfirmation(selectedModule, selectedVoice):
# User confirmed - save the configuration
self.env['runtime']['settingsManager'].setSetting('speech', 'driver', 'speechdDriver')
self.env['runtime']['settingsManager'].setSetting('speech', 'module', selectedModule)
self.env['runtime']['settingsManager'].setSetting('speech', 'voice', selectedVoice)
self.env['runtime']['outputManager'].presentText("Voice configuration saved successfully!", interrupt=True)
self.env['runtime']['outputManager'].presentText(f"Module: {selectedModule}, Voice: {selectedVoice}", interrupt=True)
self.env['runtime']['outputManager'].playSound('Accept')
else:
# User cancelled or test failed
self.env['runtime']['outputManager'].presentText("Voice configuration cancelled. Settings unchanged.", interrupt=True)
self.env['runtime']['outputManager'].playSound('Cancel')
def getSpeechdModules(self):
"""Get list of available speech-dispatcher 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 list of voices for a specific speech 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-ng voice line 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 selectBestVoice(self, voices):
"""Select the best voice from available voices, preferring English"""
# Look for English voices first
for voice in voices:
if any(lang in voice.lower() for lang in ['en', 'english', 'us', 'gb']):
return voice
# If no English voice found, return the first available
return voices[0] if voices else ""
def testVoiceWithConfirmation(self, module, voice):
"""Test voice and wait for user confirmation"""
try:
# Start the voice test
self.env['runtime']['outputManager'].presentText("Starting voice test. Listen carefully.", interrupt=True)
time.sleep(1) # Brief pause
# Use spd-say to test the voice
process = subprocess.Popen([
'spd-say', '-o', module, '-y', voice, self.testMessage
])
# Wait for the test message to finish (give it some time)
time.sleep(2)
# Now wait for user input
self.env['runtime']['outputManager'].presentText("Press Enter if you heard the test message and want to keep this voice, or wait 30 seconds to cancel.", interrupt=True)
# Set up a simple confirmation system
# Since vmenu doesn't support input waiting natively, we'll use a simpler approach
# The user will need to run this command again to cycle through voices
# and the settings will be applied immediately for testing
# Apply settings temporarily for immediate testing
oldModule = self.env['runtime']['settingsManager'].getSetting('speech', 'module')
oldVoice = self.env['runtime']['settingsManager'].getSetting('speech', 'voice')
self.env['runtime']['settingsManager'].setSetting('speech', 'module', module)
self.env['runtime']['settingsManager'].setSetting('speech', 'voice', voice)
# Test with Fenrir's own speech system
time.sleep(1)
self.env['runtime']['outputManager'].presentText("This is how the new voice sounds in Fenrir. Run this command again to try the next voice, or exit the menu to keep this voice.", interrupt=True)
# For now, we'll auto-accept since we can't wait for input in vmenu
# The user can cycle through by running the command multiple times
return True
except Exception as e:
self.env['runtime']['outputManager'].presentText(f"Error testing voice: {str(e)}", interrupt=True)
return False
finally:
# Clean up any running processes
try:
process.terminate()
except:
pass
def setCallback(self, callback):
pass

View File

@ -0,0 +1,136 @@
#!/usr/bin/env python3
import subprocess
class command():
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
self.testMessage = "This is a voice preview. The quick brown fox jumps over the lazy dog."
def shutdown(self):
pass
def getDescription(self):
return "Cycle through available voices (run multiple times to browse)"
def run(self):
# Get available modules
modules = self.getSpeechdModules()
if not modules:
self.env['runtime']['outputManager'].presentText("No speech-dispatcher modules found", interrupt=True)
self.env['runtime']['outputManager'].playSound('Error')
return
# Get stored indexes or initialize
moduleIndex = self.env['commandBuffer'].get('voicePreviewModuleIndex', 0)
voiceIndex = self.env['commandBuffer'].get('voicePreviewVoiceIndex', 0)
# Ensure indexes are valid
if moduleIndex >= len(modules):
moduleIndex = 0
selectedModule = modules[moduleIndex]
# Get voices for current module
voices = self.getModuleVoices(selectedModule)
if not voices:
self.env['runtime']['outputManager'].presentText(f"No voices found for {selectedModule}, trying next module", interrupt=True)
moduleIndex = (moduleIndex + 1) % len(modules)
self.env['commandBuffer']['voicePreviewModuleIndex'] = moduleIndex
self.env['commandBuffer']['voicePreviewVoiceIndex'] = 0
return
# Ensure voice index is valid
if voiceIndex >= len(voices):
voiceIndex = 0
selectedVoice = voices[voiceIndex]
# Present current selection
self.env['runtime']['outputManager'].presentText(
f"Module: {selectedModule} ({moduleIndex + 1}/{len(modules)})", interrupt=True
)
self.env['runtime']['outputManager'].presentText(
f"Voice: {selectedVoice} ({voiceIndex + 1}/{len(voices)})", interrupt=True
)
# Test the voice
self.env['runtime']['outputManager'].presentText("Testing voice...", interrupt=True)
if self.previewVoice(selectedModule, selectedVoice):
self.env['runtime']['outputManager'].presentText("Voice test completed", interrupt=True)
self.env['runtime']['outputManager'].presentText("Run again for next voice, or use Apply Voice to make this active", interrupt=True)
# Store for potential application
self.env['commandBuffer']['lastTestedModule'] = selectedModule
self.env['commandBuffer']['lastTestedVoice'] = selectedVoice
self.env['runtime']['outputManager'].playSound('Accept')
else:
self.env['runtime']['outputManager'].presentText("Voice test failed", interrupt=True)
self.env['runtime']['outputManager'].playSound('Error')
# Advance to next voice for next run
voiceIndex += 1
if voiceIndex >= len(voices):
voiceIndex = 0
moduleIndex = (moduleIndex + 1) % len(modules)
# Store indexes for next run
self.env['commandBuffer']['voicePreviewModuleIndex'] = moduleIndex
self.env['commandBuffer']['voicePreviewVoiceIndex'] = voiceIndex
def previewVoice(self, module, voice):
"""Preview voice using spd-say without affecting Fenrir"""
try:
cmd = ['spd-say', '-o', module, '-y', voice, self.testMessage]
result = subprocess.run(cmd, timeout=15)
return result.returncode == 0
except Exception:
return False
def getSpeechdModules(self):
"""Get list of available speech-dispatcher 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 list of voices for a specific speech 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-ng voice line 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

View File

@ -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

View File

@ -0,0 +1,62 @@
#!/usr/bin/env python3
import subprocess
class command():
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def getDescription(self):
return "Test current voice configuration"
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')
# Present current configuration
self.env['runtime']['outputManager'].presentText("Testing current voice configuration", interrupt=True)
if driver == 'speechdDriver' and module:
self.env['runtime']['outputManager'].presentText(f"Driver: {driver}", interrupt=True)
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:
pass
try:
pitchPercent = int(float(pitch) * 100)
self.env['runtime']['outputManager'].presentText(f"Pitch: {pitchPercent} percent", interrupt=True)
except:
pass
try:
volumePercent = int(float(volume) * 100)
self.env['runtime']['outputManager'].presentText(f"Volume: {volumePercent} percent", interrupt=True)
except:
pass
# Test message
testMessage = "This is a test of your current voice configuration. The quick brown fox jumps over the lazy dog. Numbers: one, two, three, four, five. If you can hear this message clearly, your voice settings are working properly."
self.env['runtime']['outputManager'].presentText("Voice test:", interrupt=True)
self.env['runtime']['outputManager'].presentText(testMessage, interrupt=True)
self.env['runtime']['outputManager'].playSound('Accept')
def setCallback(self, callback):
pass

View File

@ -0,0 +1,212 @@
#!/usr/bin/env python3
import subprocess
import importlib.util
import os
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 {self.voice} from {self.module}", interrupt=True)
# Test voice first
if self.testVoice():
self.env['runtime']['outputManager'].presentText("Voice test completed. Press Enter again to apply.", interrupt=True)
# Store for confirmation
self.env['commandBuffer']['pendingVoiceModule'] = self.module
self.env['commandBuffer']['pendingVoiceVoice'] = self.voice
self.env['commandBuffer']['voiceTestCompleted'] = True
self.env['runtime']['outputManager'].playSound('Accept')
else:
self.env['runtime']['outputManager'].presentText("Voice test failed", interrupt=True)
self.env['runtime']['outputManager'].playSound('Error')
except Exception as e:
self.env['runtime']['outputManager'].presentText(f"Voice selection error: {str(e)}", interrupt=True)
self.env['runtime']['outputManager'].playSound('Error')
def testVoice(self):
"""Test voice with spd-say"""
try:
cmd = ['spd-say', '-o', self.module, '-y', self.voice, self.testMessage]
result = subprocess.run(cmd, timeout=8)
return result.returncode == 0
except Exception:
return False
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)
# Apply to runtime settings
settingsManager = self.env['runtime']['settingsManager']
settingsManager.settings['speech']['driver'] = 'speechdDriver'
settingsManager.settings['speech']['module'] = module
settingsManager.settings['speech']['voice'] = voice
# Reinitialize speech driver
if 'speechDriver' in self.env['runtime']:
speechDriver = self.env['runtime']['speechDriver']
speechDriver.shutdown()
speechDriver.initialize(self.env)
self.env['runtime']['outputManager'].presentText("Voice applied successfully!", interrupt=True)
self.env['runtime']['outputManager'].playSound('Accept')
# Clear pending state
self.env['commandBuffer']['voiceTestCompleted'] = False
# Exit vmenu after successful application
self.env['runtime']['vmenuManager'].setActive(False)
except Exception as e:
self.env['runtime']['outputManager'].presentText(f"Apply failed: {str(e)}", interrupt=True)
self.env['runtime']['outputManager'].playSound('Error')
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 (limit to prevent huge menus)
voices = getModuleVoices(module)
if voices:
# Limit voices to keep menu usable
if len(voices) > 50:
voices = voices[:50]
moduleMenu['Note: Showing first 50 voices Action'] = createInfoCommand(f"Module {module} has {len(getModuleVoices(module))} voices, showing first 50", env)
# 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

View File

@ -152,6 +152,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:

View File

@ -4,5 +4,5 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributers.
version = "2025.06.12"
version = "2025.06.15"
codeName = "testing"