From 8d8f5dc613343edef6038d030820075ee286e1db Mon Sep 17 00:00:00 2001
From: Storm Dragon <stormdragon2976@gmail.com>
Date: Wed, 4 Dec 2024 14:06:34 -0500
Subject: [PATCH] Improved configuration tool.

---
 tools/configure-fenrir | 195 +++++++++++++++++++++++++++++++++++++++++
 tools/fenrir-conf      |  66 --------------
 2 files changed, 195 insertions(+), 66 deletions(-)
 create mode 100755 tools/configure-fenrir
 delete mode 100755 tools/fenrir-conf

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