Code cleanups, fixes to systemd files, url corrections.

This commit is contained in:
Storm Dragon
2025-11-24 08:44:49 -05:00
parent 87553bdc38
commit c184cf023a
12 changed files with 19 additions and 450 deletions

View File

@@ -1,306 +0,0 @@
#!/usr/bin/env python3
import os
import sys
import configparser
import dialog
import subprocess
import time
import select
import tempfile
from typing import Dict, List, Optional, Tuple
class FenrirConfigTool:
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)
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"
}
# Configuration presets and help text from original FenrirConfig
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']
}
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'
}
def check_root(self) -> bool:
return os.geteuid() == 0
def find_privilege_escalation_tool(self) -> Optional[str]:
for tool in ['sudo', 'doas']:
if subprocess.run(['which', tool], stdout=subprocess.PIPE).returncode == 0:
return tool
return None
def escalate_privileges(self):
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 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_command(self, cmd: List[str], needsRoot: bool = False) -> Optional[str]:
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
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(interpolation=None)
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:
config = configparser.ConfigParser(interpolation=None)
config.read(self.settingsFile)
sections = config.sections()
code, section = self.tui.menu(
"Select a section to configure:" + self.instructions['menu'],
choices=[(s, "") for s in sections] + [("Go Back", "")]
)
if code != self.tui.OK or section == "Go Back":
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 code != self.tui.OK or option == "Go Back":
break
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("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__":
configTool = FenrirConfigTool()
try:
configTool.run()
except Exception as e:
print(f"Unexpected error occurred: {str(e)}", file=sys.stderr)
sys.exit(1)

View File

@@ -1,26 +0,0 @@
#!/bin/bash
sinks=(`pacmd list-sinks | sed -n -e 's/\**[[:space:]]index:[[:space:]]\([[:digit:]]\)/\1/p'`)
sinks_count=${#sinks[@]}
active_sink_index=`pacmd list-sinks | sed -n -e 's/\*[[:space:]]index:[[:space:]]\([[:digit:]]\)/\1/p'`
newSink=${sinks[0]}
ord=0
while [ $ord -lt $sinks_count ];
do
echo ${sinks[$ord]}
if [ ${sinks[$ord]} -gt $active_sink_index ] ; then
newSink=${sinks[$ord]}
break
fi
let ord++
done
pactl list short sink-inputs|while read stream; do
streamId=$(echo $stream|cut '-d ' -f1)
echo "moving stream $streamId"
pactl move-sink-input "$streamId" "$newSink"
done
pacmd set-default-sink "$newSink"
#https://unix.stackexchange.com/questions/65246/change-pulseaudio-input-output-from-shell

View File

@@ -1,4 +1,4 @@
from pyudev import Context
context = Context()
for device in context.list_devices(subsystem='input'):
'{0} - {1}'.format(device.sys_name, device.device_type)
print('{0} - {1}'.format(device.sys_name, device.device_type))

View File

@@ -1,96 +0,0 @@
#!/bin/bash
# Make sure this script is ran as root
if [[ "$(whoami)" != "root" ]]; then
echo "Please run $0 with oot privileges."
exit 1
fi
# This script checks for, and creates if needed, the fenrirscreenreader user.
# Find out which group to use for uinput
uinput="$(stat -c '%G' /dev/uinput | grep -v root)"
if ! [[ "$uinput" =~ ^[a-zA-Z]+$ ]]; then
groupadd -r uinput
chown root:uinput /dev/uinput
fi
# find out which group to use for /dev/input.
input="$(stat -c '%G' /dev/input/* | grep -v root | head -1)"
if ! [[ "$input" =~ ^[a-zA-Z]+$ ]]; then
# Create the input group
groupadd --system input
echo 'KERNEL=="event*", NAME="input/%k", MODE="660", GROUP="input"' >> /etc/udev/rules.d/99-input.rules
input="input"
fi
# find out which group to use for /dev/tty.
tty="$(stat -c '%G' /dev/tty | grep -v root)"
if ! [[ "$tty" =~ ^[a-zA-Z]+$ ]]; then
# Create the tty group
groupadd --system tty
echo 'KERNEL=="event*", NAME="tty/%k", MODE="660", GROUP="tty"' >> /etc/udev/rules.d/99-tty.rules
tty="tty"
fi
# Add fenrirscreenreader
id fenrirscreenreader &> /dev/null ||
useradd -m -d /var/fenrirscreenreader -r -G $input,$tty,$uinput -s /bin/nologin -U fenrirscreenreader
#configure directory structure.
mkdir -p /var/log/fenrirscreenreader /etc/fenrirscreenreader
# Set directory ownership
chown -R fenrirscreenreader:fenrirscreenreader /var/log/fenrirscreenreader
chmod -R 755 /var/log/fenrirscreenreader
chown -R root:fenrirscreenreader /etc/fenrirscreenreader
# Fix permissions on tty#s
for i in /dev/tty[0-9]* ; do
chmod 660 "$i"
done
sudo -Hu fenrirscreenreader mkdir /var/fenrirscreenreader/.config/pulse
# Set up sound
cat << EOF > /var/fenrirscreenreader/.config/pulse/client.conf
# This file is part of PulseAudio.
#
# PulseAudio is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# PulseAudio is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
## Configuration file for PulseAudio clients. See pulse-client.conf(5) for
## more information. Default values are commented out. Use either ; or # for
## commenting.
; default-sink =
; default-source =
default-server = unix:/tmp/pulse.sock
; default-dbus-server =
autospawn = no
; autospawn = yes
; daemon-binary = /usr/bin/pulseaudio
; extra-arguments = --log-target=syslog
; cookie-file =
; enable-shm = yes
; shm-size-bytes = 0 # setting this 0 will use the system-default, usually 64 MiB
; auto-connect-localhost = no
; auto-connect-display = no
EOF
exit 0