diff --git a/tools/configure-fenrir b/tools/configure-fenrir new file mode 100755 index 00000000..63b09dbb --- /dev/null +++ b/tools/configure-fenrir @@ -0,0 +1,195 @@ +#!/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) diff --git a/tools/fenrir-conf b/tools/fenrir-conf deleted file mode 100755 index 16c5c6a5..00000000 --- a/tools/fenrir-conf +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 - -# Fenrir TTY screen reader -# By Chrys, Storm Dragon, and contributers. - -import os -import configparser -import dialog - -# Make sure dialog is accessible -os.environ['DIALOGOPTS'] = '--no-lines --visit-items' -# Initialize the dialog -tui = dialog.Dialog(dialog="dialog") - -# Define the path to the settings file -settings_file = '/etc/fenrirscreenreader/settings/settings.conf' - -# Check write permissions for the settings file -if not os.access(settings_file, os.W_OK): - tui.msgbox("Error: Insufficient permissions to modify the settings file. Please run as root or with sudo.") - exit() - -while True: - # Load the settings file - config = configparser.ConfigParser() - config.read(settings_file) - - # Get a list of sections in the settings file - sections = config.sections() - - # Select a section. - code, section = tui.menu("Select a section:", choices=[(s, "") for s in sections] + [("Exit", " ")]) - - # Exit if the "Exit" option is chosen - if section == "Exit": - break - - while True: - # Get the options in the selected section - options = config.options(section) - - # Select a value to edit using dialog - code, option = tui.menu(f"Select a value to edit in '{section}':", choices=[(o, "") for o in options] + [("Go Back", " ")]) - - # Go back to the section menu if the "Go Back" option is chosen - if option == "Go Back": - break - - # If something is selected, prompt for a new value. - if code == tui.OK: - value = config.get(section, option) - code, new_value = tui.inputbox(f"Enter a new value for '{option}':", init=value) - - # If a new setting is provided, update the configuration - if code == tui.OK: - config.set(section, option, new_value) - - # Save changes. - with open(settings_file, 'w') as configfile: - config.write(configfile) - - tui.msgbox("Fenrir settings saved.") - else: - tui.msgbox("Changes discarded. Your Fenrir configuration has not been modified.") - else: - tui.msgbox("Canceled.")