#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Self-voiced Terminal Menu System for Stormux game image import os import sys import time import threading import signal import curses import subprocess import speechd # Python bindings for Speech Dispatcher import configparser import pathlib import re import simpleaudio as sa class VoicedMenu: def __init__(self, title="Stormux Game Menu"): self.title = title self.menuSections = {} # Dictionary to hold sections and their items self.sectionNames = [] # List to maintain section order self.currentSection = 0 # Index of current section self.currentItemIndices = {} # Current item index for each section self.stdscr = None # System services dictionary - maps friendly names to service names self.systemMenuServices = { 'Braille': 'brltty.path', 'D L N A Server': 'minidlna.service', 'Fenrir Screen Reader': 'fenrirscreenreader-tty.service', 'Bluetooth': 'bluetooth.service', 'SSH': 'sshd.service' } # Config settings self.configDir = os.path.expanduser("~/.config/stormux") self.configFile = os.path.join(self.configDir, "game_launcher.conf") self.config = configparser.ConfigParser() # Default settings self.speechRate = 0 # Normal speech rate (0 is default in speechd) self.volume = 50 # Default volume level # Load settings self.load_settings() # Initialize speech client self.speechClient = None self.init_speech() # Track playing sound self.currentSound = None def init_speech(self): """Initialize the speech client""" try: self.speechClient = speechd.SSIPClient('stormux_menu') self.speechClient.set_priority(speechd.Priority.IMPORTANT) self.speechClient.set_punctuation(speechd.PunctuationMode.SOME) # Apply speech rate from settings self.speechClient.set_rate(self.speechRate) except Exception as e: print(f"Could not initialize speech: {e}") # Fallback to None - the speak method will handle this def load_settings(self): """Load settings from config file""" # Create default settings if they don't exist if not os.path.exists(self.configFile): self.save_settings() return try: self.config.read(self.configFile) # Load speech settings if 'Speech' in self.config: self.speechRate = self.config.getint('Speech', 'rate', fallback=0) # Load volume settings if 'Volume' in self.config: self.volume = self.config.getint('Volume', 'level', fallback=50) except Exception as e: print(f"Error loading settings: {e}") # If loading fails, we'll use default values def save_settings(self): """Save settings to config file""" # Ensure config directory exists os.makedirs(self.configDir, exist_ok=True) # Update config object if 'Speech' not in self.config: self.config['Speech'] = {} self.config['Speech']['rate'] = str(self.speechRate) # Save volume settings if 'Volume' not in self.config: self.config['Volume'] = {} self.config['Volume']['level'] = str(self.volume) # Write to file try: with open(self.configFile, 'w') as f: self.config.write(f) except Exception as e: print(f"Error saving settings: {e}") def increase_speech_rate(self): """Increase speech rate""" self.speechRate = min(100, self.speechRate + 10) # Max is 100 if self.speechClient: try: self.speechClient.set_rate(self.speechRate) self.speak(f"Speech rate: {self.speechRate}") except Exception as e: print(f"Error adjusting speech rate: {e}") # Save the new setting self.save_settings() def decrease_speech_rate(self): """Decrease speech rate""" self.speechRate = max(-100, self.speechRate - 10) # Min is -100 if self.speechClient: try: self.speechClient.set_rate(self.speechRate) self.speak(f"Speech rate: {self.speechRate}") except Exception as e: print(f"Error adjusting speech rate: {e}") # Save the new setting self.save_settings() def get_current_volume(self): """Get the current system volume percentage""" try: result = subprocess.run( ['pactl', 'get-sink-volume', '@DEFAULT_SINK@'], capture_output=True, text=True, check=True ) output = result.stdout # Extract percentage from output like "Volume: front-left: 27111 / 41% / -23.00 dB" match = re.search(r'(\d+)%', output) if match: return int(match.group(1)) return self.volume # Return saved volume if parsing fails except Exception as e: print(f"Error getting volume: {e}") return self.volume # Return saved volume if command fails def set_volume(self, volumePercent): """Set the system volume to the specified percentage""" # Ensure volume is between 0 and 150% volumePercent = max(0, min(150, volumePercent)) try: subprocess.run( ['pactl', 'set-sink-volume', '@DEFAULT_SINK@', f'{volumePercent}%'], check=True ) self.volume = volumePercent self.save_settings() return True except Exception as e: print(f"Error setting volume: {e}") return False def increase_volume(self): """Increase the system volume by 5%""" currentVolume = self.get_current_volume() newVolume = min(150, currentVolume + 5) # Max 150% if self.set_volume(newVolume): self.speak(f"Volume {newVolume} percent") self.draw_menu() def decrease_volume(self): """Decrease the system volume by 5%""" currentVolume = self.get_current_volume() newVolume = max(0, currentVolume - 5) # Min 0% if self.set_volume(newVolume): self.speak(f"Volume {newVolume} percent") self.draw_menu() def play_sound(self, soundName): """Play a sound effect using simpleaudio with ability to cancel previous sounds""" try: # Cancel any currently playing sound self.stop_sound() soundsDir = "/usr/share/sounds/stormux" soundFile = os.path.join(soundsDir, f"{soundName}.wav") # Check if the file exists if not os.path.exists(soundFile): print(f"Sound file not found: {soundFile}") return False # Play the sound and store the play_obj waveObj = sa.WaveObject.from_wave_file(soundFile) self.currentSound = waveObj.play() return True except Exception as e: print(f"Error playing sound {soundName}: {e}") return False def stop_sound(self): """Stop any currently playing sound""" if self.currentSound is not None and self.currentSound.is_playing(): self.currentSound.stop() self.currentSound = None def check_service_status(self, serviceName): """Check if a system service is active""" try: result = subprocess.run( ['systemctl', 'is-active', serviceName], capture_output=True, text=True ) return result.stdout.strip() == 'active' except Exception as e: print(f"Error checking {serviceName} status: {e}") return False def toggle_service(self, friendlyName): """Toggle a system service on/off""" # Get the actual service name from the dictionary serviceName = self.systemMenuServices.get(friendlyName) if not serviceName: print(f"Unknown service: {friendlyName}") return isActive = self.check_service_status(serviceName) # Clean up curses before running the command with sudo curses.endwin() # Clean up speech client if self.speechClient: self.speechClient.close() self.speechClient = None # Execute the appropriate command based on current status action = "" if serviceName not in ["fenrirscreenreader-tty.service", "minidlna.service"]: action = "disable" if isActive else "enable" else: action = "stop" if isActive else "start" command = f"sudo systemctl {action} {serviceName} --now" try: os.system(command) print(f"{friendlyName} {action}d successfully") except Exception as e: print(f"Error {action}ing {friendlyName}: {e}") # Restart speech client self.init_speech() # Restart curses self.stdscr = curses.initscr() curses.noecho() curses.cbreak() self.stdscr.keypad(True) # Announce the action self.speak(f"{friendlyName} {action}d") # Update the menu with the new service status self.update_service_menu_items() # Update Bluetooth menu items if Bluetooth service was toggled if friendlyName == "Bluetooth": self.update_bluetooth_menu_items() # Redraw the menu self.draw_menu() def toggle_screen(self, screenType): """Toggle between HDMI screen and headless mode""" # Remove any existing configuration try: os.system("sudo rm /etc/X11/xorg.conf.d/10-*.conf") # Copy the appropriate configuration file if screenType == "headless": configFile = "/home/stormux/.local/files/10-headless.conf" message = "HDMI Screen disabled." else: configFile = "/home/stormux/.local/files/10-screen.conf" message = "HDMI Screen enabled." # Copy the configuration file os.system(f"sudo cp {configFile} /etc/X11/xorg.conf.d/") self.speak(message, interrupt=False) except Exception as e: message = f"Error changing screen configuration: {e}" print(message) self.speak(message) 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())] # Add the appropriate items based on current status for each service for friendlyName, serviceName in self.systemMenuServices.items(): isActive = self.check_service_status(serviceName) if isActive: self.add_item("System", f"Disable {friendlyName}", lambda fn=friendlyName: self.toggle_service(fn)) else: self.add_item("System", f"Enable {friendlyName}", lambda fn=friendlyName: self.toggle_service(fn)) def update_bluetooth_menu_items(self): """Update Bluetooth-related menu items in Accessories section""" if "Accessories" in self.menuSections: # Remove any existing Bluetooth-related items self.menuSections["Accessories"] = [item for item in self.menuSections["Accessories"] if "Bluetooth" not in item[0]] # Add Bluetooth management item only if Bluetooth is enabled if self.check_service_status('bluetooth.service'): self.add_item("Accessories", "Manage Bluetooth Devices", "GAME=blueman-manager startx") def add_section(self, sectionName): """Add a new section to the menu""" if sectionName not in self.menuSections: self.menuSections[sectionName] = [] self.sectionNames.append(sectionName) self.currentItemIndices[sectionName] = 0 def add_item(self, sectionName, name, command): """Add a menu item to a specific section""" # Create section if it doesn't exist if sectionName not in self.menuSections: self.add_section(sectionName) self.menuSections[sectionName].append((name, command)) def speak(self, text, interrupt=True): """Speak the given text with option to interrupt existing speech""" if self.speechClient is None: return try: if interrupt: self.stop_speech() self.speechClient.speak(text) except Exception as e: # If speech fails, try to reinitialize and try once more try: self.init_speech() if self.speechClient: self.speechClient.speak(text) except: # If reinitializing fails, just give up silently pass def stop_speech(self): """Stop any ongoing speech""" if self.speechClient is None: return try: self.speechClient.cancel() except Exception as e: # If cancel fails, try to reinitialize self.init_speech() def get_current_items(self): """Get items from the current section""" currentSectionName = self.sectionNames[self.currentSection] return self.menuSections[currentSectionName] def get_current_item_index(self): """Get the current item index in the current section""" currentSectionName = self.sectionNames[self.currentSection] return self.currentItemIndices[currentSectionName] def set_current_item_index(self, index): """Set the current item index for the current section""" currentSectionName = self.sectionNames[self.currentSection] self.currentItemIndices[currentSectionName] = index def announce_current_section(self, interrupt=True): """Announce the currently selected section""" if 0 <= self.currentSection < len(self.sectionNames): sectionName = self.sectionNames[self.currentSection] self.speak(sectionName, interrupt=interrupt) def announce_current_item(self, interrupt=True): """Announce the currently selected menu item""" if len(self.sectionNames) > 0: items = self.get_current_items() index = self.get_current_item_index() if 0 <= index < len(items): name = items[index][0] self.speak(name, interrupt=interrupt) def execute_current_item(self): """Execute the currently selected menu item""" if len(self.sectionNames) > 0: items = self.get_current_items() index = self.get_current_item_index() if 0 <= index < len(items): name, command = items[index] # Announce we're launching the program self.speak(f"Launching {name}") # Check if command is a function (for service toggles and other functions) if callable(command): command() return # Save current terminal state before any changes savedTerminalState = None try: result = subprocess.run(['stty', '-g'], capture_output=True, text=True, check=True) savedTerminalState = result.stdout.strip() except Exception as e: print(f"Warning: Could not save terminal state: {e}") # Cleanup before running the command self.stop_speech() # Close speech client if self.speechClient: try: self.speechClient.close() except: pass self.speechClient = None # Curses cleanup - be thorough if self.stdscr: try: curses.nocbreak() self.stdscr.keypad(False) curses.echo() curses.endwin() except: pass self.stdscr = None # Complete terminal reset to ensure clean state for games try: subprocess.run(['reset'], check=False, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except: pass # Ensure we're in a new process group to avoid signal conflicts # Execute the command with proper terminal isolation try: # For applications that need dialog/curses, we need to ensure they # become the controlling process with full terminal access # Use os.system with proper terminal restoration as it gives # the subprocess complete control over the terminal # First, save our current process group original_pgrp = os.getpgrp() # Execute using os.system which gives the subprocess complete terminal control exit_code = os.system(command) # Try to regain terminal control after the subprocess exits try: # Get the current terminal file descriptor tty_fd = os.open('/dev/tty', os.O_RDWR) # Try to make our process group the foreground group again os.tcsetpgrp(tty_fd, original_pgrp) os.close(tty_fd) except: # If we can't regain control, that's okay pass except Exception as e: print(f"Error launching {name}: {e}") # Another terminal reset after game exits to clean up any changes try: subprocess.run(['reset'], check=False, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except: pass # Restore the saved terminal state if we have it if savedTerminalState: try: subprocess.run(['stty', savedTerminalState], check=False) except Exception as e: print(f"Warning: Could not restore terminal state: {e}") # Brief delay to let terminal stabilize time.sleep(0.5) # Reinitialize speech and display self.init_speech() # Reinitialize curses with better error handling try: self.stdscr = curses.initscr() curses.noecho() curses.cbreak() self.stdscr.keypad(True) self.stdscr.clear() self.stdscr.refresh() except Exception as e: print(f"Error reinitializing display: {e}") # Try a more aggressive recovery try: curses.endwin() time.sleep(1) self.stdscr = curses.initscr() curses.noecho() curses.cbreak() self.stdscr.keypad(True) self.stdscr.clear() self.stdscr.refresh() except: print("Could not recover display. Please restart the menu.") sys.exit(1) # Update service menu items and redraw self.update_service_menu_items() self.draw_menu() # Let the user know the menu is activated self.speak("Game menu activated") def speak_help(self): """Speak help information""" helpText = """ Navigation controls: Up arrow: Previous menu item. Down arrow: Next menu item. Left arrow: Previous section. Right arrow: Next section. Enter: Launch selected item. H key: Hear these instructions again. Left bracket: Decrease speech rate. Right bracket: Increase speech rate. 9 key: Decrease volume. 0 key: Increase volume. Escape: Refresh the menu. Q: Exit the menu. Any key will interrupt speech. """ self.speak(helpText) def draw_menu(self): """Draw the menu on the screen""" self.stdscr.clear() h, w = self.stdscr.getmaxyx() # Draw title title = f" {self.title} " x = max(0, w // 2 - len(title) // 2) self.stdscr.addstr(1, x, title, curses.A_BOLD) # Draw help line helpText = "Ãvigate | Enter: Select | H: Help | [ ] Rate | 9 0 Volume | Esc: Refresh" x = max(0, w // 2 - len(helpText) // 2) self.stdscr.addstr(3, x, helpText) # Draw current section if len(self.sectionNames) > 0: currentSectionName = self.sectionNames[self.currentSection] sectionText = f"== {currentSectionName} ==" x = max(0, w // 2 - len(sectionText) // 2) self.stdscr.addstr(5, x, sectionText, curses.A_BOLD) # Draw menu items for current section items = self.get_current_items() currentItemIndex = self.get_current_item_index() for i, (name, _) in enumerate(items): y = i + 7 # Start items 2 lines below section header if y < h - 1: # Ensure we don't draw outside the window # Highlight the selected item if i == currentItemIndex: text = f" > {name} " attr = curses.A_REVERSE else: text = f" {name} " attr = curses.A_NORMAL x = max(0, w // 2 - len(text) // 2) self.stdscr.addstr(y, x, text, attr) # Draw speech rate indicator rateText = f"Speech Rate: {self.speechRate}" self.stdscr.addstr(h-2, 2, rateText) # Draw volume indicator volumeText = f"Volume: {self.get_current_volume()}%" self.stdscr.addstr(h-2, w-len(volumeText)-2, volumeText) self.stdscr.refresh() def cleanup(self): """Clean up resources before exiting""" # Stop any speech self.stop_speech() # Close speech client if self.speechClient: try: self.speechClient.close() except: pass self.speechClient = None # Restore terminal settings curses.nocbreak() self.stdscr.keypad(False) curses.echo() curses.endwin() def run(self): """Run the menu system""" if not self.sectionNames: print("Menu is empty. Exiting.") return try: # Initialize curses self.stdscr = curses.initscr() curses.noecho() # Don't echo keypresses curses.cbreak() # React to keys instantly self.stdscr.keypad(True) # Enable special keys # Update all service menu items based on current status self.update_service_menu_items() # Update Bluetooth menu items based on current status self.update_bluetooth_menu_items() # Initial draw self.draw_menu() # Welcome message - don't interrupt this initial speech self.speak(f"Welcome to {self.title}. Use left and right arrows to navigate sections. Up and down for items. Press H for help.") # Wait for initial speech to finish before announcing section time.sleep(1) # Announce initial section and item without interrupting welcome speech self.announce_current_section(interrupt=False) time.sleep(0.5) # Wait before announcing first item self.announce_current_item(interrupt=False) # Main loop while True: key = self.stdscr.getch() # Stop any speech when a key is pressed self.stop_speech() # Handle navigation if key == curses.KEY_UP: # Move to previous item in current section items = self.get_current_items() if items: prevIndex = self.get_current_item_index() self.set_current_item_index((prevIndex - 1) % len(items)) newIndex = self.get_current_item_index() self.draw_menu() # Play sound if selection actually changed if newIndex != prevIndex: self.play_sound("menu_move") self.announce_current_item() elif key == curses.KEY_DOWN: # Move to next item in current section items = self.get_current_items() if items: prevIndex = self.get_current_item_index() self.set_current_item_index((prevIndex + 1) % len(items)) newIndex = self.get_current_item_index() self.draw_menu() # Play sound if selection actually changed if newIndex != prevIndex: self.play_sound("menu_move") self.announce_current_item() elif key == curses.KEY_LEFT: # Move to previous section if self.sectionNames: prevSection = self.currentSection self.currentSection = (self.currentSection - 1) % len(self.sectionNames) self.draw_menu() # Play category change sound if section actually changed if prevSection != self.currentSection: self.play_sound("menu_category") # Announce section and current item without interruption between them self.announce_current_section() time.sleep(0.5) # Brief pause between section and item announcement self.announce_current_item(interrupt=False) # Don't interrupt the section announcement elif key == curses.KEY_RIGHT: # Move to next section if self.sectionNames: prevSection = self.currentSection self.currentSection = (self.currentSection + 1) % len(self.sectionNames) self.draw_menu() # Play category change sound if section actually changed if prevSection != self.currentSection: self.play_sound("menu_category") # Announce section and current item without interruption between them self.announce_current_section() time.sleep(0.5) # Brief pause between section and item announcement self.announce_current_item(interrupt=False) # Don't interrupt the section announcement elif key == curses.KEY_ENTER or key == 10 or key == 13: # Enter key self.play_sound("menu_select") self.execute_current_item() elif key == ord('h') or key == ord('H'): # Help self.speak_help() elif key == ord('['): # Decrease speech rate self.decrease_speech_rate() self.draw_menu() elif key == ord(']'): # Increase speech rate self.increase_speech_rate() self.draw_menu() elif key == ord('9'): # Decrease volume self.decrease_volume() elif key == ord('0'): # Increase volume self.increase_volume() elif key == 27: # Esc key - restart the application 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() except Exception as e: # End curses in case of error curses.endwin() print(f"An error occurred: {e}") finally: # Clean up self.cleanup() # Example usage if __name__ == "__main__": # Create the menu with sections menu = VoicedMenu(title="Stormux Gaming Menu") # Add arcade section menu.add_section("Arcade") menu.add_item("Arcade", "Audio Disc", "GAME='Audio Disc' startx") menu.add_item("Arcade", "BallBouncer", "GAME=BallBouncer startx") menu.add_item("Arcade", "Battle of the Hunter", "GAME='Battle of the Hunter' startx") menu.add_item("Arcade", "Challenge of the Horse", "GAME='Challenge of the Horse' startx") menu.add_item("Arcade", "Clashes of the Sky", "GAME='Clashes of the Sky' startx") menu.add_item("Arcade", "Constant Motion", "GAME='Constant Motion' startx") menu.add_item("Arcade", "Crazy Party", "GAME='Crazy Party' startx") menu.add_item("Arcade", "Doom", "GAME=Doom startx") menu.add_item("Arcade", "Haunted House", "GAME='Haunted House' startx") menu.add_item("Arcade", "Haunted Party", "GAME='Haunted Party' startx") menu.add_item("Arcade", "kaskade", "GAME=Kaskade startx") menu.add_item("Arcade", "Oh Shitt", "GAME='Oh Shit' startx") menu.add_item("Arcade", "Q9 Action Game", "GAME='Q9 Action Game' startx") menu.add_item("Arcade", "River Raiders", "GAME='River Raiders' startx") menu.add_item("Arcade", "Scramble", "GAME=Scramble startx") menu.add_item("Arcade", "Screaming Strike 2", "GAME='Screaming Strike 2' startx") menu.add_item("Arcade", "Scrolling Battles", "GAME='Scrolling Battles' startx") menu.add_item("Arcade", "Shooter", "GAME=Shooter startx") menu.add_item("Arcade", "Side Party", "GAME='Side Party' startx") menu.add_item("Arcade", "Skateboarder Pro", "GAME='Skateboarder Pro' startx") menu.add_item("Arcade", "Sketchbook (Your World)", "GAME='Sketchbook (Your World)' startx") menu.add_item("Arcade", "Super Egg Hunt", "GAME='Super Egg Hunt' startx") menu.add_item("Arcade", "Super Liam", "GAME='Super Liam' startx") menu.add_item("Arcade", "The Blind Swordsman", "GAME='The Blind Swordsman' startx") menu.add_item("Arcade", "The Tornado Chicken", "GAME='The Tornado Chicken' startx") menu.add_item("Arcade", "Toy Mania", "GAME='Toy Mania' startx") menu.add_item("Arcade", "Villains From Beyond", "GAME='Villains From Beyond' startx") menu.add_item("Arcade", "Wicked Quest", "GAME='Wicked Quest' startx") menu.add_item("Arcade", "Zombowl", "GAME='Zombowl' startx") # Add board and card games section menu.add_section("Board and Card Games") menu.add_item("Board and Card Games", "RS Games", "GAME='RS Games' startx") # Add emulators section menu.add_section("Emulators") menu.add_item("Emulators", "Apple 2e", "/usr/local/bin/apple_2e.py") menu.add_item("Emulators", "Bop It", "GAME='Bop It' startx") menu.add_item("Emulators", "Dosbox", "GAME=Dosbox startx") menu.add_item("Emulators", "Game Console Menu", "/usr/local/bin/rom_launcher.py") menu.add_item("Emulators", "Retro Arch", "GAME='Retro Arch' startx") # Add MUD section menu.add_section("MUDs") menu.add_item("MUDs", "Alter Aeon", "GAME='Alter Aeon' /home/stormux/.clirc") menu.add_item("MUDs", "Empire MUD", "GAME='Empire MUD' /home/stormux/.clirc") menu.add_item("MUDs", "End of Time", "GAME='End of Time' /home/stormux/.clirc") menu.add_item("MUDs", "Kallisti MUD", "GAME='Kallisti MUD' /home/stormux/.clirc") # Add racing section menu.add_section("Racing") menu.add_item("Racing", "Mach1", "GAME=Mach1 startx") menu.add_item("Racing", "Mine Racer", "GAME='Mine Racer' startx") menu.add_item("Racing", "Top Speed 3", "GAME=\"Top Speed 3\" startx") menu.add_item("Racing", "Wheels of Prio", "GAME=\"Wheels of Prio\" startx") # Add rpg section menu.add_section("RPG") menu.add_item("RPG", "Bokurano Daibouken", "GAME='Bokurano Daibouken' startx") menu.add_item("RPG", "Bokurano Daibouken 2", "GAME='Bokurano Daibouken 2' startx") menu.add_item("RPG", "Bokurano Daibouken 3", "GAME='Bokurano Daibouken 3' startx") menu.add_item("RPG", "Fantasy Story 2", "GAME='Fantasy Story 2' startx") menu.add_item("RPG", "Manamon 2", "GAME='Manamon 2' startx") menu.add_item("RPG", "Shadow Line", "GAME='Shadow Line' startx") # Add sports section menu.add_section("Sports") menu.add_item("Sports", "Golf", "GAME=Golf startx") menu.add_item("Sports", "Pong", "GAME=Pong startx") # Add strategy section menu.add_section("Strategy") menu.add_item("Strategy", "SoundRTS", "GAME=SoundRTS startx") menu.add_item("Strategy", "Warsim", "/home/stormux/.Warsim") # Add text games section menu.add_section("Text Games") menu.add_item("Text Games", "BPG", "GAME=BPG /home/stormux/.clirc") menu.add_item("Text Games", "Colossal Cave Adventure", "GAME=/usr/bin/adventure /home/stormux/.clirc") menu.add_item("Text Games", "Go Fish", "GAME=/usr/bin/gofish /home/stormux/.clirc") menu.add_item("Text Games", "RS Games", "GAME=\"RS Games\" /home/stormux/.clirc") menu.add_item("Text Games", "Slay the Text", "GAME=\"Slay the Text\" /home/stormux/.clirc") menu.add_item("Text Games", "Stationfall", "GAME=Stationfall /home/stormux/.clirc") menu.add_item("Text Games", "Planetfall", "GAME=Planetfall /home/stormux/.clirc") menu.add_item("Text Games", "The Hitchhiker's Guide to the Galaxy", "GAME=\"The Hitchhiker's Guide to the Galaxy\" /home/stormux/.clirc") menu.add_item("Text Games", "Upheaval", "GAME=Upheaval /home/stormux/.Upheaval") menu.add_item("Text Games", "Zork 1", "GAME='Zork 1' /home/stormux/.clirc") menu.add_item("Text Games", "Zork 2", "GAME='Zork 2' /home/stormux/.clirc") menu.add_item("Text Games", "Zork 3", "GAME='Zork 3' /home/stormux/.clirc") # Add web section menu.add_section("Web") menu.add_item("Web", "Aliens", "GAME=https://files.jantrid.net/aliens// startx") menu.add_item("Web", "Echo Commander", "GAME=https://echo-commander.vercel.app/ startx") menu.add_item("Web", "Periphery Synthetic EP", "GAME=https://shiftbacktick.itch.io/periphery-synthetic-ep startx") menu.add_item("Web", "QuentinC Play Room", "GAME=https://qcsalon.net/ startx") menu.add_item("Web", "soundStrider", "GAME=https://shiftbacktick.itch.io/soundstrider startx") # Add help and documentation section menu.add_section("Help and Documentation") menu.add_item("Help and Documentation", "Navigating Help Documentation", "GAME=~/Documents/navigating_help.md /home/stormux/.clirc") menu.add_item("Help and Documentation", "Menu Controls", "GAME=~/Documents/game_menu_controls.md /home/stormux/.clirc") menu.add_item("Help and Documentation", "Game Notes", "GAME=~/Documents/game_notes.md /home/stormux/.clirc") menu.add_item("Help and Documentation", "Music Player", "GAME=~/Documents/music_player.md /home/stormux/.clirc") menu.add_item("Help and Documentation", "Terminal for Advanced Users", "GAME=~/Documents/terminal.md /home/stormux/.clirc") menu.add_item("Help and Documentation", "D L N A Server", "GAME=~/Documents/dlna.md /home/stormux/.clirc") menu.add_item("Help and Documentation", "Changing the Voice", "GAME=~/Documents/voices.md /home/stormux/.clirc") menu.add_item("Help and Documentation", "Change Log", "GAME=~/Documents/change_log.md /home/stormux/.clirc") menu.add_item("Help and Documentation", "Contacting Stormux", "GAME=~/Documents/contact.md /home/stormux/.clirc") menu.add_item("Help and Documentation", "Get help on IRC", "GAME=IRC /home/stormux/.clirc") # Add accessories section menu.add_section("Accessories") menu.add_item("Accessories", "Music Player", "/usr/local/bin/music_player.py") menu.add_item("Accessories", "Web Browser", "GAME=Brave startx") # Add system section menu.add_section("System") # Add installer only on x86_64 import platform if platform.machine() == "x86_64": 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", "Use HDMI Screen", lambda: menu.toggle_screen("screen")) menu.add_item("System", "Disable HDMI Screen", lambda: menu.toggle_screen("headless")) menu.add_item("System", "Set System Default Speech Rate", "/usr/local/bin/speechd_rate.py") menu.add_item("System", "Set Default Voice", "/usr/local/bin/set-voice.py") menu.add_item("System", "Upload Files", "/home/stormux/.local/upload_server/uploader.py") 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)") # Run the menu menu.run()