Experimental: add speech selector to startup.
This commit is contained in:
1 parent
7ee829d56c
commit
a8d4fc1fc1
10 files changed
+898
-10
No files matched your search
+327
@@ -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"
|
||||
Reference in new issue
Block a user