combined configure_fenrir and configure_speechd into a single script. This is the go-to for editing Fenrir settings without doing it by hand.

This commit is contained in:
Storm Dragon 2024-12-05 06:06:03 -05:00
parent 6785fde7c9
commit af857d7976
2 changed files with 153 additions and 279 deletions

View File

@ -4,10 +4,13 @@ import os
import sys import sys
import configparser import configparser
import dialog import dialog
from typing import Dict, List, Optional
import subprocess import subprocess
import time
import select
import tempfile
from typing import Dict, List, Optional, Tuple
class FenrirConfig: class FenrirConfigTool:
def __init__(self): def __init__(self):
os.environ['DIALOGOPTS'] = '--no-lines --visit-items' os.environ['DIALOGOPTS'] = '--no-lines --visit-items'
self.tui = dialog.Dialog(dialog="dialog") self.tui = dialog.Dialog(dialog="dialog")
@ -18,23 +21,20 @@ class FenrirConfig:
self.escalate_privileges() self.escalate_privileges()
sys.exit(0) sys.exit(0)
# Navigation instructions for different dialog types
self.instructions = { self.instructions = {
'menu': "\nNavigation: Use Up/Down arrows to move, Enter to select, Escape to go back", 'menu': "\nNavigation: Use Up/Down arrows to move, Enter to select, Escape to go back",
'radiolist': "\nNavigation: Use Up/Down arrows to move, Space to select option, Enter to confirm, Escape to cancel", 'radiolist': "\nNavigation: Use Up/Down arrows to move, Space to select option, Enter to confirm, Escape to cancel",
'inputbox': "\nEnter your value and press Enter to confirm, or Escape to cancel" 'inputbox': "\nEnter your value and press Enter to confirm, or Escape to cancel"
} }
# Predefined options for certain settings # Configuration presets and help text from original FenrirConfig
self.presetOptions = { self.presetOptions = {
# Drivers
'sound.driver': ['genericDriver', 'gstreamerDriver'], 'sound.driver': ['genericDriver', 'gstreamerDriver'],
'speech.driver': ['speechdDriver', 'genericDriver'], 'speech.driver': ['speechdDriver', 'genericDriver'],
'braille.driver': ['dummyDriver', 'brailttyDriver', 'brlapiDriver'], 'braille.driver': ['dummyDriver', 'brailttyDriver', 'brlapiDriver'],
'screen.driver': ['vcsaDriver', 'dummyDriver', 'ptyDriver', 'debugDriver'], 'screen.driver': ['vcsaDriver', 'dummyDriver', 'ptyDriver', 'debugDriver'],
'keyboard.driver': ['evdevDriver', 'dummyDriver'], 'keyboard.driver': ['evdevDriver', 'dummyDriver'],
'remote.driver': ['unixDriver', 'tcpDriver'], 'remote.driver': ['unixDriver', 'tcpDriver'],
# Other preset options
'braille.flushMode': ['word', 'char', 'fix', 'none'], 'braille.flushMode': ['word', 'char', 'fix', 'none'],
'braille.cursorFocusMode': ['page', 'fixCell'], 'braille.cursorFocusMode': ['page', 'fixCell'],
'braille.cursorFollowMode': ['review', 'last', 'none'], 'braille.cursorFollowMode': ['review', 'last', 'none'],
@ -43,7 +43,6 @@ class FenrirConfig:
'general.debugMode': ['File', 'Print'] 'general.debugMode': ['File', 'Print']
} }
# Help text for certain options
self.helpText = { self.helpText = {
'sound.volume': 'Volume level from 0 (quietest) to 1.0 (loudest)', 'sound.volume': 'Volume level from 0 (quietest) to 1.0 (loudest)',
'speech.rate': 'Speech rate from 0 (slowest) to 1.0 (fastest)', 'speech.rate': 'Speech rate from 0 (slowest) to 1.0 (fastest)',
@ -53,18 +52,15 @@ class FenrirConfig:
} }
def check_root(self) -> bool: def check_root(self) -> bool:
"""Check if the script is running with root privileges"""
return os.geteuid() == 0 return os.geteuid() == 0
def find_privilege_escalation_tool(self) -> Optional[str]: def find_privilege_escalation_tool(self) -> Optional[str]:
"""Find available privilege escalation tool (sudo or doas)"""
for tool in ['sudo', 'doas']: for tool in ['sudo', 'doas']:
if subprocess.run(['which', tool], stdout=subprocess.PIPE).returncode == 0: if subprocess.run(['which', tool], stdout=subprocess.PIPE).returncode == 0:
return tool return tool
return None return None
def escalate_privileges(self): def escalate_privileges(self):
"""Re-run the script with elevated privileges"""
tool = self.find_privilege_escalation_tool() tool = self.find_privilege_escalation_tool()
if not tool: if not tool:
self.tui.msgbox("Error: Neither sudo nor doas found. Please run this script as root.") self.tui.msgbox("Error: Neither sudo nor doas found. Please run this script as root.")
@ -78,13 +74,6 @@ class FenrirConfig:
self.tui.msgbox(f"Error escalating privileges: {str(e)}") self.tui.msgbox(f"Error escalating privileges: {str(e)}")
sys.exit(1) sys.exit(1)
def check_permissions(self) -> bool:
"""Check if we have write permissions to the settings file"""
if not os.access(self.settingsFile, os.W_OK):
self.tui.msgbox("Error: Insufficient permissions to modify the settings file even with root privileges.")
return False
return True
def is_boolean_option(self, value: str) -> bool: def is_boolean_option(self, value: str) -> bool:
"""Check if the current value is likely a boolean option""" """Check if the current value is likely a boolean option"""
return value.lower() in ['true', 'false'] return value.lower() in ['true', 'false']
@ -144,52 +133,179 @@ class FenrirConfig:
return value return value
return None return None
def run(self): def run_command(self, cmd: List[str], needsRoot: bool = False) -> Optional[str]:
if not self.check_permissions(): try:
if needsRoot and not self.check_root():
tool = self.find_privilege_escalation_tool()
if tool:
cmd = [tool] + cmd
result = subprocess.run(cmd, capture_output=True, text=True)
return result.stdout.strip() if result.returncode == 0 else None
except Exception as e:
self.tui.msgbox(f"Error running command {' '.join(cmd)}: {e}")
return None
def get_speechd_modules(self) -> List[str]:
output = self.run_command(['spd-say', '-O'], True)
if output:
lines = output.split('\n')
return [line.strip() for line in lines[1:] if line.strip()]
return []
def process_espeak_voice(self, voiceLine: str) -> Optional[str]:
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 get_module_voices(self, moduleName: str) -> List[str]:
output = self.run_command(['spd-say', '-o', moduleName, '-L'], True)
if output:
lines = output.split('\n')
voices = []
for line in lines[1:]:
if not line.strip():
continue
if moduleName.lower() == 'espeak-ng':
voice = self.process_espeak_voice(line)
if voice:
voices.append(voice)
else:
voices.append(line.strip())
return voices
return []
def configure_speech(self) -> None:
moduleList = self.get_speechd_modules()
if not moduleList:
self.tui.msgbox("No speech-dispatcher modules found!")
return return
code, moduleChoice = self.tui.menu(
"Select speech module:" + self.instructions['menu'],
choices=[(module, "") for module in moduleList]
)
if code != self.tui.OK:
return
voiceList = self.get_module_voices(moduleChoice)
if not voiceList:
self.tui.msgbox(f"No voices found for module {moduleChoice}")
return
code, voice = self.tui.menu(
f"Select voice for {moduleChoice}:" + self.instructions['menu'],
choices=[(v, "") for v in voiceList]
)
if code != self.tui.OK:
return
# Test voice configuration
if self.test_voice(moduleChoice, voice):
config = configparser.ConfigParser()
config.read(self.settingsFile)
if 'speech' not in config:
config['speech'] = {}
config['speech'].update({
'driver': 'speechdDriver',
'module': moduleChoice,
'voice': voice,
'enabled': 'True',
'rate': '0.25',
'pitch': '0.5',
'volume': '1.0'
})
with open(self.settingsFile, 'w') as configfile:
config.write(configfile)
self.tui.msgbox("Speech configuration updated successfully!\nPlease restart Fenrir for changes to take effect.")
def test_voice(self, moduleName: str, voiceName: str) -> bool:
testMessage = "If you hear this message, press Enter within 30 seconds to confirm."
try:
process = subprocess.Popen(
['spd-say', '-o', moduleName, '-y', voiceName, testMessage]
)
code = self.tui.pause(
"Waiting for voice test...\n"
"Press Enter if you hear the message, or wait for timeout.",
30
)
process.terminate()
return code == self.tui.OK
except Exception as e:
self.tui.msgbox(f"Error testing voice: {e}")
return False
def edit_general_config(self) -> None:
while True: while True:
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read(self.settingsFile) config.read(self.settingsFile)
sections = config.sections() sections = config.sections()
code, section = self.tui.menu( code, section = self.tui.menu(
"Select a section:" + self.instructions['menu'], "Select a section to configure:" + self.instructions['menu'],
choices=[(s, "") for s in sections] + [("Exit", " ")] choices=[(s, "") for s in sections] + [("Go Back", "")]
) )
if section == "Exit": if code != self.tui.OK or section == "Go Back":
break break
while True: while True:
options = config.options(section) options = config.options(section)
choices = [(o, f"Current: {config.get(section, o)}") for o in options] choices = [(o, f"Current: {config.get(section, o)}") for o in options]
choices.append(("Go Back", " ")) choices.append(("Go Back", ""))
code, option = self.tui.menu( code, option = self.tui.menu(
f"Select option to edit in '{section}':" + self.instructions['menu'], f"Select option to edit in '{section}':" + self.instructions['menu'],
choices=choices choices=choices
) )
if option == "Go Back": if code != self.tui.OK or option == "Go Back":
break break
if code == self.tui.OK: currentValue = config.get(section, option)
currentValue = config.get(section, option) newValue = self.get_value_with_presets(section, option, currentValue)
newValue = self.get_value_with_presets(section, option, currentValue)
if newValue is not None and newValue != currentValue: if newValue is not None and newValue != currentValue:
config.set(section, option, newValue) config.set(section, option, newValue)
with open(self.settingsFile, 'w') as configfile: with open(self.settingsFile, 'w') as configfile:
config.write(configfile) config.write(configfile)
self.tui.msgbox("Settings saved successfully.") self.tui.msgbox("Setting updated successfully.")
def run(self):
while True:
code, choice = self.tui.menu(
"Fenrir Configuration Tool" + self.instructions['menu'],
choices=[
("speech-dispatcher", "Configure module and voice"),
("Advanced", "Edit Fenrir settings"),
("Exit", "")
]
)
if code != self.tui.OK or choice == "Exit":
break
if choice == "speech-dispatcher":
self.configure_speech()
elif choice == "Advanced":
self.edit_general_config()
if __name__ == "__main__": if __name__ == "__main__":
configTool = FenrirConfig() configTool = FenrirConfigTool()
try: try:
configTool.run() configTool.run()
except (configparser.Error, dialog.error) as e:
sys.exit(0)
except Exception as e: except Exception as e:
print(f"Unexpected error occurred: {str(e)}", file=sys.stderr) print(f"Unexpected error occurred: {str(e)}", file=sys.stderr)
sys.exit(1) sys.exit(1)

View File

@ -1,242 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Voice configuration script
# By Storm Dragon
import configparser
import subprocess
import os
import sys
import time
import select
import tempfile
def check_root_access():
"""Check if script is running as root and determine available privilege escalation command"""
if os.geteuid() == 0:
return True, None
sudoPath = run_command(['which', 'sudo'])
if sudoPath:
return False, 'sudo'
doasPath = run_command(['which', 'doas'])
if doasPath:
return False, 'doas'
return False, None
def run_command(cmd, needsRoot=False, privilegeCmd=None):
"""Run a command with optional root privileges"""
try:
if needsRoot and privilegeCmd and os.geteuid() != 0:
fullCmd = [privilegeCmd] + cmd
else:
fullCmd = cmd
result = subprocess.run(fullCmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Command failed: {' '.join(fullCmd)}")
print(f"Error: {result.stderr}")
return None
return result.stdout.strip()
except Exception as e:
print(f"Error running command {' '.join(cmd)}: {e}")
return None
def run_command_with_output(cmd, needsRoot=False, privilegeCmd=None):
"""Run a command and return its content directly"""
try:
if needsRoot and privilegeCmd and os.geteuid() != 0:
cmd = [privilegeCmd] + cmd
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) as process:
output, error = process.communicate()
if process.returncode != 0:
print(f"Error: {error}")
return None
return output
except Exception as e:
print(f"Error running command {' '.join(cmd)}: {e}")
return None
def get_speechd_modules(privilegeCmd):
"""Get list of available speech-dispatcher modules"""
output = run_command_with_output(['spd-say', '-O'], True, privilegeCmd)
if output:
lines = output.split('\n')
if len(lines) > 1:
return [line.strip() for line in lines[1:] if line.strip()]
return []
def process_espeak_voice(voiceLine):
"""Process espeak-ng voice line to get voice name from last two columns"""
parts = [p for p in voiceLine.split() if p]
if len(parts) < 2:
return None
langCode = parts[-2].lower()
variant = parts[-1].lower()
if variant and variant != 'none':
return f"{langCode}+{variant}"
return langCode
def get_module_voices(moduleName, privilegeCmd):
"""Get available voices for a specific module"""
output = run_command_with_output(['spd-say', '-o', moduleName, '-L'], True, privilegeCmd)
if output:
lines = output.split('\n')
if len(lines) > 1:
voices = []
for line in lines[1:]:
if not line.strip():
continue
if moduleName.lower() == 'espeak-ng':
processedVoice = process_espeak_voice(line)
if processedVoice:
voices.append(processedVoice)
else:
voices.append(line.strip())
return voices
return []
def display_paginated_voices(voiceList):
"""Display voices in pages of 30"""
pageSize = 30
totalVoices = len(voiceList)
totalPages = (totalVoices + pageSize - 1) // pageSize
currentPage = 0
while currentPage < totalPages:
start = currentPage * pageSize
end = min(start + pageSize, totalVoices)
print(f"\nShowing voices {start + 1}-{end} of {totalVoices} (Page {currentPage + 1} of {totalPages})")
print("Available voices:")
for i, voice in enumerate(voiceList[start:end], start + 1):
print(f"{i}. {voice}")
if currentPage < totalPages - 1:
print("\nPress Enter for next page, 'b' for previous page, or enter a number to select a voice (0 to go back): ")
else:
print("\nPress 'b' for previous page, or enter a number to select a voice (0 to go back): ")
choice = input().strip().lower()
if choice == '':
if currentPage < totalPages - 1:
currentPage += 1
elif choice == 'b':
if currentPage > 0:
currentPage -= 1
elif choice.isdigit():
choice = int(choice)
if choice == 0:
return None
if 1 <= choice <= totalVoices:
return voiceList[choice - 1]
print("Invalid selection!")
else:
print("Invalid input!")
return None
def test_voice_config(moduleName, voiceName, privilegeCmd):
"""Test if the selected voice works"""
print("\nTesting voice configuration...")
testMessage = "If you hear this message, press Enter within 30 seconds to confirm. Otherwise, wait for timeout to return to the menu."
try:
cmd = ['spd-say', '-o', moduleName, '-y', voiceName, testMessage]
if privilegeCmd and os.geteuid() != 0:
cmd = [privilegeCmd] + cmd
process = subprocess.Popen(cmd)
startTime = time.time()
while time.time() - startTime < 30:
if select.select([sys.stdin], [], [], 0.0)[0]:
userInput = input()
process.terminate()
return True
time.sleep(0.1)
process.terminate()
return False
except Exception as e:
print(f"Error testing voice: {e}")
return False
def update_fenrir_config(configPath, moduleName, voiceName, privilegeCmd):
"""Update Fenrir configuration with new speech settings"""
config = configparser.ConfigParser()
try:
configContent = run_command_with_output(['cat', configPath], True, privilegeCmd)
if not configContent:
return False
with tempfile.NamedTemporaryFile(mode='w', delete=False) as tempFile:
tempPath = tempFile.name
tempFile.write(configContent)
config.read(tempPath)
if 'speech' not in config:
config['speech'] = {}
config['speech']['driver'] = 'speechdDriver'
config['speech']['module'] = moduleName
config['speech']['voice'] = voiceName
config['speech']['enabled'] = 'True'
config['speech']['rate'] = '0.25'
config['speech']['pitch'] = '0.5'
config['speech']['volume'] = '1.0'
with open(tempPath, 'w') as configFile:
config.write(configFile)
result = run_command(['cp', tempPath, configPath], True, privilegeCmd)
os.unlink(tempPath)
return result is not None
except Exception as e:
print(f"Error updating configuration: {e}")
if 'tempPath' in locals():
try:
os.unlink(tempPath)
except:
pass
return False
def main():
configPath = '/etc/fenrirscreenreader/settings/settings.conf'
isRoot, privilegeCmd = check_root_access()
if not isRoot and not privilegeCmd:
print("Error: This script needs root privileges. Please run with sudo or doas.")
return
configExists = run_command(['test', '-f', configPath], True, privilegeCmd) is not None
if not configExists:
print(f"Configuration file not found: {configPath}")
return
while True:
print("\nFenrir Speech-Dispatcher Configuration")
print("=====================================")
print("\nGetting available speech-dispatcher modules...")
moduleList = get_speechd_modules(privilegeCmd)
if not moduleList:
print("No speech-dispatcher modules found!")
return
print("\nAvailable modules:")
for i, moduleName in enumerate(moduleList, 1):
print(f"{i}. {moduleName}")
while True:
try:
moduleChoice = int(input("\nSelect module number (or 0 to exit): "))
if moduleChoice == 0:
return
if 1 <= moduleChoice <= len(moduleList):
selectedModule = moduleList[moduleChoice - 1]
break
print("Invalid selection!")
except ValueError:
print("Please enter a number!")
print(f"\nGetting voices for {selectedModule}...")
voiceList = get_module_voices(selectedModule, privilegeCmd)
if not voiceList:
print(f"No voices found for module {selectedModule}")
continue
selectedVoice = display_paginated_voices(voiceList)
if not selectedVoice:
continue
print("\nTesting selected voice configuration...")
if test_voice_config(selectedModule, selectedVoice, privilegeCmd):
print("\nVoice test successful!")
if update_fenrir_config(configPath, selectedModule, selectedVoice, privilegeCmd):
print("\nConfiguration updated successfully!")
print("Please restart Fenrir for changes to take effect")
return
else:
print("Failed to update configuration!")
else:
print("\nVoice test failed or timed out. Please try again.")
if __name__ == "__main__":
main()