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

3
TODO
View File

@@ -1,3 +0,0 @@
V2.0
Cleanup folders and config files.

View File

@@ -4,7 +4,7 @@ Wants=systemd-udev-settle.service
After=systemd-udev-settle.service getty.target
[Service]
Type=forking
PIDFile=/var/run/fenrir.pid
PIDFile=/run/fenrir.pid
ExecStart=/usr/bin/fenrir
ExecReload=/usr/bin/kill -HUP $MAINPID
Restart=always

View File

@@ -4,7 +4,7 @@ Wants=systemd-udev-settle.service
After=systemd-udev-settle.service sound.target
[Service]
Type=forking
PIDFile=/var/run/fenrir.pid
PIDFile=/run/fenrir.pid
ExecStart=/usr/local/bin/fenrir
ExecReload=/usr/bin/kill -HUP $MAINPID
Restart=always

4
bugs
View File

@@ -1,5 +1,5 @@
Please report Bugs and feature requests to:
https://github.com/chrys87/fenrir/issues
Please report bugs and feature requests to:
https://git.stormux.org/storm/fenrir/issues
For bugs, please provide a debug file that shows the issue.
How to create a debug file:

View File

@@ -7,14 +7,14 @@ configurable and easy to customize and extend.
=== Credit and intended audience
This document is just a customization for Slint of the genuine
https://github.com/chrys87/fenrir/blob/master/docu/user.txt[Fenrir User
https://git.stormux.org/storm/fenrir/src/branch/master/docs/user.txt[Fenrir User
Manual] motly written by Chrys, main developer of Fenrir.
It has been adapted to its intended audience: end users of Fenrir on
Slint where it is already installed, thus concentrates on its setting
and usage. You will find more information about its features,
installation and how customize and troubleshoot it and contribute to its
development on https://github.com/chrys87/fenrir[the Fenrir Git
development on https://git.stormux.org/storm/fenrir[the Fenrir Git
repository].
=== Getting started with Fenrir
@@ -2193,9 +2193,9 @@ settings.conf). Commands are python files with a special scheme. You can
assign them to a shortcut using the filename without an extension or
place them in a hook trigger like OnInput or OnScreenChange. For further
information see developer guide. Good Examples:
https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/commands/date.py["date.py"]
https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/commands/date.py["date.py"]
(announce the Date),
https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/commands/shut_up.py["shut_up.py"]
https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/commands/shut_up.py["shut_up.py"]
(interrupt output) the basic scheme for a command is as follows:
....
@@ -2218,7 +2218,7 @@ class command():
pass
....
* https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/command_template.py[Template
* https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/command_template.py[Template
lives here]
* The class needs to have the name "command".
* "initialize" is running once whilst loading the command.
@@ -2276,7 +2276,7 @@ root.
=== Bugreports and feature requests
Please report Bugs and feature requests to:
https://github.com/chrys87/fenrir/issues
https://git.stormux.org/storm/fenrir/issues
for bugs please provide a link:#Howto create a debug file[debug] file
that shows the issue.

View File

@@ -160,7 +160,7 @@ For Arch there are PKGBUILDs in the AUR:
- Download the latest stable version from the [[https://linux-a11y.org/index.php?page=fenrir-screenreader|Fenrir-Project]] site.
- Unpack the archive
- Check the needed Dependencys by running [[https://github.com/chrys87/fenrir/blob/master/check-dependencies.py|check-dependencys.py]] script
- Check the needed Dependencys by running [[https://git.stormux.org/storm/fenrir/src/branch/master/check-dependencies.py|check-dependencys.py]] script
- install the missing dependencies an standard installation requires the following:
* python3 >= 3.3 (and all the following is needed for python3 )
* python3-speechd (screen)
@@ -171,7 +171,7 @@ For Arch there are PKGBUILDs in the AUR:
* python3-pyenchant (spellchecker)
* your language for aspell (aspell-<lang>) (spellchecker)
* sox (sound)
* For an individual installation see [[#Support and Requirements|Support and Requirements]] or consult the [[https://github.com/chrys87/fenrir/blob/master/README.md|Readme]])
* For an individual installation see [[#Support and Requirements|Support and Requirements]] or consult the [[https://git.stormux.org/storm/fenrir/src/branch/master/README.md|Readme]])
- run "install.sh" as root
this installs Fenrir as the following
@@ -185,7 +185,7 @@ to remove Fenrir just run uninstall.sh as root
if you want to get the latest code you can use git to get a development snapshot:
git clone https://github.com/chrys87/fenrir.git
git clone https://git.stormux.org/storm/fenrir.git
===== Auto Start =====
@@ -1270,7 +1270,7 @@ File: ''/usr/share/fenrirscreenreader/scripts/helloWorld__-__key_h.sh'':
===== Commands =====
You can place your own commands in "/usr/share/fenrirscreenreader/commands" (path is configurable in settings.conf).
Commands are python files with a special scheme. You can assign them to a shortcut using the filename without an extension or place them in a hook trigger like OnInput or OnScreenChange. For further information see developer guide.
Good Examples: [[https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/commands/date.py|"date.py"]] (announce the Date), [[https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/commands/shut_up.py|"shut_up.py"]] (interrupt output)
Good Examples: [[https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/commands/date.py|"date.py"]] (announce the Date), [[https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/commands/shut_up.py|"shut_up.py"]] (interrupt output)
the basic scheme for a command is as follows:
from core import debug
@@ -1289,7 +1289,7 @@ the basic scheme for a command is as follows:
def setCallback(self, callback):
pass
* [[https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/command_template.py|Template lives here]]
* [[https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/command_template.py|Template lives here]]
* The class needs to have the name "command".
* "initialize" is running once whilst loading the command.
* "shutdown" is running on unload like the command (quit fenrir)
@@ -1319,7 +1319,7 @@ the basic scheme for a command is as follows:
- You can test if speech-dispatcher works by invoking it as root\\ ''sudo spd-say "hello world"''
===== Bugreports and feature requests =====
Please report Bugs and feature requests to:
[[https://github.com/chrys87/fenrir/issues|https://github.com/chrys87/fenrir/issues]]
[[https://git.stormux.org/storm/fenrir/issues|https://git.stormux.org/storm/fenrir/issues]]
for bugs please provide a [[#Howto create a debug file|debug]] file that shows the issue.
==== How-to create a debug file ====

View File

@@ -56,7 +56,7 @@ To test Fenrir:
sudo fenrir
To have Fenrir start on system boot using systemd:
download service file: https://raw.githubusercontent.com/chrys87/fenrir/master/autostart/systemd/Arch/fenrir.service
download service file: https://git.stormux.org/storm/fenrir/raw/branch/master/autostart/systemd/Arch/fenrir.service
move the service file to: /etc/systemd/system/fenrir.service
sudo systemctl enable fenrir

View File

@@ -4,5 +4,5 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
version = "2025.11.23"
version = "2025.11.24"
code_name = "testing"

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