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

+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"