#!/usr/bin/env python3 import os import sys import configparser import dialog from typing import Dict, List, Optional import subprocess class FenrirConfig: def __init__(self): os.environ['DIALOGOPTS'] = '--no-lines --visit-items' self.tui = dialog.Dialog(dialog="dialog") self.settingsFile = '/etc/fenrirscreenreader/settings/settings.conf' # Check if we need to re-run with elevated privileges if not self.check_root(): self.escalate_privileges() sys.exit(0) # Navigation instructions for different dialog types self.instructions = { '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", 'inputbox': "\nEnter your value and press Enter to confirm, or Escape to cancel" } # Predefined options for certain settings self.presetOptions = { # Drivers 'sound.driver': ['genericDriver', 'gstreamerDriver'], 'speech.driver': ['speechdDriver', 'genericDriver'], 'braille.driver': ['dummyDriver', 'brailttyDriver', 'brlapiDriver'], 'screen.driver': ['vcsaDriver', 'dummyDriver', 'ptyDriver', 'debugDriver'], 'keyboard.driver': ['evdevDriver', 'dummyDriver'], 'remote.driver': ['unixDriver', 'tcpDriver'], # Other preset options 'braille.flushMode': ['word', 'char', 'fix', 'none'], 'braille.cursorFocusMode': ['page', 'fixCell'], 'braille.cursorFollowMode': ['review', 'last', 'none'], 'keyboard.charEchoMode': ['0', '1', '2'], 'general.punctuationLevel': ['none', 'some', 'most', 'all'], 'general.debugMode': ['File', 'Print'] } # Help text for certain 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)', 'keyboard.charEchoMode': '0 = None, 1 = always, 2 = only while capslock', 'braille.flushMode': 'word = flush after words, char = flush after chars, fix = flush after time, none = manual flush' } def check_root(self) -> bool: """Check if the script is running with root privileges""" return os.geteuid() == 0 def find_privilege_escalation_tool(self) -> Optional[str]: """Find available privilege escalation tool (sudo or doas)""" for tool in ['sudo', 'doas']: if subprocess.run(['which', tool], stdout=subprocess.PIPE).returncode == 0: return tool return None def escalate_privileges(self): """Re-run the script with elevated privileges""" tool = self.find_privilege_escalation_tool() if not tool: self.tui.msgbox("Error: Neither sudo nor doas found. Please run this script as root.") sys.exit(1) try: scriptPath = os.path.abspath(sys.argv[0]) command = [tool, sys.executable, scriptPath] + sys.argv[1:] os.execvp(tool, command) except Exception as e: self.tui.msgbox(f"Error escalating privileges: {str(e)}") 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: """Check if the current value is likely a boolean option""" return value.lower() in ['true', 'false'] def validate_input(self, section: str, option: str, value: str) -> tuple[bool, str]: """Validate user input based on the option type and constraints""" try: if option.endswith('volume') or option.endswith('rate') or option.endswith('pitch'): floatVal = float(value) if not 0 <= floatVal <= 1.0: return False, "Value must be between 0 and 1.0" return True, value except ValueError: return False, "Invalid number format" def get_value_with_presets(self, section: str, option: str, currentValue: str) -> Optional[str]: """Get value using appropriate input method based on option type""" key = f"{section}.{option}" # Handle boolean options if self.is_boolean_option(currentValue): choices = [ ('True', '', currentValue.lower() == 'true'), ('False', '', currentValue.lower() == 'false') ] code, tag = self.tui.radiolist( f"Select value for '{option}'" + self.instructions['radiolist'], choices=choices ) return tag if code == self.tui.OK else None # Handle other preset options elif key in self.presetOptions: choices = [(opt, "", opt == currentValue) for opt in self.presetOptions[key]] code, tag = self.tui.radiolist( f"Select value for '{option}'" + (f"\n{self.helpText[key]}" if key in self.helpText else "") + self.instructions['radiolist'], choices=choices ) return tag if code == self.tui.OK else None # Handle free-form input else: helpText = self.helpText.get(key, "") code, value = self.tui.inputbox( f"Enter value for '{option}'" + (f"\n{helpText}" if helpText else "") + self.instructions['inputbox'], init=currentValue ) if code == self.tui.OK: isValid, message = self.validate_input(section, option, value) if not isValid: self.tui.msgbox(f"Invalid input: {message}") return None return value return None def run(self): if not self.check_permissions(): return while True: config = configparser.ConfigParser() config.read(self.settingsFile) sections = config.sections() code, section = self.tui.menu( "Select a section:" + self.instructions['menu'], choices=[(s, "") for s in sections] + [("Exit", " ")] ) if section == "Exit": break while True: options = config.options(section) choices = [(o, f"Current: {config.get(section, o)}") for o in options] choices.append(("Go Back", " ")) code, option = self.tui.menu( f"Select option to edit in '{section}':" + self.instructions['menu'], choices=choices ) if option == "Go Back": break if code == self.tui.OK: currentValue = config.get(section, option) newValue = self.get_value_with_presets(section, option, currentValue) if newValue is not None and newValue != currentValue: config.set(section, option, newValue) with open(self.settingsFile, 'w') as configfile: config.write(configfile) self.tui.msgbox("Settings saved successfully.") if __name__ == "__main__": configTool = FenrirConfig() try: configTool.run() except (configparser.Error, dialog.error) as e: sys.exit(0) except Exception as e: print(f"Unexpected error occurred: {str(e)}", file=sys.stderr) sys.exit(1)