diff --git a/etc/speech-dispatcher/speechd.conf b/etc/speech-dispatcher/speechd.conf new file mode 120000 index 0000000..5073310 --- /dev/null +++ b/etc/speech-dispatcher/speechd.conf @@ -0,0 +1 @@ +/home/stormux/.local/etc/speech-dispatcher/speechd.conf \ No newline at end of file diff --git a/etc/speech-dispatcher/speechd.conf.bak b/home/stormux/.local/.backup/speech-dispatcher/speechd.conf similarity index 100% rename from etc/speech-dispatcher/speechd.conf.bak rename to home/stormux/.local/.backup/speech-dispatcher/speechd.conf diff --git a/home/stormux/.local/bin/game_launcher.py b/home/stormux/.local/bin/game_launcher.py index 4ef7e26..09206e2 100755 --- a/home/stormux/.local/bin/game_launcher.py +++ b/home/stormux/.local/bin/game_launcher.py @@ -164,6 +164,7 @@ class VoicedMenu: # Load downloadable games registry self.downloadable_games = self.load_downloadable_games() self.savedMenuState = None + self.menuStateStack = [] self.activeGameEntry = None def load_downloadable_games(self): @@ -573,25 +574,65 @@ class VoicedMenu: except Exception as e: self.speak(f"Error reading battery status: {e}") + def handle_sigint(self, _signum, _frame): + """Make Ctrl+C behave like Escape inside the menu.""" + self.handle_escape_or_restart() + def update_service_menu_items(self): """Update all service-related menu items based on their current status""" - # Remove any existing service items - if "System" in self.menuSections: - # Find and remove any service-related items - self.menuSections["System"] = [item for item in self.menuSections["System"] - if not any(service in item[0] for service in self.systemMenuServices.keys())] + if self.sectionNames != ["Services"]: + return - # Add the appropriate items based on current status for each service + self.menuSections["Services"] = self.build_services_menu_actions() + [("Back", self.restore_saved_menu)] + + def build_service_menu_items(self): + """Build service toggle items based on current service state.""" + items = [] for friendlyName, serviceName in self.systemMenuServices.items(): isActive = self.check_service_status(serviceName) startLabel = "Start" if friendlyName == "Fenrir Screen Reader" else "Enable" stopLabel = "Stop" if friendlyName == "Fenrir Screen Reader" else "Disable" if isActive: - self.add_item("System", f"{stopLabel} {friendlyName}", - lambda fn=friendlyName: self.toggle_service(fn)) + items.append((f"{stopLabel} {friendlyName}", lambda fn=friendlyName: self.toggle_service(fn))) else: - self.add_item("System", f"{startLabel} {friendlyName}", - lambda fn=friendlyName: self.toggle_service(fn)) + items.append((f"{startLabel} {friendlyName}", lambda fn=friendlyName: self.toggle_service(fn))) + return items + + def build_services_menu_actions(self): + """Build the system services submenu.""" + return self.build_service_menu_items() + [ + ("Enable Screen", lambda: self.toggle_screen("screen")), + ("Disable Screen", lambda: self.toggle_screen("headless")), + ("Battery Status", self.report_battery_status), + ] + + def show_services_menu(self): + """Show service toggles and related runtime controls.""" + self.show_action_menu("Services", self.build_services_menu_actions()) + + def show_settings_menu(self): + """Show system settings and configuration actions.""" + actions = [] + if platform.machine() == "x86_64" and not os.path.exists("/home/stormux/.baremetal"): + actions.append(("Install System to Hard Drive", "GAME='Install to Disk' /home/stormux/.clirc")) + + actions.extend([ + ("Internet Configuration", "GAME=\"Network Configuration\" /home/stormux/.clirc"), + ("Set System Speech Settings", "/home/stormux/.local/bin/speechd_rate.py"), + ("Set Default Voice", "/home/stormux/.local/bin/set-voice.py"), + ("Set Timezone", "GAME='Set Timezone' /home/stormux/.clirc"), + ("Download Files", "/home/stormux/.local/download_server.py"), + ("Resize to fill empty space on disk", "sudo growpartfs $(df --output='source' / | tail -1)"), + ]) + self.show_action_menu("Settings", actions) + + def show_power_menu(self): + """Show power and update actions.""" + self.show_action_menu("Power", [ + ("System Update", "/home/stormux/.local/bin/system-update.sh"), + ("Restart: Can Take Several Minutes", "sudo reboot"), + ("Power Off: Wait 2 Minutes Before Disconnecting Power", "sudo poweroff"), + ]) def install_and_launch(self, executable_name, launch_mode="gui"): """Launch application via xinitrc (which handles installation if needed)""" @@ -706,7 +747,7 @@ class VoicedMenu: def save_menu_state(self): """Save the current menu state so a temporary submenu can restore it.""" - self.savedMenuState = { + menuState = { "sectionNames": self.sectionNames.copy(), "currentSection": self.currentSection, "currentItemIndices": self.currentItemIndices.copy(), @@ -715,18 +756,22 @@ class VoicedMenu: for sectionName, items in self.menuSections.items() }, } + self.menuStateStack.append(menuState) + self.savedMenuState = menuState def restore_saved_menu(self): """Restore the menu state saved before entering a temporary submenu.""" - if self.savedMenuState is None: + if not self.menuStateStack: return - self.sectionNames = self.savedMenuState["sectionNames"] - self.currentSection = self.savedMenuState["currentSection"] - self.currentItemIndices = self.savedMenuState["currentItemIndices"] - self.menuSections = self.savedMenuState["menuSections"] - self.savedMenuState = None - self.activeGameEntry = None + menuState = self.menuStateStack.pop() + self.sectionNames = menuState["sectionNames"] + self.currentSection = menuState["currentSection"] + self.currentItemIndices = menuState["currentItemIndices"] + self.menuSections = menuState["menuSections"] + self.savedMenuState = self.menuStateStack[-1] if self.menuStateStack else None + if not self.menuStateStack: + self.activeGameEntry = None self.draw_menu() self.announce_current_section() time.sleep(0.5) @@ -774,12 +819,106 @@ class VoicedMenu: def handle_escape(self): """Handle Escape based on whether a temporary submenu is active.""" - if self.savedMenuState is None: + if not self.menuStateStack: return False self.restore_saved_menu() return True + def handle_escape_or_restart(self): + """Go back from submenus, otherwise restart the menu.""" + if self.handle_escape(): + return + + try: + # Announce restart first while speech still works + self.speak("Restarting menu") + + # Wait for speech to complete + time.sleep(1) + + # Now clean up resources + self.cleanup() + + # Try to kill speech-dispatcher if needed + subprocess.run(["sudo", "killall", "speech-dispatcher"], check=False) + + # Restart the application + os.execv(sys.argv[0], sys.argv) + + except Exception as e: + print(f"Error during restart: {e}") + # If restart fails, we need to recover the UI + self.stdscr = curses.initscr() + curses.noecho() + curses.cbreak() + self.stdscr.keypad(True) + self.draw_menu() + + def jump_to_item_by_letter(self, letter): + """Move to the next item in the current menu starting with letter.""" + if not letter or not self.sectionNames: + return False + + letter = letter.lower() + items = self.get_current_items() + if not items: + return False + + currentIndex = self.get_current_item_index() + for offset in range(1, len(items) + 1): + candidateIndex = (currentIndex + offset) % len(items) + itemName = items[candidateIndex][0].strip().lower() + if itemName.startswith(letter): + self.set_current_item_index(candidateIndex) + return True + return False + + def jump_to_section_by_letter(self, letter): + """Move to the next top-level section starting with letter.""" + if not letter or self.menuStateStack or not self.sectionNames: + return False + + letter = letter.lower() + for offset in range(1, len(self.sectionNames) + 1): + candidateIndex = (self.currentSection + offset) % len(self.sectionNames) + sectionName = self.sectionNames[candidateIndex].strip().lower() + if sectionName.startswith(letter): + self.currentSection = candidateIndex + return True + return False + + def handle_letter_navigation(self, key): + """Handle printable letter navigation and reliable control shortcuts.""" + if key == 2: # Ctrl+B + self.report_battery_status() + return True + + if key < 0 or key > 255: + return False + + keyChar = chr(key) + if not keyChar.isalpha(): + return False + + moved = False + if keyChar.isupper() and not self.menuStateStack: + moved = self.jump_to_section_by_letter(keyChar) + if moved: + self.draw_menu() + self.play_sound("menu_category") + self.announce_current_section() + time.sleep(0.5) + self.announce_current_item(interrupt=False) + else: + moved = self.jump_to_item_by_letter(keyChar) + if moved: + self.draw_menu() + self.play_sound("menu_move") + self.announce_current_item() + + return moved + def speak(self, text, interrupt=True): """Speak the given text with option to interrupt existing speech""" if self.speechClient is None: @@ -986,17 +1125,20 @@ class VoicedMenu: Down arrow: Next menu item. Left arrow: Previous section. Right arrow: Next section. + Lowercase letters: Jump to menu items. + Uppercase letters: Jump to categories from the main menu. Enter: Launch selected item. H key: Hear these instructions again. - B key: Report battery status. + Control B: Report battery status. Left bracket: Decrease speech rate. Right bracket: Increase speech rate. Left brace: Decrease speech pitch. Right brace: Increase speech pitch. 9 key: Decrease volume. 0 key: Increase volume. - Escape: Refresh the menu. + Escape: Go back from submenus or refresh the main menu. Q: Exit the menu. + Control C behaves like Escape. Any key will interrupt speech. """ self.speak(helpText) @@ -1012,7 +1154,7 @@ class VoicedMenu: self.stdscr.addstr(1, x, title, curses.A_BOLD) # Draw help line - helpText = "Navigate | Enter: Select | H: Help | [ ] Rate | { } Pitch | 9 0 Volume | Esc: Refresh" + helpText = "Navigate | Letters: Jump | Enter: Select | H: Help | Ctrl+B: Battery | Esc: Back" x = max(0, w // 2 - len(helpText) // 2) self.stdscr.addstr(3, x, helpText) @@ -1087,6 +1229,7 @@ class VoicedMenu: curses.noecho() # Don't echo keypresses curses.cbreak() # React to keys instantly self.stdscr.keypad(True) # Enable special keys + signal.signal(signal.SIGINT, self.handle_sigint) # Update all service menu items based on current status self.update_service_menu_items() @@ -1203,36 +1346,16 @@ class VoicedMenu: elif key == ord('0'): # Increase volume self.increase_volume() - elif key == ord('b') or key == ord('B'): # Battery status - self.report_battery_status() + elif key == ord('q') or key == ord('Q'): # Quit + self.speak("Exiting menu") + time.sleep(0.5) + break + + elif self.handle_letter_navigation(key): + pass elif key == 27: # Esc key - go back from submenus, otherwise restart the application - if self.handle_escape(): - continue - try: - # Announce restart first while speech still works - self.speak("Restarting menu") - - # Wait for speech to complete - time.sleep(1) - - # Now clean up resources - self.cleanup() - - # Try to kill speech-dispatcher if needed - subprocess.run(["sudo", "killall", "speech-dispatcher"], check=False) - - # Restart the application - os.execv(sys.argv[0], sys.argv) - - except Exception as e: - print(f"Error during restart: {e}") - # If restart fails, we need to recover the UI - self.stdscr = curses.initscr() - curses.noecho() - curses.cbreak() - self.stdscr.keypad(True) - self.draw_menu() + self.handle_escape_or_restart() except Exception as e: # End curses in case of error curses.endwin() @@ -1241,6 +1364,14 @@ class VoicedMenu: # Clean up self.cleanup() +def configure_system_menu(menu): + """Add the nested System menu.""" + menu.add_section("System") + menu.add_item("System", "Settings", menu.show_settings_menu) + menu.add_item("System", "Services", menu.show_services_menu) + menu.add_item("System", "Power", menu.show_power_menu) + + # Example usage if __name__ == "__main__": # Create the menu with sections @@ -1279,25 +1410,7 @@ if __name__ == "__main__": menu.add_item("Accessories", "LibreOffice", lambda: menu.install_and_launch("libreoffice", "gui")) menu.add_item("Accessories", "Thunderbird", lambda: menu.install_and_launch("thunderbird", "gui")) - # Add system section - menu.add_section("System") - - # Add installer only on x86_64 and if not already installed (no .baremetal file) - if platform.machine() == "x86_64" and not os.path.exists("/home/stormux/.baremetal"): - menu.add_item("System", "Install System to Hard Drive", "GAME='Install to Disk' /home/stormux/.clirc") - - menu.add_item("System", "Internet Configuration", "GAME=\"Network Configuration\" /home/stormux/.clirc") - menu.add_item("System", "Enable Screen", lambda: menu.toggle_screen("screen")) - menu.add_item("System", "Disable Screen", lambda: menu.toggle_screen("headless")) - menu.add_item("System", "Set System Speech Settings", "/home/stormux/.local/bin/speechd_rate.py") - menu.add_item("System", "Set Default Voice", "/home/stormux/.local/bin/set-voice.py") - menu.add_item("System", "Set Timezone", "GAME='Set Timezone' /home/stormux/.clirc") - menu.add_item("System", "Download Files", "/home/stormux/.local/download_server.py") - menu.add_item("System", "Update System", "sudo /home/stormux/.local/bin/live-update.sh") - menu.add_item("System", "Restart: Can Take Several Minutes", "sudo reboot") - menu.add_item("System", "Power Off: Wait 2 Minutes Before Disconnecting Power", "sudo poweroff") - # Service menu items will be added dynamically in run() method via update_service_menu_items() - menu.add_item("System", "Resize to fill empty space on disk", "sudo growpartfs $(df --output='source' / | tail -1)") + configure_system_menu(menu) # Run the menu menu.run() diff --git a/home/stormux/.local/bin/live-update.sh b/home/stormux/.local/bin/live-update.sh deleted file mode 100755 index 3ba0275..0000000 --- a/home/stormux/.local/bin/live-update.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash - -# Stop the screen reader if this closes for any reason -trap 'sudo -n /usr/bin/systemctl stop fenrirscreenreader.service' SIGINT SIGTERM SIGHUP EXIT - -# Start Fenrir for interaction with the terminal -sudo -n /usr/bin/systemctl start fenrirscreenreader.service - -# Clear the screen before loading -clear - -# Setup logging -logDir="/home/stormux/Logs" -logFile="${logDir}/system-updates.log" -mkdir -p "${logDir}" -echo "=== System Update Started: $(date) ===" | tee "${logFile}" - -# Track errors -errorCount=0 -errorMessages=() - -# Clean up old packages keeping currently installed versions only -pacman -Sc --noconfirm --quiet 2>&1 | tee -a "${logFile}" -if [[ ${PIPESTATUS[0]} -ne 0 ]]; then - ((errorCount++)) - errorMessages+=("Package cleaning failed") -fi - -# Upgrade the system -pacman -Syu --noconfirm --quiet 2>&1 | tee -a "${logFile}" -if [[ ${PIPESTATUS[0]} -ne 0 ]]; then - ((errorCount++)) - errorMessages+=("Package update failed") -fi - -gitUrl="https://git.stormux.org/storm/gaming-image-files" -gitPath="${gitUrl##*/}" -pushd /tmp || exit -git config --global credential.helper store -git clone --quiet "${gitUrl}" 2>&1 | tee -a "${logFile}" -if [[ ${PIPESTATUS[0]} -ne 0 ]]; then - ((errorCount++)) - errorMessages+=("Git clone failed") -fi -pushd "${gitPath}" || exit -git checkout --quiet master 2>&1 | tee -a "${logFile}" -if [[ ${PIPESTATUS[0]} -ne 0 ]]; then - ((errorCount++)) - errorMessages+=("Git checkout master failed") -fi -git lfs pull 2>&1 | tee -a "${logFile}" -if [[ ${PIPESTATUS[0]} -ne 0 ]]; then - ((errorCount++)) - errorMessages+=("Git LFS pull failed") -fi -# Files and directories to ignore when copying -ignoreFiles=(".git" "./image" ".git*" "/home/stormux/.w3m" "/home/stormux/.irssi") -# Build find command with ignore patterns -findArgs=() -for ignore in "${ignoreFiles[@]}"; do - if [[ "$ignore" == .* && "$ignore" != ./* ]]; then - findArgs+=(-name "$ignore" -prune -o) - else - findArgs+=(-path "$ignore" -prune -o) - fi -done -# Copy all files as root (preserves permissions properly) -find . "${findArgs[@]}" -type f -exec bash -c 'for i ; do cp -a --preserve=all --parents "${i}" /;done' _ "{}" \; 2>&1 | tee -a "${logFile}" -if [[ ${PIPESTATUS[0]} -ne 0 ]]; then - ((errorCount++)) - errorMessages+=("File copy failed") -fi -# Fix ownership of home directory files (exclude immutable .baremetal) -find /home/stormux -path /home/stormux/.baremetal -prune -o -exec chown -h stormux:users '{}' \; 2>&1 | tee -a "${logFile}" -if [[ ${PIPESTATUS[0]} -ne 0 ]]; then - ((errorCount++)) - errorMessages+=("Ownership fix failed") -fi -popd || exit -rm -rf "${gitPath}" -popd || exit - -echo "=== System Update Completed: $(date) ===" | tee -a "${logFile}" -echo | tee -a "${logFile}" - -# Display summary -if [[ $errorCount -eq 0 ]]; then - echo "SUCCESS: All update operations completed successfully." | tee -a "${logFile}" -else - echo "ERRORS DETECTED: $errorCount error(s) occurred during update:" | tee -a "${logFile}" - for error in "${errorMessages[@]}"; do - echo " - $error" | tee -a "${logFile}" - done -fi -echo | tee -a "${logFile}" - -read -r -p "Press enter to continue." - -exit 0 diff --git a/home/stormux/.local/bin/system-update.sh b/home/stormux/.local/bin/system-update.sh new file mode 100755 index 0000000..811dd7c --- /dev/null +++ b/home/stormux/.local/bin/system-update.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repoUrl="${STORMUX_UPDATE_REPO:-https://git.stormux.org/storm/gaming-image-files}" +repoBranch="${STORMUX_UPDATE_BRANCH:-testing}" +homeDir="${STORMUX_UPDATE_HOME:-/home/stormux}" +logDir="${homeDir}/Logs" +logFile="${logDir}/system-updates.log" +menuCommand="${STORMUX_MENU_COMMAND:-${homeDir}/.local/bin/game_launcher.py}" +workRoot="" +beepPid="" +runtimeScript="${0}" + +if [[ "${STORMUX_UPDATE_SELF_COPY:-0}" != "1" ]]; then + runtimeScript="$(mktemp /tmp/stormux-system-update.XXXXXX)" + cp -f -- "$0" "${runtimeScript}" + chmod 755 "${runtimeScript}" + export STORMUX_UPDATE_SELF_COPY=1 + exec "${runtimeScript}" "$@" +fi + +speak() { + local message="$1" + local waitForSpeech="${2:-0}" + + printf '%s\n' "${message}" + if [[ "$waitForSpeech" == "1" ]]; then + spd-say -Cw "${message}" 2> /dev/null || true + else + spd-say "${message}" 2> /dev/null || true + fi +} + +progress_beep() { + [[ "${STORMUX_UPDATE_BEEPS:-1}" == "1" ]] || return 0 + command -v play > /dev/null 2>&1 || return 0 + + while true; do + play -q -n synth 0.06 sine 520 vol 0.35 > /dev/null 2>&1 || true + sleep 2 + done +} + +start_progress() { + local message="$1" + + speak "${message}" + progress_beep & + beepPid="$!" +} + +stop_progress() { + if [[ -n "${beepPid}" ]] && kill -0 "${beepPid}" 2> /dev/null; then + kill "${beepPid}" 2> /dev/null || true + wait "${beepPid}" 2> /dev/null || true + fi + beepPid="" +} + +cleanup() { + local exitCode="${1:-$?}" + + stop_progress + if [[ -n "${workRoot}" && -d "${workRoot}" ]]; then + rm -rf "${workRoot}" + fi + if [[ "${STORMUX_UPDATE_SELF_COPY:-0}" == "1" && "${runtimeScript}" == /tmp/stormux-system-update.* ]]; then + rm -f "${runtimeScript}" + fi + if [[ "${exitCode}" -ne 0 ]]; then + speak "System update failed. Check ${logFile} for details." 1 + read -r -p "Press enter to return to the menu." + exec "${menuCommand}" + fi +} + +abort_update() { + trap - EXIT INT TERM HUP + cleanup 130 + exit 130 +} + +run_logged() { + "$@" 2>&1 | tee -a "${logFile}" + return "${PIPESTATUS[0]}" +} + +main() { + local sourceHome + + mkdir -p "${logDir}" + printf 'System update started [%s]\n' "$(date)" | tee "${logFile}" + + workRoot="$(mktemp -d)" + + start_progress "Downloading updates." + run_logged git clone --depth 1 --filter=blob:none --sparse --branch "${repoBranch}" "${repoUrl}" "${workRoot}" + run_logged git -C "${workRoot}" sparse-checkout set home + if command -v git-lfs > /dev/null 2>&1; then + run_logged git -C "${workRoot}" lfs pull --include='home/**' + elif git -C "${workRoot}" lfs version > /dev/null 2>&1; then + run_logged git -C "${workRoot}" lfs pull --include='home/**' + fi + stop_progress + + sourceHome="${workRoot}/home/stormux" + if [[ ! -d "${sourceHome}" ]]; then + printf 'Missing update home directory: %s\n' "${sourceHome}" | tee -a "${logFile}" + return 1 + fi + + start_progress "Applying updates." + run_logged rsync -a \ + --exclude='.baremetal' \ + --exclude='.cache/' \ + --exclude='.config/stormux/game_launcher.conf' \ + --exclude='.irssi/' \ + --exclude='.local/.services/' \ + --exclude='.local/etc/NetworkManager/' \ + --exclude='.local/games/' \ + --exclude='.local/var/' \ + --exclude='.w3m/' \ + --exclude='Downloads/' \ + --exclude='Logs/' \ + "${sourceHome}/" "${homeDir}/" + stop_progress + + printf 'System update completed [%s]\n' "$(date)" | tee -a "${logFile}" + speak "Finalizing updates." 1 + exec "${menuCommand}" +} + +trap 'cleanup "$?"' EXIT +trap abort_update INT TERM HUP +main "$@" diff --git a/home/stormux/.local/etc/speech-dispatcher/speechd.conf b/home/stormux/.local/etc/speech-dispatcher/speechd.conf new file mode 100644 index 0000000..d146afc --- /dev/null +++ b/home/stormux/.local/etc/speech-dispatcher/speechd.conf @@ -0,0 +1,361 @@ + +# Global configuration for Speech Dispatcher +# ========================================== + +# -----SYSTEM OPTIONS----- + +# CommunicationMethod specifies the method to be used by Speech Dispatcher to communicate with +# its clients. Two basic methods are "unix_socket" and "inet_socket". +# +# unix_socket -- communication over Unix sockets represented by a file in the +# filesystem (see SocketPath below). This method works only locally, but is +# preferred for standard session setup, where every user runs his own instance of Speech +# Dispatcher to get voice feedback on his own computer. +# +# inet_socket -- alternatively, you can start Speech Dispatcher on +# a TCP port and connect to it via hostname/port. This allows for a more +# flexible setup, where you can use Speech Dispatcher over network +# from different machines. See also the Port and LocalhostAccessOnly +# configuration variables. +# +# CommunicationMethod "unix_socket" + +# SocketPath is either "default" or a full path to the filesystem +# where the driving Unix socket file should be created in case the +# CommunicationMethod is set to "unix_socket". The default is +# $XDG_RUNTIME_DIR/speech-dispatcher/speechd.sock where $XDG_RUNTIME_DIR +# is the directory specified by the XDG Base Directory Specification. +# Do not change this unless you have a reason and know what you are doing. + +# SocketPath "default" + +# The Port on which Speech Dispatcher should be available to clients if the "inet_socket" +# communication method is used. + +# Port 6560 + +# By default, if "inet_socket" communication method is used, the specified port is opened only +# for connections coming from localhost. If LocalhostAccessOnly is set to 0 it disables this +# access control. It means that the port will be accessible from all computers on the +# network. If you turn off this option, please make sure you set up some system rules on what +# computers are and are not allowed to access the Speech Dispatcher port. + +# LocalhostAccessOnly 1 + +# By default, Speech Dispatcher is configured to shut itself down after a period of +# time if no clients are connected. The timeout value is in seconds, and is started when +# the last client disconnects. A value of 0 disables the timeout. + +Timeout 0 + +# -----LOGGING CONFIGURATION----- + +# The LogLevel is a number between 0 and 5 specifying the +# verbosity of information to the logfile or screen +# 0 means nothing, 5 means everything (not recommended). + +LogLevel 3 + +# The LogDir specifies where the Speech Dispatcher logs reside +# Specify "stdout" for standard console output +# or a custom log directory path. 'default' means +# the logs are written to the default destination (e.g. a preconfigured +# system directory or the home directory if .speech-dispatcher is present) +# DO NOT COMMENT OUT THIS OPTION, leave as "default" for standard logging + +LogDir "default" +#LogDir "/var/log/speech-dispatcher/" +#LogDir "stdout" + +# The CustomLogFile allows logging all messages # regardless of +# priority, to the given destination. +#CustomLogFile "protocol" "/var/log/speech-dispatcher/speech-dispatcher-protocol.log" + +# ----- VOICE PARAMETERS ----- + +# The DefaultRate controls how fast the synthesizer is going to speak. +# The value must be between -100 (slowest) and +100 (fastest), default +# is 0. + +# DefaultRate 0 + +# The DefaultPitch controls the pitch of the synthesized voice. The +# value must be between -100 (lowest) and +100 (highest), default is +# 0. + +# DefaultPitch 0 + +# The DefaultPitchRange controls the pitch range of the synthesized voice. The +# value must be between -100 (lowest) and +100 (highest), default is +# 0. + +# DefaultPitchRange 0 + +# The DefaultVolume controls the default volume of the voice. It is +# a value between -100 (softly) and +100 (loudly). Currently, +100 +# maps to the default volume of the synthesizer. + +DefaultVolume 100 + +# The DefaultVoiceType controls which voice type should be used by default. +# Voice types are symbolic names which map to particular voices provided by +# the synthesizer according to the output module configuration. +# Please see the synthesizer-specific configuration +# in etc/speech-dispatcher/modules/ to see which voices are assigned to +# different symbolic names. The following symbolic names are +# currently supported: MALE1, MALE2, MALE3, FEMALE1, FEMALE2, FEMALE3, +# CHILD_MALE, CHILD_FEMALE + +# DefaultVoiceType "MALE1" + +# The Default language with which to speak +# Note that the spd-say client in particular always sets the language to its +# current locale language, so this particular client will never pick this +# configuration. + +# DefaultLanguage "en-US" + + +# ----- MESSAGE DISPATCHING CONTROL ----- + +# The DefaultClientName specifies the name of a client who didn't +# introduce himself at the beginning of an SSIP session. + +# DefaultClientName "unknown:unknown:unknown" + +# The Default Priority. Use with caution, normally this shouldn't be +# changed globally (at this place) + +# DefaultPriority "text" + +# The DefaultPauseContext specifies by how many index marks a speech +# cursor should return when resuming after a pause. This is roughly +# equivalent to the number of sentences before the place of the +# execution of pause that will be repeated. + +# DefaultPauseContext 0 + +# -----SPELLING/PUNCTUATION/CAPITAL LETTERS CONFIGURATION----- + +# The DefaultPunctuationMode sets the way dots, comas, exclamation +# marks, question marks etc. are interpreted. none: they are ignored +# some: some of them are sent to synthesis (see +# DefaultPunctuationSome) all: all punctuation marks are sent to +# synthesis + +# DefaultPunctuationMode "none" + +# Level of punctuation for which symbol pre-processing should be performed by +# the server rather than by the module. +# +# This is completely independent of what punctuation level is actually asked, it +# controls to which extent the server should do the punctuation work to insert +# the appropriate words or if the output module is responsible for doing it. +# +# Setting this to "no" disables pre-processing completely and leaves all +# punctuation preprocessing to the output module. Setting this to +# "none" enables only the server rules which are always enabled whatever +# the punctuation level. Setting this to "all" enables all server rules for +# punctuation. Setting this to "char" enables all server rules, including +# rules for spelling spaces. Of course, which rules actually take effect depends +# on the requested punctuation level. + +SymbolsPreproc "char" + +# Which preprocessing files should be loaded, and in which order +# +# This is done first for the most specific localization, then lesser specific +# localization, etc. I.e. for the fr_FR language for instance, fr-fr files are +# loaded first, then fr files, then en files. + +SymbolsPreprocFile "gender-neutral.dic" +SymbolsPreprocFile "font-variants.dic" +SymbolsPreprocFile "symbols.dic" +SymbolsPreprocFile "emojis.dic" +SymbolsPreprocFile "orca.dic" +SymbolsPreprocFile "orca-chars.dic" +SymbolsPreprocFile "symbols-fallback.dic" + +# The DefaultCapLetRecognition: if set to "spell", capital letters +# should be spelled (e.g. "capital b"), if set to "icon", +# capital letters are indicated by inserting a special sound +# before them but they should be read normally, it set to "none" +# capital letters are not recognized (by default) + +# DefaultCapLetRecognition "none" + +# The DefaultSpelling: if set to On, all messages will be spelt +# unless set otherwise (this is usually not something you want to do.) + +# DefaultSpelling Off + +# ----- AUDIO CONFIGURATION ----------- + +# -- AUDIO OUTPUT -- + +# Chooses between the possible sound output systems: +# "pulse" - PulseAudio +# "alsa" - Advanced Linux Sound System +# "oss" - Open Sound System +# "pipewire" - the new low-latency sound server, for near realtime speech performance +# "nas" - Network Audio System +# "libao" - A cross platform audio library +# +# Pipe Wire is the newest audio output method in this configuration, but not the default one. +# However, if it works in your deployment, it is recommended because of the +# latency related advantages it has over pulseaudio and others, however enabling +# it should be done with precaution, because it isn't tested to work in all +# configurations supported by speech dispatcher. +# For now, enabling this is at the discression of the users and packagers of +# speech dispatcher. However, because we want to eventually make this the default +# for everyone, trying it in your deployments, if non-critical, is recommended +# +# OSS, Pulse Audio and ALSA are only provided for compatibility with +# architectures that do not include Pipe Wire. +# +# NAS provides network transparency, but is not very well tested. +# +# libao is a cross platform library with plugins for different sound systems +# and provides alternative output for Pulse Audio and ALSA as well as for other +# backends. + +# AudioOutputMethod "pipewire" + +# -- Pulse Audio parameters -- + +# Pulse audio device name or "default" for the default pulse device + +#AudioPulseDevice "default" + +# Latency requested from pulseaudio, in ms. Smaller values make speech +# interruption snappier, but also uses more CPU time thus battery. +# 10ms latency is considered in HCI (Human-computer Interaction) as real-time. + +#AudioPulseMinLength 10 + +# -- ALSA parameters -- + +# Audio device for ALSA output + +#AudioALSADevice "default" + +# -- OSS parameters -- + +# Audio device for OSS output + +#AudioOSSDevice "/dev/dsp" + +# -- NAS parameters -- + +# Route to the Network Audio System server when NAS +# is chosen for the audio output. Note that NAS +# server doesn't need to run on your machine, +# you can use it also over network (for instance +# when working on remote machines). + +#AudioNASServer "tcp/localhost:5450" + + + +# -----OUTPUT MODULES CONFIGURATION----- + +# Each AddModule line loads an output module. +# Syntax: AddModule "name" "binary" "configuration" "logfile" +# - name is the name under which you can access this module +# - binary is the path to the binary executable of this module, +# either relative (to libexec/speech-dispatcher-modules/ +# or ~/.local/libexec/speech-dispatcher-modules) or absolute +# - configuration is the path to the config file of this module, +# either relative (to etc/speech-dispatcher/modules/ +# or ~/.config/speech-dispatcher) or absolute + +#AddModule "espeak" "sd_espeak" "espeak.conf" +#AddModule "espeak-ng" "sd_espeak-ng" "espeak-ng.conf" +#AddModule "festival" "sd_festival" "festival.conf" +#AddModule "flite" "sd_flite" "flite.conf" +#AddModule "ivona" "sd_ivona" "ivona.conf" +#AddModule "pico" "sd_pico" "pico.conf" +AddModule "piper-tts-generic" "sd_generic" "piper-tts-generic.conf" +#AddModule "espeak-ng-mbrola" "sd_espeak-ng-mbrola" "espeak-ng-mbrola.conf" +#AddModule "espeak-ng-mbrola-generic" "sd_generic" "espeak-ng-mbrola-generic.conf" +#AddModule "espeak-mbrola-generic" "sd_generic" "espeak-mbrola-generic.conf" +#AddModule "swift-generic" "sd_generic" "swift-generic.conf" +#AddModule "epos-generic" "sd_generic" "epos-generic.conf" +#AddModule "dtk-generic" "sd_generic" "dtk-generic.conf" +#AddModule "ibmtts" "sd_ibmtts" "ibmtts.conf" +#AddModule "cicero" "sd_cicero" "cicero.conf" +#AddModule "kali" "sd_kali" "kali.conf" +#AddModule "mary-generic" "sd_generic" "mary-generic.conf" +#AddModule "baratinoo" "sd_baratinoo" "baratinoo.conf" +AddModule "rhvoice" "sd_rhvoice" "rhvoice.conf" +AddModule "viavoice" "sd_viavoice" "viavoice.conf" +AddModule "voxin" "sd_voxin" "voxin.conf" + +# The output module testing doesn't actually connect to anything. It +# outputs the requested commands to standard output and reads +# responses from stdandard input. This way, Speech Dispatcher's +# communication with output modules can be tested easily. + +# AddModule "testing" + +# The DefaultModule selects which output module is the default. You +# must use one of the names of the modules loaded with AddModule. + +# DefaultModule espeak-ng +DefaultModule rhvoice + +# The LanguageDefaultModule selects which output modules are preferred +# for specified languages. + +#LanguageDefaultModule "en" "espeak" +#LanguageDefaultModule "cs" "festival" +#LanguageDefaultModule "es" "festival" + +# -----CLIENT SPECIFIC CONFIGURATION----- + +# Here you can include the files with client-specific configuration +# for different types of clients. They must contain one or more sections with +# this structure: +# BeginClient "emacs:*" +# DefaultPunctuationMode "some" +# ...and/or some other settings +# EndClient +# The parameter of BeginClient tells Speech Dispatcher which clients +# it should apply the settings to (it does glob-style matching, you can use +# * to match any number of characters and ? to match one character) + +# There are some sample client settings + +Include "clients/*.conf" + +# The DisableAutoSpawn option will disable the autospawn mechanism. +# Thus the server will not start automatically on requests from the clients +# DisableAutoSpawn + + +# Copyright (C) 2001-2009 Brailcom, o.p.s +# Copyright (C) 2009 Rui Batista +# Copyright (C) 2010 Andrei Kholodnyi +# Copyright (C) 2010 William Hubbs +# Copyright (C) 2010 Trevor Saunders +# Copyright (C) 2012 William Jon McCann +# Copyright (C) 2014 Rob Whyte +# Copyright (C) 2014-2016 Luke Yelavich +# Copyright (C) 2014 Hussain Jasim +# Copyright (C) 2017 Colomban Wendling +# Copyright (C) 2018 Raphaƫl POITEVIN +# Copyright (C) 2018 Florian Steinhardt +# Copyright (C) 2018-2024 Samuel Thibault +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program 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 (file +# COPYING in the root directory). +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . diff --git a/home/stormux/Documents/system_updates.md b/home/stormux/Documents/system_updates.md index aa74d20..8f60ead 100644 --- a/home/stormux/Documents/system_updates.md +++ b/home/stormux/Documents/system_updates.md @@ -2,97 +2,47 @@ ## Overview -The Stormux Gaming Image includes a system update feature that keeps your system current with the latest packages and configuration files. Updates are performed through the main menu system and require minimal user interaction. +The Stormux Gaming Image uses a read-only root filesystem on x86_64 images, so the System Update option updates managed Stormux files in your home directory. It refreshes the game menu, launcher scripts, installer recipes, documentation, and other files shipped under `/home/stormux`. + +System package updates and root filesystem changes are delivered in new image builds. ## How to Update -1. Open the Game Launcher (automatically starts on boot) -2. Navigate to the "System" menu (from initial load of the image, press left once) -3. Select "System Update" -4. Enter your username and password when prompted (only required when credentials change on the server username is always patron) -5. The update process will run automatically -6. Review the update summary when complete -7. Press Enter to return to the menu +1. Open the Game Launcher. +2. Navigate to System, then Power. +3. Select System Update. +4. Wait for the spoken update messages to finish. + +The menu restarts automatically after the update so any menu changes are loaded immediately. ## What Gets Updated -The system update performs two main operations: +System Update downloads the latest `home/` tree from the Stormux gaming image files repository and applies it to `/home/stormux`. -### 1. Package Updates -- Updates all installed system packages using pacman -- Includes kernel updates, applications, and libraries -- Runs non-interactively with automatic confirmations +The update preserves file permissions, executable bits, and symbolic links from the repository. -### 2. Configuration File Sync -- Downloads the latest custom image files and deploys them -- Updates system configuration files -- Refreshes user utilities and applications -- Updates documentation and resources -- Preserves user data and settings +## Preserved Local Data + +The update skips local state and user data, including: + +- `.baremetal` +- `.cache/` +- `.config/stormux/game_launcher.conf` +- `.irssi/` +- `.local/.services/` +- `.local/etc/NetworkManager/` +- `.local/games/` +- `.local/var/` +- `.w3m/` +- `Downloads/` +- `Logs/` ## Update Logs -All update operations are logged for troubleshooting and review: +Update output is written to: -**Log Location**: `~/Logs/system-updates.log` - -The log file contains: -- Timestamp for each update session -- Complete output from package updates -- Git operations (clone, checkout, LFS pull) -- File copy operations -- Error messages if any operations fail -- Success/failure summary - -## Understanding the Summary - -At the end of each update, you'll see one of two messages: - -### Successful Update -``` -SUCCESS: All update operations completed successfully. +```text +~/Logs/system-updates.log ``` -### Failed Update -``` -ERRORS DETECTED: 2 error(s) occurred during update: - - Package update failed - - Git clone failed -``` - -If errors are detected, review the log file for detailed information about what went wrong. - -## Common Issues - -### Authentication Required -If the server credentials have changed, you'll be prompted for your username and password. This is normal and only happens when credentials are updated on the server. - -### Network Issues -Updates require an internet connection. If git clone or package updates fail, verify your network connection and try again. - -### Insufficient Disk Space -Package updates require free disk space. If updates fail, check available space. - -## Update Frequency - - -## Safety Features - -- Updates are logged for troubleshooting -- User files and settings are preserved -- File ownership is automatically corrected -- Screen reader remains active throughout the process -- Summary shows if any operations failed - -## Getting Help - -If you experience persistent update issues: -1. Review the log file at `~/Logs/system-updates.log` -2. Contact Stormux with your log file (email address storm_dragon@stormux.org) - -## Notes - -- The script runs with root privileges to update system files -- Your personal data in ~/Music, ~/Roms, and other user directories is never modified -- Configuration files like .w3m and .irssi are excluded from updates to preserve your settings -- Updates can take several minutes depending on internet speed and the number of packages +If the update fails, review that file and contact Stormux with the log if you need help.