Experimental: add speech selector to startup.

This commit is contained in:
Storm Dragon committed 2026-09-15 22:52:13 -04:00
1 parent 7ee829d56c
commit a8d4fc1fc1
10 files changed
+898 -10

No files matched your search

+7 -3
View File
@@ -442,6 +442,7 @@ packages=(
man-pages
networkmanager
nodm-dgw
openevv-git
openssh
parted
pipewire
@@ -548,10 +549,13 @@ if command -v spd-conf > /dev/null 2>&1; then
spd-conf -n -C || echo "Warning: Speech Dispatcher system configuration failed."
fi
if [[ -f /etc/speech-dispatcher/speechd.conf ]]; then
if grep -qE '^[[:space:]]*#?[[:space:]]*DefaultModule[[:space:]]+' /etc/speech-dispatcher/speechd.conf; then
sed -i -E 's/^[[:space:]]*#?[[:space:]]*DefaultModule[[:space:]]+.*/DefaultModule rhvoice/' /etc/speech-dispatcher/speechd.conf
speechBackup="\$(mktemp)"
if /usr/local/lib/stormux/select-speech-module \
--apply-module rhvoice /etc/speech-dispatcher/speechd.conf "\$speechBackup"; then
rm -f "\$speechBackup"
else
printf '\nDefaultModule rhvoice\n' >> /etc/speech-dispatcher/speechd.conf
echo "Warning: Could not set RHVoice as the default speech engine."
rm -f "\$speechBackup"
fi
fi
# Enable linger so that hopefully sound will start at login.
@@ -60,6 +60,8 @@ if [[ -x /etc/audibleprompt.sh ]]; then
export sudoFlags=("-A")
fi
/usr/local/lib/stormux/select-speech-module
echo
echo "Starting Fenrir..."
spd-say "Starting Fenrir."
+327
View File
@@ -0,0 +1,327 @@
#!/usr/bin/env bash
set -u
speechConfig="${STORMUX_SPEECH_CONFIG:-/etc/speech-dispatcher/speechd.conf}"
selectionFile=""
spdSayCommand="${STORMUX_SPD_SAY:-spd-say}"
systemctlCommand="${STORMUX_SYSTEMCTL:-systemctl}"
killallCommand="${STORMUX_KILLALL:-killall}"
scriptPath="$(readlink -f "$0")"
runtimeDir="${STORMUX_RUNTIME_DIR:-/run/stormux}"
is_safe_module() {
[[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]
}
apply_module() {
local moduleName="$1"
local configPath="$2"
local backupPath="$3"
local tempPath
is_safe_module "$moduleName" || return 1
[[ -f "$configPath" ]] || return 1
mkdir -p "${backupPath%/*}" || return 1
cp --preserve=mode,ownership,timestamps "$configPath" "$backupPath" || return 1
tempPath="$(mktemp "${configPath}.stormux.XXXXXX")" || return 1
if ! awk -v moduleName="$moduleName" '
BEGIN { wroteDefault = 0 }
/^[[:space:]]*#?[[:space:]]*DefaultModule[[:space:]]+/ {
if (!wroteDefault) {
print "DefaultModule " moduleName
wroteDefault = 1
}
next
}
{ print }
END {
if (!wroteDefault) {
print ""
print "DefaultModule " moduleName
}
}
' "$configPath" > "$tempPath"; then
rm -f "$tempPath"
return 1
fi
chown --reference="$configPath" "$tempPath" || {
rm -f "$tempPath"
return 1
}
chmod --reference="$configPath" "$tempPath" || {
rm -f "$tempPath"
return 1
}
mv -f "$tempPath" "$configPath"
}
restore_config() {
local configPath="$1"
local backupPath="$2"
local tempPath
[[ -f "$backupPath" ]] || return 1
tempPath="$(mktemp "${configPath}.stormux.XXXXXX")" || return 1
if ! cp --preserve=mode,ownership,timestamps "$backupPath" "$tempPath"; then
rm -f "$tempPath"
return 1
fi
mv -f "$tempPath" "$configPath" || return 1
rm -f "$backupPath"
}
write_selection() {
local moduleName="$1"
local targetPath="$2"
local tempPath
is_safe_module "$moduleName" || return 1
mkdir -p "${targetPath%/*}" || return 1
tempPath="$(mktemp "${targetPath}.stormux.XXXXXX")" || return 1
printf '%s\n' "$moduleName" > "$tempPath" || {
rm -f "$tempPath"
return 1
}
chmod 644 "$tempPath" || {
rm -f "$tempPath"
return 1
}
mv -f "$tempPath" "$targetPath"
}
case "${1:-}" in
--apply-module)
[[ $# -eq 4 ]] || exit 2
apply_module "$2" "$3" "$4"
exit $?
;;
--restore-config)
[[ $# -eq 3 ]] || exit 2
restore_config "$2" "$3"
exit $?
;;
--write-selection)
[[ $# -eq 3 ]] || exit 2
write_selection "$2" "$3"
exit $?
;;
esac
while [[ $# -gt 0 ]]; do
case "$1" in
--config)
[[ $# -ge 2 ]] || exit 2
speechConfig="$2"
shift 2
;;
--selection-file)
[[ $# -ge 2 ]] || exit 2
selectionFile="$2"
shift 2
;;
*)
printf 'Unknown option: %s\n' "$1" >&2
exit 2
;;
esac
done
sudoCommand=(sudo)
if [[ -n "${SUDO_ASKPASS:-}" ]]; then
sudoCommand+=(-A)
fi
announce_failure() {
local previousModule="$1"
local message="Speech engine selection failed. Your previous speech engine is still selected."
"$spdSayCommand" -C >/dev/null 2>&1 || true
if is_safe_module "$previousModule"; then
"$spdSayCommand" -o "$previousModule" "$message" >/dev/null 2>&1 || \
"$spdSayCommand" "$message" >/dev/null 2>&1 || true
else
"$spdSayCommand" "$message" >/dev/null 2>&1 || true
fi
printf '%s\n' "$message" >&2
}
restart_speech() {
"${sudoCommand[@]}" "$killallCommand" speech-dispatcher
}
restart_fenrir_if_active() {
if "$systemctlCommand" is-active --quiet fenrirscreenreader.service 2>/dev/null; then
"${sudoCommand[@]}" "$systemctlCommand" restart fenrirscreenreader.service
fi
}
discover_modules() {
local output line candidate
local skippedHeading=false
if ! output="$("$spdSayCommand" -O 2>/dev/null)"; then
return 1
fi
modules=()
while IFS= read -r line; do
candidate="${line%$'\r'}"
candidate="${candidate#"${candidate%%[![:space:]]*}"}"
candidate="${candidate%"${candidate##*[![:space:]]}"}"
[[ -n "$candidate" ]] || continue
if [[ "$skippedHeading" == false ]]; then
skippedHeading=true
continue
fi
is_safe_module "$candidate" || continue
modules+=("$candidate")
done <<< "$output"
[[ ${#modules[@]} -gt 0 ]]
}
configured_module() {
local configured=""
if [[ -r "$speechConfig" ]]; then
configured="$(awk '$1 == "DefaultModule" { print $2; exit }' "$speechConfig")"
fi
if ! is_safe_module "$configured"; then
configured="rhvoice"
fi
printf '%s\n' "$configured"
}
module_index() {
local wanted="$1"
local index
for index in "${!modules[@]}"; do
if [[ "${modules[$index]}" == "$wanted" ]]; then
printf '%s\n' "$index"
return 0
fi
done
return 1
}
draw_menu() {
local index
printf '\033[H\033[2J'
printf 'Select the default speech engine. Use Up and Down, Enter to select, or Escape to keep the current engine.\n\n'
for index in "${!modules[@]}"; do
if [[ $index -eq $selectedIndex ]]; then
printf '> %s\n' "${modules[$index]}"
else
printf ' %s\n' "${modules[$index]}"
fi
done
}
read_menu_key() {
local first rest=""
IFS= read -rsn1 first || return 1
if [[ -z "$first" ]]; then
printf 'enter\n'
return 0
fi
if [[ "$first" == $'\033' ]]; then
IFS= read -rsn2 -t 0.1 rest || true
case "$rest" in
'[A') printf 'up\n' ;;
'[B') printf 'down\n' ;;
'') printf 'escape\n' ;;
*) printf 'other\n' ;;
esac
return 0
fi
printf 'other\n'
}
declare -a modules=()
previousModule="$(configured_module)"
if ! discover_modules; then
announce_failure "$previousModule"
exit 0
fi
if ! selectedIndex="$(module_index "$previousModule")"; then
if selectedIndex="$(module_index rhvoice)"; then
:
else
selectedIndex=0
fi
fi
draw_menu
"$spdSayCommand" "Speech engine selection. ${modules[$selectedIndex]} is highlighted. Use up and down to preview engines, Enter to select, or Escape to keep the current engine." >/dev/null 2>&1 || true
while true; do
if ! keyName="$(read_menu_key)"; then
announce_failure "$previousModule"
exit 0
fi
case "$keyName" in
up)
selectedIndex=$(( (selectedIndex - 1 + ${#modules[@]}) % ${#modules[@]} ))
;;
down)
selectedIndex=$(( (selectedIndex + 1) % ${#modules[@]} ))
;;
enter)
break
;;
escape)
"$spdSayCommand" -C >/dev/null 2>&1 || true
"$spdSayCommand" -o "$previousModule" "Keeping ${previousModule} as the default speech engine." >/dev/null 2>&1 || true
exit 0
;;
*)
continue
;;
esac
draw_menu
moduleName="${modules[$selectedIndex]}"
"$spdSayCommand" -C >/dev/null 2>&1 || true
"$spdSayCommand" -o "$moduleName" "This is ${moduleName}. If selected, I will be your default text-to-speech voice." >/dev/null 2>&1 || true
done
selectedModule="${modules[$selectedIndex]}"
backupFile="${runtimeDir}/speechd.conf.$$.backup"
"$spdSayCommand" -C >/dev/null 2>&1 || true
if ! "${sudoCommand[@]}" "$scriptPath" --apply-module "$selectedModule" "$speechConfig" "$backupFile"; then
"${sudoCommand[@]}" "$scriptPath" --restore-config "$speechConfig" "$backupFile" >/dev/null 2>&1 || true
announce_failure "$previousModule"
exit 0
fi
if ! restart_speech || ! restart_fenrir_if_active; then
"${sudoCommand[@]}" "$scriptPath" --restore-config "$speechConfig" "$backupFile" >/dev/null 2>&1 || true
restart_speech >/dev/null 2>&1 || true
restart_fenrir_if_active >/dev/null 2>&1 || true
announce_failure "$previousModule"
exit 0
fi
if [[ -n "$selectionFile" ]]; then
if ! "${sudoCommand[@]}" "$scriptPath" --write-selection "$selectedModule" "$selectionFile"; then
"${sudoCommand[@]}" "$scriptPath" --restore-config "$speechConfig" "$backupFile" >/dev/null 2>&1 || true
restart_speech >/dev/null 2>&1 || true
restart_fenrir_if_active >/dev/null 2>&1 || true
announce_failure "$previousModule"
exit 0
fi
fi
rm -f "$backupFile" 2>/dev/null || "${sudoCommand[@]}" rm -f "$backupFile" 2>/dev/null || true
"$spdSayCommand" -o "$selectedModule" "${selectedModule} is now the default speech engine." >/dev/null 2>&1 || true
printf 'Default speech engine set to %s.\n' "$selectedModule"
+184
View File
@@ -0,0 +1,184 @@
#!/usr/bin/env bash
set -euo pipefail
repoRoot="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
selector="$repoRoot/pi4/files/usr/local/lib/stormux/select-speech-module"
tempDir="$(mktemp -d)"
mockBin="$tempDir/bin"
testCount=0
cleanup() {
rm -rf "$tempDir"
}
trap cleanup EXIT
mkdir -p "$mockBin" "$tempDir/runtime"
cat > "$mockBin/sudo" <<'EOF'
#!/usr/bin/env bash
if [[ "${1:-}" == "-A" ]]; then
shift
fi
if [[ "${MOCK_SUDO_FAIL_WRITE:-no}" == "yes" && "$*" == *"--write-selection"* ]]; then
exit 1
fi
exec "$@"
EOF
cat > "$mockBin/spd-say" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "$MOCK_LOG"
if [[ "${1:-}" == "-O" ]]; then
cat "$MOCK_MODULES"
exit "${MOCK_DISCOVERY_STATUS:-0}"
fi
exit 0
EOF
cat > "$mockBin/killall" <<'EOF'
#!/usr/bin/env bash
printf 'killall %s\n' "$*" >> "$MOCK_LOG"
if [[ "${MOCK_KILLALL_FAIL:-no}" == "yes" ]]; then
exit 1
fi
EOF
cat > "$mockBin/systemctl" <<'EOF'
#!/usr/bin/env bash
printf 'systemctl %s\n' "$*" >> "$MOCK_LOG"
if [[ "${1:-}" == "is-active" ]]; then
[[ "${MOCK_FENRIR_ACTIVE:-no}" == "yes" ]]
exit
fi
exit 0
EOF
chmod 755 "$mockBin"/*
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_equals() {
local expected="$1"
local actual="$2"
local description="$3"
[[ "$actual" == "$expected" ]] || fail "$description (expected '$expected', got '$actual')"
}
assert_file_contains() {
local filePath="$1"
local pattern="$2"
local description="$3"
grep -qE -- "$pattern" "$filePath" || fail "$description"
}
assert_file_absent() {
local filePath="$1"
local description="$2"
[[ ! -e "$filePath" ]] || fail "$description"
}
reset_case() {
caseDir="$tempDir/case-$testCount"
testCount=$((testCount + 1))
mkdir -p "$caseDir/runtime"
configFile="$caseDir/speechd.conf"
selectionFile="$caseDir/selected"
modulesFile="$caseDir/modules"
mockLog="$caseDir/mock.log"
: > "$mockLog"
printf 'OUTPUT MODULES\nrhvoice\nopenevv\nespeak-ng\n' > "$modulesFile"
printf '# Speech Dispatcher\nDefaultModule rhvoice\n' > "$configFile"
}
run_selector() {
local input="$1"
printf '%b' "$input" | env \
PATH="$mockBin:$PATH" \
MOCK_LOG="$mockLog" \
MOCK_MODULES="$modulesFile" \
MOCK_DISCOVERY_STATUS="${MOCK_DISCOVERY_STATUS:-0}" \
MOCK_KILLALL_FAIL="${MOCK_KILLALL_FAIL:-no}" \
MOCK_FENRIR_ACTIVE="${MOCK_FENRIR_ACTIVE:-no}" \
MOCK_SUDO_FAIL_WRITE="${MOCK_SUDO_FAIL_WRITE:-no}" \
STORMUX_SPEECH_CONFIG="$configFile" \
STORMUX_RUNTIME_DIR="$caseDir/runtime" \
STORMUX_SPD_SAY=spd-say \
STORMUX_KILLALL=killall \
STORMUX_SYSTEMCTL=systemctl \
"$selector" --selection-file "$selectionFile" >/dev/null
}
# Enter accepts the configured initial module and collapses duplicate directives.
reset_case
printf 'DefaultModule rhvoice\n# DefaultModule espeak-ng\nDefaultModule openevv\n' > "$configFile"
MOCK_FENRIR_ACTIVE=yes run_selector '\n'
assert_equals 1 "$(grep -c '^DefaultModule ' "$configFile")" "initial selection should leave one active default"
assert_file_contains "$configFile" '^DefaultModule rhvoice$' "initial selection should retain rhvoice"
assert_equals rhvoice "$(< "$selectionFile")" "initial selection should be recorded"
assert_file_contains "$mockLog" '^systemctl restart fenrirscreenreader\.service$' "active Fenrir should restart"
# Down previews and selects the next module.
reset_case
run_selector 'x\033[B\n'
assert_file_contains "$mockLog" '^-o openevv This is openevv\.' "Down should preview OpenEVV"
assert_file_contains "$configFile" '^DefaultModule openevv$' "Down then Enter should select OpenEVV"
# Up wraps from the first module to the last.
reset_case
run_selector '\033[A\n'
assert_file_contains "$mockLog" '^-o espeak-ng This is espeak-ng\.' "Up should wrap and preview eSpeak NG"
assert_file_contains "$configFile" '^DefaultModule espeak-ng$' "wrapped module should persist"
# Escape retains the prior file and does not create an installer selection.
reset_case
cp "$configFile" "$caseDir/original.conf"
run_selector '\033'
cmp -s "$configFile" "$caseDir/original.conf" || fail "Escape should retain the configuration"
assert_file_absent "$selectionFile" "Escape should not record an installer selection"
# Empty discovery fails safely.
reset_case
printf 'OUTPUT MODULES\n' > "$modulesFile"
cp "$configFile" "$caseDir/original.conf"
run_selector '\n'
cmp -s "$configFile" "$caseDir/original.conf" || fail "empty discovery should retain the configuration"
assert_file_absent "$selectionFile" "empty discovery should not record a selection"
# Unsafe names are rejected rather than passed to spd-say or configuration.
reset_case
printf 'OUTPUT MODULES\nbad name\n;command\nopenevv\n' > "$modulesFile"
run_selector '\n'
assert_file_contains "$configFile" '^DefaultModule openevv$' "the remaining safe module should be selected"
if grep -qE 'bad name|;command' "$mockLog"; then
fail "unsafe module names should not reach spd-say"
fi
# A missing configuration simulates failed persistence and remains nonfatal.
reset_case
rm -f "$configFile"
run_selector '\n'
assert_file_absent "$selectionFile" "failed persistence should not record a selection"
assert_file_contains "$mockLog" 'selection failed' "failed persistence should be announced"
# A restart failure restores the byte-for-byte original configuration.
reset_case
cp "$configFile" "$caseDir/original.conf"
MOCK_KILLALL_FAIL=yes run_selector '\033[B\n'
cmp -s "$configFile" "$caseDir/original.conf" || fail "restart failure should roll back the configuration"
assert_file_absent "$selectionFile" "restart failure should not record a selection"
# Failure to record the live choice also rolls the system configuration back.
reset_case
cp "$configFile" "$caseDir/original.conf"
MOCK_SUDO_FAIL_WRITE=yes run_selector '\033[B\n'
cmp -s "$configFile" "$caseDir/original.conf" || fail "selection-file failure should roll back the configuration"
assert_file_absent "$selectionFile" "selection-file failure should leave no selection"
printf 'PASS: %s speech selector cases\n' "$testCount"
@@ -32,7 +32,7 @@ while [[ $volume -le 130 ]]; do
clear
pactl set-sink-volume @DEFAULT_SINK@ "${volume}%" 2>/dev/null
spd-say "If this is loud enough, press enter."
if read -t4 ; then
if read -r -t4 ; then
echo "Volume set to ${volume}%"
break
else
@@ -47,6 +47,9 @@ if [[ -x /etc/audibleprompt.sh ]]; then
export sudoFlags=("-A")
fi
/usr/local/lib/stormux/select-speech-module \
--selection-file /run/stormux/selected-speech-module
clear
cat << "EOF"
Hello, and welcome to Stormux!
@@ -94,6 +94,20 @@ locale-gen
install_sas
install_rhvoice_english_fixes
if command -v spd-conf > /dev/null 2>&1; then
spd-conf -n -C || echo "Warning: Speech Dispatcher system configuration failed."
fi
if [[ -f /etc/speech-dispatcher/speechd.conf ]]; then
speechBackup="$(mktemp)"
if /usr/local/lib/stormux/select-speech-module \
--apply-module rhvoice /etc/speech-dispatcher/speechd.conf "$speechBackup"; then
rm -f "$speechBackup"
else
echo "Warning: Could not set RHVoice as the default speech engine."
rm -f "$speechBackup"
fi
fi
# Enable system services
systemctl enable NetworkManager.service
systemctl enable fenrirscreenreader.service
+31 -6
View File
@@ -31,6 +31,14 @@ errorCount=0
warningCount=0
currentStep="initialization"
cleanupOnError=false
selectedSpeechModule="rhvoice"
if [[ -r /run/stormux/selected-speech-module ]]; then
mapfile -t selectedSpeechLines < /run/stormux/selected-speech-module
if [[ ${#selectedSpeechLines[@]} -eq 1 && "${selectedSpeechLines[0]}" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]; then
selectedSpeechModule="${selectedSpeechLines[0]}"
fi
fi
#
@@ -1412,7 +1420,7 @@ install_base_system() {
# Stormux-specific packages (installed later in chroot after keyring setup)
# Note: NOT local so it's available in configure_system()
stormuxPackages=(
yay w3m-git
openevv-git yay w3m-git
)
# Combine all packages
@@ -1902,11 +1910,28 @@ if command -v spd-conf > /dev/null 2>&1; then
fi
if [[ -f /etc/speech-dispatcher/speechd.conf ]]; then
if grep -qE '^[[:space:]]*#?[[:space:]]*DefaultModule[[:space:]]+' /etc/speech-dispatcher/speechd.conf; then
sed -i -E 's/^[[:space:]]*#?[[:space:]]*DefaultModule[[:space:]]+.*/DefaultModule rhvoice/' /etc/speech-dispatcher/speechd.conf
else
printf '\nDefaultModule rhvoice\n' >> /etc/speech-dispatcher/speechd.conf
fi
speechConfig=/etc/speech-dispatcher/speechd.conf
speechConfigTemp="\$(mktemp "\${speechConfig}.stormux.XXXXXX")"
awk -v moduleName="${selectedSpeechModule}" '
BEGIN { wroteDefault = 0 }
/^[[:space:]]*#?[[:space:]]*DefaultModule[[:space:]]+/ {
if (!wroteDefault) {
print "DefaultModule " moduleName
wroteDefault = 1
}
next
}
{ print }
END {
if (!wroteDefault) {
print ""
print "DefaultModule " moduleName
}
}
' "\$speechConfig" > "\$speechConfigTemp"
chown --reference="\$speechConfig" "\$speechConfigTemp"
chmod --reference="\$speechConfig" "\$speechConfigTemp"
mv -f "\$speechConfigTemp" "\$speechConfig"
fi
# Create system-wide accessibility configuration
+327
View File
@@ -0,0 +1,327 @@
#!/usr/bin/env bash
set -u
speechConfig="${STORMUX_SPEECH_CONFIG:-/etc/speech-dispatcher/speechd.conf}"
selectionFile=""
spdSayCommand="${STORMUX_SPD_SAY:-spd-say}"
systemctlCommand="${STORMUX_SYSTEMCTL:-systemctl}"
killallCommand="${STORMUX_KILLALL:-killall}"
scriptPath="$(readlink -f "$0")"
runtimeDir="${STORMUX_RUNTIME_DIR:-/run/stormux}"
is_safe_module() {
[[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]
}
apply_module() {
local moduleName="$1"
local configPath="$2"
local backupPath="$3"
local tempPath
is_safe_module "$moduleName" || return 1
[[ -f "$configPath" ]] || return 1
mkdir -p "${backupPath%/*}" || return 1
cp --preserve=mode,ownership,timestamps "$configPath" "$backupPath" || return 1
tempPath="$(mktemp "${configPath}.stormux.XXXXXX")" || return 1
if ! awk -v moduleName="$moduleName" '
BEGIN { wroteDefault = 0 }
/^[[:space:]]*#?[[:space:]]*DefaultModule[[:space:]]+/ {
if (!wroteDefault) {
print "DefaultModule " moduleName
wroteDefault = 1
}
next
}
{ print }
END {
if (!wroteDefault) {
print ""
print "DefaultModule " moduleName
}
}
' "$configPath" > "$tempPath"; then
rm -f "$tempPath"
return 1
fi
chown --reference="$configPath" "$tempPath" || {
rm -f "$tempPath"
return 1
}
chmod --reference="$configPath" "$tempPath" || {
rm -f "$tempPath"
return 1
}
mv -f "$tempPath" "$configPath"
}
restore_config() {
local configPath="$1"
local backupPath="$2"
local tempPath
[[ -f "$backupPath" ]] || return 1
tempPath="$(mktemp "${configPath}.stormux.XXXXXX")" || return 1
if ! cp --preserve=mode,ownership,timestamps "$backupPath" "$tempPath"; then
rm -f "$tempPath"
return 1
fi
mv -f "$tempPath" "$configPath" || return 1
rm -f "$backupPath"
}
write_selection() {
local moduleName="$1"
local targetPath="$2"
local tempPath
is_safe_module "$moduleName" || return 1
mkdir -p "${targetPath%/*}" || return 1
tempPath="$(mktemp "${targetPath}.stormux.XXXXXX")" || return 1
printf '%s\n' "$moduleName" > "$tempPath" || {
rm -f "$tempPath"
return 1
}
chmod 644 "$tempPath" || {
rm -f "$tempPath"
return 1
}
mv -f "$tempPath" "$targetPath"
}
case "${1:-}" in
--apply-module)
[[ $# -eq 4 ]] || exit 2
apply_module "$2" "$3" "$4"
exit $?
;;
--restore-config)
[[ $# -eq 3 ]] || exit 2
restore_config "$2" "$3"
exit $?
;;
--write-selection)
[[ $# -eq 3 ]] || exit 2
write_selection "$2" "$3"
exit $?
;;
esac
while [[ $# -gt 0 ]]; do
case "$1" in
--config)
[[ $# -ge 2 ]] || exit 2
speechConfig="$2"
shift 2
;;
--selection-file)
[[ $# -ge 2 ]] || exit 2
selectionFile="$2"
shift 2
;;
*)
printf 'Unknown option: %s\n' "$1" >&2
exit 2
;;
esac
done
sudoCommand=(sudo)
if [[ -n "${SUDO_ASKPASS:-}" ]]; then
sudoCommand+=(-A)
fi
announce_failure() {
local previousModule="$1"
local message="Speech engine selection failed. Your previous speech engine is still selected."
"$spdSayCommand" -C >/dev/null 2>&1 || true
if is_safe_module "$previousModule"; then
"$spdSayCommand" -o "$previousModule" "$message" >/dev/null 2>&1 || \
"$spdSayCommand" "$message" >/dev/null 2>&1 || true
else
"$spdSayCommand" "$message" >/dev/null 2>&1 || true
fi
printf '%s\n' "$message" >&2
}
restart_speech() {
"${sudoCommand[@]}" "$killallCommand" speech-dispatcher
}
restart_fenrir_if_active() {
if "$systemctlCommand" is-active --quiet fenrirscreenreader.service 2>/dev/null; then
"${sudoCommand[@]}" "$systemctlCommand" restart fenrirscreenreader.service
fi
}
discover_modules() {
local output line candidate
local skippedHeading=false
if ! output="$("$spdSayCommand" -O 2>/dev/null)"; then
return 1
fi
modules=()
while IFS= read -r line; do
candidate="${line%$'\r'}"
candidate="${candidate#"${candidate%%[![:space:]]*}"}"
candidate="${candidate%"${candidate##*[![:space:]]}"}"
[[ -n "$candidate" ]] || continue
if [[ "$skippedHeading" == false ]]; then
skippedHeading=true
continue
fi
is_safe_module "$candidate" || continue
modules+=("$candidate")
done <<< "$output"
[[ ${#modules[@]} -gt 0 ]]
}
configured_module() {
local configured=""
if [[ -r "$speechConfig" ]]; then
configured="$(awk '$1 == "DefaultModule" { print $2; exit }' "$speechConfig")"
fi
if ! is_safe_module "$configured"; then
configured="rhvoice"
fi
printf '%s\n' "$configured"
}
module_index() {
local wanted="$1"
local index
for index in "${!modules[@]}"; do
if [[ "${modules[$index]}" == "$wanted" ]]; then
printf '%s\n' "$index"
return 0
fi
done
return 1
}
draw_menu() {
local index
printf '\033[H\033[2J'
printf 'Select the default speech engine. Use Up and Down, Enter to select, or Escape to keep the current engine.\n\n'
for index in "${!modules[@]}"; do
if [[ $index -eq $selectedIndex ]]; then
printf '> %s\n' "${modules[$index]}"
else
printf ' %s\n' "${modules[$index]}"
fi
done
}
read_menu_key() {
local first rest=""
IFS= read -rsn1 first || return 1
if [[ -z "$first" ]]; then
printf 'enter\n'
return 0
fi
if [[ "$first" == $'\033' ]]; then
IFS= read -rsn2 -t 0.1 rest || true
case "$rest" in
'[A') printf 'up\n' ;;
'[B') printf 'down\n' ;;
'') printf 'escape\n' ;;
*) printf 'other\n' ;;
esac
return 0
fi
printf 'other\n'
}
declare -a modules=()
previousModule="$(configured_module)"
if ! discover_modules; then
announce_failure "$previousModule"
exit 0
fi
if ! selectedIndex="$(module_index "$previousModule")"; then
if selectedIndex="$(module_index rhvoice)"; then
:
else
selectedIndex=0
fi
fi
draw_menu
"$spdSayCommand" "Speech engine selection. ${modules[$selectedIndex]} is highlighted. Use up and down to preview engines, Enter to select, or Escape to keep the current engine." >/dev/null 2>&1 || true
while true; do
if ! keyName="$(read_menu_key)"; then
announce_failure "$previousModule"
exit 0
fi
case "$keyName" in
up)
selectedIndex=$(( (selectedIndex - 1 + ${#modules[@]}) % ${#modules[@]} ))
;;
down)
selectedIndex=$(( (selectedIndex + 1) % ${#modules[@]} ))
;;
enter)
break
;;
escape)
"$spdSayCommand" -C >/dev/null 2>&1 || true
"$spdSayCommand" -o "$previousModule" "Keeping ${previousModule} as the default speech engine." >/dev/null 2>&1 || true
exit 0
;;
*)
continue
;;
esac
draw_menu
moduleName="${modules[$selectedIndex]}"
"$spdSayCommand" -C >/dev/null 2>&1 || true
"$spdSayCommand" -o "$moduleName" "This is ${moduleName}. If selected, I will be your default text-to-speech voice." >/dev/null 2>&1 || true
done
selectedModule="${modules[$selectedIndex]}"
backupFile="${runtimeDir}/speechd.conf.$$.backup"
"$spdSayCommand" -C >/dev/null 2>&1 || true
if ! "${sudoCommand[@]}" "$scriptPath" --apply-module "$selectedModule" "$speechConfig" "$backupFile"; then
"${sudoCommand[@]}" "$scriptPath" --restore-config "$speechConfig" "$backupFile" >/dev/null 2>&1 || true
announce_failure "$previousModule"
exit 0
fi
if ! restart_speech || ! restart_fenrir_if_active; then
"${sudoCommand[@]}" "$scriptPath" --restore-config "$speechConfig" "$backupFile" >/dev/null 2>&1 || true
restart_speech >/dev/null 2>&1 || true
restart_fenrir_if_active >/dev/null 2>&1 || true
announce_failure "$previousModule"
exit 0
fi
if [[ -n "$selectionFile" ]]; then
if ! "${sudoCommand[@]}" "$scriptPath" --write-selection "$selectedModule" "$selectionFile"; then
"${sudoCommand[@]}" "$scriptPath" --restore-config "$speechConfig" "$backupFile" >/dev/null 2>&1 || true
restart_speech >/dev/null 2>&1 || true
restart_fenrir_if_active >/dev/null 2>&1 || true
announce_failure "$previousModule"
exit 0
fi
fi
rm -f "$backupFile" 2>/dev/null || "${sudoCommand[@]}" rm -f "$backupFile" 2>/dev/null || true
"$spdSayCommand" -o "$selectedModule" "${selectedModule} is now the default speech engine." >/dev/null 2>&1 || true
printf 'Default speech engine set to %s.\n' "$selectedModule"
+1
View File
@@ -90,6 +90,7 @@ nvme-cli
open-iscsi
open-vm-tools
openconnect
openevv-git
openpgp-card-tools
openssh
openvpn
+1
View File
@@ -27,6 +27,7 @@ file_permissions=(
["/usr/local/bin/livecd-sound"]="0:0:755"
["/usr/local/bin/configure-stormux"]="0:0:755"
["/usr/local/bin/install-stormux"]="0:0:755"
["/usr/local/lib/stormux/select-speech-module"]="0:0:755"
["/usr/local/bin/stormux-setup.sh"]="0:0:755"
["/usr/local/bin/init-pipewire-sound.sh"]="0:0:755"
["/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh"]="0:0:755"